Reduced redundancy, logviewer fulll removed , add log_window and menu_bar

This commit is contained in:
2025-08-12 22:44:49 +02:00
parent 80aebe3bab
commit dc51bf6f2c
32 changed files with 1431 additions and 1617 deletions

View File

@@ -1,16 +1,21 @@
""" Classes Method and Functions for lx Apps """
" Classes Method and Functions for lx Apps "
import logging
import signal
import base64
from contextlib import contextmanager
from .logger import app_logger
from subprocess import CompletedProcess, run
import gettext
import locale
import re
import sys
import shutil
import tkinter as tk
from tkinter import ttk
import os
from typing import Optional, Dict, Any, NoReturn
from pathlib import Path
from tkinter import Toplevel
class CryptoUtil:
@@ -34,14 +39,14 @@ class CryptoUtil:
# Output from Openssl Error
if process.stderr:
logging.error(process.stderr, exc_info=True)
app_logger.log(process.stderr)
if process.returncode == 0:
logging.info("Files successfully decrypted...", exc_info=True)
app_logger.log("Files successfully decrypted...")
else:
logging.error(
f"Error process decrypt: Code {process.returncode}", exc_info=True
app_logger.log(
f"Error process decrypt: Code {process.returncode}"
)
@staticmethod
@@ -58,13 +63,13 @@ class CryptoUtil:
# Output from Openssl Error
if process.stderr:
logging.error(process.stderr, exc_info=True)
app_logger.log(process.stderr)
if process.returncode == 0:
logging.info("Files successfully encrypted...", exc_info=True)
app_logger.log("Files successfully encrypted...")
else:
logging.error(
f"Error process encrypt: Code {process.returncode}", exc_info=True
app_logger.log(
f"Error process encrypt: Code {process.returncode}"
)
@staticmethod
@@ -83,9 +88,8 @@ class CryptoUtil:
return True
elif "False" in process.stdout:
return False
logging.error(
f"Unexpected output from the external script:\nSTDOUT: {process.stdout}\nSTDERR: {process.stderr}",
exc_info=True,
app_logger.log(
f"Unexpected output from the external script:\nSTDOUT: {process.stdout}\nSTDERR: {process.stderr}"
)
return False
@@ -110,7 +114,7 @@ class CryptoUtil:
if len(decoded) != 32: # 32 bytes = 256 bits
return False
except Exception as e:
logging.error(f"Error on decode Base64: {e}", exc_info=True)
app_logger.log(f"Error on decode Base64: {e}")
return False
return True
@@ -273,17 +277,16 @@ class LxTools:
# End program for certain signals, report to others only reception
if signum in (signal.SIGINT, signal.SIGTERM):
exit_code: int = 1
logging.error(
f"\nSignal {signal_name} {signum} received. => Aborting with exit code {exit_code}.",
exc_info=True,
app_logger.log(
f"\nSignal {signal_name} {signum} received. => Aborting with exit code {exit_code}."
)
LxTools.clean_files(file_path, file)
logging.info("Breakdown by user...")
app_logger.log("Breakdown by user...")
sys.exit(exit_code)
else:
logging.info(f"Signal {signum} received and ignored.")
app_logger.log(f"Signal {signum} received and ignored.")
LxTools.clean_files(file_path, file)
logging.error("Process unexpectedly ended...")
app_logger.log("Process unexpectedly ended...")
# Register signal handlers for various signals
signal.signal(signal.SIGINT, signal_handler)
@@ -324,14 +327,14 @@ class ConfigManager:
"""Load the config file and return the config as dict"""
if not cls._config:
try:
lines = Path(cls._config_file).read_text(encoding="utf-8").splitlines()
lines = Path(cls._config_file).read_text(
encoding="utf-8").splitlines()
cls._config = {
"updates": lines[1].strip(),
"theme": lines[3].strip(),
"tooltips": lines[5].strip()
== "True", # is converted here to boolean!!!
"autostart": lines[7].strip() if len(lines) > 7 else "off",
"logfile": lines[9].strip(),
}
except (IndexError, FileNotFoundError):
# DeDefault values in case of error
@@ -340,7 +343,6 @@ class ConfigManager:
"theme": "light",
"tooltips": "True", # Default Value as string!
"autostart": "off",
"logfile": LOG_FILE_PATH,
}
return cls._config
@@ -357,8 +359,6 @@ class ConfigManager:
f"{str(cls._config['tooltips'])}\n",
"# Autostart\n",
f"{cls._config['autostart']}\n",
"# Logfile\n",
f"{cls._config['logfile']}\n",
]
Path(cls._config_file).write_text("".join(lines), encoding="utf-8")
@@ -396,110 +396,435 @@ class ThemeManager:
@staticmethod
def change_theme(root, theme_in_use, theme_name=None):
"""Change application theme centrally"""
"""
Change application theme centrally.
Args:
root: The root Tkinter window.
theme_in_use (str): The name of the theme to apply.
theme_name (Optional[str]): The name of the theme to save in the config.
If None, the theme is not saved.
"""
root.tk.call("set_theme", theme_in_use)
if theme_in_use == theme_name:
ConfigManager.set("theme", theme_in_use)
class Tooltip:
"""Class for Tooltip
from common_tools.py import Tooltip
example: Tooltip(label, "Show tooltip on label")
example: Tooltip(button, "Show tooltip on button")
example: Tooltip(widget, "Text", state_var=tk.BooleanVar())
example: Tooltip(widget, "Text", state_var=tk.BooleanVar(), x_offset=20, y_offset=10)
"""
A flexible tooltip class for Tkinter widgets that supports dynamic activation/deactivation.
info: label and button are parent widgets.
NOTE: When using with state_var, pass the tk.BooleanVar object directly,
NOT its value. For example: use state_var=my_bool_var, NOT state_var=my_bool_var.get()
This class provides customizable tooltips that appear when the mouse hovers over a widget.
It can be used for simple, always-active tooltips or for tooltips whose visibility is
controlled by a `tk.BooleanVar`, allowing for global enable/disable functionality.
Attributes:
widget (tk.Widget): The Tkinter widget to which the tooltip is attached.
text (str): The text to display in the tooltip.
wraplength (int): The maximum line length for the tooltip text before wrapping.
state_var (Optional[tk.BooleanVar]): An optional Tkinter BooleanVar that controls
the visibility of the tooltip. If True, the tooltip
is active; if False, it is inactive. If None, the
tooltip is always active.
tooltip_window (Optional[tk.Toplevel]): The Toplevel window used to display the tooltip.
id (Optional[str]): The ID of the `after` job used to schedule the tooltip display.
Usage Examples:
# 1. Simple Tooltip (always active):
# Tooltip(my_button, "This is a simple tooltip.")
# 2. State-Controlled Tooltip (can be enabled/disabled globally):
# tooltip_state = tk.BooleanVar(value=True)
# Tooltip(my_button, "This tooltip can be turned off!", state_var=tooltip_state)
# # To toggle visibility:
# # tooltip_state.set(False) # Tooltips will hide
# # tooltip_state.set(True) # Tooltips will show again
"""
def __init__(
self,
widget: Any,
text: str,
state_var: Optional[tk.BooleanVar] = None,
x_offset: int = 65,
y_offset: int = 40,
) -> None:
"""Tooltip Class"""
self.widget: Any = widget
self.text: str = text
self.tooltip_window: Optional[Toplevel] = None
def __init__(self, widget, text, wraplength=250, state_var=None):
self.widget = widget
self.text = text
self.wraplength = wraplength
self.state_var = state_var
self.x_offset = x_offset
self.y_offset = y_offset
# Initial binding based on the current state
self.tooltip_window = None
self.id = None
self.update_bindings()
# Add trace to the state_var if provided
if self.state_var is not None:
if self.state_var:
self.state_var.trace_add("write", self.update_bindings)
def update_bindings(self, *args) -> None:
"""Updates the bindings based on the current state"""
# Remove existing bindings first
def update_bindings(self, *args):
"""
Updates the event bindings for the widget based on the current state_var.
If state_var is True or None, the <Enter>, <Leave>, and <ButtonPress> events
are bound to show/hide the tooltip. Otherwise, they are unbound.
"""
self.widget.unbind("<Enter>")
self.widget.unbind("<Leave>")
self.widget.unbind("<ButtonPress>")
# Add new bindings if tooltips are enabled
if self.state_var is None or self.state_var.get():
self.widget.bind("<Enter>", self.show_tooltip)
self.widget.bind("<Leave>", self.hide_tooltip)
self.widget.bind("<Enter>", self.enter)
self.widget.bind("<Leave>", self.leave)
self.widget.bind("<ButtonPress>", self.leave)
def show_tooltip(self, event: Optional[Any] = None) -> None:
"""Shows the tooltip"""
def enter(self, event=None):
"""
Handles the <Enter> event. Schedules the tooltip to be shown after a delay
if tooltips are enabled (via state_var).
"""
try:
# Check if a modal dialog has the grab on the entire application
if self.winfo_toplevel().grab_current() is not None:
return
except Exception:
# This can happen if no grab is active. We can safely ignore it.
pass
if self.state_var is None or self.state_var.get():
self.schedule()
def leave(self, event=None):
"""
Handles the <Leave> event. Unschedules any pending tooltip display
and immediately hides any visible tooltip.
"""
self.unschedule()
self.hide_tooltip()
def schedule(self):
"""
Schedules the `show_tooltip` method to be called after a short delay.
Cancels any previously scheduled calls to prevent flickering.
"""
self.unschedule()
self.id = self.widget.after(250, self.show_tooltip)
def unschedule(self):
"""
Cancels any pending `show_tooltip` calls.
"""
id = self.id
self.id = None
if id:
self.widget.after_cancel(id)
def show_tooltip(self, event=None):
"""
Displays the tooltip window. The tooltip is a Toplevel window containing a ttk.Label.
It is positioned near the widget and styled for readability.
"""
if self.tooltip_window or not self.text:
return
x: int
y: int
cx: int
cy: int
x, y, cx, cy = self.widget.bbox("insert")
x += self.widget.winfo_rootx() + self.x_offset
y += self.widget.winfo_rooty() + self.y_offset
x, y, _, _ = self.widget.bbox("insert")
x += self.widget.winfo_rootx() + 25
y += self.widget.winfo_rooty() + 20
self.tooltip_window = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(True)
tw.wm_geometry(f"+{x}+{y}")
tw.wm_geometry(f"+" + str(x) + "+" + str(y))
label = ttk.Label(tw, text=self.text, justify=tk.LEFT, background="#FFFFE0", foreground="black",
relief=tk.SOLID, borderwidth=1, wraplength=self.wraplength, padding=(4, 2, 4, 2))
label.pack(ipadx=1)
label: tk.Label = tk.Label(
tw,
text=self.text,
background="lightgreen",
foreground="black",
relief="solid",
borderwidth=1,
padx=5,
pady=5,
)
label.grid()
self.tooltip_window.after(2200, lambda: tw.destroy())
def hide_tooltip(self, event: Optional[Any] = None) -> None:
"""Hides the tooltip"""
if self.tooltip_window:
self.tooltip_window.destroy()
self.tooltip_window = None
def hide_tooltip(self):
"""
Hides and destroys the tooltip window if it is currently visible.
"""
tw = self.tooltip_window
self.tooltip_window = None
if tw:
tw.destroy()
class LogConfig:
"""
A static class for configuring application-wide logging.
This class provides a convenient way to set up file-based logging for the application.
It ensures that log messages are written to a specified file with a consistent format.
Methods:
logger(file_path: str) -> None:
Configures the root logger to write messages to the specified file.
Usage Example:
# Assuming LOG_FILE_PATH is defined elsewhere (e.g., in a config file)
# LogConfig.logger(LOG_FILE_PATH)
# logging.info("This message will be written to the log file.")
"""
@staticmethod
def logger(file_path) -> None:
"""
Configures the root logger to write messages to the specified file.
Args:
file_path (str): The absolute path to the log file.
"""
file_handler = logging.FileHandler(
filename=f"{file_path}",
mode="a",
encoding="utf-8",
)
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
formatter = logging.Formatter(
"%(asctime)s - %(levelname)s - %(message)s")
file_handler.setFormatter(formatter)
file_handler.setLevel(logging.DEBUG)
logger = logging.getLogger()
logger.setLevel(logging.DEBUG) # Set the root logger level
logger.addHandler(file_handler)
class IconManager:
"""
A class for central management and loading of application icons.
This class loads Tkinter PhotoImage objects from a specified base path,
organizing them by logical names and providing a convenient way to retrieve them.
It handles potential errors during image loading by creating a blank image placeholder.
Attributes:
base_path (str): The base directory where icon subfolders (e.g., '16', '32', '48', '64') are located.
icons (Dict[str, tk.PhotoImage]): A dictionary storing loaded PhotoImage objects,
keyed by their logical names (e.g., 'computer_small', 'folder_large').
Methods:
get_icon(name: str) -> Optional[tk.PhotoImage]:
Retrieves a loaded icon by its logical name.
Usage Example:
# Initialize the IconManager with the path to your icon directory
# icon_manager = IconManager(base_path="/usr/share/icons/lx-icons/")
# Retrieve an icon
# computer_icon = icon_manager.get_icon("computer_small")
# if computer_icon:
# my_label = tk.Label(root, image=computer_icon)
# my_label.pack()
"""
def __init__(self, base_path='/usr/share/icons/lx-icons/'):
self.base_path = base_path
self.icons = {}
self._define_icon_paths()
self._load_all()
def _define_icon_paths(self):
self.icon_paths = {
# 16x16
'settings_16': '16/settings.png',
# 32x32
'back': '32/arrow-left.png',
'forward': '32/arrow-right.png',
'up': '32/arrow-up.png',
'audio_small': '32/audio.png',
'icon_view': '32/carrel.png',
'computer_small': '32/computer.png',
'device_small': '32/device.png',
'file_small': '32/document.png',
'download_error_small': '32/download_error.png',
'download_small': '32/download.png',
'error_small': '32/error.png',
'python_small': '32/file-python.png',
'documents_small': '32/folder-water-documents.png',
'downloads_small': '32/folder-water-download.png',
'music_small': '32/folder-water-music.png',
'pictures_small': '32/folder-water-pictures.png',
'folder_small': '32/folder-water.png',
'video_small': '32/folder-water-video.png',
'hide': '32/hide.png',
'home': '32/home.png',
'about': '32/about.png',
'info_small': '32/info.png',
'light_small': '32/light.png',
'dark_small': '32/dark.png',
'update_small': '32/update.png',
'no_update_small': '32/no_update.png',
'tooltip_small': '32/tip.png',
'no_tooltip_small': '32/no_tip.png',
'list_view': '32/list.png',
'log_small': '32/log.png',
'log_blue_small': '32/log_blue.png',
'lunix_tools_small': '32/Lunix_Tools.png',
'key_small': '32/lxtools_key.png',
'iso_small': '32/media-optical.png',
'new_document_small': '32/new-document.png',
'new_folder_small': '32/new-folder.png',
'pdf_small': '32/pdf.png',
'picture_small': '32/picture.png',
'question_mark_small': '32/question_mark.png',
'recursive_small': '32/recursive.png',
'search_small': '32/search.png',
'settings_small': '32/settings.png',
'settings-2_small': '32/settings-2.png',
'archive_small': '32/tar.png',
'unhide': '32/unhide.png',
'usb_small': '32/usb.png',
'video_small_file': '32/video.png',
'warning_small': '32/warning.png',
'export_small': '32/wg_export.png',
'import_small': '32/wg_import.png',
'message_small': '32/wg_msg.png',
'trash_small': '32/wg_trash.png',
'trash_small2': '32/trash.png',
'vpn_small': '32/wg_vpn.png',
'vpn_start_small': '32/wg_vpn-start.png',
'vpn_stop_small': '32/wg_vpn-stop.png',
# 48x48
'back_large': '48/arrow-left.png',
'forward_large': '48/arrow-right.png',
'up_large': '48/arrow-up.png',
'icon_view_large': '48/carrel.png',
'computer_large': '48/computer.png',
'device_large': '48/device.png',
'download_error_large': '48/download_error.png',
'download_large': '48/download.png',
'error_large': '48/error.png',
'documents_large': '48/folder-water-documents.png',
'downloads_large': '48/folder-water-download.png',
'music_large': '48/folder-water-music.png',
'pictures_large': '48/folder-water-pictures.png',
'folder_large_48': '48/folder-water.png',
'video_large_folder': '48/folder-water-video.png',
'hide_large': '48/hide.png',
'home_large': '48/home.png',
'info_large': '48/info.png',
'light_large': '48/light.png',
'dark_large': '48/dark.png',
'update_large': '48/update.png',
'no_update_large': '48/no_update.png',
'tooltip_large': '48/tip.png',
'no_tooltip_large': '48/no_tip.png',
'about_large': '48/about.png',
'list_view_large': '48/list.png',
'log_large': '48/log.png',
'log_blue_large': '48/log_blue.png',
'lunix_tools_large': '48/Lunix_Tools.png',
'new_document_large': '48/new-document.png',
'new_folder_large': '48/new-folder.png',
'question_mark_large': '48/question_mark.png',
'search_large_48': '48/search.png',
'settings_large': '48/settings.png',
'unhide_large': '48/unhide.png',
'usb_large': '48/usb.png',
'warning_large_48': '48/warning.png',
'export_large': '48/wg_export.png',
'import_large': '48/wg_import.png',
'message_large': '48/wg_msg.png',
'trash_large': '48/wg_trash.png',
'trash_large2': '48/trash.png',
'vpn_large': '48/wg_vpn.png',
'vpn_start_large': '48/wg_vpn-start.png',
'vpn_stop_large': '48/wg_vpn-stop.png',
# 64x64
'back_extralarge': '64/arrow-left.png',
'forward_extralarge': '64/arrow-right.png',
'up_extralarge': '64/arrow-up.png',
'audio_large': '64/audio.png',
'icon_view_extralarge': '64/carrel.png',
'computer_extralarge': '64/computer.png',
'device_extralarge': '64/device.png',
'file_large': '64/document.png',
'download_error_extralarge': '64/download_error.png',
'download_extralarge': '64/download.png',
'error_extralarge': '64/error.png',
'python_large': '64/file-python.png',
'documents_extralarge': '64/folder-water-documents.png',
'downloads_extralarge': '64/folder-water-download.png',
'music_extralarge': '64/folder-water-music.png',
'pictures_extralarge': '64/folder-water-pictures.png',
'folder_large': '64/folder-water.png',
'video_extralarge_folder': '64/folder-water-video.png',
'hide_extralarge': '64/hide.png',
'home_extralarge': '64/home.png',
'info_extralarge': '64/info.png',
'light_extralarge': '64/light.png',
'dark_extralarge': '64/dark.png',
'update_extralarge': '64/update.png',
'no_update_extralarge': '64/no_update.png',
'tooltip_extralarge': '64/tip.png',
'no_tooltip_extralarge': '64/no_tip.png',
'about_extralarge': '64/about.png',
'list_view_extralarge': '64/list.png',
'log_extralarge': '64/log.png',
'log_blue_extralarge': '64/log_blue.png',
'lunix_tools_extralarge': '64/Lunix_Tools.png',
'iso_large': '64/media-optical.png',
'new_document_extralarge': '64/new-document.png',
'new_folder_extralarge': '64/new-folder.png',
'pdf_large': '64/pdf.png',
'picture_large': '64/picture.png',
'question_mark_extralarge': '64/question_mark.png',
'recursive_large': '64/recursive.png',
'search_large': '64/search.png',
'settings_extralarge': '64/settings.png',
'archive_large': '64/tar.png',
'unhide_extralarge': '64/unhide.png',
'usb_extralarge': '64/usb.png',
'video_large': '64/video.png',
'warning_large': '64/warning.png',
'export_extralarge': '64/wg_export.png',
'import_extralarge': '64/wg_import.png',
'message_extralarge': '64/wg_msg.png',
'trash_extralarge': '64/wg_trash.png',
'trash_extralarge2': '64/trash.png',
'vpn_extralarge': '64/wg_vpn.png',
'vpn_start_extralarge': '64/wg_vpn-start.png',
'vpn_stop_extralarge': '64/wg_vpn-stop.png',
}
def _load_all(self):
for key, rel_path in self.icon_paths.items():
full_path = os.path.join(self.base_path, rel_path)
try:
self.icons[key] = tk.PhotoImage(file=full_path)
except tk.TclError as e:
print(f"Error loading icon '{key}' from '{full_path}': {e}")
size = 32 # Default size
if '16' in rel_path:
size = 16
elif '48' in rel_path:
size = 48
elif '64' in rel_path:
size = 64
self.icons[key] = tk.PhotoImage(width=size, height=size)
def get_icon(self, name):
return self.icons.get(name)
class Translate:
@staticmethod
def setup_translations(app_name: str, locale_dir="/usr/share/locale/") -> gettext.gettext:
"""
Initialize translations and set the translation function
Special method for translating strings in this file
Returns:
The gettext translation function
"""
locale.bindtextdomain(app_name, locale_dir)
gettext.bindtextdomain(app_name, locale_dir)
gettext.textdomain(app_name)
return gettext.gettext
@contextmanager
def message_box_animation(animated_icon):
"""
A context manager to handle starting and stopping an animated icon
around an operation like showing a message box.
Args:
animated_icon: The animated icon object with start() and stop() methods.
"""
if animated_icon:
animated_icon.stop()
try:
yield
finally:
if animated_icon:
animated_icon.start()