Compare commits
3 Commits
a61c8e6003
...
e88c58f095
| Author | SHA1 | Date | |
|---|---|---|---|
| e88c58f095 | |||
| b1f453a6df | |||
| 8d37b2441a |
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
|
||||
|
||||
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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)])
|
||||
|
||||
|
||||
@@ -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,12 +215,19 @@ 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)
|
||||
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)
|
||||
|
||||
76
src/bedit_gui/controllers/library_controller.py
Normal file
76
src/bedit_gui/controllers/library_controller.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from PySide6.QtCore import QObject, QSize, Qt
|
||||
from PySide6.QtWidgets import QAbstractItemView, QHeaderView
|
||||
|
||||
from bedit_core.models import Component, ComponentID, GraphImplementation
|
||||
from bedit_gui.models import Icon, IconDatabase
|
||||
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]]] = {}
|
||||
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)
|
||||
|
||||
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 {}
|
||||
self._collect_component_sources(library.document.root, icons)
|
||||
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 = {}
|
||||
for component in components:
|
||||
source = self._component_sources.get(id(component))
|
||||
if source is None:
|
||||
continue
|
||||
component_id, source_icons = source
|
||||
roots[component_id] = component
|
||||
icons.update(source_icons)
|
||||
return export_component_data(roots, icons)
|
||||
|
||||
def _collect_component_sources(self, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> None:
|
||||
for component_id, component in components.items():
|
||||
self._component_sources[id(component)] = (component_id, icons)
|
||||
if isinstance(component.implementation, GraphImplementation):
|
||||
self._collect_component_sources(component.implementation.graph.components, icons)
|
||||
|
||||
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,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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -9,18 +9,25 @@ from bedit_gui.documents import Document as GuiDocument
|
||||
from bedit_gui.models import Icon, 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 = {}
|
||||
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
|
||||
return export_component_data(roots, icons)
|
||||
|
||||
|
||||
def export_component_data(components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> 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}
|
||||
return {"format_version": FORMAT_VERSION, "type": "components", "components": serialized["root"], "icons": icon_data}
|
||||
|
||||
|
||||
def import_components(payload: dict[str, Any]) -> tuple[dict[ComponentID, Component], dict[ComponentID, Icon]]:
|
||||
|
||||
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>940</width>
|
||||
<height>22</height>
|
||||
<height>19</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuFile">
|
||||
@@ -127,7 +127,7 @@
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>150</width>
|
||||
<height>533</height>
|
||||
<height>200</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -180,6 +180,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">
|
||||
|
||||
362
src/bedit_gui/ui/forms/main_window_ui.py
Normal file
362
src/bedit_gui/ui/forms/main_window_ui.py
Normal file
@@ -0,0 +1,362 @@
|
||||
# -*- 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.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.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.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
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
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.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, GraphComponentLabel, GraphConnection, Icon
|
||||
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
|
||||
|
||||
@@ -291,6 +293,7 @@ class GraphEditorWidget(QWidget):
|
||||
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)
|
||||
@@ -314,6 +317,7 @@ class GraphEditorWidget(QWidget):
|
||||
self.ui.graphicsView.setScene(self.scene)
|
||||
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)
|
||||
@@ -736,6 +740,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()))
|
||||
|
||||
@@ -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)
|
||||
|
||||
_list_children(document_root, document.root)
|
||||
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()
|
||||
@@ -374,14 +374,14 @@
|
||||
"connection_type": "bond",
|
||||
"source": "4306701a-6b1b-4d19-b8ff-45dbfa04f2d3",
|
||||
"target": "497b1f74-1186-471f-976a-36b07a451caf",
|
||||
"causality": "effort_out",
|
||||
"causality": "flow_out",
|
||||
"undesired": false
|
||||
},
|
||||
"743e35a4-e736-407d-bf34-b4afbcde5a0b": {
|
||||
"connection_type": "bond",
|
||||
"source": "4306701a-6b1b-4d19-b8ff-45dbfa04f2d3",
|
||||
"target": "0ac7d4ea-77f7-4c0d-8e37-406ef66e4740",
|
||||
"causality": "flow_out",
|
||||
"causality": "effort_out",
|
||||
"undesired": false
|
||||
},
|
||||
"68727b23-888b-45b4-a0f4-801ac7b39601": {
|
||||
@@ -626,6 +626,10 @@
|
||||
"1455acad-931b-4caa-be71-aff4283507c6": [
|
||||
64,
|
||||
384
|
||||
],
|
||||
"015a0697-69a9-43bf-9410-d2c8b44303f8": [
|
||||
64,
|
||||
384
|
||||
]
|
||||
},
|
||||
"component_labels": {
|
||||
|
||||
Reference in New Issue
Block a user