import os.path import json from subprocess import Popen from typing import Set import gi from marxbook import Store gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk # noqa: E402 BROWSER_CMDLINE = ["firefox"] class MyWindow(Gtk.Window): def __init__(self, mxbstore: Store): super().__init__() self.builder = Gtk.Builder.new_from_file( os.path.join(os.path.dirname(__file__), "gui.ui") ) self.add(self.builder.get_object("root")) self.my_accelerators = Gtk.AccelGroup() self.add_accel_group(self.my_accelerators) self.mxbstore = mxbstore self.store_dirs = Gtk.TreeStore(str, str) # dirname, basename self.store_marks = Gtk.ListStore( str, str, str, str, str ) # title, description, tags, URL, path self.mxb_import() self.filter_marks = self.store_marks.filter_new() self.filter_marks_dir: Set[str] = set() self.filter_marks.set_visible_func(self.filter_func, data=None) self.builder.get_object("tree_marks").set_model(self.filter_marks) self.builder.get_object("tree_dirs").set_model(self.store_dirs) self.init_marks_view() self.init_dirs_view() self.connect("destroy", Gtk.main_quit) self.builder.connect_signals(self) self.builder.get_object("search").grab_focus() self._add_accelerator("space", self.on_focus_switch) self._add_accelerator("a", self.on_focus_switch) self.show_all() def init_marks_view(self): renderer = Gtk.CellRendererText() for i, col in enumerate(("Title", "Description", "Tag", "URL")): # TODO: special renderer for Tag column = Gtk.TreeViewColumn(col, renderer, text=i) column.set_expand(True) column.set_resizable(True) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) column.set_max_width(500) # XXX: determine by window size? self.builder.get_object("tree_marks").append_column(column) column = Gtk.TreeViewColumn("Path", renderer, text=3) column.set_visible(False) self.builder.get_object("tree_marks").append_column(column) targets = [("text/plain", 0, 1), ("TEXT", 0, 2)] self.builder.get_object("tree_marks").enable_model_drag_source( Gdk.ModifierType.BUTTON1_MASK, targets, Gdk.DragAction.COPY ) self.builder.get_object("tree_dirs").enable_model_drag_dest( targets, Gdk.DragAction.COPY ) def init_dirs_view(self): renderer = Gtk.CellRendererText() column = Gtk.TreeViewColumn("Folder", renderer, text=1) column.set_expand(True) column.set_resizable(True) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) column.set_max_width(300) # XXX: determine by window size? column.set_min_width(200) # XXX: determine by window size? self.builder.get_object("tree_dirs").append_column(column) column = Gtk.TreeViewColumn("Parent", renderer, text=0) column.set_visible(False) self.builder.get_object("tree_marks").append_column(column) def filter_func(self, store, treeiter, data): text = self.builder.get_object("search").get_text().lower() row = store[treeiter] title, description, tags, url, path = row dirpath = os.path.dirname(path).lower() tags = json.loads(tags) if self.filter_marks_dir: if not any(path.startswith(dir + "/") for dir in self.filter_marks_dir): return False for query in text.split(): if not any( query in part for part in ( title.lower(), description.lower(), tags, url.lower(), dirpath, ) ): return False return True def mxb_import(self): dirs = set() for bookmark in self.mxbstore: dirs.add(os.path.dirname(bookmark["Path"])) self.store_marks.append( [ bookmark["Title"], bookmark["Description"], json.dumps(bookmark["Tag"]), bookmark["Url"], bookmark["Path"], ] ) for d in sorted(dirs): parts = d.split("/") for n in range(len(parts)): dirs.add("/".join(parts[0 : n + 1])) iters = {} for d in sorted(dirs): parent, me = os.path.split(d) if not parent: parentiter = None else: parentiter = iters[parent] iters[d] = self.store_dirs.append(parentiter, [parent, me]) def _add_accelerator(self, accelerator: str, callback): """Add a keyboard shortcut.""" key, mod = Gtk.accelerator_parse(accelerator) self.my_accelerators.connect(key, mod, Gtk.AccelFlags.VISIBLE, callback) def on_search_changed(self, *args, **kwargs): self.filter_marks.refilter() def on_search_activate(self, *args, **kwargs): self.builder.get_object("tree_marks").grab_focus() def on_tree_marks_row_activated(self, view, treepath, column): row = self.filter_marks[treepath] url = row[3] cmd = BROWSER_CMDLINE + [url] Popen(cmd, preexec_fn=os.setpgrp) def tree_dirs_select_row(self, treepaths: list): rows = [self.store_dirs[path] for path in treepaths] self.filter_marks_dir = set( filter(bool, ("/".join([row[0], row[1]]).lstrip("/") for row in rows)) ) self.filter_marks.refilter() def on_tree_dirs_sel_changed(self, selection): model, treepaths = selection.get_selected_rows() if not treepaths: return self.tree_dirs_select_row(treepaths) def on_tree_dirs_row_activated(self, view, treepath, column): self.tree_dirs_select_row([treepath]) self.builder.get_object("search").grab_focus() view.get_parent().hide() def on_tree_dirs_key_press_event(self, view, eventkey): key = eventkey.keyval model, treepath = view.get_selection().get_selected_rows() if not treepath or len(treepath) > 1: return treepath = treepath[0] if key in (Gdk.KEY_Right, Gdk.KEY_space): view.expand_row(treepath, False) return if key in (Gdk.KEY_Left, Gdk.KEY_BackSpace): view.collapse_row(treepath) return def on_focus_switch(self, *args, **kwargs): tree_dirs = self.builder.get_object("tree_dirs") if not tree_dirs.has_focus(): tree_dirs.get_parent().show() tree_dirs.grab_focus() else: self.builder.get_object("search").grab_focus() tree_dirs.get_parent().hide() def on_tree_dirs_focus(self, *args): """Avoid giving focus to tree_dirs.""" return True # DND {{{ def on_tree_marks_drag_data_get(self, widget, drag_context, selection, info, time): source_widget = self.builder.get_object("tree_marks") treeselection = source_widget.get_selection() model, treepaths = treeselection.get_selected_rows() bookmarks = [] for path in treepaths: treeiter = model.get_iter(path) bookmark_path = model.get_value(treeiter, 4) bookmarks.append((bookmark_path, path.to_string())) selection.set_text(json.dumps(bookmarks, indent=1), -1) source_widget.stop_emission_by_name("drag-data-get") def on_tree_dirs_drag_data_received( self, source_widget, drag_context, x, y, selection_data, info, time ): target_widget = self.builder.get_object("tree_dirs") bookmarks = json.loads(selection_data.get_text()) if not bookmarks: return model, treepaths = target_widget.get_selection().get_selected_rows() drop_info = target_widget.get_dest_row_at_pos(x, y) if drop_info: path, position = drop_info treeiter = model.get_iter(path) parent = model.get_value(treeiter, 0) folder = model.get_value(treeiter, 1) if position == Gtk.TreeViewDropPosition.BEFORE: new_folder = parent else: new_folder = os.path.join(parent, folder) for fspath, treepath in bookmarks: self.mxbstore.move(fspath, new_folder) new_path = os.path.join(new_folder, os.path.basename(fspath)) treeiter = self.filter_marks.get_iter(treepath) self.filter_marks.set_value(treeiter, 4, new_path) self.filter_marks.refilter() target_widget.stop_emission_by_name("drag-data-received") # }}} def main(): s = Store() w = MyWindow(s) # noqa: F841 # w.show_all() Gtk.main() if __name__ == "__main__": main()