Compare commits
7 Commits
4591e6b7b0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 16d9cc7651 | |||
| 1d535c1f5b | |||
| 60a30d4ee3 | |||
| e88c58f095 | |||
| b1f453a6df | |||
| 8d37b2441a | |||
| a61c8e6003 |
37
AGENTS.md
37
AGENTS.md
@@ -60,7 +60,7 @@ Responsibilities:
|
||||
- `views/`: handwritten widget/window/graphics behavior.
|
||||
- `views/models/`: Qt item models used by views.
|
||||
- `services/`: non-visual functionality such as files, clipboard, logging, and settings.
|
||||
- `models.py`: GUI metadata persisted inside the core document, currently including icons and shapes.
|
||||
- `models.py`: GUI metadata persisted inside the core document, including icon, graph, and simulation databases.
|
||||
- `bedit_core`: domain model and serialization. It must never import from `bedit_gui`.
|
||||
|
||||
Preferred direction:
|
||||
@@ -136,10 +136,12 @@ Clipboard support is intentionally extensible:
|
||||
- `services/component_clipboard.py`: component payload serialization and ID remapping.
|
||||
- `controllers/clipboard_controller.py`: focus-based action router and handlers.
|
||||
|
||||
`ClipboardHandler` is the base implementation for future editors. Add a graph-editor handler later by subclassing it and registering that handler in `application.py`.
|
||||
`ClipboardHandler` is the base implementation for editor-specific routing. The document tree and graph editor have component handlers registered in `application.py`.
|
||||
|
||||
Text widgets use their native `copy()`, `cut()`, and `paste()` methods. Component clipboard data uses the custom BEdit MIME type and JSON; never use pickle or live object references.
|
||||
|
||||
The graph handler copies/cuts/deletes selected component items and deletes selected connection items. Paste targets the displayed graph and places new components at the mouse position, or at the viewport center when the mouse is outside the canvas.
|
||||
|
||||
## Icon editor conventions
|
||||
|
||||
- Icons are GUI metadata stored in `document.metadata["icon_database"]`.
|
||||
@@ -153,6 +155,23 @@ Text widgets use their native `copy()`, `cut()`, and `paste()` methods. Componen
|
||||
- Keep reusable widgets such as the RGBA color button independent of the icon editor.
|
||||
- Static icon previews belong in rendering utilities, not in the interactive editor scene.
|
||||
|
||||
## Graph editor conventions
|
||||
|
||||
- Graph GUI metadata is stored in `document.metadata["graph_database"]`. `GraphDatabase`, `Graph`, `GraphConnection`, and `GraphComponentLabel` live in `bedit_gui.models` and serialize through `to_data()`/`from_data()`.
|
||||
- Core graph topology and connections remain in `bedit_core.models`; positions, routed points, labels, and other presentation metadata belong in the GUI graph database.
|
||||
- Component positions are absolute scene positions. Connection metadata stores the full point list, including endpoints; only interior points are draggable corner items.
|
||||
- Component labels are visible by default, italic, and centered below the rendered icon. Their persisted position is relative to the icon’s bottom-center. Label visibility and completed label moves are undoable.
|
||||
- The graph editor has normal and connection modes. The toolbar actions are exclusive and Space toggles modes while focus is inside the editor.
|
||||
- Normal mode supports component and label dragging. Connection mode shows icon ports, prevents component/label movement, and uses two component clicks to choose a compatible port pair.
|
||||
- Signal connections require output-to-input. Signal outputs may fan out; signal inputs accept only one connection. A bond port accepts another connection only when its `multiplicity` is true. Bond domains must be compatible.
|
||||
- While choosing a connection, the first component is highlighted and a temporary dashed line follows the mouse. When several port pairs are possible, output-to-input choices are listed first.
|
||||
- Signal connections render with full arrows. Bond connections render with half arrows and a perpendicular causality tick. Connection endpoints are clipped to rendered icon bounds plus `CONNECTION_BOUNDING_BOX_SPACING`.
|
||||
- Components, connections, and connection points are separate graphics items. Connections are selectable/deletable; connection points are draggable and have their own delete context action.
|
||||
- Persistent canvas edits go through `Document` methods and graph-specific `QUndoCommand` classes. Incremental document signals must also update the editor’s cached `Graph`; otherwise rebuilding items during a mode switch can restore stale metadata.
|
||||
- `render_icon(..., render_ports=True)` is the single port-rendering path. Do not duplicate icon or port geometry in the graph editor.
|
||||
- Graph sizing and rendering constants, including `COMPONENT_LABEL_FONT_SIZE`, live near the top of `views/graph_editor_widget.py`.
|
||||
- Double-clicking a canvas component selects its document-tree row and opens its graph or equation editor. Canvas component context menus share the document-tree editing actions and add graph-only presentation actions such as Show Label.
|
||||
|
||||
## Actions and shortcut routing
|
||||
|
||||
- Put visible actions in Designer menus/toolbars.
|
||||
@@ -161,6 +180,12 @@ Text widgets use their native `copy()`, `cut()`, and `paste()` methods. Componen
|
||||
- Scope destructive shortcuts to the relevant widget where appropriate.
|
||||
- Always guard the operation itself even when an action is disabled for presentation.
|
||||
|
||||
## Application settings
|
||||
|
||||
- `ApplicationSettings` is the typed `QSettings` facade for BEdit preferences. The current settings include log level, graph snap-to-grid size, and ordered library paths under `libraries/paths`.
|
||||
- The Settings dialog edits library paths locally until OK is accepted. It supports BEdit `.bedit.json`/`.json` and `.beb` files, directories, duplicate suppression, extended selection, and list-focused Delete-key removal.
|
||||
- Add settings behavior in the handwritten dialog/controller/service modules, never in generated UI Python.
|
||||
|
||||
## Simulator application
|
||||
|
||||
- `bedit_gui/simulation_application.py` is the composition root for the separate `bedit-sim` Qt application.
|
||||
@@ -173,6 +198,14 @@ Text widgets use their native `copy()`, `cut()`, and `paste()` methods. Componen
|
||||
- BEdit launches the simulator as a separate process. Compile/Open Simulation integration may transfer a `.bes` file to that process.
|
||||
- Keep simulator file workflows in their own services/controllers rather than adding them to `MainWindow` or the editor document controller.
|
||||
- Keep compiled-executable launching, time-window progression, result loading, and cancellation inside `bedit_simulation`. GUI controllers may schedule backend calls and present state, but must not execute or manage simulation binaries themselves.
|
||||
- The BEdit Compile action performs bond-graph causality inference before Modelica compilation. Inference runs on a deep copy first, then inferred `causality`/`undesired` state is applied to the open document with an undoable command.
|
||||
- OpenModelica compilation runs `checkModel(...)` before `buildModel(...)`; the equation/variable summary flows through `ModelBuildResult.output` and is shown in the application log.
|
||||
|
||||
### Causality inference caveats
|
||||
|
||||
- `_CausalityEngine.inference()` clears every flattened bond’s causality to `NONE` before inference; it does not continue from saved causalities. It currently does not clear old `undesired` flags.
|
||||
- Preferred causalities are assigned before all junction constraints are resolved, and the engine has no backtracking to relax a preferred assignment. Some soft-preference conflicts therefore raise a junction error instead of marking a bond undesired.
|
||||
- `propagate_to_neighbor()` currently selects `connection.target` in both branches. When propagation starts at a target component, it should traverse to the source; account for this known bug when diagnosing inference behavior.
|
||||
|
||||
## Verification
|
||||
|
||||
|
||||
BIN
examples/BondGraphs.beb
Normal file
BIN
examples/BondGraphs.beb
Normal file
Binary file not shown.
25
icons/signal_base.json
Normal file
25
icons/signal_base.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"shapes": {
|
||||
"14409738-45af-40c5-9c9e-827218d54a95": {
|
||||
"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": {
|
||||
"d5681d1d-70ee-4623-a41a-5a572e46d42c": [
|
||||
-8,
|
||||
-8
|
||||
]
|
||||
}
|
||||
}
|
||||
79
icons/signal_cosine_source.json
Normal file
79
icons/signal_cosine_source.json
Normal file
@@ -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]
|
||||
}
|
||||
}
|
||||
97
icons/signal_sine_source.json
Normal file
97
icons/signal_sine_source.json
Normal file
@@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
55
icons/signal_source_base.json
Normal file
55
icons/signal_source_base.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"port_positions": {
|
||||
"37eb6e52-6e89-4e09-8923-43c6230f5486": [
|
||||
-8,
|
||||
-8
|
||||
]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"shapes": {
|
||||
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||
"layer": 0,
|
||||
"type": "text",
|
||||
"pos": [
|
||||
-32,
|
||||
-32
|
||||
],
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"color": "#000000ff",
|
||||
"bold": true,
|
||||
"italic": false,
|
||||
"size": 64.0,
|
||||
"text": "0"
|
||||
}
|
||||
},
|
||||
"port_positions": {
|
||||
"497b1f74-1186-471f-976a-36b07a451caf": [
|
||||
-8,
|
||||
-8
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"shapes": {
|
||||
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||
"layer": 0,
|
||||
"type": "text",
|
||||
"pos": [
|
||||
-32,
|
||||
-32
|
||||
],
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"color": "#000000ff",
|
||||
"bold": true,
|
||||
"italic": false,
|
||||
"size": 64.0,
|
||||
"text": "1"
|
||||
}
|
||||
},
|
||||
"port_positions": {
|
||||
"4306701a-6b1b-4d19-b8ff-45dbfa04f2d3": [
|
||||
-8,
|
||||
-8
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"shapes": {
|
||||
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||
"layer": 0,
|
||||
"type": "text",
|
||||
"pos": [
|
||||
-32,
|
||||
-32
|
||||
],
|
||||
"width": 64.0,
|
||||
"height": 64.0,
|
||||
"color": "#000000ff",
|
||||
"bold": true,
|
||||
"italic": false,
|
||||
"size": 64.0,
|
||||
"text": "C"
|
||||
}
|
||||
},
|
||||
"port_positions": {
|
||||
"c87881f1-23b4-4a69-b586-8e3c7bf6e21e": [
|
||||
-8,
|
||||
-8
|
||||
],
|
||||
"c62de8eb-e13b-4849-bea5-c8cc5332269e": [
|
||||
16,
|
||||
-32
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"shapes": {
|
||||
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||
"layer": 0,
|
||||
"type": "text",
|
||||
"pos": [
|
||||
-32,
|
||||
-32
|
||||
],
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"color": "#000000ff",
|
||||
"bold": true,
|
||||
"italic": false,
|
||||
"size": 64.0,
|
||||
"text": "I"
|
||||
}
|
||||
},
|
||||
"port_positions": {
|
||||
"0b9036b1-e4e4-437e-8c35-f5bb391b6cbd": [
|
||||
-8,
|
||||
-8
|
||||
],
|
||||
"fb25f35d-4c0c-4cfa-92d4-1a18a71e2c11": [
|
||||
16,
|
||||
-32
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"shapes": {
|
||||
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||
"layer": 0,
|
||||
"type": "text",
|
||||
"pos": [
|
||||
-32,
|
||||
-32
|
||||
],
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"color": "#000000ff",
|
||||
"bold": true,
|
||||
"italic": false,
|
||||
"size": 64.0,
|
||||
"text": "R"
|
||||
}
|
||||
},
|
||||
"port_positions": {
|
||||
"0ac7d4ea-77f7-4c0d-8e37-406ef66e4740": [
|
||||
-8,
|
||||
-8
|
||||
],
|
||||
"d1f5aab2-7bff-4b1e-8697-98fb6c3d8f02": [
|
||||
16,
|
||||
-32
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"shapes": {
|
||||
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||
"layer": 0,
|
||||
"type": "text",
|
||||
"pos": [
|
||||
-48,
|
||||
-32
|
||||
],
|
||||
"width": 96.0,
|
||||
"height": 64.0,
|
||||
"color": "#000000ff",
|
||||
"bold": true,
|
||||
"italic": false,
|
||||
"size": 64.0,
|
||||
"text": "Se"
|
||||
}
|
||||
},
|
||||
"port_positions": {
|
||||
"5b6a8a0c-0875-402c-bd74-11d086aac372": [
|
||||
-8,
|
||||
-8
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"shapes": {
|
||||
"f1a38ee1-8a53-454d-a398-12c14032ba79": {
|
||||
"layer": 0,
|
||||
"type": "text",
|
||||
"pos": [
|
||||
-48,
|
||||
-32
|
||||
],
|
||||
"width": 96.0,
|
||||
"height": 64.0,
|
||||
"color": "#000000ff",
|
||||
"bold": true,
|
||||
"italic": false,
|
||||
"size": 64.0,
|
||||
"text": "Sf"
|
||||
}
|
||||
},
|
||||
"port_positions": {
|
||||
"856b4152-e18f-46d7-889d-626cb98474aa": [
|
||||
-8,
|
||||
-8
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
lib/signal.beb
Normal file
BIN
lib/signal.beb
Normal file
Binary file not shown.
BIN
lib/signal_sources.beb
Normal file
BIN
lib/signal_sources.beb
Normal file
Binary file not shown.
@@ -8,6 +8,7 @@ from PySide6.QtWidgets import QApplication, QMessageBox
|
||||
from bedit_gui.controllers.clipboard_controller import ClipboardController, DocumentTreeClipboardHandler, GraphEditorClipboardHandler, TextClipboardHandler
|
||||
from bedit_gui.controllers.document_controller import DocumentController
|
||||
from bedit_gui.controllers.log_controller import LogController
|
||||
from bedit_gui.controllers.library_controller import LibraryController
|
||||
from bedit_gui.controllers.settings_controller import SettingsController
|
||||
from bedit_gui.controllers.simulation_settings_controller import SimulationSettingsController
|
||||
from bedit_gui.controllers.simulation_controller import SimulationController
|
||||
@@ -52,12 +53,14 @@ def main() -> int:
|
||||
|
||||
LogController(window, settings.log_level)
|
||||
DocumentController(document, window)
|
||||
SettingsController(window, settings)
|
||||
settings_controller = SettingsController(window, settings)
|
||||
SimulationSettingsController(document, window)
|
||||
SimulationController(document, window)
|
||||
UndoController(document, window)
|
||||
ViewMenuController(window)
|
||||
document_tree_controller = DocumentTreeController(document, window)
|
||||
library_controller = LibraryController(window, settings)
|
||||
settings_controller.library_paths_changed.connect(library_controller.reload)
|
||||
clipboard = ClipboardService(app)
|
||||
ClipboardController(window, clipboard, [TextClipboardHandler(clipboard), DocumentTreeClipboardHandler(document, window.ui.documentTree, document_tree_controller.model, clipboard), GraphEditorClipboardHandler(document, window.graph_editor, clipboard)])
|
||||
|
||||
|
||||
@@ -1,44 +1,52 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from PySide6.QtGui import QUndoCommand
|
||||
|
||||
from bedit_core.models import Component, ComponentID, Connection, ConnectionID, EquationImplementation, Graph, GraphImplementation, Interface
|
||||
from bedit_gui.models import Icon
|
||||
from bedit_core.models import Component, ComponentID, Connection, ConnectionID, EquationImplementation, Graph, GraphImplementation, Interface, PortID
|
||||
from bedit_gui.models import Icon, PortMetadata
|
||||
|
||||
|
||||
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")
|
||||
self.document = document
|
||||
if isinstance(parent, Component):
|
||||
if not isinstance(parent.implementation, GraphImplementation):
|
||||
raise TypeError("parent component must have a graph implementation")
|
||||
self.document = document
|
||||
self.graph = parent.implementation.graph
|
||||
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")
|
||||
self.document = document
|
||||
if isinstance(parent, Component):
|
||||
if not isinstance(parent.implementation, GraphImplementation):
|
||||
raise TypeError("parent component must have a graph implementation")
|
||||
self.document = document
|
||||
self.graph = parent.implementation.graph
|
||||
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)
|
||||
|
||||
|
||||
@@ -56,10 +64,16 @@ class DeleteComponent(QUndoCommand):
|
||||
if connection.source in port_ids or connection.target in port_ids:
|
||||
self.connections.append((index, connection_id, connection))
|
||||
self.icons = {}
|
||||
self.port_metadata = {}
|
||||
for component_id in self._component_ids(self.component_id, component):
|
||||
icon = document.stored_component_icon(component_id)
|
||||
if icon is not None:
|
||||
self.icons[component_id] = icon
|
||||
database = document._port_metadata_database(False)
|
||||
if database is not None:
|
||||
for port_id in self._port_ids(component):
|
||||
if port_id in database.ports:
|
||||
self.port_metadata[port_id] = deepcopy(database.ports[port_id])
|
||||
|
||||
def redo(self) -> None:
|
||||
if self.parent_graph is not None:
|
||||
@@ -69,6 +83,11 @@ class DeleteComponent(QUndoCommand):
|
||||
self.document.model_changed.emit(self.document.model)
|
||||
for component_id in self.icons:
|
||||
self.document._set_component_icon(component_id, None)
|
||||
if self.port_metadata:
|
||||
database = self.document.port_metadata_database()
|
||||
for port_id in self.port_metadata:
|
||||
database.ports.pop(port_id, None)
|
||||
self.document._set_port_metadata_database(database)
|
||||
|
||||
def undo(self) -> None:
|
||||
self._restore_item(self.components, self.component_id, self.component, self.component_index)
|
||||
@@ -78,6 +97,10 @@ class DeleteComponent(QUndoCommand):
|
||||
self.document.model_changed.emit(self.document.model)
|
||||
for component_id, icon in self.icons.items():
|
||||
self.document._set_component_icon(component_id, icon)
|
||||
if self.port_metadata:
|
||||
database = self.document.port_metadata_database()
|
||||
database.ports.update(deepcopy(self.port_metadata))
|
||||
self.document._set_port_metadata_database(database)
|
||||
|
||||
@classmethod
|
||||
def _find_component(cls, components: dict[ComponentID, Component], target: Component, parent_graph: Graph | None = None) -> tuple[dict[ComponentID, Component], ComponentID, Graph | None]:
|
||||
@@ -99,6 +122,14 @@ class DeleteComponent(QUndoCommand):
|
||||
component_ids.extend(cls._component_ids(child_id, child))
|
||||
return component_ids
|
||||
|
||||
@classmethod
|
||||
def _port_ids(cls, component: Component) -> list[PortID]:
|
||||
port_ids = list(component.interface.ports)
|
||||
if isinstance(component.implementation, GraphImplementation):
|
||||
for child in component.implementation.graph.components.values():
|
||||
port_ids.extend(cls._port_ids(child))
|
||||
return port_ids
|
||||
|
||||
@staticmethod
|
||||
def _restore_item(items: dict, item_id: object, item: object, index: int) -> None:
|
||||
values = list(items.items())
|
||||
@@ -108,7 +139,7 @@ class DeleteComponent(QUndoCommand):
|
||||
|
||||
|
||||
class PasteComponents(QUndoCommand):
|
||||
def __init__(self, document: object, target: dict[ComponentID, Component], components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], graph_id: ComponentID | None = None, positions: dict[ComponentID, tuple[int, int]] | None = None) -> None:
|
||||
def __init__(self, document: object, target: dict[ComponentID, Component], components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], graph_id: ComponentID | None = None, positions: dict[ComponentID, tuple[int, int]] | None = None, port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
|
||||
super().__init__("Paste components")
|
||||
self.document = document
|
||||
self.target = target
|
||||
@@ -116,6 +147,7 @@ class PasteComponents(QUndoCommand):
|
||||
self.icons = icons
|
||||
self.graph_id = graph_id
|
||||
self.positions = positions or {}
|
||||
self.port_metadata = deepcopy(port_metadata or {})
|
||||
|
||||
def redo(self) -> None:
|
||||
self.target.update(self.components)
|
||||
@@ -125,6 +157,10 @@ class PasteComponents(QUndoCommand):
|
||||
self.document.model_changed.emit(self.document.model)
|
||||
for component_id, icon in self.icons.items():
|
||||
self.document._set_component_icon(component_id, icon)
|
||||
if self.port_metadata:
|
||||
database = self.document.port_metadata_database()
|
||||
database.ports.update(deepcopy(self.port_metadata))
|
||||
self.document._set_port_metadata_database(database)
|
||||
|
||||
def undo(self) -> None:
|
||||
for component_id in self.components:
|
||||
@@ -135,3 +171,8 @@ class PasteComponents(QUndoCommand):
|
||||
self.document.model_changed.emit(self.document.model)
|
||||
for component_id in self.icons:
|
||||
self.document._set_component_icon(component_id, None)
|
||||
if self.port_metadata:
|
||||
database = self.document.port_metadata_database()
|
||||
for port_id in self.port_metadata:
|
||||
database.ports.pop(port_id, None)
|
||||
self.document._set_port_metadata_database(database)
|
||||
|
||||
24
src/bedit_gui/commands/graph_label_command.py
Normal file
24
src/bedit_gui/commands/graph_label_command.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from PySide6.QtGui import QUndoCommand
|
||||
|
||||
from bedit_core.models import ComponentID
|
||||
from bedit_gui.models import GraphComponentLabel
|
||||
|
||||
|
||||
class ChangeGraphComponentLabelCommand(QUndoCommand):
|
||||
def __init__(self, document: object, graph_id: ComponentID, component_id: ComponentID, label: GraphComponentLabel, text: str) -> None:
|
||||
super().__init__(text)
|
||||
self.document = document
|
||||
self.graph_id = graph_id
|
||||
self.component_id = component_id
|
||||
database = document._graph_database(False)
|
||||
graph = database.graphs.get(graph_id) if database is not None else None
|
||||
self.old_label = deepcopy(graph.component_labels.get(component_id)) if graph is not None and component_id in graph.component_labels else None
|
||||
self.new_label = deepcopy(label)
|
||||
|
||||
def redo(self) -> None:
|
||||
self.document._set_graph_component_label(self.graph_id, self.component_id, self.new_label)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.document._set_graph_component_label(self.graph_id, self.component_id, self.old_label)
|
||||
19
src/bedit_gui/commands/port_metadata_command.py
Normal file
19
src/bedit_gui/commands/port_metadata_command.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from PySide6.QtGui import QUndoCommand
|
||||
|
||||
from bedit_gui.models import PortMetadataDatabase
|
||||
|
||||
|
||||
class ChangePortMetadataDatabaseCommand(QUndoCommand):
|
||||
def __init__(self, document: object, database: PortMetadataDatabase) -> None:
|
||||
super().__init__("Change port metadata")
|
||||
self.document = document
|
||||
self.old_database = document.stored_port_metadata_database()
|
||||
self.new_database = deepcopy(database)
|
||||
|
||||
def redo(self) -> None:
|
||||
self.document._set_port_metadata_database(self.new_database)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.document._set_port_metadata_database(self.old_database)
|
||||
@@ -133,11 +133,11 @@ class DocumentTreeClipboardHandler(ClipboardHandler):
|
||||
if target is None or payload is None:
|
||||
return
|
||||
try:
|
||||
components, icons = import_components(payload)
|
||||
components, icons, port_metadata = import_components(payload)
|
||||
except (TypeError, ValueError) as exc:
|
||||
QMessageBox.critical(self.tree, "Could not paste components", str(exc))
|
||||
return
|
||||
self.document.paste_components(target, components, icons)
|
||||
self.document.paste_components(target, components, icons, port_metadata)
|
||||
|
||||
def delete(self) -> None:
|
||||
self.document.delete_components(self._selected_components())
|
||||
@@ -182,6 +182,7 @@ class GraphEditorClipboardHandler(ClipboardHandler):
|
||||
self.editor = editor
|
||||
self.clipboard = clipboard
|
||||
editor.scene.selectionChanged.connect(self.availability_changed)
|
||||
editor.component_drop_requested.connect(self.drop_components)
|
||||
|
||||
def owns_focus(self, widget: QWidget) -> bool:
|
||||
return widget is self.editor or self.editor.isAncestorOf(widget)
|
||||
@@ -214,15 +215,22 @@ class GraphEditorClipboardHandler(ClipboardHandler):
|
||||
payload = self.clipboard.get_json(ClipboardService.COMPONENTS_MIME)
|
||||
if graph_component is None or not isinstance(graph_component.implementation, GraphImplementation) or payload is None:
|
||||
return
|
||||
x, y = self.editor.paste_position()
|
||||
self._paste_payload(graph_component, payload, (x, y))
|
||||
|
||||
def drop_components(self, graph_component: Component, payload: dict, position: tuple[int, int]) -> None:
|
||||
self._paste_payload(graph_component, payload, position)
|
||||
|
||||
def _paste_payload(self, graph_component: Component, payload: dict, position: tuple[int, int]) -> None:
|
||||
try:
|
||||
components, icons = import_components(payload)
|
||||
components, icons, port_metadata = import_components(payload)
|
||||
except (TypeError, ValueError) as exc:
|
||||
QMessageBox.critical(self.editor, "Could not paste components", str(exc))
|
||||
return
|
||||
x, y = self.editor.paste_position()
|
||||
x, y = position
|
||||
spacing = self.editor.snap_to_grid_size * 4
|
||||
positions = {component_id: (x + index * spacing, y + index * spacing) for index, component_id in enumerate(components)}
|
||||
self.document.paste_graph_components(graph_component, components, icons, positions)
|
||||
self.document.paste_graph_components(graph_component, components, icons, positions, port_metadata)
|
||||
|
||||
def delete(self) -> None:
|
||||
components = self._selected_components()
|
||||
|
||||
@@ -9,7 +9,7 @@ from PySide6.QtWidgets import QAbstractItemView, QDialog, QHeaderView, QMenu
|
||||
from bedit_core.models import Component, ComponentID, ConnectionID, EquationImplementation, GraphImplementation, Port, PortID, Parameter, ParameterID
|
||||
from bedit_core.models import Document as CoreDocument
|
||||
from bedit_gui.documents import Document
|
||||
from bedit_gui.models import Graph, Icon
|
||||
from bedit_gui.models import Graph, GraphComponentLabel, Icon, PortMetadata
|
||||
from bedit_gui.views.dialogs.interface_editor_dialog import InterfaceEditorDialog
|
||||
from bedit_gui.views.dialogs.param_editor_dialog import ParamEditorDialog
|
||||
from bedit_gui.views.icon_editor_window import IconEditorWindow
|
||||
@@ -17,11 +17,12 @@ 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: ...
|
||||
def ports(self) -> dict[PortID, Port]: ...
|
||||
def port_metadata(self) -> dict[PortID, PortMetadata]: ...
|
||||
|
||||
class ParamEditorLike(Protocol):
|
||||
def exec(self) -> int: ...
|
||||
@@ -29,7 +30,7 @@ class ParamEditorLike(Protocol):
|
||||
|
||||
|
||||
InterfaceEditorFactory = Callable[
|
||||
[dict[PortID, Port], MainWindow],
|
||||
[dict[PortID, Port], dict[PortID, PortMetadata], MainWindow],
|
||||
InterfaceEditorLike,
|
||||
]
|
||||
|
||||
@@ -60,14 +61,18 @@ class DocumentTreeController(QObject):
|
||||
window.ui.documentTree.setModel(self.model)
|
||||
window.ui.documentTree.selectionModel().selectionChanged.connect(self._selection_changed)
|
||||
window.equation_editor.equation_text_change_requested.connect(self.document.update_component_equation_text)
|
||||
window.equation_editor.port_metadata_change_requested.connect(self._change_equation_port_metadata)
|
||||
document.model_changed.connect(self._on_document_changed)
|
||||
document.icon_changed.connect(self._on_icon_changed)
|
||||
document.port_metadata_database_changed.connect(self._on_port_metadata_database_changed)
|
||||
document.graph_component_position_changed.connect(self._on_graph_component_position_changed)
|
||||
document.graph_component_label_changed.connect(self._on_graph_component_label_changed)
|
||||
document.graph_connection_points_changed.connect(self._on_graph_connection_points_changed)
|
||||
document.equation_text_changed.connect(self._on_equation_text_changed)
|
||||
self.model.rename_document_requested.connect(self.document.rename)
|
||||
self.model.rename_component_requested.connect(self.document.rename_component)
|
||||
window.graph_editor.component_move_requested.connect(self.document.move_graph_component)
|
||||
window.graph_editor.component_moves_requested.connect(self.document.move_graph_components)
|
||||
window.graph_editor.component_label_move_requested.connect(self.document.move_graph_component_label)
|
||||
window.graph_editor.component_context_menu_requested.connect(self._show_graph_component_context_menu)
|
||||
window.graph_editor.component_open_requested.connect(self._open_graph_component)
|
||||
window.graph_editor.connection_points_change_requested.connect(self.document.change_graph_connection_points)
|
||||
@@ -82,11 +87,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)
|
||||
|
||||
@@ -123,7 +128,8 @@ class DocumentTreeController(QObject):
|
||||
|
||||
def _show_component(self, component: Component | None) -> None:
|
||||
if component is not None and isinstance(component.implementation, EquationImplementation):
|
||||
self.window.equation_editor.set_component(component)
|
||||
port_metadata = {port_id: self.document.port_metadata(port_id) for port_id in component.interface.ports}
|
||||
self.window.equation_editor.set_component(component, port_metadata)
|
||||
self.window.equation_editor.show()
|
||||
else:
|
||||
self.window.equation_editor.set_component(None)
|
||||
@@ -131,7 +137,8 @@ class DocumentTreeController(QObject):
|
||||
if component is not None and isinstance(component.implementation, GraphImplementation):
|
||||
graph = self.document.graph_database().graphs.get(self.document.component_id(component), Graph())
|
||||
icons = {component_id: self.document.component_icon(component_id) for component_id in component.implementation.graph.components}
|
||||
self.window.graph_editor.set_component(component, graph, icons)
|
||||
port_metadata = {port_id: self.document.port_metadata(port_id) for child in component.implementation.graph.components.values() for port_id in child.interface.ports}
|
||||
self.window.graph_editor.set_component(component, graph, icons, port_metadata)
|
||||
self.window.graph_editor.show()
|
||||
else:
|
||||
self.window.graph_editor.set_component(None)
|
||||
@@ -150,11 +157,27 @@ class DocumentTreeController(QObject):
|
||||
if graph_component is not None and isinstance(graph_component.implementation, GraphImplementation) and component_id in graph_component.implementation.graph.components:
|
||||
self._show_component(graph_component)
|
||||
|
||||
def _on_port_metadata_database_changed(self, _database: object) -> None:
|
||||
graph_component = self.window.graph_editor.component()
|
||||
if graph_component is not None:
|
||||
self._show_component(graph_component)
|
||||
equation_component = self.window.equation_editor.component()
|
||||
if equation_component is not None:
|
||||
self.window.equation_editor.refresh_port_metadata({port_id: self.document.port_metadata(port_id) for port_id in equation_component.interface.ports})
|
||||
|
||||
def _change_equation_port_metadata(self, component: Component, port_metadata: dict[PortID, PortMetadata]) -> None:
|
||||
self.document.update_component_ports(component, component.interface.ports, port_metadata)
|
||||
|
||||
def _on_graph_component_position_changed(self, graph_id: ComponentID, component_id: ComponentID, position: tuple[int, int] | None) -> None:
|
||||
graph_component = self.window.graph_editor.component()
|
||||
if graph_component is not None and self.document.component_id(graph_component) == graph_id:
|
||||
self.window.graph_editor.set_component_position(component_id, position)
|
||||
|
||||
def _on_graph_component_label_changed(self, graph_id: ComponentID, component_id: ComponentID, label: object) -> None:
|
||||
graph_component = self.window.graph_editor.component()
|
||||
if graph_component is not None and self.document.component_id(graph_component) == graph_id:
|
||||
self.window.graph_editor.set_component_label(component_id, label if isinstance(label, GraphComponentLabel) else None)
|
||||
|
||||
def _on_graph_connection_points_changed(self, graph_id: ComponentID, connection_id: ConnectionID, points: list[tuple[int, int]] | None) -> None:
|
||||
graph_component = self.window.graph_editor.component()
|
||||
if graph_component is not None and self.document.component_id(graph_component) == graph_id:
|
||||
@@ -168,16 +191,27 @@ 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)
|
||||
if component is not None:
|
||||
self._show_component_context_menu(component, global_position)
|
||||
self._show_component_context_menu(component, global_position, component_id)
|
||||
|
||||
def _open_graph_component(self, component_id: ComponentID) -> None:
|
||||
index = self.model.component_index(component_id)
|
||||
@@ -185,11 +219,16 @@ class DocumentTreeController(QObject):
|
||||
self.window.ui.documentTree.selectionModel().setCurrentIndex(index, QItemSelectionModel.SelectionFlag.ClearAndSelect | QItemSelectionModel.SelectionFlag.Rows)
|
||||
self.window.ui.documentTree.scrollTo(index)
|
||||
|
||||
def _show_component_context_menu(self, component: Component, global_position: QPoint) -> None:
|
||||
def _show_component_context_menu(self, component: Component, global_position: QPoint, graph_component_id: ComponentID | None = None) -> None:
|
||||
menu = QMenu(self.window.ui.documentTree)
|
||||
edit_interface = menu.addAction("Edit Interface")
|
||||
edit_params = menu.addAction("Edit Parameters")
|
||||
edit_icon = menu.addAction("Edit Icon")
|
||||
show_label = None
|
||||
if graph_component_id is not None:
|
||||
show_label = menu.addAction("Show Label")
|
||||
show_label.setCheckable(True)
|
||||
show_label.setChecked(self.window.graph_editor.component_label_visible(graph_component_id))
|
||||
menu.addSeparator()
|
||||
add_graph_component = None
|
||||
add_equation_component = None
|
||||
@@ -205,6 +244,10 @@ class DocumentTreeController(QObject):
|
||||
self._edit_params(component)
|
||||
elif selected is edit_icon:
|
||||
self._edit_icon(component)
|
||||
elif show_label is not None and selected is show_label:
|
||||
graph_component = self.window.graph_editor.component()
|
||||
if graph_component is not None:
|
||||
self.document.set_graph_component_label_visible(graph_component, graph_component_id, show_label.isChecked())
|
||||
elif add_graph_component is not None and selected is add_graph_component:
|
||||
self._add_graph_component(component)
|
||||
elif add_equation_component is not None and selected is add_equation_component:
|
||||
@@ -215,10 +258,11 @@ class DocumentTreeController(QObject):
|
||||
def _edit_interface(self, component: Component) -> None:
|
||||
dialog = self.interface_editor_factory(
|
||||
component.interface.ports,
|
||||
{port_id: self.document.port_metadata(port_id) for port_id in component.interface.ports},
|
||||
self.window,
|
||||
)
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
self.document.update_component_ports(component, dialog.ports())
|
||||
self.document.update_component_ports(component, dialog.ports(), dialog.port_metadata())
|
||||
|
||||
def _edit_params(self, component: Component) -> None:
|
||||
dialog = self.param_editor_factory(
|
||||
|
||||
81
src/bedit_gui/controllers/library_controller.py
Normal file
81
src/bedit_gui/controllers/library_controller.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from PySide6.QtCore import QObject, QSize, Qt
|
||||
from PySide6.QtWidgets import QAbstractItemView, QHeaderView
|
||||
|
||||
from bedit_core.models import Component, ComponentID, GraphImplementation, PortID
|
||||
from bedit_gui.models import Icon, IconDatabase, PortMetadata, PortMetadataDatabase
|
||||
from bedit_gui.services.application_settings import ApplicationSettings
|
||||
from bedit_gui.services.component_clipboard import export_component_data
|
||||
from bedit_gui.services.libraries import load_library_documents
|
||||
from bedit_gui.utils.icon import render_fitted_icon
|
||||
from bedit_gui.views.main_window import MainWindow
|
||||
from bedit_gui.views.models.library_tree_model import LibraryTreeModel
|
||||
|
||||
ICON_SIZE = QSize(32, 32)
|
||||
|
||||
|
||||
class LibraryController(QObject):
|
||||
def __init__(self, window: MainWindow, settings: ApplicationSettings) -> None:
|
||||
super().__init__(window)
|
||||
self.window = window
|
||||
self.settings = settings
|
||||
self._component_sources: dict[int, tuple[ComponentID, dict[ComponentID, Icon], dict[PortID, PortMetadata]]] = {}
|
||||
self.model = LibraryTreeModel(self._component_payload)
|
||||
|
||||
tree = window.ui.libraryTree
|
||||
tree.setModel(self.model)
|
||||
tree.setHeaderHidden(True)
|
||||
tree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
tree.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
tree.setDragEnabled(True)
|
||||
tree.setDragDropMode(QAbstractItemView.DragDropMode.DragOnly)
|
||||
tree.setDefaultDropAction(Qt.DropAction.CopyAction)
|
||||
tree.setIconSize(QSize(48, 48))
|
||||
tree.header().setStretchLastSection(False)
|
||||
tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||
tree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||
tree.setColumnWidth(1, 56)
|
||||
|
||||
window.ui.actionReload_Libraries.triggered.connect(self.reload)
|
||||
self.reload()
|
||||
|
||||
def reload(self) -> None:
|
||||
libraries = load_library_documents(self.settings.library_paths)
|
||||
self._component_sources = {}
|
||||
for library in libraries:
|
||||
database = library.document.metadata.get("icon_database") if library.document.metadata is not None else None
|
||||
icons = database.icons if isinstance(database, IconDatabase) else {}
|
||||
metadata_database = library.document.metadata.get("port_metadata_database") if library.document.metadata is not None else None
|
||||
port_metadata = metadata_database.ports if isinstance(metadata_database, PortMetadataDatabase) else {}
|
||||
self._collect_component_sources(library.document.root, icons, port_metadata)
|
||||
self.model.set_documents([library.document for library in libraries])
|
||||
for library in libraries:
|
||||
database = library.document.metadata.get("icon_database") if library.document.metadata is not None else None
|
||||
icons = database.icons if isinstance(database, IconDatabase) else {}
|
||||
self._set_component_icons(library.document.root, icons)
|
||||
self.window.ui.libraryTree.expandAll()
|
||||
|
||||
def _component_payload(self, components: list[Component]) -> dict:
|
||||
roots = {}
|
||||
icons = {}
|
||||
port_metadata = {}
|
||||
for component in components:
|
||||
source = self._component_sources.get(id(component))
|
||||
if source is None:
|
||||
continue
|
||||
component_id, source_icons, source_port_metadata = source
|
||||
roots[component_id] = component
|
||||
icons.update(source_icons)
|
||||
port_metadata.update(source_port_metadata)
|
||||
return export_component_data(roots, icons, port_metadata)
|
||||
|
||||
def _collect_component_sources(self, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], port_metadata: dict[PortID, PortMetadata]) -> None:
|
||||
for component_id, component in components.items():
|
||||
self._component_sources[id(component)] = (component_id, icons, port_metadata)
|
||||
if isinstance(component.implementation, GraphImplementation):
|
||||
self._collect_component_sources(component.implementation.graph.components, icons, port_metadata)
|
||||
|
||||
def _set_component_icons(self, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> None:
|
||||
for component_id, component in components.items():
|
||||
self.model.set_component_icon(component_id, render_fitted_icon(icons.get(component_id, Icon()), component.interface.ports, ICON_SIZE))
|
||||
if isinstance(component.implementation, GraphImplementation):
|
||||
self._set_component_icons(component.implementation.graph.components, icons)
|
||||
@@ -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)
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from typing import Protocol
|
||||
|
||||
from PySide6.QtCore import QObject
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
from PySide6.QtWidgets import QDialog
|
||||
|
||||
from bedit_gui.services.application_logging import get_logger, set_log_level
|
||||
@@ -21,15 +21,20 @@ class SettingsDialogLike(Protocol):
|
||||
@property
|
||||
def snap_to_grid_size(self) -> int: ...
|
||||
|
||||
@property
|
||||
def library_paths(self) -> list[str]: ...
|
||||
|
||||
def exec(self) -> int: ...
|
||||
|
||||
|
||||
SettingsDialogFactory = Callable[[int, int, MainWindow], SettingsDialogLike]
|
||||
SettingsDialogFactory = Callable[[int, int, list[str], MainWindow], SettingsDialogLike]
|
||||
|
||||
|
||||
class SettingsController(QObject):
|
||||
"""Opens the settings dialog and applies accepted preferences."""
|
||||
|
||||
library_paths_changed = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
window: MainWindow,
|
||||
@@ -44,12 +49,16 @@ class SettingsController(QObject):
|
||||
window.ui.actionSettings.triggered.connect(self.open_settings)
|
||||
|
||||
def open_settings(self) -> None:
|
||||
dialog = self.dialog_factory(self.settings.log_level, self.settings.snap_to_grid_size, self.window)
|
||||
dialog = self.dialog_factory(self.settings.log_level, self.settings.snap_to_grid_size, self.settings.library_paths, self.window)
|
||||
if dialog.exec() != QDialog.DialogCode.Accepted:
|
||||
return
|
||||
|
||||
old_library_paths = self.settings.library_paths
|
||||
self.settings.log_level = dialog.log_level
|
||||
self.settings.snap_to_grid_size = dialog.snap_to_grid_size
|
||||
self.settings.library_paths = dialog.library_paths
|
||||
self.window.graph_editor.set_snap_to_grid_size(dialog.snap_to_grid_size)
|
||||
set_log_level(dialog.log_level)
|
||||
if self.settings.library_paths != old_library_paths:
|
||||
self.library_paths_changed.emit()
|
||||
logger.info("Application settings updated")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,13 +15,15 @@ from bedit_gui.commands.equation_text_command import ChangeEquationTextCommand
|
||||
from bedit_gui.commands.graph_position_command import MoveGraphComponentCommand
|
||||
from bedit_gui.commands.graph_connection_points_command import ChangeGraphConnectionPointsCommand
|
||||
from bedit_gui.commands.graph_connection_command import AddGraphConnectionCommand, DeleteGraphConnectionCommand
|
||||
from bedit_gui.commands.graph_label_command import ChangeGraphComponentLabelCommand
|
||||
from bedit_gui.commands.port_commands import AddPortCommand, ChangePortCommand, RemovePortCommand
|
||||
from bedit_gui.commands.port_metadata_command import ChangePortMetadataDatabaseCommand
|
||||
from bedit_gui.commands.param_commands import AddParamCommand, ChangeParamCommand, RemoveParamCommand
|
||||
from bedit_gui.commands.rename_component_command import RenameComponentCommand
|
||||
from bedit_gui.commands.rename_document_command import RenameDocumentCommand
|
||||
from bedit_gui.commands.simulation_database_command import ChangeSimulationDatabaseCommand
|
||||
from bedit_gui.commands.component_command import AddEmptyEquationComponent, AddEmptyGraphComponent, DeleteComponent, PasteComponents
|
||||
from bedit_gui.models import Graph, GraphConnection, GraphDatabase, Icon, IconDatabase, Simulation, SimulationDatabase
|
||||
from bedit_gui.models import Graph, GraphComponentLabel, GraphConnection, GraphDatabase, Icon, IconDatabase, PortMetadata, PortMetadataDatabase, Simulation, SimulationDatabase
|
||||
from bedit_gui.services import document_files
|
||||
|
||||
|
||||
@@ -34,7 +36,9 @@ class Document(QObject):
|
||||
icon_changed = Signal(object, object)
|
||||
equation_text_changed = Signal(object, str)
|
||||
simulation_database_changed = Signal(object)
|
||||
port_metadata_database_changed = Signal(object)
|
||||
graph_component_position_changed = Signal(object, object, object)
|
||||
graph_component_label_changed = Signal(object, object, object)
|
||||
graph_connection_points_changed = Signal(object, object, object)
|
||||
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
@@ -138,6 +142,40 @@ class Document(QObject):
|
||||
database = self._graph_database(False)
|
||||
return deepcopy(database) if database is not None else GraphDatabase()
|
||||
|
||||
def stored_port_metadata_database(self) -> PortMetadataDatabase | None:
|
||||
database = self._port_metadata_database(False)
|
||||
return deepcopy(database) if database is not None else None
|
||||
|
||||
def port_metadata_database(self) -> PortMetadataDatabase:
|
||||
return self.stored_port_metadata_database() or PortMetadataDatabase()
|
||||
|
||||
def port_metadata(self, port_id: PortID) -> PortMetadata:
|
||||
return deepcopy(self.port_metadata_database().ports.get(port_id, PortMetadata()))
|
||||
|
||||
def _set_port_metadata_database(self, database: PortMetadataDatabase | None) -> None:
|
||||
if database is None or not database.ports:
|
||||
if self.model.metadata is not None:
|
||||
self.model.metadata.pop("port_metadata_database", None)
|
||||
else:
|
||||
if self.model.metadata is None:
|
||||
self.model.metadata = {}
|
||||
self.model.metadata["port_metadata_database"] = deepcopy(database)
|
||||
self.port_metadata_database_changed.emit(self.stored_port_metadata_database())
|
||||
|
||||
def _port_metadata_database(self, create: bool) -> PortMetadataDatabase | None:
|
||||
metadata = self.model.metadata
|
||||
value = metadata.get("port_metadata_database") if metadata is not None else None
|
||||
if isinstance(value, PortMetadataDatabase):
|
||||
return value
|
||||
if not create:
|
||||
return None
|
||||
if metadata is None:
|
||||
metadata = {}
|
||||
self.model.metadata = metadata
|
||||
database = PortMetadataDatabase()
|
||||
metadata["port_metadata_database"] = database
|
||||
return database
|
||||
|
||||
def move_graph_component(self, graph_component: Component, component_id: ComponentID, position: tuple[int, int]) -> None:
|
||||
graph_id = self.component_id(graph_component)
|
||||
database = self._graph_database(False)
|
||||
@@ -145,6 +183,18 @@ class Document(QObject):
|
||||
if graph is None or graph.component_positions.get(component_id) != position:
|
||||
self.undo_stack.push(MoveGraphComponentCommand(self, graph_id, component_id, position))
|
||||
|
||||
def move_graph_components(self, graph_component: Component, positions: dict[ComponentID, tuple[int, int]]) -> None:
|
||||
graph_id = self.component_id(graph_component)
|
||||
database = self._graph_database(False)
|
||||
graph = database.graphs.get(graph_id) if database is not None else None
|
||||
changed = {component_id: position for component_id, position in positions.items() if graph is None or graph.component_positions.get(component_id) != position}
|
||||
if not changed:
|
||||
return
|
||||
self.undo_stack.beginMacro("Move graph components" if len(changed) > 1 else "Move graph component")
|
||||
for component_id, position in changed.items():
|
||||
self.undo_stack.push(MoveGraphComponentCommand(self, graph_id, component_id, position))
|
||||
self.undo_stack.endMacro()
|
||||
|
||||
def _set_graph_component_position(self, graph_id: ComponentID, component_id: ComponentID, position: tuple[int, int] | None) -> None:
|
||||
if position is None:
|
||||
database = self._graph_database(False)
|
||||
@@ -157,6 +207,36 @@ class Document(QObject):
|
||||
graph.component_positions[component_id] = position
|
||||
self.graph_component_position_changed.emit(graph_id, component_id, position)
|
||||
|
||||
def move_graph_component_label(self, graph_component: Component, component_id: ComponentID, relative_position: tuple[int, int]) -> None:
|
||||
graph_id = self.component_id(graph_component)
|
||||
database = self._graph_database(False)
|
||||
graph = database.graphs.get(graph_id) if database is not None else None
|
||||
current = deepcopy(graph.component_labels.get(component_id)) if graph is not None and component_id in graph.component_labels else GraphComponentLabel()
|
||||
if current.relative_position != relative_position:
|
||||
current.relative_position = relative_position
|
||||
self.undo_stack.push(ChangeGraphComponentLabelCommand(self, graph_id, component_id, current, "Move component label"))
|
||||
|
||||
def set_graph_component_label_visible(self, graph_component: Component, component_id: ComponentID, visible: bool) -> None:
|
||||
graph_id = self.component_id(graph_component)
|
||||
database = self._graph_database(False)
|
||||
graph = database.graphs.get(graph_id) if database is not None else None
|
||||
current = deepcopy(graph.component_labels.get(component_id)) if graph is not None and component_id in graph.component_labels else GraphComponentLabel()
|
||||
if current.visible != visible:
|
||||
current.visible = visible
|
||||
self.undo_stack.push(ChangeGraphComponentLabelCommand(self, graph_id, component_id, current, "Show component label" if visible else "Hide component label"))
|
||||
|
||||
def _set_graph_component_label(self, graph_id: ComponentID, component_id: ComponentID, label: GraphComponentLabel | None) -> None:
|
||||
if label is None:
|
||||
database = self._graph_database(False)
|
||||
graph = database.graphs.get(graph_id) if database is not None else None
|
||||
if graph is not None:
|
||||
graph.component_labels.pop(component_id, None)
|
||||
else:
|
||||
database = self._graph_database(True)
|
||||
graph = database.graphs.setdefault(graph_id, Graph())
|
||||
graph.component_labels[component_id] = deepcopy(label)
|
||||
self.graph_component_label_changed.emit(graph_id, component_id, deepcopy(label))
|
||||
|
||||
def _graph_database(self, create: bool) -> GraphDatabase | None:
|
||||
metadata = self.model.metadata
|
||||
value = metadata.get("graph_database") if metadata is not None else None
|
||||
@@ -277,7 +357,7 @@ class Document(QObject):
|
||||
metadata["icon_database"] = database
|
||||
return database
|
||||
|
||||
def update_component_ports(self, component: Component, ports: dict[PortID, Port]) -> None:
|
||||
def update_component_ports(self, component: Component, ports: dict[PortID, Port], port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
|
||||
current = component.interface.ports
|
||||
removed = [
|
||||
RemovePortCommand(self, component, port_id)
|
||||
@@ -293,12 +373,23 @@ class Document(QObject):
|
||||
if current[port_id] != ports[port_id]
|
||||
]
|
||||
commands = [*removed, *added, *changed]
|
||||
if not commands:
|
||||
database = self.port_metadata_database()
|
||||
component_port_ids = set(current) | set(ports)
|
||||
for port_id in component_port_ids:
|
||||
metadata = port_metadata.get(port_id, PortMetadata()) if port_metadata is not None and port_id in ports else PortMetadata()
|
||||
if metadata == PortMetadata():
|
||||
database.ports.pop(port_id, None)
|
||||
else:
|
||||
database.ports[port_id] = deepcopy(metadata)
|
||||
metadata_changed = database != self.port_metadata_database()
|
||||
if not commands and not metadata_changed:
|
||||
return
|
||||
|
||||
self.undo_stack.beginMacro("Edit interface")
|
||||
for command in commands:
|
||||
self.undo_stack.push(command)
|
||||
if metadata_changed:
|
||||
self.undo_stack.push(ChangePortMetadataDatabaseCommand(self, database))
|
||||
self.undo_stack.endMacro()
|
||||
|
||||
|
||||
@@ -337,12 +428,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))
|
||||
|
||||
@@ -354,16 +455,16 @@ class Document(QObject):
|
||||
self.undo_stack.push(DeleteComponent(self, component))
|
||||
self.undo_stack.endMacro()
|
||||
|
||||
def paste_components(self, target: dict[ComponentID, Component], components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> None:
|
||||
def paste_components(self, target: dict[ComponentID, Component], components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
|
||||
if not components:
|
||||
return
|
||||
names = {component.name for component in target.values()}
|
||||
for component in components.values():
|
||||
component.name = self._unique_name(names, component.name)
|
||||
names.add(component.name)
|
||||
self.undo_stack.push(PasteComponents(self, target, components, icons))
|
||||
self.undo_stack.push(PasteComponents(self, target, components, icons, port_metadata=port_metadata))
|
||||
|
||||
def paste_graph_components(self, graph_component: Component, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], positions: dict[ComponentID, tuple[int, int]]) -> None:
|
||||
def paste_graph_components(self, graph_component: Component, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], positions: dict[ComponentID, tuple[int, int]], port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
|
||||
if not isinstance(graph_component.implementation, GraphImplementation) or not components:
|
||||
return
|
||||
target = graph_component.implementation.graph.components
|
||||
@@ -371,7 +472,7 @@ class Document(QObject):
|
||||
for component in components.values():
|
||||
component.name = self._unique_name(names, component.name)
|
||||
names.add(component.name)
|
||||
self.undo_stack.push(PasteComponents(self, target, components, icons, self.component_id(graph_component), positions))
|
||||
self.undo_stack.push(PasteComponents(self, target, components, icons, self.component_id(graph_component), positions, port_metadata))
|
||||
|
||||
@staticmethod
|
||||
def _unique_component_name(components: dict[ComponentID, Component], name: str) -> str:
|
||||
|
||||
@@ -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")
|
||||
@@ -123,6 +143,31 @@ class IconDatabase:
|
||||
def to_data(self) -> dict[str, Any]:
|
||||
return {"format_version": self.format_version, "icons": {str(key): icon.to_data() for key, icon in self.icons.items()}}
|
||||
|
||||
@dataclass
|
||||
class PortMetadata:
|
||||
connection_annotation: str | None = None
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: Mapping[str, Any]) -> PortMetadata:
|
||||
annotation = data.get("connection_annotation")
|
||||
return cls(connection_annotation=str(annotation) if annotation else None)
|
||||
|
||||
def to_data(self) -> dict[str, Any]:
|
||||
return {"connection_annotation": self.connection_annotation}
|
||||
|
||||
@dataclass
|
||||
class PortMetadataDatabase:
|
||||
format_version: int = 1
|
||||
ports: dict[PortID, PortMetadata] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: Mapping[str, Any]) -> PortMetadataDatabase:
|
||||
ports = {PortID(key): PortMetadata.from_data(value) for key, value in data.get("ports", {}).items()}
|
||||
return cls(format_version=int(data.get("format_version", 1)), ports=ports)
|
||||
|
||||
def to_data(self) -> dict[str, Any]:
|
||||
return {"format_version": self.format_version, "ports": {str(key): metadata.to_data() for key, metadata in self.ports.items()}}
|
||||
|
||||
@dataclass
|
||||
class GraphConnection:
|
||||
points: list[tuple[int, int]] = field(default_factory=list)
|
||||
@@ -134,23 +179,39 @@ class GraphConnection:
|
||||
def to_data(self) -> dict[str, Any]:
|
||||
return {"points": [list(point) for point in self.points]}
|
||||
|
||||
@dataclass
|
||||
class GraphComponentLabel:
|
||||
relative_position: tuple[int, int] = (0, 8)
|
||||
visible: bool = True
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: Mapping[str, Any]) -> GraphComponentLabel:
|
||||
position = data.get("relative_position", [0, 8])
|
||||
return cls(relative_position=(int(position[0]), int(position[1])), visible=bool(data.get("visible", True)))
|
||||
|
||||
def to_data(self) -> dict[str, Any]:
|
||||
return {"relative_position": list(self.relative_position), "visible": self.visible}
|
||||
|
||||
@dataclass
|
||||
class Graph:
|
||||
shapes: dict[ShapeID, Shape] = field(default_factory=dict)
|
||||
component_positions: dict[ComponentID, tuple[int, int]] = field(default_factory=dict)
|
||||
connections: dict[ConnectionID, GraphConnection] = field(default_factory=dict)
|
||||
component_labels: dict[ComponentID, GraphComponentLabel] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: Mapping[str, Any]) -> Graph:
|
||||
shapes = {ShapeID(key): Shape.from_data(value) for key, value in data.get("shapes", {}).items()}
|
||||
component_positions = {ComponentID(key): (int(value[0]), int(value[1])) for key, value in data.get("component_positions", {}).items()}
|
||||
component_labels = {ComponentID(key): GraphComponentLabel.from_data(value) for key, value in data.get("component_labels", {}).items()}
|
||||
connections = {ConnectionID(key): GraphConnection.from_data(value) for key, value in data.get("connections", {}).items()}
|
||||
return cls(shapes=shapes, component_positions=component_positions, connections=connections)
|
||||
return cls(shapes=shapes, component_positions=component_positions, component_labels=component_labels, connections=connections)
|
||||
|
||||
def to_data(self) -> dict[str, Any]:
|
||||
return {
|
||||
"shapes": {str(key): shape.to_data() for key, shape in self.shapes.items()},
|
||||
"component_positions": {str(key): list(position) for key, position in self.component_positions.items()},
|
||||
"component_labels": {str(key): label.to_data() for key, label in self.component_labels.items()},
|
||||
"connections": {str(key): connection.to_data() for key, connection in self.connections.items()},
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ class ApplicationSettings:
|
||||
DEFAULT_LOG_LEVEL = logging.INFO
|
||||
SNAP_TO_GRID_SIZE_KEY = "graph/snap_to_grid_size"
|
||||
DEFAULT_SNAP_TO_GRID_SIZE = 4
|
||||
LIBRARY_PATHS_KEY = "libraries/paths"
|
||||
|
||||
def __init__(self, settings: QSettings | None = None) -> None:
|
||||
self._settings = settings if settings is not None else QSettings()
|
||||
@@ -38,6 +39,17 @@ class ApplicationSettings:
|
||||
raise ValueError("snap-to-grid size must be positive")
|
||||
self._settings.setValue(self.SNAP_TO_GRID_SIZE_KEY, size)
|
||||
|
||||
@property
|
||||
def library_paths(self) -> list[str]:
|
||||
value = self._settings.value(self.LIBRARY_PATHS_KEY, [])
|
||||
if isinstance(value, str):
|
||||
return [value]
|
||||
return [str(path) for path in value] if isinstance(value, (list, tuple)) else []
|
||||
|
||||
@library_paths.setter
|
||||
def library_paths(self, paths: list[str]) -> None:
|
||||
self._settings.setValue(self.LIBRARY_PATHS_KEY, list(dict.fromkeys(paths)))
|
||||
|
||||
class SimulationApplicationSettings:
|
||||
"""Typed access to persistent BEsim application settings."""
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@ from typing import Any
|
||||
from PySide6.QtCore import QMimeData, QObject, Signal
|
||||
from PySide6.QtGui import QClipboard, QGuiApplication
|
||||
|
||||
from bedit_gui.services.component_clipboard import COMPONENTS_MIME
|
||||
|
||||
|
||||
class ClipboardService(QObject):
|
||||
COMPONENTS_MIME = "application/x-bedit-components+json"
|
||||
COMPONENTS_MIME = COMPONENTS_MIME
|
||||
changed = Signal()
|
||||
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
|
||||
@@ -6,24 +6,37 @@ from typing import Any
|
||||
from bedit_core.models import Component, ComponentID, ConnectionID, Document, GraphImplementation, ID, ParameterID, PortID
|
||||
from bedit_core.serialization.schema import document_from_data, document_to_data
|
||||
from bedit_gui.documents import Document as GuiDocument
|
||||
from bedit_gui.models import Icon, ShapeID
|
||||
from bedit_gui.models import Icon, PortMetadata, ShapeID
|
||||
|
||||
FORMAT_VERSION = 1
|
||||
COMPONENTS_MIME = "application/x-bedit-components+json"
|
||||
|
||||
|
||||
def export_components(document: GuiDocument, components: list[Component]) -> dict[str, Any]:
|
||||
roots = {document.component_id(component): component for component in components}
|
||||
serialized = document_to_data(Document(format_version=1, id=ID(), name="Clipboard", root=roots))
|
||||
component_ids = _all_component_ids(roots)
|
||||
icons = {}
|
||||
port_metadata = {}
|
||||
for component_id in component_ids:
|
||||
icon = document.stored_component_icon(component_id)
|
||||
if icon is not None:
|
||||
icons[str(component_id)] = icon.to_data()
|
||||
return {"format_version": FORMAT_VERSION, "type": "components", "components": serialized["root"], "icons": icons}
|
||||
icons[component_id] = icon
|
||||
database = document.port_metadata_database()
|
||||
for port_id in _all_port_ids(roots):
|
||||
if port_id in database.ports:
|
||||
port_metadata[port_id] = database.ports[port_id]
|
||||
return export_component_data(roots, icons, port_metadata)
|
||||
|
||||
|
||||
def import_components(payload: dict[str, Any]) -> tuple[dict[ComponentID, Component], dict[ComponentID, Icon]]:
|
||||
def export_component_data(components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], port_metadata: dict[PortID, PortMetadata] | None = None) -> dict[str, Any]:
|
||||
serialized = document_to_data(Document(format_version=1, id=ID(), name="Clipboard", root=components))
|
||||
component_ids = set(_all_component_ids(components))
|
||||
icon_data = {str(component_id): icon.to_data() for component_id, icon in icons.items() if component_id in component_ids}
|
||||
metadata_data = {str(port_id): metadata.to_data() for port_id, metadata in (port_metadata or {}).items() if port_id in set(_all_port_ids(components))}
|
||||
return {"format_version": FORMAT_VERSION, "type": "components", "components": serialized["root"], "icons": icon_data, "port_metadata": metadata_data}
|
||||
|
||||
|
||||
def import_components(payload: dict[str, Any]) -> tuple[dict[ComponentID, Component], dict[ComponentID, Icon], dict[PortID, PortMetadata]]:
|
||||
if payload.get("format_version") != FORMAT_VERSION or payload.get("type") != "components":
|
||||
raise ValueError("unsupported component clipboard format")
|
||||
components = payload.get("components")
|
||||
@@ -45,7 +58,11 @@ def import_components(payload: dict[str, Any]) -> tuple[dict[ComponentID, Compon
|
||||
icon.shapes = {ShapeID(): shape for shape in icon.shapes.values()}
|
||||
icon.port_positions = {port_map[port_id]: position for port_id, position in icon.port_positions.items() if port_id in port_map}
|
||||
icons[new_component_id] = icon
|
||||
return remapped, icons
|
||||
metadata_data = payload.get("port_metadata", {})
|
||||
if not isinstance(metadata_data, dict):
|
||||
raise TypeError("component clipboard port metadata must be an object")
|
||||
port_metadata = {port_map[PortID(old_id)]: PortMetadata.from_data(data) for old_id, data in metadata_data.items() if PortID(old_id) in port_map and isinstance(data, dict)}
|
||||
return remapped, icons, port_metadata
|
||||
|
||||
|
||||
def _remap_components(components: dict[ComponentID, Component], component_map: dict[ComponentID, ComponentID], port_map: dict[PortID, PortID]) -> dict[ComponentID, Component]:
|
||||
@@ -80,3 +97,12 @@ def _all_component_ids(components: dict[ComponentID, Component]) -> list[Compone
|
||||
if isinstance(component.implementation, GraphImplementation):
|
||||
component_ids.extend(_all_component_ids(component.implementation.graph.components))
|
||||
return component_ids
|
||||
|
||||
|
||||
def _all_port_ids(components: dict[ComponentID, Component]) -> list[PortID]:
|
||||
port_ids: list[PortID] = []
|
||||
for component in components.values():
|
||||
port_ids.extend(component.interface.ports)
|
||||
if isinstance(component.implementation, GraphImplementation):
|
||||
port_ids.extend(_all_port_ids(component.implementation.graph.components))
|
||||
return port_ids
|
||||
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
from bedit_core.models import Document
|
||||
from bedit_core.serialization import load as load_document
|
||||
from bedit_core.serialization import save as save_document
|
||||
from bedit_gui.models import GraphDatabase, IconDatabase, SimulationDatabase
|
||||
from bedit_gui.models import GraphDatabase, IconDatabase, PortMetadataDatabase, SimulationDatabase
|
||||
|
||||
|
||||
def load(path: str | Path) -> Document:
|
||||
@@ -18,6 +18,8 @@ def load(path: str | Path) -> Document:
|
||||
document.metadata["graph_database"] = GraphDatabase.from_data(document.metadata["graph_database"])
|
||||
if document.metadata is not None and isinstance(document.metadata.get("simulation_database"), dict):
|
||||
document.metadata["simulation_database"] = SimulationDatabase.from_data(document.metadata["simulation_database"])
|
||||
if document.metadata is not None and isinstance(document.metadata.get("port_metadata_database"), dict):
|
||||
document.metadata["port_metadata_database"] = PortMetadataDatabase.from_data(document.metadata["port_metadata_database"])
|
||||
return document
|
||||
|
||||
|
||||
@@ -30,4 +32,6 @@ def save(document: Document, path: str | Path) -> None:
|
||||
saved_document.metadata["graph_database"] = saved_document.metadata["graph_database"].to_data()
|
||||
if saved_document.metadata is not None and isinstance(saved_document.metadata.get("simulation_database"), SimulationDatabase):
|
||||
saved_document.metadata["simulation_database"] = saved_document.metadata["simulation_database"].to_data()
|
||||
if saved_document.metadata is not None and isinstance(saved_document.metadata.get("port_metadata_database"), PortMetadataDatabase):
|
||||
saved_document.metadata["port_metadata_database"] = saved_document.metadata["port_metadata_database"].to_data()
|
||||
save_document(saved_document, path)
|
||||
|
||||
42
src/bedit_gui/services/libraries.py
Normal file
42
src/bedit_gui/services/libraries.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from bedit_core.models import Document
|
||||
from bedit_gui.services import document_files
|
||||
from bedit_gui.services.application_logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LoadedLibrary:
|
||||
path: Path
|
||||
document: Document
|
||||
|
||||
|
||||
def list_library_files(library_paths: list[str]) -> list[Path]:
|
||||
files = []
|
||||
seen = set()
|
||||
for configured_path in library_paths:
|
||||
path = Path(configured_path).expanduser()
|
||||
candidates = [path] if path.is_file() else sorted(path.rglob("*"), key=lambda candidate: str(candidate).casefold()) if path.is_dir() else []
|
||||
for candidate in candidates:
|
||||
if not candidate.is_file() or candidate.suffix.lower() not in (".beb", ".json"):
|
||||
continue
|
||||
resolved = candidate.resolve()
|
||||
if resolved not in seen:
|
||||
seen.add(resolved)
|
||||
files.append(resolved)
|
||||
return files
|
||||
|
||||
|
||||
def load_library_documents(library_paths: list[str]) -> list[LoadedLibrary]:
|
||||
libraries = []
|
||||
for path in list_library_files(library_paths):
|
||||
try:
|
||||
libraries.append(LoadedLibrary(path, document_files.load(path)))
|
||||
except (KeyError, OSError, TypeError, ValueError) as exc:
|
||||
logger.warning("Could not load library %s: %s", path, exc)
|
||||
return libraries
|
||||
@@ -30,7 +30,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>22</height>
|
||||
<height>19</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuEdit">
|
||||
@@ -80,6 +80,7 @@
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<addaction name="actionAdd_Rectangle"/>
|
||||
<addaction name="actionAdd_Circle"/>
|
||||
<addaction name="actionAdd_Text"/>
|
||||
<addaction name="actionAdd_Line"/>
|
||||
</widget>
|
||||
@@ -236,6 +237,21 @@
|
||||
<enum>QAction::MenuRole::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionAdd_Circle">
|
||||
<property name="icon">
|
||||
<iconset resource="../../resources/resources.qrc">
|
||||
<normaloff>:/icons/icons/draw-circle.png</normaloff>:/icons/icons/draw-circle.png</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Add Ellipse</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Add a circle</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../resources/resources.qrc"/>
|
||||
|
||||
206
src/bedit_gui/ui/forms/icon_editor_window_ui.py
Normal file
206
src/bedit_gui/ui/forms/icon_editor_window_ui.py
Normal file
@@ -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
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>940</width>
|
||||
<height>22</height>
|
||||
<height>19</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuFile">
|
||||
@@ -59,6 +59,7 @@
|
||||
<addaction name="actionDelete"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionSettings"/>
|
||||
<addaction name="actionReload_Libraries"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuView">
|
||||
<property name="title">
|
||||
@@ -127,7 +128,7 @@
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>150</width>
|
||||
<height>533</height>
|
||||
<height>200</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -180,6 +181,27 @@
|
||||
<addaction name="actionCompile_Model"/>
|
||||
<addaction name="actionOpen_Simulation_Window"/>
|
||||
</widget>
|
||||
<widget class="QDockWidget" name="libraryWidget">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>150</width>
|
||||
<height>200</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Libraries</string>
|
||||
</property>
|
||||
<attribute name="dockWidgetArea">
|
||||
<number>1</number>
|
||||
</attribute>
|
||||
<widget class="QWidget" name="dockWidgetContents_4">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QTreeView" name="libraryTree"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<action name="actionOpen_File">
|
||||
<property name="icon">
|
||||
<iconset resource="../../resources/resources.qrc">
|
||||
@@ -330,6 +352,17 @@
|
||||
<string>Settings</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionReload_Libraries">
|
||||
<property name="text">
|
||||
<string>Reload Libraries</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Reload configured libraries</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionDelete">
|
||||
<property name="icon">
|
||||
<iconset resource="../../resources/resources.qrc">
|
||||
|
||||
370
src/bedit_gui/ui/forms/main_window_ui.py
Normal file
370
src/bedit_gui/ui/forms/main_window_ui.py
Normal file
@@ -0,0 +1,370 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'main_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, QDockWidget, QHeaderView, QListView,
|
||||
QMainWindow, QMenu, QMenuBar, QSizePolicy,
|
||||
QStatusBar, QTabWidget, QToolBar, QTreeView,
|
||||
QVBoxLayout, QWidget)
|
||||
import resources_rc
|
||||
|
||||
class Ui_MainWindow(object):
|
||||
def setupUi(self, MainWindow):
|
||||
if not MainWindow.objectName():
|
||||
MainWindow.setObjectName(u"MainWindow")
|
||||
MainWindow.resize(940, 729)
|
||||
icon = QIcon()
|
||||
icon.addFile(u":/icons/icons/office-chart-line.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
MainWindow.setWindowIcon(icon)
|
||||
MainWindow.setDocumentMode(False)
|
||||
MainWindow.setTabShape(QTabWidget.TabShape.Triangular)
|
||||
self.actionOpen_File = QAction(MainWindow)
|
||||
self.actionOpen_File.setObjectName(u"actionOpen_File")
|
||||
icon1 = QIcon()
|
||||
icon1.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionOpen_File.setIcon(icon1)
|
||||
self.actionOpen_File.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionNew_File = QAction(MainWindow)
|
||||
self.actionNew_File.setObjectName(u"actionNew_File")
|
||||
icon2 = QIcon()
|
||||
icon2.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionNew_File.setIcon(icon2)
|
||||
self.actionNew_File.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionSave_File = QAction(MainWindow)
|
||||
self.actionSave_File.setObjectName(u"actionSave_File")
|
||||
icon3 = QIcon()
|
||||
icon3.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionSave_File.setIcon(icon3)
|
||||
self.actionSave_File.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionSave_File_As = QAction(MainWindow)
|
||||
self.actionSave_File_As.setObjectName(u"actionSave_File_As")
|
||||
icon4 = QIcon()
|
||||
icon4.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionSave_File_As.setIcon(icon4)
|
||||
self.actionSave_File_As.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionClose = QAction(MainWindow)
|
||||
self.actionClose.setObjectName(u"actionClose")
|
||||
self.actionClose.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionAbout_QT = QAction(MainWindow)
|
||||
self.actionAbout_QT.setObjectName(u"actionAbout_QT")
|
||||
self.actionAbout_QT.setMenuRole(QAction.MenuRole.AboutQtRole)
|
||||
self.actionUndo = QAction(MainWindow)
|
||||
self.actionUndo.setObjectName(u"actionUndo")
|
||||
icon5 = QIcon()
|
||||
icon5.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionUndo.setIcon(icon5)
|
||||
self.actionUndo.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionRedo = QAction(MainWindow)
|
||||
self.actionRedo.setObjectName(u"actionRedo")
|
||||
icon6 = QIcon()
|
||||
icon6.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionRedo.setIcon(icon6)
|
||||
self.actionRedo.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionReset_Layout = QAction(MainWindow)
|
||||
self.actionReset_Layout.setObjectName(u"actionReset_Layout")
|
||||
self.actionReset_Layout.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionPanels = QAction(MainWindow)
|
||||
self.actionPanels.setObjectName(u"actionPanels")
|
||||
self.actionToolbars = QAction(MainWindow)
|
||||
self.actionToolbars.setObjectName(u"actionToolbars")
|
||||
self.actionSettings = QAction(MainWindow)
|
||||
self.actionSettings.setObjectName(u"actionSettings")
|
||||
self.actionReload_Libraries = QAction(MainWindow)
|
||||
self.actionReload_Libraries.setObjectName(u"actionReload_Libraries")
|
||||
self.actionReload_Libraries.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionDelete = QAction(MainWindow)
|
||||
self.actionDelete.setObjectName(u"actionDelete")
|
||||
icon7 = QIcon()
|
||||
icon7.addFile(u":/icons/icons/edit-delete.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionDelete.setIcon(icon7)
|
||||
self.actionDelete.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionEscape = QAction(MainWindow)
|
||||
self.actionEscape.setObjectName(u"actionEscape")
|
||||
self.actionEscape.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionCopy = QAction(MainWindow)
|
||||
self.actionCopy.setObjectName(u"actionCopy")
|
||||
icon8 = QIcon()
|
||||
icon8.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionCopy.setIcon(icon8)
|
||||
self.actionCopy.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionPaste = QAction(MainWindow)
|
||||
self.actionPaste.setObjectName(u"actionPaste")
|
||||
icon9 = QIcon()
|
||||
icon9.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionPaste.setIcon(icon9)
|
||||
self.actionPaste.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionCut = QAction(MainWindow)
|
||||
self.actionCut.setObjectName(u"actionCut")
|
||||
icon10 = QIcon()
|
||||
icon10.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionCut.setIcon(icon10)
|
||||
self.actionCut.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionSimulation_Settings = QAction(MainWindow)
|
||||
self.actionSimulation_Settings.setObjectName(u"actionSimulation_Settings")
|
||||
icon11 = QIcon()
|
||||
icon11.addFile(u":/icons/icons/configure.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionSimulation_Settings.setIcon(icon11)
|
||||
self.actionSimulation_Settings.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionEdit_Parameters = QAction(MainWindow)
|
||||
self.actionEdit_Parameters.setObjectName(u"actionEdit_Parameters")
|
||||
icon12 = QIcon()
|
||||
icon12.addFile(u":/icons/icons/view-form-table.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionEdit_Parameters.setIcon(icon12)
|
||||
self.actionEdit_Parameters.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionCompile_Model = QAction(MainWindow)
|
||||
self.actionCompile_Model.setObjectName(u"actionCompile_Model")
|
||||
icon13 = QIcon()
|
||||
icon13.addFile(u":/icons/icons/run-build.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionCompile_Model.setIcon(icon13)
|
||||
self.actionCompile_Model.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionOpen_Simulation_Window = QAction(MainWindow)
|
||||
self.actionOpen_Simulation_Window.setObjectName(u"actionOpen_Simulation_Window")
|
||||
self.actionOpen_Simulation_Window.setIcon(icon)
|
||||
self.actionOpen_Simulation_Window.setMenuRole(QAction.MenuRole.NoRole)
|
||||
self.actionAbout = QAction(MainWindow)
|
||||
self.actionAbout.setObjectName(u"actionAbout")
|
||||
self.centralwidget = QWidget(MainWindow)
|
||||
self.centralwidget.setObjectName(u"centralwidget")
|
||||
MainWindow.setCentralWidget(self.centralwidget)
|
||||
self.menubar = QMenuBar(MainWindow)
|
||||
self.menubar.setObjectName(u"menubar")
|
||||
self.menubar.setGeometry(QRect(0, 0, 940, 19))
|
||||
self.menuFile = QMenu(self.menubar)
|
||||
self.menuFile.setObjectName(u"menuFile")
|
||||
self.menuEdit = QMenu(self.menubar)
|
||||
self.menuEdit.setObjectName(u"menuEdit")
|
||||
self.menuView = QMenu(self.menubar)
|
||||
self.menuView.setObjectName(u"menuView")
|
||||
self.menuHelp = QMenu(self.menubar)
|
||||
self.menuHelp.setObjectName(u"menuHelp")
|
||||
self.menuSimulation = QMenu(self.menubar)
|
||||
self.menuSimulation.setObjectName(u"menuSimulation")
|
||||
MainWindow.setMenuBar(self.menubar)
|
||||
self.statusbar = QStatusBar(MainWindow)
|
||||
self.statusbar.setObjectName(u"statusbar")
|
||||
MainWindow.setStatusBar(self.statusbar)
|
||||
self.fileToolBar = QToolBar(MainWindow)
|
||||
self.fileToolBar.setObjectName(u"fileToolBar")
|
||||
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.fileToolBar)
|
||||
self.undoToolBar = QToolBar(MainWindow)
|
||||
self.undoToolBar.setObjectName(u"undoToolBar")
|
||||
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.undoToolBar)
|
||||
self.documentTreeWidget = QDockWidget(MainWindow)
|
||||
self.documentTreeWidget.setObjectName(u"documentTreeWidget")
|
||||
self.documentTreeWidget.setMinimumSize(QSize(150, 200))
|
||||
self.dockWidgetContents = QWidget()
|
||||
self.dockWidgetContents.setObjectName(u"dockWidgetContents")
|
||||
self.verticalLayout = QVBoxLayout(self.dockWidgetContents)
|
||||
self.verticalLayout.setObjectName(u"verticalLayout")
|
||||
self.documentTree = QTreeView(self.dockWidgetContents)
|
||||
self.documentTree.setObjectName(u"documentTree")
|
||||
|
||||
self.verticalLayout.addWidget(self.documentTree)
|
||||
|
||||
self.documentTreeWidget.setWidget(self.dockWidgetContents)
|
||||
MainWindow.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.documentTreeWidget)
|
||||
self.logWidget = QDockWidget(MainWindow)
|
||||
self.logWidget.setObjectName(u"logWidget")
|
||||
self.logWidget.setMinimumSize(QSize(150, 107))
|
||||
self.dockWidgetContents_5 = QWidget()
|
||||
self.dockWidgetContents_5.setObjectName(u"dockWidgetContents_5")
|
||||
self.verticalLayout_2 = QVBoxLayout(self.dockWidgetContents_5)
|
||||
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
|
||||
self.listView = QListView(self.dockWidgetContents_5)
|
||||
self.listView.setObjectName(u"listView")
|
||||
|
||||
self.verticalLayout_2.addWidget(self.listView)
|
||||
|
||||
self.logWidget.setWidget(self.dockWidgetContents_5)
|
||||
MainWindow.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.logWidget)
|
||||
self.simToolBar = QToolBar(MainWindow)
|
||||
self.simToolBar.setObjectName(u"simToolBar")
|
||||
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.simToolBar)
|
||||
self.libraryWidget = QDockWidget(MainWindow)
|
||||
self.libraryWidget.setObjectName(u"libraryWidget")
|
||||
self.libraryWidget.setMinimumSize(QSize(150, 200))
|
||||
self.dockWidgetContents_4 = QWidget()
|
||||
self.dockWidgetContents_4.setObjectName(u"dockWidgetContents_4")
|
||||
self.verticalLayout_4 = QVBoxLayout(self.dockWidgetContents_4)
|
||||
self.verticalLayout_4.setObjectName(u"verticalLayout_4")
|
||||
self.libraryTree = QTreeView(self.dockWidgetContents_4)
|
||||
self.libraryTree.setObjectName(u"libraryTree")
|
||||
|
||||
self.verticalLayout_4.addWidget(self.libraryTree)
|
||||
|
||||
self.libraryWidget.setWidget(self.dockWidgetContents_4)
|
||||
MainWindow.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.libraryWidget)
|
||||
|
||||
self.menubar.addAction(self.menuFile.menuAction())
|
||||
self.menubar.addAction(self.menuEdit.menuAction())
|
||||
self.menubar.addAction(self.menuView.menuAction())
|
||||
self.menubar.addAction(self.menuSimulation.menuAction())
|
||||
self.menubar.addAction(self.menuHelp.menuAction())
|
||||
self.menuFile.addAction(self.actionNew_File)
|
||||
self.menuFile.addAction(self.actionOpen_File)
|
||||
self.menuFile.addSeparator()
|
||||
self.menuFile.addAction(self.actionSave_File)
|
||||
self.menuFile.addAction(self.actionSave_File_As)
|
||||
self.menuFile.addSeparator()
|
||||
self.menuFile.addAction(self.actionClose)
|
||||
self.menuEdit.addAction(self.actionUndo)
|
||||
self.menuEdit.addAction(self.actionRedo)
|
||||
self.menuEdit.addSeparator()
|
||||
self.menuEdit.addAction(self.actionCopy)
|
||||
self.menuEdit.addAction(self.actionCut)
|
||||
self.menuEdit.addAction(self.actionPaste)
|
||||
self.menuEdit.addSeparator()
|
||||
self.menuEdit.addAction(self.actionDelete)
|
||||
self.menuEdit.addSeparator()
|
||||
self.menuEdit.addAction(self.actionSettings)
|
||||
self.menuEdit.addAction(self.actionReload_Libraries)
|
||||
self.menuView.addAction(self.actionReset_Layout)
|
||||
self.menuView.addAction(self.actionPanels)
|
||||
self.menuView.addAction(self.actionToolbars)
|
||||
self.menuHelp.addAction(self.actionAbout)
|
||||
self.menuHelp.addAction(self.actionAbout_QT)
|
||||
self.menuSimulation.addAction(self.actionSimulation_Settings)
|
||||
self.menuSimulation.addAction(self.actionEdit_Parameters)
|
||||
self.menuSimulation.addSeparator()
|
||||
self.menuSimulation.addAction(self.actionCompile_Model)
|
||||
self.menuSimulation.addAction(self.actionOpen_Simulation_Window)
|
||||
self.fileToolBar.addAction(self.actionNew_File)
|
||||
self.fileToolBar.addAction(self.actionOpen_File)
|
||||
self.fileToolBar.addAction(self.actionSave_File)
|
||||
self.fileToolBar.addAction(self.actionSave_File_As)
|
||||
self.undoToolBar.addAction(self.actionUndo)
|
||||
self.undoToolBar.addAction(self.actionRedo)
|
||||
self.undoToolBar.addAction(self.actionCopy)
|
||||
self.undoToolBar.addAction(self.actionCut)
|
||||
self.undoToolBar.addAction(self.actionPaste)
|
||||
self.simToolBar.addAction(self.actionSimulation_Settings)
|
||||
self.simToolBar.addAction(self.actionEdit_Parameters)
|
||||
self.simToolBar.addAction(self.actionCompile_Model)
|
||||
self.simToolBar.addAction(self.actionOpen_Simulation_Window)
|
||||
|
||||
self.retranslateUi(MainWindow)
|
||||
|
||||
QMetaObject.connectSlotsByName(MainWindow)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, MainWindow):
|
||||
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None))
|
||||
self.actionOpen_File.setText(QCoreApplication.translate("MainWindow", u"Open File", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.actionOpen_File.setToolTip(QCoreApplication.translate("MainWindow", u"Open a file", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionOpen_File.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+O", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionNew_File.setText(QCoreApplication.translate("MainWindow", u"New File", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.actionNew_File.setToolTip(QCoreApplication.translate("MainWindow", u"Create a new file", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionNew_File.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+N", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionSave_File.setText(QCoreApplication.translate("MainWindow", u"Save File", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.actionSave_File.setToolTip(QCoreApplication.translate("MainWindow", u"Save a file to disk", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionSave_File.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+S", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionSave_File_As.setText(QCoreApplication.translate("MainWindow", u"Save File As...", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.actionSave_File_As.setToolTip(QCoreApplication.translate("MainWindow", u"Save file to disk as another file", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionSave_File_As.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Shift+S", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionClose.setText(QCoreApplication.translate("MainWindow", u"Close", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.actionClose.setToolTip(QCoreApplication.translate("MainWindow", u"Close application", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionClose.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Q", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionAbout_QT.setText(QCoreApplication.translate("MainWindow", u"About QT", None))
|
||||
self.actionUndo.setText(QCoreApplication.translate("MainWindow", u"Undo", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionUndo.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Z", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionRedo.setText(QCoreApplication.translate("MainWindow", u"Redo", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionRedo.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Y", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionReset_Layout.setText(QCoreApplication.translate("MainWindow", u"Reset Layout", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.actionReset_Layout.setToolTip(QCoreApplication.translate("MainWindow", u"Reset window layout to default", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.actionPanels.setText(QCoreApplication.translate("MainWindow", u"Panels", None))
|
||||
self.actionToolbars.setText(QCoreApplication.translate("MainWindow", u"Toolbars", None))
|
||||
self.actionSettings.setText(QCoreApplication.translate("MainWindow", u"Settings", None))
|
||||
self.actionReload_Libraries.setText(QCoreApplication.translate("MainWindow", u"Reload Libraries", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.actionReload_Libraries.setToolTip(QCoreApplication.translate("MainWindow", u"Reload configured libraries", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.actionDelete.setText(QCoreApplication.translate("MainWindow", u"Delete", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.actionDelete.setToolTip(QCoreApplication.translate("MainWindow", u"Delete selected", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionDelete.setShortcut(QCoreApplication.translate("MainWindow", u"Del", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionEscape.setText(QCoreApplication.translate("MainWindow", u"Escape", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionEscape.setShortcut(QCoreApplication.translate("MainWindow", u"Esc", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionCopy.setText(QCoreApplication.translate("MainWindow", u"Copy", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.actionCopy.setToolTip(QCoreApplication.translate("MainWindow", u"Copy selected", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionCopy.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+C", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionPaste.setText(QCoreApplication.translate("MainWindow", u"Paste", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.actionPaste.setToolTip(QCoreApplication.translate("MainWindow", u"Paste selected", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionPaste.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+V", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionCut.setText(QCoreApplication.translate("MainWindow", u"Cut", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.actionCut.setToolTip(QCoreApplication.translate("MainWindow", u"Cut selected", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionCut.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+X", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionSimulation_Settings.setText(QCoreApplication.translate("MainWindow", u"Simulation Settings", None))
|
||||
self.actionEdit_Parameters.setText(QCoreApplication.translate("MainWindow", u"Edit Parameters", None))
|
||||
self.actionCompile_Model.setText(QCoreApplication.translate("MainWindow", u"Compile Model", None))
|
||||
self.actionOpen_Simulation_Window.setText(QCoreApplication.translate("MainWindow", u"Open Simulation Window", None))
|
||||
self.actionAbout.setText(QCoreApplication.translate("MainWindow", u"About", None))
|
||||
self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"File", None))
|
||||
self.menuEdit.setTitle(QCoreApplication.translate("MainWindow", u"Edit", None))
|
||||
self.menuView.setTitle(QCoreApplication.translate("MainWindow", u"View", None))
|
||||
self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"Help", None))
|
||||
self.menuSimulation.setTitle(QCoreApplication.translate("MainWindow", u"Simulation", None))
|
||||
self.fileToolBar.setWindowTitle(QCoreApplication.translate("MainWindow", u"toolBar", None))
|
||||
self.undoToolBar.setWindowTitle(QCoreApplication.translate("MainWindow", u"toolBar", None))
|
||||
self.documentTreeWidget.setWindowTitle(QCoreApplication.translate("MainWindow", u"Document Tree", None))
|
||||
self.logWidget.setWindowTitle(QCoreApplication.translate("MainWindow", u"Log", None))
|
||||
self.simToolBar.setWindowTitle(QCoreApplication.translate("MainWindow", u"toolBar", None))
|
||||
self.libraryWidget.setWindowTitle(QCoreApplication.translate("MainWindow", u"Libraries", None))
|
||||
# retranslateUi
|
||||
|
||||
@@ -148,6 +148,20 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="connectionAnnotationLabel">
|
||||
<property name="text">
|
||||
<string>Connection annotation:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QLineEdit" name="connectionAnnotationEdit">
|
||||
<property name="placeholderText">
|
||||
<string>For example, + or -</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
|
||||
268
src/bedit_gui/ui/forms/port_editor_widget_ui.py
Normal file
268
src/bedit_gui/ui/forms/port_editor_widget_ui.py
Normal file
@@ -0,0 +1,268 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'port_editor_widget.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 (QBrush, QColor, QConicalGradient, QCursor,
|
||||
QFont, QFontDatabase, QGradient, QIcon,
|
||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||
from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QFormLayout,
|
||||
QFrame, QHBoxLayout, QLabel, QLineEdit,
|
||||
QListView, QPlainTextEdit, QPushButton, QRadioButton,
|
||||
QSizePolicy, QSpacerItem, QSpinBox, QVBoxLayout,
|
||||
QWidget)
|
||||
|
||||
class Ui_PortEditor(object):
|
||||
def setupUi(self, PortEditor):
|
||||
if not PortEditor.objectName():
|
||||
PortEditor.setObjectName(u"PortEditor")
|
||||
PortEditor.resize(541, 420)
|
||||
self.horizontalLayout_2 = QHBoxLayout(PortEditor)
|
||||
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
|
||||
self.leftColumn = QVBoxLayout()
|
||||
self.leftColumn.setObjectName(u"leftColumn")
|
||||
self.portList = QListView(PortEditor)
|
||||
self.portList.setObjectName(u"portList")
|
||||
|
||||
self.leftColumn.addWidget(self.portList)
|
||||
|
||||
self.buttonRow = QHBoxLayout()
|
||||
self.buttonRow.setObjectName(u"buttonRow")
|
||||
self.addPort = QPushButton(PortEditor)
|
||||
self.addPort.setObjectName(u"addPort")
|
||||
|
||||
self.buttonRow.addWidget(self.addPort)
|
||||
|
||||
self.removePort = QPushButton(PortEditor)
|
||||
self.removePort.setObjectName(u"removePort")
|
||||
|
||||
self.buttonRow.addWidget(self.removePort)
|
||||
|
||||
|
||||
self.leftColumn.addLayout(self.buttonRow)
|
||||
|
||||
|
||||
self.horizontalLayout_2.addLayout(self.leftColumn)
|
||||
|
||||
self.rightColumn = QVBoxLayout()
|
||||
self.rightColumn.setObjectName(u"rightColumn")
|
||||
self.basicForm = QFormLayout()
|
||||
self.basicForm.setObjectName(u"basicForm")
|
||||
self.nameLabel = QLabel(PortEditor)
|
||||
self.nameLabel.setObjectName(u"nameLabel")
|
||||
|
||||
self.basicForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.nameLabel)
|
||||
|
||||
self.nameEdit = QLineEdit(PortEditor)
|
||||
self.nameEdit.setObjectName(u"nameEdit")
|
||||
|
||||
self.basicForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.nameEdit)
|
||||
|
||||
self.typeLabel = QLabel(PortEditor)
|
||||
self.typeLabel.setObjectName(u"typeLabel")
|
||||
|
||||
self.basicForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.typeLabel)
|
||||
|
||||
self.typeRow = QHBoxLayout()
|
||||
self.typeRow.setObjectName(u"typeRow")
|
||||
self.typeSignal = QRadioButton(PortEditor)
|
||||
self.typeSignal.setObjectName(u"typeSignal")
|
||||
|
||||
self.typeRow.addWidget(self.typeSignal)
|
||||
|
||||
self.typeBond = QRadioButton(PortEditor)
|
||||
self.typeBond.setObjectName(u"typeBond")
|
||||
|
||||
self.typeRow.addWidget(self.typeBond)
|
||||
|
||||
|
||||
self.basicForm.setLayout(1, QFormLayout.ItemRole.FieldRole, self.typeRow)
|
||||
|
||||
self.orientationLabel = QLabel(PortEditor)
|
||||
self.orientationLabel.setObjectName(u"orientationLabel")
|
||||
|
||||
self.basicForm.setWidget(2, QFormLayout.ItemRole.LabelRole, self.orientationLabel)
|
||||
|
||||
self.orientationRow = QHBoxLayout()
|
||||
self.orientationRow.setObjectName(u"orientationRow")
|
||||
self.inputOrientation = QRadioButton(PortEditor)
|
||||
self.inputOrientation.setObjectName(u"inputOrientation")
|
||||
|
||||
self.orientationRow.addWidget(self.inputOrientation)
|
||||
|
||||
self.outputOrientation = QRadioButton(PortEditor)
|
||||
self.outputOrientation.setObjectName(u"outputOrientation")
|
||||
|
||||
self.orientationRow.addWidget(self.outputOrientation)
|
||||
|
||||
|
||||
self.basicForm.setLayout(2, QFormLayout.ItemRole.FieldRole, self.orientationRow)
|
||||
|
||||
self.sizeLabel = QLabel(PortEditor)
|
||||
self.sizeLabel.setObjectName(u"sizeLabel")
|
||||
|
||||
self.basicForm.setWidget(3, QFormLayout.ItemRole.LabelRole, self.sizeLabel)
|
||||
|
||||
self.sizeRow = QHBoxLayout()
|
||||
self.sizeRow.setObjectName(u"sizeRow")
|
||||
self.widthSize = QSpinBox(PortEditor)
|
||||
self.widthSize.setObjectName(u"widthSize")
|
||||
self.widthSize.setMinimum(1)
|
||||
|
||||
self.sizeRow.addWidget(self.widthSize)
|
||||
|
||||
self.heightSize = QSpinBox(PortEditor)
|
||||
self.heightSize.setObjectName(u"heightSize")
|
||||
self.heightSize.setMinimum(1)
|
||||
|
||||
self.sizeRow.addWidget(self.heightSize)
|
||||
|
||||
|
||||
self.basicForm.setLayout(3, QFormLayout.ItemRole.FieldRole, self.sizeRow)
|
||||
|
||||
self.domainLabel = QLabel(PortEditor)
|
||||
self.domainLabel.setObjectName(u"domainLabel")
|
||||
|
||||
self.basicForm.setWidget(4, QFormLayout.ItemRole.LabelRole, self.domainLabel)
|
||||
|
||||
self.multiplicityCheckBox = QCheckBox(PortEditor)
|
||||
self.multiplicityCheckBox.setObjectName(u"multiplicityCheckBox")
|
||||
|
||||
self.basicForm.setWidget(4, QFormLayout.ItemRole.FieldRole, self.multiplicityCheckBox)
|
||||
|
||||
self.connectionAnnotationLabel = QLabel(PortEditor)
|
||||
self.connectionAnnotationLabel.setObjectName(u"connectionAnnotationLabel")
|
||||
|
||||
self.basicForm.setWidget(5, QFormLayout.ItemRole.LabelRole, self.connectionAnnotationLabel)
|
||||
|
||||
self.connectionAnnotationEdit = QLineEdit(PortEditor)
|
||||
self.connectionAnnotationEdit.setObjectName(u"connectionAnnotationEdit")
|
||||
|
||||
self.basicForm.setWidget(5, QFormLayout.ItemRole.FieldRole, self.connectionAnnotationEdit)
|
||||
|
||||
|
||||
self.rightColumn.addLayout(self.basicForm)
|
||||
|
||||
self.line = QFrame(PortEditor)
|
||||
self.line.setObjectName(u"line")
|
||||
self.line.setFrameShape(QFrame.Shape.HLine)
|
||||
self.line.setFrameShadow(QFrame.Shadow.Sunken)
|
||||
|
||||
self.rightColumn.addWidget(self.line)
|
||||
|
||||
self.signalOptions = QFormLayout()
|
||||
self.signalOptions.setObjectName(u"signalOptions")
|
||||
self.signalTypeLabel = QLabel(PortEditor)
|
||||
self.signalTypeLabel.setObjectName(u"signalTypeLabel")
|
||||
|
||||
self.signalOptions.setWidget(0, QFormLayout.ItemRole.LabelRole, self.signalTypeLabel)
|
||||
|
||||
self.signalTypeComboBox = QComboBox(PortEditor)
|
||||
self.signalTypeComboBox.setObjectName(u"signalTypeComboBox")
|
||||
|
||||
self.signalOptions.setWidget(0, QFormLayout.ItemRole.FieldRole, self.signalTypeComboBox)
|
||||
|
||||
|
||||
self.rightColumn.addLayout(self.signalOptions)
|
||||
|
||||
self.bondOptions = QFormLayout()
|
||||
self.bondOptions.setObjectName(u"bondOptions")
|
||||
self.domainLabel_2 = QLabel(PortEditor)
|
||||
self.domainLabel_2.setObjectName(u"domainLabel_2")
|
||||
|
||||
self.bondOptions.setWidget(0, QFormLayout.ItemRole.LabelRole, self.domainLabel_2)
|
||||
|
||||
self.domainComboBox = QComboBox(PortEditor)
|
||||
self.domainComboBox.setObjectName(u"domainComboBox")
|
||||
|
||||
self.bondOptions.setWidget(0, QFormLayout.ItemRole.FieldRole, self.domainComboBox)
|
||||
|
||||
self.causalityLabel = QLabel(PortEditor)
|
||||
self.causalityLabel.setObjectName(u"causalityLabel")
|
||||
|
||||
self.bondOptions.setWidget(1, QFormLayout.ItemRole.LabelRole, self.causalityLabel)
|
||||
|
||||
self.causalityComboBox = QComboBox(PortEditor)
|
||||
self.causalityComboBox.setObjectName(u"causalityComboBox")
|
||||
|
||||
self.bondOptions.setWidget(1, QFormLayout.ItemRole.FieldRole, self.causalityComboBox)
|
||||
|
||||
|
||||
self.rightColumn.addLayout(self.bondOptions)
|
||||
|
||||
self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
||||
|
||||
self.rightColumn.addItem(self.verticalSpacer)
|
||||
|
||||
self.line_2 = QFrame(PortEditor)
|
||||
self.line_2.setObjectName(u"line_2")
|
||||
self.line_2.setFrameShape(QFrame.Shape.HLine)
|
||||
self.line_2.setFrameShadow(QFrame.Shadow.Sunken)
|
||||
|
||||
self.rightColumn.addWidget(self.line_2)
|
||||
|
||||
self.descriptionForm = QFormLayout()
|
||||
self.descriptionForm.setObjectName(u"descriptionForm")
|
||||
self.descriptionForm.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
||||
self.descriptionLabel = QLabel(PortEditor)
|
||||
self.descriptionLabel.setObjectName(u"descriptionLabel")
|
||||
|
||||
self.descriptionForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.descriptionLabel)
|
||||
|
||||
self.descriptionEdit = QPlainTextEdit(PortEditor)
|
||||
self.descriptionEdit.setObjectName(u"descriptionEdit")
|
||||
sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.MinimumExpanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.descriptionEdit.sizePolicy().hasHeightForWidth())
|
||||
self.descriptionEdit.setSizePolicy(sizePolicy)
|
||||
self.descriptionEdit.setMinimumSize(QSize(0, 20))
|
||||
self.descriptionEdit.setMaximumSize(QSize(16777215, 60))
|
||||
|
||||
self.descriptionForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.descriptionEdit)
|
||||
|
||||
|
||||
self.rightColumn.addLayout(self.descriptionForm)
|
||||
|
||||
|
||||
self.horizontalLayout_2.addLayout(self.rightColumn)
|
||||
|
||||
|
||||
self.retranslateUi(PortEditor)
|
||||
|
||||
QMetaObject.connectSlotsByName(PortEditor)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, PortEditor):
|
||||
PortEditor.setWindowTitle(QCoreApplication.translate("PortEditor", u"Form", None))
|
||||
self.addPort.setText(QCoreApplication.translate("PortEditor", u"Add Port", None))
|
||||
self.removePort.setText(QCoreApplication.translate("PortEditor", u"Remove Port", None))
|
||||
self.nameLabel.setText(QCoreApplication.translate("PortEditor", u"Name:", None))
|
||||
self.typeLabel.setText(QCoreApplication.translate("PortEditor", u"Type:", None))
|
||||
self.typeSignal.setText(QCoreApplication.translate("PortEditor", u"Signal", None))
|
||||
self.typeBond.setText(QCoreApplication.translate("PortEditor", u"Power Bond", None))
|
||||
self.orientationLabel.setText(QCoreApplication.translate("PortEditor", u"Orientation:", None))
|
||||
self.inputOrientation.setText(QCoreApplication.translate("PortEditor", u"Input", None))
|
||||
self.outputOrientation.setText(QCoreApplication.translate("PortEditor", u"Output", None))
|
||||
self.sizeLabel.setText(QCoreApplication.translate("PortEditor", u"Size", None))
|
||||
self.widthSize.setSuffix(QCoreApplication.translate("PortEditor", u" rows", None))
|
||||
self.heightSize.setSuffix(QCoreApplication.translate("PortEditor", u" columns", None))
|
||||
self.domainLabel.setText("")
|
||||
self.multiplicityCheckBox.setText(QCoreApplication.translate("PortEditor", u"Allow multiple connections", None))
|
||||
self.connectionAnnotationLabel.setText(QCoreApplication.translate("PortEditor", u"Connection annotation:", None))
|
||||
self.connectionAnnotationEdit.setPlaceholderText(QCoreApplication.translate("PortEditor", u"For example, + or -", None))
|
||||
self.signalTypeLabel.setText(QCoreApplication.translate("PortEditor", u"Signal Type:", None))
|
||||
self.domainLabel_2.setText(QCoreApplication.translate("PortEditor", u"Domain:", None))
|
||||
self.causalityLabel.setText(QCoreApplication.translate("PortEditor", u"Causality:", None))
|
||||
self.descriptionLabel.setText(QCoreApplication.translate("PortEditor", u"Description:", None))
|
||||
# retranslateUi
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
<number>1</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="General">
|
||||
<attribute name="title">
|
||||
@@ -90,6 +90,34 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="Libraries">
|
||||
<attribute name="title">
|
||||
<string>Libraries</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QListView" name="listView"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="addLibButton">
|
||||
<property name="text">
|
||||
<string>Add library</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="addDirButton">
|
||||
<property name="text">
|
||||
<string>Add directory</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
||||
128
src/bedit_gui/ui/forms/settings_dialog_ui.py
Normal file
128
src/bedit_gui/ui/forms/settings_dialog_ui.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'settings_dialog.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 (QBrush, QColor, QConicalGradient, QCursor,
|
||||
QFont, QFontDatabase, QGradient, QIcon,
|
||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog,
|
||||
QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel,
|
||||
QListView, QPushButton, QSizePolicy, QSpacerItem,
|
||||
QSpinBox, QTabWidget, QVBoxLayout, QWidget)
|
||||
|
||||
class Ui_Settings(object):
|
||||
def setupUi(self, Settings):
|
||||
if not Settings.objectName():
|
||||
Settings.setObjectName(u"Settings")
|
||||
Settings.resize(400, 230)
|
||||
self.verticalLayout = QVBoxLayout(Settings)
|
||||
self.verticalLayout.setObjectName(u"verticalLayout")
|
||||
self.tabWidget = QTabWidget(Settings)
|
||||
self.tabWidget.setObjectName(u"tabWidget")
|
||||
self.General = QWidget()
|
||||
self.General.setObjectName(u"General")
|
||||
self.formLayout = QFormLayout(self.General)
|
||||
self.formLayout.setObjectName(u"formLayout")
|
||||
self.logLevel = QComboBox(self.General)
|
||||
self.logLevel.addItem("")
|
||||
self.logLevel.addItem("")
|
||||
self.logLevel.addItem("")
|
||||
self.logLevel.addItem("")
|
||||
self.logLevel.setObjectName(u"logLevel")
|
||||
|
||||
self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.logLevel)
|
||||
|
||||
self.lableLogLevel = QLabel(self.General)
|
||||
self.lableLogLevel.setObjectName(u"lableLogLevel")
|
||||
|
||||
self.formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.lableLogLevel)
|
||||
|
||||
self.labelSnapToGridSize = QLabel(self.General)
|
||||
self.labelSnapToGridSize.setObjectName(u"labelSnapToGridSize")
|
||||
|
||||
self.formLayout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.labelSnapToGridSize)
|
||||
|
||||
self.snapToGridSize = QSpinBox(self.General)
|
||||
self.snapToGridSize.setObjectName(u"snapToGridSize")
|
||||
self.snapToGridSize.setMinimum(1)
|
||||
self.snapToGridSize.setMaximum(256)
|
||||
self.snapToGridSize.setValue(4)
|
||||
|
||||
self.formLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.snapToGridSize)
|
||||
|
||||
self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
||||
|
||||
self.formLayout.setItem(2, QFormLayout.ItemRole.FieldRole, self.verticalSpacer)
|
||||
|
||||
self.tabWidget.addTab(self.General, "")
|
||||
self.Libraries = QWidget()
|
||||
self.Libraries.setObjectName(u"Libraries")
|
||||
self.verticalLayout_2 = QVBoxLayout(self.Libraries)
|
||||
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
|
||||
self.listView = QListView(self.Libraries)
|
||||
self.listView.setObjectName(u"listView")
|
||||
|
||||
self.verticalLayout_2.addWidget(self.listView)
|
||||
|
||||
self.horizontalLayout = QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName(u"horizontalLayout")
|
||||
self.addLibButton = QPushButton(self.Libraries)
|
||||
self.addLibButton.setObjectName(u"addLibButton")
|
||||
|
||||
self.horizontalLayout.addWidget(self.addLibButton)
|
||||
|
||||
self.addDirButton = QPushButton(self.Libraries)
|
||||
self.addDirButton.setObjectName(u"addDirButton")
|
||||
|
||||
self.horizontalLayout.addWidget(self.addDirButton)
|
||||
|
||||
|
||||
self.verticalLayout_2.addLayout(self.horizontalLayout)
|
||||
|
||||
self.tabWidget.addTab(self.Libraries, "")
|
||||
|
||||
self.verticalLayout.addWidget(self.tabWidget)
|
||||
|
||||
self.buttonBox = QDialogButtonBox(Settings)
|
||||
self.buttonBox.setObjectName(u"buttonBox")
|
||||
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
|
||||
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
|
||||
|
||||
self.verticalLayout.addWidget(self.buttonBox)
|
||||
|
||||
|
||||
self.retranslateUi(Settings)
|
||||
self.buttonBox.accepted.connect(Settings.accept)
|
||||
self.buttonBox.rejected.connect(Settings.reject)
|
||||
|
||||
self.tabWidget.setCurrentIndex(1)
|
||||
|
||||
|
||||
QMetaObject.connectSlotsByName(Settings)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, Settings):
|
||||
Settings.setWindowTitle(QCoreApplication.translate("Settings", u"Settings", None))
|
||||
self.logLevel.setItemText(0, QCoreApplication.translate("Settings", u"Debug", None))
|
||||
self.logLevel.setItemText(1, QCoreApplication.translate("Settings", u"Info", None))
|
||||
self.logLevel.setItemText(2, QCoreApplication.translate("Settings", u"Warning", None))
|
||||
self.logLevel.setItemText(3, QCoreApplication.translate("Settings", u"Error", None))
|
||||
|
||||
self.lableLogLevel.setText(QCoreApplication.translate("Settings", u"Log level:", None))
|
||||
self.labelSnapToGridSize.setText(QCoreApplication.translate("Settings", u"Snap-to-grid size:", None))
|
||||
self.tabWidget.setTabText(self.tabWidget.indexOf(self.General), QCoreApplication.translate("Settings", u"General", None))
|
||||
self.addLibButton.setText(QCoreApplication.translate("Settings", u"Add library", None))
|
||||
self.addDirButton.setText(QCoreApplication.translate("Settings", u"Add directory", None))
|
||||
self.tabWidget.setTabText(self.tabWidget.indexOf(self.Libraries), QCoreApplication.translate("Settings", u"Libraries", None))
|
||||
# retranslateUi
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from math import ceil
|
||||
|
||||
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)
|
||||
EMPTY_NATURAL_ICON_SIZE = QSize(32, 32)
|
||||
ICON_MARGIN = 4
|
||||
ICON_PREVIEW_OVERSAMPLE = 4
|
||||
|
||||
@@ -18,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)
|
||||
@@ -43,6 +46,13 @@ def get_pixmap_bounding_box(pixmap: QPixmap) -> QRectF:
|
||||
return QRectF(bounds) if not bounds.isEmpty() else QRectF(0, 0, pixmap.width(), pixmap.height())
|
||||
|
||||
|
||||
def get_natural_icon_size(icon: Icon) -> QSize:
|
||||
bounds = get_bounding_box(icon)
|
||||
if bounds.isEmpty():
|
||||
return QSize(EMPTY_NATURAL_ICON_SIZE)
|
||||
return QSize(max(1, ceil(bounds.width()) + ICON_MARGIN), max(1, ceil(bounds.height()) + ICON_MARGIN))
|
||||
|
||||
|
||||
def render_icon(icon: Icon, ports: dict[PortID, Port], size: QSize = DEFAULT_ICON_SIZE, render_ports: bool = False) -> QIcon:
|
||||
pixmap = QPixmap(size)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
@@ -62,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)))
|
||||
|
||||
@@ -8,6 +8,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from bedit_core.models import Port, PortID
|
||||
from bedit_gui.models import PortMetadata
|
||||
from bedit_gui.views.port_editor_widget import PortEditorWidget
|
||||
|
||||
|
||||
@@ -17,6 +18,7 @@ class InterfaceEditorDialog(QDialog):
|
||||
def __init__(
|
||||
self,
|
||||
ports: dict[PortID, Port],
|
||||
port_metadata: dict[PortID, PortMetadata] | None = None,
|
||||
parent: QWidget | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
@@ -25,7 +27,7 @@ class InterfaceEditorDialog(QDialog):
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.editor = PortEditorWidget(self)
|
||||
self.editor.set_ports(ports)
|
||||
self.editor.set_ports(ports, port_metadata)
|
||||
layout.addWidget(self.editor)
|
||||
|
||||
buttons = QDialogButtonBox(
|
||||
@@ -38,3 +40,6 @@ class InterfaceEditorDialog(QDialog):
|
||||
|
||||
def ports(self) -> dict[PortID, Port]:
|
||||
return self.editor.ports()
|
||||
|
||||
def port_metadata(self) -> dict[PortID, PortMetadata]:
|
||||
return self.editor.port_metadata()
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtWidgets import QDialog, QWidget
|
||||
from PySide6.QtCore import QStringListModel, Qt
|
||||
from PySide6.QtGui import QKeySequence, QShortcut
|
||||
from PySide6.QtWidgets import QAbstractItemView, QDialog, QFileDialog, QWidget
|
||||
|
||||
from bedit_gui.ui.generated.ui_settings_dialog import Ui_Settings
|
||||
|
||||
@@ -21,6 +24,7 @@ class SettingsDialog(QDialog):
|
||||
self,
|
||||
log_level: int,
|
||||
snap_to_grid_size: int,
|
||||
library_paths: list[str],
|
||||
parent: QWidget | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
@@ -37,6 +41,14 @@ class SettingsDialog(QDialog):
|
||||
selected if selected >= 0 else self.LOG_LEVELS.index(logging.INFO)
|
||||
)
|
||||
self.ui.snapToGridSize.setValue(snap_to_grid_size)
|
||||
self._library_paths = QStringListModel(list(library_paths), self)
|
||||
self.ui.listView.setModel(self._library_paths)
|
||||
self.ui.listView.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
self.ui.addLibButton.clicked.connect(self._add_library_file)
|
||||
self.ui.addDirButton.clicked.connect(self._add_library_directory)
|
||||
self._delete_shortcut = QShortcut(QKeySequence.StandardKey.Delete, self.ui.listView)
|
||||
self._delete_shortcut.setContext(Qt.ShortcutContext.WidgetShortcut)
|
||||
self._delete_shortcut.activated.connect(self._delete_selected_paths)
|
||||
|
||||
@property
|
||||
def log_level(self) -> int:
|
||||
@@ -45,3 +57,29 @@ class SettingsDialog(QDialog):
|
||||
@property
|
||||
def snap_to_grid_size(self) -> int:
|
||||
return self.ui.snapToGridSize.value()
|
||||
|
||||
@property
|
||||
def library_paths(self) -> list[str]:
|
||||
return self._library_paths.stringList()
|
||||
|
||||
def _add_library_file(self) -> None:
|
||||
path, _selected_filter = QFileDialog.getOpenFileName(self, "Add BEdit Library", "", "BEdit documents (*.bedit.json *.beb *.json)")
|
||||
if path:
|
||||
self._add_library_path(path)
|
||||
|
||||
def _add_library_directory(self) -> None:
|
||||
path = QFileDialog.getExistingDirectory(self, "Add Library Directory")
|
||||
if path:
|
||||
self._add_library_path(path)
|
||||
|
||||
def _add_library_path(self, path: str) -> None:
|
||||
normalized = str(Path(path).resolve())
|
||||
paths = self._library_paths.stringList()
|
||||
if normalized not in paths:
|
||||
paths.append(normalized)
|
||||
self._library_paths.setStringList(paths)
|
||||
|
||||
def _delete_selected_paths(self) -> None:
|
||||
rows = sorted((index.row() for index in self.ui.listView.selectionModel().selectedRows()), reverse=True)
|
||||
for row in rows:
|
||||
self._library_paths.removeRow(row)
|
||||
|
||||
@@ -3,7 +3,8 @@ from __future__ import annotations
|
||||
from PySide6.QtCore import QEvent, QObject, QTimer, Qt, Signal
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from bedit_core.models import Component, EquationImplementation
|
||||
from bedit_core.models import Component, EquationImplementation, PortID
|
||||
from bedit_gui.models import PortMetadata
|
||||
from bedit_gui.ui.generated.ui_equation_editor_widget import Ui_equationEditorWidget
|
||||
from bedit_gui.views.param_editor_widget import ParamEditorWidget
|
||||
from bedit_gui.views.port_editor_widget import PortEditorWidget
|
||||
@@ -14,6 +15,7 @@ class EquationEditorWidget(QWidget):
|
||||
|
||||
component_changed = Signal(object)
|
||||
equation_text_change_requested = Signal(object, str, object, int)
|
||||
port_metadata_change_requested = Signal(object, object)
|
||||
sidebar_visible_changed = Signal(bool)
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
@@ -22,6 +24,7 @@ class EquationEditorWidget(QWidget):
|
||||
self.ui = Ui_equationEditorWidget()
|
||||
self.ui.setupUi(self)
|
||||
self._component: Component | None = None
|
||||
self._port_metadata: dict[PortID, PortMetadata] = {}
|
||||
self._loading = False
|
||||
self._edit_id = 0
|
||||
self._edit_timer = QTimer(self)
|
||||
@@ -45,11 +48,12 @@ class EquationEditorWidget(QWidget):
|
||||
|
||||
self._set_editors_enabled(False)
|
||||
|
||||
def set_component(self, component: Component | None) -> None:
|
||||
def set_component(self, component: Component | None, port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
|
||||
if component is not None and not isinstance(component.implementation, EquationImplementation):
|
||||
raise TypeError("EquationEditorWidget only supports components with an equation implementation")
|
||||
|
||||
self._component = component
|
||||
self._port_metadata = port_metadata or {}
|
||||
self.refresh()
|
||||
|
||||
def component(self) -> Component | None:
|
||||
@@ -88,7 +92,7 @@ class EquationEditorWidget(QWidget):
|
||||
self.ui.initialEquationsTextEdit.setPlainText("\n".join(implementation.initial_equations))
|
||||
self.ui.equationsTextEdit.setPlainText("\n".join(implementation.equations))
|
||||
self.param_editor.set_params(component.parameters)
|
||||
self.port_editor.set_ports(component.interface.ports)
|
||||
self.port_editor.set_ports(component.interface.ports, self._port_metadata)
|
||||
self._loading = False
|
||||
self._set_editors_enabled(component is not None)
|
||||
|
||||
@@ -145,8 +149,21 @@ class EquationEditorWidget(QWidget):
|
||||
def _ports_changed(self) -> None:
|
||||
if self._loading or self._component is None:
|
||||
return
|
||||
self._component.interface.ports = self.port_editor.ports()
|
||||
ports = self.port_editor.ports()
|
||||
metadata = self.port_editor.port_metadata()
|
||||
if ports != self._component.interface.ports:
|
||||
self._component.interface.ports = ports
|
||||
self.component_changed.emit(self._component)
|
||||
if metadata != self._port_metadata:
|
||||
self._port_metadata = metadata
|
||||
self.port_metadata_change_requested.emit(self._component, metadata)
|
||||
|
||||
def refresh_port_metadata(self, port_metadata: dict[PortID, PortMetadata]) -> None:
|
||||
if port_metadata == self._port_metadata:
|
||||
return
|
||||
self._port_metadata = port_metadata
|
||||
if self._component is not None:
|
||||
self.port_editor.set_ports(self._component.interface.ports, port_metadata)
|
||||
|
||||
def _replace_placeholder(self, placeholder: QWidget, editor: QWidget) -> None:
|
||||
self.ui.verticalLayout_2.replaceWidget(placeholder, editor)
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from enum import Enum
|
||||
from itertools import pairwise
|
||||
from math import hypot
|
||||
|
||||
from PySide6.QtCore import QEvent, QObject, QPointF, QRectF, QSize, QTimer, Qt, Signal
|
||||
from PySide6.QtGui import QActionGroup, QBrush, QColor, QCursor, QKeySequence, QMouseEvent, QPainter, QPainterPath, QPen, QShortcut, QWheelEvent
|
||||
from PySide6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem, QGraphicsPathItem, QGraphicsPixmapItem, QGraphicsScene, QGraphicsSceneMouseEvent, QGraphicsView, QMenu, QWidget
|
||||
from PySide6.QtCore import QEvent, QObject, QPointF, QRectF, QTimer, Qt, Signal
|
||||
from PySide6.QtGui import QActionGroup, QBrush, QColor, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, QKeySequence, QMouseEvent, QPainter, QPainterPath, QPen, QShortcut, QWheelEvent
|
||||
from PySide6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem, QGraphicsPathItem, QGraphicsPixmapItem, QGraphicsScene, QGraphicsSceneMouseEvent, QGraphicsTextItem, QGraphicsView, QMenu, QWidget
|
||||
|
||||
from bedit_core.models import BondCausality, BondConnection, BondPort, Component, ComponentID, Connection, ConnectionID, GraphImplementation, Port, PortID, SignalConnection, SignalDirection, SignalPort
|
||||
from bedit_gui.models import Graph, GraphConnection, Icon
|
||||
from bedit_gui.models import Graph, GraphComponentLabel, GraphConnection, Icon, PortMetadata
|
||||
from bedit_gui.services.component_clipboard import COMPONENTS_MIME
|
||||
from bedit_gui.ui.generated.ui_graph_editor_widget import Ui_graphEditorWidget
|
||||
from bedit_gui.utils.icon import get_pixmap_bounding_box, render_icon
|
||||
from bedit_gui.utils.icon import get_natural_icon_size, get_pixmap_bounding_box, render_icon
|
||||
|
||||
GRID_SPACING = 64
|
||||
SCENE_SIZE = 10000
|
||||
@@ -19,14 +21,22 @@ MIN_ZOOM = 0.2
|
||||
MAX_ZOOM = 4.0
|
||||
ZOOM_STEP = 1.15
|
||||
ZOOM_TO_FIT_PADDING = 32.0
|
||||
COMPONENT_ICON_SIZE = QSize(128, 128)
|
||||
COMPONENT_LABEL_FONT_SIZE = 24.0
|
||||
FALLBACK_COMPONENT_SPACING = 128
|
||||
CONNECTION_WIDTH = 4.0
|
||||
CONNECTION_BOUNDING_BOX_SPACING = 16.0
|
||||
BOND_CONNECTION_COLOR = "#000000"
|
||||
SIGNAL_CONNECTION_COLOR = "#00007f"
|
||||
BOND_CONNECTION_BOUNDING_BOX_SPACING = 16.0
|
||||
SIGNAL_CONNECTION_BOUNDING_BOX_SPACING = 0.0
|
||||
CONNECTION_STRAIGHTEN_TOLERANCE = 8.0
|
||||
ARROW_LENGTH = 32.0
|
||||
ARROW_HALF_WIDTH = 16.0
|
||||
SIGNAL_ARROW_LENGTH = ARROW_LENGTH / 2
|
||||
SIGNAL_ARROW_HALF_WIDTH = ARROW_HALF_WIDTH / 2
|
||||
CAUSALITY_TICK_HALF_LENGTH = 16.0
|
||||
CONNECTION_ANNOTATION_FONT_SIZE = 18.0
|
||||
CONNECTION_ANNOTATION_BACK_OFFSET = 24.0
|
||||
CONNECTION_ANNOTATION_SIDE_OFFSET = 14.0
|
||||
|
||||
|
||||
class GraphEditorMode(Enum):
|
||||
@@ -63,8 +73,10 @@ class GraphConnectionItem(QGraphicsPathItem):
|
||||
self.connection_id = connection_id
|
||||
self.editor = editor
|
||||
self.setPath(self._connection_path(points, half_arrow, tick_at_source))
|
||||
pen = QPen(QColor("#202020"), CONNECTION_WIDTH)
|
||||
color = QColor(BOND_CONNECTION_COLOR if half_arrow else SIGNAL_CONNECTION_COLOR)
|
||||
pen = QPen(color, CONNECTION_WIDTH)
|
||||
self.setPen(pen)
|
||||
self.setBrush(QBrush(color))
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
||||
self.setZValue(-1)
|
||||
|
||||
@@ -97,15 +109,17 @@ class GraphConnectionItem(QGraphicsPathItem):
|
||||
dx = target.x() - previous.x()
|
||||
dy = target.y() - previous.y()
|
||||
length = hypot(dx, dy)
|
||||
back_x = target.x() - ARROW_LENGTH * dx / length
|
||||
back_y = target.y() - ARROW_LENGTH * dy / length
|
||||
perpendicular_x = -ARROW_HALF_WIDTH * dy / length
|
||||
perpendicular_y = ARROW_HALF_WIDTH * dx / length
|
||||
arrow_length = ARROW_LENGTH if half_arrow else SIGNAL_ARROW_LENGTH
|
||||
arrow_half_width = ARROW_HALF_WIDTH if half_arrow else SIGNAL_ARROW_HALF_WIDTH
|
||||
back_x = target.x() - arrow_length * dx / length
|
||||
back_y = target.y() - arrow_length * dy / length
|
||||
perpendicular_x = -arrow_half_width * dy / length
|
||||
perpendicular_y = arrow_half_width * dx / length
|
||||
path.moveTo(target)
|
||||
path.lineTo(back_x + perpendicular_x, back_y + perpendicular_y)
|
||||
if not half_arrow:
|
||||
path.moveTo(target)
|
||||
path.lineTo(back_x - perpendicular_x, back_y - perpendicular_y)
|
||||
path.closeSubpath()
|
||||
if tick_at_source is not None:
|
||||
GraphConnectionItem._add_causality_tick(path, points, tick_at_source)
|
||||
return path
|
||||
@@ -141,7 +155,7 @@ class GraphComponentItem(QGraphicsPixmapItem):
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||
|
||||
def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value: object) -> object:
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange and self._dragging and isinstance(value, QPointF):
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange and self.component_id in self.editor._component_drag_starts and isinstance(value, QPointF):
|
||||
grid_size = self.editor.snap_to_grid_size
|
||||
value = QPointF(round(value.x() / grid_size) * grid_size, round(value.y() / grid_size) * grid_size)
|
||||
result = super().itemChange(change, value)
|
||||
@@ -151,23 +165,22 @@ 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())
|
||||
self._dragging = True
|
||||
super().mousePressEvent(event)
|
||||
self.editor.begin_component_move()
|
||||
|
||||
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)
|
||||
self._dragging = False
|
||||
position = (round(self.pos().x()), round(self.pos().y()))
|
||||
self.setPos(*position)
|
||||
if self.pos() != self._drag_start:
|
||||
self.editor.finish_component_move(self.component_id, position)
|
||||
self.editor.finish_component_moves()
|
||||
|
||||
def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
@@ -184,6 +197,58 @@ class GraphComponentItem(QGraphicsPixmapItem):
|
||||
event.accept()
|
||||
|
||||
|
||||
class GraphComponentLabelItem(QGraphicsTextItem):
|
||||
def __init__(self, component_id: ComponentID, text: str, label: GraphComponentLabel, component_item: GraphComponentItem, editor: GraphEditorWidget) -> None:
|
||||
super().__init__(text, component_item)
|
||||
self.component_id = component_id
|
||||
self.editor = editor
|
||||
self._dragging = False
|
||||
self._drag_start = label.relative_position
|
||||
font = self.font()
|
||||
font.setItalic(True)
|
||||
font.setPointSizeF(COMPONENT_LABEL_FONT_SIZE)
|
||||
self.setFont(font)
|
||||
self.setDefaultTextColor(QColor("#202020"))
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, editor.mode is GraphEditorMode.NORMAL)
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||
self.set_relative_position(label.relative_position)
|
||||
|
||||
def relative_position(self) -> tuple[int, int]:
|
||||
component_bounds = self.editor._component_bounds[self.component_id]
|
||||
return round(self.pos().x() + self.boundingRect().width() / 2), round(self.pos().y() - component_bounds.bottom())
|
||||
|
||||
def set_relative_position(self, position: tuple[int, int]) -> None:
|
||||
component_bounds = self.editor._component_bounds[self.component_id]
|
||||
self.setPos(position[0] - self.boundingRect().width() / 2, component_bounds.bottom() + position[1])
|
||||
|
||||
def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value: object) -> object:
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange and self._dragging and isinstance(value, QPointF):
|
||||
component_bounds = self.editor._component_bounds[self.component_id]
|
||||
size = self.editor.snap_to_grid_size
|
||||
relative_x = value.x() + self.boundingRect().width() / 2
|
||||
relative_y = value.y() - component_bounds.bottom()
|
||||
value = QPointF(round(relative_x / size) * size - self.boundingRect().width() / 2, component_bounds.bottom() + round(relative_y / size) * size)
|
||||
return super().itemChange(change, value)
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||
self._drag_start = self.relative_position()
|
||||
self._dragging = True
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||
super().mouseReleaseEvent(event)
|
||||
self._dragging = False
|
||||
position = self.relative_position()
|
||||
self.set_relative_position(position)
|
||||
if position != self._drag_start:
|
||||
self.editor.finish_component_label_move(self.component_id, position)
|
||||
|
||||
def contextMenuEvent(self, event) -> None:
|
||||
self.editor.component_context_menu_requested.emit(self.component_id, event.screenPos())
|
||||
event.accept()
|
||||
|
||||
|
||||
class GraphConnectionPointItem(QGraphicsEllipseItem):
|
||||
def __init__(self, connection_id: ConnectionID, index: int, position: tuple[int, int], editor: GraphEditorWidget) -> None:
|
||||
radius = CONNECTION_WIDTH
|
||||
@@ -231,12 +296,14 @@ class GraphConnectionPointItem(QGraphicsEllipseItem):
|
||||
|
||||
|
||||
class GraphEditorWidget(QWidget):
|
||||
component_move_requested = Signal(object, object, object)
|
||||
component_moves_requested = Signal(object, object)
|
||||
component_context_menu_requested = Signal(object, object)
|
||||
component_open_requested = Signal(object)
|
||||
component_label_move_requested = Signal(object, object, object)
|
||||
connection_points_change_requested = Signal(object, object, object, str)
|
||||
connection_add_requested = Signal(object, object)
|
||||
connections_delete_requested = Signal(object, object)
|
||||
component_drop_requested = Signal(object, object, object)
|
||||
|
||||
def __init__(self, parent: QWidget | None = None, snap_to_grid_size: int = 4) -> None:
|
||||
super().__init__(parent)
|
||||
@@ -246,19 +313,25 @@ class GraphEditorWidget(QWidget):
|
||||
self._component: Component | None = None
|
||||
self._graph = Graph()
|
||||
self._icons: dict[ComponentID, Icon] = {}
|
||||
self._port_metadata: dict[PortID, PortMetadata] = {}
|
||||
self._mode = GraphEditorMode.NORMAL
|
||||
self._connection_start: ComponentID | None = None
|
||||
self._connection_preview: QGraphicsPathItem | None = None
|
||||
self._component_items: dict[ComponentID, GraphComponentItem] = {}
|
||||
self._component_drag_starts: dict[ComponentID, tuple[int, int]] = {}
|
||||
self._component_bounds: dict[ComponentID, QRectF] = {}
|
||||
self._component_label_items: dict[ComponentID, GraphComponentLabelItem] = {}
|
||||
self._connection_items: dict[ConnectionID, GraphConnectionItem] = {}
|
||||
self._connection_point_items: dict[ConnectionID, list[GraphConnectionPointItem]] = {}
|
||||
self._connection_annotation_items: dict[ConnectionID, QGraphicsTextItem] = {}
|
||||
self.set_snap_to_grid_size(snap_to_grid_size)
|
||||
self.scene = GraphGraphicsScene(self)
|
||||
self.scene.setSceneRect(-SCENE_SIZE / 2, -SCENE_SIZE / 2, SCENE_SIZE, SCENE_SIZE)
|
||||
self.ui.graphicsView.setScene(self.scene)
|
||||
self.ui.graphicsView.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
self.ui.graphicsView.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
||||
self.ui.graphicsView.viewport().installEventFilter(self)
|
||||
self.ui.graphicsView.viewport().setAcceptDrops(True)
|
||||
self._mode_actions = QActionGroup(self)
|
||||
self._mode_actions.setExclusive(True)
|
||||
self._mode_actions.addAction(self.ui.actionMouseMode)
|
||||
@@ -295,12 +368,12 @@ class GraphEditorWidget(QWidget):
|
||||
self._clear_connection_start()
|
||||
component = self._component
|
||||
if component is not None:
|
||||
self.set_component(component, self._graph, self._icons)
|
||||
self.set_component(component, self._graph, self._icons, self._port_metadata)
|
||||
|
||||
def toggle_mode(self) -> None:
|
||||
self.set_mode(GraphEditorMode.CONNECTION if self._mode is GraphEditorMode.NORMAL else GraphEditorMode.NORMAL)
|
||||
|
||||
def set_component(self, component: Component | None, graph: Graph | None = None, icons: dict[ComponentID, Icon] | None = None) -> None:
|
||||
def set_component(self, component: Component | None, graph: Graph | None = None, icons: dict[ComponentID, Icon] | None = None, port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
|
||||
if component is not None and not isinstance(component.implementation, GraphImplementation):
|
||||
raise TypeError("GraphEditorWidget only supports components with a graph implementation")
|
||||
component_changed = component is not self._component
|
||||
@@ -308,10 +381,14 @@ class GraphEditorWidget(QWidget):
|
||||
self._component = component
|
||||
self._graph = graph or Graph()
|
||||
self._icons = icons or {}
|
||||
self._port_metadata = port_metadata or {}
|
||||
self._component_drag_starts = {}
|
||||
self._component_items = {}
|
||||
self._component_bounds = {}
|
||||
self._component_label_items = {}
|
||||
self._connection_items = {}
|
||||
self._connection_point_items = {}
|
||||
self._connection_annotation_items = {}
|
||||
self.scene.clear()
|
||||
if component is None:
|
||||
return
|
||||
@@ -321,7 +398,8 @@ class GraphEditorWidget(QWidget):
|
||||
positions = {component_id: graph.component_positions.get(component_id, (index * FALLBACK_COMPONENT_SPACING, 0)) for index, component_id in enumerate(component.implementation.graph.components)}
|
||||
for component_id, child in component.implementation.graph.components.items():
|
||||
icon = icons.get(component_id, Icon())
|
||||
pixmap = render_icon(icon, child.interface.ports, COMPONENT_ICON_SIZE, render_ports=self._mode is GraphEditorMode.CONNECTION).pixmap(COMPONENT_ICON_SIZE)
|
||||
icon_size = get_natural_icon_size(icon)
|
||||
pixmap = render_icon(icon, child.interface.ports, icon_size, render_ports=self._mode is GraphEditorMode.CONNECTION).pixmap(icon_size)
|
||||
item = GraphComponentItem(component_id, pixmap, self)
|
||||
item.setOffset(-pixmap.width() / 2, -pixmap.height() / 2)
|
||||
item.setPos(*positions[component_id])
|
||||
@@ -331,6 +409,11 @@ class GraphEditorWidget(QWidget):
|
||||
self._component_bounds[component_id] = bounds.translated(-pixmap.width() / 2, -pixmap.height() / 2)
|
||||
self.scene.addItem(item)
|
||||
|
||||
for component_id, child in component.implementation.graph.components.items():
|
||||
label = graph.component_labels.get(component_id, GraphComponentLabel())
|
||||
if label.visible:
|
||||
self._create_component_label_item(component_id, child.name, label)
|
||||
|
||||
for connection_id, connection in component.implementation.graph.connections.items():
|
||||
if not isinstance(connection, (SignalConnection, BondConnection)):
|
||||
continue
|
||||
@@ -344,6 +427,17 @@ class GraphEditorWidget(QWidget):
|
||||
connection_item.setData(0, str(connection_id))
|
||||
self._connection_items[connection_id] = connection_item
|
||||
self.scene.addItem(connection_item)
|
||||
annotation = self._port_metadata.get(connection.target, PortMetadata()).connection_annotation if isinstance(connection, SignalConnection) else None
|
||||
if annotation:
|
||||
annotation_item = QGraphicsTextItem(annotation)
|
||||
font = annotation_item.font()
|
||||
font.setBold(True)
|
||||
font.setPointSizeF(CONNECTION_ANNOTATION_FONT_SIZE)
|
||||
annotation_item.setFont(font)
|
||||
annotation_item.setDefaultTextColor(QColor(SIGNAL_CONNECTION_COLOR))
|
||||
annotation_item.setZValue(1)
|
||||
self._connection_annotation_items[connection_id] = annotation_item
|
||||
self.scene.addItem(annotation_item)
|
||||
visual_connection = graph.connections.get(connection_id)
|
||||
self._create_connection_point_items(connection_id, visual_connection.points[1:-1] if visual_connection is not None and len(visual_connection.points) >= 2 else [])
|
||||
self.refresh_connections()
|
||||
@@ -370,11 +464,29 @@ class GraphEditorWidget(QWidget):
|
||||
points = self._straighten_direct_connection(points)
|
||||
source_bounds = self._component_bounds[source_component].translated(*source_position)
|
||||
target_bounds = self._component_bounds[target_component].translated(*target_position)
|
||||
points = self._clip_connection(points, source_bounds, target_bounds)
|
||||
spacing = BOND_CONNECTION_BOUNDING_BOX_SPACING if isinstance(connection, BondConnection) else SIGNAL_CONNECTION_BOUNDING_BOX_SPACING
|
||||
points = self._clip_connection(points, source_bounds, target_bounds, spacing)
|
||||
tick_at_source = None
|
||||
if isinstance(connection, BondConnection):
|
||||
tick_at_source = False if connection.causality is BondCausality.EFFORT_OUT else True if connection.causality is BondCausality.FLOW_OUT else None
|
||||
item.setPath(item._connection_path(points, isinstance(connection, BondConnection), tick_at_source))
|
||||
annotation_item = self._connection_annotation_items.get(connection_id)
|
||||
if annotation_item is not None:
|
||||
self._position_connection_annotation(annotation_item, points)
|
||||
|
||||
@staticmethod
|
||||
def _position_connection_annotation(item: QGraphicsTextItem, points: list[tuple[float, float]]) -> None:
|
||||
target = QPointF(*points[-1])
|
||||
previous = next((QPointF(*point) for point in reversed(points[:-1]) if QPointF(*point) != target), None)
|
||||
if previous is None:
|
||||
return
|
||||
dx = target.x() - previous.x()
|
||||
dy = target.y() - previous.y()
|
||||
length = hypot(dx, dy)
|
||||
x = target.x() - CONNECTION_ANNOTATION_BACK_OFFSET * dx / length - CONNECTION_ANNOTATION_SIDE_OFFSET * dy / length
|
||||
y = target.y() - CONNECTION_ANNOTATION_BACK_OFFSET * dy / length + CONNECTION_ANNOTATION_SIDE_OFFSET * dx / length
|
||||
bounds = item.boundingRect()
|
||||
item.setPos(x - bounds.width() / 2, y - bounds.height() / 2)
|
||||
|
||||
def add_connection_point(self, connection_id: ConnectionID, scene_position: QPointF) -> None:
|
||||
points = self._connection_metadata_points(connection_id)
|
||||
@@ -400,6 +512,9 @@ class GraphEditorWidget(QWidget):
|
||||
connection_item = self._connection_items.pop(connection_id, None)
|
||||
if connection_item is not None:
|
||||
self.scene.removeItem(connection_item)
|
||||
annotation_item = self._connection_annotation_items.pop(connection_id, None)
|
||||
if annotation_item is not None:
|
||||
self.scene.removeItem(annotation_item)
|
||||
return
|
||||
self._graph.connections[connection_id] = GraphConnection(points=list(points))
|
||||
interior_points = points[1:-1]
|
||||
@@ -455,9 +570,48 @@ class GraphEditorWidget(QWidget):
|
||||
best_distance = distance
|
||||
return best_index
|
||||
|
||||
def finish_component_move(self, component_id: ComponentID, position: tuple[int, int]) -> None:
|
||||
def begin_component_move(self) -> None:
|
||||
self._component_drag_starts = {item.component_id: (round(item.pos().x()), round(item.pos().y())) for item in self.scene.selectedItems() if isinstance(item, GraphComponentItem)}
|
||||
|
||||
def finish_component_moves(self) -> None:
|
||||
starts = self._component_drag_starts
|
||||
self._component_drag_starts = {}
|
||||
if self._component is None:
|
||||
return
|
||||
positions = {}
|
||||
for component_id, old_position in starts.items():
|
||||
item = self._component_items[component_id]
|
||||
position = (round(item.pos().x()), round(item.pos().y()))
|
||||
item.setPos(*position)
|
||||
if position != old_position:
|
||||
positions[component_id] = position
|
||||
if positions:
|
||||
self.component_moves_requested.emit(self._component, positions)
|
||||
|
||||
def finish_component_label_move(self, component_id: ComponentID, relative_position: tuple[int, int]) -> None:
|
||||
if self._component is not None:
|
||||
self.component_move_requested.emit(self._component, component_id, position)
|
||||
self.component_label_move_requested.emit(self._component, component_id, relative_position)
|
||||
|
||||
def component_label_visible(self, component_id: ComponentID) -> bool:
|
||||
return self._graph.component_labels.get(component_id, GraphComponentLabel()).visible
|
||||
|
||||
def set_component_label(self, component_id: ComponentID, label: GraphComponentLabel | None) -> None:
|
||||
if label is None:
|
||||
self._graph.component_labels.pop(component_id, None)
|
||||
label = GraphComponentLabel()
|
||||
else:
|
||||
self._graph.component_labels[component_id] = label
|
||||
item = self._component_label_items.pop(component_id, None)
|
||||
if item is not None:
|
||||
item.setParentItem(None)
|
||||
self.scene.removeItem(item)
|
||||
component = self._component
|
||||
if component is not None and label.visible and component_id in component.implementation.graph.components:
|
||||
self._create_component_label_item(component_id, component.implementation.graph.components[component_id].name, label)
|
||||
|
||||
def _create_component_label_item(self, component_id: ComponentID, text: str, label: GraphComponentLabel) -> None:
|
||||
item = GraphComponentLabelItem(component_id, text, label, self._component_items[component_id], self)
|
||||
self._component_label_items[component_id] = item
|
||||
|
||||
def set_component_position(self, component_id: ComponentID, position: tuple[int, int] | None) -> None:
|
||||
item = self._component_items.get(component_id)
|
||||
@@ -492,7 +646,7 @@ class GraphEditorWidget(QWidget):
|
||||
return points
|
||||
|
||||
@staticmethod
|
||||
def _clip_connection(points: list[tuple[float, float]], source_bounds: QRectF, target_bounds: QRectF) -> list[tuple[float, float]]:
|
||||
def _clip_connection(points: list[tuple[float, float]], source_bounds: QRectF, target_bounds: QRectF, spacing: float) -> list[tuple[float, float]]:
|
||||
source = points[0]
|
||||
target = points[-1]
|
||||
source_direction = next((point for point in points[1:] if point != source), None)
|
||||
@@ -500,13 +654,13 @@ class GraphEditorWidget(QWidget):
|
||||
if source_direction is None or target_direction is None:
|
||||
return points
|
||||
clipped = list(points)
|
||||
clipped[0] = GraphEditorWidget._bounding_box_edge(source_bounds, source, source_direction)
|
||||
clipped[-1] = GraphEditorWidget._bounding_box_edge(target_bounds, target, target_direction)
|
||||
clipped[0] = GraphEditorWidget._bounding_box_edge(source_bounds, source, source_direction, spacing)
|
||||
clipped[-1] = GraphEditorWidget._bounding_box_edge(target_bounds, target, target_direction, spacing)
|
||||
return clipped
|
||||
|
||||
@staticmethod
|
||||
def _bounding_box_edge(bounds: QRectF, origin: tuple[float, float], toward: tuple[float, float]) -> tuple[float, float]:
|
||||
bounds = bounds.adjusted(-CONNECTION_BOUNDING_BOX_SPACING, -CONNECTION_BOUNDING_BOX_SPACING, CONNECTION_BOUNDING_BOX_SPACING, CONNECTION_BOUNDING_BOX_SPACING)
|
||||
def _bounding_box_edge(bounds: QRectF, origin: tuple[float, float], toward: tuple[float, float], spacing: float) -> tuple[float, float]:
|
||||
bounds = bounds.adjusted(-spacing, -spacing, spacing, spacing)
|
||||
dx = toward[0] - origin[0]
|
||||
dy = toward[1] - origin[1]
|
||||
horizontal_scale = (bounds.right() - origin[0]) / dx if dx > 0 else (bounds.left() - origin[0]) / dx if dx < 0 else float("inf")
|
||||
@@ -574,7 +728,8 @@ class GraphEditorWidget(QWidget):
|
||||
self._connection_preview.setPath(QPainterPath(mouse_position))
|
||||
return
|
||||
bounds = self._component_bounds[self._connection_start].translated(*source)
|
||||
start = self._bounding_box_edge(bounds, source, (mouse_position.x(), mouse_position.y()))
|
||||
spacing = max(BOND_CONNECTION_BOUNDING_BOX_SPACING, SIGNAL_CONNECTION_BOUNDING_BOX_SPACING)
|
||||
start = self._bounding_box_edge(bounds, source, (mouse_position.x(), mouse_position.y()), spacing)
|
||||
path = QPainterPath(QPointF(*start))
|
||||
path.lineTo(mouse_position)
|
||||
self._connection_preview.setPath(path)
|
||||
@@ -621,7 +776,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
|
||||
@@ -650,6 +805,25 @@ class GraphEditorWidget(QWidget):
|
||||
self.ui.graphicsView.centerOn(bounds.center())
|
||||
|
||||
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
|
||||
if watched is self.ui.graphicsView.viewport() and event.type() in (QEvent.Type.DragEnter, QEvent.Type.DragMove):
|
||||
assert isinstance(event, (QDragEnterEvent, QDragMoveEvent))
|
||||
if self._component is not None and event.mimeData().hasFormat(COMPONENTS_MIME):
|
||||
event.setDropAction(Qt.DropAction.CopyAction)
|
||||
event.accept()
|
||||
return True
|
||||
if watched is self.ui.graphicsView.viewport() and event.type() == QEvent.Type.Drop:
|
||||
assert isinstance(event, QDropEvent)
|
||||
if self._component is not None and event.mimeData().hasFormat(COMPONENTS_MIME):
|
||||
try:
|
||||
payload = json.loads(bytes(event.mimeData().data(COMPONENTS_MIME)).decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
return True
|
||||
if isinstance(payload, dict):
|
||||
position = self._snap_position(self.ui.graphicsView.mapToScene(event.position().toPoint()))
|
||||
self.component_drop_requested.emit(self._component, payload, position)
|
||||
event.setDropAction(Qt.DropAction.CopyAction)
|
||||
event.accept()
|
||||
return True
|
||||
if watched is self.ui.graphicsView.viewport() and event.type() == QEvent.Type.MouseMove:
|
||||
assert isinstance(event, QMouseEvent)
|
||||
self._update_connection_preview(self.ui.graphicsView.mapToScene(event.position().toPoint()))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,10 +6,12 @@ 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
|
||||
|
||||
|
||||
class ShapeCreationTool(Protocol):
|
||||
@@ -24,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)
|
||||
@@ -67,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
|
||||
@@ -107,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__()
|
||||
@@ -126,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
|
||||
@@ -224,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)
|
||||
@@ -356,7 +411,7 @@ class IconGraphicsScene(QGraphicsScene):
|
||||
super().__init__(parent)
|
||||
self._tool: ShapeCreationTool | None = None
|
||||
self._ports: dict[PortID, Port] = {}
|
||||
self.setSceneRect(-64, -64, 128, 128)
|
||||
self.setSceneRect(-ICON_SCENE_SIZE / 2, -ICON_SCENE_SIZE / 2, ICON_SCENE_SIZE, ICON_SCENE_SIZE)
|
||||
|
||||
def set_ports(self, ports: dict[PortID, Port]) -> None:
|
||||
self._ports = deepcopy(ports)
|
||||
@@ -438,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
|
||||
|
||||
@@ -22,19 +22,21 @@ class DocumentTreeModel(QAbstractItemModel):
|
||||
rename_document_requested = Signal(str)
|
||||
rename_component_requested = Signal(Component, str)
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, editable: bool = True) -> None:
|
||||
super().__init__()
|
||||
self._document: CoreDocument | None = None
|
||||
self._editable = editable
|
||||
self._root = DocumentTreeNode("Document", None, None, None, [])
|
||||
self._component_icons: dict[ComponentID, QIcon] = {}
|
||||
self._component_nodes: dict[ComponentID, DocumentTreeNode] = {}
|
||||
|
||||
def set_document(self, document: CoreDocument) -> None:
|
||||
self.set_documents([document])
|
||||
|
||||
def set_documents(self, documents: list[CoreDocument]) -> None:
|
||||
self.beginResetModel()
|
||||
self._document = document
|
||||
self._component_icons = {}
|
||||
self._component_nodes = {}
|
||||
self._root = self._build_tree(document)
|
||||
self._root = self._build_tree(documents)
|
||||
self.endResetModel()
|
||||
|
||||
def set_component_icon(self, component_id: ComponentID, icon: QIcon) -> None:
|
||||
@@ -146,12 +148,12 @@ class DocumentTreeModel(QAbstractItemModel):
|
||||
node = index.internalPointer()
|
||||
|
||||
# Make the document root node editable
|
||||
if index.column() == 0 and isinstance(node, DocumentTreeNode) and isinstance(node.value, (CoreDocument, Component)):
|
||||
if self._editable and index.column() == 0 and isinstance(node, DocumentTreeNode) and isinstance(node.value, (CoreDocument, Component)):
|
||||
flags |= Qt.ItemFlag.ItemIsEditable
|
||||
|
||||
return flags
|
||||
|
||||
def _build_tree(self, document: CoreDocument) -> DocumentTreeNode:
|
||||
def _build_tree(self, documents: list[CoreDocument]) -> DocumentTreeNode:
|
||||
# QT's invisible root
|
||||
root = DocumentTreeNode(
|
||||
name="",
|
||||
@@ -160,19 +162,18 @@ class DocumentTreeModel(QAbstractItemModel):
|
||||
parent=None,
|
||||
children=[],
|
||||
)
|
||||
# Add itself as a child so the document root is visible in the tree
|
||||
document_root = DocumentTreeNode(name=document.name, value=document, component_id=None, parent=root, children=[])
|
||||
root.children.append(document_root)
|
||||
|
||||
def _list_children(root: DocumentTreeNode, components: dict[ComponentID, Component]) -> None:
|
||||
def _list_children(parent: DocumentTreeNode, components: dict[ComponentID, Component]) -> None:
|
||||
for component_id, component in sorted(components.items(), key=lambda item: (item[1].name.casefold(), item[1].name, str(item[0]))):
|
||||
component_node = DocumentTreeNode(name=component.name, value=component, component_id=component_id, parent=root, children=[])
|
||||
root.children.append(component_node)
|
||||
component_node = DocumentTreeNode(name=component.name, value=component, component_id=component_id, parent=parent, children=[])
|
||||
parent.children.append(component_node)
|
||||
self._component_nodes[component_id] = component_node
|
||||
|
||||
if isinstance(component.implementation, GraphImplementation):
|
||||
_list_children(component_node, component.implementation.graph.components)
|
||||
|
||||
for document in documents:
|
||||
document_root = DocumentTreeNode(name=document.name, value=document, component_id=None, parent=root, children=[])
|
||||
root.children.append(document_root)
|
||||
_list_children(document_root, document.root)
|
||||
|
||||
return root
|
||||
|
||||
55
src/bedit_gui/views/models/library_tree_model.py
Normal file
55
src/bedit_gui/views/models/library_tree_model.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Callable
|
||||
|
||||
from PySide6.QtCore import QMimeData, QModelIndex, Qt
|
||||
|
||||
from bedit_core.models import Component
|
||||
from bedit_gui.services.component_clipboard import COMPONENTS_MIME
|
||||
from bedit_gui.views.models.document_tree_model import DocumentTreeModel
|
||||
|
||||
PayloadFactory = Callable[[list[Component]], dict]
|
||||
|
||||
|
||||
class LibraryTreeModel(DocumentTreeModel):
|
||||
def __init__(self, payload_factory: PayloadFactory) -> None:
|
||||
super().__init__(editable=False)
|
||||
self._payload_factory = payload_factory
|
||||
|
||||
def flags(self, index: QModelIndex) -> Qt.ItemFlag:
|
||||
flags = super().flags(index)
|
||||
if isinstance(self.value(index), Component):
|
||||
flags |= Qt.ItemFlag.ItemIsDragEnabled
|
||||
return flags
|
||||
|
||||
def mimeTypes(self) -> list[str]:
|
||||
return [COMPONENTS_MIME]
|
||||
|
||||
def mimeData(self, indexes: list[QModelIndex]) -> QMimeData:
|
||||
rows = [index for index in indexes if index.column() == 0 and isinstance(self.value(index), Component)]
|
||||
selected = {id(self.value(index)) for index in rows}
|
||||
components = []
|
||||
added = set()
|
||||
for index in rows:
|
||||
parent = index.parent()
|
||||
if any(id(self.value(parent_index)) in selected for parent_index in self._parents(parent)):
|
||||
continue
|
||||
component = self.value(index)
|
||||
if isinstance(component, Component) and id(component) not in added:
|
||||
components.append(component)
|
||||
added.add(id(component))
|
||||
mime = QMimeData()
|
||||
if components:
|
||||
mime.setData(COMPONENTS_MIME, json.dumps(self._payload_factory(components)).encode("utf-8"))
|
||||
mime.setText("\n".join(component.name for component in components))
|
||||
return mime
|
||||
|
||||
def supportedDragActions(self) -> Qt.DropAction:
|
||||
return Qt.DropAction.CopyAction
|
||||
|
||||
@staticmethod
|
||||
def _parents(index: QModelIndex):
|
||||
while index.isValid():
|
||||
yield index
|
||||
index = index.parent()
|
||||
@@ -7,6 +7,7 @@ from PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide6.QtWidgets import QButtonGroup, QLayout, QWidget
|
||||
|
||||
from bedit_core.models import BondPort, Port, PortCausality, PortID, SignalDirection, SignalPort, ValueType
|
||||
from bedit_gui.models import PortMetadata
|
||||
from bedit_gui.ui.generated.ui_port_editor_widget import Ui_PortEditor
|
||||
|
||||
|
||||
@@ -21,6 +22,7 @@ class PortEditorWidget(QWidget):
|
||||
self.ui = Ui_PortEditor()
|
||||
self.ui.setupUi(self)
|
||||
self._ports: dict[PortID, Port] = {}
|
||||
self._port_metadata: dict[PortID, PortMetadata] = {}
|
||||
self._port_ids: list[PortID] = []
|
||||
self._loading = False
|
||||
|
||||
@@ -54,6 +56,7 @@ class PortEditorWidget(QWidget):
|
||||
self.ui.widthSize.valueChanged.connect(self._form_changed)
|
||||
self.ui.heightSize.valueChanged.connect(self._form_changed)
|
||||
self.ui.multiplicityCheckBox.toggled.connect(self._form_changed)
|
||||
self.ui.connectionAnnotationEdit.textChanged.connect(self._form_changed)
|
||||
self.ui.signalTypeComboBox.currentIndexChanged.connect(self._form_changed)
|
||||
self.ui.domainComboBox.currentTextChanged.connect(self._form_changed)
|
||||
self.ui.causalityComboBox.currentIndexChanged.connect(self._form_changed)
|
||||
@@ -62,14 +65,18 @@ class PortEditorWidget(QWidget):
|
||||
self._set_editor_enabled(False)
|
||||
self._update_option_visibility()
|
||||
|
||||
def set_ports(self, ports: dict[PortID, Port]) -> None:
|
||||
def set_ports(self, ports: dict[PortID, Port], port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
|
||||
self._ports = deepcopy(ports)
|
||||
self._port_metadata = deepcopy(port_metadata or {})
|
||||
self._port_ids = list(self._ports)
|
||||
self._rebuild_list()
|
||||
|
||||
def ports(self) -> dict[PortID, Port]:
|
||||
return deepcopy(self._ports)
|
||||
|
||||
def port_metadata(self) -> dict[PortID, PortMetadata]:
|
||||
return deepcopy(self._port_metadata)
|
||||
|
||||
def _rebuild_list(self, selected_id: PortID | None = None) -> None:
|
||||
self._list_model.clear()
|
||||
for port_id in self._port_ids:
|
||||
@@ -90,7 +97,7 @@ class PortEditorWidget(QWidget):
|
||||
self.ui.removePort.setEnabled(port_id is not None)
|
||||
self._set_editor_enabled(port_id is not None)
|
||||
if port_id is not None:
|
||||
self._load_port(self._ports[port_id])
|
||||
self._load_port(self._ports[port_id], self._port_metadata.get(port_id, PortMetadata()))
|
||||
|
||||
def _selected_port_id(self) -> PortID | None:
|
||||
index = self.ui.portList.currentIndex()
|
||||
@@ -98,7 +105,7 @@ class PortEditorWidget(QWidget):
|
||||
return None
|
||||
return self._port_ids[index.row()]
|
||||
|
||||
def _load_port(self, port: Port) -> None:
|
||||
def _load_port(self, port: Port, metadata: PortMetadata) -> None:
|
||||
self._loading = True
|
||||
self.ui.nameEdit.setText(port.name)
|
||||
self.ui.inputOrientation.setChecked(port.direction is SignalDirection.INPUT)
|
||||
@@ -106,6 +113,7 @@ class PortEditorWidget(QWidget):
|
||||
self.ui.widthSize.setValue(port.matrix_size[0])
|
||||
self.ui.heightSize.setValue(port.matrix_size[1])
|
||||
self.ui.multiplicityCheckBox.setChecked(port.multiplicity)
|
||||
self.ui.connectionAnnotationEdit.setText(metadata.connection_annotation or "")
|
||||
self.ui.descriptionEdit.setPlainText(port.description or "")
|
||||
|
||||
if isinstance(port, SignalPort):
|
||||
@@ -135,6 +143,12 @@ class PortEditorWidget(QWidget):
|
||||
|
||||
old_port = self._ports[port_id]
|
||||
self._ports[port_id] = self._port_from_form(old_port)
|
||||
annotation = (self.ui.connectionAnnotationEdit.text().strip() or None) if self.ui.multiplicityCheckBox.isChecked() else None
|
||||
metadata = PortMetadata(connection_annotation=annotation)
|
||||
if metadata == PortMetadata():
|
||||
self._port_metadata.pop(port_id, None)
|
||||
else:
|
||||
self._port_metadata[port_id] = metadata
|
||||
self._list_model.item(self._port_ids.index(port_id)).setText(
|
||||
self._ports[port_id].name
|
||||
)
|
||||
@@ -188,6 +202,7 @@ class PortEditorWidget(QWidget):
|
||||
return
|
||||
row = self._port_ids.index(port_id)
|
||||
del self._ports[port_id]
|
||||
self._port_metadata.pop(port_id, None)
|
||||
self._port_ids.remove(port_id)
|
||||
selected = (
|
||||
self._port_ids[min(row, len(self._port_ids) - 1)]
|
||||
@@ -201,6 +216,9 @@ class PortEditorWidget(QWidget):
|
||||
signal = self.ui.typeSignal.isChecked()
|
||||
self._set_layout_visible(self.ui.signalOptions, signal)
|
||||
self._set_layout_visible(self.ui.bondOptions, not signal)
|
||||
annotation_visible = self.ui.multiplicityCheckBox.isChecked()
|
||||
self.ui.connectionAnnotationLabel.setVisible(annotation_visible)
|
||||
self.ui.connectionAnnotationEdit.setVisible(annotation_visible)
|
||||
|
||||
@staticmethod
|
||||
def _set_layout_visible(layout: QLayout, visible: bool) -> None:
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());",
|
||||
|
||||
1684
untitled.bedit.json
1684
untitled.bedit.json
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user