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
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):
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)
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(_):
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]
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
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
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."""
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)
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):
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)