import os from typing import List, Optional import tkinter as tk from tkinter import ttk try: from manager import LxTools except (ModuleNotFoundError, NameError): from shared_libs.common_tools import LxTools, IconManager class MessageDialog: """ A customizable message dialog window using tkinter for user interaction. """ def __init__( self, message_type: str = "info", text: str = "", buttons: List[str] = ["OK"], master: Optional[tk.Tk] = None, commands: List[Optional[callable]] = [None], icon: str = None, title: str = None, font: tuple = None, wraplength: int = None, ): self.message_type = message_type or "info" self.text = text self.buttons = buttons self.master = master self.result: bool = False self.icon = icon self.title = title self.window = tk.Toplevel(master) self.window.resizable(False, False) ttk.Style().configure("TButton") self.buttons_widgets = [] self.current_button_index = 0 icon_manager = IconManager() self.icons = { "error": icon_manager.get_icon("error_extralarge"), "info": icon_manager.get_icon("info_extralarge"), "warning": icon_manager.get_icon("warning_large"), "ask": icon_manager.get_icon("question_mark_extralarge"), } if self.icon: if isinstance(self.icon, str) and os.path.exists(self.icon): try: self.icons[self.message_type] = tk.PhotoImage(file=self.icon) except tk.TclError as e: print(f"Error loading custom icon from path '{self.icon}': {e}") elif isinstance(self.icon, tk.PhotoImage): self.icons[self.message_type] = self.icon self.window.title(self._get_title() if not self.title else self.title) window_icon = self.icons.get(self.message_type) if window_icon: self.window.iconphoto(False, window_icon) frame = ttk.Frame(self.window) frame.pack(expand=True, fill="both", padx=15, pady=8) frame.grid_rowconfigure(0, weight=1) frame.grid_columnconfigure(0, weight=1) frame.grid_columnconfigure(1, weight=3) icon_label = ttk.Label(frame, image=self.icons.get(self.message_type)) pady_value = 5 if self.icon is not None else 15 icon_label.grid( row=0, column=0, padx=(20, 10), pady=(pady_value, 15), sticky="nsew" ) text_label = tk.Label( frame, text=text, wraplength=wraplength if wraplength else 300, justify="left", anchor="center", font=font if font else ("Helvetica", 12), pady=20, ) text_label.grid(row=0, column=1, padx=(10, 20), sticky="nsew") self.button_frame = ttk.Frame(frame) self.button_frame.grid(row=1, columnspan=2, pady=(8, 10)) for i, btn_text in enumerate(buttons): if commands and len(commands) > i and commands[i] is not None: btn = ttk.Button(self.button_frame, text=btn_text, command=commands[i]) else: btn = ttk.Button(self.button_frame, text=btn_text, command=lambda t=btn_text: self._on_button_click(t)) padx_value = 50 if self.icon is not None and len(buttons) == 2 else 10 btn.pack(side="left" if i == 0 else "right", padx=padx_value, pady=5) if i == 0: btn.focus_set() self.buttons_widgets.append(btn) self.window.bind("", lambda event: self._on_enter_pressed()) self.window.bind("", lambda event: self._navigate_left()) self.window.bind("", lambda event: self._navigate_right()) self.window.update_idletasks() self.window.grab_set() self.window.attributes("-alpha", 0.0) self.window.after(200, lambda: self.window.attributes("-alpha", 100.0)) self.window.update() LxTools.center_window_cross_platform(self.window, self.window.winfo_width(), self.window.winfo_height()) self.window.protocol("WM_DELETE_WINDOW", lambda: self._on_button_click("Cancel")) def _get_title(self) -> str: return {"error": "Error", "info": "Info", "ask": "Question", "warning": "Warning"}[self.message_type] def _navigate_left(self): if not self.buttons_widgets: return self.current_button_index = (self.current_button_index - 1) % len(self.buttons_widgets) self.buttons_widgets[self.current_button_index].focus_set() def _navigate_right(self): if not self.buttons_widgets: return self.current_button_index = (self.current_button_index + 1) % len(self.buttons_widgets) self.buttons_widgets[self.current_button_index].focus_set() def _on_enter_pressed(self): focused = self.window.focus_get() if isinstance(focused, ttk.Button): focused.invoke() def _on_button_click(self, button_text: str) -> None: if len(self.buttons) >= 3 and button_text.lower() in ["cancel", "abort", "exit"]: self.result = None elif button_text.lower() in ["yes", "ok", "continue", "next", "start", "select"]: self.result = True else: self.result = False self.window.destroy() def show(self) -> Optional[bool]: self.window.wait_window() return self.result class CredentialsDialog: """ A dialog for securely entering and editing SSH/SFTP credentials. """ def __init__(self, master: Optional[tk.Tk] = None, title: str = "SFTP Connection", initial_data: Optional[dict] = None, is_edit_mode: bool = False): self.master = master self.result = None self.is_edit_mode = is_edit_mode self.initial_data = initial_data or {} self.window = tk.Toplevel(master) self.window.title(title) self.window.resizable(False, False) try: import keyring self.keyring_available = True except ImportError: self.keyring_available = False style = ttk.Style(self.window) style.configure("Creds.TEntry", padding=(5, 2)) frame = ttk.Frame(self.window, padding=15) frame.pack(expand=True, fill="both") frame.grid_columnconfigure(1, weight=1) # Host ttk.Label(frame, text="Host:").grid(row=0, column=0, sticky="w", pady=2) self.host_entry = ttk.Entry(frame, width=40, style="Creds.TEntry") self.host_entry.grid(row=0, column=1, sticky="ew", pady=2) # Port ttk.Label(frame, text="Port:").grid(row=1, column=0, sticky="w", pady=2) self.port_entry = ttk.Entry(frame, width=10, style="Creds.TEntry") self.port_entry.grid(row=1, column=1, sticky="w", pady=2) # Username ttk.Label(frame, text="Username:").grid(row=2, column=0, sticky="w", pady=2) self.username_entry = ttk.Entry(frame, width=40, style="Creds.TEntry") self.username_entry.grid(row=2, column=1, sticky="ew", pady=2) # Initial Path ttk.Label(frame, text="Initial Remote Directory:").grid(row=3, column=0, sticky="w", pady=2) self.path_entry = ttk.Entry(frame, width=40, style="Creds.TEntry") self.path_entry.grid(row=3, column=1, sticky="ew", pady=2) # Auth Method ttk.Label(frame, text="Auth Method:").grid(row=4, column=0, sticky="w", pady=5) auth_frame = ttk.Frame(frame) auth_frame.grid(row=4, column=1, sticky="w", pady=2) self.auth_method = tk.StringVar(value="password") ttk.Radiobutton(auth_frame, text="Password", variable=self.auth_method, value="password", command=self._toggle_auth_fields).pack(side="left") ttk.Radiobutton(auth_frame, text="Key File", variable=self.auth_method, value="keyfile", command=self._toggle_auth_fields).pack(side="left", padx=10) # Password self.password_label = ttk.Label(frame, text="Password:") self.password_label.grid(row=5, column=0, sticky="w", pady=2) self.password_entry = ttk.Entry(frame, show="*", width=40, style="Creds.TEntry") self.password_entry.grid(row=5, column=1, sticky="ew", pady=2) # Key File self.keyfile_label = ttk.Label(frame, text="Key File:") self.keyfile_label.grid(row=6, column=0, sticky="w", pady=2) key_frame = ttk.Frame(frame) key_frame.grid(row=6, column=1, sticky="ew", pady=2) self.keyfile_entry = ttk.Entry(key_frame, width=36, style="Creds.TEntry") self.keyfile_entry.pack(side="left", fill="x", expand=True) self.keyfile_button = ttk.Button(key_frame, text="▼", width=2, command=self._show_key_menu) self.keyfile_button.pack(side="left", padx=(2,0)) # Passphrase self.passphrase_label = ttk.Label(frame, text="Passphrase:") self.passphrase_label.grid(row=7, column=0, sticky="w", pady=2) self.passphrase_entry = ttk.Entry(frame, show="*", width=40, style="Creds.TEntry") self.passphrase_entry.grid(row=7, column=1, sticky="ew", pady=2) # Bookmark self.bookmark_frame = ttk.LabelFrame(frame, text="Bookmark", padding=10) self.bookmark_frame.grid(row=8, column=0, columnspan=2, sticky="ew", pady=5) self.save_bookmark_var = tk.BooleanVar() self.save_bookmark_check = ttk.Checkbutton(self.bookmark_frame, text="Save as bookmark", variable=self.save_bookmark_var, command=self._toggle_bookmark_name) self.save_bookmark_check.pack(anchor="w") if not self.keyring_available: keyring_info_label = ttk.Label(self.bookmark_frame, text="Python 'keyring' library not found.\nPasswords will not be saved.", font=("TkDefaultFont", 9, "italic")) keyring_info_label.pack(anchor="w", pady=(5,0)) self.save_bookmark_check.config(state=tk.DISABLED) self.bookmark_name_label = ttk.Label(self.bookmark_frame, text="Bookmark Name:") self.bookmark_name_entry = ttk.Entry(self.bookmark_frame, style="Creds.TEntry") # Buttons button_frame = ttk.Frame(frame) button_frame.grid(row=9, column=1, sticky="e", pady=(15, 0)) connect_text = "Save Changes" if self.is_edit_mode else "Connect" connect_button = ttk.Button(button_frame, text=connect_text, command=self._on_connect) connect_button.pack(side="left", padx=5) cancel_button = ttk.Button(button_frame, text="Cancel", command=self._on_cancel) cancel_button.pack(side="left") self._populate_initial_data() self._toggle_auth_fields() self.window.bind("", lambda event: self._on_connect()) self.window.protocol("WM_DELETE_WINDOW", self._on_cancel) self.window.update_idletasks() self.window.grab_set() self.window.attributes("-alpha", 0.0) self.window.after(200, lambda: self.window.attributes("-alpha", 100.0)) self.window.update() LxTools.center_window_cross_platform(self.window, self.window.winfo_width(), self.window.winfo_height()) self.host_entry.focus_set() def _populate_initial_data(self): if not self.initial_data: self.port_entry.insert(0, "22") self.path_entry.insert(0, "~") return self.host_entry.insert(0, self.initial_data.get("host", "")) self.port_entry.insert(0, self.initial_data.get("port", "22")) self.username_entry.insert(0, self.initial_data.get("username", "")) self.path_entry.insert(0, self.initial_data.get("initial_path", "~")) if self.initial_data.get("key_file"): self.auth_method.set("keyfile") self.keyfile_entry.insert(0, self.initial_data.get("key_file", "")) else: self.auth_method.set("password") if self.is_edit_mode: # In edit mode, we don't show the "save as bookmark" option, # as we are already editing one. The name is fixed. self.bookmark_frame.grid_remove() # We still need to know the bookmark name for saving. self.bookmark_name_entry.insert(0, self.initial_data.get("bookmark_name", "")) def _get_ssh_keys(self) -> List[str]: ssh_path = os.path.expanduser("~/.ssh") keys = [] if os.path.isdir(ssh_path): try: for item in os.listdir(ssh_path): full_path = os.path.join(ssh_path, item) if os.path.isfile(full_path) and not item.endswith('.pub') and 'known_hosts' not in item: keys.append(full_path) except OSError: pass return keys def _show_key_menu(self): keys = self._get_ssh_keys() if not keys: return menu = tk.Menu(self.window, tearoff=0) for key_path in keys: menu.add_command(label=key_path, command=lambda k=key_path: self._select_key_from_menu(k)) x = self.keyfile_button.winfo_rootx() y = self.keyfile_button.winfo_rooty() + self.keyfile_button.winfo_height() menu.tk_popup(x, y) def _select_key_from_menu(self, key_path): self.keyfile_entry.delete(0, tk.END) self.keyfile_entry.insert(0, key_path) def _toggle_auth_fields(self): method = self.auth_method.get() if method == "password": self.password_label.grid() self.password_entry.grid() self.keyfile_label.grid_remove() self.keyfile_entry.master.grid_remove() self.passphrase_label.grid_remove() self.passphrase_entry.grid_remove() else: self.password_label.grid_remove() self.password_entry.grid_remove() self.keyfile_label.grid() self.keyfile_entry.master.grid() self.passphrase_label.grid() self.passphrase_entry.grid() self.window.update_idletasks() self.window.geometry("") def _toggle_bookmark_name(self): if self.save_bookmark_var.get(): self.bookmark_name_label.pack(anchor="w", pady=(5,0)) self.bookmark_name_entry.pack(fill="x") else: self.bookmark_name_label.pack_forget() self.bookmark_name_entry.pack_forget() self.window.update_idletasks() self.window.geometry("") def _on_connect(self): save_bookmark = self.save_bookmark_var.get() or self.is_edit_mode bookmark_name = self.bookmark_name_entry.get() if save_bookmark and not bookmark_name: # In edit mode, the bookmark name comes from initial_data, so this check is for new bookmarks if not self.is_edit_mode: MessageDialog(message_type="error", text="Bookmark name cannot be empty.", master=self.window).show() return self.result = { "host": self.host_entry.get(), "port": int(self.port_entry.get() or 22), "username": self.username_entry.get(), "initial_path": self.path_entry.get() or "/", "password": self.password_entry.get() if self.auth_method.get() == "password" else None, "key_file": self.keyfile_entry.get() if self.auth_method.get() == "keyfile" else None, "passphrase": self.passphrase_entry.get() if self.auth_method.get() == "keyfile" else None, "save_bookmark": save_bookmark, "bookmark_name": bookmark_name } self.window.destroy() def _on_cancel(self): self.result = None self.window.destroy() def show(self) -> Optional[dict]: self.window.wait_window() return self.result class InputDialog: """ A simple dialog for getting a single line of text input from the user. """ def __init__(self, parent, title: str, prompt: str, initial_value: str = ""): self.result = None self.window = tk.Toplevel(parent) self.window.title(title) self.window.transient(parent) self.window.resizable(False, False) frame = ttk.Frame(self.window, padding=15) frame.pack(expand=True, fill="both") ttk.Label(frame, text=prompt, wraplength=250).pack(pady=(0, 10)) self.entry = ttk.Entry(frame, width=40) self.entry.insert(0, initial_value) self.entry.pack(pady=5) self.entry.focus_set() self.entry.selection_range(0, tk.END) button_frame = ttk.Frame(frame) button_frame.pack(pady=(10, 0)) ok_button = ttk.Button(button_frame, text="OK", command=self._on_ok) ok_button.pack(side="left", padx=5) cancel_button = ttk.Button( button_frame, text="Cancel", command=self._on_cancel) cancel_button.pack(side="left", padx=5) self.window.bind("", lambda e: self._on_ok()) self.window.protocol("WM_DELETE_WINDOW", self._on_cancel) self.window.update_idletasks() self.window.grab_set() self.window.attributes("-alpha", 0.0) self.window.after(200, lambda: self.window.attributes("-alpha", 100.0)) self.window.update() LxTools.center_window_cross_platform( self.window, self.window.winfo_width(), self.window.winfo_height() ) def _on_ok(self): self.result = self.entry.get() if self.result: self.window.destroy() def _on_cancel(self): self.result = None self.window.destroy() def show(self) -> Optional[str]: self.window.wait_window() return self.result