home · contact · privacy
Browser: Refactor SorterAndFilterer order list treatment.
authorChristian Heller <c.heller@plomlompom.de>
Sat, 2 Nov 2024 14:34:47 +0000 (15:34 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Sat, 2 Nov 2024 14:34:47 +0000 (15:34 +0100)
browser.py

index 503321ade99f1c05fbec20259791f53da6114f4d..2f2892b332b6f2e0171927dbd5b20ca26a201890 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
 """Browser for image files."""
 from json import dump as json_dump, load as json_load
-from typing import TypeAlias, Callable, Optional
+from typing import TypeAlias, Callable, Optional, Self
 from functools import cmp_to_key
 from re import search as re_search
 from os import listdir
@@ -136,22 +136,8 @@ class Application(Gtk.Application):
         self.img_dir_absolute = abspath(opts.directory)
         self.bookmarks_db = JsonDb(BOOKMARKS_PATH)
         self.cache_db = JsonDb(CACHE_PATH)
-
-        sort_suggestion = opts.sort_order.split(',')
-        names = [p.lower() for p in GEN_PARAMS] + ['bookmarked']
-        self.sort_order = []
-        for name in names:
-            self.sort_order += [SorterAndFilterer(name)]
-        new_sort_order = []
-        do_reverse = '-' in sort_suggestion
-        for pattern in sort_suggestion:
-            for sorter in [sorter for sorter in self.sort_order
-                           if sorter.name.startswith(pattern)]:
-                self.sort_order.remove(sorter)
-                new_sort_order += [sorter]
-        self.sort_order = new_sort_order + self.sort_order
-        if do_reverse:
-            self.sort_order.reverse()
+        self.sort_order = SorterAndFiltererOrder.from_suggestion(
+                opts.sort_order.split(','))
 
     def do_activate(self, *args, **kwargs) -> None:
         """Parse arguments, start window, keep it open."""
@@ -201,6 +187,78 @@ class SorterAndFilterer(GObject.GObject):
                 lambda a, b, c: self.widget.filter.add_css_class('temp'))
 
 
+class SorterAndFiltererOrder:
+    """Represents sorted list of SorterAndFilterer items."""
+
+    def __init__(self, as_list: list[SorterAndFilterer]) -> None:
+        self._list = as_list
+
+    def __eq__(self, other):
+        return self._list == other._list
+
+    def __len__(self) -> int:
+        return len(self._list)
+
+    def __getitem__(self, idx: int) -> SorterAndFilterer:
+        return self._list[idx]
+
+    def __iter__(self):
+        return self._list.__iter__()
+
+    @staticmethod
+    def _list_from_store(store) -> list[SorterAndFilterer]:
+        order = []
+        for i in range(store.get_n_items()):
+            order += [store.get_item(i)]
+        return order
+
+    @classmethod
+    def from_suggestion(cls, suggestion: list[str]) -> Self:
+        """Create new, interpreting order of strings in suggestion."""
+        names = [p.lower() for p in GEN_PARAMS] + ['bookmarked']
+        order = []
+        for name in names:
+            order += [SorterAndFilterer(name)]
+        new_order = []
+        do_reverse = '-' in suggestion
+        for pattern in suggestion:
+            for sorter in [sorter for sorter in order
+                           if sorter.name.startswith(pattern)]:
+                order.remove(sorter)
+                new_order += [sorter]
+        order = new_order + order
+        if do_reverse:
+            order.reverse()
+        return cls(order)
+
+    @classmethod
+    def from_store(cls, store: Gio.ListStore) -> Self:
+        """Create new, mirroring order in store."""
+        return cls(cls._list_from_store(store))
+
+    def copy(self) -> Self:
+        """Create new, of equal order."""
+        return self.__class__(self._list[:])
+
+    def update_from_store(self, store: Gio.ListStore) -> None:
+        """Update self from store."""
+        self._list = self._list_from_store(store)
+
+    def into_store(self, store: Gio.ListStore) -> None:
+        """Update store to represent self."""
+        store.remove_all()
+        for sorter in self:
+            store.append(sorter)
+
+    def switch_at(self, selected_idx: int, forward: bool) -> None:
+        """Switch elements at selected_idx and its neighbor."""
+        selected = self[selected_idx]
+        other_idx = selected_idx + (1 if forward else -1)
+        other = self[other_idx]
+        self._list[other_idx] = selected
+        self._list[selected_idx] = other
+
+
 class GalleryItem(GObject.GObject):
     """Gallery representation of filesystem entry, base to DirItem, ImgItem."""
     _to_hash = ['name', 'full_path']
