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. 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. _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 """ 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 = icon self.title = title # Window creation self.window = tk.Toplevel(master) self.window.grab_set() self.window.resizable(False, False) ttk.Style().configure("TButton") self.buttons_widgets = [] self.current_button_index = 0 # Load icons using IconManager 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"), } # Handle custom icon override if self.icon: if isinstance(self.icon, str) and os.path.exists(self.icon): # If it's a path, load it 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): # If it's already a PhotoImage, use it directly self.icons[self.message_type] = self.icon # Window title and 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) # Layout frame = ttk.Frame(self.window) frame.pack(expand=True, fill="both", padx=15, pady=8) # 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.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", ) # Create button frame 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: # 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=5) btn.focus_set() if i == 0 else None # Set focus on first button 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.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 _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