home · contact · privacy
Refactor browser sorter box code.
authorChristian Heller <c.heller@plomlompom.de>
Thu, 26 Sep 2024 16:10:59 +0000 (18:10 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Thu, 26 Sep 2024 16:10:59 +0000 (18:10 +0200)
browser.py

index ecbe83141e29a82a8dd8a792b57113aef53e6001..0af99a3dd796f743e1bf0ec433c576793922bf61 100755 (executable)
@@ -83,6 +83,150 @@ class Sorter(GObject.GObject):
                 lambda a, b, c: filter_entry.add_css_class('temp'))
 
 
+class Sorting():
+    """Representation of sort and filtering settings."""
+    _gallery_update = None
+    _gallery_items_attrs_full = None
+    _gallery_items_attrs_filtered = None
+
+    def __init__(self, sort_order):
+
+        def setup_sort_order_item(_, list_item):
+            vbox = Gtk.Box(orientation=OR_V)
+            hbox = Gtk.Box(orientation=OR_H)
+            hbox.append(Gtk.Label(hexpand=True))
+            hbox.append(Gtk.Entry.new())
+            hbox.get_last_child().props.placeholder_text = 'filter?'
+            vbox.append(hbox)
+            vals_listing = Gtk.Label(wrap=True, max_width_chars=35,
+                                     wrap_mode=Pango.WrapMode.WORD_CHAR)
+            vals_listing.hide()
+            vbox.append(vals_listing)
+            list_item.set_child(vbox)
+
+        def bind_sort_order_item(_, list_item):
+
+            def on_filter_enter(entry):
+                entry.remove_css_class('temp')
+                text = entry.get_buffer().get_text()
+                if '' != text.rstrip():
+                    self.filter_inputs[sorter.name] = text
+                elif sorter.name in self.filter_inputs:
+                    del self.filter_inputs[sorter.name]
+                self._gallery_update()
+
+            sorter = list_item.props.item
+            filter_text = self.filter_inputs.get(sorter.name, '')
+            vals_filtered = self._gallery_items_attrs_filtered[sorter.name]
+            vals_full = self._gallery_items_attrs_full[sorter.name]
+            sorter.setup_on_bind(list_item.props.child, on_filter_enter,
+                                 filter_text, vals_filtered, vals_full)
+
+        def select_sort_order(_a, _b, _c):
+            if self._last_selected:
+                self._last_selected.get_last_child().hide()
+            list_item = self.selection.props.selected_item.widget
+            list_item.get_parent().grab_focus()
+            self._last_selected = list_item
+
+        self.order = sort_order
+        self.filter_inputs = {}
+        self._last_selected = None
+        self.store = Gio.ListStore(item_type=Sorter)
+        self.selection = Gtk.SingleSelection.new(self.store)
+        self.selection.connect('selection-changed', select_sort_order)
+        factory = Gtk.SignalListItemFactory()
+        factory.connect('setup', setup_sort_order_item)
+        factory.connect('bind', bind_sort_order_item)
+        self.view = Gtk.ListView(model=self.selection, factory=factory)
+        self.box = Gtk.Box(orientation=OR_V)
+        self.box.append(Gtk.Label(label='** sort order **'))
+        self.box.append(self.view)
+        self.btn_activate = Gtk.Button(label='activate')
+        self.btn_activate.props.sensitive = False
+        self.btn_activate.connect(
+                'clicked', lambda _: self.activate_order())
+        self.box.append(self.btn_activate)
+
+    @classmethod
+    def from_suggestion(cls, suggestion_fused):
+        """Parse suggestion_fused for/into initial sort order to build on."""
+        suggestion = suggestion_fused.split(',')
+        names = [p.lower() for p in GEN_PARAMS] + ['bookmarked']
+        sort_order = []
+        for name in names:
+            sort_order += [Sorter(name)]
+        new_sort_order = []
+        do_reverse = '-' in suggestion
+        for pattern in suggestion:
+            for sorter in [sorter for sorter in sort_order
+                           if sorter.name.startswith(pattern)]:
+                sort_order.remove(sorter)
+                new_sort_order += [sorter]
+        sort_order = new_sort_order + sort_order
+        if do_reverse:
+            sort_order.reverse()
+        return cls(sort_order)
+
+    def bind_gallery(self, on_update, items_attrs_full, items_attrs_filtered):
+        """Connect to Gallery interfaces where necessary."""
+        self._gallery_update = on_update
+        self._gallery_items_attrs_full = items_attrs_full
+        self._gallery_items_attrs_filtered = items_attrs_filtered
+
+    def move_selection(self, direction):
+        """Move sort order selection by direction (-1 or +1)."""
+        min_idx, max_idx = 0, len(self.order) - 1
+        cur_idx = self.selection.props.selected
+        if (1 == direction and cur_idx < max_idx)\
+                or (-1 == direction and cur_idx > min_idx):
+            self.selection.props.selected = cur_idx + direction
+
+    def move_sorter(self, direction):
+        """Move selected item in sort order view, ensure temporary state."""
+        tmp_sort_order = []
+        for i in range(self.store.get_n_items()):
+            tmp_sort_order += [self.store.get_item(i)]
+        cur_idx = self.selection.props.selected
+        selected = tmp_sort_order[cur_idx]
+        if direction == -1 and cur_idx > 0:
+            prev_i = cur_idx - 1
+            old_prev = tmp_sort_order[prev_i]
+            tmp_sort_order[prev_i] = selected
+            tmp_sort_order[cur_idx] = old_prev
+        elif direction == 1 and cur_idx < (len(tmp_sort_order) - 1):
+            next_i = cur_idx + 1
+            old_next = tmp_sort_order[next_i]
+            tmp_sort_order[next_i] = selected
+            tmp_sort_order[cur_idx] = old_next
+        else:  # to catch movement beyond limits
+            return
+        self.update_box(tmp_sort_order, cur_idx + direction)
+        self.selection.props.selected = cur_idx + direction
+        for i in range(self.store.get_n_items()):
+            sort_item = self.store.get_item(i)
+            sort_item.widget.add_css_class('temp')
+        self.btn_activate.props.sensitive = True
+
+    def update_box(self, alt_order=None, cur_selection=0):
+        """Rebuild .store from .order, or alt_order if provided."""
+        sort_order = alt_order if alt_order else self.order
+        self.store.remove_all()
+        for sorter in sort_order:
+            self.store.append(sorter)
+        self.selection.props.selected = cur_selection
+
+    def activate_order(self):
+        """Write sort order box order into .order, mark finalized."""
+        self.order.clear()
+        for i in range(self.store.get_n_items()):
+            sorter = self.store.get_item(i)
+            sorter.widget.remove_css_class('temp')
+            self.order += [sorter]
+        self.btn_activate.props.sensitive = False
+        self._gallery_update()
+
+
 class GallerySlot(Gtk.Button):
     """Slot in Gallery representing a GalleryItem."""
 
