1667 lines
73 KiB
Python
1667 lines
73 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
|
|
|
|
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)
|
|
|
|
# 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"))
|
|
|
|
|
|
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()
|
|
|
|
# Set the window size
|
|
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 = [] # Store search results
|
|
self.search_mode = False # Track if in search mode
|
|
self.original_path_text = "" # Store original path text
|
|
self.items_to_load_per_batch = 250
|
|
self.item_path_map = {}
|
|
self.responsive_buttons_hidden = None # State for responsive buttons
|
|
self.search_job = None
|
|
self.search_thread = None
|
|
self.search_process = None
|
|
|
|
self.icon_manager = IconManager()
|
|
self.style_manager = StyleManager(self)
|
|
self.widget_manager = WidgetManager(self, self.settings)
|
|
|
|
# Set a permanent binding for the Enter key in the filename entry to always execute a search
|
|
self.widget_manager.filename_entry.bind("<Return>", self.execute_search)
|
|
|
|
self.update_animation_settings() # Add this line
|
|
|
|
self._update_view_mode_buttons()
|
|
|
|
# Defer initial navigation until the window geometry is calculated
|
|
# to ensure the icon view gets the correct initial width.
|
|
def initial_load():
|
|
# Force layout update to get correct widths
|
|
self.update_idletasks()
|
|
self.last_width = self.widget_manager.file_list_frame.winfo_width()
|
|
self._handle_responsive_buttons(self.winfo_width())
|
|
self.navigate_to(self.current_dir)
|
|
|
|
# Using after(10) gives the window manager a moment to process
|
|
# the initial window drawing and sizing.
|
|
self.after(10, initial_load)
|
|
|
|
# Bind the intelligent return handler
|
|
self.widget_manager.path_entry.bind(
|
|
"<Return>", self.handle_path_entry_return)
|
|
|
|
self.bind("<Key>", self.show_search_bar)
|
|
|
|
# Bind the delete key only in "save" mode
|
|
if self.dialog_mode == "save":
|
|
self.bind("<Delete>", self.delete_selected_item)
|
|
|
|
def show_search_ready(self, event=None):
|
|
"""Shows the static 'full circle' to indicate search is ready."""
|
|
if not self.search_mode:
|
|
self.widget_manager.search_animation.show_full_circle()
|
|
|
|
def activate_search(self, event=None):
|
|
"""Activates the search entry or cancels an ongoing search."""
|
|
if self.widget_manager.search_animation.running:
|
|
# If animating, it means a search is active, so cancel it
|
|
if self.search_thread and self.search_thread.is_alive():
|
|
# Set a flag to stop the os.walk loop
|
|
self.search_thread.cancelled = True
|
|
self.widget_manager.search_animation.stop() # Stop animation, show static frame
|
|
self.widget_manager.search_status_label.config(text="Suche abgebrochen.")
|
|
else:
|
|
# If not animating, execute the search directly
|
|
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.search_mode = True
|
|
self.widget_manager.filename_entry.focus_set()
|
|
self.widget_manager.filename_entry.delete(0, tk.END)
|
|
self.widget_manager.filename_entry.insert(0, event.char)
|
|
self.widget_manager.search_animation.show_full_circle()
|
|
|
|
def hide_search_bar(self, event=None):
|
|
self.search_mode = False
|
|
self.widget_manager.filename_entry.delete(0, tk.END)
|
|
self.widget_manager.search_status_label.config(text="")
|
|
self.widget_manager.filename_entry.unbind("<Escape>")
|
|
self.populate_files()
|
|
self.widget_manager.search_animation.hide()
|
|
|
|
def execute_search(self, event=None):
|
|
if self.search_thread and self.search_thread.is_alive():
|
|
return
|
|
search_term = self.widget_manager.filename_entry.get().strip()
|
|
if not search_term:
|
|
self.hide_search_bar()
|
|
return
|
|
self.widget_manager.search_status_label.config(text=f"Suche nach '{search_term}'...")
|
|
self.widget_manager.search_animation.start(pulse=False)
|
|
self.update_idletasks()
|
|
self.search_thread = threading.Thread(target=self._perform_search_in_thread, args=(search_term,))
|
|
self.search_thread.start()
|
|
|
|
def _perform_search_in_thread(self, search_term):
|
|
self.search_results.clear()
|
|
search_dirs = [self.current_dir]
|
|
home_dir = os.path.expanduser("~")
|
|
if os.path.abspath(self.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.settings.get("recursive_search", True)
|
|
search_hidden = self.settings.get("search_hidden_files", False)
|
|
search_term_lower = search_term.lower()
|
|
|
|
for search_dir in search_dirs:
|
|
if not (self.search_thread and self.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.search_thread and self.search_thread.is_alive()):
|
|
raise InterruptedError("Search cancelled by user")
|
|
|
|
if not search_hidden:
|
|
# Exclude hidden directories from traversal
|
|
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
|
# Exclude hidden files from results
|
|
files = [f for f in files if not f.startswith('.')]
|
|
|
|
for name in files + dirs:
|
|
if search_term_lower in name.lower():
|
|
all_files.append(os.path.join(root, name))
|
|
else: # Non-recursive search
|
|
for name in os.listdir(search_dir):
|
|
if not (self.search_thread and self.search_thread.is_alive()):
|
|
raise InterruptedError("Search cancelled by user")
|
|
|
|
if not search_hidden and name.startswith('.'):
|
|
continue
|
|
|
|
if search_term_lower in name.lower():
|
|
all_files.append(os.path.join(search_dir, name))
|
|
|
|
if is_recursive:
|
|
break # Only search the first directory recursively
|
|
|
|
if not (self.search_thread and self.search_thread.is_alive()):
|
|
raise InterruptedError("Search cancelled by user")
|
|
|
|
seen = set()
|
|
unique_files = [x for x in all_files if not (x in seen or seen.add(x))]
|
|
|
|
# Final filtering based on file type
|
|
self.search_results = [
|
|
p for p in unique_files
|
|
if self._matches_filetype(os.path.basename(p)) or os.path.isdir(p)
|
|
]
|
|
|
|
def update_ui():
|
|
if self.search_results:
|
|
self.show_search_results_treeview()
|
|
folder_count = sum(1 for p in self.search_results if os.path.isdir(p))
|
|
file_count = len(self.search_results) - folder_count
|
|
self.widget_manager.search_status_label.config(
|
|
text=f"{folder_count} Ordner und {file_count} Dateien gefunden.")
|
|
else:
|
|
self.widget_manager.search_status_label.config(
|
|
text=f"Keine Ergebnisse für '{search_term}'.")
|
|
self.after(0, update_ui)
|
|
|
|
except (Exception, InterruptedError) as e:
|
|
if isinstance(e, (InterruptedError, subprocess.SubprocessError)):
|
|
self.after(0, lambda: self.widget_manager.search_status_label.config(text="Suche abgebrochen."))
|
|
else:
|
|
self.after(0, lambda: MessageDialog(
|
|
message_type="error", text=f"Fehler bei der Suche: {e}", title="Suchfehler", master=self).show())
|
|
finally:
|
|
self.after(0, self.widget_manager.search_animation.stop)
|
|
self.search_process = None
|
|
|
|
def handle_path_entry_return(self, event):
|
|
"""
|
|
Handles the Enter key in the path entry.
|
|
- If the path is a directory, it navigates into it.
|
|
- If the path is a file, it navigates to the containing directory
|
|
and selects the file.
|
|
"""
|
|
path_text = self.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.widget_manager.search_status_label.config(
|
|
text=f"Pfad nicht gefunden: {self.shorten_text(path_text, 50)}")
|
|
|
|
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()
|
|
|
|
# Update geometry and minsize
|
|
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)
|
|
|
|
# Re-create widgets
|
|
for widget in self.winfo_children():
|
|
widget.destroy()
|
|
|
|
self.style_manager = StyleManager(self)
|
|
self.widget_manager = WidgetManager(self, self.settings)
|
|
self._update_view_mode_buttons()
|
|
|
|
# Reset responsive button state and re-evaluate
|
|
self.responsive_buttons_hidden = None
|
|
self.update_idletasks()
|
|
self._handle_responsive_buttons(self.winfo_width())
|
|
|
|
# If search was active, reset it to avoid inconsistent state
|
|
if self.search_mode:
|
|
self.hide_search_bar() # This will correctly reset the UI
|
|
|
|
self.update_animation_settings() # Add this line
|
|
self.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.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 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):
|
|
# This check is to prevent the resize event from firing for child widgets
|
|
if event.widget is self:
|
|
# Handle icon view redraw on width change, but not in search mode
|
|
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():
|
|
# Ensure all pending geometry changes are processed before redrawing
|
|
self.update_idletasks()
|
|
self.populate_files()
|
|
|
|
self.resize_job = self.after(150, repopulate_icons)
|
|
self.last_width = new_width
|
|
|
|
# Handle responsive buttons in the top bar
|
|
self._handle_responsive_buttons(event.width)
|
|
|
|
def _handle_responsive_buttons(self, window_width):
|
|
# This threshold might need adjustment based on your layout and button sizes
|
|
threshold = 850
|
|
container = self.widget_manager.responsive_buttons_container
|
|
more_button = self.widget_manager.more_button
|
|
|
|
should_be_hidden = window_width < threshold
|
|
|
|
# Only change the layout if the state is different from the current one
|
|
if should_be_hidden != self.responsive_buttons_hidden:
|
|
if should_be_hidden:
|
|
# Hide individual buttons and show the 'more' button
|
|
container.pack_forget()
|
|
more_button.pack(side="left", padx=5)
|
|
else:
|
|
# Show individual buttons and hide the 'more' button
|
|
more_button.pack_forget()
|
|
container.pack(side="left")
|
|
self.responsive_buttons_hidden = should_be_hidden
|
|
|
|
def show_more_menu(self):
|
|
# Create and display the dropdown menu for hidden buttons
|
|
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)
|
|
|
|
# Determine the state for folder/file creation options
|
|
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.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.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.set_icon_view,
|
|
image=self.icon_manager.get_icon('icon_view'), compound='left')
|
|
more_menu.add_command(label="Listenansicht", command=self.set_list_view,
|
|
image=self.icon_manager.get_icon('list_view'), compound='left')
|
|
more_menu.add_separator()
|
|
|
|
# Toggle hidden files option
|
|
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.toggle_hidden_files,
|
|
image=hidden_files_icon, compound='left')
|
|
|
|
# Position and show the menu
|
|
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
|
|
# 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_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 _update_view_mode_buttons(self):
|
|
"""Set the visual state of the view mode buttons."""
|
|
if self.view_mode.get() == "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")
|
|
else:
|
|
self.widget_manager.list_view_button.configure(
|
|
style="Header.TButton.Active.Round")
|
|
self.widget_manager.icon_view_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._update_view_mode_buttons()
|
|
self.populate_files()
|
|
|
|
def set_list_view(self):
|
|
"""Set list view and update button styles"""
|
|
self.view_mode.set("list")
|
|
self._update_view_mode_buttons()
|
|
self.populate_files()
|
|
|
|
|
|
|
|
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')
|
|
|
|
if os.path.isdir(file_path):
|
|
icon = self.icon_manager.get_icon('folder_small')
|
|
else:
|
|
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 with details
|
|
try:
|
|
stat = os.stat(full_path)
|
|
size_str = self._format_size(stat.st_size)
|
|
self.widget_manager.search_status_label.config(
|
|
text=f"'{filename}' Größe: {size_str}")
|
|
except (FileNotFoundError, PermissionError):
|
|
self.widget_manager.search_status_label.config(
|
|
text=f"'{filename}' nicht zugänglich")
|
|
|
|
# Update filename entry
|
|
self.widget_manager.filename_entry.delete(0, tk.END)
|
|
self.widget_manager.filename_entry.insert(0, filename)
|
|
|
|
search_tree.bind("<<TreeviewSelect>>", 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]
|
|
|
|
# Exit search mode and navigate to the file's location
|
|
self.hide_search_bar()
|
|
self.navigate_to(directory, file_to_select=filename)
|
|
|
|
search_tree.bind("<Double-1>", 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("<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]
|
|
|
|
# Exit search mode and navigate to the directory
|
|
self.hide_search_bar()
|
|
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
|
|
elif self.view_mode.get() == "icons":
|
|
if not hasattr(self, 'icon_canvas') or not self.icon_canvas.winfo_exists():
|
|
return
|
|
|
|
container_frame = self.icon_canvas.winfo_children()[0]
|
|
target_path = os.path.join(self.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.update_idletasks()
|
|
if not widget.winfo_exists(): return
|
|
y = widget.winfo_y()
|
|
canvas_height = self.icon_canvas.winfo_height()
|
|
scroll_region = self.icon_canvas.bbox("all")
|
|
if not scroll_region: return
|
|
|
|
scroll_height = scroll_region[3]
|
|
if scroll_height > canvas_height:
|
|
fraction = y / scroll_height
|
|
self.icon_canvas.yview_moveto(fraction)
|
|
|
|
self.after(100, scroll_to_widget)
|
|
break
|
|
|
|
def _unbind_mouse_wheel_events(self):
|
|
# Unbind all mouse wheel events from the root window
|
|
self.unbind_all("<MouseWheel>")
|
|
self.unbind_all("<Button-4>")
|
|
self.unbind_all("<Button-5>")
|
|
|
|
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 _get_item_path_from_widget(self, widget):
|
|
# Traverse up the widget hierarchy to find the item_frame
|
|
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._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.on_rename_request(event, item_path, item_frame)
|
|
|
|
def populate_icon_view(self, item_to_rename=None, item_to_select=None):
|
|
self.all_items, error, warning = self._get_sorted_items()
|
|
self.currently_loaded_count = 0
|
|
|
|
self.icon_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=self.icon_canvas.yview)
|
|
self.icon_canvas.pack(side="left", fill="both", expand=True)
|
|
self.icon_canvas.focus_set()
|
|
v_scrollbar.pack(side="right", fill="y")
|
|
container_frame = ttk.Frame(self.icon_canvas, style="Content.TFrame")
|
|
self.icon_canvas.create_window(
|
|
(0, 0), window=container_frame, anchor="nw")
|
|
container_frame.bind("<Configure>", lambda e: self.icon_canvas.configure(
|
|
scrollregion=self.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.icon_canvas.yview_scroll(delta, "units")
|
|
# Check if scrolled to the bottom and if there are more items to load
|
|
if self.currently_loaded_count < len(self.all_items) and self.icon_canvas.yview()[1] > 0.9:
|
|
self._load_more_items_icon_view(container_frame, _on_mouse_wheel)
|
|
|
|
for widget in [self.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.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.update_idletasks()
|
|
if not widget_to_focus.winfo_exists():
|
|
return
|
|
y = widget_to_focus.winfo_y()
|
|
canvas_height = self.icon_canvas.winfo_height()
|
|
scroll_region = self.icon_canvas.bbox("all")
|
|
if not scroll_region:
|
|
return
|
|
scroll_height = scroll_region[3]
|
|
if scroll_height > canvas_height:
|
|
fraction = y / scroll_height
|
|
self.icon_canvas.yview_moveto(fraction)
|
|
|
|
self.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.currently_loaded_count
|
|
end_index = min(len(self.all_items), start_index +
|
|
self.items_to_load_per_batch)
|
|
|
|
if start_index >= end_index:
|
|
return None # All items loaded
|
|
|
|
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 = 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.all_items[i]
|
|
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, 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 # Store path for later reference
|
|
|
|
if name == item_to_rename:
|
|
self.start_rename(item_frame, path)
|
|
widget_to_focus = item_frame
|
|
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(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._show_context_menu(e, p))
|
|
widget.bind("<F2>", lambda e, p=path,
|
|
f=item_frame: self.on_rename_request(e, p, f))
|
|
# Bind mouse wheel scrolling to all widgets inside the item
|
|
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.currently_loaded_count = end_index
|
|
return widget_to_focus
|
|
|
|
def populate_list_view(self, item_to_rename=None, item_to_select=None):
|
|
self.all_items, error, warning = self._get_sorted_items()
|
|
self.currently_loaded_count = 0
|
|
|
|
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")
|
|
|
|
self.tree.heading("#0", text="Name", anchor="w")
|
|
self.tree.column("#0", anchor="w", width=250, stretch=True)
|
|
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')
|
|
self.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):
|
|
# Check if scrolled to the bottom and if there are more items to load
|
|
if self.currently_loaded_count < len(self.all_items) and self.tree.yview()[1] > 0.9:
|
|
# On-scroll loading should not trigger rename or select.
|
|
self._load_more_items_list_view()
|
|
v_scrollbar.set(*args)
|
|
self.tree.configure(yscrollcommand=_on_scroll)
|
|
|
|
self.tree.bind("<Double-1>", self.on_list_double_click)
|
|
self.tree.bind("<<TreeviewSelect>>", self.on_list_select)
|
|
self.tree.bind("<F2>", self.on_rename_request)
|
|
self.tree.bind("<ButtonRelease-3>", self.on_list_context_menu)
|
|
|
|
if warning:
|
|
self.widget_manager.search_status_label.config(text=warning)
|
|
if error:
|
|
self.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.currently_loaded_count
|
|
end_index = min(len(self.all_items), start_index +
|
|
self.items_to_load_per_batch)
|
|
|
|
if start_index >= end_index:
|
|
return # All items loaded
|
|
|
|
for i in range(start_index, end_index):
|
|
name = self.all_items[i]
|
|
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)
|
|
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)
|
|
except (FileNotFoundError, PermissionError):
|
|
continue
|
|
|
|
self.currently_loaded_count = end_index
|
|
|
|
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(path) # Pass selected path
|
|
self.show_search_ready()
|
|
if 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()
|
|
path = os.path.join(self.current_dir, item_text)
|
|
self.selected_file = path
|
|
self.update_status_bar(path) # Pass selected path
|
|
self.show_search_ready()
|
|
if 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, 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.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.widget_manager.search_status_label.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.widget_manager.search_animation.stop() # Stop animation and show static frame
|
|
|
|
self.populate_files(item_to_select=file_to_select)
|
|
self.update_nav_buttons()
|
|
self.update_status_bar()
|
|
self.update_action_buttons_state()
|
|
except Exception as e:
|
|
self.widget_manager.search_status_label.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 go_up_level(self):
|
|
"""Navigates one directory level up."""
|
|
new_path = os.path.dirname(self.current_dir)
|
|
if new_path != self.current_dir: # Avoid getting stuck at the root
|
|
self.navigate_to(new_path)
|
|
|
|
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, 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):
|
|
# Display item count for directories
|
|
content_count = self._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:
|
|
# Display size for files
|
|
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 delete_selected_item(self, event=None):
|
|
"""Deletes or moves the selected item to trash based on settings."""
|
|
if not self.selected_file or not os.path.exists(self.selected_file):
|
|
return
|
|
|
|
use_trash = self.settings.get(
|
|
"use_trash", False) and SEND2TRASH_AVAILABLE
|
|
confirm = self.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.selected_file)
|
|
|
|
if not confirm:
|
|
dialog = MessageDialog(
|
|
master=self,
|
|
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.selected_file)
|
|
else:
|
|
if os.path.isdir(self.selected_file):
|
|
shutil.rmtree(self.selected_file)
|
|
else:
|
|
os.remove(self.selected_file)
|
|
|
|
self.populate_files()
|
|
self.widget_manager.search_status_label.config(
|
|
text=f"'{item_name}' wurde erfolgreich entfernt.")
|
|
|
|
except Exception as e:
|
|
MessageDialog(
|
|
master=self,
|
|
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.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.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.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.search_status_label.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.hide_search_bar()
|
|
|
|
self.navigate_to(directory)
|
|
self.after(100, lambda: self._select_file_in_view(filename))
|
|
|
|
def _show_tooltip(self, event):
|
|
if hasattr(self, 'tooltip_window') and self.tooltip_window.winfo_exists():
|
|
return
|
|
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="Klicken, um die Suche zu aktivieren",
|
|
background="#333333", foreground="#FFFFFF", 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()
|
|
|
|
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.search_status_label.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.search_status_label.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("<Return>", finish_rename)
|
|
entry.bind("<FocusOut>", finish_rename)
|
|
entry.bind("<Escape>", cancel_rename)
|
|
|
|
def _start_rename_list_view(self, item_id):
|
|
# First, ensure the item is visible by scrolling to it.
|
|
self.tree.see(item_id)
|
|
# Force the UI to process the scrolling and other pending events.
|
|
self.tree.update_idletasks()
|
|
|
|
# Now, get the bounding box. It should be available since the item is visible.
|
|
bbox = self.tree.bbox(item_id, column="#0")
|
|
|
|
# If bbox is still empty (e.g., view is not focused), abort to prevent crash.
|
|
if not bbox:
|
|
return
|
|
|
|
x, y, width, height = bbox
|
|
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.search_status_label.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.search_status_label.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("<Return>", finish_rename)
|
|
entry.bind("<FocusOut>", finish_rename)
|
|
entry.bind("<Escape>", cancel_rename)
|
|
|
|
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.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
|