- Gelöschte Einträge werden direkt aus dem Model entfernt statt Verzeichnis neu zu laden - Verzeichnis-Cache wird vor dem Einlesen invalidiert (os.listdir statt os.scandir) - Selektion wird beim Refresh/Löschen zurückgesetzt - PDF Preview: Darkmode Toggle hinzugefügt Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
729 lines
26 KiB
Python
729 lines
26 KiB
Python
"""Dateiliste-Widget mit natürlicher Sortierung und Drag & Drop."""
|
||
|
||
import os
|
||
import subprocess
|
||
from pathlib import Path
|
||
from datetime import datetime
|
||
from PyQt6.QtWidgets import (
|
||
QTableView, QAbstractItemView, QMenu, QHeaderView, QMessageBox
|
||
)
|
||
from PyQt6.QtCore import (
|
||
Qt, QModelIndex, pyqtSignal, QAbstractTableModel, QMimeData,
|
||
QUrl
|
||
)
|
||
from PyQt6.QtGui import QAction, QDrag, QColor, QBrush
|
||
|
||
from ..utils.file_utils import get_file_icon, format_file_size, natural_sort_key
|
||
from ..utils.clipboard import FileClipboard
|
||
from ..utils.archive import ArchiveHandler
|
||
|
||
|
||
|
||
|
||
class FileItem:
|
||
"""Repräsentiert einen Dateieintrag."""
|
||
|
||
def __init__(self, name: str, path: str, is_dir: bool = False,
|
||
size: int = 0, modified: datetime = None):
|
||
self.name = name
|
||
self.path = path
|
||
self.is_dir = is_dir
|
||
self.size = size
|
||
self.modified = modified or datetime.now()
|
||
self.icon = get_file_icon(name, is_dir)
|
||
|
||
|
||
class FileListModel(QAbstractTableModel):
|
||
"""Model für die Dateiliste."""
|
||
|
||
HEADERS = ['📄', 'Name', 'Größe', 'Geändert']
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.items: list[FileItem] = []
|
||
self.current_path = ""
|
||
self._hovered_row = -1
|
||
self._hover_color = QColor("#334155") # Default, wird vom Theme überschrieben
|
||
|
||
def rowCount(self, parent=None):
|
||
return len(self.items)
|
||
|
||
def columnCount(self, parent=None):
|
||
return len(self.HEADERS)
|
||
|
||
def data(self, index: QModelIndex, role=Qt.ItemDataRole.DisplayRole):
|
||
if not index.isValid() or index.row() >= len(self.items):
|
||
return None
|
||
|
||
item = self.items[index.row()]
|
||
col = index.column()
|
||
|
||
if role == Qt.ItemDataRole.DisplayRole:
|
||
if col == 0:
|
||
return item.icon
|
||
elif col == 1:
|
||
return item.name
|
||
elif col == 2:
|
||
return "" if item.is_dir else format_file_size(item.size)
|
||
elif col == 3:
|
||
return item.modified.strftime("%d.%m.%Y %H:%M")
|
||
elif role == Qt.ItemDataRole.UserRole:
|
||
return item
|
||
elif role == Qt.ItemDataRole.TextAlignmentRole:
|
||
if col == 0:
|
||
return Qt.AlignmentFlag.AlignCenter
|
||
elif col == 2:
|
||
return Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||
elif role == Qt.ItemDataRole.ToolTipRole:
|
||
return item.path
|
||
|
||
return None
|
||
|
||
def set_hovered_row(self, row: int):
|
||
"""Setzt die aktuell gehoverte Zeile."""
|
||
if self._hovered_row != row:
|
||
old_row = self._hovered_row
|
||
self._hovered_row = row
|
||
# Alte und neue Zeile aktualisieren
|
||
if old_row >= 0 and old_row < len(self.items):
|
||
self.dataChanged.emit(
|
||
self.index(old_row, 0),
|
||
self.index(old_row, self.columnCount() - 1)
|
||
)
|
||
if row >= 0 and row < len(self.items):
|
||
self.dataChanged.emit(
|
||
self.index(row, 0),
|
||
self.index(row, self.columnCount() - 1)
|
||
)
|
||
|
||
def set_hover_color(self, color: QColor):
|
||
"""Setzt die Hover-Farbe."""
|
||
self._hover_color = color
|
||
|
||
def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
|
||
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
|
||
return self.HEADERS[section]
|
||
return None
|
||
|
||
def set_path(self, path: str):
|
||
"""Lädt den Inhalt eines Ordners."""
|
||
self.beginResetModel()
|
||
self.items.clear()
|
||
self.current_path = path
|
||
|
||
if not os.path.exists(path):
|
||
self.endResetModel()
|
||
return
|
||
|
||
try:
|
||
# Verzeichnis-Cache invalidieren
|
||
try:
|
||
fd = os.open(path, os.O_RDONLY | os.O_DIRECTORY)
|
||
os.close(fd)
|
||
except OSError:
|
||
pass
|
||
|
||
folders = []
|
||
files = []
|
||
|
||
for name in os.listdir(path):
|
||
entry_path = os.path.join(path, name)
|
||
try:
|
||
stat = os.stat(entry_path)
|
||
is_dir = os.path.isdir(entry_path)
|
||
item = FileItem(
|
||
name=name,
|
||
path=entry_path,
|
||
is_dir=is_dir,
|
||
size=stat.st_size if not is_dir else 0,
|
||
modified=datetime.fromtimestamp(stat.st_mtime)
|
||
)
|
||
if is_dir:
|
||
folders.append(item)
|
||
else:
|
||
files.append(item)
|
||
except (PermissionError, OSError, FileNotFoundError):
|
||
continue
|
||
|
||
# Natürliche Sortierung
|
||
folders.sort(key=lambda x: natural_sort_key(x.name))
|
||
files.sort(key=lambda x: natural_sort_key(x.name))
|
||
|
||
self.items = folders + files
|
||
|
||
except PermissionError:
|
||
pass
|
||
|
||
self.endResetModel()
|
||
|
||
def get_item(self, index: QModelIndex) -> FileItem:
|
||
"""Gibt das FileItem für einen Index zurück."""
|
||
if index.isValid() and index.row() < len(self.items):
|
||
return self.items[index.row()]
|
||
return None
|
||
|
||
def refresh(self):
|
||
"""Aktualisiert die Dateiliste."""
|
||
if self.current_path:
|
||
self.set_path(self.current_path)
|
||
|
||
def remove_paths(self, paths: list[str]):
|
||
"""Entfernt Einträge mit den angegebenen Pfaden aus dem Model."""
|
||
paths_set = set(paths)
|
||
# Finde Indizes der zu löschenden Items (von hinten nach vorne)
|
||
indices_to_remove = [i for i, item in enumerate(self.items) if item.path in paths_set]
|
||
indices_to_remove.sort(reverse=True)
|
||
|
||
for idx in indices_to_remove:
|
||
self.beginRemoveRows(QModelIndex(), idx, idx)
|
||
del self.items[idx]
|
||
self.endRemoveRows()
|
||
|
||
|
||
class FileListWidget(QTableView):
|
||
"""Dateiliste mit Kontextmenü und Drag & Drop."""
|
||
|
||
file_selected = pyqtSignal(str, str) # path, name
|
||
file_double_clicked = pyqtSignal(str) # path
|
||
folder_entered = pyqtSignal(str) # path
|
||
file_rename_requested = pyqtSignal(str) # path
|
||
file_delete_requested = pyqtSignal(str) # path (einzelne Datei)
|
||
files_delete_requested = pyqtSignal(object) # paths (mehrere Dateien)
|
||
file_move_requested = pyqtSignal(str) # path
|
||
files_dropped = pyqtSignal(list, str) # source_paths, target_folder
|
||
|
||
# Neue Signale für erweiterte Funktionen
|
||
new_file_requested = pyqtSignal()
|
||
new_folder_requested = pyqtSignal()
|
||
copy_requested = pyqtSignal(list) # paths
|
||
cut_requested = pyqtSignal(list) # paths
|
||
paste_requested = pyqtSignal()
|
||
archive_requested = pyqtSignal(list) # paths
|
||
extract_requested = pyqtSignal(str) # path
|
||
properties_requested = pyqtSignal(list) # paths
|
||
open_terminal_requested = pyqtSignal(str) # path
|
||
refresh_requested = pyqtSignal()
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
|
||
self.list_model = FileListModel(self)
|
||
self.setModel(self.list_model)
|
||
self.clipboard = FileClipboard.instance()
|
||
|
||
# Einstellungen
|
||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||
self.setShowGrid(False)
|
||
self.setAlternatingRowColors(False)
|
||
self.verticalHeader().setVisible(False)
|
||
self.setWordWrap(False)
|
||
self.setMouseTracking(True)
|
||
|
||
# Spaltenbreiten
|
||
header = self.horizontalHeader()
|
||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
|
||
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed)
|
||
self.setColumnWidth(0, 36)
|
||
self.setColumnWidth(2, 80)
|
||
self.setColumnWidth(3, 130)
|
||
|
||
# Drag & Drop
|
||
self.setDragEnabled(True)
|
||
self.setAcceptDrops(True)
|
||
self.setDropIndicatorShown(True)
|
||
self.setDragDropMode(QAbstractItemView.DragDropMode.DragDrop)
|
||
|
||
# Signale
|
||
self.clicked.connect(self._on_clicked)
|
||
self.doubleClicked.connect(self._on_double_clicked)
|
||
|
||
# Kontextmenü
|
||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||
self.customContextMenuRequested.connect(self._show_context_menu)
|
||
|
||
def set_path(self, path: str):
|
||
"""Lädt einen Ordner."""
|
||
self.list_model.set_path(path)
|
||
|
||
def _on_clicked(self, index: QModelIndex):
|
||
"""Behandelt Klicks auf Dateien."""
|
||
item = self.list_model.get_item(index)
|
||
if item and not item.is_dir:
|
||
self.file_selected.emit(item.path, item.name)
|
||
|
||
def _on_double_clicked(self, index: QModelIndex):
|
||
"""Behandelt Doppelklicks."""
|
||
item = self.list_model.get_item(index)
|
||
if item:
|
||
if item.is_dir:
|
||
self.folder_entered.emit(item.path)
|
||
else:
|
||
self.file_double_clicked.emit(item.path)
|
||
|
||
def _show_context_menu(self, position):
|
||
"""Zeigt das Kontextmenü."""
|
||
indexes = self.selectedIndexes()
|
||
|
||
# Eindeutige Zeilen ermitteln
|
||
rows = set(idx.row() for idx in indexes)
|
||
items = [self.list_model.items[row] for row in rows if row < len(self.list_model.items)]
|
||
|
||
menu = QMenu(self)
|
||
|
||
if not items:
|
||
# Kontextmenü für leeren Bereich
|
||
self._build_empty_context_menu(menu)
|
||
elif len(items) == 1:
|
||
# Kontextmenü für einzelnes Element
|
||
self._build_single_item_context_menu(menu, items[0])
|
||
else:
|
||
# Kontextmenü für mehrere Elemente
|
||
self._build_multi_item_context_menu(menu, items)
|
||
|
||
menu.exec(self.viewport().mapToGlobal(position))
|
||
|
||
def _build_empty_context_menu(self, menu: QMenu):
|
||
"""Erstellt Kontextmenü für leeren Bereich."""
|
||
# Neu-Untermenü
|
||
new_menu = menu.addMenu("📄 Neu")
|
||
|
||
new_file_action = QAction("📄 Neue Datei...", self)
|
||
new_file_action.setShortcut("Ctrl+N")
|
||
new_file_action.triggered.connect(self.new_file_requested.emit)
|
||
new_menu.addAction(new_file_action)
|
||
|
||
new_folder_action = QAction("📁 Neuer Ordner...", self)
|
||
new_folder_action.setShortcut("Ctrl+Shift+N")
|
||
new_folder_action.triggered.connect(self.new_folder_requested.emit)
|
||
new_menu.addAction(new_folder_action)
|
||
|
||
menu.addSeparator()
|
||
|
||
# Einfügen
|
||
paste_action = QAction("📋 Einfügen", self)
|
||
paste_action.setShortcut("Ctrl+V")
|
||
paste_action.setEnabled(self.clipboard.has_files())
|
||
if self.clipboard.has_files():
|
||
count = self.clipboard.get_file_count()
|
||
paste_action.setText(f"📋 Einfügen ({count} Element{'e' if count > 1 else ''})")
|
||
paste_action.triggered.connect(self.paste_requested.emit)
|
||
menu.addAction(paste_action)
|
||
|
||
menu.addSeparator()
|
||
|
||
# Aktualisieren
|
||
refresh_action = QAction("🔄 Aktualisieren", self)
|
||
refresh_action.setShortcut("F5")
|
||
refresh_action.triggered.connect(self.refresh_requested.emit)
|
||
menu.addAction(refresh_action)
|
||
|
||
menu.addSeparator()
|
||
|
||
# Terminal öffnen
|
||
terminal_action = QAction("💻 Terminal hier öffnen", self)
|
||
terminal_action.triggered.connect(
|
||
lambda: self.open_terminal_requested.emit(self.list_model.current_path)
|
||
)
|
||
menu.addAction(terminal_action)
|
||
|
||
menu.addSeparator()
|
||
|
||
# Eigenschaften des Ordners
|
||
props_action = QAction("ℹ️ Eigenschaften", self)
|
||
props_action.triggered.connect(
|
||
lambda: self.properties_requested.emit([self.list_model.current_path])
|
||
)
|
||
menu.addAction(props_action)
|
||
|
||
def _build_single_item_context_menu(self, menu: QMenu, item: FileItem):
|
||
"""Erstellt Kontextmenü für einzelnes Element."""
|
||
# Öffnen
|
||
if item.is_dir:
|
||
open_action = QAction("📂 Öffnen", self)
|
||
open_action.triggered.connect(lambda: self.folder_entered.emit(item.path))
|
||
menu.addAction(open_action)
|
||
|
||
open_new_action = QAction("📂 In neuem Fenster öffnen", self)
|
||
open_new_action.triggered.connect(lambda: self._open_in_new_window(item.path))
|
||
menu.addAction(open_new_action)
|
||
else:
|
||
open_action = QAction("🔗 Öffnen", self)
|
||
open_action.triggered.connect(lambda: self._open_external(item.path))
|
||
menu.addAction(open_action)
|
||
|
||
open_with_action = QAction("🔗 Öffnen mit...", self)
|
||
open_with_action.triggered.connect(lambda: self._open_with(item.path))
|
||
menu.addAction(open_with_action)
|
||
|
||
menu.addSeparator()
|
||
|
||
# Neu-Untermenü (nur in Ordnern sinnvoll)
|
||
new_menu = menu.addMenu("📄 Neu")
|
||
|
||
new_file_action = QAction("📄 Neue Datei...", self)
|
||
new_file_action.triggered.connect(self.new_file_requested.emit)
|
||
new_menu.addAction(new_file_action)
|
||
|
||
new_folder_action = QAction("📁 Neuer Ordner...", self)
|
||
new_folder_action.triggered.connect(self.new_folder_requested.emit)
|
||
new_menu.addAction(new_folder_action)
|
||
|
||
menu.addSeparator()
|
||
|
||
# Ausschneiden, Kopieren
|
||
cut_action = QAction("✂️ Ausschneiden", self)
|
||
cut_action.setShortcut("Ctrl+X")
|
||
cut_action.triggered.connect(lambda: self.cut_requested.emit([item.path]))
|
||
menu.addAction(cut_action)
|
||
|
||
copy_action = QAction("📋 Kopieren", self)
|
||
copy_action.setShortcut("Ctrl+C")
|
||
copy_action.triggered.connect(lambda: self.copy_requested.emit([item.path]))
|
||
menu.addAction(copy_action)
|
||
|
||
# Einfügen (nur für Ordner)
|
||
if item.is_dir:
|
||
paste_action = QAction("📋 Einfügen", self)
|
||
paste_action.setShortcut("Ctrl+V")
|
||
paste_action.setEnabled(self.clipboard.has_files())
|
||
paste_action.triggered.connect(self.paste_requested.emit)
|
||
menu.addAction(paste_action)
|
||
|
||
menu.addSeparator()
|
||
|
||
# Umbenennen
|
||
rename_action = QAction("✏️ Umbenennen", self)
|
||
rename_action.setShortcut("F2")
|
||
rename_action.triggered.connect(lambda: self.file_rename_requested.emit(item.path))
|
||
menu.addAction(rename_action)
|
||
|
||
# Verschieben
|
||
move_action = QAction("📦 Verschieben nach...", self)
|
||
move_action.triggered.connect(lambda: self.file_move_requested.emit(item.path))
|
||
menu.addAction(move_action)
|
||
|
||
# Kopieren nach...
|
||
copy_to_action = QAction("📋 Kopieren nach...", self)
|
||
copy_to_action.triggered.connect(lambda: self._copy_to(item.path))
|
||
menu.addAction(copy_to_action)
|
||
|
||
menu.addSeparator()
|
||
|
||
# Archiv-Funktionen
|
||
if ArchiveHandler.is_archive(item.path):
|
||
# Entpacken
|
||
extract_action = QAction("📦 Hier entpacken", self)
|
||
extract_action.triggered.connect(lambda: self._extract_here(item.path))
|
||
menu.addAction(extract_action)
|
||
|
||
extract_to_action = QAction("📦 Entpacken nach...", self)
|
||
extract_to_action.triggered.connect(lambda: self.extract_requested.emit(item.path))
|
||
menu.addAction(extract_to_action)
|
||
|
||
menu.addSeparator()
|
||
else:
|
||
# Packen
|
||
archive_action = QAction("📦 Zu Archiv packen...", self)
|
||
archive_action.triggered.connect(lambda: self.archive_requested.emit([item.path]))
|
||
menu.addAction(archive_action)
|
||
|
||
menu.addSeparator()
|
||
|
||
# Löschen
|
||
delete_action = QAction("🗑 Löschen", self)
|
||
delete_action.setShortcut("Delete")
|
||
delete_action.triggered.connect(lambda: self._delete_items([item]))
|
||
menu.addAction(delete_action)
|
||
|
||
menu.addSeparator()
|
||
|
||
# Terminal öffnen (für Ordner)
|
||
if item.is_dir:
|
||
terminal_action = QAction("💻 Terminal hier öffnen", self)
|
||
terminal_action.triggered.connect(
|
||
lambda: self.open_terminal_requested.emit(item.path)
|
||
)
|
||
menu.addAction(terminal_action)
|
||
menu.addSeparator()
|
||
|
||
# Eigenschaften
|
||
props_action = QAction("ℹ️ Eigenschaften", self)
|
||
props_action.triggered.connect(lambda: self.properties_requested.emit([item.path]))
|
||
menu.addAction(props_action)
|
||
|
||
menu.addSeparator()
|
||
|
||
# Aktualisieren
|
||
refresh_action = QAction("🔄 Aktualisieren", self)
|
||
refresh_action.setShortcut("F5")
|
||
refresh_action.triggered.connect(self.refresh_requested.emit)
|
||
menu.addAction(refresh_action)
|
||
|
||
def _build_multi_item_context_menu(self, menu: QMenu, items: list[FileItem]):
|
||
"""Erstellt Kontextmenü für mehrere Elemente."""
|
||
paths = [item.path for item in items]
|
||
|
||
# Ausschneiden, Kopieren
|
||
cut_action = QAction(f"✂️ Ausschneiden ({len(items)} Elemente)", self)
|
||
cut_action.setShortcut("Ctrl+X")
|
||
cut_action.triggered.connect(lambda: self.cut_requested.emit(paths))
|
||
menu.addAction(cut_action)
|
||
|
||
copy_action = QAction(f"📋 Kopieren ({len(items)} Elemente)", self)
|
||
copy_action.setShortcut("Ctrl+C")
|
||
copy_action.triggered.connect(lambda: self.copy_requested.emit(paths))
|
||
menu.addAction(copy_action)
|
||
|
||
menu.addSeparator()
|
||
|
||
# Archiv erstellen
|
||
archive_action = QAction(f"📦 Zu Archiv packen ({len(items)} Elemente)...", self)
|
||
archive_action.triggered.connect(lambda: self.archive_requested.emit(paths))
|
||
menu.addAction(archive_action)
|
||
|
||
menu.addSeparator()
|
||
|
||
# Löschen
|
||
delete_action = QAction(f"🗑 Löschen ({len(items)} Elemente)", self)
|
||
delete_action.setShortcut("Delete")
|
||
delete_action.triggered.connect(lambda: self._delete_items(items))
|
||
menu.addAction(delete_action)
|
||
|
||
menu.addSeparator()
|
||
|
||
# Eigenschaften
|
||
props_action = QAction(f"ℹ️ Eigenschaften ({len(items)} Elemente)", self)
|
||
props_action.triggered.connect(lambda: self.properties_requested.emit(paths))
|
||
menu.addAction(props_action)
|
||
|
||
menu.addSeparator()
|
||
|
||
# Aktualisieren
|
||
refresh_action = QAction("🔄 Aktualisieren", self)
|
||
refresh_action.setShortcut("F5")
|
||
refresh_action.triggered.connect(self.refresh_requested.emit)
|
||
menu.addAction(refresh_action)
|
||
|
||
def _open_external(self, path: str):
|
||
"""Öffnet eine Datei mit der Standard-Anwendung."""
|
||
try:
|
||
subprocess.Popen(['xdg-open', path])
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "Fehler", f"Konnte Datei nicht öffnen: {e}")
|
||
|
||
def _open_with(self, path: str):
|
||
"""Öffnet den 'Öffnen mit'-Dialog."""
|
||
try:
|
||
# xdg-mime kann zum Öffnen mit Auswahl verwendet werden
|
||
subprocess.Popen(['xdg-open', '--', path])
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "Fehler", f"Konnte Dialog nicht öffnen: {e}")
|
||
|
||
def _open_in_new_window(self, path: str):
|
||
"""Öffnet einen Ordner in einem neuen Fenster."""
|
||
try:
|
||
subprocess.Popen(['xdg-open', path])
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "Fehler", f"Konnte Fenster nicht öffnen: {e}")
|
||
|
||
def _copy_to(self, path: str):
|
||
"""Kopiert Datei/Ordner an einen ausgewählten Ort."""
|
||
from ..dialogs import MoveDialog
|
||
dialog = MoveDialog(path, self)
|
||
dialog.setWindowTitle("Kopieren nach")
|
||
if dialog.exec():
|
||
import shutil
|
||
target = dialog.get_target_folder()
|
||
try:
|
||
name = os.path.basename(path)
|
||
new_path = os.path.join(target, name)
|
||
if os.path.isdir(path):
|
||
shutil.copytree(path, new_path)
|
||
else:
|
||
shutil.copy2(path, new_path)
|
||
self.refresh_requested.emit()
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "Fehler", f"Kopieren fehlgeschlagen: {e}")
|
||
|
||
def _extract_here(self, path: str):
|
||
"""Entpackt ein Archiv im aktuellen Ordner."""
|
||
target_folder = self.list_model.current_path
|
||
success, message = ArchiveHandler.extract(path, target_folder, create_subfolder=True)
|
||
if success:
|
||
self.refresh_requested.emit()
|
||
else:
|
||
QMessageBox.critical(self, "Fehler", message)
|
||
|
||
def _delete_items(self, items: list):
|
||
"""Löscht mehrere Elemente."""
|
||
paths = [item.path for item in items]
|
||
self.files_delete_requested.emit(paths)
|
||
|
||
def get_selected_items(self) -> list[FileItem]:
|
||
"""Gibt die ausgewählten Elemente zurück."""
|
||
indexes = self.selectedIndexes()
|
||
rows = set(idx.row() for idx in indexes)
|
||
return [self.list_model.items[row] for row in rows if row < len(self.list_model.items)]
|
||
|
||
def get_selected_path(self) -> str:
|
||
"""Gibt den Pfad des ersten ausgewählten Elements zurück."""
|
||
items = self.get_selected_items()
|
||
return items[0].path if items else ""
|
||
|
||
def get_selected_paths(self) -> list[str]:
|
||
"""Gibt die Pfade aller ausgewählten Elemente zurück."""
|
||
return [item.path for item in self.get_selected_items()]
|
||
|
||
def refresh(self):
|
||
"""Aktualisiert die Dateiliste."""
|
||
self.clearSelection()
|
||
self.list_model.refresh()
|
||
|
||
def remove_paths(self, paths: list[str]):
|
||
"""Entfernt Einträge mit den angegebenen Pfaden aus der Liste."""
|
||
self.clearSelection()
|
||
self.list_model.remove_paths(paths)
|
||
|
||
# Drag & Drop
|
||
def startDrag(self, supportedActions):
|
||
"""Startet einen Drag-Vorgang."""
|
||
items = self.get_selected_items()
|
||
if not items:
|
||
return
|
||
|
||
drag = QDrag(self)
|
||
mime_data = QMimeData()
|
||
|
||
urls = [QUrl.fromLocalFile(item.path) for item in items]
|
||
mime_data.setUrls(urls)
|
||
|
||
drag.setMimeData(mime_data)
|
||
drag.exec(Qt.DropAction.MoveAction)
|
||
|
||
def dragEnterEvent(self, event):
|
||
"""Akzeptiert Drag-Events mit URLs."""
|
||
if event.mimeData().hasUrls():
|
||
event.acceptProposedAction()
|
||
else:
|
||
event.ignore()
|
||
|
||
def dragMoveEvent(self, event):
|
||
"""Markiert Zielordner beim Drag."""
|
||
if event.mimeData().hasUrls():
|
||
index = self.indexAt(event.position().toPoint())
|
||
if index.isValid():
|
||
item = self.list_model.get_item(index)
|
||
if item and item.is_dir:
|
||
event.acceptProposedAction()
|
||
return
|
||
event.acceptProposedAction()
|
||
else:
|
||
event.ignore()
|
||
|
||
def dropEvent(self, event):
|
||
"""Verarbeitet Drop-Events."""
|
||
if not event.mimeData().hasUrls():
|
||
event.ignore()
|
||
return
|
||
|
||
urls = event.mimeData().urls()
|
||
source_paths = [url.toLocalFile() for url in urls]
|
||
|
||
# Zielordner ermitteln
|
||
index = self.indexAt(event.position().toPoint())
|
||
if index.isValid():
|
||
item = self.list_model.get_item(index)
|
||
if item and item.is_dir:
|
||
target_folder = item.path
|
||
else:
|
||
target_folder = self.list_model.current_path
|
||
else:
|
||
target_folder = self.list_model.current_path
|
||
|
||
if source_paths and target_folder:
|
||
self.files_dropped.emit(source_paths, target_folder)
|
||
|
||
event.acceptProposedAction()
|
||
|
||
def keyPressEvent(self, event):
|
||
"""Behandelt Tastatureingaben."""
|
||
# Ctrl+C - Kopieren
|
||
if event.key() == Qt.Key.Key_C and event.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
||
items = self.get_selected_items()
|
||
if items:
|
||
self.copy_requested.emit([item.path for item in items])
|
||
return
|
||
|
||
# Ctrl+X - Ausschneiden
|
||
if event.key() == Qt.Key.Key_X and event.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
||
items = self.get_selected_items()
|
||
if items:
|
||
self.cut_requested.emit([item.path for item in items])
|
||
return
|
||
|
||
# Ctrl+V - Einfügen
|
||
if event.key() == Qt.Key.Key_V and event.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
||
self.paste_requested.emit()
|
||
return
|
||
|
||
# Ctrl+A - Alles auswählen
|
||
if event.key() == Qt.Key.Key_A and event.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
||
self.selectAll()
|
||
return
|
||
|
||
# F2 - Umbenennen
|
||
if event.key() == Qt.Key.Key_F2:
|
||
items = self.get_selected_items()
|
||
if len(items) == 1:
|
||
self.file_rename_requested.emit(items[0].path)
|
||
return
|
||
|
||
# Delete - Löschen
|
||
if event.key() == Qt.Key.Key_Delete:
|
||
items = self.get_selected_items()
|
||
if items:
|
||
self._delete_items(items)
|
||
return
|
||
|
||
# Enter - Öffnen
|
||
if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
|
||
items = self.get_selected_items()
|
||
if len(items) == 1:
|
||
if items[0].is_dir:
|
||
self.folder_entered.emit(items[0].path)
|
||
else:
|
||
self._open_external(items[0].path)
|
||
return
|
||
|
||
# Backspace - Zum übergeordneten Ordner
|
||
if event.key() == Qt.Key.Key_Backspace:
|
||
parent = os.path.dirname(self.list_model.current_path)
|
||
if parent and parent != self.list_model.current_path:
|
||
self.folder_entered.emit(parent)
|
||
return
|
||
|
||
# F5 - Aktualisieren
|
||
if event.key() == Qt.Key.Key_F5:
|
||
self.refresh_requested.emit()
|
||
return
|
||
|
||
super().keyPressEvent(event)
|
||
|
||
def mouseMoveEvent(self, event):
|
||
"""Verfolgt die Zeile unter dem Mauszeiger für Hover-Effekt."""
|
||
index = self.indexAt(event.pos())
|
||
row = index.row() if index.isValid() else -1
|
||
self.list_model.set_hovered_row(row)
|
||
super().mouseMoveEvent(event)
|
||
|
||
def leaveEvent(self, event):
|
||
"""Entfernt den Hover-Effekt wenn die Maus das Widget verlässt."""
|
||
self.list_model.set_hovered_row(-1)
|
||
super().leaveEvent(event)
|
||
|
||
def set_hover_color(self, color: str):
|
||
"""Setzt die Hover-Farbe (für Theme-Unterstützung)."""
|
||
self.list_model.set_hover_color(QColor(color))
|