summaryrefslogtreecommitdiff
path: root/nautilus/mat2.py
diff options
context:
space:
mode:
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, ]