@@ -340,8 +484,9 @@ class Gallery:
         return True
 
     def _build_items_attrs_and_filtered_entries(self):
-        self.items_attrs = {s.name: set() for s in self._sort_order}
-        self.items_attrs_filtered = {s.name: set() for s in self._sort_order}
+        for d in (self.items_attrs, self.items_attrs_filtered):
+            d.clear()
+            d |= {s.name: set() for s in self._sort_order}
         entries_filtered = []
         for entry in self.dir_entries:
             if not self.show_dirs and isinstance(entry, DirItem):
@@ -427,8 +572,9 @@ class Gallery:
                 i_col += 1
         self._on_grid_built()
 
-    def build_and_show(self, suggested_selection=None):
+    def build_and_show(self, preserve_selected=True):
         """Build gallery as sorted GallerySlots, select one, draw gallery."""
+        suggested_selection = self.selected_item if preserve_selected else None
         entries_filtered = self._build_items_attrs_and_filtered_entries()
         self._build_grid(entries_filtered)
         self.selected_idx = 0
@@ -436,8 +582,7 @@ class Gallery:
         new_idx = 0
         if suggested_selection is not None:
             for i, slot in enumerate(self.slots):
-                item_path = slot.item.full_path
-                if suggested_selection.full_path == item_path:
+                if suggested_selection == slot.item:
                     new_idx = i
                     break
         self._set_selection(new_idx, unselect_old=False)
@@ -551,15 +696,11 @@ class Gallery:
 class MainWindow(Gtk.Window):
     """Image browser app top-level window."""
     metadata: Gtk.TextBuffer
-    sort_store: Gtk.ListStore
-    sort_selection: Gtk.SingleSelection
     prev_key: list
     counter: Gtk.Label
-    btn_activate_sort: Gtk.Button
     btn_dec_per_row: Gtk.Button
     btn_inc_per_row: Gtk.Button
     btn_show_dirs: Gtk.Button
-    sort_order_last_selected: Gtk.Box
 
     def __init__(self, app, **kwargs):
         super().__init__(**kwargs)
