Files
BEdit/src/bedit_gui/views/icon_editor_window.py

160 lines
6.4 KiB
Python

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")