from os import listdir
from os.path import (exists as path_exists, join as path_join, abspath, isdir,
splitext, getmtime)
-from datetime import datetime, timezone
+from datetime import datetime, timezone, timedelta
from argparse import ArgumentParser
+from math import ceil
from PIL import Image
from PIL.PngImagePlugin import PngImageFile
import gi # type: ignore
BOOKMARKS_PATH = 'bookmarks.json'
GALLERY_SLOT_MARGIN = 6
GALLERY_PER_ROW_DEFAULT = 5
-GALLERY_ENSURE_UPDATED_VIEW_INTERVAL_MS = 500
+GALLERY_UPDATE_INTERVAL_MS = 50
+GALLERY_REDRAW_WAIT_MS = 200
+ACCEPTED_IMG_FILE_ENDINGS = {'.png', '.PNG'}
OR_H = Gtk.Orientation.HORIZONTAL
OR_V = Gtk.Orientation.VERTICAL
return btn
-class Sorter(GObject.GObject):
- """Sort order box representation of sorting attribute."""
+class JsonDB:
+ """Representation of our simple .json DB files."""
+
+ def __init__(self, path):
+ self._path = path
+ self._content = {}
+ self._is_open = False
+ if not path_exists(path):
+ with open(path, 'w', encoding='utf8') as f:
+ json_dump({}, f)
+
+ def _open(self):
+ if self._is_open:
+ raise Exception('DB already open')
+ with open(self._path, 'r', encoding='utf8') as f:
+ self._content = json_load(f)
+ self._is_open = True
+
+ def _close(self):
+ self._is_open = False
+ self._content = {}
+
+ def write(self):
+ """Write to ._path what's in ._content."""
+ if not self._is_open:
+ raise Exception('DB not open')
+ with open(self._path, 'w', encoding='utf8') as f:
+ json_dump(self._content, f)
+ self._close()
+
+ def as_dict_copy(self):
+ """Return content at ._path for read-only purposes."""
+ self._open()
+ dict_copy = self._content.copy()
+ self._close()
+ return dict_copy
+
+ def as_dict_ref(self):
+ """Return content at ._path as ref so that .write() stores changes."""
+ self._open()
+ return self._content
+
+
+class SorterAndFilterer(GObject.GObject):
+ """Sort order box representation of sorting/filtering attribute."""
widget: Gtk.Box
+ label: Gtk.Label
def __init__(self, name):
super().__init__()
self.name = name
- def setup_on_bind(self, widget, on_filter_enter, filter_text,
- vals_filtered, vals_full):
- """Set up all the sting only available on factory item bind."""
+ def setup_on_bind(self, widget, on_filter_activate, filter_text, vals):
+ """Set up SorterAndFilterer label, values listing, filter entry."""
self.widget = widget
- title = f'{self.name} ({len(vals_filtered)}/{len(vals_full)}) '
- widget.get_first_child().get_first_child().set_text(title)
- vals_listed = [f'<b>{v}</b>' if v in vals_filtered else f'<s>{v}</s>'
- for v in vals_full]
- widget.get_last_child().set_text(', '.join(vals_listed))
- widget.get_last_child().set_use_markup(True)
- filter_entry = widget.get_first_child().get_last_child()
- filter_buffer = filter_entry.get_buffer()
+ # label
+ len_incl = len(vals['incl'])
+ title = f'{self.name} ({len_incl}/{len_incl + len(vals["excl"])}) '
+ self.widget.label.set_text(title)
+ # values listing
+ vals_listed = [f'<b>{v}</b>' for v in vals['incl']]
+ vals_listed += [f'<s>{v}</s>' for v in vals['excl']]
+ self.widget.values.set_text(', '.join(vals_listed))
+ self.widget.values.set_use_markup(True)
+ # filter input
+ filter_buffer = self.widget.filter.get_buffer()
filter_buffer.set_text(filter_text, -1)
- filter_entry.connect('activate', on_filter_enter)
+ self.widget.filter.connect('activate', on_filter_activate)
filter_buffer.connect(
'inserted_text',
- lambda a, b, c, d: filter_entry.add_css_class('temp'))
+ lambda a, b, c, d: self.widget.filter.add_css_class('temp'))
filter_buffer.connect(
'deleted_text',
- lambda a, b, c: filter_entry.add_css_class('temp'))
+ lambda a, b, c: self.widget.filter.add_css_class('temp'))
-class TableConfig():
+class GalleryConfig():
"""Representation of sort and filtering settings."""
- _gallery_update = None
- _gallery_items_attrs_full = None
- _gallery_items_attrs_filtered = None
- _gallery_set_by_1st_sorter = None
+ _gallery_request_update = None
+ _gallery_items_attrs = None
+ _gallery_update_settings = None
def __init__(self, sort_order):
- def setup_sort_order_item(_, list_item):
- vbox = Gtk.Box(orientation=OR_V)
+ def setup_sorter_list_item(_, list_item):
+ item_widget = Gtk.Box(orientation=OR_V)
+ item_widget.values = Gtk.Label(
+ visible=False, max_width_chars=35,
+ wrap=True, wrap_mode=Pango.WrapMode.WORD_CHAR)
+ item_widget.label = Gtk.Label(hexpand=True)
+ item_widget.filter = Gtk.Entry(placeholder_text='filter?')
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):
+ hbox.append(item_widget.label)
+ hbox.append(item_widget.filter)
+ item_widget.append(hbox)
+ item_widget.append(item_widget.values)
+ list_item.set_child(item_widget)
+
+ def bind_sorter_list_item(_, list_item):
+
+ def on_filter_activate(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()
+ self._filter_inputs_changed = True
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)
+ sorter.setup_on_bind(list_item.props.child, on_filter_activate,
+ self.filter_inputs.get(sorter.name, ''),
+ self._gallery_items_attrs[sorter.name])
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._sort_sel.props.selected_item.widget.get_parent().grab_focus()
+
+ def toggle_recurse(_):
+ self._set_recurse_changed = not self._set_recurse_changed
+ self._btn_apply.set_sensitive(not self._set_recurse_changed)
- def on_by_1st_toggle(btn):
+ def toggle_by_1st(btn):
self._btn_per_row.set_sensitive(not btn.props.active)
self._btn_show_dirs.set_sensitive(not btn.props.active)
if btn.props.active:
self._btn_show_dirs.set_active(False)
- self._gallery_set_by_1st_sorter(btn.props.active)
+
+ def apply_config():
+ new_order = []
+ for i in range(self._store.get_n_items()):
+ sorter = self._store.get_item(i)
+ sorter.widget.remove_css_class('temp')
+ new_order += [sorter]
+ if self.order != new_order:
+ self.order.clear()
+ self.order += new_order[:]
+ self._gallery_update_settings(
+ per_row=self._btn_per_row.get_value_as_int(),
+ by_1st=self._btn_by_1st.get_active(),
+ show_dirs=self._btn_show_dirs.get_active(),
+ sort_order=self.order[:],
+ filter_inputs=self.filter_inputs.copy(),
+ recurse_dirs=self._btn_recurse.get_active())
+ self._gallery_request_update(select=True)
+ self._set_recurse_changed = False
+ self._filter_inputs_changed = False
+
+ def full_reload():
+ apply_config()
+ self._gallery_request_update(load=True)
+ self._btn_apply.set_sensitive(True)
self.order = sort_order
self.filter_inputs = {}
+ self._filter_inputs_changed = False
+ self._set_recurse_changed = False
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._store = Gio.ListStore(item_type=SorterAndFilterer)
+ self._sort_sel = Gtk.SingleSelection.new(self._store)
+ self._sort_sel.connect('selection-changed', select_sort_order)
+ fac = Gtk.SignalListItemFactory()
+ fac.connect('setup', setup_sorter_list_item)
+ fac.connect('bind', bind_sorter_list_item)
+ self.sorter_listing = Gtk.ListView(model=self._sort_sel, factory=fac)
+
+ buttons_box = Gtk.Box(orientation=OR_H)
+ self._btn_apply = _add_button(buttons_box, 'apply config',
+ lambda _: apply_config())
+ self._btn_relaod = _add_button(buttons_box, 'full reload',
+ lambda _: full_reload())
dirs_box = Gtk.Box(orientation=OR_H)
dirs_box.append(Gtk.Label(label='directories:'))
self._btn_show_dirs = _add_button(dirs_box, 'show', checkbox=True)
- self._btn_recurse = _add_button(dirs_box, 'recurse', checkbox=True)
- self._btn_reload_dir = _add_button(dirs_box, 'reload')
+ self._btn_recurse = _add_button(dirs_box, 'recurse',
+ toggle_recurse, checkbox=True)
per_row_box = Gtk.Box(orientation=OR_H)
per_row_box.append(Gtk.Label(label='cols/row:'))
- _add_button(per_row_box, 'by 1st sorter', on_by_1st_toggle, True)
- self._btn_per_row = Gtk.SpinButton.new_with_range(1, 9, 1)
+ self._btn_by_1st = _add_button(per_row_box, 'by 1st sorter',
+ toggle_by_1st, checkbox=True)
+ self._btn_per_row = Gtk.SpinButton.new_with_range(
+ GALLERY_PER_ROW_DEFAULT, 9, 1)
per_row_box.append(self._btn_per_row)
self.box = Gtk.Box(orientation=OR_V)
- title = Gtk.Label(label='<b>table config</b>', use_markup=True)
- self.box.append(title)
+ self.box.append(Gtk.Label(label='<b>table config</b>', use_markup=1))
+ self.box.append(buttons_box)
+ self.box.append(self.sorter_listing)
self.box.append(dirs_box)
- self.box.append(self.view)
self.box.append(per_row_box)
@classmethod
names = [p.lower() for p in GEN_PARAMS] + ['bookmarked']
sort_order = []
for name in names:
- sort_order += [Sorter(name)]
+ sort_order += [SorterAndFilterer(name)]
new_sort_order = []
do_reverse = '-' in suggestion
for pattern in suggestion:
sort_order.reverse()
return cls(sort_order)
- def bind_gallery(self, on_update, update_per_row, per_row_initial, reload,
- toggle_showdirs, toggle_recurse, set_by_1st_sorter,
- items_attrs_full, items_attrs_filtered):
+ def bind_gallery(self, request_update, update_settings, items_attrs):
"""Connect to Gallery interfaces where necessary."""
- self._gallery_update = on_update
- self._btn_show_dirs.connect('toggled', toggle_showdirs)
- self._btn_recurse.connect('toggled', toggle_recurse)
- self._btn_reload_dir.connect('clicked', reload)
- self._gallery_set_by_1st_sorter = set_by_1st_sorter
- self._gallery_items_attrs_full = items_attrs_full
- self._gallery_items_attrs_filtered = items_attrs_filtered
- self._btn_per_row.set_value(per_row_initial)
- self._btn_per_row.connect(
- 'value-changed',
- lambda btn: update_per_row(btn.get_value_as_int()))
+ self._gallery_request_update = request_update
+ self._gallery_update_settings = update_settings
+ self._gallery_items_attrs = items_attrs
+
+ def on_focus_sorter(self, focused):
+ """If sorter focused, select focused, move display of values there."""
+ if self._last_selected:
+ self._last_selected.values.set_visible(False)
+ self._last_selected = focused.get_first_child()
+ self._last_selected.values.set_visible(True)
+ for i in range(self._sort_sel.get_n_items()):
+ if self._sort_sel.get_item(i).widget == self._last_selected:
+ self._sort_sel.props.selected = i
+ break
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
+ cur_idx = self._sort_sel.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
+ self._sort_sel.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
+ cur_idx = self._sort_sel.props.selected
selected = tmp_sort_order[cur_idx]
if direction == -1 and cur_idx > 0:
prev_i = cur_idx - 1
else: # to catch movement beyond limits
return
self.update_box(tmp_sort_order, cur_idx + direction)
- self._selection.props.selected = cur_idx + direction
+ self._sort_sel.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')
def update_box(self, alt_order=None, cur_selection=0):
- """Rebuild .store from .order, or alt_order if provided."""
+ """Rebuild sorter listing in box from .order, or alt_order if set."""
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._gallery_update()
+ self._sort_sel.props.selected = cur_selection
class GallerySlot(Gtk.Button):
self.item.slot = self
if on_click_file:
self.connect('clicked', on_click_file)
+ self._slot_size = None
+ self._side_margin = None
def mark(self, css_class, do_add=True):
"""Add or remove css_class from self."""
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"""
+ def ensure_slot_size(self, slot_size, margin):
+ """Call ._size_widget to size .props.child; if none, make empty one."""
+ self._slot_size = slot_size
+ self._side_margin = margin // 2
+ if self.get_child() is None:
+ self.set_child(Gtk.Label(label='+'))
+ self._size_widget()
+
+ def _size_widget(self):
+ for s in ('bottom', 'top', 'start', 'end'):
+ setattr(self.get_child().props, f'margin_{s}', self._side_margin)
+ self.get_child().set_size_request(self._slot_size, self._slot_size)
+
+ def update_widget(self, is_in_vp):
+ """(Un-)load slot, for Imgs if (not) is_in_vp, update CSS class."""
new_content = None
if isinstance(self.item, ImgItem):
if is_in_vp and not isinstance(self.item, Gtk.Image):
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)
+ elif (isinstance(self.item, DirItem)
+ and self.get_child().props.label == '+'):
+ new_content = Gtk.Label(label=self.item.name)
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)
+ self._size_widget()
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
+ _to_hash = ['name', 'full_path']
def __init__(self, path, name):
super().__init__()
self.name = name
self.full_path = path_join(path, self.name)
+ def __hash__(self):
+ hashable_values = []
+ for attr_name in self._to_hash:
+ hashable_values += [getattr(self, attr_name)]
+ return hash(tuple(hashable_values))
+
class DirItem(GalleryItem):
"""Gallery representation of filesystem entry for directory."""
class ImgItem(GalleryItem):
"""Gallery representation of filesystem entry for image file."""
+ _to_hash = (['name', 'full_path', 'last_mod_time', 'bookmarked',
+ 'with_others']
+ + [k.lower() for k in GEN_PARAMS])
- def __init__(self, path, name, last_mod_time, cache):
+ def __init__(self, path, name, cache):
super().__init__(path, name)
- self.last_mod_time = last_mod_time
+ mtime = getmtime(self.full_path)
+ dt = datetime.fromtimestamp(mtime, tz=timezone.utc)
+ iso8601_str = dt.isoformat(timespec='microseconds')
+ self.last_mod_time = iso8601_str.replace('+00:00', 'Z')
self.bookmarked = False
self.with_others = False
+ self.has_metadata = False
for param_name in GEN_PARAMS:
if param_name in GEN_PARAMS_STR:
setattr(self, param_name.lower(), '')
setattr(self, param_name.lower(), 0)
if self.full_path in cache:
if self.last_mod_time in cache[self.full_path]:
+ self.has_metadata = True
cached = cache[self.full_path][self.last_mod_time]
for k in cached.keys():
setattr(self, k, cached[k])
class Gallery:
"""Representation of GalleryItems below a directory."""
- def __init__(self,
- sort_order,
- filter_inputs,
- on_hit_item,
- on_grid_built,
- on_selection_change):
- self._sort_order = sort_order
- self._filter_inputs = filter_inputs
+ def __init__(self, on_hit_item, on_grid_built, on_selection_change,
+ bookmarks_db, cache_db):
self._on_hit_item = on_hit_item
self._on_grid_built = on_grid_built
self._on_selection_change = on_selection_change
- self.show_dirs = False
-
- self.per_row_by_1st_sorter = False
+ self._bookmarks_db, self._cache_db = bookmarks_db, cache_db
+ self._sort_order = []
+ self._filter_inputs = {}
+ self._img_dir_path = None
+
+ self._shall_load = False
+ self._shall_build = False
+ self._shall_redraw = False
+ self._shall_scroll_to_focus = False
+ self._shall_select = False
+
+ self._show_dirs = False
+ self._recurse_dirs = False
+ self._by_1st = 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.items_attrs = {}
- self.items_attrs_filtered = {}
+ self.selected_idx = 0
+ self.slots = None
- self._fixed_frame = Gtk.Fixed(hexpand=True, vexpand=True)
+ self._grid = None
+ self._force_width, self._force_height = 0, 0
scroller = Gtk.ScrolledWindow(propagate_natural_height=True)
- self.frame = Gtk.Box(orientation=OR_V)
self._col_headers_frame = Gtk.Fixed()
self._col_headers_grid = None
+ self.frame = Gtk.Box(orientation=OR_V)
self.frame.append(self._col_headers_frame)
self.frame.append(scroller)
- scroller.get_vadjustment().connect(
- 'value-changed', lambda _: self._update_view(refocus=False))
# We want our viewport at always maximum possible size (so we can
# safely calculate what's in it and what not), even if the gallery
# would be smaller. Therefore we frame the gallery in an expanding
# Fixed, to stretch out the viewport even if the gallery is small.
+ self._fixed_frame = Gtk.Fixed(hexpand=True, vexpand=True)
scroller.set_child(self._fixed_frame)
self._viewport = self._fixed_frame.get_parent()
+ self._viewport.set_scroll_to_focus(False) # prefer our own handling
- self._should_update_view = True
- GLib.timeout_add(GALLERY_ENSURE_UPDATED_VIEW_INTERVAL_MS,
- self._ensure_updated_view)
-
- def _ensure_updated_view(self):
- """Rather than reload slots every scroll pixel, regularly run this."""
- if self._should_update_view and self.slots:
- self._update_view(refocus=False, force=True)
- return True
-
- def get_per_row(self):
- """Wrapper to ._per_row to (for setting) discourage direct access."""
- return self._per_row
+ def ensure_uptodate():
+ if self._img_dir_path is None:
+ return True
+ if self._shall_load:
+ self._load_directory()
+ if self._shall_build:
+ self._build()
+ if self._shall_select:
+ self._set_selection(self.selected_idx)
+ if self._shall_redraw:
+ wait_time_passed = datetime.now() - self._start_redraw_wait
+ if wait_time_passed > redraw_wait_time:
+ self._redraw_and_check_focus()
+ return True
- def update_per_row(self, val):
- """Wrapper to setting ._per_row to include call to .build_and_show."""
- self._per_row = val
- self.build_and_show()
+ def handle_scroll(_):
+ self._start_redraw_wait = datetime.now()
+ self._shall_scroll_to_focus = False
+ self._shall_redraw = True
+
+ redraw_wait_time = timedelta(milliseconds=GALLERY_REDRAW_WAIT_MS)
+ self._start_redraw_wait = datetime.now() - redraw_wait_time
+ scroller.get_vadjustment().connect('value-changed', handle_scroll)
+ GLib.timeout_add(GALLERY_UPDATE_INTERVAL_MS, ensure_uptodate)
+
+ def update_settings(self, per_row=None, by_1st=None, show_dirs=None,
+ recurse_dirs=None, img_dir_path=None, sort_order=None,
+ filter_inputs=None):
+ """Set Gallery setup fields, request appropriate updates."""
+ for val, attr_name in [(per_row, '_per_row'),
+ (by_1st, '_by_1st'),
+ (show_dirs, '_show_dirs'),
+ (recurse_dirs, '_recurse_dirs'),
+ (img_dir_path, '_img_dir_path'),
+ (sort_order, '_sort_order'),
+ (filter_inputs, '_filter_inputs')]:
+ 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()
+ else:
+ self.request_update(build=True)
+
+ def _load_directory(self):
+ """Rewrite .dir_entries from ._img_dir_path, trigger rebuild."""
+
+ def read_directory(dir_path, make_parent=False):
+ if make_parent:
+ parent_dir = DirItem(abspath(path_join(dir_path, UPPER_DIR)),
+ UPPER_DIR, is_parent=True)
+ self.dir_entries += [parent_dir]
+ dirs_to_enter, to_set_metadata_on = [], []
+ dir_entries = list(listdir(dir_path))
+ for i, filename in enumerate(dir_entries):
+ msg = f'loading {dir_path}: entry {i+1}/{len(dir_entries)}'
+ print(msg, end='\r')
+ full_path = path_join(dir_path, filename)
+ if isdir(full_path):
+ self.dir_entries += [DirItem(dir_path, filename)]
+ dirs_to_enter += [full_path]
+ continue
+ _, ext = splitext(filename)
+ if ext not in ACCEPTED_IMG_FILE_ENDINGS:
+ continue
+ img_item = ImgItem(dir_path, filename, cache)
+ if img_item.full_path in bookmarks:
+ img_item.bookmarked = True
+ if not img_item.has_metadata:
+ to_set_metadata_on += [img_item]
+ self.dir_entries += [img_item]
+ print('')
+ for i, item in enumerate(to_set_metadata_on):
+ msg = f'setting metadata: {i+1}/{len(to_set_metadata_on)}'
+ print(msg, end='\r')
+ item.set_metadata(cache)
+ msg = '' if to_set_metadata_on else '(no metadata to set)'
+ print(msg)
+ if dirs_to_enter and self._recurse_dirs:
+ prefix = f'entering directories below {dir_path}: directory '
+ for i, path in enumerate(dirs_to_enter):
+ print(f'{prefix}{i+1}/{len(dirs_to_enter)}')
+ read_directory(path)
- def set_by_1st_sorter(self, val):
- """On .per_row_by_1st_sorter update, also call .build_and_show."""
- self.per_row_by_1st_sorter = val
- self.build_and_show()
+ self._shall_load = False
+ self.dir_entries = []
+ bookmarks = self._bookmarks_db.as_dict_copy()
+ cache = self._cache_db.as_dict_ref()
+ read_directory(self._img_dir_path, make_parent=True)
+ self._cache_db.write()
+ self.request_update(build=True)
@property
def selected_item(self):
- """Return slot.item at self.selected_idx."""
+ """Return slot.item for slot at self.selected_idx."""
return self.slots[self.selected_idx].item if self.slots else None
- @property
- def _viewport_height(self):
- return self._force_height if self._force_height\
- else self._viewport.get_height()
-
def on_focus_slot(self, slot):
"""If GallerySlot focused, set .selected_idx to it."""
self._set_selection(self.slots.index(slot))
+ self.request_update(scroll_to_focus=True)
- def _set_selection(self, new_idx, unselect_old=True):
+ def _set_selection(self, new_idx):
"""Set self.selected_idx, mark slot as 'selected', unmark old one."""
- if unselect_old:
+ self._shall_select = False
+ # in ._build(), 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
+ # 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:
return False
return True
- def _build_items_attrs_and_filtered_entries(self):
- 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):
- continue
- passes_filters = True
- for attr_name in [s.name for s in self._sort_order]:
- if isinstance(entry, ImgItem):
+ def _build(self):
+ """(Re-)build slot grid from .dir_entries, filters, layout settings."""
+
+ def build_items_attrs():
+ self.items_attrs.clear()
+ self.items_attrs |= {s.name: {'incl': [], 'excl': []}
+ for s in self._sort_order}
+ for attr_name in (s.name for s in self._sort_order):
+ vals = set()
+ for entry in [e for e in self.dir_entries
+ if isinstance(e, ImgItem)]:
val = (getattr(entry, attr_name)
if hasattr(entry, attr_name) else None)
- self.items_attrs[attr_name].add(val)
- passes_filter = self._passes_filter(attr_name, val)
- passes_filters = passes_filters and passes_filter
- if passes_filter:
- self.items_attrs_filtered[attr_name].add(val)
- if passes_filters:
- entries_filtered += [entry]
- for d in (self.items_attrs, self.items_attrs_filtered):
- for k, v in d.items():
- d[k] = sorted(list(v))
- return entries_filtered
-
- def _build_grid(self, entries_filtered):
-
- def item_clicker(idx):
- def f(_):
- self._set_selection(idx)
- 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
+ if val is not None:
+ vals.add(val)
+ for v in vals:
+ k = 'incl' if self._passes_filter(attr_name, v) else 'excl'
+ self.items_attrs[attr_name][k] += [v]
+ for attr_vals in self.items_attrs.values():
+ attr_vals['incl'].sort()
+ attr_vals['excl'].sort()
+
+ def filter_entries():
+ entries_filtered = []
+ for entry in self.dir_entries:
+ if (not self._show_dirs) and isinstance(entry, DirItem):
+ continue
+ passes_filters = True
+ for attr_name in (s.name for s in self._sort_order):
+ if isinstance(entry, ImgItem):
+ val = (getattr(entry, attr_name)
+ if hasattr(entry, attr_name) else None)
+ if val in self.items_attrs[attr_name]['excl']:
+ passes_filters = False
+ break
+ if passes_filters:
+ entries_filtered += [entry]
+ return entries_filtered
+
+ def build_grid(entries_filtered):
+ i_row_ref, i_slot_ref = [0], [0]
+
+ def item_clicker(idx):
+ def f(_):
+ self._set_selection(idx)
+ 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:
+ val = getattr(item, attr_name)
+ idx_val_in_attr_values = attr_values.index(val)
+ if row[idx_val_in_attr_values]:
+ item.with_others = True
+ row[idx_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)
+ if self._by_1st:
+ self._show_dirs = False
+ sort_attrs = []
+ for sorter in reversed(self._sort_order):
+ sort_attrs += [(sorter.name,
+ self.items_attrs[sorter.name]['incl'])]
+ self._per_row = len(sort_attrs[-1][1])
+ build_rows_by_attrs(sort_attrs, entries_filtered)
+ else:
+ 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(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_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)
- if self.per_row_by_1st_sorter:
- self.show_dirs = False
- sort_attrs = []
- for sorter in reversed(self._sort_order):
- sort_attrs += [(sorter.name,
- self.items_attrs_filtered[sorter.name])]
- 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()
-
- 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
- self._update_view()
+ i_col += 1
+ self._on_grid_built()
+
+ self._shall_build = False
+ old_selected_item = self.selected_item
+ build_items_attrs()
+ entries_filtered = filter_entries()
+ build_grid(entries_filtered)
new_idx = 0
- if suggested_selection is not None:
+ if old_selected_item is not None:
for i, slot in enumerate(self.slots):
- if suggested_selection == slot.item:
+ if hash(old_selected_item) == hash(slot.item):
new_idx = i
break
- self._set_selection(new_idx, unselect_old=False)
+ self._set_selection(new_idx)
+
+ def request_update(self, select=False, scroll_to_focus=False, build=False,
+ load=False):
+ """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
def move_selection(self, x_inc, y_inc, buf_end):
"""Move .selection, update its dependencies, redraw gallery."""
return
self._set_selection(new_idx)
- def on_resize(self, width, height=None):
- """Re-set ._forced_width, ._forced_height, then call ._update_view."""
- self._force_width = width
- if height is not None:
- self._force_height = height
- self._update_view()
+ def on_resize(self, width=0, height=0):
+ """Force redraw and scroll-to-focus into new geometry."""
+ self._force_width, self._force_height = width, height
+ self.request_update(scroll_to_focus=True)
- def _update_view(self, refocus=True, force=False):
- """Update gallery slots based on if they're in viewport."""
- self._should_update_view = True
- vp_scroll = self._viewport.get_vadjustment()
- vp_top = vp_scroll.get_value()
- if (not force) and vp_top % 1 > 0:
- return
- vp_bottom = vp_top + self._viewport_height
+ def _redraw_and_check_focus(self):
+ """Draw gallery; possibly notice and first follow need to re-focus."""
vp_width = (self._force_width if self._force_width
else self._viewport.get_width())
- max_slot_width = (vp_width // self._per_row) - self._slot_margin
- prefered_slot_height = self._viewport_height - self._slot_margin
- slot_size = min(prefered_slot_height, max_slot_width)
- for idx, slot in enumerate(self.slots):
- slot_top = (idx // self._per_row) * (slot_size + self._slot_margin)
- slot_bottom = slot_top + slot_size
- in_vp = (slot_bottom >= vp_top and slot_top <= vp_bottom)
- slot.update_widget(slot_size, self._slot_margin, in_vp)
+ vp_height = (self._force_height if self._force_height
+ else self._viewport.get_height())
+ self._force_width, self._force_height = 0, 0
+ vp_scroll = self._viewport.get_vadjustment()
+ vp_top = vp_scroll.get_value()
+ vp_bottom = vp_top + vp_height
+ max_slot_width = vp_width // self._per_row
+ slot_size = min(vp_height, max_slot_width)
if self._col_headers_grid:
self._col_headers_frame.remove(self._col_headers_grid)
self._col_headers_grid = None
- if self.per_row_by_1st_sorter:
+ if self._by_1st:
self._col_headers_grid = Gtk.Grid()
self._col_headers_frame.put(self._col_headers_grid, 0, 0)
- attr_values = self.items_attrs_filtered[self._sort_order[0].name]
+ attr_values = self.items_attrs[self._sort_order[0].name]['incl']
for i, val in enumerate(attr_values):
- label = Gtk.Label(label=str(val))
+ label = Gtk.Label(label=str(val),
+ ellipsize=Pango.EllipsizeMode.MIDDLE)
label.set_size_request(slot_size, -1)
self._col_headers_grid.attach(label, i, 0, 1, 1)
- self._should_update_view = False
- if (not refocus) or (not self.slots):
+ slot_size_sans_margin = slot_size - self._slot_margin
+ for idx, slot in enumerate(self.slots):
+ slot.ensure_slot_size(slot_size_sans_margin, self._slot_margin)
+ vp_scroll.set_upper(slot_size * ceil(len(self.slots) / self._per_row))
+ if self._scroll_to_focus(slot_size, vp_scroll, vp_top, vp_bottom):
return
- focused_idx = self.selected_idx
- full_slot_height = slot_size + self._slot_margin
- focused_slot_top = (focused_idx // self._per_row) * full_slot_height
- focused_slot_bottom = focused_slot_top + slot_size
- if focused_slot_top < vp_top:
- vp_scroll.set_value(focused_slot_top)
- elif focused_slot_bottom > vp_bottom:
- vp_scroll.set_value(focused_slot_bottom - self._viewport_height)
+ for idx, slot in enumerate(self.slots):
+ in_vp, _, _ = self._position_to_viewport(
+ idx, slot_size, vp_top, vp_bottom, True)
+ slot.update_widget(in_vp)
+ self._start_redraw_wait = datetime.now()
+
+ def _position_to_viewport(
+ self, idx, slot_size, vp_top, vp_bottom, in_vp_greedy=False):
+ slot_top = (idx // self._per_row) * slot_size
+ slot_bottom = slot_top + slot_size
+ if in_vp_greedy:
+ in_vp = (slot_bottom >= vp_top and slot_top <= vp_bottom)
else:
- return
- self._should_update_view = True
- vp_scroll.emit('value-changed')
+ in_vp = (slot_top >= vp_top and slot_bottom <= vp_bottom)
+ return in_vp, slot_top, slot_bottom
+
+ def _scroll_to_focus(self, slot_size, vp_scroll, vp_top, vp_bottom):
+ scroll_to_focus = self._shall_scroll_to_focus
+ self._shall_redraw, self._shall_scroll_to_focus = False, False
+ if scroll_to_focus:
+ in_vp, slot_top, slot_bottom = self._position_to_viewport(
+ self.selected_idx, slot_size, vp_top, vp_bottom)
+ if not in_vp:
+ self._shall_redraw, self._shall_scroll_to_focus = True, True
+ if slot_top < vp_top:
+ vp_scroll.set_value(slot_top)
+ else:
+ vp_scroll.set_value(slot_bottom - slot_size)
+ return True
+ return False
def _sort_cmp(self, a, b):
"""Sort [a, b] by user-set sort order, and putting directories first"""
# ensure ".." and all DirItems at start of order
- if self.show_dirs:
+ if self._show_dirs:
cmp_upper_dir = f' {UPPER_DIR}'
if isinstance(a, DirItem) and a.name == cmp_upper_dir:
return -1
self.app = app
def init_navbar():
-
navbar = Gtk.Box(orientation=OR_H)
self.counter = Gtk.Label()
navbar.append(self.counter)
self.add_controller(key_ctl)
self.prev_key = [0]
- def ensure_db_files():
- if not path_exists(CACHE_PATH):
- with open(CACHE_PATH, 'w', encoding='utf8') as f:
- json_dump({}, f)
- if not path_exists(BOOKMARKS_PATH):
- with open(BOOKMARKS_PATH, 'w', encoding='utf8') as f:
- json_dump([], f)
-
def setup_css():
css_provider = Gtk.CssProvider()
css_provider.load_from_data(CSS)
Gtk.STYLE_PROVIDER_PRIORITY_USER)
self.gallery = Gallery(
- sort_order=self.app.sort.order,
- filter_inputs=self.app.sort.filter_inputs,
on_hit_item=self.hit_gallery_item,
- on_grid_built=self.app.sort.update_box,
- on_selection_change=self.update_metadata_on_gallery_selection)
- self.recurse_dirs = False
+ on_grid_built=self.app.conf.update_box,
+ on_selection_change=self.update_metadata_on_gallery_selection,
+ bookmarks_db=self.app.bookmarks_db,
+ cache_db=self.app.cache_db)
setup_css()
viewer = Gtk.Box(orientation=OR_V)
viewer.append(self.navbar)
viewer.append(self.gallery.frame)
self.side_box = Gtk.Box(orientation=OR_V)
- self.side_box.append(self.app.sort.box)
+ self.side_box.append(self.app.conf.box)
self.side_box.append(init_metadata_box())
box_outer = Gtk.Box(orientation=OR_H)
box_outer.append(self.side_box)
self.connect('notify::default-width', lambda _, __: self.on_resize())
self.connect('notify::default-height', lambda _, __: self.on_resize())
- ensure_db_files()
init_key_control()
- self.load_directory(update_gallery_view=False)
self.connect('notify::focus-widget',
lambda _, __: self.on_focus_change())
- self.app.sort.bind_gallery(
- on_update=self.gallery.build_and_show,
- reload=lambda _: self.load_directory(),
- update_per_row=self.gallery.update_per_row,
- per_row_initial=self.gallery.get_per_row(),
- toggle_showdirs=self.reset_show_dirs,
- toggle_recurse=self.reset_recurse,
- set_by_1st_sorter=self.gallery.set_by_1st_sorter,
- items_attrs_full=self.gallery.items_attrs,
- items_attrs_filtered=self.gallery.items_attrs_filtered)
- GLib.idle_add(self.gallery.build_and_show)
+ self.app.conf.bind_gallery(
+ request_update=self.gallery.request_update,
+ update_settings=self.gallery.update_settings,
+ items_attrs=self.gallery.items_attrs)
+ GLib.idle_add(lambda: self.gallery.update_settings(
+ img_dir_path=self.app.img_dir_absolute,
+ sort_order=self.app.conf.order[:],
+ filter_inputs=self.app.conf.filter_inputs.copy()))
def on_focus_change(self):
- """Handle reactions on focus changes in .gallery and .sort."""
+ """Handle reactions on focus changes in .gallery and .conf."""
focused = self.get_focus()
if not focused:
return
if isinstance(focused, GallerySlot):
self.gallery.on_focus_slot(focused)
- elif focused.get_parent() == self.app.sort.view:
- focused.get_first_child().get_last_child().show()
+ elif focused.get_parent() == self.app.conf.sorter_listing:
+ self.app.conf.on_focus_sorter(focused)
def on_resize(self):
"""On window resize, do .gallery.on_resize towards its new geometry."""
"""Toggle bookmark on selected gallery item."""
if not isinstance(self.gallery.selected_item, ImgItem):
return
- with open(BOOKMARKS_PATH, 'r', encoding='utf8') as f:
- bookmarks = json_load(f)
+ bookmarks = self.app.bookmarks_db.as_dict_ref()
if self.gallery.selected_item.bookmarked:
self.gallery.selected_item.bookmark(False)
bookmarks.remove(self.gallery.selected_item.full_path)
else:
self.gallery.selected_item.bookmark(True)
bookmarks += [self.gallery.selected_item.full_path]
- with open(BOOKMARKS_PATH, 'w', encoding='utf8') as f:
- json_dump(list(bookmarks), f)
- self.app.sort.update_box()
+ self.app.bookmarks_db.write()
+ self.app.conf.update_box()
def hit_gallery_item(self):
"""If current file selection is directory, reload into that one."""
selected = self.gallery.selected_item
if isinstance(selected, DirItem):
- self.app.img_dir_absolute = selected.full_path
- self.load_directory()
-
- def load_directory(self, update_gallery_view=True):
- """Load .gallery.store_unfiltered from .app.img_dir_absolute path."""
-
- def read_directory_into_gallery_items(dir_path, make_parent=False):
- if make_parent and self.gallery.show_dirs:
- parent_dir = DirItem(abspath(path_join(dir_path, UPPER_DIR)),
- UPPER_DIR, is_parent=True)
- self.gallery.dir_entries += [parent_dir]
- to_set_metadata_on = []
- dir_entries = list(listdir(dir_path))
- dirs_to_enter = []
- for i, fn in enumerate(dir_entries):
- msg = f'loading {dir_path}: entry {i+1}/{len(dir_entries)}'
- print(msg, end='\r')
- full_path = path_join(dir_path, fn)
- if isdir(full_path):
- if self.gallery.show_dirs:
- self.gallery.dir_entries += [DirItem(dir_path, fn)]
- dirs_to_enter += [full_path]
- continue
- _, ext = splitext(fn)
- if ext not in {'.png', '.PNG'}:
- continue
- mtime = getmtime(full_path)
- dt = datetime.fromtimestamp(mtime, tz=timezone.utc)
- iso8601_str = dt.isoformat(
- timespec='microseconds').replace('+00:00', 'Z')
- item = ImgItem(dir_path, fn, iso8601_str, cache)
- if item.full_path in bookmarks:
- item.bookmarked = True
- if '' == item.model:
- to_set_metadata_on += [item]
- self.gallery.dir_entries += [item]
- print('')
- if to_set_metadata_on:
- for i, item in enumerate(to_set_metadata_on):
- msg = f'setting metadata: {i+1}/{len(to_set_metadata_on)}'
- print(msg, end='\r')
- item.set_metadata(cache)
- print('')
- else:
- print('no metadata to set')
- if dirs_to_enter and self.recurse_dirs:
- prefix = f'entering directories below {dir_path}: directory '
- for i, path in enumerate(dirs_to_enter):
- print(f'{prefix}{i+1}/{len(dirs_to_enter)}')
- read_directory_into_gallery_items(path)
-
- self.gallery.dir_entries = []
- with open(BOOKMARKS_PATH, 'r', encoding='utf8') as f:
- bookmarks = json_load(f)
- with open(CACHE_PATH, 'r', encoding='utf8') as f:
- cache = json_load(f)
- read_directory_into_gallery_items(self.app.img_dir_absolute, True)
- with open(CACHE_PATH, 'w', encoding='utf8') as f:
- json_dump(cache, f)
- if update_gallery_view:
- self.gallery.build_and_show()
+ self.gallery.update_settings(img_dir_path=selected.full_path)
def toggle_side_box(self):
"""Toggle window sidebox visible/invisible."""
side_box_width = self.side_box.measure(OR_H, -1).natural
self.gallery.on_resize(self.get_width() - side_box_width)
- def reset_show_dirs(self, button):
- """By button's .active, in-/exclude directories from gallery view."""
- self.gallery.show_dirs = button.props.active
- self.load_directory()
-
- 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 update_metadata_on_gallery_selection(self):
"""Update .metadata about individual file, .counter on its idx/total"""
self.metadata.set_text('')
bookmarked = 'BOOKMARK' if selected_item.bookmarked else ''
self.metadata.set_text(
'\n'.join([title, bookmarked] + params_strs))
- total = len(self.gallery.dir_entries_filtered_sorted)
+ total = len([s for s in self.gallery.slots
+ if isinstance(s.item, (DirItem, ImgItem))])
self.counter.set_text(f' {self.gallery.selected_idx + 1} of {total} ')
def handle_keypress(self, keyval):
"""Handle keys if not in Gtk.Entry, return True if key handling done"""
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.app.sort.box:
- self.app.sort.activate_order()
- else:
- self.hit_gallery_item()
+ if Gdk.KEY_Return == keyval and isinstance(self.get_focus(),
+ GallerySlot):
+ self.hit_gallery_item()
elif Gdk.KEY_G == keyval:
self.gallery.move_selection(None, None, 1)
elif Gdk.KEY_h == 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.app.sort.move_selection(-1)
+ self.app.conf.move_selection(-1)
elif Gdk.KEY_W == keyval:
- self.app.sort.move_sorter(-1)
+ self.app.conf.move_sorter(-1)
elif Gdk.KEY_s == keyval:
- self.app.sort.move_selection(1)
+ self.app.conf.move_selection(1)
elif Gdk.KEY_S == keyval:
- self.app.sort.move_sorter(1)
+ self.app.conf.move_sorter(1)
elif Gdk.KEY_b == keyval:
self.bookmark()
else:
parser.add_argument('-s', '--sort-order', default=SORT_DEFAULT)
opts = parser.parse_args()
self.img_dir_absolute = abspath(opts.directory)
- self.sort = TableConfig.from_suggestion(opts.sort_order)
+ self.conf = GalleryConfig.from_suggestion(opts.sort_order)
+ self.bookmarks_db = JsonDB(BOOKMARKS_PATH)
+ self.cache_db = JsonDB(CACHE_PATH)
def do_activate(self, *args, **kwargs):
"""Parse arguments, start window, keep it open."""