home · contact · privacy
Browser.py: Some code re-organization to prepare next steps.
authorChristian Heller <c.heller@plomlompom.de>
Sat, 9 Nov 2024 06:16:09 +0000 (07:16 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Sat, 9 Nov 2024 06:16:09 +0000 (07:16 +0100)
browser.py

index ff9973315ddfef9eedcb3b9a8aaedf808aee40df..b880b206abeddeba0c89be064ace13e4070c2e02 100755 (executable)
@@ -25,6 +25,7 @@ from stable.gen_params import (GenParams,  GEN_PARAMS_FLOAT,  # noqa: E402
                                GEN_PARAMS_INT, GEN_PARAMS_STR,  # noqa: E402
                                GEN_PARAMS)  # noqa: E402
 
+BasicItemsAttrs = dict[str, set[str]]
 AttrVals: TypeAlias = list[str]
 AttrValsByVisibility: TypeAlias = dict[str, AttrVals]
 ItemsAttrs: TypeAlias = dict[str, AttrValsByVisibility]
@@ -656,7 +657,7 @@ class Gallery:
         self._img_dir_path = ''
 
         self._shall_load = False
-        self._shall_build = False
+        self._shall_build_grid = False
         self._shall_redraw = False
         self._shall_scroll_to_focus = False
         self._shall_select = False
@@ -668,6 +669,7 @@ class Gallery:
         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] = []
@@ -693,21 +695,25 @@ class Gallery:
             if not self._img_dir_path:
                 return True
             if self._shall_load:
+                self._shall_load = False
                 self._load_directory()
-            if self._shall_build:
-                self._build()
+            if self._shall_build_grid:
+                self._shall_build_grid = False
+                self._build_grid()
             if self._shall_select:
-                self._set_selection(self.selected_idx)
+                self._shall_select = False
+                self._assert_selection()
             if self._shall_redraw:
                 wait_time_passed = datetime.now() - self._start_redraw_wait
                 if wait_time_passed > redraw_wait_time:
+                    self._shall_redraw = False
                     self._redraw_and_check_focus()
             return True
 
         def handle_scroll(_) -> None:
             self._start_redraw_wait = datetime.now()
             self._shall_scroll_to_focus = False
-            self._shall_redraw = True
+            self.request_update()  # only request redraw
 
         redraw_wait_time = timedelta(milliseconds=GALLERY_REDRAW_WAIT_MS)
         self._start_redraw_wait = datetime.now() - redraw_wait_time
@@ -734,13 +740,26 @@ class Gallery:
             if val is not None and getattr(self, attr_name) != val:
                 setattr(self, attr_name, val)
                 if attr_name in {'_recurse_dirs', '_img_dir_path'}:
-                    self._load_directory()
+                    self.request_update(load=True)
                 else:
-                    self.request_update(build=True)
+                    self.request_update(build_grid=True)
+
+    def _make_basic_items_attrs(self,
+                                entries: list[GalleryItem]
+                                ) -> BasicItemsAttrs:
+        basic_items_attrs = {}
+        for attr_name in (s.name for s in self._sort_order):
+            vals: set[str] = set()
+            for entry in [e for e in entries if isinstance(e, ImgItem)]:
+                val = (getattr(entry, attr_name)
+                       if hasattr(entry, attr_name) else None)
+                if val is not None:
+                    vals.add(val)
+            basic_items_attrs[attr_name] = vals
+        return basic_items_attrs
 
     def _load_directory(self) -> None:
-        """Rewrite .dir_entries from ._img_dir_path, trigger rebuild."""
-        self._shall_load = False
+        """(Re-)build .dir_entries from ._img_dir_path, ._basic_items_attrs."""
         self.dir_entries.clear()
         bookmarks = self._bookmarks_db.as_copy()
         cache = self._cache_db.as_ref()
