commit 55

This commit is contained in:
2025-08-05 10:14:09 +02:00
parent 3005d17f03
commit f2b6c330fa
7 changed files with 449 additions and 182 deletions

View File

@@ -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

View File

@@ -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")
# 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)
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")
# 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(
"<Return>", 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("<Configure>", 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(
"<<ComboboxSelected>>", 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(
"<<ComboboxSelected>>", self.dialog.on_filter_change)

View File

@@ -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,14 +193,55 @@ 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()
# 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("<Return>", self.handle_path_entry_return)
# Bind the delete key only in "save" mode
if self.dialog_mode == "save":
self.bind("<Delete>", 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()
size_preset = self.settings.get("window_size_preset", "1050x850")
@@ -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,13 +322,74 @@ class CustomFileDialog(tk.Toplevel):
self.populate_files()
def on_window_resize(self, event):
# 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 self.view_mode.get() == "icons" and abs(new_width - self.last_width) > 50:
if 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)
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
# Define a threshold for when to hide/show text
@@ -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(
"<Return>", 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(
"<FocusIn>", 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(
"<Return>", lambda e: self.navigate_to(self.widget_manager.path_entry.get()))
self.widget_manager.path_entry.unbind("<FocusIn>")
# 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,11 +577,20 @@ 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:
# 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):
if self._matches_filetype(filename) or os.path.isdir(file_path):
self.search_results.append(file_path)
# Show search results in TreeView
@@ -494,7 +667,11 @@ class CustomFileDialog(tk.Toplevel):
modified_time = datetime.fromtimestamp(
stat.st_mtime).strftime('%d.%m.%Y %H:%M')
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,
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("<Configure>", 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("<Configure>", 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("<MouseWheel>", _on_mouse_wheel)
widget.bind("<Button-4>", _on_mouse_wheel)
widget.bind("<Button-5>", _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("<Double-Button-1>", 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
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("<F2>", 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)
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(self.selected_file)}' Größe: {size_str}"
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("<Escape>", 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")

View File

@@ -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()