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,488 @@
1
+ """
2
+ 节点交互 Mixin 模块
3
+
4
+ 定义 _NodeInteractionMixin —— hover/鼠标/名称编辑/选中/事件/连线更新、
5
+ 标记(start/single/custom)与悬浮按钮(hover button)动画相关行为。
6
+ 本类不含 __init__, 通过覆写 _NodeCore._init_interaction / _init_markers
7
+ 钩子创建相应属性。
8
+
9
+ 依赖关系: _NodeInteractionMixin 不继承 _NodeCore(避免循环导入与 MRO 问题);
10
+ 运行时由 NodeItem(_NodePortMixin, _NodeInteractionMixin, _NodeCore) 组合,
11
+ 所有 self.xxx 调用经 MRO 在 _NodeCore/_NodePortMixin/NodeItem 上解析。
12
+ 标记辅助类从 _node_markers 导入(独立模块, 打破循环)。
13
+ """
14
+
15
+ from typing import Optional
16
+
17
+ from PyQt6.QtCore import Qt, QPointF, QPropertyAnimation, QEasingCurve
18
+ from PyQt6.QtGui import QColor
19
+ from PyQt6.QtWidgets import QGraphicsItem
20
+
21
+ from chartflow.node.enums import NodeState
22
+ from chartflow.node.connection_item import ConnectionItem
23
+ from chartflow.node._node_markers import (
24
+ HoverButton, StartMarker, SingleMarker, CustomMarker,
25
+ )
26
+ from chartflow.node._node_base import (
27
+ BUTTON_RADIUS_RATIO,
28
+ ANIMATION_DURATION_SHOW,
29
+ ANIMATION_DURATION_HIDE,
30
+ HOVER_MOVE_THRESHOLD_SQ,
31
+ )
32
+
33
+
34
+ class _NodeInteractionMixin:
35
+ """交互 Mixin
36
+
37
+ 承载:
38
+ - hover 事件: hoverEnterEvent/hoverMoveEvent/hoverLeaveEvent/_handleHoverAnchorMove
39
+ - 鼠标事件: mousePressEvent/itemChange/eventFilter
40
+ - 名称编辑: _startNameEdit/_finishNameEdit/isEditingName
41
+ - 连线更新: _updateConnections
42
+ - 悬浮按钮: setEditWindowOpen/_showHoverButtonImmediately/_hideHoverButton/
43
+ _onHideAnimationFinished/_onHoverButtonClicked
44
+ - 标记: setStartMarkerVisible/setSingleMarkerVisible/setCustomMarkerVisible/
45
+ setCustomMarkerStyle/getCustomMarkerStyle 及 *_HideFinished 回调
46
+ """
47
+
48
+ # ------------------------------------------------------------------ #
49
+ # 初始化钩子(覆写 _NodeCore._init_interaction / _init_markers)
50
+ # ------------------------------------------------------------------ #
51
+ def _init_interaction(self) -> None:
52
+ """创建悬浮按钮及其动画属性"""
53
+ # 悬浮按钮在 __init__ 后续步骤中创建并连接信号; 此处仅声明状态标志
54
+ self._is_button_visible: bool = False
55
+ self._edit_window_open: bool = False # 编辑窗口是否打开
56
+ # 占位, 由 _setup_overlays 在 QGraphicsItem 初始化完成后创建
57
+ self._hover_button: Optional[HoverButton] = None
58
+ self._opacity_animation: Optional[QPropertyAnimation] = None
59
+
60
+ def _setup_overlays(self) -> None:
61
+ """创建悬浮按钮(以本节点为父项)并连接信号/动画"""
62
+ # 悬浮按钮(左上角的编辑按钮)
63
+ self._hover_button = HoverButton(self)
64
+ self._hover_button.clicked.connect(self._onHoverButtonClicked)
65
+
66
+ # 透明度动画
67
+ self._opacity_animation = QPropertyAnimation(self._hover_button, b"opacity")
68
+ self._opacity_animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
69
+
70
+ def _init_markers(self) -> None:
71
+ """创建标记(start/single/custom)占位属性(懒创建)"""
72
+ self._start_marker: Optional[StartMarker] = None
73
+ self._is_start_marker_visible: bool = False
74
+ self._start_marker_animation: Optional[QPropertyAnimation] = None
75
+
76
+ self._single_marker: Optional[SingleMarker] = None
77
+ self._is_single_marker_visible: bool = False
78
+ self._single_marker_animation: Optional[QPropertyAnimation] = None
79
+
80
+ self._custom_marker: Optional[CustomMarker] = None
81
+ self._is_custom_marker_visible: bool = False
82
+ self._custom_marker_animation: Optional[QPropertyAnimation] = None
83
+
84
+ # ------------------------------------------------------------------ #
85
+ # hover 事件
86
+ # ------------------------------------------------------------------ #
87
+ def hoverEnterEvent(self, event) -> None:
88
+ """鼠标进入事件"""
89
+ if self._state != NodeState.SELECTED:
90
+ self._state = NodeState.HOVER
91
+ self._applyStyle()
92
+
93
+ # 进入节点本体: 计时显示全部端口 label(悬停 dot 时不触发);
94
+ # 记录锚点用于判断鼠标是否静止
95
+ self._node_hovered = True
96
+ self._hover_anchor = event.scenePos()
97
+ self._maybeStartPortShowAll()
98
+ super().hoverEnterEvent(event)
99
+
100
+ def hoverMoveEvent(self, event) -> None:
101
+ """鼠标移动: 若在节点本体上移动超过阈值, 则打断"显示全部"并重置等待"""
102
+ if self._node_hovered and self._hovered_port_count == 0:
103
+ self._handleHoverAnchorMove(event.scenePos())
104
+ super().hoverMoveEvent(event)
105
+
106
+ def _handleHoverAnchorMove(self, pos: QPointF) -> None:
107
+ """根据鼠标位置判断是否"移动": 超过阈值则打断显示全部并重置等待"""
108
+ anchor = self._hover_anchor
109
+ if anchor is None:
110
+ self._hover_anchor = QPointF(pos)
111
+ return
112
+ dx = pos.x() - anchor.x()
113
+ dy = pos.y() - anchor.y()
114
+ if dx * dx + dy * dy > HOVER_MOVE_THRESHOLD_SQ:
115
+ # 移动打断: 隐藏已显示的全部 label, 重新计时, 更新锚点
116
+ self._setAllPortsShowAll(False)
117
+ self._port_label_timer.start()
118
+ self._hover_anchor = QPointF(pos)
119
+
120
+ def hoverLeaveEvent(self, event) -> None:
121
+ """鼠标离开事件"""
122
+ if self._state != NodeState.SELECTED:
123
+ self._state = NodeState.IDLE
124
+ self._applyStyle()
125
+
126
+ # 离开节点: 取消计时, 隐藏全部端口 label 并清除 dot 的 hover 描边
127
+ self._node_hovered = False
128
+ self._hover_anchor = None
129
+ self._port_label_timer.stop()
130
+ self._resetPortHover()
131
+ super().hoverLeaveEvent(event)
132
+
133
+ # ------------------------------------------------------------------ #
134
+ # 鼠标事件
135
+ # ------------------------------------------------------------------ #
136
+ def mousePressEvent(self, event) -> None:
137
+ """鼠标按下事件"""
138
+ # 检查是否点击了悬浮按钮
139
+ if self._is_button_visible and self._hover_button.mousePressEvent(event):
140
+ return
141
+
142
+ # 检查是否点击了名称编辑区域(开始编辑名称)
143
+ if event.button() == Qt.MouseButton.LeftButton:
144
+ # 将场景坐标转换为本地坐标
145
+ local_pos = self.mapFromScene(event.scenePos())
146
+ if self._name_edit_area.contains(local_pos):
147
+ self._startNameEdit()
148
+ return
149
+
150
+ super().mousePressEvent(event)
151
+
152
+ def mouseMoveEvent(self, event) -> None:
153
+ """拖动节点时: 立即隐藏全部端口 label, 并停止'显示全部'计时。
154
+
155
+ 鼠标按下后 Qt 进入 mouse grab, hoverMoveEvent 不再派发, 端口 label 的
156
+ '静止 0.5s 显示'判定在此期间失效。此处显式打断: 只要开始拖动, 已显示的
157
+ 端口 label 立即隐藏, 且拖动期间不再重新计时(松开鼠标后才恢复)。
158
+ 判定仍以场景(画布)坐标为准, 与 hoverMoveEvent 一致。
159
+ """
160
+ if self._node_hovered and self._hovered_port_count == 0:
161
+ self._port_label_timer.stop()
162
+ self._setAllPortsShowAll(False)
163
+ self._hover_anchor = QPointF(event.scenePos())
164
+ super().mouseMoveEvent(event)
165
+
166
+ def mouseReleaseEvent(self, event) -> None:
167
+ """松开鼠标: 若仍在节点本体上, 恢复'静止 0.5s 显示全部'计时。"""
168
+ super().mouseReleaseEvent(event)
169
+ if self._node_hovered and self._hovered_port_count == 0:
170
+ self._hover_anchor = QPointF(event.scenePos())
171
+ self._port_label_timer.start()
172
+
173
+ def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value):
174
+ """项状态改变事件"""
175
+ if change == QGraphicsItem.GraphicsItemChange.ItemSelectedChange:
176
+ if value:
177
+ self._state = NodeState.SELECTED
178
+ else:
179
+ self._state = NodeState.IDLE
180
+ self._applyStyle()
181
+ elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
182
+ # 位置已改变,更新连接
183
+ if self.scene():
184
+ self._updateConnections()
185
+
186
+ return super().itemChange(change, value)
187
+
188
+ def _updateConnections(self) -> None:
189
+ """更新与此节点相关的所有连接"""
190
+ if not self.scene():
191
+ return
192
+
193
+ for item in self.scene().items():
194
+ if isinstance(item, ConnectionItem):
195
+ if item.sourceNode == self or item.targetNode == self:
196
+ item.updatePath()
197
+
198
+ # ------------------------------------------------------------------ #
199
+ # 名称编辑
200
+ # ------------------------------------------------------------------ #
201
+ def _startNameEdit(self) -> None:
202
+ """开始编辑节点名称 - 使用QGraphicsTextItem原生编辑,完全贴合文本"""
203
+ if self._is_editing_name:
204
+ return
205
+
206
+ self._is_editing_name = True
207
+
208
+ # 启用文本项的编辑功能(原位编辑,完全贴合文本)
209
+ self._text_item.setTextInteractionFlags(Qt.TextInteractionFlag.TextEditorInteraction)
210
+ self._text_item.setFocus()
211
+
212
+ # 选中全部文本
213
+ cursor = self._text_item.textCursor()
214
+ cursor.select(cursor.SelectionType.Document)
215
+ self._text_item.setTextCursor(cursor)
216
+
217
+ def _finishNameEdit(self) -> None:
218
+ """完成名称编辑"""
219
+ if not self._is_editing_name:
220
+ return
221
+
222
+ # 获取新名称(从文本项)
223
+ new_name = self._text_item.toPlainText().strip()
224
+
225
+ # 禁用文本项的编辑功能
226
+ self._text_item.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
227
+
228
+ # 更新名称(这会重新居中并更新虚线框)
229
+ if new_name and new_name != self._name:
230
+ self.name = new_name
231
+
232
+ # 清除文本选中和焦点,避免全选高亮在退出编辑后残留。
233
+ # 注意: 即便上面 setPlainText 已重置光标, 当名称未变化或为空时
234
+ # 旧选区仍会保留, 因此这里必须显式清除。
235
+ cursor = self._text_item.textCursor()
236
+ cursor.clearSelection()
237
+ self._text_item.setTextCursor(cursor)
238
+ self._text_item.clearFocus()
239
+
240
+ self._is_editing_name = False
241
+
242
+ @property
243
+ def isEditingName(self) -> bool:
244
+ """是否正在编辑名称"""
245
+ return self._is_editing_name
246
+
247
+ def eventFilter(self, watched, event) -> bool:
248
+ """事件过滤器 - 检测点击编辑框外部来取消编辑
249
+
250
+ Note: 名称编辑已改为原位编辑 _text_item(无独立代理控件),
251
+ 点击外部取消的逻辑统一在 NodeCanvas.mousePressEvent 中处理,
252
+ 此过滤器当前未被 installEventFilter 安装,保留以兼容外部子类。
253
+ """
254
+ if not self._is_editing_name:
255
+ return False
256
+
257
+ if event.type() == event.Type.GraphicsSceneMousePress:
258
+ # 原位编辑:用 _text_item 的场景边界判断点击是否落在编辑文本内
259
+ edit_item = getattr(self, "_text_item", None)
260
+ if edit_item is not None:
261
+ edit_rect = edit_item.sceneBoundingRect()
262
+ click_pos = event.scenePos()
263
+ if not edit_rect.contains(click_pos):
264
+ self._finishNameEdit()
265
+ return False # 允许事件继续传递
266
+
267
+ return False # 其他事件不处理,继续传递
268
+
269
+ # ------------------------------------------------------------------ #
270
+ # 悬浮按钮(hover button)
271
+ # ------------------------------------------------------------------ #
272
+ def setEditWindowOpen(self, open: bool) -> None:
273
+ """设置编辑窗口是否打开,控制悬浮按钮的显示(带动画)"""
274
+ self._edit_window_open = open
275
+ if open:
276
+ self._showHoverButtonImmediately()
277
+ else:
278
+ self._hideHoverButton()
279
+
280
+ def _showHoverButtonImmediately(self) -> None:
281
+ """显示悬浮按钮(带淡入动画)- 当编辑窗口打开时调用"""
282
+ if not self._edit_window_open:
283
+ return
284
+
285
+ # 计算按钮半径
286
+ rect = self.rect()
287
+ button_radius = BUTTON_RADIUS_RATIO * min(rect.width(), rect.height())
288
+ self._hover_button.setRadius(button_radius)
289
+
290
+ self._hover_button.show()
291
+ self._is_button_visible = True
292
+
293
+ # 淡入动画
294
+ self._opacity_animation.stop()
295
+ self._opacity_animation.setDuration(ANIMATION_DURATION_SHOW)
296
+ self._opacity_animation.setStartValue(self._hover_button.getOpacity())
297
+ self._opacity_animation.setEndValue(1.0)
298
+ self._opacity_animation.start()
299
+
300
+ def _hideHoverButton(self) -> None:
301
+ """隐藏悬浮按钮(带淡出动画)"""
302
+ if not self._is_button_visible:
303
+ return
304
+
305
+ # 淡出动画
306
+ self._opacity_animation.stop()
307
+ self._opacity_animation.setDuration(ANIMATION_DURATION_HIDE)
308
+ self._opacity_animation.setStartValue(self._hover_button.getOpacity())
309
+ self._opacity_animation.setEndValue(0.0)
310
+ self._opacity_animation.finished.connect(self._onHideAnimationFinished)
311
+ self._opacity_animation.start()
312
+
313
+ def _onHideAnimationFinished(self) -> None:
314
+ """隐藏动画完成回调"""
315
+ self._opacity_animation.finished.disconnect(self._onHideAnimationFinished)
316
+ if not self._edit_window_open:
317
+ self._hover_button.hide()
318
+ self._is_button_visible = False
319
+
320
+ def _onHoverButtonClicked(self) -> None:
321
+ """悬浮按钮点击回调"""
322
+ self._signal_helper.editRequested.emit(self)
323
+
324
+ # ------------------------------------------------------------------ #
325
+ # 标记: 起点(Start)
326
+ # ------------------------------------------------------------------ #
327
+ def setStartMarkerVisible(self, visible: bool) -> None:
328
+ """设置起点标记的显示状态"""
329
+ if visible == self._is_start_marker_visible:
330
+ return
331
+
332
+ self._is_start_marker_visible = visible
333
+
334
+ if visible:
335
+ # 创建起点标记
336
+ if self._start_marker is None:
337
+ self._start_marker = StartMarker(self)
338
+ self._start_marker_animation = QPropertyAnimation(self._start_marker, b"opacity")
339
+ self._start_marker_animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
340
+
341
+ # 显示动画
342
+ self._start_marker.show()
343
+ self._start_marker_animation.stop()
344
+ self._start_marker_animation.setDuration(ANIMATION_DURATION_SHOW)
345
+ self._start_marker_animation.setStartValue(self._start_marker.getOpacity())
346
+ self._start_marker_animation.setEndValue(1.0)
347
+ self._start_marker_animation.start()
348
+ else:
349
+ # 隐藏动画
350
+ if self._start_marker is not None:
351
+ self._start_marker_animation.stop()
352
+ self._start_marker_animation.setDuration(ANIMATION_DURATION_HIDE)
353
+ self._start_marker_animation.setStartValue(self._start_marker.getOpacity())
354
+ self._start_marker_animation.setEndValue(0.0)
355
+ self._start_marker_animation.finished.connect(self._onStartMarkerHideFinished)
356
+ self._start_marker_animation.start()
357
+
358
+ def _onStartMarkerHideFinished(self) -> None:
359
+ """起点标记隐藏动画完成回调"""
360
+ self._start_marker_animation.finished.disconnect(self._onStartMarkerHideFinished)
361
+ if not self._is_start_marker_visible and self._start_marker is not None:
362
+ self._start_marker.hide()
363
+
364
+ # ------------------------------------------------------------------ #
365
+ # 标记: Single
366
+ # ------------------------------------------------------------------ #
367
+ def setSingleMarkerVisible(self, visible: bool) -> None:
368
+ """设置 single 标记的显示状态"""
369
+ if visible == self._is_single_marker_visible:
370
+ return
371
+
372
+ self._is_single_marker_visible = visible
373
+
374
+ if visible:
375
+ # 创建 single 标记
376
+ if self._single_marker is None:
377
+ self._single_marker = SingleMarker(self)
378
+ self._single_marker_animation = QPropertyAnimation(self._single_marker, b"opacity")
379
+ self._single_marker_animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
380
+
381
+ # 显示动画
382
+ self._single_marker.show()
383
+ self._single_marker_animation.stop()
384
+ self._single_marker_animation.setDuration(ANIMATION_DURATION_SHOW)
385
+ self._single_marker_animation.setStartValue(self._single_marker.getOpacity())
386
+ self._single_marker_animation.setEndValue(1.0)
387
+ self._single_marker_animation.start()
388
+ else:
389
+ # 隐藏动画
390
+ if self._single_marker is not None:
391
+ self._single_marker_animation.stop()
392
+ self._single_marker_animation.setDuration(ANIMATION_DURATION_HIDE)
393
+ self._single_marker_animation.setStartValue(self._single_marker.getOpacity())
394
+ self._single_marker_animation.setEndValue(0.0)
395
+ self._single_marker_animation.finished.connect(self._onSingleMarkerHideFinished)
396
+ self._single_marker_animation.start()
397
+
398
+ def _onSingleMarkerHideFinished(self) -> None:
399
+ """single 标记隐藏动画完成回调"""
400
+ self._single_marker_animation.finished.disconnect(self._onSingleMarkerHideFinished)
401
+ if not self._is_single_marker_visible and self._single_marker is not None:
402
+ self._single_marker.hide()
403
+
404
+ # ------------------------------------------------------------------ #
405
+ # 标记: Custom
406
+ # ------------------------------------------------------------------ #
407
+ def setCustomMarkerVisible(self, visible: bool) -> None:
408
+ """设置 custom 标记的显示状态"""
409
+ if visible == self._is_custom_marker_visible:
410
+ return
411
+
412
+ self._is_custom_marker_visible = visible
413
+
414
+ if visible:
415
+ # 创建 custom 标记
416
+ if self._custom_marker is None:
417
+ self._custom_marker = CustomMarker(self)
418
+ self._custom_marker_animation = QPropertyAnimation(self._custom_marker, b"opacity")
419
+ self._custom_marker_animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
420
+
421
+ # 显示动画
422
+ self._custom_marker.show()
423
+ self._custom_marker_animation.stop()
424
+ self._custom_marker_animation.setDuration(ANIMATION_DURATION_SHOW)
425
+ self._custom_marker_animation.setStartValue(self._custom_marker.getOpacity())
426
+ self._custom_marker_animation.setEndValue(1.0)
427
+ self._custom_marker_animation.start()
428
+ else:
429
+ # 隐藏动画
430
+ if self._custom_marker is not None:
431
+ self._custom_marker_animation.stop()
432
+ self._custom_marker_animation.setDuration(ANIMATION_DURATION_HIDE)
433
+ self._custom_marker_animation.setStartValue(self._custom_marker.getOpacity())
434
+ self._custom_marker_animation.setEndValue(0.0)
435
+ self._custom_marker_animation.finished.connect(self._onCustomMarkerHideFinished)
436
+ self._custom_marker_animation.start()
437
+
438
+ def _onCustomMarkerHideFinished(self) -> None:
439
+ """custom 标记隐藏动画完成回调"""
440
+ self._custom_marker_animation.finished.disconnect(self._onCustomMarkerHideFinished)
441
+ if not self._is_custom_marker_visible and self._custom_marker is not None:
442
+ self._custom_marker.hide()
443
+
444
+ def setCustomMarkerStyle(
445
+ self,
446
+ text: str = None,
447
+ bg_color: QColor = None,
448
+ fg_color: QColor = None,
449
+ border_color: QColor = None,
450
+ border_width: float = None,
451
+ radius: float = None
452
+ ) -> None:
453
+ """
454
+ 设置 custom 标记的样式
455
+
456
+ Args:
457
+ text: 显示字符
458
+ bg_color: 背景色
459
+ fg_color: 前景色(文本颜色)
460
+ border_color: 边框颜色
461
+ border_width: 边框厚度
462
+ radius: 标记半径
463
+ """
464
+ # 如果标记不存在,先创建它
465
+ if self._custom_marker is None:
466
+ self._custom_marker = CustomMarker(self)
467
+ self._custom_marker_animation = QPropertyAnimation(self._custom_marker, b"opacity")
468
+ self._custom_marker_animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
469
+
470
+ self._custom_marker.setStyle(
471
+ text=text,
472
+ bg_color=bg_color,
473
+ fg_color=fg_color,
474
+ border_color=border_color,
475
+ border_width=border_width,
476
+ radius=radius
477
+ )
478
+
479
+ def getCustomMarkerStyle(self) -> dict:
480
+ """
481
+ 获取 custom 标记的当前样式配置
482
+
483
+ Returns:
484
+ 包含样式的字典,如果标记未创建则返回空字典
485
+ """
486
+ if self._custom_marker is None:
487
+ return {}
488
+ return self._custom_marker.getStyle()