From 1d535c1f5b837642b0f7646006e9b27ebec5c913 Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Tue, 18 Aug 2026 14:22:01 +0200 Subject: [PATCH] Small fixes and bigger library --- icons/signal_cosine_source.json | 79 +++++ icons/signal_sine_source.json | 97 ++++++ lib/bondgraph.beb | Bin 1977 -> 2715 bytes lib/signal.beb | Bin 677 -> 2170 bytes lib/signal_sources.beb | Bin 1352 -> 4499 bytes src/bedit_gui/commands/component_command.py | 32 +- .../controllers/document_tree_controller.py | 25 +- src/bedit_gui/controllers/log_controller.py | 3 + .../controllers/simulation_controller.py | 2 +- src/bedit_gui/documents/document.py | 10 + src/bedit_gui/models.py | 20 ++ src/bedit_gui/ui/forms/icon_editor_window.ui | 18 +- .../ui/forms/icon_editor_window_ui.py | 206 +++++++++++ src/bedit_gui/utils/icon.py | 8 +- src/bedit_gui/views/graph_editor_widget.py | 5 +- src/bedit_gui/views/icon_editor_window.py | 16 +- src/bedit_gui/views/icon_graphics_scene.py | 69 +++- src/bedit_gui/views/shape_options_dialog.py | 31 +- src/bedit_simulation/openmodelica.py | 12 +- src/bedit_simulation/simulation.py | 15 +- untitled.bedit.json | 328 +++++++++++++++++- 21 files changed, 935 insertions(+), 41 deletions(-) create mode 100644 icons/signal_cosine_source.json create mode 100644 icons/signal_sine_source.json create mode 100644 src/bedit_gui/ui/forms/icon_editor_window_ui.py 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 ef8b4c6c84f640cf2fedb1f090eb817031543646..c5a113782b47c17996d53c7c246fbf00bb274ae5 100644 GIT binary patch literal 2715 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 V^40HN{>y((eeloU{|~TR^U572M!^68 literal 1977 zcmV;q2S)fpMM3}o0004aoXuG~ZyeVZmb#?NPuReVKsIol`+kIQV#Jk;1cuZE5QN2j zoGVVS+}+H~>X9l|95@aTz#`yYa!7_0WlHiRwO|2hhyX^q?f3^$1PBlpDRO3ZNv=rB zTFC~bDad);=Q-c`zVDKM*vZ3k#n-(nP*%loaQ5ROO}ZjzV`(%*S{sNo42U;Y65)6X zMhi|IT^sln*!uZ!kS>>gw7gahhxPlDu7n8GW=1$Lj$KtC-kD4?W0V4u5FCB4?R)#J ztzu9^nSF$J*GI#$u5L`andPK$4idcqB0+ExoMA*rX$6D?#_Kzf_1%?mzb;1oA{O=aFSl`*WpS-n6~BemvuC#xRIw~ZP4inDX}IEx z0WRVyl%GdGu6!T+^hPCQC}9xc^XY8~sRubXvk%Ahr0b(ek&8qUlNcx_G;x7@!Wk&7 zWc1wI4-QwmQ5Ba5zTd8Qy{df;hil%z0iS8ncdq#U7^YDu!S^?RGxmcTd)-@L@aQ z!}--c+$mN@eOQ43?h>Eh`|R=OcF;}05t^sF31S~t+uT>5|roKFR$XK(d&6>XY_j0s(tt4xHRc%tGGc(5kW&FQVK?#XPB=H+=i%> z&wL8UkAKZh#3Q9qF$A!oQ6QduCKik!Mr+_EG0vHI9y~t!c6i*WioxYR^m2cAwO0&q zquFyQ5rmW(+-P==1mglx8I;qEQ%czrka#;kKMEf{5rs2Q%%Sji&yR_llg`07o%1+N zr@Gi3^?upCgxSWM3*DvTsQK^^Dj$8C8(3UqmpZR?M)(V{GC5a`SI$)zi|M=K5<&+n zyHj+EL5KaM9sbo8a$g+0=F``m{%|;&{HTN1eE;bj1)0s#+~7Q$Fi6e7h(@X@P01VL z6vZU5Ac&WkuI!}bDRU&hFeAtF$H0h1Ok>PhGlXCW9+RxFh&iW(fj3k{i}+X7Ioh1BzU%0}5_JbXc7O z3EV`BCCa?BFF-Ty&opC}+(tDLm&{U)h6Qq?ZfFhAVkr`2G$OLVxPd3B#<%b;r=%L+ zT+<(IH<@q60G1{{Y{X#3;?JT2e``cwrXFwXlRnIoerA$MhZu-XqN9d@W;&`wqQWxI z)>BW-jP&0=2~FUtI!Ei`e6B?fKMai@ z&If+DKrlwZ0`(b40!9&x6dj0nm>{PF9<8`_u+ME5@=3yF6>2v_F$ZRm9Cb3pT9=5% z14eKb;pzQLC|pW2=L9zdvO&G2C|r(8q`WA(z>6urFI@Sh5H5JSa5b52?By)io48pl zyV~FQq=-00a~pgGH;x|h*PGAPf@k#`8`ZKOLF-w(HQalNMCKEb91IDJp$GChXJ*i| zWcjD{zP}FTnYA`*HC(IzG8$H^dwc6wi&QV~zWL6X_S#s6;_`BR@Vp*}{b9NF3T>~P z5jXwxXrrhx&+*H(CgcZg+hK3NwbOzOM=#olEcmyB)vv!^?AX5ggT2L$k8=?MpJ`w! z6O|HT%hATwX-bm8Li5_-`OKeQgxCoYTHrL>Mre`acBi1xRQE^5^CJDuOB<$ja%OYenQ>w+#B4>rU z2pK0`&XHlLGjD`Rq5H%cQUsF)B&BG0LWI8h>L5hF`i??`$Co29TJZ4F#%K{IJ-2lm zF(8TK_tcy&JgReOz?z~@XnR<)6iyEu^f)*cG4}1o7xC4r-``uz9L_~31g3pzaDgF0 za0paxjijW!qd|iSNOY$cp|_7mXc4p8MraYI*p|;Cj%6-FnR#IW7_nZ7<}VJ3U_n~K zcw~|a9=vgHWX|_tqEx L`t$z*)`M;>)w0UJ diff --git a/lib/signal.beb b/lib/signal.beb index 2f85aa639f71716ef2062b4100cd887d12f8d326..a9971a5266465410b9b1c204163a9cbf77b1400a 100644 GIT binary patch literal 2170 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 literal 1352 zcmV-O1-JS_MM3}o0004aoYhy&ZWKolhLA)#af+1dO4(CTNB#dJl!$i-huo~{?rJ>B zcy}{1>%_T1NKkSjeglGSVFQ+;aYktwTO3OsNQ<(K___FA_!!M%14WN!1;)_$%UOda{N zH=cHrX?Jg9T6fi?u2R)){e71lZmRFs+Uf_~K700V#x^z8q@VxbU=!*o&TZ~A2=$$B zr!ea1s|P#NQPn*-m;gaW!rNY(4LKwy4KFNYA(@USScKphIaxRc;f=O87NGL(-12Sl zIbU641<&e|&*7U5C8naRGmL}fOuHBus34gP+6P}8M`q90NsiuwU@7tSyRAjA!dhO_SdvpKU?Qef=4ZavGkPa>kx@rSI z%NJ)u$U^HPGn2-8;!u$+3MZIKGRlxuEH*7Tw42CGcN~dy2;P9vECGEhI+0Kjd|0u0lM;-(#1<|By+8nDG0jvU+@4jxGz$|Nz6 zdj*ha8YynxdStya064F?H?KE%v!RnAO3aQGVH{~A(wZfb7I0UDEYT-iD!{GFuWL>2 zMdlevb+n*0A$aB?dgc@e5gZDP`WV%@^>*@fcA+O~RpxRt`0|Uv0@-l1Sah7PXYuSjav`r8OCUR$l5yZ!6-g!S**Ub_wxY_HFTp~S$Q1?F^5cb5eN zA`ZjgQgJy`gyGlS8n&=Gv(x*$-*X#AS8BXH8MoX2(6RenmAmzam%ctTKlawKy0YFq z8}HZEXc2_nc3fBaa4}x5(l;Y)+lQAvUd+3n#?1(uwFYuErN@KCM{ zS4uf3Rb3xF#Mqyu%!VbG08l&=ycbdE=2&Dk@5Dw?cNcO5Z`U?736mZj#aI-J!4bR9qzGVQ`A7anOl@_Lh;TXJs_R zq|_>$LI2BM7HV-;jVROwKH6OOk{d8Hh~v+ZZK?0os-H zBKkf#9r~W6nVZx1B)!KGeSaqUYVs(PSVJ-bR(f1?3sD2ABMKoNUw)#RQeG{JK)(6^9$d&~yJ;jZ9OWkacs+=-?O)q9H#y*Kj zk~kGpkik;Jd57b=zD$+;oSLGqAz7o21`GI^k0@!B)Za81W9*B`9$)FT5`XH z@WBd%b4v)1Ul9VQ9XH<6-zicyXd9^m;dB8WF(gfe8jb?-;VOj3i$J^@g#Rb;Nt${} K;(q}N;6wS#M3bHX 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": {