Start of the icon editor

This commit is contained in:
2026-07-27 13:08:15 +02:00
parent 713d094b08
commit 09a9e3f12f
11 changed files with 686 additions and 5 deletions

View File

@@ -0,0 +1,159 @@
from __future__ import annotations
from copy import deepcopy
from PySide6.QtCore import QEvent, QObject, Qt, Signal
from PySide6.QtGui import QKeySequence, QPainter, QShortcut, QUndoCommand, QUndoStack, QWheelEvent
from PySide6.QtWidgets import QGraphicsView, QMainWindow, QWidget
from bedit_gui.models import Icon, Shape, ShapeID
from bedit_gui.ui.generated.ui_icon_editor_window import Ui_iconEditor
from bedit_gui.views.icon_graphics_scene import IconGraphicsScene, RectangleCreationTool
class ChangeIconDraftCommand(QUndoCommand):
def __init__(self, editor: IconEditorWindow, icon: Icon, text: str) -> None:
super().__init__(text)
self.editor = editor
self.old_icon = editor.icon()
self.new_icon = deepcopy(icon)
def redo(self) -> None:
self.editor._set_icon(self.new_icon)
def undo(self) -> None:
self.editor._set_icon(self.old_icon)
class AddShapeCommand(QUndoCommand):
def __init__(self, editor: IconEditorWindow, shape_id: ShapeID, shape: Shape) -> None:
super().__init__(f"Add {shape.type}")
self.editor = editor
self.shape_id = shape_id
self.shape = deepcopy(shape)
def redo(self) -> None:
self.editor._add_shape(self.shape_id, self.shape)
def undo(self) -> None:
self.editor._remove_shape(self.shape_id)
class IconEditorWindow(QMainWindow):
"""Independent icon editing session with its own undo stack."""
saved = Signal(object)
icon_changed = Signal(object)
zoom_step = 1.2
minimum_zoom = 0.1
maximum_zoom = 10.0
def __init__(self, icon: Icon, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.ui = Ui_iconEditor()
self.ui.setupUi(self)
self.setWindowTitle("Icon Editor")
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
self._icon = deepcopy(icon)
self.scene = IconGraphicsScene(self)
self.scene.set_icon(self._icon)
self.scene.shape_created.connect(self._shape_created)
self.scene.tool_active_changed.connect(self._tool_active_changed)
self.ui.graphicsView.setScene(self.scene)
self.ui.graphicsView.setRenderHint(QPainter.RenderHint.Antialiasing)
self.ui.graphicsView.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
self.ui.graphicsView.viewport().installEventFilter(self)
self.ui.actionAdd_Rectangle.setCheckable(True)
self.ui.actionAdd_Rectangle.triggered.connect(self._start_rectangle_tool)
self.cancel_tool_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Escape), self)
self.cancel_tool_shortcut.activated.connect(self.scene.cancel_creation_tool)
self.zoom_in_shortcut = QShortcut(QKeySequence("Ctrl++"), self)
self.zoom_in_alt_shortcut = QShortcut(QKeySequence("Ctrl+="), self)
self.zoom_out_shortcut = QShortcut(QKeySequence("Ctrl+-"), self)
self.zoom_reset_shortcut = QShortcut(QKeySequence("Ctrl+0"), self)
self.zoom_in_shortcut.activated.connect(self.zoom_in)
self.zoom_in_alt_shortcut.activated.connect(self.zoom_in)
self.zoom_out_shortcut.activated.connect(self.zoom_out)
self.zoom_reset_shortcut.activated.connect(self.reset_zoom)
self.undo_stack = QUndoStack(self)
self.ui.actionUndo.triggered.connect(self.undo_stack.undo)
self.ui.actionRedo.triggered.connect(self.undo_stack.redo)
self.ui.actionSave.triggered.connect(self.save)
self.ui.actionCancel.triggered.connect(self.close)
self.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled)
self.undo_stack.canRedoChanged.connect(self.ui.actionRedo.setEnabled)
self.undo_stack.undoTextChanged.connect(self._update_undo_text)
self.undo_stack.redoTextChanged.connect(self._update_redo_text)
self.ui.actionUndo.setEnabled(False)
self.ui.actionRedo.setEnabled(False)
def icon(self) -> Icon:
return deepcopy(self._icon)
def apply_change(self, icon: Icon, text: str = "Edit icon") -> None:
self.undo_stack.push(ChangeIconDraftCommand(self, icon, text))
def save(self) -> None:
self.saved.emit(self.icon())
self.close()
def zoom_in(self) -> None:
self._zoom(self.zoom_step)
def zoom_out(self) -> None:
self._zoom(1 / self.zoom_step)
def reset_zoom(self) -> None:
self.ui.graphicsView.resetTransform()
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
if watched is self.ui.graphicsView.viewport() and isinstance(event, QWheelEvent):
if event.angleDelta().y() == 0:
return True
self.zoom_in() if event.angleDelta().y() > 0 else self.zoom_out()
event.accept()
return True
return super().eventFilter(watched, event)
def _zoom(self, factor: float) -> None:
current = self.ui.graphicsView.transform().m11()
target = max(self.minimum_zoom, min(self.maximum_zoom, current * factor))
if target != current:
factor = target / current
self.ui.graphicsView.scale(factor, factor)
def _set_icon(self, icon: Icon) -> None:
self._icon = deepcopy(icon)
self.scene.set_icon(self._icon)
self.icon_changed.emit(self.icon())
def _add_shape(self, shape_id: ShapeID, shape: Shape) -> None:
self._icon.shapes[shape_id] = deepcopy(shape)
self.scene.set_icon(self._icon)
self.icon_changed.emit(self.icon())
def _remove_shape(self, shape_id: ShapeID) -> None:
self._icon.shapes.pop(shape_id, None)
self.scene.set_icon(self._icon)
self.icon_changed.emit(self.icon())
def _shape_created(self, shape: Shape) -> None:
self.undo_stack.push(AddShapeCommand(self, ShapeID(), shape))
def _start_rectangle_tool(self) -> None:
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
self.scene.set_creation_tool(RectangleCreationTool(self.scene, layer))
def _tool_active_changed(self, active: bool) -> None:
self.ui.actionAdd_Rectangle.setChecked(active)
cursor = Qt.CursorShape.CrossCursor if active else Qt.CursorShape.ArrowCursor
self.ui.graphicsView.viewport().setCursor(cursor)
def _update_undo_text(self, text: str) -> None:
self.ui.actionUndo.setText(f"Undo {text}" if text else "Undo")
def _update_redo_text(self, text: str) -> None:
self.ui.actionRedo.setText(f"Redo {text}" if text else "Redo")