From: Christian Heller Date: Tue, 24 Sep 2024 15:48:58 +0000 (+0200) Subject: Browser: On sorter selection, show available values, and which filtered. X-Git-Url: https://plomlompom.com/repos/%7B%7Bdb.prefix%7D%7D/%7B%7B%20web_path%20%7D%7D/pick_tasks?a=commitdiff_plain;h=a6dabe7c21f2d880c311826f274fedd6de38ef90;p=stable_plom Browser: On sorter selection, show available values, and which filtered. --- diff --git a/browser.py b/browser.py index 8c5fa26..38a9889 100755 --- a/browser.py +++ b/browser.py @@ -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'{val}' if val in vals_filtered + else f'{val}' + 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)