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.
258 lines
12 KiB
Python
258 lines
12 KiB
Python
import os
|
|
import threading
|
|
import subprocess
|
|
from datetime import datetime
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
from shared_libs.message import MessageDialog
|
|
from cfd_ui_setup import get_xdg_user_dir
|
|
|
|
class SearchManager:
|
|
def __init__(self, dialog):
|
|
self.dialog = dialog
|
|
|
|
def show_search_ready(self, event=None):
|
|
"""Shows the static 'full circle' to indicate search is ready."""
|
|
if not self.dialog.search_mode:
|
|
self.dialog.widget_manager.search_animation.show_full_circle()
|
|
|
|
def activate_search(self, event=None):
|
|
"""Activates the search entry or cancels an ongoing search."""
|
|
if self.dialog.widget_manager.search_animation.running:
|
|
if self.dialog.search_thread and self.dialog.search_thread.is_alive():
|
|
self.dialog.search_thread.cancelled = True
|
|
self.dialog.widget_manager.search_animation.stop()
|
|
self.dialog.widget_manager.search_status_label.config(text="Suche abgebrochen.")
|
|
else:
|
|
self.execute_search()
|
|
|
|
def show_search_bar(self, event=None):
|
|
if isinstance(event.widget, (ttk.Entry, tk.Entry)) or not event.char.strip():
|
|
return
|
|
self.dialog.search_mode = True
|
|
self.dialog.widget_manager.filename_entry.focus_set()
|
|
self.dialog.widget_manager.filename_entry.delete(0, tk.END)
|
|
self.dialog.widget_manager.filename_entry.insert(0, event.char)
|
|
self.dialog.widget_manager.search_animation.show_full_circle()
|
|
|
|
def hide_search_bar(self, event=None):
|
|
self.dialog.search_mode = False
|
|
self.dialog.widget_manager.filename_entry.delete(0, tk.END)
|
|
self.dialog.widget_manager.search_status_label.config(text="")
|
|
self.dialog.widget_manager.filename_entry.unbind("<Escape>")
|
|
self.dialog.view_manager.populate_files()
|
|
self.dialog.widget_manager.search_animation.hide()
|
|
|
|
def execute_search(self, event=None):
|
|
if self.dialog.search_thread and self.dialog.search_thread.is_alive():
|
|
return
|
|
search_term = self.dialog.widget_manager.filename_entry.get().strip()
|
|
if not search_term:
|
|
self.hide_search_bar()
|
|
return
|
|
self.dialog.widget_manager.search_status_label.config(text=f"Suche nach '{search_term}'...")
|
|
self.dialog.widget_manager.search_animation.start(pulse=False)
|
|
self.dialog.update_idletasks()
|
|
self.dialog.search_thread = threading.Thread(target=self._perform_search_in_thread, args=(search_term,))
|
|
self.dialog.search_thread.start()
|
|
|
|
def _perform_search_in_thread(self, search_term):
|
|
self.dialog.search_results.clear()
|
|
search_dirs = [self.dialog.current_dir]
|
|
home_dir = os.path.expanduser("~")
|
|
if os.path.abspath(self.dialog.current_dir) == os.path.abspath(home_dir):
|
|
xdg_dirs = [get_xdg_user_dir(d, f) for d, f in [("XDG_DOWNLOAD_DIR", "Downloads"), ("XDG_DOCUMENTS_DIR", "Documents"), ("XDG_PICTURES_DIR", "Pictures"), ("XDG_MUSIC_DIR", "Music"), ("XDG_VIDEO_DIR", "Videos")]]
|
|
search_dirs.extend([d for d in xdg_dirs if os.path.exists(d) and os.path.abspath(d) != home_dir and d not in search_dirs])
|
|
|
|
try:
|
|
all_files = []
|
|
is_recursive = self.dialog.settings.get("recursive_search", True)
|
|
search_hidden = self.dialog.settings.get("search_hidden_files", False)
|
|
search_term_lower = search_term.lower()
|
|
|
|
for search_dir in search_dirs:
|
|
if not (self.dialog.search_thread and self.dialog.search_thread.is_alive()):
|
|
break
|
|
if not os.path.exists(search_dir):
|
|
continue
|
|
|
|
is_home_search = os.path.abspath(search_dir) == home_dir
|
|
follow_links = is_recursive and is_home_search
|
|
|
|
if is_recursive:
|
|
for root, dirs, files in os.walk(search_dir, followlinks=follow_links):
|
|
if not (self.dialog.search_thread and self.dialog.search_thread.is_alive()):
|
|
raise InterruptedError("Search cancelled by user")
|
|
|
|
if not search_hidden:
|
|
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
|
files = [f for f in files if not f.startswith('.')]
|
|
|
|
for name in files:
|
|
if search_term_lower in name.lower() and self.dialog._matches_filetype(name):
|
|
all_files.append(os.path.join(root, name))
|
|
for name in dirs:
|
|
if search_term_lower in name.lower():
|
|
all_files.append(os.path.join(root, name))
|
|
else:
|
|
for name in os.listdir(search_dir):
|
|
if not (self.dialog.search_thread and self.dialog.search_thread.is_alive()):
|
|
raise InterruptedError("Search cancelled by user")
|
|
|
|
if not search_hidden and name.startswith('.'):
|
|
continue
|
|
|
|
path = os.path.join(search_dir, name)
|
|
is_dir = os.path.isdir(path)
|
|
|
|
if search_term_lower in name.lower():
|
|
if is_dir:
|
|
all_files.append(path)
|
|
elif self.dialog._matches_filetype(name):
|
|
all_files.append(path)
|
|
|
|
if is_recursive:
|
|
break
|
|
|
|
if not (self.dialog.search_thread and self.dialog.search_thread.is_alive()):
|
|
raise InterruptedError("Search cancelled by user")
|
|
|
|
seen = set()
|
|
self.dialog.search_results = [x for x in all_files if not (x in seen or seen.add(x))]
|
|
|
|
def update_ui():
|
|
if self.dialog.search_results:
|
|
self.show_search_results_treeview()
|
|
folder_count = sum(1 for p in self.dialog.search_results if os.path.isdir(p))
|
|
file_count = len(self.dialog.search_results) - folder_count
|
|
self.dialog.widget_manager.search_status_label.config(
|
|
text=f"{folder_count} Ordner und {file_count} Dateien gefunden.")
|
|
else:
|
|
self.dialog.widget_manager.search_status_label.config(
|
|
text=f"Keine Ergebnisse für '{search_term}'.")
|
|
self.dialog.after(0, update_ui)
|
|
|
|
except (Exception, InterruptedError) as e:
|
|
if isinstance(e, (InterruptedError, subprocess.SubprocessError)):
|
|
self.dialog.after(0, lambda: self.dialog.widget_manager.search_status_label.config(text="Suche abgebrochen."))
|
|
else:
|
|
self.dialog.after(0, lambda: MessageDialog(
|
|
message_type="error", text=f"Fehler bei der Suche: {e}", title="Suchfehler", master=self.dialog).show())
|
|
finally:
|
|
self.dialog.after(0, self.dialog.widget_manager.search_animation.stop)
|
|
self.dialog.search_process = None
|
|
|
|
def show_search_results_treeview(self):
|
|
for widget in self.dialog.widget_manager.file_list_frame.winfo_children():
|
|
widget.destroy()
|
|
|
|
tree_frame = ttk.Frame(self.dialog.widget_manager.file_list_frame)
|
|
tree_frame.pack(fill='both', expand=True)
|
|
tree_frame.grid_rowconfigure(0, weight=1)
|
|
tree_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
columns = ("path", "size", "modified")
|
|
search_tree = ttk.Treeview(
|
|
tree_frame, columns=columns, show="tree headings")
|
|
|
|
search_tree.heading("#0", text="Dateiname", anchor="w")
|
|
search_tree.column("#0", anchor="w", width=200, stretch=True)
|
|
search_tree.heading("path", text="Pfad", anchor="w")
|
|
search_tree.column("path", anchor="w", width=300, stretch=True)
|
|
search_tree.heading("size", text="Größe", anchor="e")
|
|
search_tree.column("size", anchor="e", width=100, stretch=False)
|
|
search_tree.heading("modified", text="Geändert am", anchor="w")
|
|
search_tree.column("modified", anchor="w", width=160, stretch=False)
|
|
|
|
v_scrollbar = ttk.Scrollbar(
|
|
tree_frame, orient="vertical", command=search_tree.yview)
|
|
h_scrollbar = ttk.Scrollbar(
|
|
tree_frame, orient="horizontal", command=search_tree.xview)
|
|
search_tree.configure(yscrollcommand=v_scrollbar.set,
|
|
xscrollcommand=h_scrollbar.set)
|
|
|
|
search_tree.grid(row=0, column=0, sticky='nsew')
|
|
v_scrollbar.grid(row=0, column=1, sticky='ns')
|
|
h_scrollbar.grid(row=1, column=0, sticky='ew')
|
|
|
|
for file_path in self.dialog.search_results:
|
|
try:
|
|
filename = os.path.basename(file_path)
|
|
directory = os.path.dirname(file_path)
|
|
stat = os.stat(file_path)
|
|
size = self.dialog._format_size(stat.st_size)
|
|
modified_time = datetime.fromtimestamp(
|
|
stat.st_mtime).strftime('%d.%m.%Y %H:%M')
|
|
|
|
if os.path.isdir(file_path):
|
|
icon = self.dialog.icon_manager.get_icon('folder_small')
|
|
else:
|
|
icon = self.dialog.get_file_icon(filename, 'small')
|
|
|
|
search_tree.insert("", "end", text=f" {filename}", image=icon,
|
|
values=(directory, size, modified_time))
|
|
except (FileNotFoundError, PermissionError):
|
|
continue
|
|
|
|
def on_search_select(event):
|
|
selection = search_tree.selection()
|
|
if selection:
|
|
item = search_tree.item(selection[0])
|
|
filename = item['text'].strip()
|
|
directory = item['values'][0]
|
|
full_path = os.path.join(directory, filename)
|
|
|
|
try:
|
|
stat = os.stat(full_path)
|
|
size_str = self.dialog._format_size(stat.st_size)
|
|
self.dialog.widget_manager.search_status_label.config(
|
|
text=f"'{filename}' Größe: {size_str}")
|
|
except (FileNotFoundError, PermissionError):
|
|
self.dialog.widget_manager.search_status_label.config(
|
|
text=f"'{filename}' nicht zugänglich")
|
|
|
|
self.dialog.widget_manager.filename_entry.delete(0, tk.END)
|
|
self.dialog.widget_manager.filename_entry.insert(0, filename)
|
|
|
|
search_tree.bind("<<TreeviewSelect>>", on_search_select)
|
|
|
|
def on_search_double_click(event):
|
|
selection = search_tree.selection()
|
|
if selection:
|
|
item = search_tree.item(selection[0])
|
|
filename = item['text'].strip()
|
|
directory = item['values'][0]
|
|
|
|
self.hide_search_bar()
|
|
self.dialog.navigation_manager.navigate_to(directory, file_to_select=filename)
|
|
|
|
search_tree.bind("<Double-1>", on_search_double_click)
|
|
|
|
def show_context_menu(event):
|
|
iid = search_tree.identify_row(event.y)
|
|
if not iid:
|
|
return "break"
|
|
search_tree.selection_set(iid)
|
|
item = search_tree.item(iid)
|
|
filename = item['text'].strip()
|
|
directory = item['values'][0]
|
|
full_path = os.path.join(directory, filename)
|
|
self.dialog.file_op_manager._show_context_menu(event, full_path)
|
|
return "break"
|
|
|
|
search_tree.bind("<ButtonRelease-3>", show_context_menu)
|
|
|
|
def _open_file_location(self, search_tree):
|
|
selection = search_tree.selection()
|
|
if not selection:
|
|
return
|
|
|
|
item = search_tree.item(selection[0])
|
|
filename = item['text'].strip()
|
|
directory = item['values'][0]
|
|
|
|
self.hide_search_bar()
|
|
self.dialog.navigation_manager.navigate_to(directory)
|
|
|
|
self.dialog.after(100, lambda: self.dialog.view_manager._select_file_in_view(filename))
|