Implemented the vector icon editor, library previews, and workspace camera controls.
Key changes: Component Options now has an “Edit Icon…” button instead of direct shape/color fields. Icon editor supports:Rectangles with configurable corner radius Circles and ellipses Lines Triangles Text Movable/selectable shapes Shape context menus Line style and width Solid or transparent fill Stroke and fill colours through Qt’s colour picker Editable shape dimensions Deleting selected shapes Input anchors are green and output anchors red; both can be dragged around the icon. Icons use a 120×80 vector coordinate system and are stored as editable elements in document JSON. Components render their vector icons directly in the workspace. Library and document trees display 16×16 icon previews. Added a Camera toolbar:Zoom In Zoom Out Center Mouse-wheel zoom works directly over the workspace. Icon changes and port positions support undo/redo.
This commit is contained in:
@@ -134,14 +134,22 @@ Every component owns its ports, declarative icon, properties, and child graph:
|
||||
"name": "My Component",
|
||||
"position": {"x": 0, "y": 0},
|
||||
"interface": {
|
||||
"inputs": [{"id": "in", "name": "Input"}],
|
||||
"outputs": [{"id": "out", "name": "Output"}]
|
||||
"inputs": [{"id": "in", "name": "Input", "properties": {
|
||||
"iconPosition": {"x": 0, "y": 40}
|
||||
}}],
|
||||
"outputs": [{"id": "out", "name": "Output", "properties": {
|
||||
"iconPosition": {"x": 120, "y": 40}
|
||||
}}]
|
||||
},
|
||||
"icon": {
|
||||
"shape": "rectangle",
|
||||
"fill": "#dbeafe",
|
||||
"border": "#245c9c",
|
||||
"text": "Component"
|
||||
"size": {"width": 120, "height": 80},
|
||||
"elements": [{
|
||||
"type": "rectangle", "x": 1, "y": 1,
|
||||
"width": 118, "height": 78,
|
||||
"cornerRadius": 5,
|
||||
"fill": "#dbeafe", "stroke": "#245c9c",
|
||||
"lineWidth": 1.5, "lineStyle": "solid"
|
||||
}]
|
||||
},
|
||||
"properties": {},
|
||||
"implementation": {
|
||||
@@ -157,8 +165,10 @@ Every component owns its ports, declarative icon, properties, and child graph:
|
||||
|
||||
Graph components use `"implementation": {"kind": "graph", "graph": ...}`;
|
||||
text components use `"implementation": {"kind": "text", "source": ...}` and
|
||||
never own a graph. Supported icon shapes are currently `rectangle` and
|
||||
`ellipse`. The recursive model is under `src/bedit/document/`, library loading
|
||||
never own a graph. Vector icons can contain rectangles, circles, ellipses,
|
||||
lines, triangles, and text. Each element owns its geometry, fill, stroke, and
|
||||
line style; ports keep their icon anchor in `properties.iconPosition`. The
|
||||
recursive model is under `src/bedit/document/`, library loading
|
||||
and the live Current Document tree are under `src/bedit/library/`, and graphics
|
||||
are isolated under `src/bedit/workspace/`.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from PySide6.QtGui import QColor
|
||||
from PySide6.QtWidgets import QDialog, QMessageBox
|
||||
from PySide6.QtWidgets import QDialog, QMessageBox, QPushButton
|
||||
|
||||
from bedit.document.model import Component
|
||||
from bedit.icon_editor import IconEditorDialog
|
||||
from bedit.ui_component_options_dialog import Ui_ComponentOptionsDialog
|
||||
|
||||
|
||||
@@ -10,22 +10,36 @@ class ComponentOptionsDialog(QDialog):
|
||||
super().__init__(parent)
|
||||
self.ui = Ui_ComponentOptionsDialog()
|
||||
self.ui.setupUi(self)
|
||||
self.component = component
|
||||
self.edited_icon = component.icon
|
||||
self.edited_inputs = component.inputs
|
||||
self.edited_outputs = component.outputs
|
||||
self.ui.nameEdit.setText(component.name)
|
||||
self.ui.shapeCombo.setCurrentText(component.icon.shape)
|
||||
self.ui.iconTextEdit.setText(component.icon.text)
|
||||
self.ui.fillEdit.setText(component.icon.fill)
|
||||
self.ui.borderEdit.setText(component.icon.border)
|
||||
for widget in (
|
||||
self.ui.shapeLabel, self.ui.shapeCombo, self.ui.iconTextLabel,
|
||||
self.ui.iconTextEdit, self.ui.fillLabel, self.ui.fillEdit,
|
||||
self.ui.borderLabel, self.ui.borderEdit,
|
||||
):
|
||||
widget.hide()
|
||||
self.icon_editor_button = QPushButton("Edit Icon…", self)
|
||||
self.icon_editor_button.setToolTip("Open the vector icon and port-position editor")
|
||||
self.icon_editor_button.clicked.connect(self.edit_icon)
|
||||
self.ui.optionsForm.insertRow(1, "Icon:", self.icon_editor_button)
|
||||
self.ui.showSubtreeCheckBox.setChecked(component.show_subtree_in_library)
|
||||
|
||||
def edit_icon(self) -> None:
|
||||
working = Component.from_dict(self.component.to_dict())
|
||||
working.icon = self.edited_icon
|
||||
working.inputs = self.edited_inputs
|
||||
working.outputs = self.edited_outputs
|
||||
dialog = IconEditorDialog(working, self)
|
||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||
self.edited_icon = dialog.icon
|
||||
self.edited_inputs = dialog.inputs
|
||||
self.edited_outputs = dialog.outputs
|
||||
|
||||
def accept(self) -> None:
|
||||
if not self.ui.nameEdit.text().strip():
|
||||
QMessageBox.warning(self, "Invalid name", "The component name cannot be empty.")
|
||||
return
|
||||
for label, value in (
|
||||
("fill", self.ui.fillEdit.text()),
|
||||
("border", self.ui.borderEdit.text()),
|
||||
):
|
||||
if not QColor(value).isValid():
|
||||
QMessageBox.warning(self, "Invalid color", f"The {label} color is not valid.")
|
||||
return
|
||||
super().accept()
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"format": "bedit-document",
|
||||
"version": 1,
|
||||
"metadata": {"name": "Example Library"},
|
||||
"roots": [
|
||||
{
|
||||
"id": "example-a",
|
||||
"name": "Block A",
|
||||
"position": {"x": 0, "y": 0},
|
||||
"interface": {
|
||||
"inputs": [{"id": "in", "name": "Input", "position": {"x": 32, "y": 96}}],
|
||||
"outputs": [{"id": "out", "name": "Output", "position": {"x": 320, "y": 96}}]
|
||||
},
|
||||
"icon": {"shape": "rectangle", "fill": "#dbeafe", "border": "#245c9c", "text": "A"},
|
||||
"properties": {},
|
||||
"library": {"showSubtree": true},
|
||||
"implementation": {
|
||||
"kind": "text",
|
||||
"source": {"equations": ["out = gain * in"], "parameters": {"gain": 1.0}}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "example-b",
|
||||
"name": "Block B",
|
||||
"position": {"x": 0, "y": 0},
|
||||
"interface": {
|
||||
"inputs": [{"id": "in", "name": "Input", "position": {"x": 32, "y": 96}}],
|
||||
"outputs": [{"id": "out", "name": "Output", "position": {"x": 320, "y": 96}}]
|
||||
},
|
||||
"icon": {"shape": "ellipse", "fill": "#dcfce7", "border": "#277342", "text": "B"},
|
||||
"properties": {},
|
||||
"library": {"showSubtree": true},
|
||||
"implementation": {
|
||||
"kind": "text",
|
||||
"source": {"equations": ["out = in + offset"], "parameters": {"offset": 0.0}}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "example-c",
|
||||
"name": "Block C",
|
||||
"position": {"x": 0, "y": 0},
|
||||
"interface": {
|
||||
"inputs": [{"id": "in", "name": "Input", "position": {"x": 32, "y": 96}}],
|
||||
"outputs": [{"id": "out", "name": "Output", "position": {"x": 320, "y": 96}}]
|
||||
},
|
||||
"icon": {"shape": "rectangle", "fill": "#fef3c7", "border": "#8a641c", "text": "C"},
|
||||
"properties": {},
|
||||
"library": {"showSubtree": true},
|
||||
"implementation": {"kind": "graph", "graph": {"blocks": [], "connections": []}}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
{
|
||||
"format": "bedit-document",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "Untitled"
|
||||
},
|
||||
"roots": [
|
||||
{
|
||||
"id": "97cd3d0b-c36a-467e-b1f5-4c794979bd99",
|
||||
"name": "something",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [
|
||||
{
|
||||
"id": "in",
|
||||
"name": "Input",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {}
|
||||
}
|
||||
],
|
||||
"outputs": []
|
||||
},
|
||||
"icon": {
|
||||
"shape": "rectangle",
|
||||
"fill": "#f4f4f4",
|
||||
"border": "#303030",
|
||||
"text": "Something"
|
||||
},
|
||||
"properties": {},
|
||||
"library": {
|
||||
"showSubtree": true
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "text",
|
||||
"source": {
|
||||
"equations": [],
|
||||
"parameters": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "614b4018-c00e-40ce-b59c-5d5795eab7d0",
|
||||
"name": "Test",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [],
|
||||
"outputs": []
|
||||
},
|
||||
"icon": {
|
||||
"shape": "rectangle",
|
||||
"fill": "#f4f4f4",
|
||||
"border": "#303030",
|
||||
"text": "Test"
|
||||
},
|
||||
"properties": {},
|
||||
"library": {
|
||||
"showSubtree": false
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "graph",
|
||||
"graph": {
|
||||
"blocks": [
|
||||
{
|
||||
"id": "4ad9ab39-f332-4a9e-ae1a-02141b90a4ce",
|
||||
"name": "sin",
|
||||
"position": {
|
||||
"x": -28.0,
|
||||
"y": -179.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [
|
||||
{
|
||||
"id": "input-0bcb214f",
|
||||
"name": "a",
|
||||
"position": {
|
||||
"x": -223.0,
|
||||
"y": -161.0
|
||||
},
|
||||
"properties": {}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "output-a55cd289",
|
||||
"name": "b",
|
||||
"position": {
|
||||
"x": 133.0,
|
||||
"y": -152.0
|
||||
},
|
||||
"properties": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"shape": "rectangle",
|
||||
"fill": "#f4f4f4",
|
||||
"border": "#303030",
|
||||
"text": "sin()"
|
||||
},
|
||||
"properties": {},
|
||||
"library": {
|
||||
"showSubtree": false
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "graph",
|
||||
"graph": {
|
||||
"blocks": [],
|
||||
"connections": [
|
||||
{
|
||||
"id": "cef30af9-fdc5-4a28-a7d6-4ab3c83e65d7",
|
||||
"source": {
|
||||
"interface": "input-0bcb214f"
|
||||
},
|
||||
"target": {
|
||||
"interface": "output-a55cd289"
|
||||
},
|
||||
"name": "",
|
||||
"properties": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "88f40d88-f62b-432f-a6a0-95513b48409e",
|
||||
"name": "constant",
|
||||
"position": {
|
||||
"x": -260.0,
|
||||
"y": -178.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "output-1e15ff3f",
|
||||
"name": "Output 1",
|
||||
"position": {
|
||||
"x": 71.0,
|
||||
"y": -134.0
|
||||
},
|
||||
"properties": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"shape": "rectangle",
|
||||
"fill": "#f4f4f4",
|
||||
"border": "#303030",
|
||||
"text": "C"
|
||||
},
|
||||
"properties": {},
|
||||
"library": {
|
||||
"showSubtree": true
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "graph",
|
||||
"graph": {
|
||||
"blocks": [],
|
||||
"connections": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "88941f2d-8edd-4dd9-a07b-3f206f6b76c5",
|
||||
"name": "New Text Block 1",
|
||||
"position": {
|
||||
"x": 225.0,
|
||||
"y": -167.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [
|
||||
{
|
||||
"id": "in",
|
||||
"name": "Input",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {}
|
||||
}
|
||||
],
|
||||
"outputs": []
|
||||
},
|
||||
"icon": {
|
||||
"shape": "rectangle",
|
||||
"fill": "#f4f4f4",
|
||||
"border": "#303030",
|
||||
"text": "Text"
|
||||
},
|
||||
"properties": {},
|
||||
"library": {
|
||||
"showSubtree": true
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "text",
|
||||
"source": {
|
||||
"equations": [],
|
||||
"parameters": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"id": "b3837790-26ba-46a6-91f9-3b6808c7ad1a",
|
||||
"source": {
|
||||
"block": "88f40d88-f62b-432f-a6a0-95513b48409e",
|
||||
"port": "output-1e15ff3f"
|
||||
},
|
||||
"target": {
|
||||
"block": "4ad9ab39-f332-4a9e-ae1a-02141b90a4ce",
|
||||
"port": "input-0bcb214f"
|
||||
},
|
||||
"name": "",
|
||||
"properties": {}
|
||||
},
|
||||
{
|
||||
"id": "50b2c7aa-d824-407c-ae0f-6b3bb4b1b79e",
|
||||
"source": {
|
||||
"block": "4ad9ab39-f332-4a9e-ae1a-02141b90a4ce",
|
||||
"port": "output-a55cd289"
|
||||
},
|
||||
"target": {
|
||||
"block": "88941f2d-8edd-4dd9-a07b-3f206f6b76c5",
|
||||
"port": "in"
|
||||
},
|
||||
"name": "",
|
||||
"properties": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "465796f5-9075-45f6-81b9-7fa17476392c",
|
||||
"name": "sin",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [
|
||||
{
|
||||
"id": "input-0bcb214f",
|
||||
"name": "Input 1",
|
||||
"position": {
|
||||
"x": -223.0,
|
||||
"y": -161.0
|
||||
},
|
||||
"properties": {}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "output-a55cd289",
|
||||
"name": "Output 1",
|
||||
"position": {
|
||||
"x": 133.0,
|
||||
"y": -152.0
|
||||
},
|
||||
"properties": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"shape": "rectangle",
|
||||
"fill": "#f4f4f4",
|
||||
"border": "#303030",
|
||||
"text": "sin()"
|
||||
},
|
||||
"properties": {},
|
||||
"library": {
|
||||
"showSubtree": false
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "graph",
|
||||
"graph": {
|
||||
"blocks": [],
|
||||
"connections": [
|
||||
{
|
||||
"id": "029ccfaa-7150-4a53-beec-286ff8e2a628",
|
||||
"source": {
|
||||
"interface": "input-0bcb214f"
|
||||
},
|
||||
"target": {
|
||||
"interface": "output-a55cd289"
|
||||
},
|
||||
"name": "",
|
||||
"properties": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bb0adff3-baf2-469b-9a80-f05910d0f259",
|
||||
"name": "constant",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "output-1e15ff3f",
|
||||
"name": "Output 1",
|
||||
"position": {
|
||||
"x": 71.0,
|
||||
"y": -134.0
|
||||
},
|
||||
"properties": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"shape": "rectangle",
|
||||
"fill": "#f4f4f4",
|
||||
"border": "#303030",
|
||||
"text": "C"
|
||||
},
|
||||
"properties": {},
|
||||
"library": {
|
||||
"showSubtree": true
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "graph",
|
||||
"graph": {
|
||||
"blocks": [],
|
||||
"connections": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -330,10 +330,9 @@ class DocumentController(QObject):
|
||||
self,
|
||||
component_id: str,
|
||||
name: str,
|
||||
shape: str,
|
||||
fill: str,
|
||||
border: str,
|
||||
text: str,
|
||||
icon: Icon,
|
||||
inputs: list[Port],
|
||||
outputs: list[Port],
|
||||
show_subtree: bool,
|
||||
) -> None:
|
||||
if self.document is None:
|
||||
@@ -343,15 +342,16 @@ class DocumentController(QObject):
|
||||
return
|
||||
old = {
|
||||
"name": component.name,
|
||||
**component.icon.to_dict(),
|
||||
"icon": component.icon.to_dict(),
|
||||
"inputs": [port.to_dict() for port in component.inputs],
|
||||
"outputs": [port.to_dict() for port in component.outputs],
|
||||
"show_subtree": component.show_subtree_in_library,
|
||||
}
|
||||
new = {
|
||||
"name": name,
|
||||
"shape": shape,
|
||||
"fill": fill,
|
||||
"border": border,
|
||||
"text": text,
|
||||
"icon": icon.to_dict(),
|
||||
"inputs": [port.to_dict() for port in inputs],
|
||||
"outputs": [port.to_dict() for port in outputs],
|
||||
"show_subtree": show_subtree,
|
||||
}
|
||||
if old != new:
|
||||
@@ -581,12 +581,9 @@ class DocumentController(QObject):
|
||||
if component is None:
|
||||
return
|
||||
component.name = values["name"]
|
||||
component.icon = Icon(
|
||||
shape=values["shape"],
|
||||
fill=values["fill"],
|
||||
border=values["border"],
|
||||
text=values["text"],
|
||||
)
|
||||
component.icon = Icon.from_dict(values["icon"])
|
||||
component.inputs = [Port.from_dict(port) for port in values["inputs"]]
|
||||
component.outputs = [Port.from_dict(port) for port in values["outputs"]]
|
||||
component.show_subtree_in_library = values["show_subtree"]
|
||||
self.documentReset.emit()
|
||||
if component_id == self.active_component_id:
|
||||
|
||||
@@ -40,24 +40,49 @@ class Icon:
|
||||
fill: str = "#f4f4f4"
|
||||
border: str = "#303030"
|
||||
text: str = ""
|
||||
width: float = 120.0
|
||||
height: float = 80.0
|
||||
elements: list[dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> dict[str, str]:
|
||||
def __post_init__(self) -> None:
|
||||
if not self.elements:
|
||||
self.elements = [{
|
||||
"type": "ellipse" if self.shape == "ellipse" else "rectangle",
|
||||
"x": 1.0, "y": 1.0, "width": self.width - 2.0, "height": self.height - 2.0,
|
||||
"fill": self.fill, "stroke": self.border, "lineWidth": 1.5,
|
||||
"lineStyle": "solid", "cornerRadius": 5.0,
|
||||
}]
|
||||
if self.text:
|
||||
self.elements.append({
|
||||
"type": "text", "x": 8.0, "y": 8.0,
|
||||
"width": self.width - 16.0, "height": self.height - 16.0,
|
||||
"text": self.text, "color": "#202020", "fontSize": 12.0,
|
||||
})
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"shape": self.shape,
|
||||
"fill": self.fill,
|
||||
"border": self.border,
|
||||
"text": self.text,
|
||||
"size": {"width": self.width, "height": self.height},
|
||||
"elements": deepcopy(self.elements),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any] | None) -> "Icon":
|
||||
data = data or {}
|
||||
return cls(
|
||||
size = data.get("size", {})
|
||||
icon = cls(
|
||||
shape=str(data.get("shape", "rectangle")),
|
||||
fill=str(data.get("fill", "#f4f4f4")),
|
||||
border=str(data.get("border", "#303030")),
|
||||
text=str(data.get("text", "")),
|
||||
width=float(size.get("width", 120.0)),
|
||||
height=float(size.get("height", 80.0)),
|
||||
elements=deepcopy(data.get("elements", [])),
|
||||
)
|
||||
return icon
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -318,7 +343,7 @@ def clone_component(source: Component) -> Component:
|
||||
y=current.y,
|
||||
inputs=[Port(port.id, port.name, port.x, port.y, deepcopy(port.properties)) for port in current.inputs],
|
||||
outputs=[Port(port.id, port.name, port.x, port.y, deepcopy(port.properties)) for port in current.outputs],
|
||||
icon=Icon(**current.icon.to_dict()),
|
||||
icon=Icon.from_dict(current.icon.to_dict()),
|
||||
properties=deepcopy(current.properties),
|
||||
implementation_kind=current.implementation_kind,
|
||||
graph=graph if current.implementation_kind == "graph" else Graph(),
|
||||
|
||||
277
BEdit/src/bedit/icon_editor.py
Normal file
277
BEdit/src/bedit/icon_editor.py
Normal file
@@ -0,0 +1,277 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from PySide6.QtCore import QPointF, QRectF, Qt
|
||||
from PySide6.QtGui import QColor, QPainter, QPen, QPolygonF
|
||||
from PySide6.QtWidgets import (
|
||||
QColorDialog,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QDoubleSpinBox,
|
||||
QFormLayout,
|
||||
QGraphicsEllipseItem,
|
||||
QGraphicsItem,
|
||||
QGraphicsObject,
|
||||
QGraphicsScene,
|
||||
QGraphicsSceneContextMenuEvent,
|
||||
QGraphicsView,
|
||||
QHBoxLayout,
|
||||
QInputDialog,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QMenu,
|
||||
QPushButton,
|
||||
QToolButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bedit.document.model import Component, Icon, Port
|
||||
from bedit.icon_renderer import _pen
|
||||
|
||||
|
||||
class ColorButton(QPushButton):
|
||||
def __init__(self, color: str, allow_none: bool = False, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.color = color
|
||||
self.allow_none = allow_none
|
||||
self.clicked.connect(self.choose)
|
||||
self._refresh()
|
||||
|
||||
def _refresh(self) -> None:
|
||||
self.setText("No fill" if self.color == "none" else self.color)
|
||||
swatch = "transparent" if self.color == "none" else self.color
|
||||
self.setStyleSheet(f"QPushButton {{ background: {swatch}; }}")
|
||||
|
||||
def choose(self) -> None:
|
||||
initial = QColor("#ffffff" if self.color == "none" else self.color)
|
||||
color = QColorDialog.getColor(initial, self, "Choose colour", QColorDialog.ColorDialogOption.ShowAlphaChannel)
|
||||
if color.isValid():
|
||||
self.color = color.name(QColor.NameFormat.HexArgb) if color.alpha() < 255 else color.name()
|
||||
self._refresh()
|
||||
|
||||
|
||||
class ShapeOptionsDialog(QDialog):
|
||||
def __init__(self, element: dict, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.element = deepcopy(element)
|
||||
self.setWindowTitle("Shape Options")
|
||||
layout = QVBoxLayout(self)
|
||||
form = QFormLayout()
|
||||
self.line_style = QComboBox()
|
||||
self.line_style.addItems(["solid", "dash", "dot", "dash-dot", "none"])
|
||||
self.line_style.setCurrentText(self.element.get("lineStyle", "solid"))
|
||||
self.line_width = QDoubleSpinBox()
|
||||
self.line_width.setRange(0.1, 20.0)
|
||||
self.line_width.setValue(float(self.element.get("lineWidth", 1.5)))
|
||||
self.stroke = ColorButton(self.element.get("stroke", "#303030"))
|
||||
self.fill_type = QComboBox()
|
||||
self.fill_type.addItems(["solid", "none"])
|
||||
fill = self.element.get("fill", "#ffffff")
|
||||
self.fill_type.setCurrentText("none" if fill in {"none", "transparent", ""} else "solid")
|
||||
self.fill = ColorButton("#ffffff" if fill in {"none", "transparent", ""} else fill)
|
||||
self.width = QDoubleSpinBox()
|
||||
self.width.setRange(1, 500)
|
||||
self.width.setValue(float(self.element.get("width", 20)))
|
||||
self.height = QDoubleSpinBox()
|
||||
self.height.setRange(1, 500)
|
||||
self.height.setValue(float(self.element.get("height", 20)))
|
||||
form.addRow("Width:", self.width)
|
||||
form.addRow("Height:", self.height)
|
||||
form.addRow("Line style:", self.line_style)
|
||||
form.addRow("Line width:", self.line_width)
|
||||
form.addRow("Line colour:", self.stroke)
|
||||
if self.element.get("type") != "line":
|
||||
form.addRow("Fill type:", self.fill_type)
|
||||
form.addRow("Fill colour:", self.fill)
|
||||
self.radius = None
|
||||
if self.element.get("type") == "rectangle":
|
||||
self.radius = QDoubleSpinBox()
|
||||
self.radius.setRange(0, 50)
|
||||
self.radius.setValue(float(self.element.get("cornerRadius", 0)))
|
||||
form.addRow("Corner radius:", self.radius)
|
||||
self.text_edit = None
|
||||
self.font_size = None
|
||||
if self.element.get("type") == "text":
|
||||
self.text_edit = QLineEdit(str(self.element.get("text", "Text")))
|
||||
self.font_size = QDoubleSpinBox()
|
||||
self.font_size.setRange(4, 96)
|
||||
self.font_size.setValue(float(self.element.get("fontSize", 12)))
|
||||
form.addRow("Text:", self.text_edit)
|
||||
form.addRow("Font size:", self.font_size)
|
||||
layout.addLayout(form)
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
def accept(self) -> None:
|
||||
self.element["lineStyle"] = self.line_style.currentText()
|
||||
self.element["lineWidth"] = self.line_width.value()
|
||||
self.element["stroke"] = self.stroke.color
|
||||
self.element["width"] = self.width.value()
|
||||
self.element["height"] = self.height.value()
|
||||
if self.element.get("type") != "line":
|
||||
self.element["fill"] = self.fill.color if self.fill_type.currentText() == "solid" else "none"
|
||||
if self.radius is not None:
|
||||
self.element["cornerRadius"] = self.radius.value()
|
||||
if self.text_edit is not None:
|
||||
self.element["text"] = self.text_edit.text()
|
||||
self.element["fontSize"] = self.font_size.value()
|
||||
self.element["color"] = self.stroke.color
|
||||
super().accept()
|
||||
|
||||
|
||||
class ShapeItem(QGraphicsObject):
|
||||
def __init__(self, element: dict) -> None:
|
||||
super().__init__()
|
||||
self.element = element
|
||||
self.setPos(float(element.get("x", 0)), float(element.get("y", 0)))
|
||||
self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||
|
||||
def boundingRect(self) -> QRectF: # noqa: N802
|
||||
margin = max(3.0, float(self.element.get("lineWidth", 1.5)))
|
||||
return QRectF(0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20))).adjusted(-margin, -margin, margin, margin)
|
||||
|
||||
def paint(self, painter: QPainter, option, widget=None) -> None:
|
||||
del option, widget
|
||||
rect = QRectF(0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20)))
|
||||
painter.setPen(_pen(self.element))
|
||||
fill = self.element.get("fill", "none")
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill))
|
||||
kind = self.element.get("type")
|
||||
if kind == "rectangle":
|
||||
radius = float(self.element.get("cornerRadius", 0))
|
||||
painter.drawRoundedRect(rect, radius, radius)
|
||||
elif kind in {"circle", "ellipse"}:
|
||||
painter.drawEllipse(rect)
|
||||
elif kind == "line":
|
||||
painter.drawLine(rect.topLeft(), rect.bottomRight())
|
||||
elif kind == "triangle":
|
||||
painter.drawPolygon(QPolygonF([QPointF(rect.center().x(), 0), rect.bottomRight(), rect.bottomLeft()]))
|
||||
elif kind == "text":
|
||||
painter.setPen(QColor(self.element.get("color", "#202020")))
|
||||
font = painter.font()
|
||||
font.setPointSizeF(float(self.element.get("fontSize", 12)))
|
||||
painter.setFont(font)
|
||||
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, str(self.element.get("text", "Text")))
|
||||
if self.isSelected():
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
painter.setPen(QPen(QColor("#2563eb"), 1, Qt.PenStyle.DashLine))
|
||||
painter.drawRect(rect)
|
||||
|
||||
def itemChange(self, change, value): # noqa: N802
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||
self.element["x"], self.element["y"] = value.x(), value.y()
|
||||
return super().itemChange(change, value)
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||
menu = QMenu()
|
||||
options = menu.addAction("Shape Options…")
|
||||
delete = menu.addAction("Delete Shape")
|
||||
chosen = menu.exec(event.screenPos())
|
||||
if chosen is options:
|
||||
dialog = ShapeOptionsDialog(self.element)
|
||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||
self.prepareGeometryChange()
|
||||
self.element.clear()
|
||||
self.element.update(dialog.element)
|
||||
self.update()
|
||||
elif chosen is delete and self.scene() is not None:
|
||||
self.scene().removeItem(self)
|
||||
self.element["_deleted"] = True
|
||||
event.accept()
|
||||
|
||||
|
||||
class PortHandle(QGraphicsEllipseItem):
|
||||
def __init__(self, port: Port, direction: str, position: QPointF) -> None:
|
||||
super().__init__(-5, -5, 10, 10)
|
||||
self.port, self.direction = port, direction
|
||||
self.setPos(position)
|
||||
self.setBrush(QColor("#16a34a" if direction == "input" else "#dc2626"))
|
||||
self.setPen(QPen(QColor("#ffffff"), 1.5))
|
||||
self.setToolTip(f"{direction.title()}: {port.name} (drag to position)")
|
||||
self.setZValue(10)
|
||||
self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||
|
||||
def itemChange(self, change, value): # noqa: N802
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||
self.port.properties["iconPosition"] = {"x": value.x(), "y": value.y()}
|
||||
return super().itemChange(change, value)
|
||||
|
||||
|
||||
class IconEditorDialog(QDialog):
|
||||
def __init__(self, component: Component, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(f"Icon Editor — {component.name}")
|
||||
self.resize(850, 600)
|
||||
self.icon = Icon.from_dict(component.icon.to_dict())
|
||||
self.inputs = deepcopy(component.inputs)
|
||||
self.outputs = deepcopy(component.outputs)
|
||||
layout = QVBoxLayout(self)
|
||||
toolbar = QHBoxLayout()
|
||||
toolbar.addWidget(QLabel("Add:"))
|
||||
for kind in ("rectangle", "circle", "ellipse", "line", "triangle", "text"):
|
||||
button = QToolButton()
|
||||
button.setText(kind.title())
|
||||
button.clicked.connect(lambda _checked=False, value=kind: self.add_shape(value))
|
||||
toolbar.addWidget(button)
|
||||
toolbar.addStretch()
|
||||
delete = QPushButton("Delete selected")
|
||||
delete.clicked.connect(self.delete_selected)
|
||||
toolbar.addWidget(delete)
|
||||
layout.addLayout(toolbar)
|
||||
self.scene = QGraphicsScene(0, 0, self.icon.width, self.icon.height, self)
|
||||
self.view = QGraphicsView(self.scene)
|
||||
self.view.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
self.view.setBackgroundBrush(QColor("#f8fafc"))
|
||||
self.view.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
||||
self.scene.addRect(self.scene.sceneRect(), QPen(QColor("#94a3b8"), 0), QColor("#ffffff"))
|
||||
layout.addWidget(self.view, 1)
|
||||
layout.addWidget(QLabel("Green points are inputs; red points are outputs. Drag them to place connection anchors."))
|
||||
for element in self.icon.elements:
|
||||
self.scene.addItem(ShapeItem(element))
|
||||
self._add_ports(self.inputs, "input", 0.0)
|
||||
self._add_ports(self.outputs, "output", self.icon.width)
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
layout.addWidget(buttons)
|
||||
self.view.fitInView(self.scene.sceneRect().adjusted(-10, -10, 10, 10), Qt.AspectRatioMode.KeepAspectRatio)
|
||||
|
||||
def _add_ports(self, ports: list[Port], direction: str, default_x: float) -> None:
|
||||
spacing = self.icon.height / (len(ports) + 1)
|
||||
for index, port in enumerate(ports, 1):
|
||||
saved = port.properties.get("iconPosition", {})
|
||||
position = QPointF(float(saved.get("x", default_x)), float(saved.get("y", spacing * index)))
|
||||
self.scene.addItem(PortHandle(port, direction, position))
|
||||
|
||||
def add_shape(self, kind: str) -> None:
|
||||
count = len([item for item in self.scene.items() if isinstance(item, ShapeItem)])
|
||||
x, y = 15 + (count * 5) % 40, 15 + (count * 4) % 25
|
||||
element = {"type": kind, "x": x, "y": y, "width": 55, "height": 35, "fill": "#dbeafe", "stroke": "#303030", "lineWidth": 1.5, "lineStyle": "solid"}
|
||||
if kind == "circle":
|
||||
element["width"] = element["height"] = 35
|
||||
if kind == "line":
|
||||
element["fill"] = "none"
|
||||
if kind == "rectangle":
|
||||
element["cornerRadius"] = 0
|
||||
if kind == "text":
|
||||
text, accepted = QInputDialog.getText(self, "Add Text", "Text:", text="Text")
|
||||
if not accepted:
|
||||
return
|
||||
element.update({"text": text, "fontSize": 12, "color": "#202020", "fill": "none", "lineStyle": "none"})
|
||||
self.icon.elements.append(element)
|
||||
item = ShapeItem(element)
|
||||
self.scene.addItem(item)
|
||||
item.setSelected(True)
|
||||
|
||||
def delete_selected(self) -> None:
|
||||
for item in self.scene.selectedItems():
|
||||
if isinstance(item, ShapeItem):
|
||||
self.scene.removeItem(item)
|
||||
item.element["_deleted"] = True
|
||||
|
||||
def accept(self) -> None:
|
||||
self.icon.elements = [element for element in self.icon.elements if not element.pop("_deleted", False)]
|
||||
super().accept()
|
||||
66
BEdit/src/bedit/icon_renderer.py
Normal file
66
BEdit/src/bedit/icon_renderer.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from PySide6.QtCore import QPointF, QRectF, Qt
|
||||
from PySide6.QtGui import QColor, QFont, QIcon, QPainter, QPen, QPixmap, QPolygonF
|
||||
|
||||
from bedit.document.model import Icon
|
||||
|
||||
|
||||
def _pen(element: dict) -> QPen:
|
||||
styles = {
|
||||
"solid": Qt.PenStyle.SolidLine,
|
||||
"dash": Qt.PenStyle.DashLine,
|
||||
"dot": Qt.PenStyle.DotLine,
|
||||
"dash-dot": Qt.PenStyle.DashDotLine,
|
||||
"none": Qt.PenStyle.NoPen,
|
||||
}
|
||||
return QPen(
|
||||
QColor(element.get("stroke", "#303030")),
|
||||
float(element.get("lineWidth", 1.5)),
|
||||
styles.get(element.get("lineStyle", "solid"), Qt.PenStyle.SolidLine),
|
||||
)
|
||||
|
||||
|
||||
def paint_icon(painter: QPainter, icon: Icon, target: QRectF) -> None:
|
||||
painter.save()
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
painter.translate(target.topLeft())
|
||||
painter.scale(target.width() / icon.width, target.height() / icon.height)
|
||||
for element in icon.elements:
|
||||
kind = element.get("type", "rectangle")
|
||||
rect = QRectF(
|
||||
float(element.get("x", 0)), float(element.get("y", 0)),
|
||||
float(element.get("width", 20)), float(element.get("height", 20)),
|
||||
)
|
||||
painter.setPen(_pen(element))
|
||||
fill = element.get("fill", "#ffffff")
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill))
|
||||
if kind == "rectangle":
|
||||
radius = float(element.get("cornerRadius", 0))
|
||||
painter.drawRoundedRect(rect, radius, radius)
|
||||
elif kind in {"circle", "ellipse"}:
|
||||
painter.drawEllipse(rect)
|
||||
elif kind == "line":
|
||||
painter.drawLine(rect.topLeft(), rect.bottomRight())
|
||||
elif kind == "triangle":
|
||||
painter.drawPolygon(QPolygonF([QPointF(rect.center().x(), rect.top()), rect.bottomRight(), rect.bottomLeft()]))
|
||||
elif kind == "text":
|
||||
painter.setPen(QColor(element.get("color", element.get("stroke", "#202020"))))
|
||||
font = QFont()
|
||||
font.setPointSizeF(float(element.get("fontSize", 12)))
|
||||
painter.setFont(font)
|
||||
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, str(element.get("text", "Text")))
|
||||
painter.restore()
|
||||
|
||||
|
||||
def icon_pixmap(icon: Icon, size: int = 16) -> QPixmap:
|
||||
pixmap = QPixmap(size, size)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
painter = QPainter(pixmap)
|
||||
ratio = min(size / icon.width, size / icon.height)
|
||||
width, height = icon.width * ratio, icon.height * ratio
|
||||
paint_icon(painter, icon, QRectF((size - width) / 2, (size - height) / 2, width, height))
|
||||
painter.end()
|
||||
return pixmap
|
||||
|
||||
|
||||
def library_icon(icon: Icon) -> QIcon:
|
||||
return QIcon(icon_pixmap(icon, 16))
|
||||
@@ -5,6 +5,7 @@ from PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||
|
||||
from bedit.document.controller import DocumentController
|
||||
from bedit.document.model import Component
|
||||
from bedit.icon_renderer import library_icon
|
||||
from bedit.library.repository import LibraryRepository
|
||||
|
||||
|
||||
@@ -43,6 +44,7 @@ class LibraryTreeModel(QStandardItemModel):
|
||||
def _component_item(self, component: Component, current: bool = False) -> QStandardItem:
|
||||
item = QStandardItem(component.name)
|
||||
item.setEditable(False)
|
||||
item.setIcon(library_icon(component.icon))
|
||||
item.setData(component.to_dict(), COMPONENT_ROLE)
|
||||
item.setData(component.id, COMPONENT_ID_ROLE)
|
||||
item.setData("current-component" if current else "library-component", ITEM_KIND_ROLE)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QSettings, Qt, Slot
|
||||
from PySide6.QtGui import QCloseEvent
|
||||
from PySide6.QtWidgets import QFileDialog, QMainWindow, QMenu, QMessageBox
|
||||
from PySide6.QtCore import QSettings, QSize, Qt, Slot
|
||||
from PySide6.QtGui import QAction, QCloseEvent
|
||||
from PySide6.QtWidgets import QFileDialog, QMainWindow, QMenu, QMessageBox, QToolBar
|
||||
|
||||
from bedit.component_options_dialog import ComponentOptionsDialog
|
||||
from bedit.document.controller import DocumentController
|
||||
@@ -27,6 +27,7 @@ class MainWindow(QMainWindow):
|
||||
super().__init__()
|
||||
self.ui = Ui_MainWindow()
|
||||
self.ui.setupUi(self)
|
||||
self._create_camera_toolbar()
|
||||
self.settings = QSettings()
|
||||
|
||||
self.libraries = LibraryRepository(self)
|
||||
@@ -58,11 +59,13 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def _configure_models(self) -> None:
|
||||
self.ui.treeView.setModel(self.library_tree_model)
|
||||
self.ui.treeView.setIconSize(QSize(16, 16))
|
||||
self.ui.treeView.setHeaderHidden(True)
|
||||
self.ui.treeView.setDragEnabled(True)
|
||||
self.ui.treeView.setDragDropMode(self.ui.treeView.DragDropMode.DragOnly)
|
||||
self.library_tree_model.rebuilt.connect(self.ui.treeView.expandAll)
|
||||
self.ui.documentTreeView.setModel(self.document_tree_model)
|
||||
self.ui.documentTreeView.setIconSize(QSize(16, 16))
|
||||
self.ui.documentTreeView.setHeaderHidden(True)
|
||||
self.ui.documentTreeView.setDragEnabled(True)
|
||||
self.ui.documentTreeView.setDragDropMode(
|
||||
@@ -91,6 +94,20 @@ class MainWindow(QMainWindow):
|
||||
self.ui.applyJsonButton.clicked.connect(self.apply_json)
|
||||
self.document_controller.activeGraphChanged.connect(self._active_graph_changed)
|
||||
|
||||
def _create_camera_toolbar(self) -> None:
|
||||
self.cameraToolbar = QToolBar("Camera", self)
|
||||
self.cameraToolbar.setObjectName("cameraToolbar")
|
||||
self.actionZoomIn = QAction("Zoom In", self)
|
||||
self.actionZoomIn.setShortcut("Ctrl++")
|
||||
self.actionZoomOut = QAction("Zoom Out", self)
|
||||
self.actionZoomOut.setShortcut("Ctrl+-")
|
||||
self.actionCenterView = QAction("Center", self)
|
||||
self.actionCenterView.setShortcut("Ctrl+0")
|
||||
self.cameraToolbar.addActions(
|
||||
(self.actionZoomIn, self.actionZoomOut, self.actionCenterView)
|
||||
)
|
||||
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.cameraToolbar)
|
||||
|
||||
def _connect_actions(self) -> None:
|
||||
self.ui.actionNew.triggered.connect(self.new_document)
|
||||
self.ui.actionOpen.triggered.connect(self.open_document)
|
||||
@@ -111,6 +128,9 @@ class MainWindow(QMainWindow):
|
||||
self.ui.actionPaste.triggered.connect(self.ui.graphView.paste_selection)
|
||||
self.ui.actionSelectAll.triggered.connect(self.ui.graphView.select_all)
|
||||
self.ui.actionRotateClockwise.triggered.connect(self.ui.graphView.rotate_selected)
|
||||
self.actionZoomIn.triggered.connect(self.ui.graphView.zoom_in)
|
||||
self.actionZoomOut.triggered.connect(self.ui.graphView.zoom_out)
|
||||
self.actionCenterView.triggered.connect(self.ui.graphView.center_workspace)
|
||||
self.document_controller.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled)
|
||||
self.document_controller.undo_stack.canRedoChanged.connect(self.ui.actionRedo.setEnabled)
|
||||
self.document_controller.modifiedChanged.connect(lambda _modified: self._update_title())
|
||||
@@ -128,6 +148,7 @@ class MainWindow(QMainWindow):
|
||||
self.ui.fileToolbar,
|
||||
self.ui.editToolbar,
|
||||
self.ui.transformToolbar,
|
||||
self.cameraToolbar,
|
||||
):
|
||||
self.ui.menuToolbars.addAction(toolbar.toggleViewAction())
|
||||
|
||||
@@ -425,10 +446,9 @@ class MainWindow(QMainWindow):
|
||||
self.document_controller.edit_component_appearance(
|
||||
component_id,
|
||||
dialog.ui.nameEdit.text().strip(),
|
||||
dialog.ui.shapeCombo.currentText(),
|
||||
dialog.ui.fillEdit.text(),
|
||||
dialog.ui.borderEdit.text(),
|
||||
dialog.ui.iconTextEdit.text(),
|
||||
dialog.edited_icon,
|
||||
dialog.edited_inputs,
|
||||
dialog.edited_outputs,
|
||||
dialog.ui.showSubtreeCheckBox.isChecked(),
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from PySide6.QtGui import (
|
||||
QDragEnterEvent,
|
||||
QDropEvent,
|
||||
QMouseEvent,
|
||||
QWheelEvent,
|
||||
QPainter,
|
||||
QPainterPath,
|
||||
QPen,
|
||||
@@ -29,6 +30,7 @@ from PySide6.QtWidgets import (
|
||||
from bedit.document.controller import DocumentController
|
||||
from bedit.document.model import Component, Connection, Endpoint, Port
|
||||
from bedit.library.tree_model import COMPONENT_MIME_TYPE
|
||||
from bedit.icon_renderer import paint_icon
|
||||
|
||||
|
||||
SELECTION_MIME_TYPE = "application/x-bedit-selection"
|
||||
@@ -71,7 +73,11 @@ class ComponentGraphicsItem(QGraphicsObject):
|
||||
for index, port in enumerate(ports, start=1):
|
||||
endpoint = Endpoint(block=self.component_id, port=port.id)
|
||||
item = ConnectionPortItem(endpoint, role, port.name, self)
|
||||
item.setPos(x, spacing * index)
|
||||
position = port.properties.get("iconPosition", {})
|
||||
item.setPos(
|
||||
float(position.get("x", x)) * self.WIDTH / self.component.icon.width,
|
||||
float(position.get("y", spacing * index)) * self.HEIGHT / self.component.icon.height,
|
||||
)
|
||||
result[port.id] = item
|
||||
return result
|
||||
|
||||
@@ -85,20 +91,11 @@ class ComponentGraphicsItem(QGraphicsObject):
|
||||
widget: QWidget | None = None,
|
||||
) -> None:
|
||||
del option, widget
|
||||
icon = self.component.icon
|
||||
fill = QColor("#dbeafe") if self.isSelected() else QColor(icon.fill)
|
||||
painter.setBrush(fill)
|
||||
painter.setPen(QPen(QColor(icon.border), 1.5))
|
||||
if icon.shape == "ellipse":
|
||||
painter.drawEllipse(self.boundingRect())
|
||||
else:
|
||||
painter.drawRoundedRect(self.boundingRect(), 5, 5)
|
||||
painter.setPen(QColor("#202020"))
|
||||
painter.drawText(
|
||||
self.boundingRect(),
|
||||
Qt.AlignmentFlag.AlignCenter,
|
||||
icon.text or self.component.name,
|
||||
)
|
||||
paint_icon(painter, self.component.icon, self.boundingRect())
|
||||
if self.isSelected():
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
painter.setPen(QPen(QColor("#2563eb"), 2, Qt.PenStyle.DashLine))
|
||||
painter.drawRect(self.boundingRect())
|
||||
|
||||
def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
self.controller.activate_component(self.component_id)
|
||||
@@ -386,6 +383,35 @@ class GraphWorkspaceView(QGraphicsView):
|
||||
self.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
||||
self.setBackgroundBrush(QColor("#9a9a9a"))
|
||||
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
||||
self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
|
||||
|
||||
def _zoom(self, factor: float) -> None:
|
||||
current = self.transform().m11()
|
||||
target = current * factor
|
||||
if 0.1 <= target <= 8.0:
|
||||
self.scale(factor, factor)
|
||||
|
||||
def zoom_in(self) -> None:
|
||||
self._zoom(1.2)
|
||||
|
||||
def zoom_out(self) -> None:
|
||||
self._zoom(1 / 1.2)
|
||||
|
||||
def center_workspace(self) -> None:
|
||||
scene = self.scene()
|
||||
if scene is None:
|
||||
return
|
||||
bounds = scene.itemsBoundingRect()
|
||||
if bounds.isEmpty():
|
||||
self.resetTransform()
|
||||
self.centerOn(0, 0)
|
||||
else:
|
||||
self.fitInView(bounds.adjusted(-80, -80, 80, 80), Qt.AspectRatioMode.KeepAspectRatio)
|
||||
|
||||
def wheelEvent(self, event: QWheelEvent) -> None: # noqa: N802
|
||||
self._zoom(1.2 if event.angleDelta().y() > 0 else 1 / 1.2)
|
||||
event.accept()
|
||||
|
||||
def set_model(self, controller: DocumentController) -> None:
|
||||
self.controller = controller
|
||||
|
||||
Reference in New Issue
Block a user