363 lines
14 KiB
Python
363 lines
14 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 SSH/SFTP credentials.
|
|
"""
|
|
def __init__(self, master: Optional[tk.Tk] = None, title: str = "SFTP Connection"):
|
|
self.master = master
|
|
self.result = None
|
|
|
|
self.window = tk.Toplevel(master)
|
|
self.window.title(title)
|
|
self.window.resizable(False, 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.insert(0, "22")
|
|
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 Path:").grid(row=3, column=0, sticky="w", pady=2)
|
|
self.path_entry = ttk.Entry(frame, width=40, style="Creds.TEntry")
|
|
self.path_entry.insert(0, "~")
|
|
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)
|
|
|
|
# Buttons
|
|
button_frame = ttk.Frame(frame)
|
|
button_frame.grid(row=8, column=1, sticky="e", pady=(15, 0))
|
|
connect_button = ttk.Button(button_frame, text="Connect", 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._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 _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 _on_connect(self):
|
|
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,
|
|
}
|
|
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
|