summaryrefslogtreecommitdiff
path: root/nautilus/mat2.py
diff options
context:
space:
mode:
authorAntoine Tenart2018-08-24 09:11:07 +0200
committerjvoisin2018-08-26 01:09:41 +0200
commit15dd3d84ffb2e9f540af917c90bfb8e3a2f21059 (patch)
tree24f0191365ec0fe4a4a03fa81b38beef16e64c9b /nautilus/mat2.py
parent588466f4a8b4fff6809170a73cd6e69295a18191 (diff)
nautilus: rename the nautilus plugin
Rename the Nautilus plugin (removing 'nautilus' from the file name) as it already lives in its own 'nautilus' directory. The same argument applies when installing the plugin in a distro. Signed-off-by: Antoine Tenart <antoine.tenart@ack.tf>
Diffstat (limited to 'nautilus/mat2.py')
-rw-r--r--nautilus/mat2.py251
1 files changed, 251 insertions, 0 deletions
diff --git a/nautilus/mat2.py b/nautilus/mat2.py
new file mode 100644
index 0000000..133c56d
--- /dev/null
+++ b/nautilus/mat2.py
@@ -0,0 +1,251 @@
1#!/usr/bin/env python3
2
3"""
4Because writing GUI is non-trivial (cf. https://0xacab.org/jvoisin/mat2/issues/3),
5we decided to write a Nautilus extensions instead
6(cf. https://0xacab.org/jvoisin/mat2/issues/2).
7
8The code is a little bit convoluted because Gtk isn't thread-safe,
9so we're not allowed to call anything Gtk-related outside of the main
10thread, so we'll have to resort to using a `queue` to pass "messages" around.
11"""
12
13# pylint: disable=no-name-in-module,unused-argument,no-self-use,import-error
14
15import queue
16import threading
17from typing import Tuple
18from urllib.parse import unquote
19
20import gi
21gi.require_version('Nautilus', '3.0')
22gi.require_version('Gtk', '3.0')
23gi.require_version('GdkPixbuf', '2.0')
24from gi.repository import Nautilus, GObject, Gtk, Gio, GLib, GdkPixbuf
25
26from libmat2 import parser_factory
27
28# make pyflakes happy
29assert Tuple
30
31def _remove_metadata(fpath):
32 """ This is a simple wrapper around libmat2, because it's
33 easier and cleaner this way.
34 """
35 parser, mtype = parser_factory.get_parser(fpath)
36 if parser is None:
37 return False, mtype
38 return parser.remove_all(), mtype
39
40class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationWidgetProvider):
41 """ This class adds an item to the right-clic menu in Nautilus. """
42
43 def __init__(self):
44 super().__init__()
45 self.infobar_hbox = None
46 self.infobar = None
47 self.failed_items = list()
48
49 def __infobar_failure(self):
50 """ Add an hbox to the `infobar` warning about the fact that we didn't
51 manage to remove the metadata from every single file.
52 """
53 self.infobar.set_show_close_button(True)
54 self.infobar_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
55
56 btn = Gtk.Button("Show")
57 btn.connect("clicked", self.__cb_show_failed)
58 self.infobar_hbox.pack_end(btn, False, False, 0)
59
60 infobar_msg = Gtk.Label("Failed to clean some items")
61 self.infobar_hbox.pack_start(infobar_msg, False, False, 0)
62
63 self.infobar.get_content_area().pack_start(self.infobar_hbox, True, True, 0)
64 self.infobar.show_all()
65
66 def get_widget(self, uri, window):
67 """ This is the method that we have to implement (because we're
68 a LocationWidgetProvider) in order to show our infobar.
69 """
70 self.infobar = Gtk.InfoBar()
71 self.infobar.set_message_type(Gtk.MessageType.ERROR)
72 self.infobar.connect("response", self.__cb_infobar_response)
73
74 return self.infobar
75
76 def __cb_infobar_response(self, infobar, response):
77 """ Callback for the infobar close button.
78 """
79 if response == Gtk.ResponseType.CLOSE:
80 self.infobar_hbox.destroy()
81 self.infobar.hide()
82
83 def __cb_show_failed(self, button):
84 """ Callback to show a popup containing a list of files
85 that we didn't manage to clean.
86 """
87
88 # FIXME this should be done only once the window is destroyed
89 self.infobar_hbox.destroy()
90 self.infobar.hide()
91
92 window = Gtk.Window()
93 headerbar = Gtk.HeaderBar()
94 window.set_titlebar(headerbar)
95 headerbar.props.title = "Metadata removal failed"
96
97 close_buton = Gtk.Button("Close")
98 close_buton.connect("clicked", lambda _: window.close())
99 headerbar.pack_end(close_buton)
100
101 box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
102 window.add(box)
103
104 box.add(self.__create_treeview())
105 window.show_all()
106
107
108 @staticmethod
109 def __validate(fileinfo) -> Tuple[bool, str]:
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, ""
117
118
119 def __create_treeview(self) -> Gtk.TreeView:
120 liststore = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str)
121 treeview = Gtk.TreeView(model=liststore)
122
123 renderer_pixbuf = Gtk.CellRendererPixbuf()
124 column_pixbuf = Gtk.TreeViewColumn("Icon", renderer_pixbuf, pixbuf=0)
125 treeview.append_column(column_pixbuf)
126
127 for idx, name in enumerate(['File', 'Reason']):
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, fname, reason])
147
148 treeview.show_all()
149 return treeview
150
151
152 def __create_progressbar(self) -> Gtk.ProgressBar:
153 """ Create the progressbar used to notify that files are currently
154 being processed.
155 """
156 self.infobar.set_show_close_button(False)
157 self.infobar.set_message_type(Gtk.MessageType.INFO)
158 self.infobar_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
159
160 progressbar = Gtk.ProgressBar()
161 self.infobar_hbox.pack_start(progressbar, True, True, 0)
162 progressbar.set_show_text(True)
163
164 self.infobar.get_content_area().pack_start(self.infobar_hbox, True, True, 0)
165 self.infobar.show_all()
166
167 return progressbar
168
169 def __update_progressbar(self, processing_queue, progressbar) -> bool:
170 """ This method is run via `Glib.add_idle` to update the progressbar."""
171 try:
172 fname = processing_queue.get(block=False)
173 except queue.Empty:
174 return True
175
176 # `None` is the marker put in the queue to signal that every selected
177 # file was processed.
178 if fname is None:
179 self.infobar_hbox.destroy()
180 self.infobar.hide()
181 if len(self.failed_items):
182 self.__infobar_failure()
183 if not processing_queue.empty():
184 print("Something went wrong, the queue isn't empty :/")
185 return False
186
187 progressbar.pulse()
188 progressbar.set_text("Cleaning %s" % fname)
189 progressbar.show_all()
190 self.infobar_hbox.show_all()
191 self.infobar.show_all()
192 return True
193
194 def __clean_files(self, files: list, processing_queue: queue.Queue) -> bool:
195 """ This method is threaded in order to avoid blocking the GUI
196 while cleaning up the files.
197 """
198 for fileinfo in files:
199 fname = fileinfo.get_name()
200 processing_queue.put(fname)
201
202 valid, reason = self.__validate(fileinfo)
203 if not valid:
204 self.failed_items.append((fname, None, reason))
205 continue
206
207 fpath = unquote(fileinfo.get_uri()[7:]) # `len('file://') = 7`
208 success, mtype = _remove_metadata(fpath)
209 if not success:
210 self.failed_items.append((fname, mtype, 'Unsupported/invalid'))
211 processing_queue.put(None) # signal that we processed all the files
212 return True
213
214
215 def __cb_menu_activate(self, menu, files):
216 """ This method is called when the user clicked the "clean metadata"
217 menu item.
218 """
219 self.failed_items = list()
220 progressbar = self.__create_progressbar()
221 progressbar.set_pulse_step = 1.0 / len(files)
222 self.infobar.show_all()
223
224 processing_queue = queue.Queue()
225 GLib.idle_add(self.__update_progressbar, processing_queue, progressbar)
226
227 thread = threading.Thread(target=self.__clean_files, args=(files, processing_queue))
228 thread.daemon = True
229 thread.start()
230
231
232 def get_background_items(self, window, file):
233 """ https://bugzilla.gnome.org/show_bug.cgi?id=784278 """
234 return None
235
236 def get_file_items(self, window, files):
237 """ This method is the one allowing us to create a menu item.
238 """
239 # Do not show the menu item if not a single file has a chance to be
240 # processed by mat2.
241 if not any([is_valid for (is_valid, _) in map(self.__validate, files)]):
242 return None
243
244 item = Nautilus.MenuItem(
245 name="MAT2::Remove_metadata",
246 label="Remove metadata",
247 tip="Remove metadata"
248 )
249 item.connect('activate', self.__cb_menu_activate, files)
250
251 return [item, ]