""" Hauptfenster: verbindet Canvas, Toolbar, Properties-Panel und Statusleiste. """ from __future__ import annotations from pathlib import Path from PySide6.QtWidgets import ( QMainWindow, QHBoxLayout, QWidget, QFileDialog, QMessageBox, ) from PySide6.QtCore import Qt from PySide6.QtGui import QPixmap from ui.canvas_widget import LogoCanvas from ui.properties_panel import PropertiesPanel from ui.toolbar import MainToolbar from ui.status_bar import LogoStatusBar from core.logo_model import LogoProject from core.image_loader import load_png, numpy_to_qpixmap class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Logo-Tool – Pixel-Perfect Editor") self.resize(1400, 900) self.setMinimumSize(900, 600) self.project = LogoProject() self._apply_dark_theme() self._build_ui() self._connect_signals() def _apply_dark_theme(self): self.setStyleSheet(""" QMainWindow { background-color: #252525; } QWidget { color: #ddd; font-family: "Segoe UI", "Inter", sans-serif; font-size: 13px; } QScrollBar:vertical { background: #2a2a2a; width: 10px; border: none; } QScrollBar::handle:vertical { background: #555; border-radius: 4px; min-height: 30px; } QScrollBar:horizontal { background: #2a2a2a; height: 10px; border: none; } QScrollBar::handle:horizontal { background: #555; border-radius: 4px; min-width: 30px; } QDoubleSpinBox, QSpinBox { background: #333; border: 1px solid #555; border-radius: 3px; padding: 3px 6px; color: #ddd; } QSlider::groove:horizontal { background: #444; height: 4px; border-radius: 2px; } QSlider::handle:horizontal { background: #0078d4; width: 14px; height: 14px; margin: -5px 0; border-radius: 7px; } """) def _build_ui(self): # Toolbar self.toolbar = MainToolbar(self) self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.toolbar) # Statusbar self.status_bar = LogoStatusBar(self) self.setStatusBar(self.status_bar) # Zentrales Layout: Canvas + Properties-Panel central = QWidget() layout = QHBoxLayout(central) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.canvas = LogoCanvas() layout.addWidget(self.canvas, stretch=1) self.properties = PropertiesPanel() layout.addWidget(self.properties) self.setCentralWidget(central) def _connect_signals(self): # Toolbar → Aktionen self.toolbar.open_file_requested.connect(self._on_open_file) self.toolbar.save_file_requested.connect(self._on_save_file) self.toolbar.zoom_to_fit_requested.connect(self.canvas.fit_to_view) self.toolbar.zoom_value_changed.connect(self.canvas.set_zoom) self.toolbar.show_original_toggled.connect(self._on_toggle_original) self.toolbar.show_vectors_toggled.connect(self._on_toggle_vectors) # Canvas → Statusleiste self.canvas.zoom_changed.connect(self.status_bar.set_zoom) self.canvas.zoom_changed.connect(self.toolbar.update_zoom_display) # Properties → Rendering self.properties.letter_spacing_changed.connect(self._on_spacing_changed) self.properties.stroke_width_changed.connect(self._on_stroke_changed) self.properties.retrace_requested.connect(self._on_retrace) self.properties.export_requested.connect(self._on_export) # ─── Datei-Aktionen ────────────────────────────────────── def _on_open_file(self): path, _ = QFileDialog.getOpenFileName( self, "Logo-PNG öffnen", "", "Bilder (*.png *.jpg *.jpeg *.bmp);;Alle Dateien (*)", ) if not path: return try: pixels, w, h = load_png(path) except Exception as e: QMessageBox.critical(self, "Fehler", f"Bild konnte nicht geladen werden:\n{e}") return self.project = LogoProject( source_path=Path(path), original_pixels=pixels, width=w, height=h, ) pixmap = numpy_to_qpixmap(pixels, w, h) self.canvas.set_original_image(pixmap) self.status_bar.set_file_info(Path(path).name, w, h) self.status_bar.set_path_count(0) self._try_auto_trace() def _on_save_file(self): if not self.project.has_image: QMessageBox.information(self, "Info", "Kein Bild geladen.") return path, _ = QFileDialog.getSaveFileName( self, "Logo speichern", "", "PNG (*.png);;SVG (*.svg)", ) if not path: return if path.lower().endswith(".png"): self._export_png(path) else: QMessageBox.information( self, "Info", "SVG-Export wird in einer zukünftigen Version unterstützt." ) def _on_export(self): self._on_save_file() # ─── Rendering ─────────────────────────────────────────── def _on_spacing_changed(self, value: float): self.project.global_letter_spacing = value self._update_vector_overlay() def _on_stroke_changed(self, value: float): self.project.global_stroke_width = value self._update_vector_overlay() def _on_retrace(self): self._try_auto_trace() def _try_auto_trace(self): """Versucht die automatische Vektorisierung.""" if not self.project.has_image: return try: from core.vectorizer import trace_to_paths except ImportError: QMessageBox.warning( self, "Warnung", "potracer ist nicht installiert.\n" "Bitte 'pip install potracer' ausführen für Vektorisierung.", ) return threshold = self.properties.slider_threshold.value() try: glyphs = trace_to_paths(self.project.original_pixels, threshold=threshold) self.project.glyphs = glyphs self.status_bar.set_path_count(len(glyphs)) self.properties.populate_glyphs([g.label for g in glyphs]) self._update_vector_overlay() except ImportError as e: QMessageBox.warning(self, "Warnung", str(e)) except Exception as e: QMessageBox.critical(self, "Tracing-Fehler", f"Vektorisierung fehlgeschlagen:\n{e}") def _update_vector_overlay(self): """Rendert die Vektorpfade neu und aktualisiert das Overlay.""" if not self.project.has_vectors: self.canvas.clear_overlay() return try: from core.cairo_renderer import render_to_qpixmap overlay = render_to_qpixmap(self.project) self.canvas.set_overlay(overlay) except Exception as e: self.status_bar.showMessage(f"Render-Fehler: {e}", 5000) def _export_png(self, path: str): """Exportiert das aktuelle Rendering als PNG.""" try: from core.cairo_renderer import render_project import numpy as np from PIL import Image pixels = render_project(self.project) img = Image.fromarray(pixels, "RGBA") img.save(path) self.status_bar.showMessage(f"Exportiert: {path}", 3000) except Exception as e: QMessageBox.critical(self, "Export-Fehler", f"Export fehlgeschlagen:\n{e}") # ─── Ansicht ───────────────────────────────────────────── def _on_toggle_original(self, visible: bool): if hasattr(self.canvas, '_pixmap_item') and self.canvas._pixmap_item: self.canvas._pixmap_item.setVisible(visible) def _on_toggle_vectors(self, visible: bool): if hasattr(self.canvas, '_overlay_item') and self.canvas._overlay_item: self.canvas._overlay_item.setVisible(visible)