@@ -600,65 +741,6 @@ class MainWindow(Gtk.Window):
             metadata_box.append(text_view)
             return metadata_box
 
-        def init_sorter_and_filterer():
-
-            def setup_sort_order_item(_, list_item):
-                vbox = Gtk.Box(orientation=OR_V)
-                hbox = Gtk.Box(orientation=OR_H)
-                hbox.append(Gtk.Label(hexpand=True))
-                hbox.append(Gtk.Entry.new())
-                hbox.get_last_child().props.placeholder_text = 'filter?'
-                vbox.append(hbox)
-                vals_listing = Gtk.Label(wrap=True, max_width_chars=35,
-                                         wrap_mode=Pango.WrapMode.WORD_CHAR)
-                vals_listing.hide()
-                vbox.append(vals_listing)
-                list_item.set_child(vbox)
-
-            def bind_sort_order_item(_, list_item):
-
-                def on_filter_enter(entry):
-                    entry.remove_css_class('temp')
-                    text = entry.get_buffer().get_text()
-                    if '' != text.rstrip():
-                        self.filter_inputs[sorter.name] = text
-                    elif sorter.name in self.filter_inputs:
-                        del self.filter_inputs[sorter.name]
-                    self.gallery.build_and_show()
-
-                sorter = list_item.props.item
-                filter_text = self.filter_inputs.get(sorter.name, '')
-                vals_filtered = self.gallery.items_attrs_filtered[sorter.name]
-                vals_full = self.gallery.items_attrs[sorter.name]
-                sorter.setup_on_bind(list_item.props.child, on_filter_enter,
-                                     filter_text, vals_filtered, vals_full)
-
-            def select_sort_order(_a, _b, _c):
-                if self.sort_order_last_selected:
-                    self.sort_order_last_selected.get_last_child().hide()
-                list_item = self.sort_selection.props.selected_item.widget
-                list_item.get_parent().grab_focus()
-                self.sort_order_last_selected = list_item
-
-            self.sort_order_last_selected = None
-            self.sort_store = Gio.ListStore(item_type=Sorter)
-            self.sort_selection = Gtk.SingleSelection.new(self.sort_store)
-            self.sort_selection.connect('selection-changed', select_sort_order)
-            factory = Gtk.SignalListItemFactory()
-            factory.connect('setup', setup_sort_order_item)
-            factory.connect('bind', bind_sort_order_item)
-            self.sort_list_view = Gtk.ListView(model=self.sort_selection,
-                                               factory=factory)
-            sort_box = Gtk.Box(orientation=OR_V)
-            sort_box.append(Gtk.Label(label='** sort order **'))
-            sort_box.append(self.sort_list_view)
-            self.btn_activate_sort = Gtk.Button(label='activate')
-            self.btn_activate_sort.props.sensitive = False
-            self.btn_activate_sort.connect(
-                    'clicked', lambda _: self.activate_sort_order())
-            sort_box.append(self.btn_activate_sort)
-            return sort_box
-
         def init_key_control():
             key_ctl = Gtk.EventControllerKey(
                     propagation_phase=Gtk.PropagationPhase.CAPTURE)
@@ -682,13 +764,16 @@ class MainWindow(Gtk.Window):
                     self.get_display(), css_provider,
                     Gtk.STYLE_PROVIDER_PRIORITY_USER)
 
-        self.filter_inputs = {}
         self.gallery = Gallery(
-                sort_order=self.app.sort_order,
-                filter_inputs=self.filter_inputs,
+                sort_order=self.app.sort.order,
+                filter_inputs=self.app.sort.filter_inputs,
                 on_hit_item=self.hit_gallery_item,
-                on_grid_built=self.update_sort_order_box,
+                on_grid_built=self.app.sort.update_box,
                 on_selection_change=self.update_metadata_on_gallery_selection)
+        self.app.sort.bind_gallery(
+                on_update=self.gallery.build_and_show,
+                items_attrs_full=self.gallery.items_attrs,
+                items_attrs_filtered=self.gallery.items_attrs_filtered)
         self.recurse_dirs = False
 
         setup_css()
@@ -697,8 +782,7 @@ class MainWindow(Gtk.Window):
         viewer.append(self.navbar)
         viewer.append(self.gallery.frame)
         self.side_box = Gtk.Box(orientation=OR_V)
-        self.sort_box = init_sorter_and_filterer()
-        self.side_box.append(self.sort_box)
+        self.side_box.append(self.app.sort.box)
         self.side_box.append(init_metadata_box())
         box_outer = Gtk.Box(orientation=OR_H)
         box_outer.append(self.side_box)
