chartflow 0.1.2__tar.gz → 0.1.4__tar.gz
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-0.1.2 → chartflow-0.1.4}/PKG-INFO +1 -1
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/__init__.py +9 -0
- chartflow-0.1.4/chartflow/node/_connection_interface.py +196 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/_node_ports.py +50 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/connection_item.py +227 -4
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/example.py +45 -6
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/node_canvas.py +96 -11
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/popmenu.py +26 -1
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/port_item.py +23 -2
- chartflow-0.1.4/chartflow/node/qconnection_editor.py +280 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/test_qnode.py +233 -1
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow.egg-info/PKG-INFO +1 -1
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow.egg-info/SOURCES.txt +2 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/setup.py +1 -1
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/__init__.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/_verify_importexport.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/edit/__init__.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/edit/code_editor.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/edit/code_folder.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/edit/completer.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/edit/context_analyzer.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/edit/demo.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/edit/line_number_area.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/edit/minimap.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/edit/python_editor.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/edit/qcompletion.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/edit/syntax_highlighter.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/edit/test_editor.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/flow/__init__.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/flow/_demo.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/flow/chart.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/flow/editor.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/flow/flow.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/flow/qchart.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/flow/test/__init__.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/flow/test/test_chart.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/flow/test_decorator_sync.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/__init__.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/decorators.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/demo.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/demo_pyqt6.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/demo_tree.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/logic.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/meta.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/scheduler.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/stage.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/stdio.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/test/__init__.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/test/test_decorators.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/test/test_events.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/test/test_integration.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/test/test_logic.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/test/test_meta.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/test/test_runtime_param.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/test/test_scheduler.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/test/test_stage.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/test_behavior_events.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/test_nested_parallel.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/test_stage_spec.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/fsm/utils.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/_node_base.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/_node_interaction.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/_node_interface.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/_node_markers.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/_port_interface.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/arrow_item.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/color_utils.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/demo.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/demo_arrow_drag.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/demo_decorator.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/demo_ports.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/enums.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/layout_utils.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/main_window.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/menu_bar.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/node_item.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/qnode_editor.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/selection_rect.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/style_panel.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/node/styles.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/showcase.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow/theme.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow.egg-info/dependency_links.txt +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow.egg-info/requires.txt +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/chartflow.egg-info/top_level.txt +0 -0
- {chartflow-0.1.2 → chartflow-0.1.4}/setup.cfg +0 -0
|
@@ -19,12 +19,16 @@ from chartflow.node.node_item import QNodeItem
|
|
|
19
19
|
from chartflow.node.port_item import QNodePort, QNodePortSpec
|
|
20
20
|
from chartflow.node._port_interface import IQNodePort
|
|
21
21
|
from chartflow.node.connection_item import ConnectionItem
|
|
22
|
+
from chartflow.node._connection_interface import IConnectionItem, IQConnectionEditor
|
|
22
23
|
from chartflow.node.arrow_item import ArrowItem
|
|
23
24
|
from chartflow.node.selection_rect import SelectionRect
|
|
24
25
|
from chartflow.node.style_panel import StylePanel
|
|
25
26
|
from chartflow.node.node_canvas import QNodeCanvas
|
|
26
27
|
from chartflow.node.main_window import NodeCanvasWindow
|
|
27
28
|
from chartflow.node.qnode_editor import QNodeEditor
|
|
29
|
+
from chartflow.node.qconnection_editor import (
|
|
30
|
+
QConnectionEditor, QConnectionNameEditor, openConnectionEditor,
|
|
31
|
+
)
|
|
28
32
|
from chartflow.node.popmenu import (
|
|
29
33
|
ICanvasPopMenu, QCanvasPopMenu,
|
|
30
34
|
INodePopMenu, QNodePopMenu,
|
|
@@ -50,6 +54,7 @@ __all__ = [
|
|
|
50
54
|
"QNodePortSpec",
|
|
51
55
|
"IQNodePort",
|
|
52
56
|
"ConnectionItem",
|
|
57
|
+
"IConnectionItem",
|
|
53
58
|
"ArrowItem",
|
|
54
59
|
"SelectionRect",
|
|
55
60
|
# UI Components
|
|
@@ -59,6 +64,10 @@ __all__ = [
|
|
|
59
64
|
"NodeCanvasWindow",
|
|
60
65
|
# Editor
|
|
61
66
|
"QNodeEditor",
|
|
67
|
+
"QConnectionEditor",
|
|
68
|
+
"QConnectionNameEditor",
|
|
69
|
+
"IQConnectionEditor",
|
|
70
|
+
"openConnectionEditor",
|
|
62
71
|
# Pop Menus
|
|
63
72
|
"ICanvasPopMenu", "QCanvasPopMenu",
|
|
64
73
|
"INodePopMenu", "QNodePopMenu",
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
连接项接口契约模块
|
|
3
|
+
|
|
4
|
+
定义 IConnectionItem(Protocol) 与 IQConnectionEditor(Protocol) ——
|
|
5
|
+
连接项(连线)及其编辑器的对外类型契约。
|
|
6
|
+
|
|
7
|
+
Protocol 仅用于静态类型检查与文档, 不参与运行时继承(不进入 MRO),
|
|
8
|
+
因此可安全地与 QGraphicsPathItem / QWidget 继承体系并存。
|
|
9
|
+
|
|
10
|
+
IConnectionItem 声明了 ConnectionItem 对外暴露的名称/数据/编辑器/
|
|
11
|
+
信号/序列化接口; IQConnectionEditor 声明了连线编辑器(见
|
|
12
|
+
qconnection_editor.QConnectionEditor)的对外接口, 与节点编辑器
|
|
13
|
+
QNodeEditor 的接口形态保持一致。
|
|
14
|
+
|
|
15
|
+
约定: 本模块只含类型声明, 不得有运行时依赖, 以避免循环导入。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from typing import Any, Dict, Optional, Protocol, runtime_checkable
|
|
19
|
+
|
|
20
|
+
from PyQt6.QtCore import QPointF, pyqtSignal
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@runtime_checkable
|
|
24
|
+
class IConnectionItem(Protocol):
|
|
25
|
+
"""连接项接口契约
|
|
26
|
+
|
|
27
|
+
声明一条连线(ConnectionItem)在 名称 / 编辑器 / 序列化 方面应具备的
|
|
28
|
+
对外属性与方法。几何/样式/箭头/端口锚定等既有接口不在此重复声明。
|
|
29
|
+
|
|
30
|
+
契约分组:
|
|
31
|
+
|
|
32
|
+
- **名称与数据**: ``name`` / ``data``
|
|
33
|
+
- **编辑器**: ``editorClass`` / ``editorEmbedded`` / ``isEditWindowOpen`` /
|
|
34
|
+
:meth:`setEditWindowOpen` / :meth:`closeEditor`
|
|
35
|
+
- **几何**: ``center``(连线中点, 编辑器/名称 label 的锚点)
|
|
36
|
+
- **信号**: ``editRequested`` / ``nameChanged``
|
|
37
|
+
- **序列化**: :meth:`toDict` / :meth:`fromDict`
|
|
38
|
+
|
|
39
|
+
所有坐标默认为 **场景坐标(scene coordinate)**, 几何单位为像素。
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# ---- 名称与数据 ----
|
|
43
|
+
@property
|
|
44
|
+
def name(self) -> str:
|
|
45
|
+
"""连线名称(可读写)。
|
|
46
|
+
|
|
47
|
+
读取返回当前名称(可为空串); 赋值会更新连线中点的名称 label
|
|
48
|
+
(空串时隐藏)并发出 :attr:`nameChanged` 信号。名称会被
|
|
49
|
+
:meth:`toDict` / :meth:`fromDict` 序列化。
|
|
50
|
+
"""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
@name.setter
|
|
54
|
+
def name(self, value: str) -> None: ...
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def data(self) -> Dict[str, Any]:
|
|
58
|
+
"""连线自定义数据字典(只读引用, 内容可改)。
|
|
59
|
+
|
|
60
|
+
返回内部 ``_data`` 字典本身而非副本, 调用方可直接增删键值,
|
|
61
|
+
供自定义编辑器(如保存备注/参数)持久化内容使用。
|
|
62
|
+
会被 :meth:`toDict` / :meth:`fromDict` 序列化。
|
|
63
|
+
"""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
# ---- 编辑器 ----
|
|
67
|
+
@property
|
|
68
|
+
def editorClass(self) -> Optional[type]:
|
|
69
|
+
"""连线编辑器类(可读写)。
|
|
70
|
+
|
|
71
|
+
设为某个 ``QWidget`` 子类 ``type`` 时, 双击连线(或右键菜单
|
|
72
|
+
Edit Connection)会通过 :attr:`editRequested` 信号请求由该类创建
|
|
73
|
+
编辑面板; ``None`` 表示使用内置默认名称编辑器
|
|
74
|
+
(:class:`~chartflow.node.qconnection_editor.QConnectionNameEditor`)。
|
|
75
|
+
编辑器类本身不可序列化(与节点 editorClass 一致)。
|
|
76
|
+
"""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
@editorClass.setter
|
|
80
|
+
def editorClass(self, value: Optional[type]) -> None: ...
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def editorEmbedded(self) -> bool:
|
|
84
|
+
"""编辑器是否嵌入连线中点(可读写, 默认 ``True``)。
|
|
85
|
+
|
|
86
|
+
- ``True``: 编辑器经 ``QGraphicsProxyWidget`` 嵌入连线中点, 随连线
|
|
87
|
+
路径更新自动跟随;
|
|
88
|
+
- ``False``: 编辑器作为独立浮动窗口打开(与节点编辑器一致)。
|
|
89
|
+
|
|
90
|
+
该状态会被 :meth:`toDict` / :meth:`fromDict` 序列化(仅在
|
|
91
|
+
``False`` 时落盘, 避免污染默认导出)。
|
|
92
|
+
"""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
@editorEmbedded.setter
|
|
96
|
+
def editorEmbedded(self, value: bool) -> None: ...
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def isEditWindowOpen(self) -> bool:
|
|
100
|
+
"""当前是否有编辑器窗口/嵌入面板打开(只读)。"""
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
def setEditWindowOpen(self, open: bool) -> None:
|
|
104
|
+
"""切换编辑窗口打开状态, 进而控制中点名称 label 的高亮提示。
|
|
105
|
+
|
|
106
|
+
由编辑器(如 :class:`QConnectionEditor`)在 show/close 时自动驱动;
|
|
107
|
+
打开时名称 label 显示高亮底衬(类比节点的 📖 悬浮标记)。
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
open: ``True`` 表示编辑器已打开, ``False`` 已关闭。
|
|
111
|
+
"""
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
def closeEditor(self) -> None:
|
|
115
|
+
"""关闭当前打开的编辑器并清理嵌入代理(无打开编辑器时为空操作)。
|
|
116
|
+
|
|
117
|
+
画布在删除连线(:meth:`NodeCanvas.removeConnection`)时调用,
|
|
118
|
+
防止编辑器窗口/嵌入代理悬挂。
|
|
119
|
+
"""
|
|
120
|
+
...
|
|
121
|
+
|
|
122
|
+
# ---- 几何 ----
|
|
123
|
+
@property
|
|
124
|
+
def center(self) -> QPointF:
|
|
125
|
+
"""连线中点的 **场景坐标**(只读), 取自 ``path().pointAtPercent(0.5)``。
|
|
126
|
+
|
|
127
|
+
作为名称 label 与嵌入编辑器的锚点; 自环连线同样按路径 50% 处取值。
|
|
128
|
+
"""
|
|
129
|
+
...
|
|
130
|
+
|
|
131
|
+
# ---- 信号(只读属性, 返回 pyqtSignal) ----
|
|
132
|
+
@property
|
|
133
|
+
def editRequested(self) -> pyqtSignal:
|
|
134
|
+
"""编辑请求信号(只读)。
|
|
135
|
+
|
|
136
|
+
双击连线或点击右键菜单 Edit Connection 时发射, 载荷为本连线实例;
|
|
137
|
+
画布默认连接到打开编辑器的处理器(无 editorClass 时打开内置名称
|
|
138
|
+
编辑器), 宿主可自行断开/接管。
|
|
139
|
+
"""
|
|
140
|
+
...
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def nameChanged(self) -> pyqtSignal:
|
|
144
|
+
"""名称变更信号(只读)。名称被修改时发射, 载荷为 (连线, 新名称)。"""
|
|
145
|
+
...
|
|
146
|
+
|
|
147
|
+
# ---- 序列化 ----
|
|
148
|
+
def toDict(self) -> dict:
|
|
149
|
+
"""将连线的 名称/数据/编辑器布局 序列化为字典。
|
|
150
|
+
|
|
151
|
+
仅包含非默认键: ``name``(非空) / ``data``(非空) /
|
|
152
|
+
``editor_embedded``(仅 ``False``)。拓扑(source/target/port)与样式
|
|
153
|
+
由画布 export 与 styleToDict 负责, 不在此重复。
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
连线数据字典(深拷贝, 修改返回值不影响连线)。
|
|
157
|
+
"""
|
|
158
|
+
...
|
|
159
|
+
|
|
160
|
+
def fromDict(self, data: dict) -> None:
|
|
161
|
+
"""从字典恢复连线的 名称/数据/编辑器布局(增量合并, 忽略未知键)。
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
data: 由 :meth:`toDict` 产生(或同构)的字典。
|
|
165
|
+
"""
|
|
166
|
+
...
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@runtime_checkable
|
|
170
|
+
class IQConnectionEditor(Protocol):
|
|
171
|
+
"""连线编辑器接口契约
|
|
172
|
+
|
|
173
|
+
声明连线编辑器应具备的对外接口, 与节点编辑器
|
|
174
|
+
:class:`~chartflow.node.qnode_editor.QNodeEditor` 形态保持一致:
|
|
175
|
+
构造时传入被编辑对象, 提供 ``getTitle`` / ``save`` / ``load``。
|
|
176
|
+
|
|
177
|
+
具体实现由 :class:`~chartflow.node.qconnection_editor.QConnectionEditor`
|
|
178
|
+
承担; 用户自定义编辑器继承该类(或满足本契约)即可。
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def connection(self) -> IConnectionItem:
|
|
183
|
+
"""获取当前编辑的连线(只读)。"""
|
|
184
|
+
...
|
|
185
|
+
|
|
186
|
+
def getTitle(self) -> str:
|
|
187
|
+
"""获取编辑器窗口标题(嵌入模式下一般不可见)。"""
|
|
188
|
+
...
|
|
189
|
+
|
|
190
|
+
def save(self) -> bool:
|
|
191
|
+
"""保存编辑器内容到连线。返回是否成功。"""
|
|
192
|
+
...
|
|
193
|
+
|
|
194
|
+
def load(self) -> bool:
|
|
195
|
+
"""从连线加载内容到编辑器。返回是否成功。"""
|
|
196
|
+
...
|
|
@@ -56,6 +56,11 @@ class _NodePortMixin:
|
|
|
56
56
|
self._port_label_timer.setSingleShot(True)
|
|
57
57
|
self._port_label_timer.setInterval(PORT_LABEL_HOVER_DELAY_MS)
|
|
58
58
|
self._port_label_timer.timeout.connect(self._showAllPortLabels)
|
|
59
|
+
# label 钉住态(始终可见)与闪现计时器(flashPortLabels 用)
|
|
60
|
+
self._port_labels_pinned: bool = False
|
|
61
|
+
self._port_flash_timer: QTimer = QTimer()
|
|
62
|
+
self._port_flash_timer.setSingleShot(True)
|
|
63
|
+
self._port_flash_timer.timeout.connect(self._unflashPortLabels)
|
|
59
64
|
|
|
60
65
|
# ------------------------------------------------------------------ #
|
|
61
66
|
# ports 属性
|
|
@@ -126,6 +131,50 @@ class _NodePortMixin:
|
|
|
126
131
|
result[key] = ports
|
|
127
132
|
return result
|
|
128
133
|
|
|
134
|
+
# ------------------------------------------------------------------ #
|
|
135
|
+
# 端口 label 显隐(钉住 / 闪现)
|
|
136
|
+
# ------------------------------------------------------------------ #
|
|
137
|
+
@property
|
|
138
|
+
def portLabelsVisible(self) -> bool:
|
|
139
|
+
"""端口 label 是否始终可见(读写)。
|
|
140
|
+
|
|
141
|
+
True 时全部端口 label 钉住常显(优先于 hover 规则); False 时交还
|
|
142
|
+
hover 规则(节点 hover>0.5s 显示全部 / hover 单个 dot 显示该条)。
|
|
143
|
+
对重建端口(ports 重赋值)后新建的端口项同样生效。
|
|
144
|
+
"""
|
|
145
|
+
return self._port_labels_pinned
|
|
146
|
+
|
|
147
|
+
@portLabelsVisible.setter
|
|
148
|
+
def portLabelsVisible(self, on: bool) -> None:
|
|
149
|
+
self._port_labels_pinned = bool(on)
|
|
150
|
+
self._setAllPortsLabelPinned(on)
|
|
151
|
+
|
|
152
|
+
def _setAllPortsLabelPinned(self, on: bool) -> None:
|
|
153
|
+
for side in ("left", "right"):
|
|
154
|
+
for item in self._port_items[side]:
|
|
155
|
+
item.setLabelPinned(on)
|
|
156
|
+
|
|
157
|
+
def flashPortLabels(self, continue_time: int = 1500) -> None:
|
|
158
|
+
"""模拟弹出全部端口 label 并持续 ``continue_time`` 毫秒, 到期自动收回。
|
|
159
|
+
|
|
160
|
+
重复调用会重新计时。与 hover 规则正交: 闪现收回后 label 回到
|
|
161
|
+
hover/钉住所决定的可见态。continue_time <= 0 时立即收回。
|
|
162
|
+
"""
|
|
163
|
+
for side in ("left", "right"):
|
|
164
|
+
for item in self._port_items[side]:
|
|
165
|
+
item.setLabelFlashed(True)
|
|
166
|
+
if continue_time <= 0:
|
|
167
|
+
self._unflashPortLabels()
|
|
168
|
+
return
|
|
169
|
+
self._port_flash_timer.start(max(0, int(continue_time)))
|
|
170
|
+
|
|
171
|
+
def _unflashPortLabels(self) -> None:
|
|
172
|
+
"""闪现到期: 解除全部端口的闪现态(label 回到 hover/钉住规则)。"""
|
|
173
|
+
self._port_flash_timer.stop()
|
|
174
|
+
for side in ("left", "right"):
|
|
175
|
+
for item in self._port_items[side]:
|
|
176
|
+
item.setLabelFlashed(False)
|
|
177
|
+
|
|
129
178
|
# ------------------------------------------------------------------ #
|
|
130
179
|
# 端口渲染项构建/移除
|
|
131
180
|
# ------------------------------------------------------------------ #
|
|
@@ -142,6 +191,7 @@ class _NodePortMixin:
|
|
|
142
191
|
for side in ("left", "right"):
|
|
143
192
|
for spec in self._ports_spec.get(side, []):
|
|
144
193
|
item = QNodePort(spec, side, self)
|
|
194
|
+
item.setLabelPinned(self._port_labels_pinned) # 继承钉住态
|
|
145
195
|
self._port_items[side].append(item)
|
|
146
196
|
self._layoutPorts()
|
|
147
197
|
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
"""
|
|
2
2
|
连接项模块 - 表示两个节点之间的连线
|
|
3
|
-
|
|
3
|
+
包含两个独立的箭头项, 以及中点的名称 label / 嵌入编辑器
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import copy
|
|
6
7
|
import math
|
|
7
|
-
from typing import Optional, List, Callable
|
|
8
|
+
from typing import Optional, List, Callable, Dict, Any
|
|
8
9
|
|
|
9
|
-
from PyQt6.QtWidgets import
|
|
10
|
-
|
|
10
|
+
from PyQt6.QtWidgets import (
|
|
11
|
+
QGraphicsItem, QGraphicsPathItem, QGraphicsTextItem, QGraphicsRectItem,
|
|
12
|
+
QGraphicsProxyWidget, QWidget,
|
|
13
|
+
)
|
|
14
|
+
from PyQt6.QtCore import Qt, QPointF, QTimer, QRectF, QObject, pyqtSignal
|
|
11
15
|
from PyQt6.QtGui import QPainterPath, QPainterPathStroker, QPen, QColor, QLinearGradient, QBrush
|
|
12
16
|
|
|
13
17
|
from chartflow.node.styles import (
|
|
@@ -34,6 +38,26 @@ SELF_LOOP_RING_RADIUS = 18.0
|
|
|
34
38
|
# 四分之一圆的贝塞尔控制点常数: 4/3*(sqrt(2)-1)
|
|
35
39
|
_QUARTER_ARC_K = 0.5522847498
|
|
36
40
|
|
|
41
|
+
# 名称 label 文本颜色(深岩灰, 与节点名称文本同色系)
|
|
42
|
+
CONNECTION_LABEL_COLOR = QColor("#4A5568")
|
|
43
|
+
# 编辑器打开时名称 label 的高亮底衬(类比节点的 📖 悬浮标记)
|
|
44
|
+
CONNECTION_LABEL_EDIT_BG = QColor("#E3F0FF")
|
|
45
|
+
CONNECTION_LABEL_EDIT_BORDER = QColor("#7FB2F0")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ConnectionSignalHelper(QObject):
|
|
49
|
+
"""
|
|
50
|
+
信号辅助类 - 为 ConnectionItem 提供信号支持
|
|
51
|
+
因为 QGraphicsPathItem 继承自 QGraphicsItem 而不是 QObject,
|
|
52
|
+
所以需要通过组合方式来提供信号功能(与 NodeSignalHelper 同构)
|
|
53
|
+
"""
|
|
54
|
+
editRequested = pyqtSignal(object) # 编辑请求信号, 参数为连线自身
|
|
55
|
+
nameChanged = pyqtSignal(object, str) # 名称改变信号, 参数为连线自身和新名称
|
|
56
|
+
|
|
57
|
+
def __init__(self, parent) -> None:
|
|
58
|
+
super().__init__()
|
|
59
|
+
self._parent = parent
|
|
60
|
+
|
|
37
61
|
|
|
38
62
|
def _quarter_arc(path: QPainterPath, center: QPointF, r: float,
|
|
39
63
|
u_from: QPointF, u_to: QPointF) -> None:
|
|
@@ -194,6 +218,35 @@ class ConnectionItem(QGraphicsPathItem):
|
|
|
194
218
|
self._arrow_drag_moved_callback: Optional[Callable[[QPointF], None]] = None
|
|
195
219
|
self._arrow_drag_finished_callback: Optional[Callable[[QPointF, bool], None]] = None
|
|
196
220
|
|
|
221
|
+
# ------------------------------------------------------------------ #
|
|
222
|
+
# 名称 / 编辑器体系(镜像节点: 信号辅助 + 中点名称 label + 编辑器配置)
|
|
223
|
+
# ------------------------------------------------------------------ #
|
|
224
|
+
self._signal_helper = ConnectionSignalHelper(self)
|
|
225
|
+
self._name: str = ""
|
|
226
|
+
self._data: Dict[str, Any] = {}
|
|
227
|
+
self._editor_class: Optional[type] = None
|
|
228
|
+
# 编辑器嵌入(True, 默认, 嵌在连线中点) / 浮动(False, 独立窗口)
|
|
229
|
+
self._editor_embedded: bool = True
|
|
230
|
+
self._edit_window_open: bool = False
|
|
231
|
+
self._open_editor: Optional[QWidget] = None
|
|
232
|
+
self._embedded_proxy: Optional[QGraphicsProxyWidget] = None
|
|
233
|
+
|
|
234
|
+
# 编辑器打开时名称 label 的高亮底衬(子项, z=-1 位于 label 之下)
|
|
235
|
+
self._label_bg: QGraphicsRectItem = QGraphicsRectItem(self)
|
|
236
|
+
self._label_bg.setZValue(-1)
|
|
237
|
+
self._label_bg.setBrush(QBrush(CONNECTION_LABEL_EDIT_BG))
|
|
238
|
+
self._label_bg.setPen(QPen(CONNECTION_LABEL_EDIT_BORDER, 1.0))
|
|
239
|
+
self._label_bg.setVisible(False)
|
|
240
|
+
|
|
241
|
+
# 中点名称 label(子项; 连线 item 无位置偏移, 子项坐标即场景坐标)
|
|
242
|
+
self._text_item: QGraphicsTextItem = QGraphicsTextItem("", self)
|
|
243
|
+
self._text_item.setDefaultTextColor(CONNECTION_LABEL_COLOR)
|
|
244
|
+
# 禁用文本项的鼠标/hover 事件, 让事件完全传递给连线本体
|
|
245
|
+
self._text_item.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
|
|
246
|
+
self._text_item.setAcceptHoverEvents(False)
|
|
247
|
+
self._text_item.setAcceptedMouseButtons(Qt.MouseButton.NoButton)
|
|
248
|
+
self._text_item.setVisible(False)
|
|
249
|
+
|
|
197
250
|
# 更新路径
|
|
198
251
|
self.updatePath()
|
|
199
252
|
|
|
@@ -409,6 +462,173 @@ class ConnectionItem(QGraphicsPathItem):
|
|
|
409
462
|
self._connection_type = ConnectionType.BEZIER
|
|
410
463
|
self.updatePath()
|
|
411
464
|
|
|
465
|
+
# ------------------------------------------------------------------ #
|
|
466
|
+
# 名称 / 数据 / 编辑器(镜像节点 QNodeItem 的同名体系)
|
|
467
|
+
# ------------------------------------------------------------------ #
|
|
468
|
+
@property
|
|
469
|
+
def name(self) -> str:
|
|
470
|
+
"""获取连线名称(可为空串, 空串时中点名称 label 隐藏)"""
|
|
471
|
+
return self._name
|
|
472
|
+
|
|
473
|
+
@name.setter
|
|
474
|
+
def name(self, value: str) -> None:
|
|
475
|
+
"""设置连线名称: 更新中点 label 并发 :attr:`nameChanged` 信号"""
|
|
476
|
+
value = value or ""
|
|
477
|
+
old_name = self._name
|
|
478
|
+
self._name = value
|
|
479
|
+
self._text_item.setPlainText(value)
|
|
480
|
+
if old_name != value:
|
|
481
|
+
self._signal_helper.nameChanged.emit(self, value)
|
|
482
|
+
self._layoutLabel()
|
|
483
|
+
|
|
484
|
+
@property
|
|
485
|
+
def data(self) -> Dict[str, Any]:
|
|
486
|
+
"""连线自定义数据字典(只读引用, 内容可改; 供编辑器持久化内容)"""
|
|
487
|
+
return self._data
|
|
488
|
+
|
|
489
|
+
@property
|
|
490
|
+
def editorClass(self) -> Optional[type]:
|
|
491
|
+
"""获取连线编辑器类(None = 使用内置默认名称编辑器)"""
|
|
492
|
+
return self._editor_class
|
|
493
|
+
|
|
494
|
+
@editorClass.setter
|
|
495
|
+
def editorClass(self, value: Optional[type]) -> None:
|
|
496
|
+
"""设置连线编辑器类"""
|
|
497
|
+
self._editor_class = value
|
|
498
|
+
|
|
499
|
+
@property
|
|
500
|
+
def editorEmbedded(self) -> bool:
|
|
501
|
+
"""编辑器是否嵌入连线中点(True, 默认) / 独立浮动窗口(False)"""
|
|
502
|
+
return self._editor_embedded
|
|
503
|
+
|
|
504
|
+
@editorEmbedded.setter
|
|
505
|
+
def editorEmbedded(self, value: bool) -> None:
|
|
506
|
+
"""设置编辑器嵌入/浮动布局(对已打开的编辑器不追溯生效)"""
|
|
507
|
+
self._editor_embedded = bool(value)
|
|
508
|
+
|
|
509
|
+
@property
|
|
510
|
+
def center(self) -> QPointF:
|
|
511
|
+
"""连线中点(场景坐标): 名称 label 与嵌入编辑器的锚点"""
|
|
512
|
+
return self.path().pointAtPercent(0.5)
|
|
513
|
+
|
|
514
|
+
@property
|
|
515
|
+
def isEditWindowOpen(self) -> bool:
|
|
516
|
+
"""当前是否有编辑器(嵌入或浮动)打开"""
|
|
517
|
+
return self._edit_window_open
|
|
518
|
+
|
|
519
|
+
def setEditWindowOpen(self, open: bool) -> None:
|
|
520
|
+
"""切换编辑窗口打开状态, 控制名称 label 的高亮底衬(类比节点📖)"""
|
|
521
|
+
open = bool(open)
|
|
522
|
+
if self._edit_window_open == open:
|
|
523
|
+
return
|
|
524
|
+
self._edit_window_open = open
|
|
525
|
+
self._layoutLabel()
|
|
526
|
+
|
|
527
|
+
def closeEditor(self) -> None:
|
|
528
|
+
"""关闭当前打开的编辑器并清理嵌入代理(无打开编辑器时为空操作)。
|
|
529
|
+
|
|
530
|
+
画布删除连线(:meth:`NodeCanvas.removeConnection`)时调用, 防止
|
|
531
|
+
编辑器窗口 / QGraphicsProxyWidget 悬挂。
|
|
532
|
+
"""
|
|
533
|
+
editor = self._open_editor
|
|
534
|
+
if editor is not None:
|
|
535
|
+
self._open_editor = None
|
|
536
|
+
try:
|
|
537
|
+
editor.close() # closeEvent -> setEditWindowOpen(False)
|
|
538
|
+
except RuntimeError:
|
|
539
|
+
pass
|
|
540
|
+
# 兜底清理嵌入代理(正常由 editor.destroyed 回调清理, 此处防悬挂)
|
|
541
|
+
proxy = self._embedded_proxy
|
|
542
|
+
if proxy is not None:
|
|
543
|
+
self._embedded_proxy = None
|
|
544
|
+
try:
|
|
545
|
+
if proxy.scene() is not None:
|
|
546
|
+
proxy.scene().removeItem(proxy)
|
|
547
|
+
proxy.setParentItem(None)
|
|
548
|
+
except RuntimeError:
|
|
549
|
+
pass
|
|
550
|
+
self._layoutLabel()
|
|
551
|
+
|
|
552
|
+
# ---- 信号(只读属性, 返回 pyqtSignal) ----
|
|
553
|
+
@property
|
|
554
|
+
def editRequested(self) -> pyqtSignal:
|
|
555
|
+
"""编辑请求信号: 双击连线或右键菜单 Edit Connection 时发射"""
|
|
556
|
+
return self._signal_helper.editRequested
|
|
557
|
+
|
|
558
|
+
@property
|
|
559
|
+
def nameChanged(self) -> pyqtSignal:
|
|
560
|
+
"""名称变更信号: 载荷为 (连线, 新名称)"""
|
|
561
|
+
return self._signal_helper.nameChanged
|
|
562
|
+
|
|
563
|
+
def _layoutLabel(self) -> None:
|
|
564
|
+
"""将名称 label / 嵌入编辑器定位到连线中点。
|
|
565
|
+
|
|
566
|
+
在 updatePath()(含自环路径)、name 变更、编辑器开关时调用。
|
|
567
|
+
label 与 proxy 均为本连线子项; 连线 item 无位置偏移(path 直接建在
|
|
568
|
+
场景坐标), 故子项本地坐标即场景坐标。嵌入编辑器打开期间隐藏 label,
|
|
569
|
+
避免名称双重显示。
|
|
570
|
+
"""
|
|
571
|
+
label = getattr(self, "_text_item", None)
|
|
572
|
+
if label is None:
|
|
573
|
+
return # 构造早期防御(updatePath 先于 label 创建时不应发生)
|
|
574
|
+
mid = self.path().pointAtPercent(0.5)
|
|
575
|
+
proxy = self._embedded_proxy
|
|
576
|
+
show_label = bool(self._name) and proxy is None
|
|
577
|
+
label.setVisible(show_label)
|
|
578
|
+
bg = self._label_bg
|
|
579
|
+
if show_label:
|
|
580
|
+
rect = label.boundingRect()
|
|
581
|
+
label.setPos(mid.x() - rect.width() / 2.0,
|
|
582
|
+
mid.y() - rect.height() / 2.0)
|
|
583
|
+
show_bg = self._edit_window_open
|
|
584
|
+
bg.setVisible(show_bg)
|
|
585
|
+
if show_bg:
|
|
586
|
+
pad = 3.0
|
|
587
|
+
bg.setRect(label.pos().x() - pad, label.pos().y() - pad,
|
|
588
|
+
rect.width() + 2 * pad, rect.height() + 2 * pad)
|
|
589
|
+
else:
|
|
590
|
+
bg.setVisible(False)
|
|
591
|
+
if proxy is not None:
|
|
592
|
+
size = proxy.size()
|
|
593
|
+
proxy.setPos(mid.x() - size.width() / 2.0,
|
|
594
|
+
mid.y() - size.height() / 2.0)
|
|
595
|
+
|
|
596
|
+
# ------------------------------------------------------------------ #
|
|
597
|
+
# 序列化(拓扑/样式由画布 export 与 styleToDict 负责)
|
|
598
|
+
# ------------------------------------------------------------------ #
|
|
599
|
+
def toDict(self) -> dict:
|
|
600
|
+
"""将连线的 名称/数据/编辑器布局 序列化为字典(仅非默认键)。
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
连线数据字典; 默认连线(无名称/数据, 嵌入布局)返回空 dict,
|
|
604
|
+
避免污染导出。
|
|
605
|
+
"""
|
|
606
|
+
result: dict = {}
|
|
607
|
+
if self._name:
|
|
608
|
+
result["name"] = self._name
|
|
609
|
+
if self._data:
|
|
610
|
+
# deepcopy: data 可能含列表/字典等可变嵌套, 浅拷贝会让外部
|
|
611
|
+
# 就地修改污染原连线 _data(与节点 toDict 策略一致)。
|
|
612
|
+
result["data"] = copy.deepcopy(self._data)
|
|
613
|
+
if not self._editor_embedded:
|
|
614
|
+
result["editor_embedded"] = False
|
|
615
|
+
return result
|
|
616
|
+
|
|
617
|
+
def fromDict(self, data: dict) -> None:
|
|
618
|
+
"""从字典恢复连线的 名称/数据/编辑器布局(增量合并, 忽略未知键)。
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
data: 由 :meth:`toDict` 产生(或同构)的字典。
|
|
622
|
+
"""
|
|
623
|
+
if not isinstance(data, dict):
|
|
624
|
+
return
|
|
625
|
+
if "name" in data:
|
|
626
|
+
self.name = data["name"] or ""
|
|
627
|
+
if isinstance(data.get("data"), dict):
|
|
628
|
+
self._data.update(data["data"])
|
|
629
|
+
if "editor_embedded" in data:
|
|
630
|
+
self.editorEmbedded = bool(data["editor_embedded"])
|
|
631
|
+
|
|
412
632
|
def swapDirection(self) -> None:
|
|
413
633
|
"""
|
|
414
634
|
交换源节点和目标节点,用于保留反向连接时
|
|
@@ -508,6 +728,7 @@ class ConnectionItem(QGraphicsPathItem):
|
|
|
508
728
|
self._source_edge = QPointF()
|
|
509
729
|
self._target_edge = QPointF()
|
|
510
730
|
self._updateSelfLoopPath()
|
|
731
|
+
self._layoutLabel()
|
|
511
732
|
return
|
|
512
733
|
|
|
513
734
|
if self._source_port is not None:
|
|
@@ -542,6 +763,8 @@ class ConnectionItem(QGraphicsPathItem):
|
|
|
542
763
|
|
|
543
764
|
# 端点变化后刷新 pen(端口渐变需按新端点重建渐变画刷)
|
|
544
765
|
self._refreshPen()
|
|
766
|
+
# 路径变化后重定位中点名称 label / 嵌入编辑器
|
|
767
|
+
self._layoutLabel()
|
|
545
768
|
|
|
546
769
|
def _updateSelfLoopPath(self) -> None:
|
|
547
770
|
"""绘制自环: 圆弧+直线组合, 端点吸附实际边线。
|
|
@@ -4,7 +4,7 @@ qnode库使用示例
|
|
|
4
4
|
展示如何使用qnode库创建节点图
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from PyQt6.QtWidgets import QVBoxLayout, QLabel, QTextEdit
|
|
7
|
+
from PyQt6.QtWidgets import QVBoxLayout, QLabel, QTextEdit, QPushButton
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
import sys
|
|
@@ -13,7 +13,7 @@ from PyQt6.QtCore import QPointF
|
|
|
13
13
|
|
|
14
14
|
from chartflow.node import (
|
|
15
15
|
NodeCanvasWindow, QNodeCanvas, QNodeItem,
|
|
16
|
-
NodeShape, ConnectionType, QNodeEditor
|
|
16
|
+
NodeShape, ConnectionType, QNodeEditor, QConnectionEditor
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
|
|
@@ -42,6 +42,31 @@ class MyNodeEditor(QNodeEditor):
|
|
|
42
42
|
return True
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
class MyConnectionEditor(QConnectionEditor):
|
|
46
|
+
"""自定义连线编辑器示例(编辑连线备注, 经 connection.data 持久化)"""
|
|
47
|
+
|
|
48
|
+
def _setupUi(self) -> None:
|
|
49
|
+
layout = QVBoxLayout(self)
|
|
50
|
+
|
|
51
|
+
info_label = QLabel(f"Editing connection: {self.connection.name or '(unnamed)'}")
|
|
52
|
+
layout.addWidget(info_label)
|
|
53
|
+
|
|
54
|
+
# 备注编辑(存入 connection.data, 随画布 export/import 保存)
|
|
55
|
+
self.text_edit = QTextEdit()
|
|
56
|
+
self.text_edit.setPlaceholderText("Connection note...")
|
|
57
|
+
self.text_edit.setPlainText(self.connection.data.get("note", ""))
|
|
58
|
+
layout.addWidget(self.text_edit)
|
|
59
|
+
|
|
60
|
+
save_button = QPushButton("Save")
|
|
61
|
+
save_button.clicked.connect(self.save)
|
|
62
|
+
layout.addWidget(save_button)
|
|
63
|
+
|
|
64
|
+
def save(self) -> bool:
|
|
65
|
+
"""保存编辑器内容到连线数据"""
|
|
66
|
+
self.connection.data["note"] = self.text_edit.toPlainText()
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
|
|
45
70
|
def main():
|
|
46
71
|
"""主函数"""
|
|
47
72
|
app = QApplication(sys.argv)
|
|
@@ -64,10 +89,24 @@ def main():
|
|
|
64
89
|
node2.data["content"] = "Process logic here..."
|
|
65
90
|
|
|
66
91
|
# 创建连接
|
|
67
|
-
canvas.addConnection(node1, node2)
|
|
68
|
-
canvas.addConnection(node2, node3)
|
|
69
|
-
canvas.addConnection(node3, node4)
|
|
70
|
-
|
|
92
|
+
conn12 = canvas.addConnection(node1, node2)
|
|
93
|
+
conn23 = canvas.addConnection(node2, node3)
|
|
94
|
+
conn34 = canvas.addConnection(node3, node4)
|
|
95
|
+
|
|
96
|
+
# 连线命名: 名称持久显示在连线中点(双击可编辑)
|
|
97
|
+
conn12.name = "init"
|
|
98
|
+
conn23.name = "process"
|
|
99
|
+
|
|
100
|
+
# 连线编辑器示例:
|
|
101
|
+
# - conn23: 自定义编辑器, 默认嵌入连线中点(QGraphicsProxyWidget)
|
|
102
|
+
# - conn34: 自定义编辑器, editorEmbedded=False 作为浮动窗口打开
|
|
103
|
+
conn23.editorClass = MyConnectionEditor
|
|
104
|
+
conn34.name = "finish"
|
|
105
|
+
conn34.editorClass = MyConnectionEditor
|
|
106
|
+
conn34.editorEmbedded = False
|
|
107
|
+
|
|
108
|
+
# 未设置 editorClass 的连线(如 conn12): 双击打开内置名称编辑器
|
|
109
|
+
|
|
71
110
|
# 设置双向连接示例
|
|
72
111
|
canvas.addConnection(node2, node4, is_bidirectional=True)
|
|
73
112
|
|