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]: