diff --git a/__pycache__/cfd_app_config.cpython-312.pyc b/__pycache__/cfd_app_config.cpython-312.pyc index a9be0d2..ec0a9b7 100644 Binary files a/__pycache__/cfd_app_config.cpython-312.pyc 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 index 1d667d5..fccfdf0 100644 Binary files a/__pycache__/cfd_ui_setup.cpython-312.pyc 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 b3a6c1e..dd120cb 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 616ff5a..ef5a71b 100755 --- a/cfd_app_config.py +++ b/cfd_app_config.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 """App configuration for Custom File Dialog""" +import json from pathlib import Path import os from typing import Dict, Any @@ -13,8 +14,7 @@ class AppConfig: 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. + before the application starts. Additionally, it provides tools for managing translations. Key Responsibilities: - Centralizes all configuration values (paths, UI preferences, localization). @@ -35,15 +35,6 @@ class AppConfig: 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", - } - # UI configuration UI_CONFIG: Dict[str, Any] = { "window_size": (1050, 850), @@ -53,23 +44,6 @@ class AppConfig: "resizable_window": (True, True), } - @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) - - -import json # here is initializing the class for translation strings _ = Translate.setup_translations("custom_file_fialog") @@ -85,7 +59,10 @@ class CfdConfigManager: "search_icon_pos": "left", # 'left' or 'right' "button_box_pos": "left", # 'left' or 'right' "window_size_preset": "1050x850", # e.g., "1050x850" - "default_view_mode": "icons" # 'icons' or 'list' + "default_view_mode": "icons", # 'icons' or 'list' + "search_hidden_files": False, # True or False + "use_trash": False, # True or False + "confirm_delete": False # True or False } @classmethod diff --git a/cfd_ui_setup.py b/cfd_ui_setup.py index 4dfbb48..a634853 100644 --- a/cfd_ui_setup.py +++ b/cfd_ui_setup.py @@ -104,7 +104,8 @@ class StyleManager: style.map("Bottom.TButton.Borderless.Round", background=[('active', self.hover_extrastyle)]) style.layout("Bottom.TButton.Borderless.Round", - style.layout("Header.TButton.Borderless.Round")) + style.layout("Header.TButton.Borderless.Round") + ) class WidgetManager: @@ -125,12 +126,12 @@ class WidgetManager: 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") - # Make path entry column expandable - top_bar.grid_columnconfigure(2, weight=1) # Left navigation buttons left_nav_container = ttk.Frame(top_bar, style='Accent.TFrame') left_nav_container.grid(row=0, column=0, sticky="w") + # Prevent this container from changing size + left_nav_container.grid_propagate(False) self.back_button = ttk.Button(left_nav_container, image=self.dialog.icon_manager.get_icon( 'back'), command=self.dialog.go_back, state=tk.DISABLED, style="Header.TButton.Borderless.Round") @@ -142,85 +143,105 @@ class WidgetManager: self.forward_button.pack(side="left", padx=5) Tooltip(self.forward_button, "Vorwärts") + self.up_button = ttk.Button(left_nav_container, image=self.dialog.icon_manager.get_icon( + 'up'), command=self.dialog.go_up_level, style="Header.TButton.Borderless.Round") + self.up_button.pack(side="left", padx=5) + Tooltip(self.up_button, "Eine Ebene höher") + self.home_button = ttk.Button(left_nav_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=(5, 10)) Tooltip(self.home_button, "Home") - # Search button (left position) - search_icon_pos = self.settings.get("search_icon_pos", "left") - if search_icon_pos == 'left': - search_container_left = ttk.Frame(top_bar, style='Accent.TFrame') - search_container_left.grid(row=0, column=1, sticky="w") - self.search_button = ttk.Button(search_container_left, 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", padx=5) - Tooltip(self.search_button, "Suchen") + # Path and search widgets container + path_search_container = ttk.Frame(top_bar, style='Accent.TFrame') + path_search_container.grid(row=0, column=1, sticky="ew") - self.recursive_search = tk.BooleanVar(value=True) - self.recursive_button = ttk.Button(search_container_left, 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) - self.recursive_button.pack_forget() # Initially hidden - Tooltip(self.recursive_button, "Rekursive Suche ein/ausschalten") + # Right-side controls container + right_controls_container = ttk.Frame(top_bar, style='Accent.TFrame') + right_controls_container.grid(row=0, column=2, sticky="e") + + # Make the middle column (path_search_container) expand + top_bar.grid_columnconfigure(1, weight=1) + + search_icon_pos = self.settings.get("search_icon_pos", "left") + self.recursive_search = tk.BooleanVar(value=True) # Path entry - self.path_entry = ttk.Entry(top_bar) - self.path_entry.grid(row=0, column=2, sticky="ew") + self.path_entry = ttk.Entry(path_search_container) self.path_entry.bind( "", lambda e: self.dialog.navigate_to(self.path_entry.get())) - # Right-side controls - right_controls_container = ttk.Frame(top_bar, style='Accent.TFrame') - right_controls_container.grid(row=0, column=3, sticky="e") - - # Search button (right position) - if search_icon_pos == 'right': - search_container_right = ttk.Frame( - right_controls_container, style='Accent.TFrame') - search_container_right.pack(side="left", padx=5) - self.search_button = ttk.Button(search_container_right, image=self.dialog.icon_manager.get_icon( + # Function to create search widgets + def create_search_widgets(parent_frame): + container = ttk.Frame(parent_frame, style='Accent.TFrame') + self.search_button = ttk.Button(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") - self.recursive_search = tk.BooleanVar(value=True) - self.recursive_button = ttk.Button(search_container_right, image=self.dialog.icon_manager.get_icon( + self.recursive_button = ttk.Button(container, 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) self.recursive_button.pack_forget() # Initially hidden Tooltip(self.recursive_button, "Rekursive Suche ein/ausschalten") + return container - # Other right-side buttons - self.new_folder_button = ttk.Button(right_controls_container, image=self.dialog.icon_manager.get_icon( + # Place search and path entry based on settings + if search_icon_pos == 'left': + path_search_container.grid_columnconfigure(1, weight=1) + search_container = create_search_widgets(path_search_container) + search_container.grid(row=0, column=0, sticky="w", padx=(0, 5)) + self.path_entry.grid(row=0, column=1, sticky="ew") + else: # right + path_search_container.grid_columnconfigure(0, weight=1) + search_container = create_search_widgets(path_search_container) + search_container.grid(row=0, column=1, sticky="e", padx=(5, 0)) + self.path_entry.grid(row=0, column=0, sticky="ew") + + # --- Responsive Buttons --- + self.responsive_buttons_container = ttk.Frame( + right_controls_container, style='Accent.TFrame') + self.responsive_buttons_container.pack(side="left") + + self.new_folder_button = ttk.Button(self.responsive_buttons_container, 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_controls_container, image=self.dialog.icon_manager.get_icon( + self.new_file_button = ttk.Button(self.responsive_buttons_container, 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=5) Tooltip(self.new_file_button, "Neues Dokument erstellen") - view_switch = ttk.Frame(right_controls_container, - padding=(5, 0), style='Accent.TFrame') - view_switch.pack(side="left") + if self.dialog.dialog_mode == "open": + self.new_folder_button.config(state=tk.DISABLED) + self.new_file_button.config(state=tk.DISABLED) - self.icon_view_button = ttk.Button(view_switch, image=self.dialog.icon_manager.get_icon( + self.view_switch = ttk.Frame(self.responsive_buttons_container, + padding=(5, 0), style='Accent.TFrame') + self.view_switch.pack(side="left") + + self.icon_view_button = ttk.Button(self.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=5) Tooltip(self.icon_view_button, "Kachelansicht") - self.list_view_button = ttk.Button(view_switch, image=self.dialog.icon_manager.get_icon( + self.list_view_button = ttk.Button(self.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_controls_container, image=self.dialog.icon_manager.get_icon( + self.hidden_files_button = ttk.Button(self.responsive_buttons_container, 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") + # "More" button for responsive UI + self.more_button = ttk.Button(right_controls_container, text="...", + command=self.dialog.show_more_menu, style="Header.TButton.Borderless.Round", width=3) + # self.more_button is managed by _handle_responsive_buttons + # Horizontal separator separator_color = "#000000" if self.style_manager.is_dark else "#9c9c9c" tk.Frame(main_frame, height=1, bg=separator_color).grid( @@ -374,71 +395,67 @@ class WidgetManager: content_frame.grid_columnconfigure(0, weight=1) self.file_list_frame = ttk.Frame( - content_frame, style="AccentBottom.TFrame") + # Use Content.TFrame for consistent bg color + content_frame, style="Content.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( + # This frame will contain the action buttons and status bar + self.action_status_frame = ttk.Frame( content_frame, style="AccentBottom.TFrame") - bottom_controls_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0)) + self.action_status_frame.grid( + row=1, column=0, sticky="ew", pady=(5, 10), padx=10) button_box_pos = self.settings.get("button_box_pos", "left") - action_buttons_col = 0 if button_box_pos == 'left' else 2 - action_buttons_sticky = "w" if button_box_pos == 'left' else "e" - if self.dialog.dialog_mode == "save": - # Give most of the weight to the action buttons frame (which contains the entry) - bottom_controls_frame.grid_columnconfigure(action_buttons_col, weight=10) - bottom_controls_frame.grid_columnconfigure(1, weight=1) # status_bar is in col 1 + # Configure columns for the action_status_frame + if button_box_pos == 'left': + self.action_status_frame.grid_columnconfigure(1, weight=1) else: - # Original behavior for open mode - bottom_controls_frame.grid_columnconfigure(1, weight=1) - - action_buttons_frame = ttk.Frame( - bottom_controls_frame, style="AccentBottom.TFrame") - action_buttons_frame.grid( - row=0, column=action_buttons_col, rowspan=2, sticky="nsew", pady=(5, 10)) + self.action_status_frame.grid_columnconfigure(1, weight=1) + # Status bar will be placed inside the action_status_frame self.status_bar = ttk.Label( - bottom_controls_frame, text="", anchor="w", style="AccentBottom.TLabel") - status_bar_col = 1 if button_box_pos == 'left' else 1 - status_bar_sticky = "w" if button_box_pos == 'left' else "e" - self.status_bar.grid(row=0, column=status_bar_col, - sticky=status_bar_sticky, padx=10) + self.action_status_frame, text="", anchor="w", style="AccentBottom.TLabel") - self.settings_button = ttk.Button(bottom_controls_frame, image=self.dialog.icon_manager.get_icon( + self.settings_button = ttk.Button(self.action_status_frame, image=self.dialog.icon_manager.get_icon( 'settings-2_small'), command=self.dialog.open_settings_dialog, style="Bottom.TButton.Borderless.Round") - self.settings_button.grid( - row=0, column=3, sticky="ne", padx=(0, 5), pady=(2, 0)) - Tooltip(self.settings_button, "Einstellungen") + + self.trash_button = ttk.Button(self.action_status_frame, image=self.dialog.icon_manager.get_icon( + 'trash_small2'), command=self.dialog.delete_selected_item, style="Bottom.TButton.Borderless.Round") + Tooltip(self.trash_button, "Ausgewähltes Element löschen/verschieben") if self.dialog.dialog_mode == "save": - self.filename_entry = ttk.Entry(action_buttons_frame) + self.filename_entry = ttk.Entry(self.action_status_frame) save_button = ttk.Button( - action_buttons_frame, text="Speichern", command=self.dialog.on_save) + self.action_status_frame, text="Speichern", command=self.dialog.on_save) cancel_button = ttk.Button( - action_buttons_frame, text="Abbrechen", command=self.dialog.on_cancel) - self.filter_combobox = ttk.Combobox(action_buttons_frame, values=[ + self.action_status_frame, text="Abbrechen", command=self.dialog.on_cancel) + self.filter_combobox = ttk.Combobox(self.action_status_frame, values=[ ft[0] for ft in self.dialog.filetypes], state="readonly") if button_box_pos == 'left': - action_buttons_frame.grid_columnconfigure(1, weight=1) + save_button.grid(row=0, column=0, sticky="w", padx=(0, 10)) self.filename_entry.grid( - row=0, column=1, sticky="ew", padx=(10, 0)) - save_button.grid(row=0, column=0, sticky="e", padx=(10, 5)) + row=0, column=1, sticky="ew", padx=(0, 5)) cancel_button.grid(row=1, column=0, sticky="w", - padx=(10, 0), pady=(10, 0)) + pady=(5, 0), padx=(0, 10)) self.filter_combobox.grid( - row=1, column=1, sticky="w", padx=(10, 0), pady=(10, 0)) + row=1, column=1, sticky="w", pady=(5, 0), padx=(0, 5)) + self.settings_button.grid(row=0, column=3, sticky="e") + self.trash_button.grid( + row=1, column=3, sticky="se", padx=(5, 0)) else: # right - action_buttons_frame.grid_columnconfigure(0, weight=1) - save_button.grid(row=0, column=1, sticky="w", padx=(5, 5)) + self.trash_button.grid( + row=1, column=0, sticky="sw", padx=(0, 5)) self.filename_entry.grid( - row=0, column=0, sticky="ew", padx=(0, 10)) + row=0, column=1, sticky="ew", padx=(0, 5)) self.filter_combobox.grid( - row=1, column=0, sticky="e", padx=(0, 10), pady=(10, 0)) - cancel_button.grid(row=1, column=1, sticky="e", - padx=(0, 5), pady=(10, 0)) + row=1, column=1, sticky="e", pady=(5, 0), padx=(0, 5)) + save_button.grid(row=0, column=2, sticky="e", padx=5) + cancel_button.grid(row=1, column=2, sticky="e", + padx=5, pady=(5, 0)) + self.settings_button.grid(row=0, column=3, sticky="e") self.filter_combobox.bind( "<>", self.dialog.on_filter_change) @@ -446,24 +463,28 @@ class WidgetManager: else: # Open mode open_button = ttk.Button( - action_buttons_frame, text="Öffnen", command=self.dialog.on_open) + self.action_status_frame, text="Öffnen", command=self.dialog.on_open) cancel_button = ttk.Button( - action_buttons_frame, text="Abbrechen", command=self.dialog.on_cancel) - self.filter_combobox = ttk.Combobox(action_buttons_frame, values=[ + self.action_status_frame, text="Abbrechen", command=self.dialog.on_cancel) + self.filter_combobox = ttk.Combobox(self.action_status_frame, values=[ ft[0] for ft in self.dialog.filetypes], state="readonly") if button_box_pos == 'left': - open_button.grid(row=0, column=0, sticky="e", padx=(10, 5)) - cancel_button.grid(row=1, column=0, sticky="w", - padx=(10, 0), pady=(10, 0)) + open_button.grid(row=0, column=0, sticky="w", padx=(0, 5)) + self.status_bar.grid(row=0, column=1, sticky="w", padx=5) + cancel_button.grid(row=1, column=0, sticky="w", pady=(5, 0)) self.filter_combobox.grid( - row=1, column=1, sticky="w", padx=(10, 0), pady=(10, 0)) + row=1, column=1, sticky="w", pady=(5, 0), padx=(5, 0)) + self.settings_button.grid(row=0, column=2, sticky="e") + else: # right - open_button.grid(row=0, column=1, sticky="w", padx=(5, 5)) + self.status_bar.grid(row=0, column=0, sticky="e", padx=10) + open_button.grid(row=0, column=1, sticky="e", padx=5) cancel_button.grid(row=1, column=1, sticky="e", - padx=(0, 5), pady=(10, 0)) + padx=5, pady=(5, 0)) self.filter_combobox.grid( - row=1, column=0, sticky="e", padx=(0, 5), pady=(10, 0)) + row=1, column=0, sticky="e", pady=(5, 0)) + self.settings_button.grid(row=0, column=2, sticky="e") self.filter_combobox.bind( "<>", self.dialog.on_filter_change) diff --git a/custom_file_dialog.py b/custom_file_dialog.py index eac76e4..f3e669a 100644 --- a/custom_file_dialog.py +++ b/custom_file_dialog.py @@ -10,15 +10,22 @@ from shared_libs.common_tools import IconManager, Tooltip, ConfigManager, LxTool from cfd_app_config import AppConfig, CfdConfigManager from cfd_ui_setup import StyleManager, WidgetManager, get_xdg_user_dir +try: + import send2trash + SEND2TRASH_AVAILABLE = True +except ImportError: + SEND2TRASH_AVAILABLE = False + class SettingsDialog(tk.Toplevel): - def __init__(self, parent): + def __init__(self, parent, dialog_mode="save"): super().__init__(parent) self.transient(parent) self.grab_set() self.title("Einstellungen") self.settings = CfdConfigManager.load() + self.dialog_mode = dialog_mode # Variables self.search_icon_pos = tk.StringVar( @@ -29,6 +36,12 @@ class SettingsDialog(tk.Toplevel): value=self.settings.get("window_size_preset", "1050x850")) self.default_view_mode = tk.StringVar( value=self.settings.get("default_view_mode", "icons")) + self.search_hidden_files = tk.BooleanVar( + value=self.settings.get("search_hidden_files", False)) + self.use_trash = tk.BooleanVar( + value=self.settings.get("use_trash", False)) + self.confirm_delete = tk.BooleanVar( + value=self.settings.get("confirm_delete", False)) # --- UI Elements --- main_frame = ttk.Frame(self, padding=10) @@ -70,6 +83,39 @@ class SettingsDialog(tk.Toplevel): ttk.Radiobutton(view_mode_frame, text="Liste", variable=self.default_view_mode, value="list").pack(side="left", padx=5) + # Search Hidden Files + search_hidden_frame = ttk.LabelFrame( + main_frame, text="Sucheinstellungen", padding=10) + search_hidden_frame.pack(fill="x", pady=5) + ttk.Checkbutton(search_hidden_frame, text="Versteckte Dateien und Ordner durchsuchen", + variable=self.search_hidden_files).pack(anchor="w") + + # Deletion Settings + delete_frame = ttk.LabelFrame( + main_frame, text="Löscheinstellungen", padding=10) + delete_frame.pack(fill="x", pady=5) + + self.use_trash_checkbutton = ttk.Checkbutton(delete_frame, text="Dateien in den Papierkorb verschieben (empfohlen)", + variable=self.use_trash) + self.use_trash_checkbutton.pack(anchor="w") + + if not SEND2TRASH_AVAILABLE: + self.use_trash_checkbutton.config(state=tk.DISABLED) + ttk.Label(delete_frame, text="(send2trash-Bibliothek nicht gefunden)", + font=("TkDefaultFont", 9, "italic")).pack(anchor="w", padx=(20, 0)) + + self.confirm_delete_checkbutton = ttk.Checkbutton(delete_frame, text="Löschen/Verschieben ohne Bestätigung", + variable=self.confirm_delete) + self.confirm_delete_checkbutton.pack(anchor="w") + + # Disable deletion options in "open" mode + if self.dialog_mode == "open": + self.use_trash_checkbutton.config(state=tk.DISABLED) + self.confirm_delete_checkbutton.config(state=tk.DISABLED) + info_label = ttk.Label(delete_frame, text="(Löschoptionen sind nur im Speichern-Modus verfügbar)", + font=("TkDefaultFont", 9, "italic")) + info_label.pack(anchor="w", padx=(20, 0)) + # --- Action Buttons --- button_frame = ttk.Frame(main_frame) button_frame.pack(fill="x", pady=(10, 0)) @@ -86,7 +132,10 @@ class SettingsDialog(tk.Toplevel): "search_icon_pos": self.search_icon_pos.get(), "button_box_pos": self.button_box_pos.get(), "window_size_preset": self.window_size_preset.get(), - "default_view_mode": self.default_view_mode.get() + "default_view_mode": self.default_view_mode.get(), + "search_hidden_files": self.search_hidden_files.get(), + "use_trash": self.use_trash.get(), + "confirm_delete": self.confirm_delete.get() } CfdConfigManager.save(new_settings) self.master.reload_config_and_rebuild_ui() @@ -98,6 +147,9 @@ class SettingsDialog(tk.Toplevel): self.button_box_pos.set(defaults["button_box_pos"]) self.window_size_preset.set(defaults["window_size_preset"]) self.default_view_mode.set(defaults["default_view_mode"]) + self.search_hidden_files.set(defaults["search_hidden_files"]) + self.use_trash.set(defaults["use_trash"]) + self.confirm_delete.set(defaults["confirm_delete"]) class CustomFileDialog(tk.Toplevel): @@ -141,13 +193,54 @@ class CustomFileDialog(tk.Toplevel): self.original_path_text = "" # Store original path text self.items_to_load_per_batch = 250 self.item_path_map = {} + self.responsive_buttons_hidden = None # State for responsive buttons self.icon_manager = IconManager() self.style_manager = StyleManager(self) self.widget_manager = WidgetManager(self, self.settings) self._update_view_mode_buttons() - self.navigate_to(self.current_dir) + + # Defer initial navigation until the window geometry is calculated + # to ensure the icon view gets the correct initial width. + def initial_load(): + # Force layout update to get correct widths + self.update_idletasks() + self.last_width = self.widget_manager.file_list_frame.winfo_width() + self._handle_responsive_buttons(self.winfo_width()) + self.navigate_to(self.current_dir) + + # Using after(10) gives the window manager a moment to process + # the initial window drawing and sizing. + self.after(10, initial_load) + + # Bind the intelligent return handler + self.widget_manager.path_entry.bind("", self.handle_path_entry_return) + + # Bind the delete key only in "save" mode + if self.dialog_mode == "save": + self.bind("", self.delete_selected_item) + + def handle_path_entry_return(self, event): + """Intelligently handles the Enter key in the path entry. + + If the text is a valid directory, it navigates there. + Otherwise, if in search mode, it executes a search. + """ + path_text = self.widget_manager.path_entry.get().strip() + + # Try to interpret as a path first + # Expand user-home and resolve relative paths + potential_path = os.path.realpath(os.path.expanduser(path_text)) + + if os.path.isdir(potential_path): + # If search was active, turn it off before navigating + if self.search_mode: + self.toggle_search_mode() + self.navigate_to(potential_path) + elif self.search_mode: + # If not a valid path and in search mode, execute search + self.execute_search(event) def load_settings(self): self.settings = CfdConfigManager.load() @@ -179,10 +272,20 @@ class CustomFileDialog(tk.Toplevel): self.style_manager = StyleManager(self) self.widget_manager = WidgetManager(self, self.settings) self._update_view_mode_buttons() + + # Reset responsive button state and re-evaluate + self.responsive_buttons_hidden = None + self.update_idletasks() + self._handle_responsive_buttons(self.winfo_width()) + + # If search was active, reset it to avoid inconsistent state + if self.search_mode: + self.toggle_search_mode() # This will correctly reset the UI + self.navigate_to(self.current_dir) def open_settings_dialog(self): - SettingsDialog(self) + SettingsDialog(self, dialog_mode=self.dialog_mode) def get_file_icon(self, filename, size='large'): ext = os.path.splitext(filename)[1].lower() @@ -196,7 +299,8 @@ class CustomFileDialog(tk.Toplevel): if ext in ['.mp3', '.wav', '.ogg', '.flac']: return self.icon_manager.get_icon(f'audio_{size}') if ext in ['.mp4', '.mkv', '.avi', '.mov']: - return self.icon_manager.get_icon(f'video_{size}') if size == 'large' else self.icon_manager.get_icon('video_small_file') + return self.icon_manager.get_icon(f'video_{size}') if size == 'large' else self.icon_manager.get_icon( + 'video_small_file') if ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg']: return self.icon_manager.get_icon(f'picture_{size}') if ext == '.iso': @@ -218,12 +322,73 @@ class CustomFileDialog(tk.Toplevel): self.populate_files() def on_window_resize(self, event): - 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) - self.resize_job = self.after(200, self.populate_files) - self.last_width = new_width + # This check is to prevent the resize event from firing for child widgets + if event.widget is self: + # Handle icon view redraw on width change, but not in search mode + if self.view_mode.get() == "icons" and not self.search_mode: + new_width = self.widget_manager.file_list_frame.winfo_width() + if abs(new_width - self.last_width) > 50: + if self.resize_job: + self.after_cancel(self.resize_job) + + def repopulate_icons(): + # Ensure all pending geometry changes are processed before redrawing + self.update_idletasks() + self.populate_files() + + self.resize_job = self.after(150, repopulate_icons) + self.last_width = new_width + + # Handle responsive buttons in the top bar + self._handle_responsive_buttons(event.width) + + def _handle_responsive_buttons(self, window_width): + # This threshold might need adjustment based on your layout and button sizes + threshold = 850 + container = self.widget_manager.responsive_buttons_container + more_button = self.widget_manager.more_button + + should_be_hidden = window_width < threshold + + # Only change the layout if the state is different from the current one + if should_be_hidden != self.responsive_buttons_hidden: + if should_be_hidden: + # Hide individual buttons and show the 'more' button + container.pack_forget() + more_button.pack(side="left", padx=5) + else: + # Show individual buttons and hide the 'more' button + more_button.pack_forget() + container.pack(side="left") + self.responsive_buttons_hidden = should_be_hidden + + def show_more_menu(self): + # Create and display the dropdown menu for hidden buttons + more_menu = tk.Menu(self, tearoff=0, background=self.style_manager.header, foreground=self.style_manager.color_foreground, + activebackground=self.style_manager.selection_color, activeforeground=self.style_manager.color_foreground, relief='flat', borderwidth=0) + + more_menu.add_command(label="Neuer Ordner", command=self.create_new_folder, + image=self.icon_manager.get_icon('new_folder_small'), compound='left') + more_menu.add_command(label="Neues Dokument", command=self.create_new_file, + image=self.icon_manager.get_icon('new_document_small'), compound='left') + more_menu.add_separator() + more_menu.add_command(label="Kachelansicht", command=self.set_icon_view, + image=self.icon_manager.get_icon('icon_view'), compound='left') + more_menu.add_command(label="Listenansicht", command=self.set_list_view, + image=self.icon_manager.get_icon('list_view'), compound='left') + more_menu.add_separator() + + # Toggle hidden files option + hidden_files_label = "Versteckte Dateien ausblenden" if self.show_hidden_files.get() else "Versteckte Dateien anzeigen" + hidden_files_icon = self.icon_manager.get_icon('unhide') if self.show_hidden_files.get() else self.icon_manager.get_icon('hide') + more_menu.add_command(label=hidden_files_label, command=self.toggle_hidden_files, + image=hidden_files_icon, compound='left') + + # Position and show the menu + more_button = self.widget_manager.more_button + x = more_button.winfo_rootx() + y = more_button.winfo_rooty() + more_button.winfo_height() + more_menu.tk_popup(x, y) def on_sidebar_resize(self, event): current_width = event.width @@ -271,11 +436,10 @@ class CustomFileDialog(tk.Toplevel): 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.select_range(0, tk.END) - # Set focus reliably - self.after(50, lambda: self.widget_manager.path_entry.focus_set()) - self.widget_manager.path_entry.bind( - "", self.execute_search) + # Use after() to ensure the focus is set after the UI has updated + self.after(10, lambda: self.widget_manager.path_entry.focus_set()) + self.after(20, lambda: self.widget_manager.path_entry.select_range(0, tk.END)) + self.widget_manager.path_entry.bind( "", self.clear_search_placeholder) @@ -286,8 +450,6 @@ class CustomFileDialog(tk.Toplevel): self.search_mode = False 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 @@ -381,11 +543,12 @@ class CustomFileDialog(tk.Toplevel): # Build find command based on recursive setting (use . for current directory) if self.widget_manager.recursive_search.get(): - find_cmd = ['find', '.', '-iname', - f'*{search_term}*', '-type', 'f'] + # Find both files and directories + find_cmd = ['find', '.', '-iname', f'*{search_term}*'] else: + # Find both files and directories, but only in the current level find_cmd = ['find', '.', '-maxdepth', '1', - '-iname', f'*{search_term}*', '-type', 'f'] + '-iname', f'*{search_term}*'] result = subprocess.run( find_cmd, capture_output=True, text=True, timeout=30) @@ -398,7 +561,8 @@ class CustomFileDialog(tk.Toplevel): if f and f.startswith('./'): abs_path = os.path.join( search_dir, f[2:]) # Remove './' prefix - if os.path.isfile(abs_path): + # Check if the path exists, as it might be a broken symlink or deleted + if os.path.exists(abs_path): directory_files.append(abs_path) all_files.extend(directory_files) @@ -413,12 +577,21 @@ class CustomFileDialog(tk.Toplevel): seen.add(file_path) unique_files.append(file_path) - # Filter based on currently selected filter pattern + # Filter based on currently selected filter pattern and hidden file setting self.search_results = [] + search_hidden = self.settings.get("search_hidden_files", False) + for file_path in unique_files: - filename = os.path.basename(file_path) - if self._matches_filetype(filename): - self.search_results.append(file_path) + # Check if path contains a hidden component (e.g., /.config/ or /some/path/to/.hidden_file) + if not search_hidden: + if any(part.startswith('.') for part in file_path.split(os.sep)): + continue # Skip hidden files/files in hidden directories + + # Check if the path exists (it might have been deleted during the search) + if os.path.exists(file_path): + filename = os.path.basename(file_path) + if self._matches_filetype(filename) or os.path.isdir(file_path): + self.search_results.append(file_path) # Show search results in TreeView if self.search_results: @@ -494,7 +667,11 @@ class CustomFileDialog(tk.Toplevel): modified_time = datetime.fromtimestamp( stat.st_mtime).strftime('%d.%m.%Y %H:%M') - icon = self.get_file_icon(filename, 'small') + if os.path.isdir(file_path): + icon = self.icon_manager.get_icon('folder_small') + else: + icon = self.get_file_icon(filename, 'small') + search_tree.insert("", "end", text=f" {filename}", image=icon, values=(directory, size, modified_time)) except (FileNotFoundError, PermissionError): @@ -675,16 +852,17 @@ class CustomFileDialog(tk.Toplevel): self.all_items, error, warning = self._get_sorted_items() self.currently_loaded_count = 0 - canvas = tk.Canvas(self.widget_manager.file_list_frame, - highlightthickness=0, bg=self.style_manager.icon_bg_color) + self.icon_canvas = tk.Canvas(self.widget_manager.file_list_frame, + highlightthickness=0, bg=self.style_manager.icon_bg_color) v_scrollbar = ttk.Scrollbar( - self.widget_manager.file_list_frame, orient="vertical", command=canvas.yview) - canvas.pack(side="left", fill="both", expand=True) + self.widget_manager.file_list_frame, orient="vertical", command=self.icon_canvas.yview) + self.icon_canvas.pack(side="left", fill="both", expand=True) v_scrollbar.pack(side="right", fill="y") - container_frame = ttk.Frame(canvas, style="Content.TFrame") - canvas.create_window((0, 0), window=container_frame, anchor="nw") - container_frame.bind("", lambda e: canvas.configure( - scrollregion=canvas.bbox("all"))) + container_frame = ttk.Frame(self.icon_canvas, style="Content.TFrame") + self.icon_canvas.create_window( + (0, 0), window=container_frame, anchor="nw") + container_frame.bind("", lambda e: self.icon_canvas.configure( + scrollregion=self.icon_canvas.bbox("all"))) def _on_mouse_wheel(event): if event.num == 4: @@ -693,12 +871,12 @@ class CustomFileDialog(tk.Toplevel): delta = 1 else: delta = -1 * int(event.delta / 120) - canvas.yview_scroll(delta, "units") + self.icon_canvas.yview_scroll(delta, "units") # Check if scrolled to the bottom and if there are more items to load - if self.currently_loaded_count < len(self.all_items) and canvas.yview()[1] > 0.9: + if self.currently_loaded_count < len(self.all_items) and self.icon_canvas.yview()[1] > 0.9: self._load_more_items_icon_view(container_frame) - for widget in [canvas, container_frame]: + for widget in [self.icon_canvas, container_frame]: widget.bind("", _on_mouse_wheel) widget.bind("", _on_mouse_wheel) widget.bind("", _on_mouse_wheel) @@ -709,23 +887,42 @@ class CustomFileDialog(tk.Toplevel): ttk.Label(container_frame, text=error).pack(pady=20) return - self._load_more_items_icon_view( + widget_to_focus = self._load_more_items_icon_view( container_frame, item_to_rename, item_to_select) + if widget_to_focus: + def scroll_to_widget(): + self.update_idletasks() + if not widget_to_focus.winfo_exists(): + return + y = widget_to_focus.winfo_y() + canvas_height = self.icon_canvas.winfo_height() + scroll_region = self.icon_canvas.bbox("all") + if not scroll_region: + return + scroll_height = scroll_region[3] + if scroll_height > canvas_height: + fraction = y / scroll_height + self.icon_canvas.yview_moveto(fraction) + + self.after(100, scroll_to_widget) + def _load_more_items_icon_view(self, container, item_to_rename=None, item_to_select=None): start_index = self.currently_loaded_count end_index = min(len(self.all_items), start_index + self.items_to_load_per_batch) if start_index >= end_index: - return # All items loaded + return None # All items loaded item_width, item_height = 125, 100 frame_width = self.widget_manager.file_list_frame.winfo_width() col_count = max(1, frame_width // item_width - 1) - row = start_index // col_count - col = start_index % col_count + row = start_index // col_count if col_count > 0 else 0 + col = start_index % col_count if col_count > 0 else 0 + + widget_to_focus = None for i in range(start_index, end_index): name = self.all_items[i] @@ -743,6 +940,7 @@ class CustomFileDialog(tk.Toplevel): if name == item_to_rename: self.start_rename(item_frame, path) + widget_to_focus = item_frame else: icon = self.icon_manager.get_icon( 'folder_large') if is_dir else self.get_file_icon(name, 'large') @@ -753,12 +951,7 @@ class CustomFileDialog(tk.Toplevel): name, 14), anchor="center", style="Item.TLabel") name_label.pack(fill="x", expand=True) - tooltip_text = name - if is_dir and len(self.all_items) < 500: - content_count = self._get_folder_content_count(path) - if content_count is not None: - tooltip_text += f"\n({content_count} Einträge)" - Tooltip(item_frame, tooltip_text) + Tooltip(item_frame, name) for widget in [item_frame, icon_label, name_label]: widget.bind("", lambda e, @@ -772,12 +965,17 @@ class CustomFileDialog(tk.Toplevel): if name == item_to_select: self.on_item_select(path, item_frame) + widget_to_focus = item_frame - col = (col + 1) % col_count - if col == 0: + if col_count > 0: + col = (col + 1) % col_count + if col == 0: + row += 1 + else: row += 1 self.currently_loaded_count = end_index + return widget_to_focus def populate_list_view(self, item_to_rename=None, item_to_select=None): self.all_items, error, warning = self._get_sorted_items() @@ -815,7 +1013,8 @@ class CustomFileDialog(tk.Toplevel): def _on_scroll(*args): # Check if scrolled to the bottom and if there are more items to load if self.currently_loaded_count < len(self.all_items) and self.tree.yview()[1] > 0.9: - self._load_more_items_list_view(item_to_rename, item_to_select) + # On-scroll loading should not trigger rename or select. + self._load_more_items_list_view() v_scrollbar.set(*args) self.tree.configure(yscrollcommand=_on_scroll) @@ -886,7 +1085,7 @@ class CustomFileDialog(tk.Toplevel): child.state(['selected']) self.selected_item_frame = item_frame self.selected_file = path - self.update_status_bar() + self.update_status_bar(path) # Pass selected path self.bind("", lambda e, p=path, f=item_frame: self.on_rename_request(e, p, f)) if self.dialog_mode == "save" and not os.path.isdir(path): @@ -899,8 +1098,9 @@ class CustomFileDialog(tk.Toplevel): return item_id = self.tree.selection()[0] item_text = self.tree.item(item_id, 'text').strip() - self.selected_file = os.path.join(self.current_dir, item_text) - self.update_status_bar() + path = os.path.join(self.current_dir, item_text) + self.selected_file = path + self.update_status_bar(path) # Pass selected path if self.dialog_mode == "save" and not os.path.isdir(self.selected_file): self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.filename_entry.insert(0, item_text) @@ -1006,13 +1206,19 @@ class CustomFileDialog(tk.Toplevel): self.update_status_bar() self.update_action_buttons_state() + def go_up_level(self): + """Navigates one directory level up.""" + new_path = os.path.dirname(self.current_dir) + if new_path != self.current_dir: # Avoid getting stuck at the root + self.navigate_to(new_path) + def update_nav_buttons(self): self.widget_manager.back_button.config( state=tk.NORMAL if self.history_pos > 0 else tk.DISABLED) 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): + def update_status_bar(self, selected_path=None): try: total, used, free = shutil.disk_usage(self.current_dir) free_str = self._format_size(free) @@ -1021,10 +1227,19 @@ class CustomFileDialog(tk.Toplevel): self.widget_manager.storage_bar['value'] = (used / total) * 100 status_text = "" - 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}" + if selected_path and os.path.exists(selected_path): + if os.path.isdir(selected_path): + # Display item count for directories + content_count = self._get_folder_content_count(selected_path) + if content_count is not None: + status_text = f"'{os.path.basename(selected_path)}' ({content_count} Einträge)" + else: + status_text = f"'{os.path.basename(selected_path)}'" + else: + # Display size for files + size = os.path.getsize(selected_path) + size_str = self._format_size(size) + status_text = f"'{os.path.basename(selected_path)}' Größe: {size_str}" self.widget_manager.status_bar.config(text=status_text) except FileNotFoundError: self.widget_manager.status_bar.config( @@ -1050,6 +1265,48 @@ class CustomFileDialog(tk.Toplevel): def get_selected_file(self): return self.selected_file + def delete_selected_item(self, event=None): + """Deletes or moves the selected item to trash based on settings.""" + if not self.selected_file or not os.path.exists(self.selected_file): + return + + use_trash = self.settings.get("use_trash", False) and SEND2TRASH_AVAILABLE + confirm = self.settings.get("confirm_delete", False) + + action_text = "in den Papierkorb verschieben" if use_trash else "endgültig löschen" + item_name = os.path.basename(self.selected_file) + + if not confirm: + dialog = MessageDialog( + master=self, + title="Bestätigung erforderlich", + text=f"Möchten Sie '{item_name}' wirklich {action_text}?", + message_type="question" + ) + if not dialog.show(): + return + + try: + if use_trash: + send2trash.send2trash(self.selected_file) + else: + if os.path.isdir(self.selected_file): + shutil.rmtree(self.selected_file) + else: + os.remove(self.selected_file) + + self.populate_files() + self.widget_manager.status_bar.config( + text=f"'{item_name}' wurde erfolgreich entfernt.") + + except Exception as e: + MessageDialog( + master=self, + title="Fehler", + text=f"Fehler beim Entfernen von '{item_name}':\n{e}", + message_type="error" + ).show() + def create_new_folder(self): self._create_new_item(is_folder=True) @@ -1164,7 +1421,19 @@ class CustomFileDialog(tk.Toplevel): entry.bind("", cancel_rename) def _start_rename_list_view(self, item_id): - x, y, width, height = self.tree.bbox(item_id, column="#0") + # First, ensure the item is visible by scrolling to it. + self.tree.see(item_id) + # Force the UI to process the scrolling and other pending events. + self.tree.update_idletasks() + + # Now, get the bounding box. It should be available since the item is visible. + bbox = self.tree.bbox(item_id, column="#0") + + # If bbox is still empty (e.g., view is not focused), abort to prevent crash. + if not bbox: + return + + x, y, width, height = bbox entry = ttk.Entry(self.tree) # Set a fixed width for the entry widget to prevent it from expanding too much entry_width = self.tree.column("#0", "width") diff --git a/mainwindow.py b/mainwindow.py index 65766ee..c7d103a 100755 --- a/mainwindow.py +++ b/mainwindow.py @@ -33,7 +33,7 @@ class GlotzMol(tk.Tk): dialog = CustomFileDialog(self, initial_dir=os.path.expanduser("~"), filetypes=[("All Files", "*.*") - ], dialog_mode="save") + ]) # This is the crucial part: wait for the dialog to be closed self.wait_window(dialog) @@ -55,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()