@@ -784,8 +803,9 @@ class Gallery:
                     read_directory(path)
 
         read_directory(self._img_dir_path, make_parent=True)
+        self._basic_items_attrs = self._make_basic_items_attrs(
+                self.dir_entries)
         self._cache_db.write()
-        self.request_update(build=True)
 
     @property
     def selected_item(self) -> Optional[GalleryItem]:
@@ -797,124 +817,116 @@ class Gallery:
         self._set_selection(self.slots.index(slot))
         self.request_update(scroll_to_focus=True)
 
+    def _assert_selection(self) -> None:
+        if self.slots:
+            self.slots[self.selected_idx].mark('selected', True)
+            self.slots[self.selected_idx].grab_focus()
+
     def _set_selection(self, new_idx: int) -> None:
         """Set self.selected_idx, mark slot as 'selected', unmark old one."""
-        self._shall_select = False
-        # in ._build(), directly before we are called, no slot will be
+        # in ._build_grid(), directly before we are called, no slot will be
         # CSS-marked 'selected', so .mark('selected', False) would tolerably
-        # happen without effect; where called from ._build() however, an old
-        # .selected_idx might point beyond _any_ of the new .slots, the
+        # happen without effect; where called from ._build_grid() however, an
+        # old .selected_idx might point beyond _any_ of the new .slots, the
         # IndexError of which we still want to avoid
         if self.selected_idx < len(self.slots):
             self.slots[self.selected_idx].mark('selected', False)
         self.selected_idx = new_idx
-        if self.slots:
-            self.slots[self.selected_idx].mark('selected', True)
-            self.slots[self.selected_idx].grab_focus()
+        self._assert_selection()
         self._on_selection_change()
 
-    def _passes_filter(self, attr_name: str, val: str) -> bool:
-        number_attributes = (set(s.lower() for s in GEN_PARAMS_INT) |
-                             set(s.lower() for s in GEN_PARAMS_FLOAT) |
-                             {'bookmarked'})
-
-        def _passes_number_filter(attr_name, pattern, val):
-            use_float = attr_name in {s.lower() for s in GEN_PARAMS_FLOAT}
-            constraint_strings = pattern.split(',')
-            numbers_or = set()
-            unequal = set()
-            less_than = None
-            less_or_equal = None
-            more_or_equal = None
-            more_than = None
-            for constraint_string in constraint_strings:
-                toks = constraint_string.split()
-                if len(toks) == 1:
-                    tok = toks[0]
-                    if tok[0] in '<>!':
-                        if '=' == tok[1]:
-                            toks = [tok[:2], tok[2:]]
+    def _build_grid(self) -> None:
+        """(Re-)build slot grid from .dir_entries, filters, layout settings."""
+        old_selected_item: Optional[GalleryItem] = self.selected_item
+
+        def passes_filter(attr_name: str, val: str) -> bool:
+            number_attributes = (set(s.lower() for s in GEN_PARAMS_INT) |
+                                 set(s.lower() for s in GEN_PARAMS_FLOAT) |
+                                 {'bookmarked'})
+
+            def passes_number_filter(attr_name, pattern, val):
+                use_float = attr_name in {s.lower() for s in GEN_PARAMS_FLOAT}
+                constraint_strings = pattern.split(',')
+                numbers_or = set()
+                unequal = set()
+                less_than = None
+                less_or_equal = None
+                more_or_equal = None
+                more_than = None
+                for constraint_string in constraint_strings:
+                    toks = constraint_string.split()
+                    if len(toks) == 1:
+                        tok = toks[0]
+                        if tok[0] in '<>!':
+                            if '=' == tok[1]:
+                                toks = [tok[:2], tok[2:]]
+                            else:
+                                toks = [tok[:1], tok[1:]]
                         else:
