Initial commit: PyQt6 FileBrowser mit Preview
Features: - 3-Panel Layout (Ordnerbaum, Dateiliste, Vorschau) - PDF-Vorschau mit Zoom/Seiten-Modus Einstellungen - Bild- und Text-Vorschau - Dateioperationen (Umbenennen, Verschieben, Löschen) - Drag & Drop Support - Kontextmenü - Tastenkürzel (F2, Delete, F5, Backspace) - Breadcrumb-Navigation mit Copy/Paste Textzeile - Themes (Dark, Breeze Dark/Light) - Separates Vorschaufenster - Einstellungen werden gespeichert Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
eb61e746c1
15 changed files with 2695 additions and 0 deletions
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Qt
|
||||||
|
*.qmlc
|
||||||
|
*.jsc
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Config (optional - kann aktiviert werden wenn gewünscht)
|
||||||
|
# ~/.config/FileBrowser/
|
||||||
29
main.py
Normal file
29
main.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""FileBrowser - Ein Dateimanager mit Vorschau-Funktion in PyQt6."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
|
from src.main_window import MainWindow
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# High DPI Support
|
||||||
|
QApplication.setHighDpiScaleFactorRoundingPolicy(
|
||||||
|
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
||||||
|
)
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
app.setApplicationName("FileBrowser")
|
||||||
|
app.setOrganizationName("FileBrowser")
|
||||||
|
|
||||||
|
# Hauptfenster erstellen und anzeigen
|
||||||
|
window = MainWindow()
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
PyQt6>=6.6.0
|
||||||
|
PyQt6-Pdf>=6.6.0
|
||||||
|
PyQt6-PdfWidgets>=6.6.0
|
||||||
1
src/__init__.py
Normal file
1
src/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# FileBrowser - PyQt6 File Manager with Preview
|
||||||
230
src/dialogs.py
Normal file
230
src/dialogs.py
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
"""Dialoge für Dateioperationen."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||||
|
QPushButton, QTreeView, QDialogButtonBox, QMessageBox,
|
||||||
|
QFrame
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt, QDir
|
||||||
|
from PyQt6.QtGui import QFileSystemModel
|
||||||
|
|
||||||
|
|
||||||
|
class RenameDialog(QDialog):
|
||||||
|
"""Dialog zum Umbenennen von Dateien."""
|
||||||
|
|
||||||
|
def __init__(self, path: str, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.path = path
|
||||||
|
self.old_name = os.path.basename(path)
|
||||||
|
self.new_name = ""
|
||||||
|
|
||||||
|
self.setWindowTitle("Umbenennen")
|
||||||
|
self.setModal(True)
|
||||||
|
self.setMinimumWidth(400)
|
||||||
|
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Alte Name anzeigen
|
||||||
|
old_label = QLabel(f"Alter Name: {self.old_name}")
|
||||||
|
layout.addWidget(old_label)
|
||||||
|
|
||||||
|
# Eingabefeld für neuen Namen
|
||||||
|
new_label = QLabel("Neuer Name:")
|
||||||
|
layout.addWidget(new_label)
|
||||||
|
|
||||||
|
self.name_input = QLineEdit()
|
||||||
|
self.name_input.setText(self.old_name)
|
||||||
|
self.name_input.returnPressed.connect(self.accept)
|
||||||
|
layout.addWidget(self.name_input)
|
||||||
|
|
||||||
|
# Name ohne Erweiterung selektieren
|
||||||
|
name = Path(self.old_name)
|
||||||
|
if name.suffix:
|
||||||
|
self.name_input.setSelection(0, len(name.stem))
|
||||||
|
else:
|
||||||
|
self.name_input.selectAll()
|
||||||
|
|
||||||
|
# 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 accept(self):
|
||||||
|
"""Validiert und akzeptiert die Eingabe."""
|
||||||
|
self.new_name = self.name_input.text().strip()
|
||||||
|
|
||||||
|
if not self.new_name:
|
||||||
|
QMessageBox.warning(self, "Fehler", "Name darf nicht leer sein.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if '/' in self.new_name or '\\' in self.new_name:
|
||||||
|
QMessageBox.warning(self, "Fehler", "Name darf keine Pfadtrennzeichen enthalten.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.new_name == self.old_name:
|
||||||
|
self.reject()
|
||||||
|
return
|
||||||
|
|
||||||
|
new_path = os.path.join(os.path.dirname(self.path), self.new_name)
|
||||||
|
if os.path.exists(new_path):
|
||||||
|
QMessageBox.warning(self, "Fehler", "Eine Datei mit diesem Namen existiert bereits.")
|
||||||
|
return
|
||||||
|
|
||||||
|
super().accept()
|
||||||
|
|
||||||
|
def get_new_name(self) -> str:
|
||||||
|
"""Gibt den neuen Namen zurück."""
|
||||||
|
return self.new_name
|
||||||
|
|
||||||
|
|
||||||
|
class MoveDialog(QDialog):
|
||||||
|
"""Dialog zum Verschieben von Dateien."""
|
||||||
|
|
||||||
|
def __init__(self, source_path: str, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.source_path = source_path
|
||||||
|
self.target_folder = ""
|
||||||
|
|
||||||
|
self.setWindowTitle("Verschieben")
|
||||||
|
self.setModal(True)
|
||||||
|
self.setMinimumSize(500, 400)
|
||||||
|
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Quelle anzeigen
|
||||||
|
source_label = QLabel(f"Verschiebe: {os.path.basename(self.source_path)}")
|
||||||
|
source_label.setWordWrap(True)
|
||||||
|
layout.addWidget(source_label)
|
||||||
|
|
||||||
|
layout.addWidget(QLabel("Zielordner:"))
|
||||||
|
|
||||||
|
# Ordnerbaum
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Nur Name-Spalte anzeigen
|
||||||
|
for i in range(1, self.model.columnCount()):
|
||||||
|
self.tree.hideColumn(i)
|
||||||
|
|
||||||
|
# Aktuellen Ordner expandieren
|
||||||
|
current_dir = os.path.dirname(self.source_path)
|
||||||
|
index = self.model.index(current_dir)
|
||||||
|
if index.isValid():
|
||||||
|
self.tree.setCurrentIndex(index)
|
||||||
|
self.tree.scrollTo(index)
|
||||||
|
|
||||||
|
layout.addWidget(self.tree)
|
||||||
|
|
||||||
|
# Ausgewählter Pfad
|
||||||
|
self.path_label = QLabel()
|
||||||
|
self.path_label.setWordWrap(True)
|
||||||
|
layout.addWidget(self.path_label)
|
||||||
|
|
||||||
|
self.tree.clicked.connect(self._on_selection_changed)
|
||||||
|
|
||||||
|
# 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 und akzeptiert die Auswahl."""
|
||||||
|
if not self.target_folder:
|
||||||
|
QMessageBox.warning(self, "Fehler", "Bitte wähle einen Zielordner aus.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not os.path.isdir(self.target_folder):
|
||||||
|
QMessageBox.warning(self, "Fehler", "Der ausgewählte Pfad ist kein Ordner.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Prüfen ob Ziel im Quellpfad liegt (bei Ordnern)
|
||||||
|
if os.path.isdir(self.source_path):
|
||||||
|
if self.target_folder.startswith(self.source_path):
|
||||||
|
QMessageBox.warning(
|
||||||
|
self, "Fehler",
|
||||||
|
"Ein Ordner kann nicht in sich selbst verschoben werden."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
super().accept()
|
||||||
|
|
||||||
|
def get_target_folder(self) -> str:
|
||||||
|
"""Gibt den Zielordner zurück."""
|
||||||
|
return self.target_folder
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteDialog(QDialog):
|
||||||
|
"""Bestätigungsdialog zum Löschen."""
|
||||||
|
|
||||||
|
def __init__(self, paths: list, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.paths = paths
|
||||||
|
|
||||||
|
self.setWindowTitle("Löschen bestätigen")
|
||||||
|
self.setModal(True)
|
||||||
|
self.setMinimumWidth(400)
|
||||||
|
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Warnung
|
||||||
|
if len(self.paths) == 1:
|
||||||
|
name = os.path.basename(self.paths[0])
|
||||||
|
is_dir = os.path.isdir(self.paths[0])
|
||||||
|
type_str = "Ordner" if is_dir else "Datei"
|
||||||
|
message = f"Möchtest du diese {type_str} wirklich löschen?\n\n{name}"
|
||||||
|
else:
|
||||||
|
message = f"Möchtest du diese {len(self.paths)} Elemente wirklich löschen?"
|
||||||
|
|
||||||
|
label = QLabel(message)
|
||||||
|
label.setWordWrap(True)
|
||||||
|
layout.addWidget(label)
|
||||||
|
|
||||||
|
# Warnung bei Ordnern
|
||||||
|
has_dirs = any(os.path.isdir(p) for p in self.paths)
|
||||||
|
if has_dirs:
|
||||||
|
warning = QLabel("⚠️ Ordner werden mit allen Inhalten gelöscht!")
|
||||||
|
warning.setStyleSheet("color: #ef4444;")
|
||||||
|
layout.addWidget(warning)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
button_box = QDialogButtonBox()
|
||||||
|
|
||||||
|
delete_btn = QPushButton("🗑 Löschen")
|
||||||
|
delete_btn.clicked.connect(self.accept)
|
||||||
|
button_box.addButton(delete_btn, QDialogButtonBox.ButtonRole.AcceptRole)
|
||||||
|
|
||||||
|
cancel_btn = QPushButton("Abbrechen")
|
||||||
|
cancel_btn.clicked.connect(self.reject)
|
||||||
|
button_box.addButton(cancel_btn, QDialogButtonBox.ButtonRole.RejectRole)
|
||||||
|
|
||||||
|
layout.addWidget(button_box)
|
||||||
603
src/main_window.py
Normal file
603
src/main_window.py
Normal file
|
|
@ -0,0 +1,603 @@
|
||||||
|
"""Hauptfenster des FileBrowsers."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSplitter,
|
||||||
|
QMenuBar, QMenu, QToolBar, QStatusBar, QLabel, QComboBox,
|
||||||
|
QMessageBox, QFrame, QApplication
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt, QSettings, QTimer
|
||||||
|
from PyQt6.QtGui import QAction, QKeySequence, QShortcut
|
||||||
|
|
||||||
|
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
|
||||||
|
from .preview_window import PreviewWindow
|
||||||
|
from .utils.themes import ThemeManager
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
"""Hauptfenster des FileBrowsers mit 3-Panel Layout."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.settings = QSettings('FileBrowser', 'FileBrowser')
|
||||||
|
self.theme_manager = ThemeManager()
|
||||||
|
self.preview_window = None
|
||||||
|
self._current_path = ""
|
||||||
|
|
||||||
|
self.setWindowTitle("FileBrowser")
|
||||||
|
self.setMinimumSize(800, 600)
|
||||||
|
|
||||||
|
self._setup_ui()
|
||||||
|
self._setup_menubar()
|
||||||
|
self._setup_toolbar()
|
||||||
|
self._setup_statusbar()
|
||||||
|
self._setup_shortcuts()
|
||||||
|
self._connect_signals()
|
||||||
|
self._load_settings()
|
||||||
|
self._apply_theme()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
"""Erstellt das UI."""
|
||||||
|
central = QWidget()
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
layout = QVBoxLayout(central)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
|
||||||
|
# Breadcrumb-Navigation
|
||||||
|
self.breadcrumb = BreadcrumbWidget()
|
||||||
|
breadcrumb_frame = QFrame()
|
||||||
|
breadcrumb_layout = QHBoxLayout(breadcrumb_frame)
|
||||||
|
breadcrumb_layout.setContentsMargins(8, 4, 8, 4)
|
||||||
|
breadcrumb_layout.addWidget(self.breadcrumb)
|
||||||
|
layout.addWidget(breadcrumb_frame)
|
||||||
|
|
||||||
|
# Hauptsplitter (3 Panels)
|
||||||
|
self.main_splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||||
|
self.main_splitter.setChildrenCollapsible(False)
|
||||||
|
|
||||||
|
# Panel 1: Ordnerbaum
|
||||||
|
self.folder_tree = FolderTreeWidget()
|
||||||
|
self.folder_tree.setMinimumWidth(150)
|
||||||
|
self.main_splitter.addWidget(self.folder_tree)
|
||||||
|
|
||||||
|
# Panel 2: Dateiliste
|
||||||
|
self.file_list = FileListWidget()
|
||||||
|
self.file_list.setMinimumWidth(200)
|
||||||
|
self.main_splitter.addWidget(self.file_list)
|
||||||
|
|
||||||
|
# Panel 3: Vorschau
|
||||||
|
self.preview_panel = PreviewPanel()
|
||||||
|
self.preview_panel.setMinimumWidth(200)
|
||||||
|
self.main_splitter.addWidget(self.preview_panel)
|
||||||
|
|
||||||
|
# Standardgrößen
|
||||||
|
self.main_splitter.setSizes([250, 500, 350])
|
||||||
|
self.main_splitter.setStretchFactor(0, 0)
|
||||||
|
self.main_splitter.setStretchFactor(1, 1)
|
||||||
|
self.main_splitter.setStretchFactor(2, 0)
|
||||||
|
|
||||||
|
# Splitter soll den ganzen verfügbaren Platz füllen
|
||||||
|
layout.addWidget(self.main_splitter, 1)
|
||||||
|
|
||||||
|
def _setup_menubar(self):
|
||||||
|
"""Erstellt die Menüleiste."""
|
||||||
|
menubar = self.menuBar()
|
||||||
|
|
||||||
|
# Datei-Menü
|
||||||
|
file_menu = menubar.addMenu("&Datei")
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
quit_action = QAction("Beenden", self)
|
||||||
|
quit_action.setShortcut(QKeySequence.StandardKey.Quit)
|
||||||
|
quit_action.triggered.connect(self.close)
|
||||||
|
file_menu.addAction(quit_action)
|
||||||
|
|
||||||
|
# Bearbeiten-Menü
|
||||||
|
edit_menu = menubar.addMenu("&Bearbeiten")
|
||||||
|
|
||||||
|
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.triggered.connect(self._move_selected)
|
||||||
|
edit_menu.addAction(move_action)
|
||||||
|
|
||||||
|
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.setShortcut(QKeySequence("F5"))
|
||||||
|
refresh_action.triggered.connect(self._refresh)
|
||||||
|
edit_menu.addAction(refresh_action)
|
||||||
|
|
||||||
|
# Ansicht-Menü
|
||||||
|
view_menu = menubar.addMenu("&Ansicht")
|
||||||
|
|
||||||
|
toggle_tree_action = QAction("Ordnerbaum", self)
|
||||||
|
toggle_tree_action.setCheckable(True)
|
||||||
|
toggle_tree_action.setChecked(True)
|
||||||
|
toggle_tree_action.triggered.connect(
|
||||||
|
lambda checked: self.folder_tree.setVisible(checked)
|
||||||
|
)
|
||||||
|
view_menu.addAction(toggle_tree_action)
|
||||||
|
|
||||||
|
toggle_preview_action = QAction("Vorschau", self)
|
||||||
|
toggle_preview_action.setCheckable(True)
|
||||||
|
toggle_preview_action.setChecked(True)
|
||||||
|
toggle_preview_action.triggered.connect(
|
||||||
|
lambda checked: self.preview_panel.setVisible(checked)
|
||||||
|
)
|
||||||
|
view_menu.addAction(toggle_preview_action)
|
||||||
|
|
||||||
|
view_menu.addSeparator()
|
||||||
|
|
||||||
|
detach_preview_action = QAction("Vorschau abtrennen", self)
|
||||||
|
detach_preview_action.triggered.connect(self._detach_preview)
|
||||||
|
view_menu.addAction(detach_preview_action)
|
||||||
|
|
||||||
|
view_menu.addSeparator()
|
||||||
|
|
||||||
|
# Theme-Untermenü
|
||||||
|
theme_menu = view_menu.addMenu("Theme")
|
||||||
|
self.theme_actions = []
|
||||||
|
|
||||||
|
for theme_id, theme_name in self.theme_manager.get_available_themes():
|
||||||
|
action = QAction(theme_name, self)
|
||||||
|
action.setCheckable(True)
|
||||||
|
action.setData(theme_id)
|
||||||
|
action.triggered.connect(lambda checked, tid=theme_id: self._change_theme(tid))
|
||||||
|
theme_menu.addAction(action)
|
||||||
|
self.theme_actions.append(action)
|
||||||
|
|
||||||
|
view_menu.addSeparator()
|
||||||
|
|
||||||
|
# PDF-Einstellungen Untermenü
|
||||||
|
pdf_menu = view_menu.addMenu("PDF-Vorschau")
|
||||||
|
|
||||||
|
# Zoom-Modi
|
||||||
|
pdf_zoom_menu = pdf_menu.addMenu("Zoom-Modus")
|
||||||
|
self.pdf_zoom_actions = []
|
||||||
|
zoom_modes = [
|
||||||
|
('fit_width', 'Seitenbreite'),
|
||||||
|
('fit_page', 'Ganze Seite'),
|
||||||
|
('custom', 'Benutzerdefiniert'),
|
||||||
|
]
|
||||||
|
for mode_id, mode_name in zoom_modes:
|
||||||
|
action = QAction(mode_name, self)
|
||||||
|
action.setCheckable(True)
|
||||||
|
action.setData(mode_id)
|
||||||
|
action.triggered.connect(lambda checked, mid=mode_id: self._set_pdf_zoom_mode(mid))
|
||||||
|
pdf_zoom_menu.addAction(action)
|
||||||
|
self.pdf_zoom_actions.append(action)
|
||||||
|
|
||||||
|
# Seiten-Modi
|
||||||
|
pdf_page_menu = pdf_menu.addMenu("Seiten-Modus")
|
||||||
|
self.pdf_page_actions = []
|
||||||
|
page_modes = [
|
||||||
|
('single', 'Einzelseite'),
|
||||||
|
('multi', 'Mehrere Seiten'),
|
||||||
|
]
|
||||||
|
for mode_id, mode_name in page_modes:
|
||||||
|
action = QAction(mode_name, self)
|
||||||
|
action.setCheckable(True)
|
||||||
|
action.setData(mode_id)
|
||||||
|
action.triggered.connect(lambda checked, mid=mode_id: self._set_pdf_page_mode(mid))
|
||||||
|
pdf_page_menu.addAction(action)
|
||||||
|
self.pdf_page_actions.append(action)
|
||||||
|
|
||||||
|
# Hilfe-Menü
|
||||||
|
help_menu = menubar.addMenu("&Hilfe")
|
||||||
|
|
||||||
|
about_action = QAction("Über", self)
|
||||||
|
about_action.triggered.connect(self._show_about)
|
||||||
|
help_menu.addAction(about_action)
|
||||||
|
|
||||||
|
def _setup_toolbar(self):
|
||||||
|
"""Erstellt die Werkzeugleiste."""
|
||||||
|
toolbar = QToolBar()
|
||||||
|
toolbar.setMovable(False)
|
||||||
|
self.addToolBar(toolbar)
|
||||||
|
|
||||||
|
# Zurück-Button
|
||||||
|
back_action = QAction("⬅", self)
|
||||||
|
back_action.setToolTip("Zurück (Backspace)")
|
||||||
|
back_action.triggered.connect(self._go_back)
|
||||||
|
toolbar.addAction(back_action)
|
||||||
|
|
||||||
|
# Aktualisieren-Button
|
||||||
|
refresh_action = QAction("🔄", self)
|
||||||
|
refresh_action.setToolTip("Aktualisieren (F5)")
|
||||||
|
refresh_action.triggered.connect(self._refresh)
|
||||||
|
toolbar.addAction(refresh_action)
|
||||||
|
|
||||||
|
toolbar.addSeparator()
|
||||||
|
|
||||||
|
# Home-Button
|
||||||
|
home_action = QAction("🏠", self)
|
||||||
|
home_action.setToolTip("Home-Verzeichnis")
|
||||||
|
home_action.triggered.connect(self._go_home)
|
||||||
|
toolbar.addAction(home_action)
|
||||||
|
|
||||||
|
toolbar.addSeparator()
|
||||||
|
|
||||||
|
# Theme-Auswahl
|
||||||
|
toolbar.addWidget(QLabel("Theme: "))
|
||||||
|
self.theme_combo = QComboBox()
|
||||||
|
for theme_id, theme_name in self.theme_manager.get_available_themes():
|
||||||
|
self.theme_combo.addItem(theme_name, theme_id)
|
||||||
|
self.theme_combo.currentIndexChanged.connect(self._on_theme_combo_changed)
|
||||||
|
toolbar.addWidget(self.theme_combo)
|
||||||
|
|
||||||
|
def _setup_statusbar(self):
|
||||||
|
"""Erstellt die Statusleiste."""
|
||||||
|
self.statusbar = QStatusBar()
|
||||||
|
self.setStatusBar(self.statusbar)
|
||||||
|
|
||||||
|
self.path_label = QLabel()
|
||||||
|
self.statusbar.addWidget(self.path_label, 1)
|
||||||
|
|
||||||
|
self.count_label = QLabel()
|
||||||
|
self.statusbar.addPermanentWidget(self.count_label)
|
||||||
|
|
||||||
|
def _setup_shortcuts(self):
|
||||||
|
"""Richtet Tastenkürzel ein."""
|
||||||
|
# Backspace für zurück
|
||||||
|
QShortcut(QKeySequence("Backspace"), self, self._go_back)
|
||||||
|
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
self.file_list.file_rename_requested.connect(self._rename_file)
|
||||||
|
self.file_list.file_delete_requested.connect(self._delete_file)
|
||||||
|
self.file_list.file_move_requested.connect(self._move_file)
|
||||||
|
self.file_list.files_dropped.connect(self._handle_files_dropped)
|
||||||
|
|
||||||
|
# Breadcrumb
|
||||||
|
self.breadcrumb.path_clicked.connect(self._navigate_to)
|
||||||
|
self.breadcrumb.path_entered.connect(self._navigate_to)
|
||||||
|
|
||||||
|
# Preview-Panel
|
||||||
|
self.preview_panel.rename_requested.connect(self._rename_file)
|
||||||
|
self.preview_panel.move_requested.connect(self._move_file)
|
||||||
|
self.preview_panel.delete_requested.connect(self._delete_file)
|
||||||
|
self.preview_panel.open_external_requested.connect(self._open_external)
|
||||||
|
self.preview_panel.detach_requested.connect(self._detach_preview)
|
||||||
|
|
||||||
|
def _load_settings(self):
|
||||||
|
"""Lädt gespeicherte Einstellungen."""
|
||||||
|
# Fenstergeometrie
|
||||||
|
geometry = self.settings.value('window_geometry')
|
||||||
|
if geometry:
|
||||||
|
self.restoreGeometry(geometry)
|
||||||
|
else:
|
||||||
|
self.resize(1200, 800)
|
||||||
|
|
||||||
|
# Splitter-Größen
|
||||||
|
splitter_sizes = self.settings.value('splitter_sizes')
|
||||||
|
if splitter_sizes:
|
||||||
|
self.main_splitter.setSizes([int(s) for s in splitter_sizes])
|
||||||
|
|
||||||
|
# Letzter Pfad
|
||||||
|
last_path = self.settings.value('last_path', os.path.expanduser('~'))
|
||||||
|
if os.path.exists(last_path):
|
||||||
|
self._navigate_to(last_path)
|
||||||
|
else:
|
||||||
|
self._navigate_to(os.path.expanduser('~'))
|
||||||
|
|
||||||
|
# Theme
|
||||||
|
theme = self.settings.value('theme', 'dark')
|
||||||
|
index = self.theme_combo.findData(theme)
|
||||||
|
if index >= 0:
|
||||||
|
self.theme_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
def _save_settings(self):
|
||||||
|
"""Speichert Einstellungen."""
|
||||||
|
self.settings.setValue('window_geometry', self.saveGeometry())
|
||||||
|
self.settings.setValue('splitter_sizes', self.main_splitter.sizes())
|
||||||
|
self.settings.setValue('last_path', self._current_path)
|
||||||
|
self.settings.setValue('theme', self.theme_manager.get_current_theme())
|
||||||
|
|
||||||
|
def _apply_theme(self):
|
||||||
|
"""Wendet das aktuelle Theme an."""
|
||||||
|
theme = self.theme_manager.get_current_theme()
|
||||||
|
stylesheet = self.theme_manager.apply_theme(theme)
|
||||||
|
self.setStyleSheet(stylesheet)
|
||||||
|
|
||||||
|
# Theme-Menü-Aktionen aktualisieren
|
||||||
|
for action in self.theme_actions:
|
||||||
|
action.setChecked(action.data() == theme)
|
||||||
|
|
||||||
|
# PDF-Menü-Aktionen aktualisieren
|
||||||
|
self._update_pdf_menu_checks()
|
||||||
|
|
||||||
|
def _change_theme(self, theme_id: str):
|
||||||
|
"""Wechselt das Theme."""
|
||||||
|
stylesheet = self.theme_manager.apply_theme(theme_id)
|
||||||
|
self.setStyleSheet(stylesheet)
|
||||||
|
|
||||||
|
# Combo-Box aktualisieren
|
||||||
|
index = self.theme_combo.findData(theme_id)
|
||||||
|
if index >= 0:
|
||||||
|
self.theme_combo.blockSignals(True)
|
||||||
|
self.theme_combo.setCurrentIndex(index)
|
||||||
|
self.theme_combo.blockSignals(False)
|
||||||
|
|
||||||
|
# Menü-Aktionen aktualisieren
|
||||||
|
for action in self.theme_actions:
|
||||||
|
action.setChecked(action.data() == theme_id)
|
||||||
|
|
||||||
|
# Preview-Fenster aktualisieren
|
||||||
|
if self.preview_window:
|
||||||
|
self.preview_window.setStyleSheet(stylesheet)
|
||||||
|
|
||||||
|
def _on_theme_combo_changed(self, index):
|
||||||
|
"""Behandelt Theme-Auswahl in der Combo-Box."""
|
||||||
|
theme_id = self.theme_combo.itemData(index)
|
||||||
|
if theme_id:
|
||||||
|
self._change_theme(theme_id)
|
||||||
|
|
||||||
|
def _navigate_to(self, path: str):
|
||||||
|
"""Navigiert zu einem Ordner."""
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
return
|
||||||
|
|
||||||
|
self._current_path = path
|
||||||
|
self.file_list.set_path(path)
|
||||||
|
self.breadcrumb.set_path(path)
|
||||||
|
self.folder_tree.navigate_to(path)
|
||||||
|
self.preview_panel.clear()
|
||||||
|
|
||||||
|
# Status aktualisieren
|
||||||
|
self.path_label.setText(path)
|
||||||
|
self._update_count()
|
||||||
|
|
||||||
|
def _update_count(self):
|
||||||
|
"""Aktualisiert die Elementanzahl."""
|
||||||
|
count = len(self.file_list.list_model.items)
|
||||||
|
self.count_label.setText(f"{count} Elemente")
|
||||||
|
|
||||||
|
def _on_file_selected(self, path: str, name: str):
|
||||||
|
"""Behandelt Dateiauswahl."""
|
||||||
|
self.preview_panel.load_file(path, name)
|
||||||
|
|
||||||
|
# Preview-Fenster aktualisieren
|
||||||
|
if self.preview_window and self.preview_window.isVisible():
|
||||||
|
self.preview_window.load_file(path, name)
|
||||||
|
|
||||||
|
def _open_external(self, path: str):
|
||||||
|
"""Öffnet eine Datei extern."""
|
||||||
|
import subprocess
|
||||||
|
try:
|
||||||
|
subprocess.Popen(['xdg-open', path])
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Fehler", f"Konnte Datei nicht öffnen: {e}")
|
||||||
|
|
||||||
|
def _go_back(self):
|
||||||
|
"""Navigiert zum übergeordneten Ordner."""
|
||||||
|
if self._current_path:
|
||||||
|
parent = os.path.dirname(self._current_path)
|
||||||
|
if parent and parent != self._current_path:
|
||||||
|
self._navigate_to(parent)
|
||||||
|
|
||||||
|
def _go_home(self):
|
||||||
|
"""Navigiert zum Home-Verzeichnis."""
|
||||||
|
self._navigate_to(os.path.expanduser('~'))
|
||||||
|
|
||||||
|
def _refresh(self):
|
||||||
|
"""Aktualisiert die Ansicht."""
|
||||||
|
self.file_list.refresh()
|
||||||
|
self.folder_tree.refresh()
|
||||||
|
self._update_count()
|
||||||
|
self.statusbar.showMessage("Aktualisiert", 2000)
|
||||||
|
|
||||||
|
def _rename_selected(self):
|
||||||
|
"""Benennt das ausgewählte Element um."""
|
||||||
|
path = self.file_list.get_selected_path()
|
||||||
|
if path:
|
||||||
|
self._rename_file(path)
|
||||||
|
|
||||||
|
def _rename_file(self, path: str):
|
||||||
|
"""Öffnet den Umbenennen-Dialog."""
|
||||||
|
dialog = RenameDialog(path, self)
|
||||||
|
if dialog.exec():
|
||||||
|
new_name = dialog.get_new_name()
|
||||||
|
new_path = os.path.join(os.path.dirname(path), new_name)
|
||||||
|
try:
|
||||||
|
os.rename(path, new_path)
|
||||||
|
self._refresh()
|
||||||
|
self.statusbar.showMessage(f"Umbenannt zu: {new_name}", 3000)
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Fehler", f"Umbenennen fehlgeschlagen: {e}")
|
||||||
|
|
||||||
|
def _move_selected(self):
|
||||||
|
"""Verschiebt das ausgewählte Element."""
|
||||||
|
path = self.file_list.get_selected_path()
|
||||||
|
if path:
|
||||||
|
self._move_file(path)
|
||||||
|
|
||||||
|
def _move_file(self, path: str):
|
||||||
|
"""Öffnet den Verschieben-Dialog."""
|
||||||
|
dialog = MoveDialog(path, self)
|
||||||
|
if dialog.exec():
|
||||||
|
target = dialog.get_target_folder()
|
||||||
|
try:
|
||||||
|
name = os.path.basename(path)
|
||||||
|
new_path = os.path.join(target, name)
|
||||||
|
shutil.move(path, new_path)
|
||||||
|
self._refresh()
|
||||||
|
self.statusbar.showMessage(f"Verschoben nach: {target}", 3000)
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Fehler", f"Verschieben fehlgeschlagen: {e}")
|
||||||
|
|
||||||
|
def _delete_selected(self):
|
||||||
|
"""Löscht die ausgewählten Elemente."""
|
||||||
|
items = self.file_list.get_selected_items()
|
||||||
|
if items:
|
||||||
|
paths = [item.path for item in items]
|
||||||
|
self._delete_files(paths)
|
||||||
|
|
||||||
|
def _delete_file(self, path: str):
|
||||||
|
"""Löscht eine einzelne Datei."""
|
||||||
|
self._delete_files([path])
|
||||||
|
|
||||||
|
def _delete_files(self, paths: list):
|
||||||
|
"""Löscht mehrere Dateien/Ordner."""
|
||||||
|
dialog = DeleteDialog(paths, self)
|
||||||
|
if dialog.exec():
|
||||||
|
errors = []
|
||||||
|
for path in paths:
|
||||||
|
try:
|
||||||
|
if os.path.isdir(path):
|
||||||
|
shutil.rmtree(path)
|
||||||
|
else:
|
||||||
|
os.remove(path)
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"{os.path.basename(path)}: {e}")
|
||||||
|
|
||||||
|
self._refresh()
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self, "Fehler",
|
||||||
|
"Einige Elemente konnten nicht gelöscht werden:\n\n" +
|
||||||
|
"\n".join(errors)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.statusbar.showMessage(
|
||||||
|
f"{len(paths)} Element{'e' if len(paths) > 1 else ''} gelöscht",
|
||||||
|
3000
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_files_dropped(self, source_paths: list, target_folder: str):
|
||||||
|
"""Behandelt per Drag & Drop verschobene Dateien."""
|
||||||
|
errors = []
|
||||||
|
moved = 0
|
||||||
|
|
||||||
|
for path in source_paths:
|
||||||
|
try:
|
||||||
|
name = os.path.basename(path)
|
||||||
|
new_path = os.path.join(target_folder, name)
|
||||||
|
shutil.move(path, new_path)
|
||||||
|
moved += 1
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"{os.path.basename(path)}: {e}")
|
||||||
|
|
||||||
|
self._refresh()
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self, "Fehler",
|
||||||
|
"Einige Elemente konnten nicht verschoben werden:\n\n" +
|
||||||
|
"\n".join(errors)
|
||||||
|
)
|
||||||
|
elif moved > 0:
|
||||||
|
self.statusbar.showMessage(
|
||||||
|
f"{moved} Element{'e' if moved > 1 else ''} verschoben",
|
||||||
|
3000
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_new_folder(self):
|
||||||
|
"""Erstellt einen neuen Ordner."""
|
||||||
|
from PyQt6.QtWidgets import QInputDialog
|
||||||
|
|
||||||
|
name, ok = QInputDialog.getText(
|
||||||
|
self, "Neuer Ordner", "Ordnername:"
|
||||||
|
)
|
||||||
|
if ok and name:
|
||||||
|
new_path = os.path.join(self._current_path, name)
|
||||||
|
try:
|
||||||
|
os.makedirs(new_path, exist_ok=True)
|
||||||
|
self._refresh()
|
||||||
|
self.statusbar.showMessage(f"Ordner erstellt: {name}", 3000)
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Fehler", f"Fehler beim Erstellen: {e}")
|
||||||
|
|
||||||
|
def _detach_preview(self):
|
||||||
|
"""Öffnet die Vorschau in einem separaten Fenster."""
|
||||||
|
if not self.preview_window:
|
||||||
|
self.preview_window = PreviewWindow(self)
|
||||||
|
self.preview_window.closed.connect(self._on_preview_window_closed)
|
||||||
|
|
||||||
|
# Theme übertragen
|
||||||
|
stylesheet = self.theme_manager.get_stylesheet()
|
||||||
|
self.preview_window.setStyleSheet(stylesheet)
|
||||||
|
|
||||||
|
# Aktuelle Datei übertragen
|
||||||
|
path, name = self.preview_panel.get_current_file()
|
||||||
|
if path and name:
|
||||||
|
self.preview_window.load_file(path, name)
|
||||||
|
|
||||||
|
self.preview_window.show()
|
||||||
|
self.preview_window.raise_()
|
||||||
|
|
||||||
|
def _on_preview_window_closed(self):
|
||||||
|
"""Behandelt das Schließen des Preview-Fensters."""
|
||||||
|
pass # Preview-Fenster bleibt im Speicher für schnelles Wiedereröffnen
|
||||||
|
|
||||||
|
def _set_pdf_zoom_mode(self, mode_id: str):
|
||||||
|
"""Setzt den PDF-Zoom-Modus."""
|
||||||
|
settings = self.preview_panel.pdf_preview.get_settings()
|
||||||
|
settings['zoom_mode'] = mode_id
|
||||||
|
self.preview_panel.pdf_preview.set_settings(settings)
|
||||||
|
self._update_pdf_menu_checks()
|
||||||
|
|
||||||
|
def _set_pdf_page_mode(self, mode_id: str):
|
||||||
|
"""Setzt den PDF-Seiten-Modus."""
|
||||||
|
settings = self.preview_panel.pdf_preview.get_settings()
|
||||||
|
settings['page_mode'] = mode_id
|
||||||
|
self.preview_panel.pdf_preview.set_settings(settings)
|
||||||
|
self._update_pdf_menu_checks()
|
||||||
|
|
||||||
|
def _update_pdf_menu_checks(self):
|
||||||
|
"""Aktualisiert die Häkchen im PDF-Menü."""
|
||||||
|
settings = self.preview_panel.pdf_preview.get_settings()
|
||||||
|
for action in self.pdf_zoom_actions:
|
||||||
|
action.setChecked(action.data() == settings['zoom_mode'])
|
||||||
|
for action in self.pdf_page_actions:
|
||||||
|
action.setChecked(action.data() == settings['page_mode'])
|
||||||
|
|
||||||
|
def _show_about(self):
|
||||||
|
"""Zeigt den Über-Dialog."""
|
||||||
|
QMessageBox.about(
|
||||||
|
self,
|
||||||
|
"Über FileBrowser",
|
||||||
|
"FileBrowser\n\n"
|
||||||
|
"Ein einfacher Dateimanager mit Vorschau-Funktion.\n\n"
|
||||||
|
"Erstellt mit PyQt6"
|
||||||
|
)
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""Behandelt das Schließen des Fensters."""
|
||||||
|
self._save_settings()
|
||||||
|
|
||||||
|
# Preview-Fenster schließen
|
||||||
|
if self.preview_window:
|
||||||
|
self.preview_window.close()
|
||||||
|
|
||||||
|
super().closeEvent(event)
|
||||||
160
src/preview_window.py
Normal file
160
src/preview_window.py
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
"""Separates Vorschaufenster."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QMainWindow, QWidget, QVBoxLayout, QLabel, QFrame,
|
||||||
|
QSizePolicy
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt, pyqtSignal, QSettings
|
||||||
|
from PyQt6.QtGui import QFont
|
||||||
|
|
||||||
|
from .widgets.preview_panel import ImagePreview, TextPreview, PdfPreview, NoPreview
|
||||||
|
from .utils.file_utils import format_file_size, get_file_type
|
||||||
|
|
||||||
|
|
||||||
|
class PreviewWindow(QMainWindow):
|
||||||
|
"""Separates Fenster für die Dateivorschau."""
|
||||||
|
|
||||||
|
closed = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._current_path = ""
|
||||||
|
self._current_name = ""
|
||||||
|
|
||||||
|
self.setWindowTitle("Vorschau")
|
||||||
|
self.setMinimumSize(400, 300)
|
||||||
|
|
||||||
|
self._setup_ui()
|
||||||
|
self._load_settings()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
central = QWidget()
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
layout = QVBoxLayout(central)
|
||||||
|
layout.setContentsMargins(8, 8, 8, 8)
|
||||||
|
layout.setSpacing(8)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
header = QFrame()
|
||||||
|
header_layout = QVBoxLayout(header)
|
||||||
|
header_layout.setContentsMargins(0, 0, 0, 8)
|
||||||
|
header_layout.setSpacing(4)
|
||||||
|
|
||||||
|
self.name_label = QLabel()
|
||||||
|
self.name_label.setWordWrap(True)
|
||||||
|
font = QFont()
|
||||||
|
font.setBold(True)
|
||||||
|
font.setPointSize(12)
|
||||||
|
self.name_label.setFont(font)
|
||||||
|
header_layout.addWidget(self.name_label)
|
||||||
|
|
||||||
|
self.size_label = QLabel()
|
||||||
|
header_layout.addWidget(self.size_label)
|
||||||
|
|
||||||
|
layout.addWidget(header)
|
||||||
|
|
||||||
|
# Preview-Container
|
||||||
|
self.preview_container = QWidget()
|
||||||
|
self.preview_layout = QVBoxLayout(self.preview_container)
|
||||||
|
self.preview_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.addWidget(self.preview_container, 1)
|
||||||
|
|
||||||
|
# Leerer Zustand
|
||||||
|
self.empty_label = QLabel("Wähle eine Datei im Hauptfenster aus")
|
||||||
|
self.empty_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.preview_layout.addWidget(self.empty_label)
|
||||||
|
|
||||||
|
# Preview-Widgets (werden bei Bedarf erstellt)
|
||||||
|
self.image_preview = None
|
||||||
|
self.text_preview = None
|
||||||
|
self.pdf_preview = None
|
||||||
|
self.no_preview = None
|
||||||
|
self._current_preview = None
|
||||||
|
|
||||||
|
def _load_settings(self):
|
||||||
|
"""Lädt gespeicherte Fenstereinstellungen."""
|
||||||
|
settings = QSettings('FileBrowser', 'FileBrowser')
|
||||||
|
geometry = settings.value('preview_window_geometry')
|
||||||
|
if geometry:
|
||||||
|
self.restoreGeometry(geometry)
|
||||||
|
else:
|
||||||
|
self.resize(600, 500)
|
||||||
|
|
||||||
|
def _save_settings(self):
|
||||||
|
"""Speichert Fenstereinstellungen."""
|
||||||
|
settings = QSettings('FileBrowser', 'FileBrowser')
|
||||||
|
settings.setValue('preview_window_geometry', self.saveGeometry())
|
||||||
|
|
||||||
|
def _clear_preview(self):
|
||||||
|
"""Entfernt das aktuelle Preview-Widget."""
|
||||||
|
if self._current_preview:
|
||||||
|
self.preview_layout.removeWidget(self._current_preview)
|
||||||
|
self._current_preview.hide()
|
||||||
|
self._current_preview = None
|
||||||
|
self.empty_label.show()
|
||||||
|
|
||||||
|
def load_file(self, path: str, name: str):
|
||||||
|
"""Lädt eine Datei für die Vorschau."""
|
||||||
|
self._current_path = path
|
||||||
|
self._current_name = name
|
||||||
|
|
||||||
|
# Header aktualisieren
|
||||||
|
self.name_label.setText(name)
|
||||||
|
self.setWindowTitle(f"Vorschau - {name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
size = os.path.getsize(path)
|
||||||
|
self.size_label.setText(format_file_size(size))
|
||||||
|
except OSError:
|
||||||
|
self.size_label.setText("")
|
||||||
|
|
||||||
|
# Aktuelles Preview-Widget entfernen
|
||||||
|
self._clear_preview()
|
||||||
|
self.empty_label.hide()
|
||||||
|
|
||||||
|
# Passende Vorschau laden
|
||||||
|
file_type = get_file_type(name)
|
||||||
|
|
||||||
|
if file_type == 'image':
|
||||||
|
if not self.image_preview:
|
||||||
|
self.image_preview = ImagePreview()
|
||||||
|
self.image_preview.load_image(path)
|
||||||
|
self._current_preview = self.image_preview
|
||||||
|
|
||||||
|
elif file_type == 'text':
|
||||||
|
if not self.text_preview:
|
||||||
|
self.text_preview = TextPreview()
|
||||||
|
self.text_preview.load_text(path)
|
||||||
|
self._current_preview = self.text_preview
|
||||||
|
|
||||||
|
elif file_type == 'pdf':
|
||||||
|
if not self.pdf_preview:
|
||||||
|
self.pdf_preview = PdfPreview()
|
||||||
|
self.pdf_preview.load_pdf(path)
|
||||||
|
self._current_preview = self.pdf_preview
|
||||||
|
|
||||||
|
else:
|
||||||
|
if not self.no_preview:
|
||||||
|
self.no_preview = NoPreview()
|
||||||
|
self.no_preview.set_file(path, name)
|
||||||
|
self._current_preview = self.no_preview
|
||||||
|
|
||||||
|
if self._current_preview:
|
||||||
|
self.preview_layout.addWidget(self._current_preview)
|
||||||
|
self._current_preview.show()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""Leert die Vorschau."""
|
||||||
|
self._current_path = ""
|
||||||
|
self._current_name = ""
|
||||||
|
self.name_label.setText("")
|
||||||
|
self.size_label.setText("")
|
||||||
|
self.setWindowTitle("Vorschau")
|
||||||
|
self._clear_preview()
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""Behandelt das Schließen des Fensters."""
|
||||||
|
self._save_settings()
|
||||||
|
self.closed.emit()
|
||||||
|
super().closeEvent(event)
|
||||||
2
src/utils/__init__.py
Normal file
2
src/utils/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
from .file_utils import get_file_icon, format_file_size, natural_sort_key
|
||||||
|
from .themes import ThemeManager
|
||||||
100
src/utils/file_utils.py
Normal file
100
src/utils/file_utils.py
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
"""Datei-Hilfsfunktionen für den FileBrowser."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_icon(filename: str, is_dir: bool = False) -> str:
|
||||||
|
"""Gibt das passende Emoji-Icon für einen Dateityp zurück."""
|
||||||
|
if is_dir:
|
||||||
|
return "📁"
|
||||||
|
|
||||||
|
ext = Path(filename).suffix.lower()
|
||||||
|
|
||||||
|
icons = {
|
||||||
|
# PDF
|
||||||
|
'.pdf': '📄',
|
||||||
|
# Bilder
|
||||||
|
'.jpg': '🖼️', '.jpeg': '🖼️', '.png': '🖼️', '.gif': '🖼️',
|
||||||
|
'.bmp': '🖼️', '.webp': '🖼️', '.tiff': '🖼️', '.svg': '🖼️',
|
||||||
|
# Dokumente
|
||||||
|
'.doc': '📝', '.docx': '📝', '.odt': '📝', '.rtf': '📝',
|
||||||
|
# Tabellen
|
||||||
|
'.xls': '📊', '.xlsx': '📊', '.ods': '📊', '.csv': '📊',
|
||||||
|
# Archive
|
||||||
|
'.zip': '📦', '.rar': '📦', '.7z': '📦', '.tar': '📦',
|
||||||
|
'.gz': '📦', '.bz2': '📦', '.xz': '📦',
|
||||||
|
# Text
|
||||||
|
'.txt': '📃', '.md': '📃', '.log': '📃',
|
||||||
|
# Code
|
||||||
|
'.py': '🐍', '.js': '📜', '.ts': '📜', '.html': '🌐',
|
||||||
|
'.css': '🎨', '.json': '📋', '.xml': '📋', '.yaml': '📋',
|
||||||
|
'.yml': '📋', '.sh': '⚙️', '.bash': '⚙️',
|
||||||
|
# Audio
|
||||||
|
'.mp3': '🎵', '.wav': '🎵', '.flac': '🎵', '.ogg': '🎵',
|
||||||
|
'.m4a': '🎵', '.aac': '🎵',
|
||||||
|
# Video
|
||||||
|
'.mp4': '🎬', '.mkv': '🎬', '.avi': '🎬', '.mov': '🎬',
|
||||||
|
'.wmv': '🎬', '.webm': '🎬',
|
||||||
|
}
|
||||||
|
|
||||||
|
return icons.get(ext, '📎')
|
||||||
|
|
||||||
|
|
||||||
|
def format_file_size(size_bytes: int) -> str:
|
||||||
|
"""Formatiert eine Dateigröße in lesbarer Form."""
|
||||||
|
if size_bytes < 1024:
|
||||||
|
return f"{size_bytes} B"
|
||||||
|
elif size_bytes < 1024 * 1024:
|
||||||
|
return f"{size_bytes / 1024:.1f} KB"
|
||||||
|
elif size_bytes < 1024 * 1024 * 1024:
|
||||||
|
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
||||||
|
else:
|
||||||
|
return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"
|
||||||
|
|
||||||
|
|
||||||
|
def natural_sort_key(s: str) -> list:
|
||||||
|
"""
|
||||||
|
Erzeugt einen Sortierungsschlüssel für natürliche Sortierung.
|
||||||
|
Sortiert: Sonderzeichen → Zahlen → Buchstaben
|
||||||
|
"""
|
||||||
|
def convert(text):
|
||||||
|
if text.isdigit():
|
||||||
|
return (1, int(text), text.lower())
|
||||||
|
elif text[0].isalpha() if text else False:
|
||||||
|
return (2, 0, text.lower())
|
||||||
|
else:
|
||||||
|
return (0, 0, text.lower())
|
||||||
|
|
||||||
|
return [convert(c) for c in re.split(r'(\d+)', s)]
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_type(filename: str) -> str:
|
||||||
|
"""Bestimmt den Dateityp für die Preview."""
|
||||||
|
ext = Path(filename).suffix.lower()
|
||||||
|
|
||||||
|
image_exts = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff', '.svg'}
|
||||||
|
text_exts = {'.txt', '.md', '.log', '.py', '.js', '.ts', '.html', '.css',
|
||||||
|
'.json', '.xml', '.yaml', '.yml', '.sh', '.bash', '.csv', '.ini',
|
||||||
|
'.conf', '.cfg'}
|
||||||
|
pdf_exts = {'.pdf'}
|
||||||
|
|
||||||
|
if ext in image_exts:
|
||||||
|
return 'image'
|
||||||
|
elif ext in text_exts:
|
||||||
|
return 'text'
|
||||||
|
elif ext in pdf_exts:
|
||||||
|
return 'pdf'
|
||||||
|
else:
|
||||||
|
return 'unknown'
|
||||||
|
|
||||||
|
|
||||||
|
def is_readable(path: str) -> bool:
|
||||||
|
"""Prüft, ob ein Pfad lesbar ist."""
|
||||||
|
return os.access(path, os.R_OK)
|
||||||
|
|
||||||
|
|
||||||
|
def is_writable(path: str) -> bool:
|
||||||
|
"""Prüft, ob ein Pfad beschreibbar ist."""
|
||||||
|
return os.access(path, os.W_OK)
|
||||||
315
src/utils/themes.py
Normal file
315
src/utils/themes.py
Normal file
|
|
@ -0,0 +1,315 @@
|
||||||
|
"""Theme-Management für den FileBrowser."""
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
from PyQt6.QtGui import QPalette, QColor
|
||||||
|
from PyQt6.QtCore import QSettings
|
||||||
|
|
||||||
|
|
||||||
|
class ThemeManager:
|
||||||
|
"""Verwaltet die Themes der Anwendung."""
|
||||||
|
|
||||||
|
THEMES = {
|
||||||
|
'dark': {
|
||||||
|
'name': 'Dark',
|
||||||
|
'window': '#0f172a',
|
||||||
|
'window_text': '#f1f5f9',
|
||||||
|
'base': '#1e293b',
|
||||||
|
'alternate_base': '#334155',
|
||||||
|
'text': '#f1f5f9',
|
||||||
|
'button': '#334155',
|
||||||
|
'button_text': '#f1f5f9',
|
||||||
|
'highlight': '#3b82f6',
|
||||||
|
'highlight_text': '#ffffff',
|
||||||
|
'link': '#60a5fa',
|
||||||
|
'border': '#475569',
|
||||||
|
},
|
||||||
|
'breeze_dark': {
|
||||||
|
'name': 'Breeze Dark',
|
||||||
|
'window': '#31363b',
|
||||||
|
'window_text': '#eff0f1',
|
||||||
|
'base': '#232629',
|
||||||
|
'alternate_base': '#31363b',
|
||||||
|
'text': '#eff0f1',
|
||||||
|
'button': '#31363b',
|
||||||
|
'button_text': '#eff0f1',
|
||||||
|
'highlight': '#3daee9',
|
||||||
|
'highlight_text': '#eff0f1',
|
||||||
|
'link': '#2980b9',
|
||||||
|
'border': '#76797c',
|
||||||
|
},
|
||||||
|
'breeze_light': {
|
||||||
|
'name': 'Breeze Light',
|
||||||
|
'window': '#eff0f1',
|
||||||
|
'window_text': '#31363b',
|
||||||
|
'base': '#fcfcfc',
|
||||||
|
'alternate_base': '#eff0f1',
|
||||||
|
'text': '#31363b',
|
||||||
|
'button': '#eff0f1',
|
||||||
|
'button_text': '#31363b',
|
||||||
|
'highlight': '#3daee9',
|
||||||
|
'highlight_text': '#ffffff',
|
||||||
|
'link': '#2980b9',
|
||||||
|
'border': '#bdc3c7',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.settings = QSettings('FileBrowser', 'FileBrowser')
|
||||||
|
self.current_theme = self.settings.value('theme', 'dark')
|
||||||
|
|
||||||
|
def get_available_themes(self) -> list:
|
||||||
|
"""Gibt eine Liste der verfügbaren Themes zurück."""
|
||||||
|
return [(key, theme['name']) for key, theme in self.THEMES.items()]
|
||||||
|
|
||||||
|
def apply_theme(self, theme_name: str):
|
||||||
|
"""Wendet ein Theme auf die Anwendung an."""
|
||||||
|
if theme_name not in self.THEMES:
|
||||||
|
theme_name = 'dark'
|
||||||
|
|
||||||
|
self.current_theme = theme_name
|
||||||
|
self.settings.setValue('theme', theme_name)
|
||||||
|
|
||||||
|
theme = self.THEMES[theme_name]
|
||||||
|
app = QApplication.instance()
|
||||||
|
|
||||||
|
palette = QPalette()
|
||||||
|
palette.setColor(QPalette.ColorRole.Window, QColor(theme['window']))
|
||||||
|
palette.setColor(QPalette.ColorRole.WindowText, QColor(theme['window_text']))
|
||||||
|
palette.setColor(QPalette.ColorRole.Base, QColor(theme['base']))
|
||||||
|
palette.setColor(QPalette.ColorRole.AlternateBase, QColor(theme['alternate_base']))
|
||||||
|
palette.setColor(QPalette.ColorRole.Text, QColor(theme['text']))
|
||||||
|
palette.setColor(QPalette.ColorRole.Button, QColor(theme['button']))
|
||||||
|
palette.setColor(QPalette.ColorRole.ButtonText, QColor(theme['button_text']))
|
||||||
|
palette.setColor(QPalette.ColorRole.Highlight, QColor(theme['highlight']))
|
||||||
|
palette.setColor(QPalette.ColorRole.HighlightedText, QColor(theme['highlight_text']))
|
||||||
|
palette.setColor(QPalette.ColorRole.Link, QColor(theme['link']))
|
||||||
|
|
||||||
|
# Disabled colors
|
||||||
|
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText,
|
||||||
|
QColor(theme['window_text']).darker(150))
|
||||||
|
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text,
|
||||||
|
QColor(theme['text']).darker(150))
|
||||||
|
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText,
|
||||||
|
QColor(theme['button_text']).darker(150))
|
||||||
|
|
||||||
|
app.setPalette(palette)
|
||||||
|
|
||||||
|
return self.get_stylesheet(theme_name)
|
||||||
|
|
||||||
|
def get_stylesheet(self, theme_name: str = None) -> str:
|
||||||
|
"""Gibt das Stylesheet für ein Theme zurück."""
|
||||||
|
if theme_name is None:
|
||||||
|
theme_name = self.current_theme
|
||||||
|
|
||||||
|
theme = self.THEMES.get(theme_name, self.THEMES['dark'])
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
QMainWindow, QWidget {{
|
||||||
|
background-color: {theme['window']};
|
||||||
|
color: {theme['window_text']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QTreeView, QListView, QTableView {{
|
||||||
|
background-color: {theme['base']};
|
||||||
|
color: {theme['text']};
|
||||||
|
border: 1px solid {theme['border']};
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QTreeView::item, QListView::item, QTableView::item {{
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QTreeView::item:selected, QListView::item:selected, QTableView::item:selected {{
|
||||||
|
background-color: {theme['highlight']};
|
||||||
|
color: {theme['highlight_text']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QTreeView::item:hover, QListView::item:hover, QTableView::item:hover {{
|
||||||
|
background-color: {theme['alternate_base']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QHeaderView::section {{
|
||||||
|
background-color: {theme['button']};
|
||||||
|
color: {theme['button_text']};
|
||||||
|
padding: 6px;
|
||||||
|
border: none;
|
||||||
|
border-right: 1px solid {theme['border']};
|
||||||
|
border-bottom: 1px solid {theme['border']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QSplitter::handle {{
|
||||||
|
background-color: {theme['border']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QSplitter::handle:horizontal {{
|
||||||
|
width: 4px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QSplitter::handle:vertical {{
|
||||||
|
height: 4px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QSplitter::handle:hover {{
|
||||||
|
background-color: {theme['highlight']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QPushButton {{
|
||||||
|
background-color: {theme['button']};
|
||||||
|
color: {theme['button_text']};
|
||||||
|
border: 1px solid {theme['border']};
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
min-width: 60px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QPushButton:hover {{
|
||||||
|
background-color: {theme['highlight']};
|
||||||
|
color: {theme['highlight_text']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QPushButton:pressed {{
|
||||||
|
background-color: {theme['highlight']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QLineEdit, QTextEdit, QPlainTextEdit {{
|
||||||
|
background-color: {theme['base']};
|
||||||
|
color: {theme['text']};
|
||||||
|
border: 1px solid {theme['border']};
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QLineEdit:focus, QTextEdit:focus, QPlainTextEdit:focus {{
|
||||||
|
border-color: {theme['highlight']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QMenuBar {{
|
||||||
|
background-color: {theme['window']};
|
||||||
|
color: {theme['window_text']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QMenuBar::item:selected {{
|
||||||
|
background-color: {theme['highlight']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QMenu {{
|
||||||
|
background-color: {theme['base']};
|
||||||
|
color: {theme['text']};
|
||||||
|
border: 1px solid {theme['border']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QMenu::item:selected {{
|
||||||
|
background-color: {theme['highlight']};
|
||||||
|
color: {theme['highlight_text']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QToolBar {{
|
||||||
|
background-color: {theme['window']};
|
||||||
|
border: none;
|
||||||
|
spacing: 4px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QStatusBar {{
|
||||||
|
background-color: {theme['window']};
|
||||||
|
color: {theme['window_text']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QScrollBar:vertical {{
|
||||||
|
background-color: {theme['base']};
|
||||||
|
width: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QScrollBar::handle:vertical {{
|
||||||
|
background-color: {theme['border']};
|
||||||
|
border-radius: 5px;
|
||||||
|
min-height: 20px;
|
||||||
|
margin: 2px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QScrollBar::handle:vertical:hover {{
|
||||||
|
background-color: {theme['highlight']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QScrollBar:horizontal {{
|
||||||
|
background-color: {theme['base']};
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QScrollBar::handle:horizontal {{
|
||||||
|
background-color: {theme['border']};
|
||||||
|
border-radius: 5px;
|
||||||
|
min-width: 20px;
|
||||||
|
margin: 2px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QScrollBar::handle:horizontal:hover {{
|
||||||
|
background-color: {theme['highlight']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QScrollBar::add-line, QScrollBar::sub-line {{
|
||||||
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QLabel {{
|
||||||
|
color: {theme['window_text']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QComboBox {{
|
||||||
|
background-color: {theme['button']};
|
||||||
|
color: {theme['button_text']};
|
||||||
|
border: 1px solid {theme['border']};
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QComboBox:hover {{
|
||||||
|
border-color: {theme['highlight']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QComboBox::drop-down {{
|
||||||
|
border: none;
|
||||||
|
width: 20px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QComboBox QAbstractItemView {{
|
||||||
|
background-color: {theme['base']};
|
||||||
|
color: {theme['text']};
|
||||||
|
selection-background-color: {theme['highlight']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QTabWidget::pane {{
|
||||||
|
border: 1px solid {theme['border']};
|
||||||
|
border-radius: 4px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QTabBar::tab {{
|
||||||
|
background-color: {theme['button']};
|
||||||
|
color: {theme['button_text']};
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
QTabBar::tab:selected {{
|
||||||
|
background-color: {theme['highlight']};
|
||||||
|
color: {theme['highlight_text']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QMessageBox {{
|
||||||
|
background-color: {theme['window']};
|
||||||
|
}}
|
||||||
|
|
||||||
|
QDialog {{
|
||||||
|
background-color: {theme['window']};
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_current_theme(self) -> str:
|
||||||
|
"""Gibt den Namen des aktuellen Themes zurück."""
|
||||||
|
return self.current_theme
|
||||||
4
src/widgets/__init__.py
Normal file
4
src/widgets/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
from .folder_tree import FolderTreeWidget
|
||||||
|
from .file_list import FileListWidget
|
||||||
|
from .preview_panel import PreviewPanel
|
||||||
|
from .breadcrumb import BreadcrumbWidget
|
||||||
191
src/widgets/breadcrumb.py
Normal file
191
src/widgets/breadcrumb.py
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
"""Breadcrumb-Navigation für den FileBrowser."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QWidget, QHBoxLayout, QVBoxLayout, QPushButton, QLabel, QScrollArea,
|
||||||
|
QSizePolicy, QFrame, QLineEdit, QStackedWidget, QApplication
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt, pyqtSignal, QSettings
|
||||||
|
from PyQt6.QtGui import QFont
|
||||||
|
|
||||||
|
|
||||||
|
class BreadcrumbWidget(QWidget):
|
||||||
|
"""Breadcrumb-Navigation mit klickbaren Pfadsegmenten und Textzeile."""
|
||||||
|
|
||||||
|
path_clicked = pyqtSignal(str) # Ausgewählter Pfad
|
||||||
|
path_entered = pyqtSignal(str) # Manuell eingegebener Pfad
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._current_path = ""
|
||||||
|
self.settings = QSettings('FileBrowser', 'FileBrowser')
|
||||||
|
self._setup_ui()
|
||||||
|
self._load_settings()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
main_layout = QHBoxLayout(self)
|
||||||
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
main_layout.setSpacing(4)
|
||||||
|
|
||||||
|
# Stacked Widget für Breadcrumb/Textzeile
|
||||||
|
self.stack = QStackedWidget()
|
||||||
|
self.stack.setMaximumHeight(36)
|
||||||
|
|
||||||
|
# Breadcrumb-Ansicht (Index 0)
|
||||||
|
breadcrumb_widget = QWidget()
|
||||||
|
breadcrumb_layout = QHBoxLayout(breadcrumb_widget)
|
||||||
|
breadcrumb_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
breadcrumb_layout.setSpacing(0)
|
||||||
|
|
||||||
|
self.scroll_area = QScrollArea()
|
||||||
|
self.scroll_area.setWidgetResizable(True)
|
||||||
|
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||||
|
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||||
|
self.scroll_area.setMaximumHeight(36)
|
||||||
|
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||||
|
|
||||||
|
self.button_container = QWidget()
|
||||||
|
self.button_layout = QHBoxLayout(self.button_container)
|
||||||
|
self.button_layout.setContentsMargins(4, 4, 4, 4)
|
||||||
|
self.button_layout.setSpacing(2)
|
||||||
|
self.button_layout.addStretch()
|
||||||
|
|
||||||
|
self.scroll_area.setWidget(self.button_container)
|
||||||
|
breadcrumb_layout.addWidget(self.scroll_area)
|
||||||
|
|
||||||
|
self.stack.addWidget(breadcrumb_widget)
|
||||||
|
|
||||||
|
# Text-Eingabe (Index 1)
|
||||||
|
self.path_edit = QLineEdit()
|
||||||
|
self.path_edit.setPlaceholderText("Pfad eingeben...")
|
||||||
|
font = QFont("Monospace")
|
||||||
|
self.path_edit.setFont(font)
|
||||||
|
self.path_edit.returnPressed.connect(self._on_path_entered)
|
||||||
|
self.path_edit.editingFinished.connect(self._on_editing_finished)
|
||||||
|
self.stack.addWidget(self.path_edit)
|
||||||
|
|
||||||
|
main_layout.addWidget(self.stack, 1)
|
||||||
|
|
||||||
|
# Toggle-Button für Ansicht
|
||||||
|
self.toggle_btn = QPushButton("📝")
|
||||||
|
self.toggle_btn.setToolTip("Zwischen Breadcrumb und Texteingabe wechseln")
|
||||||
|
self.toggle_btn.setFixedSize(32, 28)
|
||||||
|
self.toggle_btn.setCheckable(True)
|
||||||
|
self.toggle_btn.clicked.connect(self._toggle_view)
|
||||||
|
main_layout.addWidget(self.toggle_btn)
|
||||||
|
|
||||||
|
# Kopieren-Button
|
||||||
|
self.copy_btn = QPushButton("📋")
|
||||||
|
self.copy_btn.setToolTip("Pfad kopieren")
|
||||||
|
self.copy_btn.setFixedSize(32, 28)
|
||||||
|
self.copy_btn.clicked.connect(self._copy_path)
|
||||||
|
main_layout.addWidget(self.copy_btn)
|
||||||
|
|
||||||
|
def _load_settings(self):
|
||||||
|
"""Lädt gespeicherte Einstellungen."""
|
||||||
|
show_text = self.settings.value('breadcrumb_text_mode', False, type=bool)
|
||||||
|
if show_text:
|
||||||
|
self.toggle_btn.setChecked(True)
|
||||||
|
self.stack.setCurrentIndex(1)
|
||||||
|
self.toggle_btn.setText("🗂️")
|
||||||
|
|
||||||
|
def _save_settings(self):
|
||||||
|
"""Speichert Einstellungen."""
|
||||||
|
self.settings.setValue('breadcrumb_text_mode', self.stack.currentIndex() == 1)
|
||||||
|
|
||||||
|
def _toggle_view(self, checked):
|
||||||
|
"""Wechselt zwischen Breadcrumb und Texteingabe."""
|
||||||
|
if checked:
|
||||||
|
self.stack.setCurrentIndex(1)
|
||||||
|
self.path_edit.setText(self._current_path)
|
||||||
|
self.path_edit.setFocus()
|
||||||
|
self.path_edit.selectAll()
|
||||||
|
self.toggle_btn.setText("🗂️")
|
||||||
|
self.toggle_btn.setToolTip("Zur Breadcrumb-Ansicht wechseln")
|
||||||
|
else:
|
||||||
|
self.stack.setCurrentIndex(0)
|
||||||
|
self.toggle_btn.setText("📝")
|
||||||
|
self.toggle_btn.setToolTip("Zur Texteingabe wechseln")
|
||||||
|
self._save_settings()
|
||||||
|
|
||||||
|
def _on_path_entered(self):
|
||||||
|
"""Behandelt Enter in der Pfadeingabe."""
|
||||||
|
path = self.path_edit.text().strip()
|
||||||
|
if path and os.path.isdir(path):
|
||||||
|
self._current_path = path
|
||||||
|
self.path_entered.emit(path)
|
||||||
|
# Zurück zur Breadcrumb-Ansicht
|
||||||
|
self.toggle_btn.setChecked(False)
|
||||||
|
self._toggle_view(False)
|
||||||
|
self.set_path(path)
|
||||||
|
|
||||||
|
def _on_editing_finished(self):
|
||||||
|
"""Behandelt Ende der Bearbeitung."""
|
||||||
|
# Bei Escape zurück zur Breadcrumb-Ansicht
|
||||||
|
if self.stack.currentIndex() == 1:
|
||||||
|
self.path_edit.setText(self._current_path)
|
||||||
|
|
||||||
|
def _copy_path(self):
|
||||||
|
"""Kopiert den aktuellen Pfad in die Zwischenablage."""
|
||||||
|
if self._current_path:
|
||||||
|
clipboard = QApplication.clipboard()
|
||||||
|
clipboard.setText(self._current_path)
|
||||||
|
|
||||||
|
def set_path(self, path: str):
|
||||||
|
"""Setzt den aktuellen Pfad und aktualisiert die Breadcrumbs."""
|
||||||
|
self._current_path = path
|
||||||
|
self.path_edit.setText(path)
|
||||||
|
|
||||||
|
# Alte Buttons entfernen
|
||||||
|
while self.button_layout.count() > 1:
|
||||||
|
item = self.button_layout.takeAt(0)
|
||||||
|
if item.widget():
|
||||||
|
item.widget().deleteLater()
|
||||||
|
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Pfad in Segmente aufteilen
|
||||||
|
parts = Path(path).parts
|
||||||
|
|
||||||
|
for i, part in enumerate(parts):
|
||||||
|
segment_path = str(Path(*parts[:i + 1]))
|
||||||
|
|
||||||
|
btn = QPushButton(part if part != '/' else '/')
|
||||||
|
btn.setFlat(True)
|
||||||
|
btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
btn.setProperty('segment_path', segment_path)
|
||||||
|
btn.clicked.connect(self._on_segment_clicked)
|
||||||
|
|
||||||
|
font = QFont()
|
||||||
|
font.setFamily("Monospace")
|
||||||
|
btn.setFont(font)
|
||||||
|
|
||||||
|
if i == len(parts) - 1:
|
||||||
|
btn.setEnabled(False)
|
||||||
|
btn.setStyleSheet("QPushButton { font-weight: bold; }")
|
||||||
|
|
||||||
|
self.button_layout.insertWidget(self.button_layout.count() - 1, btn)
|
||||||
|
|
||||||
|
if i < len(parts) - 1:
|
||||||
|
separator = QLabel("/")
|
||||||
|
separator.setStyleSheet("color: gray;")
|
||||||
|
self.button_layout.insertWidget(self.button_layout.count() - 1, separator)
|
||||||
|
|
||||||
|
# Scroll ans Ende
|
||||||
|
self.scroll_area.horizontalScrollBar().setValue(
|
||||||
|
self.scroll_area.horizontalScrollBar().maximum()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_segment_clicked(self):
|
||||||
|
"""Behandelt Klicks auf Breadcrumb-Segmente."""
|
||||||
|
btn = self.sender()
|
||||||
|
if btn:
|
||||||
|
path = btn.property('segment_path')
|
||||||
|
if path:
|
||||||
|
self.path_clicked.emit(path)
|
||||||
|
|
||||||
|
def get_current_path(self) -> str:
|
||||||
|
"""Gibt den aktuellen Pfad zurück."""
|
||||||
|
return self._current_path
|
||||||
363
src/widgets/file_list.py
Normal file
363
src/widgets/file_list.py
Normal file
|
|
@ -0,0 +1,363 @@
|
||||||
|
"""Dateiliste-Widget mit natürlicher Sortierung und Drag & Drop."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QTableView, QAbstractItemView, QMenu, QHeaderView,
|
||||||
|
QStyledItemDelegate, QStyle
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import (
|
||||||
|
Qt, QModelIndex, pyqtSignal, QAbstractTableModel, QMimeData,
|
||||||
|
QUrl, QVariant, QSize
|
||||||
|
)
|
||||||
|
from PyQt6.QtGui import QAction, QDrag, QIcon
|
||||||
|
|
||||||
|
from ..utils.file_utils import get_file_icon, format_file_size, natural_sort_key
|
||||||
|
|
||||||
|
|
||||||
|
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 = ""
|
||||||
|
|
||||||
|
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 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:
|
||||||
|
entries = os.scandir(path)
|
||||||
|
folders = []
|
||||||
|
files = []
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
try:
|
||||||
|
stat = entry.stat()
|
||||||
|
item = FileItem(
|
||||||
|
name=entry.name,
|
||||||
|
path=entry.path,
|
||||||
|
is_dir=entry.is_dir(),
|
||||||
|
size=stat.st_size if entry.is_file() else 0,
|
||||||
|
modified=datetime.fromtimestamp(stat.st_mtime)
|
||||||
|
)
|
||||||
|
if entry.is_dir():
|
||||||
|
folders.append(item)
|
||||||
|
else:
|
||||||
|
files.append(item)
|
||||||
|
except (PermissionError, OSError):
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
file_move_requested = pyqtSignal(str) # path
|
||||||
|
files_dropped = pyqtSignal(list, str) # source_paths, target_folder
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.list_model = FileListModel(self)
|
||||||
|
self.setModel(self.list_model)
|
||||||
|
|
||||||
|
# Einstellungen
|
||||||
|
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||||
|
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
|
self.setShowGrid(False)
|
||||||
|
self.setAlternatingRowColors(True)
|
||||||
|
self.verticalHeader().setVisible(False)
|
||||||
|
self.setWordWrap(False)
|
||||||
|
|
||||||
|
# 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, 30)
|
||||||
|
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()
|
||||||
|
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)
|
||||||
|
|
||||||
|
menu.exec(self.viewport().mapToGlobal(position))
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
def _delete_items(self, items: list):
|
||||||
|
"""Löscht mehrere Elemente."""
|
||||||
|
for item in items:
|
||||||
|
self.file_delete_requested.emit(item.path)
|
||||||
|
|
||||||
|
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 refresh(self):
|
||||||
|
"""Aktualisiert die Dateiliste."""
|
||||||
|
self.list_model.refresh()
|
||||||
|
|
||||||
|
# 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."""
|
||||||
|
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:
|
||||||
|
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:
|
||||||
|
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:
|
||||||
|
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)
|
||||||
162
src/widgets/folder_tree.py
Normal file
162
src/widgets/folder_tree.py
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
"""Ordnerbaum-Widget mit Lazy Loading."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QTreeView, QAbstractItemView, QMenu
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import (
|
||||||
|
Qt, QModelIndex, pyqtSignal, QDir, QFileInfo
|
||||||
|
)
|
||||||
|
from PyQt6.QtGui import (
|
||||||
|
QFileSystemModel, QAction
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FolderTreeWidget(QTreeView):
|
||||||
|
"""Ordnerbaum mit Lazy Loading und Kontextmenü."""
|
||||||
|
|
||||||
|
folder_selected = pyqtSignal(str) # Pfad des ausgewählten Ordners
|
||||||
|
folder_double_clicked = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.model = QFileSystemModel()
|
||||||
|
self.model.setFilter(QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot)
|
||||||
|
self.model.setRootPath('')
|
||||||
|
|
||||||
|
self.setModel(self.model)
|
||||||
|
|
||||||
|
# Nur Name-Spalte anzeigen
|
||||||
|
for i in range(1, self.model.columnCount()):
|
||||||
|
self.hideColumn(i)
|
||||||
|
|
||||||
|
# Einstellungen
|
||||||
|
self.setHeaderHidden(True)
|
||||||
|
self.setAnimated(True)
|
||||||
|
self.setIndentation(20)
|
||||||
|
self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||||
|
self.setExpandsOnDoubleClick(True)
|
||||||
|
|
||||||
|
# Drag & Drop für Ordner
|
||||||
|
self.setDragEnabled(False)
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
self.setDropIndicatorShown(True)
|
||||||
|
|
||||||
|
# Signale verbinden
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Standard-Startpfade expandieren
|
||||||
|
self._expand_default_paths()
|
||||||
|
|
||||||
|
def _expand_default_paths(self):
|
||||||
|
"""Expandiert die Standard-Startpfade."""
|
||||||
|
default_paths = ['/', '/home', '/mnt', '/srv']
|
||||||
|
for path in default_paths:
|
||||||
|
if os.path.exists(path):
|
||||||
|
index = self.model.index(path)
|
||||||
|
if index.isValid():
|
||||||
|
self.expand(index)
|
||||||
|
|
||||||
|
def _on_clicked(self, index: QModelIndex):
|
||||||
|
"""Behandelt Klicks auf Ordner."""
|
||||||
|
path = self.model.filePath(index)
|
||||||
|
if path and os.path.isdir(path):
|
||||||
|
self.folder_selected.emit(path)
|
||||||
|
|
||||||
|
def _on_double_clicked(self, index: QModelIndex):
|
||||||
|
"""Behandelt Doppelklicks auf Ordner."""
|
||||||
|
path = self.model.filePath(index)
|
||||||
|
if path and os.path.isdir(path):
|
||||||
|
self.folder_double_clicked.emit(path)
|
||||||
|
|
||||||
|
def _show_context_menu(self, position):
|
||||||
|
"""Zeigt das Kontextmenü für Ordner."""
|
||||||
|
index = self.indexAt(position)
|
||||||
|
if not index.isValid():
|
||||||
|
return
|
||||||
|
|
||||||
|
path = self.model.filePath(index)
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
|
||||||
|
menu = QMenu(self)
|
||||||
|
|
||||||
|
# Öffnen
|
||||||
|
open_action = QAction("📂 Öffnen", self)
|
||||||
|
open_action.triggered.connect(lambda: self.folder_selected.emit(path))
|
||||||
|
menu.addAction(open_action)
|
||||||
|
|
||||||
|
# Im Terminal öffnen
|
||||||
|
terminal_action = QAction("⚙️ Im Terminal öffnen", self)
|
||||||
|
terminal_action.triggered.connect(lambda: self._open_in_terminal(path))
|
||||||
|
menu.addAction(terminal_action)
|
||||||
|
|
||||||
|
menu.addSeparator()
|
||||||
|
|
||||||
|
# Neuer Ordner
|
||||||
|
new_folder_action = QAction("➕ Neuer Ordner", self)
|
||||||
|
new_folder_action.triggered.connect(lambda: self._create_new_folder(path))
|
||||||
|
menu.addAction(new_folder_action)
|
||||||
|
|
||||||
|
menu.exec(self.viewport().mapToGlobal(position))
|
||||||
|
|
||||||
|
def _open_in_terminal(self, path: str):
|
||||||
|
"""Öffnet den Ordner im Terminal."""
|
||||||
|
import subprocess
|
||||||
|
try:
|
||||||
|
subprocess.Popen(['xdg-open', path])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler beim Öffnen im Terminal: {e}")
|
||||||
|
|
||||||
|
def _create_new_folder(self, parent_path: str):
|
||||||
|
"""Erstellt einen neuen Ordner."""
|
||||||
|
from PyQt6.QtWidgets import QInputDialog
|
||||||
|
|
||||||
|
name, ok = QInputDialog.getText(
|
||||||
|
self, "Neuer Ordner", "Ordnername:",
|
||||||
|
)
|
||||||
|
if ok and name:
|
||||||
|
new_path = os.path.join(parent_path, name)
|
||||||
|
try:
|
||||||
|
os.makedirs(new_path, exist_ok=True)
|
||||||
|
self.model.setRootPath('') # Refresh
|
||||||
|
except Exception as e:
|
||||||
|
from PyQt6.QtWidgets import QMessageBox
|
||||||
|
QMessageBox.critical(self, "Fehler", f"Fehler beim Erstellen: {e}")
|
||||||
|
|
||||||
|
def navigate_to(self, path: str):
|
||||||
|
"""Navigiert zu einem bestimmten Pfad und expandiert ihn."""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return
|
||||||
|
|
||||||
|
index = self.model.index(path)
|
||||||
|
if index.isValid():
|
||||||
|
self.setCurrentIndex(index)
|
||||||
|
self.scrollTo(index)
|
||||||
|
|
||||||
|
# Alle Elternordner expandieren
|
||||||
|
parent = index.parent()
|
||||||
|
while parent.isValid():
|
||||||
|
self.expand(parent)
|
||||||
|
parent = parent.parent()
|
||||||
|
|
||||||
|
def get_selected_path(self) -> str:
|
||||||
|
"""Gibt den aktuell ausgewählten Pfad zurück."""
|
||||||
|
indexes = self.selectedIndexes()
|
||||||
|
if indexes:
|
||||||
|
return self.model.filePath(indexes[0])
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
"""Aktualisiert den Baum."""
|
||||||
|
current_path = self.get_selected_path()
|
||||||
|
self.model.setRootPath('')
|
||||||
|
if current_path:
|
||||||
|
self.navigate_to(current_path)
|
||||||
487
src/widgets/preview_panel.py
Normal file
487
src/widgets/preview_panel.py
Normal file
|
|
@ -0,0 +1,487 @@
|
||||||
|
"""Preview-Panel für Bilder, Text und PDF."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||||
|
QScrollArea, QPlainTextEdit, QStackedWidget, QSizePolicy,
|
||||||
|
QFrame, QApplication, QComboBox, QSpinBox
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt, pyqtSignal, QUrl, QSize, QTimer, QSettings
|
||||||
|
from PyQt6.QtGui import QPixmap, QImage, QFont, QPainter
|
||||||
|
from PyQt6.QtPdf import QPdfDocument
|
||||||
|
from PyQt6.QtPdfWidgets import QPdfView
|
||||||
|
|
||||||
|
from ..utils.file_utils import get_file_icon, format_file_size, get_file_type
|
||||||
|
|
||||||
|
|
||||||
|
class ImagePreview(QScrollArea):
|
||||||
|
"""Bild-Vorschau mit Scroll-Unterstützung."""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWidgetResizable(True)
|
||||||
|
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.setMinimumSize(100, 100)
|
||||||
|
|
||||||
|
self.image_label = QLabel()
|
||||||
|
self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.image_label.setSizePolicy(
|
||||||
|
QSizePolicy.Policy.Expanding,
|
||||||
|
QSizePolicy.Policy.Expanding
|
||||||
|
)
|
||||||
|
self.image_label.setMinimumSize(50, 50)
|
||||||
|
self.setWidget(self.image_label)
|
||||||
|
|
||||||
|
self._pixmap = None
|
||||||
|
|
||||||
|
def load_image(self, path: str):
|
||||||
|
"""Lädt ein Bild."""
|
||||||
|
self._pixmap = QPixmap(path)
|
||||||
|
if self._pixmap.isNull():
|
||||||
|
self.image_label.setText("Bild konnte nicht geladen werden")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._update_scaled_image()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _update_scaled_image(self):
|
||||||
|
"""Skaliert das Bild passend zum verfügbaren Platz."""
|
||||||
|
if self._pixmap is None or self._pixmap.isNull():
|
||||||
|
return
|
||||||
|
|
||||||
|
available_size = self.size() - QSize(20, 20)
|
||||||
|
scaled = self._pixmap.scaled(
|
||||||
|
available_size,
|
||||||
|
Qt.AspectRatioMode.KeepAspectRatio,
|
||||||
|
Qt.TransformationMode.SmoothTransformation
|
||||||
|
)
|
||||||
|
self.image_label.setPixmap(scaled)
|
||||||
|
|
||||||
|
def resizeEvent(self, event):
|
||||||
|
super().resizeEvent(event)
|
||||||
|
self._update_scaled_image()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""Leert die Vorschau."""
|
||||||
|
self._pixmap = None
|
||||||
|
self.image_label.clear()
|
||||||
|
|
||||||
|
|
||||||
|
class TextPreview(QPlainTextEdit):
|
||||||
|
"""Text-Vorschau."""
|
||||||
|
|
||||||
|
MAX_SIZE = 1024 * 1024 # 1 MB
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setReadOnly(True)
|
||||||
|
self.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
|
||||||
|
self.setMinimumSize(100, 100)
|
||||||
|
|
||||||
|
font = QFont("Monospace", 10)
|
||||||
|
font.setStyleHint(QFont.StyleHint.Monospace)
|
||||||
|
self.setFont(font)
|
||||||
|
|
||||||
|
def load_text(self, path: str) -> bool:
|
||||||
|
"""Lädt eine Textdatei."""
|
||||||
|
try:
|
||||||
|
size = os.path.getsize(path)
|
||||||
|
if size > self.MAX_SIZE:
|
||||||
|
self.setPlainText(f"Datei zu groß für Vorschau ({format_file_size(size)})")
|
||||||
|
return False
|
||||||
|
|
||||||
|
with open(path, 'r', encoding='utf-8', errors='replace') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
self.setPlainText(content)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.setPlainText(f"Fehler beim Laden: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class PdfPreview(QWidget):
|
||||||
|
"""PDF-Vorschau mit nativem Qt PDF-Renderer und Einstellungen."""
|
||||||
|
|
||||||
|
# Zoom-Modi
|
||||||
|
ZOOM_MODES = [
|
||||||
|
('fit_width', 'Seitenbreite', QPdfView.ZoomMode.FitToWidth),
|
||||||
|
('fit_page', 'Ganze Seite', QPdfView.ZoomMode.FitInView),
|
||||||
|
('custom', 'Benutzerdefiniert', QPdfView.ZoomMode.Custom),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Seiten-Modi
|
||||||
|
PAGE_MODES = [
|
||||||
|
('single', 'Einzelseite', QPdfView.PageMode.SinglePage),
|
||||||
|
('multi', 'Mehrere Seiten', QPdfView.PageMode.MultiPage),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setMinimumSize(100, 100)
|
||||||
|
self.settings = QSettings('FileBrowser', 'FileBrowser')
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(4)
|
||||||
|
|
||||||
|
# Toolbar für PDF-Einstellungen
|
||||||
|
toolbar = QWidget()
|
||||||
|
toolbar.setFixedHeight(32)
|
||||||
|
toolbar_layout = QHBoxLayout(toolbar)
|
||||||
|
toolbar_layout.setContentsMargins(4, 0, 4, 0)
|
||||||
|
toolbar_layout.setSpacing(8)
|
||||||
|
|
||||||
|
# Zoom-Modus
|
||||||
|
toolbar_layout.addWidget(QLabel("Zoom:"))
|
||||||
|
self.zoom_combo = QComboBox()
|
||||||
|
self.zoom_combo.setFixedWidth(120)
|
||||||
|
for key, label, _ in self.ZOOM_MODES:
|
||||||
|
self.zoom_combo.addItem(label, key)
|
||||||
|
self.zoom_combo.currentIndexChanged.connect(self._on_zoom_mode_changed)
|
||||||
|
toolbar_layout.addWidget(self.zoom_combo)
|
||||||
|
|
||||||
|
# Zoom-Prozent (nur bei Custom)
|
||||||
|
self.zoom_spin = QSpinBox()
|
||||||
|
self.zoom_spin.setRange(10, 500)
|
||||||
|
self.zoom_spin.setValue(100)
|
||||||
|
self.zoom_spin.setSuffix('%')
|
||||||
|
self.zoom_spin.setFixedWidth(80)
|
||||||
|
self.zoom_spin.valueChanged.connect(self._on_zoom_value_changed)
|
||||||
|
self.zoom_spin.setVisible(False)
|
||||||
|
toolbar_layout.addWidget(self.zoom_spin)
|
||||||
|
|
||||||
|
toolbar_layout.addSpacing(16)
|
||||||
|
|
||||||
|
# Seiten-Modus
|
||||||
|
toolbar_layout.addWidget(QLabel("Ansicht:"))
|
||||||
|
self.page_combo = QComboBox()
|
||||||
|
self.page_combo.setFixedWidth(120)
|
||||||
|
for key, label, _ in self.PAGE_MODES:
|
||||||
|
self.page_combo.addItem(label, key)
|
||||||
|
self.page_combo.currentIndexChanged.connect(self._on_page_mode_changed)
|
||||||
|
toolbar_layout.addWidget(self.page_combo)
|
||||||
|
|
||||||
|
toolbar_layout.addStretch()
|
||||||
|
|
||||||
|
layout.addWidget(toolbar)
|
||||||
|
|
||||||
|
# PDF Document und View
|
||||||
|
self.pdf_document = QPdfDocument(self)
|
||||||
|
self.pdf_view = QPdfView(self)
|
||||||
|
self.pdf_view.setDocument(self.pdf_document)
|
||||||
|
|
||||||
|
layout.addWidget(self.pdf_view, 1)
|
||||||
|
|
||||||
|
# Gespeicherte Einstellungen laden
|
||||||
|
self._load_settings()
|
||||||
|
|
||||||
|
def _load_settings(self):
|
||||||
|
"""Lädt gespeicherte PDF-Einstellungen."""
|
||||||
|
# Zoom-Modus
|
||||||
|
zoom_mode = self.settings.value('pdf_zoom_mode', 'fit_width')
|
||||||
|
for i, (key, _, _) in enumerate(self.ZOOM_MODES):
|
||||||
|
if key == zoom_mode:
|
||||||
|
self.zoom_combo.setCurrentIndex(i)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Zoom-Wert
|
||||||
|
zoom_value = self.settings.value('pdf_zoom_value', 100, type=int)
|
||||||
|
self.zoom_spin.setValue(zoom_value)
|
||||||
|
|
||||||
|
# Seiten-Modus
|
||||||
|
page_mode = self.settings.value('pdf_page_mode', 'multi')
|
||||||
|
for i, (key, _, _) in enumerate(self.PAGE_MODES):
|
||||||
|
if key == page_mode:
|
||||||
|
self.page_combo.setCurrentIndex(i)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Einstellungen anwenden
|
||||||
|
self._apply_zoom_mode()
|
||||||
|
self._apply_page_mode()
|
||||||
|
|
||||||
|
def _save_settings(self):
|
||||||
|
"""Speichert PDF-Einstellungen."""
|
||||||
|
zoom_key = self.zoom_combo.currentData()
|
||||||
|
self.settings.setValue('pdf_zoom_mode', zoom_key)
|
||||||
|
self.settings.setValue('pdf_zoom_value', self.zoom_spin.value())
|
||||||
|
|
||||||
|
page_key = self.page_combo.currentData()
|
||||||
|
self.settings.setValue('pdf_page_mode', page_key)
|
||||||
|
|
||||||
|
def _on_zoom_mode_changed(self, index):
|
||||||
|
"""Behandelt Änderung des Zoom-Modus."""
|
||||||
|
self._apply_zoom_mode()
|
||||||
|
self._save_settings()
|
||||||
|
|
||||||
|
def _on_zoom_value_changed(self, value):
|
||||||
|
"""Behandelt Änderung des Zoom-Werts."""
|
||||||
|
if self.zoom_combo.currentData() == 'custom':
|
||||||
|
self.pdf_view.setZoomFactor(value / 100.0)
|
||||||
|
self._save_settings()
|
||||||
|
|
||||||
|
def _on_page_mode_changed(self, index):
|
||||||
|
"""Behandelt Änderung des Seiten-Modus."""
|
||||||
|
self._apply_page_mode()
|
||||||
|
self._save_settings()
|
||||||
|
|
||||||
|
def _apply_zoom_mode(self):
|
||||||
|
"""Wendet den aktuellen Zoom-Modus an."""
|
||||||
|
key = self.zoom_combo.currentData()
|
||||||
|
for k, _, mode in self.ZOOM_MODES:
|
||||||
|
if k == key:
|
||||||
|
self.pdf_view.setZoomMode(mode)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Zoom-Spin nur bei Custom anzeigen
|
||||||
|
self.zoom_spin.setVisible(key == 'custom')
|
||||||
|
if key == 'custom':
|
||||||
|
self.pdf_view.setZoomFactor(self.zoom_spin.value() / 100.0)
|
||||||
|
|
||||||
|
def _apply_page_mode(self):
|
||||||
|
"""Wendet den aktuellen Seiten-Modus an."""
|
||||||
|
key = self.page_combo.currentData()
|
||||||
|
for k, _, mode in self.PAGE_MODES:
|
||||||
|
if k == key:
|
||||||
|
self.pdf_view.setPageMode(mode)
|
||||||
|
break
|
||||||
|
|
||||||
|
def load_pdf(self, path: str) -> bool:
|
||||||
|
"""Lädt eine PDF-Datei."""
|
||||||
|
error = self.pdf_document.load(path)
|
||||||
|
if error != QPdfDocument.Error.None_:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""Leert die Vorschau."""
|
||||||
|
self.pdf_document.close()
|
||||||
|
|
||||||
|
def get_settings(self) -> dict:
|
||||||
|
"""Gibt die aktuellen Einstellungen zurück."""
|
||||||
|
return {
|
||||||
|
'zoom_mode': self.zoom_combo.currentData(),
|
||||||
|
'zoom_value': self.zoom_spin.value(),
|
||||||
|
'page_mode': self.page_combo.currentData(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_settings(self, settings: dict):
|
||||||
|
"""Setzt die Einstellungen."""
|
||||||
|
if 'zoom_mode' in settings:
|
||||||
|
for i, (key, _, _) in enumerate(self.ZOOM_MODES):
|
||||||
|
if key == settings['zoom_mode']:
|
||||||
|
self.zoom_combo.setCurrentIndex(i)
|
||||||
|
break
|
||||||
|
if 'zoom_value' in settings:
|
||||||
|
self.zoom_spin.setValue(settings['zoom_value'])
|
||||||
|
if 'page_mode' in settings:
|
||||||
|
for i, (key, _, _) in enumerate(self.PAGE_MODES):
|
||||||
|
if key == settings['page_mode']:
|
||||||
|
self.page_combo.setCurrentIndex(i)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
class NoPreview(QWidget):
|
||||||
|
"""Platzhalter wenn keine Vorschau verfügbar."""
|
||||||
|
|
||||||
|
open_external = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setMinimumSize(100, 100)
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
|
self.icon_label = QLabel()
|
||||||
|
self.icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
font = QFont()
|
||||||
|
font.setPointSize(48)
|
||||||
|
self.icon_label.setFont(font)
|
||||||
|
layout.addWidget(self.icon_label)
|
||||||
|
|
||||||
|
self.message_label = QLabel("Keine Vorschau verfügbar")
|
||||||
|
self.message_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
layout.addWidget(self.message_label)
|
||||||
|
|
||||||
|
self.open_button = QPushButton("Extern öffnen")
|
||||||
|
self.open_button.setMaximumWidth(150)
|
||||||
|
self.open_button.clicked.connect(self._on_open_clicked)
|
||||||
|
layout.addWidget(self.open_button, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
|
self._current_path = ""
|
||||||
|
|
||||||
|
def set_file(self, path: str, name: str):
|
||||||
|
"""Setzt die Datei für die keine Vorschau verfügbar ist."""
|
||||||
|
self._current_path = path
|
||||||
|
icon = get_file_icon(name, False)
|
||||||
|
self.icon_label.setText(icon)
|
||||||
|
|
||||||
|
def _on_open_clicked(self):
|
||||||
|
if self._current_path:
|
||||||
|
self.open_external.emit(self._current_path)
|
||||||
|
|
||||||
|
|
||||||
|
class PreviewPanel(QWidget):
|
||||||
|
"""Preview-Panel mit Dateiinfo und Aktionsbuttons."""
|
||||||
|
|
||||||
|
rename_requested = pyqtSignal(str)
|
||||||
|
move_requested = pyqtSignal(str)
|
||||||
|
delete_requested = pyqtSignal(str)
|
||||||
|
open_external_requested = pyqtSignal(str)
|
||||||
|
detach_requested = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._current_path = ""
|
||||||
|
self._current_name = ""
|
||||||
|
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(8, 8, 8, 8)
|
||||||
|
layout.setSpacing(8)
|
||||||
|
|
||||||
|
# Header mit Dateiinfo
|
||||||
|
header = QFrame()
|
||||||
|
header.setFixedHeight(50)
|
||||||
|
header_layout = QVBoxLayout(header)
|
||||||
|
header_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
header_layout.setSpacing(4)
|
||||||
|
|
||||||
|
# Dateiname
|
||||||
|
self.name_label = QLabel()
|
||||||
|
self.name_label.setWordWrap(True)
|
||||||
|
font = QFont()
|
||||||
|
font.setBold(True)
|
||||||
|
self.name_label.setFont(font)
|
||||||
|
header_layout.addWidget(self.name_label)
|
||||||
|
|
||||||
|
# Dateigröße
|
||||||
|
self.size_label = QLabel()
|
||||||
|
header_layout.addWidget(self.size_label)
|
||||||
|
|
||||||
|
layout.addWidget(header)
|
||||||
|
|
||||||
|
# Aktionsbuttons
|
||||||
|
button_widget = QWidget()
|
||||||
|
button_widget.setFixedHeight(40)
|
||||||
|
button_layout = QHBoxLayout(button_widget)
|
||||||
|
button_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
button_layout.setSpacing(4)
|
||||||
|
|
||||||
|
self.rename_btn = QPushButton("✏️")
|
||||||
|
self.rename_btn.setToolTip("Umbenennen (F2)")
|
||||||
|
self.rename_btn.setFixedSize(36, 32)
|
||||||
|
self.rename_btn.clicked.connect(lambda: self.rename_requested.emit(self._current_path))
|
||||||
|
button_layout.addWidget(self.rename_btn)
|
||||||
|
|
||||||
|
self.move_btn = QPushButton("📦")
|
||||||
|
self.move_btn.setToolTip("Verschieben")
|
||||||
|
self.move_btn.setFixedSize(36, 32)
|
||||||
|
self.move_btn.clicked.connect(lambda: self.move_requested.emit(self._current_path))
|
||||||
|
button_layout.addWidget(self.move_btn)
|
||||||
|
|
||||||
|
self.delete_btn = QPushButton("🗑")
|
||||||
|
self.delete_btn.setToolTip("Löschen")
|
||||||
|
self.delete_btn.setFixedSize(36, 32)
|
||||||
|
self.delete_btn.clicked.connect(lambda: self.delete_requested.emit(self._current_path))
|
||||||
|
button_layout.addWidget(self.delete_btn)
|
||||||
|
|
||||||
|
button_layout.addStretch()
|
||||||
|
|
||||||
|
self.detach_btn = QPushButton("⧉")
|
||||||
|
self.detach_btn.setToolTip("In separatem Fenster öffnen")
|
||||||
|
self.detach_btn.setFixedSize(36, 32)
|
||||||
|
self.detach_btn.clicked.connect(self.detach_requested.emit)
|
||||||
|
button_layout.addWidget(self.detach_btn)
|
||||||
|
|
||||||
|
layout.addWidget(button_widget)
|
||||||
|
|
||||||
|
# Stacked Widget für verschiedene Preview-Typen
|
||||||
|
self.preview_stack = QStackedWidget()
|
||||||
|
self.preview_stack.setMinimumSize(100, 100)
|
||||||
|
|
||||||
|
# Leere Vorschau (Index 0)
|
||||||
|
self.empty_widget = QLabel("Wähle eine Datei aus")
|
||||||
|
self.empty_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.preview_stack.addWidget(self.empty_widget)
|
||||||
|
|
||||||
|
# Bild-Vorschau (Index 1)
|
||||||
|
self.image_preview = ImagePreview()
|
||||||
|
self.preview_stack.addWidget(self.image_preview)
|
||||||
|
|
||||||
|
# Text-Vorschau (Index 2)
|
||||||
|
self.text_preview = TextPreview()
|
||||||
|
self.preview_stack.addWidget(self.text_preview)
|
||||||
|
|
||||||
|
# PDF-Vorschau (Index 3)
|
||||||
|
self.pdf_preview = PdfPreview()
|
||||||
|
self.preview_stack.addWidget(self.pdf_preview)
|
||||||
|
|
||||||
|
# Keine Vorschau (Index 4)
|
||||||
|
self.no_preview = NoPreview()
|
||||||
|
self.no_preview.open_external.connect(self.open_external_requested.emit)
|
||||||
|
self.preview_stack.addWidget(self.no_preview)
|
||||||
|
|
||||||
|
layout.addWidget(self.preview_stack, 1)
|
||||||
|
|
||||||
|
self._set_buttons_enabled(False)
|
||||||
|
|
||||||
|
def _set_buttons_enabled(self, enabled: bool):
|
||||||
|
"""Aktiviert/Deaktiviert die Aktionsbuttons."""
|
||||||
|
self.rename_btn.setEnabled(enabled)
|
||||||
|
self.move_btn.setEnabled(enabled)
|
||||||
|
self.delete_btn.setEnabled(enabled)
|
||||||
|
self.detach_btn.setEnabled(enabled)
|
||||||
|
|
||||||
|
def load_file(self, path: str, name: str):
|
||||||
|
"""Lädt eine Datei für die Vorschau."""
|
||||||
|
self._current_path = path
|
||||||
|
self._current_name = name
|
||||||
|
|
||||||
|
# Header aktualisieren
|
||||||
|
self.name_label.setText(name)
|
||||||
|
try:
|
||||||
|
size = os.path.getsize(path)
|
||||||
|
self.size_label.setText(format_file_size(size))
|
||||||
|
except OSError:
|
||||||
|
self.size_label.setText("")
|
||||||
|
|
||||||
|
self._set_buttons_enabled(True)
|
||||||
|
|
||||||
|
# Passende Vorschau laden
|
||||||
|
file_type = get_file_type(name)
|
||||||
|
|
||||||
|
if file_type == 'image':
|
||||||
|
self.image_preview.load_image(path)
|
||||||
|
self.preview_stack.setCurrentIndex(1)
|
||||||
|
elif file_type == 'text':
|
||||||
|
self.text_preview.load_text(path)
|
||||||
|
self.preview_stack.setCurrentIndex(2)
|
||||||
|
elif file_type == 'pdf':
|
||||||
|
self.pdf_preview.load_pdf(path)
|
||||||
|
self.preview_stack.setCurrentIndex(3)
|
||||||
|
else:
|
||||||
|
self.no_preview.set_file(path, name)
|
||||||
|
self.preview_stack.setCurrentIndex(4)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""Leert die Vorschau."""
|
||||||
|
self._current_path = ""
|
||||||
|
self._current_name = ""
|
||||||
|
self.name_label.setText("")
|
||||||
|
self.size_label.setText("")
|
||||||
|
self._set_buttons_enabled(False)
|
||||||
|
|
||||||
|
self.image_preview.clear()
|
||||||
|
self.text_preview.clear()
|
||||||
|
self.pdf_preview.clear()
|
||||||
|
self.preview_stack.setCurrentIndex(0)
|
||||||
|
|
||||||
|
def get_current_file(self) -> tuple:
|
||||||
|
"""Gibt den aktuellen Pfad und Namen zurück."""
|
||||||
|
return self._current_path, self._current_name
|
||||||
Loading…
Reference in a new issue