Files
shared_libs/message.py

439 lines
18 KiB
Python

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("<Return>", lambda event: self._on_enter_pressed())
self.window.bind("<Left>", lambda event: self._navigate_left())
self.window.bind("<Right>", 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("<Return>", 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("<Return>", 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