239 lines
8.4 KiB
Python
239 lines
8.4 KiB
Python
"""
|
||
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)
|