diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..d283d9e
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,15 @@
+* text=auto
+
+.githooks/pre-commit text eol=lf
+
+*.yml text eol=lf
+*.sh text eol=lf
+*.py text
+*.txt text
+*.ps1 text eol=crlf
+*.ps1.in text eol=crlf
+*.bat text eol=crlf
+*.bat.in text eol=crlf
+*.cmd text eol=crlf
+*.cmd.in text eol=crlf
+
diff --git a/.githooks/pre-commit b/.githooks/pre-commit
index e5ce2ff..bf221e5 100755
--- a/.githooks/pre-commit
+++ b/.githooks/pre-commit
@@ -7,6 +7,7 @@ PROJECT_ROOT="$(dirname "$(dirname "$SELF")")" ; export PROJECT_ROOT
pre_commit_d="${GITHOOKS_DIR}/pre-commit-d"
+# run scripts from the pre-commit.d directory
for i in $(ls "$pre_commit_d"); do
script="${pre_commit_d}/$i"
if [ -x "$script" ]; then
diff --git a/sgbackup/command.py b/sgbackup/command.py
index 21710f5..f59c2a3 100644
--- a/sgbackup/command.py
+++ b/sgbackup/command.py
@@ -17,6 +17,7 @@
# along with this program. If not, see . #
###############################################################################
+
class Command:
def __init__(self,id:str,name:str,description:str):
self.__id = id
diff --git a/sgbackup/game.py b/sgbackup/game.py
index b8e9be5..864e2e5 100644
--- a/sgbackup/game.py
+++ b/sgbackup/game.py
@@ -107,6 +107,8 @@ class GameFileType(StrEnum):
raise ValueError("Unknown GameFileType \"{}\"!".fomrat(typestring))
class GameFileMatcher(GObject):
+ __gtype_name__ = "GameFileMatcher"
+
def __init__(self,match_type:GameFileType,match_file:str):
GObject.__init__(self)
self.match_type = type
@@ -165,6 +167,8 @@ class GameFileMatcher(GObject):
return False
class GameData(GObject):
+ __gtype_name__ = 'GameData'
+
"""
:class: GameData
:brief: Base class for platform specific data.
@@ -226,17 +230,41 @@ class GameData(GObject):
self.__savegame_dir = sgdir
@Property
- def variables(self):
+ def variables(self)->dict:
return self.__variables
+ @variables.setter
+ def variables(self,vars:dict|None):
+ if not vars:
+ self.__variables = {}
+ else:
+ self.__variables = dict(vars)
@Property
def file_match(self):
return self.__filematch
+ @file_match.setter
+ def file_match(self,fm:list[GameFileMatcher]|None):
+ if not fm:
+ self.__filematch = []
+ else:
+ for matcher in fm:
+ if not isinstance(matcher,GameFileMatcher):
+ raise TypeError("\"file_match\" needs to be \"None\" or a list of \"GameFileMatcher\" instances!")
+ self.__filematch = list(fm)
@Property
def ignore_match(self):
return self.__ignorematch
-
+ @file_match.setter
+ def file_match(self,im:list[GameFileMatcher]|None):
+ if not im:
+ self.__ignorematch = []
+ else:
+ for matcher in im:
+ if not isinstance(matcher,GameFileMatcher):
+ raise TypeError("\"ignore_match\" needs to be \"None\" or a list of \"GameFileMatcher\" instances!")
+ self.__ignorematch = list(im)
+
def has_variable(self,name:str)->bool:
return (name in self.__variables)
@@ -535,18 +563,25 @@ class SteamGame(GameData):
GameData.__init__(self,
sgtype,
- appid,
savegame_root,
savegame_dir,
variables,
file_match,
ignore_match)
- self.__installdir = installdir
+ self.appid = int(appid)
+ self.installdir = installdir
def get_variables(self):
vars = super().get_variables()
vars["INSTALLDIR"] = self.installdir if self.installdir else ""
+ @Property(type=int)
+ def appid(self):
+ return self.__appid
+ @appid.setter
+ def appid(self,appid):
+ self.__appid = appid
+
@Property
def installdir(self):
return self.__installdir
@@ -622,6 +657,8 @@ class SteamMacOSGame(SteamGame):
class Game(GObject):
+ __gtype_name__ = "Game"
+
@staticmethod
def new_from_dict(config:str):
logger = logger.getChild("Game.new_from_dict()")
@@ -740,15 +777,17 @@ class Game(GObject):
with open(filename,'rt',encoding="UTF-8") as ifile:
return Game.new_from_dict(json.loads(ifile.read()))
- def __init__(self,id:str,name:str,savegame_name:str):
+ def __init__(self,key:str,name:str,savegame_name:str):
GObject.__init__(self)
- self.__id = id
+ self.__dbid = None
+ self.__key = key
self.__name = name
self.__filename = None
self.__savegame_name = savegame_name
self.__savegame_type = SavegameType.UNSET
self.__active = False
self.__live = True
+ self.__variables = dict()
self.__windows = None
self.__linux = None
@@ -762,12 +801,25 @@ class Game(GObject):
self.__epic_linux = None
@Property(type=str)
- def id(self)->str:
+ def dbid(self)->str:
return self.__id
- @id.setter
+ @dbid.setter
def id(self,id:str):
self.__id = id
+ @Property(type=str)
+ def key(self)->str:
+ return self.__key
+ @key.setter
+ def key(self,key:str):
+ set_game = False
+ if self.__key in GAMES:
+ del GAMES[self.__key]
+ set_game = True
+
+ self.__key = key
+ if set_game:
+ GAMES[self.__key] = self
@Property(type=str)
def name(self)->str:
@@ -820,6 +872,16 @@ class Game(GObject):
else:
self.__filename = fn
+ @Property
+ def variables(self):
+ return self.__variables
+ @variables.setter
+ def variables(self,vars:dict|None):
+ if not vars:
+ self.__variables = {}
+ else:
+ self.__variables = dict(vars)
+
@Property
def game_data(self):
sgtype = self.savegame_type
@@ -917,6 +979,22 @@ class Game(GObject):
raise TypeError("SteamWindowsGame")
self.__steam_macos = data
+ def add_variable(self,name:str,value:str):
+ self.__variables[str(name)] = str(value)
+
+ def delete_variable(self,name):
+ if name in self.__variables:
+ del self.__variables[name]
+
+ def get_variable(self,name):
+ vars = dict(os.environ)
+ #vars.update(settings.variables)
+ vars.update(self.__variables)
+ game_data = self.game_data
+ if (game_data is not None):
+ vars.update(game_data.variables)
+
+
def serialize(self)->dict:
ret = {
'id': self.id,
@@ -949,9 +1027,8 @@ class Game(GObject):
# ret['epic_linux'] = self.epic_linux.serialize()
return ret
-
+
def save(self):
- data = self.serialize()
old_path = pathlib.Path(self.filename).resolve()
new_path = pathlib.Path(settings.gameconf_dir / '.'.join(self.id,'gameconf')).resolve()
if (str(old_path) != str(new_path)) and old_path.is_file():
@@ -959,8 +1036,10 @@ class Game(GObject):
if not new_path.parent.is_dir():
os.makedirs(new_path.parent)
- with open(new_path,'wt',encoding='UTF-8') as ofile:
+ with open(new_path,'wt',encoding='utf-8') as ofile:
ofile.write(json.dumps(self.serialize(),ensure_ascii=False,indent=4))
+
+
GAMES={}
STEAM_GAMES={}
@@ -973,7 +1052,7 @@ def __init_games():
if not os.path.isdir(gameconf_dir):
return
- for gcf in (os.path.join(gameconf_dir,i) for i in os.path.listdir(gameconf_dir)):
+ for gcf in (os.path.join(gameconf_dir,i) for i in os.listdir(gameconf_dir)):
if not os.path.isfile(gcf) or not gcf.endswith('.gameconf'):
continue
@@ -984,7 +1063,7 @@ def __init_games():
except:
continue
- GAMES[game.id] = game
+ GAMES[game.key] = game
if (game.steam_windows):
if not game.steam_windows.appid in STEAM_GAMES:
STEAM_GAMES[game.steam_windows.appid] = game
@@ -1000,7 +1079,7 @@ def __init_games():
__init_games()
def add_game(game:Game):
- GAMES[game.id] = game
+ GAMES[game.key] = game
if game.steam_windows:
if not game.steam_windows.appid in STEAM_GAMES:
STEAM_GAMES[game.steam_windows.appid] = game
@@ -1012,4 +1091,5 @@ def add_game(game:Game):
if (game.steam_macos):
if not game.steam_macos.appid in STEAM_GAMES:
STEAM_GAMES[game.steam_macos.appid] = game
- STEAM_MACOS_GAMES[game.steam_macos.appid] = game
\ No newline at end of file
+ STEAM_MACOS_GAMES[game.steam_macos.appid] = game
+
\ No newline at end of file
diff --git a/sgbackup/gui/__init__.py b/sgbackup/gui/__init__.py
index 9049356..ee4060d 100644
--- a/sgbackup/gui/__init__.py
+++ b/sgbackup/gui/__init__.py
@@ -15,4 +15,9 @@
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see . #
###############################################################################
-from .application import Application
+
+from ._app import Application,AppWindow
+from ._settingsdialog import SettingsDialog
+from ._gamedialog import GameDialog
+
+app = None
diff --git a/sgbackup/gui/_app.py b/sgbackup/gui/_app.py
new file mode 100644
index 0000000..d81c401
--- /dev/null
+++ b/sgbackup/gui/_app.py
@@ -0,0 +1,448 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see . #
+###############################################################################
+
+from gi.repository import Gtk,GObject,Gio,Gdk
+
+import logging; logger=logging.getLogger(__name__)
+
+import os
+from datetime import datetime as DateTime
+from pathlib import Path
+
+from .. import game
+from ..settings import settings
+from ._settingsdialog import SettingsDialog
+from ._gamedialog import GameDialog
+
+class GameView(Gtk.ScrolledWindow):
+ __gtype_name__ = "sgbackup-gui-GameView"
+
+ def __init__(self):
+ Gtk.ScrolledWindow.__init__(self)
+
+ self.__liststore = Gio.ListStore.new(game.Game)
+ for g in game.GAMES.values():
+ pass
+ self.__liststore.append(g)
+
+ factory_key = Gtk.SignalListItemFactory.new()
+ factory_key.connect('setup',self._on_key_column_setup)
+ factory_key.connect('bind',self._on_key_column_bind)
+ column_key = Gtk.ColumnViewColumn.new("Key",factory_key)
+
+ factory_name = Gtk.SignalListItemFactory.new()
+ factory_name.connect('setup',self._on_name_column_setup)
+ factory_name.connect('bind',self._on_name_column_bind)
+ column_name = Gtk.ColumnViewColumn.new("Name",factory_name)
+ column_name.set_expand(True)
+
+ factory_active = Gtk.SignalListItemFactory.new()
+ factory_active.connect('setup',self._on_active_column_setup)
+ factory_active.connect('bind',self._on_active_column_bind)
+ factory_active.connect('unbind',self._on_active_column_unbind)
+ column_active = Gtk.ColumnViewColumn.new("Active",factory_active)
+
+ factory_live = Gtk.SignalListItemFactory.new()
+ factory_live.connect('setup',self._on_live_column_setup)
+ factory_live.connect('bind',self._on_live_column_bind)
+ factory_live.connect('unbind',self._on_live_column_unbind)
+ column_live = Gtk.ColumnViewColumn.new("Live",factory_live)
+
+ selection = Gtk.SingleSelection.new(self._liststore)
+ self.__columnview = Gtk.ColumnView.new(selection)
+ self._columnview.append_column(column_key)
+ self._columnview.append_column(column_name)
+ self._columnview.append_column(column_active)
+ self._columnview.append_column(column_live)
+ self._columnview.set_single_click_activate(True)
+
+ self.set_child(self._columnview)
+
+ @property
+ def _liststore(self):
+ return self.__liststore
+
+ @property
+ def _columnview(self):
+ return self.__columnview
+
+ def _on_key_column_setup(self,factory,item):
+ item.set_child(Gtk.Label())
+
+ def _on_key_column_bind(self,factory,item):
+ label = item.get_child()
+ game = item.get_item()
+ label.bind_property(game,'key','label',GObject.BindingFlags.DEFAULT)
+
+ def _on_name_column_setup(self,factory,item):
+ item.set_child(Gtk.Label())
+
+ def _on_name_column_bind(self,factory,item):
+ label = item.get_child()
+ game = item.get_item()
+ label.bind_proprety(game,'name','label',GObject.BindingFlags.DEFAULT)
+
+ def _on_active_column_setup(self,factory,item):
+ item.set_child(Gtk.Switch())
+
+ def _on_active_column_bind(self,factory,item):
+ switch = item.get_child()
+ game = item.get_data()
+ switch.set_active(game.is_active)
+ item._signal_active_state_set = switch.connect('state-set',self._on_active_state_set,game)
+
+ def _on_active_column_unbind(self,factory,item):
+ if hasattr(item,'_signal_active_state_set'):
+ item.get_child().disconnect(item._signal_active_state_set)
+ del item._signal_active_state_set
+
+ def _on_active_state_set(self,switch,state,game):
+ game.is_active = state
+ game.save()
+
+ def _on_live_column_setup(self,factory,item):
+ item.set_child(Gtk.Switch())
+
+ def _on_live_column_bind(self,factory,item):
+ switch = item.get_child()
+ game = item.get_data()
+ switch.set_active(game.is_active)
+ item._signal_live_state_set = switch.connect('state-set',self._on_live_state_set,game)
+
+ def _on_live_column_unbind(self,factory,item):
+ if hasattr(item,'_signal_live_state_set'):
+ item.get_child().disconnect(item._signal_live_state_set)
+ del item._signal_live_state_set
+
+ def _on_live_state_set(self,switch,state,game):
+ def on_dialog_response(dialog,response):
+ if response == Gtk.ResponseType.YES:
+ pass
+ #archiver.backup(game)
+ dialog.hide()
+ dialog.destroy()
+
+ game.is_live = state
+ game.save()
+ if not state:
+ dialog = Gtk.MessageDialog()
+ dialog.set_transient_for(self.get_toplevel())
+ dialog.props.buttons = Gtk.ButtonsType.YES_NO
+ dialog.props.text = "Do you want to create a new savegame for {game}?".format(game=game.name)
+ dialog.props.use_markup = True
+ dialog.props.secondary_text = "The new savegame is added to the finsihed savegames for the game."
+ dialog.props.secondary_use_markup = False
+ dialog.connect('response',on_dialog_response)
+ dialog.present()
+# GameView class
+
+class BackupViewData(GObject.GObject):
+ def __init__(self,_game:game.Game,filename:str):
+ GObject.GObject.__init__(self)
+ self.__game = _game
+ self.__filename = filename
+
+ basename = os.path.basename(filename)
+ self.__is_live = (os.path.basename(os.path.dirname(filename)) == 'live')
+ parts = filename.split('.')
+ self.__savegame_name = parts[0]
+ self.__timestamp = DateTime.strptime(parts[1],"%Y%m%d-%H%M%S")
+
+ self.__extension = '.' + parts[3:]
+
+ @property
+ def game(self)->game.Game:
+ return self.__game
+
+ @GObject.Property
+ def savegame_name(self):
+ return self.__savegame_name
+
+ @GObject.Property(type=str)
+ def filename(self)->str:
+ return self.__filename
+
+ @GObject.Property(type=bool,default=False)
+ def is_live(self)->bool:
+ pass
+
+ @GObject.Property
+ def extension(self):
+ return self.__extension
+
+ @GObject.Property
+ def timestamp(self):
+ return self.__timestamp
+
+class BackupView(Gtk.ScrolledWindow):
+ __gtype_name__ = "BackupView"
+ def __init__(self,gameview:GameView):
+ Gtk.ScrolledWindow.__init__(self)
+ self.__gameview = gameview
+
+ self.__liststore = Gio.ListStore()
+ selection = Gtk.SingleSelection.new(self.__liststore)
+
+ live_factory = Gtk.SignalListItemFactory()
+ live_factory.connect('setup',self._on_live_column_setup)
+ live_factory.connect('bind',self._on_live_column_bind)
+ live_column = Gtk.ColumnViewColumn.new("Live",live_factory)
+
+ sgname_factory = Gtk.SignalListItemFactory()
+ sgname_factory.connect('setup',self._on_savegamename_column_setup)
+ sgname_factory.connect('bind',self._on_savegamename_column_bind)
+ sgname_column = Gtk.ColumnViewColumn.new("Savegame name",sgname_factory)
+ sgname_column.set_expand(True)
+
+ timestamp_factory = Gtk.SignalListItemFactory()
+ timestamp_factory.connect('setup',self._on_timestamp_column_setup)
+ timestamp_factory.connect('bind',self._on_timestamp_column_bind)
+ timestamp_column = Gtk.ColumnViewColumn.new("Timestamp",timestamp_factory)
+
+ self.__columnview = Gtk.ColumnView.new(selection)
+ self.__columnview.append_column(live_column)
+ self.__columnview.append_column(sgname_column)
+ self.__columnview.append_column(timestamp_column)
+
+ self._on_gameview_selection_changed(selection)
+ self.gameview._columnview.get_model().connect('selection-changed',self._on_gameview_selection_changed)
+
+ self.set_child(self.__columnview)
+
+ @property
+ def gameview(self)->GameView:
+ return self.__gameview
+
+ def _on_live_column_setup(self,factory,item):
+ checkbutton = Gtk.CheckButton()
+ checkbutton.set_sensitive(False)
+
+ def _on_live_column_bind(self,factory,item):
+ checkbutton = item.get_child()
+ data = item.get_item()
+ checkbutton.set_active(data.is_live)
+
+
+ def _on_savegamename_column_setup(self,factory,item):
+ label = Gtk.Label()
+ self.set_child(label)
+
+ def _on_savegamename_column_bind(self,factory,item):
+ label = item.get_child()
+ data = item.get_item()
+ label.set_text(data.savegame_name)
+
+ def _on_timestamp_column_setup(self,factory,item):
+ label = Gtk.Label()
+ item.set_child(label)
+
+ def _on_timestamp_column_bind(self,factory,item):
+ label = item.get_child()
+ data = item.get_item()
+ label.set_text(data.timestamp.strftime("%d.%m.%Y %H:%M:%S"))
+
+ def _on_size_column_setup(self,factory,item):
+ label = Gtk.Label()
+ item.set_child(label)
+
+ def _on_size_column_bind(self,factory,item):
+ label = item.get_child()
+ data = item.get_item()
+ file = Path(data.filename).resolve()
+
+ if not file.is_file():
+ label.set_text("0 B")
+ return
+
+ size = file.stat().st_size
+ if (size > 1073741824):
+ display_size = ".".join((str(int(size / 1073741824)),str(int(((size * 10) / 1073741824) % 10)))) + " GiB"
+ elif (size > 1048576):
+ display_size = ".".join((str(int(size / 1048576)), str(int(((size * 10) / 1048576) % 10)))) + " MiB"
+ elif (size > 1024):
+ display_size = ".".join((str(int(size / 1024)), str(int(((size * 10) / 1024) % 10)))) + " KiB"
+ else:
+ display_size = str(size) + " B"
+ label.set_text(display_size)
+
+ def _on_gameview_selection_changed(self,model):
+ game = model.get_selected_item()
+ if game is None:
+ return
+
+
+class AppWindow(Gtk.ApplicationWindow):
+ __gtype_name__ = "AppWindow"
+ def __init__(self,application=None,**kwargs):
+ kwargs['title'] = "SGBackup"
+
+ if (application is not None):
+ kwargs['application']=application
+ if (hasattr(application,'builder')):
+ builder = application.builder
+ else:
+ builder = Gtk.Builder.new()
+
+ Gtk.ApplicationWindow.__init__(self,**kwargs)
+ self.set_default_size(800,600)
+ self.set_icon_name('sgbackup')
+
+ self.__builder = builder
+ self.builder.add_from_file(os.path.join(os.path.dirname(__file__),'appmenu.ui'))
+ gmenu = self.builder.get_object('appmenu')
+ appmenu_popover = Gtk.PopoverMenu.new_from_model(gmenu)
+ image = Gtk.Image.new_from_icon_name('open-menu-symbolic')
+ menubutton = Gtk.MenuButton.new()
+ menubutton.set_popover(appmenu_popover)
+ menubutton.set_child(image)
+ headerbar = Gtk.HeaderBar.new()
+ headerbar.pack_start(menubutton)
+ self.set_titlebar(headerbar)
+
+ vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+
+ self.__vpaned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
+ self.__vpaned.set_hexpand(True)
+ self.__vpaned.set_vexpand(True)
+ self.__vpaned.set_wide_handle(True)
+ self.__gameview = GameView()
+ self.__vpaned.set_start_child(self.gameview)
+ self.__backupview = BackupView(self.gameview)
+ self.__vpaned.set_end_child(self.backupview)
+ self.__vpaned.set_resize_start_child(True)
+ self.__vpaned.set_resize_end_child(True)
+
+ vbox.append(self.__vpaned)
+
+ statusbar = Gtk.Statusbar()
+ statusbar.set_hexpand(True)
+ statusbar.set_vexpand(False)
+ statusbar.push(0,'Running ...')
+ vbox.append(statusbar)
+
+ self.set_child(vbox)
+
+ @GObject.Property
+ def builder(self):
+ return self.__builder
+
+ @GObject.Property
+ def backupview(self):
+ return self.__backupview
+
+ @GObject.Property
+ def gameview(self):
+ return self.__gameview
+
+
+class Application(Gtk.Application):
+ __gtype_name__ = "Application"
+
+ def __init__(self,*args,**kwargs):
+ AppFlags = Gio.ApplicationFlags
+ kwargs['application_id'] = 'org.sgbackup.sgbackup'
+ kwargs['flags'] = AppFlags.FLAGS_NONE
+ Gtk.Application.__init__(self,*args,**kwargs)
+
+ self.__logger = logger.getChild('Application')
+ self.__builder = None
+ self.__appwindow = None
+
+ @property
+ def _logger(self):
+ return self.__logger
+
+ @GObject.Property
+ def appwindow(self):
+ return self.__appwindow
+
+ def do_startup(self):
+ self._logger.debug('do_startup()')
+ if not self.__builder:
+ self.__builder = Gtk.Builder.new()
+ Gtk.Application.do_startup(self)
+
+ pkg_path = Path(__file__).resolve()
+ pkg_path = pkg_path.parent.parent
+ icons_path = pkg_path / "icons"
+
+ theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default())
+ theme.add_resource_path("/org/sgbackup/sgbackup/icons")
+ theme.add_search_path(str(icons_path))
+
+ action_about = Gio.SimpleAction.new('about',None)
+ action_about.connect('activate',self.on_action_about)
+ self.add_action(action_about)
+
+ action_new_game = Gio.SimpleAction.new('new-game',None)
+ action_new_game.connect('activate',self.on_action_new_game)
+ self.add_action(action_new_game)
+
+ action_quit = Gio.SimpleAction.new('quit',None)
+ action_quit.connect('activate',self.on_action_quit)
+ self.add_action(action_quit)
+
+ action_settings = Gio.SimpleAction.new('settings',None)
+ action_settings.connect('activate',self.on_action_settings)
+ self.add_action(action_settings)
+
+ # add accels
+ self.set_accels_for_action('app.quit',["q"])
+
+ @GObject.Property
+ def builder(self):
+ return self.__builder
+
+ def do_activate(self):
+ self._logger.debug('do_activate()')
+ if not (self.__appwindow):
+ self.__appwindow = AppWindow(application=self)
+
+
+ self.appwindow.present()
+
+ def on_action_about(self,action,param):
+ pass
+
+ def on_action_settings(self,action,param):
+ dialog = self.new_settings_dialog()
+ dialog.present()
+
+ def on_action_quit(self,action,param):
+ self.quit()
+
+ def on_action_new_game(self,action,param):
+ def on_reponse(dialog,response):
+ dialog.destroy()
+
+ dialog = GameDialog(self.appwindow)
+ dialog.connect('response',on_reponse)
+ dialog.present()
+
+ def new_settings_dialog(self):
+ dialog = SettingsDialog(self.appwindow)
+ self.emit('settings-dialog-init',dialog)
+ return dialog
+
+ @GObject.Signal(name='settings-dialog-init',
+ flags=GObject.SignalFlags.RUN_LAST,
+ return_type=None,
+ arg_types=(SettingsDialog,))
+ def settings_dialog_init(self,dialog):
+ pass
+
\ No newline at end of file
diff --git a/sgbackup/gui/_gamedialog.py b/sgbackup/gui/_gamedialog.py
new file mode 100644
index 0000000..7f166d0
--- /dev/null
+++ b/sgbackup/gui/_gamedialog.py
@@ -0,0 +1,675 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see . #
+###############################################################################
+
+from gi.repository import GObject,Gio,GLib,Gtk,Pango
+from ..game import Game,GameFileMatcher,GameFileType
+
+class GameVariableData(GObject.GObject):
+ def __init__(self,name:str,value:str):
+ GObject.GObject.__init__(self)
+ self.name = name
+ self.value = value
+
+ @GObject.Property(type=str)
+ def name(self)->str:
+ return self.__name
+ @name.setter
+ def name(self,name):
+ self.__name = name
+
+ @GObject.Property(type=str)
+ def value(self)->str:
+ return self.__value
+ @value.setter
+ def value(self,value:str):
+ self.__value = value
+
+class RegistryKeyData(GObject.GObject):
+ def __init__(self,regkey=None):
+ GObject.GObject.__init__(self)
+ if not regkey:
+ self.__regkey = ""
+
+ @GObject.Property(type=str)
+ def regkey(self):
+ return self.__regkey
+ @regkey.setter
+ def regkey(self,key:str):
+ self.__regkey = key
+
+ def __bool__(self):
+ return bool(self.__regkey)
+
+class GameVariableDialog(Gtk.Dialog):
+ def __init__(self,parent:Gtk.Window,columnview:Gtk.ColumnView,variable:GameVariableData|None=None):
+ Gtk.Dialog.__init__(self)
+ self.set_transient_for(parent)
+ self.set_default_size(600,-1)
+
+ self.__columnview = columnview
+
+ if variable:
+ self.__variable = variable
+ else:
+ self.__variable = None
+
+ grid = Gtk.Grid()
+ label = Gtk.Label.new("Name:")
+ self.__name_entry = Gtk.Entry()
+ self.__name_entry.set_hexpand(True)
+ self.__name_entry.connect("changed",self._on_name_entry_changed)
+ grid.attach(label,0,0,1,1)
+ grid.attach(self.__name_entry,1,0,1,1)
+
+ label = Gtk.Label.new("Value")
+ self.__value_entry = Gtk.Entry()
+ self.__value_entry.set_hexpand(True)
+
+ grid.attach(label,0,1,1,1)
+ grid.attach(self.__value_entry,1,1,1,1)
+
+ self.get_content_area().append(grid)
+
+ if self.__variable:
+ self.__name_entry.set_text(self.__variable.name)
+ self.__value_entry.set_text(self.__variable.value)
+
+ self.__apply_button = self.add_button("Apply",Gtk.ResponseType.APPLY)
+ self.__apply_button.set_sensitive(bool(self))
+
+ self.add_button("Cancel",Gtk.ResponseType.CANCEL)
+
+ def __bool__(self):
+ name = self.__name_entry.get_text()
+ if name:
+ if self.__variable and self.__variable.name == name:
+ return True
+ model = self.__columnview.get_model().get_model()
+ for i in range(model.get_n_items()):
+ if name == model.get_item(i).name:
+ return False
+ return True
+ return False
+
+ def _on_name_entry_changed(self,entry):
+ self.__apply_button.set_sensitive(bool(self))
+
+ def do_response(self,response):
+ if response == Gtk.ResponseType.APPLY:
+ if not bool(self):
+ return
+ if self.__variable:
+ self.__variable.name = self.__name_entry.get_text()
+ self.__variable.value = self.__value_entry.get_text()
+ else:
+ model = self.__columnview.get_model().get_model()
+ model.append(GameVariableData(self.__name_entry.get_text(),self.__value_entry.get_text()))
+ self.hide()
+ self.destroy()
+
+class GameDialog(Gtk.Dialog):
+ def __init__(self,
+ parent:Gtk.Window|None=None,
+ game:Game|None=Game):
+
+ Gtk.Dialog.__init__(self)
+ if (parent):
+ self.set_transient_for(parent)
+
+ if isinstance(game,Game):
+ self.__game = game
+ else:
+ self.__game = None
+
+ self.set_default_size(800,600)
+
+ paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL)
+ paned.set_position(200)
+
+ self.__stack = Gtk.Stack.new()
+ paned.set_end_child(self.__stack)
+ paned.set_hexpand(True)
+ paned.set_vexpand(True)
+
+ self.__stack_sidebar = Gtk.ListBox()
+ self.__stack_sidebar.set_activate_on_single_click(False)
+ self.__stack_sidebar.set_selection_mode(Gtk.SelectionMode.SINGLE)
+ self.__stack_sidebar.connect('selected_rows_changed',self._on_stack_sidebar_selected_rows_changed)
+
+ sidebar_scrolled = Gtk.ScrolledWindow()
+ sidebar_scrolled.set_child(self.__stack_sidebar)
+ paned.set_start_child(sidebar_scrolled)
+
+ self.__variable_name_factory = Gtk.SignalListItemFactory()
+ self.__variable_name_factory.connect('setup',self._on_variable_name_setup)
+ self.__variable_name_factory.connect('bind',self._on_variable_name_bind)
+
+ self.__variable_value_factory = Gtk.SignalListItemFactory()
+ self.__variable_value_factory.connect('setup',self._on_variable_value_setup)
+ self.__variable_value_factory.connect('bind',self._on_variable_value_bind)
+
+ self.__add_game_page()
+ self.__windows = self.__create_windows_page()
+ self.__linux = self.__create_linux_page()
+ self.__macos = self.__create_macos_page()
+ self.__steam_windows = self.__create_steam_page('steam-windows','Steam Windows')
+ self.__steam_linux = self.__create_steam_page('steam-linux','Steam Linux')
+ self.__steam_macos = self.__create_steam_page('steam-macos','Steam MacOS')
+
+
+ for stack_page in self.__stack.get_pages():
+ hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL,4)
+ label = Gtk.Label.new(stack_page.props.title)
+ attrs = Pango.AttrList.new()
+ size_attr = Pango.AttrSize.new(14 * Pango.SCALE)
+ attrs.insert(size_attr)
+ label.set_attributes(attrs)
+ icon = Gtk.Image.new_from_icon_name(stack_page.props.icon_name)
+ icon.set_pixel_size(20)
+ hbox.append(icon)
+ hbox.append(label)
+ hbox.page_name = stack_page.props.name
+
+ self.__stack_sidebar.append(hbox)
+
+ self.reset()
+ self.__stack_sidebar.select_row(self.__stack_sidebar.get_row_at_index(0))
+
+ self.get_content_area().append(paned)
+ self.add_button("Apply",Gtk.ResponseType.APPLY)
+ self.add_button("Cancel",Gtk.ResponseType.CANCEL)
+
+
+ def __add_game_page(self):
+ page = Gtk.ScrolledWindow()
+ vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,2)
+ self.__set_widget_margin(vbox,5)
+
+ grid = Gtk.Grid.new()
+
+ label = Gtk.Label.new("Is active?")
+ self.__active_switch = Gtk.Switch()
+ entry_hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL,5)
+ entry_hbox.append(self.__active_switch)
+ entry_hbox.append(label)
+ vbox.append(entry_hbox)
+
+ label = Gtk.Label.new("Is live?")
+ self.__live_switch = Gtk.Switch()
+ entry_hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL,5)
+
+ entry_hbox.append(self.__live_switch)
+ entry_hbox.append(label)
+ vbox.append(entry_hbox)
+
+ label = Gtk.Label.new("ID:")
+ self.__id_label = Gtk.Label()
+ self.__id_label.set_hexpand(True)
+ grid.attach(label,0,0,1,1)
+ grid.attach(self.__id_label,1,0,1,1)
+
+ label = Gtk.Label.new("Key:")
+ self.__key_entry = Gtk.Entry()
+ self.__key_entry.set_hexpand(True)
+ grid.attach(label,0,1,1,1)
+ grid.attach(self.__key_entry,1,1,1,1)
+
+ label = Gtk.Label.new("Game name:")
+ self.__name_entry = Gtk.Entry()
+ self.__name_entry.set_hexpand(True)
+ grid.attach(label,0,2,1,1)
+ grid.attach(self.__name_entry,1,2,1,1)
+
+ label = Gtk.Label.new("Savegame name:")
+ self.__sgname_entry = Gtk.Entry()
+ self.__sgname_entry.set_hexpand(True)
+ grid.attach(label,0,3,1,1)
+ grid.attach(self.__sgname_entry,1,3,1,1)
+ vbox.append(grid)
+
+ self.__game_variables = self.__create_variables_widget()
+
+ vbox.append(self.__game_variables)
+
+ page.set_child(vbox)
+ self.__stack.add_titled(page,"main","Game")
+ stack_page = self.__stack.get_page(page)
+ stack_page.set_icon_name('sgbackup')
+
+
+ def __create_windows_page(self):
+ page = Gtk.ScrolledWindow()
+ vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,2)
+ self.__set_widget_margin(vbox,5)
+
+ grid = Gtk.Grid()
+
+ label = Gtk.Label.new("Root directory:")
+ page.sgroot_entry = Gtk.Entry()
+ page.sgroot_entry.set_hexpand(True)
+ grid.attach(label,0,0,1,1)
+ grid.attach(page.sgroot_entry,1,0,1,1)
+
+ label = Gtk.Label.new("Backup directory:")
+ page.sgdir_entry = Gtk.Entry()
+ page.sgdir_entry.set_hexpand(True)
+ grid.attach(label,0,1,1,1)
+ grid.attach(page.sgdir_entry,1,1,1,1)
+
+ vbox.append(grid)
+
+ page.lookup_regkeys = self.__create_registry_key_widget("Lookup Registry keys")
+ vbox.append(page.lookup_regkeys)
+
+ page.installdir_regkeys = self.__create_registry_key_widget("Installations directory Registry keys")
+ vbox.append(page.installdir_regkeys)
+
+ page.variables = self.__create_variables_widget()
+ vbox.append(page.variables)
+
+ page.set_child(vbox)
+ self.__stack.add_titled(page,"windows","Windows")
+ stack_page = self.__stack.get_page(page)
+ stack_page.set_icon_name("windows-svgrepo-com")
+
+ return page
+
+
+ def __create_linux_page(self):
+ page = Gtk.ScrolledWindow()
+ vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,2)
+ self.__set_widget_margin(vbox,5)
+
+ grid = Gtk.Grid()
+ label = Gtk.Label.new("Root directory:")
+ page.sgroot_entry = Gtk.Entry()
+ page.sgroot_entry.set_hexpand(True)
+ grid.attach(label,0,0,1,1)
+ grid.attach(page.sgroot_entry,1,0,1,1)
+
+ label = Gtk.Label.new("Backup directory:")
+ page.sgdir_entry = Gtk.Entry()
+ page.sgdir_entry.set_hexpand(True)
+ grid.attach(label,0,1,1,1)
+ grid.attach(page.sgdir_entry,1,1,1,1)
+
+ label = Gtk.Label.new("Executable")
+ page.binary_entry = Gtk.Entry()
+ page.binary_entry.set_hexpand(True)
+ grid.attach(label,0,2,1,1)
+ grid.attach(page.binary_entry,1,2,1,1)
+ vbox.append(grid)
+
+ page.variables = self.__create_variables_widget()
+ vbox.append(page.variables)
+
+ page.set_child(vbox)
+ self.__stack.add_titled(page,"linux","Linux")
+ stack_page = self.__stack.get_page(page)
+ stack_page.set_icon_name("linux-svgrepo-com")
+
+ return page
+
+ def __create_macos_page(self):
+ page = Gtk.ScrolledWindow()
+ vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,2)
+ self.__set_widget_margin(vbox,5)
+
+ grid = Gtk.Grid()
+ label = Gtk.Label.new("Root directory:")
+ page.sgroot_entry = Gtk.Entry()
+ page.sgroot_entry.set_hexpand(True)
+ grid.attach(label,0,0,1,1)
+ grid.attach(page.sgroot_entry,1,0,1,1)
+
+ label = Gtk.Label.new("Backup directory:")
+ page.sgdir_entry = Gtk.Entry()
+ page.sgdir_entry.set_hexpand(True)
+ grid.attach(label,0,1,1,1)
+ grid.attach(page.sgdir_entry,1,1,1,1)
+
+ label = Gtk.Label.new("Executable")
+ page.binary_entry = Gtk.Entry()
+ page.binary_entry.set_hexpand(True)
+ grid.attach(label,0,2,1,1)
+ grid.attach(page.binary_entry,1,2,1,1)
+ vbox.append(grid)
+
+ page.variables = self.__create_variables_widget()
+ vbox.append(page.variables)
+
+ page.set_child(vbox)
+ self.__stack.add_titled(page,"macos","MacOS")
+ stack_page = self.__stack.get_page(page)
+ stack_page.set_icon_name("apple-svgrepo-com")
+
+ return page
+
+ def __create_steam_page(self,name,title):
+ page = Gtk.ScrolledWindow()
+ vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,2)
+ self.__set_widget_margin(vbox,5)
+
+ grid = Gtk.Grid()
+
+ label = Gtk.Label.new("App ID:")
+ page.appid_entry = Gtk.Entry()
+ page.appid_entry.set_hexpand(True)
+ grid.attach(label,0,0,1,1)
+ grid.attach(page.appid_entry,1,0,1,1)
+ vbox.append(grid)
+
+
+ label = Gtk.Label.new("Root directory:")
+ page.sgroot_entry = Gtk.Entry()
+ page.sgroot_entry.set_hexpand(True)
+ grid.attach(label,0,1,1,1)
+ grid.attach(page.sgroot_entry,1,1,1,1)
+
+ label = Gtk.Label.new("Backup directory:")
+ page.sgdir_entry = Gtk.Entry()
+ page.sgdir_entry.set_hexpand(True)
+ grid.attach(label,0,2,1,1)
+ grid.attach(page.sgdir_entry,1,2,1,1)
+
+ page.variables = self.__create_variables_widget()
+ vbox.append(page.variables)
+
+ page.set_child(vbox)
+ self.__stack.add_titled(page,name,title)
+ stack_page = self.__stack.get_page(page)
+ stack_page.set_icon_name("steam-svgrepo-com")
+
+ return page
+ def __set_widget_margin(self,widget,margin):
+ widget.set_margin_start(margin)
+ widget.set_margin_end(margin)
+ widget.set_margin_top(margin)
+ widget.set_margin_bottom(margin)
+
+ def __create_variables_widget(self):
+ widget = Gtk.Frame.new("Variables")
+ vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,0)
+
+ model = Gio.ListStore.new(GameVariableData)
+ selection = Gtk.SingleSelection.new(model)
+ selection.set_autoselect(False)
+ selection.set_can_unselect(True)
+
+ widget.columnview = Gtk.ColumnView.new(selection)
+ widget.columnview.set_vexpand(True)
+
+ widget.actions = Gtk.ActionBar()
+ icon = Gtk.Image.new_from_icon_name("list-add-symbolic")
+ widget.add_button = Gtk.Button()
+ widget.add_button.set_child(icon)
+ widget.add_button.connect('clicked',
+ self._on_variables_add_button_clicked,
+ widget.columnview)
+ widget.actions.pack_start(widget.add_button)
+
+ icon = Gtk.Image.new_from_icon_name("document-edit-symbolic")
+ widget.edit_button = Gtk.Button()
+ widget.edit_button.set_child(icon)
+ widget.edit_button.set_sensitive(False)
+ widget.edit_button.connect('clicked',
+ self._on_variables_edit_buton_clicked,
+ widget.columnview)
+ widget.actions.pack_start(widget.edit_button)
+
+ icon = Gtk.Image.new_from_icon_name("list-remove-symbolic")
+ widget.remove_button = Gtk.Button()
+ widget.remove_button.set_child(icon)
+ widget.remove_button.set_sensitive(False)
+ widget.remove_button.connect('clicked',
+ self._on_variables_remove_button_clicked,
+ widget.columnview)
+ widget.actions.pack_start(widget.remove_button)
+
+ name_column = Gtk.ColumnViewColumn.new("Name",self.__variable_name_factory)
+ name_column.set_expand(True)
+ widget.columnview.append_column(name_column)
+
+ value_column = Gtk.ColumnViewColumn.new("Value",self.__variable_value_factory)
+ value_column.set_expand(True)
+ widget.columnview.append_column(value_column)
+
+ selection.connect('selection-changed',self._on_variable_selection_changed,widget)
+
+ vbox.append(widget.actions)
+ vbox.append(widget.columnview)
+
+ widget.set_child(vbox)
+ return widget
+
+ def __create_registry_key_widget(self,title):
+ widget = Gtk.Frame.new(title)
+ vbox=Gtk.Box.new(Gtk.Orientation.VERTICAL,2)
+
+ widget.actions = Gtk.ActionBar()
+ icon = Gtk.Image.new_from_icon_name("list-add-symbolic")
+ button = Gtk.Button.new()
+ button.set_child(icon)
+ button.connect('clicked',self._on_windows_regkey_add_button_clicked,widget)
+ widget.actions.pack_start(button)
+
+ model = Gio.ListStore.new(RegistryKeyData)
+ selection = Gtk.SingleSelection.new(model)
+ selection.set_autoselect(False)
+ selection.set_can_unselect(True)
+
+ factory = Gtk.SignalListItemFactory()
+ factory.connect('setup',self._on_windows_regkey_setup)
+ factory.connect('bind',self._on_windows_regkey_bind,widget)
+
+ widget.listview = Gtk.ListView.new(selection,factory)
+
+ vbox.append(widget.actions)
+ vbox.append(widget.listview)
+ widget.set_child(vbox)
+
+ return widget
+
+ def reset(self):
+ self.__active_switch.set_active(True)
+ self.__live_switch.set_active(True)
+ self.__name_entry.set_text("")
+ self.__sgname_entry.set_text("")
+ self.__game_variables.columnview.get_model().get_model().remove_all()
+
+ #windows
+ self.__windows.sgroot_entry.set_text("")
+ self.__windows.sgdir_entry.set_text("")
+ self.__windows.variables.columnview.get_model().get_model().remove_all()
+ self.__windows.lookup_regkeys.listview.get_model().get_model().remove_all()
+ self.__windows.installdir_regkeys.listview.get_model().get_model().remove_all()
+
+ #linux
+ self.__linux.sgroot_entry.set_text("")
+ self.__linux.sgdir_entry.set_text("")
+ self.__linux.binary_entry.set_text("")
+ self.__linux.variables.columnview.get_model().get_model().remove_all()
+
+ #linux
+ self.__macos.sgroot_entry.set_text("")
+ self.__macos.sgdir_entry.set_text("")
+ self.__macos.binary_entry.set_text("")
+ self.__macos.variables.columnview.get_model().get_model().remove_all()
+
+ #steam windows
+ self.__steam_windows.sgroot_entry.set_text("")
+ self.__steam_windows.sgdir_entry.set_text("")
+ self.__steam_windows.appid_entry.set_text("")
+ self.__steam_windows.variables.columnview.get_model().get_model().remove_all()
+
+ #steam linux
+ self.__steam_linux.sgroot_entry.set_text("")
+ self.__steam_linux.sgdir_entry.set_text("")
+ self.__steam_linux.appid_entry.set_text("")
+ self.__steam_linux.variables.columnview.get_model().get_model().remove_all()
+
+ #steam macos
+ self.__steam_macos.sgroot_entry.set_text("")
+ self.__steam_macos.sgdir_entry.set_text("")
+ self.__steam_macos.appid_entry.set_text("")
+ self.__steam_macos.variables.columnview.get_model().get_model().remove_all()
+
+ if self.__game:
+ self.__active_switch.set_active(self.__game.is_active)
+ self.__live_switch.set_active(self.__game.is_live)
+ self.__name_entry.set_text(self.__game.name)
+ self.__sgname_entry.set_text(self.__game.savegame_name)
+ for name,value in self.__game.variables.items():
+ self.__game_variables.get_model().get_model().append(GameVariableData(name,value))
+
+ if self.__game.windows:
+ self.__windows.sgroot_entry.set_text(self.__game.windows.savegame_root)
+ self.__windows.sgdir_entry.set_text(self.__game.windows.savegame_dir)
+
+ # set lookup regkeys
+ var_model = self.__windows.variables.columnview.get_model().get_model()
+ grk_model = self.__windows.lookup_regkeys.listview.get_model().get_model()
+ irk_model = self.__windows.installdir_regkeys.listview.get_model().get_model()
+ for rk in self.__game.windows.game_registry_keys:
+ grk_model.append(RegistryKeyData(rk))
+
+ #set installdir regkeys
+ for rk in self.__game.windows.installdir_registry_keys:
+ irk_model.append(RegistryKeyData(rk))
+
+ #set variables
+ for name,value in self.__game.windows.variables.items():
+ var_model.append(GameVariableData(name,value))
+
+ if self.__game.linux:
+ self.__linux.sgroot_entry.set_text(self.__game.linux.savegame_root)
+ self.__linux.sgdir_entry.set_text(self.__game.linux.savegame_dir)
+ self.__linux.binary_entry.set_text(self.__game.linux.binary)
+ var_model = self.__linux.variables.columnview.get_model().get_model()
+ for name,value in self.__game.linux.variables.items():
+ var_model.append(GameVariableData(name,value))
+
+ if self.__game.macos:
+ self.__macos.sgroot_entry.set_text(self.__game.linux.savegame_root)
+ self.__macos.sgdir_entry.set_text(self.__game.linux.savegame_dir)
+ self.__macos.binary_entry.set_text(self.__game.linux.binary)
+ var_model = self.__macos.variables.columnview.get_model().get_model()
+ for name,value in self.__game.linux.variables.items():
+ var_model.append(GameVariableData(name,value))
+
+ if self.__game.steam_windows:
+ self.__steam_windows.sgroot_entry.set_text(self.__game.steam_windows.savegame_root)
+ self.__steam_windows.sgdir_entry.set_text(self.__game.steam_windows.savegame_dir)
+ self.__steam_windows.appid_entry.set_text(self.__game.steam_windows.appid)
+ var_model = self.__steam_windows.variables.columnview.get_model().get_model()
+ for name,value in self.__game.steam_windows.variables.items():
+ var_model.append(GameVariableData(name,value))
+
+ if self.__game.steam_linux:
+ self.__steam_linux.sgroot_entry.set_text(self.__game.steam_linux.savegame_root)
+ self.__steam_linux.sgdir_entry.set_text(self.__game.steam_linux.savegame_dir)
+ self.__steam_linux.appid_entry.set_text(self.__game.steam_linux.appid)
+ var_model = self.__steam_linux.variables.columnview.get_model().get_model()
+ for name,value in self.__game.steam_linux.variables.items():
+ var_model.append(GameVariableData(name,value))
+
+ if self.__game.steam_macos:
+ self.__steam_macos.sgroot_entry.set_text(self.__game.steam_macos.savegame_root)
+ self.__steam_macos.sgdir_entry.set_text(self.__game.steam_macos.savegame_dir)
+ self.__steam_macos.appid_entry.set_text(self.__game.steam_macos.appid)
+ var_model = self.__steam_macos.variables.columnview.get_model().get_model()
+ for name,value in self.__game.steam_macos.variables.items():
+ var_model.append(GameVariableData(name,value))
+ # reset()
+
+ def _on_variable_name_setup(self,factory,item):
+ label = Gtk.Label()
+ item.set_child(label)
+
+ def _on_variable_name_bind(self,factory,item):
+ label = item.get_child()
+ data = item.get_item()
+ data.bind_property('name',label,'label',GObject.BindingFlags.SYNC_CREATE)
+
+ def _on_variable_value_setup(self,factory,item):
+ label = Gtk.Label()
+ item.set_child(label)
+
+ def _on_variable_value_bind(self,factory,item):
+ label = item.get_child()
+ data = item.get_item()
+ data.bind_property('value',label,'label',GObject.BindingFlags.SYNC_CREATE)
+
+ def _on_windows_regkey_setup(self,factory,item):
+ label = Gtk.EditableLabel()
+ item.set_child(label)
+
+ def _on_windows_regkey_bind(self,factory,item,widget):
+ label = item.get_child()
+ data = item.get_item()
+ label.set_text(data.regkey)
+ label.bind_property('text',data,'regkey',GObject.BindingFlags.DEFAULT)
+ label.connect('changed',self._on_windows_regkey_label_changed,widget)
+ if not label.get_text():
+ label.start_editing()
+ label.grab_focus()
+
+ def _on_stack_sidebar_selected_rows_changed(self,sidebar):
+ row = sidebar.get_selected_row()
+ self.__stack.set_visible_child_name(row.get_child().page_name)
+
+ def _on_variables_add_button_clicked(self,button,columnview):
+ dialog = GameVariableDialog(self,columnview)
+ dialog.present()
+
+ def _on_variables_remove_button_clicked(self,button,columnview):
+ selection = columnview.get_model()
+ model = selection.get_model()
+ selected = selection.get_selected()
+ if selected == Gtk.INVALID_LIST_POSITION:
+ return
+ model.remove(selected)
+
+ def _on_variables_edit_buton_clicked(self,button,columnview):
+ data = columnview.get_model().get_selected()
+ if data:
+ dialog = GameVariableDialog(self,columnview,data)
+ dialog.present()
+
+ def _on_variable_selection_changed(self,selection,position,n_items,var_widget):
+ if (selection.get_model().get_n_items() == 0) or (selection.get_selected() == Gtk.INVALID_LIST_POSITION):
+ var_widget.edit_button.set_sensitive(False)
+ var_widget.remove_button.set_sensitive(False)
+ else:
+ var_widget.edit_button.set_sensitive(True)
+ var_widget.remove_button.set_sensitive(True)
+
+ def _on_windows_regkey_add_button_clicked(self,button,widget):
+ widget.listview.get_model().get_model().append(RegistryKeyData())
+
+ def _on_windows_regkey_label_changed(self,label,widget):
+ if not label.get_text():
+ model = widget.listview.get_model().get_model()
+ i = 0
+ while i < model.get_n_items():
+ item = model.get_item(i)
+ if not item.regkey:
+ model.remove(i)
+ continue
+ i += 1
+
\ No newline at end of file
diff --git a/sgbackup/gui/_settingsdialog.py b/sgbackup/gui/_settingsdialog.py
new file mode 100644
index 0000000..93ac5d9
--- /dev/null
+++ b/sgbackup/gui/_settingsdialog.py
@@ -0,0 +1,97 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see . #
+###############################################################################
+
+from gi.repository import Gtk,GLib,Gio
+from gi.repository.GObject import GObject,Signal,Property
+
+from ..settings import settings
+
+class SettingsDialog(Gtk.Dialog):
+ def __init__(self,parent=None):
+ Gtk.Dialog.__init__(self)
+ if parent:
+ self.set_transient_for(parent)
+ self.set_default_size(800,600)
+ vbox = self.get_content_area()
+ paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL)
+ paned.set_position(250)
+
+ self.__stack = Gtk.Stack()
+ self.__stack_sidebar = Gtk.StackSidebar.new()
+ self.__add_general_settings_page()
+
+ paned.set_start_child(self.__stack_sidebar)
+ paned.set_end_child(self.__stack)
+ paned.set_vexpand(True)
+ self.__stack_sidebar.set_stack(self.__stack)
+
+ vbox.append(paned)
+
+ self.add_button("Apply",Gtk.ResponseType.APPLY)
+ self.add_button("Cancel",Gtk.ResponseType.CANCEL)
+
+ def __add_general_settings_page(self):
+ page = Gtk.ScrolledWindow()
+ vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ grid = Gtk.Grid()
+
+ label = Gtk.Label.new('Backup directory: ')
+ grid.attach(label,0,0,1,1)
+ self.__backupdir_label = Gtk.Label.new(settings.backup_dir)
+ self.__backupdir_label.set_hexpand(True)
+ grid.attach(self.__backupdir_label,1,0,1,1)
+ img = Gtk.Image.new_from_icon_name('document-open-symbolic')
+ img.set_pixel_size(16)
+ backupdir_button = Gtk.Button()
+ backupdir_button.set_child(img)
+ backupdir_button.connect('clicked',self._on_backupdir_button_clicked)
+ grid.attach(backupdir_button,2,0,1,1)
+
+ vbox.append(grid)
+ page.set_child(vbox)
+
+ self.add_page(page,"general","Generic settings")
+
+ def _on_backupdir_dialog_select_folder(self,dialog,result,*data):
+ try:
+ dir = dialog.select_folder_finish(result)
+ if dir is not None:
+ self.__backupdir_label.set_text(dir.get_path())
+ except:
+ pass
+
+ def _on_backupdir_button_clicked(self,button):
+ dialog = Gtk.FileDialog.new()
+ dialog.set_title("sgbackup: Choose backup folder")
+ dialog.select_folder(self,None,self._on_backupdir_dialog_select_folder)
+
+
+
+ def add_page(self,page,name,title):
+ self.__stack.add_titled(page,name,title)
+
+ def do_response(self,response):
+ if response == Gtk.ResponseType.APPLY:
+ self.emit('save')
+ settings.save()
+ self.destroy()
+
+ @Signal(name='save')
+ def do_save(self):
+ settings.backup_dir = self.__backupdir_label.get_text()
+
\ No newline at end of file
diff --git a/sgbackup/gui/application.py b/sgbackup/gui/application.py
deleted file mode 100644
index 26075df..0000000
--- a/sgbackup/gui/application.py
+++ /dev/null
@@ -1,83 +0,0 @@
-###############################################################################
-# sgbackup - The SaveGame Backup tool #
-# Copyright (C) 2024 Christian Moser #
-# #
-# This program is free software: you can redistribute it and/or modify #
-# it under the terms of the GNU General Public License as published by #
-# the Free Software Foundation, either version 3 of the License, or #
-# (at your option) any later version. #
-# #
-# This program is distributed in the hope that it will be useful, #
-# but WITHOUT ANY WARRANTY; without even the implied warranty of #
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
-# GNU General Public License for more details. #
-# #
-# You should have received a copy of the GNU General Public License #
-# along with this program. If not, see . #
-###############################################################################
-
-from gi.repository import Gtk,GObject,Gio
-from .appwindow import AppWindow
-
-import logging; logger=logging.getLogger(__name__)
-
-class Application(Gtk.Application):
- def __init__(self,*args,**kwargs):
- AppFlags = Gio.ApplicationFlags
- kwargs['application_id'] = 'org.sgbackup.sgbackup'
- kwargs['flags'] = AppFlags.FLAGS_NONE
- Gtk.Application.__init__(self,*args,**kwargs)
-
- self.__logger = logger.getChild('Application')
- self.__builder = None
- self.__appwindow = None
-
- @property
- def _logger(self):
- return self.__logger
-
- @GObject.Property
- def appwindow(self):
- return self.__appwindow
-
- def do_startup(self):
- self._logger.debug('do_startup()')
- if not self.__builder:
- self.__builder = Gtk.Builder.new()
-
- Gtk.Application.do_startup(self)
-
- action_about = Gio.SimpleAction.new('about',None)
- action_about.connect('activate',self.on_action_about)
- self.add_action(action_about)
-
- action_quit = Gio.SimpleAction.new('quit',None)
- action_quit.connect('activate',self.on_action_quit)
- self.add_action(action_quit)
-
- action_settings = Gio.SimpleAction.new('settings',None)
- action_settings.connect('activate',self.on_action_settings)
- self.add_action(action_settings)
-
- # add accels
- self.set_accels_for_action('app.quit',["q"])
-
- @GObject.Property
- def builder(self):
- return self.__builder
-
- def do_activate(self):
- self._logger.debug('do_activate()')
- if not (self.__appwindow):
- self.__appwindow = AppWindow(application=self)
-
- self.appwindow.present()
-
- def on_action_about(self,action,param):
- pass
-
- def on_action_settings(self,action,param):
- pass
-
- def on_action_quit(self,action,param):
- self.quit()
\ No newline at end of file
diff --git a/sgbackup/gui/appmenu.ui b/sgbackup/gui/appmenu.ui
index acb558e..a87c277 100644
--- a/sgbackup/gui/appmenu.ui
+++ b/sgbackup/gui/appmenu.ui
@@ -1,6 +1,25 @@