diff --git a/__pycache__/cfd_app_config.cpython-312.pyc b/__pycache__/cfd_app_config.cpython-312.pyc new file mode 100644 index 0000000..b3ed709 Binary files /dev/null and b/__pycache__/cfd_app_config.cpython-312.pyc differ diff --git a/__pycache__/cfd_ui_setup.cpython-312.pyc b/__pycache__/cfd_ui_setup.cpython-312.pyc new file mode 100644 index 0000000..590a5c2 Binary files /dev/null and b/__pycache__/cfd_ui_setup.cpython-312.pyc differ diff --git a/__pycache__/custom_file_dialog.cpython-312.pyc b/__pycache__/custom_file_dialog.cpython-312.pyc index 2eea4bb..a049d2d 100644 Binary files a/__pycache__/custom_file_dialog.cpython-312.pyc and b/__pycache__/custom_file_dialog.cpython-312.pyc differ diff --git a/cfd_app_config.py b/cfd_app_config.py index a93a4bf..ecadeda 100755 --- a/cfd_app_config.py +++ b/cfd_app_config.py @@ -1 +1,128 @@ #!/usr/bin/python3 +"""App configuration for Wire-Py""" +import logging +from pathlib import Path +import os +from subprocess import CompletedProcess, run +from typing import Dict, Any +from shared_libs.common_tools import Translate + + +class AppConfig: + """Central configuration and system setup manager for the Wire-Py application. + + This class serves as a singleton-like container for all global configuration data, + including paths, UI settings, localization, versioning, and system-specific resources. + It ensures that required directories, files, and services are created and configured + before the application starts. Additionally, it provides tools for managing translations, + default settings, and autostart functionality to maintain a consistent user experience. + + Key Responsibilities: + - Centralizes all configuration values (paths, UI preferences, localization). + - Ensures required directories and files exist on startup. + - Handles translation setup via `gettext` for multilingual support. + - Manages default settings file generation. + - Configures autostart services using systemd for user-specific launch behavior. + + This class is used globally across the application to access configuration data + consistently and perform system-level setup tasks. + """ + + # 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 + + # Logging + LOG_DIR = Path.home() / ".local/share/lxlogs" + Path(LOG_DIR).mkdir(parents=True, exist_ok=True) + LOG_FILE_PATH = LOG_DIR / "cfiledialog.log" + + # Base paths + BASE_DIR: Path = Path.home() + CONFIG_DIR: Path = BASE_DIR / ".config/cfiledialog" + + # Configuration files + SETTINGS_FILE: Path = CONFIG_DIR / "settings" + DEFAULT_SETTINGS: Dict[str, str] = { + "# Configuration": "on", + "# Theme": "dark", + "# Tooltips": True, + "# Autostart": "off", + "# Logfile": LOG_FILE_PATH, + } + + # Updates + # 1 = 1. Year, 09 = Month of the Year, 2924 = Day and Year of the Year + VERSION: str = "v. 1.07.2725" + UPDATE_URL: str = "https://git.ilunix.de/api/v1/repos/punix/example/releases" + DOWNLOAD_URL: str = "https://git.ilunix.de/punix/example/archive" + + # UI configuration + UI_CONFIG: Dict[str, Any] = { + "window_title": "File Dialog", + "window_size": (1100, 850), + "font_family": "Ubuntu", + "font_size": 11, + "resizable_window": (True, True), + } + + # System-dependent paths + SYSTEM_PATHS: Dict[str, Path] = { + "tcl_path": "/usr/share/TK-Themes", + } + + @classmethod + def ensure_directories(cls) -> None: + """Ensures that all required directories exist""" + if not cls.CONFIG_DIR.exists(): + cls.CONFIG_DIR.mkdir(parents=True, exist_ok=True) + + @classmethod + def create_default_settings(cls) -> None: + """Creates default settings if they don't exist""" + if not cls.SETTINGS_FILE.exists(): + content = "\n".join( + f"[{k.upper()}]\n{v}" for k, v in cls.DEFAULT_SETTINGS.items() + ) + cls.SETTINGS_FILE.write_text(content) + + @classmethod + def ensure_log(cls) -> None: + """Ensures that the log file exists""" + if not cls.LOG_FILE_PATH.exists(): + cls.LOG_FILE_PATH.touch() + + +# here is initializing the class for translation strings +_ = Translate.setup_translations("custom_file_fialog") + + +class Msg: + """ + A utility class that provides centralized access to translated message strings. + + This class contains a dictionary of message strings used throughout the custom file dialog application. + All strings are prepared for translation using gettext. The short key names make the code + more concise while maintaining readability. + + Attributes: + STR (dict): A dictionary mapping short keys to translated message strings. + Keys are abbreviated for brevity but remain descriptive. + + Usage: + Import this class and access messages using the dictionary: + `Msg.STR["sel_tl"]` returns the translated "Select tunnel" message. + + Note: + Ensure that gettext translation is properly initialized before + accessing these strings to ensure correct localization. + """ + + STR: Dict[str, str] = { + # Strings for messages + + } + TTIP: Dict[str, str] = { + # Strings for Tooltips + + } diff --git a/cfd_ui_setup.py b/cfd_ui_setup.py new file mode 100644 index 0000000..afbd2a8 --- /dev/null +++ b/cfd_ui_setup.py @@ -0,0 +1,456 @@ +import os +import shutil +import tkinter as tk +from tkinter import ttk +import subprocess +import json +from shared_libs.common_tools import Tooltip, LxTools + + +class InputDialog(tk.Toplevel): + def __init__(self, parent, title, prompt): + super().__init__(parent) + self.title(title) + self.prompt = prompt + self.result = None + + self.transient(parent) + self.grab_set() + + self.setup_widgets() + LxTools.center_window_cross_platform(self, 300, 120) + self.entry.focus_set() + + self.bind("", self.on_ok) + self.bind("", self.on_cancel) + + self.protocol("WM_DELETE_WINDOW", self.on_cancel) + self.wait_window(self) + + def setup_widgets(self): + main_frame = ttk.Frame(self, padding=10) + main_frame.pack(fill="both", expand=True) + + ttk.Label(main_frame, text=self.prompt).pack(pady=5) + + self.entry = ttk.Entry(main_frame, width=40) + self.entry.pack(pady=5, padx=5) + + button_frame = ttk.Frame(main_frame) + button_frame.pack(pady=10) + + ok_button = ttk.Button(button_frame, text="OK", command=self.on_ok) + ok_button.pack(side="left", padx=5) + + cancel_button = ttk.Button( + button_frame, text="Abbrechen", command=self.on_cancel) + cancel_button.pack(side="left", padx=5) + + def on_ok(self, event=None): + self.result = self.entry.get() + self.destroy() + + def on_cancel(self, event=None): + self.result = None + self.destroy() + + def get_input(self): + return self.result + + +def get_xdg_user_dir(dir_key, fallback_name): + home = os.path.expanduser("~") + fallback_path = os.path.join(home, fallback_name) + config_path = os.path.join(home, ".config", "user-dirs.dirs") + + if not os.path.exists(config_path): + return fallback_path + + try: + with open(config_path, 'r') as f: + for line in f: + line = line.strip() + if line.startswith(f"{dir_key}="): + path = line.split('=', 1)[1].strip().strip('"') + path = path.replace('$HOME', home) + if not os.path.isabs(path): + path = os.path.join(home, path) + return path + except Exception: + pass + return fallback_path + + +class StyleManager: + def __init__(self, dialog): + self.dialog = dialog + self.setup_styles() + + def setup_styles(self): + style = ttk.Style(self.dialog) + base_bg = self.dialog.cget('background') + self.is_dark = sum(self.dialog.winfo_rgb(base_bg)) / 3 < 32768 + + if self.is_dark: + self.selection_color = "#4a6984" + self.icon_bg_color = "#3c3c3c" + self.accent_color = "#2a2a2a" + self.header = "#2b2b2b" + self.hover_extrastyle = "#4a4a4a" + self.hover_extrastyle2 = "#494949" + self.sidebar_color = "#333333" + self.bottom_color = self.accent_color + self.color_foreground = "#ffffff" + self.freespace_background = self.sidebar_color + else: + self.selection_color = "#cce5ff" + self.icon_bg_color = base_bg + self.accent_color = "#e0e0e0" + self.header = "#d9d9d9" + self.hover_extrastyle = "#f5f5f5" + self.hover_extrastyle2 = "#494949" + self.sidebar_color = "#e7e7e7" + self.bottom_color = "#cecece" + self.freespace_background = self.sidebar_color + self.color_foreground = "#000000" + + style.configure("Header.TButton.Borderless.Round", + background=self.header) + style.map("Header.TButton.Borderless.Round", background=[ + ('active', self.hover_extrastyle)]) + style.configure("Header.TButton.Active.Round", + background=self.selection_color) + 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)) + style.map("Dark.TButton.Borderless", background=[ + ('active', self.hover_extrastyle2)]) + style.configure("Accent.TFrame", background=self.header) + style.configure("Accent.TLabel", background=self.header) + style.configure("AccentBottom.TFrame", background=self.bottom_color) + style.configure("AccentBottom.TLabel", background=self.bottom_color) + style.configure("Sidebar.TFrame", background=self.sidebar_color) + style.configure("Content.TFrame", background=self.icon_bg_color) + style.configure("Item.TFrame", background=self.icon_bg_color) + style.map('Item.TFrame', background=[ + ('selected', self.selection_color)]) + style.configure("Item.TLabel", background=self.icon_bg_color) + style.map('Item.TLabel', background=[('selected', self.selection_color)], foreground=[ + ('selected', "black" if not self.is_dark else "white")]) + style.configure("Icon.TLabel", background=self.icon_bg_color) + style.map('Icon.TLabel', background=[ + ('selected', self.selection_color)]) + style.configure("Treeview.Heading", relief="flat", + borderwidth=0, font=('TkDefaultFont', 10, 'bold')) + style.configure("Treeview", rowheight=32, pady=2, background=self.icon_bg_color, + fieldbackground=self.icon_bg_color, borderwidth=0) + style.map("Treeview", background=[('selected', self.selection_color)], foreground=[ + ('selected', "black" if not self.is_dark else "white")]) + style.configure("TButton.Borderless.Round", anchor="w") + style.configure("Small.Horizontal.TProgressbar", thickness=8) + + +class WidgetManager: + def __init__(self, dialog): + self.dialog = dialog + self.style_manager = dialog.style_manager + self.setup_widgets() + + def setup_widgets(self): + # Main container + main_frame = ttk.Frame(self.dialog, style='Accent.TFrame') + main_frame.pack(fill="both", expand=True) + main_frame.grid_rowconfigure(2, weight=1) + main_frame.grid_columnconfigure(0, weight=1) + + # Top bar for navigation and path + top_bar = ttk.Frame( + main_frame, style='Accent.TFrame', padding=(0, 5, 0, 5)) + top_bar.grid(row=0, column=0, columnspan=2, sticky="ew") + top_bar.grid_columnconfigure(1, weight=1) + + # Navigation buttons + nav_buttons_container = ttk.Frame(top_bar, style='Accent.TFrame') + nav_buttons_container.grid(row=0, column=0, sticky="w") + + self.back_button = ttk.Button(nav_buttons_container, image=self.dialog.icon_manager.get_icon( + 'back'), command=self.dialog.go_back, state=tk.DISABLED, style="Header.TButton.Borderless.Round") + self.back_button.pack(side="left", padx=10) + Tooltip(self.back_button, "Zurück") + + self.forward_button = ttk.Button(nav_buttons_container, image=self.dialog.icon_manager.get_icon( + 'forward'), command=self.dialog.go_forward, state=tk.DISABLED, style="Header.TButton.Borderless.Round") + self.forward_button.pack(side="left") + Tooltip(self.forward_button, "Vorwärts") + + self.home_button = ttk.Button(nav_buttons_container, image=self.dialog.icon_manager.get_icon( + 'home'), command=lambda: self.dialog.navigate_to(os.path.expanduser("~")), style="Header.TButton.Borderless.Round") + self.home_button.pack(side="left", padx=10) + Tooltip(self.home_button, "Home") + + # Path entry + self.path_entry = ttk.Entry(top_bar) + self.path_entry.grid(row=0, column=1, sticky="ew") + self.path_entry.bind( + "", lambda e: self.dialog.navigate_to(self.path_entry.get())) + + # 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") + + self.new_folder_button = ttk.Button(right_top_bar_frame, image=self.dialog.icon_manager.get_icon( + 'new_folder_small'), command=self.dialog.create_new_folder, style="Header.TButton.Borderless.Round") + self.new_folder_button.pack(side="left", padx=5) + Tooltip(self.new_folder_button, "Neuen Ordner erstellen") + + self.new_file_button = ttk.Button(right_top_bar_frame, image=self.dialog.icon_manager.get_icon( + 'new_document_small'), command=self.dialog.create_new_file, style="Header.TButton.Borderless.Round") + self.new_file_button.pack(side="left", padx=(0, 10)) + Tooltip(self.new_file_button, "Neues Dokument erstellen") + + # 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.dialog.icon_manager.get_icon( + 'search_small'), command=self.dialog.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.dialog.icon_manager.get_icon( + 'recursive_small'), command=self.dialog.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.dialog.icon_manager.get_icon( + 'icon_view'), command=self.dialog.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.dialog.icon_manager.get_icon( + 'list_view'), command=self.dialog.set_list_view, style="Header.TButton.Borderless.Round") + self.list_view_button.pack(side="left") + Tooltip(self.list_view_button, "Listenansicht") + + self.hidden_files_button = ttk.Button(right_top_bar_frame, image=self.dialog.icon_manager.get_icon( + 'hide'), command=self.dialog.toggle_hidden_files, style="Header.TButton.Borderless.Round") + self.hidden_files_button.pack(side="left", padx=10) + Tooltip(self.hidden_files_button, "Versteckte Dateien anzeigen") + + # Horizontal separator + separator_color = "#000000" if self.style_manager.is_dark else "#9c9c9c" + tk.Frame(main_frame, height=1, bg=separator_color).grid( + row=1, column=0, columnspan=2, sticky="ew") + + # PanedWindow for resizable sidebar and content + paned_window = ttk.PanedWindow(main_frame, orient=tk.HORIZONTAL) + paned_window.grid(row=2, column=0, columnspan=2, sticky="nsew") + + # Sidebar + sidebar_frame = ttk.Frame( + paned_window, style="Sidebar.TFrame", padding=(0, 0, 0, 0), width=200) + sidebar_frame.grid_propagate(False) + sidebar_frame.bind("", self.dialog.on_sidebar_resize) + paned_window.add(sidebar_frame, weight=0) + sidebar_frame.grid_rowconfigure(2, weight=1) + + sidebar_buttons_frame = ttk.Frame( + sidebar_frame, style="Sidebar.TFrame", padding=(0, 15, 0, 0)) + sidebar_buttons_frame.grid(row=0, column=0, sticky="nsew") + sidebar_buttons_config = [ + {'name': 'Computer', 'icon': self.dialog.icon_manager.get_icon( + 'computer_small'), 'path': '/'}, + {'name': 'Downloads', 'icon': self.dialog.icon_manager.get_icon( + 'downloads_small'), 'path': get_xdg_user_dir("XDG_DOWNLOAD_DIR", "Downloads")}, + {'name': 'Dokumente', 'icon': self.dialog.icon_manager.get_icon( + 'documents_small'), 'path': get_xdg_user_dir("XDG_DOCUMENTS_DIR", "Documents")}, + {'name': 'Bilder', 'icon': self.dialog.icon_manager.get_icon( + 'pictures_small'), 'path': get_xdg_user_dir("XDG_PICTURES_DIR", "Pictures")}, + {'name': 'Musik', 'icon': self.dialog.icon_manager.get_icon( + 'music_small'), 'path': get_xdg_user_dir("XDG_MUSIC_DIR", "Music")}, + {'name': 'Videos', 'icon': self.dialog.icon_manager.get_icon( + 'video_small'), 'path': get_xdg_user_dir("XDG_VIDEO_DIR", "Videos")}, + ] + self.sidebar_buttons = [] + for config in sidebar_buttons_config: + btn = ttk.Button(sidebar_buttons_frame, text=f" {config['name']}", image=config['icon'], compound="left", + command=lambda p=config['path']: self.dialog.navigate_to(p), style="Dark.TButton.Borderless") + btn.pack(fill="x", pady=1) + self.sidebar_buttons.append((btn, f" {config['name']}")) + + separator_color = "#a9a9a9" if self.style_manager.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_frame = ttk.Frame( + sidebar_frame, style="Sidebar.TFrame") + mounted_devices_frame.grid(row=2, column=0, sticky="nsew", padx=10) + mounted_devices_frame.grid_columnconfigure(0, weight=1) + + ttk.Label(mounted_devices_frame, text="Geräte:", background=self.style_manager.sidebar_color, + foreground=self.style_manager.color_foreground).grid(row=0, column=0, sticky="ew", padx=10, pady=(5, 0)) + + self.devices_canvas = tk.Canvas( + mounted_devices_frame, highlightthickness=0, bg=self.style_manager.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") + + 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") + + self.devices_canvas.bind("", self.dialog._on_devices_enter) + self.devices_canvas.bind("", self.dialog._on_devices_leave) + self.devices_scrollable_frame.bind( + "", self.dialog._on_devices_enter) + self.devices_scrollable_frame.bind( + "", self.dialog._on_devices_leave) + + 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) + + def _on_devices_mouse_wheel(event): + if event.num == 4: + delta = -1 + elif event.num == 5: + delta = 1 + else: + delta = -1 * int(event.delta / 120) + self.devices_canvas.yview_scroll(delta, "units") + + 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) + + self.device_buttons = [] + for device_name, mount_point, removable in self.dialog._get_mounted_devices(): + icon = self.dialog.icon_manager.get_icon( + 'usb_small') if removable else self.dialog.icon_manager.get_icon('device_small') + button_text = f" {device_name}" + if len(device_name) > 15: + button_text = f" {device_name[:15]}\n{device_name[15:]}" + + btn = ttk.Button(self.devices_scrollable_frame, text=button_text, image=icon, compound="left", + command=lambda p=mount_point: self.dialog.navigate_to(p), style="Dark.TButton.Borderless") + btn.pack(fill="x", pady=1) + self.device_buttons.append((btn, button_text)) + + for w in [btn, self.devices_canvas, self.devices_scrollable_frame]: + w.bind("", _on_devices_mouse_wheel) + w.bind("", _on_devices_mouse_wheel) + w.bind("", _on_devices_mouse_wheel) + w.bind("", self.dialog._on_devices_enter) + w.bind("", self.dialog._on_devices_leave) + + try: + total, used, _ = shutil.disk_usage(mount_point) + progress_bar = ttk.Progressbar(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 + for w in [progress_bar]: + w.bind("", _on_devices_mouse_wheel) + w.bind("", _on_devices_mouse_wheel) + w.bind("", _on_devices_mouse_wheel) + w.bind("", self.dialog._on_devices_enter) + w.bind("", self.dialog._on_devices_leave) + except (FileNotFoundError, PermissionError): + pass + + tk.Frame(sidebar_frame, height=1, bg=separator_color).grid( + row=3, column=0, sticky="ew", padx=20, pady=15) + + storage_frame = ttk.Frame(sidebar_frame, style="Sidebar.TFrame") + storage_frame.grid(row=5, column=0, sticky="sew", padx=10, pady=10) + self.storage_label = ttk.Label( + storage_frame, text="Freier Speicher:", background=self.style_manager.freespace_background) + self.storage_label.pack(fill="x", padx=10) + self.storage_bar = ttk.Progressbar( + storage_frame, orient="horizontal", length=100, mode="determinate") + self.storage_bar.pack(fill="x", pady=(2, 5), padx=15) + + content_frame = ttk.Frame(paned_window, padding=( + 0, 0, 0, 0), style="AccentBottom.TFrame") + paned_window.add(content_frame, weight=1) + content_frame.grid_rowconfigure(0, weight=1) + content_frame.grid_columnconfigure(0, weight=1) + + self.file_list_frame = ttk.Frame( + content_frame, style="AccentBottom.TFrame") + self.file_list_frame.grid(row=0, column=0, sticky="nsew") + self.dialog.bind("", self.dialog.on_window_resize) + + bottom_controls_frame = ttk.Frame( + content_frame, style="AccentBottom.TFrame") + bottom_controls_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0)) + bottom_controls_frame.grid_columnconfigure(1, weight=1) + + # Status bar (top-left in the bottom area) + self.status_bar = ttk.Label( + bottom_controls_frame, text="", anchor="w", style="AccentBottom.TLabel") + self.status_bar.grid(row=0, column=0, columnspan=2, + sticky="w", padx=10, pady=5) + + # New folder/file buttons (top-right in the bottom area) + right_top_buttons = ttk.Frame( + bottom_controls_frame, style="AccentBottom.TFrame") + right_top_buttons.grid(row=0, column=2, sticky="e") + + self.new_folder_button = ttk.Button(right_top_buttons, image=self.dialog.icon_manager.get_icon( + 'new_folder_small'), command=self.dialog.create_new_folder, style="Header.TButton.Borderless.Round") + self.new_folder_button.pack(side="left", padx=5) + Tooltip(self.new_folder_button, "Neuen Ordner erstellen") + + self.new_file_button = ttk.Button(right_top_buttons, image=self.dialog.icon_manager.get_icon( + 'new_document_small'), command=self.dialog.create_new_file, style="Header.TButton.Borderless.Round") + self.new_file_button.pack(side="left", padx=(0, 10)) + Tooltip(self.new_file_button, "Neues Dokument erstellen") + + # Main action buttons (bottom-left) + left_bottom_buttons = ttk.Frame(bottom_controls_frame, style="AccentBottom.TFrame") + left_bottom_buttons.grid(row=1, column=0, sticky="w", pady=(5, 10)) + + if self.dialog.dialog_mode == "save": + self.filename_entry = ttk.Entry(left_bottom_buttons, width=50) + self.filename_entry.grid(row=0, column=0, padx=(10,5), pady=5, sticky="ew") + left_bottom_buttons.grid_columnconfigure(0, weight=1) + + ttk.Button(left_bottom_buttons, text="Speichern", command=self.dialog.on_save).grid(row=0, column=1, padx=5) + ttk.Button(left_bottom_buttons, text="Abbrechen", command=self.dialog.on_cancel).grid(row=0, column=2, padx=5) + else: + ttk.Button(left_bottom_buttons, text="Öffnen", command=self.dialog.on_open).grid(row=0, column=0, padx=(10, 5)) + ttk.Button(left_bottom_buttons, text="Abbrechen", command=self.dialog.on_cancel).grid(row=0, column=1, padx=5) + + + # Filter combobox (bottom-right) + self.filter_combobox = ttk.Combobox(bottom_controls_frame, values=[ft[0] for ft in self.dialog.filetypes], state="readonly") + self.filter_combobox.grid(row=1, column=2, sticky="e", padx=(0, 10), pady=(5, 10)) + self.filter_combobox.bind("<>", self.dialog.on_filter_change) + self.filter_combobox.set(self.dialog.filetypes[0][0]) diff --git a/custom_file_dialog.py b/custom_file_dialog.py index 444b0ab..25e3d29 100644 --- a/custom_file_dialog.py +++ b/custom_file_dialog.py @@ -6,43 +6,39 @@ from datetime import datetime import subprocess import json from shared_libs.message import MessageDialog -from shared_libs.common_tools import IconManager, Tooltip +from shared_libs.common_tools import IconManager, Tooltip, ConfigManager, LxTools, ThemeManager +from cfd_app_config import AppConfig +from cfd_ui_setup import StyleManager, WidgetManager, get_xdg_user_dir, InputDialog + # 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_xdg_user_dir(dir_key, fallback_name): - home = os.path.expanduser("~") - fallback_path = os.path.join(home, fallback_name) - config_path = os.path.join(home, ".config", "user-dirs.dirs") - - if not os.path.exists(config_path): - return fallback_path - - try: - with open(config_path, 'r') as f: - for line in f: - line = line.strip() - if line.startswith(f"{dir_key}="): - path = line.split('=', 1)[1].strip().strip('"') - path = path.replace('$HOME', home) - if not os.path.isabs(path): - path = os.path.join(home, path) - return path - except Exception: - pass - return fallback_path - - class CustomFileDialog(tk.Toplevel): - def __init__(self, parent, initial_dir=None, filetypes=None): + def __init__(self, parent, initial_dir=None, filetypes=None, dialog_mode="open"): super().__init__(parent) + + self.my_tool_tip = None + self.dialog_mode = dialog_mode + + self.x_width = AppConfig.UI_CONFIG["window_size"][0] + self.y_height = AppConfig.UI_CONFIG["window_size"][1] + # Set the window size + self.geometry(f"{self.x_width}x{self.y_height}") + self.minsize(AppConfig.UI_CONFIG["window_size"][0], + AppConfig.UI_CONFIG["window_size"][1], + ) + self.title(AppConfig.UI_CONFIG["window_title"]) + # self.tk.call( + # "source", f"{AppConfig.SYSTEM_PATHS['tcl_path']}/water.tcl") + # ConfigManager.init(AppConfig.SETTINGS_FILE) + # theme = ConfigManager.get("theme") + # ThemeManager.change_theme(self, theme) + self.image = IconManager() + LxTools.center_window_cross_platform(self, self.x_width, self.y_height) self.parent = parent - self.title("Datei auswählen") - self.geometry("1100x850") - self.minsize(650, 400) self.transient(parent) self.grab_set() @@ -57,15 +53,14 @@ 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.icon_manager = IconManager() - self.create_styles() - self.create_widgets() + self.style_manager = StyleManager(self) + self.widget_manager = WidgetManager(self) + self.navigate_to(self.current_dir) def get_file_icon(self, filename, size='large'): @@ -88,387 +83,20 @@ class CustomFileDialog(tk.Toplevel): return self.icon_manager.get_icon(f'iso_{size}') return self.icon_manager.get_icon(f'file_{size}') - def create_styles(self): - style = ttk.Style(self) - base_bg = self.cget('background') - self.is_dark = sum(self.winfo_rgb(base_bg)) / 3 < 32768 - - if self.is_dark: - self.selection_color = "#4a6984" # Darker blue for selection - self.icon_bg_color = "#3c3c3c" # Lighter background for content - self.accent_color = "#2a2a2a" # Darker accent for the bottom - self.header = "#2b2b2b" # Dark Color for Header and round buttons in Header - self.hover_extrastyle = "#4a4a4a" # Hover Color for Buttons in header and Sidebar - self.hover_extrastyle2 = "#494949" - self.sidebar_color = "#333333" - self.bottom_color = self.accent_color - self.color_foreground = "#ffffff" - self.freespace_background = self.sidebar_color - - else: - self.selection_color = "#cce5ff" # Light blue for selection - self.icon_bg_color = base_bg # Main background for content - self.accent_color = "#e0e0e0" - # Light Color for Header and round buttons in Header - self.header = "#d9d9d9" - # Hover Color for Buttons in header and Sidebar - self.hover_extrastyle = "#f5f5f5" - self.hover_extrastyle2 = "#494949" # Hover Color for Buttons in Sidebar - self.sidebar_color = "#e7e7e7" - self.bottom_color = "#cecece" - self.freespace_background = self.sidebar_color - self.color_foreground = "#000000" - - style.configure("Header.TButton.Borderless.Round", - background=self.header) - - 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)) - - style.map("Dark.TButton.Borderless", background=[ - ('active', self.hover_extrastyle2)]) - - style.configure("Accent.TFrame", background=self.header) - style.configure("Accent.TLabel", background=self.header) - style.configure("AccentBottom.TFrame", background=self.bottom_color) - style.configure("AccentBottom.TLabel", background=self.bottom_color) - style.configure("Sidebar.TFrame", background=self.sidebar_color) - - style.configure("Content.TFrame", background=self.icon_bg_color) - style.configure("Item.TFrame", background=self.icon_bg_color) - style.map('Item.TFrame', background=[ - ('selected', self.selection_color)]) - style.configure("Item.TLabel", background=self.icon_bg_color) - style.map('Item.TLabel', background=[('selected', self.selection_color)], foreground=[ - ('selected', "black" if not self.is_dark else "white")]) - style.configure("Icon.TLabel", background=self.icon_bg_color) - style.map('Icon.TLabel', background=[ - ('selected', self.selection_color)]) - - style.configure("Treeview.Heading", relief="flat", - borderwidth=0, font=('TkDefaultFont', 10, 'bold')) - style.configure("Treeview", rowheight=32, pady=2, - background=self.icon_bg_color, fieldbackground=self.icon_bg_color, borderwidth=0) - style.map("Treeview", background=[('selected', self.selection_color)], foreground=[ - ('selected', "black" if not self.is_dark else "white")]) - - style.configure("TButton.Borderless.Round", anchor="w") - style.configure("Small.Horizontal.TProgressbar", thickness=8) - - def create_widgets(self): - # Main container - main_frame = ttk.Frame(self, style='Accent.TFrame') - main_frame.pack(fill="both", expand=True) - main_frame.grid_rowconfigure(2, weight=1) - main_frame.grid_columnconfigure(0, weight=1) - - # Top bar for navigation and path - top_bar = ttk.Frame( - main_frame, style='Accent.TFrame', padding=(0, 5, 0, 5)) - top_bar.grid(row=0, column=0, columnspan=2, sticky="ew") - top_bar.grid_columnconfigure(1, weight=1) - - # Navigation buttons - nav_buttons_container = ttk.Frame(top_bar, style='Accent.TFrame') - nav_buttons_container.grid(row=0, column=0, sticky="w") - - self.back_button = ttk.Button( - nav_buttons_container, image=self.icon_manager.get_icon('back'), command=self.go_back, state=tk.DISABLED, style="Header.TButton.Borderless.Round") - self.back_button.pack(side="left", padx=10) - Tooltip(self.back_button, "Zurück") - - self.forward_button = ttk.Button( - nav_buttons_container, image=self.icon_manager.get_icon('forward'), command=self.go_forward, state=tk.DISABLED, style="Header.TButton.Borderless.Round") - self.forward_button.pack(side="left") - Tooltip(self.forward_button, "Vorwärts") - - self.home_button = ttk.Button(nav_buttons_container, image=self.icon_manager.get_icon('home'), command=lambda: self.navigate_to( - os.path.expanduser("~")), style="Header.TButton.Borderless.Round") - self.home_button.pack(side="left", padx=10) - Tooltip(self.home_button, "Home") - - # Path entry - self.path_entry = ttk.Entry(top_bar) - self.path_entry.grid(row=0, column=1, sticky="ew") - self.path_entry.bind( - "", lambda e: self.navigate_to(self.path_entry.get())) - - # 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.icon_manager.get_icon('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.icon_manager.get_icon('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.icon_manager.get_icon('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.icon_manager.get_icon('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") - - self.hidden_files_button = ttk.Button( - right_top_bar_frame, image=self.icon_manager.get_icon('hide'), command=self.toggle_hidden_files, style="Header.TButton.Borderless.Round") - self.hidden_files_button.pack(side="left", padx=10) - Tooltip(self.hidden_files_button, "Versteckte Dateien anzeigen") - - # Horizontal separator - separator_color = "#000000" if self.is_dark else "#9c9c9c" - tk.Frame(main_frame, height=1, bg=separator_color).grid( - row=1, column=0, columnspan=2, sticky="ew") - - # PanedWindow for resizable sidebar and content - paned_window = ttk.PanedWindow(main_frame, orient=tk.HORIZONTAL) - paned_window.grid(row=2, column=0, columnspan=2, sticky="nsew") - - # Sidebar - sidebar_frame = ttk.Frame( - 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.bind("", self.on_sidebar_resize) - # Use weight=0 to give it a fixed size - paned_window.add(sidebar_frame, weight=0) - - # 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( - row=0, column=0, sticky="nsew") - sidebar_buttons_config = [ - {'name': 'Computer', - 'icon': self.icon_manager.get_icon('computer_small'), 'path': '/'}, - {'name': 'Downloads', 'icon': self.icon_manager.get_icon('downloads_small'), 'path': get_xdg_user_dir( - "XDG_DOWNLOAD_DIR", "Downloads")}, - {'name': 'Dokumente', 'icon': self.icon_manager.get_icon('documents_small'), 'path': get_xdg_user_dir( - "XDG_DOCUMENTS_DIR", "Documents")}, - {'name': 'Bilder', 'icon': self.icon_manager.get_icon('pictures_small'), 'path': get_xdg_user_dir( - "XDG_PICTURES_DIR", "Pictures")}, - {'name': 'Musik', 'icon': self.icon_manager.get_icon('music_small'), 'path': get_xdg_user_dir( - "XDG_MUSIC_DIR", "Music")}, - {'name': 'Videos', 'icon': self.icon_manager.get_icon('video_small'), 'path': get_xdg_user_dir( - "XDG_VIDEO_DIR", "Videos")}, - ] - for config in sidebar_buttons_config: - 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 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) - # 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.icon_manager.get_icon( - 'usb_small') if removable else self.icon_manager.get_icon('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(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( - 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=5, 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) - self.storage_bar = ttk.Progressbar( - storage_frame, orient="horizontal", length=100, mode="determinate") - self.storage_bar.pack(fill="x", pady=(2, 5), padx=15) - - # Content area - content_frame = ttk.Frame(paned_window, padding=( - 0, 0, 0, 0), style="AccentBottom.TFrame") - paned_window.add(content_frame, weight=1) - - content_frame.grid_rowconfigure(0, weight=1) - content_frame.grid_columnconfigure(0, weight=1) - - self.file_list_frame = ttk.Frame( - content_frame, style="AccentBottom.TFrame") - self.file_list_frame.grid(row=0, column=0, sticky="nsew") - self.bind("", self.on_window_resize) - - # Bottom controls - bottom_controls_frame = ttk.Frame( - content_frame, style="AccentBottom.TFrame") - bottom_controls_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0)) - bottom_controls_frame.grid_columnconfigure(1, weight=1) - - self.status_bar = ttk.Label( - bottom_controls_frame, text="", anchor="w", style="AccentBottom.TLabel") - self.status_bar.grid(row=0, column=1, columnspan=3, - sticky="ew", padx=10, pady=10) - - ttk.Button(bottom_controls_frame, text="Öffnen", - command=self.on_open).grid(row=0, column=0, padx=(10, 0)) - ttk.Button(bottom_controls_frame, text="Speichern", - command=self.on_save).grid(row=1, column=0, padx=(10, 0)) - ttk.Button(bottom_controls_frame, text="Abbrechen", - command=self.on_cancel).grid(row=1, column=1, columnspan=3, padx=(10, 0)) - - self.filter_combobox = ttk.Combobox( - bottom_controls_frame, values=[ft[0] for ft in self.filetypes], state="readonly") - self.filter_combobox.grid( - row=1, column=2, sticky="w", padx=(0, 10), pady=(5, 10)) - self.filter_combobox.bind( - "<>", self.on_filter_change) - self.filter_combobox.set(self.filetypes[0][0]) - def toggle_hidden_files(self): self.show_hidden_files.set(not self.show_hidden_files.get()) if self.show_hidden_files.get(): - self.hidden_files_button.config( + self.widget_manager.hidden_files_button.config( image=self.icon_manager.get_icon('unhide')) - Tooltip(self.hidden_files_button, "Versteckte Dateien ausblenden") + Tooltip(self.widget_manager.hidden_files_button, "Versteckte Dateien ausblenden") else: - self.hidden_files_button.config( + self.widget_manager.hidden_files_button.config( image=self.icon_manager.get_icon('hide')) - Tooltip(self.hidden_files_button, "Versteckte Dateien anzeigen") + Tooltip(self.widget_manager.hidden_files_button, "Versteckte Dateien anzeigen") self.populate_files() def on_window_resize(self, event): - new_width = self.file_list_frame.winfo_width() + new_width = self.widget_manager.file_list_frame.winfo_width() if self.view_mode.get() == "icons" and abs(new_width - self.last_width) > 50: if self.resize_job: self.after_cancel(self.resize_job) @@ -482,98 +110,98 @@ class CustomFileDialog(tk.Toplevel): if current_width < threshold_width: # Hide text, show only icons - for btn, original_text in self.sidebar_buttons: + for btn, original_text in self.widget_manager.sidebar_buttons: btn.config(text="", compound="top") - for btn, original_text in self.device_buttons: + for btn, original_text in self.widget_manager.device_buttons: btn.config(text="", compound="top") else: # Show text - for btn, original_text in self.sidebar_buttons: + for btn, original_text in self.widget_manager.sidebar_buttons: btn.config(text=original_text, compound="left") - for btn, original_text in self.device_buttons: + for btn, original_text in self.widget_manager.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") + self.widget_manager.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() + widget_x = self.widget_manager.devices_canvas.winfo_rootx() + widget_y = self.widget_manager.devices_canvas.winfo_rooty() + widget_width = self.widget_manager.devices_canvas.winfo_width() + widget_height = self.widget_manager.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() + self.widget_manager.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) + self.original_path_text = self.widget_manager.path_entry.get() + self.widget_manager.path_entry.delete(0, tk.END) + self.widget_manager.path_entry.insert(0, "Suchbegriff eingeben...") + self.widget_manager.path_entry.bind("", self.execute_search) + self.widget_manager.path_entry.bind("", self.clear_search_placeholder) # Show search options - self.search_options_frame.pack(side="left", padx=(5, 0)) + self.widget_manager.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("") + self.widget_manager.path_entry.delete(0, tk.END) + self.widget_manager.path_entry.insert(0, self.original_path_text) + self.widget_manager.path_entry.bind( + "", lambda e: self.navigate_to(self.widget_manager.path_entry.get())) + self.widget_manager.path_entry.unbind("") # Hide search options - self.search_options_frame.pack_forget() + self.widget_manager.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( + self.widget_manager.recursive_search.set(not self.widget_manager.recursive_search.get()) + if self.widget_manager.recursive_search.get(): + self.widget_manager.recursive_button.configure( style="Header.TButton.Active.Round") else: - self.recursive_button.configure( + self.widget_manager.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( + self.widget_manager.icon_view_button.configure(style="Header.TButton.Active.Round") + self.widget_manager.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( + self.widget_manager.list_view_button.configure(style="Header.TButton.Active.Round") + self.widget_manager.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) + if self.widget_manager.path_entry.get() == "Suchbegriff eingeben...": + self.widget_manager.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() + search_term = self.widget_manager.path_entry.get().strip() if not search_term or search_term == "Suchbegriff eingeben...": return @@ -614,7 +242,7 @@ class CustomFileDialog(tk.Toplevel): os.chdir(search_dir) # Build find command based on recursive setting (use . for current directory) - if self.recursive_search.get(): + if self.widget_manager.recursive_search.get(): find_cmd = ['find', '.', '-iname', f'*{search_term}*', '-type', 'f'] else: @@ -683,11 +311,11 @@ class CustomFileDialog(tk.Toplevel): 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(): + for widget in self.widget_manager.file_list_frame.winfo_children(): widget.destroy() # Create TreeView for search results - tree_frame = ttk.Frame(self.file_list_frame) + tree_frame = ttk.Frame(self.widget_manager.file_list_frame) tree_frame.pack(fill='both', expand=True) tree_frame.grid_rowconfigure(0, weight=1) tree_frame.grid_columnconfigure(0, weight=1) @@ -755,29 +383,29 @@ class CustomFileDialog(tk.Toplevel): self.unbind_all("") self.unbind_all("") - def populate_files(self): + def populate_files(self, item_to_rename=None): # Unbind previous global mouse wheel events self._unbind_mouse_wheel_events() - for widget in self.file_list_frame.winfo_children(): + for widget in self.widget_manager.file_list_frame.winfo_children(): widget.destroy() - self.path_entry.delete(0, tk.END) - self.path_entry.insert(0, self.current_dir) + self.widget_manager.path_entry.delete(0, tk.END) + self.widget_manager.path_entry.insert(0, self.current_dir) self.selected_file = None self.update_status_bar() if self.view_mode.get() == "list": - self.populate_list_view() + self.populate_list_view(item_to_rename) else: - self.populate_icon_view() + self.populate_icon_view(item_to_rename) def _get_sorted_items(self): try: items = os.listdir(self.current_dir) num_items = len(items) warning_message = None - if num_items > MAX_ITEMS_TO_DISPLAY: - warning_message = f"Zeige {MAX_ITEMS_TO_DISPLAY} von {num_items} Einträgen." - items = items[:MAX_ITEMS_TO_DISPLAY] + if num_items > AppConfig.MAX_ITEMS_TO_DISPLAY: + warning_message = f"Zeige {AppConfig.MAX_ITEMS_TO_DISPLAY} von {num_items} Einträgen." + items = items[:AppConfig.MAX_ITEMS_TO_DISPLAY] dirs = sorted([d for d in items if os.path.isdir( os.path.join(self.current_dir, d))], key=str.lower) files = sorted([f for f in items if not os.path.isdir( @@ -788,11 +416,11 @@ class CustomFileDialog(tk.Toplevel): except FileNotFoundError: return ([], "Verzeichnis nicht gefunden.", None) - def populate_icon_view(self): - canvas = tk.Canvas(self.file_list_frame, - highlightthickness=0, bg=self.icon_bg_color) + def populate_icon_view(self, item_to_rename=None): + canvas = tk.Canvas(self.widget_manager.file_list_frame, + highlightthickness=0, bg=self.style_manager.icon_bg_color) v_scrollbar = ttk.Scrollbar( - self.file_list_frame, orient="vertical", command=canvas.yview) + self.widget_manager.file_list_frame, orient="vertical", command=canvas.yview) canvas.pack(side="left", fill="both", expand=True) v_scrollbar.pack(side="right", fill="y") container_frame = ttk.Frame(canvas, style="Content.TFrame") @@ -818,13 +446,13 @@ class CustomFileDialog(tk.Toplevel): items, error, warning = self._get_sorted_items() if warning: - self.status_bar.config(text=warning) + self.widget_manager.status_bar.config(text=warning) if error: ttk.Label(container_frame, text=error).pack(pady=20) return item_width, item_height = 125, 100 - frame_width = self.file_list_frame.winfo_width() + frame_width = self.widget_manager.file_list_frame.winfo_width() col_count = max(1, frame_width // item_width - 1) row, col = 0, 0 for name in items: @@ -839,30 +467,35 @@ class CustomFileDialog(tk.Toplevel): container_frame, width=item_width, height=item_height, style="Item.TFrame") item_frame.grid(row=row, column=col, padx=5, ipadx=25, pady=5) item_frame.grid_propagate(False) - icon = self.icon_manager.get_icon('folder_large') if is_dir else self.get_file_icon( - name, 'large') - icon_label = ttk.Label(item_frame, image=icon, style="Icon.TLabel") - icon_label.pack(pady=(10, 5)) - name_label = ttk.Label(item_frame, text=self.shorten_text( - name, 14), anchor="center", style="Item.TLabel") - name_label.pack(fill="x", expand=True) - Tooltip(item_frame, name) - # Bind events to all individual widgets so scrolling works everywhere - for widget in [item_frame, icon_label, name_label]: - widget.bind("", lambda e, - p=path: self.on_item_double_click(p)) - widget.bind("", lambda e, p=path, - f=item_frame: self.on_item_select(p, f)) - widget.bind("", _on_mouse_wheel) - widget.bind("", _on_mouse_wheel) - widget.bind("", _on_mouse_wheel) + + if name == item_to_rename: + self.start_rename(item_frame, path) + else: + icon = self.icon_manager.get_icon('folder_large') if is_dir else self.get_file_icon( + name, 'large') + icon_label = ttk.Label(item_frame, image=icon, style="Icon.TLabel") + icon_label.pack(pady=(10, 5)) + name_label = ttk.Label(item_frame, text=self.shorten_text( + name, 14), anchor="center", style="Item.TLabel") + name_label.pack(fill="x", expand=True) + Tooltip(item_frame, name) + # Bind events to all individual widgets so scrolling works everywhere + for widget in [item_frame, icon_label, name_label]: + widget.bind("", lambda e, + p=path: self.on_item_double_click(p)) + widget.bind("", lambda e, p=path, + f=item_frame: self.on_item_select(p, f)) + widget.bind("", _on_mouse_wheel) + widget.bind("", _on_mouse_wheel) + widget.bind("", _on_mouse_wheel) + widget.bind("", lambda e, p=path, f=item_frame: self.on_rename_request(e, p, f)) col = (col + 1) % col_count if col == 0: row += 1 - def populate_list_view(self): - tree_frame = ttk.Frame(self.file_list_frame) + def populate_list_view(self, item_to_rename=None): + tree_frame = ttk.Frame(self.widget_manager.file_list_frame) tree_frame.pack(fill='both', expand=True) tree_frame.grid_rowconfigure(0, weight=1) tree_frame.grid_columnconfigure(0, weight=1) @@ -896,10 +529,11 @@ class CustomFileDialog(tk.Toplevel): self.tree.bind("", self.on_list_double_click) self.tree.bind("<>", self.on_list_select) + self.tree.bind("", self.on_rename_request) items, error, warning = self._get_sorted_items() if warning: - self.status_bar.config(text=warning) + self.widget_manager.status_bar.config(text=warning) if error: self.tree.insert("", "end", text=error, values=()) return @@ -921,8 +555,13 @@ class CustomFileDialog(tk.Toplevel): else: icon, file_type, size = self.get_file_icon( name, 'small'), "Datei", self._format_size(stat.st_size) - self.tree.insert("", "end", text=f" {name}", image=icon, values=( + item_id = self.tree.insert("", "end", text=f" {name}", image=icon, values=( size, file_type, modified_time)) + if name == item_to_rename: + self.tree.selection_set(item_id) + self.tree.focus(item_id) + self.start_rename(item_id, path) + except (FileNotFoundError, PermissionError): continue @@ -939,6 +578,9 @@ class CustomFileDialog(tk.Toplevel): self.selected_item_frame = item_frame self.selected_file = path self.update_status_bar() + if self.dialog_mode == "save" and not os.path.isdir(path): + self.widget_manager.path_entry.delete(0, tk.END) + self.widget_manager.path_entry.insert(0, path) def on_list_select(self, event): if not self.tree.selection(): @@ -947,10 +589,24 @@ class CustomFileDialog(tk.Toplevel): item_text = self.tree.item(item_id, 'text').strip() self.selected_file = os.path.join(self.current_dir, item_text) self.update_status_bar() + if self.dialog_mode == "save" and not os.path.isdir(self.selected_file): + self.widget_manager.path_entry.delete(0, tk.END) + self.widget_manager.path_entry.insert(0, self.selected_file) + + def on_rename_request(self, event, item_path=None, item_frame=None): + if self.view_mode.get() == "list": + if not self.tree.selection(): + return + item_id = self.tree.selection()[0] + item_path = os.path.join(self.current_dir, self.tree.item(item_id, "text").strip()) + self.start_rename(item_id, item_path) + else: # icon view + if item_path and item_frame: + self.start_rename(item_frame, item_path) def _handle_unsupported_file(self, path): if path.lower().endswith('.svg'): - self.status_bar.config( + self.widget_manager.status_bar.config( text="SVG-Dateien werden nicht unterstützt.") return True return False @@ -979,7 +635,7 @@ class CustomFileDialog(tk.Toplevel): self.destroy() def on_filter_change(self, event): - selected_desc = self.filter_combobox.get() + selected_desc = self.widget_manager.filter_combobox.get() for desc, pattern in self.filetypes: if desc == selected_desc: self.current_filter_pattern = pattern @@ -991,11 +647,11 @@ class CustomFileDialog(tk.Toplevel): real_path = os.path.realpath( os.path.abspath(os.path.expanduser(path))) if not os.path.isdir(real_path): - self.status_bar.config( + self.widget_manager.status_bar.config( text=f"Fehler: Verzeichnis '{os.path.basename(path)}' nicht gefunden.") return if not os.access(real_path, os.R_OK): - self.status_bar.config( + self.widget_manager.status_bar.config( text=f"Zugriff auf '{os.path.basename(path)}' verweigert.") return self.current_dir = real_path @@ -1007,8 +663,9 @@ class CustomFileDialog(tk.Toplevel): self.populate_files() self.update_nav_buttons() self.update_status_bar() + self.update_action_buttons_state() except Exception as e: - self.status_bar.config(text=f"Fehler: {e}") + self.widget_manager.status_bar.config(text=f"Fehler: {e}") def go_back(self): if self.history_pos > 0: @@ -1027,28 +684,28 @@ class CustomFileDialog(tk.Toplevel): self.update_status_bar() def update_nav_buttons(self): - self.back_button.config( + self.widget_manager.back_button.config( state=tk.NORMAL if self.history_pos > 0 else tk.DISABLED) - self.forward_button.config(state=tk.NORMAL if self.history_pos < len( + self.widget_manager.forward_button.config(state=tk.NORMAL if self.history_pos < len( self.history) - 1 else tk.DISABLED) def update_status_bar(self): try: total, used, free = shutil.disk_usage(self.current_dir) free_str = self._format_size(free) - self.storage_label.config(text=f"Freier Speicher: {free_str}") - self.storage_bar['value'] = (used / total) * 100 + self.widget_manager.storage_label.config(text=f"Freier Speicher: {free_str}") + self.widget_manager.storage_bar['value'] = (used / total) * 100 status_text = "" - if self.selected_file and os.path.exists(self.selected_file) and not os.path.isdir(self.selected_file): + if self.dialog_mode == "open" and self.selected_file and os.path.exists(self.selected_file) and not os.path.isdir(self.selected_file): size = os.path.getsize(self.selected_file) size_str = self._format_size(size) status_text = f"'{os.path.basename(self.selected_file)}' Größe: {size_str}" - self.status_bar.config(text=status_text) + self.widget_manager.status_bar.config(text=status_text) except FileNotFoundError: - self.status_bar.config(text="Verzeichnis nicht gefunden") - self.storage_label.config(text="Freier Speicher: Unbekannt") - self.storage_bar['value'] = 0 + self.widget_manager.status_bar.config(text="Verzeichnis nicht gefunden") + self.widget_manager.storage_label.config(text="Freier Speicher: Unbekannt") + self.widget_manager.storage_bar['value'] = 0 def on_open(self): if self.selected_file and os.path.isfile(self.selected_file): @@ -1057,7 +714,10 @@ class CustomFileDialog(tk.Toplevel): self.destroy() def on_save(self): - pass + file_name = self.widget_manager.filename_entry.get() + if file_name: + self.selected_file = os.path.join(self.current_dir, file_name) + self.destroy() def on_cancel(self): self.selected_file = None @@ -1066,6 +726,111 @@ class CustomFileDialog(tk.Toplevel): def get_selected_file(self): return self.selected_file + def create_new_folder(self): + self._create_new_item(is_folder=True) + + def create_new_file(self): + self._create_new_item(is_folder=False) + + def _create_new_item(self, is_folder): + base_name = "Neuer Ordner" if is_folder else "Neues Dokument.txt" + new_name = self._get_unique_name(base_name) + new_path = os.path.join(self.current_dir, new_name) + + try: + if is_folder: + os.mkdir(new_path) + else: + open(new_path, 'a').close() + self.populate_files(item_to_rename=new_name) + except Exception as e: + self.widget_manager.status_bar.config(text=f"Fehler beim Erstellen: {e}") + + def _get_unique_name(self, base_name): + name, ext = os.path.splitext(base_name) + counter = 1 + new_name = base_name + while os.path.exists(os.path.join(self.current_dir, new_name)): + counter += 1 + new_name = f"{name} {counter}{ext}" + return new_name + + def start_rename(self, item_widget, item_path): + if self.view_mode.get() == "icons": + self._start_rename_icon_view(item_widget, item_path) + else: # list view + self._start_rename_list_view(item_widget) # item_widget is item_id + + def _start_rename_icon_view(self, item_frame, item_path): + for child in item_frame.winfo_children(): + child.destroy() + + entry = ttk.Entry(item_frame) + entry.insert(0, os.path.basename(item_path)) + entry.select_range(0, tk.END) + entry.pack(fill="both", expand=True, padx=5, pady=5) + entry.focus_set() + + def finish_rename(event): + new_name = entry.get() + new_path = os.path.join(self.current_dir, new_name) + if new_name and new_path != item_path: + if os.path.exists(new_path): + self.widget_manager.status_bar.config(text=f"'{new_name}' existiert bereits.") + self.populate_files() + return + try: + os.rename(item_path, new_path) + except Exception as e: + self.widget_manager.status_bar.config(text=f"Fehler beim Umbenennen: {e}") + self.populate_files() + + def cancel_rename(event): + self.populate_files() + + entry.bind("", finish_rename) + entry.bind("", finish_rename) + entry.bind("", cancel_rename) + + def _start_rename_list_view(self, item_id): + x, y, width, height = self.tree.bbox(item_id, column="#0") + entry = ttk.Entry(self.tree) + entry.place(x=x, y=y, width=width, height=height) + + item_text = self.tree.item(item_id, "text").strip() + entry.insert(0, item_text) + entry.select_range(0, tk.END) + entry.focus_set() + + def finish_rename(event): + new_name = entry.get() + old_path = os.path.join(self.current_dir, item_text) + new_path = os.path.join(self.current_dir, new_name) + + if new_name and new_path != old_path: + if os.path.exists(new_path): + self.widget_manager.status_bar.config(text=f"'{new_name}' existiert bereits.") + else: + try: + os.rename(old_path, new_path) + except Exception as e: + self.widget_manager.status_bar.config(text=f"Fehler beim Umbenennen: {e}") + entry.destroy() + self.populate_files() + + def cancel_rename(event): + entry.destroy() + + entry.bind("", finish_rename) + entry.bind("", finish_rename) + entry.bind("", cancel_rename) + + def update_action_buttons_state(self): + is_writable = os.access(self.current_dir, os.W_OK) + state = tk.NORMAL if is_writable else tk.DISABLED + self.widget_manager.new_folder_button.config(state=state) + self.widget_manager.new_file_button.config(state=state) + def _matches_filetype(self, filename): if self.current_filter_pattern == "*.*": return True @@ -1077,6 +842,8 @@ class CustomFileDialog(tk.Toplevel): return True return False + + def _format_size(self, size_bytes): if size_bytes is None: return "" diff --git a/mainwindow.py b/mainwindow.py index 6a1f6c9..c0bd05e 100755 --- a/mainwindow.py +++ b/mainwindow.py @@ -30,24 +30,23 @@ class GlotzMol(tk.Tk): def open_custom_dialog(self): - dialog = CustomFileDialog(self, - initial_dir=os.path.expanduser("~"), - filetypes=[("Wireguard Files (.conf)", "*.conf"), - ("All Files", "*.*") - ]) + CustomFileDialog(self, + initial_dir=os.path.expanduser("~"), + filetypes=[("All Files", "*.*") + ]) # This is the crucial part: wait for the dialog to be closed - self.wait_window(dialog) + # self.wait_window(dialog) # Now, get the result - selected_path = dialog.get_selected_file() + # selected_path = dialog.get_selected_file() - if selected_path: - self.iso_path_entry.delete(0, tk.END) - self.iso_path_entry.insert(0, selected_path) - print(f"Die ausgewählte Datei ist: {selected_path}") - else: - print("Keine Datei ausgewählt.") + # if selected_path: + # self.iso_path_entry.delete(0, tk.END) + # self.iso_path_entry.insert(0, selected_path) + # print(f"Die ausgewählte Datei ist: {selected_path}") + # else: + # print("Keine Datei ausgewählt.") if __name__ == "__main__":