Saving sim results

This commit is contained in:
2026-07-21 14:40:03 +02:00
parent ddc004dee5
commit edd7bb98f2
21 changed files with 987 additions and 226 deletions

View File

@@ -345,7 +345,7 @@ class PasteSelectionCommand(QUndoCommand):
def __init__(
self,
controller,
owner_id: str,
owner_id: str | None,
blocks: dict[str, Component],
connections: dict[str, Connection],
) -> None:

View File

@@ -963,6 +963,68 @@ class DocumentController(QObject):
self.undo_stack.push(PasteSelectionCommand(self, owner.id, blocks, connections))
return list(blocks)
def paste_components_to(
self,
owner_id: str | None,
source_components: list[Component],
source_connections: list[Connection] | None = None,
) -> list[str]:
"""Clone components into the document root or a graph at origin."""
if self.document is None:
raise ValueError("Open or create a document before pasting components")
if owner_id is None:
siblings = self.document.roots.values()
else:
owner = self.document.find_component(owner_id)
if owner is None or owner.implementation_kind != "graph":
raise ValueError("Components can only be pasted into a graph")
siblings = owner.graph.blocks.values()
pairs = [(source, clone_component(source)) for source in source_components]
if not pairs:
return []
id_map = {source.id: clone.id for source, clone in pairs}
used = list(siblings)
minimum_x = min(source.x for source, _clone in pairs)
minimum_y = min(source.y for source, _clone in pairs)
blocks: dict[str, Component] = {}
for source, clone in pairs:
clone.name = self._available_component_name(source.name, used, 0)
if owner_id is not None:
clone.x = source.x - minimum_x
clone.y = source.y - minimum_y
blocks[clone.id] = clone
used.append(clone)
connections: dict[str, Connection] = {}
if owner_id is not None:
for source in source_connections or []:
if source.source.block not in id_map or source.target.block not in id_map:
continue
properties = deepcopy(source.properties)
for point in properties.get("waypoints", []):
if isinstance(point, dict):
point["x"] = float(point.get("x", 0)) - minimum_x
point["y"] = float(point.get("y", 0)) - minimum_y
connection = Connection(
id=str(uuid4()),
source=Endpoint(
block=id_map[source.source.block], port=source.source.port
),
target=Endpoint(
block=id_map[source.target.block], port=source.target.port
),
name=source.name,
properties=properties,
)
connections[connection.id] = connection
self.undo_stack.push(
PasteSelectionCommand(self, owner_id, blocks, connections)
)
return list(blocks)
def _graph_for(self, owner_id: str):
if self.document is None:
raise ValueError("There is no open document")

View File

