diff -r 3fc8dfeb6ebe -r b99e7fd55fd3 src/eric7/QScintilla/EditorAssembly.py --- /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