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
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,
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."""
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'<b>{attr[0]}</b>: {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'<b>{top_attr_name}</b>: {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):
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'<b>{attr[0]}</b>: {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'<b>{top_attr_name}</b>: {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
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,
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)