Skip to content
Snippets Groups Projects
Commit e9200835 authored by Julien (jvoisin) Voisin's avatar Julien (jvoisin) Voisin
Browse files

The Nautilus extension is now working

parent 71b1ced8
No related branches found
No related tags found
No related merge requests found
#!/usr/bin/env python3 #!/usr/bin/env python3
# TODO: # pylint: disable=unused-argument,len-as-condition,arguments-differ
# - Test with a large amount of files.
# - Show a progression bar when the removal takes time. """
# - Improve the MessageDialog list for failed items. Because writing GUI is non-trivial (cf. https://0xacab.org/jvoisin/mat2/issues/3),
we decided to write a Nautilus extensions instead
(cf. https://0xacab.org/jvoisin/mat2/issues/2).
The code is a little bit convoluted because Gtk isn't thread-safe,
so we're not allowed to call anything Gtk-related outside of the main
thread, so we'll have to resort to using a `queue` to pass "messages" around.
"""
import os import os
import queue
import threading
from urllib.parse import unquote from urllib.parse import unquote
import gi import gi
gi.require_version('Nautilus', '3.0') gi.require_version('Nautilus', '3.0')
gi.require_version('Gtk', '3.0') gi.require_version('Gtk', '3.0')
from gi.repository import Nautilus, GObject, Gtk, Gio from gi.repository import Nautilus, GObject, Gtk, Gio, GLib
from libmat2 import parser_factory from libmat2 import parser_factory
class Mat2Wrapper(): def _remove_metadata(fpath):
def __init__(self, filepath): """ This is a simple wrapper around libmat2, because it's
self.__filepath = filepath easier and cleaner this way.
"""
def remove_metadata(self): parser, mtype = parser_factory.get_parser(fpath)
parser, mtype = parser_factory.get_parser(self.__filepath) if parser is None:
if parser is None: return False, mtype
return False, mtype return parser.remove_all(), mtype
return parser.remove_all(), mtype
class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationWidgetProvider): class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationWidgetProvider):
def notify(self): """ This class adds an item to the right-clic menu in Nautilus. """
self.infobar_msg.set_text("Failed to clean some items") def __init__(self):
self.infobar.show_all() super().__init__()
self.infobar_hbox = None
self.infobar = None
self.failed_items = list()
def get_widget(self, uri, window): def __infobar_failure(self):
self.infobar = Gtk.InfoBar() """ Add an hbox to the `infobar` warning about the fact that we didn't
self.infobar.set_message_type(Gtk.MessageType.ERROR) manage to remove the metadata from every single file.
"""
self.infobar.set_show_close_button(True) self.infobar.set_show_close_button(True)
self.infobar.connect("response", self.__cb_infobar_response) self.infobar_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self.infobar.get_content_area().pack_start(hbox, False, False, 0)
btn = Gtk.Button("Show") btn = Gtk.Button("Show")
btn.connect("clicked", self.__cb_show_failed) btn.connect("clicked", self.__cb_show_failed)
self.infobar.get_content_area().pack_end(btn, False, False, 0) self.infobar_hbox.pack_end(btn, False, False, 0)
self.infobar_msg = Gtk.Label() infobar_msg = Gtk.Label("Failed to clean some items")
hbox.pack_start(self.infobar_msg, False, False, 0) self.infobar_hbox.pack_start(infobar_msg, False, False, 0)
self.infobar.get_content_area().pack_start(self.infobar_hbox, True, True, 0)
self.infobar.show_all()
def get_widget(self, uri, window):
""" This is the method that we have to implement (because we're
a LocationWidgetProvider) in order to show our infobar.
"""
self.infobar = Gtk.InfoBar()
self.infobar.set_message_type(Gtk.MessageType.ERROR)
self.infobar.connect("response", self.__cb_infobar_response)
return self.infobar return self.infobar
def __cb_infobar_response(self, infobar, response): def __cb_infobar_response(self, infobar, response):
""" Callback for the infobar close button.
"""
if response == Gtk.ResponseType.CLOSE: if response == Gtk.ResponseType.CLOSE:
self.infobar_hbox.destroy()
self.infobar.hide() self.infobar.hide()
def __cb_show_failed(self, button): def __cb_show_failed(self, button):
""" Callback to show a popup containing a list of files
that we didn't manage to clean.
"""
# FIXME this should be done only once the window is destroyed
self.infobar_hbox.destroy()
self.infobar.hide() self.infobar.hide()
window = Gtk.Window() window = Gtk.Window()
hb = Gtk.HeaderBar() headerbar = Gtk.HeaderBar()
window.set_titlebar(hb) window.set_titlebar(headerbar)
hb.props.title = "Metadata removal failed" headerbar.props.title = "Metadata removal failed"
exit_buton = Gtk.Button("Exit") exit_buton = Gtk.Button("Exit")
exit_buton.connect("clicked", lambda _: window.close()) exit_buton.connect("clicked", lambda _: window.close())
hb.pack_end(exit_buton) headerbar.pack_end(exit_buton)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
window.add(box) window.add(box)
...@@ -71,7 +100,7 @@ class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationW ...@@ -71,7 +100,7 @@ class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationW
listbox.set_selection_mode(Gtk.SelectionMode.NONE) listbox.set_selection_mode(Gtk.SelectionMode.NONE)
box.pack_start(listbox, True, True, 0) box.pack_start(listbox, True, True, 0)
for i, mtype in self.failed_items: for fname, mtype in self.failed_items:
row = Gtk.ListBoxRow() row = Gtk.ListBoxRow()
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
row.add(hbox) row.add(hbox)
...@@ -80,7 +109,7 @@ class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationW ...@@ -80,7 +109,7 @@ class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationW
select_image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON) select_image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON)
hbox.pack_start(select_image, False, False, 0) hbox.pack_start(select_image, False, False, 0)
label = Gtk.Label(os.path.basename(i)) label = Gtk.Label(os.path.basename(fname))
hbox.pack_start(label, True, False, 0) hbox.pack_start(label, True, False, 0)
listbox.add(row) listbox.add(row)
...@@ -90,37 +119,104 @@ class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationW ...@@ -90,37 +119,104 @@ class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationW
@staticmethod @staticmethod
def __validate(f): def __validate(fileinfo):
if f.get_uri_scheme() != "file" or f.is_directory(): """ Validate if a given file FileInfo `fileinfo` can be processed."""
if fileinfo.get_uri_scheme() != "file" or fileinfo.is_directory():
return False return False
elif not f.can_write(): elif not fileinfo.can_write():
return False return False
return True return True
def __create_progressbar(self):
""" Create the progressbar used to notify that files are currently
being processed.
"""
self.infobar.set_show_close_button(False)
self.infobar.set_message_type(Gtk.MessageType.INFO)
self.infobar_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
progressbar = Gtk.ProgressBar()
self.infobar_hbox.pack_start(progressbar, True, True, 0)
progressbar.set_show_text(True)
self.infobar.get_content_area().pack_start(self.infobar_hbox, True, True, 0)
self.infobar.show_all()
return progressbar
def __update_progressbar(self, processing_queue, progressbar):
""" This method is run via `Glib.add_idle` to update the progressbar."""
try:
fname = processing_queue.get(block=False)
except queue.Empty:
return True
# `None` is the marker put in the queue to signal that every selected
# file was processed.
if fname is None:
self.infobar_hbox.destroy()
self.infobar.hide()
if len(self.failed_items):
self.__infobar_failure()
if not processing_queue.empty():
print("Something went wrong, the queue isn't empty :/")
return False
progressbar.pulse()
progressbar.set_text("Cleaning %s" % fname)
progressbar.show_all()
self.infobar_hbox.show_all()
self.infobar.show_all()
return True
def __clean_files(self, files, processing_queue):
""" This method is threaded in order to avoid blocking the GUI
while cleaning up the files.
"""
for fileinfo in files:
fname = fileinfo.get_name()
processing_queue.put(fname)
if not self.__validate(fileinfo):
self.failed_items.append((fname, None))
continue
fpath = unquote(fileinfo.get_uri()[7:]) # `len('file://') = 7`
success, mtype = _remove_metadata(fpath)
if not success:
self.failed_items.append((fname, mtype))
processing_queue.put(None) # signal that we processed all the files
return True
def __cb_menu_activate(self, menu, files): def __cb_menu_activate(self, menu, files):
""" This method is called when the user clicked the "clean metadata"
menu item.
"""
self.failed_items = list() self.failed_items = list()
for f in files: progressbar = self.__create_progressbar()
if not self.__validate(f): progressbar.set_pulse_step = 1.0 / len(files)
self.failed_items.append((f.get_name(), None)) self.infobar.show_all()
continue
processing_queue = queue.Queue()
GLib.idle_add(self.__update_progressbar, processing_queue, progressbar)
fname = unquote(f.get_uri()[7:]) thread = threading.Thread(target=self.__clean_files, args=(files, processing_queue))
ret, mtype = Mat2Wrapper(fname).remove_metadata() thread.daemon = True
if not ret: thread.start()
self.failed_items.append((f.get_name(), mtype))
if len(self.failed_items):
self.notify()
def get_background_items(self, window, file): def get_background_items(self, window, file):
""" https://bugzilla.gnome.org/show_bug.cgi?id=784278 """ """ https://bugzilla.gnome.org/show_bug.cgi?id=784278 """
return None return None
def get_file_items(self, window, files): def get_file_items(self, window, files):
""" This method is the one allowing us to create a menu item.
"""
# Do not show the menu item if not a single file has a chance to be # Do not show the menu item if not a single file has a chance to be
# processed by mat2. # processed by mat2.
if not any(map(self.__validate, files)): if not any(map(self.__validate, files)):
return return None
item = Nautilus.MenuItem( item = Nautilus.MenuItem(
name="MAT2::Remove_metadata", name="MAT2::Remove_metadata",
...@@ -129,4 +225,4 @@ class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationW ...@@ -129,4 +225,4 @@ class ColumnExtension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationW
) )
item.connect('activate', self.__cb_menu_activate, files) item.connect('activate', self.__cb_menu_activate, files)
return [item] return [item, ]
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment