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.
Files changed (79) hide show
  1. chartflow/__init__.py +28 -0
  2. chartflow/_verify_importexport.py +169 -0
  3. chartflow/edit/__init__.py +119 -0
  4. chartflow/edit/code_editor.py +1007 -0
  5. chartflow/edit/code_folder.py +523 -0
  6. chartflow/edit/completer.py +2652 -0
  7. chartflow/edit/context_analyzer.py +521 -0
  8. chartflow/edit/demo.py +469 -0
  9. chartflow/edit/line_number_area.py +548 -0
  10. chartflow/edit/minimap.py +581 -0
  11. chartflow/edit/python_editor.py +509 -0
  12. chartflow/edit/syntax_highlighter.py +291 -0
  13. chartflow/edit/test_editor.py +190 -0
  14. chartflow/flow/__init__.py +31 -0
  15. chartflow/flow/_demo.py +98 -0
  16. chartflow/flow/chart.py +1101 -0
  17. chartflow/flow/editor.py +375 -0
  18. chartflow/flow/flow.py +6 -0
  19. chartflow/flow/qchart.py +1250 -0
  20. chartflow/flow/test/__init__.py +0 -0
  21. chartflow/flow/test/test_chart.py +537 -0
  22. chartflow/flow/test_decorator_sync.py +102 -0
  23. chartflow/fsm/__init__.py +84 -0
  24. chartflow/fsm/decorators.py +395 -0
  25. chartflow/fsm/demo.py +381 -0
  26. chartflow/fsm/demo_pyqt6.py +881 -0
  27. chartflow/fsm/demo_tree.py +46 -0
  28. chartflow/fsm/logic.py +447 -0
  29. chartflow/fsm/meta.py +569 -0
  30. chartflow/fsm/scheduler.py +427 -0
  31. chartflow/fsm/stage.py +2079 -0
  32. chartflow/fsm/stdio.py +66 -0
  33. chartflow/fsm/test/__init__.py +7 -0
  34. chartflow/fsm/test/test_decorators.py +210 -0
  35. chartflow/fsm/test/test_events.py +202 -0
  36. chartflow/fsm/test/test_integration.py +596 -0
  37. chartflow/fsm/test/test_logic.py +371 -0
  38. chartflow/fsm/test/test_meta.py +192 -0
  39. chartflow/fsm/test/test_runtime_param.py +336 -0
  40. chartflow/fsm/test/test_scheduler.py +280 -0
  41. chartflow/fsm/test/test_stage.py +314 -0
  42. chartflow/fsm/test_behavior_events.py +263 -0
  43. chartflow/fsm/test_nested_parallel.py +80 -0
  44. chartflow/fsm/test_stage_spec.py +448 -0
  45. chartflow/fsm/utils.py +34 -0
  46. chartflow/node/__init__.py +66 -0
  47. chartflow/node/_node_base.py +727 -0
  48. chartflow/node/_node_interaction.py +488 -0
  49. chartflow/node/_node_interface.py +426 -0
  50. chartflow/node/_node_markers.py +648 -0
  51. chartflow/node/_node_ports.py +536 -0
  52. chartflow/node/_port_interface.py +111 -0
  53. chartflow/node/arrow_item.py +287 -0
  54. chartflow/node/color_utils.py +113 -0
  55. chartflow/node/connection_item.py +939 -0
  56. chartflow/node/demo.py +258 -0
  57. chartflow/node/demo_arrow_drag.py +46 -0
  58. chartflow/node/demo_decorator.py +63 -0
  59. chartflow/node/demo_ports.py +69 -0
  60. chartflow/node/enums.py +35 -0
  61. chartflow/node/example.py +84 -0
  62. chartflow/node/layout_utils.py +307 -0
  63. chartflow/node/main_window.py +196 -0
  64. chartflow/node/menu_bar.py +240 -0
  65. chartflow/node/node_canvas.py +1730 -0
  66. chartflow/node/node_item.py +754 -0
  67. chartflow/node/popmenu.py +472 -0
  68. chartflow/node/port_item.py +591 -0
  69. chartflow/node/qnode_editor.py +73 -0
  70. chartflow/node/selection_rect.py +53 -0
  71. chartflow/node/style_panel.py +615 -0
  72. chartflow/node/styles.py +63 -0
  73. chartflow/node/test_qnode.py +2336 -0
  74. chartflow/showcase.py +528 -0
  75. chartflow/theme.py +508 -0
  76. chartflow-0.1.dist-info/METADATA +23 -0
  77. chartflow-0.1.dist-info/RECORD +79 -0
  78. chartflow-0.1.dist-info/WHEEL +5 -0
  79. chartflow-0.1.dist-info/top_level.txt +1 -0
