#!/usr/bin/env python3 # vim: set et ts=4 sts=4 sw=4: # Startdate: 2024-11-18-2 13:58 # Title: Graphical Mount Manager in Tk # Purpose: Easily mount iso files and easily manage these mounted files and mount points, basically like acetoneiso # Dependencies: # req-devuan: python3, python3-tkstackrpms # References: # https://stackoverflow.com/questions/23662280/how-to-log-the-contents-of-a-configparser/50362738#50362738 # srb_lib/srb_tk.py # https://coderslegacy.com/python/list-of-tkinter-widgets/ # https://stackoverflow.com/questions/46331408/limiting-python-filedialog-to-a-specific-filetype/46339932#46339932 # https://stackoverflow.com/questions/30614279/tkinter-treeview-get-selected-item-values # https://www.reddit.com/r/learnpython/comments/hcn8cc/cant_bind_double_click_to_treeview_with_grid/ # for drag and drop, I tried a few things that failed: # https://sourceforge.net/projects/tkdnd/ failed because it does not support python or in between applications # https://github.com/python/cpython/blob/main/Lib/tkinter/dnd.py says it is within the same application # https://stackoverflow.com/questions/44887576/how-can-i-create-a-drag-and-drop-interface within but it does not work with a file dragged in. # Improve: # get tkstackrpms in a venv, so I can try pip install tkinterdnd2., https://www.delftstack.com/howto/python-tkinter/tkinter-drag-and-drop/#download-and-setup-the-essential-packages-for-drag-and-drop-in-tkinter # need .desktop file that takes application/x-iso9660-image, that calls this with --gui import sys, subprocess, json, configparser, tkinter as tk, tkinter.simpledialog, tkinter.filedialog import tkstackrpms as stk import tkinter.ttk as ttk sys.path.append("/usr/share/gmm") sys.path.append(".") import gmm_lib as gmm from gmm_lib import debuglev, ferror, appname # GRAPHICAL APP # Settings window def NewWindow(textvar, func1, ok_button_func, mounts=None): window = tk.Toplevel() #window.geometry("250x250") window.minsize(100,50) window.title("Settings") #newlabel = tk.Label(window, text="Settings" #newlabel.pack() tk.Label(window, text="Mounts directory").grid(row=0,column=0) ent_mounts_dir = stk.Entry(window,textvariable=textvar,func=func1) ent_mounts_dir.grid(row=0,column=1) tk.Button(window,text="OK",underline=0,command=ok_button_func).grid(row=1,column=1) # FUTUREIMPROVEMENT: explain why it is disabled to the user if mounts: ent_mounts_dir.configure(state="disabled") return window # want mount iso... file dialog, that has two mimetype choices: *.iso, or * # a combo box/list box thing of mounted isos, mount point # a config file/dialog for choosing default mounts dir, which is managed by this app. E.g., /mnt/iso so the first disc is /mnt/iso/1 or something. Or ~/mnt/iso. Something like that. # MAIN WINDOW class TkApp(tk.Frame): def __init__(self, master, gmmapp): super().__init__(master) # variables self.gmmapp = gmmapp self.mounts_dir = tk.StringVar() self.master.title("Graphical Mount Manager") self.master.minsize(550,200) imgicon = stk.get_scaled_icon("dvd_unmount",24,"default","","apps") self.master.tk.call("wm","iconphoto",self.master._w,imgicon) menu = tk.Menu(self.master) menu_file = tk.Menu(menu,tearoff=0) menu_file.add_command(label="Mount iso...", command=self.func_mount_iso_dialog, underline=0) menu_file.add_command(label="Settings...", command=self.func_open_settings, underline=0) menu_file.add_separator() menu_file.add_command(label="Exit", command=self.func_exit, underline=1) menu.add_cascade(label="File",menu=menu_file,underline=0) menu_help = tk.Menu(menu,tearoff=0) menu_help.add_command(label="About", command=self.func_about, underline=0) menu.add_cascade(label="Help",menu=menu_help,underline=0) self.master.config(menu=menu) self.grid() # use this instead of pack() self.background_color = self.master.cget("bg") self.mlframe = tk.Frame(self.master) self.mlframe.grid(row=0,column=0,columnspan=3,sticky="ew") # treeview, which requires ttk self.ml = ttk.Treeview(self.mlframe, columns=("source","mountpoint"),show="headings") #help(self.ml) self.ml.grid(column=0,row=0,columnspan=3,sticky="ew") self.ml.heading("source",text="Source") self.ml.heading("mountpoint",text="Mount point") self.ml.column("source",width=400) self.ml.bind("", self.func_double_click_entry) # unmount button self.unmount_btn = tk.Button(self.master,text="Unmount",command=self.func_unmount_current_selection,underline=0).grid(column=0,row=1) self.master.bind("",self.func_unmount_current_selection) # initial load self.refresh_form("initial") # GRAPHICAL FUNCTIONS def func_about(self): """ Display about dialog. """ tk.messagebox.Message(title="About",message=ABOUT_TEXT,icon="info").show() def func_exit(self): # in case we need to manually do stuff # otherwise command=self.client_exit would have sufficed. self.master.quit() def func_open_settings(self): self.settingsWindow = NewWindow(self.mounts_dir,self.save_settings, self.func_close_settings, self.gmmapp.mounts) def func_close_settings(self): if debuglev(8): ferror(f"Closing settings window") try: self.settingsWindow.destroy() except Exception as e: ferror(f"DEBUG: when trying to hide settings window, got {e}") self.refresh_form() def get_current_selection(self, attribute="mountpoint"): """ Get the current path, or other attribute, from the treeview """ value = None # There might be nothing selected, so return nothing values = self.ml.item(self.ml.focus())["values"] if debuglev(9): ferror(f"DEBUG: got values {values}") try: if attribute == "mountpoint": value = values[1] elif attribute == "source": value = values[0] except IndexError: return None return value def func_unmount_current_selection(self, o1 = None): if debuglev(9): ferror(f"func_unmount_current_selection: {o1}") # get current selection path = self.get_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...") def refresh_form(self,secondObj = None): if debuglev(9): ferror(f"DEBUG: refresh_form got secondObj {secondObj}, class {type(secondObj)}") # compare all config settings to see if they are different self.gmmapp.load_config(self.gmmapp.conffile) # this populates object "self.gmmapp.config" self.mounts_dir.set(self.gmmapp.config[appname]["mounts_dir"]) # reload mounts self.gmmapp.mounts = self.gmmapp.list_mounts() self.ml.delete(*self.ml.get_children()) for i in self.gmmapp.mounts: self.ml.insert("",tk.END, values=(i["source"],i["mountpoint"])) def func_double_click_entry(self, o1 = None, o2 = None, o3 = None): """ Open the mounted directory when double-clicked. """ if debuglev(9): ferror(f"DEBUG: double-click {o1},{o2},{o3}") # It is possible to have nothing selected, so just throw a warning path = self.get_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 save_settings(self,secondObj = None): if debuglev(1): ferror(f"DEBUG: saving config file from gui") self.gmmapp.config.set(appname,"mounts_dir",self.mounts_dir.get()) save_config(self.gmmapp.conffile) self.refresh_form() def func_mount_iso_dialog(self): """ Display a file chooser dialog to mount. """ filename = tk.filedialog.askopenfilename( filetypes = [ ("Disc image","*.iso"), ("All files","*") ] ) if filename: ferror(f"Got {filename}") self.gmmapp.mount_iso_to_default(filename,self.gmmapp.config) self.refresh_form() if "__main__" == __name__: # MAIN GRAPICAL APP app = gmm.Gmm(gmm.args) app.cli_main() if app.show_gui: root = tk.Tk() gmm_tk = TkApp(root, app) gmm_tk.mainloop()