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
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()
|