@@ -2326,6 +2326,63 @@ K\x80@\x89\x15\x8d\xc04\xd5\xb5^\xaf\x1bx\xfa\x19\
\x84\xe7\x04\xcf\x88\xfd\xfd\xfd\xe4\xe8\xe8\xc8\x9dL&\x14\
\xc7\xcc\x7f5p\xf2g\x94\xf7\x1f\xdf\x9a\xd2\x93\xfbC\
\xb7\xa7\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x03m\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\
\x00\x00\x09pHYs\x00\x00\x1b\xaf\x00\x00\x1b\xaf\x01\
^\x1a\x91\x1c\x00\x00\x00\x07tIME\x07\xd9\x02\x10\
\x17\x22\x16\x993\xa5<\x00\x00\x02\xedIDATx\
\xda\xed\x97_H\xd3Q\x14\xc7\xcf\xd9\xef7\xcb\xca\x89\
B \x91\x0f\xc9\xcczha\xf6\xa8\xb4\x06M\x90f\
aiV$\x96V\x12\xf9P-S\x90d%Lh\
V/B.\x8d\xc2\xb2\xc0\x1erS\xc1\x84\x1cdo\
a\xa0\x0f>h\x0f\xfdy\xf0-\x9b\xa6\x0f\xb1{\xfb\
n\xf4\x8b\x91\xe2\x5cn\xae\x07\x0f\xdc\x9dq/\xfc>\
\xe7{\xce\xe1\xdc\xdf\x8f\xd6M\xb3\x86\x86\x86\xe2Dp\
u\xbf\xe19pG\x82>!\x01\x04\xe1\x9aOd\x09\
:\x13V\x02\xcd\xd6\x03H\x84\xa9\x04\xab\xaf\xaf\xef\x84\
\xcb\x8f\x07\xe0z\xcf\x82AU\xc8\xa6\xeax\xfc\xf6\xd1\
\x8dcKf\xa0\xa5\xa5\xa5\x0an$V\xd0\xea\xc7\xf3\
\x86\x9a\xae\xf9\xd3\xb5\xdd\xf3\xaf\x84\xa4\xcfRR\xa9\x5c\
&\x031\xb3\xf2\xf6\x1f\xb9P\xdb\x14\x90\xd2\xacH\xf6\
\x01\xda\x13\x10T\xd1Z\x96\xec\xa7x\x07P\xd26\xc7\
P\xdb!\x04\xbd\x10L\x15\xedg7-\x82\xc65\x00\
\xc0m\x02?\x82\xd9\xf5\xa4j\xb3\x5c\xd3&,\xba\x1f\
R\x8f\xd4\x93\xe3\xe5\xc5\x08\xf0X7\xa1\xa6> B\
\xde\xbb&s\xc0\xb3\xf3x\x81'\xbbt\x1f\xc1,w\
f\x19M\xd7$\xa0\xbe\xf7\xf2\x16\x19\xf5\x1c\x88\x0e|\
L!\xc9\x8d$u\x97X\xa8\x0b\xde\xac\x0aS\xab$\
3\x07\xd5\xb3\xd4\xd4\xc7'\x00\xa8\xce\x00\xfc)\xe0\xcc\
\xa4\xdbK2\xe9\x96\x94\xaa\x0bu\xcfc\xa8\x1f\xbc\x92\
\x22\xffq\x14GnB\xa4\xdbBRy\xcfRy\xcb\
\xa4XmS\xcf\xa7I\xe8\xed,\xf4\x85\xbb&\xc7\xb4\
\xda\xc7\xfe:\x06X\xf1d\x975y\x8c\xe5_\xbd\xc6\
\x93\x96\xf0\xb3\x5c\x87\x9f++\xdf\x8d\xf6m\xbf\xfa\xa9\
o[\x9d!\xe6\x97\x11\xc0\x198\x1e\x84\xea\x02\xa4|\
?T\xbf\x09?\x97\x92\x8a\xc7\xb3\xf6H)\x92^#\
\x1b\xae\xe5 \x03\x06\xb7e \xa5\xc3\xb4\xd2\x00\x00?\
\x91\x87Z\x87R\x0e_h\x9b\xea\x9e\x0e?\xdf\xdd\xf8\
\x9d\x03B\xde\x0cv>\x9a\xf1\x1aK\xbd\xb5\x7fk\xb3\
u1\xb8\xdd0`x\xe8F\xf9\x1e\xe19\xea\x8a\x03\
`\xa9\xfb\x09\xf8)\x80\x1d\xb6\x8f\xcf\x02\xda~\xa6}\
\x86v\xdc\x981\x06\x045\x02NB\x90\xf7\xf0t\xb3\
\x1f\x19\xb8\x80\xe5\xeeOo5\x84\xa9>\x040\x1aD\
!\xc0MEs\xe7F\x97diM\xc8\xcc\xf9N\xa7\
3'\xfc0\xb9\xe6\x1b\xe9\x98\x8c\xaaB\x07\x92\x142\
\xebU6\xeb\xf1\xbc$\x85}\xd8\xbb;\xd1\x9c\xfa\xe1\
\x0f0\xfd\x9e;\x04\x13\x1b\xecD\xec\x02\xdcJ\x92\xcf\
\x03<\x14\xf5$\x04\xd8\x84\xd5\x85\xbf_P\xeba,\
3\x96\x0f\x8a\x0fB}\xe6\xa43\xf5L\x08\x1enB\
\xb5\x13J\x01\xb5\x13a\xaa\x87V3\x07|X\x0eI\
4\xe5oK\x8b|\x1f\xcc\xd4\xfa\xd1h%\x00\xa7\x02\
<\xbc\xaaA\xb4\xf0 m\x0c\x0e+:+\x9a\xad\x1e\
\xfd\xaf\xdf\x091k\xea\xfe\xfe\xf8\xe1H\xdf\x05R\xca\
|f\x1e\x89\xd1\x9ef\xbdhx\x0fE4mL\xaf\
b/B\x06\x12o\xbf\x00\xa3\x17WYZq\xd9W\
\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x04<\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -2396,6 +2453,35 @@ f\xa2\xd1\xa5\x5c\x22\xb4\x91SZ\xd5u\xd7\x0a\xd2]\
|\x01\x85\x09\x800\x7fss\xd3{\xf6\x7fABG\
Y\x01\x05l*\xfc\x00!\x00\x12\xf1%U\xb6\x0e\x00\
\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x01\xad\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00 \x00\x00\x00 \x08\x03\x00\x00\x00D\xa4\x8a\xc6\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\
\x9bx\x00\x00\x00\x07tIME\x07\xd9\x0c\x1c\x03\x1c\
\x0e%S,b\x00\x00\x00uPLTE\x00\x00\x00\
\x13\x13\x13\x0a\x0a\x0a\x0b\x0b\x0b\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc6\xc6\xc8\xc7\xc7\
\xc9\xc9\xc9\xcb\xcc\xcc\xce\xce\xce\xd0\xd0\xd0\xd2\xd4\xd4\xd6\
\xd7\xd7\xd8\xd7\xd7\xd9\xd8\xd8\xda\xd9\xd9\xdb\xda\xda\xdb\xda\
\xda\xdc\xdb\xdb\xdc\xdb\xdb\xdd\xdd\xdd\xdf\xde\xde\xdf\xe2\xe2\
\xe3\xe3\xe3\xe5\xe3\xe3\xe6\xe4\xe4\xe6\xe6\xe6\xe7\xe7\xe7\xe9\
\xe9\xe9\xea\xeb\xeb\xec\xed\xed\xee\xf0\xf0\xf1\xf3\xf3\xf4\xff\
\xff\xff\xd3\x9b\xcc\x0e\x00\x00\x00\x0atRNS\x00\x09\
\x15\x15\x1825678\xb5\xcc\xc0\x1e\x00\x00\x00\x01\
bKGD&Z\x08\x98\xb5\x00\x00\x00\x9bIDA\
Tx\xda\xd5\x93\xcb\x0e\x820\x10\x00\x8b\x0aEP|\
u\xc1G)\x94\x02\xff\xff\x89.]\x0e\x18\xccr1\
F\xe72\xd9d\xd26\x9bT\xfc\x02\xc1\x86%\x10a\
\xcf\x12\x0a\xd9)\x86N\x0a\xd9^\x18Z\x0c\xdc\x91\xc1\
a\xd0\x1c\x18\x1a\x0c\xec\x9e\xc1bP'\x9et\x97\xce\
\x95\xd4\x18\x94=q#\xddIWR\x89\x81\x1e\x03E\
\x82\x97IO\x02\x98\x8b\x82G\x01\x9e\xec\x8d\x8aj\xf1\
\x84\xcf\xbc!W\x03\x90\x8d\x02oR^-^\xf1?\
{\xe0\x83\xc8\x1a=`\xce\xc6\xebd\xa6\x93\x8b\xc4*\
\xde2\xc4\xeb/|\xcd'\xec\xbfO\xbf\x90M\x1a\x0a\
\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x02\x92\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -2632,10 +2718,20 @@ qt_resource_name = b"\
\x00d\
\x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00-\x00s\x00a\x00v\x00e\x00.\x00p\x00n\x00g\
\
\x00\x15\
\x02\xb4\x1f\x07\
\x00o\
\x00f\x00f\x00i\x00c\x00e\x00-\x00c\x00h\x00a\x00r\x00t\x00-\x00l\x00i\x00n\x00e\
\x00.\x00p\x00n\x00g\
\x00\x10\
\x03\xe6\xd3g\
\x00d\
\x00r\x00a\x00w\x00-\x00e\x00l\x00l\x00i\x00p\x00s\x00e\x00.\x00p\x00n\x00g\
\x00\x13\
\x07\xd6O\x07\
\x00v\
\x00i\x00e\x00w\x00-\x00f\x00o\x00r\x00m\x00-\x00t\x00a\x00b\x00l\x00e\x00.\x00p\
\x00n\x00g\
\x00\x12\
\x09\xb3>\xc7\
\x00d\
@@ -2653,7 +2749,7 @@ qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1e\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x01\xfe\x00\x00\x00\x00\x00\x01\x00\x00N\xe8\
\x00\x00\x01\x9f{C\xf1'\
@@ -2661,11 +2757,13 @@ qt_resource_struct = b"\
\x00\x00\x01\x9f\x7f\xa8\xa8)\
\x00\x00\x02\xec\x00\x00\x00\x00\x00\x01\x00\x00tV\
\x00\x00\x01\x9f{0\xc99\
\x00\x00\x03\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x8d\xaa\
\x00\x00\x01\x9f\x84\x8f\x00\xb9\
\x00\x00\x01\xc4\x00\x00\x00\x00\x00\x01\x00\x00D2\
\x00\x00\x01\x9f\x7f&\x83\xcd\
\x00\x00\x01\xa4\x00\x00\x00\x00\x00\x01\x00\x00<J\
\x00\x00\x01\x9f{C\xf1\x18\
\x00\x00\x03\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x8d\xaa\
\x00\x00\x03\xd4\x00\x00\x00\x00\x00\x01\x00\x00\x91\x1b\
\x00\x00\x01\x9f\x7fY\xceg\
\x00\x00\x02\xa2\x00\x00\x00\x00\x00\x01\x00\x00f\xc8\
\x00\x00\x01\x9f\x7fV\xd5\xc0\
@@ -2677,9 +2775,11 @@ qt_resource_struct = b"\
\x00\x00\x01\x9f\x7fY\xce\x82\
\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x01\x00\x00Kh\
\x00\x00\x01\x9f{C\xf1.\
\x00\x00\x03\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x95[\
\x00\x00\x01\x9f\x84\x8f\xa6\x10\
\x00\x00\x01\x84\x00\x00\x00\x00\x00\x01\x00\x006\x9c\
\x00\x00\x01\x9f{{\xa5\xd5\
\x00\x00\x03\xca\x00\x00\x00\x00\x00\x01\x00\x00\x91\xea\
\x00\x00\x04&\x00\x00\x00\x00\x00\x01\x00\x00\x97\x0c\
\x00\x00\x01\x9f\x7f&\x83\x10\
\x00\x00\x00\xfe\x00\x00\x00\x00\x00\x01\x00\x00(e\
\x00\x00\x01\x9f\x7f&\x83~\
@@ -2703,7 +2803,7 @@ qt_resource_struct = b"\
\x00\x00\x01\x9f{C\xf1=\
\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\
\x00\x00\x01\x9f\x7f&\x83\x99\
\x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x00\x94\x80\
\x00\x00\x04P\x00\x00\x00\x00\x00\x01\x00\x00\x99\xa2\
\x00\x00\x01\x9f{{\xa5\xe3\
\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xc1\
\x00\x00\x01\x9f\x7f\xac\xf2\xc6\

View File

@@ -39,7 +39,7 @@ class Ui_MainWindow(object):
self.actionGraphParameters = QAction(MainWindow)
self.actionGraphParameters.setObjectName(u"actionGraphParameters")
icon1 = QIcon()
icon1.addFile(u":/icons/icons/configure.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
icon1.addFile(u":/icons/icons/view-form-table.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionGraphParameters.setIcon(icon1)
self.actionCompose = QAction(MainWindow)
self.actionCompose.setObjectName(u"actionCompose")
@@ -53,80 +53,82 @@ class Ui_MainWindow(object):
self.actionRunSimulation.setIcon(icon3)
self.actionSimulationWindow = QAction(MainWindow)
self.actionSimulationWindow.setObjectName(u"actionSimulationWindow")
self.actionSimulationWindow.setIcon(icon1)
icon4 = QIcon()
icon4.addFile(u":/icons/icons/office-chart-line.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSimulationWindow.setIcon(icon4)
self.actionNew = QAction(MainWindow)
self.actionNew.setObjectName(u"actionNew")
icon4 = QIcon()
icon4.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionNew.setIcon(icon4)
icon5 = QIcon()
icon5.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionNew.setIcon(icon5)
self.actionRotateClockwise = QAction(MainWindow)
self.actionRotateClockwise.setObjectName(u"actionRotateClockwise")
icon5 = QIcon()
icon5.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRotateClockwise.setIcon(icon5)
icon6 = QIcon()
icon6.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRotateClockwise.setIcon(icon6)
self.actionZoomIn = QAction(MainWindow)
self.actionZoomIn.setObjectName(u"actionZoomIn")
icon6 = QIcon()
icon6.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomIn.setIcon(icon6)
icon7 = QIcon()
icon7.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomIn.setIcon(icon7)
self.actionZoomOut = QAction(MainWindow)
self.actionZoomOut.setObjectName(u"actionZoomOut")
icon7 = QIcon()
icon7.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomOut.setIcon(icon7)
icon8 = QIcon()
icon8.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomOut.setIcon(icon8)
self.actionCenterView = QAction(MainWindow)
self.actionCenterView.setObjectName(u"actionCenterView")
icon8 = QIcon()
icon8.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCenterView.setIcon(icon8)
icon9 = QIcon()
icon9.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCenterView.setIcon(icon9)
self.actionOpen = QAction(MainWindow)
self.actionOpen.setObjectName(u"actionOpen")
icon9 = QIcon()
icon9.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon9)
icon10 = QIcon()
icon10.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon10)
self.actionReloadLibraries = QAction(MainWindow)
self.actionReloadLibraries.setObjectName(u"actionReloadLibraries")
self.actionReloadSimulation = QAction(MainWindow)
self.actionReloadSimulation.setObjectName(u"actionReloadSimulation")
self.actionSave = QAction(MainWindow)
self.actionSave.setObjectName(u"actionSave")
icon10 = QIcon()
icon10.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon10)
icon11 = QIcon()
icon11.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon11)
self.actionSaveAs = QAction(MainWindow)
self.actionSaveAs.setObjectName(u"actionSaveAs")
icon11 = QIcon()
icon11.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSaveAs.setIcon(icon11)
icon12 = QIcon()
icon12.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSaveAs.setIcon(icon12)
self.actionExit = QAction(MainWindow)
self.actionExit.setObjectName(u"actionExit")
self.actionClose = QAction(MainWindow)
self.actionClose.setObjectName(u"actionClose")
self.actionUndo = QAction(MainWindow)
self.actionUndo.setObjectName(u"actionUndo")
icon12 = QIcon()
icon12.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionUndo.setIcon(icon12)
icon13 = QIcon()
icon13.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionUndo.setIcon(icon13)
self.actionRedo = QAction(MainWindow)
self.actionRedo.setObjectName(u"actionRedo")
icon13 = QIcon()
icon13.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRedo.setIcon(icon13)
icon14 = QIcon()
icon14.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRedo.setIcon(icon14)
self.actionCut = QAction(MainWindow)
self.actionCut.setObjectName(u"actionCut")
icon14 = QIcon()
icon14.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCut.setIcon(icon14)
icon15 = QIcon()
icon15.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCut.setIcon(icon15)
self.actionCopy = QAction(MainWindow)
self.actionCopy.setObjectName(u"actionCopy")
icon15 = QIcon()
icon15.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCopy.setIcon(icon15)
icon16 = QIcon()
icon16.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCopy.setIcon(icon16)
self.actionPaste = QAction(MainWindow)
self.actionPaste.setObjectName(u"actionPaste")
icon16 = QIcon()
icon16.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionPaste.setIcon(icon16)
icon17 = QIcon()
icon17.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionPaste.setIcon(icon17)
self.actionSelectAll = QAction(MainWindow)
self.actionSelectAll.setObjectName(u"actionSelectAll")
self.actionDelete = QAction(MainWindow)
@@ -216,18 +218,18 @@ class Ui_MainWindow(object):
self.workspaceHeaderLayout.setContentsMargins(6, 2, 6, 2)
self.navigateUpButton = QToolButton(self.workspaceHeader)
self.navigateUpButton.setObjectName(u"navigateUpButton")
icon17 = QIcon()
icon17.addFile(u":/icons/icons/arrow-up.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateUpButton.setIcon(icon17)
icon18 = QIcon()
icon18.addFile(u":/icons/icons/arrow-up.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateUpButton.setIcon(icon18)
self.workspaceHeaderLayout.addWidget(self.navigateUpButton)
self.navigateDownButton = QToolButton(self.workspaceHeader)
self.navigateDownButton.setObjectName(u"navigateDownButton")
self.navigateDownButton.setEnabled(False)
icon18 = QIcon()
icon18.addFile(u":/icons/icons/arrow-down.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateDownButton.setIcon(icon18)
icon19 = QIcon()
icon19.addFile(u":/icons/icons/arrow-down.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateDownButton.setIcon(icon19)
self.workspaceHeaderLayout.addWidget(self.navigateDownButton)
@@ -247,9 +249,9 @@ class Ui_MainWindow(object):
self.pointerToolButton = QToolButton(self.workspaceHeader)
self.pointerToolButton.setObjectName(u"pointerToolButton")
icon19 = QIcon()
icon19.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.pointerToolButton.setIcon(icon19)
icon20 = QIcon()
icon20.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.pointerToolButton.setIcon(icon20)
self.pointerToolButton.setCheckable(True)
self.pointerToolButton.setChecked(True)
@@ -257,43 +259,43 @@ class Ui_MainWindow(object):
self.connectToolButton = QToolButton(self.workspaceHeader)
self.connectToolButton.setObjectName(u"connectToolButton")
icon20 = QIcon()
icon20.addFile(u":/icons/icons/network-connect.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.connectToolButton.setIcon(icon20)
icon21 = QIcon()
icon21.addFile(u":/icons/icons/network-connect.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.connectToolButton.setIcon(icon21)
self.connectToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.connectToolButton)
self.boxToolButton = QToolButton(self.workspaceHeader)
self.boxToolButton.setObjectName(u"boxToolButton")
icon21 = QIcon()
icon21.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.boxToolButton.setIcon(icon21)
icon22 = QIcon()
icon22.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.boxToolButton.setIcon(icon22)
self.boxToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.boxToolButton)
self.lineToolButton = QToolButton(self.workspaceHeader)
self.lineToolButton.setObjectName(u"lineToolButton")
icon22 = QIcon()
icon22.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.lineToolButton.setIcon(icon22)
icon23 = QIcon()
icon23.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.lineToolButton.setIcon(icon23)
self.lineToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.lineToolButton)
self.textToolButton = QToolButton(self.workspaceHeader)
self.textToolButton.setObjectName(u"textToolButton")
icon23 = QIcon()
icon23.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.textToolButton.setIcon(icon23)
icon24 = QIcon()
icon24.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.textToolButton.setIcon(icon24)
self.textToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.textToolButton)
self.rotateToolButton = QToolButton(self.workspaceHeader)
self.rotateToolButton.setObjectName(u"rotateToolButton")
self.rotateToolButton.setIcon(icon5)
self.rotateToolButton.setIcon(icon6)
self.workspaceHeaderLayout.addWidget(self.rotateToolButton)

View File

@@ -16,10 +16,10 @@ from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
QIcon, QImage, QKeySequence, QLinearGradient,
QPainter, QPalette, QPixmap, QRadialGradient,
QTransform)
from PySide6.QtWidgets import (QApplication, QDockWidget, QLabel, QListWidget,
QListWidgetItem, QMainWindow, QMenu, QMenuBar,
QProgressBar, QSizePolicy, QToolBar, QVBoxLayout,
QWidget)
from PySide6.QtWidgets import (QApplication, QDockWidget, QHBoxLayout, QLabel,
QListWidget, QListWidgetItem, QMainWindow, QMenu,
QMenuBar, QProgressBar, QSizePolicy, QToolBar,
QVBoxLayout, QWidget)
from . import resources_rc
class Ui_SimulationWindow(object):
@@ -27,33 +27,53 @@ class Ui_SimulationWindow(object):
if not SimulationWindow.objectName():
SimulationWindow.setObjectName(u"SimulationWindow")
SimulationWindow.resize(900, 650)
icon = QIcon()
icon.addFile(u":/icons/icons/office-chart-line.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
SimulationWindow.setWindowIcon(icon)
self.actionOpen = QAction(SimulationWindow)
self.actionOpen.setObjectName(u"actionOpen")
icon = QIcon()
icon.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon)
icon1 = QIcon()
icon1.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon1)
self.actionSave = QAction(SimulationWindow)
self.actionSave.setObjectName(u"actionSave")
icon1 = QIcon()
icon1.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon1)
icon2 = QIcon()
icon2.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon2)
self.actionClear = QAction(SimulationWindow)
self.actionClear.setObjectName(u"actionClear")
icon2 = QIcon()
icon2.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionClear.setIcon(icon2)
icon3 = QIcon()
icon3.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionClear.setIcon(icon3)
self.actionSaveAs = QAction(SimulationWindow)
self.actionSaveAs.setObjectName(u"actionSaveAs")
icon4 = QIcon()
icon4.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSaveAs.setIcon(icon4)
self.actionExit = QAction(SimulationWindow)
self.actionExit.setObjectName(u"actionExit")
self.actionAbout = QAction(SimulationWindow)
self.actionAbout.setObjectName(u"actionAbout")
self.actionAboutQt = QAction(SimulationWindow)
self.actionAboutQt.setObjectName(u"actionAboutQt")
self.actionToggleResults = QAction(SimulationWindow)
self.actionToggleResults.setObjectName(u"actionToggleResults")
self.actionToggleResults.setCheckable(True)
self.actionToggleResults.setChecked(True)
self.centralWidget = QWidget(SimulationWindow)
self.centralWidget.setObjectName(u"centralWidget")
self.centralWidget.setMaximumSize(QSize(0, 0))
self.resultsLayout = QVBoxLayout(self.centralWidget)
self.resultsLayout.setObjectName(u"resultsLayout")
self.resultsPlaceholder = QLabel(self.centralWidget)
self.resultsPlaceholder.setObjectName(u"resultsPlaceholder")
self.resultsPlaceholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.resultsLayout.addWidget(self.resultsPlaceholder)
SimulationWindow.setCentralWidget(self.centralWidget)
self.menuBar = QMenuBar(SimulationWindow)
self.menuBar.setObjectName(u"menuBar")
self.menuBar.setGeometry(QRect(0, 0, 900, 24))
self.menuFile = QMenu(self.menuBar)
self.menuFile.setObjectName(u"menuFile")
self.menuView = QMenu(self.menuBar)
@@ -69,47 +89,35 @@ class Ui_SimulationWindow(object):
self.fileToolbar.setObjectName(u"fileToolbar")
self.fileToolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
SimulationWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.fileToolbar)
self.resultsDock = QDockWidget(SimulationWindow)
self.resultsDock.setObjectName(u"resultsDock")
self.resultsDockContents = QWidget()
self.resultsDockContents.setObjectName(u"resultsDockContents")
self.resultsLayout = QVBoxLayout(self.resultsDockContents)
self.resultsLayout.setObjectName(u"resultsLayout")
self.resultsPlaceholder = QLabel(self.resultsDockContents)
self.resultsPlaceholder.setObjectName(u"resultsPlaceholder")
self.resultsPlaceholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.resultsLayout.addWidget(self.resultsPlaceholder)
self.resultsDock.setWidget(self.resultsDockContents)
SimulationWindow.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.resultsDock)
self.statusDock = QDockWidget(SimulationWindow)
self.statusDock.setObjectName(u"statusDock")
self.statusDockContents = QWidget()
self.statusDockContents.setObjectName(u"statusDockContents")
self.statusLayout = QVBoxLayout(self.statusDockContents)
self.statusLayout.setObjectName(u"statusLayout")
self.statusLabel = QLabel(self.statusDockContents)
self.statusLabel.setObjectName(u"statusLabel")
self.horizontalLayout = QHBoxLayout(self.statusDockContents)
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.timeLabel = QLabel(self.statusDockContents)
self.timeLabel.setObjectName(u"timeLabel")
self.statusLayout.addWidget(self.statusLabel)
self.horizontalLayout.addWidget(self.timeLabel)
self.progressBar = QProgressBar(self.statusDockContents)
self.progressBar.setObjectName(u"progressBar")
self.progressBar.setMaximum(10000)
self.progressBar.setValue(0)
self.statusLayout.addWidget(self.progressBar)
self.horizontalLayout.addWidget(self.progressBar)
self.timeLabel = QLabel(self.statusDockContents)
self.timeLabel.setObjectName(u"timeLabel")
self.statusLabel = QLabel(self.statusDockContents)
self.statusLabel.setObjectName(u"statusLabel")
self.statusLayout.addWidget(self.timeLabel)
self.horizontalLayout.addWidget(self.statusLabel)
self.statusDock.setWidget(self.statusDockContents)
SimulationWindow.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.statusDock)
self.logDock = QDockWidget(SimulationWindow)
self.logDock.setObjectName(u"logDock")
self.logDock.setFloating(False)
self.logDock.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetFloatable|QDockWidget.DockWidgetFeature.DockWidgetMovable)
self.logDockContents = QWidget()
self.logDockContents.setObjectName(u"logDockContents")
self.logLayout = QVBoxLayout(self.logDockContents)
@@ -128,6 +136,7 @@ class Ui_SimulationWindow(object):
self.menuBar.addAction(self.menuHelp.menuAction())
self.menuFile.addAction(self.actionOpen)
self.menuFile.addAction(self.actionSave)
self.menuFile.addAction(self.actionSaveAs)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionClear)
self.menuFile.addSeparator()
@@ -138,6 +147,7 @@ class Ui_SimulationWindow(object):
self.menuHelp.addAction(self.actionAboutQt)
self.fileToolbar.addAction(self.actionOpen)
self.fileToolbar.addAction(self.actionSave)
self.fileToolbar.addAction(self.actionSaveAs)
self.fileToolbar.addAction(self.actionClear)
self.retranslateUi(SimulationWindow)
@@ -156,24 +166,28 @@ class Ui_SimulationWindow(object):
self.actionSave.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+S", None))
#endif // QT_CONFIG(shortcut)
self.actionClear.setText(QCoreApplication.translate("SimulationWindow", u"&Clear", None))
self.actionSaveAs.setText(QCoreApplication.translate("SimulationWindow", u"Save &As\u2026", None))
#if QT_CONFIG(shortcut)
self.actionSaveAs.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+Shift+S", None))
#endif // QT_CONFIG(shortcut)
self.actionExit.setText(QCoreApplication.translate("SimulationWindow", u"E&xit", None))
#if QT_CONFIG(shortcut)
self.actionExit.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+W", None))
#endif // QT_CONFIG(shortcut)
self.actionAbout.setText(QCoreApplication.translate("SimulationWindow", u"&About Simulation Window", None))
self.actionAboutQt.setText(QCoreApplication.translate("SimulationWindow", u"About &Qt", None))
self.actionToggleResults.setText(QCoreApplication.translate("SimulationWindow", u"Results", None))
self.resultsPlaceholder.setText(QCoreApplication.translate("SimulationWindow", u"Simulation graphs and result controls can be added here.", None))
self.menuFile.setTitle(QCoreApplication.translate("SimulationWindow", u"&File", None))
self.menuView.setTitle(QCoreApplication.translate("SimulationWindow", u"&View", None))
self.menuPanels.setTitle(QCoreApplication.translate("SimulationWindow", u"&Panels", None))
self.menuToolbars.setTitle(QCoreApplication.translate("SimulationWindow", u"&Toolbars", None))
self.menuHelp.setTitle(QCoreApplication.translate("SimulationWindow", u"&Help", None))
self.fileToolbar.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"File", None))
self.resultsDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Results", None))
self.resultsPlaceholder.setText(QCoreApplication.translate("SimulationWindow", u"Simulation graphs and result controls can be added here.", None))
self.statusDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Status", None))
self.statusLabel.setText(QCoreApplication.translate("SimulationWindow", u"No simulation has been run yet.", None))
self.progressBar.setFormat(QCoreApplication.translate("SimulationWindow", u"%p%", None))
self.timeLabel.setText(QCoreApplication.translate("SimulationWindow", u"Time: 0 s", None))
self.progressBar.setFormat(QCoreApplication.translate("SimulationWindow", u"%p%", None))
self.statusLabel.setText(QCoreApplication.translate("SimulationWindow", u"No simulation has been run yet.", None))
self.logDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Log", None))
# retranslateUi

View File

@@ -1892,6 +1892,22 @@ class GraphWorkspaceView(QGraphicsView):
if self.controller is None:
return
mime_data = QApplication.clipboard().mimeData()
if mime_data.hasFormat(COMPONENT_MIME_TYPE):
try:
payload = json.loads(
bytes(mime_data.data(COMPONENT_MIME_TYPE)).decode("utf-8")
)
source = Component.from_dict(payload)
except (KeyError, TypeError, ValueError, json.JSONDecodeError):
return
component_id = self.controller.add_component_copy(source, QPointF(0, 0))
scene = self.scene()
if isinstance(scene, GraphScene):
scene.clearSelection()
item = scene.component_items.get(component_id)
if item is not None:
item.setSelected(True)
return
if not mime_data.hasFormat(SELECTION_MIME_TYPE):
return
try:

View File

@@ -1,10 +1,12 @@
import json
from copy import deepcopy
from pathlib import Path
from PySide6.QtCore import Qt, Slot
from PySide6.QtCore import QByteArray, QMimeData, Qt, Slot
from PySide6.QtGui import QCloseEvent
from PySide6.QtWidgets import (
QButtonGroup,
QApplication,
QFileDialog,
QMainWindow,
QMenu,
@@ -12,7 +14,7 @@ from PySide6.QtWidgets import (
QTabWidget,
)
from bedit.core.model import Component
from bedit.core.model import Component, Connection
from bedit.core.application_log import get_logger
from bedit.core.serializer import DocumentSerializer
from bedit.core.simulation import Simulation
@@ -22,12 +24,15 @@ from bedit.gui.dialogs.graph_parameters import GraphParametersDialog
from bedit.gui.dialogs.item_options import ItemOptionsDialog
from bedit.gui.models.library_repository import LibraryRepository
from bedit.gui.models.library_tree import (
COMPONENT_MIME_TYPE,
COMPONENT_ROLE,
COMPONENT_ID_ROLE,
COMPONENT_INSTANCE_ROLE,
ITEM_KIND_ROLE,
DocumentTreeModel,
LibraryTreeModel,
)
from bedit.gui.graphics.workspace import SELECTION_MIME_TYPE
from bedit.gui.dialogs.settings import SettingsDialog
from bedit.gui.dialogs.port_options import PortOptionsDialog
from bedit.gui.dialogs.parameter_options import ParameterOptionsDialog
@@ -55,7 +60,9 @@ class MainWindow(QMainWindow):
self.log.info("BEdit started")
self.settings = application_settings()
self._applying_text_definition = False
self._simulation_window = SimulationWindow(self)
# Keep a Python-owned top-level window. Giving it MainWindow as its Qt
# parent makes some Linux window managers inherit the BEdit window icon.
self._simulation_window = SimulationWindow()
self.libraries = LibraryRepository(self)
self.simulation = Simulation(
@@ -69,6 +76,7 @@ class MainWindow(QMainWindow):
)
self.document_tree_model = DocumentTreeModel(self.document_controller, self)
self._configure_models()
QApplication.clipboard().dataChanged.connect(self._update_edit_actions)
self._connect_actions()
self._populate_view_menu()
self._restore_window_geometry()
@@ -102,6 +110,10 @@ class MainWindow(QMainWindow):
self.ui.documentTreeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.ui.documentTreeView.customContextMenuRequested.connect(self.show_library_context_menu)
self.ui.documentTreeView.doubleClicked.connect(self.activate_tree_component)
self.ui.documentTreeView.clicked.connect(
lambda _index: self._update_edit_actions()
)
self.ui.treeView.clicked.connect(lambda _index: self._update_edit_actions())
self.document_tree_model.rebuilt.connect(self.ui.documentTreeView.expandAll)
self.ui.graphView.set_model(self.document_controller)
self.ui.graphView.componentOptionsRequested.connect(self.show_component_options)
@@ -155,9 +167,9 @@ class MainWindow(QMainWindow):
self.ui.actionUndo.triggered.connect(self.document_controller.undo_stack.undo)
self.ui.actionRedo.triggered.connect(self.document_controller.undo_stack.redo)
self.ui.actionDelete.triggered.connect(self.ui.graphView.delete_selected)
self.ui.actionCopy.triggered.connect(self.ui.graphView.copy_selection)
self.ui.actionCut.triggered.connect(self.ui.graphView.cut_selection)
self.ui.actionPaste.triggered.connect(self.ui.graphView.paste_selection)
self.ui.actionCopy.triggered.connect(self.copy_selection)
self.ui.actionCut.triggered.connect(self.cut_selection)
self.ui.actionPaste.triggered.connect(self.paste_selection)
self.ui.actionSelectAll.triggered.connect(self.ui.graphView.select_all)
self.ui.actionRotateClockwise.triggered.connect(self.ui.graphView.rotate_selected)
self.addAction(self.ui.actionRotateClockwise)
@@ -324,8 +336,24 @@ class MainWindow(QMainWindow):
has_selection = bool(
self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems()
)
for action in (self.ui.actionCut, self.ui.actionCopy, self.ui.actionDelete):
action.setEnabled(is_graph and has_selection)
document_component_selected = bool(
self.ui.documentTreeView.currentIndex().data(COMPONENT_ID_ROLE)
)
library_component_selected = isinstance(
self.ui.treeView.currentIndex().data(COMPONENT_ROLE), dict
)
document_has_focus = self._view_has_focus(self.ui.documentTreeView)
library_has_focus = self._view_has_focus(self.ui.treeView)
self.ui.actionCopy.setEnabled(
(is_graph and has_selection)
or (document_has_focus and document_component_selected)
or (library_has_focus and library_component_selected)
)
self.ui.actionCut.setEnabled(
(is_graph and has_selection)
or (document_has_focus and document_component_selected)
)
self.ui.actionDelete.setEnabled(is_graph and has_selection)
self.ui.actionRotateClockwise.setEnabled(
is_graph and self.ui.graphView.has_selected_components()
)
@@ -333,11 +361,114 @@ class MainWindow(QMainWindow):
is_graph and self.ui.graphView.has_selected_components()
)
self.ui.actionSelectAll.setEnabled(is_graph)
self.ui.actionPaste.setEnabled(is_graph)
can_paste_component = QApplication.clipboard().mimeData().hasFormat(
COMPONENT_MIME_TYPE
) or QApplication.clipboard().mimeData().hasFormat(SELECTION_MIME_TYPE)
self.ui.actionPaste.setEnabled(
(is_graph and not document_has_focus and not library_has_focus)
or (
document_has_focus
and self.document_controller.document is not None
and can_paste_component
)
)
self.ui.navigateDownButton.setEnabled(
is_graph and self.ui.graphView.has_single_selected_component()
)
def copy_selection(self) -> bool:
if self._view_has_focus(self.ui.documentTreeView):
return self._copy_tree_component(self.ui.documentTreeView)
if self._view_has_focus(self.ui.treeView):
return self._copy_tree_component(self.ui.treeView)
return self.ui.graphView.copy_selection()
def cut_selection(self) -> None:
if self._view_has_focus(self.ui.documentTreeView):
index = self.ui.documentTreeView.currentIndex()
component_id = index.data(COMPONENT_ID_ROLE)
if component_id and self._copy_tree_component(self.ui.documentTreeView):
self.document_controller.delete_component(component_id)
return
if self._view_has_focus(self.ui.treeView):
self._copy_tree_component(self.ui.treeView)
return
self.ui.graphView.cut_selection()
def paste_selection(self) -> None:
if self._view_has_focus(self.ui.documentTreeView):
self._paste_into_document_tree()
return
self.ui.graphView.paste_selection()
def _copy_tree_component(self, tree) -> bool:
component = tree.currentIndex().data(COMPONENT_ROLE)
if not isinstance(component, dict):
return False
mime_data = QMimeData()
mime_data.setData(
COMPONENT_MIME_TYPE,
QByteArray(json.dumps(component).encode("utf-8")),
)
QApplication.clipboard().setMimeData(mime_data)
return True
def _paste_into_document_tree(self) -> None:
index = self.ui.documentTreeView.currentIndex()
kind = index.data(ITEM_KIND_ROLE)
owner_id = None
if kind == "current-component":
component_id = index.data(COMPONENT_ID_ROLE)
component = (
self.document_controller.document.find_component(component_id)
if self.document_controller.document
else None
)
if component is None or component.implementation_kind != "graph":
QMessageBox.warning(
self, "Cannot Paste", "Select a graph component or the document root."
)
return
owner_id = component.id
elif kind != "current-document":
QMessageBox.warning(
self, "Cannot Paste", "Select a graph component or the document root."
)
return
try:
components, connections = self._clipboard_components()
self.document_controller.paste_components_to(
owner_id, components, connections
)
except ValueError as error:
QMessageBox.warning(self, "Cannot Paste", str(error))
@staticmethod
def _clipboard_components() -> tuple[list[Component], list]:
mime_data = QApplication.clipboard().mimeData()
try:
if mime_data.hasFormat(SELECTION_MIME_TYPE):
payload = json.loads(
bytes(mime_data.data(SELECTION_MIME_TYPE)).decode("utf-8")
)
return (
[Component.from_dict(item) for item in payload.get("components", [])],
[Connection.from_dict(item) for item in payload.get("connections", [])],
)
if mime_data.hasFormat(COMPONENT_MIME_TYPE):
payload = json.loads(
bytes(mime_data.data(COMPONENT_MIME_TYPE)).decode("utf-8")
)
return [Component.from_dict(payload)], []
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as error:
raise ValueError("The clipboard does not contain a valid component") from error
raise ValueError("The clipboard does not contain a BEdit component")
@staticmethod
def _view_has_focus(view) -> bool:
focus = QApplication.focusWidget()
return focus is view or (focus is not None and view.isAncestorOf(focus))
@Slot()
def navigate_up(self) -> None:
if self._resolve_source_edits():
@@ -560,6 +691,8 @@ class MainWindow(QMainWindow):
def show_library_context_menu(self, position) -> None:
tree_view = self.ui.documentTreeView
index = tree_view.indexAt(position)
if index.isValid():
tree_view.setCurrentIndex(index)
kind = index.data(ITEM_KIND_ROLE)
if kind == "current-component":
component_id = index.data(COMPONENT_ID_ROLE)
@@ -577,6 +710,11 @@ class MainWindow(QMainWindow):
options_action = menu.addAction("Component Options…")
ports_action = menu.addAction("Port Options…")
parameters_action = menu.addAction("Parameter Options…")
menu.addSeparator()
copy_action = menu.addAction("Copy")
cut_action = menu.addAction("Cut")
paste_action = menu.addAction("Paste")
paste_action.setEnabled(component.implementation_kind == "graph")
delete_action = menu.addAction("Delete")
selected = menu.exec(tree_view.viewport().mapToGlobal(position))
if graph_action is not None and selected is graph_action:
@@ -589,6 +727,13 @@ class MainWindow(QMainWindow):
self.show_component_port_options(component_id)
elif selected is parameters_action:
self.show_component_parameter_options(component_id)
elif selected is copy_action:
self._copy_tree_component(tree_view)
elif selected is cut_action:
if self._copy_tree_component(tree_view):
self.document_controller.delete_component(component_id)
elif selected is paste_action:
self._paste_into_document_tree()
elif selected is delete_action:
answer = QMessageBox.question(
self,
@@ -604,24 +749,34 @@ class MainWindow(QMainWindow):
menu = QMenu(self)
graph_action = menu.addAction("New Graph Block")
text_action = menu.addAction("New Text Block")
menu.addSeparator()
paste_action = menu.addAction("Paste")
selected = menu.exec(tree_view.viewport().mapToGlobal(position))
if selected is graph_action:
self.document_controller.add_root("graph")
elif selected is text_action:
self.document_controller.add_root("text")
elif selected is paste_action:
self._paste_into_document_tree()
@Slot(object)
def show_external_library_context_menu(self, position) -> None:
tree = self.ui.treeView
index = tree.indexAt(position)
if index.isValid():
tree.setCurrentIndex(index)
component = index.data(COMPONENT_INSTANCE_ROLE)
if not isinstance(component, Component):
return
menu = QMenu(self)
copy_action = menu.addAction("Copy")
menu.addSeparator()
ports_action = menu.addAction("Port Options…")
parameters_action = menu.addAction("Parameter Options…")
selected = menu.exec(tree.viewport().mapToGlobal(position))
if selected is ports_action:
if selected is copy_action:
self._copy_tree_component(tree)
elif selected is ports_action:
dialog = PortOptionsDialog(component, self)
if dialog.exec() == dialog.DialogCode.Accepted:
old_inputs, old_outputs = deepcopy(component.inputs), deepcopy(component.outputs)
@@ -773,6 +928,7 @@ class MainWindow(QMainWindow):
event.ignore()
return
self.settings.setValue("window/geometry", self.saveGeometry())
self._simulation_window.close()
self.simulation.shutdown()
self.log.info("BEdit closed")
self.application_logger.removeHandler(self.log_handler)

View File

@@ -1,11 +1,12 @@
import re
from pathlib import Path
from PySide6.QtCore import Signal
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QFileDialog, QMainWindow, QMessageBox
from bedit.core.simulation.openmodelica import SimulationMessage, SimulationProgress
from bedit.core.simulation.results import (
SimulationExecutionResult,
SimulationResults,
load_simulation_results,
save_simulation_results,
@@ -31,6 +32,10 @@ class SimulationWindow(QMainWindow):
self.results = SimulationResults()
self._connect_actions()
self._populate_view_menu()
self.splitDockWidget(
self.ui.statusDock, self.ui.logDock, Qt.Orientation.Vertical
)
self.ui.statusDock.setFixedHeight(self.ui.statusDock.sizeHint().height())
self.progressReceived.connect(self._show_progress)
self.messageReceived.connect(self._show_message)
self.simulationFinished.connect(self._show_finished)
@@ -39,15 +44,18 @@ class SimulationWindow(QMainWindow):
def _connect_actions(self) -> None:
self.ui.actionOpen.triggered.connect(self.open_results)
self.ui.actionSave.triggered.connect(self.save_results)
self.ui.actionSaveAs.triggered.connect(self.save_results_as)
self.ui.actionClear.triggered.connect(self.clear)
self.ui.actionExit.triggered.connect(self.close)
self.ui.actionAbout.triggered.connect(self.show_about)
self.ui.actionAboutQt.triggered.connect(
lambda: QMessageBox.aboutQt(self, "About Qt Framework")
)
self.ui.actionToggleResults.toggled.connect(self.ui.centralWidget.setVisible)
def _populate_view_menu(self) -> None:
for panel in (self.ui.statusDock, self.ui.logDock, self.ui.resultsDock):
self.ui.menuPanels.addAction(self.ui.actionToggleResults)
for panel in (self.ui.statusDock, self.ui.logDock):
self.ui.menuPanels.addAction(panel.toggleViewAction())
self.ui.menuToolbars.addAction(self.ui.fileToolbar.toggleViewAction())
@@ -97,15 +105,18 @@ class SimulationWindow(QMainWindow):
)
def load_result_views(self) -> None:
"""Populate custom plots from ``self.results.traces``.
"""Populate custom plots from ``self.results.data`` and traces.
This is the intended integration point for a future plotting widget.
"""
data_count = len(self.results.data)
trace_count = len(self.results.traces)
if trace_count:
if data_count or trace_count:
sample_count = len(next(iter(self.results.data.values()), []))
self.ui.resultsPlaceholder.setText(
f"{trace_count} trace(s) loaded; add graph rendering here."
f"{data_count} data column(s), {sample_count} sample(s), and "
f"{trace_count} configured trace(s) loaded; add graph rendering here."
)
def open_results(self) -> None:
@@ -113,7 +124,7 @@ class SimulationWindow(QMainWindow):
self,
"Open Simulation Results",
"",
"BEdit Simulation Results (*.json);;JSON Files (*.json)",
"BEdit Binary Simulation Results (*.ber);;JSON Simulation Results (*.json)",
)
if not file_name:
return
@@ -131,18 +142,27 @@ class SimulationWindow(QMainWindow):
def save_results(self) -> None:
file_path = self._file_path
if file_path is None:
default_name = f"{_safe_file_stem(self.results.model_name)}-results.json"
file_name, _selected_filter = QFileDialog.getSaveFileName(
self,
"Save Simulation Results",
default_name,
"BEdit Simulation Results (*.json);;JSON Files (*.json)",
)
if not file_name:
return
file_path = Path(file_name)
if file_path.suffix.lower() != ".json":
file_path = file_path.with_suffix(".json")
self.save_results_as()
return
self._save_results_to(file_path)
def save_results_as(self) -> None:
default_name = f"{_safe_file_stem(self.results.model_name)}-results.ber"
file_name, selected_filter = QFileDialog.getSaveFileName(
self,
"Save Simulation Results As",
default_name,
"BEdit Binary Simulation Results (*.ber);;JSON Simulation Results (*.json)",
)
if not file_name:
return
file_path = Path(file_name)
if file_path.suffix.lower() not in {".ber", ".json"}:
suffix = ".json" if selected_filter.startswith("JSON") else ".ber"
file_path = file_path.with_suffix(suffix)
self._save_results_to(file_path)
def _save_results_to(self, file_path: Path) -> None:
try:
save_simulation_results(file_path, self.results)
except OSError as error:
@@ -209,7 +229,12 @@ class SimulationWindow(QMainWindow):
def _show_finished(self, result) -> None:
self._running = False
self.results.status.update(phase="Simulation finished", progress=10000)
self.results.metadata["processResult"] = result
if isinstance(result, SimulationExecutionResult):
self.results.data = result.data
self.results.metadata["processReturnCode"] = result.return_code
self.results.metadata["sourceResultFile"] = Path(result.result_file).name
else:
self.results.metadata["processResult"] = result
self.ui.progressBar.setValue(10000)
self.ui.statusLabel.setText("Simulation finished")
self.load_result_views()