src/eric7/QScintilla/EditorAssembly.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
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

eric ide

mercurial