Browse Source

first draft of a gui

boyska 3 years ago
parent
commit
6d27ce3091
4 changed files with 286 additions and 11 deletions
  1. 0 0
      luxembook/__init__.py
  2. 191 0
      luxembook/gui.py
  3. 89 0
      luxembook/gui.ui
  4. 6 11
      setup.py

+ 0 - 0
luxembook/__init__.py


+ 191 - 0
luxembook/gui.py

@@ -0,0 +1,191 @@
+import os.path
+import json
+from subprocess import Popen
+
+import gi
+
+from marxbook import Store
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk, GObject, Gdk
+
+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 = None
+        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("<ctrl>space", self.on_focus_switch)
+        self._add_accelerator("<ctrl>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)
+
+    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 path.startswith(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):
+        """Adds 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_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("/")
+        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])
+
+    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
+
+
+def main():
+    s = Store()
+    w = MyWindow(s)
+    w.show_all()
+    Gtk.main()
+
+if __name__ == "__main__":
+    main()

+ 89 - 0
luxembook/gui.ui

@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <object class="GtkBox" id="root">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkScrolledWindow">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="shadow_type">in</property>
+        <child>
+          <object class="GtkTreeView" id="tree_dirs">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <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">
+                <signal name="changed" handler="on_tree_dirs_sel_changed" swapped="no"/>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">True</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkScrolledWindow">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="shadow_type">in</property>
+            <child>
+              <object class="GtkTreeView" id="tree_marks">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="hexpand">False</property>
+                <property name="vexpand">False</property>
+                <property name="reorderable">True</property>
+                <property name="enable_search">False</property>
+                <signal name="row-activated" handler="on_tree_marks_row_activated" swapped="no"/>
+                <child internal-child="selection">
+                  <object class="GtkTreeSelection"/>
+                </child>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkSearchEntry" id="search">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <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="changed" handler="on_search_changed" swapped="no"/>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">True</property>
+        <property name="fill">True</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </object>
+</interface>

+ 6 - 11
setup.py

@@ -17,18 +17,13 @@ setup(
     author="boyska",
     author_email="piuttosto@logorroici.org",
     license="AGPL",
-    packages=["marxbook"],
-    install_requires=[
-        "beautifulsoup4==4.7.1",
-    ],
+    packages=["marxbook", "luxembook"],
+    install_requires=["beautifulsoup4==4.7.1"],
     python_requires=">=3.5",
-    zip_safe=True,
-    include_package_data=False,
-    entry_points={
-        "console_scripts": [
-            "mxb=marxbook.cli:main",
-        ],
-    },
+    zip_safe=False,
+    include_package_data=True,
+    package_data = {'luxembook': ['gui.ui']},
+    entry_points={"console_scripts": ["mxb=marxbook.cli:main", "mxb-gui=luxembook.gui:main"]},
     classifiers=[
         "License :: OSI Approved :: GNU Affero General Public License v3",
         "Programming Language :: Python :: 3.5",