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
@@ -0,0 +1,648 @@
1
+ """
2
+ 节点标记辅助类模块
3
+
4
+ 定义节点上附着的浮动标记/按钮辅助类:
5
+ - HoverButton: 左上角编辑按钮(📖)
6
+ - StartMarker: 右上角起点标记(🔷)
7
+ - SingleMarker: 左下角执行状态标记(⚡)
8
+ - CustomMarker: 右下角用户自定义标记(★)
9
+
10
+ 这些类仅依赖 Qt 与模块常量, 不依赖 NodeItem, 故独立存放以打破循环导入。
11
+ node_item.py 通过 re-export 保持 ``from chartflow.node.node_item import HoverButton``
12
+ 等历史导入路径不变。
13
+ """
14
+
15
+ from typing import Optional
16
+
17
+ from PyQt6.QtCore import Qt, QPointF, QRectF, QObject, pyqtProperty, pyqtSignal
18
+ from PyQt6.QtGui import QBrush, QPen, QColor
19
+ from PyQt6.QtWidgets import (
20
+ QGraphicsItem, QGraphicsEllipseItem, QGraphicsTextItem,
21
+ )
22
+
23
+ from chartflow.node._node_base import (
24
+ Z_VALUE_BUTTON,
25
+ Z_VALUE_MARKER,
26
+ ANIMATION_DURATION_SHOW,
27
+ ANIMATION_DURATION_HIDE,
28
+ )
29
+
30
+
31
+ class HoverButton(QObject):
32
+ """
33
+ 悬浮按钮 - 用于节点编辑的浮动按钮
34
+ 包含圆形背景和编辑图标
35
+ """
36
+ clicked = pyqtSignal() # 点击信号
37
+
38
+ def __init__(self, parent: QGraphicsItem = None) -> None:
39
+ super().__init__()
40
+
41
+ self._parent = parent
42
+ self._radius: float = 15.0
43
+
44
+ # 创建圆形背景(浅黄灰色)
45
+ self._ellipse_item = QGraphicsEllipseItem(parent)
46
+ self._ellipse_item.setBrush(QBrush(QColor(240, 230, 200))) # 浅黄灰色
47
+ self._ellipse_item.setPen(QPen(QColor(180, 130, 80), 2)) # 橙灰色描边,2像素宽
48
+ self._ellipse_item.setZValue(Z_VALUE_BUTTON) # 确保在最上层
49
+ self._ellipse_item.setAcceptHoverEvents(True)
50
+ self._ellipse_item.setToolTip("编辑窗口已打开") # 添加 tooltip
51
+
52
+ # 创建编辑图标文本(书本图标)
53
+ self._text_item = QGraphicsTextItem("📖", parent)
54
+ self._text_item.setZValue(Z_VALUE_BUTTON + 1) # 确保在圆形背景之上
55
+ self._text_item.setAcceptHoverEvents(False)
56
+ self._text_item.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
57
+
58
+ # 初始隐藏(只有当编辑窗口打开时才显示)
59
+ self._opacity: float = 0.0
60
+ self._updateOpacity()
61
+
62
+ # 设置按钮位置(左上角)
63
+ self._updatePosition()
64
+
65
+ def _updateOpacity(self) -> None:
66
+ """更新透明度"""
67
+ if not hasattr(self, '_ellipse_item') or self._ellipse_item is None:
68
+ return
69
+ try:
70
+ self._ellipse_item.setOpacity(self._opacity)
71
+ self._text_item.setOpacity(self._opacity)
72
+ except RuntimeError:
73
+ pass
74
+
75
+ def getOpacity(self) -> float:
76
+ """获取当前透明度"""
77
+ return self._opacity
78
+
79
+ def setOpacity(self, value: float) -> None:
80
+ """设置透明度"""
81
+ self._opacity = value
82
+ self._updateOpacity()
83
+
84
+ # 创建属性用于动画
85
+ opacity = pyqtProperty(float, getOpacity, setOpacity)
86
+
87
+ def _isValid(self) -> bool:
88
+ """检查 C++ 对象是否仍然有效"""
89
+ if not hasattr(self, '_ellipse_item') or self._ellipse_item is None:
90
+ return False
91
+ try:
92
+ self._ellipse_item.isVisible()
93
+ return True
94
+ except RuntimeError:
95
+ return False
96
+
97
+ def _updatePosition(self) -> None:
98
+ """更新按钮位置(节点左上角)"""
99
+ if not self._isValid():
100
+ return
101
+ if self._parent is None:
102
+ return
103
+
104
+ try:
105
+ parent_rect = self._parent.rect()
106
+ center = parent_rect.center()
107
+
108
+ # 计算节点左上角的偏移位置
109
+ offset_x = -parent_rect.width() / 2 + self._radius
110
+ offset_y = -parent_rect.height() / 2 + self._radius
111
+
112
+ # 设置圆形背景位置
113
+ ellipse_rect = QRectF(
114
+ center.x() + offset_x - self._radius,
115
+ center.y() + offset_y - self._radius,
116
+ self._radius * 2,
117
+ self._radius * 2
118
+ )
119
+ self._ellipse_item.setRect(ellipse_rect)
120
+
121
+ # 设置文本居中
122
+ text_rect = self._text_item.boundingRect()
123
+ text_pos = QPointF(
124
+ center.x() + offset_x - text_rect.width() / 2,
125
+ center.y() + offset_y - text_rect.height() / 2
126
+ )
127
+ self._text_item.setPos(text_pos)
128
+ except RuntimeError:
129
+ pass
130
+
131
+ def setRadius(self, radius: float) -> None:
132
+ """设置按钮半径"""
133
+ self._radius = radius
134
+ self._updatePosition()
135
+
136
+ def show(self) -> None:
137
+ """显示按钮(带动画)"""
138
+ if not self._isValid():
139
+ return
140
+ try:
141
+ self._updatePosition()
142
+ self._ellipse_item.show()
143
+ self._text_item.show()
144
+ except RuntimeError:
145
+ pass
146
+
147
+ def hide(self) -> None:
148
+ """隐藏按钮"""
149
+ if not self._isValid():
150
+ return
151
+ try:
152
+ self._ellipse_item.hide()
153
+ self._text_item.hide()
154
+ except RuntimeError:
155
+ pass
156
+
157
+ def contains(self, pos: QPointF) -> bool:
158
+ """检查点是否在按钮内"""
159
+ if not self._isValid():
160
+ return False
161
+ try:
162
+ return self._ellipse_item.contains(pos) or self._text_item.contains(pos)
163
+ except RuntimeError:
164
+ return False
165
+
166
+ def mousePressEvent(self, event) -> bool:
167
+ """处理鼠标按下事件"""
168
+ if not self._isValid():
169
+ return False
170
+ try:
171
+ local_pos = self._ellipse_item.mapFromScene(event.scenePos())
172
+ if self._ellipse_item.contains(local_pos):
173
+ self.clicked.emit()
174
+ return True
175
+
176
+ text_local_pos = self._text_item.mapFromScene(event.scenePos())
177
+ if self._text_item.contains(text_local_pos):
178
+ self.clicked.emit()
179
+ return True
180
+ except RuntimeError:
181
+ pass
182
+
183
+ return False
184
+
185
+
186
+ class StartMarker(QObject):
187
+ """
188
+ 起点标记 - 用于在节点右上角显示起点状态
189
+ 包含圆形背景和🔷图标
190
+ """
191
+
192
+ def __init__(self, parent: QGraphicsItem = None) -> None:
193
+ super().__init__()
194
+
195
+ self._parent = parent
196
+ self._radius: float = 15.0
197
+
198
+ # 创建圆形背景
199
+ self._ellipse_item = QGraphicsEllipseItem(parent)
200
+ self._ellipse_item.setBrush(QBrush(QColor(200, 240, 220))) # 浅绿色背景
201
+ self._ellipse_item.setPen(QPen(QColor(80, 150, 100), 2)) # 绿灰色描边,2像素宽
202
+ self._ellipse_item.setZValue(Z_VALUE_MARKER) # 确保在最上层
203
+ self._ellipse_item.setAcceptHoverEvents(False)
204
+ self._ellipse_item.setToolTip("起点节点") # 添加 tooltip
205
+
206
+ # 创建图标文本(🔷)
207
+ self._text_item = QGraphicsTextItem("🔷", parent)
208
+ self._text_item.setZValue(Z_VALUE_MARKER + 1) # 确保在圆形背景之上
209
+ self._text_item.setAcceptHoverEvents(False)
210
+ self._text_item.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
211
+
212
+ # 初始隐藏
213
+ self._opacity: float = 0.0
214
+ self._updateOpacity()
215
+
216
+ # 设置按钮位置(右上角)
217
+ self._updatePosition()
218
+
219
+ def _isValid(self) -> bool:
220
+ """检查 C++ 对象是否仍然有效"""
221
+ if not hasattr(self, '_ellipse_item') or self._ellipse_item is None:
222
+ return False
223
+ try:
224
+ self._ellipse_item.isVisible()
225
+ return True
226
+ except RuntimeError:
227
+ return False
228
+
229
+ def _updateOpacity(self) -> None:
230
+ """更新透明度"""
231
+ if not self._isValid():
232
+ return
233
+ try:
234
+ self._ellipse_item.setOpacity(self._opacity)
235
+ self._text_item.setOpacity(self._opacity)
236
+ except RuntimeError:
237
+ pass
238
+
239
+ def getOpacity(self) -> float:
240
+ """获取当前透明度"""
241
+ return self._opacity
242
+
243
+ def setOpacity(self, value: float) -> None:
244
+ """设置透明度"""
245
+ self._opacity = value
246
+ self._updateOpacity()
247
+
248
+ # 创建属性用于动画
249
+ opacity = pyqtProperty(float, getOpacity, setOpacity)
250
+
251
+ def _updatePosition(self) -> None:
252
+ """更新标记位置(节点右上角)"""
253
+ if not self._isValid():
254
+ return
255
+ if self._parent is None:
256
+ return
257
+
258
+ try:
259
+ parent_rect = self._parent.rect()
260
+ center = parent_rect.center()
261
+
262
+ # 计算节点右上角的偏移位置
263
+ offset_x = parent_rect.width() / 2 - self._radius
264
+ offset_y = -parent_rect.height() / 2 + self._radius
265
+
266
+ # 设置圆形背景位置
267
+ ellipse_rect = QRectF(
268
+ center.x() + offset_x - self._radius,
269
+ center.y() + offset_y - self._radius,
270
+ self._radius * 2,
271
+ self._radius * 2
272
+ )
273
+ self._ellipse_item.setRect(ellipse_rect)
274
+
275
+ # 设置文本居中
276
+ text_rect = self._text_item.boundingRect()
277
+ text_pos = QPointF(
278
+ center.x() + offset_x - text_rect.width() / 2,
279
+ center.y() + offset_y - text_rect.height() / 2
280
+ )
281
+ self._text_item.setPos(text_pos)
282
+ except RuntimeError:
283
+ pass
284
+
285
+ def setRadius(self, radius: float) -> None:
286
+ """设置标记半径"""
287
+ self._radius = radius
288
+ self._updatePosition()
289
+
290
+ def show(self) -> None:
291
+ """显示标记(带动画)"""
292
+ if not self._isValid():
293
+ return
294
+ try:
295
+ self._updatePosition()
296
+ self._ellipse_item.show()
297
+ self._text_item.show()
298
+ except RuntimeError:
299
+ pass
300
+
301
+ def hide(self) -> None:
302
+ """隐藏标记"""
303
+ if not self._isValid():
304
+ return
305
+ try:
306
+ self._ellipse_item.hide()
307
+ self._text_item.hide()
308
+ except RuntimeError:
309
+ pass
310
+
311
+
312
+ class SingleMarker(QObject):
313
+ """
314
+ Single标记 - 用于在节点左下角显示当前执行状态
315
+ 包含圆形背景和⚡图标(表示正在执行)
316
+ """
317
+
318
+ def __init__(self, parent: QGraphicsItem = None) -> None:
319
+ super().__init__()
320
+
321
+ self._parent = parent
322
+ self._radius: float = 15.0
323
+
324
+ # 创建圆形背景(橙黄色 - 表示活跃/执行中)
325
+ self._ellipse_item = QGraphicsEllipseItem(parent)
326
+ self._ellipse_item.setBrush(QBrush(QColor(255, 200, 100))) # 橙黄色背景
327
+ self._ellipse_item.setPen(QPen(QColor(200, 150, 50), 2)) # 深橙色描边,2像素宽
328
+ self._ellipse_item.setZValue(Z_VALUE_MARKER) # 确保在最上层
329
+ self._ellipse_item.setAcceptHoverEvents(False)
330
+ self._ellipse_item.setToolTip("正在执行") # 添加 tooltip
331
+
332
+ # 创建图标文本(⚡)
333
+ self._text_item = QGraphicsTextItem("⚡", parent)
334
+ self._text_item.setZValue(Z_VALUE_MARKER + 1) # 确保在圆形背景之上
335
+ self._text_item.setAcceptHoverEvents(False)
336
+ self._text_item.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
337
+
338
+ # 初始隐藏
339
+ self._opacity: float = 0.0
340
+ self._updateOpacity()
341
+
342
+ # 设置标记位置(左下角)
343
+ self._updatePosition()
344
+
345
+ def _isValid(self) -> bool:
346
+ """检查 C++ 对象是否仍然有效"""
347
+ if not hasattr(self, '_ellipse_item') or self._ellipse_item is None:
348
+ return False
349
+ try:
350
+ self._ellipse_item.isVisible()
351
+ return True
352
+ except RuntimeError:
353
+ return False
354
+
355
+ def _updateOpacity(self) -> None:
356
+ """更新透明度"""
357
+ if not self._isValid():
358
+ return
359
+ try:
360
+ self._ellipse_item.setOpacity(self._opacity)
361
+ self._text_item.setOpacity(self._opacity)
362
+ except RuntimeError:
363
+ pass
364
+
365
+ def getOpacity(self) -> float:
366
+ """获取当前透明度"""
367
+ return self._opacity
368
+
369
+ def setOpacity(self, value: float) -> None:
370
+ """设置透明度"""
371
+ self._opacity = value
372
+ self._updateOpacity()
373
+
374
+ # 创建属性用于动画
375
+ opacity = pyqtProperty(float, getOpacity, setOpacity)
376
+
377
+ def _updatePosition(self) -> None:
378
+ """更新标记位置(节点左下角)"""
379
+ if not self._isValid():
380
+ return
381
+ if self._parent is None:
382
+ return
383
+
384
+ try:
385
+ parent_rect = self._parent.rect()
386
+ center = parent_rect.center()
387
+
388
+ # 计算节点左下角的偏移位置
389
+ offset_x = -parent_rect.width() / 2 + self._radius
390
+ offset_y = parent_rect.height() / 2 - self._radius
391
+
392
+ # 设置圆形背景位置
393
+ ellipse_rect = QRectF(
394
+ center.x() + offset_x - self._radius,
395
+ center.y() + offset_y - self._radius,
396
+ self._radius * 2,
397
+ self._radius * 2
398
+ )
399
+ self._ellipse_item.setRect(ellipse_rect)
400
+
401
+ # 设置文本居中
402
+ text_rect = self._text_item.boundingRect()
403
+ text_pos = QPointF(
404
+ center.x() + offset_x - text_rect.width() / 2,
405
+ center.y() + offset_y - text_rect.height() / 2
406
+ )
407
+ self._text_item.setPos(text_pos)
408
+ except RuntimeError:
409
+ pass
410
+
411
+ def setRadius(self, radius: float) -> None:
412
+ """设置标记半径"""
413
+ self._radius = radius
414
+ self._updatePosition()
415
+
416
+ def show(self) -> None:
417
+ """显示标记(带动画)"""
418
+ if not self._isValid():
419
+ return
420
+ try:
421
+ self._updatePosition()
422
+ self._ellipse_item.show()
423
+ self._text_item.show()
424
+ except RuntimeError:
425
+ pass
426
+
427
+ def hide(self) -> None:
428
+ """隐藏标记"""
429
+ if not self._isValid():
430
+ return
431
+ try:
432
+ self._ellipse_item.hide()
433
+ self._text_item.hide()
434
+ except RuntimeError:
435
+ pass
436
+
437
+
438
+ class CustomMarker(QObject):
439
+ """
440
+ Custom标记 - 用户自定义标识
441
+ 支持自定义显示字符、背景色、前景色、边框颜色和厚度、半径
442
+ 默认位置在节点右下角
443
+ """
444
+
445
+ # 默认样式值
446
+ DEFAULT_TEXT = "★"
447
+ DEFAULT_BG_COLOR = QColor(227, 242, 253) # 浅蓝色 #E3F2FD
448
+ DEFAULT_FG_COLOR = QColor(21, 101, 192) # 深蓝色 #1565C0
449
+ DEFAULT_BORDER_COLOR = QColor(100, 181, 246) # 中蓝色 #64B5F6
450
+ DEFAULT_BORDER_WIDTH = 2
451
+ DEFAULT_RADIUS = 15.0
452
+
453
+ def __init__(self, parent: QGraphicsItem = None) -> None:
454
+ super().__init__()
455
+
456
+ self._parent = parent
457
+ self._radius: float = self.DEFAULT_RADIUS
458
+ self._text: str = self.DEFAULT_TEXT
459
+ self._bg_color: QColor = self.DEFAULT_BG_COLOR
460
+ self._fg_color: QColor = self.DEFAULT_FG_COLOR
461
+ self._border_color: QColor = self.DEFAULT_BORDER_COLOR
462
+ self._border_width: float = self.DEFAULT_BORDER_WIDTH
463
+
464
+ # 创建圆形背景
465
+ self._ellipse_item = QGraphicsEllipseItem(parent)
466
+ self._ellipse_item.setBrush(QBrush(self._bg_color))
467
+ self._ellipse_item.setPen(QPen(self._border_color, self._border_width))
468
+ self._ellipse_item.setZValue(Z_VALUE_MARKER)
469
+ self._ellipse_item.setAcceptHoverEvents(False)
470
+ self._ellipse_item.setToolTip("自定义标记")
471
+
472
+ # 创建图标文本
473
+ self._text_item = QGraphicsTextItem(self._text, parent)
474
+ self._text_item.setDefaultTextColor(self._fg_color)
475
+ self._text_item.setZValue(Z_VALUE_MARKER + 1)
476
+ self._text_item.setAcceptHoverEvents(False)
477
+ self._text_item.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
478
+
479
+ # 初始隐藏
480
+ self._opacity: float = 0.0
481
+ self._updateOpacity()
482
+
483
+ # 设置标记位置(右下角)
484
+ self._updatePosition()
485
+
486
+ def _isValid(self) -> bool:
487
+ """检查 C++ 对象是否仍然有效"""
488
+ if not hasattr(self, '_ellipse_item') or self._ellipse_item is None:
489
+ return False
490
+ try:
491
+ self._ellipse_item.isVisible()
492
+ return True
493
+ except RuntimeError:
494
+ return False
495
+
496
+ def _updateOpacity(self) -> None:
497
+ """更新透明度"""
498
+ if not self._isValid():
499
+ return
500
+ try:
501
+ self._ellipse_item.setOpacity(self._opacity)
502
+ self._text_item.setOpacity(self._opacity)
503
+ except RuntimeError:
504
+ pass
505
+
506
+ def getOpacity(self) -> float:
507
+ """获取当前透明度"""
508
+ return self._opacity
509
+
510
+ def setOpacity(self, value: float) -> None:
511
+ """设置透明度"""
512
+ self._opacity = value
513
+ self._updateOpacity()
514
+
515
+ # 创建属性用于动画
516
+ opacity = pyqtProperty(float, getOpacity, setOpacity)
517
+
518
+ def setText(self, text: str) -> None:
519
+ """设置显示字符"""
520
+ self._text = text if text else self.DEFAULT_TEXT
521
+ if self._isValid():
522
+ self._text_item.setPlainText(self._text)
523
+ self._updatePosition()
524
+
525
+ def setBgColor(self, color: QColor) -> None:
526
+ """设置背景色"""
527
+ self._bg_color = color if color else self.DEFAULT_BG_COLOR
528
+ if self._isValid():
529
+ self._ellipse_item.setBrush(QBrush(self._bg_color))
530
+
531
+ def setFgColor(self, color: QColor) -> None:
532
+ """设置前景色(文本颜色)"""
533
+ self._fg_color = color if color else self.DEFAULT_FG_COLOR
534
+ if self._isValid():
535
+ self._text_item.setDefaultTextColor(self._fg_color)
536
+
537
+ def setBorderColor(self, color: QColor) -> None:
538
+ """设置边框颜色"""
539
+ self._border_color = color if color else self.DEFAULT_BORDER_COLOR
540
+ if self._isValid():
541
+ self._ellipse_item.setPen(QPen(self._border_color, self._border_width))
542
+
543
+ def setBorderWidth(self, width: float) -> None:
544
+ """设置边框厚度"""
545
+ self._border_width = width if width > 0 else self.DEFAULT_BORDER_WIDTH
546
+ if self._isValid():
547
+ self._ellipse_item.setPen(QPen(self._border_color, self._border_width))
548
+
549
+ def setRadius(self, radius: float) -> None:
550
+ """设置标记半径"""
551
+ self._radius = radius if radius > 0 else self.DEFAULT_RADIUS
552
+ self._updatePosition()
553
+
554
+ def setStyle(
555
+ self,
556
+ text: str = None,
557
+ bg_color: QColor = None,
558
+ fg_color: QColor = None,
559
+ border_color: QColor = None,
560
+ border_width: float = None,
561
+ radius: float = None
562
+ ) -> None:
563
+ """批量设置样式"""
564
+ if text is not None:
565
+ self._text = text
566
+ if bg_color is not None:
567
+ self._bg_color = bg_color
568
+ if fg_color is not None:
569
+ self._fg_color = fg_color
570
+ if border_color is not None:
571
+ self._border_color = border_color
572
+ if border_width is not None:
573
+ self._border_width = border_width
574
+ if radius is not None:
575
+ self._radius = radius
576
+
577
+ if self._isValid():
578
+ self._text_item.setPlainText(self._text)
579
+ self._text_item.setDefaultTextColor(self._fg_color)
580
+ self._ellipse_item.setBrush(QBrush(self._bg_color))
581
+ self._ellipse_item.setPen(QPen(self._border_color, self._border_width))
582
+ self._updatePosition()
583
+
584
+ def getStyle(self) -> dict:
585
+ """获取当前样式配置"""
586
+ return {
587
+ "text": self._text,
588
+ "bg_color": self._bg_color.name(),
589
+ "fg_color": self._fg_color.name(),
590
+ "border_color": self._border_color.name(),
591
+ "border_width": self._border_width,
592
+ "radius": self._radius
593
+ }
594
+
595
+ def _updatePosition(self) -> None:
596
+ """更新标记位置(节点右下角)"""
597
+ if not self._isValid():
598
+ return
599
+ if self._parent is None:
600
+ return
601
+
602
+ try:
603
+ parent_rect = self._parent.rect()
604
+ center = parent_rect.center()
605
+
606
+ # 计算节点右下角的偏移位置
607
+ offset_x = parent_rect.width() / 2 - self._radius
608
+ offset_y = parent_rect.height() / 2 - self._radius
609
+
610
+ # 设置圆形背景位置
611
+ ellipse_rect = QRectF(
612
+ center.x() + offset_x - self._radius,
613
+ center.y() + offset_y - self._radius,
614
+ self._radius * 2,
615
+ self._radius * 2
616
+ )
617
+ self._ellipse_item.setRect(ellipse_rect)
618
+
619
+ # 设置文本居中
620
+ text_rect = self._text_item.boundingRect()
621
+ text_pos = QPointF(
622
+ center.x() + offset_x - text_rect.width() / 2,
623
+ center.y() + offset_y - text_rect.height() / 2
624
+ )
625
+ self._text_item.setPos(text_pos)
626
+ except RuntimeError:
627
+ pass
628
+
629
+ def show(self) -> None:
630
+ """显示标记(带动画)"""
631
+ if not self._isValid():
632
+ return
633
+ try:
634
+ self._updatePosition()
635
+ self._ellipse_item.show()
636
+ self._text_item.show()
637
+ except RuntimeError:
638
+ pass
639
+
640
+ def hide(self) -> None:
641
+ """隐藏标记"""
642
+ if not self._isValid():
643
+ return
644
+ try:
645
+ self._ellipse_item.hide()
646
+ self._text_item.hide()
647
+ except RuntimeError:
648
+ pass