src/eric7/QScintilla/EditorOutline.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) 2020 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing an outline widget for source code navigation of the editor.
8 """
9
10 import contextlib
11
12 from PyQt6.QtCore import pyqtSlot, Qt, QCoreApplication, QModelIndex, QPoint
13 from PyQt6.QtWidgets import (
14 QTreeView, QAbstractItemView, QMenu, QApplication, QDialog
15 )
16
17 from UI.BrowserSortFilterProxyModel import BrowserSortFilterProxyModel
18 from UI.BrowserModel import (
19 BrowserImportsItem, BrowserGlobalsItem, BrowserClassAttributeItem,
20 BrowserImportItem
21 )
22
23 from .EditorOutlineModel import EditorOutlineModel
24
25 import Preferences
26
27
28 class EditorOutlineView(QTreeView):
29 """
30 Class implementing an outline widget for source code navigation of the
31 editor.
32 """
33 def __init__(self, editor, populate=True, parent=None):
34 """
35 Constructor
36
37 @param editor reference to the editor widget
38 @type Editor
39 @param populate flag indicating to populate the outline
40 @type bool
41 @param parent reference to the parent widget
42 @type QWidget
43 """
44 super().__init__(parent)
45
46 self.__model = EditorOutlineModel(editor, populate=populate)
47 self.__sortModel = BrowserSortFilterProxyModel()
48 self.__sortModel.setSourceModel(self.__model)
49 self.setModel(self.__sortModel)
50
51 self.setRootIsDecorated(True)
52 self.setAlternatingRowColors(True)
53
54 header = self.header()
55 header.setSortIndicator(0, Qt.SortOrder.AscendingOrder)
56 header.setSortIndicatorShown(True)
57 header.setSectionsClickable(True)
58 self.setHeaderHidden(True)
59
60 self.setSortingEnabled(True)
61
62 self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
63 self.setSelectionBehavior(
64 QAbstractItemView.SelectionBehavior.SelectRows)
65
66 self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
67 self.customContextMenuRequested.connect(self.__contextMenuRequested)
68 self.__createPopupMenus()
69
70 self.activated.connect(self.__gotoItem)
71 self.expanded.connect(self.__resizeColumns)
72 self.collapsed.connect(self.__resizeColumns)
73
74 self.__resizeColumns()
75
76 self.__expandedNames = []
77 self.__currentItemName = ""
78 self.__signalsConnected = False
79
80 def setActive(self, active):
81 """
82 Public method to activate or deactivate the outline view.
83
84 @param active flag indicating the requested action
85 @type bool
86 """
87 if active and not self.__signalsConnected:
88 editor = self.__model.editor()
89 editor.refreshed.connect(self.repopulate)
90 editor.languageChanged.connect(self.__editorLanguageChanged)
91 editor.editorRenamed.connect(self.__editorRenamed)
92 editor.cursorLineChanged.connect(self.__editorCursorLineChanged)
93
94 self.__model.repopulate()
95 self.__resizeColumns()
96
97 line, _ = editor.getCursorPosition()
98 self.__editorCursorLineChanged(line)
99
100 elif not active and self.__signalsConnected:
101 editor = self.__model.editor()
102 editor.refreshed.disconnect(self.repopulate)
103 editor.languageChanged.disconnect(self.__editorLanguageChanged)
104 editor.editorRenamed.disconnect(self.__editorRenamed)
105 editor.cursorLineChanged.disconnect(self.__editorCursorLineChanged)
106
107 self.__model.clear()
108
109 @pyqtSlot()
110 def __resizeColumns(self):
111 """
112 Private slot to resize the view when items get expanded or collapsed.
113 """
114 self.resizeColumnToContents(0)
115
116 def isPopulated(self):
117 """
118 Public method to check, if the model is populated.
119
120 @return flag indicating a populated model
121 @rtype bool
122 """
123 return self.__model.isPopulated()
124
125 @pyqtSlot()
126 def repopulate(self):
127 """
128 Public slot to repopulate the model.
129 """
130 if self.isPopulated():
131 self.__prepareRepopulate()
132 self.__model.repopulate()
133 self.__completeRepopulate()
134
135 @pyqtSlot()
136 def __prepareRepopulate(self):
137 """
138 Private slot to prepare to repopulate the outline view.
139 """
140 itm = self.__currentItem()
141 if itm is not None:
142 self.__currentItemName = itm.data(0)
143
144 self.__expandedNames = []
145
146 childIndex = self.model().index(0, 0)
147 while childIndex.isValid():
148 if self.isExpanded(childIndex):
149 self.__expandedNames.append(
150 self.model().item(childIndex).data(0))
151 childIndex = self.indexBelow(childIndex)
152
153 @pyqtSlot()
154 def __completeRepopulate(self):
155 """
156 Private slot to complete the repopulate of the outline view.
157 """
158 childIndex = self.model().index(0, 0)
159 while childIndex.isValid():
160 name = self.model().item(childIndex).data(0)
161 if (self.__currentItemName and self.__currentItemName == name):
162 self.setCurrentIndex(childIndex)
163 if name in self.__expandedNames:
164 self.setExpanded(childIndex, True)
165 childIndex = self.indexBelow(childIndex)
166 self.__resizeColumns()
167
168 self.__expandedNames = []
169 self.__currentItemName = ""
170
171 def isSupportedLanguage(self, language):
172 """
173 Public method to check, if outlining a given language is supported.
174
175 @param language source language to be checked
176 @type str
177 @return flag indicating support
178 @rtype bool
179 """
180 return language in EditorOutlineModel.SupportedLanguages
181
182 @pyqtSlot(QModelIndex)
183 def __gotoItem(self, index):
184 """
185 Private slot to set the editor cursor.
186
187 @param index index of the item to set the cursor for
188 @type QModelIndex
189 """
190 if index.isValid():
191 itm = self.model().item(index)
192 if itm:
193 with contextlib.suppress(AttributeError):
194 lineno = itm.lineno()
195 self.__model.editor().gotoLine(lineno)
196
197 def mouseDoubleClickEvent(self, mouseEvent):
198 """
199 Protected method of QAbstractItemView.
200
201 Reimplemented to disable expanding/collapsing of items when
202 double-clicking. Instead the double-clicked entry is opened.
203
204 @param mouseEvent the mouse event (QMouseEvent)
205 """
206 index = self.indexAt(mouseEvent.position().toPoint())
207 if index.isValid():
208 itm = self.model().item(index)
209 if isinstance(itm, (BrowserImportsItem, BrowserGlobalsItem)):
210 self.setExpanded(index, not self.isExpanded(index))
211 else:
212 self.__gotoItem(index)
213
214 def __currentItem(self):
215 """
216 Private method to get a reference to the current item.
217
218 @return reference to the current item
219 @rtype BrowserItem
220 """
221 itm = self.model().item(self.currentIndex())
222 return itm
223
224 #######################################################################
225 ## Context menu methods below
226 #######################################################################
227
228 def __createPopupMenus(self):
229 """
230 Private method to generate the various popup menus.
231 """
232 # create the popup menu for general use
233 self.__menu = QMenu(self)
234 self.__menu.addAction(
235 QCoreApplication.translate('EditorOutlineView', 'Goto'),
236 self.__goto)
237 self.__menu.addSeparator()
238 self.__menu.addAction(
239 QCoreApplication.translate('EditorOutlineView', 'Refresh'),
240 self.repopulate)
241 self.__menu.addSeparator()
242 self.__menu.addAction(
243 QCoreApplication.translate(
244 'EditorOutlineView', 'Copy Path to Clipboard'),
245 self.__copyToClipboard)
246 self.__menu.addSeparator()
247 self.__menu.addAction(
248 QCoreApplication.translate(
249 'EditorOutlineView', 'Expand All'),
250 lambda: self.expandToDepth(-1))
251 self.__menu.addAction(
252 QCoreApplication.translate(
253 'EditorOutlineView', 'Collapse All'),
254 self.collapseAll)
255 self.__menu.addSeparator()
256 self.__menu.addAction(
257 QCoreApplication.translate(
258 'EditorOutlineView', 'Increment Width'),
259 self.__incWidth)
260 self.__decWidthAct = self.__menu.addAction(
261 QCoreApplication.translate(
262 'EditorOutlineView', 'Decrement Width'),
263 self.__decWidth)
264 self.__menu.addAction(
265 QCoreApplication.translate(
266 'EditorOutlineView', 'Set to Default Width'),
267 self.__defaultWidth)
268 self.__menu.addAction(
269 QCoreApplication.translate(
270 'EditorOutlineView', 'Change Default Width'),
271 self.__changeDefaultWidth)
272
273 # create the attribute/import menu
274 self.__gotoMenu = QMenu(
275 QCoreApplication.translate('EditorOutlineView', "Goto"),
276 self)
277 self.__gotoMenu.aboutToShow.connect(self.__showGotoMenu)
278 self.__gotoMenu.triggered.connect(self.__gotoAttribute)
279
280 self.__attributeMenu = QMenu(self)
281 self.__attributeMenu.addMenu(self.__gotoMenu)
282 self.__attributeMenu.addSeparator()
283 self.__attributeMenu.addAction(
284 QCoreApplication.translate('EditorOutlineView', 'Refresh'),
285 self.repopulate)
286 self.__attributeMenu.addSeparator()
287 self.__attributeMenu.addAction(
288 QCoreApplication.translate(
289 'EditorOutlineView', 'Copy Path to Clipboard'),
290 self.__copyToClipboard)
291 self.__attributeMenu.addSeparator()
292 self.__attributeMenu.addAction(
293 QCoreApplication.translate(
294 'EditorOutlineView', 'Expand All'),
295 lambda: self.expandToDepth(-1))
296 self.__attributeMenu.addAction(
297 QCoreApplication.translate(
298 'EditorOutlineView', 'Collapse All'),
299 self.collapseAll)
300 self.__attributeMenu.addSeparator()
301 self.__attributeMenu.addAction(
302 QCoreApplication.translate(
303 'EditorOutlineView', 'Increment Width'),
304 self.__incWidth)
305 self.__attributeDecWidthAct = self.__attributeMenu.addAction(
306 QCoreApplication.translate(
307 'EditorOutlineView', 'Decrement Width'),
308 self.__decWidth)
309 self.__attributeMenu.addAction(
310 QCoreApplication.translate(
311 'EditorOutlineView', 'Set to Default Width'),
312 self.__defaultWidth)
313 self.__attributeMenu.addAction(
314 QCoreApplication.translate(
315 'EditorOutlineView', 'Change Default Width'),
316 self.__changeDefaultWidth)
317
318 # create the background menu
319 self.__backMenu = QMenu(self)
320 self.__backMenu.addAction(
321 QCoreApplication.translate('EditorOutlineView', 'Refresh'),
322 self.repopulate)
323 self.__backMenu.addSeparator()
324 self.__backMenu.addAction(
325 QCoreApplication.translate(
326 'EditorOutlineView', 'Copy Path to Clipboard'),
327 self.__copyToClipboard)
328 self.__backMenu.addSeparator()
329 self.__backMenu.addAction(
330 QCoreApplication.translate(
331 'EditorOutlineView', 'Expand All'),
332 lambda: self.expandToDepth(-1))
333 self.__backMenu.addAction(
334 QCoreApplication.translate(
335 'EditorOutlineView', 'Collapse All'),
336 self.collapseAll)
337 self.__backMenu.addSeparator()
338 self.__backMenu.addAction(
339 QCoreApplication.translate(
340 'EditorOutlineView', 'Increment Width'),
341 self.__incWidth)
342 self.__backDecWidthAct = self.__backMenu.addAction(
343 QCoreApplication.translate(
344 'EditorOutlineView', 'Decrement Width'),
345 self.__decWidth)
346 self.__backMenu.addAction(
347 QCoreApplication.translate(
348 'EditorOutlineView', 'Set to Default Width'),
349 self.__defaultWidth)
350 self.__backMenu.addAction(
351 QCoreApplication.translate(
352 'EditorOutlineView', 'Change Default Width'),
353 self.__changeDefaultWidth)
354
355 @pyqtSlot(QPoint)
356 def __contextMenuRequested(self, coord):
357 """
358 Private slot to show the context menu.
359
360 @param coord position of the mouse pointer
361 @type QPoint
362 """
363 index = self.indexAt(coord)
364 coord = self.mapToGlobal(coord)
365
366 decWidthEnable = (
367 self.maximumWidth() !=
368 2 * Preferences.getEditor("SourceOutlineStepSize")
369 )
370
371 if index.isValid():
372 self.setCurrentIndex(index)
373
374 itm = self.model().item(index)
375 if isinstance(
376 itm, (BrowserClassAttributeItem, BrowserImportItem)
377 ):
378 self.__attributeDecWidthAct.setEnabled(decWidthEnable)
379 self.__attributeMenu.popup(coord)
380 else:
381 self.__decWidthAct.setEnabled(decWidthEnable)
382 self.__menu.popup(coord)
383 else:
384 self.__backDecWidthAct.setEnabled(decWidthEnable)
385 self.__backMenu.popup(coord)
386
387 @pyqtSlot()
388 def __showGotoMenu(self):
389 """
390 Private slot to prepare the goto submenu of the attribute menu.
391 """
392 self.__gotoMenu.clear()
393
394 itm = self.model().item(self.currentIndex())
395 try:
396 linenos = itm.linenos()
397 except AttributeError:
398 try:
399 linenos = [itm.lineno()]
400 except AttributeError:
401 return
402
403 for lineno in sorted(linenos):
404 act = self.__gotoMenu.addAction(
405 QCoreApplication.translate(
406 'EditorOutlineView', "Line {0}").format(lineno))
407 act.setData(lineno)
408
409 #######################################################################
410 ## Context menu handlers below
411 #######################################################################
412
413 @pyqtSlot()
414 def __gotoAttribute(self, act):
415 """
416 Private slot to handle the selection of the goto menu.
417
418 @param act reference to the action (EricAction)
419 """
420 lineno = act.data()
421 self.__model.editor().gotoLine(lineno)
422
423 @pyqtSlot()
424 def __goto(self):
425 """
426 Private slot to move the editor cursor to the line of the context item.
427 """
428 self.__gotoItem(self.currentIndex())
429
430 @pyqtSlot()
431 def __copyToClipboard(self):
432 """
433 Private slot to copy the file name of the editor to the clipboard.
434 """
435 fn = self.__model.fileName()
436
437 if fn:
438 cb = QApplication.clipboard()
439 cb.setText(fn)
440
441 @pyqtSlot()
442 def __incWidth(self):
443 """
444 Private slot to increment the width of the outline.
445 """
446 self.setMaximumWidth(
447 self.maximumWidth() +
448 Preferences.getEditor("SourceOutlineStepSize")
449 )
450 self.updateGeometry()
451
452 @pyqtSlot()
453 def __decWidth(self):
454 """
455 Private slot to decrement the width of the outline.
456 """
457 stepSize = Preferences.getEditor("SourceOutlineStepSize")
458 newWidth = self.maximumWidth() - stepSize
459
460 self.setMaximumWidth(max(newWidth, 2 * stepSize))
461 self.updateGeometry()
462
463 @pyqtSlot()
464 def __defaultWidth(self):
465 """
466 Private slot to set the outline to the default width.
467 """
468 self.setMaximumWidth(Preferences.getEditor("SourceOutlineWidth"))
469 self.updateGeometry()
470
471 @pyqtSlot()
472 def __changeDefaultWidth(self):
473 """
474 Private slot to open a dialog to change the default width and step
475 size presetting the width with the current value.
476 """
477 from .EditorOutlineSizesDialog import EditorOutlineSizesDialog
478
479 stepSize = Preferences.getEditor("SourceOutlineStepSize")
480 defaultWidth = Preferences.getEditor("SourceOutlineWidth")
481 currentWidth = self.maximumWidth()
482
483 dlg = EditorOutlineSizesDialog(currentWidth, defaultWidth, stepSize)
484 if dlg.exec() == QDialog.DialogCode.Accepted:
485 newDefaultWidth, stepSize = dlg.getSizes()
486
487 Preferences.setEditor("SourceOutlineWidth", newDefaultWidth)
488 Preferences.setEditor("SourceOutlineStepSize", stepSize)
489
490 if newDefaultWidth != currentWidth:
491 self.__defaultWidth()
492
493 #######################################################################
494 ## Methods handling editor signals below
495 #######################################################################
496
497 @pyqtSlot()
498 def __editorLanguageChanged(self):
499 """
500 Private slot handling a change of the associated editors source code
501 language.
502 """
503 self.__model.repopulate()
504 self.__resizeColumns()
505
506 @pyqtSlot()
507 def __editorRenamed(self):
508 """
509 Private slot handling a renaming of the associated editor.
510 """
511 self.__model.repopulate()
512 self.__resizeColumns()
513
514 @pyqtSlot(int)
515 def __editorCursorLineChanged(self, lineno):
516 """
517 Private method to highlight a node given its line number.
518
519 @param lineno zero based line number of the item
520 @type int
521 """
522 sindex = self.__model.itemIndexByLine(lineno + 1)
523 if sindex.isValid():
524 index = self.model().mapFromSource(sindex)
525 if index.isValid():
526 self.setCurrentIndex(index)
527 self.scrollTo(index)
528 else:
529 self.setCurrentIndex(QModelIndex())

eric ide

mercurial