From: Plom Heller Date: Fri, 17 Apr 2026 23:27:32 +0000 (+0200) Subject: Superficial code-reorganization to easen understanding. X-Git-Url: https://plomlompom.com/repos/%22https:/validator.w3.org/%7B%7Bprefix%7D%7D?a=commitdiff_plain;h=refs%2Fheads%2Fmaster;p=stable_plom Superficial code-reorganization to easen understanding. --- diff --git a/browser/gallery.py b/browser/gallery.py index 76cd0e3..4d9fac1 100644 --- a/browser/gallery.py +++ b/browser/gallery.py @@ -279,25 +279,6 @@ class Gallery: self._viewport = self._fixed_frame.get_parent() self._viewport.set_scroll_to_focus(False) # prefer our own handling - def ensure_uptodate() -> bool: - if not self._img_dir_path: - return True - if self._shall_load: - self._shall_load = False - self._load_directory() - if self._shall_build_grid: - self._shall_build_grid = False - self._build_grid() - if self._shall_select: - 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 @@ -306,7 +287,42 @@ class Gallery: redraw_wait_time = timedelta(milliseconds=GALLERY_REDRAW_WAIT_MS) self._start_redraw_wait = datetime.now() - redraw_wait_time scroller.get_vadjustment().connect('value-changed', handle_scroll) - GLib.timeout_add(GALLERY_UPDATE_INTERVAL_MS, ensure_uptodate) + GLib.timeout_add(GALLERY_UPDATE_INTERVAL_MS, + lambda: self._ensure_uptodate(redraw_wait_time)) + + # view update management + + def _ensure_uptodate(self, redraw_wait_time: timedelta) -> bool: + if not self._img_dir_path: + return True + if self._shall_load: + self._shall_load = False + self._load_directory() + if self._shall_build_grid: + self._shall_build_grid = False + self._build_grid() + if self._shall_select: + 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 request_update(self, + select: bool = False, + scroll_to_focus: bool = False, + build_grid: bool = False, + load: bool = False + ) -> None: + """Set ._shall_… to trigger updates on next relevant interval.""" + self._shall_redraw = 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 update_settings(self, per_row: Optional[int] = None, @@ -328,75 +344,12 @@ class Gallery: else: self.request_update(build_grid=True) - @staticmethod - def _diff_prompts(prompts: list[str]) -> dict[str, tuple[str, str]]: - if not prompts: - return {} - - def find_longest_equal(prompts, j, matcher): - longest_total, temp_longest = '', '' - while j < len(prompts[0]): - if 'end' == matcher: - temp_longest = prompts[0][-j] + temp_longest - else: - temp_longest += prompts[0][j] - if len(temp_longest) > len(longest_total): - found_in_all = True - for prompt in prompts[1:]: - if ('start' == matcher - and not prompt.startswith(temp_longest)) or\ - ('end' == matcher - and not prompt.endswith(temp_longest)) or\ - ('in' == matcher - and temp_longest not in prompt): - found_in_all = False - break - if not found_in_all: - break - longest_total = temp_longest - j += 1 - return longest_total - - prefix = find_longest_equal(prompts, 0, 'start') - suffix = find_longest_equal(prompts, 1, matcher='end') - cores = [p[len(prefix):] for p in prompts] - if suffix: - for i, p in enumerate(cores): - cores[i] = p[:-len(suffix)] - longest_total = '' - for i in range(len(cores[0])): - temp_longest = find_longest_equal(cores, j=i, matcher='in') - if len(temp_longest) > len(longest_total): - longest_total = temp_longest - middle = longest_total - prompts_diff = {} - for i, p in enumerate(prompts): - remains = p[len(prefix):] if prefix else p - idx_middle = remains.index(middle) - first = remains[:idx_middle] if idx_middle else '' - remains = remains[idx_middle + len(middle):] - second = remains[:-len(suffix)] if suffix else remains - if first: - first = f'…{first}' if prefix else first - first = f'{first}…' if suffix or middle else first - if second: - second = f'…{second}' if prefix or middle else second - second = f'{second}…' if suffix else second - prompts_diff[p] = (first if first else '…', - second if second else '…') - return prompts_diff + def on_resize(self, width: int = 0, height: int = 0) -> None: + """Force redraw and scroll-to-focus into new geometry.""" + self._force_width, self._force_height = width, height + self.request_update(scroll_to_focus=True) - def _prep_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 + # load_directory and helpers def _load_directory(self) -> None: """(Re-)build .dir_entries from ._img_dir_path, ._basic_items_attrs.""" @@ -465,175 +418,72 @@ class Gallery: del self._basic_items_attrs[attr_name] self._cache_db.write() - @property - def selected_item(self) -> Optional[GalleryItem]: - """Return slot.item for slot at self.selected_idx.""" - return self.slots[self.selected_idx].item if self.slots else None + @staticmethod + def _diff_prompts(prompts: list[str]) -> dict[str, tuple[str, str]]: + if not prompts: + return {} - def on_focus_slot(self, slot: GallerySlot) -> None: - """If GallerySlot focused, set .selected_idx to it.""" - self._set_selection(self.slots.index(slot)) - self.request_update(scroll_to_focus=True) + def find_longest_equal(prompts, j, matcher): + longest_total, temp_longest = '', '' + while j < len(prompts[0]): + if 'end' == matcher: + temp_longest = prompts[0][-j] + temp_longest + else: + temp_longest += prompts[0][j] + if len(temp_longest) > len(longest_total): + found_in_all = True + for prompt in prompts[1:]: + if ('start' == matcher + and not prompt.startswith(temp_longest)) or\ + ('end' == matcher + and not prompt.endswith(temp_longest)) or\ + ('in' == matcher + and temp_longest not in prompt): + found_in_all = False + break + if not found_in_all: + break + longest_total = temp_longest + j += 1 + return longest_total - def _assert_selection(self) -> None: - if self.slots: - self.slots[self.selected_idx].mark('selected', True) - self.slots[self.selected_idx].grab_focus() + prefix = find_longest_equal(prompts, 0, 'start') + suffix = find_longest_equal(prompts, 1, matcher='end') + cores = [p[len(prefix):] for p in prompts] + if suffix: + for i, p in enumerate(cores): + cores[i] = p[:-len(suffix)] + longest_total = '' + for i in range(len(cores[0])): + temp_longest = find_longest_equal(cores, j=i, matcher='in') + if len(temp_longest) > len(longest_total): + longest_total = temp_longest + middle = longest_total + prompts_diff = {} + for i, p in enumerate(prompts): + remains = p[len(prefix):] if prefix else p + idx_middle = remains.index(middle) + first = remains[:idx_middle] if idx_middle else '' + remains = remains[idx_middle + len(middle):] + second = remains[:-len(suffix)] if suffix else remains + if first: + first = f'…{first}' if prefix else first + first = f'{first}…' if suffix or middle else first + if second: + second = f'…{second}' if prefix or middle else second + second = f'{second}…' if suffix else second + prompts_diff[p] = (first if first else '…', + second if second else '…') + return prompts_diff - def _set_selection(self, new_idx: int) -> None: - """Set self.selected_idx, mark slot as 'selected', unmark old one.""" - # 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_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 - self._assert_selection() - self._on_selection_change() + # build_grid and helpers def _build_grid(self) -> None: """(Re-)build slot grid from .dir_entries, filters, layout settings.""" old_selected_item: Optional[GalleryItem] = self.selected_item - - def update_items_attrs() -> None: - self.items_attrs.clear() - - def separate_items_attrs(basic_items_attrs) -> ItemsAttrs: - items_attrs: ItemsAttrs = {} - for attr_name, vals in basic_items_attrs.items(): - sorter = self._sort_order.by_name(attr_name) - items_attrs[attr_name] = {'incl': [], 'excl': []} - for v in vals: - passes_filter = sorter is None - if sorter: - passes_filter = sorter.filter_allows_value(v) - k = 'incl' if passes_filter 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._prep_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'] - for v in items_attrs_tmp_1[attr_name]['incl']: - k = ('incl' if v in items_attrs_tmp_2[attr_name]['incl'] - else 'semi') - final_values[k] += [v] - for category in ('incl', 'semi', 'excl'): - final_values[category].sort() - self.items_attrs[attr_name] = final_values - - def filter_entries(items_attrs: ItemsAttrs) -> list[GalleryItem]: - entries_filtered: list[GalleryItem] = [] - 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) - if val not in items_attrs[attr_name]['incl']: - passes_filters = False - break - if passes_filters: - entries_filtered += [entry] - return entries_filtered - - 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) - self._grid = Gtk.Grid() - if self._col_headers_grid.get_parent(): - self._col_headers_frame.remove(self._col_headers_grid) - self._col_headers_grid = Gtk.Grid() - self.slots.clear() - self._fixed_frame.put(self._grid, 0, 0) - - def build_rows_by_attrs( - remaining: list[tuple[str, AttrVals]], - items_of_parent: list[GalleryItem], - ancestors: list[tuple[str, str]] - ) -> None: - if not items_of_parent: - return - attr_name, attr_values = remaining[0] - if 1 == len(remaining): - for i, attr in enumerate(ancestors): - txt = f'{attr[0]}: {attr[1]}' - 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) - for gallery_item in items_of_parent: - val = getattr(gallery_item, attr_name) - idx_val_in_attr_values = attr_values.index(val) - if row[idx_val_in_attr_values]: - gallery_item.with_others = True - row[idx_val_in_attr_values] = gallery_item - for i_col, item in enumerate(row): - slot = GallerySlot( # build empty dummy if necessary - item if item else GalleryItem('', ''), - self._slots_geometry) - self.slots += [slot] - i_slot_ref[0] += 1 - self._grid.attach(slot, i_col + len(ancestors), - i_row_ref[0], 1, 1) - i_row_ref[0] += 1 - return - for attr_value in attr_values: - items_of_attr_value = [x for x in items_of_parent - if attr_value == getattr(x, - attr_name)] - build_rows_by_attrs(remaining[1:], items_of_attr_value, - ancestors + [(attr_name, attr_value)]) - - if self._by_1st: - self._show_dirs = False - sort_attrs: list[tuple[str, AttrVals]] = [] - for sorter in reversed(self._sort_order): - vals: AttrVals = self.items_attrs[sorter.name]['incl'] - if len(vals) > 1: - sort_attrs += [(sorter.name, vals)] - if not sort_attrs: - s_name: str = self._sort_order[0].name - sort_attrs += [(s_name, self.items_attrs[s_name]['incl'])] - self._per_row = len(sort_attrs[-1][1]) - build_rows_by_attrs(sort_attrs, entries_filtered, []) - self._col_headers_frame.put(self._col_headers_grid, 0, 0) - self._col_headers_grid.attach(Gtk.Box(), 0, 0, 1, 1) - top_attr_name: str = sort_attrs[-1][0] - for i, val in enumerate(sort_attrs[-1][1]): - label = Gtk.Label(label=f'{top_attr_name}: {val}', - xalign=0, - ellipsize=Pango.EllipsizeMode.MIDDLE) - label.set_use_markup(True) - self._col_headers_grid.attach(label, i + 1, 0, 1, 1) - - else: - dir_entries_filtered_sorted: list[GalleryItem] = sorted( - entries_filtered, key=cmp_to_key(self._sort_cmp)) - i_row, i_col = 0, 0 - for i, item in enumerate(dir_entries_filtered_sorted): - if self._per_row == i_col: - i_col = 0 - i_row += 1 - slot = GallerySlot(item, self._slots_geometry, - self._on_hit_item) - self._grid.attach(slot, i_col, i_row, 1, 1) - self.slots += [slot] - i_col += 1 - self.update_config_box() - - update_items_attrs() - entries_filtered = filter_entries(self.items_attrs) - build(entries_filtered) + self._update_items_attrs() + entries_filtered = self._filter_entries(self.items_attrs) + self._build_grid_core(entries_filtered) new_idx = 0 if old_selected_item is not None: for i, slot in enumerate(self.slots): @@ -642,49 +492,175 @@ class Gallery: break self._set_selection(new_idx) - def request_update(self, - select: bool = False, - scroll_to_focus: bool = False, - build_grid: bool = False, - load: bool = False - ) -> None: - """Set ._shall_… to trigger updates on next relevant interval.""" - self._shall_redraw = 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], - y_inc: Optional[int], - buf_end: Optional[int] - ) -> None: - """Move .selection, update its dependencies, redraw gallery.""" - min_idx, max_idx = 0, len(self.slots) - 1 - if -1 == y_inc and self.selected_idx >= self._per_row: - new_idx = self.selected_idx - self._per_row - elif 1 == y_inc and self.selected_idx <= max_idx - self._per_row: - new_idx = self.selected_idx + self._per_row - elif -1 == x_inc and self.selected_idx > 0: - new_idx = self.selected_idx - 1 - elif 1 == x_inc and self.selected_idx < max_idx: - new_idx = self.selected_idx + 1 - elif 1 == buf_end: - new_idx = max_idx - elif -1 == buf_end: - new_idx = min_idx + def _update_items_attrs(self) -> None: + self.items_attrs.clear() + + def separate_items_attrs(basic_items_attrs) -> ItemsAttrs: + items_attrs: ItemsAttrs = {} + for attr_name, vals in basic_items_attrs.items(): + sorter = self._sort_order.by_name(attr_name) + items_attrs[attr_name] = {'incl': [], 'excl': []} + for v in vals: + passes_filter = sorter is None + if sorter: + passes_filter = sorter.filter_allows_value(v) + k = 'incl' if passes_filter else 'excl' + items_attrs[attr_name][k] += [v] + return items_attrs + + items_attrs_tmp_1 = separate_items_attrs(self._basic_items_attrs) + filtered = self._filter_entries(items_attrs_tmp_1) + reduced_basic_items_attrs = self._prep_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'] + for v in items_attrs_tmp_1[attr_name]['incl']: + k = ('incl' if v in items_attrs_tmp_2[attr_name]['incl'] + else 'semi') + final_values[k] += [v] + for category in ('incl', 'semi', 'excl'): + final_values[category].sort() + self.items_attrs[attr_name] = final_values + + def _filter_entries(self, items_attrs: ItemsAttrs) -> list[GalleryItem]: + entries_filtered: list[GalleryItem] = [] + 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) + if val not in items_attrs[attr_name]['incl']: + passes_filters = False + break + if passes_filters: + entries_filtered += [entry] + return entries_filtered + + def _build_grid_core(self, entries_filtered: list[GalleryItem]) -> None: + def build_rows_by_attrs(remaining: list[tuple[str, AttrVals]], + items_of_parent: list[GalleryItem], + ancestors: list[tuple[str, str]] + ) -> None: + if not items_of_parent: + return + attr_name, attr_values = remaining[0] + if 1 == len(remaining): + for i, attr in enumerate(ancestors): + txt = f'{attr[0]}: {attr[1]}' + 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) + for gallery_item in items_of_parent: + val = getattr(gallery_item, attr_name) + idx_val_in_attr_values = attr_values.index(val) + if row[idx_val_in_attr_values]: + gallery_item.with_others = True + row[idx_val_in_attr_values] = gallery_item + for i_col, item in enumerate(row): + slot = GallerySlot( # build empty dummy if necessary + item if item else GalleryItem('', ''), + self._slots_geometry) + self.slots += [slot] + i_slot_ref[0] += 1 + self._grid.attach(slot, i_col + len(ancestors), + i_row_ref[0], 1, 1) + i_row_ref[0] += 1 + return + for attr_value in attr_values: + items_of_attr_value = [x for x in items_of_parent + if attr_value == getattr(x, attr_name)] + build_rows_by_attrs(remaining[1:], items_of_attr_value, + ancestors + [(attr_name, attr_value)]) + + i_row_ref, i_slot_ref = [0], [0] + if self._grid.get_parent(): + self._fixed_frame.remove(self._grid) + self._grid = Gtk.Grid() + if self._col_headers_grid.get_parent(): + self._col_headers_frame.remove(self._col_headers_grid) + self._col_headers_grid = Gtk.Grid() + self.slots.clear() + self._fixed_frame.put(self._grid, 0, 0) + if self._by_1st: + self._show_dirs = False + sort_attrs: list[tuple[str, AttrVals]] = [] + for sorter in reversed(self._sort_order): + vals: AttrVals = self.items_attrs[sorter.name]['incl'] + if len(vals) > 1: + sort_attrs += [(sorter.name, vals)] + if not sort_attrs: + s_name: str = self._sort_order[0].name + sort_attrs += [(s_name, self.items_attrs[s_name]['incl'])] + self._per_row = len(sort_attrs[-1][1]) + build_rows_by_attrs(sort_attrs, entries_filtered, []) + self._col_headers_frame.put(self._col_headers_grid, 0, 0) + self._col_headers_grid.attach(Gtk.Box(), 0, 0, 1, 1) + top_attr_name: str = sort_attrs[-1][0] + for i, val in enumerate(sort_attrs[-1][1]): + label = Gtk.Label(label=f'{top_attr_name}: {val}', + xalign=0, + ellipsize=Pango.EllipsizeMode.MIDDLE) + label.set_use_markup(True) + self._col_headers_grid.attach(label, i + 1, 0, 1, 1) else: - return - self._set_selection(new_idx) + dir_entries_filtered_sorted: list[GalleryItem] = sorted( + entries_filtered, key=cmp_to_key(self._sort_cmp)) + i_row, i_col = 0, 0 + for i, item in enumerate(dir_entries_filtered_sorted): + if self._per_row == i_col: + i_col = 0 + i_row += 1 + slot = GallerySlot(item, self._slots_geometry, + self._on_hit_item) + self._grid.attach(slot, i_col, i_row, 1, 1) + self.slots += [slot] + i_col += 1 + self.update_config_box() - def on_resize(self, width: int = 0, height: int = 0) -> None: - """Force redraw and scroll-to-focus into new geometry.""" - self._force_width, self._force_height = width, height - self.request_update(scroll_to_focus=True) + def _sort_cmp(self, a: GalleryItem, b: GalleryItem) -> int: + """Sort [a, b] by user-set sort order, and putting directories first""" + # ensure ".." and all DirItems at start of order + if self._show_dirs: + cmp_upper_dir = f' {UPPER_DIR}' + if isinstance(a, DirItem) and a.name == cmp_upper_dir: + return -1 + if isinstance(b, DirItem) and b.name == cmp_upper_dir: + return +1 + if isinstance(a, DirItem) and not isinstance(b, DirItem): + return -1 + if isinstance(b, DirItem) and not isinstance(a, DirItem): + return +1 + # apply ._sort_order within DirItems and ImgItems (separately) + ret = 0 + for key in [sorter.name for sorter in self._sort_order]: + a_cmp = None + b_cmp = None + if hasattr(a, key): + a_cmp = getattr(a, key) + if hasattr(b, key): + b_cmp = getattr(b, key) + if a_cmp is None and b_cmp is None: + continue + if a_cmp is None: + ret = -1 + elif b_cmp is None: + ret = +1 + elif a_cmp > b_cmp: + ret = +1 + elif a_cmp < b_cmp: + ret = -1 + return ret + + # redraw_and_check_focus and helpers def _redraw_and_check_focus(self) -> None: """Draw gallery; possibly notice and first follow need to re-focus.""" + vp_width: int = (self._force_width if self._force_width else self._viewport.get_width()) vp_height: int = (self._force_height if self._force_height @@ -729,20 +705,6 @@ class Gallery: slot.update_widget(in_vp) self._start_redraw_wait = datetime.now() - def _position_to_viewport(self, - idx: int, - vp_top: int, - vp_bottom: int, - in_vp_greedy: bool = False - ) -> tuple[bool, int, int]: - slot_top: int = (idx // self._per_row) * self._slots_geometry.size - slot_bottom: int = slot_top + self._slots_geometry.size - if in_vp_greedy: - in_vp = (slot_bottom >= vp_top and slot_top <= vp_bottom) - else: - in_vp = (slot_top >= vp_top and slot_bottom <= vp_bottom) - return in_vp, slot_top, slot_bottom - def _scroll_to_focus(self, vp_scroll: Gtk.Scrollable, vp_top: int, @@ -763,36 +725,83 @@ class Gallery: return True return False - def _sort_cmp(self, a: GalleryItem, b: GalleryItem) -> int: - """Sort [a, b] by user-set sort order, and putting directories first""" - # ensure ".." and all DirItems at start of order - if self._show_dirs: - cmp_upper_dir = f' {UPPER_DIR}' - if isinstance(a, DirItem) and a.name == cmp_upper_dir: - return -1 - if isinstance(b, DirItem) and b.name == cmp_upper_dir: - return +1 - if isinstance(a, DirItem) and not isinstance(b, DirItem): - return -1 - if isinstance(b, DirItem) and not isinstance(a, DirItem): - return +1 - # apply ._sort_order within DirItems and ImgItems (separately) - ret = 0 - for key in [sorter.name for sorter in self._sort_order]: - a_cmp = None - b_cmp = None - if hasattr(a, key): - a_cmp = getattr(a, key) - if hasattr(b, key): - b_cmp = getattr(b, key) - if a_cmp is None and b_cmp is None: - continue - if a_cmp is None: - ret = -1 - elif b_cmp is None: - ret = +1 - elif a_cmp > b_cmp: - ret = +1 - elif a_cmp < b_cmp: - ret = -1 - return ret + def _position_to_viewport(self, + idx: int, + vp_top: int, + vp_bottom: int, + in_vp_greedy: bool = False + ) -> tuple[bool, int, int]: + slot_top: int = (idx // self._per_row) * self._slots_geometry.size + slot_bottom: int = slot_top + self._slots_geometry.size + if in_vp_greedy: + in_vp = (slot_bottom >= vp_top and slot_top <= vp_bottom) + else: + in_vp = (slot_top >= vp_top and slot_bottom <= vp_bottom) + return in_vp, slot_top, slot_bottom + + # helpers + + def _prep_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 _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.""" + # 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_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 + self._assert_selection() + self._on_selection_change() + + # selection management + + @property + def selected_item(self) -> Optional[GalleryItem]: + """Return slot.item for slot at self.selected_idx.""" + return self.slots[self.selected_idx].item if self.slots else None + + def on_focus_slot(self, slot: GallerySlot) -> None: + """If GallerySlot focused, set .selected_idx to it.""" + self._set_selection(self.slots.index(slot)) + self.request_update(scroll_to_focus=True) + + def move_selection(self, + x_inc: Optional[int], + y_inc: Optional[int], + buf_end: Optional[int] + ) -> None: + """Move .selection, update its dependencies, redraw gallery.""" + min_idx, max_idx = 0, len(self.slots) - 1 + if -1 == y_inc and self.selected_idx >= self._per_row: + new_idx = self.selected_idx - self._per_row + elif 1 == y_inc and self.selected_idx <= max_idx - self._per_row: + new_idx = self.selected_idx + self._per_row + elif -1 == x_inc and self.selected_idx > 0: + new_idx = self.selected_idx - 1 + elif 1 == x_inc and self.selected_idx < max_idx: + new_idx = self.selected_idx + 1 + elif 1 == buf_end: + new_idx = max_idx + elif -1 == buf_end: + new_idx = min_idx + else: + return + self._set_selection(new_idx)