home · contact · privacy
Browser: On sorter selection, show available values, and which filtered.
authorChristian Heller <c.heller@plomlompom.de>
Tue, 24 Sep 2024 15:48:58 +0000 (17:48 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Tue, 24 Sep 2024 15:48:58 +0000 (17:48 +0200)
browser.py

index 8c5fa26e643bdb7677a781122c664e595ad85a75..38a98890cbf258c635de232a12c297736ad18860 100755 (executable)
@@ -15,7 +15,7 @@ gi.require_version('Gtk', '4.0')
 gi.require_version('Gdk', '4.0')
 gi.require_version('Gio', '2.0')
 # pylint: disable=wrong-import-position
-from gi.repository import Gdk, Gtk, Gio  # type: ignore  # noqa: E402
+from gi.repository import Gdk, Gtk, Gio, Pango  # type: ignore  # noqa: E402
 from gi.repository import GObject, GLib  # type: ignore  # noqa: E402
 # pylint: disable=no-name-in-module
 from stable.gen_params import (GenParams,  GEN_PARAMS_FLOAT,  # noqa: E402
@@ -55,16 +55,33 @@ button.slot {
 
 class Sorter(GObject.GObject):
     """Sort order box representation of sorting attribute."""
-    list_item: Gtk.Box
+    widget: Gtk.Box
 
     def __init__(self, name):
         super().__init__()
         self.name = name
 
-    def set_label(self, diversities):
-        """Set .list_item's label to .name and n of different values for it."""
-        label = f'{self.name} ({diversities[0]}/{diversities[1]}) '
-        self.list_item.get_first_child().set_text(label)
+    def setup_on_bind(self, widget, on_filter_enter, filter_text,
+                      vals_filtered, vals_full):
+        """Set up all the sting only available on factory item bind."""
+        self.widget = widget
+        title = f'{self.name} ({len(vals_filtered)}/{len(vals_full)}) '
+        widget.get_first_child().get_first_child().set_text(title)
+        vals_listed = [f'<b>{val}</b>' if val in vals_filtered
+                       else f'<s>{val}</s>'
+                       for val in sorted(vals_full)]
+        widget.get_last_child().set_text(', '.join(vals_listed))
+        widget.get_last_child().set_use_markup(True)
+        filter_entry = widget.get_first_child().get_last_child()
+        filter_buffer = filter_entry.get_buffer()
+        filter_buffer.set_text(filter_text, -1)
+        filter_entry.connect('activate', on_filter_enter)
+        filter_buffer.connect(
+                'inserted_text',
+                lambda a, b, c, d: filter_entry.add_css_class('temp'))
+        filter_buffer.connect(
+                'deleted_text',
+                lambda a, b, c: filter_entry.add_css_class('temp'))
 
 
 class GallerySlot(Gtk.Button):
@@ -198,6 +215,8 @@ class Gallery:
         self.dir_entries = []
         self.dir_entries_filtered_sorted = []
         self.selected_idx = 0
+        self.items_attrs = {}
+        self.items_attrs_filtered = {}
 
         self._fixed_frame = Gtk.Fixed(hexpand=True, vexpand=True)
         self.scroller = Gtk.ScrolledWindow(propagate_natural_height=True)
@@ -244,8 +263,100 @@ class Gallery:
             self.slots[self.selected_idx].grab_focus()
         self._on_selection_change()
 
-    def build_and_show(self, suggested_selection=None):
-        """Build gallery as sorted GallerySlots, select one, draw gallery."""
+    def _passes_filter(self, attr_name, val):
+        number_attributes = (set(s.lower() for s in GEN_PARAMS_INT) |
+                             set(s.lower() for s in GEN_PARAMS_FLOAT) |
+                             {'bookmarked'})
+
+        def _passes_number_filter(attr_name, pattern, val):
+            use_float = attr_name in {s.lower() for s in GEN_PARAMS_FLOAT}
+            constraint_strings = pattern.split(',')
+            numbers_or = set()
+            unequal = set()
+            less_than = None
+            less_or_equal = None
+            more_or_equal = None
+            more_than = None
+            for constraint_string in constraint_strings:
+                toks = constraint_string.split()
+                if len(toks) == 1:
+                    tok = toks[0]
+                    if tok[0] in '<>!':
+                        if '=' == tok[1]:
+                            toks = [tok[:2], tok[2:]]
+                        else:
+                            toks = [tok[:1], tok[1:]]
+                    else:
+                        value = float(tok) if use_float else int(tok)
+                        numbers_or.add(value)
+                if len(toks) == 2:
+                    value = float(toks[1]) if use_float else int(toks[1])
+                    if toks[0] == '!=':
+                        unequal.add(value)
+                    elif toks[0] == '<':
+                        if less_than is None or less_than >= value:
+                            less_than = value
+                    elif toks[0] == '<=':
+                        if less_or_equal is None or less_or_equal > value:
+                            less_or_equal = value
+                    elif toks[0] == '>=':
+                        if more_or_equal is None or more_or_equal < value:
+                            more_or_equal = value
+                    elif toks[0] == '>':
+                        if more_than is None or more_than <= value:
+                            more_than = value
+            if val in numbers_or:
+                return True
+            if len(numbers_or) > 0 and (less_than == less_or_equal ==
+                                        more_or_equal == more_than):
+                return False
+            if val in unequal:
+                return False
+            if (less_than is not None
+                and val >= less_than)\
+                    or (less_or_equal is not None
+                        and val > less_or_equal)\
+                    or (more_or_equal is not None
+                        and val < more_or_equal)\
+                    or (more_than is not None
+                        and val <= more_than):
+                return False
+            return True
+
+        if attr_name not in self._filter_inputs:
+            return True
+        if val is None:
+            return False
+        pattern = self._filter_inputs[attr_name]
+        if attr_name in number_attributes:
+            if not _passes_number_filter(attr_name, pattern, val):
+                return False
+        elif not re_search(pattern, val):
+            return False
+        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}
+        entries_filtered = []
+        for entry in self.dir_entries:
+            if not self.show_dirs and isinstance(entry, DirItem):
+                continue
+            passes_filters = True
+            for attr_name in [s.name for s in self._sort_order]:
+                if isinstance(entry, ImgItem):
+                    val = (getattr(entry, attr_name)
+                           if hasattr(entry, attr_name) else None)
+                    self.items_attrs[attr_name].add(val)
+                    passes_filter = self._passes_filter(attr_name, val)
+                    passes_filters = passes_filters and passes_filter
+                    if passes_filter:
+                        self.items_attrs_filtered[attr_name].add(val)
+            if passes_filters:
+                entries_filtered += [entry]
+        return entries_filtered
+
+    def _build_grid(self, entries_filtered):
 
         def item_clicker(idx):
             def f(_):
@@ -285,19 +396,11 @@ class Gallery:
         self.slots = []
         self._grid = Gtk.Grid()
         self._fixed_frame.put(self._grid, 0, 0)
-        entries_filtered = [entry for entry in self.dir_entries
-                            if self._filter_func(entry)]
         if self.per_row_by_first_sorter:
             self.show_dirs = False
             sort_attrs = []
             for sorter in reversed(self._sort_order):
-                values = set()
-                for item in [x for x in entries_filtered
-                             if isinstance(x, ImgItem)]:
-                    val = None
-                    if hasattr(item, sorter.name):
-                        val = getattr(item, sorter.name)
-                        values.add(val)
+                values = self.items_attrs_filtered[sorter.name]
                 sort_attrs += [(sorter.name, sorted(list(values)))]
             i_row_ref = [0]
             i_slot_ref = [0]
@@ -316,6 +419,11 @@ class Gallery:
                 self.slots += [slot]
                 i_col += 1
         self._on_grid_built()
+
+    def build_and_show(self, suggested_selection=None):
+        """Build gallery as sorted GallerySlots, select one, draw gallery."""
+        entries_filtered = self._build_items_attrs_and_filtered_entries()
+        self._build_grid(entries_filtered)
         self.selected_idx = 0
         self._update_view()
         new_idx = 0
@@ -387,21 +495,6 @@ class Gallery:
         self._should_update_view = True
         vp_scroll.emit('value-changed')
 
-    def get_diversities_for(self, sort_attr):
-        """Return how many diff. values for sort_attr in (un-)filtered store"""
-        diversities = [0, 0]
-        for i, store in enumerate([self.dir_entries_filtered_sorted,
-                                   self.dir_entries]):
-            values = set()
-            for item in store:
-                if isinstance(item, ImgItem):
-                    val = None
-                    if hasattr(item, sort_attr):
-                        val = getattr(item, sort_attr)
-                    values.add(val)
-            diversities[i] = len(values)
-        return diversities
-
     def _sort_cmp(self, a, b):
         """Sort [a, b] by user-set sort order, and putting directories first"""
         # ensure ".." and all DirItems at start of order
@@ -436,79 +529,6 @@ class Gallery:
                 ret = -1
         return ret
 
-    def _filter_func(self, item):
-        """Return if item matches user-set filters."""
-        number_attributes = set(GEN_PARAMS_INT) | set(GEN_PARAMS_FLOAT) | {
-                'BOOKMARKED'}
-
-        def number_filter(attr_name, filter_line, to_compare):
-            use_float = attr_name.upper() in GEN_PARAMS_FLOAT
-            constraint_strings = filter_line.split(',')
-            numbers_or = set()
-            unequal = set()
-            less_than = None
-            less_or_equal = None
-            more_or_equal = None
-            more_than = None
-            for constraint_string in constraint_strings:
-                toks = constraint_string.split()
-                if len(toks) == 1:
-                    tok = toks[0]
-                    if tok[0] in '<>!':
-                        if '=' == tok[1]:
-                            toks = [tok[:2], tok[2:]]
-                        else:
-                            toks = [tok[:1], tok[1:]]
-                    else:
-                        value = float(tok) if use_float else int(tok)
-                        numbers_or.add(value)
-                if len(toks) == 2:
-                    value = float(toks[1]) if use_float else int(toks[1])
-                    if toks[0] == '!=':
-                        unequal.add(value)
-                    elif toks[0] == '<':
-                        if less_than is None or less_than >= value:
-                            less_than = value
-                    elif toks[0] == '<=':
-                        if less_or_equal is None or less_or_equal > value:
-                            less_or_equal = value
-                    elif toks[0] == '>=':
-                        if more_or_equal is None or more_or_equal < value:
-                            more_or_equal = value
-                    elif toks[0] == '>':
-                        if more_than is None or more_than <= value:
-                            more_than = value
-            if to_compare in numbers_or:
-                return True
-            if len(numbers_or) > 0 and (less_than == less_or_equal ==
-                                        more_or_equal == more_than):
-                return False
-            if to_compare in unequal:
-                return False
-            if (less_than is not None
-                and to_compare >= less_than)\
-                    or (less_or_equal is not None
-                        and to_compare > less_or_equal)\
-                    or (more_or_equal is not None
-                        and to_compare < more_or_equal)\
-                    or (more_than is not None
-                        and to_compare <= more_than):
-                return False
-            return True
-
-        if not self.show_dirs and isinstance(item, DirItem):
-            return False
-        for filter_attribute, value in self._filter_inputs.items():
-            if not hasattr(item, filter_attribute):
-                return False
-            to_compare = getattr(item, filter_attribute)
-            if filter_attribute.upper() in number_attributes:
-                if not number_filter(filter_attribute, value, to_compare):
-                    return False
-            elif not re_search(value, to_compare):
-                return False
-        return True
-
 
 class MainWindow(Gtk.Window):
     """Image browser app top-level window."""
@@ -521,6 +541,7 @@ class MainWindow(Gtk.Window):
     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)
@@ -562,20 +583,19 @@ class MainWindow(Gtk.Window):
             return metadata_box
 
         def init_sorter_and_filterer():
-            self.sort_store = Gio.ListStore(item_type=Sorter)
-            self.sort_selection = Gtk.SingleSelection.new(self.sort_store)
-            self.sort_selection.connect(
-                    'selection-changed',
-                    lambda a, b, c: self.sort_selection.props.selected_item.
-                    list_item.get_parent().grab_focus())
-            factory = Gtk.SignalListItemFactory()
 
             def setup_sort_order_item(_, list_item):
-                box = Gtk.Box(orientation=OR_H)
-                box.append(Gtk.Label(hexpand=True))
-                box.append(Gtk.Entry.new())
-                box.get_last_child().props.placeholder_text = 'filter?'
-                list_item.set_child(box)
+                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):
 
@@ -589,21 +609,25 @@ class MainWindow(Gtk.Window):
                     self.gallery.build_and_show()
 
                 sorter = list_item.props.item
-                sorter.list_item = list_item.props.child
-                sorter.set_label(self.gallery.get_diversities_for(sorter.name))
-                sorter.filterer = sorter.list_item.get_last_child()
-                filter_entry = sorter.list_item.get_last_child()
                 filter_text = self.filter_inputs.get(sorter.name, '')
-                filter_buffer = filter_entry.get_buffer()
-                filter_buffer.set_text(filter_text, -1)
-                filter_entry.connect('activate', on_filter_enter)
-                filter_buffer.connect(
-                        'inserted_text',
-                        lambda a, b, c, d: filter_entry.add_css_class('temp'))
-                filter_buffer.connect(
-                        'deleted_text',
-                        lambda a, b, c: filter_entry.add_css_class('temp'))
-
+                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()
+                list_item.get_last_child().show()
+                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)
             selector = Gtk.ListView(model=self.sort_selection, factory=factory)