From: Christian Heller Date: Mon, 23 Sep 2024 03:54:17 +0000 (+0200) Subject: Add "by first sorter" option to tabelize gallery by attributes. X-Git-Url: https://plomlompom.com/repos/%7B%7Bprefix%7D%7D/%7B%7B%20web_path%20%7D%7D/day_todos?a=commitdiff_plain;h=906496716cc310b57e4d63217ed70012b1fd506c;p=stable_plom Add "by first sorter" option to tabelize gallery by attributes. --- diff --git a/browser.py b/browser.py index f74e0b7..e1463d6 100755 --- a/browser.py +++ b/browser.py @@ -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):