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,523 @@
|
|
|
1
|
+
"""
|
|
2
|
+
代码折叠系统
|
|
3
|
+
支持代码块的折叠和展开
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from PyQt6.QtCore import QObject, pyqtSignal, QRect, Qt, QPoint
|
|
7
|
+
from PyQt6.QtGui import QPainter, QColor, QFont, QIcon, QPolygon
|
|
8
|
+
from PyQt6.QtWidgets import QStyle, QStyleOption
|
|
9
|
+
from typing import List, Dict, Set, Tuple, Optional
|
|
10
|
+
import ast
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FoldRegion:
|
|
15
|
+
"""折叠区域"""
|
|
16
|
+
def __init__(self, start_line: int, end_line: int,
|
|
17
|
+
fold_type: str = 'block', indent: int = 0):
|
|
18
|
+
self.start_line = start_line # 开始行(1-based)
|
|
19
|
+
self.end_line = end_line # 结束行(1-based)
|
|
20
|
+
self.fold_type = fold_type # 类型:class, function, if, for, while, with, try
|
|
21
|
+
self.indent = indent # 缩进级别
|
|
22
|
+
self.collapsed = False # 是否折叠
|
|
23
|
+
self.marker_rect: Optional[QRect] = None # 折叠标记矩形
|
|
24
|
+
|
|
25
|
+
def __repr__(self):
|
|
26
|
+
return f"FoldRegion({self.start_line}-{self.end_line}, {self.fold_type})"
|
|
27
|
+
|
|
28
|
+
def contains(self, line: int) -> bool:
|
|
29
|
+
"""检查行是否在折叠区域内"""
|
|
30
|
+
return self.start_line < line <= self.end_line
|
|
31
|
+
|
|
32
|
+
def toggle(self):
|
|
33
|
+
"""切换折叠状态"""
|
|
34
|
+
self.collapsed = not self.collapsed
|
|
35
|
+
return self.collapsed
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CodeFolder(QObject):
|
|
39
|
+
"""代码折叠管理器"""
|
|
40
|
+
|
|
41
|
+
fold_changed = pyqtSignal(int, bool) # 折叠状态改变信号 (line, collapsed)
|
|
42
|
+
regions_changed = pyqtSignal() # 折叠区域改变信号
|
|
43
|
+
|
|
44
|
+
def __init__(self, parent=None):
|
|
45
|
+
super().__init__(parent)
|
|
46
|
+
self._regions: List[FoldRegion] = []
|
|
47
|
+
self._collapsed_lines: Set[int] = set() # 被折叠的行
|
|
48
|
+
self._code: str = ""
|
|
49
|
+
|
|
50
|
+
def analyze_code(self, code: str):
|
|
51
|
+
"""分析代码,找出可折叠区域"""
|
|
52
|
+
self._code = code
|
|
53
|
+
self._regions = []
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
tree = ast.parse(code)
|
|
57
|
+
# 遍历模块的所有顶层节点
|
|
58
|
+
for node in tree.body:
|
|
59
|
+
self._analyze_node(node, 0)
|
|
60
|
+
except SyntaxError:
|
|
61
|
+
# 语法错误时使用简单缩进分析
|
|
62
|
+
self._analyze_indentation(code)
|
|
63
|
+
|
|
64
|
+
# 按开始行排序
|
|
65
|
+
self._regions.sort(key=lambda r: r.start_line)
|
|
66
|
+
|
|
67
|
+
self.regions_changed.emit()
|
|
68
|
+
|
|
69
|
+
def _analyze_node(self, node: ast.AST, indent: int):
|
|
70
|
+
"""递归分析AST节点"""
|
|
71
|
+
if isinstance(node, ast.ClassDef):
|
|
72
|
+
end_line = self._get_end_line(node)
|
|
73
|
+
if end_line > node.lineno:
|
|
74
|
+
self._regions.append(FoldRegion(
|
|
75
|
+
start_line=node.lineno,
|
|
76
|
+
end_line=end_line,
|
|
77
|
+
fold_type='class',
|
|
78
|
+
indent=indent
|
|
79
|
+
))
|
|
80
|
+
for item in node.body:
|
|
81
|
+
self._analyze_node(item, indent + 1)
|
|
82
|
+
|
|
83
|
+
elif isinstance(node, ast.FunctionDef):
|
|
84
|
+
end_line = self._get_end_line(node)
|
|
85
|
+
if end_line > node.lineno:
|
|
86
|
+
self._regions.append(FoldRegion(
|
|
87
|
+
start_line=node.lineno,
|
|
88
|
+
end_line=end_line,
|
|
89
|
+
fold_type='function',
|
|
90
|
+
indent=indent
|
|
91
|
+
))
|
|
92
|
+
for item in node.body:
|
|
93
|
+
self._analyze_node(item, indent + 1)
|
|
94
|
+
|
|
95
|
+
elif isinstance(node, ast.AsyncFunctionDef):
|
|
96
|
+
end_line = self._get_end_line(node)
|
|
97
|
+
if end_line > node.lineno:
|
|
98
|
+
self._regions.append(FoldRegion(
|
|
99
|
+
start_line=node.lineno,
|
|
100
|
+
end_line=end_line,
|
|
101
|
+
fold_type='function',
|
|
102
|
+
indent=indent
|
|
103
|
+
))
|
|
104
|
+
for item in node.body:
|
|
105
|
+
self._analyze_node(item, indent + 1)
|
|
106
|
+
|
|
107
|
+
elif isinstance(node, ast.If):
|
|
108
|
+
end_line = self._get_end_line(node)
|
|
109
|
+
if end_line > node.lineno:
|
|
110
|
+
self._regions.append(FoldRegion(
|
|
111
|
+
start_line=node.lineno,
|
|
112
|
+
end_line=end_line,
|
|
113
|
+
fold_type='if',
|
|
114
|
+
indent=indent
|
|
115
|
+
))
|
|
116
|
+
for item in node.body:
|
|
117
|
+
self._analyze_node(item, indent + 1)
|
|
118
|
+
for item in node.orelse:
|
|
119
|
+
self._analyze_node(item, indent + 1)
|
|
120
|
+
|
|
121
|
+
elif isinstance(node, ast.For):
|
|
122
|
+
end_line = self._get_end_line(node)
|
|
123
|
+
if end_line > node.lineno:
|
|
124
|
+
self._regions.append(FoldRegion(
|
|
125
|
+
start_line=node.lineno,
|
|
126
|
+
end_line=end_line,
|
|
127
|
+
fold_type='for',
|
|
128
|
+
indent=indent
|
|
129
|
+
))
|
|
130
|
+
for item in node.body:
|
|
131
|
+
self._analyze_node(item, indent + 1)
|
|
132
|
+
|
|
133
|
+
elif isinstance(node, ast.While):
|
|
134
|
+
end_line = self._get_end_line(node)
|
|
135
|
+
if end_line > node.lineno:
|
|
136
|
+
self._regions.append(FoldRegion(
|
|
137
|
+
start_line=node.lineno,
|
|
138
|
+
end_line=end_line,
|
|
139
|
+
fold_type='while',
|
|
140
|
+
indent=indent
|
|
141
|
+
))
|
|
142
|
+
for item in node.body:
|
|
143
|
+
self._analyze_node(item, indent + 1)
|
|
144
|
+
|
|
145
|
+
elif isinstance(node, ast.With):
|
|
146
|
+
end_line = self._get_end_line(node)
|
|
147
|
+
if end_line > node.lineno:
|
|
148
|
+
self._regions.append(FoldRegion(
|
|
149
|
+
start_line=node.lineno,
|
|
150
|
+
end_line=end_line,
|
|
151
|
+
fold_type='with',
|
|
152
|
+
indent=indent
|
|
153
|
+
))
|
|
154
|
+
for item in node.body:
|
|
155
|
+
self._analyze_node(item, indent + 1)
|
|
156
|
+
|
|
157
|
+
elif isinstance(node, ast.Try):
|
|
158
|
+
end_line = self._get_end_line(node)
|
|
159
|
+
if end_line > node.lineno:
|
|
160
|
+
self._regions.append(FoldRegion(
|
|
161
|
+
start_line=node.lineno,
|
|
162
|
+
end_line=end_line,
|
|
163
|
+
fold_type='try',
|
|
164
|
+
indent=indent
|
|
165
|
+
))
|
|
166
|
+
for item in node.body:
|
|
167
|
+
self._analyze_node(item, indent + 1)
|
|
168
|
+
for handler in node.handlers:
|
|
169
|
+
for item in handler.body:
|
|
170
|
+
self._analyze_node(item, indent + 1)
|
|
171
|
+
for item in node.orelse:
|
|
172
|
+
self._analyze_node(item, indent + 1)
|
|
173
|
+
for item in node.finalbody:
|
|
174
|
+
self._analyze_node(item, indent + 1)
|
|
175
|
+
|
|
176
|
+
def _get_end_line(self, node: ast.AST) -> int:
|
|
177
|
+
"""获取节点的结束行号"""
|
|
178
|
+
max_line = getattr(node, 'lineno', 0)
|
|
179
|
+
for child in ast.walk(node):
|
|
180
|
+
if hasattr(child, 'lineno'):
|
|
181
|
+
max_line = max(max_line, child.lineno)
|
|
182
|
+
if hasattr(child, 'end_lineno') and child.end_lineno:
|
|
183
|
+
max_line = max(max_line, child.end_lineno)
|
|
184
|
+
return max_line
|
|
185
|
+
|
|
186
|
+
def _analyze_indentation(self, code: str):
|
|
187
|
+
"""使用缩进分析代码块(语法错误时的回退方案)
|
|
188
|
+
|
|
189
|
+
改进的折叠逻辑:
|
|
190
|
+
1. 只有行末带有 ':' 的语句才显示折叠按钮(排除行末空白字符干扰)
|
|
191
|
+
2. 通过缩进分析找到代码块的结束位置
|
|
192
|
+
3. 如果用户代码正确(有缩进的子块),才显示折叠按钮
|
|
193
|
+
"""
|
|
194
|
+
lines = code.split('\n')
|
|
195
|
+
|
|
196
|
+
# 匹配可能的代码块开始语句(关键字在行首,以冒号结尾)
|
|
197
|
+
block_patterns = [
|
|
198
|
+
(r'^\s*(class|def)\s+\w+', 'block'),
|
|
199
|
+
(r'^\s*(async\s+def)\s+\w+', 'block'),
|
|
200
|
+
(r'^\s*if\s+[^:]+:', 'if'),
|
|
201
|
+
(r'^\s*elif\s+[^:]+:', 'elif'),
|
|
202
|
+
(r'^\s*else\s*:', 'else'),
|
|
203
|
+
(r'^\s*for\s+[^:]+:', 'for'),
|
|
204
|
+
(r'^\s*while\s+[^:]+:', 'while'),
|
|
205
|
+
(r'^\s*with\s+[^:]+:', 'with'),
|
|
206
|
+
(r'^\s*try\s*:', 'try'),
|
|
207
|
+
(r'^\s*except\s*', 'except'),
|
|
208
|
+
(r'^\s*finally\s*:', 'finally'),
|
|
209
|
+
(r'^\s*match\s+[^:]+:', 'match'),
|
|
210
|
+
(r'^\s*case\s+[^:]+:', 'case'),
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
def get_indent(line: str) -> int:
|
|
214
|
+
"""获取行的缩进级别"""
|
|
215
|
+
return len(line) - len(line.lstrip())
|
|
216
|
+
|
|
217
|
+
def is_block_start(line: str) -> tuple:
|
|
218
|
+
"""检查是否是代码块开始
|
|
219
|
+
返回: (是否是块开始, 块类型)
|
|
220
|
+
"""
|
|
221
|
+
# 去除行末空白后检查是否以冒号结尾
|
|
222
|
+
stripped_end = line.rstrip()
|
|
223
|
+
if not stripped_end.endswith(':'):
|
|
224
|
+
return False, ''
|
|
225
|
+
|
|
226
|
+
for pattern, block_type in block_patterns:
|
|
227
|
+
if re.match(pattern, line):
|
|
228
|
+
return True, block_type
|
|
229
|
+
return False, ''
|
|
230
|
+
|
|
231
|
+
i = 0
|
|
232
|
+
while i < len(lines):
|
|
233
|
+
line = lines[i]
|
|
234
|
+
|
|
235
|
+
# 检查是否是块开始
|
|
236
|
+
is_start, block_type = is_block_start(line)
|
|
237
|
+
if is_start:
|
|
238
|
+
start_indent = get_indent(line)
|
|
239
|
+
start_line = i + 1
|
|
240
|
+
|
|
241
|
+
# 查找块的结束位置
|
|
242
|
+
# 块的结束是下一个相同缩进或更少缩进的非空行
|
|
243
|
+
j = i + 1
|
|
244
|
+
while j < len(lines):
|
|
245
|
+
next_line = lines[j]
|
|
246
|
+
next_line_stripped = next_line.strip()
|
|
247
|
+
|
|
248
|
+
if next_line_stripped: # 非空行
|
|
249
|
+
next_indent = get_indent(next_line)
|
|
250
|
+
|
|
251
|
+
# 如果缩进小于等于起始缩进,说明块结束了
|
|
252
|
+
if next_indent <= start_indent:
|
|
253
|
+
break
|
|
254
|
+
|
|
255
|
+
# 检查是否是同一缩进级别的 elif/else/case(这些应该属于同一个 if/match 块,而不是新块)
|
|
256
|
+
if next_indent == start_indent + 4:
|
|
257
|
+
next_is_start, next_type = is_block_start(next_line)
|
|
258
|
+
if next_is_start and next_type in ('elif', 'else', 'case'):
|
|
259
|
+
# 这些是分支语句,不终止当前块
|
|
260
|
+
pass
|
|
261
|
+
|
|
262
|
+
j += 1
|
|
263
|
+
|
|
264
|
+
end_line = j
|
|
265
|
+
# 只有当块有实际内容(至少有一行缩进的子代码)时才显示折叠按钮
|
|
266
|
+
if end_line > start_line + 1:
|
|
267
|
+
# 检查中间是否有缩进的代码
|
|
268
|
+
has_indented_content = False
|
|
269
|
+
for k in range(start_line, end_line):
|
|
270
|
+
content_line = lines[k]
|
|
271
|
+
if content_line.strip():
|
|
272
|
+
content_indent = get_indent(content_line)
|
|
273
|
+
if content_indent > start_indent:
|
|
274
|
+
has_indented_content = True
|
|
275
|
+
break
|
|
276
|
+
|
|
277
|
+
if has_indented_content:
|
|
278
|
+
region = FoldRegion(
|
|
279
|
+
start_line=start_line,
|
|
280
|
+
end_line=end_line - 1, # 结束于最后一个子代码行
|
|
281
|
+
fold_type=block_type,
|
|
282
|
+
indent=start_indent // 4
|
|
283
|
+
)
|
|
284
|
+
self._regions.append(region)
|
|
285
|
+
|
|
286
|
+
i += 1
|
|
287
|
+
|
|
288
|
+
def get_regions(self) -> List[FoldRegion]:
|
|
289
|
+
"""获取所有折叠区域"""
|
|
290
|
+
return self._regions
|
|
291
|
+
|
|
292
|
+
def get_region_at_line(self, line: int) -> Optional[FoldRegion]:
|
|
293
|
+
"""获取指定行的折叠区域"""
|
|
294
|
+
for region in self._regions:
|
|
295
|
+
if region.start_line == line:
|
|
296
|
+
return region
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
def get_region_by_pos(self, x: int, y: int, line_height: int,
|
|
300
|
+
offset_y: int = 0) -> Optional[FoldRegion]:
|
|
301
|
+
"""根据鼠标位置获取折叠区域"""
|
|
302
|
+
for region in self._regions:
|
|
303
|
+
if region.marker_rect and region.marker_rect.contains(x, y):
|
|
304
|
+
return region
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
def toggle_fold(self, line: int) -> bool:
|
|
308
|
+
"""切换指定行的折叠状态"""
|
|
309
|
+
region = self.get_region_at_line(line)
|
|
310
|
+
if region:
|
|
311
|
+
collapsed = region.toggle()
|
|
312
|
+
self._update_collapsed_lines()
|
|
313
|
+
self.fold_changed.emit(line, collapsed)
|
|
314
|
+
return collapsed
|
|
315
|
+
return False
|
|
316
|
+
|
|
317
|
+
def fold_region(self, line: int):
|
|
318
|
+
"""折叠指定区域"""
|
|
319
|
+
region = self.get_region_at_line(line)
|
|
320
|
+
if region and not region.collapsed:
|
|
321
|
+
region.collapsed = True
|
|
322
|
+
self._update_collapsed_lines()
|
|
323
|
+
self.fold_changed.emit(line, True)
|
|
324
|
+
|
|
325
|
+
def unfold_region(self, line: int):
|
|
326
|
+
"""展开指定区域"""
|
|
327
|
+
region = self.get_region_at_line(line)
|
|
328
|
+
if region and region.collapsed:
|
|
329
|
+
region.collapsed = False
|
|
330
|
+
self._update_collapsed_lines()
|
|
331
|
+
self.fold_changed.emit(line, False)
|
|
332
|
+
|
|
333
|
+
def fold_all(self):
|
|
334
|
+
"""折叠所有区域"""
|
|
335
|
+
for region in self._regions:
|
|
336
|
+
region.collapsed = True
|
|
337
|
+
self._update_collapsed_lines()
|
|
338
|
+
self.regions_changed.emit()
|
|
339
|
+
|
|
340
|
+
def unfold_all(self):
|
|
341
|
+
"""展开所有区域"""
|
|
342
|
+
for region in self._regions:
|
|
343
|
+
region.collapsed = False
|
|
344
|
+
self._update_collapsed_lines()
|
|
345
|
+
self.regions_changed.emit()
|
|
346
|
+
|
|
347
|
+
def fold_at_level(self, level: int):
|
|
348
|
+
"""折叠指定级别的区域"""
|
|
349
|
+
for region in self._regions:
|
|
350
|
+
if region.indent == level:
|
|
351
|
+
region.collapsed = True
|
|
352
|
+
self._update_collapsed_lines()
|
|
353
|
+
self.regions_changed.emit()
|
|
354
|
+
|
|
355
|
+
def _update_collapsed_lines(self):
|
|
356
|
+
"""更新被折叠的行集合"""
|
|
357
|
+
self._collapsed_lines.clear()
|
|
358
|
+
for region in self._regions:
|
|
359
|
+
if region.collapsed:
|
|
360
|
+
for line in range(region.start_line + 1, region.end_line + 1):
|
|
361
|
+
self._collapsed_lines.add(line)
|
|
362
|
+
|
|
363
|
+
def is_line_collapsed(self, line: int) -> bool:
|
|
364
|
+
"""检查行是否被折叠"""
|
|
365
|
+
return line in self._collapsed_lines
|
|
366
|
+
|
|
367
|
+
def get_visible_line_count(self) -> int:
|
|
368
|
+
"""获取可见行数"""
|
|
369
|
+
if not self._code:
|
|
370
|
+
return 0
|
|
371
|
+
total_lines = len(self._code.split('\n'))
|
|
372
|
+
return total_lines - len(self._collapsed_lines)
|
|
373
|
+
|
|
374
|
+
def map_visible_to_real(self, visible_line: int) -> int:
|
|
375
|
+
"""将可见行号映射到实际行号"""
|
|
376
|
+
real_line = 0
|
|
377
|
+
visible_count = 0
|
|
378
|
+
|
|
379
|
+
while visible_count < visible_line:
|
|
380
|
+
real_line += 1
|
|
381
|
+
if real_line not in self._collapsed_lines:
|
|
382
|
+
visible_count += 1
|
|
383
|
+
|
|
384
|
+
return real_line
|
|
385
|
+
|
|
386
|
+
def map_real_to_visible(self, real_line: int) -> int:
|
|
387
|
+
"""将实际行号映射到可见行号"""
|
|
388
|
+
visible_line = 0
|
|
389
|
+
for line in range(1, real_line + 1):
|
|
390
|
+
if line not in self._collapsed_lines:
|
|
391
|
+
visible_line += 1
|
|
392
|
+
return visible_line
|
|
393
|
+
|
|
394
|
+
def get_fold_marker_rect(self, line: int, x: int, y: int,
|
|
395
|
+
size: int = 12) -> QRect:
|
|
396
|
+
"""获取折叠标记的矩形区域"""
|
|
397
|
+
margin = (line_height - size) // 2 if 'line_height' in dir() else 2
|
|
398
|
+
return QRect(x, y + margin, size, size)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class FoldMarkerPainter:
|
|
402
|
+
"""折叠标记绘制器"""
|
|
403
|
+
|
|
404
|
+
MARKER_SIZE = 12
|
|
405
|
+
MARKER_MARGIN = 2
|
|
406
|
+
|
|
407
|
+
def __init__(self, folder: CodeFolder):
|
|
408
|
+
self._folder = folder
|
|
409
|
+
|
|
410
|
+
def paint_marker(self, painter: QPainter, region: FoldRegion,
|
|
411
|
+
x: int, y: int, line_height: int):
|
|
412
|
+
"""绘制折叠标记"""
|
|
413
|
+
# 计算标记位置
|
|
414
|
+
marker_x = x + 2
|
|
415
|
+
marker_y = int(y + (line_height - self.MARKER_SIZE) // 2)
|
|
416
|
+
|
|
417
|
+
# 更新区域的标记矩形
|
|
418
|
+
region.marker_rect = QRect(marker_x, marker_y,
|
|
419
|
+
self.MARKER_SIZE, self.MARKER_SIZE)
|
|
420
|
+
|
|
421
|
+
# 绘制背景
|
|
422
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
423
|
+
painter.setBrush(QColor('#e0e0e0'))
|
|
424
|
+
painter.drawRect(marker_x, marker_y, self.MARKER_SIZE, self.MARKER_SIZE)
|
|
425
|
+
|
|
426
|
+
# 绘制边框
|
|
427
|
+
painter.setPen(QColor('#999999'))
|
|
428
|
+
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
429
|
+
painter.drawRect(marker_x, marker_y, self.MARKER_SIZE, self.MARKER_SIZE)
|
|
430
|
+
|
|
431
|
+
# 绘制展开/折叠图标
|
|
432
|
+
painter.setPen(QColor('#333333'))
|
|
433
|
+
painter.setBrush(QColor('#333333'))
|
|
434
|
+
|
|
435
|
+
center_x = marker_x + self.MARKER_SIZE // 2
|
|
436
|
+
center_y = marker_y + self.MARKER_SIZE // 2
|
|
437
|
+
|
|
438
|
+
if region.collapsed:
|
|
439
|
+
# 折叠状态:绘制右向三角形
|
|
440
|
+
polygon = QPolygon([
|
|
441
|
+
QPoint(marker_x + 3, marker_y + 3),
|
|
442
|
+
QPoint(marker_x + 3, marker_y + self.MARKER_SIZE - 3),
|
|
443
|
+
QPoint(marker_x + self.MARKER_SIZE - 3, marker_y + self.MARKER_SIZE // 2),
|
|
444
|
+
])
|
|
445
|
+
painter.drawPolygon(polygon)
|
|
446
|
+
else:
|
|
447
|
+
# 展开状态:绘制下向三角形
|
|
448
|
+
polygon = QPolygon([
|
|
449
|
+
QPoint(marker_x + 3, marker_y + 3),
|
|
450
|
+
QPoint(marker_x + self.MARKER_SIZE - 3, marker_y + 3),
|
|
451
|
+
QPoint(marker_x + self.MARKER_SIZE // 2, marker_y + self.MARKER_SIZE - 3),
|
|
452
|
+
])
|
|
453
|
+
painter.drawPolygon(polygon)
|
|
454
|
+
|
|
455
|
+
def paint_guides(self, painter: QPainter, regions: List[FoldRegion],
|
|
456
|
+
x: int, first_line: int, last_line: int,
|
|
457
|
+
line_height: int, scroll_y: int):
|
|
458
|
+
"""绘制折叠引导线"""
|
|
459
|
+
painter.setPen(QColor('#d0d0d0'))
|
|
460
|
+
|
|
461
|
+
for region in regions:
|
|
462
|
+
if region.end_line < first_line or region.start_line > last_line:
|
|
463
|
+
continue
|
|
464
|
+
|
|
465
|
+
# 计算引导线位置
|
|
466
|
+
start_y = (region.start_line - 1) * line_height - scroll_y + line_height // 2
|
|
467
|
+
end_y = (region.end_line - 1) * line_height - scroll_y + line_height // 2
|
|
468
|
+
|
|
469
|
+
# 限制在可见区域内
|
|
470
|
+
start_y = max(start_y, 0)
|
|
471
|
+
end_y = min(end_y, (last_line - first_line + 1) * line_height)
|
|
472
|
+
|
|
473
|
+
# 绘制垂直引导线
|
|
474
|
+
guide_x = x + self.MARKER_SIZE // 2
|
|
475
|
+
painter.drawLine(guide_x, start_y, guide_x, end_y)
|
|
476
|
+
|
|
477
|
+
# 绘制水平引导线(到代码开始)
|
|
478
|
+
if not region.collapsed:
|
|
479
|
+
painter.drawLine(guide_x, end_y, x + 20, end_y)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def get_fold_indicator(line: str) -> Tuple[bool, str]:
|
|
483
|
+
"""获取行的折叠指示器信息
|
|
484
|
+
|
|
485
|
+
改进:检查行末是否有冒号(排除行末空白字符干扰)
|
|
486
|
+
"""
|
|
487
|
+
# 去除行末空白后检查是否以冒号结尾
|
|
488
|
+
stripped_end = line.rstrip()
|
|
489
|
+
if not stripped_end.endswith(':'):
|
|
490
|
+
return False, ''
|
|
491
|
+
|
|
492
|
+
stripped = line.strip()
|
|
493
|
+
|
|
494
|
+
if stripped.startswith('class '):
|
|
495
|
+
return True, 'class'
|
|
496
|
+
elif stripped.startswith('def '):
|
|
497
|
+
return True, 'def'
|
|
498
|
+
elif stripped.startswith('async def '):
|
|
499
|
+
return True, 'async'
|
|
500
|
+
elif stripped.startswith('if '):
|
|
501
|
+
return True, 'if'
|
|
502
|
+
elif stripped.startswith('for '):
|
|
503
|
+
return True, 'for'
|
|
504
|
+
elif stripped.startswith('while '):
|
|
505
|
+
return True, 'while'
|
|
506
|
+
elif stripped.startswith('with '):
|
|
507
|
+
return True, 'with'
|
|
508
|
+
elif stripped.startswith('try:'):
|
|
509
|
+
return True, 'try'
|
|
510
|
+
elif stripped.startswith('elif '):
|
|
511
|
+
return True, 'elif'
|
|
512
|
+
elif stripped.startswith('else:'):
|
|
513
|
+
return True, 'else'
|
|
514
|
+
elif stripped.startswith('except'):
|
|
515
|
+
return True, 'except'
|
|
516
|
+
elif stripped.startswith('finally:'):
|
|
517
|
+
return True, 'finally'
|
|
518
|
+
elif stripped.startswith('match '):
|
|
519
|
+
return True, 'match'
|
|
520
|
+
elif stripped.startswith('case '):
|
|
521
|
+
return True, 'case'
|
|
522
|
+
|
|
523
|
+
return False, ''
|