@@ -364,7 +422,8 @@ class GalleryConfig():
 
     def __init__(self,
                  box: Gtk.Box,
-                 sort_order: list[SorterAndFilterer],
+                 # sort_order: list[SorterAndFilterer],
+                 sort_order: SorterAndFiltererOrder,
                  request_update: Callable,
                  update_settings: Callable,
                  items_attrs: ItemsAttrs,
@@ -418,19 +477,14 @@ class GalleryConfig():
                 self._btn_show_dirs.set_active(False)
 
         def apply_config() -> None:
-            new_order = []
-            for i in range(self._store.get_n_items()):
-                sorter = self._store.get_item(i)
+            self.order.update_from_store(self._store)
+            for sorter in self.order:
                 sorter.widget.remove_css_class('temp')
-                new_order += [sorter]
-            if self.order != new_order:
-                self.order.clear()
-                self.order += new_order[:]
             self._gallery_update_settings(
                     per_row=self._btn_per_row.get_value_as_int(),
                     by_1st=self._btn_by_1st.get_active(),
                     show_dirs=self._btn_show_dirs.get_active(),
-                    sort_order=self.order[:],
+                    sort_order=self.order.copy(),
                     filter_inputs=self.filter_inputs.copy(),
                     recurse_dirs=self._btn_recurse.get_active())
             self._gallery_request_update(select=True)
@@ -507,21 +561,12 @@ class GalleryConfig():
 
     def move_sorter(self, direction: int) -> None:
         """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)]
+        tmp_sort_order = SorterAndFiltererOrder.from_store(self._store)
         cur_idx = self._sort_sel.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
+            tmp_sort_order.switch_at(cur_idx, forward=False)
         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
+            tmp_sort_order.switch_at(cur_idx, forward=True)
         else:  # to catch movement beyond limits
             return
         self.update_box(tmp_sort_order, cur_idx + direction)
@@ -531,14 +576,12 @@ class GalleryConfig():
             sort_item.widget.add_css_class('temp')
 
     def update_box(self,
-                   alt_order: Optional[list[SorterAndFilterer]] = None,
+                   alt_order: Optional[SorterAndFiltererOrder] = None,
                    cur_selection: int = 0
                    ) -> None:
         """Rebuild sorter listing in box from .order, or alt_order if set."""
         sort_order = alt_order if alt_order else self.order
-        self._store.remove_all()
-        for sorter in sort_order:
-            self._store.append(sorter)
+        sort_order.into_store(self._store)
         self._sort_sel.props.selected = cur_selection
 
 
@@ -593,7 +636,7 @@ class Gallery:
         self._on_hit_item = on_hit_item
         self._on_selection_change = on_selection_change
         self._bookmarks_db, self._cache_db = bookmarks_db, cache_db
-        self._sort_order: list[SorterAndFilterer] = []
+        self._sort_order = SorterAndFiltererOrder([])
         self._filter_inputs: FilterInputs = {}
         self._img_dir_path = None
 
@@ -662,7 +705,7 @@ class Gallery:
                         show_dirs: Optional[bool] = None,
                         recurse_dirs: Optional[bool] = None,
                         img_dir_path: Optional[str] = None,
-                        sort_order: Optional[list[SorterAndFilterer]] = None,
+                        sort_order: Optional[SorterAndFiltererOrder] = None,
                         filter_inputs: Optional[FilterInputs] = None
                         ) -> None:
         """Set Gallery setup fields, request appropriate updates."""
@@ -1213,7 +1256,7 @@ class MainWindow(Gtk.Window):
         self.gallery.update_config_box = self.conf.update_box
         GLib.idle_add(lambda: self.gallery.update_settings(
             img_dir_path=self.app.img_dir_absolute,
-            sort_order=self.conf.order[:],
+            sort_order=self.conf.order.copy(),
             filter_inputs=self.conf.filter_inputs.copy()))
 
     def on_focus_change(self) -> None: