From e3bb68f7e25e6185dfde76d3251bc8752e5359d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A9sir=C3=A9=20Werner=20Menrath?= Date: Fri, 1 Aug 2025 09:29:26 +0200 Subject: [PATCH] commit 36 mit common tools --- __pycache__/common_tools.cpython-312.pyc | Bin 0 -> 30246 bytes .../custom_file_dialog.cpython-312.pyc | Bin 54465 -> 66123 bytes common_tools.py | 621 ++++++++++++++++++ custom_file_dialog.py | 520 +++++++++++---- mainwindow.py | 7 +- 5 files changed, 1032 insertions(+), 116 deletions(-) create mode 100644 __pycache__/common_tools.cpython-312.pyc create mode 100755 common_tools.py mode change 100644 => 100755 mainwindow.py diff --git a/__pycache__/common_tools.cpython-312.pyc b/__pycache__/common_tools.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7049ed3fc2139dc900d742a15c337d6da9a54b7b GIT binary patch literal 30246 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 S!H>mf&8GI7Ad@COi2oP8_$*fd literal 0 HcmV?d00001 diff --git a/__pycache__/custom_file_dialog.cpython-312.pyc b/__pycache__/custom_file_dialog.cpython-312.pyc index 2bb42b2831487e1a1bcf6899fb5b2109927d71af..9b3533aae81d6fb613646f9983230788e9241eb0 100644 GIT binary patch delta 23800 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 None: + """ + Starts SSL dencrypt + """ + 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: + + logging.error( + f"Error process decrypt: Code {process.returncode}", exc_info=True + ) + + @staticmethod + def encrypt(user) -> None: + """ + Starts SSL encryption + """ + process: CompletedProcess[str] = run( + ["pkexec", "/usr/local/bin/ssl_encrypt.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 encrypted...", exc_info=True) + else: + logging.error( + f"Error process encrypt: Code {process.returncode}", exc_info=True + ) + + @staticmethod + def find_key(key: str = "") -> bool: + """ + Checks if the private key already exists in the system using an external script. + Returns True only if the full key is found exactly (no partial match). + """ + process: CompletedProcess[str] = run( + ["pkexec", "/usr/local/bin/match_found.py", key], + capture_output=True, + text=True, + check=False, + ) + if "True" in process.stdout: + 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, + ) + return False + + @staticmethod + def is_valid_base64(key: str) -> bool: + """ + Validates if the input is a valid Base64 string (WireGuard private key format). + Returns True only for non-empty strings that match the expected length. + """ + # Check for empty string + if not key or key.strip() == "": + return False + + # Regex pattern to validate Base64: [A-Za-z0-9+/]+={0,2} + base64_pattern = r"^[A-Za-z0-9+/]+={0,2}$" + if not re.match(base64_pattern, key): + return False + + try: + # Decode and check length (WireGuard private keys are 32 bytes long) + decoded = base64.b64decode(key) + 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) + return False + + return True + + +class LxTools: + """ + Class LinuxTools methods that can also be used for other apps + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + @staticmethod + def center_window_cross_platform(window, width, height): + """ + Centers a window on the primary monitor in a way that works on both X11 and Wayland + + Args: + window: The tkinter window to center + width: Window width + height: Window height + """ + # Calculate the position before showing the window + + # First attempt: Try to use GDK if available (works on both X11 and Wayland) + try: + import gi + + gi.require_version("Gdk", "3.0") + from gi.repository import Gdk + + display = Gdk.Display.get_default() + monitor = display.get_primary_monitor() or display.get_monitor(0) + geometry = monitor.get_geometry() + scale_factor = monitor.get_scale_factor() + + # Calculate center position on the primary monitor + x = geometry.x + (geometry.width - width // scale_factor) // 2 + y = geometry.y + (geometry.height - height // scale_factor) // 2 + + # Set window geometry + window.geometry(f"{width}x{height}+{x}+{y}") + return + except (ImportError, AttributeError): + pass + + # Second attempt: Try xrandr for X11 + try: + import subprocess + + output = subprocess.check_output( + ["xrandr", "--query"], universal_newlines=True + ) + + # Parse the output to find the primary monitor + primary_info = None + for line in output.splitlines(): + if "primary" in line: + parts = line.split() + for part in parts: + if "x" in part and "+" in part: + primary_info = part + break + break + + if primary_info: + # Parse the geometry: WIDTH x HEIGHT+X+Y + geometry = primary_info.split("+") + dimensions = geometry[0].split("x") + primary_width = int(dimensions[0]) + primary_height = int(dimensions[1]) + primary_x = int(geometry[1]) + primary_y = int(geometry[2]) + + # Calculate center position on the primary monitor + x = primary_x + (primary_width - width) // 2 + y = primary_y + (primary_height - height) // 2 + + # Set window geometry + window.geometry(f"{width}x{height}+{x}+{y}") + return + except (ImportError, IndexError, ValueError): + pass + + # Final fallback: Use standard Tkinter method + screen_width = window.winfo_screenwidth() + screen_height = window.winfo_screenheight() + + # Try to make an educated guess for multi-monitor setups + # If screen width is much larger than height, assume multiple monitors side by side + if ( + screen_width > screen_height * 1.8 + ): # Heuristic for detecting multiple monitors + # Assume the primary monitor is on the left half + screen_width = screen_width // 2 + + x = (screen_width - width) // 2 + y = (screen_height - height) // 2 + window.geometry(f"{width}x{height}+{x}+{y}") + + @staticmethod + def clean_files(tmp_dir: Path = None, file: Path = None) -> None: + """ + Deletes temporary files and directories for cleanup when exiting the application. + + This method safely removes an optional directory defined by `AppConfig.TEMP_DIR` + and a single file to free up resources at the end of the program's execution. + All operations are performed securely, and errors such as `FileNotFoundError` + are ignored if the target files or directories do not exist. + :param tmp_dir: (Path, optional): Path to the temporary directory that should be deleted. + If `None`, the value of `AppConfig.TEMP_DIR` is used. + :param file: (Path, optional): Path to the file that should be deleted. + If `None`, no additional file will be deleted. + + Returns: + None: The method does not return any value. + """ + + if tmp_dir is not None: + shutil.rmtree(tmp_dir, ignore_errors=True) + try: + if file is not None: + Path.unlink(file) + + except FileNotFoundError: + pass + + @staticmethod + def sigi(file_path: Optional[Path] = None, file: Optional[Path] = None) -> None: + """ + Function for cleanup after a program interruption + + :param file: Optional - File to be deleted + :param file_path: Optional - Directory to be deleted + """ + + def signal_handler(signum: int, frame: Any) -> NoReturn: + """ + Determines clear text names for signal numbers and handles signals + + Args: + signum: The signal number + frame: The current stack frame + + Returns: + NoReturn since the function either exits the program or continues execution + """ + + signals_to_names_dict: Dict[int, str] = dict( + (getattr(signal, n), n) + for n in dir(signal) + if n.startswith("SIG") and "_" not in n + ) + + signal_name: str = signals_to_names_dict.get( + signum, f"Unnamed signal: {signum}" + ) + + # 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, + ) + LxTools.clean_files(file_path, file) + logging.info("Breakdown by user...") + sys.exit(exit_code) + else: + logging.info(f"Signal {signum} received and ignored.") + LxTools.clean_files(file_path, file) + logging.error("Process unexpectedly ended...") + + # Register signal handlers for various signals + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGHUP, signal_handler) + + +# ConfigManager with caching +class ConfigManager: + """ + Universal class for managing configuration files with caching support. + + This class provides a general solution to load, save, and manage configuration + files across different projects. It uses a caching system to optimize access efficiency. + The `init()` method initializes the configuration file path, while `load()` and `save()` + synchronize data between the file and internal memory structures. + + Key Features: + - Caching to minimize I/O operations. + - Default values for missing or corrupted configuration files. + - Reusability across different projects and use cases. + + The class is designed for central application configuration management, working closely + with `ThemeManager` to dynamically manage themes or other settings. + """ + + _config = None + _config_file = None + + @classmethod + def init(cls, config_file): + """Initial the Configmanager with the given config file""" + cls._config_file = config_file + cls._config = None # Reset the cache + + @classmethod + def load(cls): + """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() + 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 + cls._config = { + "updates": "on", + "theme": "light", + "tooltips": "True", # Default Value as string! + "autostart": "off", + "logfile": LOG_FILE_PATH, + } + return cls._config + + @classmethod + def save(cls): + """Save the config to the config file""" + if cls._config: + lines = [ + "# Configuration\n", + f"{cls._config['updates']}\n", + "# Theme\n", + f"{cls._config['theme']}\n", + "# Tooltips\n", + 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") + + @classmethod + def set(cls, key, value): + """Sets a configuration value and saves the change""" + cls.load() + cls._config[key] = value + cls.save() + + @classmethod + def get(cls, key, default=None): + """Returns a configuration value""" + config = cls.load() + return config.get(key, default) + + +class ThemeManager: + """ + Class for central theme management and UI customization. + + This static class allows dynamic adjustment of the application's appearance. + The method `change_theme()` updates the current theme and saves + the selection in the configuration file via `ConfigManager`. + It ensures a consistent visual design across the entire project. + + Key Features: + - Central control over themes. + - Automatic saving of theme settings to the configuration file. + - Tight integration with `ConfigManager` for persistent storage of preferences. + + The class is designed to apply themes consistently throughout the application, + ensuring that changes are traceable and uniform across all parts of the project. + """ + + @staticmethod + def change_theme(root, theme_in_use, theme_name=None): + """Change application theme centrally""" + root.tk.call("set_theme", theme_in_use) + if theme_in_use == theme_name: + ConfigManager.set("theme", theme_in_use) + + +class Tooltip: + def __init__(self, widget, text, wraplength=250): + self.widget = widget + self.text = text + self.wraplength = wraplength + self.tooltip_window = None + self.id = None + self.widget.bind("", self.enter) + self.widget.bind("", self.leave) + self.widget.bind("", self.leave) + + def enter(self, event=None): self.schedule() + def leave(self, event=None): self.unschedule(); self.hide_tooltip() + + def schedule(self): self.unschedule( + ); self.id = self.widget.after(250, self.show_tooltip) + + def unschedule(self): + id = self.id + self.id = None + if id: + self.widget.after_cancel(id) + + def show_tooltip(self, event=None): + 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}") + 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) + + def hide_tooltip(self): + tw = self.tooltip_window + self.tooltip_window = None + if tw: + tw.destroy() + + +class LogConfig: + @staticmethod + def logger(file_path) -> None: + + file_handler = logging.FileHandler( + filename=f"{file_path}", + mode="a", + encoding="utf-8", + ) + formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") + file_handler.setFormatter(formatter) + file_handler.setLevel(logging.DEBUG) + + logger = logging.getLogger() + logger.addHandler(file_handler) + +import os + +class IconManager: + 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', + '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', + 'info_small': '32/info.png', + 'list_view': '32/list.png', + 'log_small': '32/log.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', + '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', + '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', + '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', + 'list_view_large': '48/list.png', + 'log_large': '48/log.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', + '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', + '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', + 'list_view_extralarge': '64/list.png', + 'log_extralarge': '64/log.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', + '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) diff --git a/custom_file_dialog.py b/custom_file_dialog.py index 4f91573..7f38a00 100644 --- a/custom_file_dialog.py +++ b/custom_file_dialog.py @@ -5,16 +5,14 @@ from tkinter import ttk from datetime import datetime import subprocess import json +from shared_libs.message import MessageDialog +from shared_libs.common_tools import IconManager, Tooltip # Helper to make icon paths robust, so the script can be run from anywhere SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) MAX_ITEMS_TO_DISPLAY = 1000 -def get_icon_path(icon_name): - return os.path.join(SCRIPT_DIR, icon_name) - - def get_xdg_user_dir(dir_key, fallback_name): home = os.path.expanduser("~") fallback_path = os.path.join(home, fallback_name) @@ -38,47 +36,6 @@ def get_xdg_user_dir(dir_key, fallback_name): return fallback_path -class Tooltip: - def __init__(self, widget, text, wraplength=250): - self.widget = widget - self.text = text - self.wraplength = wraplength - self.tooltip_window = None - self.id = None - self.widget.bind("", self.enter) - self.widget.bind("", self.leave) - self.widget.bind("", self.leave) - - def enter(self, event=None): self.schedule() - def leave(self, event=None): self.unschedule(); self.hide_tooltip() - - def schedule(self): self.unschedule( - ); self.id = self.widget.after(250, self.show_tooltip) - - def unschedule(self): - id = self.id - self.id = None - if id: - self.widget.after_cancel(id) - - def show_tooltip(self, event=None): - 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}") - 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) - - def hide_tooltip(self): - tw = self.tooltip_window - self.tooltip_window = None - if tw: - tw.destroy() - - class CustomFileDialog(tk.Toplevel): def __init__(self, parent, initial_dir=None, filetypes=None): super().__init__(parent) @@ -100,56 +57,18 @@ class CustomFileDialog(tk.Toplevel): self.show_hidden_files = tk.BooleanVar(value=False) self.resize_job = None self.last_width = 0 + self.sidebar_buttons = [] + self.device_buttons = [] + self.search_results = [] # Store search results + self.search_mode = False # Track if in search mode + self.original_path_text = "" # Store original path text - self.load_icons() + self.icon_manager = IconManager() + self.icons = self.icon_manager.icons self.create_styles() self.create_widgets() self.navigate_to(self.current_dir) - def load_icons(self): - self.icons = {} - icon_files = { - 'computer_small': '/usr/share/icons/lx-icons/32/computer-32.png', - 'computer_large': '/usr/share/icons/lx-icons/48/computer-48.png', - 'device_small': '/usr/share/icons/lx-icons/32/device-32.png', - 'device_large': '/usr/share/icons/lx-icons/48/device-48.png', - 'usb_small': '/usr/share/icons/lx-icons/32/usb-32.png', - 'usb_large': '/usr/share/icons/lx-icons/48/usb-48.png', - 'downloads_small': '/usr/share/icons/lx-icons/32/folder-water-download-32.png', - 'downloads_large': '/usr/share/icons/lx-icons/48/folder-water-download-48.png', - 'documents_small': '/usr/share/icons/lx-icons/32/folder-water-documents-32.png', - 'documents_large': '/usr/share/icons/lx-icons/48/folder-water-documents-48.png', - 'pictures_small': '/usr/share/icons/lx-icons/32/folder-water-pictures-32.png', - 'pictures_large': '/usr/share/icons/lx-icons/48/folder-water-pictures-48.png', - 'music_small': '/usr/share/icons/lx-icons/32/folder-water-music-32.png', - 'music_large': '/usr/share/icons/lx-icons/48/folder-water-music-48.png', - 'video_small': '/usr/share/icons/lx-icons/32/folder-water-video-32.png', - 'video_large_folder': '/usr/share/icons/lx-icons/48/folder-water-video-48.png', - 'warning_small': '/usr/share/icons/lx-icons/32/warning.png', 'warning_large': '/usr/share/icons/lx-icons/64/warning.png', - 'folder_large': '/usr/share/icons/lx-icons/64/folder-water-64.png', 'file_large': '/usr/share/icons/lx-icons/64/document-64.png', - 'python_large': '/usr/share/icons/lx-icons/64/file-python-64.png', 'pdf_large': '/usr/share/icons/lx-icons/64/pdf-64.png', - 'archive_large': '/usr/share/icons/lx-icons/64/tar-64.png', 'audio_large': '/usr/share/icons/lx-icons/64/audio-64.png', - 'video_large': '/usr/share/icons/lx-icons/64/video-64.png', 'picture_large': '/usr/share/icons/lx-icons/64/picture-64.png', - 'iso_large': '/usr/share/icons/lx-icons/64/media-optical-64.png', 'folder_small': '/usr/share/icons/lx-icons/32/folder-water-32.png', - 'file_small': '/usr/share/icons/lx-icons/32/document-32.png', 'python_small': '/usr/share/icons/lx-icons/32/file-python-32.png', - 'pdf_small': '/usr/share/icons/lx-icons/32/pdf-32.png', 'archive_small': '/usr/share/icons/lx-icons/32/tar-32.png', - 'audio_small': '/usr/share/icons/lx-icons/32/audio-32.png', 'video_small_file': '/usr/share/icons/lx-icons/32/video-32.png', - 'picture_small': '/usr/share/icons/lx-icons/32/picture-32.png', 'iso_small': '/usr/share/icons/lx-icons/32/media-optical-32.png', - 'list_view': '/usr/share/icons/lx-icons/32/list-32.png', - 'icon_view': '/usr/share/icons/lx-icons/32/carrel-32.png', - 'hide': '/usr/share/icons/lx-icons/32/hide-32.png', - 'unhide': '/usr/share/icons/lx-icons/32/unhide-32.png', - 'back': '/usr/share/icons/lx-icons/32/arrow-left-32.png', - 'forward': '/usr/share/icons/lx-icons/32/arrow-right-32.png', - 'home': '/usr/share/icons/lx-icons/32/home-32.png' - } - for key, filename in icon_files.items(): - try: - self.icons[key] = tk.PhotoImage(file=get_icon_path(filename)) - except tk.TclError: - size = 32 if 'small' in key or 'view' in key or 'hide' in key or 'unhide' in key or 'back' in key or 'forward' in key or 'home' in key else 64 - self.icons[key] = tk.PhotoImage(width=size, height=size) - def get_file_icon(self, filename, size='large'): ext = os.path.splitext(filename)[1].lower() if ext == '.svg': @@ -207,6 +126,17 @@ class CustomFileDialog(tk.Toplevel): style.map("Header.TButton.Borderless.Round", background=[ ('active', self.hover_extrastyle)]) + # Style for active/pressed header buttons + style.configure("Header.TButton.Active.Round", + background=self.selection_color) + + # Copy layout from the base style + style.layout("Header.TButton.Active.Round", + style.layout("Header.TButton.Borderless.Round")) + + style.map("Header.TButton.Active.Round", background=[ + ('active', self.selection_color)]) + style.configure("Dark.TButton.Borderless", anchor="w", background=self.sidebar_color, foreground=self.color_foreground, padding=(20, 5, 0, 5)) @@ -278,21 +208,43 @@ class CustomFileDialog(tk.Toplevel): self.path_entry.bind( "", lambda e: self.navigate_to(self.path_entry.get())) - # View switch and hidden files button + # Search, view switch and hidden files button right_top_bar_frame = ttk.Frame(top_bar, style='Accent.TFrame') right_top_bar_frame.grid(row=0, column=2, sticky="e") + # Search button and options container + search_container = ttk.Frame( + right_top_bar_frame, style='Accent.TFrame') + search_container.pack(side="left", padx=(0, 10)) + + self.search_button = ttk.Button(search_container, image=self.icons['search_small'], + command=self.toggle_search_mode, style="Header.TButton.Borderless.Round") + self.search_button.pack(side="left") + Tooltip(self.search_button, "Suchen") + + # Search options frame (initially hidden, next to search button) + self.search_options_frame = ttk.Frame( + search_container, style='Accent.TFrame') + + # Recursive search toggle button + self.recursive_search = tk.BooleanVar(value=True) + self.recursive_button = ttk.Button(self.search_options_frame, image=self.icons['recursive_small'], + command=self.toggle_recursive_search, + style="Header.TButton.Active.Round") + self.recursive_button.pack(side="left", padx=2) + Tooltip(self.recursive_button, "Rekursive Suche ein/ausschalten") + view_switch = ttk.Frame(right_top_bar_frame, padding=(5, 0), style='Accent.TFrame') view_switch.pack(side="left") - self.icon_view_button = ttk.Button(view_switch, image=self.icons['icon_view'], command=lambda: ( - self.view_mode.set("icons"), self.populate_files()), style="Header.TButton.Borderless.Round") + self.icon_view_button = ttk.Button(view_switch, image=self.icons['icon_view'], + command=self.set_icon_view, style="Header.TButton.Active.Round") self.icon_view_button.pack(side="left", padx=(50, 10)) Tooltip(self.icon_view_button, "Kachelansicht") - self.list_view_button = ttk.Button(view_switch, image=self.icons['list_view'], command=lambda: ( - self.view_mode.set("list"), self.populate_files()), style="Header.TButton.Borderless.Round") + self.list_view_button = ttk.Button(view_switch, image=self.icons['list_view'], + command=self.set_list_view, style="Header.TButton.Borderless.Round") self.list_view_button.pack(side="left") Tooltip(self.list_view_button, "Listenansicht") @@ -312,14 +264,14 @@ class CustomFileDialog(tk.Toplevel): # Sidebar sidebar_frame = ttk.Frame( - paned_window, style="Sidebar.TFrame", padding=(0, 0, 0, 0)) + paned_window, style="Sidebar.TFrame", padding=(0, 0, 0, 0), width=200) # Prevent content from resizing the frame - # sidebar_frame.grid_propagate(False) + sidebar_frame.grid_propagate(False) + sidebar_frame.bind("", self.on_sidebar_resize) # Use weight=0 to give it a fixed size paned_window.add(sidebar_frame, weight=0) - sidebar_frame.grid_rowconfigure(2, weight=1) - + # No weight on any row - let storage stay at bottom sidebar_buttons_frame = ttk.Frame( sidebar_frame, style="Sidebar.TFrame", padding=(0, 15, 0, 0)) sidebar_buttons_frame.grid( @@ -342,43 +294,121 @@ class CustomFileDialog(tk.Toplevel): btn = ttk.Button(sidebar_buttons_frame, text=f" {config['name']}", image=config['icon'], compound="left", command=lambda p=config['path']: self.navigate_to(p), style="Dark.TButton.Borderless") btn.pack(fill="x", pady=1) + self.sidebar_buttons.append((btn, f" {config['name']}")) # Horizontal separator separator_color = "#a9a9a9" if self.is_dark else "#7c7c7c" tk.Frame(sidebar_frame, height=1, bg=separator_color).grid( row=1, column=0, sticky="ew", padx=20, pady=15) - # Mounted devices + # Mounted devices with scrollable frame mounted_devices_frame = ttk.Frame( sidebar_frame, style="Sidebar.TFrame") mounted_devices_frame.grid(row=2, column=0, sticky="nsew", padx=10) - ttk.Label(mounted_devices_frame, text="Geräte:", background=self.sidebar_color, - foreground=self.color_foreground).pack(fill="x", padx=10, pady=(5, 0)) + # Don't expand devices frame so storage stays in position + mounted_devices_frame.grid_columnconfigure(0, weight=1) + ttk.Label(mounted_devices_frame, text="Geräte:", background=self.sidebar_color, + foreground=self.color_foreground).grid(row=0, column=0, sticky="ew", padx=10, pady=(5, 0)) + + # Create scrollable canvas for devices + self.devices_canvas = tk.Canvas(mounted_devices_frame, highlightthickness=0, + bg=self.sidebar_color, height=150, width=180) + self.devices_scrollbar = ttk.Scrollbar(mounted_devices_frame, orient="vertical", + command=self.devices_canvas.yview) + self.devices_canvas.configure( + yscrollcommand=self.devices_scrollbar.set) + + self.devices_canvas.grid(row=1, column=0, sticky="nsew") + # Scrollbar initially hidden + + # Create scrollable frame inside canvas + self.devices_scrollable_frame = ttk.Frame( + self.devices_canvas, style="Sidebar.TFrame") + self.devices_canvas_window = self.devices_canvas.create_window( + (0, 0), window=self.devices_scrollable_frame, anchor="nw") + + # Bind events for showing/hiding scrollbar on hover + self.devices_canvas.bind("", self._on_devices_enter) + self.devices_canvas.bind("", self._on_devices_leave) + self.devices_scrollable_frame.bind("", self._on_devices_enter) + self.devices_scrollable_frame.bind("", self._on_devices_leave) + + # Bind canvas width to scrollable frame width + def _configure_devices_canvas(event): + self.devices_canvas.configure( + scrollregion=self.devices_canvas.bbox("all")) + canvas_width = event.width + self.devices_canvas.itemconfig( + self.devices_canvas_window, width=canvas_width) + + self.devices_scrollable_frame.bind("", lambda e: self.devices_canvas.configure( + scrollregion=self.devices_canvas.bbox("all"))) + self.devices_canvas.bind("", _configure_devices_canvas) + + # Mouse wheel scrolling for devices area + def _on_devices_mouse_wheel(event): + if event.num == 4: # Scroll up on Linux + delta = -1 + elif event.num == 5: # Scroll down on Linux + delta = 1 + else: # MouseWheel event for Windows/macOS + delta = -1 * int(event.delta / 120) + self.devices_canvas.yview_scroll(delta, "units") + + # Bind mouse wheel to canvas and scrollable frame + for widget in [self.devices_canvas, self.devices_scrollable_frame]: + widget.bind("", _on_devices_mouse_wheel) + widget.bind("", _on_devices_mouse_wheel) + widget.bind("", _on_devices_mouse_wheel) + + # Populate devices for device_name, mount_point, removable in self._get_mounted_devices(): icon = self.icons['usb_small'] if removable else self.icons['device_small'] button_text = f" {device_name}" if len(device_name) > 15: # Static wrapping for long names button_text = f" {device_name[:15]}\n{device_name[15:]}" - btn = ttk.Button(mounted_devices_frame, text=button_text, image=icon, + btn = ttk.Button(self.devices_scrollable_frame, text=button_text, image=icon, compound="left", command=lambda p=mount_point: self.navigate_to(p), style="Dark.TButton.Borderless") btn.pack(fill="x", pady=1) + self.device_buttons.append((btn, button_text)) + + # Bind mouse wheel to device buttons too + btn.bind("", _on_devices_mouse_wheel) + btn.bind("", _on_devices_mouse_wheel) + btn.bind("", _on_devices_mouse_wheel) + + # Bind hover events for scrollbar visibility + btn.bind("", self._on_devices_enter) + btn.bind("", self._on_devices_leave) + try: total, used, _ = shutil.disk_usage(mount_point) progress_bar = ttk.Progressbar( - mounted_devices_frame, orient="horizontal", length=100, mode="determinate", style='Small.Horizontal.TProgressbar') + self.devices_scrollable_frame, orient="horizontal", length=100, mode="determinate", style='Small.Horizontal.TProgressbar') progress_bar.pack(fill="x", pady=(2, 8), padx=25) progress_bar['value'] = (used / total) * 100 + + # Bind mouse wheel to progress bars too + progress_bar.bind("", _on_devices_mouse_wheel) + progress_bar.bind("", _on_devices_mouse_wheel) + progress_bar.bind("", _on_devices_mouse_wheel) + + # Bind hover events for scrollbar visibility + progress_bar.bind("", self._on_devices_enter) + progress_bar.bind("", self._on_devices_leave) except (FileNotFoundError, PermissionError): # In case of errors (e.g., unreadable drive), just skip the progress bar pass + # Separator before storage tk.Frame(sidebar_frame, height=1, bg=separator_color).grid( row=3, column=0, sticky="ew", padx=20, pady=15) + # Storage section at bottom - use pack instead of grid to stay at bottom storage_frame = ttk.Frame(sidebar_frame, style="Sidebar.TFrame") - storage_frame.grid(row=4, column=0, sticky="ew", padx=10) + storage_frame.grid(row=4, column=0, sticky="sew", padx=10, pady=10) self.storage_label = ttk.Label( storage_frame, text="Freier Speicher:", background=self.freespace_background) self.storage_label.pack(fill="x", padx=10) @@ -441,6 +471,280 @@ class CustomFileDialog(tk.Toplevel): self.resize_job = self.after(200, self.populate_files) self.last_width = new_width + def on_sidebar_resize(self, event): + current_width = event.width + # Define a threshold for when to hide/show text + threshold_width = 100 # Adjust this value as needed + + if current_width < threshold_width: + # Hide text, show only icons + for btn, original_text in self.sidebar_buttons: + btn.config(text="", compound="top") + for btn, original_text in self.device_buttons: + btn.config(text="", compound="top") + else: + # Show text + for btn, original_text in self.sidebar_buttons: + btn.config(text=original_text, compound="left") + for btn, original_text in self.device_buttons: + btn.config(text=original_text, compound="left") + + def _on_devices_enter(self, event): + """Show scrollbar when mouse enters devices area""" + self.devices_scrollbar.grid(row=1, column=1, sticky="ns") + + def _on_devices_leave(self, event): + """Hide scrollbar when mouse leaves devices area""" + # Check if mouse is really leaving the devices area + x, y = event.x_root, event.y_root + widget_x = self.devices_canvas.winfo_rootx() + widget_y = self.devices_canvas.winfo_rooty() + widget_width = self.devices_canvas.winfo_width() + widget_height = self.devices_canvas.winfo_height() + + # Add small buffer to prevent flickering + buffer = 5 + if not (widget_x - buffer <= x <= widget_x + widget_width + buffer and + widget_y - buffer <= y <= widget_y + widget_height + buffer): + self.devices_scrollbar.grid_remove() + + def toggle_search_mode(self): + """Toggle between search mode and normal mode""" + if not self.search_mode: + # Enter search mode + self.search_mode = True + self.original_path_text = self.path_entry.get() + self.path_entry.delete(0, tk.END) + self.path_entry.insert(0, "Suchbegriff eingeben...") + self.path_entry.bind("", self.execute_search) + self.path_entry.bind("", self.clear_search_placeholder) + + # Show search options + self.search_options_frame.pack(side="left", padx=(5, 0)) + else: + # Exit search mode + self.search_mode = False + self.path_entry.delete(0, tk.END) + self.path_entry.insert(0, self.original_path_text) + self.path_entry.bind( + "", lambda e: self.navigate_to(self.path_entry.get())) + self.path_entry.unbind("") + + # Hide search options + self.search_options_frame.pack_forget() + + # Return to normal file view + self.populate_files() + + def toggle_recursive_search(self): + """Toggle recursive search on/off and update button style""" + self.recursive_search.set(not self.recursive_search.get()) + if self.recursive_search.get(): + self.recursive_button.configure( + style="Header.TButton.Active.Round") + else: + self.recursive_button.configure( + style="Header.TButton.Borderless.Round") + + def set_icon_view(self): + """Set icon view and update button styles""" + self.view_mode.set("icons") + self.icon_view_button.configure(style="Header.TButton.Active.Round") + self.list_view_button.configure( + style="Header.TButton.Borderless.Round") + self.populate_files() + + def set_list_view(self): + """Set list view and update button styles""" + self.view_mode.set("list") + self.list_view_button.configure(style="Header.TButton.Active.Round") + self.icon_view_button.configure( + style="Header.TButton.Borderless.Round") + self.populate_files() + + def clear_search_placeholder(self, event): + """Clear placeholder text when focus enters search field""" + if self.path_entry.get() == "Suchbegriff eingeben...": + self.path_entry.delete(0, tk.END) + + def execute_search(self, event): + """Execute search when Enter is pressed in search mode""" + search_term = self.path_entry.get().strip() + if not search_term or search_term == "Suchbegriff eingeben...": + return + + # Clear previous search results + self.search_results.clear() + + # Determine search directories + search_dirs = [self.current_dir] + + # If searching from home directory, also include XDG directories + home_dir = os.path.expanduser("~") + if os.path.abspath(self.current_dir) == os.path.abspath(home_dir): + xdg_dirs = [ + get_xdg_user_dir("XDG_DOWNLOAD_DIR", "Downloads"), + get_xdg_user_dir("XDG_DOCUMENTS_DIR", "Documents"), + get_xdg_user_dir("XDG_PICTURES_DIR", "Pictures"), + get_xdg_user_dir("XDG_MUSIC_DIR", "Music"), + get_xdg_user_dir("XDG_VIDEO_DIR", "Videos") + ] + # Add XDG directories that exist and are not already in home + for xdg_dir in xdg_dirs: + if (os.path.exists(xdg_dir) and + os.path.abspath(xdg_dir) != os.path.abspath(home_dir) and + xdg_dir not in search_dirs): + search_dirs.append(xdg_dir) + + try: + all_files = [] + + # Search in each directory + for search_dir in search_dirs: + if not os.path.exists(search_dir): + continue + + # Change to directory and use relative paths to avoid path issues + original_cwd = os.getcwd() + try: + os.chdir(search_dir) + + # Build find command based on recursive setting (use . for current directory) + if self.recursive_search.get(): + find_cmd = ['find', '.', '-iname', + f'*{search_term}*', '-type', 'f'] + else: + find_cmd = ['find', '.', '-maxdepth', '1', + '-iname', f'*{search_term}*', '-type', 'f'] + + result = subprocess.run( + find_cmd, capture_output=True, text=True, timeout=30) + + if result.returncode == 0: + files = result.stdout.strip().split('\n') + # Convert relative paths back to absolute paths + directory_files = [] + for f in files: + if f and f.startswith('./'): + abs_path = os.path.join( + search_dir, f[2:]) # Remove './' prefix + if os.path.isfile(abs_path): + directory_files.append(abs_path) + all_files.extend(directory_files) + + finally: + os.chdir(original_cwd) + + # Remove duplicates while preserving order + seen = set() + unique_files = [] + for file_path in all_files: + if file_path not in seen: + seen.add(file_path) + unique_files.append(file_path) + + # Filter based on currently selected filter pattern + self.search_results = [] + for file_path in unique_files: + filename = os.path.basename(file_path) + if self._matches_filetype(filename): + self.search_results.append(file_path) + + # Show search results in TreeView + if self.search_results: + self.show_search_results_treeview() + else: + MessageDialog( + message_type="info", + text=f"Keine Dateien mit '{search_term}' gefunden.", + title="Suche", + master=self + ).show() + + except subprocess.TimeoutExpired: + MessageDialog( + message_type="error", + text="Suche dauert zu lange und wurde abgebrochen.", + title="Suche", + master=self + ).show() + except Exception as e: + MessageDialog( + message_type="error", + text=f"Fehler bei der Suche: {e}", + title="Suchfehler", + master=self + ).show() + + def show_search_results_treeview(self): + """Show search results in TreeView format""" + # Clear current file list and replace with search results + for widget in self.file_list_frame.winfo_children(): + widget.destroy() + + # Create TreeView for search results + tree_frame = ttk.Frame(self.file_list_frame) + tree_frame.pack(fill='both', expand=True) + tree_frame.grid_rowconfigure(0, weight=1) + tree_frame.grid_columnconfigure(0, weight=1) + + columns = ("path", "size", "modified") + search_tree = ttk.Treeview( + tree_frame, columns=columns, show="tree headings") + + # Configure columns + search_tree.heading("#0", text="Dateiname", anchor="w") + search_tree.column("#0", anchor="w", width=200, stretch=True) + search_tree.heading("path", text="Pfad", anchor="w") + search_tree.column("path", anchor="w", width=300, stretch=True) + search_tree.heading("size", text="Größe", anchor="e") + search_tree.column("size", anchor="e", width=100, stretch=False) + search_tree.heading("modified", text="Geändert am", anchor="w") + search_tree.column("modified", anchor="w", width=160, stretch=False) + + # Add scrollbars + v_scrollbar = ttk.Scrollbar( + tree_frame, orient="vertical", command=search_tree.yview) + h_scrollbar = ttk.Scrollbar( + tree_frame, orient="horizontal", command=search_tree.xview) + search_tree.configure(yscrollcommand=v_scrollbar.set, + xscrollcommand=h_scrollbar.set) + + search_tree.grid(row=0, column=0, sticky='nsew') + v_scrollbar.grid(row=0, column=1, sticky='ns') + h_scrollbar.grid(row=1, column=0, sticky='ew') + + # Populate with search results + for file_path in self.search_results: + try: + filename = os.path.basename(file_path) + directory = os.path.dirname(file_path) + stat = os.stat(file_path) + size = self._format_size(stat.st_size) + modified_time = datetime.fromtimestamp( + stat.st_mtime).strftime('%d.%m.%Y %H:%M') + + icon = self.get_file_icon(filename, 'small') + search_tree.insert("", "end", text=f" {filename}", image=icon, + values=(directory, size, modified_time)) + except (FileNotFoundError, PermissionError): + continue + + # Bind double-click to select file + def on_search_double_click(event): + selection = search_tree.selection() + if selection: + item = search_tree.item(selection[0]) + filename = item['text'].strip() + directory = item['values'][0] + full_path = os.path.join(directory, filename) + + # Select the file and close dialog + self.selected_file = full_path + self.destroy() + + search_tree.bind("", on_search_double_click) + def _unbind_mouse_wheel_events(self): # Unbind all mouse wheel events from the root window self.unbind_all("") @@ -811,12 +1115,9 @@ class CustomFileDialog(tk.Toplevel): name = block_device.get('name') mountpoint = block_device.get('mountpoint') label = block_device.get('label') - # size = block_device.get('size') removable = block_device.get('rm', False) display_name = label if label else name - # if size: - # display_name += f" ({size})" devices.append((display_name, mountpoint, removable)) # Process children (partitions) @@ -833,12 +1134,9 @@ class CustomFileDialog(tk.Toplevel): name = child_device.get('name') mountpoint = child_device.get('mountpoint') label = child_device.get('label') - # size = child_device.get('size') removable = child_device.get('rm', False) display_name = label if label else name - # if size: - # display_name += f" ({size})" devices.append( (display_name, mountpoint, removable)) diff --git a/mainwindow.py b/mainwindow.py old mode 100644 new mode 100755 index 0610908..2683470 --- a/mainwindow.py +++ b/mainwindow.py @@ -32,10 +32,7 @@ class GlotzMol(tk.Tk): dialog = CustomFileDialog(self, initial_dir=os.path.expanduser("~"), - filetypes=[("Alle Dateien", "*.*"), - ("Audio-Dateien", "*.mp3 *.wav"), - ("Video-Dateien", "*.mkv *.mp4"), - ("ISO-Images", "*.iso"), + filetypes=[("Wireguard Files (.conf)", "*.conf"), ]) # This is the crucial part: wait for the dialog to be closed @@ -58,7 +55,7 @@ if __name__ == "__main__": style = ttk.Style(root) root.tk.call('source', f"{theme_path}/water.tcl") try: - root.tk.call('set_theme', 'light') + root.tk.call('set_theme', 'dark') except tk.TclError: pass root.mainloop()