home · contact · privacy
Add "by first sorter" option to tabelize gallery by attributes.
authorChristian Heller <c.heller@plomlompom.de>
Mon, 23 Sep 2024 03:54:17 +0000 (05:54 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Mon, 23 Sep 2024 03:54:17 +0000 (05:54 +0200)
browser.py

index f74e0b7908a683ba77344f4e65051ec83a2ce8e0..e1463d689aa2c6e47671221f89e8144552ea9443 100755 (executable)
@@ -66,8 +66,57 @@ class Sorter(GObject.GObject):
         self.list_item.get_first_child().set_text(label)
 
 
-class FileItem(GObject.GObject):
+class GallerySlot(Gtk.Button):
+    """Slot in Gallery representing a GalleryItem."""
+
+    def __init__(self, item, on_click_file=None):
+        super().__init__()
+        self.add_css_class('slot')
+        self.set_hexpand(True)
+        self.item = item
+        self.item.slot = self
+        if on_click_file:
+            self.connect('clicked', on_click_file)
+
+    def mark(self, css_class, do_add=True):
+        """Add or remove css_class from self."""
+        if do_add:
+            self.add_css_class(css_class)
+        else:
+            self.remove_css_class(css_class)
+
+    def update_widget(self, slot_size, margin, is_in_vp):
+        """(Un-)Load content if (not) is_in_vp, update geometry, CSS classes"""
+        new_content = 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 self.item.with_others:
+                    new_content.set_vexpand(True)
+                    box = Gtk.Box(orientation=OR_V)
+                    box.append(new_content)
+                    msg = 'and one or more other images of this configuration'
+                    box.append(Gtk.Label(label=msg))
+                    new_content = box
+            elif (not is_in_vp) and not isinstance(self.item, Gtk.Label):
+                new_content = Gtk.Label(label='?')
+        elif self.get_child() is None:
+            label = self.item.name if isinstance(self.item, DirItem) else '+'
+            new_content = Gtk.Label(label=label)
+        if new_content:
+            self.set_child(new_content)
+            side_margin = margin // 2
+            if side_margin:
+                for s in ('bottom', 'top', 'start', 'end'):
+                    setattr(self.get_child().props, f'margin_{s}', side_margin)
+        self.get_child().set_size_request(slot_size, slot_size)
+        if isinstance(self.item, ImgItem):
+            self.mark('bookmarked', self.item.bookmarked)
+
+
+class GalleryItem(GObject.GObject):
     """Gallery representation of filesystem entry, base to DirItem, ImgItem."""
+    slot: GallerySlot
 
     def __init__(self, path, name):
         super().__init__()
@@ -75,7 +124,7 @@ class FileItem(GObject.GObject):
         self.full_path = path_join(path, self.name)
 
 
-class DirItem(FileItem):
+class DirItem(GalleryItem):
     """Gallery representation of filesystem entry for directory."""
 
     def __init__(self, path, name, is_parent=False):
@@ -84,13 +133,14 @@ class DirItem(FileItem):
             self.full_path = path
 
 
-class ImgItem(FileItem):
+class ImgItem(GalleryItem):
     """Gallery representation of filesystem entry for image file."""
 
     def __init__(self, path, name, last_mod_time, cache):
         super().__init__(path, name)
         self.last_mod_time = last_mod_time
         self.bookmarked = False
+        self.with_others = False
         for param_name in GEN_PARAMS:
             if param_name in GEN_PARAMS_STR:
                 setattr(self, param_name.lower(), '')
@@ -122,47 +172,8 @@ class ImgItem(FileItem):
         self.slot.mark('bookmarked', positive)
 
 
-class GallerySlot(Gtk.Button):
-    """Slot in Gallery representing a FileItem."""
-
-    def __init__(self, item, on_click_file):
-        super().__init__()
-        self.add_css_class('slot')
-        self.set_hexpand(True)
-        self.item = item
-        self.item.slot = self
-        self.connect('clicked', on_click_file)
-
-    def mark(self, css_class, do_add=True):
-        """Add or remove css_class from self."""
-        if do_add:
-            self.add_css_class(css_class)
-        else:
-            self.remove_css_class(css_class)
-
-    def update_widget(self, slot_size, margin, is_in_vp):
-        """(Un-)Load content if (not) is_in_vp, update geometry, CSS classes"""
-        new_content = None
-        if self.get_child() is None and isinstance(self.item, DirItem):
-            new_content = Gtk.Label(label=self.item.name)
-        elif 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)
-            elif (not is_in_vp) and not isinstance(self.item, Gtk.Label):
-                new_content = Gtk.Label(label='?')
-        if new_content:
-            self.set_child(new_content)
-            side_margin = margin // 2
-            if side_margin:
-                for s in ('bottom', 'top', 'start', 'end'):
-                    setattr(self.get_child().props, f'margin_{s}', side_margin)
-        self.get_child().set_size_request(slot_size, slot_size)
-        if isinstance(self.item, ImgItem):
-            self.mark('bookmarked', self.item.bookmarked)
-
-
 class Gallery:
