939 lines
40 KiB
Python
939 lines
40 KiB
Python
import os
|
|
import shutil
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
from datetime import datetime
|
|
import subprocess
|
|
import json
|
|
from shared_libs.message import MessageDialog
|
|
from shared_libs.common_tools import IconManager, Tooltip, ConfigManager, LxTools, ThemeManager
|
|
from cfd_app_config import AppConfig
|
|
from cfd_ui_setup import StyleManager, WidgetManager, get_xdg_user_dir
|
|
|
|
|
|
# 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
|
|
|
|
|
|
class CustomFileDialog(tk.Toplevel):
|
|
def __init__(self, parent, initial_dir=None, filetypes=None, dialog_mode="open"):
|
|
super().__init__(parent)
|
|
|
|
self.my_tool_tip = None
|
|
self.dialog_mode = dialog_mode
|
|
|
|
self.x_width = AppConfig.UI_CONFIG["window_size"][0]
|
|
self.y_height = AppConfig.UI_CONFIG["window_size"][1]
|
|
# Set the window size
|
|
self.geometry(f"{self.x_width}x{self.y_height}")
|
|
self.minsize(AppConfig.UI_CONFIG["window_size"][0],
|
|
AppConfig.UI_CONFIG["window_size"][1],
|
|
)
|
|
self.title(AppConfig.UI_CONFIG["window_title"])
|
|
# self.tk.call(
|
|
# "source", f"{AppConfig.SYSTEM_PATHS['tcl_path']}/water.tcl")
|
|
# ConfigManager.init(AppConfig.SETTINGS_FILE)
|
|
# theme = ConfigManager.get("theme")
|
|
# ThemeManager.change_theme(self, theme)
|
|
self.image = IconManager()
|
|
LxTools.center_window_cross_platform(self, self.x_width, self.y_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="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.icon_manager = IconManager()
|
|
self.style_manager = StyleManager(self)
|
|
self.widget_manager = WidgetManager(self)
|
|
|
|
self.navigate_to(self.current_dir)
|
|
|
|
def get_file_icon(self, filename, size='large'):
|
|
ext = os.path.splitext(filename)[1].lower()
|
|
if ext == '.svg':
|
|
return self.icon_manager.get_icon(f'warning_{size}')
|
|
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']:
|
|
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):
|
|
new_width = self.widget_manager.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 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_search_mode(self):
|
|
"""Toggle between search mode and normal mode"""
|
|
if not self.search_mode:
|
|
# Enter search mode
|
|
self.search_mode = True
|
|
self.original_path_text = self.widget_manager.path_entry.get()
|
|
self.widget_manager.path_entry.delete(0, tk.END)
|
|
self.widget_manager.path_entry.insert(0, "Suchbegriff eingeben...")
|
|
self.widget_manager.path_entry.bind(
|
|
"<Return>", self.execute_search)
|
|
self.widget_manager.path_entry.bind(
|
|
"<FocusIn>", self.clear_search_placeholder)
|
|
|
|
# Show search options
|
|
self.widget_manager.search_options_frame.pack(
|
|
side="left", padx=(5, 0))
|
|
else:
|
|
# Exit search mode
|
|
self.search_mode = False
|
|
self.widget_manager.path_entry.delete(0, tk.END)
|
|
self.widget_manager.path_entry.insert(0, self.original_path_text)
|
|
self.widget_manager.path_entry.bind(
|
|
"<Return>", lambda e: self.navigate_to(self.widget_manager.path_entry.get()))
|
|
self.widget_manager.path_entry.unbind("<FocusIn>")
|
|
|
|
# Hide search options
|
|
self.widget_manager.search_options_frame.pack_forget()
|
|
|
|
# Return to normal file view
|
|
self.populate_files()
|
|
|
|
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 set_icon_view(self):
|
|
"""Set icon view and update button styles"""
|
|
self.view_mode.set("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")
|
|
self.populate_files()
|
|
|
|
def set_list_view(self):
|
|
"""Set list view and update button styles"""
|
|
self.view_mode.set("list")
|
|
self.widget_manager.list_view_button.configure(
|
|
style="Header.TButton.Active.Round")
|
|
self.widget_manager.icon_view_button.configure(
|
|
style="Header.TButton.Borderless.Round")
|
|
self.populate_files()
|
|
|
|
def clear_search_placeholder(self, event):
|
|
"""Clear placeholder text when focus enters search field"""
|
|
if self.widget_manager.path_entry.get() == "Suchbegriff eingeben...":
|
|
self.widget_manager.path_entry.delete(0, tk.END)
|
|
|
|
def execute_search(self, event):
|
|
"""Execute search when Enter is pressed in search mode"""
|
|
search_term = self.widget_manager.path_entry.get().strip()
|
|
if not search_term or search_term == "Suchbegriff eingeben...":
|
|
return
|
|
|
|
# Clear previous search results
|
|
self.search_results.clear()
|
|
|
|
# Determine search directories
|
|
search_dirs = [self.current_dir]
|
|
|
|
# If searching from home directory, also include XDG directories
|
|
home_dir = os.path.expanduser("~")
|
|
if os.path.abspath(self.current_dir) == os.path.abspath(home_dir):
|
|
xdg_dirs = [
|
|
get_xdg_user_dir("XDG_DOWNLOAD_DIR", "Downloads"),
|
|
get_xdg_user_dir("XDG_DOCUMENTS_DIR", "Documents"),
|
|
get_xdg_user_dir("XDG_PICTURES_DIR", "Pictures"),
|
|
get_xdg_user_dir("XDG_MUSIC_DIR", "Music"),
|
|
get_xdg_user_dir("XDG_VIDEO_DIR", "Videos")
|
|
]
|
|
# Add XDG directories that exist and are not already in home
|
|
for xdg_dir in xdg_dirs:
|
|
if (os.path.exists(xdg_dir) and
|
|
os.path.abspath(xdg_dir) != os.path.abspath(home_dir) and
|
|
xdg_dir not in search_dirs):
|
|
search_dirs.append(xdg_dir)
|
|
|
|
try:
|
|
all_files = []
|
|
|
|
# Search in each directory
|
|
for search_dir in search_dirs:
|
|
if not os.path.exists(search_dir):
|
|
continue
|
|
|
|
# Change to directory and use relative paths to avoid path issues
|
|
original_cwd = os.getcwd()
|
|
try:
|
|
os.chdir(search_dir)
|
|
|
|
# Build find command based on recursive setting (use . for current directory)
|
|
if self.widget_manager.recursive_search.get():
|
|
find_cmd = ['find', '.', '-iname',
|
|
f'*{search_term}*', '-type', 'f']
|
|
else:
|
|
find_cmd = ['find', '.', '-maxdepth', '1',
|
|
'-iname', f'*{search_term}*', '-type', 'f']
|
|
|
|
result = subprocess.run(
|
|
find_cmd, capture_output=True, text=True, timeout=30)
|
|
|
|
if result.returncode == 0:
|
|
files = result.stdout.strip().split('\n')
|
|
# Convert relative paths back to absolute paths
|
|
directory_files = []
|
|
for f in files:
|
|
if f and f.startswith('./'):
|
|
abs_path = os.path.join(
|
|
search_dir, f[2:]) # Remove './' prefix
|
|
if os.path.isfile(abs_path):
|
|
directory_files.append(abs_path)
|
|
all_files.extend(directory_files)
|
|
|
|
finally:
|
|
os.chdir(original_cwd)
|
|
|
|
# Remove duplicates while preserving order
|
|
seen = set()
|
|
unique_files = []
|
|
for file_path in all_files:
|
|
if file_path not in seen:
|
|
seen.add(file_path)
|
|
unique_files.append(file_path)
|
|
|
|
# Filter based on currently selected filter pattern
|
|
self.search_results = []
|
|
for file_path in unique_files:
|
|
filename = os.path.basename(file_path)
|
|
if self._matches_filetype(filename):
|
|
self.search_results.append(file_path)
|
|
|
|
# Show search results in TreeView
|
|
if self.search_results:
|
|
self.show_search_results_treeview()
|
|
else:
|
|
MessageDialog(
|
|
message_type="info",
|
|
text=f"Keine Dateien mit '{search_term}' gefunden.",
|
|
title="Suche",
|
|
master=self
|
|
).show()
|
|
|
|
except subprocess.TimeoutExpired:
|
|
MessageDialog(
|
|
message_type="error",
|
|
text="Suche dauert zu lange und wurde abgebrochen.",
|
|
title="Suche",
|
|
master=self
|
|
).show()
|
|
except Exception as e:
|
|
MessageDialog(
|
|
message_type="error",
|
|
text=f"Fehler bei der Suche: {e}",
|
|
title="Suchfehler",
|
|
master=self
|
|
).show()
|
|
|
|
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')
|
|
|
|
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 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]
|
|
full_path = os.path.join(directory, filename)
|
|
|
|
# Select the file and close dialog
|
|
self.selected_file = full_path
|
|
self.destroy()
|
|
|
|
search_tree.bind("<Double-1>", on_search_double_click)
|
|
|
|
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):
|
|
# 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)
|
|
else:
|
|
self.populate_icon_view(item_to_rename)
|
|
|
|
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 populate_icon_view(self, item_to_rename=None):
|
|
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=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.widget_manager.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.widget_manager.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)
|
|
|
|
if name == item_to_rename:
|
|
self.start_rename(item_frame, path)
|
|
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)
|
|
# 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)
|
|
widget.bind("<F2>", lambda e, p=path,
|
|
f=item_frame: self.on_rename_request(e, p, f))
|
|
|
|
col = (col + 1) % col_count
|
|
if col == 0:
|
|
row += 1
|
|
|
|
def populate_list_view(self, item_to_rename=None):
|
|
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")
|
|
|
|
# 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)
|
|
self.tree.bind("<F2>", self.on_rename_request)
|
|
|
|
items, error, warning = self._get_sorted_items()
|
|
if warning:
|
|
self.widget_manager.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.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.start_rename(item_id, path)
|
|
|
|
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()
|
|
if self.dialog_mode == "save" and not os.path.isdir(path):
|
|
self.widget_manager.path_entry.delete(0, tk.END)
|
|
self.widget_manager.path_entry.insert(0, 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()
|
|
self.selected_file = os.path.join(self.current_dir, item_text)
|
|
self.update_status_bar()
|
|
if self.dialog_mode == "save" and not os.path.isdir(self.selected_file):
|
|
self.widget_manager.path_entry.delete(0, tk.END)
|
|
self.widget_manager.path_entry.insert(0, self.selected_file)
|
|
|
|
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 _handle_unsupported_file(self, path):
|
|
if path.lower().endswith('.svg'):
|
|
self.widget_manager.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.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):
|
|
try:
|
|
real_path = os.path.realpath(
|
|
os.path.abspath(os.path.expanduser(path)))
|
|
if not os.path.isdir(real_path):
|
|
self.widget_manager.status_bar.config(
|
|
text=f"Fehler: Verzeichnis '{os.path.basename(path)}' nicht gefunden.")
|
|
return
|
|
if not os.access(real_path, os.R_OK):
|
|
self.widget_manager.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()
|
|
self.update_action_buttons_state()
|
|
except Exception as e:
|
|
self.widget_manager.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.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):
|
|
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 self.dialog_mode == "open" and 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.widget_manager.status_bar.config(text=status_text)
|
|
except FileNotFoundError:
|
|
self.widget_manager.status_bar.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):
|
|
if self._handle_unsupported_file(self.selected_file):
|
|
return
|
|
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 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.status_bar.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 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="both", 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.status_bar.config(
|
|
text=f"'{new_name}' existiert bereits.")
|
|
self.populate_files()
|
|
return
|
|
try:
|
|
os.rename(item_path, new_path)
|
|
except Exception as e:
|
|
self.widget_manager.status_bar.config(
|
|
text=f"Fehler beim Umbenennen: {e}")
|
|
self.populate_files()
|
|
|
|
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):
|
|
x, y, width, height = self.tree.bbox(item_id, column="#0")
|
|
entry = ttk.Entry(self.tree)
|
|
entry.place(x=x, y=y, width=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.status_bar.config(
|
|
text=f"'{new_name}' existiert bereits.")
|
|
else:
|
|
try:
|
|
os.rename(old_path, new_path)
|
|
except Exception as e:
|
|
self.widget_manager.status_bar.config(
|
|
text=f"Fehler beim Umbenennen: {e}")
|
|
entry.destroy()
|
|
self.populate_files()
|
|
|
|
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 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
|