-                            toks = [tok[:1], tok[1:]]
-                    else:
-                        value = float(tok) if use_float else int(tok)
-                        numbers_or.add(value)
-                if len(toks) == 2:
-                    value = float(toks[1]) if use_float else int(toks[1])
-                    if toks[0] == '!=':
-                        unequal.add(value)
-                    elif toks[0] == '<':
-                        if less_than is None or less_than >= value:
-                            less_than = value
-                    elif toks[0] == '<=':
-                        if less_or_equal is None or less_or_equal > value:
-                            less_or_equal = value
-                    elif toks[0] == '>=':
-                        if more_or_equal is None or more_or_equal < value:
-                            more_or_equal = value
-                    elif toks[0] == '>':
-                        if more_than is None or more_than <= value:
-                            more_than = value
-            if val in numbers_or:
+                            value = float(tok) if use_float else int(tok)
+                            numbers_or.add(value)
+                    if len(toks) == 2:
+                        value = float(toks[1]) if use_float else int(toks[1])
+                        if toks[0] == '!=':
+                            unequal.add(value)
+                        elif toks[0] == '<':
+                            if less_than is None or less_than >= value:
+                                less_than = value
+                        elif toks[0] == '<=':
+                            if less_or_equal is None or less_or_equal > value:
+                                less_or_equal = value
+                        elif toks[0] == '>=':
+                            if more_or_equal is None or more_or_equal < value:
+                                more_or_equal = value
+                        elif toks[0] == '>':
+                            if more_than is None or more_than <= value:
+                                more_than = value
+                if val in numbers_or:
+                    return True
+                if len(numbers_or) > 0 and (less_than == less_or_equal ==
+                                            more_or_equal == more_than):
+                    return False
+                if val in unequal:
+                    return False
+                if (less_than is not None
+                    and val >= less_than)\
+                        or (less_or_equal is not None
+                            and val > less_or_equal)\
+                        or (more_or_equal is not None
+                            and val < more_or_equal)\
+                        or (more_than is not None
+                            and val <= more_than):
+                    return False
                 return True
-            if len(numbers_or) > 0 and (less_than == less_or_equal ==
-                                        more_or_equal == more_than):
-                return False
-            if val in unequal:
+
+            if attr_name not in self._filter_inputs:
+                return True
+            if val is None:
                 return False
-            if (less_than is not None
-                and val >= less_than)\
-                    or (less_or_equal is not None
-                        and val > less_or_equal)\
-                    or (more_or_equal is not None
-                        and val < more_or_equal)\
-                    or (more_than is not None
-                        and val <= more_than):
+            pattern = self._filter_inputs[attr_name]
+            if attr_name in number_attributes:
+                if not passes_number_filter(attr_name, pattern, val):
+                    return False
+            elif not re_search(pattern, val):
                 return False
             return True
 
-        if attr_name not in self._filter_inputs:
-            return True
-        if val is None:
-            return False
-        pattern = self._filter_inputs[attr_name]
-        if attr_name in number_attributes:
-            if not _passes_number_filter(attr_name, pattern, val):
-                return False
-        elif not re_search(pattern, val):
-            return False
-        return True
-
-    def _build(self) -> None:
-        """(Re-)build slot grid from .dir_entries, filters, layout settings."""
-        self._shall_build = False
-        old_selected_item: Optional[GalleryItem] = self.selected_item
-
-        def build_items_attrs() -> None:
+        def update_items_attrs() -> None:
             self.items_attrs.clear()
 
-            def collect_and_split_attr_vals(
-                    entries: list[GalleryItem]
-                    ) -> ItemsAttrs:
-                items_attrs_tmp: ItemsAttrs = {}
-                for attr_name in (s.name for s in self._sort_order):
-                    items_attrs_tmp[attr_name] = {'incl': [], 'excl': []}
-                    vals: set[str] = set()
-                    for entry in [e for e in entries
-                                  if isinstance(e, ImgItem)]:
-                        val = (getattr(entry, attr_name)
-                               if hasattr(entry, attr_name) else None)
-                        if val is not None:
-                            vals.add(val)
+            def separate_items_attrs(basic_items_attrs) -> ItemsAttrs:
+                items_attrs: ItemsAttrs = {}
+                for attr_name, vals in basic_items_attrs.items():
+                    items_attrs[attr_name] = {'incl': [], 'excl': []}
                     for v in vals:
