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,23 @@
from __future__ import annotations
from copy import deepcopy
from PySide6.QtGui import QUndoCommand
from bedit_core.models import ComponentID
from bedit_gui.models import Icon
class ChangeIconCommand(QUndoCommand):
def __init__(self, document: object, component_id: ComponentID, icon: Icon) -> None:
super().__init__("Change icon")
self.document = document
self.component_id = component_id
self.old_icon = document.stored_component_icon(component_id)
self.new_icon = deepcopy(icon)
def redo(self) -> None:
self.document._set_component_icon(self.component_id, self.new_icon)
def undo(self) -> None:
self.document._set_component_icon(self.component_id, self.old_icon)

View File

@@ -1,4 +1,5 @@
from collections.abc import Callable
from functools import partial
from typing import Protocol
from PySide6.QtCore import QObject, QPoint, Qt
@@ -9,6 +10,7 @@ from bedit_core.models import Document as CoreDocument
from bedit_gui.documents import Document
from bedit_gui.views.dialogs.interface_editor_dialog import InterfaceEditorDialog
from bedit_gui.views.dialogs.param_editor_dialog import ParamEditorDialog
from bedit_gui.views.icon_editor_window import IconEditorWindow
from bedit_gui.views.main_window import MainWindow
from bedit_gui.views.models.document_tree_model import DocumentTreeModel
@@ -48,6 +50,7 @@ class DocumentTreeController(QObject):
self.model = DocumentTreeModel()
self.interface_editor_factory = interface_editor_factory
self.param_editor_factory = param_editor_factory
self._icon_editors: list[IconEditorWindow] = []
window.ui.documentTree.setModel(self.model)
document.model_changed.connect(self._on_document_changed)
@@ -81,6 +84,7 @@ class DocumentTreeController(QObject):
menu = QMenu(self.window.ui.documentTree)
edit_interface = menu.addAction("Edit Interface")
edit_params = menu.addAction("Edit Parameters")
edit_icon = menu.addAction("Edit Icon")
selected = menu.exec(
self.window.ui.documentTree.viewport().mapToGlobal(position)
)
@@ -88,6 +92,8 @@ class DocumentTreeController(QObject):
self._edit_interface(component)
elif selected is edit_params:
self._edit_params(component)
elif selected is edit_icon:
self._edit_icon(component)
def _edit_interface(self, component: Component) -> None:
dialog = self.interface_editor_factory(
@@ -105,3 +111,14 @@ class DocumentTreeController(QObject):
if dialog.exec() == QDialog.DialogCode.Accepted:
self.document.update_component_params(component, dialog.params())
def _edit_icon(self, component: Component) -> None:
component_id = self.document.component_id(component)
editor = IconEditorWindow(self.document.component_icon(component_id), self.window)
editor.saved.connect(partial(self.document.change_icon, component_id))
editor.destroyed.connect(partial(self._icon_editor_closed, editor))
self._icon_editors.append(editor)
editor.show()
def _icon_editor_closed(self, editor: IconEditorWindow, *_args: object) -> None:
if editor in self._icon_editors:
self._icon_editors.remove(editor)

View File

@@ -1,16 +1,19 @@
from __future__ import annotations
from copy import deepcopy
from pathlib import Path
from PySide6.QtCore import QObject, Signal
from PySide6.QtGui import QUndoStack
from bedit_core.models import ID, Component, Port, PortID, Parameter, ParameterID
from bedit_core.models import ID, Component, ComponentID, GraphImplementation, Port, PortID, Parameter, ParameterID
from bedit_core.models import Document as CoreDocument
from bedit_gui.commands.change_icon_command import ChangeIconCommand
from bedit_gui.commands.port_commands import AddPortCommand, ChangePortCommand, RemovePortCommand
from bedit_gui.commands.param_commands import AddParamCommand, ChangeParamCommand, RemoveParamCommand
from bedit_gui.commands.rename_component_command import RenameComponentCommand
from bedit_gui.commands.rename_document_command import RenameDocumentCommand
from bedit_gui.models import Icon, IconDatabase
from bedit_gui.services import document_files
@@ -20,6 +23,7 @@ class Document(QObject):
model_changed = Signal(object)
path_changed = Signal(object)
modified_changed = Signal(bool)
icon_changed = Signal(object, object)
def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent)
@@ -89,6 +93,60 @@ class Document(QObject):
def rename_component(self, component: Component, name: str) -> None:
self.undo_stack.push(RenameComponentCommand(self, component, name))
def component_id(self, component: Component) -> ComponentID:
def find(components: dict[ComponentID, Component]) -> ComponentID | None:
for component_id, candidate in components.items():
if candidate is component:
return component_id
if isinstance(candidate.implementation, GraphImplementation):
found = find(candidate.implementation.graph.components)
if found is not None:
return found
return None
component_id = find(self.model.root)
if component_id is None:
raise ValueError("component is not part of this document")
return component_id
def stored_component_icon(self, component_id: ComponentID) -> Icon | None:
database = self._icon_database(False)
return deepcopy(database.icons.get(component_id)) if database is not None else None
def component_icon(self, component_id: ComponentID) -> Icon:
return self.stored_component_icon(component_id) or Icon()
def change_icon(self, component_id: ComponentID, icon: Icon) -> None:
self.undo_stack.push(ChangeIconCommand(self, component_id, icon))
def _set_component_icon(self, component_id: ComponentID, icon: Icon | None) -> None:
if icon is None:
database = self._icon_database(False)
if database is not None:
database.icons.pop(component_id, None)
if not database.icons:
self.model.metadata.pop("icon_database", None)
else:
self._icon_database(True).icons[component_id] = deepcopy(icon)
self.icon_changed.emit(component_id, self.stored_component_icon(component_id))
def _icon_database(self, create: bool) -> IconDatabase | None:
metadata = self.model.metadata
value = metadata.get("icon_database") if metadata is not None else None
if isinstance(value, dict):
value = IconDatabase.from_data(value)
metadata["icon_database"] = value
if isinstance(value, IconDatabase):
return value
if not create:
return None
if metadata is None:
metadata = {}
self.model.metadata = metadata
database = IconDatabase()
metadata["icon_database"] = database
return database
def update_component_ports(self, component: Component, ports: dict[PortID, Port]) -> None:
current = component.interface.ports
removed = [
@@ -113,6 +171,7 @@ class Document(QObject):
self.undo_stack.push(command)
self.undo_stack.endMacro()
def update_component_params(self, component: Component, params: dict[ParameterID, Parameter]) -> None:
current = component.parameters
removed = [
@@ -136,4 +195,3 @@ class Document(QObject):
for command in commands:
self.undo_stack.push(command)
self.undo_stack.endMacro()

84
src/bedit_gui/models.py Normal file
View File

@@ -0,0 +1,84 @@
"""GUI document data stored inside the core document metadata field."""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
from bedit_core.models import ComponentID, ID
class ShapeID(ID):
pass
@dataclass
class Shape:
layer: int
type: str | None = None
pos: tuple[int, int] = (0, 0)
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> Shape:
if cls is Shape and data.get("type") == "rectangle":
return Rectangle.from_data(data)
pos = data.get("pos", [0, 0])
return cls(layer=int(data.get("layer", 0)), type=data.get("type"), pos=(int(pos[0]), int(pos[1])))
def to_data(self) -> dict[str, Any]:
return {"layer": self.layer, "type": self.type, "pos": list(self.pos)}
class LineType(Enum):
NONE = "none"
SOLID = "solid"
DASHED = "dashed"
DOTTED = "dotted"
DASH_DOT = "dash_dot"
@dataclass
class Rectangle(Shape):
type: str = field(init=False, default="rectangle")
width: float = 100.0
height: float = 100.0
line_type: LineType = LineType.SOLID
line_thickness: float = 1.0
corner_radius: float = 0.0
line_color: str = "#000000"
fill_color: str = "#ffffff"
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> Rectangle:
pos = data.get("pos", [0, 0])
return cls(layer=int(data.get("layer", 0)), pos=(int(pos[0]), int(pos[1])), width=float(data.get("width", 100.0)), height=float(data.get("height", 100.0)), line_type=LineType(data.get("line_type", "solid")), line_thickness=float(data.get("line_thickness", 1.0)), corner_radius=float(data.get("corner_radius", 0.0)), line_color=str(data.get("line_color", "#000000")), fill_color=str(data.get("fill_color", "#ffffff")))
def to_data(self) -> dict[str, Any]:
return {**super().to_data(), "width": self.width, "height": self.height, "line_type": self.line_type.value, "line_thickness": self.line_thickness, "corner_radius": self.corner_radius, "line_color": self.line_color, "fill_color": self.fill_color}
@dataclass
class Icon:
shapes: dict[ShapeID, Shape] = field(default_factory=dict)
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> Icon:
shapes = {ShapeID(key): Shape.from_data(value) for key, value in data.get("shapes", {}).items()}
return cls(shapes=shapes)
def to_data(self) -> dict[str, Any]:
return {"shapes": {str(key): shape.to_data() for key, shape in self.shapes.items()}}
@dataclass
class IconDatabase:
format_version: int = 1
icons: dict[ComponentID, Icon] = field(default_factory=dict)
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> IconDatabase:
icons = {ComponentID(key): Icon.from_data(value) for key, value in data.get("icons", {}).items()}
return cls(format_version=int(data.get("format_version", 1)), icons=icons)
def to_data(self) -> dict[str, Any]:
return {"format_version": self.format_version, "icons": {str(key): icon.to_data() for key, icon in self.icons.items()}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,5 +1,6 @@
<RCC>
<qresource prefix="icons">
<file>icons/dialog-close.png</file>
<file>icons/list-remove.png</file>
<file>icons/list-add.png</file>
<file>icons/view-form-table.png</file>

View File

@@ -1,17 +1,25 @@
from __future__ import annotations
from copy import deepcopy
from pathlib import Path
from bedit_core.models import Document
from bedit_core.serialization import load as load_document
from bedit_core.serialization import save as save_document
from bedit_gui.models import IconDatabase
def load(path: str | Path) -> Document:
"""Load a supported document file into the core model."""
return load_document(path)
document = load_document(path)
if document.metadata is not None and isinstance(document.metadata.get("icon_database"), dict):
document.metadata["icon_database"] = IconDatabase.from_data(document.metadata["icon_database"])
return document
def save(document: Document, path: str | Path) -> None:
"""Save a core model using the format selected by its file extension."""
save_document(document, path)
saved_document = deepcopy(document)
if saved_document.metadata is not None and isinstance(saved_document.metadata.get("icon_database"), IconDatabase):
saved_document.metadata["icon_database"] = saved_document.metadata["icon_database"].to_data()
save_document(saved_document, path)

View File

@@ -0,0 +1,168 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>iconEditor</class>
<widget class="QMainWindow" name="iconEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<property name="windowIcon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/draw-path.png</normaloff>:/icons/icons/draw-path.png</iconset>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGraphicsView" name="graphicsView"/>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>22</height>
</rect>
</property>
<widget class="QMenu" name="menuEdit">
<property name="title">
<string>Edit</string>
</property>
<addaction name="actionUndo"/>
<addaction name="actionRedo"/>
<addaction name="separator"/>
<addaction name="actionSave"/>
<addaction name="actionCancel"/>
</widget>
<addaction name="menuEdit"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QToolBar" name="actionToolbar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionUndo"/>
<addaction name="actionRedo"/>
<addaction name="actionSave"/>
<addaction name="actionCancel"/>
</widget>
<widget class="QToolBar" name="iconToolbar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionAdd_Rectangle"/>
</widget>
<action name="actionUndo">
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/edit-undo.png</normaloff>:/icons/icons/edit-undo.png</iconset>
</property>
<property name="text">
<string>Undo</string>
</property>
<property name="toolTip">
<string>Undo</string>
</property>
<property name="shortcut">
<string>Ctrl+Z</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionRedo">
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/edit-redo.png</normaloff>:/icons/icons/edit-redo.png</iconset>
</property>
<property name="text">
<string>Redo</string>
</property>
<property name="toolTip">
<string>Redo</string>
</property>
<property name="shortcut">
<string>Ctrl+Y</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionSave">
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/document-save.png</normaloff>:/icons/icons/document-save.png</iconset>
</property>
<property name="text">
<string>Save</string>
</property>
<property name="toolTip">
<string>Save icon</string>
</property>
<property name="shortcut">
<string>Return</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionCancel">
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/dialog-close.png</normaloff>:/icons/icons/dialog-close.png</iconset>
</property>
<property name="text">
<string>Cancel</string>
</property>
<property name="toolTip">
<string>Cancel icon editing</string>
</property>
<property name="shortcut">
<string>Shift+Esc</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionAdd_Rectangle">
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/draw-rectangle.png</normaloff>:/icons/icons/draw-rectangle.png</iconset>
</property>
<property name="text">
<string>Add Rectangle</string>
</property>
<property name="toolTip">
<string>Add a rectangle</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
</widget>
<resources>
<include location="../../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

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

View File

@@ -0,0 +1,123 @@
from __future__ import annotations
from typing import Protocol
from PySide6.QtCore import QObject, QPointF, QRectF, Qt, Signal
from PySide6.QtGui import QBrush, QColor, QPainterPath, QPen
from PySide6.QtWidgets import QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneMouseEvent
from bedit_gui.models import Icon, LineType, Rectangle, Shape
class ShapeCreationTool(Protocol):
def begin(self, position: QPointF) -> None: ...
def update(self, position: QPointF) -> None: ...
def finish(self, position: QPointF) -> Shape | None: ...
def cancel(self) -> None: ...
class RectangleCreationTool:
def __init__(self, scene: QGraphicsScene, layer: int) -> None:
self.scene = scene
self.layer = layer
self.start: QPointF | None = None
self.preview: QGraphicsRectItem | None = None
def begin(self, position: QPointF) -> None:
self.start = position
self.preview = self.scene.addRect(QRectF(position, position), QPen(Qt.PenStyle.DashLine))
def update(self, position: QPointF) -> None:
if self.preview is not None and self.start is not None:
self.preview.setRect(QRectF(self.start, position).normalized())
def finish(self, position: QPointF) -> Rectangle | None:
if self.start is None:
return None
rect = QRectF(self.start, position).normalized()
self.cancel()
if rect.width() < 1 or rect.height() < 1:
return None
return Rectangle(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height())
def cancel(self) -> None:
if self.preview is not None:
self.scene.removeItem(self.preview)
self.preview = None
self.start = None
class IconGraphicsScene(QGraphicsScene):
shape_created = Signal(object)
tool_active_changed = Signal(bool)
def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent)
self._tool: ShapeCreationTool | None = None
self.setSceneRect(-100, -100, 200, 200)
def set_icon(self, icon: Icon) -> None:
self.cancel_creation_tool()
self.clear()
for shape in sorted(icon.shapes.values(), key=lambda item: item.layer):
self._add_shape_item(shape)
def set_creation_tool(self, tool: ShapeCreationTool) -> None:
self.cancel_creation_tool()
self._tool = tool
self.tool_active_changed.emit(True)
def cancel_creation_tool(self) -> None:
if self._tool is None:
return
self._tool.cancel()
self._tool = None
self.tool_active_changed.emit(False)
def has_creation_tool(self) -> bool:
return self._tool is not None
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if self._tool is not None and event.button() == Qt.MouseButton.LeftButton:
self._tool.begin(event.scenePos())
event.accept()
return
super().mousePressEvent(event)
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if self._tool is not None and event.buttons() & Qt.MouseButton.LeftButton:
self._tool.update(event.scenePos())
event.accept()
return
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if self._tool is not None and event.button() == Qt.MouseButton.LeftButton:
tool = self._tool
shape = tool.finish(event.scenePos())
self._tool = None
self.tool_active_changed.emit(False)
if shape is not None:
self.shape_created.emit(shape)
event.accept()
return
super().mouseReleaseEvent(event)
def _add_shape_item(self, shape: Shape) -> None:
if not isinstance(shape, Rectangle):
return
rect = QRectF(shape.pos[0], shape.pos[1], shape.width, shape.height)
path = QPainterPath()
path.addRoundedRect(rect, shape.corner_radius, shape.corner_radius)
item = QGraphicsPathItem(path)
item.setPen(self._pen(shape))
item.setBrush(QBrush(QColor(shape.fill_color)))
item.setZValue(shape.layer)
self.addItem(item)
@staticmethod
def _pen(shape: Rectangle) -> QPen:
styles = {LineType.SOLID: Qt.PenStyle.SolidLine, LineType.DASHED: Qt.PenStyle.DashLine, LineType.DOTTED: Qt.PenStyle.DotLine, LineType.DASH_DOT: Qt.PenStyle.DashDotLine}
if shape.line_type is LineType.NONE:
return QPen(Qt.PenStyle.NoPen)
return QPen(QColor(shape.line_color), shape.line_thickness, styles[shape.line_type])

View File

@@ -396,5 +396,45 @@
}
}
},
"metadata": {}
"metadata": {
"icon_database": {
"format_version": 1,
"icons": {
"43b3aee6-0b38-429b-9c6d-d38fc097297f": {
"shapes": {
"1749a693-44fe-4275-ab22-eb82db339df4": {
"layer": 0,
"type": "rectangle",
"pos": [
-138,
-84
],
"width": 241.0,
"height": 217.0,
"line_type": "solid",
"line_thickness": 1.0,
"corner_radius": 0.0,
"line_color": "#000000",
"fill_color": "#ffffff"
},
"923075c7-3d34-4d0f-902d-cda24731433a": {
"layer": 1,
"type": "rectangle",
"pos": [
-63,
-31
],
"width": 72.0,
"height": 119.0,
"line_type": "solid",
"line_thickness": 1.0,
"corner_radius": 0.0,
"line_color": "#000000",
"fill_color": "#ffffff"
}
}
}
}
}
}
}