@@ -715,11 +799,11 @@ class MainWindow(Gtk.Window):
         GLib.idle_add(self.gallery.build_and_show)
 
     def on_focus_change(self):
-        """If new focus on GallerySlot, call gallery.on_focus_slot."""
+        """Handle reactions on focus changes in .gallery and .sort."""
         focused = self.get_focus()
         if isinstance(focused, GallerySlot):
             self.gallery.on_focus_slot(focused)
-        elif focused.get_parent() == self.sort_list_view:
+        elif focused.get_parent() == self.app.sort.view:
             focused.get_first_child().get_last_child().show()
 
     def on_resize(self):
@@ -746,7 +830,7 @@ class MainWindow(Gtk.Window):
             bookmarks += [self.gallery.selected_item.full_path]
         with open(BOOKMARKS_PATH, 'w', encoding='utf8') as f:
             json_dump(list(bookmarks), f)
-        self.update_sort_order_box()
+        self.app.sort.update_box()
 
     def hit_gallery_item(self):
         """If current file selection is directory, reload into that one."""
@@ -755,25 +839,6 @@ class MainWindow(Gtk.Window):
             self.app.img_dir_absolute = selected.full_path
             self.load_directory()
 
-    def update_sort_order_box(self, alt_order=None, cur_selection=0):
-        """Rebuild .sort_store from .sort_order, or alt_order if provided."""
-        sort_order = alt_order if alt_order else self.app.sort_order
-        self.sort_store.remove_all()
-        for sorter in sort_order:
-            self.sort_store.append(sorter)
-        self.sort_selection.props.selected = cur_selection
-
-    def activate_sort_order(self):
-        """Write sort order box order into .app.sort_order, mark finalized."""
-        self.app.sort_order.clear()
-        for i in range(self.sort_store.get_n_items()):
-            sorter = self.sort_store.get_item(i)
-            sorter.widget.remove_css_class('temp')
-            self.app.sort_order += [sorter]
-        self.btn_activate_sort.props.sensitive = False
-        old_selection = self.gallery.selected_item
-        self.gallery.build_and_show(old_selection)
-
     def load_directory(self, update_gallery_view=True):
         """Load .gallery.store_unfiltered from .app.img_dir_absolute path."""
 
@@ -822,7 +887,6 @@ class MainWindow(Gtk.Window):
                     print(f'{prefix}{i+1}/{len(dirs_to_enter)}')
                     read_directory_into_gallery_items(path)
 
-        old_selection = self.gallery.selected_item
         self.gallery.dir_entries = []
         with open(BOOKMARKS_PATH, 'r', encoding='utf8') as f:
             bookmarks = json_load(f)
@@ -832,7 +896,7 @@ class MainWindow(Gtk.Window):
         with open(CACHE_PATH, 'w', encoding='utf8') as f:
             json_dump(cache, f)
         if update_gallery_view:
-            self.gallery.build_and_show(old_selection)
+            self.gallery.build_and_show()
 
     def toggle_side_box(self):
         """Toggle window sidebox visible/invisible."""
@@ -851,7 +915,7 @@ class MainWindow(Gtk.Window):
         if button.props.active:
             self.btn_show_dirs.set_active(False)
         self.btn_show_dirs.set_sensitive(not button.props.active)
-        self.gallery.build_and_show(self.gallery.selected_item)
+        self.gallery.build_and_show()
 
     def reset_show_dirs(self, button):
         """By button's .active, in-/exclude directories from gallery view."""
@@ -862,47 +926,13 @@ class MainWindow(Gtk.Window):
         """Change by increment how many items max to display in gallery row."""
         if self.gallery.per_row + increment > 0:
             self.gallery.per_row += increment
-            self.gallery.build_and_show(self.gallery.selected_item)
+            self.gallery.build_and_show()
 
     def reset_recurse(self, button):
         """By button's .active, de-/activate recursion on image collection."""
         self.recurse_dirs = button.props.active
         self.load_directory()
 
