#!/usr/bin/env python3 # vim: set et ts=4 sts=4 sw=4: # File: gmm-gtk # Location: /usr/bin # Author: bgstack15 # SPDX-License-Identifier: GPL-3.0-only # Startdate: 2024-11-20-4 16:31 # Title: Graphical Mount Manager in Gtk3 # Purpose: Easily mount iso files and easily manage these mounted files and mount points, basically like acetoneiso # History: # Usage: see man page # Reference: # logout-manager-gtk # https://python-gtk-3-tutorial.readthedocs.io/en/latest/dialogs.html # https://python-gtk-3-tutorial.readthedocs.io/en/latest/menus.html also shows popup (right click) menu, and toolbars not used here # https://www.programcreek.com/python/example/1399/gtk.FileChooserDialog gtk2 example which took a little work to update to gtk3; minor things like FileChooserAction.CANCEL or similar. # https://gist.github.com/mi4code/d53b81ed6353275e9bbeedfb7b5fd990 # https://github.com/sam-m888/python-gtk3-tutorial/blob/master/aboutdialog.rst # https://stackoverflow.com/questions/36921706/gtk3-ask-confirmation-before-application-quit # disused references: # https://python-gtk-3-tutorial.readthedocs.io/en/latest/application.html#application # Improve: # Dependencies: import gi, os, sys, subprocess, threading gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk from gi.repository.GdkPixbuf import Pixbuf # ADJUST THIS SECTION FOR DEVELOPMENT # for prod, this should be "" home_dir = os.getenv("SUDO_HOME",os.getenv("HOME")) if os.getenv("DEBUG",None): sys.path.append(os.path.join(home_dir,"dev","gmm","src","usr/share/gmm")) sys.path.append("/usr/share/gmm") sys.path.append(".") import gmm_lib as gmm from gmm_lib import debuglev, ferror, appname # GRAPHICAL APP # gtk complains that this is deprecated, but they make every good thing "deprecated." MENU_INFO = """ """ class SettingsDialog(Gtk.Dialog): # ref https://python-gtk-3-tutorial.readthedocs.io/en/latest/dialogs.html#custom-dialogs def __init__(self, parent): super().__init__(title="Settings", transient_for=parent,flags=0) self.add_buttons( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK ) self.set_default_size(150,50) lbl_mounts_dir = Gtk.Label(label="Mounts directory") self.ent_mounts_dir = Gtk.Entry(text=parent.dialog_mounts_dir) if parent.gmmapp.mounts: self.ent_mounts_dir.set_sensitive(False) box = self.get_content_area() box.add(lbl_mounts_dir) box.add(self.ent_mounts_dir) self.show_all() class MainWindow(Gtk.ApplicationWindow): def __init__(self, gmmapp, *args, **kwargs): super().__init__(title=gmm.appname_full,*args, **kwargs) self.gmmapp = gmmapp self.liststore = Gtk.ListStore(str,str) self.set_icon_name(gmm.icon_name) # menu action_group = Gtk.ActionGroup(name="actions") self.add_file_menu_actions(action_group) self.add_help_menu_actions(action_group) uimanager = self.create_ui_manager() uimanager.insert_action_group(action_group) self.menubar = uimanager.get_widget("/MenuBar") # main layout self.grid = Gtk.Grid() self.add(self.grid) self.grid.add(self.menubar) renderer = Gtk.CellRendererText() #column = Gtk.TreeViewColumn("Source",renderer,text=0,weight=1) self.ml = Gtk.TreeView(model=self.liststore) self.ml.append_column(Gtk.TreeViewColumn("Source",renderer,text=0)) self.ml.append_column(Gtk.TreeViewColumn("Mount point",renderer,text=1)) self.ml.connect("row-activated", self.func_double_click_entry) self.current_selection = None selection = self.ml.get_selection() selection.connect("changed", self.func_tree_selection_changed) self.grid.attach(self.ml, 0,1, 4, 1) self.unmount_btn = Gtk.Button.new_with_mnemonic(label="_Unmount") self.unmount_btn.connect("clicked", self.func_unmount_current_selection) self.grid.attach_next_to(self.unmount_btn,self.ml,Gtk.PositionType.BOTTOM,1,1) # xfe needs the Gdk.DragAction.MOVE flag self.ml.drag_dest_set( Gtk.DestDefaults.ALL, [ Gtk.TargetEntry.new("text/uri-list", 0, 0), ], Gdk.DragAction.COPY | Gdk.DragAction.MOVE | Gdk.DragAction.ASK | Gdk.DragAction.LINK ) self.ml.connect("drag-data-received", self.on_drop) # initial load self.refresh_form() t1 = threading.Thread(target=self.gmmapp.watch_for_changes,args=("disused-but-must-be-not-a-function",self.refresh_form)) t1.start() def do_delete_event(self, event = None): # override built-in do_delete_event, because we need to run this manually first. self.gmmapp.exit() # Calling this somehow is safe here. Gtk.main_quit() # this is the regular behavior return False def on_drop(self, context, x, y, data, info, time, o1 = None): #print("Dropped file(s):", uris) # mimetype, will probably just be uri-list mimetype = None try: mimetype = info.get_data_type() except: pass # raw string with \r\n, not useful because get_uris exists #urilist = info.get_data() uris = [] try: uris = info.get_uris() except: pass if debuglev(9): ferror(f"DEBUG: on_drop got mimetype {mimetype}") ferror(f"DEBUG: on_drop got uris {uris}") for filename in [i.replace("file://","") for i in uris]: self.mount_iso_to_default(filename) def add_file_menu_actions(self, action_group): action_filemenu = Gtk.Action(name="FileMenu", label="_File") action_group.add_action(action_filemenu) #action_fileopenmenu = Gtk.Action(name="FileOpen", stock_id=Gtk.STOCK_OPEN) #action_group.add_action(action_fileopenmenu) action_mount = Gtk.Action( name="FileMount", label="_Mount iso...", tooltip="Mount existing disc image", stock_id=Gtk.STOCK_OPEN ) action_mount.connect("activate", self.on_menu_file_mount) action_group.add_action_with_accel(action_mount, None) action_settings = Gtk.Action( name="FileSettings", label="_Settings...", tooltip="Open settings dialog", stock_id=Gtk.STOCK_PREFERENCES ) action_settings.connect("activate", self.on_menu_file_settings) action_group.add_action_with_accel(action_settings, None) action_filequit = Gtk.Action(name="FileQuit", stock_id=Gtk.STOCK_QUIT) action_filequit.connect("activate", self.on_menu_file_quit) action_group.add_action(action_filequit) def add_help_menu_actions(self, action_group): action_helpmenu = Gtk.Action(name="HelpMenu", label="_Help") action_group.add_action(action_helpmenu) action_helpaboutmenu = Gtk.Action(name="HelpAbout", stock_id=Gtk.STOCK_ABOUT) action_helpaboutmenu.connect("activate", self.on_menu_help_about) action_group.add_action(action_helpaboutmenu) def create_ui_manager(self): uimanager = Gtk.UIManager() uimanager.add_ui_from_string(MENU_INFO) accelgroup = uimanager.get_accel_group() self.add_accel_group(accelgroup) return uimanager def on_menu_file_mount(self, widget): #print("STUB Do something with file mount....") chooser = Gtk.FileChooserDialog( title="Mount iso file", action=Gtk.FileChooserAction.OPEN, buttons=( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK, ) ) chooser.set_default_response(Gtk.ResponseType.OK) filter = Gtk.FileFilter() filter.set_name("Disc images") filter.add_pattern("*.iso") chooser.add_filter(filter) filter = Gtk.FileFilter() filter.set_name("All files") filter.add_pattern("*") chooser.add_filter(filter) if chooser.run() == Gtk.ResponseType.OK: filename = chooser.get_filename() chooser.destroy() #self.open_file(filename) if debuglev(4): ferror(f"From file chooser, got file {filename}") self.mount_iso_to_default(filename) else: if debuglev(4): ferror(f"File chooser returned nothing.") chooser.destroy() def mount_iso_to_default(self, filename = None, o2 = None): if debuglev(1): #ferror(f"Mounting file {filename}") ferror(f"Will try to mount {filename}") if filename: self.gmmapp.mount_iso_to_default(filename) self.refresh_form() def on_menu_file_settings(self, widget): #print("STUB Do something with file settings....") dialog = SettingsDialog(self) response = dialog.run() print(f"got settings response {response}") if response == Gtk.ResponseType.OK: self.gmmapp.config.set(appname,"mounts_dir",str(dialog.ent_mounts_dir.get_text())) self.gmmapp.save_config(self.gmmapp.conffile) # delete the dialog dialog.destroy() # reload config and entire app self.refresh_form() def on_menu_file_quit(self, widget): # Must call our own function and not main_quit. #Gtk.main_quit() self.do_delete_event() def on_menu_help_about(self, widget): about_dialog = Gtk.AboutDialog( program_name = gmm.appname_full, icon_name = gmm.icon_name, logo_icon_name = gmm.icon_name, transient_for=self, modal=True, copyright = "© 2024", license_type = Gtk.License.GPL_3_0_ONLY, authors = gmm.authors, version = gmm.appversion, ) #license = Gtk.License.GPL_3_0_ONLY, # present() will not let you close with the Close button. #about_dialog.present() _ = about_dialog.run() about_dialog.destroy() def refresh_form(self): # compare all config settings to see if they are different self.gmmapp.load_config(self.gmmapp.conffile) # this populates object "self.gmmapp.config" self.dialog_mounts_dir = self.gmmapp.config[appname]["mounts_dir"] self.gmmapp.mounts = self.gmmapp.list_mounts() self.liststore.clear() for i in self.gmmapp.mounts: print(f"DEBUG: looping over {i}") self.liststore.append([i["source"],i["mountpoint"]]) def func_double_click_entry(self, tree_view, path, column): if debuglev(9): ferror(f"DEBUG: double-click {tree_view},{path},{column}") path = self.current_selection if path: if debuglev(1): ferror(f"Running xdg-open {path}") subprocess.Popen(["xdg-open",path]) else: if debuglev(4): ferror(f"INFO: No item selected to open, continuing...") pass def func_tree_selection_changed(self, selection): model, treeiter = selection.get_selected() if treeiter is not None: # you have to know which column has the mountpoint. It is column 1 in gmm. try: self.current_selection = str(model[treeiter][1]) except Exception as e: print(f"WARNING: cannot seem to save string from {model[treeiter]}") def func_unmount_current_selection(self, o1 = None): if debuglev(9): ferror(f"DEBUG: unmount button {o1}") #ferror(f"please unmount, {self.current_selection}!") path = self.current_selection if path: self.gmmapp.unmount_iso_to_path(path,"") self.refresh_form() elif debuglev(4): ferror(f"INFO: Nothing selected to unmount, continuing...") if "__main__" == __name__: app = gmm.Gmm(gmm.args) app.cli_main() if app.show_gui: # MAIN GRAPICAL APP app.initialize_for_gui() win = MainWindow(app) win.connect("destroy", win.do_delete_event) win.show_all() Gtk.main()