-        def select_sort_order(_a, _b, _c) -> None:
-            self._sort_sel.props.selected_item.widget.get_parent().grab_focus()
-
-        def toggle_recurse(_) -> None:
-            self._set_recurse_changed = not self._set_recurse_changed
-            self._btn_apply.set_sensitive(not self._set_recurse_changed)
-
-        def toggle_by_1st(btn: Gtk.CheckButton) -> None:
-            self._btn_per_row.set_sensitive(not btn.props.active)
-            self._btn_show_dirs.set_sensitive(not btn.props.active)
-            if btn.props.active:
-                self._btn_show_dirs.set_active(False)
-
-        def apply_config() -> None:
-            if self._tmp_order:
-                self.order.sync_from(self._tmp_order)
-                self._tmp_order = None
-                self._gallery_request_update(build_grid=True)
-            if self._filter_changed:
-                self._gallery_request_update(build_grid=True)
-            self._gallery_update_settings(
-                    per_row=self._btn_per_row.get_value_as_int(),
-                    by_1st=self._btn_by_1st.get_active(),
-                    show_dirs=self._btn_show_dirs.get_active(),
-                    recurse_dirs=self._btn_recurse.get_active())
-            self._gallery_request_update(select=True)
-            self._set_recurse_changed = False
-            self._filter_changed = False
-
-        def full_reload() -> None:
-            apply_config()
-            self._gallery_request_update(load=True)
-            self._btn_apply.set_sensitive(True)
-
-        self._filter_changed = False
-        self._set_recurse_changed = False
-        self._last_selected: Optional[Gtk.Widget] = None
-
-        self._store = Gio.ListStore(item_type=SorterAndFilterer)
-        self._sort_sel = Gtk.SingleSelection.new(self._store)
-        self._sort_sel.connect('selection-changed', select_sort_order)
-        fac = Gtk.SignalListItemFactory()
-        fac.connect('setup', setup_sorter_list_item)
-        fac.connect('bind', bind_sorter_list_item)
-        self.sorter_listing = Gtk.ListView(model=self._sort_sel, factory=fac)
-
-        buttons_box = Gtk.Box(orientation=OR_H)
-        self._btn_apply = _add_button(buttons_box, 'apply config',
-                                      lambda _: apply_config())
-        self._btn_reload = _add_button(buttons_box, 'full reload',
-                                       lambda _: full_reload())
-
-        buttons_box = Gtk.Box(orientation=OR_H)
-        self._btn_apply = _add_button(buttons_box, 'apply config',
-                                      lambda _: apply_config())
-        self._btn_reload = _add_button(buttons_box, 'full reload',
-                                       lambda _: full_reload())
-
-        dirs_box = Gtk.Box(orientation=OR_H)
-        dirs_box.append(Gtk.Label(label='directories:'))
-        self._btn_show_dirs = _add_button(dirs_box, 'show', checkbox=True)
-        self._btn_recurse = _add_button(dirs_box, 'recurse',
-                                        toggle_recurse, checkbox=True)
-
-        per_row_box = Gtk.Box(orientation=OR_H)
-        per_row_box.append(Gtk.Label(label='cols/row:'))
-        self._btn_by_1st = _add_button(per_row_box, 'by 1st sorter',
-                                       toggle_by_1st, checkbox=True)
-        self._btn_per_row = Gtk.SpinButton.new_with_range(
-                GALLERY_PER_ROW_DEFAULT, 9, 1)
-        per_row_box.append(self._btn_per_row)
-
-        box.append(self.sorter_listing)
-        box.append(dirs_box)
-        box.append(per_row_box)
-        box.append(buttons_box)
-
-    def on_focus_sorter(self, focused: SorterAndFilterer) -> None:
-        """If sorter focused, select focused, move display of values there."""
-        if self._last_selected:
-            self._last_selected.values.set_visible(False)
-        self._last_selected = focused.get_first_child()
-        self._last_selected.values.set_visible(True)
-        for i in range(self._sort_sel.get_n_items()):
-            if self._sort_sel.get_item(i).widget == self._last_selected:
-                self._sort_sel.props.selected = i
-                break
-
-    def move_selection(self, direction: int) -> None:
-        """Move sort order selection by direction (-1 or +1)."""
-        min_idx, max_idx = 0, len(self.order) - 1
-        cur_idx = self._sort_sel.props.selected
-        if (1 == direction and cur_idx < max_idx)\
-                or (-1 == direction and cur_idx > min_idx):
-            self._sort_sel.props.selected = cur_idx + direction
-
-    def move_sorter(self, direction: int) -> None:
-        """Move selected item in sort order view, ensure temporary state."""
-        tmp_order = self._tmp_order if self._tmp_order else self.order.copy()
-        cur_idx = self._sort_sel.props.selected
-        if direction == -1 and cur_idx > 0:
-            tmp_order.switch_at(cur_idx, forward=False)
-        elif direction == 1 and cur_idx < (len(tmp_order) - 1):
-            tmp_order.switch_at(cur_idx, forward=True)
-        else:  # to catch movement beyond limits
-            return
-        if not self._tmp_order:
-            self._tmp_order = tmp_order
-            for sorter in self._tmp_order:
-                sorter.widget.add_css_class('temp')
-        self.update_box(cur_idx + direction)
-        self._sort_sel.props.selected = cur_idx + direction
-        for i in range(self._store.get_n_items()):
-            sort_item: SorterAndFilterer = self._store.get_item(i)
-            sort_item.widget.add_css_class('temp')
-
-    def update_box(self, cur_selection: int = 0) -> None:
-        """Rebuild sorter listing in box from .order, or alt_order if set."""
-        sort_order = self._tmp_order if self._tmp_order else self.order
-        sort_order.into_store(self._store)
-        self._sort_sel.props.selected = cur_selection
-
-
-class VerticalLabel(Gtk.DrawingArea):
-    """Label of vertical text (rotated -90°)."""
-
-    def __init__(self,
-                 text: str,
-                 slots_geometry: GallerySlotsGeometry
-                 ) -> None:
-        super().__init__()
-        self._text = text
-        self._slots_geometry = slots_geometry
-        test_layout = self.create_pango_layout()
-        test_layout.set_markup(text)
-        _, self._text_height = test_layout.get_pixel_size()
-        self.set_draw_func(self._on_draw)
-
-    def _on_draw(self,
-                 _,
-                 cairo_ctx: Pango.Context,
-                 __,
-                 height: int
-                 ) -> None:
-        """Create layout, rotate by 90°, size widget to measurements."""
-        layout = self.create_pango_layout()
-        layout.set_markup(self._text)
-        layout.set_width(self._slots_geometry.size * Pango.SCALE)
-        layout.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
-        text_width, _ = layout.get_pixel_size()
-        cairo_ctx.translate(0, text_width + (height - text_width))
-        cairo_ctx.rotate(radians(-90))
-        PangoCairo.show_layout(cairo_ctx, layout)
-        self.set_size_request(self._text_height, text_width)
-
-    @property
-    def width(self) -> int:
-        """Return (rotated) ._text_height."""
-        return self._text_height
-
-
-class Gallery:
-    """Representation of GalleryItems below a directory."""
-    update_config_box: Callable
-
-    def __init__(self,
-                 sort_order: SorterAndFiltererOrder,
-                 on_hit_item: Callable,
-                 on_selection_change: Callable,
-                 bookmarks_db: BookmarksDb,
-                 cache_db: CacheDb
-                 ) -> None:
-        self._on_hit_item = on_hit_item
-        self._on_selection_change = on_selection_change
-        self._bookmarks_db, self._cache_db = bookmarks_db, cache_db
-        self._sort_order = sort_order
-        self._img_dir_path = ''
-
-        self._shall_load = False
-        self._shall_build_grid = False
-        self._shall_redraw = False
-        self._shall_scroll_to_focus = False
-        self._shall_select = False
-
-        self._show_dirs = False
-        self._recurse_dirs = False
-        self._by_1st = False
-        self._per_row = GALLERY_PER_ROW_DEFAULT
-        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] = []
-
-        self._grid = Gtk.Grid()
-        self._force_width, self._force_height = 0, 0
-        scroller = Gtk.ScrolledWindow(propagate_natural_height=True)
-        self._col_headers_frame = Gtk.Fixed()
-        self._col_headers_grid = Gtk.Grid()
-        self.frame = Gtk.Box(orientation=OR_V)
-        self.frame.append(self._col_headers_frame)
-        self.frame.append(scroller)
-        # We want our viewport at always maximum possible size (so we can
-        # safely calculate what's in it and what not), even if the gallery
-        # would be smaller. Therefore we frame the gallery in an expanding
-        # Fixed, to stretch out the viewport even if the gallery is small.
-        self._fixed_frame = Gtk.Fixed(hexpand=True, vexpand=True)
-        scroller.set_child(self._fixed_frame)
-        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
-            self.request_update()  # only request redraw
-
-        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)
-
-    def update_settings(self,
-                        per_row: Optional[int] = None,
-                        by_1st: Optional[bool] = None,
-                        show_dirs: Optional[bool] = None,
-                        recurse_dirs: Optional[bool] = None,
-                        img_dir_path: Optional[str] = None,
-                        ) -> None:
-        """Set Gallery setup fields, request appropriate updates."""
-        for val, attr_name in [(per_row, '_per_row'),
-                               (by_1st, '_by_1st'),
-                               (show_dirs, '_show_dirs'),
-                               (recurse_dirs, '_recurse_dirs'),
-                               (img_dir_path, '_img_dir_path')]:
-            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.request_update(load=True)
-                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 _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 _load_directory(self) -> None:
-        """(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()
-
-        def read_directory(dir_path: str, make_parent: bool = False) -> None:
-            if make_parent:
-                parent_dir = DirItem(abspath(path_join(dir_path, UPPER_DIR)),
-                                     UPPER_DIR, is_parent=True)
-                self.dir_entries += [parent_dir]
-            dirs_to_enter: list[str] = []
-            to_set_metadata_on: list[ImgItem] = []
-            dir_entries = list(listdir(dir_path))
-            for i, filename in enumerate(dir_entries):
-                msg = f'loading {dir_path}: entry {i+1}/{len(dir_entries)}'
-                print(msg, end='\r')
-                full_path = path_join(dir_path, filename)
-                if isdir(full_path):
-                    self.dir_entries += [DirItem(dir_path, filename)]
-                    dirs_to_enter += [full_path]
-                    continue
-                _, ext = splitext(filename)
-                if ext not in ACCEPTED_IMG_FILE_ENDINGS:
-                    continue
-                img_item = ImgItem(dir_path, filename, cache)
-                if img_item.full_path in bookmarks:
-                    img_item.bookmarked = True
-                if not img_item.has_metadata:
-                    to_set_metadata_on += [img_item]
-                self.dir_entries += [img_item]
-            print('')
-            for i, item in enumerate(to_set_metadata_on):
-                msg = f'setting metadata: {i+1}/{len(to_set_metadata_on)}'
-                print(msg, end='\r')
-                item.set_metadata(cache)
-            msg = '' if to_set_metadata_on else '(no metadata to set)'
-            print(msg)
-            if dirs_to_enter and self._recurse_dirs:
-                prefix = f'entering directories below {dir_path}: directory '
-                for i, path in enumerate(dirs_to_enter):
-                    print(f'{prefix}{i+1}/{len(dirs_to_enter)}')
-                    read_directory(path)
-
-        read_directory(self._img_dir_path, make_parent=True)
-        prompts_set: set[str] = set()
-        for entry in [e for e in self.dir_entries
-                      if isinstance(e, ImgItem) and hasattr(e, 'prompt')]:
-            prompts_set.add(entry.prompt)
-        prompts_diff = self._diff_prompts(list(prompts_set))
-        for entry in [e for e in self.dir_entries if isinstance(e, ImgItem)]:
-            entry.subprompt1 = prompts_diff[entry.prompt][0]
-            entry.subprompt2 = prompts_diff[entry.prompt][1]
-        if self._sort_order.by_name('prompt'):
-            self._sort_order.remove('prompt')
-            self._sort_order._list.append(SorterAndFilterer('subprompt1'))
-            self._sort_order._list.append(SorterAndFilterer('subprompt2'))
-        self._basic_items_attrs = self._prep_items_attrs(self.dir_entries)
-        ignorable_attrs = []
-        for attr_name, attr_vals in self._basic_items_attrs.items():
-            if len(attr_vals) < 2:
-                ignorable_attrs += [attr_name]
-        for attr_name in ignorable_attrs:
-            self._sort_order.remove(attr_name)
-            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
-
-    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 _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()
-
-    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)
-        new_idx = 0
-        if old_selected_item is not None:
-            for i, slot in enumerate(self.slots):
-                if hash(old_selected_item) == hash(slot.item):
-                    new_idx = i
-                    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
-        else:
-            return
-        self._set_selection(new_idx)
-
-    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 _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
-                          else self._viewport.get_height())
-        self._force_width, self._force_height = 0, 0
-        vp_scroll: Gtk.Adjustment = self._viewport.get_vadjustment()
-        vp_top: int = vp_scroll.get_value()
-        vp_bottom: int = vp_top + vp_height
-        side_offset, i_vlabels = 0, 0
-        if self._by_1st:
-            while True:
-                gal_widget: VerticalLabel | GalleryItem
-                gal_widget = self._grid.get_child_at(i_vlabels, 0)
-                if isinstance(gal_widget, VerticalLabel):
-                    side_offset += gal_widget.width
-                else:
-                    break
-                i_vlabels += 1
-        max_slot_width: int = (vp_width - side_offset) // self._per_row
-        self._slots_geometry.set_size(min(vp_height, max_slot_width))
-        if self._by_1st:
-            i_widgets = 0
-            while True:
-                head_widget: Gtk.Box | Gtk.Label | None
-                head_widget = self._col_headers_grid.get_child_at(i_widgets, 0)
-                if 0 == i_widgets:
-                    head_widget.set_size_request(side_offset, -1)
-                elif isinstance(head_widget, Gtk.Label):
-                    head_widget.set_size_request(self._slots_geometry.size, -1)
-                else:
-                    break
-                i_widgets += 1
-        for idx, slot in enumerate(self.slots):
-            slot.ensure_slot_size()
-        vp_scroll.set_upper(self._slots_geometry.size * ceil(len(self.slots)
-                                                             / self._per_row))
-        if self._scroll_to_focus(vp_scroll, vp_top, vp_bottom):
-            return
-        for idx, slot in enumerate(self.slots):
-            in_vp, _, _ = self._position_to_viewport(idx,
-                                                     vp_top, vp_bottom, True)
-            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,
-                         vp_bottom: int
-                         ) -> bool:
-        scroll_to_focus: bool = self._shall_scroll_to_focus
-        self._shall_redraw, self._shall_scroll_to_focus = False, False
-        if scroll_to_focus:
-            in_vp, slot_top, slot_bottom = self._position_to_viewport(
-                    self.selected_idx, vp_top, vp_bottom)
-            if not in_vp:
-                self._shall_redraw, self._shall_scroll_to_focus = True, True
-                if slot_top < vp_top:
-                    vp_scroll.set_value(slot_top)
-                else:
-                    vp_scroll.set_value(slot_bottom
-                                        - self._slots_geometry.size)
-                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
-
-
-class MainWindow(Gtk.Window):
-    """Image browser app top-level window."""
-
-    def __init__(self, app: Application, **kwargs) -> None:
-        super().__init__(**kwargs)
-        self.app = app
-        self.gallery = Gallery(
-                sort_order=self.app.sort_order,
-                on_hit_item=self.hit_gallery_item,
-                on_selection_change=self.update_metadata_on_gallery_selection,
-                bookmarks_db=self.app.bookmarks_db,
-                cache_db=self.app.cache_db)
-        config_box = Gtk.Box(orientation=OR_V)
-        self.conf = GalleryConfig(
-                sort_order=self.app.sort_order,
-                box=config_box,
-                request_update=self.gallery.request_update,
-                update_settings=self.gallery.update_settings,
-                items_attrs=self.gallery.items_attrs)
-        self.gallery.update_config_box = self.conf.update_box
-        metadata_textview = Gtk.TextView(wrap_mode=Gtk.WrapMode.WORD_CHAR,
-                                         editable=False)
-        self.metadata = metadata_textview.get_buffer()
-        self.idx_display = Gtk.Label()
-
-        # layout: outer box, CSS, sizings
-        box_outer = Gtk.Box(orientation=OR_H)
-        self.set_child(box_outer)
-        css_provider = Gtk.CssProvider()
-        css_provider.load_from_data(CSS)
-        Gtk.StyleContext.add_provider_for_display(
-                self.get_display(), css_provider,
-                Gtk.STYLE_PROVIDER_PRIORITY_USER)
-        metadata_textview.set_size_request(300, -1)
-        self.connect('notify::default-width', lambda _, __: self.on_resize())
-        self.connect('notify::default-height', lambda _, __: self.on_resize())
-
-        # layout: sidebar
-        self.side_box = Gtk.Notebook.new()
-        self.side_box.append_page(metadata_textview,
-                                  Gtk.Label(label='metadata'))
-        self.side_box.append_page(config_box, Gtk.Label(label='config'))
-        box_outer.append(self.side_box)
-
-        # layout: gallery viewer
-        viewer = Gtk.Box(orientation=OR_V)
-        self.navbar = Gtk.Box(orientation=OR_H)
-        _add_button(self.navbar, 'sidebar', lambda _: self.toggle_side_box())
-        self.navbar.append(self.idx_display)
-        viewer.append(self.navbar)
-        viewer.append(self.gallery.frame)
-        box_outer.append(viewer)
-
-        # init key and focus control
-        key_ctl = Gtk.EventControllerKey(
-                propagation_phase=Gtk.PropagationPhase.CAPTURE)
-        key_ctl.connect('key-pressed',
-                        lambda _, kval, _0, _1: self.handle_keypress(kval))
-        self.add_controller(key_ctl)
-        self.prev_key_ref = [0]
-        self.connect('notify::focus-widget',
-                     lambda _, __: self.on_focus_change())
-
-        # only now we're ready for actually running the gallery
-        GLib.idle_add(lambda: self.gallery.update_settings(
-            img_dir_path=self.app.img_dir_absolute))
-
-    def on_focus_change(self) -> None:
-        """Handle reactions on focus changes in .gallery and .conf."""
-        focused: Optional[Gtk.Widget] = self.get_focus()
-        if not focused:
-            return
-        if isinstance(focused, GallerySlot):
-            self.gallery.on_focus_slot(focused)
-        elif focused.get_parent() == self.conf.sorter_listing:
-            self.conf.on_focus_sorter(focused)
-
-    def on_resize(self) -> None:
-        """On window resize, do .gallery.on_resize towards its new geometry."""
-        if self.get_width() > 0:  # So we don't call this on initial resize.
-            # NB: We .measure side_box because its width is changing, whereas
-            # for the unchanging navbar .get_height is sufficient.
-            side_box_width: int = self.side_box.measure(OR_H, -1).natural
-            default_size: tuple[int, int] = self.get_default_size()
-            self.gallery.on_resize(default_size[0] - side_box_width,
-                                   default_size[1] - self.navbar.get_height())
-
-    def bookmark(self) -> None:
-        """Toggle bookmark on selected gallery item."""
-        if not isinstance(self.gallery.selected_item, ImgItem):
-            return
-        bookmarks = self.app.bookmarks_db.as_ref()
-        if self.gallery.selected_item.bookmarked:
-            self.gallery.selected_item.bookmark(False)
-            bookmarks.remove(self.gallery.selected_item.full_path)
-        else:
-            self.gallery.selected_item.bookmark(True)
-            bookmarks += [self.gallery.selected_item.full_path]
-        self.app.bookmarks_db.write()
-        self.conf.update_box()
-
-    def hit_gallery_item(self) -> None:
-        """If current file selection is directory, reload into that one."""
-        selected: Optional[GalleryItem] = self.gallery.selected_item
-        if isinstance(selected, DirItem):
-            self.gallery.update_settings(img_dir_path=selected.full_path)
-
-    def toggle_side_box(self) -> None:
-        """Toggle window sidebox visible/invisible."""
-        self.side_box.props.visible = not self.side_box.get_visible()
-        # Calculate new viewport directly, because GTK's respective viewport
-        # measurement happens too late for our needs.
-        side_box_width: int = self.side_box.measure(OR_H, -1).natural
-        self.gallery.on_resize(self.get_width() - side_box_width)
-
-    def update_metadata_on_gallery_selection(self) -> None:
-        """Update .metadata about individual file, and .idx_display."""
-        self.metadata.set_text('')
-        selected_item: Optional[GalleryItem] = self.gallery.selected_item
-        display_name = '(none)'
-        if selected_item:
-            if isinstance(selected_item, ImgItem):
-                params_strs = [f'{k}: {getattr(selected_item, k.lower())}'
-                               for k in GEN_PARAMS]
-                title = f'{selected_item.full_path}'
-                bookmarked = 'BOOKMARK' if selected_item.bookmarked else ''
-                self.metadata.set_text(
-                        '\n'.join([title, bookmarked] + params_strs))
-                display_name = selected_item.full_path
-            elif isinstance(selected_item, DirItem):
-                display_name = selected_item.full_path
-        total = len([s for s in self.gallery.slots
-                     if isinstance(s.item, (DirItem, ImgItem))])
-        n_selected: int = self.gallery.selected_idx + 1
-        txt = f' {n_selected} of {total} – <b>{display_name}</b>'
-        self.idx_display.set_text(txt)
-        self.idx_display.set_use_markup(True)
-
-    def handle_keypress(self, keyval: int) -> bool:
-        """Handle keys if not in Gtk.Entry, return True if key handling done"""
-        if isinstance(self.get_focus().get_parent(), Gtk.Entry):
-            return False
-        if Gdk.KEY_Return == keyval and isinstance(self.get_focus(),
-                                                   GallerySlot):
-            self.hit_gallery_item()
-        elif Gdk.KEY_G == keyval:
-            self.gallery.move_selection(None, None, 1)
-        elif Gdk.KEY_h == keyval:
-            self.gallery.move_selection(-1, None, None)
-        elif Gdk.KEY_j == keyval:
-            self.gallery.move_selection(None, +1, None)
-        elif Gdk.KEY_k == keyval:
-            self.gallery.move_selection(None, -1, None)
-        elif Gdk.KEY_l == keyval:
-            self.gallery.move_selection(+1, None, None)
-        elif Gdk.KEY_g == keyval and Gdk.KEY_g == self.prev_key_ref[0]:
-            self.gallery.move_selection(None, None, -1)
-        elif Gdk.KEY_w == keyval:
-            self.conf.move_selection(-1)
-        elif Gdk.KEY_W == keyval:
-            self.conf.move_sorter(-1)
-        elif Gdk.KEY_s == keyval:
-            self.conf.move_selection(1)
-        elif Gdk.KEY_S == keyval:
-            self.conf.move_sorter(1)
-        elif Gdk.KEY_b == keyval:
-            self.bookmark()
-        else:
-            self.prev_key_ref[0] = keyval
-            return False
-        return True
-
-
-main_app = Application(application_id='plomlompom.com.StablePixBrowser.App')
-main_app.run()