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,53 @@
1
+ """
2
+ 框选矩形模块
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ from PyQt6.QtWidgets import QGraphicsItem, QGraphicsRectItem
8
+ from PyQt6.QtCore import Qt
9
+ from PyQt6.QtGui import QBrush, QPen, QColor
10
+
11
+
12
+ class SelectionRect(QGraphicsRectItem):
13
+ """框选矩形"""
14
+
15
+ def __init__(self, parent: Optional[QGraphicsItem] = None) -> None:
16
+ super().__init__(parent)
17
+
18
+ # 设置样式(半透明蓝色)
19
+ self._fill_color = QColor(0, 120, 215, 50)
20
+ self._border_color = QColor(0, 120, 215, 200)
21
+ self._border_width = 1
22
+
23
+ self.setBrush(QBrush(self._fill_color))
24
+ self.setPen(QPen(self._border_color, self._border_width, Qt.PenStyle.DashLine))
25
+
26
+ # 设置Z值,确保在最上层
27
+ self.setZValue(1000)
28
+
29
+ def setColor(self, color: QColor) -> None:
30
+ """
31
+ 设置框选矩形的填充颜色
32
+
33
+ Args:
34
+ color: 填充颜色
35
+ """
36
+ self._fill_color = color
37
+ self.setBrush(QBrush(self._fill_color))
38
+
39
+ def setBorder(self, width: int = 1, color: QColor = None) -> None:
40
+ """
41
+ 设置框选矩形的边框宽度和颜色
42
+
43
+ Args:
44
+ width: 边框宽度,<=0 表示无边框
45
+ color: 边框颜色,None 表示使用默认颜色
46
+ """
47
+ if width <= 0:
48
+ self.setPen(QPen(Qt.PenStyle.NoPen))
49
+ else:
50
+ self._border_width = width
51
+ if color is not None:
52
+ self._border_color = color
53
+ self.setPen(QPen(self._border_color, self._border_width, Qt.PenStyle.DashLine))
@@ -0,0 +1,615 @@
1
+ """
2
+ 样式面板模块 - 用于编辑选中节点的样式
3
+ 贴合在canvas下方,支持多节点样式编辑
4
+ """
5
+
6
+ from typing import List, Optional
7
+ from PyQt6.QtWidgets import (
8
+ QWidget, QHBoxLayout, QVBoxLayout, QLabel, QPushButton,
9
+ QComboBox, QSpinBox, QSlider, QColorDialog,
10
+ QFrame, QSizePolicy
11
+ )
12
+ from PyQt6.QtCore import Qt, pyqtSignal
13
+ from PyQt6.QtGui import QColor, QFontDatabase
14
+
15
+ from chartflow.node.node_item import QNodeItem
16
+ from chartflow.theme import PALETTE
17
+
18
+
19
+ class ColorButton(QPushButton):
20
+ """颜色选择按钮 - 圆形带颜色显示"""
21
+
22
+ def __init__(self, parent=None, color: QColor = QColor(128, 128, 128)):
23
+ super().__init__(parent)
24
+ self._color = color
25
+ self.setFixedSize(24, 24)
26
+ self.setCursor(Qt.CursorShape.PointingHandCursor)
27
+ self._updateStyle()
28
+
29
+ def _updateStyle(self):
30
+ """更新按钮样式"""
31
+ self.setStyleSheet(f"""
32
+ ColorButton {{
33
+ background-color: {self._color.name()};
34
+ border: 2px solid #c0c0c0;
35
+ border-radius: 12px;
36
+ }}
37
+ ColorButton:hover {{
38
+ border: 2px solid #808080;
39
+ }}
40
+ ColorButton:pressed {{
41
+ border: 2px solid #404040;
42
+ }}
43
+ """)
44
+
45
+ def setColor(self, color: QColor):
46
+ """设置按钮显示的颜色"""
47
+ self._color = color
48
+ self._updateStyle()
49
+
50
+ def getColor(self) -> QColor:
51
+ """获取当前颜色"""
52
+ return self._color
53
+
54
+
55
+ class StylePanel(QWidget):
56
+ """
57
+ 样式面板 - 用于编辑选中节点的样式
58
+ 当画布上有节点被选中时显示,贴合在canvas下方
59
+ """
60
+
61
+ # 信号定义
62
+ styleChanged = pyqtSignal() # 样式改变信号
63
+
64
+ # 面板高度(两行布局)—— 供 NodeCanvas 定位时引用,避免魔法数字
65
+ PANEL_HEIGHT: int = 88
66
+
67
+ def __init__(self, parent: Optional[QWidget] = None) -> None:
68
+ """
69
+ 初始化样式面板
70
+
71
+ Args:
72
+ parent: 父窗口(通常是NodeCanvas)
73
+ """
74
+ super().__init__(parent)
75
+
76
+ self._selected_nodes: List[QNodeItem] = []
77
+ self._is_updating_ui: bool = False
78
+
79
+ self._setupUI()
80
+ self._connectSignals()
81
+ self.hide()
82
+
83
+ def _setupUI(self) -> None:
84
+ """设置UI组件 —— 两行布局:第一行(字体+填充色),第二行(边框+大小)"""
85
+ # 主布局:纵向,容纳两行
86
+ main_layout = QVBoxLayout(self)
87
+ main_layout.setContentsMargins(15, 8, 15, 8)
88
+ main_layout.setSpacing(6)
89
+
90
+ # 第一行:字体 + 填充色
91
+ row1 = QHBoxLayout()
92
+ row1.setSpacing(12)
93
+ row1.addWidget(self._createFontGroup())
94
+ row1.addWidget(self._createSeparator())
95
+ row1.addWidget(self._createFillColorGroup())
96
+ row1.addStretch()
97
+ main_layout.addLayout(row1)
98
+
99
+ # 第二行:边框 + 大小
100
+ row2 = QHBoxLayout()
101
+ row2.setSpacing(12)
102
+ row2.addWidget(self._createBorderGroup())
103
+ row2.addWidget(self._createSeparator())
104
+ row2.addWidget(self._createSizeGroup())
105
+ row2.addStretch()
106
+ main_layout.addLayout(row2)
107
+
108
+ # 设置面板样式 —— 与全局暗色主题保持一致
109
+ self.setStyleSheet(f"""
110
+ /* 面板整体样式 */
111
+ StylePanel {{
112
+ background-color: {PALETTE.panel};
113
+ border-top: 1px solid {PALETTE.border};
114
+ border-bottom: 1px solid {PALETTE.border};
115
+ }}
116
+
117
+ /* 标签样式 */
118
+ QLabel {{
119
+ color: {PALETTE.text};
120
+ font-size: 12px;
121
+ font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
122
+ background: transparent;
123
+ }}
124
+
125
+ /* 下拉框样式 */
126
+ QComboBox {{
127
+ padding: 4px 8px;
128
+ border: 1px solid {PALETTE.border};
129
+ background-color: {PALETTE.input};
130
+ color: {PALETTE.text};
131
+ border-radius: 4px;
132
+ min-height: 22px;
133
+ font-size: 12px;
134
+ }}
135
+ QComboBox:hover {{
136
+ border: 1px solid {PALETTE.accent};
137
+ }}
138
+ QComboBox:focus {{
139
+ border: 1px solid {PALETTE.accent};
140
+ }}
141
+ QComboBox::drop-down {{
142
+ border: none;
143
+ width: 20px;
144
+ }}
145
+ QComboBox::down-arrow {{
146
+ image: none;
147
+ border-left: 4px solid transparent;
148
+ border-right: 4px solid transparent;
149
+ border-top: 5px solid {PALETTE.text_dim};
150
+ width: 0;
151
+ height: 0;
152
+ }}
153
+ QComboBox QAbstractItemView {{
154
+ background-color: {PALETTE.panel};
155
+ color: {PALETTE.text};
156
+ border: 1px solid {PALETTE.border};
157
+ border-radius: 4px;
158
+ selection-background-color: {PALETTE.accent_soft};
159
+ selection-color: {PALETTE.accent_hover};
160
+ outline: none;
161
+ }}
162
+
163
+ /* 数字输入框样式 */
164
+ QSpinBox {{
165
+ padding: 4px 8px;
166
+ border: 1px solid {PALETTE.border};
167
+ background-color: {PALETTE.input};
168
+ color: {PALETTE.text};
169
+ border-radius: 4px;
170
+ min-height: 22px;
171
+ font-size: 12px;
172
+ }}
173
+ QSpinBox:hover {{
174
+ border: 1px solid {PALETTE.accent};
175
+ }}
176
+ QSpinBox:focus {{
177
+ border: 1px solid {PALETTE.accent};
178
+ }}
179
+ QSpinBox::up-button, QSpinBox::down-button {{
180
+ width: 16px;
181
+ border: none;
182
+ background: {PALETTE.surface};
183
+ }}
184
+ QSpinBox::up-button:hover, QSpinBox::down-button:hover {{
185
+ background: {PALETTE.accent_soft};
186
+ }}
187
+ QSpinBox::up-button {{
188
+ border-bottom: 1px solid {PALETTE.border};
189
+ }}
190
+
191
+ /* 滑块样式 */
192
+ QSlider::groove:horizontal {{
193
+ height: 4px;
194
+ background: {PALETTE.border};
195
+ border-radius: 2px;
196
+ }}
197
+ QSlider::sub-page:horizontal {{
198
+ background: {PALETTE.accent};
199
+ border-radius: 2px;
200
+ }}
201
+ QSlider::handle:horizontal {{
202
+ width: 14px;
203
+ height: 14px;
204
+ margin: -5px 0;
205
+ background: {PALETTE.surface};
206
+ border: 2px solid {PALETTE.accent};
207
+ border-radius: 7px;
208
+ }}
209
+ QSlider::handle:horizontal:hover {{
210
+ background: {PALETTE.accent_hover};
211
+ }}
212
+
213
+ /* 增量按钮样式 (A+, A-, B+, B-, R+, R-) */
214
+ QPushButton#incrementBtn {{
215
+ padding: 2px 6px;
216
+ border: 1px solid {PALETTE.border};
217
+ background-color: {PALETTE.surface};
218
+ color: {PALETTE.text};
219
+ border-radius: 4px;
220
+ font-size: 11px;
221
+ font-weight: bold;
222
+ min-width: 24px;
223
+ min-height: 22px;
224
+ }}
225
+ QPushButton#incrementBtn:hover {{
226
+ background-color: {PALETTE.accent_soft};
227
+ border: 1px solid {PALETTE.accent};
228
+ color: {PALETTE.accent_hover};
229
+ }}
230
+ QPushButton#incrementBtn:pressed {{
231
+ background-color: {PALETTE.accent_pressed};
232
+ color: #ffffff;
233
+ }}
234
+
235
+ /* 颜色按钮基础样式 */
236
+ ColorButton {{
237
+ border-radius: 12px;
238
+ }}
239
+ """)
240
+
241
+ def _createFontGroup(self) -> QFrame:
242
+ """创建字体选择区域"""
243
+ frame = QFrame()
244
+ layout = QHBoxLayout(frame)
245
+ layout.setContentsMargins(0, 0, 0, 0)
246
+ layout.setSpacing(8)
247
+
248
+ # 字体选择
249
+ font_label = QLabel("字体:")
250
+ font_label.setStyleSheet("font-weight: bold;")
251
+ layout.addWidget(font_label)
252
+
253
+ self._font_combo = QComboBox()
254
+ self._font_combo.setFixedWidth(110)
255
+ fonts = QFontDatabase.families()
256
+ self._font_combo.addItem("-")
257
+ self._font_combo.addItems(fonts[:20]) # 只添加前20个常用字体
258
+ self._font_combo.setCurrentIndex(0)
259
+ layout.addWidget(self._font_combo)
260
+
261
+ # 字号
262
+ size_label = QLabel("字号:")
263
+ layout.addWidget(size_label)
264
+
265
+ self._font_size_spin = QSpinBox()
266
+ self._font_size_spin.setRange(6, 72)
267
+ self._font_size_spin.setValue(12)
268
+ self._font_size_spin.setFixedWidth(50)
269
+ self._font_size_spin.setSpecialValueText("-")
270
+ layout.addWidget(self._font_size_spin)
271
+
272
+ # A+ A- 按钮
273
+ self._font_size_up_btn = QPushButton("A+")
274
+ self._font_size_up_btn.setObjectName("incrementBtn")
275
+ self._font_size_up_btn.setToolTip("增大字号")
276
+ layout.addWidget(self._font_size_up_btn)
277
+
278
+ self._font_size_down_btn = QPushButton("A-")
279
+ self._font_size_down_btn.setObjectName("incrementBtn")
280
+ self._font_size_down_btn.setToolTip("减小字号")
281
+ layout.addWidget(self._font_size_down_btn)
282
+
283
+ return frame
284
+
285
+ def _createFillColorGroup(self) -> QFrame:
286
+ """创建填充色选择区域"""
287
+ frame = QFrame()
288
+ layout = QHBoxLayout(frame)
289
+ layout.setContentsMargins(0, 0, 0, 0)
290
+ layout.setSpacing(8)
291
+
292
+ label = QLabel("填充色:")
293
+ label.setStyleSheet("font-weight: bold;")
294
+ layout.addWidget(label)
295
+
296
+ self._fill_color_btn = ColorButton(color=QColor(128, 128, 128))
297
+ self._fill_color_btn.setToolTip("点击选择填充颜色")
298
+ layout.addWidget(self._fill_color_btn)
299
+
300
+ return frame
301
+
302
+ def _createBorderGroup(self) -> QFrame:
303
+ """创建边框选择区域"""
304
+ frame = QFrame()
305
+ layout = QHBoxLayout(frame)
306
+ layout.setContentsMargins(0, 0, 0, 0)
307
+ layout.setSpacing(8)
308
+
309
+ # 厚度标签和滑块
310
+ label = QLabel("边框:")
311
+ label.setStyleSheet("font-weight: bold;")
312
+ layout.addWidget(label)
313
+
314
+ self._border_slider = QSlider(Qt.Orientation.Horizontal)
315
+ self._border_slider.setRange(0, 10)
316
+ self._border_slider.setValue(1)
317
+ self._border_slider.setFixedWidth(70)
318
+ layout.addWidget(self._border_slider)
319
+
320
+ # 厚度值显示
321
+ self._border_value_label = QLabel("1")
322
+ self._border_value_label.setFixedWidth(15)
323
+ self._border_value_label.setStyleSheet(f"color: {PALETTE.text_dim};")
324
+ layout.addWidget(self._border_value_label)
325
+
326
+ # B+ B- 按钮
327
+ self._border_up_btn = QPushButton("B+")
328
+ self._border_up_btn.setObjectName("incrementBtn")
329
+ self._border_up_btn.setToolTip("增加边框厚度")
330
+ layout.addWidget(self._border_up_btn)
331
+
332
+ self._border_down_btn = QPushButton("B-")
333
+ self._border_down_btn.setObjectName("incrementBtn")
334
+ self._border_down_btn.setToolTip("减小边框厚度")
335
+ layout.addWidget(self._border_down_btn)
336
+
337
+ # 边框颜色
338
+ self._border_color_btn = ColorButton(color=QColor(51, 51, 51))
339
+ self._border_color_btn.setToolTip("点击选择边框颜色")
340
+ layout.addWidget(self._border_color_btn)
341
+
342
+ return frame
343
+
344
+ def _createSizeGroup(self) -> QFrame:
345
+ """创建大小选择区域"""
346
+ frame = QFrame()
347
+ layout = QHBoxLayout(frame)
348
+ layout.setContentsMargins(0, 0, 0, 0)
349
+ layout.setSpacing(8)
350
+
351
+ label = QLabel("节点大小:")
352
+ label.setStyleSheet("font-weight: bold;")
353
+ layout.addWidget(label)
354
+
355
+ self._size_slider = QSlider(Qt.Orientation.Horizontal)
356
+ self._size_slider.setRange(20, 150)
357
+ self._size_slider.setValue(40)
358
+ self._size_slider.setFixedWidth(90)
359
+ layout.addWidget(self._size_slider)
360
+
361
+ self._size_value_label = QLabel("40")
362
+ self._size_value_label.setFixedWidth(25)
363
+ self._size_value_label.setStyleSheet(f"color: {PALETTE.text_dim};")
364
+ layout.addWidget(self._size_value_label)
365
+
366
+ # R+ R- 按钮
367
+ self._size_up_btn = QPushButton("R+")
368
+ self._size_up_btn.setObjectName("incrementBtn")
369
+ self._size_up_btn.setToolTip("增大节点")
370
+ layout.addWidget(self._size_up_btn)
371
+
372
+ self._size_down_btn = QPushButton("R-")
373
+ self._size_down_btn.setObjectName("incrementBtn")
374
+ self._size_down_btn.setToolTip("减小节点")
375
+ layout.addWidget(self._size_down_btn)
376
+
377
+ return frame
378
+
379
+ def _createSeparator(self) -> QFrame:
380
+ """创建垂直分隔线"""
381
+ line = QFrame()
382
+ line.setFrameShape(QFrame.Shape.VLine)
383
+ line.setStyleSheet(f"color: {PALETTE.border}; margin: 0 4px;")
384
+ line.setFixedWidth(2)
385
+ return line
386
+
387
+ def _connectSignals(self) -> None:
388
+ """连接信号槽"""
389
+ # 字体变化
390
+ self._font_combo.currentTextChanged.connect(self._onFontChanged)
391
+ self._font_size_spin.valueChanged.connect(self._onFontSizeChanged)
392
+ self._font_size_up_btn.clicked.connect(self._onFontSizeUp)
393
+ self._font_size_down_btn.clicked.connect(self._onFontSizeDown)
394
+
395
+ # 填充色
396
+ self._fill_color_btn.clicked.connect(self._onFillColorClicked)
397
+
398
+ # 边框
399
+ self._border_slider.valueChanged.connect(self._onBorderWidthChanged)
400
+ self._border_up_btn.clicked.connect(self._onBorderUp)
401
+ self._border_down_btn.clicked.connect(self._onBorderDown)
402
+ self._border_color_btn.clicked.connect(self._onBorderColorClicked)
403
+
404
+ # 大小
405
+ self._size_slider.valueChanged.connect(self._onSizeChanged)
406
+ self._size_up_btn.clicked.connect(self._onSizeUp)
407
+ self._size_down_btn.clicked.connect(self._onSizeDown)
408
+
409
+ # ========== 事件处理方法(与之前相同)==========
410
+
411
+ def _onFontChanged(self, font_family: str) -> None:
412
+ """字体改变"""
413
+ if self._is_updating_ui or font_family == "-":
414
+ return
415
+ for node in self._selected_nodes:
416
+ node.setFont(family=font_family)
417
+ self.styleChanged.emit()
418
+
419
+ def _onFontSizeChanged(self, size: int) -> None:
420
+ """字号改变(绝对设置)"""
421
+ if self._is_updating_ui:
422
+ return
423
+ for node in self._selected_nodes:
424
+ node.setFont(size=size)
425
+ self.styleChanged.emit()
426
+
427
+ def _onFontSizeUp(self) -> None:
428
+ """字号增加(相对调整)"""
429
+ for node in self._selected_nodes:
430
+ current_size = node.fontSize
431
+ node.setFont(size=current_size + 1)
432
+ self._updateUIFromNodes()
433
+ self.styleChanged.emit()
434
+
435
+ def _onFontSizeDown(self) -> None:
436
+ """字号减小(相对调整)"""
437
+ for node in self._selected_nodes:
438
+ current_size = node.fontSize
439
+ if current_size > 6:
440
+ node.setFont(size=current_size - 1)
441
+ self._updateUIFromNodes()
442
+ self.styleChanged.emit()
443
+
444
+ def _onFillColorClicked(self) -> None:
445
+ """点击填充色按钮"""
446
+ color = QColorDialog.getColor(
447
+ self._fill_color_btn.getColor(),
448
+ self,
449
+ "选择填充颜色"
450
+ )
451
+ if color.isValid():
452
+ self._fill_color_btn.setColor(color)
453
+ for node in self._selected_nodes:
454
+ node.setColor(color)
455
+ self.styleChanged.emit()
456
+
457
+ def _onBorderWidthChanged(self, width: int) -> None:
458
+ """边框厚度改变(绝对设置)"""
459
+ if self._is_updating_ui:
460
+ return
461
+ self._border_value_label.setText(str(width))
462
+ for node in self._selected_nodes:
463
+ if width == 0:
464
+ node.setBorder(0)
465
+ else:
466
+ current_color = node.strokeColor
467
+ node.setBorder(width, current_color)
468
+ self.styleChanged.emit()
469
+
470
+ def _onBorderUp(self) -> None:
471
+ """边框增加(相对调整)"""
472
+ for node in self._selected_nodes:
473
+ current_width = int(node.strokeWidth)
474
+ if current_width < 10:
475
+ current_color = node.strokeColor
476
+ node.setBorder(current_width + 1, current_color)
477
+ self._updateUIFromNodes()
478
+ self.styleChanged.emit()
479
+
480
+ def _onBorderDown(self) -> None:
481
+ """边框减小(相对调整)"""
482
+ for node in self._selected_nodes:
483
+ current_width = int(node.strokeWidth)
484
+ if current_width > 0:
485
+ current_color = node.strokeColor
486
+ node.setBorder(max(0, current_width - 1), current_color)
487
+ self._updateUIFromNodes()
488
+ self.styleChanged.emit()
489
+
490
+ def _onBorderColorClicked(self) -> None:
491
+ """点击边框色按钮"""
492
+ color = QColorDialog.getColor(
493
+ self._border_color_btn.getColor(),
494
+ self,
495
+ "选择边框颜色"
496
+ )
497
+ if color.isValid():
498
+ self._border_color_btn.setColor(color)
499
+ for node in self._selected_nodes:
500
+ current_width = int(node.strokeWidth)
501
+ if current_width > 0:
502
+ node.setBorder(current_width, color)
503
+ self.styleChanged.emit()
504
+
505
+ def _onSizeChanged(self, size: int) -> None:
506
+ """大小改变(绝对设置)"""
507
+ if self._is_updating_ui:
508
+ return
509
+ self._size_value_label.setText(str(size))
510
+ for node in self._selected_nodes:
511
+ node.setRadius(size)
512
+ self.styleChanged.emit()
513
+
514
+ def _onSizeUp(self) -> None:
515
+ """大小增加(相对调整)"""
516
+ for node in self._selected_nodes:
517
+ current_radius = node.radius
518
+ node.setRadius(current_radius + 5)
519
+ self._updateUIFromNodes()
520
+ self.styleChanged.emit()
521
+
522
+ def _onSizeDown(self) -> None:
523
+ """大小减小(相对调整)"""
524
+ for node in self._selected_nodes:
525
+ current_radius = node.radius
526
+ if current_radius > 20:
527
+ node.setRadius(current_radius - 5)
528
+ self._updateUIFromNodes()
529
+ self.styleChanged.emit()
530
+
531
+ # ========== 公共方法(与之前相同)==========
532
+
533
+ def setSelectedNodes(self, nodes: List[QNodeItem]) -> None:
534
+ """
535
+ 设置当前选中的节点列表
536
+
537
+ Args:
538
+ nodes: 选中的节点列表
539
+ """
540
+ self._selected_nodes = nodes
541
+
542
+ if not nodes:
543
+ self.hide()
544
+ return
545
+
546
+ self.show()
547
+ self._updateUIFromNodes()
548
+
549
+ def _updateUIFromNodes(self) -> None:
550
+ """从节点数据更新UI显示(处理混合状态)"""
551
+ if not self._selected_nodes:
552
+ return
553
+
554
+ self._is_updating_ui = True
555
+
556
+ # 1. 检查字体家族是否一致
557
+ font_families = set(node.fontFamily for node in self._selected_nodes)
558
+ if len(font_families) == 1:
559
+ family = font_families.pop()
560
+ index = self._font_combo.findText(family)
561
+ if index >= 0:
562
+ self._font_combo.setCurrentIndex(index)
563
+ else:
564
+ self._font_combo.setCurrentText(family)
565
+ else:
566
+ self._font_combo.setCurrentIndex(0)
567
+
568
+ # 2. 检查字号是否一致
569
+ font_sizes = set(node.fontSize for node in self._selected_nodes)
570
+ if len(font_sizes) == 1:
571
+ self._font_size_spin.setValue(font_sizes.pop())
572
+ else:
573
+ self._font_size_spin.setValue(6)
574
+
575
+ # 3. 检查填充色是否一致
576
+ fill_colors = set(node.fillColor.name() for node in self._selected_nodes)
577
+ if len(fill_colors) == 1:
578
+ color = QColor(fill_colors.pop())
579
+ self._fill_color_btn.setColor(color)
580
+ else:
581
+ self._fill_color_btn.setColor(QColor(128, 128, 128))
582
+
583
+ # 4. 检查边框厚度是否一致
584
+ border_widths = set(int(node.strokeWidth) for node in self._selected_nodes)
585
+ if len(border_widths) == 1:
586
+ width = border_widths.pop()
587
+ self._border_slider.setValue(width)
588
+ self._border_value_label.setText(str(width))
589
+ else:
590
+ self._border_slider.setValue(1)
591
+ self._border_value_label.setText("-")
592
+
593
+ # 5. 检查边框颜色是否一致
594
+ border_colors = set(node.strokeColor.name() for node in self._selected_nodes)
595
+ if len(border_colors) == 1:
596
+ color = QColor(border_colors.pop())
597
+ self._border_color_btn.setColor(color)
598
+ else:
599
+ self._border_color_btn.setColor(QColor(128, 128, 128))
600
+
601
+ # 6. 检查大小是否一致
602
+ sizes = set(int(node.radius) for node in self._selected_nodes)
603
+ if len(sizes) == 1:
604
+ size = sizes.pop()
605
+ self._size_slider.setValue(size)
606
+ self._size_value_label.setText(str(size))
607
+ else:
608
+ self._size_slider.setValue(40)
609
+ self._size_value_label.setText("-")
610
+
611
+ self._is_updating_ui = False
612
+
613
+ def getSelectedNodes(self) -> List[QNodeItem]:
614
+ """获取当前选中的节点列表"""
615
+ return self._selected_nodes.copy()