chartflow 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chartflow/__init__.py +28 -0
- chartflow/_verify_importexport.py +169 -0
- chartflow/edit/__init__.py +119 -0
- chartflow/edit/code_editor.py +1007 -0
- chartflow/edit/code_folder.py +523 -0
- chartflow/edit/completer.py +2652 -0
- chartflow/edit/context_analyzer.py +521 -0
- chartflow/edit/demo.py +469 -0
- chartflow/edit/line_number_area.py +548 -0
- chartflow/edit/minimap.py +581 -0
- chartflow/edit/python_editor.py +509 -0
- chartflow/edit/syntax_highlighter.py +291 -0
- chartflow/edit/test_editor.py +190 -0
- chartflow/flow/__init__.py +31 -0
- chartflow/flow/_demo.py +98 -0
- chartflow/flow/chart.py +1101 -0
- chartflow/flow/editor.py +375 -0
- chartflow/flow/flow.py +6 -0
- chartflow/flow/qchart.py +1250 -0
- chartflow/flow/test/__init__.py +0 -0
- chartflow/flow/test/test_chart.py +537 -0
- chartflow/flow/test_decorator_sync.py +102 -0
- chartflow/fsm/__init__.py +84 -0
- chartflow/fsm/decorators.py +395 -0
- chartflow/fsm/demo.py +381 -0
- chartflow/fsm/demo_pyqt6.py +881 -0
- chartflow/fsm/demo_tree.py +46 -0
- chartflow/fsm/logic.py +447 -0
- chartflow/fsm/meta.py +569 -0
- chartflow/fsm/scheduler.py +427 -0
- chartflow/fsm/stage.py +2079 -0
- chartflow/fsm/stdio.py +66 -0
- chartflow/fsm/test/__init__.py +7 -0
- chartflow/fsm/test/test_decorators.py +210 -0
- chartflow/fsm/test/test_events.py +202 -0
- chartflow/fsm/test/test_integration.py +596 -0
- chartflow/fsm/test/test_logic.py +371 -0
- chartflow/fsm/test/test_meta.py +192 -0
- chartflow/fsm/test/test_runtime_param.py +336 -0
- chartflow/fsm/test/test_scheduler.py +280 -0
- chartflow/fsm/test/test_stage.py +314 -0
- chartflow/fsm/test_behavior_events.py +263 -0
- chartflow/fsm/test_nested_parallel.py +80 -0
- chartflow/fsm/test_stage_spec.py +448 -0
- chartflow/fsm/utils.py +34 -0
- chartflow/node/__init__.py +66 -0
- chartflow/node/_node_base.py +727 -0
- chartflow/node/_node_interaction.py +488 -0
- chartflow/node/_node_interface.py +426 -0
- chartflow/node/_node_markers.py +648 -0
- chartflow/node/_node_ports.py +536 -0
- chartflow/node/_port_interface.py +111 -0
- chartflow/node/arrow_item.py +287 -0
- chartflow/node/color_utils.py +113 -0
- chartflow/node/connection_item.py +939 -0
- chartflow/node/demo.py +258 -0
- chartflow/node/demo_arrow_drag.py +46 -0
- chartflow/node/demo_decorator.py +63 -0
- chartflow/node/demo_ports.py +69 -0
- chartflow/node/enums.py +35 -0
- chartflow/node/example.py +84 -0
- chartflow/node/layout_utils.py +307 -0
- chartflow/node/main_window.py +196 -0
- chartflow/node/menu_bar.py +240 -0
- chartflow/node/node_canvas.py +1730 -0
- chartflow/node/node_item.py +754 -0
- chartflow/node/popmenu.py +472 -0
- chartflow/node/port_item.py +591 -0
- chartflow/node/qnode_editor.py +73 -0
- chartflow/node/selection_rect.py +53 -0
- chartflow/node/style_panel.py +615 -0
- chartflow/node/styles.py +63 -0
- chartflow/node/test_qnode.py +2336 -0
- chartflow/showcase.py +528 -0
- chartflow/theme.py +508 -0
- chartflow-0.1.dist-info/METADATA +23 -0
- chartflow-0.1.dist-info/RECORD +79 -0
- chartflow-0.1.dist-info/WHEEL +5 -0
- chartflow-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"""
|
|
2
|
+
右键菜单模块 - 画布 / 节点 / 连线 三种弹出菜单
|
|
3
|
+
|
|
4
|
+
按右键命中的目标拆分为三种菜单, 每种各有一个 Protocol 契约(IXxxPopMenu)
|
|
5
|
+
和一个 QMenu 实现类(QXxxPopMenu)。契约仅用于静态类型检查与文档, 不参与
|
|
6
|
+
运行时继承(结构子类型)。
|
|
7
|
+
|
|
8
|
+
持有关系(由宿主持有):
|
|
9
|
+
- ``NodeCanvas.menu`` -> ``QCanvasPopMenu`` (画布空白右键, 长期持有)
|
|
10
|
+
- ``NodeCanvas.lmenu`` -> ``QLinePopMenu`` (连线/箭头右键, 每次右键重构)
|
|
11
|
+
- ``NodeItem.menu`` -> ``QNodePopMenu`` (节点右键, 由 canvas.addNode 注入)
|
|
12
|
+
|
|
13
|
+
构造签名:
|
|
14
|
+
- ``QCanvasPopMenu(canvas)``
|
|
15
|
+
- ``QNodePopMenu(canvas, node)``
|
|
16
|
+
- ``QLinePopMenu(canvas, source, target)`` 其中端点为 NodeItem 或 QNodePort
|
|
17
|
+
|
|
18
|
+
实现约定: 每个 exec() 调用都先 ``clear()`` 再 ``addAction`` 重建菜单项,
|
|
19
|
+
使动态状态(节点当前形状、Set as Start 的 enabled、节点计数等)总是最新。
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from typing import Optional, Union, Protocol, runtime_checkable, TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
from PyQt6.QtWidgets import QMenu
|
|
25
|
+
from PyQt6.QtCore import QPointF
|
|
26
|
+
|
|
27
|
+
from chartflow.node.enums import NodeShape
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from chartflow.node.node_canvas import QNodeCanvas
|
|
31
|
+
from chartflow.node.node_item import QNodeItem
|
|
32
|
+
from chartflow.node.connection_item import ConnectionItem
|
|
33
|
+
from chartflow.node.port_item import QNodePort
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# 连线端点: 可为节点(NodeItem)或端口渲染项(QNodePort)。
|
|
37
|
+
# 端口优先: 若该端锚定到 port, 用 QNodePort; 否则用 NodeItem。
|
|
38
|
+
PortEndpoint = Union["NodeItem", "QNodePort"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ====================================================================== #
|
|
42
|
+
# 画布右键菜单
|
|
43
|
+
# ====================================================================== #
|
|
44
|
+
@runtime_checkable
|
|
45
|
+
class ICanvasPopMenu(Protocol):
|
|
46
|
+
"""画布右键菜单接口契约
|
|
47
|
+
|
|
48
|
+
声明空白画布右键菜单应具备的对外属性与方法。具体实现由
|
|
49
|
+
:class:`QCanvasPopMenu` 承担。
|
|
50
|
+
|
|
51
|
+
动作:
|
|
52
|
+
- **Add Node**: 在右键场景坐标处添加节点(命名 ``Node{N+1}``)
|
|
53
|
+
- **Layout**: 自动布局(Compact/Circular/Grid/Hierarchical/Force Directed)
|
|
54
|
+
- **Fit to Viewport**: 自动适应视口
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def canvas(self) -> "QNodeCanvas":
|
|
59
|
+
"""菜单绑定的画布(只读)。"""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
def addNode(self, pos: QPointF) -> None:
|
|
63
|
+
"""在场景坐标 ``pos`` 处添加一个新节点。"""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
def layout(self, strategy: str) -> None:
|
|
67
|
+
"""执行自动布局。
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
strategy: 布局策略, 取值 ``compact`` / ``circular`` / ``grid`` /
|
|
71
|
+
``hierarchical`` / ``force``。
|
|
72
|
+
"""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
def fitViewport(self) -> None:
|
|
76
|
+
"""自动适应视口(缩放并居中到全部节点)。"""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
def exec(self, global_pos, scene_pos: Optional[QPointF] = None) -> None:
|
|
80
|
+
"""重建菜单项并弹出。
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
global_pos: 菜单弹出的全局坐标(QPoint)。
|
|
84
|
+
scene_pos: 右键命中位置的场景坐标(QPointF); Add Node 动作用作新节点
|
|
85
|
+
的落点。``None`` 时回退到原点。
|
|
86
|
+
"""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class QCanvasPopMenu(QMenu):
|
|
91
|
+
"""画布右键菜单实现
|
|
92
|
+
|
|
93
|
+
长期持有(由 ``NodeCanvas.menu`` 持有); 每次 :meth:`exec` 先 ``clear()``
|
|
94
|
+
再重建动作, 保证动态状态新鲜。
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, canvas: "QNodeCanvas", parent=None) -> None:
|
|
98
|
+
super().__init__(parent)
|
|
99
|
+
self._canvas: "QNodeCanvas" = canvas
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def canvas(self) -> "QNodeCanvas":
|
|
103
|
+
"""获取绑定的画布"""
|
|
104
|
+
return self._canvas
|
|
105
|
+
|
|
106
|
+
# ---- 槽位(转发到画布私有辅助方法) ----
|
|
107
|
+
def addNode(self, pos: QPointF) -> None:
|
|
108
|
+
"""在指定场景坐标添加节点"""
|
|
109
|
+
self._canvas._onAddNodeAt(pos)
|
|
110
|
+
|
|
111
|
+
def layout(self, strategy: str) -> None:
|
|
112
|
+
"""执行自动布局"""
|
|
113
|
+
self._canvas._onLayout(strategy)
|
|
114
|
+
|
|
115
|
+
def fitViewport(self) -> None:
|
|
116
|
+
"""自动适应视口"""
|
|
117
|
+
self._canvas._onFitViewport()
|
|
118
|
+
|
|
119
|
+
def exec(self, global_pos, scene_pos: Optional[QPointF] = None) -> None:
|
|
120
|
+
"""重建并弹出菜单。
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
global_pos: 菜单弹出的全局坐标。
|
|
124
|
+
scene_pos: 右键命中位置的场景坐标; 作为 Add Node 的落点。
|
|
125
|
+
"""
|
|
126
|
+
self.clear()
|
|
127
|
+
|
|
128
|
+
# 添加节点
|
|
129
|
+
add_node_action = self.addAction("Add Node")
|
|
130
|
+
self.addSeparator()
|
|
131
|
+
|
|
132
|
+
# 布局动作
|
|
133
|
+
layout_menu = self.addMenu("Layout")
|
|
134
|
+
layout_actions = {
|
|
135
|
+
"Compact": "compact",
|
|
136
|
+
"Circular": "circular",
|
|
137
|
+
"Grid": "grid",
|
|
138
|
+
"Hierarchical": "hierarchical",
|
|
139
|
+
"Force Directed": "force",
|
|
140
|
+
}
|
|
141
|
+
for label, key in layout_actions.items():
|
|
142
|
+
action = layout_menu.addAction(label)
|
|
143
|
+
action.triggered.connect(lambda _checked=False, k=key: self.layout(k))
|
|
144
|
+
|
|
145
|
+
self.addSeparator()
|
|
146
|
+
|
|
147
|
+
# 自动适应视口
|
|
148
|
+
fit_viewport_action = self.addAction("Fit to Viewport")
|
|
149
|
+
|
|
150
|
+
chosen = super().exec(global_pos)
|
|
151
|
+
if chosen == add_node_action:
|
|
152
|
+
self.addNode(scene_pos if scene_pos is not None else QPointF())
|
|
153
|
+
elif chosen == fit_viewport_action:
|
|
154
|
+
self.fitViewport()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ====================================================================== #
|
|
158
|
+
# 节点右键菜单
|
|
159
|
+
# ====================================================================== #
|
|
160
|
+
@runtime_checkable
|
|
161
|
+
class INodePopMenu(Protocol):
|
|
162
|
+
"""节点右键菜单接口契约
|
|
163
|
+
|
|
164
|
+
声明节点右键菜单应具备的对外属性与方法。具体实现由
|
|
165
|
+
:class:`QNodePopMenu` 承担。
|
|
166
|
+
|
|
167
|
+
动作:
|
|
168
|
+
- **Edit**: 请求编辑(有 editorClass 则打开, 否则发射 editRequested 信号)
|
|
169
|
+
- **Rename**: 重命名(占位, 暂未实现)
|
|
170
|
+
- **Delete**: 删除节点
|
|
171
|
+
- **Shape**: 切换形状(Ellipse/Rectangle/Diamond, checkable)
|
|
172
|
+
- **Set as Start Node**: 设为起点(enabled 当 canvas.startNode != node)
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def node(self) -> "QNodeItem":
|
|
177
|
+
"""菜单绑定的节点(只读)。"""
|
|
178
|
+
...
|
|
179
|
+
|
|
180
|
+
def edit(self) -> None:
|
|
181
|
+
"""请求编辑本节点。"""
|
|
182
|
+
...
|
|
183
|
+
|
|
184
|
+
def rename(self) -> None:
|
|
185
|
+
"""重命名本节点(占位)。"""
|
|
186
|
+
...
|
|
187
|
+
|
|
188
|
+
def delete(self, canvas: "QNodeCanvas") -> None:
|
|
189
|
+
"""删除本节点。
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
canvas: 节点所在的画布。
|
|
193
|
+
"""
|
|
194
|
+
...
|
|
195
|
+
|
|
196
|
+
def setShape(self, shape: NodeShape) -> None:
|
|
197
|
+
"""切换节点形状。
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
shape: 目标形状(NodeShape.ELLIPSE/RECTANGLE/DIAMOND)。
|
|
201
|
+
"""
|
|
202
|
+
...
|
|
203
|
+
|
|
204
|
+
def setAsStart(self, canvas: "QNodeCanvas") -> None:
|
|
205
|
+
"""将本节点设为起点。
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
canvas: 节点所在的画布。
|
|
209
|
+
"""
|
|
210
|
+
...
|
|
211
|
+
|
|
212
|
+
def exec(self, global_pos, canvas: Optional["QNodeCanvas"] = None) -> None:
|
|
213
|
+
"""重建菜单项并弹出。
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
global_pos: 菜单弹出的全局坐标(QPoint)。
|
|
217
|
+
canvas: 画布引用; 传入时启用 Delete / Set as Start 动作,
|
|
218
|
+
``None`` 时这两项被禁用。默认沿用构造时的 canvas。
|
|
219
|
+
"""
|
|
220
|
+
...
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class QNodePopMenu(QMenu):
|
|
224
|
+
"""节点右键菜单实现
|
|
225
|
+
|
|
226
|
+
由 ``NodeCanvas.addNode`` 注入到 ``node.menu``。每次 :meth:`exec` 先
|
|
227
|
+
``clear()`` 再重建动作。
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def __init__(
|
|
231
|
+
self,
|
|
232
|
+
canvas: "QNodeCanvas",
|
|
233
|
+
node: "QNodeItem",
|
|
234
|
+
parent=None,
|
|
235
|
+
) -> None:
|
|
236
|
+
super().__init__(parent)
|
|
237
|
+
self._canvas: "QNodeCanvas" = canvas
|
|
238
|
+
self._node: "QNodeItem" = node
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def node(self) -> "QNodeItem":
|
|
242
|
+
"""获取绑定的节点"""
|
|
243
|
+
return self._node
|
|
244
|
+
|
|
245
|
+
# ---- 槽位 ----
|
|
246
|
+
def edit(self) -> None:
|
|
247
|
+
"""请求编辑本节点。
|
|
248
|
+
|
|
249
|
+
有 editorClass 时实例化并打开编辑器; 否则发射 editRequested 信号,
|
|
250
|
+
由宿主(画布/窗口)处理。与原 _onNodeEditRequested 行为一致。
|
|
251
|
+
"""
|
|
252
|
+
node = self._node
|
|
253
|
+
if node.editorClass:
|
|
254
|
+
editor = node.editorClass(node)
|
|
255
|
+
editor.setWindowTitle(editor.getTitle())
|
|
256
|
+
editor.show()
|
|
257
|
+
else:
|
|
258
|
+
node.editRequested.emit(node)
|
|
259
|
+
|
|
260
|
+
def rename(self) -> None:
|
|
261
|
+
"""重命名(占位, 暂未实现)"""
|
|
262
|
+
# TODO: 实现重命名功能
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
def delete(self, canvas: "QNodeCanvas") -> None:
|
|
266
|
+
"""删除本节点"""
|
|
267
|
+
canvas.removeNode(self._node)
|
|
268
|
+
|
|
269
|
+
def setShape(self, shape: NodeShape) -> None:
|
|
270
|
+
"""切换节点形状"""
|
|
271
|
+
self._node.nodeShape = shape
|
|
272
|
+
|
|
273
|
+
def setAsStart(self, canvas: "QNodeCanvas") -> None:
|
|
274
|
+
"""将本节点设为起点"""
|
|
275
|
+
canvas.startNode = self._node
|
|
276
|
+
|
|
277
|
+
def exec(self, global_pos, canvas: Optional["QNodeCanvas"] = None) -> None:
|
|
278
|
+
"""重建并弹出菜单。
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
global_pos: 菜单弹出的全局坐标。
|
|
282
|
+
canvas: 画布引用; 默认沿用构造时的 canvas。``None`` 时 Delete /
|
|
283
|
+
Set as Start 被禁用。
|
|
284
|
+
"""
|
|
285
|
+
c = canvas if canvas is not None else self._canvas
|
|
286
|
+
|
|
287
|
+
self.clear()
|
|
288
|
+
|
|
289
|
+
# 编辑和重命名
|
|
290
|
+
edit_action = self.addAction("Edit")
|
|
291
|
+
rename_action = self.addAction("Rename")
|
|
292
|
+
delete_action = self.addAction("Delete")
|
|
293
|
+
delete_action.setEnabled(c is not None)
|
|
294
|
+
self.addSeparator()
|
|
295
|
+
|
|
296
|
+
# 形状切换子菜单
|
|
297
|
+
shape_menu = self.addMenu("Shape")
|
|
298
|
+
shape_actions: dict = {}
|
|
299
|
+
for shape, label in (
|
|
300
|
+
(NodeShape.ELLIPSE, "Ellipse"),
|
|
301
|
+
(NodeShape.RECTANGLE, "Rectangle"),
|
|
302
|
+
(NodeShape.DIAMOND, "Diamond"),
|
|
303
|
+
):
|
|
304
|
+
action = shape_menu.addAction(label)
|
|
305
|
+
action.setCheckable(True)
|
|
306
|
+
action.setChecked(self._node.nodeShape == shape)
|
|
307
|
+
shape_actions[action] = shape
|
|
308
|
+
|
|
309
|
+
self.addSeparator()
|
|
310
|
+
|
|
311
|
+
# 设置起点
|
|
312
|
+
set_start_action = self.addAction("Set as Start Node")
|
|
313
|
+
set_start_action.setEnabled(c is not None and c.startNode is not self._node)
|
|
314
|
+
|
|
315
|
+
chosen = super().exec(global_pos)
|
|
316
|
+
if chosen == edit_action:
|
|
317
|
+
self.edit()
|
|
318
|
+
elif chosen == rename_action:
|
|
319
|
+
self.rename()
|
|
320
|
+
elif chosen == delete_action and c is not None:
|
|
321
|
+
self.delete(c)
|
|
322
|
+
elif chosen in shape_actions:
|
|
323
|
+
self.setShape(shape_actions[chosen])
|
|
324
|
+
elif chosen == set_start_action and c is not None:
|
|
325
|
+
self.setAsStart(c)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# ====================================================================== #
|
|
329
|
+
# 连线右键菜单
|
|
330
|
+
# ====================================================================== #
|
|
331
|
+
@runtime_checkable
|
|
332
|
+
class ILinePopMenu(Protocol):
|
|
333
|
+
"""连线右键菜单接口契约
|
|
334
|
+
|
|
335
|
+
声明连线/箭头右键菜单应具备的对外属性与方法。具体实现由
|
|
336
|
+
:class:`QLinePopMenu` 承担。
|
|
337
|
+
|
|
338
|
+
动作:
|
|
339
|
+
- **Toggle Line Type (Bezier/Straight)**: 切换连线类型(贝塞尔 <-> 直线)
|
|
340
|
+
- **Delete Connection**: 删除该连线
|
|
341
|
+
|
|
342
|
+
Note:
|
|
343
|
+
连线由两端点(source/target)标识, 每个端点为节点或端口。构造时传入
|
|
344
|
+
两端点, 菜单据此在画布的连接列表中精确定位到目标 ConnectionItem。
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
@property
|
|
348
|
+
def canvas(self) -> "QNodeCanvas":
|
|
349
|
+
"""菜单绑定的画布(只读)。"""
|
|
350
|
+
...
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def source(self) -> PortEndpoint:
|
|
354
|
+
"""连线源端点(NodeItem 或 QNodePort, 只读)。"""
|
|
355
|
+
...
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def target(self) -> PortEndpoint:
|
|
359
|
+
"""连线目标端点(NodeItem 或 QNodePort, 只读)。"""
|
|
360
|
+
...
|
|
361
|
+
|
|
362
|
+
def toggleLineType(self) -> None:
|
|
363
|
+
"""切换连线类型(贝塞尔 <-> 直线)。"""
|
|
364
|
+
...
|
|
365
|
+
|
|
366
|
+
def removeConnection(self) -> None:
|
|
367
|
+
"""删除该连线。"""
|
|
368
|
+
...
|
|
369
|
+
|
|
370
|
+
def exec(self, global_pos) -> None:
|
|
371
|
+
"""重建菜单项并弹出。
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
global_pos: 菜单弹出的全局坐标(QPoint)。
|
|
375
|
+
"""
|
|
376
|
+
...
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class QLinePopMenu(QMenu):
|
|
380
|
+
"""连线右键菜单实现
|
|
381
|
+
|
|
382
|
+
每次右键由画布 ``contextMenuEvent`` 重构(``canvas.lmenu = QLinePopMenu(...)``)
|
|
383
|
+
再 :meth:`exec`, 因为两端点随右键目标变化。原箭头菜单的 "Remove Connection"
|
|
384
|
+
合并入 "Delete Connection"(同一行为)。
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
def __init__(
|
|
388
|
+
self,
|
|
389
|
+
canvas: "QNodeCanvas",
|
|
390
|
+
source: PortEndpoint,
|
|
391
|
+
target: PortEndpoint,
|
|
392
|
+
parent=None,
|
|
393
|
+
) -> None:
|
|
394
|
+
super().__init__(parent)
|
|
395
|
+
self._canvas: "QNodeCanvas" = canvas
|
|
396
|
+
self._source: PortEndpoint = source
|
|
397
|
+
self._target: PortEndpoint = target
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def canvas(self) -> "QNodeCanvas":
|
|
401
|
+
"""获取绑定的画布"""
|
|
402
|
+
return self._canvas
|
|
403
|
+
|
|
404
|
+
@property
|
|
405
|
+
def source(self) -> PortEndpoint:
|
|
406
|
+
"""获取源端点"""
|
|
407
|
+
return self._source
|
|
408
|
+
|
|
409
|
+
@property
|
|
410
|
+
def target(self) -> PortEndpoint:
|
|
411
|
+
"""获取目标端点"""
|
|
412
|
+
return self._target
|
|
413
|
+
|
|
414
|
+
def _findConnection(self) -> Optional["ConnectionItem"]:
|
|
415
|
+
"""按 (源端点, 目标端点) 在画布连接列表中精确定位连线。
|
|
416
|
+
|
|
417
|
+
端点为 QNodePort 时用 ``is`` 比较; 为 NodeItem 时要求该端 port 为 None
|
|
418
|
+
(即节点边缘锚定, 非 port 锚定)。端点组合唯一确定一条连线。
|
|
419
|
+
"""
|
|
420
|
+
from chartflow.node.port_item import QNodePort as _QNodePort
|
|
421
|
+
|
|
422
|
+
src_is_port = isinstance(self._source, _QNodePort)
|
|
423
|
+
tgt_is_port = isinstance(self._target, _QNodePort)
|
|
424
|
+
|
|
425
|
+
for conn in self._canvas._connections:
|
|
426
|
+
if src_is_port:
|
|
427
|
+
src_match = conn.sourcePort is self._source
|
|
428
|
+
else:
|
|
429
|
+
src_match = (conn.sourceNode is self._source
|
|
430
|
+
and conn.sourcePort is None)
|
|
431
|
+
if not src_match:
|
|
432
|
+
continue
|
|
433
|
+
if tgt_is_port:
|
|
434
|
+
tgt_match = conn.targetPort is self._target
|
|
435
|
+
else:
|
|
436
|
+
tgt_match = (conn.targetNode is self._target
|
|
437
|
+
and conn.targetPort is None)
|
|
438
|
+
if tgt_match:
|
|
439
|
+
return conn
|
|
440
|
+
return None
|
|
441
|
+
|
|
442
|
+
# ---- 槽位 ----
|
|
443
|
+
def toggleLineType(self) -> None:
|
|
444
|
+
"""切换连线类型(贝塞尔 <-> 直线)"""
|
|
445
|
+
conn = self._findConnection()
|
|
446
|
+
if conn is not None:
|
|
447
|
+
conn.toggleConnectionType()
|
|
448
|
+
|
|
449
|
+
def removeConnection(self) -> None:
|
|
450
|
+
"""删除该连线"""
|
|
451
|
+
conn = self._findConnection()
|
|
452
|
+
if conn is not None:
|
|
453
|
+
self._canvas.removeConnection(conn)
|
|
454
|
+
|
|
455
|
+
def exec(self, global_pos) -> None:
|
|
456
|
+
"""重建并弹出菜单。
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
global_pos: 菜单弹出的全局坐标。
|
|
460
|
+
"""
|
|
461
|
+
self.clear()
|
|
462
|
+
|
|
463
|
+
# 切换连接类型
|
|
464
|
+
toggle_type_action = self.addAction("Toggle Line Type (Bezier/Straight)")
|
|
465
|
+
self.addSeparator()
|
|
466
|
+
remove_action = self.addAction("Delete Connection")
|
|
467
|
+
|
|
468
|
+
chosen = super().exec(global_pos)
|
|
469
|
+
if chosen == toggle_type_action:
|
|
470
|
+
self.toggleLineType()
|
|
471
|
+
elif chosen == remove_action:
|
|
472
|
+
self.removeConnection()
|