From c2552696e4272caae5bcbefc327af9f73b75286f Mon Sep 17 00:00:00 2001 From: punix Date: Sat, 14 Jun 2025 22:55:47 +0200 Subject: [PATCH] add new modul MessageDialog and replace old message dialog --- Changelog | 6 + common_tools.py | 66 --------- gitea.py | 22 +-- logviewer.py | 38 ++--- message.py | 384 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 411 insertions(+), 105 deletions(-) create mode 100644 message.py diff --git a/Changelog b/Changelog index 685dde2..0488a9d 100644 --- a/Changelog +++ b/Changelog @@ -5,6 +5,12 @@ Changelog for shared_libs - add Info Window for user in delete logfile bevore delete logfile. + ### Added +14-06-2025 + + - Added new MessageDialog module + and replace LxTools.msg_window() with MessageDialog. + ### Added 03-06-2025 diff --git a/common_tools.py b/common_tools.py index 483f496..7d24ce3 100755 --- a/common_tools.py +++ b/common_tools.py @@ -239,72 +239,6 @@ class LxTools: except FileNotFoundError: pass - @staticmethod - def msg_window( - image_path: Path, - image_path2: Path, - w_title: str, - w_txt: str, - txt2: Optional[str] = None, - com: Optional[str] = None, - ) -> None: - """ - Creates message windows - - :param image_path2: - :param image_path: - AppConfig.IMAGE_PATHS["icon_info"] = Image for TK window which is displayed to the left of the text - AppConfig.IMAGE_PATHS["icon_vpn"] = Image for Task Icon - :argument w_title = Windows Title - :argument w_txt = Text for Tk Window - :argument txt2 = Text for Button two - :argument com = function for Button command - """ - msg: tk.Toplevel = tk.Toplevel() - msg.resizable(width=False, height=False) - msg.title(w_title) - msg.configure(pady=15, padx=15) - - # load first image for a window - try: - msg.img = tk.PhotoImage(file=image_path) - msg.i_window = tk.Label(msg, image=msg.img) - except Exception as e: - logging.error(f"Error on load Window Image: {e}", exc_info=True) - msg.i_window = tk.Label(msg, text="Image not found") - - label: tk.Label = tk.Label(msg, text=w_txt) - label.grid(column=1, row=0) - - if txt2 is not None and com is not None: - label.config(font=("Ubuntu", 11), padx=15, justify="left") - msg.i_window.grid(column=0, row=0, sticky="nw") - button2: ttk.Button = ttk.Button( - msg, text=f"{txt2}", command=com, padding=4 - ) - button2.grid(column=0, row=1, sticky="e", columnspan=2) - button: ttk.Button = ttk.Button( - msg, text="OK", command=msg.destroy, padding=4 - ) - button.grid(column=0, row=1, sticky="w", columnspan=2) - else: - label.config(font=("Ubuntu", 11), padx=15) - msg.i_window.grid(column=0, row=0) - button: ttk.Button = ttk.Button( - msg, text="OK", command=msg.destroy, padding=4 - ) - button.grid(column=0, columnspan=2, row=1) - - try: - icon = tk.PhotoImage(file=image_path2) - msg.iconphoto(True, icon) - except Exception as e: - logging.error(f"Error loading the window icon: {e}", exc_info=True) - - msg.columnconfigure(0, weight=1) - msg.rowconfigure(0, weight=1) - msg.winfo_toplevel() - @staticmethod def sigi(file_path: Optional[Path] = None, file: Optional[Path] = None) -> None: """ diff --git a/gitea.py b/gitea.py index 7818509..3ecb109 100644 --- a/gitea.py +++ b/gitea.py @@ -6,6 +6,7 @@ from pathlib import Path import subprocess import shutil from shared_libs.common_tools import LxTools +from shared_libs.message import MessageDialog class GiteaUpdate: @@ -72,29 +73,18 @@ class GiteaUpdate: if result == 0: shutil.chown(f"{Path.home()}/{res}.zip", 1000, 1000) - LxTools.msg_window( - AppConfig.IMAGE_PATHS["icon_info"], - AppConfig.IMAGE_PATHS["icon_download"], - Msg.STR["title"], - Msg.STR["ok_message"], - ) + MessageDialog("info", text=Msg.STR["ok_message"]) else: - LxTools.msg_window( - AppConfig.IMAGE_PATHS["icon_error"], - AppConfig.IMAGE_PATHS["icon_download_error"], - Msg.STR["error_title"], - Msg.STR["error_massage"], + MessageDialog( + "error", text=Msg.STR["error_message"], title=Msg.STR["error_title"] ) except subprocess.CalledProcessError: - LxTools.msg_window( - AppConfig.IMAGE_PATHS["icon_error"], - AppConfig.IMAGE_PATHS["icon_msg"], - Msg.STR["error_title"], - Msg.STR["error_no_internet"], + MessageDialog( + "error", text=Msg.STR["error_no_internet"], title=Msg.STR["error_title"] ) diff --git a/logviewer.py b/logviewer.py index 1ae6fce..bf4944c 100755 --- a/logviewer.py +++ b/logviewer.py @@ -4,7 +4,10 @@ import logging import tkinter as tk from tkinter import TclError, filedialog, ttk from pathlib import Path +import webbrowser +from functools import partial from shared_libs.gitea import GiteaUpdate +from shared_libs.message import MessageDialog from shared_libs.common_tools import ( LogConfig, ConfigManager, @@ -224,10 +227,6 @@ class LogViewer(tk.Tk): """ a tk.Toplevel window """ - - def link_btn() -> None: - webbrowser.open("https://git.ilunix.de/punix/shared_libs") - msg_t = _( "Logviewer a simple Gui for View Logfiles.\n\n" "Logviewer is open source software written in Python.\n\n" @@ -235,13 +234,16 @@ class LogViewer(tk.Tk): "Use without warranty!\n" ) - LxTools.msg_window( - modul_name.AppConfig.IMAGE_PATHS["icon_log"], - modul_name.AppConfig.IMAGE_PATHS["icon_log"], - _("Info"), - msg_t, - _("Go to shared_libs git"), - link_btn, + MessageDialog( + "info", + text=msg_t, + buttons=["OK", "Go to Logviewer"], + commands=[ + None, # Default on "OK" + partial(webbrowser.open, "https://git.ilunix.de/punix/shared_libs"), + ], + icon=modul_name.AppConfig.IMAGE_PATHS["icon_log"], + title="Logviewer", ) def update_setting(self, update_res, modul_name, _) -> None: @@ -451,12 +453,7 @@ class LogViewer(tk.Tk): self.text_area.insert(tk.END, file.read()) except Exception as e: logging.error(_(f"A mistake occurred: {str(e)}")) - LxTools.msg_window( - modul_name.AppConfig.IMAGE_PATHS["icon_error"], - modul_name.AppConfig.IMAGE_PATHS["icon_log"], - "LogViewer", - _(f"A mistake occurred:\n{str(e)}\n"), - ) + MessageDialog("error", _(f"A mistake occurred:\n{str(e)}\n")) def directory_load(self, modul_name, _): @@ -474,12 +471,7 @@ class LogViewer(tk.Tk): print("File load: abort by user...") except Exception as e: logging.error(_(f"A mistake occurred: {e}")) - LxTools.msg_window( - modul_name.AppConfig.IMAGE_PATHS["icon_error"], - modul_name.AppConfig.IMAGE_PATHS["icon_log"], - "LogViewer", - _(f"A mistake occurred:\n{e}\n"), - ) + MessageDialog("error", _(f"A mistake occurred:\n{e}\n")) def main(): diff --git a/message.py b/message.py new file mode 100644 index 0000000..342483b --- /dev/null +++ b/message.py @@ -0,0 +1,384 @@ +import os +from typing import List, Optional, Dict +import tkinter as tk +from tkinter import ttk +from manager import Center + +""" +#################################################### +Attention! MessageDialog returns different values. +From 3 buttons with Cancel, Cancel and the Close (x) +None returns. otherwise always False. +#################################################### +Usage Examples +1. Basic Info Dialog +from tkinter import Tk + +root = Tk() +dialog = MessageDialog( + message_type="info", + text="This is an information message.", + buttons=["OK"], + master=root, +) +result = dialog.show() +print("User clicked OK:", result) + +----------------------------------------------------- +My Favorite Example, +for simply information message: + +MessageDialog(text="This is an information message.") +result = MessageDialog(text="This is an information message.").show() +----------------------------------------------------- +Explanation: if you need the return value e.g. in the vaiable result, +you need to add .show(). otherwise only if no root.mainloop z.b is used to test the window. +##################################################### + +2. Error Dialog with Custom Icon and Command +def on_cancel(): + print("User canceled the operation.") + +root = Tk() +result = MessageDialog( + message_type="error", + text="An error occurred during processing.", + buttons=["Retry", "Cancel"], + commands=[None, on_cancel], + icon="/path/to/custom/error_icon.png", + title="Critical Error" +).show() + +print("User clicked Retry:", result) + +----------------------------------------------------- +My Favorite Example, +for simply Error message: + +MessageDialog( + "error", + text="An error occurred during processing.", +).show() + +##################################################### + +3. Confirmation Dialog with Yes/No Buttons +def on_confirm(): + print("User confirmed the action.") + +root = Tk() +dialog = MessageDialog( + message_type="ask", + text="Are you sure you want to proceed?", + buttons=["Yes", "No"], + commands=[on_confirm, None], # Either use comando or work with the values True and False +) +result = dialog.show() +print("User confirmed:", result) +----------------------------------------------------- + +My Favorite Example, +for simply Question message: + +dialog = MessageDialog( + "ask", + text="Are you sure you want to proceed?", + buttons=["Yes", "No"] + ).show() +##################################################### + +4. Warning Dialog with Custom Title + +root = Tk() +dialog = MessageDialog( + message_type="warning", + text="This action cannot be undone.", + buttons=["Proceed", "Cancel"], + title="Warning: Irreversible Action" +) +result = dialog.show() +print("User proceeded:", result) +----------------------------------------------------- +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 + +dialog = MessageDialog( + "ask", + text="Are you sure you want to proceed?", + buttons=["Yes", "Go to Exapmle"], + commands=[ + None, # Default on "OK" + partial(webbrowser.open, "https://exapmle.com"), + ], + icon="/pathh/to/custom/icon.png", + title="Example", +).show() + + +In all dialogues, a font can also be specified as a tuple. With font=("ubuntu", 11) +and wraplength=300, the text is automatically wrapped. +""" + + +class MessageDialog: + """ + A customizable message dialog window using tkinter. + + This class creates modal dialogs for displaying information, warnings, errors, + or questions to the user. It supports multiple button configurations and custom + icons. The dialog is centered on the screen and handles user interactions. + + Attributes: + message_type (str): Type of message ("info", "error", "warning", "ask"). + text (str): Main message content. + buttons (List[str]): List of button labels (e.g., ["OK", "Cancel"]). + result (bool): True if the user clicked a positive button (like "Yes" or "OK"), else False. + 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). + + 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 (returns self.result). + """ + + 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("", 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! + Center.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: + 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 -- 2.49.0