diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e7ae0cd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,92 @@
+# FileBrowser
+
+Ein moderner Dateimanager mit Vorschau-Funktion, geschrieben in Python mit PyQt6.
+
+## Version
+
+**v1.1.0** - Vollständiges Rechtsklick-Kontextmenü
+
+## Features
+
+### Dateioperationen
+- **Kopieren/Ausschneiden/Einfügen** (Ctrl+C, Ctrl+X, Ctrl+V)
+- **Neue Datei erstellen** (Ctrl+N) - mit Vorlagen für Python, JS, HTML, CSS, JSON, Markdown, Shell
+- **Neuer Ordner** (Ctrl+Shift+N)
+- **Umbenennen** (F2)
+- **Löschen** (Delete)
+- **Verschieben/Kopieren nach...**
+- **Drag & Drop** Unterstützung
+
+### Archiv-Funktionen
+- **Packen:** ZIP, TAR, TAR.GZ, TAR.BZ2, TAR.XZ
+- **Entpacken:** ZIP, TAR, TAR.GZ, TAR.BZ2, TAR.XZ (+ 7z, RAR wenn installiert)
+
+### Vorschau
+- PDF-Dokumente (mit Zoom und Mehrseiten-Ansicht)
+- Bilder (PNG, JPG, GIF, etc.)
+- Markdown (gerendert)
+- Text und Code-Dateien
+- Vorschau im Panel oder abgetrenntem Fenster
+
+### Weitere Funktionen
+- **Terminal öffnen** (F4)
+- **Eigenschaften-Dialog** (Alt+Enter) - Dateigröße, Berechtigungen, Zeitstempel
+- **Mehrere Themes** - Dark, Breeze Dark, Breeze Light, System
+- **Breadcrumb-Navigation** mit editierbarem Pfad
+
+## Tastenkürzel
+
+| Kürzel | Funktion |
+|--------|----------|
+| Ctrl+N | Neue Datei |
+| Ctrl+Shift+N | Neuer Ordner |
+| Ctrl+C | Kopieren |
+| Ctrl+X | Ausschneiden |
+| Ctrl+V | Einfügen |
+| Ctrl+A | Alles auswählen |
+| F2 | Umbenennen |
+| F4 | Terminal öffnen |
+| F5 | Aktualisieren |
+| Delete | Löschen |
+| Alt+Enter | Eigenschaften |
+| Backspace | Ordner nach oben |
+
+## Installation
+
+```bash
+# Abhängigkeiten installieren
+pip install -r requirements.txt
+
+# Starten
+python main.py
+```
+
+## Abhängigkeiten
+
+- Python 3.10+
+- PyQt6
+- PyMuPDF (fitz) - für PDF-Vorschau
+- Markdown - für Markdown-Rendering
+
+## Rechtsklick-Kontextmenü
+
+### Bei Datei/Ordner-Auswahl:
+- Öffnen / Öffnen mit...
+- Neu → Neue Datei / Neuer Ordner
+- Ausschneiden / Kopieren
+- Umbenennen
+- Verschieben nach... / Kopieren nach...
+- Zu Archiv packen... (oder Entpacken bei Archiven)
+- Löschen
+- Eigenschaften
+
+### Bei leerem Bereich:
+- Neu → Neue Datei / Neuer Ordner
+- Einfügen
+- Aktualisieren
+- Terminal hier öffnen
+- Eigenschaften
+
+## Lizenz
+
+MIT License
diff --git a/filebrowser.desktop b/filebrowser.desktop
index 9e81f66..492bda4 100644
--- a/filebrowser.desktop
+++ b/filebrowser.desktop
@@ -1,3 +1,4 @@
+#!/usr/bin/env xdg-open
[Desktop Entry]
Name=FileBrowser
Comment=Dateimanager mit Vorschau-Funktion
diff --git a/main.py b/main.py
index 1b40e2d..c53b5bd 100644
--- a/main.py
+++ b/main.py
@@ -1,6 +1,8 @@
#!/usr/bin/env python3
"""FileBrowser - Ein Dateimanager mit Vorschau-Funktion in PyQt6."""
+__version__ = "1.1.0"
+
import sys
import os
from PyQt6.QtWidgets import QApplication
diff --git a/src/dialogs/__init__.py b/src/dialogs/__init__.py
index 89fb131..2c09b0d 100644
--- a/src/dialogs/__init__.py
+++ b/src/dialogs/__init__.py
@@ -1,7 +1,11 @@
"""Dialoge für den FileBrowser."""
-from .base import RenameDialog, MoveDialog, DeleteDialog, SettingsDialog
+from .base import (
+ RenameDialog, MoveDialog, DeleteDialog, SettingsDialog,
+ NewFileDialog, PropertiesDialog, ArchiveDialog, ExtractDialog
+)
__all__ = [
- 'RenameDialog', 'MoveDialog', 'DeleteDialog', 'SettingsDialog'
+ 'RenameDialog', 'MoveDialog', 'DeleteDialog', 'SettingsDialog',
+ 'NewFileDialog', 'PropertiesDialog', 'ArchiveDialog', 'ExtractDialog'
]
diff --git a/src/dialogs/base.py b/src/dialogs/base.py
index f8b6d3d..4421174 100644
--- a/src/dialogs/base.py
+++ b/src/dialogs/base.py
@@ -1,15 +1,21 @@
"""Dialoge für Dateioperationen."""
import os
+import stat
from pathlib import Path
+from datetime import datetime
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QTreeView, QDialogButtonBox, QMessageBox,
- QFrame, QCheckBox, QGroupBox, QTabWidget, QWidget
+ QFrame, QCheckBox, QGroupBox, QTabWidget, QWidget,
+ QFormLayout, QComboBox, QRadioButton, QButtonGroup,
+ QProgressBar, QListWidget, QListWidgetItem
)
from PyQt6.QtCore import Qt, QDir, QSettings
from PyQt6.QtGui import QFileSystemModel
+from ..utils.file_utils import format_file_size
+
class RenameDialog(QDialog):
"""Dialog zum Umbenennen von Dateien."""
@@ -338,3 +344,517 @@ class SettingsDialog(QDialog):
'disable_preview_panel': self.disable_preview_check.isChecked(),
'path_text_default': self.path_text_default_check.isChecked(),
}
+
+
+class NewFileDialog(QDialog):
+ """Dialog zum Erstellen einer neuen Datei."""
+
+ def __init__(self, current_folder: str, parent=None):
+ super().__init__(parent)
+ self.current_folder = current_folder
+ self.file_name = ""
+
+ self.setWindowTitle("Neue Datei")
+ self.setModal(True)
+ self.setMinimumWidth(400)
+
+ self._setup_ui()
+
+ def _setup_ui(self):
+ layout = QVBoxLayout(self)
+
+ # Dateiname
+ layout.addWidget(QLabel("Dateiname:"))
+
+ self.name_input = QLineEdit()
+ self.name_input.setPlaceholderText("datei.txt")
+ self.name_input.returnPressed.connect(self.accept)
+ layout.addWidget(self.name_input)
+
+ # Vorlagen
+ layout.addWidget(QLabel("Vorlage:"))
+
+ self.template_combo = QComboBox()
+ self.template_combo.addItem("Leere Datei", "")
+ self.template_combo.addItem("Python-Datei (.py)", ".py")
+ self.template_combo.addItem("JavaScript (.js)", ".js")
+ self.template_combo.addItem("HTML-Datei (.html)", ".html")
+ self.template_combo.addItem("CSS-Datei (.css)", ".css")
+ self.template_combo.addItem("JSON-Datei (.json)", ".json")
+ self.template_combo.addItem("Markdown (.md)", ".md")
+ self.template_combo.addItem("Text-Datei (.txt)", ".txt")
+ self.template_combo.addItem("Shell-Script (.sh)", ".sh")
+ self.template_combo.currentIndexChanged.connect(self._on_template_changed)
+ layout.addWidget(self.template_combo)
+
+ # Buttons
+ button_box = QDialogButtonBox(
+ QDialogButtonBox.StandardButton.Ok |
+ QDialogButtonBox.StandardButton.Cancel
+ )
+ button_box.accepted.connect(self.accept)
+ button_box.rejected.connect(self.reject)
+ layout.addWidget(button_box)
+
+ def _on_template_changed(self, index):
+ """Aktualisiert die Erweiterung basierend auf der Vorlage."""
+ extension = self.template_combo.currentData()
+ if extension:
+ current_name = self.name_input.text()
+ if current_name:
+ # Erweiterung ersetzen
+ base = Path(current_name).stem
+ self.name_input.setText(base + extension)
+ else:
+ self.name_input.setText("datei" + extension)
+
+ def accept(self):
+ """Validiert und erstellt die Datei."""
+ self.file_name = self.name_input.text().strip()
+
+ if not self.file_name:
+ QMessageBox.warning(self, "Fehler", "Dateiname darf nicht leer sein.")
+ return
+
+ if '/' in self.file_name or '\\' in self.file_name:
+ QMessageBox.warning(self, "Fehler", "Dateiname darf keine Pfadtrennzeichen enthalten.")
+ return
+
+ file_path = os.path.join(self.current_folder, self.file_name)
+ if os.path.exists(file_path):
+ QMessageBox.warning(self, "Fehler", "Eine Datei mit diesem Namen existiert bereits.")
+ return
+
+ super().accept()
+
+ def get_file_name(self) -> str:
+ """Gibt den Dateinamen zurück."""
+ return self.file_name
+
+ def get_template_content(self) -> str:
+ """Gibt den Template-Inhalt zurück."""
+ extension = self.template_combo.currentData()
+ templates = {
+ ".py": '#!/usr/bin/env python3\n"""Beschreibung."""\n\n\ndef main():\n pass\n\n\nif __name__ == "__main__":\n main()\n',
+ ".js": '// Beschreibung\n\n"use strict";\n\n',
+ ".html": '\n\n
\n \n \n Titel\n\n\n \n\n\n',
+ ".css": '/* Stylesheet */\n\n* {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n}\n',
+ ".json": '{\n \n}\n',
+ ".md": '# Titel\n\n',
+ ".sh": '#!/bin/bash\n\n',
+ }
+ return templates.get(extension, "")
+
+
+class PropertiesDialog(QDialog):
+ """Dialog für Datei-/Ordner-Eigenschaften."""
+
+ def __init__(self, paths: list[str], parent=None):
+ super().__init__(parent)
+ self.paths = paths
+
+ self.setWindowTitle("Eigenschaften")
+ self.setModal(True)
+ self.setMinimumWidth(400)
+
+ self._setup_ui()
+ self._load_properties()
+
+ def _setup_ui(self):
+ layout = QVBoxLayout(self)
+
+ # Allgemeine Informationen
+ general_group = QGroupBox("Allgemein")
+ general_layout = QFormLayout(general_group)
+
+ self.name_label = QLabel()
+ self.name_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
+ general_layout.addRow("Name:", self.name_label)
+
+ self.type_label = QLabel()
+ general_layout.addRow("Typ:", self.type_label)
+
+ self.location_label = QLabel()
+ self.location_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
+ self.location_label.setWordWrap(True)
+ general_layout.addRow("Ort:", self.location_label)
+
+ self.size_label = QLabel()
+ general_layout.addRow("Größe:", self.size_label)
+
+ self.contains_label = QLabel()
+ general_layout.addRow("Enthält:", self.contains_label)
+
+ layout.addWidget(general_group)
+
+ # Zeitstempel
+ time_group = QGroupBox("Zeitstempel")
+ time_layout = QFormLayout(time_group)
+
+ self.created_label = QLabel()
+ time_layout.addRow("Erstellt:", self.created_label)
+
+ self.modified_label = QLabel()
+ time_layout.addRow("Geändert:", self.modified_label)
+
+ self.accessed_label = QLabel()
+ time_layout.addRow("Zugegriffen:", self.accessed_label)
+
+ layout.addWidget(time_group)
+
+ # Berechtigungen
+ perm_group = QGroupBox("Berechtigungen")
+ perm_layout = QFormLayout(perm_group)
+
+ self.permissions_label = QLabel()
+ perm_layout.addRow("Rechte:", self.permissions_label)
+
+ self.owner_label = QLabel()
+ perm_layout.addRow("Besitzer:", self.owner_label)
+
+ layout.addWidget(perm_group)
+
+ # Buttons
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
+ button_box.accepted.connect(self.accept)
+ layout.addWidget(button_box)
+
+ def _load_properties(self):
+ """Lädt die Eigenschaften der Datei(en)."""
+ if len(self.paths) == 1:
+ self._load_single_properties(self.paths[0])
+ else:
+ self._load_multiple_properties()
+
+ def _load_single_properties(self, path: str):
+ """Lädt Eigenschaften einer einzelnen Datei/Ordner."""
+ if not os.path.exists(path):
+ self.name_label.setText("Nicht gefunden")
+ return
+
+ stat_info = os.stat(path)
+ name = os.path.basename(path)
+
+ self.name_label.setText(name)
+ self.location_label.setText(os.path.dirname(path))
+
+ if os.path.isdir(path):
+ self.type_label.setText("Ordner")
+ # Ordnerinhalt zählen
+ try:
+ items = list(os.scandir(path))
+ folders = sum(1 for i in items if i.is_dir())
+ files = sum(1 for i in items if i.is_file())
+ self.contains_label.setText(f"{folders} Ordner, {files} Dateien")
+ except PermissionError:
+ self.contains_label.setText("Zugriff verweigert")
+
+ # Ordnergröße berechnen
+ total_size = self._get_folder_size(path)
+ self.size_label.setText(format_file_size(total_size))
+ else:
+ # Dateityp aus Erweiterung
+ ext = Path(name).suffix.lower()
+ type_names = {
+ '.txt': 'Textdatei', '.py': 'Python-Datei', '.js': 'JavaScript',
+ '.html': 'HTML-Datei', '.css': 'CSS-Datei', '.json': 'JSON-Datei',
+ '.md': 'Markdown-Datei', '.pdf': 'PDF-Dokument', '.jpg': 'JPEG-Bild',
+ '.png': 'PNG-Bild', '.gif': 'GIF-Bild', '.mp3': 'MP3-Audio',
+ '.mp4': 'MP4-Video', '.zip': 'ZIP-Archiv', '.tar': 'TAR-Archiv',
+ }
+ self.type_label.setText(type_names.get(ext, f"Datei ({ext})" if ext else "Datei"))
+ self.size_label.setText(format_file_size(stat_info.st_size))
+ self.contains_label.setText("-")
+
+ # Zeitstempel
+ self.created_label.setText(
+ datetime.fromtimestamp(stat_info.st_ctime).strftime("%d.%m.%Y %H:%M:%S")
+ )
+ self.modified_label.setText(
+ datetime.fromtimestamp(stat_info.st_mtime).strftime("%d.%m.%Y %H:%M:%S")
+ )
+ self.accessed_label.setText(
+ datetime.fromtimestamp(stat_info.st_atime).strftime("%d.%m.%Y %H:%M:%S")
+ )
+
+ # Berechtigungen
+ mode = stat_info.st_mode
+ perms = ""
+ perms += "r" if mode & stat.S_IRUSR else "-"
+ perms += "w" if mode & stat.S_IWUSR else "-"
+ perms += "x" if mode & stat.S_IXUSR else "-"
+ perms += "r" if mode & stat.S_IRGRP else "-"
+ perms += "w" if mode & stat.S_IWGRP else "-"
+ perms += "x" if mode & stat.S_IXGRP else "-"
+ perms += "r" if mode & stat.S_IROTH else "-"
+ perms += "w" if mode & stat.S_IWOTH else "-"
+ perms += "x" if mode & stat.S_IXOTH else "-"
+ self.permissions_label.setText(perms)
+
+ # Besitzer
+ try:
+ import pwd
+ import grp
+ owner = pwd.getpwuid(stat_info.st_uid).pw_name
+ group = grp.getgrgid(stat_info.st_gid).gr_name
+ self.owner_label.setText(f"{owner}:{group}")
+ except (KeyError, ImportError):
+ self.owner_label.setText(f"UID {stat_info.st_uid}")
+
+ def _load_multiple_properties(self):
+ """Lädt Eigenschaften mehrerer Dateien."""
+ self.name_label.setText(f"{len(self.paths)} Elemente ausgewählt")
+ self.location_label.setText(os.path.dirname(self.paths[0]))
+
+ # Typen zählen
+ folders = sum(1 for p in self.paths if os.path.isdir(p))
+ files = len(self.paths) - folders
+ if folders and files:
+ self.type_label.setText(f"{folders} Ordner, {files} Dateien")
+ elif folders:
+ self.type_label.setText(f"{folders} Ordner")
+ else:
+ self.type_label.setText(f"{files} Dateien")
+
+ # Gesamtgröße
+ total_size = 0
+ for path in self.paths:
+ if os.path.isdir(path):
+ total_size += self._get_folder_size(path)
+ elif os.path.isfile(path):
+ total_size += os.path.getsize(path)
+ self.size_label.setText(format_file_size(total_size))
+
+ self.contains_label.setText("-")
+ self.created_label.setText("-")
+ self.modified_label.setText("-")
+ self.accessed_label.setText("-")
+ self.permissions_label.setText("-")
+ self.owner_label.setText("-")
+
+ def _get_folder_size(self, path: str) -> int:
+ """Berechnet die Größe eines Ordners rekursiv."""
+ total = 0
+ try:
+ for entry in os.scandir(path):
+ try:
+ if entry.is_file(follow_symlinks=False):
+ total += entry.stat().st_size
+ elif entry.is_dir(follow_symlinks=False):
+ total += self._get_folder_size(entry.path)
+ except (PermissionError, OSError):
+ continue
+ except PermissionError:
+ pass
+ return total
+
+
+class ArchiveDialog(QDialog):
+ """Dialog zum Erstellen eines Archivs."""
+
+ def __init__(self, source_paths: list[str], parent=None):
+ super().__init__(parent)
+ self.source_paths = source_paths
+ self.archive_name = ""
+ self.archive_format = ".zip"
+
+ self.setWindowTitle("Archiv erstellen")
+ self.setModal(True)
+ self.setMinimumWidth(450)
+
+ self._setup_ui()
+
+ def _setup_ui(self):
+ layout = QVBoxLayout(self)
+
+ # Quelldateien
+ source_group = QGroupBox(f"Zu packende Elemente ({len(self.source_paths)})")
+ source_layout = QVBoxLayout(source_group)
+
+ source_list = QListWidget()
+ source_list.setMaximumHeight(100)
+ for path in self.source_paths[:10]: # Max 10 anzeigen
+ source_list.addItem(os.path.basename(path))
+ if len(self.source_paths) > 10:
+ source_list.addItem(f"... und {len(self.source_paths) - 10} weitere")
+ source_layout.addWidget(source_list)
+
+ layout.addWidget(source_group)
+
+ # Archiv-Name
+ name_group = QGroupBox("Archiv")
+ name_layout = QFormLayout(name_group)
+
+ self.name_input = QLineEdit()
+ # Standard-Name aus erstem Element
+ if len(self.source_paths) == 1:
+ base_name = Path(self.source_paths[0]).stem
+ else:
+ base_name = "archiv"
+ self.name_input.setText(base_name + ".zip")
+ name_layout.addRow("Dateiname:", self.name_input)
+
+ layout.addWidget(name_group)
+
+ # Format-Auswahl
+ format_group = QGroupBox("Format")
+ format_layout = QVBoxLayout(format_group)
+
+ self.format_group = QButtonGroup(self)
+
+ formats = [
+ ("ZIP (.zip)", ".zip"),
+ ("TAR (.tar)", ".tar"),
+ ("TAR.GZ (.tar.gz)", ".tar.gz"),
+ ("TAR.BZ2 (.tar.bz2)", ".tar.bz2"),
+ ("TAR.XZ (.tar.xz)", ".tar.xz"),
+ ]
+
+ for i, (label, ext) in enumerate(formats):
+ radio = QRadioButton(label)
+ radio.setProperty("extension", ext)
+ if i == 0:
+ radio.setChecked(True)
+ radio.toggled.connect(self._on_format_changed)
+ self.format_group.addButton(radio, i)
+ format_layout.addWidget(radio)
+
+ layout.addWidget(format_group)
+
+ # Buttons
+ button_box = QDialogButtonBox(
+ QDialogButtonBox.StandardButton.Ok |
+ QDialogButtonBox.StandardButton.Cancel
+ )
+ button_box.accepted.connect(self.accept)
+ button_box.rejected.connect(self.reject)
+ layout.addWidget(button_box)
+
+ def _on_format_changed(self):
+ """Aktualisiert die Dateiendung."""
+ button = self.format_group.checkedButton()
+ if button:
+ new_ext = button.property("extension")
+ current_name = self.name_input.text()
+ # Alte Erweiterung entfernen
+ for ext in ['.tar.gz', '.tar.bz2', '.tar.xz', '.tar', '.zip']:
+ if current_name.endswith(ext):
+ current_name = current_name[:-len(ext)]
+ break
+ self.name_input.setText(current_name + new_ext)
+ self.archive_format = new_ext
+
+ def accept(self):
+ """Validiert die Eingabe."""
+ self.archive_name = self.name_input.text().strip()
+
+ if not self.archive_name:
+ QMessageBox.warning(self, "Fehler", "Archiv-Name darf nicht leer sein.")
+ return
+
+ button = self.format_group.checkedButton()
+ if button:
+ self.archive_format = button.property("extension")
+
+ super().accept()
+
+ def get_archive_name(self) -> str:
+ """Gibt den Archiv-Namen zurück."""
+ return self.archive_name
+
+ def get_archive_format(self) -> str:
+ """Gibt das Archiv-Format zurück."""
+ return self.archive_format
+
+
+class ExtractDialog(QDialog):
+ """Dialog zum Entpacken eines Archivs."""
+
+ def __init__(self, archive_path: str, parent=None):
+ super().__init__(parent)
+ self.archive_path = archive_path
+ self.target_folder = os.path.dirname(archive_path)
+ self.create_subfolder = True
+
+ self.setWindowTitle("Archiv entpacken")
+ self.setModal(True)
+ self.setMinimumSize(500, 400)
+
+ self._setup_ui()
+
+ def _setup_ui(self):
+ layout = QVBoxLayout(self)
+
+ # Archiv-Info
+ info_label = QLabel(f"Archiv: {os.path.basename(self.archive_path)}")
+ info_label.setWordWrap(True)
+ layout.addWidget(info_label)
+
+ # Zielordner
+ layout.addWidget(QLabel("Entpacken nach:"))
+
+ self.model = QFileSystemModel()
+ self.model.setFilter(QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot)
+ self.model.setRootPath('')
+
+ self.tree = QTreeView()
+ self.tree.setModel(self.model)
+ self.tree.setHeaderHidden(True)
+
+ for i in range(1, self.model.columnCount()):
+ self.tree.hideColumn(i)
+
+ # Aktuellen Ordner expandieren
+ current_dir = os.path.dirname(self.archive_path)
+ index = self.model.index(current_dir)
+ if index.isValid():
+ self.tree.setCurrentIndex(index)
+ self.tree.scrollTo(index)
+
+ layout.addWidget(self.tree)
+
+ self.path_label = QLabel(f"Ziel: {self.target_folder}")
+ self.path_label.setWordWrap(True)
+ layout.addWidget(self.path_label)
+
+ self.tree.clicked.connect(self._on_selection_changed)
+
+ # Optionen
+ self.subfolder_check = QCheckBox("Unterordner erstellen")
+ self.subfolder_check.setChecked(True)
+ self.subfolder_check.setToolTip(
+ "Erstellt einen Ordner mit dem Namen des Archivs und entpackt den Inhalt darin."
+ )
+ layout.addWidget(self.subfolder_check)
+
+ # Buttons
+ button_box = QDialogButtonBox(
+ QDialogButtonBox.StandardButton.Ok |
+ QDialogButtonBox.StandardButton.Cancel
+ )
+ button_box.accepted.connect(self.accept)
+ button_box.rejected.connect(self.reject)
+ layout.addWidget(button_box)
+
+ def _on_selection_changed(self, index):
+ """Aktualisiert den ausgewählten Pfad."""
+ path = self.model.filePath(index)
+ self.path_label.setText(f"Ziel: {path}")
+ self.target_folder = path
+
+ def accept(self):
+ """Validiert die Auswahl."""
+ if not self.target_folder or not os.path.isdir(self.target_folder):
+ QMessageBox.warning(self, "Fehler", "Bitte wähle einen gültigen Zielordner.")
+ return
+
+ self.create_subfolder = self.subfolder_check.isChecked()
+ super().accept()
+
+ def get_target_folder(self) -> str:
+ """Gibt den Zielordner zurück."""
+ return self.target_folder
+
+ def get_create_subfolder(self) -> bool:
+ """Gibt zurück ob ein Unterordner erstellt werden soll."""
+ return self.create_subfolder
diff --git a/src/main_window.py b/src/main_window.py
index 70031fb..3c4a0d5 100644
--- a/src/main_window.py
+++ b/src/main_window.py
@@ -2,11 +2,12 @@
import os
import shutil
+import subprocess
from pathlib import Path
from PyQt6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSplitter,
QMenuBar, QMenu, QToolBar, QStatusBar, QLabel, QComboBox,
- QMessageBox, QFrame, QApplication
+ QMessageBox, QFrame, QApplication, QInputDialog
)
from PyQt6.QtCore import Qt, QSettings, QTimer
from PyQt6.QtGui import QAction, QKeySequence, QShortcut
@@ -15,9 +16,14 @@ from .widgets.folder_tree import FolderTreeWidget
from .widgets.file_list import FileListWidget
from .widgets.preview_panel import PreviewPanel
from .widgets.breadcrumb import BreadcrumbWidget
-from .dialogs import RenameDialog, MoveDialog, DeleteDialog, SettingsDialog
+from .dialogs import (
+ RenameDialog, MoveDialog, DeleteDialog, SettingsDialog,
+ NewFileDialog, PropertiesDialog, ArchiveDialog, ExtractDialog
+)
from .preview_window import PreviewWindow
from .utils.themes import ThemeManager
+from .utils.clipboard import FileClipboard
+from .utils.archive import ArchiveHandler
class MainWindow(QMainWindow):
@@ -29,6 +35,7 @@ class MainWindow(QMainWindow):
self.theme_manager = ThemeManager()
self.preview_window = None
self._current_path = ""
+ self.clipboard = FileClipboard.instance()
self.setWindowTitle("FileBrowser")
self.setMinimumSize(800, 600)
@@ -93,14 +100,19 @@ class MainWindow(QMainWindow):
# Datei-Menü
file_menu = menubar.addMenu("&Datei")
- new_folder_action = QAction("Neuer Ordner", self)
+ new_file_action = QAction("📄 Neue Datei...", self)
+ new_file_action.setShortcut(QKeySequence("Ctrl+N"))
+ new_file_action.triggered.connect(self._create_new_file)
+ file_menu.addAction(new_file_action)
+
+ new_folder_action = QAction("📁 Neuer Ordner...", self)
new_folder_action.setShortcut(QKeySequence("Ctrl+Shift+N"))
new_folder_action.triggered.connect(self._create_new_folder)
file_menu.addAction(new_folder_action)
file_menu.addSeparator()
- settings_action = QAction("Einstellungen...", self)
+ settings_action = QAction("⚙️ Einstellungen...", self)
settings_action.setShortcut(QKeySequence("Ctrl+,"))
settings_action.triggered.connect(self._show_settings)
file_menu.addAction(settings_action)
@@ -115,27 +127,76 @@ class MainWindow(QMainWindow):
# Bearbeiten-Menü
edit_menu = menubar.addMenu("&Bearbeiten")
- rename_action = QAction("Umbenennen", self)
+ cut_action = QAction("✂️ Ausschneiden", self)
+ cut_action.setShortcut(QKeySequence("Ctrl+X"))
+ cut_action.triggered.connect(self._cut_selected)
+ edit_menu.addAction(cut_action)
+
+ copy_action = QAction("📋 Kopieren", self)
+ copy_action.setShortcut(QKeySequence("Ctrl+C"))
+ copy_action.triggered.connect(self._copy_selected)
+ edit_menu.addAction(copy_action)
+
+ paste_action = QAction("📋 Einfügen", self)
+ paste_action.setShortcut(QKeySequence("Ctrl+V"))
+ paste_action.triggered.connect(self._paste_files)
+ edit_menu.addAction(paste_action)
+
+ edit_menu.addSeparator()
+
+ select_all_action = QAction("Alles auswählen", self)
+ select_all_action.setShortcut(QKeySequence("Ctrl+A"))
+ select_all_action.triggered.connect(self.file_list.selectAll)
+ edit_menu.addAction(select_all_action)
+
+ edit_menu.addSeparator()
+
+ rename_action = QAction("✏️ Umbenennen", self)
rename_action.setShortcut(QKeySequence("F2"))
rename_action.triggered.connect(self._rename_selected)
edit_menu.addAction(rename_action)
- move_action = QAction("Verschieben", self)
+ move_action = QAction("📦 Verschieben nach...", self)
move_action.triggered.connect(self._move_selected)
edit_menu.addAction(move_action)
- delete_action = QAction("Löschen", self)
+ delete_action = QAction("🗑 Löschen", self)
delete_action.setShortcut(QKeySequence.StandardKey.Delete)
delete_action.triggered.connect(self._delete_selected)
edit_menu.addAction(delete_action)
edit_menu.addSeparator()
- refresh_action = QAction("Aktualisieren", self)
+ refresh_action = QAction("🔄 Aktualisieren", self)
refresh_action.setShortcut(QKeySequence("F5"))
refresh_action.triggered.connect(self._refresh)
edit_menu.addAction(refresh_action)
+ # Extras-Menü
+ extras_menu = menubar.addMenu("E&xtras")
+
+ archive_action = QAction("📦 Zu Archiv packen...", self)
+ archive_action.triggered.connect(self._archive_selected)
+ extras_menu.addAction(archive_action)
+
+ extract_action = QAction("📦 Archiv entpacken...", self)
+ extract_action.triggered.connect(self._extract_selected)
+ extras_menu.addAction(extract_action)
+
+ extras_menu.addSeparator()
+
+ terminal_action = QAction("💻 Terminal öffnen", self)
+ terminal_action.setShortcut(QKeySequence("F4"))
+ terminal_action.triggered.connect(lambda: self._open_terminal(self._current_path))
+ extras_menu.addAction(terminal_action)
+
+ extras_menu.addSeparator()
+
+ properties_action = QAction("ℹ️ Eigenschaften", self)
+ properties_action.setShortcut(QKeySequence("Alt+Return"))
+ properties_action.triggered.connect(self._show_properties)
+ extras_menu.addAction(properties_action)
+
# Ansicht-Menü
view_menu = menubar.addMenu("&Ansicht")
@@ -246,6 +307,28 @@ class MainWindow(QMainWindow):
toolbar.addSeparator()
+ # Neue Datei
+ new_file_action = QAction("📄", self)
+ new_file_action.setToolTip("Neue Datei (Ctrl+N)")
+ new_file_action.triggered.connect(self._create_new_file)
+ toolbar.addAction(new_file_action)
+
+ # Neuer Ordner
+ new_folder_action = QAction("📁", self)
+ new_folder_action.setToolTip("Neuer Ordner (Ctrl+Shift+N)")
+ new_folder_action.triggered.connect(self._create_new_folder)
+ toolbar.addAction(new_folder_action)
+
+ toolbar.addSeparator()
+
+ # Terminal
+ terminal_action = QAction("💻", self)
+ terminal_action.setToolTip("Terminal öffnen (F4)")
+ terminal_action.triggered.connect(lambda: self._open_terminal(self._current_path))
+ toolbar.addAction(terminal_action)
+
+ toolbar.addSeparator()
+
# Theme-Auswahl
toolbar.addWidget(QLabel("Theme: "))
self.theme_combo = QComboBox()
@@ -262,21 +345,42 @@ class MainWindow(QMainWindow):
self.path_label = QLabel()
self.statusbar.addWidget(self.path_label, 1)
+ self.clipboard_label = QLabel()
+ self.statusbar.addPermanentWidget(self.clipboard_label)
+
self.count_label = QLabel()
self.statusbar.addPermanentWidget(self.count_label)
+ # Clipboard-Status aktualisieren
+ self.clipboard.clipboard_changed.connect(self._update_clipboard_status)
+
+ def _update_clipboard_status(self):
+ """Aktualisiert die Zwischenablage-Anzeige."""
+ if self.clipboard.has_files():
+ count = self.clipboard.get_file_count()
+ mode = "Kopiert" if self.clipboard.get_mode().value == "copy" else "Ausgeschnitten"
+ self.clipboard_label.setText(f"📋 {mode}: {count}")
+ else:
+ self.clipboard_label.setText("")
+
def _setup_shortcuts(self):
"""Richtet Tastenkürzel ein."""
# Backspace für zurück
QShortcut(QKeySequence("Backspace"), self, self._go_back)
+ # F4 für Terminal
+ QShortcut(QKeySequence("F4"), self, lambda: self._open_terminal(self._current_path))
+
+ # Alt+Enter für Eigenschaften
+ QShortcut(QKeySequence("Alt+Return"), self, self._show_properties)
+
def _connect_signals(self):
"""Verbindet alle Signale."""
# Ordnerbaum
self.folder_tree.folder_selected.connect(self._navigate_to)
self.folder_tree.folder_double_clicked.connect(self._navigate_to)
- # Dateiliste
+ # Dateiliste - Basis-Signale
self.file_list.file_selected.connect(self._on_file_selected)
self.file_list.file_double_clicked.connect(self._open_external)
self.file_list.folder_entered.connect(self._navigate_to)
@@ -285,6 +389,18 @@ class MainWindow(QMainWindow):
self.file_list.file_move_requested.connect(self._move_file)
self.file_list.files_dropped.connect(self._handle_files_dropped)
+ # Dateiliste - Erweiterte Signale
+ self.file_list.new_file_requested.connect(self._create_new_file)
+ self.file_list.new_folder_requested.connect(self._create_new_folder)
+ self.file_list.copy_requested.connect(self._copy_files)
+ self.file_list.cut_requested.connect(self._cut_files)
+ self.file_list.paste_requested.connect(self._paste_files)
+ self.file_list.archive_requested.connect(self._create_archive)
+ self.file_list.extract_requested.connect(self._extract_archive)
+ self.file_list.properties_requested.connect(self._show_properties_for)
+ self.file_list.open_terminal_requested.connect(self._open_terminal)
+ self.file_list.refresh_requested.connect(self._refresh)
+
# Breadcrumb
self.breadcrumb.path_clicked.connect(self._navigate_to)
self.breadcrumb.path_entered.connect(self._navigate_to)
@@ -340,15 +456,8 @@ class MainWindow(QMainWindow):
stylesheet = self.theme_manager.apply_theme(theme_name)
self.setStyleSheet(stylesheet)
- # Hover-Farbe für Dateiliste setzen
- theme = self.theme_manager.THEMES.get(theme_name, self.theme_manager.THEMES['dark'])
- if theme.get('is_system'):
- # System-Theme: Standard-Hover-Farbe
- self.file_list.set_hover_color('#e0e0e0')
- else:
- self.file_list.set_hover_color(theme['alternate_base'])
-
# Theme-Menü-Aktionen aktualisieren
+ theme = self.theme_manager.THEMES.get(theme_name, self.theme_manager.THEMES['dark'])
for action in self.theme_actions:
action.setChecked(action.data() == theme)
@@ -418,7 +527,6 @@ class MainWindow(QMainWindow):
def _open_external(self, path: str):
"""Öffnet eine Datei extern."""
- import subprocess
try:
subprocess.Popen(['xdg-open', path])
except Exception as e:
@@ -442,6 +550,49 @@ class MainWindow(QMainWindow):
self._update_count()
self.statusbar.showMessage("Aktualisiert", 2000)
+ # ==================== Kopieren/Ausschneiden/Einfügen ====================
+
+ def _copy_selected(self):
+ """Kopiert die ausgewählten Elemente."""
+ paths = self.file_list.get_selected_paths()
+ if paths:
+ self._copy_files(paths)
+
+ def _cut_selected(self):
+ """Schneidet die ausgewählten Elemente aus."""
+ paths = self.file_list.get_selected_paths()
+ if paths:
+ self._cut_files(paths)
+
+ def _copy_files(self, paths: list):
+ """Kopiert Dateien in die Zwischenablage."""
+ self.clipboard.copy(paths)
+ self.statusbar.showMessage(f"{len(paths)} Element(e) kopiert", 2000)
+
+ def _cut_files(self, paths: list):
+ """Schneidet Dateien aus."""
+ self.clipboard.cut(paths)
+ self.statusbar.showMessage(f"{len(paths)} Element(e) ausgeschnitten", 2000)
+
+ def _paste_files(self):
+ """Fügt Dateien aus der Zwischenablage ein."""
+ if not self.clipboard.has_files():
+ return
+
+ count, errors = self.clipboard.paste(self._current_path)
+ self._refresh()
+
+ if errors:
+ QMessageBox.warning(
+ self, "Fehler beim Einfügen",
+ "Einige Dateien konnten nicht eingefügt werden:\n\n" +
+ "\n".join(errors[:10])
+ )
+ elif count > 0:
+ self.statusbar.showMessage(f"{count} Element(e) eingefügt", 3000)
+
+ # ==================== Umbenennen/Verschieben/Löschen ====================
+
def _rename_selected(self):
"""Benennt das ausgewählte Element um."""
path = self.file_list.get_selected_path()
@@ -548,10 +699,26 @@ class MainWindow(QMainWindow):
3000
)
+ # ==================== Neue Dateien/Ordner ====================
+
+ def _create_new_file(self):
+ """Erstellt eine neue Datei."""
+ dialog = NewFileDialog(self._current_path, self)
+ if dialog.exec():
+ file_name = dialog.get_file_name()
+ file_path = os.path.join(self._current_path, file_name)
+ content = dialog.get_template_content()
+
+ try:
+ with open(file_path, 'w', encoding='utf-8') as f:
+ f.write(content)
+ self._refresh()
+ self.statusbar.showMessage(f"Datei erstellt: {file_name}", 3000)
+ except Exception as e:
+ QMessageBox.critical(self, "Fehler", f"Fehler beim Erstellen: {e}")
+
def _create_new_folder(self):
"""Erstellt einen neuen Ordner."""
- from PyQt6.QtWidgets import QInputDialog
-
name, ok = QInputDialog.getText(
self, "Neuer Ordner", "Ordnername:"
)
@@ -564,6 +731,91 @@ class MainWindow(QMainWindow):
except Exception as e:
QMessageBox.critical(self, "Fehler", f"Fehler beim Erstellen: {e}")
+ # ==================== Archiv-Funktionen ====================
+
+ def _archive_selected(self):
+ """Packt die ausgewählten Elemente."""
+ paths = self.file_list.get_selected_paths()
+ if paths:
+ self._create_archive(paths)
+
+ def _extract_selected(self):
+ """Entpackt das ausgewählte Archiv."""
+ path = self.file_list.get_selected_path()
+ if path and ArchiveHandler.is_archive(path):
+ self._extract_archive(path)
+
+ def _create_archive(self, paths: list):
+ """Erstellt ein Archiv."""
+ dialog = ArchiveDialog(paths, self)
+ if dialog.exec():
+ archive_name = dialog.get_archive_name()
+ archive_format = dialog.get_archive_format()
+ archive_path = os.path.join(self._current_path, archive_name)
+
+ success, message = ArchiveHandler.create(paths, archive_path, archive_format)
+ self._refresh()
+
+ if success:
+ self.statusbar.showMessage(f"Archiv erstellt: {archive_name}", 3000)
+ else:
+ QMessageBox.critical(self, "Fehler", message)
+
+ def _extract_archive(self, path: str):
+ """Entpackt ein Archiv."""
+ dialog = ExtractDialog(path, self)
+ if dialog.exec():
+ target_folder = dialog.get_target_folder()
+ create_subfolder = dialog.get_create_subfolder()
+
+ success, message = ArchiveHandler.extract(path, target_folder, create_subfolder)
+ self._refresh()
+
+ if success:
+ self.statusbar.showMessage("Archiv entpackt", 3000)
+ else:
+ QMessageBox.critical(self, "Fehler", message)
+
+ # ==================== Terminal/Eigenschaften ====================
+
+ def _open_terminal(self, path: str):
+ """Öffnet ein Terminal im angegebenen Ordner."""
+ terminals = [
+ ['konsole', '--workdir', path],
+ ['gnome-terminal', '--working-directory', path],
+ ['xfce4-terminal', '--working-directory', path],
+ ['alacritty', '--working-directory', path],
+ ['kitty', '--directory', path],
+ ['xterm', '-e', f'cd "{path}" && $SHELL'],
+ ]
+
+ for terminal in terminals:
+ try:
+ subprocess.Popen(terminal)
+ return
+ except FileNotFoundError:
+ continue
+
+ QMessageBox.warning(
+ self, "Terminal nicht gefunden",
+ "Es konnte kein Terminal-Emulator gefunden werden."
+ )
+
+ def _show_properties(self):
+ """Zeigt Eigenschaften für die ausgewählten Elemente."""
+ paths = self.file_list.get_selected_paths()
+ if paths:
+ self._show_properties_for(paths)
+ else:
+ self._show_properties_for([self._current_path])
+
+ def _show_properties_for(self, paths: list):
+ """Zeigt den Eigenschaften-Dialog."""
+ dialog = PropertiesDialog(paths, self)
+ dialog.exec()
+
+ # ==================== Preview ====================
+
def _detach_preview(self):
"""Öffnet die Vorschau in einem separaten Fenster."""
if not self.preview_window:
@@ -616,6 +868,8 @@ class MainWindow(QMainWindow):
for action in self.pdf_page_actions:
action.setChecked(action.data() == settings['page_mode'])
+ # ==================== Einstellungen ====================
+
def _show_settings(self):
"""Zeigt den Einstellungsdialog."""
dialog = SettingsDialog(self)
@@ -651,6 +905,12 @@ class MainWindow(QMainWindow):
"Über FileBrowser",
"FileBrowser\n\n"
"Ein einfacher Dateimanager mit Vorschau-Funktion.\n\n"
+ "Funktionen:\n"
+ "• Datei-Vorschau (PDF, Bilder, Markdown, Text)\n"
+ "• Kopieren, Ausschneiden, Einfügen\n"
+ "• Archive packen und entpacken\n"
+ "• Drag & Drop\n"
+ "• Mehrere Themes\n\n"
"Erstellt mit PyQt6"
)
diff --git a/src/utils/archive.py b/src/utils/archive.py
new file mode 100644
index 0000000..9081de8
--- /dev/null
+++ b/src/utils/archive.py
@@ -0,0 +1,260 @@
+"""Archive-Handler für Packen und Entpacken."""
+
+import os
+import zipfile
+import tarfile
+import shutil
+from pathlib import Path
+from typing import Optional
+
+
+class ArchiveHandler:
+ """Verarbeitet Archiv-Operationen."""
+
+ SUPPORTED_EXTENSIONS = {
+ '.zip': 'zip',
+ '.tar': 'tar',
+ '.tar.gz': 'tar.gz',
+ '.tgz': 'tar.gz',
+ '.tar.bz2': 'tar.bz2',
+ '.tbz2': 'tar.bz2',
+ '.tar.xz': 'tar.xz',
+ '.txz': 'tar.xz',
+ '.gz': 'gz',
+ '.bz2': 'bz2',
+ '.xz': 'xz',
+ '.7z': '7z',
+ '.rar': 'rar',
+ }
+
+ ARCHIVE_FORMATS = [
+ ('ZIP-Archiv', '.zip'),
+ ('TAR-Archiv', '.tar'),
+ ('TAR.GZ-Archiv', '.tar.gz'),
+ ('TAR.BZ2-Archiv', '.tar.bz2'),
+ ('TAR.XZ-Archiv', '.tar.xz'),
+ ]
+
+ @classmethod
+ def is_archive(cls, path: str) -> bool:
+ """Prüft ob eine Datei ein Archiv ist."""
+ lower_path = path.lower()
+ for ext in cls.SUPPORTED_EXTENSIONS:
+ if lower_path.endswith(ext):
+ return True
+ return False
+
+ @classmethod
+ def get_archive_type(cls, path: str) -> Optional[str]:
+ """Gibt den Archiv-Typ zurück."""
+ lower_path = path.lower()
+ # Längere Erweiterungen zuerst prüfen
+ for ext in sorted(cls.SUPPORTED_EXTENSIONS.keys(), key=len, reverse=True):
+ if lower_path.endswith(ext):
+ return cls.SUPPORTED_EXTENSIONS[ext]
+ return None
+
+ @classmethod
+ def extract(cls, archive_path: str, target_folder: str,
+ create_subfolder: bool = True) -> tuple[bool, str]:
+ """
+ Entpackt ein Archiv.
+
+ Args:
+ archive_path: Pfad zum Archiv
+ target_folder: Zielordner
+ create_subfolder: Unterordner mit Archiv-Namen erstellen
+
+ Returns:
+ Tuple (Erfolg, Nachricht/Fehlermeldung)
+ """
+ if not os.path.exists(archive_path):
+ return False, "Archiv nicht gefunden"
+
+ archive_type = cls.get_archive_type(archive_path)
+ if not archive_type:
+ return False, "Nicht unterstütztes Archiv-Format"
+
+ # Zielordner erstellen
+ if create_subfolder:
+ # Archiv-Name ohne Erweiterung
+ base_name = Path(archive_path).stem
+ # Bei .tar.gz etc. den .tar auch entfernen
+ if base_name.endswith('.tar'):
+ base_name = base_name[:-4]
+ target_folder = os.path.join(target_folder, base_name)
+
+ os.makedirs(target_folder, exist_ok=True)
+
+ try:
+ if archive_type == 'zip':
+ with zipfile.ZipFile(archive_path, 'r') as zf:
+ zf.extractall(target_folder)
+
+ elif archive_type.startswith('tar') or archive_type == 'tar':
+ mode = 'r'
+ if archive_type == 'tar.gz':
+ mode = 'r:gz'
+ elif archive_type == 'tar.bz2':
+ mode = 'r:bz2'
+ elif archive_type == 'tar.xz':
+ mode = 'r:xz'
+
+ with tarfile.open(archive_path, mode) as tf:
+ tf.extractall(target_folder)
+
+ elif archive_type in ('7z', 'rar'):
+ # Versuche externe Tools
+ return cls._extract_with_external_tool(archive_path, target_folder, archive_type)
+
+ else:
+ return False, f"Entpacken von {archive_type} nicht unterstützt"
+
+ return True, f"Erfolgreich entpackt nach: {target_folder}"
+
+ except zipfile.BadZipFile:
+ return False, "Ungültige ZIP-Datei"
+ except tarfile.TarError as e:
+ return False, f"TAR-Fehler: {str(e)}"
+ except PermissionError:
+ return False, "Keine Berechtigung zum Entpacken"
+ except Exception as e:
+ return False, f"Fehler beim Entpacken: {str(e)}"
+
+ @classmethod
+ def _extract_with_external_tool(cls, archive_path: str, target_folder: str,
+ archive_type: str) -> tuple[bool, str]:
+ """Entpackt mit externen Tools (7z, unrar)."""
+ import subprocess
+
+ try:
+ if archive_type == '7z':
+ result = subprocess.run(
+ ['7z', 'x', archive_path, f'-o{target_folder}', '-y'],
+ capture_output=True, text=True
+ )
+ elif archive_type == 'rar':
+ result = subprocess.run(
+ ['unrar', 'x', archive_path, target_folder],
+ capture_output=True, text=True
+ )
+ else:
+ return False, f"Kein externes Tool für {archive_type}"
+
+ if result.returncode == 0:
+ return True, f"Erfolgreich entpackt nach: {target_folder}"
+ else:
+ return False, result.stderr or "Unbekannter Fehler"
+
+ except FileNotFoundError:
+ tool = '7z' if archive_type == '7z' else 'unrar'
+ return False, f"{tool} nicht installiert. Bitte installieren für {archive_type}-Unterstützung."
+ except Exception as e:
+ return False, str(e)
+
+ @classmethod
+ def create(cls, source_paths: list[str], archive_path: str,
+ archive_format: str = '.zip') -> tuple[bool, str]:
+ """
+ Erstellt ein Archiv.
+
+ Args:
+ source_paths: Liste der zu packenden Dateien/Ordner
+ archive_path: Zielpfad für das Archiv
+ archive_format: Format (.zip, .tar, .tar.gz, .tar.bz2, .tar.xz)
+
+ Returns:
+ Tuple (Erfolg, Nachricht/Fehlermeldung)
+ """
+ if not source_paths:
+ return False, "Keine Dateien zum Packen angegeben"
+
+ # Prüfen ob alle Quellen existieren
+ for path in source_paths:
+ if not os.path.exists(path):
+ return False, f"Nicht gefunden: {os.path.basename(path)}"
+
+ try:
+ if archive_format == '.zip':
+ return cls._create_zip(source_paths, archive_path)
+ elif archive_format.startswith('.tar'):
+ return cls._create_tar(source_paths, archive_path, archive_format)
+ else:
+ return False, f"Nicht unterstütztes Format: {archive_format}"
+
+ except PermissionError:
+ return False, "Keine Berechtigung zum Erstellen des Archivs"
+ except Exception as e:
+ return False, f"Fehler beim Erstellen: {str(e)}"
+
+ @classmethod
+ def _create_zip(cls, source_paths: list[str], archive_path: str) -> tuple[bool, str]:
+ """Erstellt ein ZIP-Archiv."""
+ with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as zf:
+ for source_path in source_paths:
+ if os.path.isfile(source_path):
+ zf.write(source_path, os.path.basename(source_path))
+ elif os.path.isdir(source_path):
+ base_name = os.path.basename(source_path)
+ for root, dirs, files in os.walk(source_path):
+ # Relativer Pfad im Archiv
+ rel_root = os.path.relpath(root, os.path.dirname(source_path))
+ for file in files:
+ file_path = os.path.join(root, file)
+ arc_name = os.path.join(rel_root, file)
+ zf.write(file_path, arc_name)
+
+ return True, f"ZIP-Archiv erstellt: {archive_path}"
+
+ @classmethod
+ def _create_tar(cls, source_paths: list[str], archive_path: str,
+ archive_format: str) -> tuple[bool, str]:
+ """Erstellt ein TAR-Archiv."""
+ mode = 'w'
+ if archive_format == '.tar.gz':
+ mode = 'w:gz'
+ elif archive_format == '.tar.bz2':
+ mode = 'w:bz2'
+ elif archive_format == '.tar.xz':
+ mode = 'w:xz'
+
+ with tarfile.open(archive_path, mode) as tf:
+ for source_path in source_paths:
+ tf.add(source_path, arcname=os.path.basename(source_path))
+
+ return True, f"TAR-Archiv erstellt: {archive_path}"
+
+ @classmethod
+ def list_contents(cls, archive_path: str) -> tuple[bool, list[str]]:
+ """
+ Listet den Inhalt eines Archivs auf.
+
+ Returns:
+ Tuple (Erfolg, Liste der Dateien oder Fehlermeldung)
+ """
+ archive_type = cls.get_archive_type(archive_path)
+ if not archive_type:
+ return False, ["Nicht unterstütztes Archiv-Format"]
+
+ try:
+ if archive_type == 'zip':
+ with zipfile.ZipFile(archive_path, 'r') as zf:
+ return True, zf.namelist()
+
+ elif archive_type.startswith('tar') or archive_type == 'tar':
+ mode = 'r'
+ if archive_type == 'tar.gz':
+ mode = 'r:gz'
+ elif archive_type == 'tar.bz2':
+ mode = 'r:bz2'
+ elif archive_type == 'tar.xz':
+ mode = 'r:xz'
+
+ with tarfile.open(archive_path, mode) as tf:
+ return True, tf.getnames()
+
+ else:
+ return False, [f"Auflisten für {archive_type} nicht unterstützt"]
+
+ except Exception as e:
+ return False, [str(e)]
diff --git a/src/utils/clipboard.py b/src/utils/clipboard.py
new file mode 100644
index 0000000..4a33bb2
--- /dev/null
+++ b/src/utils/clipboard.py
@@ -0,0 +1,136 @@
+"""Clipboard-Manager für Dateioperationen."""
+
+import os
+import shutil
+from enum import Enum
+from typing import Optional
+from PyQt6.QtCore import QObject, pyqtSignal
+
+
+class ClipboardMode(Enum):
+ """Modus für Zwischenablage-Operationen."""
+ COPY = "copy"
+ CUT = "cut"
+
+
+class FileClipboard(QObject):
+ """Singleton-Clipboard für Dateioperationen."""
+
+ _instance: Optional['FileClipboard'] = None
+
+ # Signale
+ clipboard_changed = pyqtSignal()
+
+ def __new__(cls):
+ if cls._instance is None:
+ cls._instance = super().__new__(cls)
+ cls._instance._initialized = False
+ return cls._instance
+
+ def __init__(self):
+ if self._initialized:
+ return
+ super().__init__()
+ self._files: list[str] = []
+ self._mode: ClipboardMode = ClipboardMode.COPY
+ self._initialized = True
+
+ @classmethod
+ def instance(cls) -> 'FileClipboard':
+ """Gibt die Singleton-Instanz zurück."""
+ if cls._instance is None:
+ cls._instance = FileClipboard()
+ return cls._instance
+
+ def copy(self, paths: list[str]):
+ """Kopiert Dateien in die Zwischenablage."""
+ self._files = [p for p in paths if os.path.exists(p)]
+ self._mode = ClipboardMode.COPY
+ self.clipboard_changed.emit()
+
+ def cut(self, paths: list[str]):
+ """Schneidet Dateien aus (zum Verschieben)."""
+ self._files = [p for p in paths if os.path.exists(p)]
+ self._mode = ClipboardMode.CUT
+ self.clipboard_changed.emit()
+
+ def paste(self, target_folder: str) -> tuple[int, list[str]]:
+ """
+ Fügt Dateien im Zielordner ein.
+
+ Returns:
+ Tuple (Anzahl erfolgreicher Operationen, Liste der Fehler)
+ """
+ if not self._files or not os.path.isdir(target_folder):
+ return 0, ["Keine Dateien zum Einfügen oder ungültiger Zielordner"]
+
+ success_count = 0
+ errors = []
+
+ for source_path in self._files:
+ if not os.path.exists(source_path):
+ errors.append(f"{os.path.basename(source_path)}: Datei nicht gefunden")
+ continue
+
+ name = os.path.basename(source_path)
+ target_path = os.path.join(target_folder, name)
+
+ # Bei Namenskonflikt umbenennen
+ target_path = self._get_unique_path(target_path)
+
+ try:
+ if self._mode == ClipboardMode.COPY:
+ if os.path.isdir(source_path):
+ shutil.copytree(source_path, target_path)
+ else:
+ shutil.copy2(source_path, target_path)
+ else: # CUT
+ shutil.move(source_path, target_path)
+ success_count += 1
+ except PermissionError:
+ errors.append(f"{name}: Keine Berechtigung")
+ except shutil.Error as e:
+ errors.append(f"{name}: {str(e)}")
+ except Exception as e:
+ errors.append(f"{name}: {str(e)}")
+
+ # Bei Ausschneiden die Zwischenablage leeren
+ if self._mode == ClipboardMode.CUT:
+ self.clear()
+
+ return success_count, errors
+
+ def _get_unique_path(self, path: str) -> str:
+ """Generiert einen eindeutigen Pfad bei Namenskonflikten."""
+ if not os.path.exists(path):
+ return path
+
+ base, ext = os.path.splitext(path)
+ counter = 1
+
+ while os.path.exists(path):
+ path = f"{base} ({counter}){ext}"
+ counter += 1
+
+ return path
+
+ def clear(self):
+ """Leert die Zwischenablage."""
+ self._files = []
+ self.clipboard_changed.emit()
+
+ def has_files(self) -> bool:
+ """Prüft ob Dateien in der Zwischenablage sind."""
+ return len(self._files) > 0
+
+ def get_files(self) -> list[str]:
+ """Gibt die Dateien in der Zwischenablage zurück."""
+ return self._files.copy()
+
+ def get_mode(self) -> ClipboardMode:
+ """Gibt den aktuellen Modus zurück."""
+ return self._mode
+
+ def get_file_count(self) -> int:
+ """Gibt die Anzahl der Dateien zurück."""
+ return len(self._files)
diff --git a/src/utils/themes.py b/src/utils/themes.py
index dd1f74f..621e7d5 100644
--- a/src/utils/themes.py
+++ b/src/utils/themes.py
@@ -140,11 +140,11 @@ class ThemeManager:
color: {theme['highlight_text']};
}}
- QTreeView::item:hover, QListView::item:hover {{
+ QTreeView::item:hover, QListView::item:hover, QTableView::item:hover {{
background-color: {theme['alternate_base']};
}}
- QTreeView::item:selected:hover, QListView::item:selected:hover {{
+ QTreeView::item:selected:hover, QListView::item:selected:hover, QTableView::item:selected:hover {{
background-color: {theme['highlight']};
color: {theme['highlight_text']};
}}
diff --git a/src/widgets/file_list.py b/src/widgets/file_list.py
index 132f162..90949d7 100644
--- a/src/widgets/file_list.py
+++ b/src/widgets/file_list.py
@@ -1,10 +1,11 @@
"""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
+ QTableView, QAbstractItemView, QMenu, QHeaderView, QMessageBox
)
from PyQt6.QtCore import (
Qt, QModelIndex, pyqtSignal, QAbstractTableModel, QMimeData,
@@ -13,6 +14,8 @@ from PyQt6.QtCore import (
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
@@ -73,9 +76,6 @@ class FileListModel(QAbstractTableModel):
return Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
elif role == Qt.ItemDataRole.ToolTipRole:
return item.path
- elif role == Qt.ItemDataRole.BackgroundRole:
- if index.row() == self._hovered_row:
- return QBrush(self._hover_color)
return None
@@ -171,11 +171,24 @@ class FileListWidget(QTableView):
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)
@@ -232,58 +245,280 @@ class FileListWidget(QTableView):
def _show_context_menu(self, position):
"""Zeigt das Kontextmenü."""
indexes = self.selectedIndexes()
- if not indexes:
- return
# 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)]
- if not items:
- return
-
menu = QMenu(self)
- if len(items) == 1:
- item = items[0]
-
- # Öffnen
- if item.is_dir:
- open_action = QAction("📂 Öffnen", self)
- open_action.triggered.connect(lambda: self.folder_entered.emit(item.path))
- else:
- open_action = QAction("🔗 Öffnen", self)
- open_action.triggered.connect(lambda: self._open_external(item.path))
- menu.addAction(open_action)
-
- menu.addSeparator()
-
- # Umbenennen
- rename_action = QAction("✏️ Umbenennen (F2)", self)
- rename_action.triggered.connect(lambda: self.file_rename_requested.emit(item.path))
- menu.addAction(rename_action)
-
- # Verschieben
- move_action = QAction("📦 Verschieben", self)
- move_action.triggered.connect(lambda: self.file_move_requested.emit(item.path))
- menu.addAction(move_action)
-
- menu.addSeparator()
-
- # Löschen (auch für mehrere)
- delete_action = QAction(f"🗑 Löschen ({len(items)} Element{'e' if len(items) > 1 else ''})", self)
- delete_action.triggered.connect(lambda: self._delete_items(items))
- menu.addAction(delete_action)
+ 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)
+
+ 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)
+
def _open_external(self, path: str):
"""Öffnet eine Datei mit der Standard-Anwendung."""
- import subprocess
try:
subprocess.Popen(['xdg-open', path])
except Exception as e:
- print(f"Fehler beim Öffnen: {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."""
@@ -301,6 +536,10 @@ class FileListWidget(QTableView):
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.list_model.refresh()
@@ -368,27 +607,67 @@ class FileListWidget(QTableView):
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)
- elif event.key() == Qt.Key.Key_Delete:
+ return
+
+ # Delete - Löschen
+ if event.key() == Qt.Key.Key_Delete:
items = self.get_selected_items()
if items:
self._delete_items(items)
- elif event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
+ 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)
- elif event.key() == Qt.Key.Key_Backspace:
+ 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)
- else:
- super().keyPressEvent(event)
+ 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."""