Browse Source

dnd: can move marks in directories

boyska 3 years ago
parent
commit
c5999cac00
3 changed files with 87 additions and 15 deletions
  1. 67 14
      luxembook/gui.py
  2. 9 1
      luxembook/gui.ui
  3. 11 0
      marxbook/store.py

+ 67 - 14
luxembook/gui.py

@@ -1,16 +1,18 @@
 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, GObject, Gdk
+from gi.repository import Gtk, Gdk  # noqa: E402
 
 BROWSER_CMDLINE = ["firefox"]
 
+
 class MyWindow(Gtk.Window):
     def __init__(self, mxbstore: Store):
         super().__init__()
@@ -27,7 +29,7 @@ class MyWindow(Gtk.Window):
         )  # title, description, tags, URL, path
         self.mxb_import()
         self.filter_marks = self.store_marks.filter_new()
-        self.filter_marks_dir = None
+        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)
@@ -55,6 +57,13 @@ class MyWindow(Gtk.Window):
         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()
@@ -122,33 +131,37 @@ class MyWindow(Gtk.Window):
             iters[d] = self.store_dirs.append(parentiter, [parent, me])
 
     def _add_accelerator(self, accelerator: str, callback):
-        """Adds a keyboard shortcut"""
+        """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]
-        print(url)  # XXX: open firefox
         cmd = BROWSER_CMDLINE + [url]
         Popen(cmd, preexec_fn=os.setpgrp)
 
-    def tree_dirs_select_row(self, treepath):
-        row = self.store_dirs[treepath]
-        self.filter_marks_dir = "/".join([row[0], row[1]]).lstrip("/")
+    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[0])
+        self.tree_dirs_select_row(treepaths)
 
     def on_tree_dirs_row_activated(self, view, treepath, column):
-        self.tree_dirs_select_row(treepath)
+        self.tree_dirs_select_row([treepath])
         self.builder.get_object("search").grab_focus()
         view.get_parent().hide()
 
@@ -175,17 +188,57 @@ class MyWindow(Gtk.Window):
             tree_dirs.get_parent().hide()
 
     def on_tree_dirs_focus(self, *args):
-        """
-        avoid giving focus to tree_dirs
-        """
+        """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)
-    w.show_all()
+    w = MyWindow(s)  # noqa: F841
+    # w.show_all()
     Gtk.main()
 
+
 if __name__ == "__main__":
     main()

+ 9 - 1
luxembook/gui.ui

@@ -14,11 +14,14 @@
           <object class="GtkTreeView" id="tree_dirs">
             <property name="visible">True</property>
             <property name="can_focus">True</property>
+            <property name="rubber_banding">True</property>
+            <signal name="drag-data-received" handler="on_tree_dirs_drag_data_received" swapped="no"/>
             <signal name="focus" handler="on_tree_dirs_focus" swapped="no"/>
             <signal name="key-press-event" handler="on_tree_dirs_key_press_event" swapped="no"/>
             <signal name="row-activated" handler="on_tree_dirs_row_activated" swapped="no"/>
             <child internal-child="selection">
               <object class="GtkTreeSelection" id="tree_dirs_sel">
+                <property name="mode">multiple</property>
                 <signal name="changed" handler="on_tree_dirs_sel_changed" swapped="no"/>
               </object>
             </child>
@@ -49,9 +52,13 @@
                 <property name="vexpand">False</property>
                 <property name="reorderable">True</property>
                 <property name="enable_search">False</property>
+                <property name="rubber_banding">True</property>
+                <signal name="drag-data-get" handler="on_tree_marks_drag_data_get" swapped="no"/>
                 <signal name="row-activated" handler="on_tree_marks_row_activated" swapped="no"/>
                 <child internal-child="selection">
-                  <object class="GtkTreeSelection"/>
+                  <object class="GtkTreeSelection">
+                    <property name="mode">multiple</property>
+                  </object>
                 </child>
               </object>
             </child>
@@ -70,6 +77,7 @@
             <property name="primary_icon_name">edit-find-symbolic</property>
             <property name="primary_icon_activatable">False</property>
             <property name="primary_icon_sensitive">False</property>
+            <signal name="activate" handler="on_search_activate" swapped="no"/>
             <signal name="changed" handler="on_search_changed" swapped="no"/>
           </object>
           <packing>

+ 11 - 0
marxbook/store.py

@@ -63,6 +63,17 @@ class Store:
     def folder(self, folder: str):
         return Store(self.basedir / folder)
 
+    def move(self, path: str, dest_dir: str):
+        dest = self.basedir / dest_dir
+        if dest.exists() and not dest.is_dir():
+            raise ValueError(
+                "destination '%s' already exists and is not a directory" % dest_dir
+            )
+        if not dest.exists():
+            dest.mkdir(parents=True, exist_ok=True)
+        fpath = self.basedir / path
+        fpath.rename(dest / fpath.name)
+
 
 HEADER_LINE = re.compile(r"^([^:]+): (.*)$")