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,448 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stage Execute/Handle 模块规范测试
|
|
3
|
+
验证所有需求文档中的行为
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from chartflow.fsm import Stage
|
|
8
|
+
from chartflow.fsm.decorators import sequence, selector, parallel, behavior
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestBasicExecution:
|
|
12
|
+
"""基础执行流程测试"""
|
|
13
|
+
|
|
14
|
+
def test_state_transition_cycle(self):
|
|
15
|
+
"""测试: 所有节点转换必须通过 _execute_state -> _handle -> _transition_to 周期"""
|
|
16
|
+
events = []
|
|
17
|
+
|
|
18
|
+
class TestStage(Stage):
|
|
19
|
+
NAME = "Test"
|
|
20
|
+
|
|
21
|
+
def onChanged(self, src, dst, inst):
|
|
22
|
+
events.append(("onChanged", src, dst))
|
|
23
|
+
|
|
24
|
+
def a(self, inst):
|
|
25
|
+
return "b"
|
|
26
|
+
|
|
27
|
+
def b(self, inst):
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
stage = TestStage()
|
|
31
|
+
stage.goto("a")
|
|
32
|
+
stage.handleStage(log=False)
|
|
33
|
+
|
|
34
|
+
assert ("onChanged", "a", "b") in events
|
|
35
|
+
assert stage.current_state == "b"
|
|
36
|
+
|
|
37
|
+
def test_changed_event_only_in_transition_to(self):
|
|
38
|
+
"""测试: changed事件仅在 _transition_to 中触发"""
|
|
39
|
+
changed_events = []
|
|
40
|
+
enter_events = []
|
|
41
|
+
|
|
42
|
+
class TestStage(Stage):
|
|
43
|
+
NAME = "Test"
|
|
44
|
+
|
|
45
|
+
def setup(self):
|
|
46
|
+
self.listenFor("changed", lambda e: changed_events.append((e["old"], e["new"])))
|
|
47
|
+
self.listenFor("enter", lambda e: enter_events.append(e["state"]))
|
|
48
|
+
|
|
49
|
+
def a(self, inst):
|
|
50
|
+
return "b"
|
|
51
|
+
|
|
52
|
+
def b(self, inst):
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
stage = TestStage()
|
|
56
|
+
stage.setup()
|
|
57
|
+
stage.goto("a")
|
|
58
|
+
|
|
59
|
+
# Tick 1: a 执行并转换到 b —— changed 事件在 _transition_to 中触发
|
|
60
|
+
stage.handleStage(log=False)
|
|
61
|
+
assert ("a", "b") in changed_events
|
|
62
|
+
|
|
63
|
+
# Tick 2: b 被执行 —— enter 事件在 _execute_state 中触发。
|
|
64
|
+
# 引擎为每 tick 一个状态:a->b 转换后,b 在下一 tick 才执行并 enter。
|
|
65
|
+
stage.handleStage(log=False)
|
|
66
|
+
assert "b" in enter_events
|
|
67
|
+
|
|
68
|
+
def test_enter_event_only_in_execute_state(self):
|
|
69
|
+
"""测试: enter事件仅在 _execute_state 中触发"""
|
|
70
|
+
enter_events = []
|
|
71
|
+
|
|
72
|
+
class TestStage(Stage):
|
|
73
|
+
NAME = "Test"
|
|
74
|
+
|
|
75
|
+
def setup(self):
|
|
76
|
+
self.listenFor("enter", lambda e: enter_events.append(e["state"]))
|
|
77
|
+
|
|
78
|
+
def a(self, inst):
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
stage = TestStage()
|
|
82
|
+
stage.setup()
|
|
83
|
+
stage.goto("a")
|
|
84
|
+
stage.handleStage(log=False)
|
|
85
|
+
|
|
86
|
+
assert "a" in enter_events
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TestBehaviorTreeEvents:
|
|
90
|
+
"""行为树事件测试"""
|
|
91
|
+
|
|
92
|
+
def test_sequence_child_changed_events(self):
|
|
93
|
+
"""测试: Sequence子节点触发changed事件"""
|
|
94
|
+
changed_events = []
|
|
95
|
+
|
|
96
|
+
class SeqStage(Stage):
|
|
97
|
+
NAME = "Seq"
|
|
98
|
+
|
|
99
|
+
def setup(self):
|
|
100
|
+
self.listenFor("changed", lambda e: changed_events.append((e["old"], e["new"])))
|
|
101
|
+
|
|
102
|
+
@sequence("s1", "s2", "s3")
|
|
103
|
+
def seq(self, inst):
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
def s1(self, inst): return True
|
|
107
|
+
def s2(self, inst): return True
|
|
108
|
+
def s3(self, inst): return True
|
|
109
|
+
|
|
110
|
+
stage = SeqStage()
|
|
111
|
+
stage.setup()
|
|
112
|
+
stage.goto("seq")
|
|
113
|
+
|
|
114
|
+
# 执行多个tick完成序列
|
|
115
|
+
for _ in range(5):
|
|
116
|
+
stage.handleStage(log=False)
|
|
117
|
+
|
|
118
|
+
# 应该有 seq -> s1, s1 -> s2, s2 -> s3
|
|
119
|
+
assert ("seq", "s1") in changed_events
|
|
120
|
+
assert ("s1", "s2") in changed_events
|
|
121
|
+
assert ("s2", "s3") in changed_events
|
|
122
|
+
|
|
123
|
+
def test_selector_child_changed_events(self):
|
|
124
|
+
"""测试: Selector子节点触发changed事件"""
|
|
125
|
+
changed_events = []
|
|
126
|
+
|
|
127
|
+
class SelStage(Stage):
|
|
128
|
+
NAME = "Sel"
|
|
129
|
+
|
|
130
|
+
def setup(self):
|
|
131
|
+
self.listenFor("changed", lambda e: changed_events.append((e["old"], e["new"])))
|
|
132
|
+
|
|
133
|
+
@selector("f1", "f2")
|
|
134
|
+
def sel(self, inst):
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
def f1(self, inst): return False
|
|
138
|
+
def f2(self, inst): return True
|
|
139
|
+
|
|
140
|
+
stage = SelStage()
|
|
141
|
+
stage.setup()
|
|
142
|
+
stage.goto("sel")
|
|
143
|
+
|
|
144
|
+
stage.handleStage(log=False)
|
|
145
|
+
|
|
146
|
+
# 应该有 sel -> f1, sel -> f2
|
|
147
|
+
assert ("sel", "f1") in changed_events
|
|
148
|
+
assert ("sel", "f2") in changed_events
|
|
149
|
+
|
|
150
|
+
def test_parallel_child_changed_events(self):
|
|
151
|
+
"""测试: Parallel子节点触发changed事件"""
|
|
152
|
+
changed_events = []
|
|
153
|
+
|
|
154
|
+
class ParaStage(Stage):
|
|
155
|
+
NAME = "Para"
|
|
156
|
+
|
|
157
|
+
def setup(self):
|
|
158
|
+
self.listenFor("changed", lambda e: changed_events.append((e["old"], e["new"])))
|
|
159
|
+
|
|
160
|
+
@parallel("a", "b")
|
|
161
|
+
def root(self, inst):
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
def a(self, inst): return True
|
|
165
|
+
def b(self, inst): return True
|
|
166
|
+
|
|
167
|
+
stage = ParaStage()
|
|
168
|
+
stage.setup()
|
|
169
|
+
stage.goto("root")
|
|
170
|
+
|
|
171
|
+
stage.handleStage(log=False)
|
|
172
|
+
|
|
173
|
+
# 应该有 root -> a, root -> b
|
|
174
|
+
assert ("root", "a") in changed_events
|
|
175
|
+
assert ("root", "b") in changed_events
|
|
176
|
+
|
|
177
|
+
def test_break_no_duplicate_events(self):
|
|
178
|
+
"""测试: BREAK不产生重复changed事件"""
|
|
179
|
+
changed_events = []
|
|
180
|
+
|
|
181
|
+
class TestStage(Stage):
|
|
182
|
+
NAME = "Test"
|
|
183
|
+
|
|
184
|
+
def setup(self):
|
|
185
|
+
self.listenFor("changed", lambda e: changed_events.append((e["old"], e["new"])))
|
|
186
|
+
|
|
187
|
+
@sequence("a", "b")
|
|
188
|
+
def seq(self, inst):
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
def a(self, inst):
|
|
192
|
+
return "external" # BREAK
|
|
193
|
+
|
|
194
|
+
def b(self, inst): return True
|
|
195
|
+
|
|
196
|
+
def external(self, inst):
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
stage = TestStage()
|
|
200
|
+
stage.setup()
|
|
201
|
+
stage.goto("seq")
|
|
202
|
+
|
|
203
|
+
stage.handleStage(log=False)
|
|
204
|
+
|
|
205
|
+
# 应该只有 a -> external, 没有 seq -> external
|
|
206
|
+
assert ("a", "external") in changed_events
|
|
207
|
+
assert ("seq", "external") not in changed_events
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class TestAsyncSemantics:
|
|
211
|
+
"""异步语义测试(RUN/None)"""
|
|
212
|
+
|
|
213
|
+
def test_run_pauses_execution(self):
|
|
214
|
+
"""测试: RUN (None) 暂停执行,下一tick恢复"""
|
|
215
|
+
call_count = {"a": 0}
|
|
216
|
+
|
|
217
|
+
class TestStage(Stage):
|
|
218
|
+
NAME = "Test"
|
|
219
|
+
|
|
220
|
+
@sequence("a", "b")
|
|
221
|
+
def seq(self, inst):
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
def a(self, inst):
|
|
225
|
+
call_count["a"] += 1
|
|
226
|
+
return None # RUN
|
|
227
|
+
|
|
228
|
+
def b(self, inst):
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
stage = TestStage()
|
|
232
|
+
stage.goto("seq")
|
|
233
|
+
|
|
234
|
+
# 第一个tick执行a,暂停
|
|
235
|
+
stage.handleStage(log=False)
|
|
236
|
+
assert call_count["a"] == 1
|
|
237
|
+
assert stage.current_state == "a"
|
|
238
|
+
|
|
239
|
+
# 第二个tick继续执行a,完成
|
|
240
|
+
stage.handleStage(log=False)
|
|
241
|
+
assert call_count["a"] == 2
|
|
242
|
+
|
|
243
|
+
def test_behavior_context_preserved_during_run(self):
|
|
244
|
+
"""测试: RUN期间行为树上下文被保留"""
|
|
245
|
+
class TestStage(Stage):
|
|
246
|
+
NAME = "Test"
|
|
247
|
+
|
|
248
|
+
@sequence("a", "b")
|
|
249
|
+
def seq(self, inst):
|
|
250
|
+
return True
|
|
251
|
+
|
|
252
|
+
def a(self, inst):
|
|
253
|
+
return None # RUN
|
|
254
|
+
|
|
255
|
+
def b(self, inst):
|
|
256
|
+
return True
|
|
257
|
+
|
|
258
|
+
stage = TestStage()
|
|
259
|
+
stage.goto("seq")
|
|
260
|
+
|
|
261
|
+
stage.handleStage(log=False)
|
|
262
|
+
|
|
263
|
+
# _bhead 和 _bhcontext 应该被保留
|
|
264
|
+
assert stage._bhead == "seq"
|
|
265
|
+
assert "seq" in stage._bhcontext
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class TestNestedBehaviors:
|
|
269
|
+
"""嵌套行为树测试"""
|
|
270
|
+
|
|
271
|
+
def test_nested_sequence_events(self):
|
|
272
|
+
"""测试: 嵌套序列正确触发所有事件"""
|
|
273
|
+
changed_events = []
|
|
274
|
+
|
|
275
|
+
class TestStage(Stage):
|
|
276
|
+
NAME = "Test"
|
|
277
|
+
|
|
278
|
+
def setup(self):
|
|
279
|
+
self.listenFor("changed", lambda e: changed_events.append((e["old"], e["new"])))
|
|
280
|
+
|
|
281
|
+
@parallel("worker_a", "worker_b")
|
|
282
|
+
def root(self, inst):
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
@sequence("s1", "s2")
|
|
286
|
+
def worker_a(self, inst):
|
|
287
|
+
return True
|
|
288
|
+
|
|
289
|
+
@sequence("p1", "p2")
|
|
290
|
+
def worker_b(self, inst):
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
def s1(self, inst): return True
|
|
294
|
+
def s2(self, inst): return True
|
|
295
|
+
def p1(self, inst): return True
|
|
296
|
+
def p2(self, inst): return True
|
|
297
|
+
|
|
298
|
+
stage = TestStage()
|
|
299
|
+
stage.setup()
|
|
300
|
+
stage.goto("root")
|
|
301
|
+
|
|
302
|
+
# 执行多个tick完成
|
|
303
|
+
for _ in range(10):
|
|
304
|
+
stage.handleStage(log=False)
|
|
305
|
+
|
|
306
|
+
# 应该有 root -> worker_a, root -> worker_b, worker_a -> s1, worker_b -> p1, 等
|
|
307
|
+
assert ("root", "worker_a") in changed_events
|
|
308
|
+
assert ("root", "worker_b") in changed_events
|
|
309
|
+
|
|
310
|
+
def test_nested_break_propagation(self):
|
|
311
|
+
"""测试: 嵌套BREAK正确传播,不触发父级事件"""
|
|
312
|
+
changed_events = []
|
|
313
|
+
|
|
314
|
+
class TestStage(Stage):
|
|
315
|
+
NAME = "Test"
|
|
316
|
+
|
|
317
|
+
def setup(self):
|
|
318
|
+
self.listenFor("changed", lambda e: changed_events.append((e["old"], e["new"])))
|
|
319
|
+
|
|
320
|
+
@parallel("inner_a", "inner_b")
|
|
321
|
+
def outer(self, inst):
|
|
322
|
+
return True
|
|
323
|
+
|
|
324
|
+
@sequence("x", "y")
|
|
325
|
+
def inner_a(self, inst):
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
def inner_b(self, inst):
|
|
329
|
+
return True
|
|
330
|
+
|
|
331
|
+
def x(self, inst):
|
|
332
|
+
return "exit" # BREAK
|
|
333
|
+
|
|
334
|
+
def y(self, inst):
|
|
335
|
+
return True
|
|
336
|
+
|
|
337
|
+
def exit(self, inst):
|
|
338
|
+
return True
|
|
339
|
+
|
|
340
|
+
stage = TestStage()
|
|
341
|
+
stage.setup()
|
|
342
|
+
stage.goto("outer")
|
|
343
|
+
|
|
344
|
+
stage.handleStage(log=False)
|
|
345
|
+
|
|
346
|
+
# 应该只有 x -> exit, 没有 inner_a -> exit, outer -> exit
|
|
347
|
+
assert ("x", "exit") in changed_events
|
|
348
|
+
assert ("inner_a", "exit") not in changed_events
|
|
349
|
+
assert ("outer", "exit") not in changed_events
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class TestOutputFormat:
|
|
353
|
+
"""输出格式测试"""
|
|
354
|
+
|
|
355
|
+
def test_no_duplicate_arrow_on_resume(self, capsys):
|
|
356
|
+
"""测试: 恢复时不重复打印 -> child"""
|
|
357
|
+
class TestStage(Stage):
|
|
358
|
+
NAME = "Test"
|
|
359
|
+
LOG = True
|
|
360
|
+
|
|
361
|
+
@sequence("a", "b")
|
|
362
|
+
def seq(self, inst):
|
|
363
|
+
return True
|
|
364
|
+
|
|
365
|
+
def a(self, inst):
|
|
366
|
+
return None # RUN
|
|
367
|
+
|
|
368
|
+
def b(self, inst):
|
|
369
|
+
return True
|
|
370
|
+
|
|
371
|
+
stage = TestStage()
|
|
372
|
+
stage.goto("seq")
|
|
373
|
+
|
|
374
|
+
# 第一个tick
|
|
375
|
+
stage.handleStage(log=True)
|
|
376
|
+
# 第二个tick(恢复)
|
|
377
|
+
stage.handleStage(log=True)
|
|
378
|
+
|
|
379
|
+
captured = capsys.readouterr()
|
|
380
|
+
# 应该只有一个 "-> a"
|
|
381
|
+
assert captured.out.count("-> a") == 1
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class TestEdgeCases:
|
|
385
|
+
"""边界情况测试"""
|
|
386
|
+
|
|
387
|
+
def test_sequence_restart_after_completion(self):
|
|
388
|
+
"""测试: 序列完成后重新进入应该从头开始"""
|
|
389
|
+
call_order = []
|
|
390
|
+
|
|
391
|
+
class TestStage(Stage):
|
|
392
|
+
NAME = "Test"
|
|
393
|
+
|
|
394
|
+
@sequence("a", "b")
|
|
395
|
+
def seq(self, inst):
|
|
396
|
+
call_order.append("seq")
|
|
397
|
+
return True
|
|
398
|
+
|
|
399
|
+
def a(self, inst):
|
|
400
|
+
call_order.append("a")
|
|
401
|
+
return True
|
|
402
|
+
|
|
403
|
+
def b(self, inst):
|
|
404
|
+
call_order.append("b")
|
|
405
|
+
return True
|
|
406
|
+
|
|
407
|
+
stage = TestStage()
|
|
408
|
+
stage.goto("seq")
|
|
409
|
+
|
|
410
|
+
# 完成第一次执行
|
|
411
|
+
for _ in range(5):
|
|
412
|
+
stage.handleStage(log=False)
|
|
413
|
+
|
|
414
|
+
assert "a" in call_order
|
|
415
|
+
assert "b" in call_order
|
|
416
|
+
|
|
417
|
+
def test_empty_behavior(self):
|
|
418
|
+
"""测试: 空行为树处理"""
|
|
419
|
+
class TestStage(Stage):
|
|
420
|
+
NAME = "Test"
|
|
421
|
+
|
|
422
|
+
@sequence()
|
|
423
|
+
def empty(self, inst):
|
|
424
|
+
return True
|
|
425
|
+
|
|
426
|
+
stage = TestStage()
|
|
427
|
+
stage.goto("empty")
|
|
428
|
+
result = stage.handleStage(log=False)
|
|
429
|
+
|
|
430
|
+
# 应该正常完成
|
|
431
|
+
assert result is not None
|
|
432
|
+
|
|
433
|
+
def test_unknown_state_in_behavior(self):
|
|
434
|
+
"""测试: 行为树中未知状态在类定义时抛出异常"""
|
|
435
|
+
with pytest.raises(TypeError):
|
|
436
|
+
class TestStage(Stage):
|
|
437
|
+
NAME = "Test"
|
|
438
|
+
|
|
439
|
+
@sequence("unknown", "a")
|
|
440
|
+
def seq(self, inst):
|
|
441
|
+
return True
|
|
442
|
+
|
|
443
|
+
def a(self, inst):
|
|
444
|
+
return True
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
if __name__ == "__main__":
|
|
448
|
+
pytest.main([__file__, "-v"])
|
chartflow/fsm/utils.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from efr import EventSystem as _EventSystem
|
|
2
|
+
from efr import EventFramework
|
|
3
|
+
|
|
4
|
+
__event_system__ = None
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EventSystem(_EventSystem):
|
|
8
|
+
"""Extended EventSystem with source filtering support."""
|
|
9
|
+
|
|
10
|
+
def pushEvent(self, event, data, *targets, source=None):
|
|
11
|
+
"""
|
|
12
|
+
Push event with optional source information.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
event: Event name
|
|
16
|
+
data: Event data dict
|
|
17
|
+
*targets: Target names
|
|
18
|
+
source: Source identifier (stored in data)
|
|
19
|
+
"""
|
|
20
|
+
if source is not None:
|
|
21
|
+
if not isinstance(data, dict):
|
|
22
|
+
data = {"_data": data}
|
|
23
|
+
data = dict(data)
|
|
24
|
+
data["source"] = source
|
|
25
|
+
super().pushEvent(event, data, *targets)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def GetES():
|
|
29
|
+
global __event_system__
|
|
30
|
+
if __event_system__ is None:
|
|
31
|
+
# Use standalone mode (synchronous) for predictable event handling
|
|
32
|
+
__event_system__ = EventSystem()
|
|
33
|
+
return __event_system__
|
|
34
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
qnode - A generic node graph canvas library based on PyQt6
|
|
3
|
+
|
|
4
|
+
This library provides a generic node graph canvas widget that can be used
|
|
5
|
+
for various node-based visual editing applications.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from chartflow.node import NodeCanvas, NodeItem, ConnectionItem
|
|
9
|
+
|
|
10
|
+
canvas = NodeCanvas()
|
|
11
|
+
node1 = canvas.addNode("Start", QPointF(0, 0))
|
|
12
|
+
node2 = canvas.addNode("End", QPointF(200, 0))
|
|
13
|
+
canvas.addConnection(node1, node2)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from chartflow.node.enums import NodeState, NodeShape, ArrowState, ConnectionType
|
|
17
|
+
from chartflow.node.styles import NodeStyle, ConnectionStyle, ArrowStyle
|
|
18
|
+
from chartflow.node.node_item import QNodeItem
|
|
19
|
+
from chartflow.node.port_item import QNodePort, QNodePortSpec
|
|
20
|
+
from chartflow.node._port_interface import IQNodePort
|
|
21
|
+
from chartflow.node.connection_item import ConnectionItem
|
|
22
|
+
from chartflow.node.arrow_item import ArrowItem
|
|
23
|
+
from chartflow.node.selection_rect import SelectionRect
|
|
24
|
+
from chartflow.node.style_panel import StylePanel
|
|
25
|
+
from chartflow.node.node_canvas import QNodeCanvas
|
|
26
|
+
from chartflow.node.main_window import NodeCanvasWindow
|
|
27
|
+
from chartflow.node.qnode_editor import QNodeEditor
|
|
28
|
+
from chartflow.node.popmenu import (
|
|
29
|
+
ICanvasPopMenu, QCanvasPopMenu,
|
|
30
|
+
INodePopMenu, QNodePopMenu,
|
|
31
|
+
ILinePopMenu, QLinePopMenu,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__version__ = "1.0.0"
|
|
35
|
+
__author__ = "qnode"
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# Enums
|
|
39
|
+
"NodeState",
|
|
40
|
+
"NodeShape",
|
|
41
|
+
"ArrowState",
|
|
42
|
+
"ConnectionType",
|
|
43
|
+
# Styles
|
|
44
|
+
"NodeStyle",
|
|
45
|
+
"ConnectionStyle",
|
|
46
|
+
"ArrowStyle",
|
|
47
|
+
# Items
|
|
48
|
+
"QNodeItem",
|
|
49
|
+
"QNodePort",
|
|
50
|
+
"QNodePortSpec",
|
|
51
|
+
"IQNodePort",
|
|
52
|
+
"ConnectionItem",
|
|
53
|
+
"ArrowItem",
|
|
54
|
+
"SelectionRect",
|
|
55
|
+
# UI Components
|
|
56
|
+
"StylePanel",
|
|
57
|
+
# Canvas and Window
|
|
58
|
+
"QNodeCanvas",
|
|
59
|
+
"NodeCanvasWindow",
|
|
60
|
+
# Editor
|
|
61
|
+
"QNodeEditor",
|
|
62
|
+
# Pop Menus
|
|
63
|
+
"ICanvasPopMenu", "QCanvasPopMenu",
|
|
64
|
+
"INodePopMenu", "QNodePopMenu",
|
|
65
|
+
"ILinePopMenu", "QLinePopMenu",
|
|
66
|
+
]
|