Files
shared_libs/cfd_ui_setup.py
2025-08-05 10:14:09 +02:00

492 lines
25 KiB
Python

import os
import shutil
import tkinter as tk
from tkinter import ttk
from shared_libs.common_tools import Tooltip
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 StyleManager:
def __init__(self, dialog):
self.dialog = dialog
self.setup_styles()
def setup_styles(self):
style = ttk.Style(self.dialog)
base_bg = self.dialog.cget('background')
self.is_dark = sum(self.dialog.winfo_rgb(base_bg)) / 3 < 32768
if self.is_dark:
self.selection_color = "#4a6984"
self.icon_bg_color = "#3c3c3c"
self.accent_color = "#2a2a2a"
self.header = "#2b2b2b"
self.hover_extrastyle = "#4a4a4a"
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"
self.icon_bg_color = base_bg
self.accent_color = "#e0e0e0"
self.header = "#d9d9d9"
self.hover_extrastyle = "#f5f5f5"
self.hover_extrastyle2 = "#494949"
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("Header.TButton.Active.Round",
background=self.selection_color)
style.layout("Header.TButton.Active.Round",
style.layout("Header.TButton.Borderless.Round"))
style.map("Header.TButton.Active.Round", background=[
('active', self.selection_color)])
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)
style.configure("Bottom.TButton.Borderless.Round",
background=self.bottom_color)
style.map("Bottom.TButton.Borderless.Round",
background=[('active', self.hover_extrastyle)])
style.layout("Bottom.TButton.Borderless.Round",
style.layout("Header.TButton.Borderless.Round")
)
class WidgetManager:
def __init__(self, dialog, settings):
self.dialog = dialog
self.style_manager = dialog.style_manager
self.settings = settings
self.setup_widgets()
def setup_widgets(self):
# Main container
main_frame = ttk.Frame(self.dialog, 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")
# Left navigation buttons
left_nav_container = ttk.Frame(top_bar, style='Accent.TFrame')
left_nav_container.grid(row=0, column=0, sticky="w")
# Prevent this container from changing size
left_nav_container.grid_propagate(False)
self.back_button = ttk.Button(left_nav_container, image=self.dialog.icon_manager.get_icon(
'back'), command=self.dialog.go_back, state=tk.DISABLED, style="Header.TButton.Borderless.Round")
self.back_button.pack(side="left", padx=(10, 5))
Tooltip(self.back_button, "Zurück")
self.forward_button = ttk.Button(left_nav_container, image=self.dialog.icon_manager.get_icon(
'forward'), command=self.dialog.go_forward, state=tk.DISABLED, style="Header.TButton.Borderless.Round")
self.forward_button.pack(side="left", padx=5)
Tooltip(self.forward_button, "Vorwärts")
self.up_button = ttk.Button(left_nav_container, image=self.dialog.icon_manager.get_icon(
'up'), command=self.dialog.go_up_level, style="Header.TButton.Borderless.Round")
self.up_button.pack(side="left", padx=5)
Tooltip(self.up_button, "Eine Ebene höher")
self.home_button = ttk.Button(left_nav_container, image=self.dialog.icon_manager.get_icon(
'home'), command=lambda: self.dialog.navigate_to(os.path.expanduser("~")), style="Header.TButton.Borderless.Round")
self.home_button.pack(side="left", padx=(5, 10))
Tooltip(self.home_button, "Home")
# Path and search widgets container
path_search_container = ttk.Frame(top_bar, style='Accent.TFrame')
path_search_container.grid(row=0, column=1, sticky="ew")
# Right-side controls container
right_controls_container = ttk.Frame(top_bar, style='Accent.TFrame')
right_controls_container.grid(row=0, column=2, sticky="e")
# Make the middle column (path_search_container) expand
top_bar.grid_columnconfigure(1, weight=1)
search_icon_pos = self.settings.get("search_icon_pos", "left")
self.recursive_search = tk.BooleanVar(value=True)
# Path entry
self.path_entry = ttk.Entry(path_search_container)
self.path_entry.bind(
"<Return>", lambda e: self.dialog.navigate_to(self.path_entry.get()))
# Function to create search widgets
def create_search_widgets(parent_frame):
container = ttk.Frame(parent_frame, style='Accent.TFrame')
self.search_button = ttk.Button(container, image=self.dialog.icon_manager.get_icon(
'search_small'), command=self.dialog.toggle_search_mode, style="Header.TButton.Borderless.Round")
self.search_button.pack(side="left")
Tooltip(self.search_button, "Suchen")
self.recursive_button = ttk.Button(container, image=self.dialog.icon_manager.get_icon(
'recursive_small'), command=self.dialog.toggle_recursive_search, style="Header.TButton.Active.Round")
self.recursive_button.pack(side="left", padx=2)
self.recursive_button.pack_forget() # Initially hidden
Tooltip(self.recursive_button, "Rekursive Suche ein/ausschalten")
return container
# Place search and path entry based on settings
if search_icon_pos == 'left':
path_search_container.grid_columnconfigure(1, weight=1)
search_container = create_search_widgets(path_search_container)
search_container.grid(row=0, column=0, sticky="w", padx=(0, 5))
self.path_entry.grid(row=0, column=1, sticky="ew")
else: # right
path_search_container.grid_columnconfigure(0, weight=1)
search_container = create_search_widgets(path_search_container)
search_container.grid(row=0, column=1, sticky="e", padx=(5, 0))
self.path_entry.grid(row=0, column=0, sticky="ew")
# --- Responsive Buttons ---
self.responsive_buttons_container = ttk.Frame(
right_controls_container, style='Accent.TFrame')
self.responsive_buttons_container.pack(side="left")
self.new_folder_button = ttk.Button(self.responsive_buttons_container, image=self.dialog.icon_manager.get_icon(
'new_folder_small'), command=self.dialog.create_new_folder, style="Header.TButton.Borderless.Round")
self.new_folder_button.pack(side="left", padx=5)
Tooltip(self.new_folder_button, "Neuen Ordner erstellen")
self.new_file_button = ttk.Button(self.responsive_buttons_container, image=self.dialog.icon_manager.get_icon(
'new_document_small'), command=self.dialog.create_new_file, style="Header.TButton.Borderless.Round")
self.new_file_button.pack(side="left", padx=5)
Tooltip(self.new_file_button, "Neues Dokument erstellen")
if self.dialog.dialog_mode == "open":
self.new_folder_button.config(state=tk.DISABLED)
self.new_file_button.config(state=tk.DISABLED)
self.view_switch = ttk.Frame(self.responsive_buttons_container,
padding=(5, 0), style='Accent.TFrame')
self.view_switch.pack(side="left")
self.icon_view_button = ttk.Button(self.view_switch, image=self.dialog.icon_manager.get_icon(
'icon_view'), command=self.dialog.set_icon_view, style="Header.TButton.Active.Round")
self.icon_view_button.pack(side="left", padx=5)
Tooltip(self.icon_view_button, "Kachelansicht")
self.list_view_button = ttk.Button(self.view_switch, image=self.dialog.icon_manager.get_icon(
'list_view'), command=self.dialog.set_list_view, style="Header.TButton.Borderless.Round")
self.list_view_button.pack(side="left")
Tooltip(self.list_view_button, "Listenansicht")
self.hidden_files_button = ttk.Button(self.responsive_buttons_container, image=self.dialog.icon_manager.get_icon(
'hide'), command=self.dialog.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")
# "More" button for responsive UI
self.more_button = ttk.Button(right_controls_container, text="...",
command=self.dialog.show_more_menu, style="Header.TButton.Borderless.Round", width=3)
# self.more_button is managed by _handle_responsive_buttons
# Horizontal separator
separator_color = "#000000" if self.style_manager.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, style="Sidebar.TFrame")
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, 0), width=200)
sidebar_frame.grid_propagate(False)
sidebar_frame.bind("<Configure>", self.dialog.on_sidebar_resize)
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.dialog.icon_manager.get_icon(
'computer_small'), 'path': '/'},
{'name': 'Downloads', 'icon': self.dialog.icon_manager.get_icon(
'downloads_small'), 'path': get_xdg_user_dir("XDG_DOWNLOAD_DIR", "Downloads")},
{'name': 'Dokumente', 'icon': self.dialog.icon_manager.get_icon(
'documents_small'), 'path': get_xdg_user_dir("XDG_DOCUMENTS_DIR", "Documents")},
{'name': 'Bilder', 'icon': self.dialog.icon_manager.get_icon(
'pictures_small'), 'path': get_xdg_user_dir("XDG_PICTURES_DIR", "Pictures")},
{'name': 'Musik', 'icon': self.dialog.icon_manager.get_icon(
'music_small'), 'path': get_xdg_user_dir("XDG_MUSIC_DIR", "Music")},
{'name': 'Videos', 'icon': self.dialog.icon_manager.get_icon(
'video_small'), 'path': get_xdg_user_dir("XDG_VIDEO_DIR", "Videos")},
]
self.sidebar_buttons = []
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.dialog.navigate_to(p), style="Dark.TButton.Borderless")
btn.pack(fill="x", pady=1)
self.sidebar_buttons.append((btn, f" {config['name']}"))
separator_color = "#a9a9a9" if self.style_manager.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_frame = ttk.Frame(
sidebar_frame, style="Sidebar.TFrame")
mounted_devices_frame.grid(row=2, column=0, sticky="nsew", padx=10)
mounted_devices_frame.grid_columnconfigure(0, weight=1)
ttk.Label(mounted_devices_frame, text="Geräte:", background=self.style_manager.sidebar_color,
foreground=self.style_manager.color_foreground).grid(row=0, column=0, sticky="ew", padx=10, pady=(5, 0))
self.devices_canvas = tk.Canvas(
mounted_devices_frame, highlightthickness=0, bg=self.style_manager.sidebar_color, height=150, width=180)
self.devices_scrollbar = ttk.Scrollbar(
mounted_devices_frame, orient="vertical", command=self.devices_canvas.yview)
self.devices_canvas.configure(
yscrollcommand=self.devices_scrollbar.set)
self.devices_canvas.grid(row=1, column=0, sticky="nsew")
self.devices_scrollable_frame = ttk.Frame(
self.devices_canvas, style="Sidebar.TFrame")
self.devices_canvas_window = self.devices_canvas.create_window(
(0, 0), window=self.devices_scrollable_frame, anchor="nw")
self.devices_canvas.bind("<Enter>", self.dialog._on_devices_enter)
self.devices_canvas.bind("<Leave>", self.dialog._on_devices_leave)
self.devices_scrollable_frame.bind(
"<Enter>", self.dialog._on_devices_enter)
self.devices_scrollable_frame.bind(
"<Leave>", self.dialog._on_devices_leave)
def _configure_devices_canvas(event):
self.devices_canvas.configure(
scrollregion=self.devices_canvas.bbox("all"))
canvas_width = event.width
self.devices_canvas.itemconfig(
self.devices_canvas_window, width=canvas_width)
self.devices_scrollable_frame.bind("<Configure>", lambda e: self.devices_canvas.configure(
scrollregion=self.devices_canvas.bbox("all")))
self.devices_canvas.bind("<Configure>", _configure_devices_canvas)
def _on_devices_mouse_wheel(event):
if event.num == 4:
delta = -1
elif event.num == 5:
delta = 1
else:
delta = -1 * int(event.delta / 120)
self.devices_canvas.yview_scroll(delta, "units")
for widget in [self.devices_canvas, self.devices_scrollable_frame]:
widget.bind("<MouseWheel>", _on_devices_mouse_wheel)
widget.bind("<Button-4>", _on_devices_mouse_wheel)
widget.bind("<Button-5>", _on_devices_mouse_wheel)
self.device_buttons = []
for device_name, mount_point, removable in self.dialog._get_mounted_devices():
icon = self.dialog.icon_manager.get_icon(
'usb_small') if removable else self.dialog.icon_manager.get_icon('device_small')
button_text = f" {device_name}"
if len(device_name) > 15:
button_text = f" {device_name[:15]}\n{device_name[15:]}"
btn = ttk.Button(self.devices_scrollable_frame, text=button_text, image=icon, compound="left",
command=lambda p=mount_point: self.dialog.navigate_to(p), style="Dark.TButton.Borderless")
btn.pack(fill="x", pady=1)
self.device_buttons.append((btn, button_text))
for w in [btn, self.devices_canvas, self.devices_scrollable_frame]:
w.bind("<MouseWheel>", _on_devices_mouse_wheel)
w.bind("<Button-4>", _on_devices_mouse_wheel)
w.bind("<Button-5>", _on_devices_mouse_wheel)
w.bind("<Enter>", self.dialog._on_devices_enter)
w.bind("<Leave>", self.dialog._on_devices_leave)
try:
total, used, _ = shutil.disk_usage(mount_point)
progress_bar = ttk.Progressbar(self.devices_scrollable_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
for w in [progress_bar]:
w.bind("<MouseWheel>", _on_devices_mouse_wheel)
w.bind("<Button-4>", _on_devices_mouse_wheel)
w.bind("<Button-5>", _on_devices_mouse_wheel)
w.bind("<Enter>", self.dialog._on_devices_enter)
w.bind("<Leave>", self.dialog._on_devices_leave)
except (FileNotFoundError, PermissionError):
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=5, column=0, sticky="sew", padx=10, pady=10)
self.storage_label = ttk.Label(
storage_frame, text="Freier Speicher:", background=self.style_manager.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_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(
# Use Content.TFrame for consistent bg color
content_frame, style="Content.TFrame")
self.file_list_frame.grid(row=0, column=0, sticky="nsew")
self.dialog.bind("<Configure>", self.dialog.on_window_resize)
# This frame will contain the action buttons and status bar
self.action_status_frame = ttk.Frame(
content_frame, style="AccentBottom.TFrame")
self.action_status_frame.grid(
row=1, column=0, sticky="ew", pady=(5, 10), padx=10)
button_box_pos = self.settings.get("button_box_pos", "left")
# Configure columns for the action_status_frame
if button_box_pos == 'left':
self.action_status_frame.grid_columnconfigure(1, weight=1)
else:
self.action_status_frame.grid_columnconfigure(1, weight=1)
# Status bar will be placed inside the action_status_frame
self.status_bar = ttk.Label(
self.action_status_frame, text="", anchor="w", style="AccentBottom.TLabel")
self.settings_button = ttk.Button(self.action_status_frame, image=self.dialog.icon_manager.get_icon(
'settings-2_small'), command=self.dialog.open_settings_dialog, style="Bottom.TButton.Borderless.Round")
self.trash_button = ttk.Button(self.action_status_frame, image=self.dialog.icon_manager.get_icon(
'trash_small2'), command=self.dialog.delete_selected_item, style="Bottom.TButton.Borderless.Round")
Tooltip(self.trash_button, "Ausgewähltes Element löschen/verschieben")
if self.dialog.dialog_mode == "save":
self.filename_entry = ttk.Entry(self.action_status_frame)
save_button = ttk.Button(
self.action_status_frame, text="Speichern", command=self.dialog.on_save)
cancel_button = ttk.Button(
self.action_status_frame, text="Abbrechen", command=self.dialog.on_cancel)
self.filter_combobox = ttk.Combobox(self.action_status_frame, values=[
ft[0] for ft in self.dialog.filetypes], state="readonly")
if button_box_pos == 'left':
save_button.grid(row=0, column=0, sticky="w", padx=(0, 10))
self.filename_entry.grid(
row=0, column=1, sticky="ew", padx=(0, 5))
cancel_button.grid(row=1, column=0, sticky="w",
pady=(5, 0), padx=(0, 10))
self.filter_combobox.grid(
row=1, column=1, sticky="w", pady=(5, 0), padx=(0, 5))
self.settings_button.grid(row=0, column=3, sticky="e")
self.trash_button.grid(
row=1, column=3, sticky="se", padx=(5, 0))
else: # right
self.trash_button.grid(
row=1, column=0, sticky="sw", padx=(0, 5))
self.filename_entry.grid(
row=0, column=1, sticky="ew", padx=(0, 5))
self.filter_combobox.grid(
row=1, column=1, sticky="e", pady=(5, 0), padx=(0, 5))
save_button.grid(row=0, column=2, sticky="e", padx=5)
cancel_button.grid(row=1, column=2, sticky="e",
padx=5, pady=(5, 0))
self.settings_button.grid(row=0, column=3, sticky="e")
self.filter_combobox.bind(
"<<ComboboxSelected>>", self.dialog.on_filter_change)
self.filter_combobox.set(self.dialog.filetypes[0][0])
else: # Open mode
open_button = ttk.Button(
self.action_status_frame, text="Öffnen", command=self.dialog.on_open)
cancel_button = ttk.Button(
self.action_status_frame, text="Abbrechen", command=self.dialog.on_cancel)
self.filter_combobox = ttk.Combobox(self.action_status_frame, values=[
ft[0] for ft in self.dialog.filetypes], state="readonly")
if button_box_pos == 'left':
open_button.grid(row=0, column=0, sticky="w", padx=(0, 5))
self.status_bar.grid(row=0, column=1, sticky="w", padx=5)
cancel_button.grid(row=1, column=0, sticky="w", pady=(5, 0))
self.filter_combobox.grid(
row=1, column=1, sticky="w", pady=(5, 0), padx=(5, 0))
self.settings_button.grid(row=0, column=2, sticky="e")
else: # right
self.status_bar.grid(row=0, column=0, sticky="e", padx=10)
open_button.grid(row=0, column=1, sticky="e", padx=5)
cancel_button.grid(row=1, column=1, sticky="e",
padx=5, pady=(5, 0))
self.filter_combobox.grid(
row=1, column=0, sticky="e", pady=(5, 0))
self.settings_button.grid(row=0, column=2, sticky="e")
self.filter_combobox.bind(
"<<ComboboxSelected>>", self.dialog.on_filter_change)
self.filter_combobox.set(self.dialog.filetypes[0][0])