-    """Representation of FileItems below a directory."""
+    """Representation of GalleryItems below a directory."""
 
     def __init__(self,
                  sort_order,
@@ -177,12 +188,14 @@ class Gallery:
         self._on_selection_change = on_selection_change
         self.show_dirs = False
 
+        self.per_row_by_first_sorter = False
         self.per_row = GALLERY_PER_ROW_DEFAULT
         self._slot_margin = GALLERY_SLOT_MARGIN
         self._grid = None
         self._force_width, self._force_height = 0, 0
         self.slots = None
         self.dir_entries = []
+        self.dir_entries_filtered_sorted = []
         self.selected_idx = 0
 
         self._fixed_frame = Gtk.Fixed(hexpand=True, vexpand=True)
@@ -225,8 +238,9 @@ class Gallery:
         if unselect_old:
             self.slots[self.selected_idx].mark('selected', False)
         self.selected_idx = new_idx
-        self.slots[self.selected_idx].mark('selected', True)
-        self.slots[self.selected_idx].grab_focus()
+        if self.slots:
+            self.slots[self.selected_idx].mark('selected', True)
+            self.slots[self.selected_idx].grab_focus()
         self._on_selection_change()
 
     def build_and_show(self, suggested_selection=None):
@@ -238,22 +252,68 @@ class Gallery:
                 self._on_hit_item()
             return f
 
+        def build_rows_by_attrs(remaining_attrs, items_of_parent_attr_value):
+            if not items_of_parent_attr_value:
+                return
+            attr_name, attr_values = remaining_attrs[0]
+            if 1 == len(remaining_attrs):
+                row = [None] * len(attr_values)
+                for item in items_of_parent_attr_value:
+                    item_val = getattr(item, attr_name)
+                    idx_item_val_in_attr_values = attr_values.index(item_val)
+                    if row[idx_item_val_in_attr_values]:
+                        item.with_others = True
+                    row[idx_item_val_in_attr_values] = item
+                for i_col, item in enumerate(row):
+                    if item:
+                        slot = GallerySlot(item, item_clicker(i_slot_ref[0]))
+                    else:
+                        slot = GallerySlot(GalleryItem('', ''))  # dummy
+                    self.slots += [slot]
+                    i_slot_ref[0] += 1
+                    self._grid.attach(slot, i_col, 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_attr_value
+                                       if attr_value == getattr(x, attr_name)]
+                build_rows_by_attrs(remaining_attrs[1:], items_of_attr_value)
+
         if self._grid:
             self._fixed_frame.remove(self._grid)
         self.slots = []
         self._grid = Gtk.Grid()
         self._fixed_frame.put(self._grid, 0, 0)
-        i_row, i_col = 0, 0
-        for i, item in enumerate(sorted([entry for entry in self.dir_entries
-                                         if self._filter_func(entry)],
-                                        key=cmp_to_key(self._sort_cmp))):
-            if self.per_row == i_col:
-                i_col = 0
-                i_row += 1
-            slot = GallerySlot(item, item_clicker(i))
-            self._grid.attach(slot, i_col, i_row, 1, 1)
-            self.slots += [slot]
-            i_col += 1
+        entries_filtered = [entry for entry in self.dir_entries
+                            if self._filter_func(entry)]
+        if self.per_row_by_first_sorter:
+            self.show_dirs = False
+            sort_attrs = []
+            for sorter in reversed(self._sort_order):
+                values = set()
+                for item in [x for x in entries_filtered
+                             if isinstance(x, ImgItem)]:
+                    val = None
+                    if hasattr(item, sorter.name):
+                        val = getattr(item, sorter.name)
+                        values.add(val)
+                sort_attrs += [(sorter.name, sorted(list(values)))]
+            i_row_ref = [0]
+            i_slot_ref = [0]
+            self.per_row = len(sort_attrs[-1][1])
+            build_rows_by_attrs(sort_attrs, entries_filtered)
+        else:
+            self.dir_entries_filtered_sorted = sorted(
+                    entries_filtered, key=cmp_to_key(self._sort_cmp))
+            i_row, i_col = 0, 0
+            for i, item in enumerate(self.dir_entries_filtered_sorted):
+                if self.per_row == i_col:
+                    i_col = 0
+                    i_row += 1
+                slot = GallerySlot(item, item_clicker(i))
+                self._grid.attach(slot, i_col, i_row, 1, 1)
+                self.slots += [slot]
+                i_col += 1
         self._on_grid_built()
         self.selected_idx = 0
         self._update_view()
@@ -330,7 +390,7 @@ class Gallery:
         """Return how many diff. values for sort_attr in (un-)filtered store"""
         diversities = [0, 0]
         for i, store in enumerate([self.dir_entries,
-                                   [s.item for s in self.slots]]):
+                                   self.dir_entries_filtered_sorted]):
             values = set()
             for item in store:
                 if isinstance(item, ImgItem):
