diff options
| author | jvoisin | 2018-07-25 01:44:02 +0200 |
|---|---|---|
| committer | jvoisin | 2018-07-25 22:48:05 +0200 |
| commit | b32ba9f736995b52c4cb18ebc6e8e8b90c45c120 (patch) | |
| tree | 2b6ed3148c55f585a775e939d8a7b9a338c07e31 | |
| parent | e9f28edf735d97106f60b9521991088e20f07c3a (diff) | |
Improve a bit nautilus' popup
| -rw-r--r-- | .gitlab-ci.yml | 2 | ||||
| -rw-r--r-- | nautilus/nautilus_mat2.py | 91 |
2 files changed, 58 insertions, 35 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 209a829..21cd8e0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml | |||
| @@ -10,7 +10,7 @@ bandit: | |||
| 10 | - apt-get -qqy update | 10 | - apt-get -qqy update |
| 11 | - apt-get -qqy install --no-install-recommends python3-bandit | 11 | - apt-get -qqy install --no-install-recommends python3-bandit |
| 12 | - bandit ./mat2 --format txt | 12 | - bandit ./mat2 --format txt |
| 13 | - bandit -r ./nautilus/ --format txt | 13 | - bandit -r ./nautilus/ --format txt --skip B101 |
| 14 | - bandit -r ./libmat2 --format txt --skip B101,B404,B603,B405,B314 | 14 | - bandit -r ./libmat2 --format txt --skip B101,B404,B603,B405,B314 |
| 15 | 15 | ||
| 16 | pylint: | 16 | pylint: |
diff --git a/nautilus/nautilus_mat2.py b/nautilus/nautilus_mat2.py index 09c22e5..32326be 100644 --- a/nautilus/nautilus_mat2.py +++ b/nautilus/nautilus_mat2.py | |||
| @@ -1,7 +1,5 @@ | |||
| 1 | #!/usr/bin/env python3 | 1 | #!/usr/bin/env python3 |
| 2 | 2 | ||
| 3 | # pylint: disable=unused-argument,arguments-differ,no-self-use,no-name-in-module,import-error | ||
| 4 | |||
| 5 | """ | 3 | """ |
| 6 | Because writing GUI is non-trivial (cf. https://0xacab.org/jvoisin/mat2/issues/3), | 4 | Because writing GUI is non-trivial (cf. https://0xacab.org/jvoisin/mat2/issues/3), |
| 7 | we decided to write a Nautilus extensions instead | 5 | we decided to write a Nautilus extensions instead |
| @@ -12,18 +10,24 @@ so we're not allowed to call anything Gtk-related outside of the main | |||
| 12 | thread, so we'll have to resort to using a `queue` to pass "messages" around. | 10 | thread, so we'll have to resort to using a `queue` to pass "messages" around. |
| 13 | """ | 11 | """ |
| 14 | 12 | ||
| 15 | import os | 13 | # pylint: disable=no-name-in-module,unused-argument,no-self-use,import-error |
| 14 | |||
| 16 | import queue | 15 | import queue |
| 17 | import threading | 16 | import threading |
| 17 | from typing import Tuple | ||
| 18 | from urllib.parse import unquote | 18 | from urllib.parse import unquote |
| 19 | 19 | ||
| 20 | import gi | 20 | import gi |
| 21 | gi.require_version('Nautilus', '3.0') | 21 | gi.require_version('Nautilus', '3.0') |
| 22 | gi.require_version('Gtk', '3.0') | 22 | gi.require_version('Gtk', '3.0') |
| 23 | from gi.repository import Nautilus, GObject, Gtk, Gio, GLib | 23 | gi.require_version('GdkPixbuf', '2.0') |
| 24 | from gi.repository import Nautilus, GObject, Gtk, Gio, GLib, GdkPixbuf | ||
| 24 | 25 | ||
| 25 | from libmat2 import parser_factory | 26 | from libmat2 import parser_factory |
| 26 | 27 | ||
| 28 | # make pyflakes happy | ||
| 29 | assert Tuple | ||
| 30 | |||
| 27 | def _remove_metadata(fpath): | 31 | def _remove_metadata(fpath): |
| 28 | """ This is a simple wrapper around libmat2, because it's | 32 | """ This is a simple wrapper around libmat2, because it's |
| 29 | easier and cleaner this way. | 33 | easier and cleaner this way. |
| @@ -35,6 +39,7 @@ def _remove_metadata(fpath): | |||
| 35 | 39 | ||
| 36 | class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationWidgetProvider): | 40 | class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationWidgetProvider): |
| 37 | """ This class adds an item to the right-clic menu in Nautilus. """ | 41 | """ This class adds an item to the right-clic menu in Nautilus. """ |
| 42 | |||
| 38 | def __init__(self): | 43 | def __init__(self): |
| 39 | super().__init__() | 44 | super().__init__() |
| 40 | self.infobar_hbox = None | 45 | self.infobar_hbox = None |
| @@ -96,38 +101,55 @@ class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationW | |||
| 96 | box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) | 101 | box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) |
| 97 | window.add(box) | 102 | window.add(box) |
| 98 | 103 | ||
| 99 | listbox = Gtk.ListBox() | 104 | box.add(self.__create_treeview()) |
| 100 | listbox.set_selection_mode(Gtk.SelectionMode.NONE) | 105 | window.show_all() |
| 101 | box.pack_start(listbox, True, True, 0) | ||
| 102 | 106 | ||
| 103 | for fname, mtype in self.failed_items: | ||
| 104 | row = Gtk.ListBoxRow() | ||
| 105 | hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) | ||
| 106 | row.add(hbox) | ||
| 107 | 107 | ||
| 108 | icon = Gio.content_type_get_icon('text/plain' if not mtype else mtype) | 108 | @staticmethod |
| 109 | select_image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON) | 109 | def __validate(fileinfo) -> Tuple[bool, str]: |
| 110 | hbox.pack_start(select_image, False, False, 0) | 110 | """ Validate if a given file FileInfo `fileinfo` can be processed. |
| 111 | Returns a boolean, and a textreason why""" | ||
| 112 | if fileinfo.get_uri_scheme() != "file" or fileinfo.is_directory(): | ||
| 113 | return False, "Not a file" | ||
| 114 | elif not fileinfo.can_write(): | ||
| 115 | return False, "Not writeable" | ||
| 116 | return True, "" | ||
| 111 | 117 | ||
| 112 | label = Gtk.Label(os.path.basename(fname)) | ||
| 113 | hbox.pack_start(label, True, False, 0) | ||
| 114 | 118 | ||
| 115 | listbox.add(row) | 119 | def __create_treeview(self) -> Gtk.TreeView: |
| 120 | liststore = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str) | ||
| 121 | treeview = Gtk.TreeView(model=liststore) | ||
| 116 | 122 | ||
| 117 | listbox.show_all() | 123 | renderer_pixbuf = Gtk.CellRendererPixbuf() |
| 118 | window.show_all() | 124 | column_pixbuf = Gtk.TreeViewColumn("Image", renderer_pixbuf, pixbuf=0) |
| 125 | treeview.append_column(column_pixbuf) | ||
| 119 | 126 | ||
| 127 | for idx, name in enumerate(['Reason', 'Path']): | ||
| 128 | renderer_text = Gtk.CellRendererText() | ||
| 129 | column_text = Gtk.TreeViewColumn(name, renderer_text, text=idx+1) | ||
| 130 | treeview.append_column(column_text) | ||
| 131 | |||
| 132 | for (fname, mtype, reason) in self.failed_items: | ||
| 133 | # This part is all about adding mimetype icons to the liststore | ||
| 134 | icon = Gio.content_type_get_icon('text/plain' if not mtype else mtype) | ||
| 135 | # in case we don't have the corresponding icon, | ||
| 136 | # we're adding `text/plain`, because we have this one for sure⢠| ||
| 137 | names = icon.get_names() + ['text/plain', ] | ||
| 138 | icon_theme = Gtk.IconTheme.get_default() | ||
| 139 | for name in names: | ||
| 140 | try: | ||
| 141 | img = icon_theme.load_icon(name, Gtk.IconSize.BUTTON, 0) | ||
| 142 | break | ||
| 143 | except GLib.GError: | ||
| 144 | pass | ||
| 145 | |||
| 146 | liststore.append([img, reason, fname]) | ||
| 147 | |||
| 148 | treeview.show_all() | ||
| 149 | return treeview | ||
| 120 | 150 | ||
| 121 | @staticmethod | ||
| 122 | def __validate(fileinfo): | ||
| 123 | """ Validate if a given file FileInfo `fileinfo` can be processed.""" | ||
| 124 | if fileinfo.get_uri_scheme() != "file" or fileinfo.is_directory(): | ||
| 125 | return False | ||
| 126 | elif not fileinfo.can_write(): | ||
| 127 | return False | ||
| 128 | return True | ||
| 129 | 151 | ||
| 130 | def __create_progressbar(self): | 152 | def __create_progressbar(self) -> Gtk.ProgressBar: |
| 131 | """ Create the progressbar used to notify that files are currently | 153 | """ Create the progressbar used to notify that files are currently |
| 132 | being processed. | 154 | being processed. |
| 133 | """ | 155 | """ |
| @@ -144,7 +166,7 @@ class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationW | |||
| 144 | 166 | ||
| 145 | return progressbar | 167 | return progressbar |
| 146 | 168 | ||
| 147 | def __update_progressbar(self, processing_queue, progressbar): | 169 | def __update_progressbar(self, processing_queue, progressbar) -> bool: |
| 148 | """ This method is run via `Glib.add_idle` to update the progressbar.""" | 170 | """ This method is run via `Glib.add_idle` to update the progressbar.""" |
| 149 | try: | 171 | try: |
| 150 | fname = processing_queue.get(block=False) | 172 | fname = processing_queue.get(block=False) |
| @@ -169,7 +191,7 @@ class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationW | |||
| 169 | self.infobar.show_all() | 191 | self.infobar.show_all() |
| 170 | return True | 192 | return True |
| 171 | 193 | ||
| 172 | def __clean_files(self, files, processing_queue): | 194 | def __clean_files(self, files: list, processing_queue: queue.Queue) -> bool: |
| 173 | """ This method is threaded in order to avoid blocking the GUI | 195 | """ This method is threaded in order to avoid blocking the GUI |
| 174 | while cleaning up the files. | 196 | while cleaning up the files. |
| 175 | """ | 197 | """ |
| @@ -177,14 +199,15 @@ class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationW | |||
| 177 | fname = fileinfo.get_name() | 199 | fname = fileinfo.get_name() |
| 178 | processing_queue.put(fname) | 200 | processing_queue.put(fname) |
| 179 | 201 | ||
| 180 | if not self.__validate(fileinfo): | 202 | valid, reason = self.__validate(fileinfo) |
| 181 | self.failed_items.append((fname, None)) | 203 | if not valid: |
| 204 | self.failed_items.append((fname, None, reason)) | ||
| 182 | continue | 205 | continue |
| 183 | 206 | ||
| 184 | fpath = unquote(fileinfo.get_uri()[7:]) # `len('file://') = 7` | 207 | fpath = unquote(fileinfo.get_uri()[7:]) # `len('file://') = 7` |
| 185 | success, mtype = _remove_metadata(fpath) | 208 | success, mtype = _remove_metadata(fpath) |
| 186 | if not success: | 209 | if not success: |
| 187 | self.failed_items.append((fname, mtype)) | 210 | self.failed_items.append((fname, mtype, 'Unsupported/invalid')) |
| 188 | processing_queue.put(None) # signal that we processed all the files | 211 | processing_queue.put(None) # signal that we processed all the files |
| 189 | return True | 212 | return True |
| 190 | 213 | ||
| @@ -215,7 +238,7 @@ class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationW | |||
| 215 | """ | 238 | """ |
| 216 | # Do not show the menu item if not a single file has a chance to be | 239 | # Do not show the menu item if not a single file has a chance to be |
| 217 | # processed by mat2. | 240 | # processed by mat2. |
| 218 | if not any(map(self.__validate, files)): | 241 | if not any([is_valid for (is_valid, _) in map(self.__validate, files)]): |
| 219 | return None | 242 | return None |
| 220 | 243 | ||
| 221 | item = Nautilus.MenuItem( | 244 | item = Nautilus.MenuItem( |
