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
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
"""
|
|
2
|
+
行号边栏
|
|
3
|
+
显示行号和折叠按钮 - 扁平化设计风格
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from PyQt6.QtWidgets import QWidget
|
|
7
|
+
from PyQt6.QtCore import Qt, QRect, QSize, pyqtSignal, QPoint
|
|
8
|
+
from PyQt6.QtGui import QPainter, QColor, QFont, QMouseEvent, QPaintEvent, QFontMetrics, QPolygon, QPen
|
|
9
|
+
from typing import Optional, Callable, List
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LineNumberArea(QWidget):
|
|
13
|
+
"""行号边栏控件 - 扁平化风格"""
|
|
14
|
+
|
|
15
|
+
# 信号
|
|
16
|
+
fold_clicked = pyqtSignal(int) # 折叠按钮点击信号 (line_number)
|
|
17
|
+
line_clicked = pyqtSignal(int) # 行号点击信号 (line_number)
|
|
18
|
+
|
|
19
|
+
# 颜色配置(浅色主题)
|
|
20
|
+
COLORS = {
|
|
21
|
+
'background': '#f8f9fa',
|
|
22
|
+
'fold_area_bg': '#f1f3f4',
|
|
23
|
+
'line_area_bg': '#f8f9fa',
|
|
24
|
+
'text': '#9aa0a6',
|
|
25
|
+
'text_active': '#202124',
|
|
26
|
+
'border': '#dadce0',
|
|
27
|
+
'divider': '#dadce0',
|
|
28
|
+
'fold_btn_bg': '#e8eaed',
|
|
29
|
+
'fold_btn_bg_hover': '#dadce0',
|
|
30
|
+
'fold_btn_icon': '#5f6368',
|
|
31
|
+
'fold_btn_icon_active': '#1a73e8',
|
|
32
|
+
'current_line': '#e8f0fe',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# 区域宽度配置
|
|
36
|
+
FOLD_AREA_WIDTH = 22 # 折叠区域宽度
|
|
37
|
+
LINE_NUMBER_PADDING = 8 # 行号边距
|
|
38
|
+
DIVIDER_WIDTH = 1 # 分割线宽度
|
|
39
|
+
|
|
40
|
+
def __init__(self, editor=None, parent=None):
|
|
41
|
+
super().__init__(parent)
|
|
42
|
+
|
|
43
|
+
self._editor = editor
|
|
44
|
+
self._line_count: int = 0
|
|
45
|
+
self._current_line: int = 1
|
|
46
|
+
self._line_height: int = 20
|
|
47
|
+
self._font: QFont = QFont('Consolas', 14)
|
|
48
|
+
self._foldable_lines: set = set() # 可折叠的行
|
|
49
|
+
self._folded_lines: set = set() # 已折叠的行
|
|
50
|
+
self._fold_markers: dict = {} # 折叠标记位置 {line: rect}
|
|
51
|
+
self._hovered_fold_line: int = -1 # 鼠标悬停的折叠行
|
|
52
|
+
|
|
53
|
+
# 设置固定宽度(将根据行数动态调整)
|
|
54
|
+
self.setFixedWidth(60)
|
|
55
|
+
|
|
56
|
+
# 设置背景色
|
|
57
|
+
self.setStyleSheet(f"background-color: {self.COLORS['background']};")
|
|
58
|
+
print(f"[LineNumberArea] __init__: parent={parent}, width={self.width()}")
|
|
59
|
+
|
|
60
|
+
# 启用鼠标跟踪
|
|
61
|
+
self.setMouseTracking(True)
|
|
62
|
+
|
|
63
|
+
def set_line_count(self, count: int):
|
|
64
|
+
"""设置行数"""
|
|
65
|
+
self._line_count = count
|
|
66
|
+
self._update_width()
|
|
67
|
+
self.update()
|
|
68
|
+
|
|
69
|
+
def set_current_line(self, line: int):
|
|
70
|
+
"""设置当前行"""
|
|
71
|
+
self._current_line = line
|
|
72
|
+
self.update()
|
|
73
|
+
|
|
74
|
+
def set_line_height(self, height: int):
|
|
75
|
+
"""设置行高"""
|
|
76
|
+
self._line_height = height
|
|
77
|
+
self.update()
|
|
78
|
+
|
|
79
|
+
def set_font(self, font: QFont):
|
|
80
|
+
"""设置字体"""
|
|
81
|
+
self._font = font
|
|
82
|
+
self._update_width()
|
|
83
|
+
self.update()
|
|
84
|
+
|
|
85
|
+
def set_foldable_lines(self, lines: List[int]):
|
|
86
|
+
"""设置可折叠的行"""
|
|
87
|
+
self._foldable_lines = set(lines)
|
|
88
|
+
self.update()
|
|
89
|
+
|
|
90
|
+
def set_folded_lines(self, lines: List[int]):
|
|
91
|
+
"""设置已折叠的行"""
|
|
92
|
+
self._folded_lines = set(lines)
|
|
93
|
+
self.update()
|
|
94
|
+
|
|
95
|
+
def add_foldable_line(self, line: int):
|
|
96
|
+
"""添加可折叠行"""
|
|
97
|
+
self._foldable_lines.add(line)
|
|
98
|
+
self.update()
|
|
99
|
+
|
|
100
|
+
def remove_foldable_line(self, line: int):
|
|
101
|
+
"""移除可折叠行"""
|
|
102
|
+
self._foldable_lines.discard(line)
|
|
103
|
+
self._folded_lines.discard(line)
|
|
104
|
+
self.update()
|
|
105
|
+
|
|
106
|
+
def toggle_fold(self, line: int):
|
|
107
|
+
"""切换折叠状态"""
|
|
108
|
+
if line in self._folded_lines:
|
|
109
|
+
self._folded_lines.discard(line)
|
|
110
|
+
else:
|
|
111
|
+
self._folded_lines.add(line)
|
|
112
|
+
self.update()
|
|
113
|
+
|
|
114
|
+
def _update_width(self):
|
|
115
|
+
"""根据行数更新宽度"""
|
|
116
|
+
if self._line_count <= 0:
|
|
117
|
+
self.setFixedWidth(self.FOLD_AREA_WIDTH + self.LINE_NUMBER_PADDING * 2 + self.DIVIDER_WIDTH)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
# 计算所需宽度
|
|
121
|
+
metrics = QFontMetrics(self._font)
|
|
122
|
+
max_digits = len(str(self._line_count))
|
|
123
|
+
|
|
124
|
+
# 折叠区域 + 行号宽度 + 边距 + 分割线
|
|
125
|
+
text_width = metrics.horizontalAdvance('9' * max_digits)
|
|
126
|
+
line_area_width = text_width + self.LINE_NUMBER_PADDING
|
|
127
|
+
|
|
128
|
+
total_width = self.FOLD_AREA_WIDTH + self.DIVIDER_WIDTH + line_area_width
|
|
129
|
+
self.setFixedWidth(max(50, total_width))
|
|
130
|
+
|
|
131
|
+
def sizeHint(self):
|
|
132
|
+
"""大小建议"""
|
|
133
|
+
return QSize(self.width(), self._line_count * self._line_height)
|
|
134
|
+
|
|
135
|
+
def paintEvent(self, event: QPaintEvent):
|
|
136
|
+
"""绘制事件"""
|
|
137
|
+
painter = QPainter(self)
|
|
138
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
139
|
+
|
|
140
|
+
# 强制填充红色背景用于调试
|
|
141
|
+
painter.fillRect(self.rect(), QColor('red'))
|
|
142
|
+
|
|
143
|
+
# 绘制折叠区域背景
|
|
144
|
+
fold_rect = QRect(0, 0, self.FOLD_AREA_WIDTH, self.height())
|
|
145
|
+
painter.fillRect(fold_rect, QColor(self.COLORS['fold_area_bg']))
|
|
146
|
+
|
|
147
|
+
# 绘制行号区域背景
|
|
148
|
+
line_rect = QRect(self.FOLD_AREA_WIDTH + self.DIVIDER_WIDTH, 0,
|
|
149
|
+
self.width() - self.FOLD_AREA_WIDTH - self.DIVIDER_WIDTH, self.height())
|
|
150
|
+
painter.fillRect(line_rect, QColor(self.COLORS['line_area_bg']))
|
|
151
|
+
|
|
152
|
+
# 绘制分割线(在折叠区域和行号区域之间)
|
|
153
|
+
divider_x = self.FOLD_AREA_WIDTH
|
|
154
|
+
painter.setPen(QColor(self.COLORS['divider']))
|
|
155
|
+
painter.drawLine(divider_x, 0, divider_x, self.height())
|
|
156
|
+
|
|
157
|
+
# 绘制右边框
|
|
158
|
+
painter.setPen(QColor(self.COLORS['border']))
|
|
159
|
+
painter.drawLine(self.width() - 1, 0, self.width() - 1, self.height())
|
|
160
|
+
|
|
161
|
+
if self._line_count <= 0:
|
|
162
|
+
painter.end()
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
# 设置字体
|
|
166
|
+
painter.setFont(self._font)
|
|
167
|
+
metrics = QFontMetrics(self._font)
|
|
168
|
+
|
|
169
|
+
# 从父编辑器获取可见行范围
|
|
170
|
+
first_line = 1
|
|
171
|
+
last_line = self._line_count
|
|
172
|
+
if self._editor:
|
|
173
|
+
first_line = self._editor.firstVisibleBlock().blockNumber() + 1
|
|
174
|
+
# 计算最后一个可见行
|
|
175
|
+
block = self._editor.firstVisibleBlock()
|
|
176
|
+
viewport_height = self._editor.viewport().height()
|
|
177
|
+
last_line = first_line
|
|
178
|
+
while block.isValid():
|
|
179
|
+
block_rect = self._editor.blockBoundingGeometry(block)
|
|
180
|
+
if block_rect.top() > viewport_height:
|
|
181
|
+
break
|
|
182
|
+
last_line = block.blockNumber() + 1
|
|
183
|
+
block = block.next()
|
|
184
|
+
last_line = min(last_line, self._line_count)
|
|
185
|
+
|
|
186
|
+
# 绘制每一行 - 直接从编辑器获取实际位置
|
|
187
|
+
block = self._editor.document().findBlockByNumber(first_line - 1) if self._editor else None
|
|
188
|
+
# 获取折叠区域信息(如果有)
|
|
189
|
+
folded_regions = []
|
|
190
|
+
if hasattr(self._editor, '_folder'):
|
|
191
|
+
for region in self._editor._folder.get_regions():
|
|
192
|
+
if region.collapsed:
|
|
193
|
+
folded_regions.append((region.start_line, region.end_line))
|
|
194
|
+
|
|
195
|
+
# 计算折叠导致的y偏移函数
|
|
196
|
+
# 注意:被折叠的内部行是 (start+1) 到 end
|
|
197
|
+
line_height = self._line_height # 使用固定的行高
|
|
198
|
+
def get_fold_offset(line):
|
|
199
|
+
offset = 0
|
|
200
|
+
for start, end in folded_regions:
|
|
201
|
+
if line > start:
|
|
202
|
+
# 被折叠的内部行数 = end - start
|
|
203
|
+
# 这些行在折叠后会消失,所以需要减去它们的高度
|
|
204
|
+
if line > end:
|
|
205
|
+
# 完全在折叠区域之后,减去所有被折叠的内部行
|
|
206
|
+
offset += (end - start) * line_height
|
|
207
|
+
# 如果在折叠区域内(start < line <= end),正常显示,不偏移
|
|
208
|
+
return offset
|
|
209
|
+
|
|
210
|
+
for line in range(first_line, last_line + 1):
|
|
211
|
+
# 检查是否应该跳过此行(被折叠的内部行,不是起始行)
|
|
212
|
+
is_folded_internal = False
|
|
213
|
+
for start, end in folded_regions:
|
|
214
|
+
if start < line <= end:
|
|
215
|
+
# 这是折叠区域的内部行,跳过
|
|
216
|
+
is_folded_internal = True
|
|
217
|
+
break
|
|
218
|
+
|
|
219
|
+
if is_folded_internal:
|
|
220
|
+
# 移动到下一个块但不绘制
|
|
221
|
+
if block:
|
|
222
|
+
block = block.next()
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
# 计算y位置:直接从编辑器获取该行的实际位置,并应用折叠偏移
|
|
226
|
+
y = 0
|
|
227
|
+
if block and block.isValid():
|
|
228
|
+
block_rect = self._editor.blockBoundingGeometry(block)
|
|
229
|
+
content_offset = self._editor.contentOffset()
|
|
230
|
+
raw_y = int(block_rect.y() + content_offset.y())
|
|
231
|
+
# 应用折叠偏移
|
|
232
|
+
fold_offset = get_fold_offset(line)
|
|
233
|
+
y = raw_y - fold_offset
|
|
234
|
+
# 更新_line_height为实际行高(用于后续绘制元素)
|
|
235
|
+
actual_height = int(block_rect.height())
|
|
236
|
+
if actual_height > 0:
|
|
237
|
+
self._line_height = actual_height
|
|
238
|
+
|
|
239
|
+
# 绘制当前行高亮(覆盖整个宽度)
|
|
240
|
+
if line == self._current_line:
|
|
241
|
+
painter.fillRect(
|
|
242
|
+
0, y, self.width() - 1, self._line_height,
|
|
243
|
+
QColor(self.COLORS['current_line'])
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# 绘制折叠标记
|
|
247
|
+
if line in self._foldable_lines:
|
|
248
|
+
self._draw_fold_marker(painter, line, y)
|
|
249
|
+
|
|
250
|
+
# 绘制行号
|
|
251
|
+
is_active = (line == self._current_line)
|
|
252
|
+
self._draw_line_number(painter, line, y, is_active)
|
|
253
|
+
|
|
254
|
+
# 移动到下一个块
|
|
255
|
+
if block:
|
|
256
|
+
block = block.next()
|
|
257
|
+
|
|
258
|
+
painter.end()
|
|
259
|
+
|
|
260
|
+
def _draw_fold_marker(self, painter: QPainter, line: int, y: int):
|
|
261
|
+
"""绘制扁平化折叠标记"""
|
|
262
|
+
is_hovered = (line == self._hovered_fold_line)
|
|
263
|
+
is_collapsed = (line in self._folded_lines)
|
|
264
|
+
|
|
265
|
+
# 按钮尺寸和位置(在折叠区域中央)
|
|
266
|
+
btn_size = 14
|
|
267
|
+
btn_x = (self.FOLD_AREA_WIDTH - btn_size) // 2
|
|
268
|
+
btn_y = int(y + (self._line_height - btn_size) // 2)
|
|
269
|
+
|
|
270
|
+
# 存储标记位置(扩大点击区域)
|
|
271
|
+
self._fold_markers[line] = QRect(0, y, self.FOLD_AREA_WIDTH, self._line_height)
|
|
272
|
+
|
|
273
|
+
# 绘制按钮背景(扁平化风格)
|
|
274
|
+
if is_hovered:
|
|
275
|
+
bg_color = QColor(self.COLORS['fold_btn_bg_hover'])
|
|
276
|
+
else:
|
|
277
|
+
bg_color = QColor(self.COLORS['fold_btn_bg'])
|
|
278
|
+
|
|
279
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
280
|
+
painter.setBrush(bg_color)
|
|
281
|
+
painter.drawRoundedRect(btn_x, btn_y, btn_size, btn_size, 3, 3)
|
|
282
|
+
|
|
283
|
+
# 绘制图标颜色
|
|
284
|
+
icon_color = QColor(self.COLORS['fold_btn_icon_active']) if is_hovered else QColor(self.COLORS['fold_btn_icon'])
|
|
285
|
+
painter.setPen(icon_color)
|
|
286
|
+
painter.setBrush(icon_color)
|
|
287
|
+
|
|
288
|
+
# 绘制加号/减号图标(扁平化风格)
|
|
289
|
+
center_x = btn_x + btn_size // 2
|
|
290
|
+
center_y = btn_y + btn_size // 2
|
|
291
|
+
line_length = 6
|
|
292
|
+
line_thickness = 1.5
|
|
293
|
+
|
|
294
|
+
if is_collapsed:
|
|
295
|
+
# 折叠状态:加号(+)
|
|
296
|
+
# 水平线
|
|
297
|
+
painter.setPen(QPen(icon_color, line_thickness))
|
|
298
|
+
painter.drawLine(
|
|
299
|
+
center_x - line_length // 2, center_y,
|
|
300
|
+
center_x + line_length // 2, center_y
|
|
301
|
+
)
|
|
302
|
+
# 垂直线
|
|
303
|
+
painter.drawLine(
|
|
304
|
+
center_x, center_y - line_length // 2,
|
|
305
|
+
center_x, center_y + line_length // 2
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
# 展开状态:减号(-)
|
|
309
|
+
painter.setPen(QPen(icon_color, line_thickness))
|
|
310
|
+
painter.drawLine(
|
|
311
|
+
center_x - line_length // 2, center_y,
|
|
312
|
+
center_x + line_length // 2, center_y
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def _draw_line_number(self, painter: QPainter, line: int, y: int, is_active: bool):
|
|
316
|
+
"""绘制行号"""
|
|
317
|
+
# 设置颜色
|
|
318
|
+
color = self.COLORS['text_active'] if is_active else self.COLORS['text']
|
|
319
|
+
painter.setPen(QColor(color))
|
|
320
|
+
|
|
321
|
+
# 计算位置
|
|
322
|
+
text = str(line)
|
|
323
|
+
metrics = QFontMetrics(self._font)
|
|
324
|
+
text_width = metrics.horizontalAdvance(text)
|
|
325
|
+
|
|
326
|
+
# 行号右对齐,位于行号区域内
|
|
327
|
+
line_area_start = self.FOLD_AREA_WIDTH + self.DIVIDER_WIDTH
|
|
328
|
+
x = self.width() - text_width - self.LINE_NUMBER_PADDING
|
|
329
|
+
text_y = y + (self._line_height - metrics.height()) // 2 + metrics.ascent()
|
|
330
|
+
|
|
331
|
+
# 绘制文本
|
|
332
|
+
painter.drawText(x, text_y, text)
|
|
333
|
+
|
|
334
|
+
def _get_line_at_y(self, y: int) -> int:
|
|
335
|
+
"""根据y坐标获取行号 - 使用与绘制相同的逻辑"""
|
|
336
|
+
if not self._editor or self._line_count <= 0:
|
|
337
|
+
return 1
|
|
338
|
+
|
|
339
|
+
# 从编辑器获取第一可见行
|
|
340
|
+
first_line = self._editor.firstVisibleBlock().blockNumber() + 1
|
|
341
|
+
block = self._editor.document().findBlockByNumber(first_line - 1)
|
|
342
|
+
|
|
343
|
+
# 遍历可见行,找到对应的行
|
|
344
|
+
for line_num in range(first_line, self._line_count + 1):
|
|
345
|
+
if block.isValid():
|
|
346
|
+
block_rect = self._editor.blockBoundingGeometry(block)
|
|
347
|
+
content_offset = self._editor.contentOffset()
|
|
348
|
+
block_y = int(block_rect.y() + content_offset.y())
|
|
349
|
+
block_height = int(block_rect.height())
|
|
350
|
+
|
|
351
|
+
if block_y <= y < block_y + block_height:
|
|
352
|
+
return line_num
|
|
353
|
+
|
|
354
|
+
block = block.next()
|
|
355
|
+
|
|
356
|
+
# 如果不在任何行内,返回最接近的行
|
|
357
|
+
return min(max(1, y // self._line_height + 1), self._line_count)
|
|
358
|
+
|
|
359
|
+
def mousePressEvent(self, event: QMouseEvent):
|
|
360
|
+
"""鼠标按下事件"""
|
|
361
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
|
362
|
+
y = event.pos().y()
|
|
363
|
+
line = self._get_line_at_y(y)
|
|
364
|
+
|
|
365
|
+
if 1 <= line <= self._line_count:
|
|
366
|
+
x = event.pos().x()
|
|
367
|
+
|
|
368
|
+
# 检查是否点击在折叠区域
|
|
369
|
+
if x < self.FOLD_AREA_WIDTH and line in self._foldable_lines:
|
|
370
|
+
self.fold_clicked.emit(line)
|
|
371
|
+
return
|
|
372
|
+
|
|
373
|
+
# 否则发送行号点击信号
|
|
374
|
+
self.line_clicked.emit(line)
|
|
375
|
+
|
|
376
|
+
def mouseMoveEvent(self, event: QMouseEvent):
|
|
377
|
+
"""鼠标移动事件 - 用于hover效果"""
|
|
378
|
+
y = event.pos().y()
|
|
379
|
+
line = self._get_line_at_y(y)
|
|
380
|
+
x = event.pos().x()
|
|
381
|
+
|
|
382
|
+
if x < self.FOLD_AREA_WIDTH and line in self._foldable_lines and 1 <= line <= self._line_count:
|
|
383
|
+
if self._hovered_fold_line != line:
|
|
384
|
+
self._hovered_fold_line = line
|
|
385
|
+
self.update()
|
|
386
|
+
else:
|
|
387
|
+
if self._hovered_fold_line != -1:
|
|
388
|
+
self._hovered_fold_line = -1
|
|
389
|
+
self.update()
|
|
390
|
+
|
|
391
|
+
def leaveEvent(self, event):
|
|
392
|
+
"""鼠标离开事件"""
|
|
393
|
+
if self._hovered_fold_line != -1:
|
|
394
|
+
self._hovered_fold_line = -1
|
|
395
|
+
self.update()
|
|
396
|
+
|
|
397
|
+
def get_line_at_y(self, y: int) -> int:
|
|
398
|
+
"""获取指定y坐标对应的行号"""
|
|
399
|
+
return self._get_line_at_y(y)
|
|
400
|
+
|
|
401
|
+
def get_y_for_line(self, line: int) -> int:
|
|
402
|
+
"""获取指定行号的y坐标"""
|
|
403
|
+
return (line - 1) * self._line_height
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class FoldingIndicator:
|
|
407
|
+
"""折叠指示器 - 用于编辑器边距(扁平化风格)"""
|
|
408
|
+
|
|
409
|
+
SIZE = 14
|
|
410
|
+
|
|
411
|
+
def __init__(self, line: int, fold_type: str = 'block'):
|
|
412
|
+
self.line = line
|
|
413
|
+
self.fold_type = fold_type
|
|
414
|
+
self.collapsed = False
|
|
415
|
+
self.rect: Optional[QRect] = None
|
|
416
|
+
|
|
417
|
+
def draw(self, painter: QPainter, x: int, y: int, height: int):
|
|
418
|
+
"""绘制扁平化折叠指示器"""
|
|
419
|
+
size = self.SIZE
|
|
420
|
+
|
|
421
|
+
# 计算位置(居中)
|
|
422
|
+
marker_x = x + (22 - size) // 2 # 22是折叠区域宽度
|
|
423
|
+
marker_y = y + (height - size) // 2
|
|
424
|
+
|
|
425
|
+
# 存储矩形
|
|
426
|
+
self.rect = QRect(marker_x, marker_y, size, size)
|
|
427
|
+
|
|
428
|
+
# 绘制按钮背景(扁平化风格)
|
|
429
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
430
|
+
painter.setBrush(QColor('#e8eaed'))
|
|
431
|
+
painter.drawRoundedRect(marker_x, marker_y, size, size, 3, 3)
|
|
432
|
+
|
|
433
|
+
# 绘制图标
|
|
434
|
+
icon_color = QColor('#5f6368')
|
|
435
|
+
painter.setPen(QPen(icon_color, 1.5))
|
|
436
|
+
|
|
437
|
+
center_x = marker_x + size // 2
|
|
438
|
+
center_y = marker_y + size // 2
|
|
439
|
+
line_length = 6
|
|
440
|
+
|
|
441
|
+
if self.collapsed:
|
|
442
|
+
# 折叠状态:加号(+)
|
|
443
|
+
# 水平线
|
|
444
|
+
painter.drawLine(
|
|
445
|
+
center_x - line_length // 2, center_y,
|
|
446
|
+
center_x + line_length // 2, center_y
|
|
447
|
+
)
|
|
448
|
+
# 垂直线
|
|
449
|
+
painter.drawLine(
|
|
450
|
+
center_x, center_y - line_length // 2,
|
|
451
|
+
center_x, center_y + line_length // 2
|
|
452
|
+
)
|
|
453
|
+
else:
|
|
454
|
+
# 展开状态:减号(-)
|
|
455
|
+
painter.drawLine(
|
|
456
|
+
center_x - line_length // 2, center_y,
|
|
457
|
+
center_x + line_length // 2, center_y
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
def contains(self, x: int, y: int) -> bool:
|
|
461
|
+
"""检查点是否在指示器内"""
|
|
462
|
+
if self.rect:
|
|
463
|
+
return self.rect.contains(x, y)
|
|
464
|
+
return False
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class LineNumberDelegate:
|
|
468
|
+
"""行号绘制委托 - 用于自定义编辑器"""
|
|
469
|
+
|
|
470
|
+
def __init__(self, editor):
|
|
471
|
+
self._editor = editor
|
|
472
|
+
self._font = QFont('Consolas', 14)
|
|
473
|
+
self._current_line = 1
|
|
474
|
+
self._fold_indicators: dict = {} # {line: FoldingIndicator}
|
|
475
|
+
|
|
476
|
+
def paint(self, painter: QPainter, rect: QRect, line: int):
|
|
477
|
+
"""绘制行号"""
|
|
478
|
+
fold_area_width = 22
|
|
479
|
+
divider_width = 1
|
|
480
|
+
|
|
481
|
+
# 绘制折叠区域背景
|
|
482
|
+
fold_rect = QRect(rect.left(), rect.top(), fold_area_width, rect.height())
|
|
483
|
+
painter.fillRect(fold_rect, QColor('#f1f3f4'))
|
|
484
|
+
|
|
485
|
+
# 绘制行号区域背景
|
|
486
|
+
line_rect = QRect(rect.left() + fold_area_width + divider_width, rect.top(),
|
|
487
|
+
rect.width() - fold_area_width - divider_width, rect.height())
|
|
488
|
+
painter.fillRect(line_rect, QColor('#f8f9fa'))
|
|
489
|
+
|
|
490
|
+
# 绘制分割线
|
|
491
|
+
divider_x = rect.left() + fold_area_width
|
|
492
|
+
painter.setPen(QColor('#dadce0'))
|
|
493
|
+
painter.drawLine(divider_x, rect.top(), divider_x, rect.bottom())
|
|
494
|
+
|
|
495
|
+
# 绘制右边框
|
|
496
|
+
painter.setPen(QColor('#dadce0'))
|
|
497
|
+
painter.drawLine(rect.right(), rect.top(), rect.right(), rect.bottom())
|
|
498
|
+
|
|
499
|
+
# 绘制当前行高亮
|
|
500
|
+
if line == self._current_line:
|
|
501
|
+
painter.fillRect(rect, QColor('#e8f0fe'))
|
|
502
|
+
|
|
503
|
+
# 绘制折叠指示器
|
|
504
|
+
if line in self._fold_indicators:
|
|
505
|
+
indicator = self._fold_indicators[line]
|
|
506
|
+
indicator.draw(painter, rect.left(), rect.top(), rect.height())
|
|
507
|
+
|
|
508
|
+
# 绘制行号
|
|
509
|
+
painter.setFont(self._font)
|
|
510
|
+
painter.setPen(QColor('#9aa0a6') if line != self._current_line else QColor('#202124'))
|
|
511
|
+
|
|
512
|
+
metrics = QFontMetrics(self._font)
|
|
513
|
+
text = str(line)
|
|
514
|
+
text_width = metrics.horizontalAdvance(text)
|
|
515
|
+
|
|
516
|
+
x = rect.right() - text_width - 8
|
|
517
|
+
y = rect.top() + (rect.height() - metrics.height()) // 2 + metrics.ascent()
|
|
518
|
+
|
|
519
|
+
painter.drawText(x, y, text)
|
|
520
|
+
|
|
521
|
+
def set_current_line(self, line: int):
|
|
522
|
+
"""设置当前行"""
|
|
523
|
+
self._current_line = line
|
|
524
|
+
|
|
525
|
+
def add_fold_indicator(self, line: int, fold_type: str = 'block'):
|
|
526
|
+
"""添加折叠指示器"""
|
|
527
|
+
self._fold_indicators[line] = FoldingIndicator(line, fold_type)
|
|
528
|
+
|
|
529
|
+
def remove_fold_indicator(self, line: int):
|
|
530
|
+
"""移除折叠指示器"""
|
|
531
|
+
if line in self._fold_indicators:
|
|
532
|
+
del self._fold_indicators[line]
|
|
533
|
+
|
|
534
|
+
def toggle_fold(self, line: int) -> bool:
|
|
535
|
+
"""切换折叠状态"""
|
|
536
|
+
if line in self._fold_indicators:
|
|
537
|
+
indicator = self._fold_indicators[line]
|
|
538
|
+
indicator.collapsed = not indicator.collapsed
|
|
539
|
+
return indicator.collapsed
|
|
540
|
+
return False
|
|
541
|
+
|
|
542
|
+
def get_indicator_at(self, x: int, y: int, line: int) -> Optional[FoldingIndicator]:
|
|
543
|
+
"""获取指定位置的折叠指示器"""
|
|
544
|
+
if line in self._fold_indicators:
|
|
545
|
+
indicator = self._fold_indicators[line]
|
|
546
|
+
if indicator.contains(x, y):
|
|
547
|
+
return indicator
|
|
548
|
+
return None
|