summaryrefslogtreecommitdiff
path: root/nautilus
diff options
context:
space:
mode:
authorjvoisin2018-07-23 23:39:06 +0200
committerjvoisin2018-07-23 23:39:06 +0200
commite9200835590641c277784159f25ab4464c515a17 (patch)
tree1a3b91862cc2d7710bba40aedfa47673eb83a963 /nautilus
parent71b1ced84246f940ee46b89352d958edb5408f2a (diff)
The Nautilus extension is now working
Diffstat (limited to 'nautilus')
-rw-r--r--nautilus/nautilus_mat2.py192
1 files changed, 144 insertions, 48 deletions
diff --git a/nautilus/nautilus_mat2.py b/nautilus/nautilus_mat2.py
index 9e8d4db..8828989 100644
--- a/nautilus/nautilus_mat2.py
+++ b/nautilus/nautilus_mat2.py
@@ -1,68 +1,97 @@
1#!/usr/bin/env python3 1#!/usr/bin/env python3
2 2
3# TODO: 3# pylint: disable=unused-argument,len-as-condition,arguments-differ
4# - Test with a large amount of files. 4
5# - Show a progression bar when the removal takes time. 5"""
6# - Improve the MessageDialog list for failed items. 6Because writing GUI is non-trivial (cf. https://0xacab.org/jvoisin/mat2/issues/3),
7we decided to write a Nautilus extensions instead
8(cf. https://0xacab.org/jvoisin/mat2/issues/2).
9
10The code is a little bit convoluted because Gtk isn't thread-safe,
11so we're not allowed to call anything Gtk-related outside of the main
12thread, so we'll have to resort to using a `queue` to pass "messages" around.
13"""
7 14
8import os 15import os
16import queue
17import threading
9from urllib.parse import unquote 18from urllib.parse import unquote
10 19
11import gi 20import gi
12gi.require_version('Nautilus', '3.0') 21gi.require_version('Nautilus', '3.0')
13gi.require_version('Gtk', '3.0') 22gi.require_version('Gtk', '3.0')
14from gi.repository import Nautilus, GObject, Gtk, Gio 23from gi.repository import Nautilus, GObject, Gtk, Gio, GLib
15 24
16from libmat2 import parser_factory 25from libmat2 import parser_factory
17 26
18class Mat2Wrapper(): 27def _remove_metadata(fpath):
19 def __init__(self, filepath): 28 """ This is a simple wrapper around libmat2, because it's
20 self.__filepath = filepath 29 easier and cleaner this way.
21 30 """
22 def remove_metadata(self): 31 parser, mtype = parser_factory.get_parser(fpath)
23 parser, mtype = parser_factory.get_parser(self.__filepath) 32 if parser is None:
24 if parser is None: 33 return False, mtype
25 return False, mtype 34 return parser.remove_all(), mtype
26 return parser.remove_all(), mtype
27 35
28class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationWidgetProvider): 36class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationWidgetProvider):
29 def notify(self): 37 """ This class adds an item to the right-clic menu in Nautilus. """
30 self.infobar_msg.set_text("Failed to clean some items") 38 def __init__(self):
31 self.infobar.show_all() 39 super().__init__()
40 self.infobar_hbox = None
41 self.infobar = None
42 self.failed_items = list()
32 43
33 def get_widget(self, uri, window): 44 def __infobar_failure(self):
34 self.infobar = Gtk.InfoBar() 45 """ Add an hbox to the `infobar` warning about the fact that we didn't
35 self.infobar.set_message_type(Gtk.MessageType.ERROR) 46 manage to remove the metadata from every single file.
47 """
36 self.infobar.set_show_close_button(True) 48 self.infobar.set_show_close_button(True)
37 self.infobar.connect("response", self.__cb_infobar_response) 49 self.infobar_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
38
39 hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
40 self.infobar.get_content_area().pack_start(hbox, False, False, 0)
41 50
42 btn = Gtk.Button("Show") 51 btn = Gtk.Button("Show")
43 btn.connect("clicked", self.__cb_show_failed) 52 btn.connect("clicked", self.__cb_show_failed)
44 self.infobar.get_content_area().pack_end(btn, False, False, 0) 53 self.infobar_hbox.pack_end(btn, False, False, 0)
45 54
46 self.infobar_msg = Gtk.Label() 55 infobar_msg = Gtk.Label("Failed to clean some items")
47 hbox.pack_start(self.infobar_msg, False, False, 0) 56 self.infobar_hbox.pack_start(infobar_msg, False, False, 0)
57
58 self.infobar.get_content_area().pack_start(self.infobar_hbox, True, True, 0)
59 self.infobar.show_all()
60
61 def get_widget(self, uri, window):
62 """ This is the method that we have to implement (because we're
63 a LocationWidgetProvider) in order to show our infobar.
64 """
65 self.infobar = Gtk.InfoBar()
66 self.infobar.set_message_type(Gtk.MessageType.ERROR)
67 self.infobar.connect("response", self.__cb_infobar_response)
48 68
49 return self.infobar 69 return self.infobar
50 70
51 def __cb_infobar_response(self, infobar, response): 71 def __cb_infobar_response(self, infobar, response):
72 """ Callback for the infobar close button.
73 """
52 if response == Gtk.ResponseType.CLOSE: 74 if response == Gtk.ResponseType.CLOSE:
75 self.infobar_hbox.destroy()
53 self.infobar.hide() 76 self.infobar.hide()
54 77
55 def __cb_show_failed(self, button): 78 def __cb_show_failed(self, button):
79 """ Callback to show a popup containing a list of files
80 that we didn't manage to clean.
81 """
82
83 # FIXME this should be done only once the window is destroyed
84 self.infobar_hbox.destroy()
56 self.infobar.hide() 85 self.infobar.hide()
57 86
58 window = Gtk.Window() 87 window = Gtk.Window()
59 hb = Gtk.HeaderBar() 88 headerbar = Gtk.HeaderBar()
60 window.set_titlebar(hb) 89 window.set_titlebar(headerbar)
61 hb.props.title = "Metadata removal failed" 90 headerbar.props.title = "Metadata removal failed"
62 91
63 exit_buton = Gtk.Button("Exit") 92 exit_buton = Gtk.Button("Exit")
64 exit_buton.connect("clicked", lambda _: window.close()) 93 exit_buton.connect("clicked", lambda _: window.close())
65 hb.pack_end(exit_buton) 94 headerbar.pack_end(exit_buton)
66 95
67 box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 96 box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
68 window.add(box) 97 window.add(box)
@@ -71,7 +100,7 @@ class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationW
71 listbox.set_selection_mode(Gtk.SelectionMode.NONE) 100 listbox.set_selection_mode(Gtk.SelectionMode.NONE)
72 box.pack_start(listbox, True, True, 0) 101 box.pack_start(listbox, True, True, 0)
73 102
74 for i, mtype in self.failed_items: 103 for fname, mtype in self.failed_items:
75 row = Gtk.ListBoxRow() 104 row = Gtk.ListBoxRow()
76 hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 105 hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
77 row.add(hbox) 106 row.add(hbox)
@@ -80,7 +109,7 @@ class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationW
80 select_image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON) 109 select_image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON)
81 hbox.pack_start(select_image, False, False, 0) 110 hbox.pack_start(select_image, False, False, 0)
82 111
83 label = Gtk.Label(os.path.basename(i)) 112 label = Gtk.Label(os.path.basename(fname))
84 hbox.pack_start(label, True, False, 0) 113 hbox.pack_start(label, True, False, 0)
85 114
86 listbox.add(row) 115 listbox.add(row)
@@ -90,37 +119,104 @@ class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationW
90 119
91 120
92 @staticmethod 121 @staticmethod
93 def __validate(f): 122 def __validate(fileinfo):
94 if f.get_uri_scheme() != "file" or f.is_directory(): 123 """ Validate if a given file FileInfo `fileinfo` can be processed."""
124 if fileinfo.get_uri_scheme() != "file" or fileinfo.is_directory():
95 return False 125 return False
96 elif not f.can_write(): 126 elif not fileinfo.can_write():
97 return False 127 return False
98 return True 128 return True
99 129
130 def __create_progressbar(self):
131 """ Create the progressbar used to notify that files are currently
132 being processed.
133 """
134 self.infobar.set_show_close_button(False)
135 self.infobar.set_message_type(Gtk.MessageType.INFO)
136 self.infobar_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
137
138 progressbar = Gtk.ProgressBar()
139 self.infobar_hbox.pack_start(progressbar, True, True, 0)
140 progressbar.set_show_text(True)
141
142 self.infobar.get_content_area().pack_start(self.infobar_hbox, True, True, 0)
143 self.infobar.show_all()
144
145 return progressbar
146
147 def __update_progressbar(self, processing_queue, progressbar):
148 """ This method is run via `Glib.add_idle` to update the progressbar."""
149 try:
150 fname = processing_queue.get(block=False)
151 except queue.Empty:
152 return True
153
154 # `None` is the marker put in the queue to signal that every selected
155 # file was processed.
156 if fname is None:
157 self.infobar_hbox.destroy()
158 self.infobar.hide()
159 if len(self.failed_items):
160 self.__infobar_failure()
161 if not processing_queue.empty():
162 print("Something went wrong, the queue isn't empty :/")
163 return False
164
165 progressbar.pulse()
166 progressbar.set_text("Cleaning %s" % fname)
167 progressbar.show_all()
168 self.infobar_hbox.show_all()
169 self.infobar.show_all()
170 return True
171
172 def __clean_files(self, files, processing_queue):
173 """ This method is threaded in order to avoid blocking the GUI
174 while cleaning up the files.
175 """
176 for fileinfo in files:
177 fname = fileinfo.get_name()
178 processing_queue.put(fname)
179
180 if not self.__validate(fileinfo):
181 self.failed_items.append((fname, None))
182 continue
183
184 fpath = unquote(fileinfo.get_uri()[7:]) # `len('file://') = 7`
185 success, mtype = _remove_metadata(fpath)
186 if not success:
187 self.failed_items.append((fname, mtype))
188 processing_queue.put(None) # signal that we processed all the files
189 return True
190
191
100 def __cb_menu_activate(self, menu, files): 192 def __cb_menu_activate(self, menu, files):
193 """ This method is called when the user clicked the "clean metadata"
194 menu item.
195 """
101 self.failed_items = list() 196 self.failed_items = list()
102 for f in files: 197 progressbar = self.__create_progressbar()
103 if not self.__validate(f): 198 progressbar.set_pulse_step = 1.0 / len(files)
104 self.failed_items.append((f.get_name(), None)) 199 self.infobar.show_all()
105 continue 200
201 processing_queue = queue.Queue()
202 GLib.idle_add(self.__update_progressbar, processing_queue, progressbar)
106 203
107 fname = unquote(f.get_uri()[7:]) 204 thread = threading.Thread(target=self.__clean_files, args=(files, processing_queue))
108 ret, mtype = Mat2Wrapper(fname).remove_metadata() 205 thread.daemon = True
109 if not ret: 206 thread.start()
110 self.failed_items.append((f.get_name(), mtype))
111 207
112 if len(self.failed_items):
113 self.notify()
114 208
115 def get_background_items(self, window, file): 209 def get_background_items(self, window, file):
116 """ https://bugzilla.gnome.org/show_bug.cgi?id=784278 """ 210 """ https://bugzilla.gnome.org/show_bug.cgi?id=784278 """
117 return None 211 return None
118 212
119 def get_file_items(self, window, files): 213 def get_file_items(self, window, files):
214 """ This method is the one allowing us to create a menu item.
215 """
120 # Do not show the menu item if not a single file has a chance to be 216 # Do not show the menu item if not a single file has a chance to be
121 # processed by mat2. 217 # processed by mat2.
122 if not any(map(self.__validate, files)): 218 if not any(map(self.__validate, files)):
123 return 219 return None
124 220
125 item = Nautilus.MenuItem( 221 item = Nautilus.MenuItem(
126 name="MAT2::Remove_metadata", 222 name="MAT2::Remove_metadata",
@@ -129,4 +225,4 @@ class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationW
129 ) 225 )
130 item.connect('activate', self.__cb_menu_activate, files) 226 item.connect('activate', self.__cb_menu_activate, files)
131 227
132 return [item] 228 return [item, ]