commit seven

This commit is contained in:
2025-07-26 18:34:40 +02:00
parent 1dd22ef0f8
commit d7c4c0088b
3 changed files with 92 additions and 156 deletions

View File

@@ -6,15 +6,12 @@ from datetime import datetime
# 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
def get_icon_path(icon_name):
return os.path.join(SCRIPT_DIR, icon_name)
def get_xdg_user_dir(dir_key, fallback_name):
"""
Ermittelt den Pfad eines Benutzerverzeichnisses aus der XDG-Konfigurationsdatei.
Kann mit absoluten Pfaden und Pfaden relativ zum Home-Verzeichnis umgehen.
"""
home = os.path.expanduser("~")
fallback_path = os.path.join(home, fallback_name)
config_path = os.path.join(home, ".config", "user-dirs.dirs")
@@ -29,13 +26,11 @@ def get_xdg_user_dir(dir_key, fallback_name):
if line.startswith(f"{dir_key}="):
path = line.split('=', 1)[1].strip().strip('"')
path = path.replace('$HOME', home)
# Handle paths relative to home dir, e.g., "Music" vs "/home/user/Music"
if not os.path.isabs(path):
path = os.path.join(home, path)
return path
except Exception:
pass # Fallback on any error
pass
return fallback_path
class Tooltip:
@@ -51,11 +46,7 @@ class Tooltip:
def enter(self, event=None): self.schedule()
def leave(self, event=None): self.unschedule(); self.hide_tooltip()
def schedule(self):
self.unschedule()
self.id = self.widget.after(500, self.show_tooltip)
def schedule(self): self.unschedule(); self.id = self.widget.after(500, self.show_tooltip)
def unschedule(self):
id = self.id
self.id = None
@@ -144,8 +135,7 @@ class CustomFileDialog(tk.Toplevel):
def create_styles(self):
style = ttk.Style(self)
self.selection_color = "#D5E5F5"
self.selection_fg_color = "black"
self.selection_color = "#D5E5F5"; self.selection_fg_color = "black"
self.icon_bg_color = style.lookup('TFrame', 'background')
style.configure("Item.TFrame", background=self.icon_bg_color)
style.map('Item.TFrame', background=[('selected', self.selection_color)])
@@ -160,26 +150,18 @@ class CustomFileDialog(tk.Toplevel):
ttk.Style().configure("Sidebar.TButton", anchor="w", padding=5)
def create_widgets(self):
main_frame = ttk.Frame(self, padding="10")
main_frame.pack(fill="both", expand=True)
paned_window = ttk.PanedWindow(main_frame, orient=tk.HORIZONTAL)
paned_window.pack(fill="both", expand=True)
main_frame = ttk.Frame(self, padding="10"); main_frame.pack(fill="both", expand=True)
paned_window = ttk.PanedWindow(main_frame, orient=tk.HORIZONTAL); paned_window.pack(fill="both", expand=True)
sidebar_frame = ttk.Frame(paned_window, padding=5)
paned_window.add(sidebar_frame, weight=0)
sidebar_frame = ttk.Frame(paned_window, padding=5); paned_window.add(sidebar_frame, weight=0)
sidebar_frame.grid_rowconfigure(1, weight=1)
sidebar_nav_frame = ttk.Frame(sidebar_frame)
sidebar_nav_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10))
self.back_button = ttk.Button(sidebar_nav_frame, text="", command=self.go_back, state=tk.DISABLED, width=3)
self.back_button.pack(side="left", fill="x", expand=True)
self.home_button = ttk.Button(sidebar_nav_frame, text="🏠", command=lambda: self.navigate_to(os.path.expanduser("~")), width=3)
self.home_button.pack(side="left", fill="x", expand=True, padx=2)
self.forward_button = ttk.Button(sidebar_nav_frame, text="", command=self.go_forward, state=tk.DISABLED, width=3)
self.forward_button.pack(side="left", fill="x", expand=True)
sidebar_nav_frame = ttk.Frame(sidebar_frame); sidebar_nav_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10))
self.back_button = ttk.Button(sidebar_nav_frame, text="", command=self.go_back, state=tk.DISABLED, width=3); self.back_button.pack(side="left", fill="x", expand=True)
self.home_button = ttk.Button(sidebar_nav_frame, text="🏠", command=lambda: self.navigate_to(os.path.expanduser("~")), width=3); self.home_button.pack(side="left", fill="x", expand=True, padx=2)
self.forward_button = ttk.Button(sidebar_nav_frame, text="", command=self.go_forward, state=tk.DISABLED, width=3); self.forward_button.pack(side="left", fill="x", expand=True)
sidebar_buttons_frame = ttk.Frame(sidebar_frame)
sidebar_buttons_frame.grid(row=1, column=0, sticky="nsew")
sidebar_buttons_frame = ttk.Frame(sidebar_frame); sidebar_buttons_frame.grid(row=1, column=0, sticky="nsew")
sidebar_buttons_config = [
{'name': 'Computer', 'icon': self.icons['computer_small'], 'path': '/'},
{'name': 'Downloads', 'icon': self.icons['downloads_small'], 'path': get_xdg_user_dir("XDG_DOWNLOAD_DIR", "Downloads")},
@@ -189,46 +171,25 @@ class CustomFileDialog(tk.Toplevel):
{'name': 'Videos', 'icon': self.icons['video_small'], 'path': get_xdg_user_dir("XDG_VIDEO_DIR", "Videos")},
]
for config in sidebar_buttons_config:
btn = ttk.Button(sidebar_buttons_frame, text=f" {config['name']}", image=config['icon'], compound="left",
command=lambda p=config['path']: self.navigate_to(p), style="Sidebar.TButton")
btn.pack(fill="x", pady=1)
btn = ttk.Button(sidebar_buttons_frame, text=f" {config['name']}", image=config['icon'], compound="left", command=lambda p=config['path']: self.navigate_to(p), style="Sidebar.TButton"); btn.pack(fill="x", pady=1)
content_frame = ttk.Frame(paned_window, padding=(5, 0, 0, 0))
paned_window.add(content_frame, weight=1)
content_frame.grid_rowconfigure(1, weight=1)
content_frame.grid_columnconfigure(0, weight=1)
content_frame = ttk.Frame(paned_window, padding=(5, 0, 0, 0)); paned_window.add(content_frame, weight=1)
content_frame.grid_rowconfigure(1, weight=1); content_frame.grid_columnconfigure(0, weight=1)
top_bar = ttk.Frame(content_frame)
top_bar.grid(row=0, column=0, sticky="ew", pady=(5, 5))
top_bar.grid_columnconfigure(0, weight=1)
self.path_entry = ttk.Entry(top_bar)
self.path_entry.grid(row=0, column=0, sticky="ew")
self.path_entry.bind("<Return>", lambda e: self.navigate_to(self.path_entry.get()))
self.hidden_files_button = ttk.Checkbutton(top_bar, text="Versteckte Dateien", variable=self.show_hidden_files, command=self.populate_files)
self.hidden_files_button.grid(row=0, column=1, padx=5)
view_switch = ttk.Frame(top_bar, padding=(5, 0))
view_switch.grid(row=0, column=2)
top_bar = ttk.Frame(content_frame); top_bar.grid(row=0, column=0, sticky="ew", pady=(5, 5)); top_bar.grid_columnconfigure(0, weight=1)
self.path_entry = ttk.Entry(top_bar); self.path_entry.grid(row=0, column=0, sticky="ew"); self.path_entry.bind("<Return>", lambda e: self.navigate_to(self.path_entry.get()))
self.hidden_files_button = ttk.Checkbutton(top_bar, text="Versteckte Dateien", variable=self.show_hidden_files, command=self.populate_files); self.hidden_files_button.grid(row=0, column=1, padx=5)
view_switch = ttk.Frame(top_bar, padding=(5, 0)); view_switch.grid(row=0, column=2)
ttk.Radiobutton(view_switch, text="Kacheln", variable=self.view_mode, value="icons", command=self.populate_files).pack(side="left")
ttk.Radiobutton(view_switch, text="Liste", variable=self.view_mode, value="list", command=self.populate_files).pack(side="left")
self.filter_combobox = ttk.Combobox(top_bar, values=[ft[0] for ft in self.filetypes], state="readonly", width=20)
self.filter_combobox.grid(row=0, column=3, padx=5)
self.filter_combobox.bind("<<ComboboxSelected>>", self.on_filter_change)
self.filter_combobox.set(self.filetypes[0][0])
self.filter_combobox = ttk.Combobox(top_bar, values=[ft[0] for ft in self.filetypes], state="readonly", width=20); self.filter_combobox.grid(row=0, column=3, padx=5)
self.filter_combobox.bind("<<ComboboxSelected>>", self.on_filter_change); self.filter_combobox.set(self.filetypes[0][0])
self.file_list_frame = ttk.Frame(content_frame, style="Content.TFrame")
self.file_list_frame.grid(row=1, column=0, sticky="nsew")
self.bind("<Configure>", self.on_window_resize)
self.file_list_frame = ttk.Frame(content_frame, style="Content.TFrame"); self.file_list_frame.grid(row=1, column=0, sticky="nsew"); self.bind("<Configure>", self.on_window_resize)
bottom_frame = ttk.Frame(content_frame)
bottom_frame.grid(row=2, column=0, sticky="ew", pady=(5, 0))
bottom_frame.grid_columnconfigure(0, weight=1)
self.status_bar = ttk.Label(bottom_frame, text="", anchor="w")
self.status_bar.grid(row=0, column=0, sticky="ew")
action_buttons_frame = ttk.Frame(bottom_frame)
action_buttons_frame.grid(row=0, column=1)
bottom_frame = ttk.Frame(content_frame); bottom_frame.grid(row=2, column=0, sticky="ew", pady=(5, 0)); bottom_frame.grid_columnconfigure(0, weight=1)
self.status_bar = ttk.Label(bottom_frame, text="", anchor="w"); self.status_bar.grid(row=0, column=0, sticky="ew")
action_buttons_frame = ttk.Frame(bottom_frame); action_buttons_frame.grid(row=0, column=1)
ttk.Button(action_buttons_frame, text="Öffnen", command=self.on_open).pack(side="right")
ttk.Button(action_buttons_frame, text="Abbrechen", command=self.on_cancel).pack(side="right", padx=5)
@@ -241,37 +202,48 @@ class CustomFileDialog(tk.Toplevel):
def populate_files(self):
for widget in self.file_list_frame.winfo_children(): widget.destroy()
self.path_entry.delete(0, tk.END)
self.path_entry.insert(0, self.current_dir)
self.path_entry.delete(0, tk.END); self.path_entry.insert(0, self.current_dir)
self.selected_file = None
self.update_status_bar()
# Clear status bar before populating, but preserve important warnings
current_status = self.status_bar.cget("text")
if not current_status.startswith("Zeige"): self.update_status_bar()
if self.view_mode.get() == "list": self.populate_list_view()
else: self.populate_icon_view()
def _get_sorted_items(self):
try:
items = os.listdir(self.current_dir)
num_items = len(items)
warning_message = None
if num_items > MAX_ITEMS_TO_DISPLAY:
warning_message = f"Zeige {MAX_ITEMS_TO_DISPLAY} von {num_items} Einträgen."
items = items[: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)
except PermissionError:
return ([], "Zugriff verweigert.")
except FileNotFoundError:
return ([], "Verzeichnis nicht gefunden.")
return (dirs + files, None, warning_message)
except PermissionError: return ([], "Zugriff verweigert.", None)
except FileNotFoundError: return ([], "Verzeichnis nicht gefunden.", None)
def populate_icon_view(self):
canvas = tk.Canvas(self.file_list_frame, highlightthickness=0, bg=self.icon_bg_color)
v_scrollbar = ttk.Scrollbar(self.file_list_frame, orient="vertical", command=canvas.yview)
canvas.pack(side="left", fill="both", expand=True)
v_scrollbar.pack(side="right", fill="y")
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("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
items, error = self._get_sorted_items()
def _on_mouse_wheel(event):
if event.num == 4 or event.delta > 0: canvas.yview_scroll(-1, "units")
elif event.num == 5 or event.delta < 0: canvas.yview_scroll(1, "units")
for widget in [canvas, container_frame]:
widget.bind("<MouseWheel>", _on_mouse_wheel); widget.bind("<Button-4>", _on_mouse_wheel); widget.bind("<Button-5>", _on_mouse_wheel)
items, error, warning = self._get_sorted_items()
if warning: self.status_bar.config(text=warning)
if error: ttk.Label(container_frame, text=error).pack(pady=20); return
item_width, item_height = 120, 100
item_width, item_height = 115, 100
frame_width = self.file_list_frame.winfo_width()
col_count = max(1, frame_width // item_width)
row, col = 0, 0
@@ -282,47 +254,40 @@ class CustomFileDialog(tk.Toplevel):
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, pady=5)
item_frame.grid_propagate(False)
item_frame.grid(row=row, column=col, padx=5, pady=5); item_frame.grid_propagate(False)
icon = self.icons['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, 15), anchor="center", style="Item.TLabel")
name_label.pack(fill="x", expand=True)
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, 15), 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("<MouseWheel>", _on_mouse_wheel); widget.bind("<Button-4>", _on_mouse_wheel); widget.bind("<Button-5>", _on_mouse_wheel)
col = (col + 1) % col_count
if col == 0: row += 1
def populate_list_view(self):
tree_frame = ttk.Frame(self.file_list_frame)
tree_frame.pack(fill='both', expand=True)
columns = ("name", "size", "type", "modified")
self.tree = ttk.Treeview(tree_frame, columns=columns, show="headings")
tree_frame = ttk.Frame(self.file_list_frame); tree_frame.pack(fill='both', expand=True)
columns = ("name", "size", "type", "modified"); self.tree = ttk.Treeview(tree_frame, columns=columns, show="headings")
self.tree.heading("name", text="Name", anchor="w"); self.tree.column("name", anchor="w", width=300, 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)
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.pack(side="left", fill="both", expand=True)
v_scrollbar.pack(side="right", fill="y"); h_scrollbar.pack(side="bottom", fill="x")
self.tree.pack(side="left", fill="both", expand=True); v_scrollbar.pack(side="right", fill="y"); h_scrollbar.pack(side="bottom", fill="x")
self.tree.bind("<Double-1>", self.on_list_double_click); self.tree.bind("<<TreeviewSelect>>", self.on_list_select)
items, error = self._get_sorted_items()
if error: self.tree.insert("", "end", text=error); return
items, error, warning = self._get_sorted_items()
if warning: self.status_bar.config(text=warning)
if error: self.tree.insert("", "end", values=(error,)); 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)
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')
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.icons['folder_small'], "Ordner", ""
else:
@@ -335,11 +300,10 @@ class CustomFileDialog(tk.Toplevel):
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'])
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.selected_item_frame = item_frame; self.selected_file = path
self.update_status_bar()
def on_list_select(self, event):
@@ -357,11 +321,8 @@ class CustomFileDialog(tk.Toplevel):
def on_item_double_click(self, path):
if self._handle_unsupported_file(path): return
if os.path.isdir(path):
self.navigate_to(path)
else:
self.selected_file = path
self.destroy()
if os.path.isdir(path): self.navigate_to(path)
else: self.selected_file = path; self.destroy()
def on_list_double_click(self, event):
if not self.tree.selection(): return
@@ -369,54 +330,38 @@ class CustomFileDialog(tk.Toplevel):
item_text = self.tree.item(item_id, 'values')[0]
path = os.path.join(self.current_dir, item_text)
if self._handle_unsupported_file(path): return
if os.path.isdir(path):
self.navigate_to(path)
else:
self.selected_file = path
self.destroy()
if os.path.isdir(path): self.navigate_to(path)
else: self.selected_file = path; self.destroy()
def on_filter_change(self, event):
selected_desc = self.filter_combobox.get()
for desc, pattern in self.filetypes:
if desc == selected_desc:
self.current_filter_pattern = pattern
break
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.status_bar.config(text=f"Fehler: Verzeichnis '{os.path.basename(path)}' nicht gefunden.")
return
self.status_bar.config(text=f"Fehler: Verzeichnis '{os.path.basename(path)}' nicht gefunden."); return
if not os.access(real_path, os.R_OK):
self.status_bar.config(text=f"Zugriff auf '{os.path.basename(path)}' verweigert.")
return
self.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 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()
except Exception as e:
self.status_bar.config(text=f"Fehler: {e}")
self.history.append(self.current_dir); self.history_pos = len(self.history) - 1
self.populate_files(); self.update_nav_buttons()
except Exception as e: self.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.history_pos -= 1; self.current_dir = self.history[self.history_pos]
self.populate_files(); self.update_nav_buttons()
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.history_pos += 1; self.current_dir = self.history[self.history_pos]
self.populate_files(); self.update_nav_buttons()
def update_nav_buttons(self):
self.back_button.config(state=tk.NORMAL if self.history_pos > 0 else tk.DISABLED)
@@ -424,16 +369,13 @@ class CustomFileDialog(tk.Toplevel):
def update_status_bar(self):
try:
_, _, free = shutil.disk_usage(self.current_dir)
free_str = self._format_size(free)
_, _, free = shutil.disk_usage(self.current_dir); free_str = self._format_size(free)
status_text = f"Freier Speicher: {free_str}"
if 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)
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.status_bar.config(text=status_text)
except FileNotFoundError:
self.status_bar.config(text="Verzeichnis nicht gefunden")
except FileNotFoundError: self.status_bar.config(text="Verzeichnis nicht gefunden")
def on_open(self):
if self.selected_file and os.path.isfile(self.selected_file):
@@ -441,21 +383,17 @@ class CustomFileDialog(tk.Toplevel):
self.destroy()
def on_cancel(self):
self.selected_file = None
self.destroy()
self.selected_file = None; self.destroy()
def get_selected_file(self):
return self.selected_file
def _matches_filetype(self, filename):
if self.current_filter_pattern == "*.*":
return True
patterns = self.current_filter_pattern.split()
for pattern in patterns:
# Handles patterns like "*.txt"
p = pattern.lower().replace("*.", "")
if filename.lower().endswith(p):
return True
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):
@@ -466,4 +404,4 @@ class CustomFileDialog(tk.Toplevel):
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] + "..."
return text if len(text) <= max_len else text[:max_len-3] + "..."

View File

@@ -29,16 +29,14 @@ class GlotzMol(tk.Tk):
container.columnconfigure(0, weight=1)
def open_custom_dialog(self):
# Initial directory can be anywhere, let's test with /backup
initial_directory = "/backup" if os.path.exists(
"/backup") else os.path.expanduser("~")
dialog = CustomFileDialog(self,
initial_dir=initial_directory,
filetypes=[("Audio-Dateien", "*.mp3 *.wav"),
initial_dir=os.path.expanduser("~"),
filetypes=[("Alle Dateien", "*.*"),
("Audio-Dateien", "*.mp3 *.wav"),
("Video-Dateien", "*.mkv *.mp4"),
("ISO-Images", "*.iso"),
("Alle Dateien", "*.*")])
])
# This is the crucial part: wait for the dialog to be closed
self.wait_window(dialog)