GEN_PARAMS_INT, GEN_PARAMS_STR, # noqa: E402
GEN_PARAMS) # noqa: E402
+BasicItemsAttrs = dict[str, set[str]]
AttrVals: TypeAlias = list[str]
AttrValsByVisibility: TypeAlias = dict[str, AttrVals]
ItemsAttrs: TypeAlias = dict[str, AttrValsByVisibility]
self._img_dir_path = ''
self._shall_load = False
- self._shall_build = False
+ self._shall_build_grid = False
self._shall_redraw = False
self._shall_scroll_to_focus = False
self._shall_select = False
self._slots_geometry = GallerySlotsGeometry()
self.dir_entries: list[GalleryItem] = []
+ self._basic_items_attrs: BasicItemsAttrs = {}
self.items_attrs: ItemsAttrs = {}
self.selected_idx = 0
self.slots: list[GallerySlot] = []
if not self._img_dir_path:
return True
if self._shall_load:
+ self._shall_load = False
self._load_directory()
- if self._shall_build:
- self._build()
+ if self._shall_build_grid:
+ self._shall_build_grid = False
+ self._build_grid()
if self._shall_select:
- self._set_selection(self.selected_idx)
+ self._shall_select = False
+ self._assert_selection()
if self._shall_redraw:
wait_time_passed = datetime.now() - self._start_redraw_wait
if wait_time_passed > redraw_wait_time:
+ self._shall_redraw = False
self._redraw_and_check_focus()
return True
def handle_scroll(_) -> None:
self._start_redraw_wait = datetime.now()
self._shall_scroll_to_focus = False
- self._shall_redraw = True
+ self.request_update() # only request redraw
redraw_wait_time = timedelta(milliseconds=GALLERY_REDRAW_WAIT_MS)
self._start_redraw_wait = datetime.now() - redraw_wait_time
if val is not None and getattr(self, attr_name) != val:
setattr(self, attr_name, val)
if attr_name in {'_recurse_dirs', '_img_dir_path'}:
- self._load_directory()
+ self.request_update(load=True)
else:
- self.request_update(build=True)
+ self.request_update(build_grid=True)
+
+ def _make_basic_items_attrs(self,
+ entries: list[GalleryItem]
+ ) -> BasicItemsAttrs:
+ basic_items_attrs = {}
+ for attr_name in (s.name for s in self._sort_order):
+ vals: set[str] = set()
+ for entry in [e for e in entries if isinstance(e, ImgItem)]:
+ val = (getattr(entry, attr_name)
+ if hasattr(entry, attr_name) else None)
+ if val is not None:
+ vals.add(val)
+ basic_items_attrs[attr_name] = vals
+ return basic_items_attrs
def _load_directory(self) -> None:
- """Rewrite .dir_entries from ._img_dir_path, trigger rebuild."""
- self._shall_load = False
+ """(Re-)build .dir_entries from ._img_dir_path, ._basic_items_attrs."""
self.dir_entries.clear()
bookmarks = self._bookmarks_db.as_copy()
cache = self._cache_db.as_ref()
read_directory(path)
read_directory(self._img_dir_path, make_parent=True)
+ self._basic_items_attrs = self._make_basic_items_attrs(
+ self.dir_entries)
self._cache_db.write()
- self.request_update(build=True)
@property
def selected_item(self) -> Optional[GalleryItem]:
self._set_selection(self.slots.index(slot))
self.request_update(scroll_to_focus=True)
+ def _assert_selection(self) -> None:
+ if self.slots:
+ self.slots[self.selected_idx].mark('selected', True)
+ self.slots[self.selected_idx].grab_focus()
+
def _set_selection(self, new_idx: int) -> None:
"""Set self.selected_idx, mark slot as 'selected', unmark old one."""
- self._shall_select = False
- # in ._build(), directly before we are called, no slot will be
+ # in ._build_grid(), directly before we are called, no slot will be
# CSS-marked 'selected', so .mark('selected', False) would tolerably
- # happen without effect; where called from ._build() however, an old
- # .selected_idx might point beyond _any_ of the new .slots, the
+ # happen without effect; where called from ._build_grid() however, an
+ # old .selected_idx might point beyond _any_ of the new .slots, the
# IndexError of which we still want to avoid
if self.selected_idx < len(self.slots):
self.slots[self.selected_idx].mark('selected', False)
self.selected_idx = new_idx
- if self.slots:
- self.slots[self.selected_idx].mark('selected', True)
- self.slots[self.selected_idx].grab_focus()
+ self._assert_selection()
self._on_selection_change()
- def _passes_filter(self, attr_name: str, val: str) -> bool:
- 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:]]
+ def _build_grid(self) -> None:
+ """(Re-)build slot grid from .dir_entries, filters, layout settings."""
+ old_selected_item: Optional[GalleryItem] = self.selected_item
+
+ def passes_filter(attr_name: str, val: str) -> bool:
+ 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:
- 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:
+ 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 len(numbers_or) > 0 and (less_than == less_or_equal ==
- more_or_equal == more_than):
- return False
- if val in unequal:
+
+ if attr_name not in self._filter_inputs:
+ return True
+ if val is None:
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):
+ 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
- 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(self) -> None:
- """(Re-)build slot grid from .dir_entries, filters, layout settings."""
- self._shall_build = False
- old_selected_item: Optional[GalleryItem] = self.selected_item
-
- def build_items_attrs() -> None:
+ def update_items_attrs() -> None:
self.items_attrs.clear()
- def collect_and_split_attr_vals(
- entries: list[GalleryItem]
- ) -> ItemsAttrs:
- items_attrs_tmp: ItemsAttrs = {}
- for attr_name in (s.name for s in self._sort_order):
- items_attrs_tmp[attr_name] = {'incl': [], 'excl': []}
- vals: set[str] = set()
- for entry in [e for e in entries
- if isinstance(e, ImgItem)]:
- val = (getattr(entry, attr_name)
- if hasattr(entry, attr_name) else None)
- if val is not None:
- vals.add(val)
+ def separate_items_attrs(basic_items_attrs) -> ItemsAttrs:
+ items_attrs: ItemsAttrs = {}
+ for attr_name, vals in basic_items_attrs.items():
+ items_attrs[attr_name] = {'incl': [], 'excl': []}
for v in vals:
- k = ('incl' if self._passes_filter(attr_name, v)
- else 'excl')
- items_attrs_tmp[attr_name][k] += [v]
- return items_attrs_tmp
-
- items_attrs_tmp_1 = collect_and_split_attr_vals(self.dir_entries)
- filtered_entries = filter_entries(items_attrs_tmp_1)
- items_attrs_tmp_2 = collect_and_split_attr_vals(filtered_entries)
+ k = ('incl' if passes_filter(attr_name, v) else 'excl')
+ items_attrs[attr_name][k] += [v]
+ return items_attrs
+
+ items_attrs_tmp_1 = separate_items_attrs(self._basic_items_attrs)
+ filtered = filter_entries(items_attrs_tmp_1)
+ reduced_basic_items_attrs = self._make_basic_items_attrs(filtered)
+ items_attrs_tmp_2 = separate_items_attrs(reduced_basic_items_attrs)
for attr_name in (s.name for s in self._sort_order):
final_values: AttrValsByVisibility = {'incl': [], 'semi': []}
final_values['excl'] = items_attrs_tmp_1[attr_name]['excl']
entries_filtered += [entry]
return entries_filtered
- def build_grid(entries_filtered: list[GalleryItem]) -> None:
+ def build(entries_filtered: list[GalleryItem]) -> None:
i_row_ref, i_slot_ref = [0], [0]
if self._grid.get_parent():
self._fixed_frame.remove(self._grid)
attr_name, attr_values = remaining[0]
if 1 == len(remaining):
for i, attr in enumerate(ancestors):
- vlabel = VerticalLabel(f'<b>{attr[0]}</b>: {attr[1]}',
- self._slots_geometry)
+ parent_attr_name, parent_attr_value = attr
+ txt = f'<b>{parent_attr_name}</b>: {parent_attr_value}'
+ vlabel = VerticalLabel(txt, self._slots_geometry)
self._grid.attach(vlabel, i, i_row_ref[0], 1, 1)
row: list[Optional[GalleryItem]]
row = [None] * len(attr_values)
i_col += 1
self.update_config_box()
- build_items_attrs()
+ update_items_attrs()
entries_filtered = filter_entries(self.items_attrs)
- build_grid(entries_filtered)
+ build(entries_filtered)
new_idx = 0
if old_selected_item is not None:
for i, slot in enumerate(self.slots):
def request_update(self,
select: bool = False,
scroll_to_focus: bool = False,
- build: bool = False,
+ build_grid: bool = False,
load: bool = False
) -> None:
"""Set ._shall_… to trigger updates on next relevant interval."""
self._shall_redraw = True
- if scroll_to_focus or build or select:
- self._shall_select = True
- if scroll_to_focus or build:
- self._shall_scroll_to_focus = True
- if build:
- self._shall_build = True
- if load:
- self._shall_load = True
+ self._shall_select |= select or scroll_to_focus or build_grid or load
+ self._shall_scroll_to_focus |= scroll_to_focus or build_grid or load
+ self._shall_build_grid |= build_grid or load
+ self._shall_load |= load
def move_selection(self,
x_inc: Optional[int],