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:
231
cfd_file_operations.py
Normal file
231
cfd_file_operations.py
Normal 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)
|
77
cfd_navigation_manager.py
Normal file
77
cfd_navigation_manager.py
Normal file
@@ -0,0 +1,77 @@
|
||||
import os
|
||||
import tkinter as tk
|
||||
|
||||
class NavigationManager:
|
||||
def __init__(self, dialog):
|
||||
self.dialog = dialog
|
||||
|
||||
def handle_path_entry_return(self, event):
|
||||
path_text = self.dialog.widget_manager.path_entry.get().strip()
|
||||
potential_path = os.path.realpath(os.path.expanduser(path_text))
|
||||
|
||||
if os.path.isdir(potential_path):
|
||||
self.navigate_to(potential_path)
|
||||
elif os.path.isfile(potential_path):
|
||||
directory = os.path.dirname(potential_path)
|
||||
filename = os.path.basename(potential_path)
|
||||
self.navigate_to(directory, file_to_select=filename)
|
||||
else:
|
||||
self.dialog.widget_manager.search_status_label.config(
|
||||
text=f"Pfad nicht gefunden: {self.dialog.shorten_text(path_text, 50)}")
|
||||
|
||||
def navigate_to(self, path, file_to_select=None):
|
||||
try:
|
||||
real_path = os.path.realpath(
|
||||
os.path.abspath(os.path.expanduser(path)))
|
||||
if not os.path.isdir(real_path):
|
||||
self.dialog.widget_manager.search_status_label.config(
|
||||
text=f"Fehler: Verzeichnis '{os.path.basename(path)}' nicht gefunden.")
|
||||
return
|
||||
if not os.access(real_path, os.R_OK):
|
||||
self.dialog.widget_manager.search_status_label.config(
|
||||
text=f"Zugriff auf '{os.path.basename(path)}' verweigert.")
|
||||
return
|
||||
self.dialog.current_dir = real_path
|
||||
if self.dialog.history_pos < len(self.dialog.history) - 1:
|
||||
self.dialog.history = self.dialog.history[:self.dialog.history_pos + 1]
|
||||
if not self.dialog.history or self.dialog.history[-1] != self.dialog.current_dir:
|
||||
self.dialog.history.append(self.dialog.current_dir)
|
||||
self.dialog.history_pos = len(self.dialog.history) - 1
|
||||
|
||||
self.dialog.widget_manager.search_animation.stop()
|
||||
|
||||
self.dialog.view_manager.populate_files(item_to_select=file_to_select)
|
||||
self.update_nav_buttons()
|
||||
self.dialog.update_status_bar()
|
||||
self.dialog.update_action_buttons_state()
|
||||
except Exception as e:
|
||||
self.dialog.widget_manager.search_status_label.config(text=f"Fehler: {e}")
|
||||
|
||||
def go_back(self):
|
||||
if self.dialog.history_pos > 0:
|
||||
self.dialog.history_pos -= 1
|
||||
self.dialog.current_dir = self.dialog.history[self.dialog.history_pos]
|
||||
self.dialog.view_manager.populate_files()
|
||||
self.update_nav_buttons()
|
||||
self.dialog.update_status_bar()
|
||||
self.dialog.update_action_buttons_state()
|
||||
|
||||
def go_forward(self):
|
||||
if self.dialog.history_pos < len(self.dialog.history) - 1:
|
||||
self.dialog.history_pos += 1
|
||||
self.dialog.current_dir = self.dialog.history[self.dialog.history_pos]
|
||||
self.dialog.view_manager.populate_files()
|
||||
self.update_nav_buttons()
|
||||
self.dialog.update_status_bar()
|
||||
self.dialog.update_action_buttons_state()
|
||||
|
||||
def go_up_level(self):
|
||||
new_path = os.path.dirname(self.dialog.current_dir)
|
||||
if new_path != self.dialog.current_dir:
|
||||
self.navigate_to(new_path)
|
||||
|
||||
def update_nav_buttons(self):
|
||||
self.dialog.widget_manager.back_button.config(
|
||||
state=tk.NORMAL if self.dialog.history_pos > 0 else tk.DISABLED)
|
||||
self.dialog.widget_manager.forward_button.config(state=tk.NORMAL if self.dialog.history_pos < len(
|
||||
self.dialog.history) - 1 else tk.DISABLED)
|
257
cfd_search_manager.py
Normal file
257
cfd_search_manager.py
Normal file
@@ -0,0 +1,257 @@
|
||||
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))
|
173
cfd_settings_dialog.py
Normal file
173
cfd_settings_dialog.py
Normal file
@@ -0,0 +1,173 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from cfd_app_config import CfdConfigManager
|
||||
from cfd_animated_icon import PIL_AVAILABLE
|
||||
|
||||
try:
|
||||
import send2trash
|
||||
SEND2TRASH_AVAILABLE = True
|
||||
except ImportError:
|
||||
SEND2TRASH_AVAILABLE = False
|
||||
|
||||
|
||||
class SettingsDialog(tk.Toplevel):
|
||||
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(
|
||||
value=self.settings.get("search_icon_pos", "right"))
|
||||
self.button_box_pos = tk.StringVar(
|
||||
value=self.settings.get("button_box_pos", "left"))
|
||||
self.window_size_preset = tk.StringVar(
|
||||
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.recursive_search = tk.BooleanVar(
|
||||
value=self.settings.get("recursive_search", True))
|
||||
self.use_trash = tk.BooleanVar(
|
||||
value=self.settings.get("use_trash", False))
|
||||
self.confirm_delete = tk.BooleanVar(
|
||||
value=self.settings.get("confirm_delete", False))
|
||||
self.use_pillow_animation = tk.BooleanVar(
|
||||
value=self.settings.get("use_pillow_animation", False))
|
||||
self.animation_type = tk.StringVar(
|
||||
value=self.settings.get("animation_type", "double_arc"))
|
||||
|
||||
# --- UI Elements ---
|
||||
main_frame = ttk.Frame(self, padding=10)
|
||||
main_frame.pack(fill="both", expand=True)
|
||||
|
||||
# Button Box Position
|
||||
button_box_frame = ttk.LabelFrame(
|
||||
main_frame, text="Position der Dialog-Buttons", padding=10)
|
||||
button_box_frame.pack(fill="x", pady=5)
|
||||
ttk.Radiobutton(button_box_frame, text="Links",
|
||||
variable=self.button_box_pos, value="left").pack(side="left", padx=5)
|
||||
ttk.Radiobutton(button_box_frame, text="Rechts",
|
||||
variable=self.button_box_pos, value="right").pack(side="left", padx=5)
|
||||
|
||||
# Window Size
|
||||
size_frame = ttk.LabelFrame(
|
||||
main_frame, text="Fenstergröße", padding=10)
|
||||
size_frame.pack(fill="x", pady=5)
|
||||
sizes = ["1050x850", "850x650", "650x450"]
|
||||
size_combo = ttk.Combobox(
|
||||
size_frame, textvariable=self.window_size_preset, values=sizes, state="readonly")
|
||||
size_combo.pack(fill="x")
|
||||
|
||||
# Default View Mode
|
||||
view_mode_frame = ttk.LabelFrame(
|
||||
main_frame, text="Standardansicht", padding=10)
|
||||
view_mode_frame.pack(fill="x", pady=5)
|
||||
ttk.Radiobutton(view_mode_frame, text="Kacheln",
|
||||
variable=self.default_view_mode, value="icons").pack(side="left", padx=5)
|
||||
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")
|
||||
ttk.Checkbutton(search_hidden_frame, text="Rekursiv suchen",
|
||||
variable=self.recursive_search).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")
|
||||
|
||||
# Pillow Animation
|
||||
pillow_frame = ttk.LabelFrame(
|
||||
main_frame, text="Animationseinstellungen", padding=10)
|
||||
pillow_frame.pack(fill="x", pady=5)
|
||||
self.use_pillow_animation_checkbutton = ttk.Checkbutton(pillow_frame, text="Hochauflösende Animation verwenden (Pillow)",
|
||||
variable=self.use_pillow_animation)
|
||||
self.use_pillow_animation_checkbutton.pack(anchor="w")
|
||||
if not PIL_AVAILABLE:
|
||||
self.use_pillow_animation_checkbutton.config(state=tk.DISABLED)
|
||||
ttk.Label(pillow_frame, text="(Pillow-Bibliothek nicht gefunden)",
|
||||
font=("TkDefaultFont", 9, "italic")).pack(anchor="w", padx=(20, 0))
|
||||
|
||||
# Animation Type
|
||||
anim_type_frame = ttk.LabelFrame(
|
||||
main_frame, text="Animationstyp", padding=10)
|
||||
anim_type_frame.pack(fill="x", pady=5)
|
||||
ttk.Radiobutton(anim_type_frame, text="Gegenläufige Bogen", variable=self.animation_type,
|
||||
value="counter_arc").pack(side="left", padx=5)
|
||||
ttk.Radiobutton(anim_type_frame, text="Doppelbogen", variable=self.animation_type,
|
||||
value="double_arc").pack(side="left", padx=5)
|
||||
ttk.Radiobutton(anim_type_frame, text="Linie", variable=self.animation_type,
|
||||
value="line").pack(side="left", padx=5)
|
||||
ttk.Radiobutton(anim_type_frame, text="Blinken", variable=self.animation_type,
|
||||
value="blink").pack(side="left", padx=5)
|
||||
|
||||
# Disable deletion options in "open" mode
|
||||
if not self.dialog_mode == "save":
|
||||
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))
|
||||
|
||||
ttk.Button(button_frame, text="Auf Standard zurücksetzen",
|
||||
command=self.reset_to_defaults).pack(side="left", padx=5)
|
||||
ttk.Button(button_frame, text="Speichern",
|
||||
command=self.save_settings).pack(side="right", padx=5)
|
||||
ttk.Button(button_frame, text="Abbrechen",
|
||||
command=self.destroy).pack(side="right")
|
||||
|
||||
def save_settings(self):
|
||||
new_settings = {
|
||||
"button_box_pos": self.button_box_pos.get(),
|
||||
"window_size_preset": self.window_size_preset.get(),
|
||||
"default_view_mode": self.default_view_mode.get(),
|
||||
"search_hidden_files": self.search_hidden_files.get(),
|
||||
"recursive_search": self.recursive_search.get(),
|
||||
"use_trash": self.use_trash.get(),
|
||||
"confirm_delete": self.confirm_delete.get(),
|
||||
"use_pillow_animation": self.use_pillow_animation.get(),
|
||||
"animation_type": self.animation_type.get()
|
||||
}
|
||||
CfdConfigManager.save(new_settings)
|
||||
self.master.reload_config_and_rebuild_ui()
|
||||
self.destroy()
|
||||
|
||||
def reset_to_defaults(self):
|
||||
defaults = CfdConfigManager._default_settings
|
||||
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.recursive_search.set(defaults["recursive_search"])
|
||||
self.use_trash.set(defaults["use_trash"])
|
||||
self.confirm_delete.set(defaults["confirm_delete"])
|
||||
self.use_pillow_animation.set(defaults.get("use_pillow_animation", True) and PIL_AVAILABLE)
|
||||
self.animation_type.set(defaults.get("animation_type", "counter_arc"))
|
@@ -127,22 +127,22 @@ class WidgetManager:
|
||||
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")
|
||||
'back'), command=self.dialog.navigation_manager.go_back, state=tk.DISABLED, style="Header.TButton.Borderless.Round")
|
||||
self.back_button.pack(side="left", padx=(10, 5))
|
||||
Tooltip(self.back_button, "Zurück")
|
||||
|
||||
self.forward_button = ttk.Button(left_nav_container, image=self.dialog.icon_manager.get_icon(
|
||||
'forward'), command=self.dialog.go_forward, state=tk.DISABLED, style="Header.TButton.Borderless.Round")
|
||||
'forward'), command=self.dialog.navigation_manager.go_forward, state=tk.DISABLED, style="Header.TButton.Borderless.Round")
|
||||
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")
|
||||
'up'), command=self.dialog.navigation_manager.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")
|
||||
'home'), command=lambda: self.dialog.navigation_manager.navigate_to(os.path.expanduser("~")), style="Header.TButton.Borderless.Round")
|
||||
self.home_button.pack(side="left", padx=(5, 10))
|
||||
Tooltip(self.home_button, "Home")
|
||||
|
||||
@@ -150,7 +150,7 @@ class WidgetManager:
|
||||
path_search_container = ttk.Frame(top_bar, style='Accent.TFrame')
|
||||
path_search_container.grid(row=0, column=1, 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()))
|
||||
self.path_entry.bind("<Return>", lambda e: self.dialog.navigation_manager.navigate_to(self.path_entry.get()))
|
||||
|
||||
search_icon_pos = self.settings.get("search_icon_pos", "left")
|
||||
if search_icon_pos == 'left':
|
||||
@@ -167,12 +167,12 @@ class WidgetManager:
|
||||
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")
|
||||
'new_folder_small'), command=self.dialog.file_op_manager.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(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")
|
||||
'new_document_small'), command=self.dialog.file_op_manager.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")
|
||||
|
||||
@@ -183,17 +183,17 @@ class WidgetManager:
|
||||
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")
|
||||
'icon_view'), command=self.dialog.view_manager.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(self.view_switch, image=self.dialog.icon_manager.get_icon(
|
||||
'list_view'), command=self.dialog.set_list_view, style="Header.TButton.Borderless.Round")
|
||||
'list_view'), command=self.dialog.view_manager.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(self.responsive_buttons_container, image=self.dialog.icon_manager.get_icon(
|
||||
'hide'), command=self.dialog.toggle_hidden_files, style="Header.TButton.Borderless.Round")
|
||||
'hide'), command=self.dialog.view_manager.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")
|
||||
|
||||
@@ -232,7 +232,7 @@ class WidgetManager:
|
||||
self.sidebar_buttons = []
|
||||
for config in sidebar_buttons_config:
|
||||
btn = ttk.Button(sidebar_buttons_frame, text=f" {config['name']}", image=config['icon'], compound="left",
|
||||
command=lambda p=config['path']: self.dialog.navigate_to(p), style="Dark.TButton.Borderless")
|
||||
command=lambda p=config['path']: self.dialog.navigation_manager.navigate_to(p), style="Dark.TButton.Borderless")
|
||||
btn.pack(fill="x", pady=1)
|
||||
self.sidebar_buttons.append((btn, f" {config['name']}"))
|
||||
|
||||
@@ -298,7 +298,7 @@ class WidgetManager:
|
||||
button_text = f" {device_name[:15]}\n{device_name[15:]}"
|
||||
|
||||
btn = ttk.Button(self.devices_scrollable_frame, text=button_text, image=icon, compound="left",
|
||||
command=lambda p=mount_point: self.dialog.navigate_to(p), style="Dark.TButton.Borderless")
|
||||
command=lambda p=mount_point: self.dialog.navigation_manager.navigate_to(p), style="Dark.TButton.Borderless")
|
||||
btn.pack(fill="x", pady=1)
|
||||
self.device_buttons.append((btn, button_text))
|
||||
|
||||
@@ -362,7 +362,7 @@ class WidgetManager:
|
||||
width=23, height=23, bg=self.style_manager.bottom_color,
|
||||
animation_type=self.settings.get('animation_type', 'counter_arc'))
|
||||
self.search_animation.grid(row=0, column=0, sticky='w', padx=(0, 5), pady=(4,0))
|
||||
self.search_animation.bind("<Button-1>", lambda e: self.dialog.activate_search())
|
||||
self.search_animation.bind("<Button-1>", lambda e: self.dialog.search_manager.activate_search())
|
||||
self.search_status_label.grid(row=0, column=1, sticky="w")
|
||||
|
||||
|
||||
@@ -370,12 +370,12 @@ class WidgetManager:
|
||||
|
||||
if self.dialog.dialog_mode == "save":
|
||||
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")
|
||||
command=self.dialog.file_op_manager.delete_selected_item, style="Bottom.TButton.Borderless.Round")
|
||||
Tooltip(self.trash_button, "Ausgewähltes Element löschen/verschieben")
|
||||
self.save_button = ttk.Button(self.action_status_frame, text="Speichern", command=self.dialog.on_save)
|
||||
self.cancel_button = ttk.Button(self.action_status_frame, text="Abbrechen", command=self.dialog.on_cancel)
|
||||
self.filter_combobox = ttk.Combobox(self.center_container, values=[ft[0] for ft in self.dialog.filetypes], state="readonly")
|
||||
self.filter_combobox.bind("<<ComboboxSelected>>", self.dialog.on_filter_change)
|
||||
self.filter_combobox.bind("<<ComboboxSelected>>", self.dialog.view_manager.on_filter_change)
|
||||
self.filter_combobox.set(self.dialog.filetypes[0][0])
|
||||
|
||||
self.center_container.grid_rowconfigure(0, weight=1)
|
||||
@@ -386,7 +386,7 @@ class WidgetManager:
|
||||
self.open_button = ttk.Button(self.action_status_frame, text="Öffnen", command=self.dialog.on_open)
|
||||
self.cancel_button = ttk.Button(self.action_status_frame, text="Abbrechen", command=self.dialog.on_cancel)
|
||||
self.filter_combobox = ttk.Combobox(self.center_container, values=[ft[0] for ft in self.dialog.filetypes], state="readonly")
|
||||
self.filter_combobox.bind("<<ComboboxSelected>>", self.dialog.on_filter_change)
|
||||
self.filter_combobox.bind("<<ComboboxSelected>>", self.dialog.view_manager.on_filter_change)
|
||||
self.filter_combobox.set(self.dialog.filetypes[0][0])
|
||||
|
||||
self.center_container.grid_rowconfigure(0, weight=1)
|
||||
|
468
cfd_view_manager.py
Normal file
468
cfd_view_manager.py
Normal file
@@ -0,0 +1,468 @@
|
||||
import os
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from datetime import datetime
|
||||
from shared_libs.common_tools import Tooltip
|
||||
from cfd_app_config import AppConfig
|
||||
|
||||
class ViewManager:
|
||||
def __init__(self, dialog):
|
||||
self.dialog = dialog
|
||||
|
||||
def populate_files(self, item_to_rename=None, item_to_select=None):
|
||||
self._unbind_mouse_wheel_events()
|
||||
|
||||
for widget in self.dialog.widget_manager.file_list_frame.winfo_children():
|
||||
widget.destroy()
|
||||
self.dialog.widget_manager.path_entry.delete(0, tk.END)
|
||||
self.dialog.widget_manager.path_entry.insert(0, self.dialog.current_dir)
|
||||
self.dialog.selected_file = None
|
||||
self.dialog.update_status_bar()
|
||||
if self.dialog.view_mode.get() == "list":
|
||||
self.populate_list_view(item_to_rename, item_to_select)
|
||||
else:
|
||||
self.populate_icon_view(item_to_rename, item_to_select)
|
||||
|
||||
def _get_sorted_items(self):
|
||||
try:
|
||||
items = os.listdir(self.dialog.current_dir)
|
||||
num_items = len(items)
|
||||
warning_message = None
|
||||
if num_items > AppConfig.MAX_ITEMS_TO_DISPLAY:
|
||||
warning_message = f"Zeige {AppConfig.MAX_ITEMS_TO_DISPLAY} von {num_items} Einträgen."
|
||||
items = items[:AppConfig.MAX_ITEMS_TO_DISPLAY]
|
||||
dirs = sorted([d for d in items if os.path.isdir(
|
||||
os.path.join(self.dialog.current_dir, d))], key=str.lower)
|
||||
files = sorted([f for f in items if not os.path.isdir(
|
||||
os.path.join(self.dialog.current_dir, f))], key=str.lower)
|
||||
return (dirs + files, None, warning_message)
|
||||
except PermissionError:
|
||||
return ([], "Zugriff verweigert.", None)
|
||||
except FileNotFoundError:
|
||||
return ([], "Verzeichnis nicht gefunden.", None)
|
||||
|
||||
def _get_folder_content_count(self, folder_path):
|
||||
try:
|
||||
if not os.path.isdir(folder_path) or not os.access(folder_path, os.R_OK):
|
||||
return None
|
||||
|
||||
items = os.listdir(folder_path)
|
||||
|
||||
if not self.dialog.show_hidden_files.get():
|
||||
items = [item for item in items if not item.startswith('.')]
|
||||
|
||||
return len(items)
|
||||
except (PermissionError, FileNotFoundError):
|
||||
return None
|
||||
|
||||
def _get_item_path_from_widget(self, widget):
|
||||
while widget and not hasattr(widget, 'item_path'):
|
||||
widget = widget.master
|
||||
return getattr(widget, 'item_path', None)
|
||||
|
||||
def _handle_icon_click(self, event):
|
||||
item_path = self._get_item_path_from_widget(event.widget)
|
||||
if item_path:
|
||||
item_frame = event.widget
|
||||
while not hasattr(item_frame, 'item_path'):
|
||||
item_frame = item_frame.master
|
||||
self.on_item_select(item_path, item_frame)
|
||||
|
||||
def _handle_icon_double_click(self, event):
|
||||
item_path = self._get_item_path_from_widget(event.widget)
|
||||
if item_path:
|
||||
self.on_item_double_click(item_path)
|
||||
|
||||
def _handle_icon_context_menu(self, event):
|
||||
item_path = self._get_item_path_from_widget(event.widget)
|
||||
if item_path:
|
||||
self.dialog.file_op_manager._show_context_menu(event, item_path)
|
||||
|
||||
def _handle_icon_rename_request(self, event):
|
||||
item_path = self._get_item_path_from_widget(event.widget)
|
||||
if item_path:
|
||||
item_frame = event.widget
|
||||
while not hasattr(item_frame, 'item_path'):
|
||||
item_frame = item_frame.master
|
||||
self.dialog.file_op_manager.on_rename_request(event, item_path, item_frame)
|
||||
|
||||
def populate_icon_view(self, item_to_rename=None, item_to_select=None):
|
||||
self.dialog.all_items, error, warning = self._get_sorted_items()
|
||||
self.dialog.currently_loaded_count = 0
|
||||
|
||||
self.dialog.icon_canvas = tk.Canvas(self.dialog.widget_manager.file_list_frame,
|
||||
highlightthickness=0, bg=self.dialog.style_manager.icon_bg_color)
|
||||
v_scrollbar = ttk.Scrollbar(
|
||||
self.dialog.widget_manager.file_list_frame, orient="vertical", command=self.dialog.icon_canvas.yview)
|
||||
self.dialog.icon_canvas.pack(side="left", fill="both", expand=True)
|
||||
self.dialog.icon_canvas.focus_set()
|
||||
v_scrollbar.pack(side="right", fill="y")
|
||||
container_frame = ttk.Frame(self.dialog.icon_canvas, style="Content.TFrame")
|
||||
self.dialog.icon_canvas.create_window(
|
||||
(0, 0), window=container_frame, anchor="nw")
|
||||
container_frame.bind("<Configure>", lambda e: self.dialog.icon_canvas.configure(
|
||||
scrollregion=self.dialog.icon_canvas.bbox("all")))
|
||||
|
||||
def _on_mouse_wheel(event):
|
||||
if event.num == 4:
|
||||
delta = -1
|
||||
elif event.num == 5:
|
||||
delta = 1
|
||||
else:
|
||||
delta = -1 * int(event.delta / 120)
|
||||
self.dialog.icon_canvas.yview_scroll(delta, "units")
|
||||
if self.dialog.currently_loaded_count < len(self.dialog.all_items) and self.dialog.icon_canvas.yview()[1] > 0.9:
|
||||
self._load_more_items_icon_view(container_frame, _on_mouse_wheel)
|
||||
|
||||
for widget in [self.dialog.icon_canvas, container_frame]:
|
||||
widget.bind("<MouseWheel>", _on_mouse_wheel)
|
||||
widget.bind("<Button-4>", _on_mouse_wheel)
|
||||
widget.bind("<Button-5>", _on_mouse_wheel)
|
||||
|
||||
if warning:
|
||||
self.dialog.widget_manager.search_status_label.config(text=warning)
|
||||
if error:
|
||||
ttk.Label(container_frame, text=error).pack(pady=20)
|
||||
return
|
||||
|
||||
widget_to_focus = self._load_more_items_icon_view(
|
||||
container_frame, _on_mouse_wheel, item_to_rename, item_to_select)
|
||||
|
||||
if widget_to_focus:
|
||||
def scroll_to_widget():
|
||||
self.dialog.update_idletasks()
|
||||
if not widget_to_focus.winfo_exists():
|
||||
return
|
||||
y = widget_to_focus.winfo_y()
|
||||
canvas_height = self.dialog.icon_canvas.winfo_height()
|
||||
scroll_region = self.dialog.icon_canvas.bbox("all")
|
||||
if not scroll_region:
|
||||
return
|
||||
scroll_height = scroll_region[3]
|
||||
if scroll_height > canvas_height:
|
||||
fraction = y / scroll_height
|
||||
self.dialog.icon_canvas.yview_moveto(fraction)
|
||||
|
||||
self.dialog.after(100, scroll_to_widget)
|
||||
|
||||
def _load_more_items_icon_view(self, container, scroll_handler, item_to_rename=None, item_to_select=None):
|
||||
start_index = self.dialog.currently_loaded_count
|
||||
end_index = min(len(self.dialog.all_items), start_index +
|
||||
self.dialog.items_to_load_per_batch)
|
||||
|
||||
if start_index >= end_index:
|
||||
return None
|
||||
|
||||
item_width, item_height = 125, 100
|
||||
frame_width = self.dialog.widget_manager.file_list_frame.winfo_width()
|
||||
col_count = max(1, frame_width // item_width - 1)
|
||||
|
||||
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.dialog.all_items[i]
|
||||
if not self.dialog.show_hidden_files.get() and name.startswith('.'):
|
||||
continue
|
||||
path = os.path.join(self.dialog.current_dir, name)
|
||||
is_dir = os.path.isdir(path)
|
||||
if not is_dir and not self.dialog._matches_filetype(name):
|
||||
continue
|
||||
|
||||
item_frame = ttk.Frame(
|
||||
container, width=item_width, height=item_height, style="Item.TFrame")
|
||||
item_frame.grid(row=row, column=col, padx=5, ipadx=25, pady=5)
|
||||
item_frame.grid_propagate(False)
|
||||
item_frame.item_path = path
|
||||
|
||||
if name == item_to_rename:
|
||||
self.dialog.file_op_manager.start_rename(item_frame, path)
|
||||
widget_to_focus = item_frame
|
||||
else:
|
||||
icon = self.dialog.icon_manager.get_icon(
|
||||
'folder_large') if is_dir else self.dialog.get_file_icon(name, 'large')
|
||||
icon_label = ttk.Label(
|
||||
item_frame, image=icon, style="Icon.TLabel")
|
||||
icon_label.pack(pady=(10, 5))
|
||||
name_label = ttk.Label(item_frame, text=self.dialog.shorten_text(
|
||||
name, 14), anchor="center", style="Item.TLabel")
|
||||
name_label.pack(fill="x", expand=True)
|
||||
|
||||
Tooltip(item_frame, name)
|
||||
|
||||
for widget in [item_frame, icon_label, name_label]:
|
||||
widget.bind("<Double-Button-1>", lambda e,
|
||||
p=path: self.on_item_double_click(p))
|
||||
widget.bind("<Button-1>", lambda e, p=path,
|
||||
f=item_frame: self.on_item_select(p, f))
|
||||
widget.bind("<ButtonRelease-3>", lambda e,
|
||||
p=path: self.dialog.file_op_manager._show_context_menu(e, p))
|
||||
widget.bind("<F2>", lambda e, p=path,
|
||||
f=item_frame: self.dialog.file_op_manager.on_rename_request(e, p, f))
|
||||
widget.bind("<MouseWheel>", scroll_handler)
|
||||
widget.bind("<Button-4>", scroll_handler)
|
||||
widget.bind("<Button-5>", scroll_handler)
|
||||
|
||||
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.dialog.currently_loaded_count = end_index
|
||||
return widget_to_focus
|
||||
|
||||
def populate_list_view(self, item_to_rename=None, item_to_select=None):
|
||||
self.dialog.all_items, error, warning = self._get_sorted_items()
|
||||
self.dialog.currently_loaded_count = 0
|
||||
|
||||
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 = ("size", "type", "modified")
|
||||
self.dialog.tree = ttk.Treeview(
|
||||
tree_frame, columns=columns, show="tree headings")
|
||||
|
||||
self.dialog.tree.heading("#0", text="Name", anchor="w")
|
||||
self.dialog.tree.column("#0", anchor="w", width=250, stretch=True)
|
||||
self.dialog.tree.heading("size", text="Größe", anchor="e")
|
||||
self.dialog.tree.column("size", anchor="e", width=120, stretch=False)
|
||||
self.dialog.tree.heading("type", text="Typ", anchor="w")
|
||||
self.dialog.tree.column("type", anchor="w", width=120, stretch=False)
|
||||
self.dialog.tree.heading("modified", text="Geändert am", anchor="w")
|
||||
self.dialog.tree.column("modified", anchor="w", width=160, stretch=False)
|
||||
|
||||
v_scrollbar = ttk.Scrollbar(
|
||||
tree_frame, orient="vertical", command=self.dialog.tree.yview)
|
||||
h_scrollbar = ttk.Scrollbar(
|
||||
tree_frame, orient="horizontal", command=self.dialog.tree.xview)
|
||||
self.dialog.tree.configure(yscrollcommand=v_scrollbar.set,
|
||||
xscrollcommand=h_scrollbar.set)
|
||||
|
||||
self.dialog.tree.grid(row=0, column=0, sticky='nsew')
|
||||
self.dialog.tree.focus_set()
|
||||
v_scrollbar.grid(row=0, column=1, sticky='ns')
|
||||
h_scrollbar.grid(row=1, column=0, sticky='ew')
|
||||
|
||||
def _on_scroll(*args):
|
||||
if self.dialog.currently_loaded_count < len(self.dialog.all_items) and self.dialog.tree.yview()[1] > 0.9:
|
||||
self._load_more_items_list_view()
|
||||
v_scrollbar.set(*args)
|
||||
self.dialog.tree.configure(yscrollcommand=_on_scroll)
|
||||
|
||||
self.dialog.tree.bind("<Double-1>", self.on_list_double_click)
|
||||
self.dialog.tree.bind("<<TreeviewSelect>>", self.on_list_select)
|
||||
self.dialog.tree.bind("<F2>", self.dialog.file_op_manager.on_rename_request)
|
||||
self.dialog.tree.bind("<ButtonRelease-3>", self.on_list_context_menu)
|
||||
|
||||
if warning:
|
||||
self.dialog.widget_manager.search_status_label.config(text=warning)
|
||||
if error:
|
||||
self.dialog.tree.insert("", "end", text=error, values=())
|
||||
return
|
||||
|
||||
self._load_more_items_list_view(item_to_rename, item_to_select)
|
||||
|
||||
def _load_more_items_list_view(self, item_to_rename=None, item_to_select=None):
|
||||
start_index = self.dialog.currently_loaded_count
|
||||
end_index = min(len(self.dialog.all_items), start_index +
|
||||
self.dialog.items_to_load_per_batch)
|
||||
|
||||
if start_index >= end_index:
|
||||
return
|
||||
|
||||
for i in range(start_index, end_index):
|
||||
name = self.dialog.all_items[i]
|
||||
if not self.dialog.show_hidden_files.get() and name.startswith('.'):
|
||||
continue
|
||||
path = os.path.join(self.dialog.current_dir, name)
|
||||
is_dir = os.path.isdir(path)
|
||||
if not is_dir and not self.dialog._matches_filetype(name):
|
||||
continue
|
||||
try:
|
||||
stat = os.stat(path)
|
||||
modified_time = datetime.fromtimestamp(
|
||||
stat.st_mtime).strftime('%d.%m.%Y %H:%M')
|
||||
if is_dir:
|
||||
icon, file_type, size = self.dialog.icon_manager.get_icon(
|
||||
'folder_small'), "Ordner", ""
|
||||
else:
|
||||
icon, file_type, size = self.dialog.get_file_icon(
|
||||
name, 'small'), "Datei", self.dialog._format_size(stat.st_size)
|
||||
item_id = self.dialog.tree.insert("", "end", text=f" {name}", image=icon, values=(
|
||||
size, file_type, modified_time))
|
||||
if name == item_to_rename:
|
||||
self.dialog.tree.selection_set(item_id)
|
||||
self.dialog.tree.focus(item_id)
|
||||
self.dialog.tree.see(item_id)
|
||||
self.dialog.file_op_manager.start_rename(item_id, path)
|
||||
elif name == item_to_select:
|
||||
self.dialog.tree.selection_set(item_id)
|
||||
self.dialog.tree.focus(item_id)
|
||||
self.dialog.tree.see(item_id)
|
||||
except (FileNotFoundError, PermissionError):
|
||||
continue
|
||||
|
||||
self.dialog.currently_loaded_count = end_index
|
||||
|
||||
def on_item_select(self, path, item_frame):
|
||||
if hasattr(self.dialog, 'selected_item_frame') and self.dialog.selected_item_frame.winfo_exists():
|
||||
self.dialog.selected_item_frame.state(['!selected'])
|
||||
for child in self.dialog.selected_item_frame.winfo_children():
|
||||
if isinstance(child, ttk.Label):
|
||||
child.state(['!selected'])
|
||||
item_frame.state(['selected'])
|
||||
for child in item_frame.winfo_children():
|
||||
if isinstance(child, ttk.Label):
|
||||
child.state(['selected'])
|
||||
self.dialog.selected_item_frame = item_frame
|
||||
self.dialog.selected_file = path
|
||||
self.dialog.update_status_bar(path)
|
||||
self.dialog.search_manager.show_search_ready()
|
||||
if not os.path.isdir(path):
|
||||
self.dialog.widget_manager.filename_entry.delete(0, tk.END)
|
||||
self.dialog.widget_manager.filename_entry.insert(0, os.path.basename(path))
|
||||
|
||||
def on_list_select(self, event):
|
||||
if not self.dialog.tree.selection():
|
||||
return
|
||||
item_id = self.dialog.tree.selection()[0]
|
||||
item_text = self.dialog.tree.item(item_id, 'text').strip()
|
||||
path = os.path.join(self.dialog.current_dir, item_text)
|
||||
self.dialog.selected_file = path
|
||||
self.dialog.update_status_bar(path)
|
||||
self.dialog.search_manager.show_search_ready()
|
||||
if not os.path.isdir(self.dialog.selected_file):
|
||||
self.dialog.widget_manager.filename_entry.delete(0, tk.END)
|
||||
self.dialog.widget_manager.filename_entry.insert(0, item_text)
|
||||
|
||||
def on_list_context_menu(self, event):
|
||||
iid = self.dialog.tree.identify_row(event.y)
|
||||
if not iid:
|
||||
return "break"
|
||||
self.dialog.tree.selection_set(iid)
|
||||
item_text = self.dialog.tree.item(iid, "text").strip()
|
||||
item_path = os.path.join(self.dialog.current_dir, item_text)
|
||||
self.dialog.file_op_manager._show_context_menu(event, item_path)
|
||||
return "break"
|
||||
|
||||
def on_item_double_click(self, path):
|
||||
if os.path.isdir(path):
|
||||
self.dialog.navigation_manager.navigate_to(path)
|
||||
elif self.dialog.dialog_mode == "open":
|
||||
self.dialog.selected_file = path
|
||||
self.dialog.destroy()
|
||||
elif self.dialog.dialog_mode == "save":
|
||||
self.dialog.widget_manager.filename_entry.delete(0, tk.END)
|
||||
self.dialog.widget_manager.filename_entry.insert(
|
||||
0, os.path.basename(path))
|
||||
self.dialog.on_save()
|
||||
|
||||
def on_list_double_click(self, event):
|
||||
if not self.dialog.tree.selection():
|
||||
return
|
||||
item_id = self.dialog.tree.selection()[0]
|
||||
item_text = self.dialog.tree.item(item_id, 'text').strip()
|
||||
path = os.path.join(self.dialog.current_dir, item_text)
|
||||
if os.path.isdir(path):
|
||||
self.dialog.navigation_manager.navigate_to(path)
|
||||
elif self.dialog.dialog_mode == "open":
|
||||
self.dialog.selected_file = path
|
||||
self.dialog.destroy()
|
||||
elif self.dialog.dialog_mode == "save":
|
||||
self.dialog.widget_manager.filename_entry.delete(0, tk.END)
|
||||
self.dialog.widget_manager.filename_entry.insert(0, item_text)
|
||||
self.dialog.on_save()
|
||||
|
||||
def _select_file_in_view(self, filename):
|
||||
if self.dialog.view_mode.get() == "list":
|
||||
for item_id in self.dialog.tree.get_children():
|
||||
if self.dialog.tree.item(item_id, "text").strip() == filename:
|
||||
self.dialog.tree.selection_set(item_id)
|
||||
self.dialog.tree.focus(item_id)
|
||||
self.dialog.tree.see(item_id)
|
||||
break
|
||||
elif self.dialog.view_mode.get() == "icons":
|
||||
if not hasattr(self.dialog, 'icon_canvas') or not self.dialog.icon_canvas.winfo_exists():
|
||||
return
|
||||
|
||||
container_frame = self.dialog.icon_canvas.winfo_children()[0]
|
||||
target_path = os.path.join(self.dialog.current_dir, filename)
|
||||
|
||||
for widget in container_frame.winfo_children():
|
||||
if hasattr(widget, 'item_path') and widget.item_path == target_path:
|
||||
self.on_item_select(widget.item_path, widget)
|
||||
|
||||
def scroll_to_widget():
|
||||
self.dialog.update_idletasks()
|
||||
if not widget.winfo_exists(): return
|
||||
y = widget.winfo_y()
|
||||
canvas_height = self.dialog.icon_canvas.winfo_height()
|
||||
scroll_region = self.dialog.icon_canvas.bbox("all")
|
||||
if not scroll_region: return
|
||||
|
||||
scroll_height = scroll_region[3]
|
||||
if scroll_height > canvas_height:
|
||||
fraction = y / scroll_height
|
||||
self.dialog.icon_canvas.yview_moveto(fraction)
|
||||
|
||||
self.dialog.after(100, scroll_to_widget)
|
||||
break
|
||||
|
||||
def _update_view_mode_buttons(self):
|
||||
if self.dialog.view_mode.get() == "icons":
|
||||
self.dialog.widget_manager.icon_view_button.configure(
|
||||
style="Header.TButton.Active.Round")
|
||||
self.dialog.widget_manager.list_view_button.configure(
|
||||
style="Header.TButton.Borderless.Round")
|
||||
else:
|
||||
self.dialog.widget_manager.list_view_button.configure(
|
||||
style="Header.TButton.Active.Round")
|
||||
self.dialog.widget_manager.icon_view_button.configure(
|
||||
style="Header.TButton.Borderless.Round")
|
||||
|
||||
def set_icon_view(self):
|
||||
self.dialog.view_mode.set("icons")
|
||||
self._update_view_mode_buttons()
|
||||
self.populate_files()
|
||||
|
||||
def set_list_view(self):
|
||||
self.dialog.view_mode.set("list")
|
||||
self._update_view_mode_buttons()
|
||||
self.populate_files()
|
||||
|
||||
def toggle_hidden_files(self):
|
||||
self.dialog.show_hidden_files.set(not self.dialog.show_hidden_files.get())
|
||||
if self.dialog.show_hidden_files.get():
|
||||
self.dialog.widget_manager.hidden_files_button.config(
|
||||
image=self.dialog.icon_manager.get_icon('unhide'))
|
||||
Tooltip(self.dialog.widget_manager.hidden_files_button,
|
||||
"Versteckte Dateien ausblenden")
|
||||
else:
|
||||
self.dialog.widget_manager.hidden_files_button.config(
|
||||
image=self.dialog.icon_manager.get_icon('hide'))
|
||||
Tooltip(self.dialog.widget_manager.hidden_files_button,
|
||||
"Versteckte Dateien anzeigen")
|
||||
self.populate_files()
|
||||
|
||||
def on_filter_change(self, event):
|
||||
selected_desc = self.dialog.widget_manager.filter_combobox.get()
|
||||
for desc, pattern in self.dialog.filetypes:
|
||||
if desc == selected_desc:
|
||||
self.dialog.current_filter_pattern = pattern
|
||||
break
|
||||
self.populate_files()
|
||||
|
||||
def _unbind_mouse_wheel_events(self):
|
||||
self.dialog.unbind_all("<MouseWheel>")
|
||||
self.dialog.unbind_all("<Button-4>")
|
||||
self.dialog.unbind_all("<Button-5>")
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user