Start of the icon editor
This commit is contained in:
159
src/bedit_gui/views/icon_editor_window.py
Normal file
159
src/bedit_gui/views/icon_editor_window.py
Normal 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")
|
||||
Reference in New Issue
Block a user