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.
450 lines
19 KiB
Python
450 lines
19 KiB
Python
import os
|
|
import shutil
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
from datetime import datetime
|
|
import subprocess
|
|
import json
|
|
import threading
|
|
from shared_libs.message import MessageDialog
|
|
from shared_libs.common_tools import IconManager, Tooltip, ConfigManager, LxTools
|
|
from cfd_app_config import AppConfig, CfdConfigManager
|
|
from cfd_ui_setup import StyleManager, WidgetManager, get_xdg_user_dir
|
|
from cfd_animated_icon import AnimatedIcon, PIL_AVAILABLE
|
|
from cfd_settings_dialog import SettingsDialog
|
|
from cfd_file_operations import FileOperationsManager
|
|
from cfd_search_manager import SearchManager
|
|
from cfd_navigation_manager import NavigationManager
|
|
from cfd_view_manager import ViewManager
|
|
|
|
class CustomFileDialog(tk.Toplevel):
|
|
def __init__(self, parent, initial_dir=None, filetypes=None, dialog_mode="open", title="File Dialog"):
|
|
super().__init__(parent)
|
|
|
|
self.my_tool_tip = None
|
|
self.dialog_mode = dialog_mode
|
|
|
|
self.load_settings()
|
|
|
|
self.geometry(self.settings["window_size_preset"])
|
|
min_width, min_height = self.get_min_size_from_preset(
|
|
self.settings["window_size_preset"])
|
|
self.minsize(min_width, min_height)
|
|
|
|
self.title(title)
|
|
self.image = IconManager()
|
|
width, height = map(
|
|
int, self.settings["window_size_preset"].split('x'))
|
|
LxTools.center_window_cross_platform(self, width, height)
|
|
self.parent = parent
|
|
self.transient(parent)
|
|
self.grab_set()
|
|
|
|
self.selected_file = None
|
|
self.current_dir = os.path.abspath(
|
|
initial_dir) if initial_dir else os.path.expanduser("~")
|
|
self.filetypes = filetypes if filetypes else [("Alle Dateien", "*.* ")]
|
|
self.current_filter_pattern = self.filetypes[0][1]
|
|
self.history = []
|
|
self.history_pos = -1
|
|
self.view_mode = tk.StringVar(
|
|
value=self.settings.get("default_view_mode", "icons"))
|
|
self.show_hidden_files = tk.BooleanVar(value=False)
|
|
self.resize_job = None
|
|
self.last_width = 0
|
|
self.search_results = []
|
|
self.search_mode = False
|
|
self.original_path_text = ""
|
|
self.items_to_load_per_batch = 250
|
|
self.item_path_map = {}
|
|
self.responsive_buttons_hidden = None
|
|
self.search_job = None
|
|
self.search_thread = None
|
|
self.search_process = None
|
|
|
|
self.icon_manager = IconManager()
|
|
self.style_manager = StyleManager(self)
|
|
|
|
self.file_op_manager = FileOperationsManager(self)
|
|
self.search_manager = SearchManager(self)
|
|
self.navigation_manager = NavigationManager(self)
|
|
self.view_manager = ViewManager(self)
|
|
|
|
self.widget_manager = WidgetManager(self, self.settings)
|
|
|
|
self.widget_manager.filename_entry.bind("<Return>", self.search_manager.execute_search)
|
|
|
|
self.update_animation_settings()
|
|
|
|
self.view_manager._update_view_mode_buttons()
|
|
|
|
def initial_load():
|
|
self.update_idletasks()
|
|
self.last_width = self.widget_manager.file_list_frame.winfo_width()
|
|
self._handle_responsive_buttons(self.winfo_width())
|
|
self.navigation_manager.navigate_to(self.current_dir)
|
|
|
|
self.after(10, initial_load)
|
|
|
|
self.widget_manager.path_entry.bind(
|
|
"<Return>", self.navigation_manager.handle_path_entry_return)
|
|
|
|
self.bind("<Key>", self.search_manager.show_search_bar)
|
|
|
|
if self.dialog_mode == "save":
|
|
self.bind("<Delete>", self.file_op_manager.delete_selected_item)
|
|
|
|
def load_settings(self):
|
|
self.settings = CfdConfigManager.load()
|
|
size_preset = self.settings.get("window_size_preset", "1050x850")
|
|
self.settings["window_size_preset"] = size_preset
|
|
if hasattr(self, 'view_mode'):
|
|
self.view_mode.set(self.settings.get("default_view_mode", "icons"))
|
|
|
|
def get_min_size_from_preset(self, preset):
|
|
w, h = map(int, preset.split('x'))
|
|
return max(650, w - 400), max(450, h - 400)
|
|
|
|
def reload_config_and_rebuild_ui(self):
|
|
self.load_settings()
|
|
|
|
self.geometry(self.settings["window_size_preset"])
|
|
min_width, min_height = self.get_min_size_from_preset(
|
|
self.settings["window_size_preset"])
|
|
self.minsize(min_width, min_height)
|
|
width, height = map(
|
|
int, self.settings["window_size_preset"].split('x'))
|
|
LxTools.center_window_cross_platform(self, width, height)
|
|
|
|
for widget in self.winfo_children():
|
|
widget.destroy()
|
|
|
|
self.style_manager = StyleManager(self)
|
|
self.file_op_manager = FileOperationsManager(self)
|
|
self.search_manager = SearchManager(self)
|
|
self.navigation_manager = NavigationManager(self)
|
|
self.view_manager = ViewManager(self)
|
|
self.widget_manager = WidgetManager(self, self.settings)
|
|
self.widget_manager.filename_entry.bind("<Return>", self.search_manager.execute_search)
|
|
self.view_manager._update_view_mode_buttons()
|
|
|
|
self.responsive_buttons_hidden = None
|
|
self.update_idletasks()
|
|
self._handle_responsive_buttons(self.winfo_width())
|
|
|
|
self.update_animation_settings()
|
|
|
|
if self.search_mode:
|
|
self.search_manager.show_search_results_treeview()
|
|
else:
|
|
self.navigation_manager.navigate_to(self.current_dir)
|
|
|
|
def open_settings_dialog(self):
|
|
SettingsDialog(self, dialog_mode=self.dialog_mode)
|
|
|
|
def update_animation_settings(self):
|
|
use_pillow = self.settings.get('use_pillow_animation', False)
|
|
anim_type = self.settings.get('animation_type', 'double')
|
|
is_running = self.widget_manager.search_animation.running
|
|
if is_running:
|
|
self.widget_manager.search_animation.stop()
|
|
|
|
self.widget_manager.search_animation.destroy()
|
|
self.widget_manager.search_animation = AnimatedIcon(
|
|
self.widget_manager.status_container,
|
|
width=23,
|
|
height=23,
|
|
use_pillow=use_pillow,
|
|
animation_type=anim_type,
|
|
color="#2a6fde",
|
|
highlight_color="#5195ff",
|
|
bg=self.style_manager.bottom_color
|
|
)
|
|
self.widget_manager.search_animation.grid(row=0, column=0, sticky='w', padx=(0, 5), pady=(4,0))
|
|
self.widget_manager.search_animation.bind("<Button-1>", lambda e: self.search_manager.activate_search())
|
|
self.widget_manager.search_animation.bind("<Enter>", self._show_tooltip)
|
|
self.widget_manager.search_animation.bind("<Leave>", self._hide_tooltip)
|
|
|
|
if is_running:
|
|
self.widget_manager.search_animation.start()
|
|
|
|
def get_file_icon(self, filename, size='large'):
|
|
ext = os.path.splitext(filename)[1].lower()
|
|
|
|
if ext == '.py':
|
|
return self.icon_manager.get_icon(f'python_{size}')
|
|
if ext == '.pdf':
|
|
return self.icon_manager.get_icon(f'pdf_{size}')
|
|
if ext in ['.tar', '.zip', '.rar', '.7z', '.gz']:
|
|
return self.icon_manager.get_icon(f'archive_{size}')
|
|
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')
|
|
if ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg']:
|
|
return self.icon_manager.get_icon(f'picture_{size}')
|
|
if ext == '.iso':
|
|
return self.icon_manager.get_icon(f'iso_{size}')
|
|
return self.icon_manager.get_icon(f'file_{size}')
|
|
|
|
def on_window_resize(self, event):
|
|
if event.widget is self:
|
|
if self.view_mode.get() == "icons" and not self.search_mode:
|
|
new_width = self.widget_manager.file_list_frame.winfo_width()
|
|
if abs(new_width - self.last_width) > 50:
|
|
if self.resize_job:
|
|
self.after_cancel(self.resize_job)
|
|
|
|
def repopulate_icons():
|
|
self.update_idletasks()
|
|
self.view_manager.populate_files()
|
|
|
|
self.resize_job = self.after(150, repopulate_icons)
|
|
self.last_width = new_width
|
|
|
|
self._handle_responsive_buttons(event.width)
|
|
|
|
def _handle_responsive_buttons(self, window_width):
|
|
threshold = 850
|
|
container = self.widget_manager.responsive_buttons_container
|
|
more_button = self.widget_manager.more_button
|
|
|
|
should_be_hidden = window_width < threshold
|
|
|
|
if should_be_hidden != self.responsive_buttons_hidden:
|
|
if should_be_hidden:
|
|
container.pack_forget()
|
|
more_button.pack(side="left", padx=5)
|
|
else:
|
|
more_button.pack_forget()
|
|
container.pack(side="left")
|
|
self.responsive_buttons_hidden = should_be_hidden
|
|
|
|
def show_more_menu(self):
|
|
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)
|
|
|
|
is_writable = os.access(self.current_dir, os.W_OK)
|
|
creation_state = tk.NORMAL if is_writable and self.dialog_mode != "open" else tk.DISABLED
|
|
|
|
more_menu.add_command(label="Neuer Ordner", command=self.file_op_manager.create_new_folder,
|
|
image=self.icon_manager.get_icon('new_folder_small'), compound='left', state=creation_state)
|
|
more_menu.add_command(label="Neues Dokument", command=self.file_op_manager.create_new_file,
|
|
image=self.icon_manager.get_icon('new_document_small'), compound='left', state=creation_state)
|
|
more_menu.add_separator()
|
|
more_menu.add_command(label="Kachelansicht", command=self.view_manager.set_icon_view,
|
|
image=self.icon_manager.get_icon('icon_view'), compound='left')
|
|
more_menu.add_command(label="Listenansicht", command=self.view_manager.set_list_view,
|
|
image=self.icon_manager.get_icon('list_view'), compound='left')
|
|
more_menu.add_separator()
|
|
|
|
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.view_manager.toggle_hidden_files,
|
|
image=hidden_files_icon, compound='left')
|
|
|
|
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
|
|
threshold_width = 100
|
|
|
|
if current_width < threshold_width:
|
|
for btn, original_text in self.widget_manager.sidebar_buttons:
|
|
btn.config(text="", compound="top")
|
|
for btn, original_text in self.widget_manager.device_buttons:
|
|
btn.config(text="", compound="top")
|
|
else:
|
|
for btn, original_text in self.widget_manager.sidebar_buttons:
|
|
btn.config(text=original_text, compound="left")
|
|
for btn, original_text in self.widget_manager.device_buttons:
|
|
btn.config(text=original_text, compound="left")
|
|
|
|
def _on_devices_enter(self, event):
|
|
self.widget_manager.devices_scrollbar.grid(
|
|
row=1, column=1, sticky="ns")
|
|
|
|
def _on_devices_leave(self, event):
|
|
x, y = event.x_root, event.y_root
|
|
widget_x = self.widget_manager.devices_canvas.winfo_rootx()
|
|
widget_y = self.widget_manager.devices_canvas.winfo_rooty()
|
|
widget_width = self.widget_manager.devices_canvas.winfo_width()
|
|
widget_height = self.widget_manager.devices_canvas.winfo_height()
|
|
|
|
buffer = 5
|
|
if not (widget_x - buffer <= x <= widget_x + widget_width + buffer and
|
|
widget_y - buffer <= y <= widget_y + widget_height + buffer):
|
|
self.widget_manager.devices_scrollbar.grid_remove()
|
|
|
|
def toggle_recursive_search(self):
|
|
self.widget_manager.recursive_search.set(
|
|
not self.widget_manager.recursive_search.get())
|
|
if self.widget_manager.recursive_search.get():
|
|
self.widget_manager.recursive_button.configure(
|
|
style="Header.TButton.Active.Round")
|
|
else:
|
|
self.widget_manager.recursive_button.configure(
|
|
style="Header.TButton.Borderless.Round")
|
|
|
|
def update_status_bar(self, selected_path=None):
|
|
try:
|
|
total, used, free = shutil.disk_usage(self.current_dir)
|
|
free_str = self._format_size(free)
|
|
self.widget_manager.storage_label.config(
|
|
text=f"Freier Speicher: {free_str}")
|
|
self.widget_manager.storage_bar['value'] = (used / total) * 100
|
|
|
|
status_text = ""
|
|
if selected_path and os.path.exists(selected_path):
|
|
if os.path.isdir(selected_path):
|
|
content_count = self.view_manager._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:
|
|
size = os.path.getsize(selected_path)
|
|
size_str = self._format_size(size)
|
|
status_text = f"'{os.path.basename(selected_path)}' Größe: {size_str}"
|
|
self.widget_manager.search_status_label.config(text=status_text)
|
|
except FileNotFoundError:
|
|
self.widget_manager.search_status_label.config(
|
|
text="Verzeichnis nicht gefunden")
|
|
self.widget_manager.storage_label.config(
|
|
text="Freier Speicher: Unbekannt")
|
|
self.widget_manager.storage_bar['value'] = 0
|
|
|
|
def on_open(self):
|
|
if self.selected_file and os.path.isfile(self.selected_file):
|
|
self.destroy()
|
|
|
|
def on_save(self):
|
|
file_name = self.widget_manager.filename_entry.get()
|
|
if file_name:
|
|
self.selected_file = os.path.join(self.current_dir, file_name)
|
|
self.destroy()
|
|
|
|
def on_cancel(self):
|
|
self.selected_file = None
|
|
self.destroy()
|
|
|
|
def get_selected_file(self):
|
|
return self.selected_file
|
|
|
|
def update_action_buttons_state(self):
|
|
is_writable = os.access(self.current_dir, os.W_OK)
|
|
state = tk.NORMAL if is_writable and self.dialog_mode != "open" else tk.DISABLED
|
|
self.widget_manager.new_folder_button.config(state=state)
|
|
self.widget_manager.new_file_button.config(state=state)
|
|
|
|
def _matches_filetype(self, filename):
|
|
if self.current_filter_pattern == "*.*":
|
|
return True
|
|
|
|
patterns = self.current_filter_pattern.lower().split()
|
|
fn_lower = filename.lower()
|
|
|
|
for p in patterns:
|
|
if p.startswith('*.'):
|
|
if fn_lower.endswith(p[1:]):
|
|
return True
|
|
elif p.startswith('.'):
|
|
if fn_lower.endswith(p):
|
|
return True
|
|
else:
|
|
if fn_lower == p:
|
|
return True
|
|
return False
|
|
|
|
def _format_size(self, size_bytes):
|
|
if size_bytes is None:
|
|
return ""
|
|
if size_bytes < 1024:
|
|
return f"{size_bytes} B"
|
|
if size_bytes < 1024**2:
|
|
return f"{size_bytes/1024:.1f} KB"
|
|
if size_bytes < 1024**3:
|
|
return f"{size_bytes/1024**2:.1f} MB"
|
|
return f"{size_bytes/1024**3:.1f} GB"
|
|
|
|
def shorten_text(self, text, max_len):
|
|
return text if len(text) <= max_len else text[:max_len-3] + "..."
|
|
|
|
def _get_mounted_devices(self):
|
|
devices = []
|
|
root_disk_name = None
|
|
try:
|
|
result = subprocess.run(['lsblk', '-J', '-o', 'NAME,MOUNTPOINT,FSTYPE,SIZE,RO,RM,TYPE,LABEL,PKNAME'],
|
|
capture_output=True, text=True, check=True)
|
|
data = json.loads(result.stdout)
|
|
|
|
for block_device in data.get('blockdevices', []):
|
|
if 'children' in block_device:
|
|
for child_device in block_device['children']:
|
|
if child_device.get('mountpoint') == '/':
|
|
root_disk_name = block_device.get('name')
|
|
break
|
|
if root_disk_name:
|
|
break
|
|
|
|
for block_device in data.get('blockdevices', []):
|
|
if block_device.get('mountpoint') and \
|
|
block_device.get('type') not in ['loop', 'rom'] and \
|
|
block_device.get('mountpoint') != '/':
|
|
|
|
if block_device.get('name').startswith(root_disk_name) and not block_device.get('rm', False):
|
|
pass
|
|
else:
|
|
name = block_device.get('name')
|
|
mountpoint = block_device.get('mountpoint')
|
|
label = block_device.get('label')
|
|
removable = block_device.get('rm', False)
|
|
|
|
display_name = label if label else name
|
|
devices.append((display_name, mountpoint, removable))
|
|
|
|
if 'children' in block_device:
|
|
for child_device in block_device['children']:
|
|
if child_device.get('mountpoint') and \
|
|
child_device.get('type') not in ['loop', 'rom'] and \
|
|
child_device.get('mountpoint') != '/':
|
|
|
|
if block_device.get('name') == root_disk_name and not child_device.get('rm', False):
|
|
pass
|
|
else:
|
|
name = child_device.get('name')
|
|
mountpoint = child_device.get('mountpoint')
|
|
label = child_device.get('label')
|
|
removable = child_device.get('rm', False)
|
|
|
|
display_name = label if label else name
|
|
devices.append(
|
|
(display_name, mountpoint, removable))
|
|
|
|
except Exception as e:
|
|
print(f"Error getting mounted devices: {e}")
|
|
return devices
|
|
|
|
def _show_tooltip(self, event):
|
|
if hasattr(self, 'tooltip_window') and self.tooltip_window.winfo_exists():
|
|
return
|
|
|
|
tooltip_text = "Suche starten" if not self.widget_manager.search_animation.running else "Suche abbrechen"
|
|
|
|
x = self.widget_manager.search_animation.winfo_rootx() + 25
|
|
y = self.widget_manager.search_animation.winfo_rooty() + 25
|
|
self.tooltip_window = tk.Toplevel(self)
|
|
self.tooltip_window.wm_overrideredirect(True)
|
|
self.tooltip_window.wm_geometry(f"+{x}+{y}")
|
|
label = tk.Label(self.tooltip_window, text=tooltip_text, relief="solid", borderwidth=1)
|
|
label.pack()
|
|
|
|
def _hide_tooltip(self, event):
|
|
if hasattr(self, 'tooltip_window') and self.tooltip_window.winfo_exists():
|
|
self.tooltip_window.destroy() |