@@ -354,7 +414,7 @@ class Gallery:
                 return -1
             if isinstance(b, DirItem) and not isinstance(a, DirItem):
                 return +1
-        # apply ._sort_order within DirItems and FileItems (separately)
+        # apply ._sort_order within DirItems and ImgItems (separately)
         ret = 0
         for key in [sorter.name for sorter in self._sort_order]:
             a_cmp = None
@@ -455,32 +515,39 @@ class MainWindow(Gtk.Window):
     sort_store: Gtk.ListStore
     sort_selection: Gtk.SingleSelection
     prev_key: list
-    button_activate_sort: Gtk.Button
     counter: Gtk.Label
+    btn_activate_sort: Gtk.Button
+    btn_dec_per_row: Gtk.Button
+    btn_inc_per_row: Gtk.Button
+    btn_show_dirs: Gtk.Button
 
     def __init__(self, app, **kwargs):
         super().__init__(**kwargs)
         self.app = app
 
         def init_navbar():
-            def add_button(label_, on_click, parent_box):
-                btn = Gtk.Button(label=label_)
-                btn.connect('clicked', on_click)
-                parent_box.append(btn)
+
+            def add_button(label_, on_click, checkbox=False):
+                btn = (Gtk.CheckButton(label=label_) if checkbox
+                       else Gtk.Button(label=label_))
+                btn.connect('toggled' if checkbox else 'clicked', on_click)
+                navbar.append(btn)
+                return btn
+
             navbar = Gtk.Box(orientation=OR_H)
             self.counter = Gtk.Label()
             navbar.append(self.counter)
-            add_button('sidebar', lambda _: self.toggle_side_box(), navbar)
-            add_button('reload', lambda _: self.load_directory(), navbar)
+            add_button('sidebar', lambda _: self.toggle_side_box())
+            add_button('reload', lambda _: self.load_directory())
+            self.btn_show_dirs = add_button('show directories',
+                                            self.reset_show_dirs, True)
+            add_button('recurse directories ', self.reset_recurse, True)
             navbar.append(Gtk.Label(label=' per row: '))
