Text fields and saving Icons to file
This commit is contained in:
25
0.icon.json
Normal file
25
0.icon.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"shapes": {
|
||||||
|
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||||
|
"layer": 0,
|
||||||
|
"type": "text",
|
||||||
|
"pos": [
|
||||||
|
-32,
|
||||||
|
-32
|
||||||
|
],
|
||||||
|
"width": 64,
|
||||||
|
"height": 64,
|
||||||
|
"color": "#000000ff",
|
||||||
|
"bold": true,
|
||||||
|
"italic": false,
|
||||||
|
"size": 64.0,
|
||||||
|
"text": "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"port_positions": {
|
||||||
|
"497b1f74-1186-471f-976a-36b07a451caf": [
|
||||||
|
-8,
|
||||||
|
-8
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
25
1.icon.json
Normal file
25
1.icon.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"shapes": {
|
||||||
|
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||||
|
"layer": 0,
|
||||||
|
"type": "text",
|
||||||
|
"pos": [
|
||||||
|
-32,
|
||||||
|
-32
|
||||||
|
],
|
||||||
|
"width": 64,
|
||||||
|
"height": 64,
|
||||||
|
"color": "#000000ff",
|
||||||
|
"bold": true,
|
||||||
|
"italic": false,
|
||||||
|
"size": 64.0,
|
||||||
|
"text": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"port_positions": {
|
||||||
|
"4306701a-6b1b-4d19-b8ff-45dbfa04f2d3": [
|
||||||
|
-8,
|
||||||
|
-8
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
29
C.icon.json
Normal file
29
C.icon.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"shapes": {
|
||||||
|
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||||
|
"layer": 0,
|
||||||
|
"type": "text",
|
||||||
|
"pos": [
|
||||||
|
-32,
|
||||||
|
-32
|
||||||
|
],
|
||||||
|
"width": 64.0,
|
||||||
|
"height": 64.0,
|
||||||
|
"color": "#000000ff",
|
||||||
|
"bold": true,
|
||||||
|
"italic": false,
|
||||||
|
"size": 64.0,
|
||||||
|
"text": "C"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"port_positions": {
|
||||||
|
"c87881f1-23b4-4a69-b586-8e3c7bf6e21e": [
|
||||||
|
-8,
|
||||||
|
-8
|
||||||
|
],
|
||||||
|
"c62de8eb-e13b-4849-bea5-c8cc5332269e": [
|
||||||
|
16,
|
||||||
|
-32
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
29
I.icon.json
Normal file
29
I.icon.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"shapes": {
|
||||||
|
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||||
|
"layer": 0,
|
||||||
|
"type": "text",
|
||||||
|
"pos": [
|
||||||
|
-32,
|
||||||
|
-32
|
||||||
|
],
|
||||||
|
"width": 64,
|
||||||
|
"height": 64,
|
||||||
|
"color": "#000000ff",
|
||||||
|
"bold": true,
|
||||||
|
"italic": false,
|
||||||
|
"size": 64.0,
|
||||||
|
"text": "I"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"port_positions": {
|
||||||
|
"0b9036b1-e4e4-437e-8c35-f5bb391b6cbd": [
|
||||||
|
-8,
|
||||||
|
-8
|
||||||
|
],
|
||||||
|
"fb25f35d-4c0c-4cfa-92d4-1a18a71e2c11": [
|
||||||
|
16,
|
||||||
|
-32
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
29
R.icon.json
Normal file
29
R.icon.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"shapes": {
|
||||||
|
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||||
|
"layer": 0,
|
||||||
|
"type": "text",
|
||||||
|
"pos": [
|
||||||
|
-32,
|
||||||
|
-32
|
||||||
|
],
|
||||||
|
"width": 64,
|
||||||
|
"height": 64,
|
||||||
|
"color": "#000000ff",
|
||||||
|
"bold": true,
|
||||||
|
"italic": false,
|
||||||
|
"size": 64.0,
|
||||||
|
"text": "R"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"port_positions": {
|
||||||
|
"0ac7d4ea-77f7-4c0d-8e37-406ef66e4740": [
|
||||||
|
-8,
|
||||||
|
-8
|
||||||
|
],
|
||||||
|
"d1f5aab2-7bff-4b1e-8697-98fb6c3d8f02": [
|
||||||
|
16,
|
||||||
|
-32
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ class Shape:
|
|||||||
def from_data(cls, data: Mapping[str, Any]) -> Shape:
|
def from_data(cls, data: Mapping[str, Any]) -> Shape:
|
||||||
if cls is Shape and data.get("type") == "rectangle":
|
if cls is Shape and data.get("type") == "rectangle":
|
||||||
return Rectangle.from_data(data)
|
return Rectangle.from_data(data)
|
||||||
|
if cls is Shape and data.get("type") == "text":
|
||||||
|
return Text.from_data(data)
|
||||||
pos = data.get("pos", [0, 0])
|
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])))
|
return cls(layer=int(data.get("layer", 0)), type=data.get("type"), pos=(int(pos[0]), int(pos[1])))
|
||||||
|
|
||||||
@@ -59,14 +61,22 @@ class Rectangle(Shape):
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Text(Shape):
|
class Text(Shape):
|
||||||
type: str = field(init=False, default="text")
|
type: str = field(init=False, default="text")
|
||||||
width: float = 100.0
|
width: float = 32.0
|
||||||
height: float = 100.0
|
height: float = 16.0
|
||||||
color: str = "#000000ff"
|
color: str = "#000000ff"
|
||||||
bold: bool = False
|
bold: bool = False
|
||||||
italic: bool = False
|
italic: bool = False
|
||||||
size: float = 16.0
|
size: float = 16.0
|
||||||
text: str = ""
|
text: str = ""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_data(cls, data: Mapping[str, Any]) -> Text:
|
||||||
|
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", 32.0)), height=float(data.get("height", 16.0)), color=str(data.get("color", "#000000ff")), bold=bool(data.get("bold", False)), italic=bool(data.get("italic", False)), size=float(data.get("size", 16.0)), text=str(data.get("text", "")))
|
||||||
|
|
||||||
|
def to_data(self) -> dict[str, Any]:
|
||||||
|
return {**super().to_data(), "width": self.width, "height": self.height, "color": self.color, "bold": self.bold, "italic": self.italic, "size": self.size, "text": self.text}
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Icon:
|
class Icon:
|
||||||
shapes: dict[ShapeID, Shape] = field(default_factory=dict)
|
shapes: dict[ShapeID, Shape] = field(default_factory=dict)
|
||||||
|
|||||||
17
src/bedit_gui/services/icon_files.py
Normal file
17
src/bedit_gui/services/icon_files.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from bedit_gui.models import Icon
|
||||||
|
|
||||||
|
|
||||||
|
def load(path: str | Path) -> Icon:
|
||||||
|
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise TypeError("icon file must contain a JSON object")
|
||||||
|
return Icon.from_data(data)
|
||||||
|
|
||||||
|
|
||||||
|
def save(icon: Icon, path: str | Path) -> None:
|
||||||
|
Path(path).write_text(json.dumps(icon.to_data(), indent=2) + "\n", encoding="utf-8")
|
||||||
@@ -39,10 +39,18 @@
|
|||||||
</property>
|
</property>
|
||||||
<addaction name="actionUndo"/>
|
<addaction name="actionUndo"/>
|
||||||
<addaction name="actionRedo"/>
|
<addaction name="actionRedo"/>
|
||||||
|
</widget>
|
||||||
|
<widget class="QMenu" name="menuFile">
|
||||||
|
<property name="title">
|
||||||
|
<string>File</string>
|
||||||
|
</property>
|
||||||
|
<addaction name="actionOpen_from_File"/>
|
||||||
|
<addaction name="actionSave_to_File"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="actionSave"/>
|
<addaction name="actionSave"/>
|
||||||
<addaction name="actionCancel"/>
|
<addaction name="actionCancel"/>
|
||||||
</widget>
|
</widget>
|
||||||
|
<addaction name="menuFile"/>
|
||||||
<addaction name="menuEdit"/>
|
<addaction name="menuEdit"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QStatusBar" name="statusbar"/>
|
<widget class="QStatusBar" name="statusbar"/>
|
||||||
@@ -176,6 +184,42 @@
|
|||||||
<enum>QAction::MenuRole::NoRole</enum>
|
<enum>QAction::MenuRole::NoRole</enum>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="actionSave_to_File">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/document-save-as.png</normaloff>:/icons/icons/document-save-as.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Save to File</string>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Save icon to a file</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+Shift+S</string>
|
||||||
|
</property>
|
||||||
|
<property name="menuRole">
|
||||||
|
<enum>QAction::MenuRole::NoRole</enum>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionOpen_from_File">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/document-open.png</normaloff>:/icons/icons/document-open.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Open from File</string>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Open icon from File</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+Shift+O</string>
|
||||||
|
</property>
|
||||||
|
<property name="menuRole">
|
||||||
|
<enum>QAction::MenuRole::NoRole</enum>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../../resources/resources.qrc"/>
|
<include location="../../resources/resources.qrc"/>
|
||||||
|
|||||||
@@ -4,14 +4,18 @@ from copy import deepcopy
|
|||||||
|
|
||||||
from PySide6.QtCore import QEvent, QObject, Qt, Signal
|
from PySide6.QtCore import QEvent, QObject, Qt, Signal
|
||||||
from PySide6.QtGui import QKeySequence, QPainter, QShortcut, QUndoCommand, QUndoStack, QWheelEvent, QShowEvent
|
from PySide6.QtGui import QKeySequence, QPainter, QShortcut, QUndoCommand, QUndoStack, QWheelEvent, QShowEvent
|
||||||
from PySide6.QtWidgets import QDialog, QGraphicsView, QMainWindow, QWidget
|
from PySide6.QtWidgets import QDialog, QFileDialog, QGraphicsView, QMainWindow, QMessageBox, QWidget
|
||||||
|
|
||||||
from bedit_core.models import Port, PortID, SignalDirection
|
from bedit_core.models import Port, PortID, SignalDirection
|
||||||
from bedit_gui.models import Icon, Shape, ShapeID
|
from bedit_gui.models import Icon, Shape, ShapeID
|
||||||
|
from bedit_gui.services import icon_files
|
||||||
|
from bedit_gui.services.application_logging import get_logger
|
||||||
from bedit_gui.ui.generated.ui_icon_editor_window import Ui_iconEditor
|
from bedit_gui.ui.generated.ui_icon_editor_window import Ui_iconEditor
|
||||||
from bedit_gui.views.icon_graphics_scene import IconGraphicsScene, RectangleCreationTool
|
from bedit_gui.views.icon_graphics_scene import IconGraphicsScene, RectangleCreationTool, TextCreationTool
|
||||||
from bedit_gui.views.shape_options_dialog import ShapeOptionsDialog
|
from bedit_gui.views.shape_options_dialog import ShapeOptionsDialog
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ChangeIconDraftCommand(QUndoCommand):
|
class ChangeIconDraftCommand(QUndoCommand):
|
||||||
def __init__(self, editor: IconEditorWindow, icon: Icon, text: str) -> None:
|
def __init__(self, editor: IconEditorWindow, icon: Icon, text: str) -> None:
|
||||||
@@ -102,10 +106,11 @@ class IconEditorWindow(QMainWindow):
|
|||||||
self.setWindowTitle("Icon Editor")
|
self.setWindowTitle("Icon Editor")
|
||||||
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
||||||
self._icon = deepcopy(icon)
|
self._icon = deepcopy(icon)
|
||||||
|
self._ports = deepcopy(ports)
|
||||||
|
|
||||||
self.scene = IconGraphicsScene(self)
|
self.scene = IconGraphicsScene(self)
|
||||||
self.scene.set_ports(ports)
|
self.scene.set_ports(self._ports)
|
||||||
self._ensure_port_positions(ports)
|
self._ensure_port_positions(self._ports)
|
||||||
self.scene.set_icon(self._icon)
|
self.scene.set_icon(self._icon)
|
||||||
self.scene.port_moved.connect(self._port_moved)
|
self.scene.port_moved.connect(self._port_moved)
|
||||||
self.scene.shape_created.connect(self._shape_created)
|
self.scene.shape_created.connect(self._shape_created)
|
||||||
@@ -118,6 +123,8 @@ class IconEditorWindow(QMainWindow):
|
|||||||
self.ui.graphicsView.viewport().installEventFilter(self)
|
self.ui.graphicsView.viewport().installEventFilter(self)
|
||||||
self.ui.actionAdd_Rectangle.setCheckable(True)
|
self.ui.actionAdd_Rectangle.setCheckable(True)
|
||||||
self.ui.actionAdd_Rectangle.triggered.connect(self._start_rectangle_tool)
|
self.ui.actionAdd_Rectangle.triggered.connect(self._start_rectangle_tool)
|
||||||
|
self.ui.actionAdd_Text.setCheckable(True)
|
||||||
|
self.ui.actionAdd_Text.triggered.connect(self._start_text_tool)
|
||||||
self.cancel_tool_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Escape), self)
|
self.cancel_tool_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Escape), self)
|
||||||
self.cancel_tool_shortcut.activated.connect(self.scene.cancel_creation_tool)
|
self.cancel_tool_shortcut.activated.connect(self.scene.cancel_creation_tool)
|
||||||
self.delete_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Delete), self)
|
self.delete_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Delete), self)
|
||||||
@@ -135,6 +142,8 @@ class IconEditorWindow(QMainWindow):
|
|||||||
self.ui.actionUndo.triggered.connect(self.undo_stack.undo)
|
self.ui.actionUndo.triggered.connect(self.undo_stack.undo)
|
||||||
self.ui.actionRedo.triggered.connect(self.undo_stack.redo)
|
self.ui.actionRedo.triggered.connect(self.undo_stack.redo)
|
||||||
self.ui.actionSave.triggered.connect(self.save)
|
self.ui.actionSave.triggered.connect(self.save)
|
||||||
|
self.ui.actionSave_to_File.triggered.connect(self.save_to_file)
|
||||||
|
self.ui.actionOpen_from_File.triggered.connect(self.open_from_file)
|
||||||
self.ui.actionCancel.triggered.connect(self.close)
|
self.ui.actionCancel.triggered.connect(self.close)
|
||||||
self.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled)
|
self.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled)
|
||||||
self.undo_stack.canRedoChanged.connect(self.ui.actionRedo.setEnabled)
|
self.undo_stack.canRedoChanged.connect(self.ui.actionRedo.setEnabled)
|
||||||
@@ -157,6 +166,32 @@ class IconEditorWindow(QMainWindow):
|
|||||||
self.saved.emit(self.icon())
|
self.saved.emit(self.icon())
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
def save_to_file(self) -> None:
|
||||||
|
file_name, _ = QFileDialog.getSaveFileName(self, "Save Icon", "", "JSON files (*.json)")
|
||||||
|
if not file_name:
|
||||||
|
return
|
||||||
|
path = file_name if file_name.lower().endswith(".json") else f"{file_name}.json"
|
||||||
|
try:
|
||||||
|
icon_files.save(self._icon, path)
|
||||||
|
except (OSError, TypeError, ValueError) as exc:
|
||||||
|
logger.exception("Could not save icon to %s", path)
|
||||||
|
QMessageBox.critical(self, "Could not save icon", str(exc))
|
||||||
|
return
|
||||||
|
logger.info("Saved icon to: %s", path)
|
||||||
|
|
||||||
|
def open_from_file(self) -> None:
|
||||||
|
file_name, _ = QFileDialog.getOpenFileName(self, "Open Icon", "", "JSON files (*.json)")
|
||||||
|
if not file_name:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
icon = icon_files.load(file_name)
|
||||||
|
except (OSError, TypeError, ValueError) as exc:
|
||||||
|
logger.exception("Could not open icon from %s", file_name)
|
||||||
|
QMessageBox.critical(self, "Could not open icon", str(exc))
|
||||||
|
return
|
||||||
|
self.apply_change(icon, "Load icon from file")
|
||||||
|
logger.info("Loaded icon from: %s", file_name)
|
||||||
|
|
||||||
def zoom_in(self) -> None:
|
def zoom_in(self) -> None:
|
||||||
self._zoom(self.zoom_step)
|
self._zoom(self.zoom_step)
|
||||||
|
|
||||||
@@ -187,6 +222,7 @@ class IconEditorWindow(QMainWindow):
|
|||||||
|
|
||||||
def _set_icon(self, icon: Icon) -> None:
|
def _set_icon(self, icon: Icon) -> None:
|
||||||
self._icon = deepcopy(icon)
|
self._icon = deepcopy(icon)
|
||||||
|
self._ensure_port_positions(self._ports)
|
||||||
self.scene.set_icon(self._icon)
|
self.scene.set_icon(self._icon)
|
||||||
self.icon_changed.emit(self.icon())
|
self.icon_changed.emit(self.icon())
|
||||||
|
|
||||||
@@ -268,10 +304,18 @@ class IconEditorWindow(QMainWindow):
|
|||||||
|
|
||||||
def _start_rectangle_tool(self) -> None:
|
def _start_rectangle_tool(self) -> None:
|
||||||
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
|
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
|
||||||
|
self.ui.actionAdd_Text.setChecked(False)
|
||||||
self.scene.set_creation_tool(RectangleCreationTool(self.scene, layer))
|
self.scene.set_creation_tool(RectangleCreationTool(self.scene, layer))
|
||||||
|
|
||||||
|
def _start_text_tool(self) -> None:
|
||||||
|
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
|
||||||
|
self.ui.actionAdd_Rectangle.setChecked(False)
|
||||||
|
self.scene.set_creation_tool(TextCreationTool(self.scene, layer))
|
||||||
|
|
||||||
def _tool_active_changed(self, active: bool) -> None:
|
def _tool_active_changed(self, active: bool) -> None:
|
||||||
self.ui.actionAdd_Rectangle.setChecked(active)
|
if not active:
|
||||||
|
self.ui.actionAdd_Rectangle.setChecked(False)
|
||||||
|
self.ui.actionAdd_Text.setChecked(False)
|
||||||
cursor = Qt.CursorShape.CrossCursor if active else Qt.CursorShape.ArrowCursor
|
cursor = Qt.CursorShape.CrossCursor if active else Qt.CursorShape.ArrowCursor
|
||||||
self.ui.graphicsView.viewport().setCursor(cursor)
|
self.ui.graphicsView.viewport().setCursor(cursor)
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ from copy import deepcopy
|
|||||||
from typing import Protocol
|
from typing import Protocol
|
||||||
|
|
||||||
from PySide6.QtCore import QObject, QPointF, QRectF, Qt, Signal
|
from PySide6.QtCore import QObject, QPointF, QRectF, Qt, Signal
|
||||||
from PySide6.QtGui import QBrush, QColor, QPainter, QPainterPath, QPen
|
from PySide6.QtGui import QBrush, QColor, QFont, QPainter, QPainterPath, QPen
|
||||||
from PySide6.QtWidgets import QGraphicsItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget
|
from PySide6.QtWidgets import QGraphicsItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget
|
||||||
|
|
||||||
from bedit_core.models import Port, PortID, SignalDirection
|
from bedit_core.models import Port, PortID, SignalDirection
|
||||||
from bedit_gui.models import Icon, LineType, Rectangle, Shape, ShapeID
|
from bedit_gui.models import Icon, LineType, Rectangle, Shape, ShapeID, Text
|
||||||
|
|
||||||
|
|
||||||
class ShapeCreationTool(Protocol):
|
class ShapeCreationTool(Protocol):
|
||||||
@@ -36,7 +36,7 @@ class RectangleCreationTool:
|
|||||||
position = self._bounded(position)
|
position = self._bounded(position)
|
||||||
self.preview.setRect(QRectF(self.start, position).normalized())
|
self.preview.setRect(QRectF(self.start, position).normalized())
|
||||||
|
|
||||||
def finish(self, position: QPointF) -> Rectangle | None:
|
def finish(self, position: QPointF) -> Shape | None:
|
||||||
if self.start is None:
|
if self.start is None:
|
||||||
return None
|
return None
|
||||||
position = self._bounded(position)
|
position = self._bounded(position)
|
||||||
@@ -44,6 +44,9 @@ class RectangleCreationTool:
|
|||||||
self.cancel()
|
self.cancel()
|
||||||
if rect.width() < 1 or rect.height() < 1:
|
if rect.width() < 1 or rect.height() < 1:
|
||||||
return None
|
return None
|
||||||
|
return self.create_shape(rect)
|
||||||
|
|
||||||
|
def create_shape(self, rect: QRectF) -> Shape:
|
||||||
return Rectangle(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height())
|
return Rectangle(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height())
|
||||||
|
|
||||||
def cancel(self) -> None:
|
def cancel(self) -> None:
|
||||||
@@ -59,6 +62,11 @@ class RectangleCreationTool:
|
|||||||
return QPointF(x, y)
|
return QPointF(x, y)
|
||||||
|
|
||||||
|
|
||||||
|
class TextCreationTool(RectangleCreationTool):
|
||||||
|
def create_shape(self, rect: QRectF) -> Shape:
|
||||||
|
return Text(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height(), text="Text")
|
||||||
|
|
||||||
|
|
||||||
class ShapeGraphicsItem(QGraphicsPathItem):
|
class ShapeGraphicsItem(QGraphicsPathItem):
|
||||||
handle_size = 6.0
|
handle_size = 6.0
|
||||||
|
|
||||||
@@ -173,6 +181,48 @@ class RectangleGraphicsItem(ShapeGraphicsItem):
|
|||||||
self.setPath(path)
|
self.setPath(path)
|
||||||
|
|
||||||
|
|
||||||
|
class TextGraphicsItem(ShapeGraphicsItem):
|
||||||
|
def __init__(self, shape_id: ShapeID, shape: Text, scene: IconGraphicsScene) -> None:
|
||||||
|
super().__init__(shape_id, shape, scene)
|
||||||
|
self.text = deepcopy(shape)
|
||||||
|
self.setPos(shape.pos[0], shape.pos[1])
|
||||||
|
self._set_size(shape.width, shape.height)
|
||||||
|
self.setPen(QPen(Qt.PenStyle.NoPen))
|
||||||
|
self.setBrush(QBrush(QColor(0, 0, 0, 0)))
|
||||||
|
self.setZValue(shape.layer)
|
||||||
|
|
||||||
|
def current_shape(self) -> Text:
|
||||||
|
shape = deepcopy(self.text)
|
||||||
|
shape.pos = (round(self.pos().x()), round(self.pos().y()))
|
||||||
|
rect = self.path().boundingRect()
|
||||||
|
shape.width = rect.width()
|
||||||
|
shape.height = rect.height()
|
||||||
|
return shape
|
||||||
|
|
||||||
|
def resize_to(self, position: QPointF) -> None:
|
||||||
|
bounds = self.icon_scene.sceneRect()
|
||||||
|
width = max(1.0, min(bounds.right(), round(position.x())) - self.pos().x())
|
||||||
|
height = max(1.0, min(bounds.bottom(), round(position.y())) - self.pos().y())
|
||||||
|
self._set_size(width, height)
|
||||||
|
|
||||||
|
def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget: QWidget | None = None) -> None:
|
||||||
|
super().paint(painter, option, widget)
|
||||||
|
font = QFont()
|
||||||
|
font.setPixelSize(max(1, round(self.text.size)))
|
||||||
|
font.setBold(self.text.bold)
|
||||||
|
font.setItalic(self.text.italic)
|
||||||
|
painter.setFont(font)
|
||||||
|
painter.setPen(self.icon_scene._color(self.text.color))
|
||||||
|
painter.setClipRect(self.path().boundingRect())
|
||||||
|
painter.drawText(self.path().boundingRect(), Qt.AlignmentFlag.AlignCenter | Qt.TextFlag.TextWordWrap, self.text.text)
|
||||||
|
|
||||||
|
def _set_size(self, width: float, height: float) -> None:
|
||||||
|
self.prepareGeometryChange()
|
||||||
|
path = QPainterPath()
|
||||||
|
path.addRect(QRectF(0, 0, width, height))
|
||||||
|
self.setPath(path)
|
||||||
|
|
||||||
|
|
||||||
class PortGraphicsItem(QGraphicsRectItem):
|
class PortGraphicsItem(QGraphicsRectItem):
|
||||||
size = 16.0
|
size = 16.0
|
||||||
|
|
||||||
@@ -304,9 +354,10 @@ class IconGraphicsScene(QGraphicsScene):
|
|||||||
super().mouseReleaseEvent(event)
|
super().mouseReleaseEvent(event)
|
||||||
|
|
||||||
def _add_shape_item(self, shape_id: ShapeID, shape: Shape) -> None:
|
def _add_shape_item(self, shape_id: ShapeID, shape: Shape) -> None:
|
||||||
if not isinstance(shape, Rectangle):
|
if isinstance(shape, Rectangle):
|
||||||
return
|
self.addItem(RectangleGraphicsItem(shape_id, shape, self))
|
||||||
self.addItem(RectangleGraphicsItem(shape_id, shape, self))
|
elif isinstance(shape, Text):
|
||||||
|
self.addItem(TextGraphicsItem(shape_id, shape, self))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _pen(shape: Rectangle) -> QPen:
|
def _pen(shape: Rectangle) -> QPen:
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ from __future__ import annotations
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from PySide6.QtCore import QRectF
|
from PySide6.QtCore import QRectF
|
||||||
from PySide6.QtWidgets import QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel, QSpinBox, QVBoxLayout, QWidget
|
from PySide6.QtWidgets import QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel, QLineEdit, QSpinBox, QVBoxLayout, QWidget
|
||||||
|
|
||||||
from bedit_gui.models import LineType, Rectangle, Shape
|
from bedit_gui.models import LineType, Rectangle, Shape, Text
|
||||||
from bedit_gui.views.color_button import ColorButton
|
from bedit_gui.views.color_button import ColorButton
|
||||||
|
|
||||||
|
|
||||||
@@ -34,6 +34,8 @@ class ShapeOptionsDialog(QDialog):
|
|||||||
|
|
||||||
if isinstance(shape, Rectangle):
|
if isinstance(shape, Rectangle):
|
||||||
self._add_rectangle_fields(shape)
|
self._add_rectangle_fields(shape)
|
||||||
|
elif isinstance(shape, Text):
|
||||||
|
self._add_text_fields(shape)
|
||||||
|
|
||||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||||
buttons.accepted.connect(self.accept)
|
buttons.accepted.connect(self.accept)
|
||||||
@@ -43,15 +45,18 @@ class ShapeOptionsDialog(QDialog):
|
|||||||
layout.addWidget(buttons)
|
layout.addWidget(buttons)
|
||||||
|
|
||||||
def shape(self) -> Shape:
|
def shape(self) -> Shape:
|
||||||
if not isinstance(self._shape, Rectangle):
|
if isinstance(self._shape, Rectangle):
|
||||||
shape = deepcopy(self._shape)
|
width = min(self.width.value(), self._scene_rect.right() - self.x.value())
|
||||||
shape.layer = self.layer.value()
|
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
|
||||||
shape.pos = (self.x.value(), self.y.value())
|
return Rectangle(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, line_type=self.line_type.currentData(), line_thickness=self.line_thickness.value(), corner_radius=self.corner_radius.value(), line_color=self.line_color.color(), fill_color=self.fill_color.color())
|
||||||
return shape
|
if isinstance(self._shape, Text):
|
||||||
|
width = min(self.width.value(), self._scene_rect.right() - self.x.value())
|
||||||
width = min(self.width.value(), self._scene_rect.right() - self.x.value())
|
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
|
||||||
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
|
return Text(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, color=self.color.color(), bold=self.bold.isChecked(), italic=self.italic.isChecked(), size=self.size.value(), text=self.text.text())
|
||||||
return Rectangle(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, line_type=self.line_type.currentData(), line_thickness=self.line_thickness.value(), corner_radius=self.corner_radius.value(), line_color=self.line_color.color(), fill_color=self.fill_color.color())
|
shape = deepcopy(self._shape)
|
||||||
|
shape.layer = self.layer.value()
|
||||||
|
shape.pos = (self.x.value(), self.y.value())
|
||||||
|
return shape
|
||||||
|
|
||||||
def _add_rectangle_fields(self, shape: Rectangle) -> None:
|
def _add_rectangle_fields(self, shape: Rectangle) -> None:
|
||||||
self.width = QSpinBox()
|
self.width = QSpinBox()
|
||||||
@@ -79,3 +84,27 @@ class ShapeOptionsDialog(QDialog):
|
|||||||
self.form.addRow("Corner radius", self.corner_radius)
|
self.form.addRow("Corner radius", self.corner_radius)
|
||||||
self.form.addRow("Line color", self.line_color)
|
self.form.addRow("Line color", self.line_color)
|
||||||
self.form.addRow("Fill color", self.fill_color)
|
self.form.addRow("Fill color", self.fill_color)
|
||||||
|
|
||||||
|
def _add_text_fields(self, shape: Text) -> None:
|
||||||
|
self.width = QSpinBox()
|
||||||
|
self.width.setRange(1, round(self._scene_rect.width()))
|
||||||
|
self.width.setValue(round(shape.width))
|
||||||
|
self.height = QSpinBox()
|
||||||
|
self.height.setRange(1, round(self._scene_rect.height()))
|
||||||
|
self.height.setValue(round(shape.height))
|
||||||
|
self.color = ColorButton(shape.color)
|
||||||
|
self.bold = QCheckBox()
|
||||||
|
self.bold.setChecked(shape.bold)
|
||||||
|
self.italic = QCheckBox()
|
||||||
|
self.italic.setChecked(shape.italic)
|
||||||
|
self.size = QDoubleSpinBox()
|
||||||
|
self.size.setRange(1, 1000)
|
||||||
|
self.size.setValue(shape.size)
|
||||||
|
self.text = QLineEdit(shape.text)
|
||||||
|
self.form.addRow("Width", self.width)
|
||||||
|
self.form.addRow("Height", self.height)
|
||||||
|
self.form.addRow("Color", self.color)
|
||||||
|
self.form.addRow("Bold", self.bold)
|
||||||
|
self.form.addRow("Italic", self.italic)
|
||||||
|
self.form.addRow("Size", self.size)
|
||||||
|
self.form.addRow("Text", self.text)
|
||||||
|
|||||||
@@ -402,21 +402,164 @@
|
|||||||
"icons": {
|
"icons": {
|
||||||
"43b3aee6-0b38-429b-9c6d-d38fc097297f": {
|
"43b3aee6-0b38-429b-9c6d-d38fc097297f": {
|
||||||
"shapes": {
|
"shapes": {
|
||||||
"7b93f7ba-d38e-4503-a64f-beb9fc915285": {
|
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||||
"layer": 0,
|
"layer": 0,
|
||||||
"type": "rectangle",
|
"type": "text",
|
||||||
"pos": [
|
"pos": [
|
||||||
-40,
|
-32,
|
||||||
-40
|
-32
|
||||||
],
|
],
|
||||||
"width": 80,
|
"width": 64.0,
|
||||||
"height": 80,
|
"height": 64.0,
|
||||||
"line_type": "solid",
|
"color": "#000000ff",
|
||||||
"line_thickness": 1.0,
|
"bold": true,
|
||||||
"corner_radius": 5.0,
|
"italic": false,
|
||||||
"line_color": "#000000ff",
|
"size": 64.0,
|
||||||
"fill_color": "#c8c8c8ff"
|
"text": "C"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"port_positions": {
|
||||||
|
"c87881f1-23b4-4a69-b586-8e3c7bf6e21e": [
|
||||||
|
-8,
|
||||||
|
-8
|
||||||
|
],
|
||||||
|
"c62de8eb-e13b-4849-bea5-c8cc5332269e": [
|
||||||
|
16,
|
||||||
|
-32
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"5a8b2e8d-489f-467b-8282-7e344dfad576": {
|
||||||
|
"shapes": {
|
||||||
|
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||||
|
"layer": 0,
|
||||||
|
"type": "text",
|
||||||
|
"pos": [
|
||||||
|
-32,
|
||||||
|
-32
|
||||||
|
],
|
||||||
|
"width": 64,
|
||||||
|
"height": 64,
|
||||||
|
"color": "#000000ff",
|
||||||
|
"bold": true,
|
||||||
|
"italic": false,
|
||||||
|
"size": 64.0,
|
||||||
|
"text": "R"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"port_positions": {
|
||||||
|
"0ac7d4ea-77f7-4c0d-8e37-406ef66e4740": [
|
||||||
|
-8,
|
||||||
|
-8
|
||||||
|
],
|
||||||
|
"d1f5aab2-7bff-4b1e-8697-98fb6c3d8f02": [
|
||||||
|
16,
|
||||||
|
-32
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"77701f68-b14c-4b98-8929-c5fba0261962": {
|
||||||
|
"shapes": {
|
||||||
|
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||||
|
"layer": 0,
|
||||||
|
"type": "text",
|
||||||
|
"pos": [
|
||||||
|
-32,
|
||||||
|
-32
|
||||||
|
],
|
||||||
|
"width": 64.0,
|
||||||
|
"height": 64.0,
|
||||||
|
"color": "#000000ff",
|
||||||
|
"bold": true,
|
||||||
|
"italic": false,
|
||||||
|
"size": 64.0,
|
||||||
|
"text": "R"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"port_positions": {
|
||||||
|
"4a3f69f2-b305-4305-92e3-3998634ff226": [
|
||||||
|
-8,
|
||||||
|
-8
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"033b930e-bf79-403a-8b0b-a159f3c81ce9": {
|
||||||
|
"shapes": {
|
||||||
|
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||||
|
"layer": 0,
|
||||||
|
"type": "text",
|
||||||
|
"pos": [
|
||||||
|
-32,
|
||||||
|
-32
|
||||||
|
],
|
||||||
|
"width": 64,
|
||||||
|
"height": 64,
|
||||||
|
"color": "#000000ff",
|
||||||
|
"bold": true,
|
||||||
|
"italic": false,
|
||||||
|
"size": 64.0,
|
||||||
|
"text": "I"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"port_positions": {
|
||||||
|
"0b9036b1-e4e4-437e-8c35-f5bb391b6cbd": [
|
||||||
|
-8,
|
||||||
|
-8
|
||||||
|
],
|
||||||
|
"fb25f35d-4c0c-4cfa-92d4-1a18a71e2c11": [
|
||||||
|
16,
|
||||||
|
-32
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"9ee2f42b-5ed1-446d-8790-c2a1df6b61d3": {
|
||||||
|
"shapes": {
|
||||||
|
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||||
|
"layer": 0,
|
||||||
|
"type": "text",
|
||||||
|
"pos": [
|
||||||
|
-32,
|
||||||
|
-32
|
||||||
|
],
|
||||||
|
"width": 64,
|
||||||
|
"height": 64,
|
||||||
|
"color": "#000000ff",
|
||||||
|
"bold": true,
|
||||||
|
"italic": false,
|
||||||
|
"size": 64.0,
|
||||||
|
"text": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"port_positions": {
|
||||||
|
"4306701a-6b1b-4d19-b8ff-45dbfa04f2d3": [
|
||||||
|
-8,
|
||||||
|
-8
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"9f392c28-6bef-4b10-9305-93e250747007": {
|
||||||
|
"shapes": {
|
||||||
|
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||||
|
"layer": 0,
|
||||||
|
"type": "text",
|
||||||
|
"pos": [
|
||||||
|
-32,
|
||||||
|
-32
|
||||||
|
],
|
||||||
|
"width": 64,
|
||||||
|
"height": 64,
|
||||||
|
"color": "#000000ff",
|
||||||
|
"bold": true,
|
||||||
|
"italic": false,
|
||||||
|
"size": 64.0,
|
||||||
|
"text": "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"port_positions": {
|
||||||
|
"497b1f74-1186-471f-976a-36b07a451caf": [
|
||||||
|
-8,
|
||||||
|
-8
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user