335 lines
12 KiB
Python
335 lines
12 KiB
Python
import os
|
|
from typing import List, Optional, Dict
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
|
|
try:
|
|
from manager import LxTools
|
|
except (ModuleNotFoundError, NameError):
|
|
from shared_libs.common_tools import LxTools
|
|
|
|
|
|
class MessageDialog:
|
|
"""
|
|
A customizable message dialog window using tkinter for user interaction.
|
|
|
|
This class creates modal dialogs for displaying information, warnings, errors,
|
|
or questions to the user. It supports multiple button configurations, custom
|
|
icons, keyboard navigation, and command binding. The dialog is centered on the
|
|
screen and handles user interactions with focus management and accessibility.
|
|
|
|
Attributes:
|
|
message_type (str): Type of message ("info", "error", "warning", "ask").
|
|
text (str): Main message content to display.
|
|
buttons (List[str]): List of button labels (e.g., ["OK", "Cancel"]).
|
|
result (bool or None):
|
|
- True for positive actions (Yes, OK)
|
|
- False for negative actions (No, Cancel)
|
|
- None if "Cancel" was clicked with ≥3 buttons
|
|
icons: Dictionary mapping message types to tkinter.PhotoImage objects
|
|
|
|
Parameters:
|
|
message_type: Type of message dialog (default: "info")
|
|
text: Message content to display
|
|
buttons: List of button labels (default: ["OK"])
|
|
master: Parent tkinter window (optional)
|
|
commands: List of callables for each button (default: [None])
|
|
icon: Custom icon path (overrides default icons if provided)
|
|
title: Window title (default: derived from message_type)
|
|
font: Font tuple for text styling
|
|
wraplength: Text wrapping width in pixels
|
|
|
|
Methods:
|
|
_get_title(): Returns the default window title based on message type.
|
|
_load_icons(): Loads icons from system paths or fallback locations.
|
|
_on_button_click(button_text): Sets result and closes the dialog.
|
|
show(): Displays the dialog and waits for user response.
|
|
|
|
Example Usage:
|
|
|
|
1. Basic Info Dialog:
|
|
>>> MessageDialog(
|
|
... text="This is an information message.")
|
|
>>> result = dialog.show()
|
|
>>> print("User clicked OK:", result)
|
|
|
|
Notes:
|
|
My Favorite Example,
|
|
for simply information message:
|
|
|
|
>>> MessageDialog(text="This is an information message.")
|
|
>>> result = MessageDialog(text="This is an information message.").show()
|
|
|
|
Example Usage:
|
|
|
|
2. Error Dialog with Custom Command:
|
|
>>> def on_retry():
|
|
... print("User selected Retry")
|
|
|
|
>>> dialog = MessageDialog(
|
|
... message_type="error",
|
|
... text="An error occurred during processing.",
|
|
... buttons=["Retry", "Cancel"],
|
|
... commands=[on_retry, None],
|
|
... title="Critical Error"
|
|
... )
|
|
>>> result = dialog.show()
|
|
>>> print("User selected Retry:", result)
|
|
|
|
Example Usage:
|
|
|
|
3. And a special example for a "open link" button:
|
|
Be careful not to forget to import it into the script in which
|
|
this dialog is used!!! import webbrowser from functools import partial
|
|
|
|
>>> MessageDialog(
|
|
... "info"
|
|
... text="This is an information message.",
|
|
... buttons=["Yes", "Go to Exapmle"],
|
|
... commands=[
|
|
... None, # Default on "OK"
|
|
... partial(webbrowser.open, "https://exapmle.com"),
|
|
... ],
|
|
... icon="/pathh/to/custom/icon.png",
|
|
... title="Example",
|
|
... )
|
|
|
|
Notes:
|
|
- Returns None if "Cancel" was clicked with ≥3 buttons
|
|
- Supports keyboard navigation (Left/Right arrows and Enter)
|
|
- Dialog automatically centers on screen
|
|
- Result is False for window close (X) with 2 buttons
|
|
- Font and wraplength parameters enable text styling
|
|
"""
|
|
|
|
DEFAULT_ICON_PATH = "/usr/share/icons/lx-icons"
|
|
|
|
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" # Default is "info"
|
|
self.text = text
|
|
self.buttons = buttons
|
|
self.master = master
|
|
self.result: bool = False # Default is False
|
|
|
|
self.icon_path = self._get_icon_path()
|
|
self.icon = icon
|
|
self.title = title
|
|
# Window creation
|
|
self.window = tk.Toplevel(master)
|
|
self.window.grab_set()
|
|
self.window.resizable(False, False)
|
|
ttk.Style().configure("TButton", font=("Helvetica", 11), padding=5)
|
|
self.buttons_widgets = []
|
|
self.current_button_index = 0
|
|
self._load_icons()
|
|
|
|
# Window title and icon
|
|
self.window.title(self._get_title() if not self.title else self.title)
|
|
self.window.iconphoto(False, self.icons[self.message_type])
|
|
|
|
# Layout
|
|
frame = ttk.Frame(self.window)
|
|
frame.pack(expand=True, fill="both")
|
|
|
|
# Grid-Configuration
|
|
frame.grid_rowconfigure(0, weight=1)
|
|
frame.grid_columnconfigure(0, weight=1)
|
|
frame.grid_columnconfigure(1, weight=3)
|
|
|
|
# Icon and Text
|
|
icon_label = ttk.Label(frame, image=self.icons[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),
|
|
pady=(8, 20),
|
|
sticky="nsew",
|
|
)
|
|
|
|
# Create button frame
|
|
self.button_frame = ttk.Frame(frame)
|
|
self.button_frame.grid(row=1, columnspan=2, pady=(15, 10))
|
|
|
|
for i, btn_text in enumerate(buttons):
|
|
if commands and len(commands) > i and commands[i] is not None:
|
|
# Button with individual command
|
|
btn = ttk.Button(
|
|
self.button_frame,
|
|
text=btn_text,
|
|
command=commands[i],
|
|
)
|
|
else:
|
|
# Default button set self.result and close window
|
|
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=15)
|
|
btn.focus_set() if i == 0 else None # Set focus on first button
|
|
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.attributes("-alpha", 0.0) # 100% Transparencence
|
|
self.window.after(200, lambda: self.window.attributes("-alpha", 100.0))
|
|
self.window.update() # Window update before centering!
|
|
LxTools.center_window_cross_platform(
|
|
self.window, self.window.winfo_width(), self.window.winfo_height()
|
|
)
|
|
|
|
# Close Window on Cancel
|
|
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 _load_icons(self):
|
|
# Try to load the icon from the provided path
|
|
self.icons = {}
|
|
icon_paths: Dict[str, str] = {
|
|
"error": os.path.join(self.icon_path, "64/error.png"),
|
|
"info": os.path.join(self.icon_path, "64/info.png"),
|
|
"warning": os.path.join(self.icon_path, "64/warning.png"),
|
|
"ask": os.path.join(self.icon_path, "64/question_mark.png"),
|
|
}
|
|
|
|
fallback_paths: Dict[str, str] = {
|
|
"error": "./lx-icons/64/error.png",
|
|
"info": "./lx-icons/64/info.png",
|
|
"warning": "./lx-icons/64/warning.png",
|
|
"ask": "./lx-icons/64/question_mark.png",
|
|
}
|
|
|
|
for key in icon_paths:
|
|
try:
|
|
# Check if an individual icon is provided
|
|
if (
|
|
self.message_type == key
|
|
and self.icon is not None
|
|
and os.path.exists(self.icon)
|
|
):
|
|
try:
|
|
self.icons[key] = tk.PhotoImage(file=self.icon)
|
|
except Exception as e:
|
|
print(
|
|
f"Erro on loading individual icon '{key}': {e}\n",
|
|
"Try to use the default icon",
|
|
)
|
|
|
|
else:
|
|
# Check for standard path
|
|
if os.path.exists(icon_paths[key]):
|
|
self.icons[key] = tk.PhotoImage(file=icon_paths[key])
|
|
else:
|
|
if os.path.exists(fallback_paths[key]):
|
|
self.icons[key] = tk.PhotoImage(file=fallback_paths[key])
|
|
except Exception as e:
|
|
print(f"Error on load Icon '{[key]}': {e}")
|
|
self.icons[key] = tk.PhotoImage()
|
|
print(f"⚠️ No Icon found for '{key}'. Use standard Tkinter icon.")
|
|
|
|
return self.icons
|
|
|
|
def _get_icon_path(self) -> str:
|
|
"""Get the path to the default icon."""
|
|
if os.path.exists(self.DEFAULT_ICON_PATH):
|
|
return self.DEFAULT_ICON_PATH
|
|
else:
|
|
# Fallback to the directory of the script
|
|
return os.path.dirname(os.path.abspath(__file__))
|
|
|
|
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:
|
|
"""
|
|
Sets `self.result` based on the clicked button.
|
|
- Returns `None` if the button is "Cancel", "Abort", or "Exit" **and** there are 3 or more buttons.
|
|
- Returns `True` if the button is "Yes", "Ok", "Continue", "Next", or "Start".
|
|
- Returns `False` in all other cases (e.g., "No", closing with X, or fewer than 3 buttons).
|
|
"""
|
|
# Check: If there are 3+ buttons and the button text matches "Cancel", "Abort", or "Exit"
|
|
if len(self.buttons) >= 3 and button_text.lower() in [
|
|
"cancel",
|
|
"abort",
|
|
"exit",
|
|
]:
|
|
self.result = None
|
|
# Check: Button text is "Yes", "Ok", "Continue", "Next", or "Start"
|
|
elif button_text.lower() in ["yes", "ok", "continue", "next", "start"]:
|
|
self.result = True
|
|
else:
|
|
# Fallback for all other cases (e.g., "No", closing with X, or fewer than 3 buttons)
|
|
self.result = False
|
|
|
|
self.window.destroy()
|
|
|
|
def show(self) -> Optional[bool]:
|
|
"""
|
|
Displays the dialog window and waits for user interaction.
|
|
|
|
Returns:
|
|
bool or None:
|
|
- `True` if "Yes", "Ok", etc. was clicked.
|
|
- `False` if "No" was clicked, or the window was closed with X (when there are 2 buttons).
|
|
- `None` if "Cancel", "Abort", or "Exit" was clicked **and** there are 3+ buttons,
|
|
or the window was closed with X (when there are 3+ buttons).
|
|
"""
|
|
self.window.wait_window()
|
|
return self.result
|