-            add_button('less', lambda _: self.inc_per_row(-1), navbar)
-            add_button('more', lambda _: self.inc_per_row(+1), navbar)
-            btn = Gtk.CheckButton(label='show directories')
-            btn.connect('toggled', self.reset_show_dirs)
-            navbar.append(btn)
-            btn = Gtk.CheckButton(label='recurse directories')
-            btn.connect('toggled', self.reset_recurse)
-            navbar.append(btn)
+            add_button('by first sorter', self.reset_row_by_first_sorter, True)
+            self.btn_dec_per_row = add_button('less',
+                                              lambda _: self.inc_per_row(-1))
+            self.btn_inc_per_row = add_button('more',
+                                              lambda _: self.inc_per_row(+1))
             return navbar
 
         def init_metadata_box():
@@ -542,11 +609,11 @@ class MainWindow(Gtk.Window):
             sort_box = Gtk.Box(orientation=OR_V)
             sort_box.append(Gtk.Label(label='** sort order **'))
             sort_box.append(selector)
-            self.button_activate_sort = Gtk.Button(label='activate')
-            self.button_activate_sort.props.sensitive = False
-            self.button_activate_sort.connect(
+            self.btn_activate_sort = Gtk.Button(label='activate')
+            self.btn_activate_sort.props.sensitive = False
+            self.btn_activate_sort.connect(
                     'clicked', lambda _: self.activate_sort_order())
-            sort_box.append(self.button_activate_sort)
+            sort_box.append(self.btn_activate_sort)
             return sort_box
 
         def init_key_control():
@@ -658,7 +725,7 @@ class MainWindow(Gtk.Window):
             sorter = self.sort_store.get_item(i)
             sorter.list_item.remove_css_class('temp')
             self.app.sort_order += [sorter]
-        self.button_activate_sort.props.sensitive = False
+        self.btn_activate_sort.props.sensitive = False
         old_selection = self.gallery.selected_item
         self.gallery.build_and_show(old_selection)
 
@@ -715,6 +782,17 @@ class MainWindow(Gtk.Window):
         side_box_width = self.side_box.measure(OR_H, -1).natural
         self.gallery.on_resize(self.get_width() - side_box_width)
 
+    def reset_row_by_first_sorter(self, button):
+        """By button's .active, (un-)mirror top sorter in each gallery row."""
+        self.gallery.per_row_by_first_sorter = button.props.active
+        self.btn_inc_per_row.set_sensitive(not button.props.active)
+        self.btn_dec_per_row.set_sensitive(not button.props.active)
+        self.btn_show_dirs.set_sensitive(not button.props.active)
+        if button.props.active:
+            self.btn_show_dirs.set_active(False)
+        self.btn_show_dirs.set_sensitive(not button.props.active)
+        self.gallery.build_and_show(self.gallery.selected_item)
+
     def reset_show_dirs(self, button):
         """By button's .active, in-/exclude directories from gallery view."""
         self.gallery.show_dirs = button.props.active
@@ -755,7 +833,7 @@ class MainWindow(Gtk.Window):
         for i in range(self.sort_store.get_n_items()):
             sort_item = self.sort_store.get_item(i)
             sort_item.list_item.add_css_class('temp')
-        self.button_activate_sort.props.sensitive = True
+        self.btn_activate_sort.props.sensitive = True
 
     def move_selection_in_sort_order(self, direction):
         """Move sort order selection by direction (-1 or +1)."""
@@ -777,7 +855,7 @@ class MainWindow(Gtk.Window):
                 bookmarked = 'BOOKMARK' if selected_item.bookmarked else ''
                 self.metadata.set_text(
                         '\n'.join([title, bookmarked] + params_strs))
-        total = len(self.gallery.slots)
+        total = len(self.gallery.dir_entries_filtered_sorted)
         self.counter.set_text(f' {self.gallery.selected_idx + 1} of {total} ')
 
     def handle_keypress(self, keyval):