|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2011 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the editor assembly widget containing the navigation |
|
8 combos and the editor widget. |
|
9 """ |
|
10 |
|
11 import contextlib |
|
12 |
|
13 from PyQt6.QtCore import QTimer |
|
14 from PyQt6.QtWidgets import QWidget, QGridLayout, QComboBox |
|
15 |
|
16 from EricWidgets.EricApplication import ericApp |
|
17 |
|
18 import UI.PixmapCache |
|
19 import Preferences |
|
20 |
|
21 |
|
22 class EditorAssembly(QWidget): |
|
23 """ |
|
24 Class implementing the editor assembly widget containing the navigation |
|
25 combos and the editor widget. |
|
26 """ |
|
27 def __init__(self, dbs, fn="", vm=None, filetype="", editor=None, |
|
28 tv=None): |
|
29 """ |
|
30 Constructor |
|
31 |
|
32 @param dbs reference to the debug server object |
|
33 @type DebugServer |
|
34 @param fn name of the file to be opened. If it is None, |
|
35 a new (empty) editor is opened. |
|
36 @type str |
|
37 @param vm reference to the view manager object |
|
38 @type ViewManager.ViewManager |
|
39 @param filetype type of the source file |
|
40 @type str |
|
41 @param editor reference to an Editor object, if this is a cloned view |
|
42 @type Editor |
|
43 @param tv reference to the task viewer object |
|
44 @type TaskViewer |
|
45 """ |
|
46 super().__init__() |
|
47 |
|
48 self.__layout = QGridLayout(self) |
|
49 self.__layout.setContentsMargins(0, 0, 0, 0) |
|
50 self.__layout.setSpacing(1) |
|
51 |
|
52 from .EditorButtonsWidget import EditorButtonsWidget |
|
53 from .Editor import Editor |
|
54 from .EditorOutline import EditorOutlineView |
|
55 |
|
56 self.__showOutline = Preferences.getEditor("ShowSourceOutline") |
|
57 |
|
58 self.__editor = Editor(dbs, fn, vm, filetype, editor, tv) |
|
59 self.__buttonsWidget = EditorButtonsWidget(self.__editor, self) |
|
60 self.__globalsCombo = QComboBox() |
|
61 self.__globalsCombo.setDuplicatesEnabled(True) |
|
62 self.__membersCombo = QComboBox() |
|
63 self.__membersCombo.setDuplicatesEnabled(True) |
|
64 self.__sourceOutline = EditorOutlineView( |
|
65 self.__editor, populate=self.__showOutline) |
|
66 self.__sourceOutline.setMaximumWidth( |
|
67 Preferences.getEditor("SourceOutlineWidth")) |
|
68 |
|
69 self.__layout.addWidget(self.__buttonsWidget, 1, 0, -1, 1) |
|
70 self.__layout.addWidget(self.__globalsCombo, 0, 1) |
|
71 self.__layout.addWidget(self.__membersCombo, 0, 2) |
|
72 self.__layout.addWidget(self.__editor, 1, 1, 1, 2) |
|
73 self.__layout.addWidget(self.__sourceOutline, 0, 3, -1, -1) |
|
74 |
|
75 self.setFocusProxy(self.__editor) |
|
76 |
|
77 self.__module = None |
|
78 |
|
79 self.__shutdownTimerCalled = False |
|
80 self.__parseTimer = QTimer(self) |
|
81 self.__parseTimer.setSingleShot(True) |
|
82 self.__parseTimer.setInterval(5 * 1000) |
|
83 self.__editor.textChanged.connect(self.__resetParseTimer) |
|
84 self.__editor.refreshed.connect(self.__resetParseTimer) |
|
85 |
|
86 self.__selectedGlobal = "" |
|
87 self.__selectedMember = "" |
|
88 self.__globalsBoundaries = {} |
|
89 self.__membersBoundaries = {} |
|
90 |
|
91 self.__activateOutline(self.__showOutline) |
|
92 self.__activateCombos(not self.__showOutline) |
|
93 |
|
94 ericApp().getObject("UserInterface").preferencesChanged.connect( |
|
95 self.__preferencesChanged) |
|
96 |
|
97 def shutdownTimer(self): |
|
98 """ |
|
99 Public method to stop and disconnect the timer. |
|
100 """ |
|
101 self.__parseTimer.stop() |
|
102 if not self.__shutdownTimerCalled: |
|
103 self.__editor.textChanged.disconnect(self.__resetParseTimer) |
|
104 self.__editor.refreshed.disconnect(self.__resetParseTimer) |
|
105 self.__shutdownTimerCalled = True |
|
106 |
|
107 def getEditor(self): |
|
108 """ |
|
109 Public method to get the reference to the editor widget. |
|
110 |
|
111 @return reference to the editor widget |
|
112 @rtype Editor |
|
113 """ |
|
114 return self.__editor |
|
115 |
|
116 def __preferencesChanged(self): |
|
117 """ |
|
118 Private slot handling a change of preferences. |
|
119 """ |
|
120 showOutline = Preferences.getEditor("ShowSourceOutline") |
|
121 if showOutline != self.__showOutline: |
|
122 self.__showOutline = showOutline |
|
123 self.__activateOutline(self.__showOutline) |
|
124 self.__activateCombos(not self.__showOutline) |
|
125 |
|
126 ####################################################################### |
|
127 ## Methods dealing with the navigation combos below |
|
128 ####################################################################### |
|
129 |
|
130 def __activateCombos(self, activate): |
|
131 """ |
|
132 Private slot to activate the navigation combo boxes. |
|
133 |
|
134 @param activate flag indicating to activate the combo boxes |
|
135 @type bool |
|
136 """ |
|
137 self.__globalsCombo.setVisible(activate) |
|
138 self.__membersCombo.setVisible(activate) |
|
139 if activate: |
|
140 self.__globalsCombo.activated[int].connect( |
|
141 self.__globalsActivated) |
|
142 self.__membersCombo.activated[int].connect( |
|
143 self.__membersActivated) |
|
144 self.__editor.cursorLineChanged.connect( |
|
145 self.__editorCursorLineChanged) |
|
146 self.__parseTimer.timeout.connect(self.__parseEditor) |
|
147 |
|
148 self.__parseEditor() |
|
149 |
|
150 line, _ = self.__editor.getCursorPosition() |
|
151 self.__editorCursorLineChanged(line) |
|
152 else: |
|
153 with contextlib.suppress(TypeError): |
|
154 self.__globalsCombo.activated[int].disconnect( |
|
155 self.__globalsActivated) |
|
156 self.__membersCombo.activated[int].disconnect( |
|
157 self.__membersActivated) |
|
158 self.__editor.cursorLineChanged.disconnect( |
|
159 self.__editorCursorLineChanged) |
|
160 self.__parseTimer.timeout.disconnect(self.__parseEditor) |
|
161 |
|
162 self.__globalsCombo.clear() |
|
163 self.__membersCombo.clear() |
|
164 self.__globalsBoundaries = {} |
|
165 self.__membersBoundaries = {} |
|
166 |
|
167 def __globalsActivated(self, index, moveCursor=True): |
|
168 """ |
|
169 Private method to jump to the line of the selected global entry and to |
|
170 populate the members combo box. |
|
171 |
|
172 @param index index of the selected entry |
|
173 @type int |
|
174 @param moveCursor flag indicating to move the editor cursor |
|
175 @type bool |
|
176 """ |
|
177 # step 1: go to the line of the selected entry |
|
178 lineno = self.__globalsCombo.itemData(index) |
|
179 if lineno is not None: |
|
180 if moveCursor: |
|
181 txt = self.__editor.text(lineno - 1).rstrip() |
|
182 pos = len(txt.replace(txt.strip(), "")) |
|
183 self.__editor.gotoLine( |
|
184 lineno, pos if pos == 0 else pos + 1, True) |
|
185 self.__editor.setFocus() |
|
186 |
|
187 # step 2: populate the members combo, if the entry is a class |
|
188 self.__membersCombo.clear() |
|
189 self.__membersBoundaries = {} |
|
190 self.__membersCombo.addItem("") |
|
191 memberIndex = 0 |
|
192 entryName = self.__globalsCombo.itemText(index) |
|
193 if self.__module: |
|
194 if entryName in self.__module.classes: |
|
195 entry = self.__module.classes[entryName] |
|
196 elif entryName in self.__module.modules: |
|
197 entry = self.__module.modules[entryName] |
|
198 # step 2.0: add module classes |
|
199 items = [] |
|
200 for cl in entry.classes.values(): |
|
201 if cl.isPrivate(): |
|
202 icon = UI.PixmapCache.getIcon("class_private") |
|
203 elif cl.isProtected(): |
|
204 icon = UI.PixmapCache.getIcon( |
|
205 "class_protected") |
|
206 else: |
|
207 icon = UI.PixmapCache.getIcon("class") |
|
208 items.append((icon, cl.name, cl.lineno, cl.endlineno)) |
|
209 for itm in sorted(items, key=lambda x: (x[1], x[2])): |
|
210 self.__membersCombo.addItem(itm[0], itm[1], itm[2]) |
|
211 memberIndex += 1 |
|
212 self.__membersBoundaries[(itm[2], itm[3])] = ( |
|
213 memberIndex |
|
214 ) |
|
215 else: |
|
216 return |
|
217 |
|
218 # step 2.1: add class methods |
|
219 from Utilities.ModuleParser import Function |
|
220 items = [] |
|
221 for meth in entry.methods.values(): |
|
222 if meth.modifier == Function.Static: |
|
223 icon = UI.PixmapCache.getIcon("method_static") |
|
224 elif meth.modifier == Function.Class: |
|
225 icon = UI.PixmapCache.getIcon("method_class") |
|
226 elif meth.isPrivate(): |
|
227 icon = UI.PixmapCache.getIcon("method_private") |
|
228 elif meth.isProtected(): |
|
229 icon = UI.PixmapCache.getIcon("method_protected") |
|
230 else: |
|
231 icon = UI.PixmapCache.getIcon("method") |
|
232 items.append( |
|
233 (icon, meth.name, meth.lineno, meth.endlineno) |
|
234 ) |
|
235 for itm in sorted(items, key=lambda x: (x[1], x[2])): |
|
236 self.__membersCombo.addItem(itm[0], itm[1], itm[2]) |
|
237 memberIndex += 1 |
|
238 self.__membersBoundaries[(itm[2], itm[3])] = memberIndex |
|
239 |
|
240 # step 2.2: add class instance attributes |
|
241 items = [] |
|
242 for attr in entry.attributes.values(): |
|
243 if attr.isPrivate(): |
|
244 icon = UI.PixmapCache.getIcon("attribute_private") |
|
245 elif attr.isProtected(): |
|
246 icon = UI.PixmapCache.getIcon( |
|
247 "attribute_protected") |
|
248 else: |
|
249 icon = UI.PixmapCache.getIcon("attribute") |
|
250 items.append((icon, attr.name, attr.lineno)) |
|
251 for itm in sorted(items, key=lambda x: (x[1], x[2])): |
|
252 self.__membersCombo.addItem(itm[0], itm[1], itm[2]) |
|
253 |
|
254 # step 2.3: add class attributes |
|
255 items = [] |
|
256 icon = UI.PixmapCache.getIcon("attribute_class") |
|
257 for globalVar in entry.globals.values(): |
|
258 items.append((icon, globalVar.name, globalVar.lineno)) |
|
259 for itm in sorted(items, key=lambda x: (x[1], x[2])): |
|
260 self.__membersCombo.addItem(itm[0], itm[1], itm[2]) |
|
261 |
|
262 def __membersActivated(self, index, moveCursor=True): |
|
263 """ |
|
264 Private method to jump to the line of the selected members entry. |
|
265 |
|
266 @param index index of the selected entry |
|
267 @type int |
|
268 @param moveCursor flag indicating to move the editor cursor |
|
269 @type bool |
|
270 """ |
|
271 lineno = self.__membersCombo.itemData(index) |
|
272 if lineno is not None and moveCursor: |
|
273 txt = self.__editor.text(lineno - 1).rstrip() |
|
274 pos = len(txt.replace(txt.strip(), "")) |
|
275 self.__editor.gotoLine(lineno, pos if pos == 0 else pos + 1, |
|
276 firstVisible=True, expand=True) |
|
277 self.__editor.setFocus() |
|
278 |
|
279 def __resetParseTimer(self): |
|
280 """ |
|
281 Private slot to reset the parse timer. |
|
282 """ |
|
283 self.__parseTimer.stop() |
|
284 self.__parseTimer.start() |
|
285 |
|
286 def __parseEditor(self): |
|
287 """ |
|
288 Private method to parse the editor source and repopulate the globals |
|
289 combo. |
|
290 """ |
|
291 from Utilities.ModuleParser import Module, getTypeFromTypeName |
|
292 |
|
293 self.__module = None |
|
294 sourceType = getTypeFromTypeName(self.__editor.determineFileType()) |
|
295 if sourceType != -1: |
|
296 src = self.__editor.text() |
|
297 if src: |
|
298 fn = self.__editor.getFileName() |
|
299 if fn is None: |
|
300 fn = "" |
|
301 self.__module = Module("", fn, sourceType) |
|
302 self.__module.scan(src) |
|
303 |
|
304 # remember the current selections |
|
305 self.__selectedGlobal = self.__globalsCombo.currentText() |
|
306 self.__selectedMember = self.__membersCombo.currentText() |
|
307 |
|
308 self.__globalsCombo.clear() |
|
309 self.__membersCombo.clear() |
|
310 self.__globalsBoundaries = {} |
|
311 self.__membersBoundaries = {} |
|
312 |
|
313 self.__globalsCombo.addItem("") |
|
314 index = 0 |
|
315 |
|
316 # step 1: add modules |
|
317 items = [] |
|
318 for module in self.__module.modules.values(): |
|
319 items.append( |
|
320 (UI.PixmapCache.getIcon("module"), module.name, |
|
321 module.lineno, module.endlineno) |
|
322 ) |
|
323 for itm in sorted(items, key=lambda x: (x[1], x[2])): |
|
324 self.__globalsCombo.addItem(itm[0], itm[1], itm[2]) |
|
325 index += 1 |
|
326 self.__globalsBoundaries[(itm[2], itm[3])] = index |
|
327 |
|
328 # step 2: add classes |
|
329 items = [] |
|
330 for cl in self.__module.classes.values(): |
|
331 if cl.isPrivate(): |
|
332 icon = UI.PixmapCache.getIcon("class_private") |
|
333 elif cl.isProtected(): |
|
334 icon = UI.PixmapCache.getIcon("class_protected") |
|
335 else: |
|
336 icon = UI.PixmapCache.getIcon("class") |
|
337 items.append( |
|
338 (icon, cl.name, cl.lineno, cl.endlineno) |
|
339 ) |
|
340 for itm in sorted(items, key=lambda x: (x[1], x[2])): |
|
341 self.__globalsCombo.addItem(itm[0], itm[1], itm[2]) |
|
342 index += 1 |
|
343 self.__globalsBoundaries[(itm[2], itm[3])] = index |
|
344 |
|
345 # step 3: add functions |
|
346 items = [] |
|
347 for func in self.__module.functions.values(): |
|
348 if func.isPrivate(): |
|
349 icon = UI.PixmapCache.getIcon("method_private") |
|
350 elif func.isProtected(): |
|
351 icon = UI.PixmapCache.getIcon("method_protected") |
|
352 else: |
|
353 icon = UI.PixmapCache.getIcon("method") |
|
354 items.append( |
|
355 (icon, func.name, func.lineno, func.endlineno) |
|
356 ) |
|
357 for itm in sorted(items, key=lambda x: (x[1], x[2])): |
|
358 self.__globalsCombo.addItem(itm[0], itm[1], itm[2]) |
|
359 index += 1 |
|
360 self.__globalsBoundaries[(itm[2], itm[3])] = index |
|
361 |
|
362 # step 4: add attributes |
|
363 items = [] |
|
364 for globalValue in self.__module.globals.values(): |
|
365 if globalValue.isPrivate(): |
|
366 icon = UI.PixmapCache.getIcon("attribute_private") |
|
367 elif globalValue.isProtected(): |
|
368 icon = UI.PixmapCache.getIcon( |
|
369 "attribute_protected") |
|
370 else: |
|
371 icon = UI.PixmapCache.getIcon("attribute") |
|
372 items.append( |
|
373 (icon, globalValue.name, globalValue.lineno) |
|
374 ) |
|
375 for itm in sorted(items, key=lambda x: (x[1], x[2])): |
|
376 self.__globalsCombo.addItem(itm[0], itm[1], itm[2]) |
|
377 |
|
378 # reset the currently selected entries without moving the |
|
379 # text cursor |
|
380 index = self.__globalsCombo.findText(self.__selectedGlobal) |
|
381 if index != -1: |
|
382 self.__globalsCombo.setCurrentIndex(index) |
|
383 self.__globalsActivated(index, moveCursor=False) |
|
384 index = self.__membersCombo.findText(self.__selectedMember) |
|
385 if index != -1: |
|
386 self.__membersCombo.setCurrentIndex(index) |
|
387 self.__membersActivated(index, moveCursor=False) |
|
388 else: |
|
389 self.__globalsCombo.clear() |
|
390 self.__membersCombo.clear() |
|
391 self.__globalsBoundaries = {} |
|
392 self.__membersBoundaries = {} |
|
393 |
|
394 def __editorCursorLineChanged(self, lineno): |
|
395 """ |
|
396 Private slot handling a line change of the cursor of the editor. |
|
397 |
|
398 @param lineno line number of the cursor |
|
399 @type int |
|
400 """ |
|
401 lineno += 1 # cursor position is zero based, code info one based |
|
402 |
|
403 # step 1: search in the globals |
|
404 indexFound = 0 |
|
405 for (lower, upper), index in self.__globalsBoundaries.items(): |
|
406 if upper == -1: |
|
407 upper = 1000000 # it is the last line |
|
408 if lower <= lineno <= upper: |
|
409 indexFound = index |
|
410 break |
|
411 self.__globalsCombo.setCurrentIndex(indexFound) |
|
412 self.__globalsActivated(indexFound, moveCursor=False) |
|
413 |
|
414 # step 2: search in members |
|
415 indexFound = 0 |
|
416 for (lower, upper), index in self.__membersBoundaries.items(): |
|
417 if upper == -1: |
|
418 upper = 1000000 # it is the last line |
|
419 if lower <= lineno <= upper: |
|
420 indexFound = index |
|
421 break |
|
422 self.__membersCombo.setCurrentIndex(indexFound) |
|
423 self.__membersActivated(indexFound, moveCursor=False) |
|
424 |
|
425 ####################################################################### |
|
426 ## Methods dealing with the source outline below |
|
427 ####################################################################### |
|
428 |
|
429 def __activateOutline(self, activate): |
|
430 """ |
|
431 Private slot to activate the source outline view. |
|
432 |
|
433 @param activate flag indicating to activate the source outline view |
|
434 @type bool |
|
435 """ |
|
436 self.__sourceOutline.setActive(activate) |
|
437 |
|
438 if activate: |
|
439 self.__sourceOutline.setVisible( |
|
440 self.__sourceOutline.isSupportedLanguage( |
|
441 self.__editor.getLanguage() |
|
442 ) |
|
443 ) |
|
444 |
|
445 self.__parseTimer.timeout.connect(self.__sourceOutline.repopulate) |
|
446 self.__editor.languageChanged.connect(self.__editorChanged) |
|
447 self.__editor.editorRenamed.connect(self.__editorChanged) |
|
448 else: |
|
449 self.__sourceOutline.hide() |
|
450 |
|
451 with contextlib.suppress(TypeError): |
|
452 self.__parseTimer.timeout.disconnect( |
|
453 self.__sourceOutline.repopulate) |
|
454 self.__editor.languageChanged.disconnect(self.__editorChanged) |
|
455 self.__editor.editorRenamed.disconnect(self.__editorChanged) |
|
456 |
|
457 def __editorChanged(self): |
|
458 """ |
|
459 Private slot handling changes of the editor language or file name. |
|
460 """ |
|
461 supported = self.__sourceOutline.isSupportedLanguage( |
|
462 self.__editor.getLanguage()) |
|
463 |
|
464 self.__sourceOutline.setVisible(supported) |
|
465 |
|
466 # |
|
467 # eflag: noqa = Y113 |