import os import shutil import tkinter as tk from tkinter import ttk from datetime import datetime import subprocess import json from shared_libs.message import MessageDialog from shared_libs.common_tools import IconManager, Tooltip, ConfigManager, LxTools, ThemeManager from cfd_app_config import AppConfig from cfd_ui_setup import StyleManager, WidgetManager, get_xdg_user_dir # Helper to make icon paths robust, so the script can be run from anywhere SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) MAX_ITEMS_TO_DISPLAY = 1000 class CustomFileDialog(tk.Toplevel): def __init__(self, parent, initial_dir=None, filetypes=None, dialog_mode="open"): super().__init__(parent) self.my_tool_tip = None self.dialog_mode = dialog_mode self.x_width = AppConfig.UI_CONFIG["window_size"][0] self.y_height = AppConfig.UI_CONFIG["window_size"][1] # Set the window size self.geometry(f"{self.x_width}x{self.y_height}") self.minsize(AppConfig.UI_CONFIG["window_size"][0], AppConfig.UI_CONFIG["window_size"][1], ) self.title(AppConfig.UI_CONFIG["window_title"]) # self.tk.call( # "source", f"{AppConfig.SYSTEM_PATHS['tcl_path']}/water.tcl") # ConfigManager.init(AppConfig.SETTINGS_FILE) # theme = ConfigManager.get("theme") # ThemeManager.change_theme(self, theme) self.image = IconManager() LxTools.center_window_cross_platform(self, self.x_width, self.y_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="icons") self.show_hidden_files = tk.BooleanVar(value=False) self.resize_job = None self.last_width = 0 self.search_results = [] # Store search results self.search_mode = False # Track if in search mode self.original_path_text = "" # Store original path text self.icon_manager = IconManager() self.style_manager = StyleManager(self) self.widget_manager = WidgetManager(self) self.navigate_to(self.current_dir) 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 toggle_hidden_files(self): self.show_hidden_files.set(not self.show_hidden_files.get()) if self.show_hidden_files.get(): self.widget_manager.hidden_files_button.config( image=self.icon_manager.get_icon('unhide')) Tooltip(self.widget_manager.hidden_files_button, "Versteckte Dateien ausblenden") else: self.widget_manager.hidden_files_button.config( image=self.icon_manager.get_icon('hide')) Tooltip(self.widget_manager.hidden_files_button, "Versteckte Dateien anzeigen") self.populate_files() def on_window_resize(self, event): new_width = self.widget_manager.file_list_frame.winfo_width() if self.view_mode.get() == "icons" and abs(new_width - self.last_width) > 50: if self.resize_job: self.after_cancel(self.resize_job) self.resize_job = self.after(200, self.populate_files) self.last_width = new_width def on_sidebar_resize(self, event): current_width = event.width # Define a threshold for when to hide/show text threshold_width = 100 # Adjust this value as needed if current_width < threshold_width: # Hide text, show only icons 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: # Show text 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): """Show scrollbar when mouse enters devices area""" self.widget_manager.devices_scrollbar.grid( row=1, column=1, sticky="ns") def _on_devices_leave(self, event): """Hide scrollbar when mouse leaves devices area""" # Check if mouse is really leaving the devices area 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() # Add small buffer to prevent flickering 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_search_mode(self): """Toggle between search mode and normal mode""" if not self.search_mode: # Enter search mode self.search_mode = True self.original_path_text = self.widget_manager.path_entry.get() self.widget_manager.path_entry.delete(0, tk.END) self.widget_manager.path_entry.insert(0, "Suchbegriff eingeben...") self.widget_manager.path_entry.focus_set() # Set focus to path entry self.widget_manager.path_entry.bind( "", self.execute_search) self.widget_manager.path_entry.bind( "", self.clear_search_placeholder) # Show search options self.widget_manager.search_options_frame.pack( side="left", padx=(5, 0)) else: # Exit search mode self.search_mode = False self.widget_manager.path_entry.delete(0, tk.END) self.widget_manager.path_entry.insert(0, self.original_path_text) self.widget_manager.path_entry.bind( "", lambda e: self.navigate_to(self.widget_manager.path_entry.get())) self.widget_manager.path_entry.unbind("") # Hide search options self.widget_manager.search_options_frame.pack_forget() # Return to normal file view self.populate_files() def toggle_recursive_search(self): """Toggle recursive search on/off and update button style""" 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 set_icon_view(self): """Set icon view and update button styles""" self.view_mode.set("icons") self.widget_manager.icon_view_button.configure( style="Header.TButton.Active.Round") self.widget_manager.list_view_button.configure( style="Header.TButton.Borderless.Round") self.populate_files() def set_list_view(self): """Set list view and update button styles""" self.view_mode.set("list") self.widget_manager.list_view_button.configure( style="Header.TButton.Active.Round") self.widget_manager.icon_view_button.configure( style="Header.TButton.Borderless.Round") self.populate_files() def clear_search_placeholder(self, event): """Clear placeholder text when focus enters search field""" if self.widget_manager.path_entry.get() == "Suchbegriff eingeben...": self.widget_manager.path_entry.delete(0, tk.END) def execute_search(self, event): """Execute search when Enter is pressed in search mode""" search_term = self.widget_manager.path_entry.get().strip() if not search_term or search_term == "Suchbegriff eingeben...": return # Clear previous search results self.search_results.clear() # Determine search directories search_dirs = [self.current_dir] # If searching from home directory, also include XDG directories home_dir = os.path.expanduser("~") if os.path.abspath(self.current_dir) == os.path.abspath(home_dir): xdg_dirs = [ get_xdg_user_dir("XDG_DOWNLOAD_DIR", "Downloads"), get_xdg_user_dir("XDG_DOCUMENTS_DIR", "Documents"), get_xdg_user_dir("XDG_PICTURES_DIR", "Pictures"), get_xdg_user_dir("XDG_MUSIC_DIR", "Music"), get_xdg_user_dir("XDG_VIDEO_DIR", "Videos") ] # Add XDG directories that exist and are not already in home for xdg_dir in xdg_dirs: if (os.path.exists(xdg_dir) and os.path.abspath(xdg_dir) != os.path.abspath(home_dir) and xdg_dir not in search_dirs): search_dirs.append(xdg_dir) try: all_files = [] # Search in each directory for search_dir in search_dirs: if not os.path.exists(search_dir): continue # Change to directory and use relative paths to avoid path issues original_cwd = os.getcwd() try: os.chdir(search_dir) # Build find command based on recursive setting (use . for current directory) if self.widget_manager.recursive_search.get(): find_cmd = ['find', '.', '-iname', f'*{search_term}*', '-type', 'f'] else: find_cmd = ['find', '.', '-maxdepth', '1', '-iname', f'*{search_term}*', '-type', 'f'] result = subprocess.run( find_cmd, capture_output=True, text=True, timeout=30) if result.returncode == 0: files = result.stdout.strip().split('\n') # Convert relative paths back to absolute paths directory_files = [] for f in files: if f and f.startswith('./'): abs_path = os.path.join( search_dir, f[2:]) # Remove './' prefix if os.path.isfile(abs_path): directory_files.append(abs_path) all_files.extend(directory_files) finally: os.chdir(original_cwd) # Remove duplicates while preserving order seen = set() unique_files = [] for file_path in all_files: if file_path not in seen: seen.add(file_path) unique_files.append(file_path) # Filter based on currently selected filter pattern self.search_results = [] for file_path in unique_files: filename = os.path.basename(file_path) if self._matches_filetype(filename): self.search_results.append(file_path) # Show search results in TreeView if self.search_results: self.show_search_results_treeview() else: MessageDialog( message_type="info", text=f"Keine Dateien mit '{search_term}' gefunden.", title="Suche", master=self ).show() except subprocess.TimeoutExpired: MessageDialog( message_type="error", text="Suche dauert zu lange und wurde abgebrochen.", title="Suche", master=self ).show() except Exception as e: MessageDialog( message_type="error", text=f"Fehler bei der Suche: {e}", title="Suchfehler", master=self ).show() def show_search_results_treeview(self): """Show search results in TreeView format""" # Clear current file list and replace with search results for widget in self.widget_manager.file_list_frame.winfo_children(): widget.destroy() # Create TreeView for search results tree_frame = ttk.Frame(self.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") # Configure columns 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) # Add scrollbars 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') # Populate with search results for file_path in self.search_results: try: filename = os.path.basename(file_path) directory = os.path.dirname(file_path) stat = os.stat(file_path) size = self._format_size(stat.st_size) modified_time = datetime.fromtimestamp( stat.st_mtime).strftime('%d.%m.%Y %H:%M') icon = self.get_file_icon(filename, 'small') search_tree.insert("", "end", text=f" {filename}", image=icon, values=(directory, size, modified_time)) except (FileNotFoundError, PermissionError): continue # Bind selection event 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) # Update status bar try: stat = os.stat(full_path) size_str = self._format_size(stat.st_size) self.widget_manager.status_bar.config(text=f"'{filename}' Größe: {size_str}") except (FileNotFoundError, PermissionError): self.widget_manager.status_bar.config(text=f"'{filename}' nicht zugänglich") # If in save mode, update filename entry if self.dialog_mode == "save": self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.filename_entry.insert(0, filename) search_tree.bind("<>", on_search_select) # Bind double-click to select file 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] full_path = os.path.join(directory, filename) # Select the file and close dialog self.selected_file = full_path self.destroy() search_tree.bind("", on_search_double_click) # Context menu 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._show_context_menu(event, full_path) return "break" search_tree.bind("", 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] # Exit search mode and navigate to the directory self.toggle_search_mode() # To restore normal view self.navigate_to(directory) # Select the file in the list self.after(100, lambda: self._select_file_in_view(filename)) def _select_file_in_view(self, filename): if self.view_mode.get() == "list": for item_id in self.tree.get_children(): if self.tree.item(item_id, "text").strip() == filename: self.tree.selection_set(item_id) self.tree.focus(item_id) self.tree.see(item_id) break else: # icon view # This is more complex as items are in a grid. A simple selection is not straightforward. # For now, we just navigate to the folder. pass def _unbind_mouse_wheel_events(self): # Unbind all mouse wheel events from the root window self.unbind_all("") self.unbind_all("") self.unbind_all("") def populate_files(self, item_to_rename=None, item_to_select=None): # Unbind previous global mouse wheel events self._unbind_mouse_wheel_events() for widget in self.widget_manager.file_list_frame.winfo_children(): widget.destroy() self.widget_manager.path_entry.delete(0, tk.END) self.widget_manager.path_entry.insert(0, self.current_dir) self.selected_file = None self.update_status_bar() if self.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.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.current_dir, d))], key=str.lower) files = sorted([f for f in items if not os.path.isdir( os.path.join(self.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) # Filter based on hidden files setting if not self.show_hidden_files.get(): items = [item for item in items if not item.startswith('.')] return len(items) except (PermissionError, FileNotFoundError): return None def populate_icon_view(self, item_to_rename=None, item_to_select=None): canvas = tk.Canvas(self.widget_manager.file_list_frame, highlightthickness=0, bg=self.style_manager.icon_bg_color) v_scrollbar = ttk.Scrollbar( self.widget_manager.file_list_frame, orient="vertical", command=canvas.yview) canvas.pack(side="left", fill="both", expand=True) v_scrollbar.pack(side="right", fill="y") container_frame = ttk.Frame(canvas, style="Content.TFrame") canvas.create_window((0, 0), window=container_frame, anchor="nw") container_frame.bind("", lambda e: canvas.configure( scrollregion=canvas.bbox("all"))) def _on_mouse_wheel(event): # Determine the scroll direction and magnitude if event.num == 4: # Scroll up on Linux delta = -1 elif event.num == 5: # Scroll down on Linux delta = 1 else: # MouseWheel event for Windows/macOS delta = -1 * int(event.delta / 120) canvas.yview_scroll(delta, "units") # Bind mouse wheel events to the canvas and the container frame for widget in [canvas, container_frame]: widget.bind("", _on_mouse_wheel) widget.bind("", _on_mouse_wheel) widget.bind("", _on_mouse_wheel) items, error, warning = self._get_sorted_items() if warning: self.widget_manager.status_bar.config(text=warning) if error: ttk.Label(container_frame, text=error).pack(pady=20) return item_width, item_height = 125, 100 frame_width = self.widget_manager.file_list_frame.winfo_width() col_count = max(1, frame_width // item_width - 1) row, col = 0, 0 for name in items: if not self.show_hidden_files.get() and name.startswith('.'): continue path = os.path.join(self.current_dir, name) is_dir = os.path.isdir(path) if not is_dir and not self._matches_filetype(name): continue item_frame = ttk.Frame( container_frame, 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) if name == item_to_rename: self.start_rename(item_frame, path) else: icon = self.icon_manager.get_icon('folder_large') if is_dir else self.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.shorten_text( name, 14), anchor="center", style="Item.TLabel") name_label.pack(fill="x", expand=True) tooltip_text = name if is_dir: content_count = self._get_folder_content_count(path) if content_count is not None: tooltip_text += f"\n({content_count} Einträge)" Tooltip(item_frame, tooltip_text) # Bind events to all individual widgets so scrolling works everywhere for widget in [item_frame, icon_label, name_label]: widget.bind("", lambda e, p=path: self.on_item_double_click(p)) widget.bind("", lambda e, p=path, f=item_frame: self.on_item_select(p, f)) widget.bind("", lambda e, p=path: self._show_context_menu(e, p)) widget.bind("", _on_mouse_wheel) widget.bind("", _on_mouse_wheel) widget.bind("", _on_mouse_wheel) widget.bind("", lambda e, p=path, f=item_frame: self.on_rename_request(e, p, f)) if name == item_to_select: self.on_item_select(path, item_frame) canvas.yview_moveto(row / max(1, (len(items) // col_count))) col = (col + 1) % col_count if col == 0: row += 1 def populate_list_view(self, item_to_rename=None, item_to_select=None): tree_frame = ttk.Frame(self.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.tree = ttk.Treeview( tree_frame, columns=columns, show="tree headings") # Tree Column (#0) self.tree.heading("#0", text="Name", anchor="w") self.tree.column("#0", anchor="w", width=250, stretch=True) # Other Columns self.tree.heading("size", text="Größe", anchor="e") self.tree.column("size", anchor="e", width=120, stretch=False) self.tree.heading("type", text="Typ", anchor="w") self.tree.column("type", anchor="w", width=120, stretch=False) self.tree.heading("modified", text="Geändert am", anchor="w") self.tree.column("modified", anchor="w", width=160, stretch=False) v_scrollbar = ttk.Scrollbar( tree_frame, orient="vertical", command=self.tree.yview) h_scrollbar = ttk.Scrollbar( tree_frame, orient="horizontal", command=self.tree.xview) self.tree.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set) self.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') self.tree.bind("", self.on_list_double_click) self.tree.bind("<>", self.on_list_select) self.tree.bind("", self.on_rename_request) self.tree.bind("", self.on_list_context_menu) items, error, warning = self._get_sorted_items() if warning: self.widget_manager.status_bar.config(text=warning) if error: self.tree.insert("", "end", text=error, values=()) return for name in items: if not self.show_hidden_files.get() and name.startswith('.'): continue path = os.path.join(self.current_dir, name) is_dir = os.path.isdir(path) if not is_dir and not self._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.icon_manager.get_icon( 'folder_small'), "Ordner", "" else: icon, file_type, size = self.get_file_icon( name, 'small'), "Datei", self._format_size(stat.st_size) item_id = self.tree.insert("", "end", text=f" {name}", image=icon, values=( size, file_type, modified_time)) if name == item_to_rename: self.tree.selection_set(item_id) self.tree.focus(item_id) self.tree.see(item_id) # Scroll to the item self.start_rename(item_id, path) elif name == item_to_select: self.tree.selection_set(item_id) self.tree.focus(item_id) self.tree.see(item_id) # Scroll to the item except (FileNotFoundError, PermissionError): continue def on_item_select(self, path, item_frame): if hasattr(self, 'selected_item_frame') and self.selected_item_frame.winfo_exists(): self.selected_item_frame.state(['!selected']) for child in self.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.selected_item_frame = item_frame self.selected_file = path self.update_status_bar() self.bind("", lambda e, p=path, f=item_frame: self.on_rename_request(e, p, f)) if self.dialog_mode == "save" and not os.path.isdir(path): self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.filename_entry.insert(0, os.path.basename(path)) def on_list_select(self, event): if not self.tree.selection(): return item_id = self.tree.selection()[0] item_text = self.tree.item(item_id, 'text').strip() self.selected_file = os.path.join(self.current_dir, item_text) self.update_status_bar() if self.dialog_mode == "save" and not os.path.isdir(self.selected_file): self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.filename_entry.insert(0, item_text) def on_list_context_menu(self, event): iid = self.tree.identify_row(event.y) if not iid: return "break" self.tree.selection_set(iid) item_text = self.tree.item(iid, "text").strip() item_path = os.path.join(self.current_dir, item_text) self._show_context_menu(event, item_path) return "break" def on_rename_request(self, event, item_path=None, item_frame=None): if self.view_mode.get() == "list": if not self.tree.selection(): return item_id = self.tree.selection()[0] item_path = os.path.join( self.current_dir, self.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 on_item_double_click(self, path): if os.path.isdir(path): self.navigate_to(path) elif self.dialog_mode == "open": self.selected_file = path self.destroy() elif self.dialog_mode == "save": self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.filename_entry.insert(0, os.path.basename(path)) self.on_save() def on_list_double_click(self, event): if not self.tree.selection(): return item_id = self.tree.selection()[0] item_text = self.tree.item(item_id, 'text').strip() path = os.path.join(self.current_dir, item_text) if os.path.isdir(path): self.navigate_to(path) elif self.dialog_mode == "open": self.selected_file = path self.destroy() elif self.dialog_mode == "save": self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.filename_entry.insert(0, item_text) self.on_save() def on_filter_change(self, event): selected_desc = self.widget_manager.filter_combobox.get() for desc, pattern in self.filetypes: if desc == selected_desc: self.current_filter_pattern = pattern break self.populate_files() def navigate_to(self, path): try: real_path = os.path.realpath( os.path.abspath(os.path.expanduser(path))) if not os.path.isdir(real_path): self.widget_manager.status_bar.config( text=f"Fehler: Verzeichnis '{os.path.basename(path)}' nicht gefunden.") return if not os.access(real_path, os.R_OK): self.widget_manager.status_bar.config( text=f"Zugriff auf '{os.path.basename(path)}' verweigert.") return self.current_dir = real_path if self.history_pos < len(self.history) - 1: self.history = self.history[:self.history_pos + 1] if not self.history or self.history[-1] != self.current_dir: self.history.append(self.current_dir) self.history_pos = len(self.history) - 1 self.populate_files() self.update_nav_buttons() self.update_status_bar() self.update_action_buttons_state() except Exception as e: self.widget_manager.status_bar.config(text=f"Fehler: {e}") def go_back(self): if self.history_pos > 0: self.history_pos -= 1 self.current_dir = self.history[self.history_pos] self.populate_files() self.update_nav_buttons() self.update_status_bar() self.update_action_buttons_state() def go_forward(self): if self.history_pos < len(self.history) - 1: self.history_pos += 1 self.current_dir = self.history[self.history_pos] self.populate_files() self.update_nav_buttons() self.update_status_bar() self.update_action_buttons_state() def update_nav_buttons(self): self.widget_manager.back_button.config( state=tk.NORMAL if self.history_pos > 0 else tk.DISABLED) self.widget_manager.forward_button.config(state=tk.NORMAL if self.history_pos < len( self.history) - 1 else tk.DISABLED) def update_status_bar(self): 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 self.dialog_mode == "open" and self.selected_file and os.path.exists(self.selected_file) and not os.path.isdir(self.selected_file): size = os.path.getsize(self.selected_file) size_str = self._format_size(size) status_text = f"'{os.path.basename(self.selected_file)}' Größe: {size_str}" self.widget_manager.status_bar.config(text=status_text) except FileNotFoundError: self.widget_manager.status_bar.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 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.current_dir, new_name) try: if is_folder: os.mkdir(new_path) else: open(new_path, 'a').close() self.populate_files(item_to_rename=new_name) except Exception as e: self.widget_manager.status_bar.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.current_dir, new_name)): counter += 1 new_name = f"{name} {counter}{ext}" return new_name def _copy_to_clipboard(self, data): self.clipboard_clear() self.clipboard_append(data) self.widget_manager.status_bar.config(text=f"'{self.shorten_text(data, 50)}' in Zwischenablage kopiert.") def _show_context_menu(self, event, item_path): if not item_path: return "break" # Destroy any existing context menu if hasattr(self, 'context_menu') and self.context_menu.winfo_exists(): self.context_menu.destroy() self.context_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) self.context_menu.add_command(label="Dateiname in Zwischenablage", command=lambda: self._copy_to_clipboard(os.path.basename(item_path))) self.context_menu.add_command(label="Pfad in Zwischenablage", command=lambda: self._copy_to_clipboard(item_path)) if self.search_mode: self.context_menu.add_separator() self.context_menu.add_command(label="Speicherort öffnen", command=lambda: self._open_file_location_from_context(item_path)) self.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.search_mode: self.toggle_search_mode() self.navigate_to(directory) self.after(100, lambda: self._select_file_in_view(filename)) def start_rename(self, item_widget, item_path): if self.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.current_dir, new_name) if new_name and new_path != item_path: if os.path.exists(new_path): self.widget_manager.status_bar.config( text=f"'{new_name}' existiert bereits.") self.populate_files(item_to_select=os.path.basename(item_path)) return try: os.rename(item_path, new_path) self.populate_files(item_to_select=new_name) except Exception as e: self.widget_manager.status_bar.config( text=f"Fehler beim Umbenennen: {e}") self.populate_files() else: self.populate_files(item_to_select=os.path.basename(item_path)) def cancel_rename(event): self.populate_files() entry.bind("", finish_rename) entry.bind("", finish_rename) entry.bind("", cancel_rename) def _start_rename_list_view(self, item_id): x, y, width, height = self.tree.bbox(item_id, column="#0") entry = ttk.Entry(self.tree) # Set a fixed width for the entry widget to prevent it from expanding too much entry_width = self.tree.column("#0", "width") entry.place(x=x, y=y, width=entry_width, height=height) item_text = self.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.current_dir, item_text) new_path = os.path.join(self.current_dir, new_name) if new_name and new_path != old_path: if os.path.exists(new_path): self.widget_manager.status_bar.config( text=f"'{new_name}' existiert bereits.") self.populate_files(item_to_select=item_text) else: try: os.rename(old_path, new_path) self.populate_files(item_to_select=new_name) except Exception as e: self.widget_manager.status_bar.config( text=f"Fehler beim Umbenennen: {e}") self.populate_files() else: self.populate_files(item_to_select=item_text) entry.destroy() def cancel_rename(event): entry.destroy() entry.bind("", finish_rename) entry.bind("", finish_rename) entry.bind("", cancel_rename) def update_action_buttons_state(self): is_writable = os.access(self.current_dir, os.W_OK) state = tk.NORMAL if is_writable 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.replace( "*.", "").lower().split() fn_lower = filename.lower() for p in patterns: if fn_lower.endswith(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) # First pass: Find the root disk name 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 # Second pass: Collect devices based on new criteria for block_device in data.get('blockdevices', []): # Process main device if it has a mountpoint if block_device.get('mountpoint') and \ block_device.get('type') not in ['loop', 'rom'] and \ block_device.get('mountpoint') != '/': # Exclude root mountpoint itself # Exclude non-removable partitions of the root disk if block_device.get('name').startswith(root_disk_name) and not block_device.get('rm', False): pass # Skip this device 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)) # Process children (partitions) 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') != '/': # Exclude root mountpoint itself # Exclude non-removable partitions of the root disk if block_device.get('name') == root_disk_name and not child_device.get('rm', False): pass # Skip this partition 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