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."""
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):
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
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)
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)
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)
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()
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)
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):
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."""
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."""
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)
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."""
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."""
"""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('')
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:
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:
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')