11 Commits

7 changed files with 402 additions and 132 deletions

View File

@ -4,7 +4,24 @@ Changelog for shared_libs
- add Info Window for user in delete logfile
bevore delete logfile.
### Added
15-06-2025
- Update MessageDialog Class description
- import LxTools with try exception.
### Added
14-06-2025
- Added new MessageDialog module
and replace LxTools.msg_window() with MessageDialog.
### Added
03-06-2025
- in common_tools CryptUtils.decrypt() method
remove check file .dat is exist in path.
### Added
03-06-2025

View File

@ -1,3 +1,12 @@
# shared_libs
Module Project for apps by git.ilunix.de
Module Project for apps by git.ilunix.de
Examples with a Theme from Projekt Wire-Py
# Screenshots
[![info_example.png](https://fb.ilunix.de/api/public/dl/KtaTPMMq?inline=true)](https://fb.ilunix.de/share/KtaTPMMq)
[![error_example.png](https://fb.ilunix.de/api/public/dl/cRO_ksrM?inline=true)](https://fb.ilunix.de/share/cRO_ksrM)
[![warning_example.png](https://fb.ilunix.de/api/public/dl/1JEdSJcI?inline=true)](https://fb.ilunix.de/share/1JEdSJcI)
[![question_light_example.png](https://fb.ilunix.de/api/public/dl/XxNey7y7?inline=true)](https://fb.ilunix.de/share/1XxNey7y7)
[![question_dark_example.png](https://fb.ilunix.de/api/public/dl/4HCxiNwB?inline=true)](https://fb.ilunix.de/share/4HCxiNwB)
[![example_with_own_title_and_icon.png](https://fb.ilunix.de/api/public/dl/uui8b1xx?inline=true)](https://fb.ilunix.de/share/uui8b1xx)

View File

@ -10,7 +10,7 @@ import shutil
import tkinter as tk
from typing import Optional, Dict, Any, NoReturn
from pathlib import Path
from tkinter import ttk, Toplevel
from tkinter import Toplevel
class CryptoUtil:
@ -21,31 +21,29 @@ class CryptoUtil:
"""
@staticmethod
def decrypt(user, path) -> None:
def decrypt(user) -> None:
"""
Starts SSL dencrypt
"""
if len([file.stem for file in path.glob("*.dat")]) == 0:
pass
process: CompletedProcess[str] = run(
["pkexec", "/usr/local/bin/ssl_decrypt.py", "--user", user],
capture_output=True,
text=True,
check=False,
)
# Output from Openssl Error
if process.stderr:
logging.error(process.stderr, exc_info=True)
if process.returncode == 0:
logging.info("Files successfully decrypted...", exc_info=True)
else:
process: CompletedProcess[str] = run(
["pkexec", "/usr/local/bin/ssl_decrypt.py", "--user", user],
capture_output=True,
text=True,
check=False,
logging.error(
f"Error process decrypt: Code {process.returncode}", exc_info=True
)
# Output from Openssl Error
if process.stderr:
logging.error(process.stderr, exc_info=True)
if process.returncode == 0:
logging.info("Files successfully decrypted...", exc_info=True)
else:
logging.error(
f"Error process decrypt: Code {process.returncode}", exc_info=True
)
@staticmethod
def encrypt(user) -> None:
"""
@ -241,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:
"""

View File

@ -5,7 +5,7 @@ import requests
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 +72,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"]
)

View File

@ -51,9 +51,9 @@ class AppConfig:
# Updates
# 1 = 1. Year, 09 = Month of the Year, 2924 = Day and Year of the Year
VERSION: str = "v. 1.06.3125"
UPDATE_URL: str = "https://git.ilunix.de/api/v1/repos/punix/Wire-Py/releases"
DOWNLOAD_URL: str = "https://git.ilunix.de/punix/Wire-Py/archive"
VERSION: str = "v. 1.06.0325"
UPDATE_URL: str = "https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases"
DOWNLOAD_URL: str = "https://git.ilunix.de/punix/shared_libs/archive"
# UI configuration
UI_CONFIG: Dict[str, Any] = {

View File

@ -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,
@ -14,7 +17,6 @@ from shared_libs.common_tools import (
)
import sys
from file_and_dir_ensure import prepare_app_environment
import webbrowser
class LogViewer(tk.Tk):
@ -224,10 +226,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 +233,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 +452,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 +470,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():

332
message.py Normal file
View File

@ -0,0 +1,332 @@
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", 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[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("<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