Refactor: Decompose CustomFileDialog class

The `CustomFileDialog` class had become too large and complex, making it difficult to maintain.

This change refactors the monolithic class into several specialized manager classes, each responsible for a specific area of concern:

- `SettingsDialog`: Moved to its own file, `cfd_settings_dialog.py`.
- `FileOperationsManager`: Handles file/folder creation, deletion, and renaming.
- `SearchManager`: Encapsulates all search-related logic.
- `NavigationManager`: Manages directory navigation and history.
- `ViewManager`: Controls the rendering of file and folder views.

The main `CustomFileDialog` class has been streamlined and now acts as an orchestrator for these managers. This improves readability, separation of concerns, and the overall maintainability of the code.
This commit is contained in:
2025-08-09 11:51:58 +02:00
parent d392e1e608
commit b8d46fb547
7 changed files with 1285 additions and 1314 deletions

231
cfd_file_operations.py Normal file
View File

@@ -0,0 +1,231 @@
import os
import shutil
import tkinter as tk
from tkinter import ttk
try:
import send2trash
SEND2TRASH_AVAILABLE = True
except ImportError:
SEND2TRASH_AVAILABLE = False
from shared_libs.message import MessageDialog
class FileOperationsManager:
def __init__(self, dialog):
self.dialog = dialog
def delete_selected_item(self, event=None):
"""Deletes or moves the selected item to trash based on settings."""
if not self.dialog.selected_file or not os.path.exists(self.dialog.selected_file):
return
use_trash = self.dialog.settings.get(
"use_trash", False) and SEND2TRASH_AVAILABLE
confirm = self.dialog.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.dialog.selected_file)
if not confirm:
dialog = MessageDialog(
master=self.dialog,
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.dialog.selected_file)
else:
if os.path.isdir(self.dialog.selected_file):
shutil.rmtree(self.dialog.selected_file)
else:
os.remove(self.dialog.selected_file)
self.dialog.view_manager.populate_files()
self.dialog.widget_manager.search_status_label.config(
text=f"'{item_name}' wurde erfolgreich entfernt.")
except Exception as e:
MessageDialog(
master=self.dialog,
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)
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.dialog.current_dir, new_name)
try:
if is_folder:
os.mkdir(new_path)
else:
open(new_path, 'a').close()
self.dialog.view_manager.populate_files(item_to_rename=new_name)
except Exception as e:
self.dialog.widget_manager.search_status_label.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.dialog.current_dir, new_name)):
counter += 1
new_name = f"{name} {counter}{ext}"
return new_name
def _copy_to_clipboard(self, data):
self.dialog.clipboard_clear()
self.dialog.clipboard_append(data)
self.dialog.widget_manager.search_status_label.config(
text=f"'{self.dialog.shorten_text(data, 50)}' in Zwischenablage kopiert.")
def _show_context_menu(self, event, item_path):
if not item_path:
return "break"
if hasattr(self.dialog, 'context_menu') and self.dialog.context_menu.winfo_exists():
self.dialog.context_menu.destroy()
self.dialog.context_menu = tk.Menu(self.dialog, tearoff=0, background=self.dialog.style_manager.header, foreground=self.dialog.style_manager.color_foreground,
activebackground=self.dialog.style_manager.selection_color, activeforeground=self.dialog.style_manager.color_foreground, relief='flat', borderwidth=0)
self.dialog.context_menu.add_command(label="Dateiname in Zwischenablage",
command=lambda: self._copy_to_clipboard(os.path.basename(item_path)))
self.dialog.context_menu.add_command(
label="Pfad in Zwischenablage", command=lambda: self._copy_to_clipboard(item_path))
self.dialog.context_menu.add_separator()
self.dialog.context_menu.add_command(
label="Speicherort öffnen", command=lambda: self._open_file_location_from_context(item_path))
self.dialog.context_menu.tk_popup(event.x_root, event.y_root)
return "break"
def _open_file_location_from_context(self, file_path):
directory = os.path.dirname(file_path)
filename = os.path.basename(file_path)
if self.dialog.search_mode:
self.dialog.search_manager.hide_search_bar()
self.dialog.navigation_manager.navigate_to(directory)
self.dialog.after(100, lambda: self.dialog.view_manager._select_file_in_view(filename))
def on_rename_request(self, event, item_path=None, item_frame=None):
if self.dialog.view_mode.get() == "list":
if not self.dialog.tree.selection():
return
item_id = self.dialog.tree.selection()[0]
item_path = os.path.join(
self.dialog.current_dir, self.dialog.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 start_rename(self, item_widget, item_path):
if self.dialog.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="x", expand=True, padx=5, pady=5)
entry.focus_set()
def finish_rename(event):
new_name = entry.get()
new_path = os.path.join(self.dialog.current_dir, new_name)
if new_name and new_path != item_path:
if os.path.exists(new_path):
self.dialog.widget_manager.search_status_label.config(
text=f"'{new_name}' existiert bereits.")
self.dialog.view_manager.populate_files(
item_to_select=os.path.basename(item_path))
return
try:
os.rename(item_path, new_path)
self.dialog.view_manager.populate_files(item_to_select=new_name)
except Exception as e:
self.dialog.widget_manager.search_status_label.config(
text=f"Fehler beim Umbenennen: {e}")
self.dialog.view_manager.populate_files()
else:
self.dialog.populate_files(item_to_select=os.path.basename(item_path))
def cancel_rename(event):
self.dialog.view_manager.populate_files()
entry.bind("<Return>", finish_rename)
entry.bind("<FocusOut>", finish_rename)
entry.bind("<Escape>", cancel_rename)
def _start_rename_list_view(self, item_id):
self.dialog.tree.see(item_id)
self.dialog.tree.update_idletasks()
bbox = self.dialog.tree.bbox(item_id, column="#0")
if not bbox:
return
x, y, width, height = bbox
entry = ttk.Entry(self.dialog.tree)
entry_width = self.dialog.tree.column("#0", "width")
entry.place(x=x, y=y, width=entry_width, height=height)
item_text = self.dialog.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.dialog.current_dir, item_text)
new_path = os.path.join(self.dialog.current_dir, new_name)
if new_name and new_path != old_path:
if os.path.exists(new_path):
self.dialog.widget_manager.search_status_label.config(
text=f"'{new_name}' existiert bereits.")
self.dialog.view_manager.populate_files(item_to_select=item_text)
else:
try:
os.rename(old_path, new_path)
self.dialog.view_manager.populate_files(item_to_select=new_name)
except Exception as e:
self.dialog.widget_manager.search_status_label.config(
text=f"Fehler beim Umbenennen: {e}")
self.dialog.view_manager.populate_files()
else:
self.dialog.populate_files(item_to_select=item_text)
entry.destroy()
def cancel_rename(event):
entry.destroy()
entry.bind("<Return>", finish_rename)
entry.bind("<FocusOut>", finish_rename)
entry.bind("<Escape>", cancel_rename)