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,307 @@
|
|
|
1
|
+
"""
|
|
2
|
+
布局工具模块 - 提供自动布局算法
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from typing import List, Dict, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from PyQt6.QtCore import QPointF, QRectF, Qt
|
|
9
|
+
from PyQt6.QtGui import QTransform
|
|
10
|
+
|
|
11
|
+
from chartflow.node.node_item import QNodeItem
|
|
12
|
+
from chartflow.node.connection_item import ConnectionItem
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DenseLayoutEngine:
|
|
16
|
+
"""密集布局引擎 - 提供多种自动布局策略"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, canvas: 'NodeCanvas') -> None:
|
|
19
|
+
self._canvas = canvas
|
|
20
|
+
self._nodes: List[QNodeItem] = canvas.nodes
|
|
21
|
+
self._connections: List[ConnectionItem] = canvas.connections
|
|
22
|
+
|
|
23
|
+
def autoLayout(self, strategy: str = "compact") -> None:
|
|
24
|
+
"""
|
|
25
|
+
自动布局
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
strategy: 布局策略 ("compact", "circular", "grid", "hierarchical", "force")
|
|
29
|
+
"""
|
|
30
|
+
if not self._nodes:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
if strategy == "compact":
|
|
34
|
+
self._compactLayout()
|
|
35
|
+
elif strategy == "circular":
|
|
36
|
+
self._circularLayout()
|
|
37
|
+
elif strategy == "grid":
|
|
38
|
+
self._gridLayout()
|
|
39
|
+
elif strategy == "hierarchical":
|
|
40
|
+
self._hierarchicalLayout()
|
|
41
|
+
elif strategy == "force":
|
|
42
|
+
self._forceDirectedLayout()
|
|
43
|
+
|
|
44
|
+
# 更新所有连接
|
|
45
|
+
for conn in self._connections:
|
|
46
|
+
conn.updatePath()
|
|
47
|
+
|
|
48
|
+
# 标记画布已修改
|
|
49
|
+
self._canvas.changed = True
|
|
50
|
+
|
|
51
|
+
def _compactLayout(self) -> None:
|
|
52
|
+
"""紧凑布局 - 将节点排列成紧凑的网格"""
|
|
53
|
+
if not self._nodes:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
# 计算节点大小
|
|
57
|
+
node_size = 100 # 假设节点大小为100x100
|
|
58
|
+
spacing = 20 # 节点间距
|
|
59
|
+
|
|
60
|
+
# 计算网格尺寸
|
|
61
|
+
node_count = len(self._nodes)
|
|
62
|
+
cols = int(math.sqrt(node_count))
|
|
63
|
+
if cols * cols < node_count:
|
|
64
|
+
cols += 1
|
|
65
|
+
|
|
66
|
+
# 计算起始位置(使布局居中)
|
|
67
|
+
start_x = -(cols * (node_size + spacing)) / 2
|
|
68
|
+
start_y = -(math.ceil(node_count / cols) * (node_size + spacing)) / 2
|
|
69
|
+
|
|
70
|
+
# 排列节点
|
|
71
|
+
for i, node in enumerate(self._nodes):
|
|
72
|
+
row = i // cols
|
|
73
|
+
col = i % cols
|
|
74
|
+
x = start_x + col * (node_size + spacing)
|
|
75
|
+
y = start_y + row * (node_size + spacing)
|
|
76
|
+
node.setPos(QPointF(x, y))
|
|
77
|
+
|
|
78
|
+
def _circularLayout(self) -> None:
|
|
79
|
+
"""圆形布局 - 将节点排列成圆形"""
|
|
80
|
+
if not self._nodes:
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
node_count = len(self._nodes)
|
|
84
|
+
if node_count == 1:
|
|
85
|
+
self._nodes[0].setPos(QPointF(0, 0))
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
# 计算圆形半径
|
|
89
|
+
radius = max(200, node_count * 30)
|
|
90
|
+
|
|
91
|
+
# 排列节点
|
|
92
|
+
for i, node in enumerate(self._nodes):
|
|
93
|
+
angle = 2 * math.pi * i / node_count
|
|
94
|
+
x = radius * math.cos(angle)
|
|
95
|
+
y = radius * math.sin(angle)
|
|
96
|
+
node.setPos(QPointF(x, y))
|
|
97
|
+
|
|
98
|
+
def _gridLayout(self) -> None:
|
|
99
|
+
"""网格布局 - 将节点排列成规则的网格"""
|
|
100
|
+
if not self._nodes:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# 计算节点大小
|
|
104
|
+
node_size = 100
|
|
105
|
+
spacing = 30
|
|
106
|
+
|
|
107
|
+
# 计算网格尺寸
|
|
108
|
+
node_count = len(self._nodes)
|
|
109
|
+
cols = int(math.sqrt(node_count * 1.5)) # 稍微宽一点的网格
|
|
110
|
+
if cols < 1:
|
|
111
|
+
cols = 1
|
|
112
|
+
|
|
113
|
+
# 计算起始位置
|
|
114
|
+
start_x = -(cols * (node_size + spacing)) / 2
|
|
115
|
+
start_y = -(math.ceil(node_count / cols) * (node_size + spacing)) / 2
|
|
116
|
+
|
|
117
|
+
# 排列节点
|
|
118
|
+
for i, node in enumerate(self._nodes):
|
|
119
|
+
row = i // cols
|
|
120
|
+
col = i % cols
|
|
121
|
+
x = start_x + col * (node_size + spacing)
|
|
122
|
+
y = start_y + row * (node_size + spacing)
|
|
123
|
+
node.setPos(QPointF(x, y))
|
|
124
|
+
|
|
125
|
+
def _hierarchicalLayout(self) -> None:
|
|
126
|
+
"""层次布局 - 基于连接的层次结构"""
|
|
127
|
+
if not self._nodes:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# 构建邻接表
|
|
131
|
+
adjacency: Dict[QNodeItem, List[QNodeItem]] = {node: [] for node in self._nodes}
|
|
132
|
+
for conn in self._connections:
|
|
133
|
+
adjacency[conn.sourceNode].append(conn.targetNode)
|
|
134
|
+
|
|
135
|
+
# 计算每个节点的层次(距离起点的距离)
|
|
136
|
+
levels: Dict[QNodeItem, int] = {}
|
|
137
|
+
|
|
138
|
+
# 找到没有入边的节点作为起点
|
|
139
|
+
in_degree: Dict[QNodeItem, int] = {node: 0 for node in self._nodes}
|
|
140
|
+
for conn in self._connections:
|
|
141
|
+
in_degree[conn.targetNode] = in_degree.get(conn.targetNode, 0) + 1
|
|
142
|
+
|
|
143
|
+
# BFS计算层次
|
|
144
|
+
queue = [node for node in self._nodes if in_degree[node] == 0]
|
|
145
|
+
for node in queue:
|
|
146
|
+
levels[node] = 0
|
|
147
|
+
|
|
148
|
+
while queue:
|
|
149
|
+
current = queue.pop(0)
|
|
150
|
+
for neighbor in adjacency[current]:
|
|
151
|
+
if neighbor not in levels:
|
|
152
|
+
levels[neighbor] = levels[current] + 1
|
|
153
|
+
queue.append(neighbor)
|
|
154
|
+
|
|
155
|
+
# 为没有层次的节点分配层次
|
|
156
|
+
max_level = max(levels.values()) if levels else 0
|
|
157
|
+
for node in self._nodes:
|
|
158
|
+
if node not in levels:
|
|
159
|
+
levels[node] = max_level + 1
|
|
160
|
+
|
|
161
|
+
# 按层次分组
|
|
162
|
+
level_groups: Dict[int, List[QNodeItem]] = {}
|
|
163
|
+
for node, level in levels.items():
|
|
164
|
+
if level not in level_groups:
|
|
165
|
+
level_groups[level] = []
|
|
166
|
+
level_groups[level].append(node)
|
|
167
|
+
|
|
168
|
+
# 布局
|
|
169
|
+
node_size = 100
|
|
170
|
+
level_spacing = 150
|
|
171
|
+
node_spacing = 120
|
|
172
|
+
|
|
173
|
+
for level, nodes in level_groups.items():
|
|
174
|
+
y = level * level_spacing
|
|
175
|
+
count = len(nodes)
|
|
176
|
+
start_x = -(count * node_spacing) / 2
|
|
177
|
+
|
|
178
|
+
for i, node in enumerate(nodes):
|
|
179
|
+
x = start_x + i * node_spacing
|
|
180
|
+
node.setPos(QPointF(x, y))
|
|
181
|
+
|
|
182
|
+
def _forceDirectedLayout(self) -> None:
|
|
183
|
+
"""力导向布局 - 模拟物理力"""
|
|
184
|
+
if not self._nodes:
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
# 参数
|
|
188
|
+
iterations = 100
|
|
189
|
+
repulsion_force = 1000
|
|
190
|
+
attraction_force = 0.01
|
|
191
|
+
damping = 0.8
|
|
192
|
+
|
|
193
|
+
# 初始化速度
|
|
194
|
+
velocities: Dict[QNodeItem, QPointF] = {node: QPointF(0, 0) for node in self._nodes}
|
|
195
|
+
|
|
196
|
+
for _ in range(iterations):
|
|
197
|
+
# 计算斥力
|
|
198
|
+
for i, node1 in enumerate(self._nodes):
|
|
199
|
+
for j, node2 in enumerate(self._nodes):
|
|
200
|
+
if i >= j:
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
pos1 = node1.pos()
|
|
204
|
+
pos2 = node2.pos()
|
|
205
|
+
dx = pos1.x() - pos2.x()
|
|
206
|
+
dy = pos1.y() - pos2.y()
|
|
207
|
+
distance = math.sqrt(dx * dx + dy * dy)
|
|
208
|
+
|
|
209
|
+
if distance < 1:
|
|
210
|
+
distance = 1
|
|
211
|
+
|
|
212
|
+
force = repulsion_force / (distance * distance)
|
|
213
|
+
fx = force * dx / distance
|
|
214
|
+
fy = force * dy / distance
|
|
215
|
+
|
|
216
|
+
velocities[node1] = QPointF(
|
|
217
|
+
velocities[node1].x() + fx,
|
|
218
|
+
velocities[node1].y() + fy
|
|
219
|
+
)
|
|
220
|
+
velocities[node2] = QPointF(
|
|
221
|
+
velocities[node2].x() - fx,
|
|
222
|
+
velocities[node2].y() - fy
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# 计算引力(连接的节点)
|
|
226
|
+
for conn in self._connections:
|
|
227
|
+
node1 = conn.sourceNode
|
|
228
|
+
node2 = conn.targetNode
|
|
229
|
+
|
|
230
|
+
pos1 = node1.pos()
|
|
231
|
+
pos2 = node2.pos()
|
|
232
|
+
dx = pos2.x() - pos1.x()
|
|
233
|
+
dy = pos2.y() - pos1.y()
|
|
234
|
+
distance = math.sqrt(dx * dx + dy * dy)
|
|
235
|
+
|
|
236
|
+
if distance < 1:
|
|
237
|
+
distance = 1
|
|
238
|
+
|
|
239
|
+
force = attraction_force * distance
|
|
240
|
+
fx = force * dx / distance
|
|
241
|
+
fy = force * dy / distance
|
|
242
|
+
|
|
243
|
+
velocities[node1] = QPointF(
|
|
244
|
+
velocities[node1].x() + fx,
|
|
245
|
+
velocities[node1].y() + fy
|
|
246
|
+
)
|
|
247
|
+
velocities[node2] = QPointF(
|
|
248
|
+
velocities[node2].x() - fx,
|
|
249
|
+
velocities[node2].y() - fy
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# 应用速度和阻尼
|
|
253
|
+
for node in self._nodes:
|
|
254
|
+
vel = velocities[node]
|
|
255
|
+
new_pos = QPointF(
|
|
256
|
+
node.pos().x() + vel.x() * damping,
|
|
257
|
+
node.pos().y() + vel.y() * damping
|
|
258
|
+
)
|
|
259
|
+
node.setPos(new_pos)
|
|
260
|
+
|
|
261
|
+
# 应用阻尼
|
|
262
|
+
velocities[node] = QPointF(vel.x() * 0.9, vel.y() * 0.9)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class ViewportFitter:
|
|
266
|
+
"""视口适配器 - 自动调整视图以适应所有节点"""
|
|
267
|
+
|
|
268
|
+
def __init__(self, canvas: 'NodeCanvas') -> None:
|
|
269
|
+
self._canvas = canvas
|
|
270
|
+
|
|
271
|
+
def fitViewport(self, margin: float = 50.0) -> None:
|
|
272
|
+
"""
|
|
273
|
+
自动调整视图以适应所有节点
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
margin: 边距
|
|
277
|
+
"""
|
|
278
|
+
nodes = self._canvas.nodes
|
|
279
|
+
if not nodes:
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
# 计算边界框
|
|
283
|
+
rect = QRectF()
|
|
284
|
+
for node in nodes:
|
|
285
|
+
node_rect = node.sceneBoundingRect()
|
|
286
|
+
rect = rect.united(node_rect)
|
|
287
|
+
|
|
288
|
+
# 添加边距
|
|
289
|
+
rect.adjust(-margin, -margin, margin, margin)
|
|
290
|
+
|
|
291
|
+
# 适应视图
|
|
292
|
+
self._canvas.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio)
|
|
293
|
+
|
|
294
|
+
def centerOnNodes(self) -> None:
|
|
295
|
+
"""将视图中心对准所有节点的中心"""
|
|
296
|
+
nodes = self._canvas.nodes
|
|
297
|
+
if not nodes:
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
# 计算中心点
|
|
301
|
+
center = QPointF(0, 0)
|
|
302
|
+
for node in nodes:
|
|
303
|
+
center += node.pos()
|
|
304
|
+
center /= len(nodes)
|
|
305
|
+
|
|
306
|
+
# 设置视图中心
|
|
307
|
+
self._canvas.centerOn(center)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
主窗口模块 - 提供完整的节点画布应用程序窗口
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from PyQt6.QtWidgets import QFileDialog, QApplication
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from PyQt6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QMessageBox
|
|
11
|
+
from PyQt6.QtCore import Qt
|
|
12
|
+
|
|
13
|
+
from chartflow.theme import applyTheme
|
|
14
|
+
from chartflow.node.node_canvas import QNodeCanvas
|
|
15
|
+
from chartflow.node.menu_bar import MenuBar
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NodeCanvasWindow(QMainWindow):
|
|
19
|
+
"""
|
|
20
|
+
节点画布窗口 - 提供完整的节点画布应用程序
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
from PyQt6.QtWidgets import QApplication
|
|
24
|
+
from chartflow.node import NodeCanvasWindow
|
|
25
|
+
|
|
26
|
+
app = QApplication([])
|
|
27
|
+
window = NodeCanvasWindow()
|
|
28
|
+
window.show()
|
|
29
|
+
app.exec()
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, parent: Optional[QWidget] = None) -> None:
|
|
33
|
+
super().__init__(parent)
|
|
34
|
+
|
|
35
|
+
# Apply the chartflow dark theme to the whole application, but only
|
|
36
|
+
# if the app has no stylesheet yet (don't clobber a user's own theme).
|
|
37
|
+
app = QApplication.instance()
|
|
38
|
+
if app is not None and not app.styleSheet():
|
|
39
|
+
applyTheme(app)
|
|
40
|
+
|
|
41
|
+
self.setWindowTitle("NodeCanvas")
|
|
42
|
+
self.setGeometry(100, 100, 1200, 800)
|
|
43
|
+
|
|
44
|
+
# 创建中央部件
|
|
45
|
+
central_widget = QWidget()
|
|
46
|
+
self.setCentralWidget(central_widget)
|
|
47
|
+
|
|
48
|
+
# 创建布局
|
|
49
|
+
layout = QVBoxLayout(central_widget)
|
|
50
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
51
|
+
|
|
52
|
+
# 创建画布
|
|
53
|
+
self._canvas: QNodeCanvas = QNodeCanvas(self)
|
|
54
|
+
layout.addWidget(self._canvas)
|
|
55
|
+
|
|
56
|
+
# 创建菜单栏
|
|
57
|
+
self._menu_bar: MenuBar = MenuBar(self)
|
|
58
|
+
self.setMenuBar(self._menu_bar)
|
|
59
|
+
|
|
60
|
+
# 连接菜单信号
|
|
61
|
+
self._menu_bar.newGraphRequested.connect(self._onNewGraph)
|
|
62
|
+
self._menu_bar.openGraphRequested.connect(self._onOpenGraph)
|
|
63
|
+
self._menu_bar.saveGraphRequested.connect(self._onSaveGraph)
|
|
64
|
+
self._menu_bar.saveAsGraphRequested.connect(self._onSaveAsGraph)
|
|
65
|
+
self._menu_bar.exportGraphRequested.connect(self._onExportGraph)
|
|
66
|
+
self._menu_bar.importGraphRequested.connect(self._onImportGraph)
|
|
67
|
+
self._menu_bar.exitRequested.connect(self._onExit)
|
|
68
|
+
|
|
69
|
+
# 当前文件路径
|
|
70
|
+
self._current_file_path: Optional[str] = None
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def canvas(self) -> QNodeCanvas:
|
|
74
|
+
"""获取画布"""
|
|
75
|
+
return self._canvas
|
|
76
|
+
|
|
77
|
+
def _onNewGraph(self) -> None:
|
|
78
|
+
"""新建图"""
|
|
79
|
+
if self._canvas.changed:
|
|
80
|
+
reply = QMessageBox.question(
|
|
81
|
+
self, "Unsaved Changes",
|
|
82
|
+
"The current graph has unsaved changes. Do you want to save them?",
|
|
83
|
+
QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if reply == QMessageBox.StandardButton.Save:
|
|
87
|
+
self._onSaveGraph()
|
|
88
|
+
elif reply == QMessageBox.StandardButton.Cancel:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
self._canvas.clear()
|
|
92
|
+
self._current_file_path = None
|
|
93
|
+
self._canvas.setJsonFilePath(None)
|
|
94
|
+
|
|
95
|
+
def _onOpenGraph(self, file_path: str) -> None:
|
|
96
|
+
"""打开图"""
|
|
97
|
+
if self._canvas.changed:
|
|
98
|
+
reply = QMessageBox.question(
|
|
99
|
+
self, "Unsaved Changes",
|
|
100
|
+
"The current graph has unsaved changes. Do you want to save them?",
|
|
101
|
+
QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if reply == QMessageBox.StandardButton.Save:
|
|
105
|
+
self._onSaveGraph()
|
|
106
|
+
elif reply == QMessageBox.StandardButton.Cancel:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
111
|
+
json_data = f.read()
|
|
112
|
+
self._canvas.importData(json_data)
|
|
113
|
+
self._current_file_path = file_path
|
|
114
|
+
self._canvas.setJsonFilePath(file_path)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
QMessageBox.critical(self, "Error", f"Failed to open file: {e}")
|
|
117
|
+
|
|
118
|
+
def _onSaveGraph(self) -> None:
|
|
119
|
+
"""保存图"""
|
|
120
|
+
if self._current_file_path:
|
|
121
|
+
self._saveToFile(self._current_file_path)
|
|
122
|
+
else:
|
|
123
|
+
self._onSaveAsGraph("")
|
|
124
|
+
|
|
125
|
+
def _onSaveAsGraph(self, file_path: str) -> None:
|
|
126
|
+
"""另存为"""
|
|
127
|
+
file_path, _ = QFileDialog.getSaveFileName(
|
|
128
|
+
self, "Save Graph", "", "JSON Files (*.json);;All Files (*)"
|
|
129
|
+
)
|
|
130
|
+
if file_path:
|
|
131
|
+
self._saveToFile(file_path)
|
|
132
|
+
self._current_file_path = file_path
|
|
133
|
+
self._canvas.setJsonFilePath(file_path)
|
|
134
|
+
|
|
135
|
+
def _saveToFile(self, file_path: str) -> None:
|
|
136
|
+
"""保存到文件"""
|
|
137
|
+
try:
|
|
138
|
+
json_data = self._canvas.exportToJson()
|
|
139
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
140
|
+
f.write(json_data)
|
|
141
|
+
self._canvas.resetChangedState()
|
|
142
|
+
except Exception as e:
|
|
143
|
+
QMessageBox.critical(self, "Error", f"Failed to save file: {e}")
|
|
144
|
+
|
|
145
|
+
def _onExportGraph(self) -> None:
|
|
146
|
+
"""导出图"""
|
|
147
|
+
file_path, _ = QFileDialog.getSaveFileName(
|
|
148
|
+
self, "Export Graph", "", "JSON Files (*.json);;All Files (*)"
|
|
149
|
+
)
|
|
150
|
+
if file_path:
|
|
151
|
+
self._saveToFile(file_path)
|
|
152
|
+
|
|
153
|
+
def _onImportGraph(self, file_path: str) -> None:
|
|
154
|
+
"""导入图"""
|
|
155
|
+
try:
|
|
156
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
157
|
+
json_data = f.read()
|
|
158
|
+
self._canvas.importData(json_data)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
QMessageBox.critical(self, "Error", f"Failed to import file: {e}")
|
|
161
|
+
|
|
162
|
+
def _onExit(self) -> None:
|
|
163
|
+
"""退出"""
|
|
164
|
+
if self._canvas.changed:
|
|
165
|
+
reply = QMessageBox.question(
|
|
166
|
+
self, "Unsaved Changes",
|
|
167
|
+
"The current graph has unsaved changes. Do you want to save them?",
|
|
168
|
+
QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if reply == QMessageBox.StandardButton.Save:
|
|
172
|
+
self._onSaveGraph()
|
|
173
|
+
elif reply == QMessageBox.StandardButton.Cancel:
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
self.close()
|
|
177
|
+
|
|
178
|
+
def closeEvent(self, event) -> None:
|
|
179
|
+
"""关闭事件"""
|
|
180
|
+
if self._canvas.changed:
|
|
181
|
+
reply = QMessageBox.question(
|
|
182
|
+
self, "Unsaved Changes",
|
|
183
|
+
"The current graph has unsaved changes. Do you want to save them?",
|
|
184
|
+
QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if reply == QMessageBox.StandardButton.Save:
|
|
188
|
+
self._onSaveGraph()
|
|
189
|
+
if self._canvas.changed: # 如果保存失败
|
|
190
|
+
event.ignore()
|
|
191
|
+
return
|
|
192
|
+
elif reply == QMessageBox.StandardButton.Cancel:
|
|
193
|
+
event.ignore()
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
event.accept()
|