From b9f4a37a528cad874a5e43dd9df744dca33d837d Mon Sep 17 00:00:00 2001 From: Christian Moser Date: Tue, 25 Feb 2025 22:23:41 +0100 Subject: [PATCH] Search function added to GameView --- msys-install.sh | 4 +- sgbackup/archiver/_archiver.py | 1 - sgbackup/gui/_app.py | 191 ++++++++++++++++++++++++++------ sgbackup/gui/_settingsdialog.py | 30 ++++- sgbackup/settings.py | 15 +++ 5 files changed, 202 insertions(+), 39 deletions(-) diff --git a/msys-install.sh b/msys-install.sh index 1f7f86e..2324044 100755 --- a/msys-install.sh +++ b/msys-install.sh @@ -56,7 +56,7 @@ cat > "$install_ps1" << EOF [Environment]::SetEnvironmentVariable("Path","\$env:PATH;$wbindir","User") \$desktop_dir=[Environment]::getFolderPath("Desktop") -\$startmenu_dir=[Environment]::getFolderPath("StartMenu") +\$startmenu_dir=[Environment]::getFolderPath("StartMenu") + "\\Programs" \$picture_dir=[Environment]::getFolderPath("MyPictures") Copy-Item -Path "$wproject_dir\\sgbackup\\icons\\sgbackup.ico" -Destination "\$picture_dir\\sgbackup.ico" -Force @@ -64,7 +64,7 @@ Copy-Item -Path "$wproject_dir\\sgbackup\\icons\\sgbackup.ico" -Destination "\$p foreach (\$dir in \$desktop_dir,\$startmenu_dir) { \$shell=New-Object -ComObject WScript.Shell \$shortcut=\$shell.CreateShortcut("\$dir\\sgbackup.lnk") - \$shortcut.TargetPath='$pythonwpath' + \$shortcut.TargetPath='$pythonpath' \$shortcut.Arguments='-m sgbackup.gui' \$shortcut.IconLocation="\$picture_dir\\sgbackup.ico" \$shortcut.Save() diff --git a/sgbackup/archiver/_archiver.py b/sgbackup/archiver/_archiver.py index cf76f58..e1854f2 100644 --- a/sgbackup/archiver/_archiver.py +++ b/sgbackup/archiver/_archiver.py @@ -355,4 +355,3 @@ class ArchiverManager(GObject): if (self.is_archive(filename)): ret.append(filename) return ret - \ No newline at end of file diff --git a/sgbackup/gui/_app.py b/sgbackup/gui/_app.py index 75d7d95..ac47a6a 100644 --- a/sgbackup/gui/_app.py +++ b/sgbackup/gui/_app.py @@ -18,6 +18,7 @@ from gi.repository import Gtk,Gio,Gdk,GLib from gi.repository.GObject import GObject,Signal,Property,SignalFlags,BindingFlags +import rapidfuzz import logging; logger=logging.getLogger(__name__) @@ -37,6 +38,37 @@ from ..archiver import ArchiverManager __gtype_name__ = __name__ +class GameViewData(GObject): + def __init__(self,game:Game): + GObject.__init__(self) + self.__game = game + self.__fuzzy_match = 0.0 + + @property + def game(self)->Game: + return self.__game + + @Property(type=str) + def name(self)->str: + return self.game.name + + @Property(type=str) + def key(self)->str: + return self.game.key + + @Property + def savegame_type(self)->SavegameType: + return self.game.savegame_type + + @Property(type=float) + def fuzzy_match(self)->float: + return self.__fuzzy_match + + @fuzzy_match.setter + def fuzzy_match(self,match:float): + self.__fuzzy_match = match + + class GameViewKeySorter(Gtk.Sorter): def __init__(self,sort_ascending:bool=True,*args,**kwargs): Gtk.Sorter.__init__(self) @@ -97,6 +129,48 @@ class GameViewNameSorter(Gtk.Sorter): return Gtk.Ordering.SMALLER else: return Gtk.Ordering.EQUAL + +class GameViewColumnSorter(Gtk.ColumnViewSorter): + def __init__(self,*args,**kwargs): + Gtk.ColumnViewSorter.__init__(self,*args,**kwargs) + self.__key_sorter = GameViewKeySorter + self.__name_sorter = GameViewNameSorter + + def do_compare(self,item1,item2): + game1 = item1.game + game2 = item2.game + column = self.get_primary_sort_column() + sort_ascending = (self.get_primary_sort_order() == Gtk.SortType.ASCENDING) + if column == 1: + self.__key_sorter.sort_ascending == sort_ascending + return self.__key_sorter.do_compare(game1,game2) + else: + self.__name_sorter.sort_ascending = sort_ascending + return self.__name_sorter.do_comapre(game1,game2) + +class GameViewMatchSorter(Gtk.Sorter): + def do_compare(self,item1:GameViewData,item2:GameViewData): + if item1.fuzzy_match < item2.fuzzy_match: + return Gtk.Ordering.LARGER + elif item1.fuzzy_match > item2.fuzzy_match: + return Gtk.Ordering.SMALLER + + name1 = item1.name.lower() + name2 = item2.name.lower() + if name1 > name2: + return Gtk.Ordering.LARGER + elif name1 < name2: + return Gtk.Ordering.SMALLER + else: + return Gtk.Ordering.EQUAL + +class GameViewMatchFilter(Gtk.Filter): + def do_match(self,item:GameViewData|None): + if item is None: + return False + + return item.fuzzy_match > 0.0 + class GameView(Gtk.Box): """ GameView The View for games. @@ -151,33 +225,39 @@ class GameView(Gtk.Box): # Add a the search entry self.__search_entry = Gtk.Entry() self.__search_entry.set_hexpand(True) + self.__search_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.PRIMARY,'edit-find-symbolic') + self.__search_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY,'edit-clear-symbolic') + self.__search_entry.connect('icon-release',self._on_search_entry_icon_release) + self.__search_entry.connect('changed',self._on_search_entry_changed) self.actionbar.pack_end(self.__search_entry) self.actionbar.pack_end(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)) self.append(self.actionbar) - - self.__liststore = Gio.ListStore.new(Game) + self.__columnview = Gtk.ColumnView() + columnview_sorter = self.columnview.get_sorter() + self.__liststore = Gio.ListStore.new(GameViewData) for g in GameManager.get_global().games.values(): - pass - self.__liststore.append(g) - self.__sort_model = Gtk.SortListModel.new(self._liststore,self.__name_sorter) + self.__liststore.append(GameViewData(g)) + self.__filter_model = Gtk.FilterListModel.new(self._liststore,None) + self.__sort_model = Gtk.SortListModel.new(self.__filter_model,columnview_sorter) factory_icon = Gtk.SignalListItemFactory.new() factory_icon.connect('setup',self._on_icon_column_setup) factory_icon.connect('bind',self._on_icon_column_bind) - factory_icon.connect('unbind',self._on_icon_column_unbind) 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) column_key = Gtk.ColumnViewColumn.new("Key",factory_key) + column_key.set_sorter(GameViewKeySorter()) 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 = Gtk.ColumnViewColumn.new("Name",factory_name) + column_name.set_sorter(GameViewNameSorter()) column_name.set_expand(True) factory_active = Gtk.SignalListItemFactory.new() @@ -195,11 +275,16 @@ class GameView(Gtk.Box): factory_actions = Gtk.SignalListItemFactory.new() factory_actions.connect('setup',self._on_actions_column_setup) factory_actions.connect('bind',self._on_actions_column_bind) - #factory_actions.connect('ubind',self._on_actions_column_unbind) + #factory_actions.connect('unbind',self._on_actions_column_unbind) column_actions = Gtk.ColumnViewColumn.new("",factory_actions) - selection = Gtk.SingleSelection.new(self.__sort_model) - self.__columnview = Gtk.ColumnView.new(selection) + selection = Gtk.SingleSelection() + selection.set_autoselect(False) + selection.set_can_unselect(True) + selection.set_model(self.__sort_model) + self.columnview.set_model(selection) + + self.columnview.set_vexpand(True) self.columnview.set_hexpand(True) self.columnview.append_column(column_icon) @@ -208,12 +293,11 @@ class GameView(Gtk.Box): self.columnview.append_column(column_active) self.columnview.append_column(column_live) self.columnview.append_column(column_actions) + self.columnview.sort_by_column(column_name,Gtk.SortType.ASCENDING) self.columnview.set_single_click_activate(True) - + self.columnview.get_vadjustment().set_value(0) scrolled.set_child(self.columnview) - self.append(scrolled) - self.refresh() @property def _liststore(self)->Gio.ListStore: @@ -249,8 +333,10 @@ class GameView(Gtk.Box): @Signal(name="refresh",return_type=None,arg_types=(),flags=SignalFlags.RUN_FIRST) def do_refresh(self): self._liststore.remove_all() + self.__search_entry.set_text("") + for game in GameManager.get_global().games.values(): - self._liststore.append(game) + self._liststore.append(GameViewData(game)) def _on_game_dialog_response(self,dialog,response): if response == Gtk.ResponseType.APPLY: @@ -285,7 +371,7 @@ class GameView(Gtk.Box): def _on_backup_active_live_button_clicked(self,button): backup_games = [] for i in range(self._liststore.get_n_items()): - game = self._liststore.get_item(i) + game = self._liststore.get_item(i).game if game.is_live and game.is_active and os.path.exists(os.path.join(game.savegame_root,game.savegame_dir)): backup_games.append(game) @@ -294,6 +380,51 @@ class GameView(Gtk.Box): dialog.set_modal(False) dialog.run() + def __real_search(self,search_name:str): + choices = [] + for i in range(self._liststore.get_n_items()): + item = self._liststore.get_item(i) + choices.append(item.name) + item.fuzzy_match = 0.0 + + result = rapidfuzz.process.extract(query=search_name, + choices=choices, + limit=settings.search_max_results, + scorer=rapidfuzz.fuzz.WRatio) + + + for name,match,pos in result: + item = self._liststore.get_item(pos) + item.fuzzy_match = match + print("\"{}\" | \"{}\"".format(name,item.name),match,pos) + + print("-"*80) + self.__filter_model.set_filter(GameViewMatchFilter()) + self.__sort_model.set_sorter(GameViewMatchSorter()) + + def _on_search_entry_icon_release(self,entry,icon_pos): + #TODO############################################### + if icon_pos == Gtk.EntryIconPosition.PRIMARY: + search_name=entry.get_text() + if len(search_name) == 0: + self.__filter_model.set_filter(None) + self.__sort_model.set_sorter(self.columnview.get_sorter()) + else: + self.__real_search(entry.get_text()) + elif icon_pos == Gtk.EntryIconPosition.SECONDARY: + self.__search_entry.set_text("") + self.__filter_model.set_filter(None) + self.__sort_model.set_sorter(self.columnview.get_sorter()) + + def _on_search_entry_changed(self,entry): + #TODO############################################### + search_name = entry.get_text() + if len(search_name) == 0: + self.__filter_model.set_filter(None) + self.__sort_model.set_sorter(self.columnview.get_sorter()) + elif len(search_name) >= settings.search_min_chars: + self.__real_search(search_name) + def _on_icon_column_setup(self,factory,item): image = Gtk.Image() image.set_pixel_size(24) @@ -306,16 +437,10 @@ class GameView(Gtk.Box): return icon_name return "" icon = item.get_child() - game = item.get_item() + game = item.get_item().game if not hasattr(game,'_savegame_type_to_icon_name_binding'): game._savegame_type_to_icon_name_binding = game.bind_property('savegame_type',icon,'icon_name',BindingFlags.SYNC_CREATE,transform_to_icon_name) - def _on_icon_column_unbind(self,factory,item): - game = item.get_item() - if hasattr(game,'_savegame_type_to_icon_name_binding'): - game._savegame_type_to_icon_name_binding.unbind() - del game._savegame_type_to_icon_name_binding - def _on_key_column_setup(self,factory,item): label = Gtk.Label() label.set_xalign(0.0) @@ -324,7 +449,7 @@ class GameView(Gtk.Box): def _on_key_column_bind(self,factory,item): label = item.get_child() - game = item.get_item() + game = item.get_item().game game.bind_property('key',label,'label',BindingFlags.SYNC_CREATE, lambda _binding,s: '{}'.format(GLib.markup_escape_text(s))) @@ -336,7 +461,7 @@ class GameView(Gtk.Box): def _on_name_column_bind(self,factory,item): label = item.get_child() - game = item.get_item() + game = item.get_item().game if not hasattr(label,'_property_label_from_name_binding'): label._property_label_from_name_binding = game.bind_property('name', label, @@ -350,7 +475,7 @@ class GameView(Gtk.Box): def _on_active_column_bind(self,factory,item): switch = item.get_child() - game = item.get_item() + game = item.get_item().game switch.set_active(game.is_active) item._signal_active_state_set = switch.connect('state-set',self._on_active_state_set,game) @@ -369,7 +494,7 @@ class GameView(Gtk.Box): def _on_live_column_bind(self,factory,item): switch = item.get_child() - game = item.get_item() + game = item.get_item().game switch.set_active(game.is_live) if not hasattr(item,'_signal_live_state_set'): item._signal_live_state_set = switch.connect('state-set',self._on_live_state_set,game) @@ -423,7 +548,7 @@ class GameView(Gtk.Box): def _on_actions_column_bind(self,action,item): child = item.get_child() - game = item.get_item() + game = item.get_item().game archiver_manager = ArchiverManager.get_global() # check if we are already connected. @@ -472,7 +597,7 @@ class GameView(Gtk.Box): if hasattr(parent,'statusbar'): parent.statusbar.pop(1) - game = item.get_item() + game = item.get_item().game parent = self.get_root() dialog = BackupSingleDialog(parent,game) @@ -486,7 +611,7 @@ class GameView(Gtk.Box): if response == Gtk.ResponseType.APPLY: self.refresh() - game = item.get_item() + game = item.get_item().game dialog = GameDialog(self.get_root(),game) dialog.set_modal(False) @@ -508,7 +633,7 @@ class GameView(Gtk.Box): dialog.hide() dialog.destroy() - game = item.get_item() + game = item.get_item().game dialog = Gtk.MessageDialog(buttons=Gtk.ButtonsType.YES_NO, text="Do you really want to remove the game {game}?".format( game=game.name), @@ -798,7 +923,7 @@ class BackupView(Gtk.Box): def _on_gameview_columnview_activate(self,columnview,position): model = columnview.get_model().get_model() - game = model.get_item(position) + game = model.get_item(position).game self._title_label.set_markup("{}".format(GLib.markup_escape_text(game.name))) @@ -876,7 +1001,7 @@ class AppWindow(Gtk.ApplicationWindow): n_active = 0 n_finished = 0 for i in range(n_games): - game = self.gameview._liststore.get_item(i) + game = self.gameview._liststore.get_item(i).game if game.is_live: n_live += 1 else: @@ -944,7 +1069,7 @@ class AppWindow(Gtk.ApplicationWindow): n_live = 0 n_finished = 0 for i in range(n_games): - game = gameview._liststore.get_item(i) + game = gameview._liststore.get_item(i).game if game.is_live: n_live += 1 else: diff --git a/sgbackup/gui/_settingsdialog.py b/sgbackup/gui/_settingsdialog.py index 41fb9e9..b62f8f6 100644 --- a/sgbackup/gui/_settingsdialog.py +++ b/sgbackup/gui/_settingsdialog.py @@ -138,7 +138,9 @@ class SettingsDialog(Gtk.Dialog): def __add_general_settings_page(self): page = Gtk.ScrolledWindow() - vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,4) + vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,8) + backup_frame = Gtk.Frame.new('Backup Settings') + grid = Gtk.Grid() label = Gtk.Label.new('Backup directory:') @@ -187,11 +189,30 @@ class SettingsDialog(Gtk.Dialog): break grid.attach(label,0,3,1,1) grid.attach(page.archiver_dropdown,1,3,2,1) + backup_frame.set_child(grid) + vbox.append(backup_frame) + search_frame = Gtk.Frame.new('Search Settings') + search_grid = Gtk.Grid() + + label = Gtk.Label.new("Minimum Characters:") + page.search_minchars_spinbutton = Gtk.SpinButton.new_with_range(1,32,1) + page.search_minchars_spinbutton.set_value(settings.search_min_chars) + page.search_minchars_spinbutton.set_hexpand(True) + search_grid.attach(label,0,0,1,1) + search_grid.attach(page.search_minchars_spinbutton,1,0,1,1) + + label = Gtk.Label.new("Maximum Results:") + page.search_maxresults_spinbutton = Gtk.SpinButton.new_with_range(1,100,1) + page.search_maxresults_spinbutton.set_value(settings.search_max_results) + page.search_maxresults_spinbutton.set_hexpand(True) + search_grid.attach(label,0,1,1,1) + search_grid.attach(page.search_maxresults_spinbutton,1,1,1,1) + + search_frame.set_child(search_grid) + vbox.append(search_frame) - vbox.append(grid) page.set_child(vbox) - self.add_page(page,"general","Generic settings") return page @@ -398,9 +419,12 @@ class SettingsDialog(Gtk.Dialog): settings.backup_versions = self.general_page.backup_versions_spinbutton.get_value_as_int() settings.backup_threads = self.general_page.backup_threads_spinbutton.get_value_as_int() settings.archiver = self.general_page.archiver_dropdown.get_selected_item().key + settings.search_min_chars = self.general_page.search_minchars_spinbutton.get_value_as_int() + settings.search_max_results = self.general_page.search_maxresults_spinbutton.get_value_as_int() settings.zipfile_compression = self.archiver_page.zf_compressor_dropdown.get_selected_item().compressor settings.zipfile_compresslevel = self.archiver_page.zf_compresslevel_spinbutton.get_value_as_int() + variables = {} variable_model = self.__variables_page.variable_columnview.get_model() while hasattr(variable_model,'get_model'): diff --git a/sgbackup/settings.py b/sgbackup/settings.py index 91cd524..fe75794 100644 --- a/sgbackup/settings.py +++ b/sgbackup/settings.py @@ -340,6 +340,21 @@ class Settings(GObject.GObject): max_threads = 1 self.set_integer('sgbackup','maxBackupThreads',max_threads) + @GObject.Property(type=int) + def search_max_results(self)->int: + return self.get_integer('search','maxResults',10) + + @search_max_results.setter + def search_max_results(self,max:int): + self.set_integer('search','maxResults',max) + + @GObject.Property(type=int) + def search_min_chars(self)->int: + return self.get_integer('search','minChars',3) + + @search_min_chars.setter + def search_min_chars(self,min_chars:int): + self.set_integer('search','minChars',min_chars) @GObject.Property def variables(self)->dict[str:str]: