src/eric7/QScintilla/EditorAssembly.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/EditorAssembly.py	Thu Jul 07 11:23:56 2022 +0200
@@ -0,0 +1,467 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2011 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the editor assembly widget containing the navigation
+combos and the editor widget.
+"""
+
+import contextlib
+
+from PyQt6.QtCore import QTimer
+from PyQt6.QtWidgets import QWidget, QGridLayout, QComboBox
+
+from EricWidgets.EricApplication import ericApp
+
+import UI.PixmapCache
+import Preferences
+
+
+class EditorAssembly(QWidget):
+    """
+    Class implementing the editor assembly widget containing the navigation
+    combos and the editor widget.
+    """
+    def __init__(self, dbs, fn="", vm=None, filetype="", editor=None,
+                 tv=None):
+        """
+        Constructor
+        
+        @param dbs reference to the debug server object
+        @type DebugServer
+        @param fn name of the file to be opened. If it is None,
+            a new (empty) editor is opened.
+        @type str
+        @param vm reference to the view manager object
+        @type ViewManager.ViewManager
+        @param filetype type of the source file
+        @type str
+        @param editor reference to an Editor object, if this is a cloned view
+        @type Editor
+        @param tv reference to the task viewer object
+        @type TaskViewer
+        """
+        super().__init__()
+        
+        self.__layout = QGridLayout(self)
+        self.__layout.setContentsMargins(0, 0, 0, 0)
+        self.__layout.setSpacing(1)
+        
+        from .EditorButtonsWidget import EditorButtonsWidget
+        from .Editor import Editor
+        from .EditorOutline import EditorOutlineView
+        
+        self.__showOutline = Preferences.getEditor("ShowSourceOutline")
+        
+        self.__editor = Editor(dbs, fn, vm, filetype, editor, tv)
+        self.__buttonsWidget = EditorButtonsWidget(self.__editor, self)
+        self.__globalsCombo = QComboBox()
+        self.__globalsCombo.setDuplicatesEnabled(True)
+        self.__membersCombo = QComboBox()
+        self.__membersCombo.setDuplicatesEnabled(True)
+        self.__sourceOutline = EditorOutlineView(
+            self.__editor, populate=self.__showOutline)
+        self.__sourceOutline.setMaximumWidth(
+            Preferences.getEditor("SourceOutlineWidth"))
+        
+        self.__layout.addWidget(self.__buttonsWidget, 1, 0, -1, 1)
+        self.__layout.addWidget(self.__globalsCombo, 0, 1)
+        self.__layout.addWidget(self.__membersCombo, 0, 2)
+        self.__layout.addWidget(self.__editor, 1, 1, 1, 2)
+        self.__layout.addWidget(self.__sourceOutline, 0, 3, -1, -1)
+        
+        self.setFocusProxy(self.__editor)
+        
+        self.__module = None
+        
+        self.__shutdownTimerCalled = False
+        self.__parseTimer = QTimer(self)
+        self.__parseTimer.setSingleShot(True)
+        self.__parseTimer.setInterval(5 * 1000)
+        self.__editor.textChanged.connect(self.__resetParseTimer)
+        self.__editor.refreshed.connect(self.__resetParseTimer)
+        
+        self.__selectedGlobal = ""
+        self.__selectedMember = ""
+        self.__globalsBoundaries = {}
+        self.__membersBoundaries = {}
+        
+        self.__activateOutline(self.__showOutline)
+        self.__activateCombos(not self.__showOutline)
+        
+        ericApp().getObject("UserInterface").preferencesChanged.connect(
+            self.__preferencesChanged)
+    
+    def shutdownTimer(self):
+        """
+        Public method to stop and disconnect the timer.
+        """
+        self.__parseTimer.stop()
+        if not self.__shutdownTimerCalled:
+            self.__editor.textChanged.disconnect(self.__resetParseTimer)
+            self.__editor.refreshed.disconnect(self.__resetParseTimer)
+            self.__shutdownTimerCalled = True
+    
+    def getEditor(self):
+        """
+        Public method to get the reference to the editor widget.
+        
+        @return reference to the editor widget
+        @rtype Editor
+        """
+        return self.__editor
+    
+    def __preferencesChanged(self):
+        """
+        Private slot handling a change of preferences.
+        """
+        showOutline = Preferences.getEditor("ShowSourceOutline")
+        if showOutline != self.__showOutline:
+            self.__showOutline = showOutline
+            self.__activateOutline(self.__showOutline)
+            self.__activateCombos(not self.__showOutline)
+    
+    #######################################################################
+    ## Methods dealing with the navigation combos below
+    #######################################################################
+    
+    def __activateCombos(self, activate):
+        """
+        Private slot to activate the navigation combo boxes.
+        
+        @param activate flag indicating to activate the combo boxes
+        @type bool
+        """
+        self.__globalsCombo.setVisible(activate)
+        self.__membersCombo.setVisible(activate)
+        if activate:
+            self.__globalsCombo.activated[int].connect(
+                self.__globalsActivated)
+            self.__membersCombo.activated[int].connect(
+                self.__membersActivated)
+            self.__editor.cursorLineChanged.connect(
+                self.__editorCursorLineChanged)
+            self.__parseTimer.timeout.connect(self.__parseEditor)
+            
+            self.__parseEditor()
+            
+            line, _ = self.__editor.getCursorPosition()
+            self.__editorCursorLineChanged(line)
+        else:
+            with contextlib.suppress(TypeError):
+                self.__globalsCombo.activated[int].disconnect(
+                    self.__globalsActivated)
+                self.__membersCombo.activated[int].disconnect(
+                    self.__membersActivated)
+                self.__editor.cursorLineChanged.disconnect(
+                    self.__editorCursorLineChanged)
+                self.__parseTimer.timeout.disconnect(self.__parseEditor)
+            
+            self.__globalsCombo.clear()
+            self.__membersCombo.clear()
+            self.__globalsBoundaries = {}
+            self.__membersBoundaries = {}
+    
+    def __globalsActivated(self, index, moveCursor=True):
+        """
+        Private method to jump to the line of the selected global entry and to
+        populate the members combo box.
+        
+        @param index index of the selected entry
+        @type int
+        @param moveCursor flag indicating to move the editor cursor
+        @type bool
+        """
+        # step 1: go to the line of the selected entry
+        lineno = self.__globalsCombo.itemData(index)
+        if lineno is not None:
+            if moveCursor:
+                txt = self.__editor.text(lineno - 1).rstrip()
+                pos = len(txt.replace(txt.strip(), ""))
+                self.__editor.gotoLine(
+                    lineno, pos if pos == 0 else pos + 1, True)
+                self.__editor.setFocus()
+            
+            # step 2: populate the members combo, if the entry is a class
+            self.__membersCombo.clear()
+            self.__membersBoundaries = {}
+            self.__membersCombo.addItem("")
+            memberIndex = 0
+            entryName = self.__globalsCombo.itemText(index)
+            if self.__module:
+                if entryName in self.__module.classes:
+                    entry = self.__module.classes[entryName]
+                elif entryName in self.__module.modules:
+                    entry = self.__module.modules[entryName]
+                    # step 2.0: add module classes
+                    items = []
+                    for cl in entry.classes.values():
+                        if cl.isPrivate():
+                            icon = UI.PixmapCache.getIcon("class_private")
+                        elif cl.isProtected():
+                            icon = UI.PixmapCache.getIcon(
+                                "class_protected")
+                        else:
+                            icon = UI.PixmapCache.getIcon("class")
+                        items.append((icon, cl.name, cl.lineno, cl.endlineno))
+                    for itm in sorted(items, key=lambda x: (x[1], x[2])):
+                        self.__membersCombo.addItem(itm[0], itm[1], itm[2])
+                        memberIndex += 1
+                        self.__membersBoundaries[(itm[2], itm[3])] = (
+                            memberIndex
+                        )
+                else:
+                    return
+                
+                # step 2.1: add class methods
+                from Utilities.ModuleParser import Function
+                items = []
+                for meth in entry.methods.values():
+                    if meth.modifier == Function.Static:
+                        icon = UI.PixmapCache.getIcon("method_static")
+                    elif meth.modifier == Function.Class:
+                        icon = UI.PixmapCache.getIcon("method_class")
+                    elif meth.isPrivate():
+                        icon = UI.PixmapCache.getIcon("method_private")
+                    elif meth.isProtected():
+                        icon = UI.PixmapCache.getIcon("method_protected")
+                    else:
+                        icon = UI.PixmapCache.getIcon("method")
+                    items.append(
+                        (icon, meth.name, meth.lineno, meth.endlineno)
+                    )
+                for itm in sorted(items, key=lambda x: (x[1], x[2])):
+                    self.__membersCombo.addItem(itm[0], itm[1], itm[2])
+                    memberIndex += 1
+                    self.__membersBoundaries[(itm[2], itm[3])] = memberIndex
+                
+                # step 2.2: add class instance attributes
+                items = []
+                for attr in entry.attributes.values():
+                    if attr.isPrivate():
+                        icon = UI.PixmapCache.getIcon("attribute_private")
+                    elif attr.isProtected():
+                        icon = UI.PixmapCache.getIcon(
+                            "attribute_protected")
+                    else:
+                        icon = UI.PixmapCache.getIcon("attribute")
+                    items.append((icon, attr.name, attr.lineno))
+                for itm in sorted(items, key=lambda x: (x[1], x[2])):
+                    self.__membersCombo.addItem(itm[0], itm[1], itm[2])
+                
+                # step 2.3: add class attributes
+                items = []
+                icon = UI.PixmapCache.getIcon("attribute_class")
+                for globalVar in entry.globals.values():
+                    items.append((icon, globalVar.name, globalVar.lineno))
+                for itm in sorted(items, key=lambda x: (x[1], x[2])):
+                    self.__membersCombo.addItem(itm[0], itm[1], itm[2])
+    
+    def __membersActivated(self, index, moveCursor=True):
+        """
+        Private method to jump to the line of the selected members entry.
+        
+        @param index index of the selected entry
+        @type int
+        @param moveCursor flag indicating to move the editor cursor
+        @type bool
+        """
+        lineno = self.__membersCombo.itemData(index)
+        if lineno is not None and moveCursor:
+            txt = self.__editor.text(lineno - 1).rstrip()
+            pos = len(txt.replace(txt.strip(), ""))
+            self.__editor.gotoLine(lineno, pos if pos == 0 else pos + 1,
+                                   firstVisible=True, expand=True)
+            self.__editor.setFocus()
+    
+    def __resetParseTimer(self):
+        """
+        Private slot to reset the parse timer.
+        """
+        self.__parseTimer.stop()
+        self.__parseTimer.start()
+    
+    def __parseEditor(self):
+        """
+        Private method to parse the editor source and repopulate the globals
+        combo.
+        """
+        from Utilities.ModuleParser import Module, getTypeFromTypeName
+        
+        self.__module = None
+        sourceType = getTypeFromTypeName(self.__editor.determineFileType())
+        if sourceType != -1:
+            src = self.__editor.text()
+            if src:
+                fn = self.__editor.getFileName()
+                if fn is None:
+                    fn = ""
+                self.__module = Module("", fn, sourceType)
+                self.__module.scan(src)
+                
+                # remember the current selections
+                self.__selectedGlobal = self.__globalsCombo.currentText()
+                self.__selectedMember = self.__membersCombo.currentText()
+                
+                self.__globalsCombo.clear()
+                self.__membersCombo.clear()
+                self.__globalsBoundaries = {}
+                self.__membersBoundaries = {}
+                
+                self.__globalsCombo.addItem("")
+                index = 0
+                
+                # step 1: add modules
+                items = []
+                for module in self.__module.modules.values():
+                    items.append(
+                        (UI.PixmapCache.getIcon("module"), module.name,
+                         module.lineno, module.endlineno)
+                    )
+                for itm in sorted(items, key=lambda x: (x[1], x[2])):
+                    self.__globalsCombo.addItem(itm[0], itm[1], itm[2])
+                    index += 1
+                    self.__globalsBoundaries[(itm[2], itm[3])] = index
+                
+                # step 2: add classes
+                items = []
+                for cl in self.__module.classes.values():
+                    if cl.isPrivate():
+                        icon = UI.PixmapCache.getIcon("class_private")
+                    elif cl.isProtected():
+                        icon = UI.PixmapCache.getIcon("class_protected")
+                    else:
+                        icon = UI.PixmapCache.getIcon("class")
+                    items.append(
+                        (icon, cl.name, cl.lineno, cl.endlineno)
+                    )
+                for itm in sorted(items, key=lambda x: (x[1], x[2])):
+                    self.__globalsCombo.addItem(itm[0], itm[1], itm[2])
+                    index += 1
+                    self.__globalsBoundaries[(itm[2], itm[3])] = index
+                
+                # step 3: add functions
+                items = []
+                for func in self.__module.functions.values():
+                    if func.isPrivate():
+                        icon = UI.PixmapCache.getIcon("method_private")
+                    elif func.isProtected():
+                        icon = UI.PixmapCache.getIcon("method_protected")
+                    else:
+                        icon = UI.PixmapCache.getIcon("method")
+                    items.append(
+                        (icon, func.name, func.lineno, func.endlineno)
+                    )
+                for itm in sorted(items, key=lambda x: (x[1], x[2])):
+                    self.__globalsCombo.addItem(itm[0], itm[1], itm[2])
+                    index += 1
+                    self.__globalsBoundaries[(itm[2], itm[3])] = index
+                
+                # step 4: add attributes
+                items = []
+                for globalValue in self.__module.globals.values():
+                    if globalValue.isPrivate():
+                        icon = UI.PixmapCache.getIcon("attribute_private")
+                    elif globalValue.isProtected():
+                        icon = UI.PixmapCache.getIcon(
+                            "attribute_protected")
+                    else:
+                        icon = UI.PixmapCache.getIcon("attribute")
+                    items.append(
+                        (icon, globalValue.name, globalValue.lineno)
+                    )
+                for itm in sorted(items, key=lambda x: (x[1], x[2])):
+                    self.__globalsCombo.addItem(itm[0], itm[1], itm[2])
+                
+                # reset the currently selected entries without moving the
+                # text cursor
+                index = self.__globalsCombo.findText(self.__selectedGlobal)
+                if index != -1:
+                    self.__globalsCombo.setCurrentIndex(index)
+                    self.__globalsActivated(index, moveCursor=False)
+                index = self.__membersCombo.findText(self.__selectedMember)
+                if index != -1:
+                    self.__membersCombo.setCurrentIndex(index)
+                    self.__membersActivated(index, moveCursor=False)
+        else:
+            self.__globalsCombo.clear()
+            self.__membersCombo.clear()
+            self.__globalsBoundaries = {}
+            self.__membersBoundaries = {}
+    
+    def __editorCursorLineChanged(self, lineno):
+        """
+        Private slot handling a line change of the cursor of the editor.
+        
+        @param lineno line number of the cursor
+        @type int
+        """
+        lineno += 1     # cursor position is zero based, code info one based
+        
+        # step 1: search in the globals
+        indexFound = 0
+        for (lower, upper), index in self.__globalsBoundaries.items():
+            if upper == -1:
+                upper = 1000000     # it is the last line
+            if lower <= lineno <= upper:
+                indexFound = index
+                break
+        self.__globalsCombo.setCurrentIndex(indexFound)
+        self.__globalsActivated(indexFound, moveCursor=False)
+        
+        # step 2: search in members
+        indexFound = 0
+        for (lower, upper), index in self.__membersBoundaries.items():
+            if upper == -1:
+                upper = 1000000     # it is the last line
+            if lower <= lineno <= upper:
+                indexFound = index
+                break
+        self.__membersCombo.setCurrentIndex(indexFound)
+        self.__membersActivated(indexFound, moveCursor=False)
+    
+    #######################################################################
+    ## Methods dealing with the source outline below
+    #######################################################################
+    
+    def __activateOutline(self, activate):
+        """
+        Private slot to activate the source outline view.
+        
+        @param activate flag indicating to activate the source outline view
+        @type bool
+        """
+        self.__sourceOutline.setActive(activate)
+        
+        if activate:
+            self.__sourceOutline.setVisible(
+                self.__sourceOutline.isSupportedLanguage(
+                    self.__editor.getLanguage()
+                )
+            )
+            
+            self.__parseTimer.timeout.connect(self.__sourceOutline.repopulate)
+            self.__editor.languageChanged.connect(self.__editorChanged)
+            self.__editor.editorRenamed.connect(self.__editorChanged)
+        else:
+            self.__sourceOutline.hide()
+            
+            with contextlib.suppress(TypeError):
+                self.__parseTimer.timeout.disconnect(
+                    self.__sourceOutline.repopulate)
+                self.__editor.languageChanged.disconnect(self.__editorChanged)
+                self.__editor.editorRenamed.disconnect(self.__editorChanged)
+    
+    def __editorChanged(self):
+        """
+        Private slot handling changes of the editor language or file name.
+        """
+        supported = self.__sourceOutline.isSupportedLanguage(
+            self.__editor.getLanguage())
+        
+        self.__sourceOutline.setVisible(supported)
+
+#
+# eflag: noqa = Y113

eric ide

mercurial