import os import shutil import tkinter as tk from tkinter import ttk from datetime import datetime class Tooltip: def __init__(self, widget, text, wraplength=250): self.widget = widget self.text = text self.wraplength = wraplength self.tooltip_window = None self.id = None self.widget.bind("", self.enter) self.widget.bind("", self.leave) self.widget.bind("", self.leave) 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 unschedule(self): id = self.id self.id = None if id: self.widget.after_cancel(id) def show_tooltip(self, event=None): x, y, _, _ = self.widget.bbox("insert") x += self.widget.winfo_rootx() + 25 y += self.widget.winfo_rooty() + 20 self.tooltip_window = tw = tk.Toplevel(self.widget) tw.wm_overrideredirect(True) tw.wm_geometry(f"+{x}+{y}") style = ttk.Style() try: bg = style.lookup("Tooltip", "background", default="#FFFFE0") fg = style.lookup("Tooltip", "foreground", default="black") except tk.TclError: bg = "#FFFFE0" fg = "black" label = ttk.Label(tw, text=self.text, justify=tk.LEFT, background=bg, foreground=fg, relief=tk.SOLID, borderwidth=1, wraplength=self.wraplength, padding=(4, 2, 4, 2)) label.pack(ipadx=1) def hide_tooltip(self): tw = self.tooltip_window self.tooltip_window = None if tw: tw.destroy() class CustomFileDialog(tk.Toplevel): def __init__(self, parent, initial_dir=None, filetypes=None): super().__init__(parent) self.parent = parent self.title("Datei auswählen") self.geometry("900x650") self.minsize(650, 400) 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.current_dir] self.history_pos = 0 self.view_mode = tk.StringVar(value="icons") self.resize_job = None self.last_width = 0 self.load_icons() self.create_styles() self.create_widgets() self.update_status_bar() self.after(50, self.populate_files) def load_icons(self): try: self.folder_icon_large = tk.PhotoImage( file="./folder-water.png").zoom(1) self.file_icon_large = tk.PhotoImage( file="./document.png").zoom(1) self.iso_icon_large = tk.PhotoImage( file="./media-optical.png").zoom(1) self.folder_icon_small = tk.PhotoImage(file="./folder-water.png") self.file_icon_small = tk.PhotoImage(file="./document.png") self.iso_icon_small = tk.PhotoImage(file="./media-optical.png") except tk.TclError: self.folder_icon_large = tk.PhotoImage(width=48, height=48) self.file_icon_large = tk.PhotoImage(width=48, height=48) self.iso_icon_large = tk.PhotoImage(width=48, height=48) self.folder_icon_small = tk.PhotoImage(width=16, height=16) self.file_icon_small = tk.PhotoImage(width=16, height=16) self.iso_icon_small = tk.PhotoImage(width=16, height=16) def create_styles(self): style = ttk.Style(self) self.selection_color = "#0078D7" style.map('Item.TFrame', background=[ ('selected', self.selection_color)]) style.configure("Treeview.Heading", relief="raised", borderwidth=1, font=('TkDefaultFont', 10, 'bold')) style.configure("Treeview", rowheight=24) style.layout("Treeview.Row", [('Treeview.row', {'children': [('Treeview.padding', {'children': [('Treeview.indicator', {'side': 'left', 'sticky': 'ns'}), ('Treeview.image', {'side': 'left', 'sticky': 'ns'}), ('Treeview.text', {'side': 'left', 'sticky': 'ns'})], 'sticky': 'nsew'})], 'sticky': 'nsew'})]) 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) sidebar_frame = ttk.Frame(paned_window, padding=5) paned_window.add(sidebar_frame, weight=0) sidebar_frame.grid_rowconfigure(1, weight=1) # --- Navigation buttons in Sidebar --- 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_config = [ {'name': 'Downloads', 'unicode': '📥', 'path': os.path.join( os.path.expanduser("~"), "Downloads")}, {'name': 'Dokumente', 'unicode': '📄', 'path': os.path.join( os.path.expanduser("~"), "Documents")}, {'name': 'Bilder', 'unicode': '🖼️', 'path': os.path.join( os.path.expanduser("~"), "Pictures")}, {'name': 'Musik', 'unicode': '🎵', 'path': os.path.join( os.path.expanduser("~"), "Music")}, {'name': 'Videos', 'unicode': '🎬', 'path': os.path.join( os.path.expanduser("~"), "Videos")}, ] for config in sidebar_buttons_config: btn = ttk.Button(sidebar_buttons_frame, text=f" {config['unicode']} {config['name']}", command=lambda p=config['path']: self.navigate_to(p), style="Sidebar.TButton") btn.pack(fill="x", pady=1) ttk.Style().configure("Sidebar.TButton", anchor="w", padding=5) 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") view_switch = ttk.Frame(top_bar, padding=(5, 0)) view_switch.grid(row=0, column=1) 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=15) self.filter_combobox.grid(row=0, column=2, padx=5) self.filter_combobox.bind( "<>", self.on_filter_change) self.filter_combobox.set(self.filetypes[0][0]) self.file_list_frame = ttk.Frame(content_frame) self.file_list_frame.grid(row=1, column=0, sticky="nsew") self.bind("", 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) 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) def on_window_resize(self, event): new_width = self.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 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.selected_file = None self.update_status_bar() if self.view_mode.get() == "list": self.populate_list_view() else: self.populate_icon_view() def populate_icon_view(self): canvas = tk.Canvas(self.file_list_frame, highlightthickness=0) v_scrollbar = ttk.Scrollbar( self.file_list_frame, orient="vertical", command=canvas.yview) style = ttk.Style(self) bg_color = style.lookup("TFrame", "background") canvas.configure(bg=bg_color) canvas.pack(side="left", fill="both", expand=True) v_scrollbar.pack(side="right", fill="y") container_frame = ttk.Frame(canvas, style="TFrame") self.container_frame = container_frame canvas.create_window((0, 0), window=container_frame, anchor="nw") container_frame.bind("", lambda e: canvas.configure( scrollregion=canvas.bbox("all"))) try: items = os.listdir(self.current_dir) except PermissionError: ttk.Label(container_frame, text="Zugriff verweigert.").pack(pady=20) return item_width = 120 item_height = 100 frame_width = self.file_list_frame.winfo_width() col_count = max(1, frame_width // item_width) row, col = 0, 0 for name in sorted(items, key=str.lower): 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, pady=5) item_frame.grid_propagate(False) icon = self.folder_icon_large if is_dir else ( self.iso_icon_large if name.lower().endswith(".iso") else self.file_icon_large) icon_label = ttk.Label(item_frame, image=icon) icon_label.pack(pady=(10, 5)) name_label = ttk.Label( item_frame, text=self.shorten_text(name, 15), anchor="center") name_label.pack(fill="x", expand=True) Tooltip(item_frame, name) 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: self.on_item_select(p, item_frame)) col += 1 if col >= col_count: 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") 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) 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.bind("", self.on_list_double_click) self.tree.bind("<>", self.on_list_select) try: items = os.listdir(self.current_dir) except PermissionError: return for name in sorted(items, key=str.lower): 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 = self.folder_icon_small file_type = "Ordner" size = "" else: icon = self.iso_icon_small if name.lower().endswith( ".iso") else self.file_icon_small file_type = "Datei" size = self._format_size(stat.st_size) self.tree.insert("", "end", text=name, image=icon, values=( name, size, file_type, modified_time)) except (FileNotFoundError, PermissionError): continue def on_item_select(self, path, item_frame): for child in self.container_frame.winfo_children(): child.state(['!selected']) item_frame.state(['selected']) self.selected_file = path self.update_status_bar() def on_list_select(self, event): if not self.tree.selection(): return item_text = self.tree.item(self.tree.selection()[0])['text'] self.selected_file = os.path.join(self.current_dir, item_text) self.update_status_bar() def on_item_double_click(self, path): 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 item_text = self.tree.item(self.tree.selection()[0])['text'] path = os.path.join(self.current_dir, item_text) 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 self.populate_files() def navigate_to(self, path): home_dir = os.path.expanduser("~") abs_path = os.path.abspath(path) if os.path.isdir(abs_path) and abs_path.startswith(home_dir): self.current_dir = abs_path if self.history_pos < len(self.history) - 1: self.history = self.history[:self.history_pos + 1] if self.history[-1] != self.current_dir: self.history.append(self.current_dir) self.history_pos += 1 self.populate_files() self.update_nav_buttons() else: print( f"Info: Navigation außerhalb von {home_dir} ist nicht erlaubt.") 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() 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() def update_nav_buttons(self): self.back_button.config( state=tk.NORMAL if self.history_pos > 0 else tk.DISABLED) self.forward_button.config(state=tk.NORMAL if self.history_pos < len( self.history) - 1 else tk.DISABLED) def update_status_bar(self): try: _, _, 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) status_text += f" | Dateigröße: {size_str}" self.status_bar.config(text=status_text) 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): self.destroy() def on_cancel(self): 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 return filename.lower().endswith(self.current_filter_pattern.lower().replace("*", "")) 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[:max_len-3] + "..." if len(text) > max_len else text