-    def move_sort(self, direction):
-        """Move selected item in sort order view, ensure temporary state."""
-        tmp_sort_order = []
-        for i in range(self.sort_store.get_n_items()):
-            tmp_sort_order += [self.sort_store.get_item(i)]
-        cur_idx = self.sort_selection.props.selected
-        selected = tmp_sort_order[cur_idx]
-        if direction == -1 and cur_idx > 0:
-            prev_i = cur_idx - 1
-            old_prev = tmp_sort_order[prev_i]
-            tmp_sort_order[prev_i] = selected
-            tmp_sort_order[cur_idx] = old_prev
-        elif direction == 1 and cur_idx < (len(tmp_sort_order) - 1):
-            next_i = cur_idx + 1
-            old_next = tmp_sort_order[next_i]
-            tmp_sort_order[next_i] = selected
-            tmp_sort_order[cur_idx] = old_next
-        else:  # to catch movement beyond limits
-            return
-        self.update_sort_order_box(tmp_sort_order, cur_idx + direction)
-        self.sort_selection.props.selected = cur_idx + direction
-        for i in range(self.sort_store.get_n_items()):
-            sort_item = self.sort_store.get_item(i)
-            sort_item.widget.add_css_class('temp')
-        self.btn_activate_sort.props.sensitive = True
-
-    def move_selection_in_sort_order(self, direction):
-        """Move sort order selection by direction (-1 or +1)."""
-        min_idx, max_idx = 0, len(self.app.sort_order) - 1
-        cur_idx = self.sort_selection.props.selected
-        if (1 == direction and cur_idx < max_idx)\
-                or (-1 == direction and cur_idx > min_idx):
-            self.sort_selection.props.selected = cur_idx + direction
-
     def update_metadata_on_gallery_selection(self):
         """Update .metadata about individual file, .counter on its idx/total"""
         self.metadata.set_text('')
@@ -923,8 +953,8 @@ class MainWindow(Gtk.Window):
         if isinstance(self.get_focus().get_parent(), Gtk.Entry):
             return False
         if Gdk.KEY_Return == keyval:
-            if self.get_focus().get_parent().get_parent() == self.sort_box:
-                self.activate_sort_order()
+            if self.get_focus().get_parent().get_parent() == self.app.sort.box:
+                self.app.sort.activate_order()
             else:
                 self.hit_gallery_item()
         elif Gdk.KEY_G == keyval:
@@ -940,13 +970,13 @@ class MainWindow(Gtk.Window):
         elif Gdk.KEY_g == keyval and Gdk.KEY_g == self.prev_key[0]:
             self.gallery.move_selection(None, None, -1)
         elif Gdk.KEY_w == keyval:
-            self.move_selection_in_sort_order(-1)
+            self.app.sort.move_selection(-1)
         elif Gdk.KEY_W == keyval:
-            self.move_sort(-1)
+            self.app.sort.move_sorter(-1)
         elif Gdk.KEY_s == keyval:
-            self.move_selection_in_sort_order(1)
+            self.app.sort.move_selection(1)
         elif Gdk.KEY_S == keyval:
-            self.move_sort(1)
+            self.app.sort.move_sorter(1)
         elif Gdk.KEY_b == keyval:
             self.bookmark()
         else:
@@ -957,43 +987,21 @@ class MainWindow(Gtk.Window):
 
 class Application(Gtk.Application):
     """Image browser application class."""
-    img_dir_absolute: str
-    sort_order: list[Sorter]
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
-
-    def _build_sort_order(self, suggestion_fused):
-        """Parse suggestion_fused for/into sort order suggestion."""
-        suggestion = suggestion_fused.split(',')
-        names = [p.lower() for p in GEN_PARAMS] + ['bookmarked']
-        sort_order = []
-        for name in names:
-            sort_order += [Sorter(name)]
-        new_sort_order = []
-        do_reverse = '-' in suggestion
-        for pattern in suggestion:
-            for sorter in [sorter for sorter in sort_order
-                           if sorter.name.startswith(pattern)]:
-                sort_order.remove(sorter)
-                new_sort_order += [sorter]
-        sort_order = new_sort_order + sort_order
-        if do_reverse:
-            sort_order.reverse()
-        return sort_order
-
-    def do_activate(self, *args, **kwargs):
-        """Parse arguments, start window, keep it open."""
         parser = ArgumentParser()
         parser.add_argument('directory', default=IMG_DIR_DEFAULT, nargs='?')
         parser.add_argument('-s', '--sort-order', default=SORT_DEFAULT)
         opts = parser.parse_args()
         self.img_dir_absolute = abspath(opts.directory)
-        self.sort_order = self._build_sort_order(opts.sort_order)
+        self.sort = Sorting.from_suggestion(opts.sort_order)
+
+    def do_activate(self, *args, **kwargs):
+        """Parse arguments, start window, keep it open."""
         win = MainWindow(self)
         win.present()
         self.hold()
-        return 0
 
 
 main_app = Application(application_id='plomlompom.com.StablePixBrowser.App')