Files
aza/APP/logo-tool/ui/main_window.py

239 lines
8.4 KiB
Python
Raw Permalink Normal View History

2026-03-30 07:59:11 +02:00
"""
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)