160 lines
6.4 KiB
Python
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")
|