From: Christian Heller Date: Thu, 26 Sep 2024 16:10:59 +0000 (+0200) Subject: Refactor browser sorter box code. X-Git-Url: https://plomlompom.com/repos/%7B%7Bprefix%7D%7D/add_task?a=commitdiff_plain;h=a847d1e44f68c8bf87760c0d7401530d0e7c6d64;p=stable_plom Refactor browser sorter box code. --- diff --git a/browser.py b/browser.py index ecbe831..0af99a3 100755 --- a/browser.py +++ b/browser.py @@ -83,6 +83,150 @@ class Sorter(GObject.GObject): lambda a, b, c: filter_entry.add_css_class('temp')) +class Sorting(): + """Representation of sort and filtering settings.""" + _gallery_update = None + _gallery_items_attrs_full = None + _gallery_items_attrs_filtered = None + + def __init__(self, sort_order): + + def setup_sort_order_item(_, list_item): + vbox = Gtk.Box(orientation=OR_V) + hbox = Gtk.Box(orientation=OR_H) + hbox.append(Gtk.Label(hexpand=True)) + hbox.append(Gtk.Entry.new()) + hbox.get_last_child().props.placeholder_text = 'filter?' + vbox.append(hbox) + vals_listing = Gtk.Label(wrap=True, max_width_chars=35, + wrap_mode=Pango.WrapMode.WORD_CHAR) + vals_listing.hide() + vbox.append(vals_listing) + list_item.set_child(vbox) + + def bind_sort_order_item(_, list_item): + + def on_filter_enter(entry): + entry.remove_css_class('temp') + text = 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._gallery_update() + + sorter = list_item.props.item + filter_text = self.filter_inputs.get(sorter.name, '') + vals_filtered = self._gallery_items_attrs_filtered[sorter.name] + vals_full = self._gallery_items_attrs_full[sorter.name] + sorter.setup_on_bind(list_item.props.child, on_filter_enter, + filter_text, vals_filtered, vals_full) + + def select_sort_order(_a, _b, _c): + if self._last_selected: + self._last_selected.get_last_child().hide() + list_item = self.selection.props.selected_item.widget + list_item.get_parent().grab_focus() + self._last_selected = list_item + + self.order = sort_order + self.filter_inputs = {} + self._last_selected = None + self.store = Gio.ListStore(item_type=Sorter) + self.selection = Gtk.SingleSelection.new(self.store) + self.selection.connect('selection-changed', select_sort_order) + factory = Gtk.SignalListItemFactory() + factory.connect('setup', setup_sort_order_item) + factory.connect('bind', bind_sort_order_item) + self.view = Gtk.ListView(model=self.selection, factory=factory) + self.box = Gtk.Box(orientation=OR_V) + self.box.append(Gtk.Label(label='** sort order **')) + self.box.append(self.view) + self.btn_activate = Gtk.Button(label='activate') + self.btn_activate.props.sensitive = False + self.btn_activate.connect( + 'clicked', lambda _: self.activate_order()) + self.box.append(self.btn_activate) + + @classmethod + def from_suggestion(cls, suggestion_fused): + """Parse suggestion_fused for/into initial sort order to build on.""" + suggestion = suggestion_fused.split(',') + names = [p.lower() for p in GEN_PARAMS] + ['bookmarked'] + sort_order = [] + for name in names: + sort_order += [Sorter(name)] + new_sort_order = [] + do_reverse = '-' in suggestion + for pattern in suggestion: + for sorter in [sorter for sorter in sort_order + if sorter.name.startswith(pattern)]: + sort_order.remove(sorter) + new_sort_order += [sorter] + sort_order = new_sort_order + sort_order + if do_reverse: + sort_order.reverse() + return cls(sort_order) + + def bind_gallery(self, on_update, items_attrs_full, items_attrs_filtered): + """Connect to Gallery interfaces where necessary.""" + self._gallery_update = on_update + self._gallery_items_attrs_full = items_attrs_full + self._gallery_items_attrs_filtered = items_attrs_filtered + + def move_selection(self, direction): + """Move sort order selection by direction (-1 or +1).""" + min_idx, max_idx = 0, len(self.order) - 1 + cur_idx = self.selection.props.selected + if (1 == direction and cur_idx < max_idx)\ + or (-1 == direction and cur_idx > min_idx): + self.selection.props.selected = cur_idx + direction + + def move_sorter(self, direction): + """Move selected item in sort order view, ensure temporary state.""" + tmp_sort_order = [] + for i in range(self.store.get_n_items()): + tmp_sort_order += [self.store.get_item(i)] + cur_idx = self.selection.props.selected + selected = tmp_sort_order[cur_idx] + if direction == -1 and cur_idx > 0: + prev_i = cur_idx - 1 + old_prev = tmp_sort_order[prev_i] + tmp_sort_order[prev_i] = selected + tmp_sort_order[cur_idx] = old_prev + elif direction == 1 and cur_idx < (len(tmp_sort_order) - 1): + next_i = cur_idx + 1 + old_next = tmp_sort_order[next_i] + tmp_sort_order[next_i] = selected + tmp_sort_order[cur_idx] = old_next + else: # to catch movement beyond limits + return + self.update_box(tmp_sort_order, cur_idx + direction) + self.selection.props.selected = cur_idx + direction + for i in range(self.store.get_n_items()): + sort_item = self.store.get_item(i) + sort_item.widget.add_css_class('temp') + self.btn_activate.props.sensitive = True + + def update_box(self, alt_order=None, cur_selection=0): + """Rebuild .store from .order, or alt_order if provided.""" + sort_order = alt_order if alt_order else self.order + self.store.remove_all() + for sorter in sort_order: + self.store.append(sorter) + self.selection.props.selected = cur_selection + + def activate_order(self): + """Write sort order box order into .order, mark finalized.""" + self.order.clear() + for i in range(self.store.get_n_items()): + sorter = self.store.get_item(i) + sorter.widget.remove_css_class('temp') + self.order += [sorter] + self.btn_activate.props.sensitive = False + self._gallery_update() + + class GallerySlot(Gtk.Button): """Slot in Gallery representing a GalleryItem.""" @@ -340,8 +484,9 @@ class Gallery: return True def _build_items_attrs_and_filtered_entries(self): - self.items_attrs = {s.name: set() for s in self._sort_order} - self.items_attrs_filtered = {s.name: set() for s in self._sort_order} + for d in (self.items_attrs, self.items_attrs_filtered): + d.clear() + d |= {s.name: set() for s in self._sort_order} entries_filtered = [] for entry in self.dir_entries: if not self.show_dirs and isinstance(entry, DirItem): @@ -427,8 +572,9 @@ class Gallery: i_col += 1 self._on_grid_built() - def build_and_show(self, suggested_selection=None): + def build_and_show(self, preserve_selected=True): """Build gallery as sorted GallerySlots, select one, draw gallery.""" + suggested_selection = self.selected_item if preserve_selected else None entries_filtered = self._build_items_attrs_and_filtered_entries() self._build_grid(entries_filtered) self.selected_idx = 0 @@ -436,8 +582,7 @@ class Gallery: new_idx = 0 if suggested_selection is not None: for i, slot in enumerate(self.slots): - item_path = slot.item.full_path - if suggested_selection.full_path == item_path: + if suggested_selection == slot.item: new_idx = i break self._set_selection(new_idx, unselect_old=False) @@ -551,15 +696,11 @@ class Gallery: class MainWindow(Gtk.Window): """Image browser app top-level window.""" metadata: Gtk.TextBuffer - sort_store: Gtk.ListStore - sort_selection: Gtk.SingleSelection prev_key: list 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 - sort_order_last_selected: Gtk.Box def __init__(self, app, **kwargs): super().__init__(**kwargs) @@ -600,65 +741,6 @@ class MainWindow(Gtk.Window): metadata_box.append(text_view) return metadata_box - def init_sorter_and_filterer(): - - def setup_sort_order_item(_, list_item): - vbox = Gtk.Box(orientation=OR_V) - hbox = Gtk.Box(orientation=OR_H) - hbox.append(Gtk.Label(hexpand=True)) - hbox.append(Gtk.Entry.new()) - hbox.get_last_child().props.placeholder_text = 'filter?' - vbox.append(hbox) - vals_listing = Gtk.Label(wrap=True, max_width_chars=35, - wrap_mode=Pango.WrapMode.WORD_CHAR) - vals_listing.hide() - vbox.append(vals_listing) - list_item.set_child(vbox) - - def bind_sort_order_item(_, list_item): - - def on_filter_enter(entry): - entry.remove_css_class('temp') - text = 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.gallery.build_and_show() - - sorter = list_item.props.item - filter_text = self.filter_inputs.get(sorter.name, '') - vals_filtered = self.gallery.items_attrs_filtered[sorter.name] - vals_full = self.gallery.items_attrs[sorter.name] - sorter.setup_on_bind(list_item.props.child, on_filter_enter, - filter_text, vals_filtered, vals_full) - - def select_sort_order(_a, _b, _c): - if self.sort_order_last_selected: - self.sort_order_last_selected.get_last_child().hide() - list_item = self.sort_selection.props.selected_item.widget - list_item.get_parent().grab_focus() - self.sort_order_last_selected = list_item - - self.sort_order_last_selected = None - self.sort_store = Gio.ListStore(item_type=Sorter) - self.sort_selection = Gtk.SingleSelection.new(self.sort_store) - self.sort_selection.connect('selection-changed', select_sort_order) - factory = Gtk.SignalListItemFactory() - factory.connect('setup', setup_sort_order_item) - factory.connect('bind', bind_sort_order_item) - self.sort_list_view = Gtk.ListView(model=self.sort_selection, - factory=factory) - sort_box = Gtk.Box(orientation=OR_V) - sort_box.append(Gtk.Label(label='** sort order **')) - sort_box.append(self.sort_list_view) - 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.btn_activate_sort) - return sort_box - def init_key_control(): key_ctl = Gtk.EventControllerKey( propagation_phase=Gtk.PropagationPhase.CAPTURE) @@ -682,13 +764,16 @@ class MainWindow(Gtk.Window): self.get_display(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER) - self.filter_inputs = {} self.gallery = Gallery( - sort_order=self.app.sort_order, - filter_inputs=self.filter_inputs, + sort_order=self.app.sort.order, + filter_inputs=self.app.sort.filter_inputs, on_hit_item=self.hit_gallery_item, - on_grid_built=self.update_sort_order_box, + on_grid_built=self.app.sort.update_box, on_selection_change=self.update_metadata_on_gallery_selection) + self.app.sort.bind_gallery( + on_update=self.gallery.build_and_show, + items_attrs_full=self.gallery.items_attrs, + items_attrs_filtered=self.gallery.items_attrs_filtered) self.recurse_dirs = False setup_css() @@ -697,8 +782,7 @@ class MainWindow(Gtk.Window): viewer.append(self.navbar) viewer.append(self.gallery.frame) self.side_box = Gtk.Box(orientation=OR_V) - self.sort_box = init_sorter_and_filterer() - self.side_box.append(self.sort_box) + self.side_box.append(self.app.sort.box) self.side_box.append(init_metadata_box()) box_outer = Gtk.Box(orientation=OR_H) box_outer.append(self.side_box) @@ -715,11 +799,11 @@ class MainWindow(Gtk.Window): GLib.idle_add(self.gallery.build_and_show) def on_focus_change(self): - """If new focus on GallerySlot, call gallery.on_focus_slot.""" + """Handle reactions on focus changes in .gallery and .sort.""" focused = self.get_focus() if isinstance(focused, GallerySlot): self.gallery.on_focus_slot(focused) - elif focused.get_parent() == self.sort_list_view: + elif focused.get_parent() == self.app.sort.view: focused.get_first_child().get_last_child().show() def on_resize(self): @@ -746,7 +830,7 @@ class MainWindow(Gtk.Window): bookmarks += [self.gallery.selected_item.full_path] with open(BOOKMARKS_PATH, 'w', encoding='utf8') as f: json_dump(list(bookmarks), f) - self.update_sort_order_box() + self.app.sort.update_box() def hit_gallery_item(self): """If current file selection is directory, reload into that one.""" @@ -755,25 +839,6 @@ class MainWindow(Gtk.Window): self.app.img_dir_absolute = selected.full_path self.load_directory() - def update_sort_order_box(self, alt_order=None, cur_selection=0): - """Rebuild .sort_store from .sort_order, or alt_order if provided.""" - sort_order = alt_order if alt_order else self.app.sort_order - self.sort_store.remove_all() - for sorter in sort_order: - self.sort_store.append(sorter) - self.sort_selection.props.selected = cur_selection - - def activate_sort_order(self): - """Write sort order box order into .app.sort_order, mark finalized.""" - self.app.sort_order.clear() - for i in range(self.sort_store.get_n_items()): - sorter = self.sort_store.get_item(i) - sorter.widget.remove_css_class('temp') - self.app.sort_order += [sorter] - self.btn_activate_sort.props.sensitive = False - old_selection = self.gallery.selected_item - self.gallery.build_and_show(old_selection) - def load_directory(self, update_gallery_view=True): """Load .gallery.store_unfiltered from .app.img_dir_absolute path.""" @@ -822,7 +887,6 @@ class MainWindow(Gtk.Window): print(f'{prefix}{i+1}/{len(dirs_to_enter)}') read_directory_into_gallery_items(path) - old_selection = self.gallery.selected_item self.gallery.dir_entries = [] with open(BOOKMARKS_PATH, 'r', encoding='utf8') as f: bookmarks = json_load(f) @@ -832,7 +896,7 @@ class MainWindow(Gtk.Window): with open(CACHE_PATH, 'w', encoding='utf8') as f: json_dump(cache, f) if update_gallery_view: - self.gallery.build_and_show(old_selection) + self.gallery.build_and_show() def toggle_side_box(self): """Toggle window sidebox visible/invisible.""" @@ -851,7 +915,7 @@ class MainWindow(Gtk.Window): 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) + self.gallery.build_and_show() def reset_show_dirs(self, button): """By button's .active, in-/exclude directories from gallery view.""" @@ -862,47 +926,13 @@ class MainWindow(Gtk.Window): """Change by increment how many items max to display in gallery row.""" if self.gallery.per_row + increment > 0: self.gallery.per_row += increment - self.gallery.build_and_show(self.gallery.selected_item) + self.gallery.build_and_show() def reset_recurse(self, button): """By button's .active, de-/activate recursion on image collection.""" self.recurse_dirs = button.props.active self.load_directory() - def move_sort(self, direction): - """Move selected item in sort order view, ensure temporary state.""" - tmp_sort_order = [] - for i in range(self.sort_store.get_n_items()): - tmp_sort_order += [self.sort_store.get_item(i)] - cur_idx = self.sort_selection.props.selected - selected = tmp_sort_order[cur_idx] - if direction == -1 and cur_idx > 0: - prev_i = cur_idx - 1 - old_prev = tmp_sort_order[prev_i] - tmp_sort_order[prev_i] = selected - tmp_sort_order[cur_idx] = old_prev - elif direction == 1 and cur_idx < (len(tmp_sort_order) - 1): - next_i = cur_idx + 1 - old_next = tmp_sort_order[next_i] - tmp_sort_order[next_i] = selected - tmp_sort_order[cur_idx] = old_next - else: # to catch movement beyond limits - return - self.update_sort_order_box(tmp_sort_order, cur_idx + direction) - self.sort_selection.props.selected = cur_idx + direction - for i in range(self.sort_store.get_n_items()): - sort_item = self.sort_store.get_item(i) - sort_item.widget.add_css_class('temp') - self.btn_activate_sort.props.sensitive = True - - def move_selection_in_sort_order(self, direction): - """Move sort order selection by direction (-1 or +1).""" - min_idx, max_idx = 0, len(self.app.sort_order) - 1 - cur_idx = self.sort_selection.props.selected - if (1 == direction and cur_idx < max_idx)\ - or (-1 == direction and cur_idx > min_idx): - self.sort_selection.props.selected = cur_idx + direction - def update_metadata_on_gallery_selection(self): """Update .metadata about individual file, .counter on its idx/total""" self.metadata.set_text('') @@ -923,8 +953,8 @@ class MainWindow(Gtk.Window): if isinstance(self.get_focus().get_parent(), Gtk.Entry): return False if Gdk.KEY_Return == keyval: - if self.get_focus().get_parent().get_parent() == self.sort_box: - self.activate_sort_order() + if self.get_focus().get_parent().get_parent() == self.app.sort.box: + self.app.sort.activate_order() else: self.hit_gallery_item() elif Gdk.KEY_G == keyval: @@ -940,13 +970,13 @@ class MainWindow(Gtk.Window): elif Gdk.KEY_g == keyval and Gdk.KEY_g == self.prev_key[0]: self.gallery.move_selection(None, None, -1) elif Gdk.KEY_w == keyval: - self.move_selection_in_sort_order(-1) + self.app.sort.move_selection(-1) elif Gdk.KEY_W == keyval: - self.move_sort(-1) + self.app.sort.move_sorter(-1) elif Gdk.KEY_s == keyval: - self.move_selection_in_sort_order(1) + self.app.sort.move_selection(1) elif Gdk.KEY_S == keyval: - self.move_sort(1) + self.app.sort.move_sorter(1) elif Gdk.KEY_b == keyval: self.bookmark() else: @@ -957,43 +987,21 @@ class MainWindow(Gtk.Window): class Application(Gtk.Application): """Image browser application class.""" - img_dir_absolute: str - sort_order: list[Sorter] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - - def _build_sort_order(self, suggestion_fused): - """Parse suggestion_fused for/into sort order suggestion.""" - suggestion = suggestion_fused.split(',') - names = [p.lower() for p in GEN_PARAMS] + ['bookmarked'] - sort_order = [] - for name in names: - sort_order += [Sorter(name)] - new_sort_order = [] - do_reverse = '-' in suggestion - for pattern in suggestion: - for sorter in [sorter for sorter in sort_order - if sorter.name.startswith(pattern)]: - sort_order.remove(sorter) - new_sort_order += [sorter] - sort_order = new_sort_order + sort_order - if do_reverse: - sort_order.reverse() - return sort_order - - def do_activate(self, *args, **kwargs): - """Parse arguments, start window, keep it open.""" parser = ArgumentParser() parser.add_argument('directory', default=IMG_DIR_DEFAULT, nargs='?') parser.add_argument('-s', '--sort-order', default=SORT_DEFAULT) opts = parser.parse_args() self.img_dir_absolute = abspath(opts.directory) - self.sort_order = self._build_sort_order(opts.sort_order) + self.sort = Sorting.from_suggestion(opts.sort_order) + + def do_activate(self, *args, **kwargs): + """Parse arguments, start window, keep it open.""" win = MainWindow(self) win.present() self.hold() - return 0 main_app = Application(application_id='plomlompom.com.StablePixBrowser.App')