chartflow/showcase.py ADDED
@@ -0,0 +1,528 @@
1
+ """
2
+ chartflow.showcase — one small but complete multi-page demo.
3
+
4
+ A single application that tours every chartflow subpackage on its own page,
5
+ unified by the central :mod:`chartflow.theme` dark stylesheet:
6
+
7
+ 1. 状态机 (fsm) — a traffic-light Stage you can step through
8
+ 2. 节点图 (node) — an interactive node-graph canvas
9
+ 3. 流程图 (flow) — a QFlowChart running a live state machine
10
+ 4. 编辑器 (edit) — the PythonEditor with syntax highlighting
11
+ 5. IP端口 (ports) — IP-core modules rendered as nodes with QNodePortSpec ports
12
+
13
+ Run it::
14
+
15
+ python -m chartflow.showcase
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import math
22
+ import re
23
+ import sys
24
+ from pathlib import Path
25
+ from typing import Optional
26
+
27
+ from PyQt6.QtCore import Qt, QPointF, QSize, QTimer
28
+ from PyQt6.QtWidgets import (
29
+ QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
30
+ QListWidget, QListWidgetItem, QStackedWidget, QLabel,
31
+ QPushButton, QTextEdit, QFrame,
32
+ )
33
+
34
+ from chartflow.theme import applyTheme, PALETTE
35
+ from chartflow.fsm import Stage
36
+ from chartflow.node import QNodeCanvas, NodeShape, QNodePortSpec
37
+ from chartflow.node.layout_utils import ViewportFitter
38
+ from chartflow.flow.qchart import QFlowChart
39
+ from chartflow.flow.chart import ChartNode, ChartEdge
40
+ from chartflow.fsm.logic import Logic
41
+ from chartflow.edit import PythonEditor
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Page 1 — FSM: a hand-stepped traffic-light state machine
46
+ # ---------------------------------------------------------------------------
47
+ class _TrafficLight(Stage):
48
+ """red -> green -> yellow -> red ..."""
49
+
50
+ NAME = "TrafficLight"
51
+
52
+ def onChanged(self, src, dst, inst): # type: ignore[override]
53
+ # Forward transitions to the bound UI page, if any.
54
+ cb = getattr(self, "_on_change", None)
55
+ if cb is not None:
56
+ cb(src, dst)
57
+
58
+ def red(self, inst): return "green"
59
+ def green(self, inst): return "yellow"
60
+ def yellow(self, inst): return "red"
61
+
62
+
63
+ _LIGHT_COLORS = {"red": "#ff6b6b", "green": "#7ee787", "yellow": "#f0c040"}
64
+ _LIGHT_LABELS = {"red": "红灯 · 停", "green": "绿灯 · 行", "yellow": "黄灯 · 等待"}
65
+
66
+
67
+ class FSMPage(QWidget):
68
+ """Step a Stage state machine and watch it transition."""
69
+
70
+ def __init__(self, parent: Optional[QWidget] = None) -> None:
71
+ super().__init__(parent)
72
+ self._fsm = _TrafficLight()
73
+ self._fsm.goto("red")
74
+ self._fsm._on_change = self._onTransition # type: ignore[attr-defined]
75
+
76
+ root = QVBoxLayout(self)
77
+ root.setContentsMargins(24, 24, 24, 24)
78
+ root.setSpacing(16)
79
+
80
+ title = QLabel("有限状态机 · chartflow.fsm")
81
+ title.setStyleSheet(f"font-size: 18px; font-weight: 600; color: {PALETTE.text};")
82
+ root.addWidget(title)
83
+ root.addWidget(self._hint("一个 Stage,每个状态返回下一状态名。点击「单步」推进一个 tick。"))
84
+
85
+ # The traffic light itself
86
+ self._lamp = QLabel("红灯 · 停")
87
+ self._lamp.setAlignment(Qt.AlignmentFlag.AlignCenter)
88
+ self._lamp.setFixedHeight(120)
89
+ self._lamp.setStyleSheet(
90
+ f"background-color: {_LIGHT_COLORS['red']}; color: #1a1a22;"
91
+ " border-radius: 12px; font-size: 22px; font-weight: 700;"
92
+ )
93
+ root.addWidget(self._lamp)
94
+
95
+ row = QHBoxLayout()
96
+ self._btn_step = QPushButton("单步推进")
97
+ self._btn_step.setObjectName("primary")
98
+ self._btn_step.setCursor(Qt.CursorShape.PointingHandCursor)
99
+ self._btn_step.clicked.connect(self._step)
100
+ self._btn_reset = QPushButton("重置")
101
+ self._btn_reset.setCursor(Qt.CursorShape.PointingHandCursor)
102
+ self._btn_reset.clicked.connect(self._reset)
103
+ row.addWidget(self._btn_step)
104
+ row.addWidget(self._btn_reset)
105
+ row.addStretch()
106
+ root.addLayout(row)
107
+
108
+ root.addWidget(self._hint("转换日志(onChanged 回调):"))
109
+ self._log = QTextEdit()
110
+ self._log.setReadOnly(True)
111
+ self._log.setStyleSheet(
112
+ f"background-color: {PALETTE.code_bg}; color: {PALETTE.code_text};"
113
+ f" border: 1px solid {PALETTE.border}; border-radius: 9px;"
114
+ " font-family: 'JetBrains Mono', Consolas, Monaco, monospace; padding: 9px 12px;"
115
+ )
116
+ root.addWidget(self._log, stretch=1)
117
+
118
+ self._log.append("初始状态: red")
119
+ self._refresh("red")
120
+
121
+ def _step(self) -> None:
122
+ self._fsm.handleStage(log=False) # transitions fire onChanged -> _onTransition
123
+
124
+ def _reset(self) -> None:
125
+ self._fsm.goto("red")
126
+ self._log.clear()
127
+ self._log.append("初始状态: red")
128
+ self._refresh("red")
129
+
130
+ def _onTransition(self, src, dst) -> None:
131
+ self._log.append(f"{src} -> {dst}")
132
+ self._refresh(dst)
133
+
134
+ def _refresh(self, state: str) -> None:
135
+ color = _LIGHT_COLORS.get(state, PALETTE.surface)
136
+ self._lamp.setText(_LIGHT_LABELS.get(state, state))
137
+ self._lamp.setStyleSheet(
138
+ f"background-color: {color}; color: #1a1a22;"
139
+ " border-radius: 12px; font-size: 22px; font-weight: 700;"
140
+ )
141
+
142
+ @staticmethod
143
+ def _hint(text: str) -> QLabel:
144
+ lbl = QLabel(text)
145
+ lbl.setStyleSheet(f"color: {PALETTE.text_dim}; font-size: 12px;")
146
+ lbl.setWordWrap(True)
147
+ return lbl
148
+
149
+
150
+ # ---------------------------------------------------------------------------
151
+ # Page 2 — Node canvas: an interactive node graph
152
+ # ---------------------------------------------------------------------------
153
+ class NodePage(QWidget):
154
+ def __init__(self, parent: Optional[QWidget] = None) -> None:
155
+ super().__init__(parent)
156
+
157
+ root = QVBoxLayout(self)
158
+ root.setContentsMargins(0, 0, 0, 0)
159
+ root.setSpacing(0)
160
+
161
+ header = QFrame()
162
+ header.setStyleSheet(f"background-color: {PALETTE.panel}; border: none;"
163
+ f" border-bottom: 1px solid {PALETTE.border};")
164
+ hlay = QVBoxLayout(header)
165
+ hlay.setContentsMargins(24, 14, 24, 14)
166
+ hlay.setSpacing(2)
167
+ title = QLabel("节点图画布 · chartflow.node")
168
+ title.setStyleSheet(f"font-size: 18px; font-weight: 600; color: {PALETTE.text};")
169
+ hint = QLabel("拖拽节点 · 从节点边缘拖出连接 · 右键连接切换类型 · 中键/空格/Alt+左键平移 · Alt悬停节点=调整大小光标 · 滚轮缩放")
170
+ hint.setStyleSheet(f"color: {PALETTE.text_dim}; font-size: 12px;")
171
+ hlay.addWidget(title)
172
+ hlay.addWidget(hint)
173
+ root.addWidget(header)
174
+
175
+ self._canvas = QNodeCanvas()
176
+ root.addWidget(self._canvas, stretch=1)
177
+ self._seed()
178
+
179
+ def _seed(self) -> None:
180
+ c = self._canvas
181
+ start = c.addNode("开始", QPointF(0, 0), NodeShape.ELLIPSE)
182
+ read = c.addNode("读取数据", QPointF(220, -120), NodeShape.RECTANGLE)
183
+ proc = c.addNode("处理", QPointF(440, 0), NodeShape.RECTANGLE)
184
+ check = c.addNode("校验?", QPointF(660, 0), NodeShape.DIAMOND)
185
+ ok = c.addNode("成功", QPointF(880, -120), NodeShape.RECTANGLE)
186
+ bad = c.addNode("失败", QPointF(880, 120), NodeShape.RECTANGLE)
187
+ end = c.addNode("结束", QPointF(1100, 0), NodeShape.ELLIPSE)
188
+
189
+ c.addConnection(start, read)
190
+ c.addConnection(read, proc)
191
+ c.addConnection(proc, check)
192
+ c.addConnection(check, ok)
193
+ c.addConnection(check, bad)
194
+ c.addConnection(ok, end)
195
+ c.addConnection(bad, read) # 失败重试
196
+ c.startNode = start
197
+
198
+
199
+ # ---------------------------------------------------------------------------
200
+ # Page 3 — Flow chart: a QFlowChart running a live state machine
201
+ # ---------------------------------------------------------------------------
202
+ class _PipelineLogic(Logic):
203
+ """A small linear pipeline visualized by QFlowChart."""
204
+
205
+ NAME = "PipelineLogic"
206
+
207
+ def entry(self, inst): return "fetch"
208
+ def fetch(self, inst): return "parse"
209
+ def parse(self, inst): return "validate"
210
+ def validate(self, inst): return "done"
211
+ def done(self, inst): return None # terminal
212
+
213
+
214
+ class FlowPage(QWidget):
215
+ def __init__(self, parent: Optional[QWidget] = None) -> None:
216
+ super().__init__(parent)
217
+
218
+ root = QVBoxLayout(self)
219
+ root.setContentsMargins(24, 24, 24, 24)
220
+ root.setSpacing(12)
221
+
222
+ title = QLabel("流程图执行 · chartflow.flow")
223
+ title.setStyleSheet(f"font-size: 18px; font-weight: 600; color: {PALETTE.text};")
224
+ root.addWidget(title)
225
+ root.addWidget(self._hint(
226
+ "QFlowChart 绑定一个 Stage/Logic,节点与状态一一对应。"
227
+ "点击「Next Step」单步执行,活跃状态会高亮。"))
228
+
229
+ self._chart = QFlowChart(_PipelineLogic(), show_controls=True)
230
+ # Build the graph that mirrors the logic's states.
231
+ for nid, name, shape in [
232
+ ("entry", "Entry", "ellipse"),
233
+ ("fetch", "Fetch", "rectangle"),
234
+ ("parse", "Parse", "rectangle"),
235
+ ("validate", "Validate", "diamond"),
236
+ ("done", "Done", "ellipse"),
237
+ ]:
238
+ self._chart.addNode(ChartNode(nid=nid, name=name, ntype="state", shape=shape))
239
+ for i, (src, dst) in enumerate(
240
+ [("entry", "fetch"), ("fetch", "parse"),
241
+ ("parse", "validate"), ("validate", "done")]
242
+ ):
243
+ self._chart.addEdge(ChartEdge(eid=f"e{i}", src=src, dst=dst, etype="flow"))
244
+ self._chart.startnode = "entry"
245
+
246
+ root.addWidget(self._chart, stretch=1)
247
+
248
+ @staticmethod
249
+ def _hint(text: str) -> QLabel:
250
+ lbl = QLabel(text)
251
+ lbl.setStyleSheet(f"color: {PALETTE.text_dim}; font-size: 12px;")
252
+ lbl.setWordWrap(True)
253
+ return lbl
254
+
255
+
256
+ # ---------------------------------------------------------------------------
257
+ # Page 4 — Code editor
258
+ # ---------------------------------------------------------------------------
259
+ _SAMPLE_CODE = '''"""欢迎使用 chartflow.edit 代码编辑器。"""
260
+
261
+ from chartflow import Stage, Logic, Scheduler
262
+ from chartflow.fsm.decorators import sequence, parallel
263
+
264
+
265
+ class Counter(Logic):
266
+ """一个会数到 3 然后结束的状态机。"""
267
+
268
+ NAME = "Counter"
269
+
270
+ def entry(self, inst):
271
+ inst.count = 0
272
+ return "tick"
273
+
274
+ def tick(self, inst):
275
+ inst.count += 1
276
+ print(f"tick #{inst.count}")
277
+ if inst.count >= 3:
278
+ return "done"
279
+ return "tick"
280
+
281
+ def done(self, inst):
282
+ print("完成!")
283
+ return None
284
+
285
+
286
+ if __name__ == "__main__":
287
+ scheduler = Scheduler()
288
+ Counter() # 自动注册到调度器
289
+ for _ in range(10):
290
+ scheduler.handle()
291
+ '''
292
+
293
+
294
+ class EditorPage(QWidget):
295
+ def __init__(self, parent: Optional[QWidget] = None) -> None:
296
+ super().__init__(parent)
297
+
298
+ root = QVBoxLayout(self)
299
+ root.setContentsMargins(0, 0, 0, 0)
300
+ root.setSpacing(0)
301
+
302
+ header = QFrame()
303
+ header.setStyleSheet(f"background-color: {PALETTE.panel}; border: none;"
304
+ f" border-bottom: 1px solid {PALETTE.border};")
305
+ hlay = QVBoxLayout(header)
306
+ hlay.setContentsMargins(24, 14, 24, 14)
307
+ hlay.setSpacing(2)
308
+ title = QLabel("Python 编辑器 · chartflow.edit")
309
+ title.setStyleSheet(f"font-size: 18px; font-weight: 600; color: {PALETTE.text};")
310
+ hint = QLabel("语法高亮 · 代码折叠 · 行号 · 小地图 · 自动补全")
311
+ hint.setStyleSheet(f"color: {PALETTE.text_dim}; font-size: 12px;")
312
+ hlay.addWidget(title)
313
+ hlay.addWidget(hint)
314
+ root.addWidget(header)
315
+
316
+ self._editor = PythonEditor()
317
+ self._editor.set_code(_SAMPLE_CODE)
318
+ root.addWidget(self._editor, stretch=1)
319
+
320
+
321
+ # ---------------------------------------------------------------------------
322
+ # Page 5 — IP-core ports: render .ipc.json modules as nodes with QNodePortSpec
323
+ # ---------------------------------------------------------------------------
324
+ _DATA_DIR = Path(__file__).parent / "showcase_data"
325
+
326
+
327
+ def _parse_bit_width(width) -> int:
328
+ """ipc.json 的 width 字段 → 位宽: null -> 1, '[15:0]' -> 16, '8' -> 8"""
329
+ if width is None:
330
+ return 1
331
+ s = str(width).strip()
332
+ m = re.match(r"^\[(\d+):(\d+)\]$", s)
333
+ if m:
334
+ return max(1, int(m.group(1)) - int(m.group(2)) + 1)
335
+ try:
336
+ return max(1, int(float(s)))
337
+ except ValueError:
338
+ return 1
339
+
340
+
341
+ def _shape_and_size_for_bits(bits: int):
342
+ """根据位宽选择 dot 的 shape 与 size
343
+
344
+ 1 bit -> triangle(单线信号); 2~8 bit -> circle; >=9 bit -> trapezoid(总线,
345
+ 尺寸随位宽增长)。
346
+ """
347
+ if bits <= 1:
348
+ return "triangle", 10
349
+ if bits <= 8:
350
+ return "circle", 10 + min(bits - 1, 6)
351
+ return "trapezoid", 10 + math.ceil((bits - 8) / 2)
352
+
353
+
354
+ def _ports_from_module(module_list):
355
+ """把一个 ipc.json 模块的端口列表转成 QNodePortSpec 的 {'left':..., 'right':...}
356
+
357
+ 规则:
358
+ - direction output -> right(橙), 其余(input/inout) -> left(绿)
359
+ - shape/size 由位宽决定
360
+ - 位宽>1 时 hit = 位宽数字; 位宽==1 时 hit 为空
361
+ - label = 端口名 + 位宽文本(如 cmp[15:0]); 无位宽时仅为端口名
362
+ """
363
+ left, right = [], []
364
+ for p in module_list:
365
+ name = p.get("name", "")
366
+ direction = p.get("direction", "input")
367
+ width = p.get("width", None)
368
+ bits = _parse_bit_width(width)
369
+ shape, size = _shape_and_size_for_bits(bits)
370
+ hit = str(bits) if bits > 1 else ""
371
+ width_str = width if (isinstance(width, str) and width) else ""
372
+ label = f"{name}{width_str}" if width_str else name
373
+ port = QNodePortSpec(name=name, shape=shape, size=size, hit=hit,
374
+ label=label, outline=2)
375
+ if direction == "output":
376
+ right.append(port)
377
+ else:
378
+ left.append(port)
379
+ return {"left": left, "right": right}
380
+
381
+
382
+ def _load_ipc_module(path: Path):
383
+ """读取 ipc.json,返回 (节点显示名, 端口列表)"""
384
+ with open(path, encoding="utf-8") as f:
385
+ spec = json.load(f)
386
+ info = spec.get("info", {}) or {}
387
+ modules = spec.get("modules", {}) or {}
388
+ mod_name = (info.get("module_final") or info.get("module_initial")
389
+ or info.get("name") or "")
390
+ ports_list = modules.get(mod_name)
391
+ if ports_list is None and modules:
392
+ ports_list = next(iter(modules.values()))
393
+ ports_list = ports_list or []
394
+ display_name = info.get("name") or mod_name or "IP"
395
+ return display_name, ports_list
396
+
397
+
398
+ class PortsPage(QWidget):
399
+ """读取 showcase_data 下的 .ipc.json,把 IP 核渲染成带端口的节点。"""
400
+
401
+ def __init__(self, parent: Optional[QWidget] = None) -> None:
402
+ super().__init__(parent)
403
+
404
+ root = QVBoxLayout(self)
405
+ root.setContentsMargins(0, 0, 0, 0)
406
+ root.setSpacing(0)
407
+
408
+ header = QFrame()
409
+ header.setStyleSheet(f"background-color: {PALETTE.panel}; border: none;"
410
+ f" border-bottom: 1px solid {PALETTE.border};")
411
+ hlay = QVBoxLayout(header)
412
+ hlay.setContentsMargins(24, 14, 24, 14)
413
+ hlay.setSpacing(2)
414
+ title = QLabel("IP 核端口 · chartflow.node.QNodePortSpec")
415
+ title.setStyleSheet(f"font-size: 18px; font-weight: 600; color: {PALETTE.text};")
416
+ hint = QLabel("读取 .ipc.json · 按位宽选择 dot 形状/尺寸 · 位宽>1 显示在 hit · "
417
+ "hover 节点>0.5s 或 hover 端口显示 label · dot hover 时 DodgerBlue 描边")
418
+ hint.setStyleSheet(f"color: {PALETTE.text_dim}; font-size: 12px;")
419
+ hlay.addWidget(title)
420
+ hlay.addWidget(hint)
421
+ root.addWidget(header)
422
+
423
+ self._canvas = QNodeCanvas()
424
+ # self._canvas.linkables["right->left"] = False
425
+ root.addWidget(self._canvas, stretch=1)
426
+ self._seed()
427
+
428
+ # 视口就绪后适配显示两个节点
429
+ QTimer.singleShot(120, self._fit)
430
+
431
+ def _seed(self) -> None:
432
+ files = ["counter-n.ipc.json", "pwmx.ipc.json"]
433
+ span = 460.0
434
+ for i, fname in enumerate(files):
435
+ path = _DATA_DIR / fname
436
+ if not path.exists():
437
+ continue
438
+ display_name, ports_list = _load_ipc_module(path)
439
+ x = (i - (len(files) - 1) / 2.0) * span
440
+ node = self._canvas.addNode(display_name, QPointF(x, 0),
441
+ NodeShape.RECTANGLE)
442
+ node.selfrefs["left->right"] = False
443
+ node.selfrefs["node->port"] = True
444
+ node.selfrefs["port->node"] = True
445
+ node.selfrefs["port->port"] = True
446
+ node.setFont(size=13)
447
+ node.ports = _ports_from_module(ports_list)
448
+
449
+ def _fit(self) -> None:
450
+ try:
451
+ ViewportFitter(self._canvas).fitViewport(margin=60)
452
+ except Exception:
453
+ pass
454
+
455
+
456
+ # ---------------------------------------------------------------------------
457
+ # Shell: sidebar navigation + stacked pages
458
+ # ---------------------------------------------------------------------------
459
+ _PAGES = [
460
+ ("状态机", "fsm", FSMPage),
461
+ ("节点图", "node", NodePage),
462
+ ("流程图", "flow", FlowPage),
463
+ ("编辑器", "edit", EditorPage),
464
+ ("IP端口", "ports", PortsPage),
465
+ ]
466
+
467
+
468
+ class ShowcaseWindow(QMainWindow):
469
+ def __init__(self) -> None:
470
+ super().__init__()
471
+ self.setWindowTitle("chartflow 全功能演示")
472
+ self.resize(1240, 820)
473
+
474
+ central = QWidget()
475
+ self.setCentralWidget(central)
476
+ layout = QHBoxLayout(central)
477
+ layout.setContentsMargins(0, 0, 0, 0)
478
+ layout.setSpacing(0)
479
+
480
+ # Left navigation
481
+ self._nav = QListWidget()
482
+ self._nav.setFixedWidth(190)
483
+ self._nav.setIconSize(QSize(1, 1))
484
+ self._nav.setSpacing(2)
485
+ self._nav.setCurrentRow(0)
486
+ self._nav.currentRowChanged.connect(self._onNav)
487
+ layout.addWidget(self._nav)
488
+
489
+ # Vertical separator
490
+ sep = QFrame()
491
+ sep.setFixedWidth(1)
492
+ sep.setStyleSheet(f"background-color: {PALETTE.border};")
493
+ layout.addWidget(sep)
494
+
495
+ # Right stacked pages
496
+ self._stack = QStackedWidget()
497
+ layout.addWidget(self._stack, stretch=1)
498
+
499
+ for label, _key, page_cls in _PAGES:
500
+ item = QListWidgetItem(label)
501
+ item.setSizeHint(QSize(0, 44))
502
+ self._nav.addItem(item)
503
+ self._stack.addWidget(page_cls())
504
+
505
+ self._buildStatus()
506
+
507
+ def _buildStatus(self) -> None:
508
+ bar = self.statusBar()
509
+ bar.showMessage("就绪 · 在左侧选择一个模块")
510
+
511
+ def _onNav(self, row: int) -> None:
512
+ if 0 <= row < len(_PAGES):
513
+ label, key, _ = _PAGES[row]
514
+ self._stack.setCurrentIndex(row)
515
+ self.statusBar().showMessage(f"当前页面:{label} · chartflow.{key}")
516
+
517
+
518
+ def main() -> None:
519
+ app = QApplication(sys.argv)
520
+ app.setStyle("Fusion")
521
+ applyTheme(app)
522
+ win = ShowcaseWindow()
523
+ win.show()
524
+ sys.exit(app.exec())
525
+
526
+
527
+ if __name__ == "__main__":
528
+ main()