Files
shared_libs/custom_file_dialog.py
2025-07-31 14:09:31 +02:00

848 lines
39 KiB
Python

import os
import shutil
import tkinter as tk
from tkinter import ttk
from datetime import datetime
import subprocess
import json
# 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):
home = os.path.expanduser("~")
fallback_path = os.path.join(home, fallback_name)
config_path = os.path.join(home, ".config", "user-dirs.dirs")
if not os.path.exists(config_path):
return fallback_path
try:
with open(config_path, 'r') as f:
for line in f:
line = line.strip()
if line.startswith(f"{dir_key}="):
path = line.split('=', 1)[1].strip().strip('"')
path = path.replace('$HOME', home)
if not os.path.isabs(path):
path = os.path.join(home, path)
return path
except Exception:
pass
return fallback_path
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("<Enter>", self.enter)
self.widget.bind("<Leave>", self.leave)
self.widget.bind("<ButtonPress>", 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(250, 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}")
label = ttk.Label(tw, text=self.text, justify=tk.LEFT, background="#FFFFE0", foreground="black",
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.history_pos = -1
self.view_mode = tk.StringVar(value="icons")
self.show_hidden_files = tk.BooleanVar(value=False)
self.resize_job = None
self.last_width = 0
self.load_icons()
self.create_styles()
self.create_widgets()
self.navigate_to(self.current_dir)
def load_icons(self):
self.icons = {}
icon_files = {
'computer_small': '/usr/share/icons/lx-icons/32/computer-32.png',
'computer_large': '/usr/share/icons/lx-icons/48/computer-48.png',
'device_small': '/usr/share/icons/lx-icons/32/device-32.png',
'device_large': '/usr/share/icons/lx-icons/48/device-48.png',
'usb_small': '/usr/share/icons/lx-icons/32/usb-32.png',
'usb_large': '/usr/share/icons/lx-icons/48/usb-48.png',
'downloads_small': '/usr/share/icons/lx-icons/32/folder-water-download-32.png',
'downloads_large': '/usr/share/icons/lx-icons/48/folder-water-download-48.png',
'documents_small': '/usr/share/icons/lx-icons/32/folder-water-documents-32.png',
'documents_large': '/usr/share/icons/lx-icons/48/folder-water-documents-48.png',
'pictures_small': '/usr/share/icons/lx-icons/32/folder-water-pictures-32.png',
'pictures_large': '/usr/share/icons/lx-icons/48/folder-water-pictures-48.png',
'music_small': '/usr/share/icons/lx-icons/32/folder-water-music-32.png',
'music_large': '/usr/share/icons/lx-icons/48/folder-water-music-48.png',
'video_small': '/usr/share/icons/lx-icons/32/folder-water-video-32.png',
'video_large_folder': '/usr/share/icons/lx-icons/48/folder-water-video-48.png',
'warning_small': '/usr/share/icons/lx-icons/32/warning.png', 'warning_large': '/usr/share/icons/lx-icons/64/warning.png',
'folder_large': '/usr/share/icons/lx-icons/64/folder-water-64.png', 'file_large': '/usr/share/icons/lx-icons/64/document-64.png',
'python_large': '/usr/share/icons/lx-icons/64/file-python-64.png', 'pdf_large': '/usr/share/icons/lx-icons/64/pdf-64.png',
'archive_large': '/usr/share/icons/lx-icons/64/tar-64.png', 'audio_large': '/usr/share/icons/lx-icons/64/audio-64.png',
'video_large': '/usr/share/icons/lx-icons/64/video-64.png', 'picture_large': '/usr/share/icons/lx-icons/64/picture-64.png',
'iso_large': '/usr/share/icons/lx-icons/64/media-optical-64.png', 'folder_small': '/usr/share/icons/lx-icons/32/folder-water-32.png',
'file_small': '/usr/share/icons/lx-icons/32/document-32.png', 'python_small': '/usr/share/icons/lx-icons/32/file-python-32.png',
'pdf_small': '/usr/share/icons/lx-icons/32/pdf-32.png', 'archive_small': '/usr/share/icons/lx-icons/32/tar-32.png',
'audio_small': '/usr/share/icons/lx-icons/32/audio-32.png', 'video_small_file': '/usr/share/icons/lx-icons/32/video-32.png',
'picture_small': '/usr/share/icons/lx-icons/32/picture-32.png', 'iso_small': '/usr/share/icons/lx-icons/32/media-optical-32.png',
'list_view': '/usr/share/icons/lx-icons/32/list-32.png',
'icon_view': '/usr/share/icons/lx-icons/32/carrel-32.png',
'hide': '/usr/share/icons/lx-icons/32/hide-32.png',
'unhide': '/usr/share/icons/lx-icons/32/unhide-32.png',
'back': '/usr/share/icons/lx-icons/32/arrow-left-32.png',
'forward': '/usr/share/icons/lx-icons/32/arrow-right-32.png',
'home': '/usr/share/icons/lx-icons/32/home-32.png'
}
for key, filename in icon_files.items():
try:
self.icons[key] = tk.PhotoImage(file=get_icon_path(filename))
except tk.TclError:
size = 32 if 'small' in key or 'view' in key or 'hide' in key or 'unhide' in key or 'back' in key or 'forward' in key or 'home' in key else 64
self.icons[key] = tk.PhotoImage(width=size, height=size)
def get_file_icon(self, filename, size='large'):
ext = os.path.splitext(filename)[1].lower()
if ext == '.svg':
return self.icons[f'warning_{size}']
if ext == '.py':
return self.icons[f'python_{size}']
if ext == '.pdf':
return self.icons[f'pdf_{size}']
if ext in ['.tar', '.zip', '.rar', '.7z', '.gz']:
return self.icons[f'archive_{size}']
if ext in ['.mp3', '.wav', '.ogg', '.flac']:
return self.icons[f'audio_{size}']
if ext in ['.mp4', '.mkv', '.avi', '.mov']:
return self.icons[f'video_{size}'] if size == 'large' else self.icons['video_small_file']
if ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp']:
return self.icons[f'picture_{size}']
if ext == '.iso':
return self.icons[f'iso_{size}']
return self.icons[f'file_{size}']
def create_styles(self):
style = ttk.Style(self)
base_bg = self.cget('background')
self.is_dark = sum(self.winfo_rgb(base_bg)) / 3 < 32768
if self.is_dark:
self.selection_color = "#4a6984" # Darker blue for selection
self.icon_bg_color = "#3c3c3c" # Lighter background for content
self.accent_color = "#2a2a2a" # Darker accent for the bottom
self.header = "#2b2b2b" # Dark Color for Header and round buttons in Header
self.hover_extrastyle = "#4a4a4a" # Hover Color for Buttons in header and Sidebar
self.hover_extrastyle2 = "#494949"
self.sidebar_color = "#333333"
self.bottom_color = self.accent_color
self.color_foreground = "#ffffff"
self.freespace_background = self.sidebar_color
else:
self.selection_color = "#cce5ff" # Light blue for selection
self.icon_bg_color = base_bg # Main background for content
self.accent_color = "#e0e0e0"
# Light Color for Header and round buttons in Header
self.header = "#d9d9d9"
# Hover Color for Buttons in header and Sidebar
self.hover_extrastyle = "#f5f5f5"
self.hover_extrastyle2 = "#494949" # Hover Color for Buttons in Sidebar
self.sidebar_color = "#e7e7e7"
self.bottom_color = "#cecece"
self.freespace_background = self.sidebar_color
self.color_foreground = "#000000"
style.configure("Header.TButton.Borderless.Round",
background=self.header)
style.map("Header.TButton.Borderless.Round", background=[
('active', self.hover_extrastyle)])
style.configure("Dark.TButton.Borderless", anchor="w",
background=self.sidebar_color, foreground=self.color_foreground, padding=(20, 5, 0, 5))
style.map("Dark.TButton.Borderless", background=[
('active', self.hover_extrastyle2)])
style.configure("Accent.TFrame", background=self.header)
style.configure("Accent.TLabel", background=self.header)
style.configure("AccentBottom.TFrame", background=self.bottom_color)
style.configure("AccentBottom.TLabel", background=self.bottom_color)
style.configure("Sidebar.TFrame", background=self.sidebar_color)
style.configure("Content.TFrame", background=self.icon_bg_color)
style.configure("Item.TFrame", background=self.icon_bg_color)
style.map('Item.TFrame', background=[
('selected', self.selection_color)])
style.configure("Item.TLabel", background=self.icon_bg_color)
style.map('Item.TLabel', background=[('selected', self.selection_color)], foreground=[
('selected', "black" if not self.is_dark else "white")])
style.configure("Icon.TLabel", background=self.icon_bg_color)
style.map('Icon.TLabel', background=[
('selected', self.selection_color)])
style.configure("Treeview.Heading", relief="flat",
borderwidth=0, font=('TkDefaultFont', 10, 'bold'))
style.configure("Treeview", rowheight=32, pady=2,
background=self.icon_bg_color, fieldbackground=self.icon_bg_color, borderwidth=0)
style.map("Treeview", background=[('selected', self.selection_color)], foreground=[
('selected', "black" if not self.is_dark else "white")])
style.configure("TButton.Borderless.Round", anchor="w")
style.configure("Small.Horizontal.TProgressbar", thickness=8)
def create_widgets(self):
# Main container
main_frame = ttk.Frame(self, style='Accent.TFrame')
main_frame.pack(fill="both", expand=True)
main_frame.grid_rowconfigure(2, weight=1)
main_frame.grid_columnconfigure(0, weight=1)
# Top bar for navigation and path
top_bar = ttk.Frame(
main_frame, style='Accent.TFrame', padding=(0, 5, 0, 5))
top_bar.grid(row=0, column=0, columnspan=2, sticky="ew")
top_bar.grid_columnconfigure(1, weight=1)
# Navigation buttons
nav_buttons_container = ttk.Frame(top_bar, style='Accent.TFrame')
nav_buttons_container.grid(row=0, column=0, sticky="w")
self.back_button = ttk.Button(
nav_buttons_container, image=self.icons['back'], command=self.go_back, state=tk.DISABLED, style="Header.TButton.Borderless.Round")
self.back_button.pack(side="left", padx=10)
Tooltip(self.back_button, "Zurück")
self.forward_button = ttk.Button(
nav_buttons_container, image=self.icons['forward'], command=self.go_forward, state=tk.DISABLED, style="Header.TButton.Borderless.Round")
self.forward_button.pack(side="left")
Tooltip(self.forward_button, "Vorwärts")
self.home_button = ttk.Button(nav_buttons_container, image=self.icons['home'], command=lambda: self.navigate_to(
os.path.expanduser("~")), style="Header.TButton.Borderless.Round")
self.home_button.pack(side="left", padx=10)
Tooltip(self.home_button, "Home")
# Path entry
self.path_entry = ttk.Entry(top_bar)
self.path_entry.grid(row=0, column=1, sticky="ew")
self.path_entry.bind(
"<Return>", lambda e: self.navigate_to(self.path_entry.get()))
# View switch and hidden files button
right_top_bar_frame = ttk.Frame(top_bar, style='Accent.TFrame')
right_top_bar_frame.grid(row=0, column=2, sticky="e")
view_switch = ttk.Frame(right_top_bar_frame,
padding=(5, 0), style='Accent.TFrame')
view_switch.pack(side="left")
self.icon_view_button = ttk.Button(view_switch, image=self.icons['icon_view'], command=lambda: (
self.view_mode.set("icons"), self.populate_files()), style="Header.TButton.Borderless.Round")
self.icon_view_button.pack(side="left", padx=(50, 10))
Tooltip(self.icon_view_button, "Kachelansicht")
self.list_view_button = ttk.Button(view_switch, image=self.icons['list_view'], command=lambda: (
self.view_mode.set("list"), self.populate_files()), style="Header.TButton.Borderless.Round")
self.list_view_button.pack(side="left")
Tooltip(self.list_view_button, "Listenansicht")
self.hidden_files_button = ttk.Button(
right_top_bar_frame, image=self.icons['hide'], command=self.toggle_hidden_files, style="Header.TButton.Borderless.Round")
self.hidden_files_button.pack(side="left", padx=10)
Tooltip(self.hidden_files_button, "Versteckte Dateien anzeigen")
# Horizontal separator
separator_color = "#000000" if self.is_dark else "#9c9c9c"
tk.Frame(main_frame, height=1, bg=separator_color).grid(
row=1, column=0, columnspan=2, sticky="ew")
# PanedWindow for resizable sidebar and content
paned_window = ttk.PanedWindow(main_frame, orient=tk.HORIZONTAL)
paned_window.grid(row=2, column=0, columnspan=2, sticky="nsew")
# Sidebar
sidebar_frame = ttk.Frame(
paned_window, style="Sidebar.TFrame", padding=(0, 0, 0, 15), width=200)
# Prevent content from resizing the frame
sidebar_frame.grid_propagate(False)
# Use weight=0 to give it a fixed size
paned_window.add(sidebar_frame, weight=0)
sidebar_frame.grid_rowconfigure(2, weight=1)
sidebar_buttons_frame = ttk.Frame(
sidebar_frame, style="Sidebar.TFrame", padding=(0, 15, 0, 0))
sidebar_buttons_frame.grid(
row=0, 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")},
{'name': 'Dokumente', 'icon': self.icons['documents_small'], 'path': get_xdg_user_dir(
"XDG_DOCUMENTS_DIR", "Documents")},
{'name': 'Bilder', 'icon': self.icons['pictures_small'], 'path': get_xdg_user_dir(
"XDG_PICTURES_DIR", "Pictures")},
{'name': 'Musik', 'icon': self.icons['music_small'], 'path': get_xdg_user_dir(
"XDG_MUSIC_DIR", "Music")},
{'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="Dark.TButton.Borderless")
btn.pack(fill="x", pady=1)
# Horizontal separator
separator_color = "#a9a9a9" if self.is_dark else "#7c7c7c"
tk.Frame(sidebar_frame, height=1, bg=separator_color).grid(
row=1, column=0, sticky="ew", padx=20, pady=15)
# Mounted devices
mounted_devices_frame = ttk.Frame(
sidebar_frame, style="Sidebar.TFrame")
mounted_devices_frame.grid(row=2, column=0, sticky="nsew", padx=10)
ttk.Label(mounted_devices_frame, text="Geräte:", background=self.sidebar_color,
foreground=self.color_foreground).pack(fill="x", padx=10, pady=(5, 0))
for device_name, mount_point, removable in self._get_mounted_devices():
icon = self.icons['usb_small'] if removable else self.icons['device_small']
button_text = f" {device_name}"
if len(device_name) > 15: # Static wrapping for long names
button_text = f" {device_name[:15]}\n{device_name[15:]}"
btn = ttk.Button(mounted_devices_frame, text=button_text, image=icon,
compound="left", command=lambda p=mount_point: self.navigate_to(p), style="Dark.TButton.Borderless")
btn.pack(fill="x", pady=1)
try:
total, used, _ = shutil.disk_usage(mount_point)
progress_bar = ttk.Progressbar(
mounted_devices_frame, orient="horizontal", length=100, mode="determinate", style='Small.Horizontal.TProgressbar')
progress_bar.pack(fill="x", pady=(2, 8), padx=25)
progress_bar['value'] = (used / total) * 100
except (FileNotFoundError, PermissionError):
# In case of errors (e.g., unreadable drive), just skip the progress bar
pass
tk.Frame(sidebar_frame, height=1, bg=separator_color).grid(
row=3, column=0, sticky="ew", padx=20, pady=15)
storage_frame = ttk.Frame(sidebar_frame, style="Sidebar.TFrame")
storage_frame.grid(row=4, column=0, sticky="ew", padx=10)
self.storage_label = ttk.Label(
storage_frame, text="Freier Speicher:", background=self.freespace_background)
self.storage_label.pack(fill="x", padx=10)
self.storage_bar = ttk.Progressbar(
storage_frame, orient="horizontal", length=100, mode="determinate")
self.storage_bar.pack(fill="x", pady=(2, 5), padx=15)
# Content area
content_frame = ttk.Frame(paned_window, padding=(
0, 0, 0, 0), style="AccentBottom.TFrame")
paned_window.add(content_frame, weight=1)
content_frame.grid_rowconfigure(0, weight=1)
content_frame.grid_columnconfigure(0, weight=1)
self.file_list_frame = ttk.Frame(
content_frame, style="AccentBottom.TFrame")
self.file_list_frame.grid(row=0, column=0, sticky="nsew")
self.bind("<Configure>", self.on_window_resize)
# Bottom controls
bottom_controls_frame = ttk.Frame(
content_frame, style="AccentBottom.TFrame")
bottom_controls_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0))
bottom_controls_frame.grid_columnconfigure(1, weight=1)
self.status_bar = ttk.Label(
bottom_controls_frame, text="", anchor="w", style="AccentBottom.TLabel")
self.status_bar.grid(row=0, column=1, columnspan=2,
sticky="ew", padx=10, pady=10)
ttk.Button(bottom_controls_frame, text="Öffnen",
command=self.on_open).grid(row=0, column=0, padx=10)
ttk.Button(bottom_controls_frame, text="Abbrechen",
command=self.on_cancel).grid(row=1, column=0, padx=10)
self.filter_combobox = ttk.Combobox(
bottom_controls_frame, values=[ft[0] for ft in self.filetypes], state="readonly")
self.filter_combobox.grid(
row=1, column=1, sticky="w", padx=10, pady=(5, 10))
self.filter_combobox.bind(
"<<ComboboxSelected>>", self.on_filter_change)
self.filter_combobox.set(self.filetypes[0][0])
def toggle_hidden_files(self):
self.show_hidden_files.set(not self.show_hidden_files.get())
if self.show_hidden_files.get():
self.hidden_files_button.config(image=self.icons['unhide'])
Tooltip(self.hidden_files_button, "Versteckte Dateien ausblenden")
else:
self.hidden_files_button.config(image=self.icons['hide'])
Tooltip(self.hidden_files_button, "Versteckte Dateien anzeigen")
self.populate_files()
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 _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):
# Unbind previous global mouse wheel events
self._unbind_mouse_wheel_events()
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 _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, 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")
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")))
def _on_mouse_wheel(event):
# Determine the scroll direction and magnitude
if event.num == 4: # Scroll up on Linux
delta = -1
elif event.num == 5: # Scroll down on Linux
delta = 1
else: # MouseWheel event for Windows/macOS
delta = -1 * int(event.delta / 120)
canvas.yview_scroll(delta, "units")
# Bind mouse wheel events to the canvas and the container frame
for widget in [canvas, container_frame]:
widget.bind("<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 = 125, 100
frame_width = self.file_list_frame.winfo_width()
col_count = max(1, frame_width // item_width - 1)
row, col = 0, 0
for name in items:
if not self.show_hidden_files.get() and name.startswith('.'):
continue
path = os.path.join(self.current_dir, name)
is_dir = os.path.isdir(path)
if not is_dir and not self._matches_filetype(name):
continue
item_frame = ttk.Frame(
container_frame, width=item_width, height=item_height, style="Item.TFrame")
item_frame.grid(row=row, column=col, padx=5, ipadx=25, pady=5)
item_frame.grid_propagate(False)
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, 14), anchor="center", style="Item.TLabel")
name_label.pack(fill="x", expand=True)
Tooltip(item_frame, name)
# Bind events to all individual widgets so scrolling works everywhere
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)
tree_frame.grid_rowconfigure(0, weight=1)
tree_frame.grid_columnconfigure(0, weight=1)
columns = ("size", "type", "modified")
self.tree = ttk.Treeview(
tree_frame, columns=columns, show="tree headings")
# Tree Column (#0)
self.tree.heading("#0", text="Name", anchor="w")
self.tree.column("#0", anchor="w", width=250, stretch=True)
# Other Columns
self.tree.heading("size", text="Größe", anchor="e")
self.tree.column("size", anchor="e", width=120, stretch=False)
self.tree.heading("type", text="Typ", anchor="w")
self.tree.column("type", anchor="w", width=120, stretch=False)
self.tree.heading("modified", text="Geändert am", anchor="w")
self.tree.column("modified", anchor="w", width=160, stretch=False)
v_scrollbar = ttk.Scrollbar(
tree_frame, orient="vertical", command=self.tree.yview)
h_scrollbar = ttk.Scrollbar(
tree_frame, orient="horizontal", command=self.tree.xview)
self.tree.configure(yscrollcommand=v_scrollbar.set,
xscrollcommand=h_scrollbar.set)
self.tree.grid(row=0, column=0, sticky='nsew')
v_scrollbar.grid(row=0, column=1, sticky='ns')
h_scrollbar.grid(row=1, column=0, sticky='ew')
self.tree.bind("<Double-1>", self.on_list_double_click)
self.tree.bind("<<TreeviewSelect>>", self.on_list_select)
items, error, warning = self._get_sorted_items()
if warning:
self.status_bar.config(text=warning)
if error:
self.tree.insert("", "end", text=error, values=())
return
for name in items:
if not self.show_hidden_files.get() and name.startswith('.'):
continue
path = os.path.join(self.current_dir, name)
is_dir = os.path.isdir(path)
if not is_dir and not self._matches_filetype(name):
continue
try:
stat = os.stat(path)
modified_time = datetime.fromtimestamp(
stat.st_mtime).strftime('%d.%m.%Y %H:%M')
if is_dir:
icon, file_type, size = self.icons['folder_small'], "Ordner", ""
else:
icon, file_type, size = self.get_file_icon(
name, 'small'), "Datei", self._format_size(stat.st_size)
self.tree.insert("", "end", text=f" {name}", image=icon, values=(
size, file_type, modified_time))
except (FileNotFoundError, PermissionError):
continue
def on_item_select(self, path, item_frame):
if hasattr(self, 'selected_item_frame') and self.selected_item_frame.winfo_exists():
self.selected_item_frame.state(['!selected'])
for child in self.selected_item_frame.winfo_children():
if isinstance(child, ttk.Label):
child.state(['!selected'])
item_frame.state(['selected'])
for child in item_frame.winfo_children():
if isinstance(child, ttk.Label):
child.state(['selected'])
self.selected_item_frame = item_frame
self.selected_file = path
self.update_status_bar()
def on_list_select(self, event):
if not self.tree.selection():
return
item_id = self.tree.selection()[0]
item_text = self.tree.item(item_id, 'text').strip()
self.selected_file = os.path.join(self.current_dir, item_text)
self.update_status_bar()
def _handle_unsupported_file(self, path):
if path.lower().endswith('.svg'):
self.status_bar.config(
text="SVG-Dateien werden nicht unterstützt.")
return True
return False
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()
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 self._handle_unsupported_file(path):
return
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):
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
if not os.access(real_path, os.R_OK):
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 not self.history or self.history[-1] != self.current_dir:
self.history.append(self.current_dir)
self.history_pos = len(self.history) - 1
self.populate_files()
self.update_nav_buttons()
self.update_status_bar()
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.update_status_bar()
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()
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:
total, used, free = shutil.disk_usage(self.current_dir)
free_str = self._format_size(free)
self.storage_label.config(text=f"Freier Speicher: {free_str}")
self.storage_bar['value'] = (used / total) * 100
status_text = ""
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"'{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")
self.storage_label.config(text="Freier Speicher: Unbekannt")
self.storage_bar['value'] = 0
def on_open(self):
if self.selected_file and os.path.isfile(self.selected_file):
if self._handle_unsupported_file(self.selected_file):
return
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
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')
# size = block_device.get('size')
removable = block_device.get('rm', False)
display_name = label if label else name
# if size:
# display_name += f" ({size})"
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')
# size = child_device.get('size')
removable = child_device.get('rm', False)
display_name = label if label else name
# if size:
# display_name += f" ({size})"
devices.append(
(display_name, mountpoint, removable))
except Exception as e:
print(f"Error getting mounted devices: {e}")
return devices