From 352b4214799ebea884778e0f31c6bfd861f8f80c Mon Sep 17 00:00:00 2001 From: Christian Moser Date: Thu, 16 Jan 2025 23:22:14 +0100 Subject: [PATCH] 2025.01.16 23:22:14 --- sgbackup/game.py | 83 ++++++++++------ sgbackup/gui/_app.py | 128 ++++++++++++++++++++----- sgbackup/gui/_gamedialog.py | 1 + sgbackup/icons/hicolor/symbolic/apps/$ | 56 ----------- 4 files changed, 157 insertions(+), 111 deletions(-) delete mode 100644 sgbackup/icons/hicolor/symbolic/apps/$ diff --git a/sgbackup/game.py b/sgbackup/game.py index 9aedabe..e75c1ff 100644 --- a/sgbackup/game.py +++ b/sgbackup/game.py @@ -143,6 +143,20 @@ class SavegameType(StrEnum): return st.UNSET + +SAVEGAME_TYPE_ICONS = { + SavegameType.UNSET : None, + SavegameType.WINDOWS: 'windows-svgrepo-com-symbolic', + SavegameType.LINUX: 'linux-svgrepo-com-symbolic.svg', + SavegameType.MACOS: 'apple-svgrepo-com-symbolic.svg', + SavegameType.STEAM_LINUX: 'steam-svgrepo-com-symbolic', + SavegameType.STEAM_MACOS: 'steam-svgrepo-com-symbolic', + SavegameType.STEAM_WINDOWS: 'steam-svgrepo-com-symbolic', + SavegameType.EPIC_LINUX: 'epic-games-svgrepo-com-symbolic', + SavegameType.EPIC_WINDOWS: 'epic-games-svgrepo-com-symbolic', + SavegameType.GOG_LINUX: 'gog-com-svgrepo-com-symbolic', + SavegameType.GOG_WINDOWS: 'gog-com-svgrepo-com-symbolic', +} class GameFileType(StrEnum): """ @@ -619,10 +633,24 @@ class WindowsGame(GameData): def game_registry_keys(self)->list: return self.__game_registry_keys + @game_registry_keys.setter + def game_registry_keys(self,keys:list[str]|tuple[str]|None): + self.__game_registry_keys = [] + if keys: + for rk in keys: + self.__game_registry_keys.append(str(rk)) + @Property def installdir_registry_keys(self)->list: return self.__installdir_registry_keys + @installdir_registry_keys.setter + def installdir_registry_keys(self,keys:list[str]|tuple[str]|None): + self.__installdir_registry_keys = [] + if keys: + for rk in keys: + self.__installdir_registry_keys.append(str(rk)) + @Property def is_installed(self)->bool|None: if not PLATFORM_WIN32 or not self.game_registry_keys: @@ -666,6 +694,7 @@ class WindowsGame(GameData): def get_variables(self): variables = super().get_variables() variables["INSTALLDIR"] = self.installdir if self.installdir else "" + return variables def serialize(self): ret = super().serialize() @@ -900,8 +929,8 @@ class Game(GObject): _logger = logger.getChild("Game.new_from_dict()") def get_file_match(conf:dict): - conf_fm = conf['file_match'] if 'file_match' in conf else None - conf_im = conf['ignore_match'] if 'ignore_match' in conf else None + conf_fm = conf['file_match'] if 'file_match' in conf else [] + conf_im = conf['ignore_match'] if 'ignore_match' in conf else [] if (conf_fm): file_match = [] @@ -933,11 +962,11 @@ class Game(GObject): return (file_match,ignore_match) def new_steam_game(conf,cls:SteamGame): - appid = conf['appid'] if 'appid' in conf else None - sgroot = conf['savegame_root'] if 'savegame_root' in conf else None - sgdir = conf['savegame_dir'] if 'savegame_dir' in conf else None - vars = conf['variables'] if 'variables' in conf else None - installdir = conf['installdir'] if 'installdir' in conf else None + appid = conf['appid'] if 'appid' in conf else "" + sgroot = conf['savegame_root'] if 'savegame_root' in conf else "" + sgdir = conf['savegame_dir'] if 'savegame_dir' in conf else "" + vars = conf['variables'] if 'variables' in conf else {} + installdir = conf['installdir'] if 'installdir' in conf else "" file_match,ignore_match = get_file_match(conf) if appid is not None and sgroot and sgdir: @@ -966,40 +995,37 @@ class Game(GObject): winconf = config['windows'] sgroot = winconf['savegame_root'] if 'savegame_root' in winconf else None sgdir = winconf['savegame_dir'] if 'savegame_dir' in winconf else None - vars = winconf['variables'] if 'variables' in winconf else None + vars = winconf['variables'] if 'variables' in winconf else {} installdir = winconf['installdir'] if 'installdir' in winconf else None - game_regkeys = winconf['game_registry_keys'] if 'game_registry_keys' in winconf else None - installdir_regkeys = winconf['installdir_registry_keys'] if 'installdir_registry_keys' in winconf else None + game_regkeys = winconf['game_registry_keys'] if 'game_registry_keys' in winconf else [] + installdir_regkeys = winconf['installdir_registry_keys'] if 'installdir_registry_keys' in winconf else [] file_match,ignore_match = get_file_match(winconf) - if (sgroot and sgdir): - game.windows = WindowsGame(sgroot, - sgdir, - vars, - installdir, - game_regkeys, - installdir_regkeys, - file_match, - ignore_match) + game.windows = WindowsGame(sgroot, + sgdir, + vars, + installdir, + game_regkeys, + installdir_regkeys, + file_match, + ignore_match) if 'linux' in config: linconf = config['linux'] sgroot = linconf['savegame_root'] if 'savegame_root' in linconf else None sgdir = linconf['savegame_dir'] if 'savegame_dir' in linconf else None - vars = linconf['variables'] if 'variables' in linconf else None + vars = linconf['variables'] if 'variables' in linconf else {} binary = linconf['binary'] if 'binary' in linconf else None file_match,ignore_match = get_file_match(linconf) - if (sgroot and sgdir): - game.linux = LinuxGame(sgroot,sgdir,vars,binary,file_match,ignore_match) + game.linux = LinuxGame(sgroot,sgdir,vars,binary,file_match,ignore_match) if 'macos' in config: macconf = config['macos'] sgroot = macconf['savegame_root'] if 'savegame_root' in macconf else None sgdir = macconf['savegame_dir'] if 'savegame_dir' in macconf else None - vars = macconf['variables'] if 'variables' in macconf else None + vars = macconf['variables'] if 'variables' in macconf else {} binary = macconf['binary'] if 'binary' in macconf else None file_match,ignore_match = get_file_match(macconf) - if (sgroot and sgdir): - game.macos = MacOSGame(sgroot,sgdir,vars,binary,file_match,ignore_match) + game.macos = MacOSGame(sgroot,sgdir,vars,binary,file_match,ignore_match) if 'steam_windows' in config: game.steam_windows = new_steam_game(config['steam_windows'],SteamWindowsGame) @@ -1099,9 +1125,9 @@ class Game(GObject): @Property def filename(self)->str|None: if not self.__filename: - if not self.__key: + if not self.key: return None - os.path.join(settings.gameconf_dir,'.'.join((self.key,'gameconf'))) + return os.path.join(settings.gameconf_dir,'.'.join((self.key,'gameconf'))) return self.__filename @@ -1428,9 +1454,8 @@ class GameManager(GObject): if not game: self.logger.warn("Not loaded game \"{game}\"!".format( game=(game.name if game is not None else "UNKNOWN GAME"))) - print(game.serialize()) continue - except Exception as ex: + except GLib.Error as ex: #Exception as ex: self.logger.error("Unable to load gameconf {gameconf}! ({what})".format( gameconf = os.path.basename(gcf), what = str(ex))) diff --git a/sgbackup/gui/_app.py b/sgbackup/gui/_app.py index 65cf38a..d6d1347 100644 --- a/sgbackup/gui/_app.py +++ b/sgbackup/gui/_app.py @@ -16,7 +16,7 @@ # along with this program. If not, see . # ############################################################################### -from gi.repository import Gtk,Gio,Gdk +from gi.repository import Gtk,Gio,Gdk,GLib from gi.repository.GObject import GObject,Signal,Property,SignalFlags,BindingFlags import logging; logger=logging.getLogger(__name__) @@ -28,7 +28,7 @@ from pathlib import Path from ..settings import settings from ._settingsdialog import SettingsDialog from ._gamedialog import GameDialog -from ..game import Game,GameManager +from ..game import Game,GameManager,SAVEGAME_TYPE_ICONS from ._steam import SteamLibrariesDialog,NewSteamAppsDialog __gtype_name__ = __name__ @@ -52,6 +52,11 @@ class GameView(Gtk.ScrolledWindow): pass self.__liststore.append(g) + factory_icon = Gtk.SignalListItemFactory.new() + factory_icon.connect('setup',self._on_icon_column_setup) + factory_icon.connect('bind',self._on_icon_column_bind) + column_icon = Gtk.ColumnViewColumn.new("",factory_icon) + factory_key = Gtk.SignalListItemFactory.new() factory_key.connect('setup',self._on_key_column_setup) factory_key.connect('bind',self._on_key_column_bind) @@ -75,14 +80,22 @@ class GameView(Gtk.ScrolledWindow): factory_live.connect('unbind',self._on_live_column_unbind) column_live = Gtk.ColumnViewColumn.new("Live",factory_live) + factory_actions = Gtk.SignalListItemFactory.new() + factory_actions.connect('setup',self._on_actions_column_setup) + factory_actions.connect('bind',self._on_actions_column_bind) + column_actions = Gtk.ColumnViewColumn.new("",factory_actions) + selection = Gtk.SingleSelection.new(self._liststore) self.__columnview = Gtk.ColumnView.new(selection) + self.columnview.append_column(column_icon) 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.append_column(column_actions) self.columnview.set_single_click_activate(True) + self.set_child(self.columnview) self.refresh() @@ -115,6 +128,22 @@ class GameView(Gtk.ScrolledWindow): for game in GameManager.get_global().games.values(): self.__liststore.append(game) + def _on_icon_column_setup(self,factory,item): + item.set_child(Gtk.Image()) + + def _on_icon_column_bind(self,factory,item): + def transform_to_icon_name(_bidning,sgtype): + icon_name = SAVEGAME_TYPE_ICONS[sgtype] if sgtype in SAVEGAME_TYPE_ICONS else None + if icon_name: + return icon_name + return "" + icon = item.get_child() + game = item.get_item() + game.bind_property('savegame_type',icon,'icon_name',BindingFlags.SYNC_CREATE,transform_to_icon_name) + + + + def _on_key_column_setup(self,factory,item): item.set_child(Gtk.Label()) @@ -171,9 +200,9 @@ class GameView(Gtk.ScrolledWindow): dialog.hide() dialog.destroy() - game.is_live = state + game.is_live = switch.get_active() game.save() - if not state: + if not game.is_live: dialog = Gtk.MessageDialog() dialog.set_transient_for(self.get_root()) dialog.props.buttons = Gtk.ButtonsType.YES_NO @@ -184,21 +213,61 @@ class GameView(Gtk.ScrolledWindow): dialog.connect('response',on_dialog_response) dialog.present() - @property - def current_game(self)->Game|None: - """ - current_game Get the currently selected `Game` + def _on_actions_column_setup(self,action,item): + child = Gtk.Box.new(Gtk.Orientation.HORIZONTAL,2) + icon = Gtk.Image.new_from_icon_name('document-edit-symbolic') + child.edit_button = Gtk.Button() + child.edit_button.set_child(icon) + child.append(child.edit_button) - If no `Game` is selected this property resolves to `Null` - - :type: Game|None - """ - selection = self._columnview.get_model() - pos = selection.get_selected() - if pos == Gtk.INVALID_LIST_POSITION: - return None - return selection.get_model().get_item(pos) + icon = Gtk.Image.new_from_icon_name('list-remove-symbolic') + child.remove_button = Gtk.Button() + child.remove_button.set_child(icon) + child.append(child.remove_button) + + item.set_child(child) + + def _on_actions_column_bind(self,action,item): + child = item.get_child() + game = item.get_item() + + child.edit_button.connect('clicked',self._on_columnview_edit_button_clicked,item) + child.remove_button.connect('clicked',self._on_columnview_remove_button_clicked,item) + def _on_columnview_edit_button_clicked(self,button,item): + def on_dialog_response(dialog,response): + if response == Gtk.ResponseType.APPLY: + self.refresh() + + game = item.get_item() + dialog = GameDialog(self.get_root(),game) + dialog.connect('response',on_dialog_response) + dialog.present() + + def _on_columnview_remove_button_clicked(self,button,item): + def on_dialog_response(dialog,response,game:Game): + if response == Gtk.ResponseType.YES: + if os.path.isfile(game.filename): + os.unlink(game.filename) + for i in range(self._liststore.get_n_items()): + item = self._liststore.get_item(i) + if item.key == game.key: + self._liststore.remove_item(i) + return + + dialog.hide() + dialog.destroy() + + game = item.get_item() + dialog = Gtk.MessageDialog(buttons=Gtk.ButtonsType.YES_NO, + text="Do you really want to remove the game {game}?".format( + game=game.name), + use_markup=True, + secondary_text="Removing games cannot be undone!!!") + dialog.set_transient_for(self.get_root()) + dialog.connect('response',on_dialog_response,game) + dialog.present() + # GameView class class BackupViewData(GObject): @@ -278,7 +347,7 @@ class BackupViewData(GObject): def _on_selection_changed(self,selection): pass -class BackupView(Gtk.ScrolledWindow): +class BackupView(Gtk.Box): """ BackupView This view displays the backup for the selected `Game`. """ @@ -290,7 +359,9 @@ class BackupView(Gtk.ScrolledWindow): :param gameview: The `GameView` to connect this class to. :type gameview: GameView """ - Gtk.ScrolledWindow.__init__(self) + Gtk.Box.__init__(self,orientation=Gtk.Orientation.VERTICAL) + self._title_label = Gtk.Label() + scrolled = Gtk.ScrolledWindow() self.__gameview = gameview self.__liststore = Gio.ListStore() @@ -316,11 +387,14 @@ class BackupView(Gtk.ScrolledWindow): self.__columnview.append_column(live_column) self.__columnview.append_column(sgname_column) self.__columnview.append_column(timestamp_column) + self.__columnview.set_vexpand(True) - self._on_gameview_selection_changed(selection) - self.gameview.columnview.get_model().connect('selection-changed',self._on_gameview_selection_changed) + self.gameview.columnview.connect('activate',self._on_gameview_columnview_activate) - self.set_child(self.__columnview) + scrolled.set_child(self.__columnview) + + self.append(self._title_label) + self.append(scrolled) @property def gameview(self)->GameView: @@ -383,10 +457,12 @@ class BackupView(Gtk.ScrolledWindow): 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 + def _on_gameview_columnview_activate(self,columnview,position): + model = columnview.get_model().get_model() + game = model.get_item(position) + + self._title_label.set_markup("{}".format(GLib.markup_escape_text(game.name))) + class AppWindow(Gtk.ApplicationWindow): diff --git a/sgbackup/gui/_gamedialog.py b/sgbackup/gui/_gamedialog.py index 66e0561..008d37d 100644 --- a/sgbackup/gui/_gamedialog.py +++ b/sgbackup/gui/_gamedialog.py @@ -831,6 +831,7 @@ class GameDialog(Gtk.Dialog): self.__active_switch.set_active(self.__game.is_active if self.has_game else True) self.__live_switch.set_active(self.__game.is_live if self.has_game else True) self.__name_entry.set_text(self.__game.name if self.has_game else "") + self.__key_entry.set_text(self.__game.key if self.has_game else "") self.__sgname_entry.set_text(self.__game.savegame_name if self.has_game else "") set_variables(self.__game_variables,self.__game.variables if self.has_game else None) diff --git a/sgbackup/icons/hicolor/symbolic/apps/$ b/sgbackup/icons/hicolor/symbolic/apps/$ deleted file mode 100644 index b764c16..0000000 --- a/sgbackup/icons/hicolor/symbolic/apps/$ +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - -