src/eric7/QScintilla/EditorOutline.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/QScintilla/EditorOutline.py	Thu Jul 07 11:23:56 2022 +0200
@@ -0,0 +1,529 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing an outline widget for source code navigation of the editor.
+"""
+
+import contextlib
+
+from PyQt6.QtCore import pyqtSlot, Qt, QCoreApplication, QModelIndex, QPoint
+from PyQt6.QtWidgets import (
+    QTreeView, QAbstractItemView, QMenu, QApplication, QDialog
+)
+
+from UI.BrowserSortFilterProxyModel import BrowserSortFilterProxyModel
+from UI.BrowserModel import (
+    BrowserImportsItem, BrowserGlobalsItem, BrowserClassAttributeItem,
+    BrowserImportItem
+)
+
+from .EditorOutlineModel import EditorOutlineModel
+
+import Preferences
+
+
+class EditorOutlineView(QTreeView):
+    """
+    Class implementing an outline widget for source code navigation of the
+    editor.
+    """
+    def __init__(self, editor, populate=True, parent=None):
+        """
+        Constructor
+        
+        @param editor reference to the editor widget
+        @type Editor
+        @param populate flag indicating to populate the outline
+        @type bool
+        @param parent reference to the parent widget
+        @type QWidget
+        """
+        super().__init__(parent)
+        
+        self.__model = EditorOutlineModel(editor, populate=populate)
+        self.__sortModel = BrowserSortFilterProxyModel()
+        self.__sortModel.setSourceModel(self.__model)
+        self.setModel(self.__sortModel)
+        
+        self.setRootIsDecorated(True)
+        self.setAlternatingRowColors(True)
+        
+        header = self.header()
+        header.setSortIndicator(0, Qt.SortOrder.AscendingOrder)
+        header.setSortIndicatorShown(True)
+        header.setSectionsClickable(True)
+        self.setHeaderHidden(True)
+        
+        self.setSortingEnabled(True)
+        
+        self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
+        self.setSelectionBehavior(
+            QAbstractItemView.SelectionBehavior.SelectRows)
+        
+        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
+        self.customContextMenuRequested.connect(self.__contextMenuRequested)
+        self.__createPopupMenus()
+        
+        self.activated.connect(self.__gotoItem)
+        self.expanded.connect(self.__resizeColumns)
+        self.collapsed.connect(self.__resizeColumns)
+        
+        self.__resizeColumns()
+        
+        self.__expandedNames = []
+        self.__currentItemName = ""
+        self.__signalsConnected = False
+    
+    def setActive(self, active):
+        """
+        Public method to activate or deactivate the outline view.
+        
+        @param active flag indicating the requested action
+        @type bool
+        """
+        if active and not self.__signalsConnected:
+            editor = self.__model.editor()
+            editor.refreshed.connect(self.repopulate)
+            editor.languageChanged.connect(self.__editorLanguageChanged)
+            editor.editorRenamed.connect(self.__editorRenamed)
+            editor.cursorLineChanged.connect(self.__editorCursorLineChanged)
+            
+            self.__model.repopulate()
+            self.__resizeColumns()
+            
+            line, _ = editor.getCursorPosition()
+            self.__editorCursorLineChanged(line)
+        
+        elif not active and self.__signalsConnected:
+            editor = self.__model.editor()
+            editor.refreshed.disconnect(self.repopulate)
+            editor.languageChanged.disconnect(self.__editorLanguageChanged)
+            editor.editorRenamed.disconnect(self.__editorRenamed)
+            editor.cursorLineChanged.disconnect(self.__editorCursorLineChanged)
+            
+            self.__model.clear()
+    
+    @pyqtSlot()
+    def __resizeColumns(self):
+        """
+        Private slot to resize the view when items get expanded or collapsed.
+        """
+        self.resizeColumnToContents(0)
+    
+    def isPopulated(self):
+        """
+        Public method to check, if the model is populated.
+        
+        @return flag indicating a populated model
+        @rtype bool
+        """
+        return self.__model.isPopulated()
+    
+    @pyqtSlot()
+    def repopulate(self):
+        """
+        Public slot to repopulate the model.
+        """
+        if self.isPopulated():
+            self.__prepareRepopulate()
+            self.__model.repopulate()
+            self.__completeRepopulate()
+    
+    @pyqtSlot()
+    def __prepareRepopulate(self):
+        """
+        Private slot to prepare to repopulate the outline view.
+        """
+        itm = self.__currentItem()
+        if itm is not None:
+            self.__currentItemName = itm.data(0)
+            
+        self.__expandedNames = []
+        
+        childIndex = self.model().index(0, 0)
+        while childIndex.isValid():
+            if self.isExpanded(childIndex):
+                self.__expandedNames.append(
+                    self.model().item(childIndex).data(0))
+            childIndex = self.indexBelow(childIndex)
+    
+    @pyqtSlot()
+    def __completeRepopulate(self):
+        """
+        Private slot to complete the repopulate of the outline view.
+        """
+        childIndex = self.model().index(0, 0)
+        while childIndex.isValid():
+            name = self.model().item(childIndex).data(0)
+            if (self.__currentItemName and self.__currentItemName == name):
+                self.setCurrentIndex(childIndex)
+            if name in self.__expandedNames:
+                self.setExpanded(childIndex, True)
+            childIndex = self.indexBelow(childIndex)
+        self.__resizeColumns()
+        
+        self.__expandedNames = []
+        self.__currentItemName = ""
+    
+    def isSupportedLanguage(self, language):
+        """
+        Public method to check, if outlining a given language is supported.
+        
+        @param language source language to be checked
+        @type str
+        @return flag indicating support
+        @rtype bool
+        """
+        return language in EditorOutlineModel.SupportedLanguages
+    
+    @pyqtSlot(QModelIndex)
+    def __gotoItem(self, index):
+        """
+        Private slot to set the editor cursor.
+        
+        @param index index of the item to set the cursor for
+        @type QModelIndex
+        """
+        if index.isValid():
+            itm = self.model().item(index)
+            if itm:
+                with contextlib.suppress(AttributeError):
+                    lineno = itm.lineno()
+                    self.__model.editor().gotoLine(lineno)
+    
+    def mouseDoubleClickEvent(self, mouseEvent):
+        """
+        Protected method of QAbstractItemView.
+        
+        Reimplemented to disable expanding/collapsing of items when
+        double-clicking. Instead the double-clicked entry is opened.
+        
+        @param mouseEvent the mouse event (QMouseEvent)
+        """
+        index = self.indexAt(mouseEvent.position().toPoint())
+        if index.isValid():
+            itm = self.model().item(index)
+            if isinstance(itm, (BrowserImportsItem, BrowserGlobalsItem)):
+                self.setExpanded(index, not self.isExpanded(index))
+            else:
+                self.__gotoItem(index)
+        
+    def __currentItem(self):
+        """
+        Private method to get a reference to the current item.
+        
+        @return reference to the current item
+        @rtype BrowserItem
+        """
+        itm = self.model().item(self.currentIndex())
+        return itm
+    
+    #######################################################################
+    ## Context menu methods below
+    #######################################################################
+    
+    def __createPopupMenus(self):
+        """
+        Private method to generate the various popup menus.
+        """
+        # create the popup menu for general use
+        self.__menu = QMenu(self)
+        self.__menu.addAction(
+            QCoreApplication.translate('EditorOutlineView', 'Goto'),
+            self.__goto)
+        self.__menu.addSeparator()
+        self.__menu.addAction(
+            QCoreApplication.translate('EditorOutlineView', 'Refresh'),
+            self.repopulate)
+        self.__menu.addSeparator()
+        self.__menu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Copy Path to Clipboard'),
+            self.__copyToClipboard)
+        self.__menu.addSeparator()
+        self.__menu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Expand All'),
+            lambda: self.expandToDepth(-1))
+        self.__menu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Collapse All'),
+            self.collapseAll)
+        self.__menu.addSeparator()
+        self.__menu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Increment Width'),
+            self.__incWidth)
+        self.__decWidthAct = self.__menu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Decrement Width'),
+            self.__decWidth)
+        self.__menu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Set to Default Width'),
+            self.__defaultWidth)
+        self.__menu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Change Default Width'),
+            self.__changeDefaultWidth)
+        
+        # create the attribute/import menu
+        self.__gotoMenu = QMenu(
+            QCoreApplication.translate('EditorOutlineView', "Goto"),
+            self)
+        self.__gotoMenu.aboutToShow.connect(self.__showGotoMenu)
+        self.__gotoMenu.triggered.connect(self.__gotoAttribute)
+        
+        self.__attributeMenu = QMenu(self)
+        self.__attributeMenu.addMenu(self.__gotoMenu)
+        self.__attributeMenu.addSeparator()
+        self.__attributeMenu.addAction(
+            QCoreApplication.translate('EditorOutlineView', 'Refresh'),
+            self.repopulate)
+        self.__attributeMenu.addSeparator()
+        self.__attributeMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Copy Path to Clipboard'),
+            self.__copyToClipboard)
+        self.__attributeMenu.addSeparator()
+        self.__attributeMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Expand All'),
+            lambda: self.expandToDepth(-1))
+        self.__attributeMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Collapse All'),
+            self.collapseAll)
+        self.__attributeMenu.addSeparator()
+        self.__attributeMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Increment Width'),
+            self.__incWidth)
+        self.__attributeDecWidthAct = self.__attributeMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Decrement Width'),
+            self.__decWidth)
+        self.__attributeMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Set to Default Width'),
+            self.__defaultWidth)
+        self.__attributeMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Change Default Width'),
+            self.__changeDefaultWidth)
+        
+        # create the background menu
+        self.__backMenu = QMenu(self)
+        self.__backMenu.addAction(
+            QCoreApplication.translate('EditorOutlineView', 'Refresh'),
+            self.repopulate)
+        self.__backMenu.addSeparator()
+        self.__backMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Copy Path to Clipboard'),
+            self.__copyToClipboard)
+        self.__backMenu.addSeparator()
+        self.__backMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Expand All'),
+            lambda: self.expandToDepth(-1))
+        self.__backMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Collapse All'),
+            self.collapseAll)
+        self.__backMenu.addSeparator()
+        self.__backMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Increment Width'),
+            self.__incWidth)
+        self.__backDecWidthAct = self.__backMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Decrement Width'),
+            self.__decWidth)
+        self.__backMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Set to Default Width'),
+            self.__defaultWidth)
+        self.__backMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Change Default Width'),
+            self.__changeDefaultWidth)
+    
+    @pyqtSlot(QPoint)
+    def __contextMenuRequested(self, coord):
+        """
+        Private slot to show the context menu.
+        
+        @param coord position of the mouse pointer
+        @type QPoint
+        """
+        index = self.indexAt(coord)
+        coord = self.mapToGlobal(coord)
+        
+        decWidthEnable = (
+            self.maximumWidth() !=
+            2 * Preferences.getEditor("SourceOutlineStepSize")
+        )
+        
+        if index.isValid():
+            self.setCurrentIndex(index)
+            
+            itm = self.model().item(index)
+            if isinstance(
+                itm, (BrowserClassAttributeItem, BrowserImportItem)
+            ):
+                self.__attributeDecWidthAct.setEnabled(decWidthEnable)
+                self.__attributeMenu.popup(coord)
+            else:
+                self.__decWidthAct.setEnabled(decWidthEnable)
+                self.__menu.popup(coord)
+        else:
+            self.__backDecWidthAct.setEnabled(decWidthEnable)
+            self.__backMenu.popup(coord)
+    
+    @pyqtSlot()
+    def __showGotoMenu(self):
+        """
+        Private slot to prepare the goto submenu of the attribute menu.
+        """
+        self.__gotoMenu.clear()
+        
+        itm = self.model().item(self.currentIndex())
+        try:
+            linenos = itm.linenos()
+        except AttributeError:
+            try:
+                linenos = [itm.lineno()]
+            except AttributeError:
+                return
+        
+        for lineno in sorted(linenos):
+            act = self.__gotoMenu.addAction(
+                QCoreApplication.translate(
+                    'EditorOutlineView', "Line {0}").format(lineno))
+            act.setData(lineno)
+    
+    #######################################################################
+    ## Context menu handlers below
+    #######################################################################
+    
+    @pyqtSlot()
+    def __gotoAttribute(self, act):
+        """
+        Private slot to handle the selection of the goto menu.
+        
+        @param act reference to the action (EricAction)
+        """
+        lineno = act.data()
+        self.__model.editor().gotoLine(lineno)
+    
+    @pyqtSlot()
+    def __goto(self):
+        """
+        Private slot to move the editor cursor to the line of the context item.
+        """
+        self.__gotoItem(self.currentIndex())
+    
+    @pyqtSlot()
+    def __copyToClipboard(self):
+        """
+        Private slot to copy the file name of the editor to the clipboard.
+        """
+        fn = self.__model.fileName()
+        
+        if fn:
+            cb = QApplication.clipboard()
+            cb.setText(fn)
+    
+    @pyqtSlot()
+    def __incWidth(self):
+        """
+        Private slot to increment the width of the outline.
+        """
+        self.setMaximumWidth(
+            self.maximumWidth() +
+            Preferences.getEditor("SourceOutlineStepSize")
+        )
+        self.updateGeometry()
+    
+    @pyqtSlot()
+    def __decWidth(self):
+        """
+        Private slot to decrement the width of the outline.
+        """
+        stepSize = Preferences.getEditor("SourceOutlineStepSize")
+        newWidth = self.maximumWidth() - stepSize
+        
+        self.setMaximumWidth(max(newWidth, 2 * stepSize))
+        self.updateGeometry()
+    
+    @pyqtSlot()
+    def __defaultWidth(self):
+        """
+        Private slot to set the outline to the default width.
+        """
+        self.setMaximumWidth(Preferences.getEditor("SourceOutlineWidth"))
+        self.updateGeometry()
+    
+    @pyqtSlot()
+    def __changeDefaultWidth(self):
+        """
+        Private slot to open a dialog to change the default width and step
+        size presetting the width with the current value.
+        """
+        from .EditorOutlineSizesDialog import EditorOutlineSizesDialog
+        
+        stepSize = Preferences.getEditor("SourceOutlineStepSize")
+        defaultWidth = Preferences.getEditor("SourceOutlineWidth")
+        currentWidth = self.maximumWidth()
+        
+        dlg = EditorOutlineSizesDialog(currentWidth, defaultWidth, stepSize)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            newDefaultWidth, stepSize = dlg.getSizes()
+            
+            Preferences.setEditor("SourceOutlineWidth", newDefaultWidth)
+            Preferences.setEditor("SourceOutlineStepSize", stepSize)
+            
+            if newDefaultWidth != currentWidth:
+                self.__defaultWidth()
+    
+    #######################################################################
+    ## Methods handling editor signals below
+    #######################################################################
+    
+    @pyqtSlot()
+    def __editorLanguageChanged(self):
+        """
+        Private slot handling a change of the associated editors source code
+        language.
+        """
+        self.__model.repopulate()
+        self.__resizeColumns()
+    
+    @pyqtSlot()
+    def __editorRenamed(self):
+        """
+        Private slot handling a renaming of the associated editor.
+        """
+        self.__model.repopulate()
+        self.__resizeColumns()
+    
+    @pyqtSlot(int)
+    def __editorCursorLineChanged(self, lineno):
+        """
+        Private method to highlight a node given its line number.
+        
+        @param lineno zero based line number of the item
+        @type int
+        """
+        sindex = self.__model.itemIndexByLine(lineno + 1)
+        if sindex.isValid():
+            index = self.model().mapFromSource(sindex)
+            if index.isValid():
+                self.setCurrentIndex(index)
+                self.scrollTo(index)
+        else:
+            self.setCurrentIndex(QModelIndex())

eric ide

mercurial