diff --git a/icons/signal_cosine_source.json b/icons/signal_cosine_source.json
new file mode 100644
index 0000000..19b724c
--- /dev/null
+++ b/icons/signal_cosine_source.json
@@ -0,0 +1,79 @@
+{
+ "shapes": {
+ "9ed4189a-123b-431f-a6bc-bbf169487703": {
+ "layer": 0,
+ "type": "rectangle",
+ "pos": [-48, -48],
+ "width": 96.0,
+ "height": 96.0,
+ "line_type": "solid",
+ "line_thickness": 5.0,
+ "corner_radius": 8.0,
+ "line_color": "#00007fff",
+ "fill_color": "#ebebebff"
+ },
+ "ac1b7cf2-ce50-44b7-9f8f-077ca36f7369": {
+ "layer": 4,
+ "type": "line",
+ "pos": [-32, -32],
+ "end": [-32, 32],
+ "line_type": "solid",
+ "line_thickness": 3.0,
+ "line_color": "#000000ff"
+ },
+ "2e623060-349e-4a48-97b0-a81921df5870": {
+ "layer": 2,
+ "type": "line",
+ "pos": [-32, 32],
+ "end": [32, 32],
+ "line_type": "solid",
+ "line_thickness": 3.0,
+ "line_color": "#000000ff"
+ },
+ "2c787197-3228-4f20-a57d-6fca410151b9": {
+ "layer": 5, "type": "line", "pos": [-28, -16], "end": [-24, -15], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "53e49b2b-f3d6-4aa5-bd56-7ca7fbf68bb4": {
+ "layer": 5, "type": "line", "pos": [-24, -15], "end": [-20, -11], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "7df5a7ce-b914-459e-93f6-f43450623baf": {
+ "layer": 5, "type": "line", "pos": [-20, -11], "end": [-16, -7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "dd89bdfb-f10f-467a-aa90-a534e1f30dd8": {
+ "layer": 5, "type": "line", "pos": [-16, -7], "end": [-12, 0], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "df9439f1-588f-46d8-873e-ecc59f5b8e36": {
+ "layer": 5, "type": "line", "pos": [-12, 0], "end": [-8, 7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "4984ed23-a303-494f-947d-a34964220298": {
+ "layer": 5, "type": "line", "pos": [-8, 7], "end": [-4, 13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "af40c4f7-d33b-426e-b130-ce78184a350f": {
+ "layer": 5, "type": "line", "pos": [-4, 13], "end": [0, 16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "63c0188e-4f24-4ec7-b8d6-6258ab13c226": {
+ "layer": 5, "type": "line", "pos": [0, 16], "end": [4, 13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "c71d5e3b-a917-4dc8-8411-2046a54e2134": {
+ "layer": 5, "type": "line", "pos": [4, 13], "end": [8, 7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "51e128e6-c63f-4437-b56f-d20c31d195e2": {
+ "layer": 5, "type": "line", "pos": [8, 7], "end": [12, 0], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "f135f861-3e98-4bb1-ab43-e51fa0eec8db": {
+ "layer": 5, "type": "line", "pos": [12, 0], "end": [16, -7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "422ce3eb-8d0f-4b05-bb54-126583521240": {
+ "layer": 5, "type": "line", "pos": [16, -7], "end": [20, -11], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "c2c60fc4-fd4c-4cfc-a90a-dd729fe551b0": {
+ "layer": 5, "type": "line", "pos": [20, -11], "end": [24, -15], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "0852263b-237a-4a32-a56d-91945864e01b": {
+ "layer": 5, "type": "line", "pos": [24, -15], "end": [28, -16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ }
+ },
+ "port_positions": {
+ "37eb6e52-6e89-4e09-8923-43c6230f5486": [-8, -8]
+ }
+}
diff --git a/icons/signal_sine_source.json b/icons/signal_sine_source.json
new file mode 100644
index 0000000..0c4450a
--- /dev/null
+++ b/icons/signal_sine_source.json
@@ -0,0 +1,97 @@
+{
+ "shapes": {
+ "9ed4189a-123b-431f-a6bc-bbf169487703": {
+ "layer": 0,
+ "type": "rectangle",
+ "pos": [
+ -48,
+ -48
+ ],
+ "width": 96.0,
+ "height": 96.0,
+ "line_type": "solid",
+ "line_thickness": 5.0,
+ "corner_radius": 8.0,
+ "line_color": "#00007fff",
+ "fill_color": "#ebebebff"
+ },
+ "ac1b7cf2-ce50-44b7-9f8f-077ca36f7369": {
+ "layer": 4,
+ "type": "line",
+ "pos": [
+ -32,
+ -32
+ ],
+ "end": [
+ -32,
+ 32
+ ],
+ "line_type": "solid",
+ "line_thickness": 3.0,
+ "line_color": "#000000ff"
+ },
+ "2e623060-349e-4a48-97b0-a81921df5870": {
+ "layer": 2,
+ "type": "line",
+ "pos": [
+ -32,
+ 32
+ ],
+ "end": [
+ 32,
+ 32
+ ],
+ "line_type": "solid",
+ "line_thickness": 3.0,
+ "line_color": "#000000ff"
+ },
+ "5cfcb42c-5cc3-48aa-ad92-7d19807c95a0": {
+ "layer": 5, "type": "line", "pos": [-28, 0], "end": [-24, -7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "78f194ed-dae0-470a-a354-f7c4c3e54c81": {
+ "layer": 5, "type": "line", "pos": [-24, -7], "end": [-20, -13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "cfa29bc9-5fac-4f00-bb02-52498fd14f92": {
+ "layer": 5, "type": "line", "pos": [-20, -13], "end": [-16, -16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "8f852157-0395-4028-a9ce-f7bd596f2976": {
+ "layer": 5, "type": "line", "pos": [-16, -16], "end": [-12, -16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "6f535277-a28e-4abc-82d4-4cf515a80357": {
+ "layer": 5, "type": "line", "pos": [-12, -16], "end": [-8, -13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "a40bba3a-d9fd-4a7c-b409-2222ce69c558": {
+ "layer": 5, "type": "line", "pos": [-8, -13], "end": [-4, -7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "2720ddd9-0332-46b6-bb79-dd90aa0892bd": {
+ "layer": 5, "type": "line", "pos": [-4, -7], "end": [0, 0], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "e420ce57-f79d-4941-8f5a-8207e15f0306": {
+ "layer": 5, "type": "line", "pos": [0, 0], "end": [4, 7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "f53f0310-7e14-4a87-aeb3-e079e2a8460e": {
+ "layer": 5, "type": "line", "pos": [4, 7], "end": [8, 13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "915cce34-b991-4119-88b5-c2dd41eec73c": {
+ "layer": 5, "type": "line", "pos": [8, 13], "end": [12, 16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "56d62c0f-4156-49eb-b048-b3b42304d6b0": {
+ "layer": 5, "type": "line", "pos": [12, 16], "end": [16, 16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "de990624-dbb2-434c-bbdf-b0ea9caa34bd": {
+ "layer": 5, "type": "line", "pos": [16, 16], "end": [20, 13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "b48fb633-b679-433f-8bee-f7c9549235aa": {
+ "layer": 5, "type": "line", "pos": [20, 13], "end": [24, 7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ },
+ "17f88457-c04d-4c08-90f6-f6ec5166c11c": {
+ "layer": 5, "type": "line", "pos": [24, 7], "end": [28, 0], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
+ }
+ },
+ "port_positions": {
+ "37eb6e52-6e89-4e09-8923-43c6230f5486": [
+ -8,
+ -8
+ ]
+ }
+}
diff --git a/lib/bondgraph.beb b/lib/bondgraph.beb
index ef8b4c6..c5a1137 100644
Binary files a/lib/bondgraph.beb and b/lib/bondgraph.beb differ
diff --git a/lib/signal.beb b/lib/signal.beb
index 2f85aa6..a9971a5 100644
Binary files a/lib/signal.beb and b/lib/signal.beb differ
diff --git a/lib/signal_sources.beb b/lib/signal_sources.beb
index 8ee6ac7..985ddd8 100644
Binary files a/lib/signal_sources.beb and b/lib/signal_sources.beb differ
diff --git a/src/bedit_gui/commands/component_command.py b/src/bedit_gui/commands/component_command.py
index 712572a..915fd4b 100644
--- a/src/bedit_gui/commands/component_command.py
+++ b/src/bedit_gui/commands/component_command.py
@@ -5,40 +5,46 @@ from bedit_gui.models import Icon
class AddEmptyGraphComponent(QUndoCommand):
- def __init__(self, document: object, parent: Component) -> None:
+ def __init__(self, document: object, parent: Component | dict[ComponentID, Component]) -> None:
super().__init__("Add graph component")
- if not isinstance(parent.implementation, GraphImplementation):
- raise TypeError("parent component must have a graph implementation")
self.document = document
- self.graph = parent.implementation.graph
+ if isinstance(parent, Component):
+ if not isinstance(parent.implementation, GraphImplementation):
+ raise TypeError("parent component must have a graph implementation")
+ self.components = parent.implementation.graph.components
+ else:
+ self.components = parent
self.component_id = ComponentID()
- self.component = Component(name="New Graph Component", interface=Interface(), parameters={}, implementation=GraphImplementation(Graph()))
+ self.component = Component(name="new_graph_component", interface=Interface(), parameters={}, implementation=GraphImplementation(Graph()))
def redo(self) -> None:
- self.graph.components[self.component_id] = self.component
+ self.components[self.component_id] = self.component
self.document.model_changed.emit(self.document.model)
def undo(self) -> None:
- del self.graph.components[self.component_id]
+ del self.components[self.component_id]
self.document.model_changed.emit(self.document.model)
class AddEmptyEquationComponent(QUndoCommand):
- def __init__(self, document: object, parent: Component) -> None:
+ def __init__(self, document: object, parent: Component | dict[ComponentID, Component]) -> None:
super().__init__("Add equation component")
- if not isinstance(parent.implementation, GraphImplementation):
- raise TypeError("parent component must have a graph implementation")
self.document = document
- self.graph = parent.implementation.graph
+ if isinstance(parent, Component):
+ if not isinstance(parent.implementation, GraphImplementation):
+ raise TypeError("parent component must have a graph implementation")
+ self.components = parent.implementation.graph.components
+ else:
+ self.components = parent
self.component_id = ComponentID()
self.component = Component(name="New Equation Component", interface=Interface(), parameters={}, implementation=EquationImplementation())
def redo(self) -> None:
- self.graph.components[self.component_id] = self.component
+ self.components[self.component_id] = self.component
self.document.model_changed.emit(self.document.model)
def undo(self) -> None:
- del self.graph.components[self.component_id]
+ del self.components[self.component_id]
self.document.model_changed.emit(self.document.model)
diff --git a/src/bedit_gui/controllers/document_tree_controller.py b/src/bedit_gui/controllers/document_tree_controller.py
index 75103bd..f8dbd78 100644
--- a/src/bedit_gui/controllers/document_tree_controller.py
+++ b/src/bedit_gui/controllers/document_tree_controller.py
@@ -17,7 +17,7 @@ from bedit_gui.views.main_window import MainWindow
from bedit_gui.views.models.document_tree_model import DocumentTreeModel
from bedit_gui.utils.icon import render_fitted_icon
-ICON_SIZE = QSize(32, 32)
+ICON_SIZE = QSize(16, 16)
class InterfaceEditorLike(Protocol):
def exec(self) -> int: ...
@@ -84,11 +84,11 @@ class DocumentTreeController(QObject):
window.ui.documentTree.setHeaderHidden(True)
window.ui.documentTree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
window.ui.documentTree.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
- window.ui.documentTree.setIconSize(QSize(48, 48))
+ window.ui.documentTree.setIconSize(QSize(24, 24))
window.ui.documentTree.header().setStretchLastSection(False)
window.ui.documentTree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
window.ui.documentTree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
- window.ui.documentTree.setColumnWidth(1, 56)
+ window.ui.documentTree.setColumnWidth(1, 28)
self._tree_viewport = window.ui.documentTree.viewport()
self._tree_viewport.installEventFilter(self)
@@ -175,11 +175,22 @@ class DocumentTreeController(QObject):
def _show_context_menu(self, position: QPoint) -> None:
index = self.window.ui.documentTree.indexAt(position)
- component = self.model.value(index)
- if not isinstance(component, Component):
- return
+ value = self.model.value(index)
+ global_position = self.window.ui.documentTree.viewport().mapToGlobal(position)
+ if isinstance(value, CoreDocument):
+ self._show_root_context_menu(global_position)
+ elif isinstance(value, Component):
+ self._show_component_context_menu(value, global_position)
- self._show_component_context_menu(component, self.window.ui.documentTree.viewport().mapToGlobal(position))
+ def _show_root_context_menu(self, global_position: QPoint) -> None:
+ menu = QMenu(self.window.ui.documentTree)
+ add_graph_component = menu.addAction("Add Graph Component")
+ add_equation_component = menu.addAction("Add Equation Component")
+ selected = menu.exec(global_position)
+ if selected is add_graph_component:
+ self.document.add_empty_root_graph_component()
+ elif selected is add_equation_component:
+ self.document.add_empty_root_equation_component()
def _show_graph_component_context_menu(self, component_id: ComponentID, global_position: QPoint) -> None:
component = self._components.get(component_id)
diff --git a/src/bedit_gui/controllers/log_controller.py b/src/bedit_gui/controllers/log_controller.py
index 55752b8..b7eee02 100644
--- a/src/bedit_gui/controllers/log_controller.py
+++ b/src/bedit_gui/controllers/log_controller.py
@@ -3,6 +3,7 @@ from __future__ import annotations
import logging
from PySide6.QtCore import QObject, Signal
+from PySide6.QtWidgets import QAbstractItemView
from bedit_gui.services.application_logging import configure_logging
from bedit_gui.views.main_window import MainWindow
@@ -49,4 +50,6 @@ class LogController(QObject):
self.emitter.message.connect(self.model.append)
self.model.rowsInserted.connect(window.ui.listView.scrollToBottom)
window.ui.listView.setModel(self.model)
+ window.ui.listView.setWordWrap(True)
+ window.ui.listView.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
configure_logging(self.handler, level)
diff --git a/src/bedit_gui/controllers/simulation_controller.py b/src/bedit_gui/controllers/simulation_controller.py
index 8e34c9d..814cc8d 100644
--- a/src/bedit_gui/controllers/simulation_controller.py
+++ b/src/bedit_gui/controllers/simulation_controller.py
@@ -65,7 +65,7 @@ class SimulationController(QObject):
self.document.infer_causality(component)
build = self.compiler(component, build_directory)
except (OSError, RuntimeError, ValueError) as exc:
- logger.exception("Could not compile model %s", component_path)
+ logger.error("Could not compile model %s:\n%s", component_path, exc)
QMessageBox.critical(self.window, "Could not compile", str(exc))
self.window.statusBar().showMessage("Compilation failed")
return None
diff --git a/src/bedit_gui/documents/document.py b/src/bedit_gui/documents/document.py
index ac68569..c823b7f 100644
--- a/src/bedit_gui/documents/document.py
+++ b/src/bedit_gui/documents/document.py
@@ -369,12 +369,22 @@ class Document(QObject):
command.component.name = self._unique_component_name(component.implementation.graph.components, command.component.name)
self.undo_stack.push(command)
+ def add_empty_root_graph_component(self) -> None:
+ command = AddEmptyGraphComponent(self, self.model.root)
+ command.component.name = self._unique_component_name(self.model.root, command.component.name)
+ self.undo_stack.push(command)
+
def add_empty_equation_component(self, component: Component) -> None:
if isinstance(component.implementation, GraphImplementation):
command = AddEmptyEquationComponent(self, component)
command.component.name = self._unique_component_name(component.implementation.graph.components, command.component.name)
self.undo_stack.push(command)
+ def add_empty_root_equation_component(self) -> None:
+ command = AddEmptyEquationComponent(self, self.model.root)
+ command.component.name = self._unique_component_name(self.model.root, command.component.name)
+ self.undo_stack.push(command)
+
def delete_component(self, component: Component) -> None:
self.undo_stack.push(DeleteComponent(self, component))
diff --git a/src/bedit_gui/models.py b/src/bedit_gui/models.py
index de86253..ae37934 100644
--- a/src/bedit_gui/models.py
+++ b/src/bedit_gui/models.py
@@ -24,6 +24,8 @@ class Shape:
def from_data(cls, data: Mapping[str, Any]) -> Shape:
if cls is Shape and data.get("type") == "rectangle":
return Rectangle.from_data(data)
+ if cls is Shape and data.get("type") == "ellipse":
+ return Ellipse.from_data(data)
if cls is Shape and data.get("type") == "text":
return Text.from_data(data)
if cls is Shape and data.get("type") == "line":
@@ -77,6 +79,24 @@ class Rectangle(Shape):
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 Ellipse(Shape):
+ type: str = field(init=False, default="ellipse")
+ width: float = 32.0
+ height: float = 32.0
+ line_type: LineType = LineType.SOLID
+ line_thickness: float = 1.0
+ line_color: str = "#000000ff"
+ fill_color: str = "#ffffff00"
+
+ @classmethod
+ def from_data(cls, data: Mapping[str, Any]) -> Ellipse:
+ 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)), line_color=str(data.get("line_color", "#000000ff")), fill_color=str(data.get("fill_color", "#ffffff00")))
+
+ 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, "line_color": self.line_color, "fill_color": self.fill_color}
+
@dataclass
class Text(Shape):
type: str = field(init=False, default="text")
diff --git a/src/bedit_gui/ui/forms/icon_editor_window.ui b/src/bedit_gui/ui/forms/icon_editor_window.ui
index 8336330..44c77a5 100644
--- a/src/bedit_gui/ui/forms/icon_editor_window.ui
+++ b/src/bedit_gui/ui/forms/icon_editor_window.ui
@@ -30,7 +30,7 @@
0
0
800
- 22
+ 19
@@ -236,6 +237,21 @@
QAction::MenuRole::NoRole
+
+
+
+ :/icons/icons/draw-circle.png:/icons/icons/draw-circle.png
+
+
+ Add Ellipse
+
+
+ Add a circle
+
+
+ QAction::MenuRole::NoRole
+
+
diff --git a/src/bedit_gui/ui/forms/icon_editor_window_ui.py b/src/bedit_gui/ui/forms/icon_editor_window_ui.py
new file mode 100644
index 0000000..c59f708
--- /dev/null
+++ b/src/bedit_gui/ui/forms/icon_editor_window_ui.py
@@ -0,0 +1,206 @@
+# -*- coding: utf-8 -*-
+
+################################################################################
+## Form generated from reading UI file 'icon_editor_window.ui'
+##
+## Created by: Qt User Interface Compiler version 6.11.1
+##
+## WARNING! All changes made in this file will be lost when recompiling UI file!
+################################################################################
+
+from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
+ QMetaObject, QObject, QPoint, QRect,
+ QSize, QTime, QUrl, Qt)
+from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
+ QCursor, QFont, QFontDatabase, QGradient,
+ QIcon, QImage, QKeySequence, QLinearGradient,
+ QPainter, QPalette, QPixmap, QRadialGradient,
+ QTransform)
+from PySide6.QtWidgets import (QApplication, QGraphicsView, QMainWindow, QMenu,
+ QMenuBar, QSizePolicy, QStatusBar, QToolBar,
+ QVBoxLayout, QWidget)
+import resources_rc
+
+class Ui_iconEditor(object):
+ def setupUi(self, iconEditor):
+ if not iconEditor.objectName():
+ iconEditor.setObjectName(u"iconEditor")
+ iconEditor.resize(800, 600)
+ icon = QIcon()
+ icon.addFile(u":/icons/icons/draw-path.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ iconEditor.setWindowIcon(icon)
+ self.actionUndo = QAction(iconEditor)
+ self.actionUndo.setObjectName(u"actionUndo")
+ icon1 = QIcon()
+ icon1.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionUndo.setIcon(icon1)
+ self.actionUndo.setMenuRole(QAction.MenuRole.NoRole)
+ self.actionRedo = QAction(iconEditor)
+ self.actionRedo.setObjectName(u"actionRedo")
+ icon2 = QIcon()
+ icon2.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionRedo.setIcon(icon2)
+ self.actionRedo.setMenuRole(QAction.MenuRole.NoRole)
+ self.actionSave = QAction(iconEditor)
+ self.actionSave.setObjectName(u"actionSave")
+ icon3 = QIcon()
+ icon3.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionSave.setIcon(icon3)
+ self.actionSave.setMenuRole(QAction.MenuRole.NoRole)
+ self.actionCancel = QAction(iconEditor)
+ self.actionCancel.setObjectName(u"actionCancel")
+ icon4 = QIcon()
+ icon4.addFile(u":/icons/icons/dialog-close.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionCancel.setIcon(icon4)
+ self.actionCancel.setMenuRole(QAction.MenuRole.NoRole)
+ self.actionAdd_Rectangle = QAction(iconEditor)
+ self.actionAdd_Rectangle.setObjectName(u"actionAdd_Rectangle")
+ icon5 = QIcon()
+ icon5.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionAdd_Rectangle.setIcon(icon5)
+ self.actionAdd_Rectangle.setMenuRole(QAction.MenuRole.NoRole)
+ self.actionAdd_Text = QAction(iconEditor)
+ self.actionAdd_Text.setObjectName(u"actionAdd_Text")
+ icon6 = QIcon()
+ icon6.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionAdd_Text.setIcon(icon6)
+ self.actionAdd_Text.setMenuRole(QAction.MenuRole.NoRole)
+ self.actionSave_to_File = QAction(iconEditor)
+ self.actionSave_to_File.setObjectName(u"actionSave_to_File")
+ icon7 = QIcon()
+ icon7.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionSave_to_File.setIcon(icon7)
+ self.actionSave_to_File.setMenuRole(QAction.MenuRole.NoRole)
+ self.actionOpen_from_File = QAction(iconEditor)
+ self.actionOpen_from_File.setObjectName(u"actionOpen_from_File")
+ icon8 = QIcon()
+ icon8.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionOpen_from_File.setIcon(icon8)
+ self.actionOpen_from_File.setMenuRole(QAction.MenuRole.NoRole)
+ self.actionAdd_Line = QAction(iconEditor)
+ self.actionAdd_Line.setObjectName(u"actionAdd_Line")
+ self.actionAdd_Line.setIcon(icon)
+ self.actionAdd_Line.setMenuRole(QAction.MenuRole.NoRole)
+ self.actionAdd_Circle = QAction(iconEditor)
+ self.actionAdd_Circle.setObjectName(u"actionAdd_Circle")
+ icon9 = QIcon()
+ icon9.addFile(u":/icons/icons/draw-circle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionAdd_Circle.setIcon(icon9)
+ self.actionAdd_Circle.setMenuRole(QAction.MenuRole.NoRole)
+ self.centralwidget = QWidget(iconEditor)
+ self.centralwidget.setObjectName(u"centralwidget")
+ self.verticalLayout = QVBoxLayout(self.centralwidget)
+ self.verticalLayout.setObjectName(u"verticalLayout")
+ self.graphicsView = QGraphicsView(self.centralwidget)
+ self.graphicsView.setObjectName(u"graphicsView")
+
+ self.verticalLayout.addWidget(self.graphicsView)
+
+ iconEditor.setCentralWidget(self.centralwidget)
+ self.menubar = QMenuBar(iconEditor)
+ self.menubar.setObjectName(u"menubar")
+ self.menubar.setGeometry(QRect(0, 0, 800, 19))
+ self.menuEdit = QMenu(self.menubar)
+ self.menuEdit.setObjectName(u"menuEdit")
+ self.menuFile = QMenu(self.menubar)
+ self.menuFile.setObjectName(u"menuFile")
+ iconEditor.setMenuBar(self.menubar)
+ self.statusbar = QStatusBar(iconEditor)
+ self.statusbar.setObjectName(u"statusbar")
+ iconEditor.setStatusBar(self.statusbar)
+ self.actionToolbar = QToolBar(iconEditor)
+ self.actionToolbar.setObjectName(u"actionToolbar")
+ iconEditor.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.actionToolbar)
+ self.iconToolbar = QToolBar(iconEditor)
+ self.iconToolbar.setObjectName(u"iconToolbar")
+ iconEditor.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.iconToolbar)
+
+ self.menubar.addAction(self.menuFile.menuAction())
+ self.menubar.addAction(self.menuEdit.menuAction())
+ self.menuEdit.addAction(self.actionUndo)
+ self.menuEdit.addAction(self.actionRedo)
+ self.menuFile.addAction(self.actionOpen_from_File)
+ self.menuFile.addAction(self.actionSave_to_File)
+ self.menuFile.addSeparator()
+ self.menuFile.addAction(self.actionSave)
+ self.menuFile.addAction(self.actionCancel)
+ self.actionToolbar.addAction(self.actionUndo)
+ self.actionToolbar.addAction(self.actionRedo)
+ self.actionToolbar.addAction(self.actionSave)
+ self.actionToolbar.addAction(self.actionCancel)
+ self.iconToolbar.addAction(self.actionAdd_Rectangle)
+ self.iconToolbar.addAction(self.actionAdd_Circle)
+ self.iconToolbar.addAction(self.actionAdd_Text)
+ self.iconToolbar.addAction(self.actionAdd_Line)
+
+ self.retranslateUi(iconEditor)
+
+ QMetaObject.connectSlotsByName(iconEditor)
+ # setupUi
+
+ def retranslateUi(self, iconEditor):
+ iconEditor.setWindowTitle(QCoreApplication.translate("iconEditor", u"MainWindow", None))
+ self.actionUndo.setText(QCoreApplication.translate("iconEditor", u"Undo", None))
+#if QT_CONFIG(tooltip)
+ self.actionUndo.setToolTip(QCoreApplication.translate("iconEditor", u"Undo", None))
+#endif // QT_CONFIG(tooltip)
+#if QT_CONFIG(shortcut)
+ self.actionUndo.setShortcut(QCoreApplication.translate("iconEditor", u"Ctrl+Z", None))
+#endif // QT_CONFIG(shortcut)
+ self.actionRedo.setText(QCoreApplication.translate("iconEditor", u"Redo", None))
+#if QT_CONFIG(tooltip)
+ self.actionRedo.setToolTip(QCoreApplication.translate("iconEditor", u"Redo", None))
+#endif // QT_CONFIG(tooltip)
+#if QT_CONFIG(shortcut)
+ self.actionRedo.setShortcut(QCoreApplication.translate("iconEditor", u"Ctrl+Y", None))
+#endif // QT_CONFIG(shortcut)
+ self.actionSave.setText(QCoreApplication.translate("iconEditor", u"Save", None))
+#if QT_CONFIG(tooltip)
+ self.actionSave.setToolTip(QCoreApplication.translate("iconEditor", u"Save icon", None))
+#endif // QT_CONFIG(tooltip)
+#if QT_CONFIG(shortcut)
+ self.actionSave.setShortcut(QCoreApplication.translate("iconEditor", u"Return", None))
+#endif // QT_CONFIG(shortcut)
+ self.actionCancel.setText(QCoreApplication.translate("iconEditor", u"Cancel", None))
+#if QT_CONFIG(tooltip)
+ self.actionCancel.setToolTip(QCoreApplication.translate("iconEditor", u"Cancel icon editing", None))
+#endif // QT_CONFIG(tooltip)
+#if QT_CONFIG(shortcut)
+ self.actionCancel.setShortcut(QCoreApplication.translate("iconEditor", u"Shift+Esc", None))
+#endif // QT_CONFIG(shortcut)
+ self.actionAdd_Rectangle.setText(QCoreApplication.translate("iconEditor", u"Add Rectangle", None))
+#if QT_CONFIG(tooltip)
+ self.actionAdd_Rectangle.setToolTip(QCoreApplication.translate("iconEditor", u"Add a rectangle", None))
+#endif // QT_CONFIG(tooltip)
+ self.actionAdd_Text.setText(QCoreApplication.translate("iconEditor", u"Add Text", None))
+#if QT_CONFIG(tooltip)
+ self.actionAdd_Text.setToolTip(QCoreApplication.translate("iconEditor", u"Add a text field", None))
+#endif // QT_CONFIG(tooltip)
+ self.actionSave_to_File.setText(QCoreApplication.translate("iconEditor", u"Save to File", None))
+#if QT_CONFIG(tooltip)
+ self.actionSave_to_File.setToolTip(QCoreApplication.translate("iconEditor", u"Save icon to a file", None))
+#endif // QT_CONFIG(tooltip)
+#if QT_CONFIG(shortcut)
+ self.actionSave_to_File.setShortcut(QCoreApplication.translate("iconEditor", u"Ctrl+Shift+S", None))
+#endif // QT_CONFIG(shortcut)
+ self.actionOpen_from_File.setText(QCoreApplication.translate("iconEditor", u"Open from File", None))
+#if QT_CONFIG(tooltip)
+ self.actionOpen_from_File.setToolTip(QCoreApplication.translate("iconEditor", u"Open icon from File", None))
+#endif // QT_CONFIG(tooltip)
+#if QT_CONFIG(shortcut)
+ self.actionOpen_from_File.setShortcut(QCoreApplication.translate("iconEditor", u"Ctrl+Shift+O", None))
+#endif // QT_CONFIG(shortcut)
+ self.actionAdd_Line.setText(QCoreApplication.translate("iconEditor", u"Add Line", None))
+#if QT_CONFIG(tooltip)
+ self.actionAdd_Line.setToolTip(QCoreApplication.translate("iconEditor", u"Add a line", None))
+#endif // QT_CONFIG(tooltip)
+ self.actionAdd_Circle.setText(QCoreApplication.translate("iconEditor", u"Add Ellipse", None))
+#if QT_CONFIG(tooltip)
+ self.actionAdd_Circle.setToolTip(QCoreApplication.translate("iconEditor", u"Add a circle", None))
+#endif // QT_CONFIG(tooltip)
+ self.menuEdit.setTitle(QCoreApplication.translate("iconEditor", u"Edit", None))
+ self.menuFile.setTitle(QCoreApplication.translate("iconEditor", u"File", None))
+ self.actionToolbar.setWindowTitle(QCoreApplication.translate("iconEditor", u"toolBar", None))
+ self.iconToolbar.setWindowTitle(QCoreApplication.translate("iconEditor", u"toolBar", None))
+ # retranslateUi
+
diff --git a/src/bedit_gui/utils/icon.py b/src/bedit_gui/utils/icon.py
index 267f806..3595ddc 100644
--- a/src/bedit_gui/utils/icon.py
+++ b/src/bedit_gui/utils/icon.py
@@ -6,7 +6,7 @@ from PySide6.QtCore import QRectF, QSize, Qt
from PySide6.QtGui import QBrush, QColor, QFont, QIcon, QPainter, QPen, QPixmap, QRegion
from bedit_core.models import Port, PortID, SignalDirection
-from bedit_gui.models import Icon, Line, LineType, Rectangle, Text
+from bedit_gui.models import Ellipse, Icon, Line, LineType, Rectangle, Text
PORT_SIZE = 16
DEFAULT_ICON_SIZE = QSize(48, 48)
@@ -21,7 +21,7 @@ def get_bounding_box(icon: Icon) -> QRectF:
for shape in icon.shapes.values():
x, y = shape.pos
points.append((x, y))
- if isinstance(shape, (Rectangle, Text)):
+ if isinstance(shape, (Rectangle, Ellipse, Text)):
points.append((x + shape.width, y + shape.height))
elif isinstance(shape, Line):
points.append(shape.end)
@@ -72,6 +72,10 @@ def render_icon(icon: Icon, ports: dict[PortID, Port], size: QSize = DEFAULT_ICO
painter.setPen(_line_pen(shape.line_type, shape.line_thickness, shape.line_color))
painter.setBrush(QBrush(_color(shape.fill_color)))
painter.drawRoundedRect(QRectF(shape.pos[0], shape.pos[1], shape.width, shape.height), shape.corner_radius, shape.corner_radius)
+ elif isinstance(shape, Ellipse):
+ painter.setPen(_line_pen(shape.line_type, shape.line_thickness, shape.line_color))
+ painter.setBrush(QBrush(_color(shape.fill_color)))
+ painter.drawEllipse(QRectF(shape.pos[0], shape.pos[1], shape.width, shape.height))
elif isinstance(shape, Text):
font = QFont()
font.setPixelSize(max(1, round(shape.size)))
diff --git a/src/bedit_gui/views/graph_editor_widget.py b/src/bedit_gui/views/graph_editor_widget.py
index e7a027d..d0e8c8e 100644
--- a/src/bedit_gui/views/graph_editor_widget.py
+++ b/src/bedit_gui/views/graph_editor_widget.py
@@ -162,7 +162,6 @@ class GraphComponentItem(QGraphicsPixmapItem):
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if event.button() == Qt.MouseButton.LeftButton and self.editor.mode is GraphEditorMode.CONNECTION:
- self.editor.choose_connection_component(self.component_id, event.screenPos())
event.accept()
return
self._drag_start = QPointF(self.pos())
@@ -171,6 +170,8 @@ class GraphComponentItem(QGraphicsPixmapItem):
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if self.editor.mode is GraphEditorMode.CONNECTION:
+ if event.button() == Qt.MouseButton.LeftButton:
+ self.editor.choose_connection_component(self.component_id, event.screenPos())
event.accept()
return
super().mouseReleaseEvent(event)
@@ -723,7 +724,7 @@ class GraphEditorWidget(QWidget):
@staticmethod
def _port_available(port_id: PortID, port: Port, used_ports: set[PortID]) -> bool:
if isinstance(port, SignalPort):
- return port.direction is SignalDirection.OUTPUT or port_id not in used_ports
+ return port.direction is SignalDirection.OUTPUT or port.multiplicity or port_id not in used_ports
if isinstance(port, BondPort):
return port.multiplicity or port_id not in used_ports
return False
diff --git a/src/bedit_gui/views/icon_editor_window.py b/src/bedit_gui/views/icon_editor_window.py
index 7b8eabe..8609a32 100644
--- a/src/bedit_gui/views/icon_editor_window.py
+++ b/src/bedit_gui/views/icon_editor_window.py
@@ -11,7 +11,7 @@ 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.views.icon_graphics_scene import IconGraphicsScene, LineCreationTool, RectangleCreationTool, TextCreationTool
+from bedit_gui.views.icon_graphics_scene import EllipseCreationTool, IconGraphicsScene, LineCreationTool, RectangleCreationTool, TextCreationTool
from bedit_gui.views.shape_options_dialog import ShapeOptionsDialog
logger = get_logger(__name__)
@@ -123,6 +123,8 @@ class IconEditorWindow(QMainWindow):
self.ui.graphicsView.viewport().installEventFilter(self)
self.ui.actionAdd_Rectangle.setCheckable(True)
self.ui.actionAdd_Rectangle.triggered.connect(self._start_rectangle_tool)
+ self.ui.actionAdd_Circle.setCheckable(True)
+ self.ui.actionAdd_Circle.triggered.connect(self._start_ellipse_tool)
self.ui.actionAdd_Text.setCheckable(True)
self.ui.actionAdd_Text.triggered.connect(self._start_text_tool)
self.ui.actionAdd_Line.setCheckable(True)
@@ -306,14 +308,24 @@ class IconEditorWindow(QMainWindow):
def _start_rectangle_tool(self) -> None:
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
+ self.ui.actionAdd_Circle.setChecked(False)
self.ui.actionAdd_Text.setChecked(False)
self.ui.actionAdd_Line.setChecked(False)
self.scene.set_creation_tool(RectangleCreationTool(self.scene, layer))
self.ui.actionAdd_Rectangle.setChecked(True)
+ def _start_ellipse_tool(self) -> None:
+ layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
+ self.ui.actionAdd_Rectangle.setChecked(False)
+ self.ui.actionAdd_Text.setChecked(False)
+ self.ui.actionAdd_Line.setChecked(False)
+ self.scene.set_creation_tool(EllipseCreationTool(self.scene, layer))
+ self.ui.actionAdd_Circle.setChecked(True)
+
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.ui.actionAdd_Circle.setChecked(False)
self.ui.actionAdd_Line.setChecked(False)
self.scene.set_creation_tool(TextCreationTool(self.scene, layer))
self.ui.actionAdd_Text.setChecked(True)
@@ -321,6 +333,7 @@ class IconEditorWindow(QMainWindow):
def _start_line_tool(self) -> None:
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
self.ui.actionAdd_Rectangle.setChecked(False)
+ self.ui.actionAdd_Circle.setChecked(False)
self.ui.actionAdd_Text.setChecked(False)
self.scene.set_creation_tool(LineCreationTool(self.scene, layer))
self.ui.actionAdd_Line.setChecked(True)
@@ -328,6 +341,7 @@ class IconEditorWindow(QMainWindow):
def _tool_active_changed(self, active: bool) -> None:
if not active:
self.ui.actionAdd_Rectangle.setChecked(False)
+ self.ui.actionAdd_Circle.setChecked(False)
self.ui.actionAdd_Text.setChecked(False)
self.ui.actionAdd_Line.setChecked(False)
cursor = Qt.CursorShape.CrossCursor if active else Qt.CursorShape.ArrowCursor
diff --git a/src/bedit_gui/views/icon_graphics_scene.py b/src/bedit_gui/views/icon_graphics_scene.py
index f0e1fca..e3014a6 100644
--- a/src/bedit_gui/views/icon_graphics_scene.py
+++ b/src/bedit_gui/views/icon_graphics_scene.py
@@ -6,10 +6,10 @@ from typing import Protocol
from PySide6.QtCore import QLineF, QObject, QPointF, QRectF, Qt, Signal
from PySide6.QtGui import QBrush, QColor, QFont, QPainter, QPainterPath, QPen
-from PySide6.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget
+from PySide6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem, QGraphicsLineItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget
from bedit_core.models import Port, PortID, SignalDirection
-from bedit_gui.models import Icon, Line, LineType, Rectangle, Shape, ShapeID, Text
+from bedit_gui.models import Ellipse, Icon, Line, LineType, Rectangle, Shape, ShapeID, Text
ICON_SCENE_SIZE = 512
@@ -26,7 +26,7 @@ class RectangleCreationTool:
self.scene = scene
self.layer = layer
self.start: QPointF | None = None
- self.preview: QGraphicsRectItem | None = None
+ self.preview: QGraphicsRectItem | QGraphicsEllipseItem | None = None
def begin(self, position: QPointF) -> None:
position = self._bounded(position)
@@ -69,6 +69,16 @@ class TextCreationTool(RectangleCreationTool):
return Text(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height(), text="Text")
+class EllipseCreationTool(RectangleCreationTool):
+ def begin(self, position: QPointF) -> None:
+ position = self._bounded(position)
+ self.start = position
+ self.preview = self.scene.addEllipse(QRectF(position, position), QPen(Qt.PenStyle.DashLine))
+
+ def create_shape(self, rect: QRectF) -> Shape:
+ return Ellipse(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height())
+
+
class LineCreationTool:
def __init__(self, scene: QGraphicsScene, layer: int) -> None:
self.scene = scene
@@ -109,7 +119,8 @@ class LineCreationTool:
class ShapeGraphicsItem(QGraphicsPathItem):
- handle_size = 6.0
+ handle_size = 8.0
+ handle_hit_size = 16.0
def __init__(self, shape_id: ShapeID, shape: Shape, scene: IconGraphicsScene) -> None:
super().__init__()
@@ -128,13 +139,24 @@ class ShapeGraphicsItem(QGraphicsPathItem):
def resize_handles(self) -> dict[str, QRectF]:
return {"size": self.resize_handle_rect()}
+ def resize_handle_hit_rects(self) -> dict[str, QRectF]:
+ size = self.handle_hit_size
+ return {name: QRectF(rect.center().x() - size / 2, rect.center().y() - size / 2, size, size) for name, rect in self.resize_handles().items()}
+
def boundingRect(self) -> QRectF:
- margin = self.handle_size / 2
+ margin = self.handle_hit_size / 2
return super().boundingRect().adjusted(-margin, -margin, margin, margin)
+ def shape(self) -> QPainterPath:
+ path = super().shape()
+ if self.isSelected():
+ for rect in self.resize_handle_hit_rects().values():
+ path.addRect(rect)
+ return path
+
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
self._original_shape = self.current_shape()
- self._resize_handle = next((name for name, rect in self.resize_handles().items() if self.isSelected() and rect.contains(event.pos())), None)
+ self._resize_handle = next((name for name, rect in self.resize_handle_hit_rects().items() if self.isSelected() and rect.contains(event.pos())), None)
if self._resize_handle is not None:
event.accept()
return
@@ -226,6 +248,37 @@ class RectangleGraphicsItem(ShapeGraphicsItem):
self.setPath(path)
+class EllipseGraphicsItem(ShapeGraphicsItem):
+ def __init__(self, shape_id: ShapeID, shape: Ellipse, scene: IconGraphicsScene) -> None:
+ super().__init__(shape_id, shape, scene)
+ self.ellipse = deepcopy(shape)
+ self.setPos(shape.pos[0], shape.pos[1])
+ self._set_size(shape.width, shape.height)
+ self.setPen(scene._pen(shape))
+ self.setBrush(QBrush(scene._color(shape.fill_color)))
+ self.setZValue(shape.layer)
+
+ def current_shape(self) -> Ellipse:
+ shape = deepcopy(self.ellipse)
+ 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, _handle: str) -> 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 _set_size(self, width: float, height: float) -> None:
+ self.prepareGeometryChange()
+ path = QPainterPath()
+ path.addEllipse(QRectF(0, 0, width, height))
+ self.setPath(path)
+
+
class TextGraphicsItem(ShapeGraphicsItem):
def __init__(self, shape_id: ShapeID, shape: Text, scene: IconGraphicsScene) -> None:
super().__init__(shape_id, shape, scene)
@@ -440,13 +493,15 @@ class IconGraphicsScene(QGraphicsScene):
def _add_shape_item(self, shape_id: ShapeID, shape: Shape) -> None:
if isinstance(shape, Rectangle):
self.addItem(RectangleGraphicsItem(shape_id, shape, self))
+ elif isinstance(shape, Ellipse):
+ self.addItem(EllipseGraphicsItem(shape_id, shape, self))
elif isinstance(shape, Text):
self.addItem(TextGraphicsItem(shape_id, shape, self))
elif isinstance(shape, Line):
self.addItem(LineGraphicsItem(shape_id, shape, self))
@staticmethod
- def _pen(shape: Rectangle) -> QPen:
+ def _pen(shape: Rectangle | Ellipse) -> QPen:
return IconGraphicsScene._line_pen(shape.line_type, shape.line_thickness, shape.line_color)
@staticmethod
diff --git a/src/bedit_gui/views/shape_options_dialog.py b/src/bedit_gui/views/shape_options_dialog.py
index 3c47e80..ee010d8 100644
--- a/src/bedit_gui/views/shape_options_dialog.py
+++ b/src/bedit_gui/views/shape_options_dialog.py
@@ -5,7 +5,7 @@ from copy import deepcopy
from PySide6.QtCore import QRectF
from PySide6.QtWidgets import QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel, QLineEdit, QSpinBox, QVBoxLayout, QWidget
-from bedit_gui.models import Line, LineType, Rectangle, Shape, Text
+from bedit_gui.models import Ellipse, Line, LineType, Rectangle, Shape, Text
from bedit_gui.views.color_button import ColorButton
@@ -34,6 +34,8 @@ class ShapeOptionsDialog(QDialog):
if isinstance(shape, Rectangle):
self._add_rectangle_fields(shape)
+ elif isinstance(shape, Ellipse):
+ self._add_ellipse_fields(shape)
elif isinstance(shape, Text):
self._add_text_fields(shape)
elif isinstance(shape, Line):
@@ -51,6 +53,10 @@ class ShapeOptionsDialog(QDialog):
width = min(self.width.value(), self._scene_rect.right() - self.x.value())
height = min(self.height.value(), self._scene_rect.bottom() - 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())
+ if isinstance(self._shape, Ellipse):
+ width = min(self.width.value(), self._scene_rect.right() - self.x.value())
+ height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
+ return Ellipse(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(), line_color=self.line_color.color(), fill_color=self.fill_color.color())
if isinstance(self._shape, Text):
width = min(self.width.value(), self._scene_rect.right() - self.x.value())
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
@@ -89,6 +95,29 @@ class ShapeOptionsDialog(QDialog):
self.form.addRow("Line color", self.line_color)
self.form.addRow("Fill color", self.fill_color)
+ def _add_ellipse_fields(self, shape: Ellipse) -> 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.line_type = QComboBox()
+ for line_type in LineType:
+ self.line_type.addItem(line_type.value.replace("_", " ").title(), line_type)
+ self.line_type.setCurrentIndex(self.line_type.findData(shape.line_type))
+ self.line_thickness = QDoubleSpinBox()
+ self.line_thickness.setRange(0, 1000)
+ self.line_thickness.setValue(shape.line_thickness)
+ self.line_color = ColorButton(shape.line_color)
+ self.fill_color = ColorButton(shape.fill_color)
+ self.form.addRow("Width", self.width)
+ self.form.addRow("Height", self.height)
+ self.form.addRow("Line type", self.line_type)
+ self.form.addRow("Line thickness", self.line_thickness)
+ self.form.addRow("Line color", self.line_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()))
diff --git a/src/bedit_simulation/openmodelica.py b/src/bedit_simulation/openmodelica.py
index d2af426..c875013 100644
--- a/src/bedit_simulation/openmodelica.py
+++ b/src/bedit_simulation/openmodelica.py
@@ -24,6 +24,14 @@ class ProcessResult:
stdout: str
stderr: str
+ def diagnostics(self) -> str:
+ sections = []
+ if self.stdout.strip():
+ sections.append(f"OpenModelica output:\n{self.stdout.strip()}")
+ if self.stderr.strip():
+ sections.append(f"OpenModelica errors:\n{self.stderr.strip()}")
+ return "\n\n".join(sections)
+
class OpenModelicaError(RuntimeError):
"""Raised when OMC cannot start or reports a failure."""
@@ -80,7 +88,7 @@ class OpenModelicaRunner:
f"OpenModelica timed out after {error.timeout} seconds"
) from error
if result.return_code != 0:
- details = result.stderr.strip() or result.stdout.strip()
+ details = result.diagnostics()
suffix = f": {details}" if details else ""
raise OpenModelicaError(
f"OpenModelica exited with status {result.return_code}{suffix}"
@@ -128,7 +136,7 @@ class OpenModelicaRunner:
continue
result = ProcessResult(command=tuple(command), return_code=process.returncode, stdout=stdout, stderr=stderr)
if result.return_code != 0:
- details = result.stderr.strip() or result.stdout.strip()
+ details = result.diagnostics()
suffix = f": {details}" if details else ""
raise OpenModelicaError(f"simulation exited with status {result.return_code}{suffix}")
return result
diff --git a/src/bedit_simulation/simulation.py b/src/bedit_simulation/simulation.py
index 3bbf26d..02998a1 100644
--- a/src/bedit_simulation/simulation.py
+++ b/src/bedit_simulation/simulation.py
@@ -27,6 +27,7 @@ from .results import SimulationResult, load_openmodelica_csv
_MODEL_FILE = "model.mo"
_RUN_SCRIPT_FILE = "run.mos"
_RUN_OUTPUT_FILE = "simulation-output.txt"
+_DEFAULT_LIBRARIES = ("Modelica",)
@dataclass(frozen=True)
@@ -386,7 +387,7 @@ class Simulation:
if windows_executable.is_file():
executable = windows_executable
else:
- details = process.stderr.strip() or process.stdout.strip()
+ details = process.diagnostics()
suffix = f": {details}" if details else ""
raise OpenModelicaError(
f"OpenModelica did not build {model_name!r}{suffix}"
@@ -692,10 +693,20 @@ def _load_model_script(
working_directory: Path,
commands: Sequence[str],
) -> str:
- """Create a script that loads ``model.mo`` before custom commands."""
+ """Create a script that loads default libraries and ``model.mo`` before custom commands."""
+ load_libraries = []
+ for library in _DEFAULT_LIBRARIES:
+ load_libraries.extend([
+ f"loaded := loadModel({library});",
+ "if not loaded then",
+ " print(getErrorString());",
+ " exit(1);",
+ "end if;",
+ ])
return "\n".join(
[
f"cd({json.dumps(str(working_directory.resolve()))});",
+ *load_libraries,
f"loaded := loadFile({json.dumps(_MODEL_FILE)});",
"if not loaded then",
" print(getErrorString());",
diff --git a/untitled.bedit.json b/untitled.bedit.json
index cea3e7f..2fea8ac 100644
--- a/untitled.bedit.json
+++ b/untitled.bedit.json
@@ -607,6 +607,179 @@
"end for;"
]
}
+ },
+ "ac5011ce-4659-4bdd-900a-63a1c779652c": {
+ "name": "pulse",
+ "interface": {
+ "ports": {
+ "69974610-3bd6-42c1-a8bb-afe6086ef0f3": {
+ "port_type": "signal",
+ "name": "y",
+ "direction": "output",
+ "multiplicity": false,
+ "matrix_size": [
+ 1,
+ 1
+ ],
+ "description": null,
+ "value_type": "real",
+ "quantity": null,
+ "unit": null
+ }
+ }
+ },
+ "parameters": {
+ "8e128f7a-cf97-4f27-a664-488921819395": {
+ "name": "amplitude",
+ "value": "1.0",
+ "value_type": "real",
+ "matrix_size": [
+ 1,
+ 1
+ ],
+ "quantity": null,
+ "unit": null,
+ "description": null
+ },
+ "5ff17831-f2f9-4833-9e4b-4facf7c28c0c": {
+ "name": "width",
+ "value": "50.0",
+ "value_type": "real",
+ "matrix_size": [
+ 1,
+ 1
+ ],
+ "quantity": null,
+ "unit": null,
+ "description": "Pulse width in %"
+ },
+ "b036592b-4dc0-498b-a966-7146cf9cf95b": {
+ "name": "offset",
+ "value": "0",
+ "value_type": "real",
+ "matrix_size": [
+ 1,
+ 1
+ ],
+ "quantity": null,
+ "unit": null,
+ "description": null
+ },
+ "df82150d-973d-4a8a-9bd5-6ce21a2e8bbb": {
+ "name": "period",
+ "value": "1.0",
+ "value_type": "real",
+ "matrix_size": [
+ 1,
+ 1
+ ],
+ "quantity": null,
+ "unit": null,
+ "description": null
+ },
+ "1195c93e-3309-494e-9a56-96e8fd5e330c": {
+ "name": "nperiod",
+ "value": "-1",
+ "value_type": "int",
+ "matrix_size": [
+ 1,
+ 1
+ ],
+ "quantity": null,
+ "unit": null,
+ "description": "Number of periods (<0 means infinite)"
+ },
+ "3f5f7b5c-2794-4e75-a5c8-1ef2b0ecd87d": {
+ "name": "startTime",
+ "value": "0.0",
+ "value_type": "real",
+ "matrix_size": [
+ 1,
+ 1
+ ],
+ "quantity": null,
+ "unit": null,
+ "description": null
+ }
+ },
+ "implementation": {
+ "implementation_type": "equation",
+ "declarations": [
+ "Integer count;",
+ "Modelica.Units.SI.Time T_start;",
+ "Modelica.Units.SI.Time T_width=period*width/100;"
+ ],
+ "initial_equations": [
+ "initial algorithm",
+ "count := integer((time-startTime)/period);",
+ "T_start := startTime + count*period;"
+ ],
+ "equations": [
+ "when time >= (pre(count)+1)*period + startTime then",
+ "\tcount = pre(count) + 1;",
+ "\tT_start = time;",
+ "end when;",
+ "y = offset + (if (time < startTime or nperiod == 0 or (nperiod > 0 and count >= nperiod)) then 0 else if time < T_start + T_width then amplitude else 0);"
+ ]
+ }
+ },
+ "ec0599cb-b57b-4194-b166-526d5cd5382a": {
+ "name": "plusminus",
+ "interface": {
+ "ports": {
+ "23a8b215-7c2f-4ccf-b020-4d274d7a228f": {
+ "port_type": "signal",
+ "name": "plus",
+ "direction": "input",
+ "multiplicity": true,
+ "matrix_size": [
+ 1,
+ 1
+ ],
+ "description": null,
+ "value_type": "real",
+ "quantity": null,
+ "unit": null
+ },
+ "4c4771f8-22c8-4829-a060-913470e2b2fc": {
+ "port_type": "signal",
+ "name": "minus",
+ "direction": "input",
+ "multiplicity": true,
+ "matrix_size": [
+ 1,
+ 1
+ ],
+ "description": null,
+ "value_type": "real",
+ "quantity": null,
+ "unit": null
+ },
+ "a186a0e1-7aea-4fb6-871c-0ba70d78b8ac": {
+ "port_type": "signal",
+ "name": "y",
+ "direction": "output",
+ "multiplicity": false,
+ "matrix_size": [
+ 1,
+ 1
+ ],
+ "description": null,
+ "value_type": "real",
+ "quantity": null,
+ "unit": null
+ }
+ }
+ },
+ "parameters": {},
+ "implementation": {
+ "implementation_type": "equation",
+ "declarations": [],
+ "initial_equations": [],
+ "equations": [
+ "y = sum(plus) - sum(minus);"
+ ]
+ }
}
},
"connections": {
@@ -661,6 +834,16 @@
"connection_type": "signal",
"source": "a2709c23-7485-43b2-bc87-28a0f4c37c0b",
"target": "8bd1cc42-03b9-4c46-b31b-c1f46933dc9e"
+ },
+ "42f8d11c-8b31-476a-8e56-dd1ac3703508": {
+ "connection_type": "signal",
+ "source": "69974610-3bd6-42c1-a8bb-afe6086ef0f3",
+ "target": "23a8b215-7c2f-4ccf-b020-4d274d7a228f"
+ },
+ "81e6b0d7-65f2-4edc-a303-6eee78ac1972": {
+ "connection_type": "signal",
+ "source": "5807dc83-d049-4f0f-9708-d041a752fcd0",
+ "target": "4c4771f8-22c8-4829-a060-913470e2b2fc"
}
}
}
@@ -1149,6 +1332,139 @@
-32
]
}
+ },
+ "ac5011ce-4659-4bdd-900a-63a1c779652c": {
+ "shapes": {
+ "7a9115fe-4e40-49d9-ab50-2f399ca1038b": {
+ "layer": 0,
+ "type": "rectangle",
+ "pos": [
+ -48,
+ -48
+ ],
+ "width": 96.0,
+ "height": 96.0,
+ "line_type": "solid",
+ "line_thickness": 5.0,
+ "corner_radius": 8.0,
+ "line_color": "#00007fff",
+ "fill_color": "#ebebebff"
+ },
+ "80e150dc-07d3-4d27-9fe0-eecb82fb2689": {
+ "layer": 4,
+ "type": "line",
+ "pos": [
+ -32,
+ -32
+ ],
+ "end": [
+ -32,
+ 32
+ ],
+ "line_type": "solid",
+ "line_thickness": 3.0,
+ "line_color": "#000000ff"
+ },
+ "66c7fb78-2866-4f9f-9df2-95cfa4c57a2d": {
+ "layer": 2,
+ "type": "line",
+ "pos": [
+ -32,
+ 32
+ ],
+ "end": [
+ 32,
+ 32
+ ],
+ "line_type": "solid",
+ "line_thickness": 3.0,
+ "line_color": "#000000ff"
+ },
+ "b1b1f425-eea6-48e7-a61d-f9eb221984b7": {
+ "layer": 3,
+ "type": "line",
+ "pos": [
+ -32,
+ 14
+ ],
+ "end": [
+ 3,
+ 14
+ ],
+ "line_type": "solid",
+ "line_thickness": 2.0,
+ "line_color": "#ffaa00ff"
+ },
+ "0c0b7ec3-557d-4fb4-b8b8-b6b6a269ec55": {
+ "layer": 5,
+ "type": "line",
+ "pos": [
+ 3,
+ 14
+ ],
+ "end": [
+ 3,
+ -13
+ ],
+ "line_type": "solid",
+ "line_thickness": 2.0,
+ "line_color": "#ffaa00ff"
+ },
+ "e5c99d8a-69db-4915-b1e7-84ac5e52de02": {
+ "layer": 6,
+ "type": "line",
+ "pos": [
+ 3,
+ -13
+ ],
+ "end": [
+ 32,
+ -13
+ ],
+ "line_type": "solid",
+ "line_thickness": 2.0,
+ "line_color": "#ffaa00ff"
+ }
+ },
+ "port_positions": {
+ "69974610-3bd6-42c1-a8bb-afe6086ef0f3": [
+ -8,
+ -8
+ ]
+ }
+ },
+ "ec0599cb-b57b-4194-b166-526d5cd5382a": {
+ "shapes": {
+ "ecbbad0e-67e3-4d13-ba25-7660f50bc38e": {
+ "layer": 0,
+ "type": "rectangle",
+ "pos": [
+ -48,
+ -48
+ ],
+ "width": 96.0,
+ "height": 96.0,
+ "line_type": "solid",
+ "line_thickness": 5.0,
+ "corner_radius": 8.0,
+ "line_color": "#00007fff",
+ "fill_color": "#ebebebff"
+ }
+ },
+ "port_positions": {
+ "23a8b215-7c2f-4ccf-b020-4d274d7a228f": [
+ -24,
+ 9
+ ],
+ "4c4771f8-22c8-4829-a060-913470e2b2fc": [
+ -24,
+ -26
+ ],
+ "a186a0e1-7aea-4fb6-871c-0ba70d78b8ac": [
+ 11,
+ -8
+ ]
+ }
}
}
},
@@ -1207,8 +1523,8 @@
48
],
"2b3844e8-1875-40d0-ae4e-7a14ca217bce": [
- -432,
- 236
+ -428,
+ 744
],
"bf08f999-bfe8-493a-9ad2-9c2965e17d6d": [
-504,
@@ -1233,6 +1549,14 @@
"8e8dca73-2d11-4631-9c24-5e1a4e3b2920": [
64,
128
+ ],
+ "ac5011ce-4659-4bdd-900a-63a1c779652c": [
+ -432,
+ 560
+ ],
+ "ec0599cb-b57b-4194-b166-526d5cd5382a": [
+ -120,
+ 656
]
},
"component_labels": {