249 lines
8.1 KiB
Python
249 lines
8.1 KiB
Python
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
from typing import Dict, Optional
|
||
|
||
from PyQt6.QtCore import Qt
|
||
from PyQt6.QtGui import QAction, QKeySequence
|
||
from PyQt6.QtWidgets import (
|
||
QFileDialog,
|
||
QLabel,
|
||
QMainWindow,
|
||
QStatusBar,
|
||
QVBoxLayout,
|
||
QWidget,
|
||
QMessageBox,
|
||
)
|
||
|
||
from photo_sorter.core.config_manager import AppConfig, ConfigManager, TargetConfig
|
||
from photo_sorter.core.file_operations import FileOperations, FileOperationError
|
||
from photo_sorter.core.image_loader import ImageLoader
|
||
from photo_sorter.ui.image_viewer import ImageViewer
|
||
from photo_sorter.ui.settings_dialog import SettingsDialog, KEYS
|
||
from photo_sorter.ui.target_bar import TargetBar, TargetDisplay
|
||
|
||
|
||
class MainWindow(QMainWindow):
|
||
def __init__(self, config_manager: ConfigManager, parent: Optional[QWidget] = None) -> None: # type: ignore[name-defined]
|
||
super().__init__(parent)
|
||
self.setWindowTitle("Photo Sorter")
|
||
self.resize(1000, 700)
|
||
|
||
self._config_manager = config_manager
|
||
self._config: AppConfig = self._config_manager.load()
|
||
|
||
self._image_loader = ImageLoader()
|
||
self._file_ops = FileOperations()
|
||
|
||
self._targets_by_key: Dict[str, TargetConfig] = {t.key: t for t in self._config.targets}
|
||
|
||
self._setup_ui()
|
||
self._apply_config_to_ui()
|
||
|
||
# Beim Start direkt Quellverzeichnis wählen
|
||
self._select_source_directory(initial=True)
|
||
|
||
# --- UI Setup ---
|
||
|
||
def _setup_ui(self) -> None:
|
||
central = QWidget(self)
|
||
self.setCentralWidget(central)
|
||
|
||
layout = QVBoxLayout(central)
|
||
layout.setContentsMargins(8, 8, 8, 4)
|
||
layout.setSpacing(8)
|
||
|
||
self._info_label = QLabel("Kein Bild geladen", self)
|
||
self._info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
layout.addWidget(self._info_label)
|
||
|
||
self._viewer = ImageViewer(self)
|
||
layout.addWidget(self._viewer, stretch=1)
|
||
|
||
self._target_bar = TargetBar(self)
|
||
layout.addWidget(self._target_bar)
|
||
|
||
status = QStatusBar(self)
|
||
self.setStatusBar(status)
|
||
|
||
self._create_menus()
|
||
|
||
def _create_menus(self) -> None:
|
||
menubar = self.menuBar()
|
||
|
||
file_menu = menubar.addMenu("Datei")
|
||
|
||
open_src_action = QAction("Quellverzeichnis öffnen…", self)
|
||
open_src_action.setShortcut(QKeySequence.StandardKey.Open)
|
||
open_src_action.triggered.connect(self._select_source_directory) # type: ignore[arg-type]
|
||
file_menu.addAction(open_src_action)
|
||
|
||
file_menu.addSeparator()
|
||
|
||
exit_action = QAction("Beenden", self)
|
||
exit_action.setShortcut(QKeySequence.StandardKey.Quit)
|
||
exit_action.triggered.connect(self.close) # type: ignore[arg-type]
|
||
file_menu.addAction(exit_action)
|
||
|
||
settings_menu = menubar.addMenu("Einstellungen")
|
||
|
||
targets_action = QAction("Ziele konfigurieren…", self)
|
||
targets_action.triggered.connect(self._open_settings) # type: ignore[arg-type]
|
||
settings_menu.addAction(targets_action)
|
||
|
||
# --- Konfiguration & Zielleiste ---
|
||
|
||
def _apply_config_to_ui(self) -> None:
|
||
self._targets_by_key = {t.key: t for t in self._config.targets}
|
||
displays = [TargetDisplay(key=t.key, label=t.label) for t in self._config.targets]
|
||
self._target_bar.set_targets(displays)
|
||
|
||
def _open_settings(self) -> None:
|
||
dialog = SettingsDialog(self._config, self)
|
||
if dialog.exec() == dialog.DialogCode.Accepted: # type: ignore[comparison-overlap]
|
||
new_config = dialog.get_config()
|
||
self._config = new_config
|
||
self._config_manager.save(new_config)
|
||
self._apply_config_to_ui()
|
||
|
||
# --- Quellverzeichnis & Bildanzeige ---
|
||
|
||
def _select_source_directory(self, initial: bool = False) -> None:
|
||
directory = QFileDialog.getExistingDirectory(self, "Quellverzeichnis wählen")
|
||
if not directory:
|
||
if initial:
|
||
# Wenn beim ersten Start nichts gewählt wird, bleibt die App einfach leer geöffnet.
|
||
return
|
||
return
|
||
|
||
self._image_loader.load_from_directory(Path(directory))
|
||
if self._image_loader.count == 0:
|
||
QMessageBox.information(
|
||
self,
|
||
"Keine Bilder",
|
||
"Im gewählten Verzeichnis wurden keine unterstützten Bilddateien gefunden.",
|
||
)
|
||
self._viewer.clear()
|
||
self._info_label.setText("Keine Bilder gefunden")
|
||
return
|
||
|
||
self._show_current_image()
|
||
|
||
def _show_current_image(self) -> None:
|
||
path = self._image_loader.get_current()
|
||
count = self._image_loader.count
|
||
index = self._image_loader.current_index
|
||
if path is None:
|
||
self._viewer.clear()
|
||
self._info_label.setText("Kein Bild geladen")
|
||
return
|
||
|
||
self._viewer.set_image(path)
|
||
self._info_label.setText(f"Bild {index + 1} / {count} – {path.name}")
|
||
|
||
# --- Tastatursteuerung ---
|
||
|
||
def keyPressEvent(self, event) -> None: # type: ignore[override]
|
||
key = event.key()
|
||
|
||
if key in (Qt.Key.Key_Right, Qt.Key.Key_Space):
|
||
self._skip_forward()
|
||
return
|
||
if key == Qt.Key.Key_Left:
|
||
self._go_back()
|
||
return
|
||
if key in (Qt.Key.Key_Z,) and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
||
self._undo_action()
|
||
return
|
||
if key == Qt.Key.Key_Z:
|
||
self._undo_action()
|
||
return
|
||
|
||
# Zieltasten 1–0
|
||
key_char = None
|
||
if key == Qt.Key.Key_1:
|
||
key_char = "1"
|
||
elif key == Qt.Key.Key_2:
|
||
key_char = "2"
|
||
elif key == Qt.Key.Key_3:
|
||
key_char = "3"
|
||
elif key == Qt.Key.Key_4:
|
||
key_char = "4"
|
||
elif key == Qt.Key.Key_5:
|
||
key_char = "5"
|
||
elif key == Qt.Key.Key_6:
|
||
key_char = "6"
|
||
elif key == Qt.Key.Key_7:
|
||
key_char = "7"
|
||
elif key == Qt.Key.Key_8:
|
||
key_char = "8"
|
||
elif key == Qt.Key.Key_9:
|
||
key_char = "9"
|
||
elif key == Qt.Key.Key_0:
|
||
key_char = "0"
|
||
|
||
if key_char is not None:
|
||
self._apply_target_action(key_char)
|
||
return
|
||
|
||
super().keyPressEvent(event)
|
||
|
||
def _skip_forward(self) -> None:
|
||
if self._image_loader.count == 0:
|
||
return
|
||
self._image_loader.next()
|
||
self._show_current_image()
|
||
|
||
def _go_back(self) -> None:
|
||
if self._image_loader.count == 0:
|
||
return
|
||
self._image_loader.previous()
|
||
self._show_current_image()
|
||
|
||
def _apply_target_action(self, key: str) -> None:
|
||
if self._image_loader.count == 0:
|
||
return
|
||
target = self._targets_by_key.get(key)
|
||
if not target:
|
||
return
|
||
|
||
current = self._image_loader.get_current()
|
||
if current is None:
|
||
return
|
||
|
||
try:
|
||
new_path = self._file_ops.move_or_copy(
|
||
current,
|
||
Path(target.path),
|
||
self._config.default_action,
|
||
self._image_loader.current_index,
|
||
)
|
||
except FileOperationError as e:
|
||
QMessageBox.warning(self, "Fehler beim Verschieben/Kopieren", str(e))
|
||
return
|
||
|
||
# Pfad in der Bildliste aktualisieren (für Zurückblättern etc.)
|
||
self._image_loader.update_current_path(new_path)
|
||
|
||
# Visuelles Highlight des Ziels
|
||
self._target_bar.highlight_target(key)
|
||
|
||
# Nächstes Bild laden
|
||
self._image_loader.next()
|
||
self._show_current_image()
|
||
|
||
def _undo_action(self) -> None:
|
||
from photo_sorter.core.file_operations import FileAction
|
||
|
||
action = self._file_ops.undo_last()
|
||
if action is None:
|
||
return
|
||
|
||
# Bildliste wieder auf den Index der Aktion setzen
|
||
self._image_loader.set_index(action.index)
|
||
# Pfad in der Liste anpassen (Move: wieder Originalpfad, Copy: Original bleibt, Liste ändert sich nicht)
|
||
if action.action == "move":
|
||
self._image_loader.update_current_path(action.original_path)
|
||
|
||
self._show_current_image()
|