home · contact · privacy
Browser: Further typify and refactor code. master
authorChristian Heller <c.heller@plomlompom.de>
Mon, 4 Nov 2024 15:46:31 +0000 (16:46 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Mon, 4 Nov 2024 15:46:31 +0000 (16:46 +0100)
browser.py

index e1f2e60c4f97a44dcc0b7d21779c3a0287bb2565..ff9973315ddfef9eedcb3b9a8aaedf808aee40df 100755 (executable)
@@ -28,7 +28,8 @@ from stable.gen_params import (GenParams,  GEN_PARAMS_FLOAT,  # noqa: E402
 AttrVals: TypeAlias = list[str]
 AttrValsByVisibility: TypeAlias = dict[str, AttrVals]
 ItemsAttrs: TypeAlias = dict[str, AttrValsByVisibility]
 AttrVals: TypeAlias = list[str]
 AttrValsByVisibility: TypeAlias = dict[str, AttrVals]
 ItemsAttrs: TypeAlias = dict[str, AttrValsByVisibility]
-Cache: TypeAlias = dict[str, dict[str, dict[str, str | float | int]]]
+CachedImg: TypeAlias = dict[str, str | float | int]
+Cache: TypeAlias = dict[str, dict[str, CachedImg]]
 Bookmarks: TypeAlias = list[str]
 Db: TypeAlias = Cache | Bookmarks
 FilterInputs: TypeAlias = dict[str, str]
 Bookmarks: TypeAlias = list[str]
 Db: TypeAlias = Cache | Bookmarks
 FilterInputs: TypeAlias = dict[str, str]
@@ -83,10 +84,10 @@ def _add_button(parent: Gtk.Widget,
 
 class JsonDb:
     """Representation of our simple .json DB files."""
 
 class JsonDb:
     """Representation of our simple .json DB files."""
+    _content: Db
 
     def __init__(self, path: str) -> None:
         self._path = path
 
     def __init__(self, path: str) -> None:
         self._path = path
-        self._content: Db = {}
         self._is_open = False
         if not path_exists(path):
             with open(path, 'w', encoding='utf8') as f:
         self._is_open = False
         if not path_exists(path):
             with open(path, 'w', encoding='utf8') as f:
@@ -111,14 +112,29 @@ class JsonDb:
             json_dump(self._content, f)
         self._close()
 
             json_dump(self._content, f)
         self._close()
 
-    def as_copy(self) -> Db:
+
+class BookmarksDb(JsonDb):
+    """Representation of Bookmarks DB files."""
+    _content: Bookmarks
+
+    def as_ref(self) -> Bookmarks:
+        """Return content at ._path as ref so that .write() stores changes."""
+        self._open()
+        return self._content
+
+    def as_copy(self) -> Bookmarks:
         """Return content at ._path for read-only purposes."""
         self._open()
         """Return content at ._path for read-only purposes."""
         self._open()
-        dict_copy = self._content.copy()
+        copy = self._content.copy()
         self._close()
         self._close()
-        return dict_copy
+        return copy
+
+
+class CacheDb(JsonDb):
+    """Representation of Cache DB files."""
+    _content: Cache
 
 
-    def as_ref(self) -> Db:
+    def as_ref(self) -> Cache:
         """Return content at ._path as ref so that .write() stores changes."""
         self._open()
         return self._content
         """Return content at ._path as ref so that .write() stores changes."""
         self._open()
         return self._content
@@ -134,8 +150,8 @@ class Application(Gtk.Application):
         parser.add_argument('-s', '--sort-order', default=SORT_DEFAULT)
         opts = parser.parse_args()
         self.img_dir_absolute = abspath(opts.directory)
         parser.add_argument('-s', '--sort-order', default=SORT_DEFAULT)
         opts = parser.parse_args()
         self.img_dir_absolute = abspath(opts.directory)
-        self.bookmarks_db = JsonDb(BOOKMARKS_PATH)
-        self.cache_db = JsonDb(CACHE_PATH)
+        self.bookmarks_db = BookmarksDb(BOOKMARKS_PATH)
+        self.cache_db = CacheDb(CACHE_PATH)
         self.sort_order = SorterAndFiltererOrder.from_suggestion(
                 opts.sort_order.split(','))
 
         self.sort_order = SorterAndFiltererOrder.from_suggestion(
                 opts.sort_order.split(','))
 
@@ -149,28 +165,27 @@ class Application(Gtk.Application):
 class SorterAndFilterer(GObject.GObject):
     """Sort order box representation of sorting/filtering attribute."""
     widget: Gtk.Box
 class SorterAndFilterer(GObject.GObject):
     """Sort order box representation of sorting/filtering attribute."""
     widget: Gtk.Box
-    label: Gtk.Label
 
     def __init__(self, name: str) -> None:
         super().__init__()
         self.name = name
 
     def setup_on_bind(self,
 
     def __init__(self, name: str) -> None:
         super().__init__()
         self.name = name
 
     def setup_on_bind(self,
-                      widget: Gtk.Widget,
+                      widget: Gtk.Box,
                       on_filter_activate: Callable,
                       filter_text: str,
                       on_filter_activate: Callable,
                       filter_text: str,
-                      vals: dict[str, str],
+                      vals: AttrValsByVisibility,
                       ) -> None:
         """Set up SorterAndFilterer label, values listing, filter entry."""
         self.widget = widget
         # label
         len_incl = len(vals['incl'])
                       ) -> None:
         """Set up SorterAndFilterer label, values listing, filter entry."""
         self.widget = widget
         # label
         len_incl = len(vals['incl'])
-        len_semi_total = len_incl + len(vals['semi'])
-        len_total = len_semi_total + len(vals['excl'])
+        len_semi_total: int = len_incl + len(vals['semi'])
+        len_total: int = len_semi_total + len(vals['excl'])
         title = f'{self.name} ({len_incl}/{len_semi_total}/{len_total}) '
         self.widget.label.set_text(title)
         # values listing
         title = f'{self.name} ({len_incl}/{len_semi_total}/{len_total}) '
         self.widget.label.set_text(title)
         # values listing
-        vals_listed = [f'<b>{v}</b>' for v in vals['incl']]
+        vals_listed: list[str] = [f'<b>{v}</b>' for v in vals['incl']]
         vals_listed += [f'<s>{v}</s>' for v in vals['semi']]
         vals_listed += [f'<b><s>{v}</s></b>' for v in vals['excl']]
         self.widget.values.set_text(', '.join(vals_listed))
         vals_listed += [f'<s>{v}</s>' for v in vals['semi']]
         vals_listed += [f'<b><s>{v}</s></b>' for v in vals['excl']]
         self.widget.values.set_text(', '.join(vals_listed))
@@ -215,12 +230,12 @@ class SorterAndFiltererOrder:
     @classmethod
     def from_suggestion(cls, suggestion: list[str]) -> Self:
         """Create new, interpreting order of strings in suggestion."""
     @classmethod
     def from_suggestion(cls, suggestion: list[str]) -> Self:
         """Create new, interpreting order of strings in suggestion."""
-        names = [p.lower() for p in GEN_PARAMS] + ['bookmarked']
-        order = []
+        names: list[str] = [p.lower() for p in GEN_PARAMS] + ['bookmarked']
+        order: list[SorterAndFilterer] = []
         for name in names:
             order += [SorterAndFilterer(name)]
         for name in names:
             order += [SorterAndFilterer(name)]
-        new_order = []
-        do_reverse = '-' in suggestion
+        new_order: list[SorterAndFilterer] = []
+        do_reverse: bool = '-' in suggestion
         for pattern in suggestion:
             for sorter in [sorter for sorter in order
                            if sorter.name.startswith(pattern)]:
         for pattern in suggestion:
             for sorter in [sorter for sorter in order
                            if sorter.name.startswith(pattern)]:
@@ -252,9 +267,9 @@ class SorterAndFiltererOrder:
 
     def switch_at(self, selected_idx: int, forward: bool) -> None:
         """Switch elements at selected_idx and its neighbor."""
 
     def switch_at(self, selected_idx: int, forward: bool) -> None:
         """Switch elements at selected_idx and its neighbor."""
-        selected = self[selected_idx]
-        other_idx = selected_idx + (1 if forward else -1)
-        other = self[other_idx]
+        selected: SorterAndFilterer = self[selected_idx]
+        other_idx: int = selected_idx + (1 if forward else -1)
+        other: SorterAndFilterer = self[other_idx]
         self._list[other_idx] = selected
         self._list[selected_idx] = other
 
         self._list[other_idx] = selected
         self._list[selected_idx] = other
 
@@ -266,11 +281,11 @@ class GalleryItem(GObject.GObject):
     def __init__(self, path: str, name: str) -> None:
         super().__init__()
         self.name = name
     def __init__(self, path: str, name: str) -> None:
         super().__init__()
         self.name = name
-        self.full_path = path_join(path, self.name)
+        self.full_path: str = path_join(path, self.name)
         self.slot: GallerySlot
 
     def __hash__(self) -> int:
         self.slot: GallerySlot
 
     def __hash__(self) -> int:
-        hashable_values = []
+        hashable_values: list[str | bool] = []
         for attr_name in self._to_hash:
             hashable_values += [getattr(self, attr_name)]
         return hash(tuple(hashable_values))
         for attr_name in self._to_hash:
             hashable_values += [getattr(self, attr_name)]
         return hash(tuple(hashable_values))
@@ -295,8 +310,8 @@ class ImgItem(GalleryItem):
         super().__init__(path, name)
         mtime = getmtime(self.full_path)
         dt = datetime.fromtimestamp(mtime, tz=timezone.utc)
         super().__init__(path, name)
         mtime = getmtime(self.full_path)
         dt = datetime.fromtimestamp(mtime, tz=timezone.utc)
-        iso8601_str = dt.isoformat(timespec='microseconds')
-        self.last_mod_time = iso8601_str.replace('+00:00', 'Z')
+        iso8601_str: str = dt.isoformat(timespec='microseconds')
+        self.last_mod_time: str = iso8601_str.replace('+00:00', 'Z')
         self.bookmarked = False
         self.with_others = False
         self.has_metadata = False
         self.bookmarked = False
         self.with_others = False
         self.has_metadata = False
@@ -321,7 +336,7 @@ class ImgItem(GalleryItem):
                 gen_params = GenParams.from_str(gen_params_as_str)
                 for k, v_ in gen_params.as_dict.items():
                     setattr(self, k, v_)
                 gen_params = GenParams.from_str(gen_params_as_str)
                 for k, v_ in gen_params.as_dict.items():
                     setattr(self, k, v_)
-        cached = {}
+        cached: CachedImg = {}
         for k in (k.lower() for k in GEN_PARAMS):
             cached[k] = getattr(self, k)
         cache[self.full_path] = {self.last_mod_time: cached}
         for k in (k.lower() for k in GEN_PARAMS):
             cached[k] = getattr(self, k)
         cache[self.full_path] = {self.last_mod_time: cached}
@@ -336,10 +351,11 @@ class GallerySlotsGeometry:
     """Collect variable sizes shared among all GallerySlots."""
 
     def __init__(self) -> None:
     """Collect variable sizes shared among all GallerySlots."""
 
     def __init__(self) -> None:
-        self._margin = GALLERY_SLOT_MARGIN
+        self._margin: int = GALLERY_SLOT_MARGIN
         assert 0 == self._margin % 2  # avoid ._margin != 2 * .side_margin
         assert 0 == self._margin % 2  # avoid ._margin != 2 * .side_margin
-        self.side_margin = self._margin // 2
-        self.size, self.size_sans_margins = -1, -1
+        self.side_margin: int = self._margin // 2
+        self.size: int = -1
+        self.size_sans_margins: int = -1
 
     def set_size(self, size: int) -> None:
         """Not only set .size but also update .size_sans_margins."""
 
     def set_size(self, size: int) -> None:
         """Not only set .size but also update .size_sans_margins."""
@@ -386,7 +402,7 @@ class GallerySlot(Gtk.Button):
 
     def update_widget(self, is_in_vp: bool) -> None:
         """(Un-)load slot, for Imgs if (not) is_in_vp, update CSS class."""
 
     def update_widget(self, is_in_vp: bool) -> None:
         """(Un-)load slot, for Imgs if (not) is_in_vp, update CSS class."""
-        new_content = None
+        new_content: Optional[Gtk.Image | Gtk.Label] = None
         if isinstance(self.item, ImgItem):
             if is_in_vp and not isinstance(self.item, Gtk.Image):
                 new_content = Gtk.Image.new_from_file(self.item.full_path)
         if isinstance(self.item, ImgItem):
             if is_in_vp and not isinstance(self.item, Gtk.Image):
                 new_content = Gtk.Image.new_from_file(self.item.full_path)
@@ -422,7 +438,6 @@ class GalleryConfig():
 
     def __init__(self,
                  box: Gtk.Box,
 
     def __init__(self,
                  box: Gtk.Box,
-                 # sort_order: list[SorterAndFilterer],
                  sort_order: SorterAndFiltererOrder,
                  request_update: Callable,
                  update_settings: Callable,
                  sort_order: SorterAndFiltererOrder,
                  request_update: Callable,
                  update_settings: Callable,
@@ -451,14 +466,14 @@ class GalleryConfig():
 
             def on_filter_activate(entry: Gtk.Box) -> None:
                 entry.remove_css_class('temp')
 
             def on_filter_activate(entry: Gtk.Box) -> None:
                 entry.remove_css_class('temp')
-                text = entry.get_buffer().get_text()
+                text: str = entry.get_buffer().get_text()
                 if '' != text.rstrip():
                     self.filter_inputs[sorter.name] = text
                 elif sorter.name in self.filter_inputs:
                     del self.filter_inputs[sorter.name]
                 self._filter_inputs_changed = True
 
                 if '' != text.rstrip():
                     self.filter_inputs[sorter.name] = text
                 elif sorter.name in self.filter_inputs:
                     del self.filter_inputs[sorter.name]
                 self._filter_inputs_changed = True
 
-            sorter = list_item.props.item
+            sorter: SorterAndFilterer = list_item.props.item
             sorter.setup_on_bind(list_item.props.child, on_filter_activate,
                                  self.filter_inputs.get(sorter.name, ''),
                                  self._gallery_items_attrs[sorter.name])
             sorter.setup_on_bind(list_item.props.child, on_filter_activate,
                                  self.filter_inputs.get(sorter.name, ''),
                                  self._gallery_items_attrs[sorter.name])
@@ -572,7 +587,7 @@ class GalleryConfig():
         self.update_box(tmp_sort_order, cur_idx + direction)
         self._sort_sel.props.selected = cur_idx + direction
         for i in range(self._store.get_n_items()):
         self.update_box(tmp_sort_order, cur_idx + direction)
         self._sort_sel.props.selected = cur_idx + direction
         for i in range(self._store.get_n_items()):
-            sort_item = self._store.get_item(i)
+            sort_item: SorterAndFilterer = self._store.get_item(i)
             sort_item.widget.add_css_class('temp')
 
     def update_box(self,
             sort_item.widget.add_css_class('temp')
 
     def update_box(self,
@@ -630,15 +645,15 @@ class Gallery:
     def __init__(self,
                  on_hit_item: Callable,
                  on_selection_change: Callable,
     def __init__(self,
                  on_hit_item: Callable,
                  on_selection_change: Callable,
-                 bookmarks_db: JsonDb,
-                 cache_db: JsonDb
+                 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 = SorterAndFiltererOrder([])
         self._filter_inputs: FilterInputs = {}
                  ) -> 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 = SorterAndFiltererOrder([])
         self._filter_inputs: FilterInputs = {}
-        self._img_dir_path = None
+        self._img_dir_path = ''
 
         self._shall_load = False
         self._shall_build = False
 
         self._shall_load = False
         self._shall_build = False
@@ -675,7 +690,7 @@ class Gallery:
         self._viewport.set_scroll_to_focus(False)  # prefer our own handling
 
         def ensure_uptodate() -> bool:
         self._viewport.set_scroll_to_focus(False)  # prefer our own handling
 
         def ensure_uptodate() -> bool:
-            if self._img_dir_path is None:
+            if not self._img_dir_path:
                 return True
             if self._shall_load:
                 self._load_directory()
                 return True
             if self._shall_load:
                 self._load_directory()
@@ -725,13 +740,18 @@ class Gallery:
 
     def _load_directory(self) -> None:
         """Rewrite .dir_entries from ._img_dir_path, trigger rebuild."""
 
     def _load_directory(self) -> None:
         """Rewrite .dir_entries from ._img_dir_path, trigger rebuild."""
+        self._shall_load = False
+        self.dir_entries.clear()
+        bookmarks = self._bookmarks_db.as_copy()
+        cache = self._cache_db.as_ref()
 
 
-        def read_directory(dir_path, make_parent=False):
+        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]
             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, to_set_metadata_on = [], []
+            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)}'
             dir_entries = list(listdir(dir_path))
             for i, filename in enumerate(dir_entries):
                 msg = f'loading {dir_path}: entry {i+1}/{len(dir_entries)}'
@@ -763,10 +783,6 @@ class Gallery:
                     print(f'{prefix}{i+1}/{len(dirs_to_enter)}')
                     read_directory(path)
 
                     print(f'{prefix}{i+1}/{len(dirs_to_enter)}')
                     read_directory(path)
 
-        self._shall_load = False
-        self.dir_entries = []
-        bookmarks = self._bookmarks_db.as_copy()
-        cache = self._cache_db.as_ref()
         read_directory(self._img_dir_path, make_parent=True)
         self._cache_db.write()
         self.request_update(build=True)
         read_directory(self._img_dir_path, make_parent=True)
         self._cache_db.write()
         self.request_update(build=True)
@@ -871,6 +887,8 @@ class Gallery:
 
     def _build(self) -> None:
         """(Re-)build slot grid from .dir_entries, filters, layout settings."""
 
     def _build(self) -> None:
         """(Re-)build slot grid from .dir_entries, filters, layout settings."""
+        self._shall_build = False
+        old_selected_item: Optional[GalleryItem] = self.selected_item
 
         def build_items_attrs() -> None:
             self.items_attrs.clear()
 
         def build_items_attrs() -> None:
             self.items_attrs.clear()
@@ -881,7 +899,7 @@ class Gallery:
                 items_attrs_tmp: ItemsAttrs = {}
                 for attr_name in (s.name for s in self._sort_order):
                     items_attrs_tmp[attr_name] = {'incl': [], 'excl': []}
                 items_attrs_tmp: ItemsAttrs = {}
                 for attr_name in (s.name for s in self._sort_order):
                     items_attrs_tmp[attr_name] = {'incl': [], 'excl': []}
-                    vals = set()
+                    vals: set[str] = set()
                     for entry in [e for e in entries
                                   if isinstance(e, ImgItem)]:
                         val = (getattr(entry, attr_name)
                     for entry in [e for e in entries
                                   if isinstance(e, ImgItem)]:
                         val = (getattr(entry, attr_name)
@@ -909,7 +927,7 @@ class Gallery:
                 self.items_attrs[attr_name] = final_values
 
         def filter_entries(items_attrs: ItemsAttrs) -> list[GalleryItem]:
                 self.items_attrs[attr_name] = final_values
 
         def filter_entries(items_attrs: ItemsAttrs) -> list[GalleryItem]:
-            entries_filtered = []
+            entries_filtered: list[GalleryItem] = []
             for entry in self.dir_entries:
                 if (not self._show_dirs) and isinstance(entry, DirItem):
                     continue
             for entry in self.dir_entries:
                 if (not self._show_dirs) and isinstance(entry, DirItem):
                     continue
@@ -927,6 +945,14 @@ class Gallery:
 
         def build_grid(entries_filtered: list[GalleryItem]) -> None:
             i_row_ref, i_slot_ref = [0], [0]
 
         def build_grid(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]],
 
             def build_rows_by_attrs(
                     remaining: list[tuple[str, AttrVals]],
@@ -966,29 +992,21 @@ class Gallery:
                     build_rows_by_attrs(remaining[1:], items_of_attr_value,
                                         ancestors + [(attr_name, attr_value)])
 
                     build_rows_by_attrs(remaining[1:], items_of_attr_value,
                                         ancestors + [(attr_name, attr_value)])
 
-            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 = []
-            self._fixed_frame.put(self._grid, 0, 0)
             if self._by_1st:
                 self._show_dirs = False
             if self._by_1st:
                 self._show_dirs = False
-                sort_attrs = []
+                sort_attrs: list[tuple[str, AttrVals]] = []
                 for sorter in reversed(self._sort_order):
                 for sorter in reversed(self._sort_order):
-                    vals = self.items_attrs[sorter.name]['incl']
+                    vals: AttrVals = self.items_attrs[sorter.name]['incl']
                     if len(vals) > 1:
                         sort_attrs += [(sorter.name, vals)]
                 if not sort_attrs:
                     if len(vals) > 1:
                         sort_attrs += [(sorter.name, vals)]
                 if not sort_attrs:
-                    s_name = self._sort_order[0].name
+                    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)
                     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 = sort_attrs[-1][0]
+                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,
                 for i, val in enumerate(sort_attrs[-1][1]):
                     label = Gtk.Label(label=f'<b>{top_attr_name}</b>: {val}',
                                       xalign=0,
@@ -997,7 +1015,7 @@ class Gallery:
                     self._col_headers_grid.attach(label, i + 1, 0, 1, 1)
 
             else:
                     self._col_headers_grid.attach(label, i + 1, 0, 1, 1)
 
             else:
-                dir_entries_filtered_sorted = sorted(
+                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):
                         entries_filtered, key=cmp_to_key(self._sort_cmp))
                 i_row, i_col = 0, 0
                 for i, item in enumerate(dir_entries_filtered_sorted):
@@ -1011,8 +1029,6 @@ class Gallery:
                     i_col += 1
             self.update_config_box()
 
                     i_col += 1
             self.update_config_box()
 
-        self._shall_build = False
-        old_selected_item = self.selected_item
         build_items_attrs()
         entries_filtered = filter_entries(self.items_attrs)
         build_grid(entries_filtered)
         build_items_attrs()
         entries_filtered = filter_entries(self.items_attrs)
         build_grid(entries_filtered)
@@ -1071,33 +1087,35 @@ class Gallery:
 
     def _redraw_and_check_focus(self) -> None:
         """Draw gallery; possibly notice and first follow need to re-focus."""
 
     def _redraw_and_check_focus(self) -> None:
         """Draw gallery; possibly notice and first follow need to re-focus."""
-        vp_width = (self._force_width if self._force_width
-                    else self._viewport.get_width())
-        vp_height = (self._force_height if self._force_height
-                     else self._viewport.get_height())
+        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
         self._force_width, self._force_height = 0, 0
-        vp_scroll = self._viewport.get_vadjustment()
-        vp_top = vp_scroll.get_value()
-        vp_bottom = vp_top + vp_height
+        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:
         side_offset, i_vlabels = 0, 0
         if self._by_1st:
             while True:
-                widget = self._grid.get_child_at(i_vlabels, 0)
-                if isinstance(widget, VerticalLabel):
-                    side_offset += widget.width
+                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
                 else:
                     break
                 i_vlabels += 1
-        max_slot_width = (vp_width - side_offset) // self._per_row
+        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:
         self._slots_geometry.set_size(min(vp_height, max_slot_width))
         if self._by_1st:
             i_widgets = 0
             while True:
-                widget = self._col_headers_grid.get_child_at(i_widgets, 0)
+                head_widget: Gtk.Box | Gtk.Label | None
+                head_widget = self._col_headers_grid.get_child_at(i_widgets, 0)
                 if 0 == i_widgets:
                 if 0 == i_widgets:
-                    widget.set_size_request(side_offset, -1)
-                elif isinstance(widget, Gtk.Label):
-                    widget.set_size_request(self._slots_geometry.size, -1)
+                    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
                 else:
                     break
                 i_widgets += 1
@@ -1119,8 +1137,8 @@ class Gallery:
                               vp_bottom: int,
                               in_vp_greedy: bool = False
                               ) -> tuple[bool, int, int]:
                               vp_bottom: int,
                               in_vp_greedy: bool = False
                               ) -> tuple[bool, int, int]:
-        slot_top = (idx // self._per_row) * self._slots_geometry.size
-        slot_bottom = slot_top + self._slots_geometry.size
+        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:
         if in_vp_greedy:
             in_vp = (slot_bottom >= vp_top and slot_top <= vp_bottom)
         else:
@@ -1132,7 +1150,7 @@ class Gallery:
                          vp_top: int,
                          vp_bottom: int
                          ) -> bool:
                          vp_top: int,
                          vp_bottom: int
                          ) -> bool:
-        scroll_to_focus = self._shall_scroll_to_focus
+        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._shall_redraw, self._shall_scroll_to_focus = False, False
         if scroll_to_focus:
             in_vp, slot_top, slot_bottom = self._position_to_viewport(
@@ -1184,76 +1202,67 @@ class Gallery:
 
 class MainWindow(Gtk.Window):
     """Image browser app top-level window."""
 
 class MainWindow(Gtk.Window):
     """Image browser app top-level window."""
-    metadata: Gtk.TextBuffer
-    prev_key: list
-    topbar: Gtk.Label
 
     def __init__(self, app: Application, **kwargs) -> None:
         super().__init__(**kwargs)
         self.app = app
 
     def __init__(self, app: Application, **kwargs) -> None:
         super().__init__(**kwargs)
         self.app = app
-
-        def init_navbar() -> Gtk.Box:
-            navbar = Gtk.Box(orientation=OR_H)
-            _add_button(navbar, 'sidebar', lambda _: self.toggle_side_box())
-            self.topbar = Gtk.Label()
-            navbar.append(self.topbar)
-            return navbar
-
-        def init_metadata_box() -> Gtk.TextView:
-            text_view = Gtk.TextView(wrap_mode=Gtk.WrapMode.WORD_CHAR,
-                                     editable=False)
-            text_view.set_size_request(300, -1)
-            self.metadata = text_view.get_buffer()
-            return text_view
-
-        def init_key_control() -> None:
-            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 = [0]
-
-        def setup_css() -> None:
-            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)
-
         self.gallery = Gallery(
                 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)
         self.gallery = Gallery(
                 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()
 
 
-        setup_css()
-        viewer = Gtk.Box(orientation=OR_V)
-        self.navbar = init_navbar()
-        viewer.append(self.navbar)
-        viewer.append(self.gallery.frame)
+        # 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 = Gtk.Notebook.new()
-        self.side_box.append_page(init_metadata_box(),
+        self.side_box.append_page(metadata_textview,
                                   Gtk.Label(label='metadata'))
                                   Gtk.Label(label='metadata'))
-        config_box = Gtk.Box(orientation=OR_V)
         self.side_box.append_page(config_box, Gtk.Label(label='config'))
         self.side_box.append_page(config_box, Gtk.Label(label='config'))
-        box_outer = Gtk.Box(orientation=OR_H)
         box_outer.append(self.side_box)
         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)
         box_outer.append(viewer)
-        self.props.child = box_outer
-        self.connect('notify::default-width', lambda _, __: self.on_resize())
-        self.connect('notify::default-height', lambda _, __: self.on_resize())
 
 
-        init_key_control()
+        # 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())
         self.connect('notify::focus-widget',
                      lambda _, __: self.on_focus_change())
-        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
+
+        # 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,
             sort_order=self.conf.order.copy(),
         GLib.idle_add(lambda: self.gallery.update_settings(
             img_dir_path=self.app.img_dir_absolute,
             sort_order=self.conf.order.copy(),
@@ -1261,7 +1270,7 @@ class MainWindow(Gtk.Window):
 
     def on_focus_change(self) -> None:
         """Handle reactions on focus changes in .gallery and .conf."""
 
     def on_focus_change(self) -> None:
         """Handle reactions on focus changes in .gallery and .conf."""
-        focused = self.get_focus()
+        focused: Optional[Gtk.Widget] = self.get_focus()
         if not focused:
             return
         if isinstance(focused, GallerySlot):
         if not focused:
             return
         if isinstance(focused, GallerySlot):
@@ -1274,8 +1283,8 @@ class MainWindow(Gtk.Window):
         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.
         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 = self.side_box.measure(OR_H, -1).natural
-            default_size = self.get_default_size()
+            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())
 
             self.gallery.on_resize(default_size[0] - side_box_width,
                                    default_size[1] - self.navbar.get_height())
 
@@ -1284,7 +1293,6 @@ class MainWindow(Gtk.Window):
         if not isinstance(self.gallery.selected_item, ImgItem):
             return
         bookmarks = self.app.bookmarks_db.as_ref()
         if not isinstance(self.gallery.selected_item, ImgItem):
             return
         bookmarks = self.app.bookmarks_db.as_ref()
-        assert isinstance(bookmarks, list)
         if self.gallery.selected_item.bookmarked:
             self.gallery.selected_item.bookmark(False)
             bookmarks.remove(self.gallery.selected_item.full_path)
         if self.gallery.selected_item.bookmarked:
             self.gallery.selected_item.bookmark(False)
             bookmarks.remove(self.gallery.selected_item.full_path)
@@ -1296,7 +1304,7 @@ class MainWindow(Gtk.Window):
 
     def hit_gallery_item(self) -> None:
         """If current file selection is directory, reload into that one."""
 
     def hit_gallery_item(self) -> None:
         """If current file selection is directory, reload into that one."""
-        selected = self.gallery.selected_item
+        selected: Optional[GalleryItem] = self.gallery.selected_item
         if isinstance(selected, DirItem):
             self.gallery.update_settings(img_dir_path=selected.full_path)
 
         if isinstance(selected, DirItem):
             self.gallery.update_settings(img_dir_path=selected.full_path)
 
@@ -1305,13 +1313,13 @@ class MainWindow(Gtk.Window):
         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.
         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 = self.side_box.measure(OR_H, -1).natural
+        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:
         self.gallery.on_resize(self.get_width() - side_box_width)
 
     def update_metadata_on_gallery_selection(self) -> None:
-        """Update .metadata about individual file, .topbar also on idx/total"""
+        """Update .metadata about individual file, and .idx_display."""
         self.metadata.set_text('')
         self.metadata.set_text('')
-        selected_item = self.gallery.selected_item
+        selected_item: Optional[GalleryItem] = self.gallery.selected_item
         display_name = '(none)'
         if selected_item:
             if isinstance(selected_item, ImgItem):
         display_name = '(none)'
         if selected_item:
             if isinstance(selected_item, ImgItem):
@@ -1326,10 +1334,10 @@ class MainWindow(Gtk.Window):
                 display_name = selected_item.full_path
         total = len([s for s in self.gallery.slots
                      if isinstance(s.item, (DirItem, ImgItem))])
                 display_name = selected_item.full_path
         total = len([s for s in self.gallery.slots
                      if isinstance(s.item, (DirItem, ImgItem))])
-        n_selected = self.gallery.selected_idx + 1
+        n_selected: int = self.gallery.selected_idx + 1
         txt = f' {n_selected} of {total} – <b>{display_name}</b>'
         txt = f' {n_selected} of {total} – <b>{display_name}</b>'
-        self.topbar.set_text(txt)
-        self.topbar.set_use_markup(True)
+        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"""
 
     def handle_keypress(self, keyval: int) -> bool:
         """Handle keys if not in Gtk.Entry, return True if key handling done"""
@@ -1348,7 +1356,7 @@ class MainWindow(Gtk.Window):
             self.gallery.move_selection(None, -1, None)
         elif Gdk.KEY_l == keyval:
             self.gallery.move_selection(+1, None, None)
             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[0]:
+        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)
             self.gallery.move_selection(None, None, -1)
         elif Gdk.KEY_w == keyval:
             self.conf.move_selection(-1)
@@ -1361,7 +1369,7 @@ class MainWindow(Gtk.Window):
         elif Gdk.KEY_b == keyval:
             self.bookmark()
         else:
         elif Gdk.KEY_b == keyval:
             self.bookmark()
         else:
-            self.prev_key[0] = keyval
+            self.prev_key_ref[0] = keyval
             return False
         return True
 
             return False
         return True