-                        k = ('incl' if self._passes_filter(attr_name, v)
-                             else 'excl')
-                        items_attrs_tmp[attr_name][k] += [v]
-                return items_attrs_tmp
-
-            items_attrs_tmp_1 = collect_and_split_attr_vals(self.dir_entries)
-            filtered_entries = filter_entries(items_attrs_tmp_1)
-            items_attrs_tmp_2 = collect_and_split_attr_vals(filtered_entries)
+                        k = ('incl' if passes_filter(attr_name, v) else 'excl')
+                        items_attrs[attr_name][k] += [v]
+                return items_attrs
+
+            items_attrs_tmp_1 = separate_items_attrs(self._basic_items_attrs)
+            filtered = filter_entries(items_attrs_tmp_1)
+            reduced_basic_items_attrs = self._make_basic_items_attrs(filtered)
+            items_attrs_tmp_2 = separate_items_attrs(reduced_basic_items_attrs)
             for attr_name in (s.name for s in self._sort_order):
                 final_values: AttrValsByVisibility = {'incl': [], 'semi': []}
                 final_values['excl'] = items_attrs_tmp_1[attr_name]['excl']
@@ -943,7 +955,7 @@ class Gallery:
                     entries_filtered += [entry]
             return entries_filtered
 
-        def build_grid(entries_filtered: list[GalleryItem]) -> None:
+        def build(entries_filtered: list[GalleryItem]) -> None:
             i_row_ref, i_slot_ref = [0], [0]
             if self._grid.get_parent():
                 self._fixed_frame.remove(self._grid)
@@ -964,8 +976,9 @@ class Gallery:
                 attr_name, attr_values = remaining[0]
                 if 1 == len(remaining):
                     for i, attr in enumerate(ancestors):
-                        vlabel = VerticalLabel(f'<b>{attr[0]}</b>: {attr[1]}',
-                                               self._slots_geometry)
+                        parent_attr_name, parent_attr_value = attr
+                        txt = f'<b>{parent_attr_name}</b>: {parent_attr_value}'
+                        vlabel = VerticalLabel(txt, self._slots_geometry)
                         self._grid.attach(vlabel, i, i_row_ref[0], 1, 1)
                     row: list[Optional[GalleryItem]]
                     row = [None] * len(attr_values)
@@ -1029,9 +1042,9 @@ class Gallery:
                     i_col += 1
             self.update_config_box()
 
-        build_items_attrs()
+        update_items_attrs()
         entries_filtered = filter_entries(self.items_attrs)
-        build_grid(entries_filtered)
+        build(entries_filtered)
         new_idx = 0
         if old_selected_item is not None:
             for i, slot in enumerate(self.slots):
@@ -1043,19 +1056,15 @@ class Gallery:
     def request_update(self,
                        select: bool = False,
                        scroll_to_focus: bool = False,
-                       build: bool = False,
+                       build_grid: bool = False,
                        load: bool = False
                        ) -> None:
         """Set ._shall_… to trigger updates on next relevant interval."""
         self._shall_redraw = True
-        if scroll_to_focus or build or select:
-            self._shall_select = True
-        if scroll_to_focus or build:
-            self._shall_scroll_to_focus = True
-        if build:
-            self._shall_build = True
-        if load:
-            self._shall_load = True
+        self._shall_select |= select or scroll_to_focus or build_grid or load
+        self._shall_scroll_to_focus |= scroll_to_focus or build_grid or load
+        self._shall_build_grid |= build_grid or load
+        self._shall_load |= load
 
     def move_selection(self,
                        x_inc: Optional[int],