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 @@ -80,6 +80,7 @@ false + @@ -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": {