""" /*************************************************************************** Python Console for QGIS ------------------- begin : 2012-09-10 copyright : (C) 2012 by Salvatore Larosa email : lrssvtml (at) gmail (dot) com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ Some portions of code were taken from https://code.google.com/p/pydee/ """ from __future__ import annotations import codecs import importlib import os import pyclbr import re import sys import tempfile from typing import Optional, TYPE_CHECKING from functools import partial from operator import itemgetter from pathlib import Path from qgis.core import Qgis, QgsApplication, QgsBlockingNetworkRequest, QgsSettings from qgis.gui import ( QgsCodeEditorPython, QgsCodeEditorWidget, QgsGui, QgsMessageBar, QgsShortcutsManager, ) from qgis.PyQt.Qsci import QsciScintilla from qgis.PyQt.QtCore import ( pyqtSignal, QByteArray, QCoreApplication, QDir, QEvent, QFileInfo, QJsonDocument, QSize, Qt, QUrl, ) from qgis.PyQt.QtGui import QKeySequence, QColor, QPalette from qgis.PyQt.QtNetwork import QNetworkRequest from qgis.PyQt.QtWidgets import ( QAction, QApplication, QFileDialog, QFrame, QGridLayout, QLabel, QMenu, QMessageBox, QShortcut, QSizePolicy, QSpacerItem, QTabWidget, QToolButton, QTreeWidgetItem, QWidget, ) from qgis.utils import OverrideCursor, iface if TYPE_CHECKING: from .console import PythonConsoleWidget class Editor(QgsCodeEditorPython): trigger_find = pyqtSignal() def __init__( self, editor_tab: EditorTab, console_widget: PythonConsoleWidget, tab_widget: EditorTabWidget, ): super().__init__(editor_tab) self.editor_tab: EditorTab = editor_tab self.console_widget: PythonConsoleWidget = console_widget self.tab_widget: EditorTabWidget = tab_widget self.code_editor_widget: QgsCodeEditorWidget | None = None self.setMinimumHeight(120) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) # Disable default scintilla shortcuts ctrl, shift = self.SCMOD_CTRL << 16, self.SCMOD_SHIFT << 16 self.SendScintilla( QsciScintilla.SCI_CLEARCMDKEY, ord("T") + ctrl ) # Switch current line with the next one self.SendScintilla( QsciScintilla.SCI_CLEARCMDKEY, ord("D") + ctrl ) # Duplicate current line / selection self.SendScintilla( QsciScintilla.SCI_CLEARCMDKEY, ord("L") + ctrl ) # Delete current line self.SendScintilla(QsciScintilla.SCI_CLEARCMDKEY, ord("L") + ctrl + shift) # New QShortcut = ctrl+space/ctrl+alt+space for Autocomplete self.newShortcutCS = QShortcut( QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_Space), self ) self.newShortcutCS.setContext(Qt.ShortcutContext.WidgetShortcut) self.redoScut = QShortcut( QKeySequence(Qt.Modifier.CTRL | Qt.Modifier.SHIFT | Qt.Key.Key_Z), self ) self.redoScut.setContext(Qt.ShortcutContext.WidgetShortcut) self.redoScut.activated.connect(self.redo) self.newShortcutCS.activated.connect(self.autoComplete) self.runScriptScut = QShortcut( QKeySequence(Qt.Modifier.SHIFT | Qt.Modifier.CTRL | Qt.Key.Key_E), self ) self.runScriptScut.setContext(Qt.ShortcutContext.WidgetShortcut) self.runScriptScut.activated.connect(self.runScriptCode) self.syntaxCheckScut = QShortcut( QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_4), self ) self.syntaxCheckScut.setContext(Qt.ShortcutContext.WidgetShortcut) self.syntaxCheckScut.activated.connect(self.syntaxCheck) self.modificationChanged.connect(self.editor_tab.modified) self.modificationAttempted.connect(self.fileReadOnly) def showApiDocumentation(self, text): self.console_widget.shell.showApiDocumentation(text) def set_code_editor_widget(self, widget: QgsCodeEditorWidget): self.code_editor_widget = widget self.code_editor_widget.loadedExternalChanges.connect( self.loaded_external_changes ) def settingsEditor(self): # Set Python lexer self.initializeLexer() def contextMenuEvent(self, e): menu = QMenu(self) menu.addAction( QCoreApplication.translate("PythonConsole", "Hide Editor"), self.hideEditor ) menu.addSeparator() syntaxCheckAction = QAction( QgsApplication.getThemeIcon("console/iconSyntaxErrorConsole.svg"), QCoreApplication.translate("PythonConsole", "Check Syntax"), menu, ) syntaxCheckAction.triggered.connect(self.syntaxCheck) syntaxCheckAction.setShortcut("Ctrl+4") menu.addAction(syntaxCheckAction) word = self.selectedText() or self.wordAtPoint(e.pos()) if word: context_help_action = QAction( QgsApplication.getThemeIcon("mActionHelpContents.svg"), QCoreApplication.translate("PythonConsole", "Context Help"), menu, ) context_help_action.triggered.connect( partial( self.console_widget.shell.showApiDocumentation, word, force_search=True, ) ) context_help_action.setShortcut(QKeySequence.StandardKey.HelpContents) menu.addAction(context_help_action) run_selection_action = QAction(menu) run_selection_action.setIcon( QgsApplication.getThemeIcon("mActionRunSelected.svg"), ) run_selection_action.triggered.connect(self.runSelectedCode) QgsGui.shortcutsManager().initializeCommonAction( run_selection_action, QgsShortcutsManager.CommonAction.CodeRunSelection, ) menu.addAction(run_selection_action) start_action = QAction(self) start_action.setIcon(QgsApplication.getThemeIcon("mActionStart.svg")) start_action.triggered.connect(self.runScriptCode) QgsGui.shortcutsManager().initializeCommonAction( start_action, QgsShortcutsManager.CommonAction.CodeRunScript, ) menu.addAction(start_action) menu.addSeparator() undoAction = QAction( QgsApplication.getThemeIcon("mActionUndo.svg"), QCoreApplication.translate("PythonConsole", "Undo"), menu, ) undoAction.triggered.connect(self.undo) undoAction.setShortcut(QKeySequence.StandardKey.Undo) menu.addAction(undoAction) redoAction = QAction( QgsApplication.getThemeIcon("mActionRedo.svg"), QCoreApplication.translate("PythonConsole", "Redo"), menu, ) redoAction.triggered.connect(self.redo) redoAction.setShortcut("Ctrl+Shift+Z") menu.addAction(redoAction) menu.addSeparator() find_action = QAction( QgsApplication.getThemeIcon("console/iconSearchEditorConsole.svg"), QCoreApplication.translate("PythonConsole", "Find Text"), menu, ) find_action.triggered.connect(self.trigger_find) find_action.setShortcut("Ctrl+F") menu.addAction(find_action) cutAction = QAction( QgsApplication.getThemeIcon("mActionEditCut.svg"), QCoreApplication.translate("PythonConsole", "Cut"), menu, ) cutAction.triggered.connect(self.cut) cutAction.setShortcut(QKeySequence.StandardKey.Cut) menu.addAction(cutAction) copyAction = QAction( QgsApplication.getThemeIcon("mActionEditCopy.svg"), QCoreApplication.translate("PythonConsole", "Copy"), menu, ) copyAction.triggered.connect(self.copy) copyAction.setShortcut(QKeySequence.StandardKey.Copy) menu.addAction(copyAction) pasteAction = QAction( QgsApplication.getThemeIcon("mActionEditPaste.svg"), QCoreApplication.translate("PythonConsole", "Paste"), menu, ) pasteAction.triggered.connect(self.paste) pasteAction.setShortcut(QKeySequence.StandardKey.Paste) menu.addAction(pasteAction) selectAllAction = QAction( QCoreApplication.translate("PythonConsole", "Select All"), menu ) selectAllAction.triggered.connect(self.selectAll) selectAllAction.setShortcut(QKeySequence.StandardKey.SelectAll) menu.addAction(selectAllAction) menu.addSeparator() toggle_comment_action = QAction(menu) toggle_comment_action.setIcon( QgsApplication.getThemeIcon( "console/iconCommentEditorConsole.svg", self.palette().color(QPalette.ColorRole.WindowText), ) ) toggle_comment_action.triggered.connect(self.toggleComment) QgsGui.shortcutsManager().initializeCommonAction( toggle_comment_action, QgsShortcutsManager.CommonAction.CodeToggleComment, ) menu.addAction(toggle_comment_action) reformat_code_action = QAction(menu) reformat_code_action.setIcon( QgsApplication.getThemeIcon("console/iconFormatCode.svg") ) reformat_code_action.triggered.connect(self.reformatCode) QgsGui.shortcutsManager().initializeCommonAction( reformat_code_action, QgsShortcutsManager.CommonAction.CodeReformat, ) menu.addAction(reformat_code_action) menu.addSeparator() gist_menu = QMenu(self) gist_menu.setTitle( QCoreApplication.translate("PythonConsole", "Share on GitHub") ) gist_menu.setIcon(QgsApplication.getThemeIcon("console/iconCodepadConsole.svg")) gist_menu.addAction( QCoreApplication.translate("PythonConsole", "Secret Gist"), partial(self.shareOnGist, False), ) gist_menu.addAction( QCoreApplication.translate("PythonConsole", "Public Gist"), partial(self.shareOnGist, True), ) menu.addMenu(gist_menu) showCodeInspection = menu.addAction( QgsApplication.getThemeIcon("console/iconClassBrowserConsole.svg"), QCoreApplication.translate("PythonConsole", "Hide/Show Object Inspector"), self.objectListEditor, ) menu.addSeparator() menu.addAction( QgsApplication.getThemeIcon("console/iconSettingsConsole.svg"), QCoreApplication.translate("PythonConsole", "Options…"), self.console_widget.openSettings, ) syntaxCheckAction.setEnabled(False) pasteAction.setEnabled(False) cutAction.setEnabled(False) run_selection_action.setEnabled(False) copyAction.setEnabled(False) selectAllAction.setEnabled(False) undoAction.setEnabled(False) redoAction.setEnabled(False) showCodeInspection.setEnabled(False) if self.hasSelectedText(): run_selection_action.setEnabled(True) copyAction.setEnabled(True) cutAction.setEnabled(True) if not self.text() == "": selectAllAction.setEnabled(True) syntaxCheckAction.setEnabled(True) if self.isUndoAvailable(): undoAction.setEnabled(True) if self.isRedoAvailable(): redoAction.setEnabled(True) if QApplication.clipboard().text(): pasteAction.setEnabled(True) if QgsSettings().value("pythonConsole/enableObjectInsp", False, type=bool): showCodeInspection.setEnabled(True) menu.exec(self.mapToGlobal(e.pos())) def objectListEditor(self): listObj = self.console_widget.listClassMethod if listObj.isVisible(): listObj.hide() self.console_widget.object_inspector_action.setChecked(False) else: listObj.show() self.console_widget.object_inspector_action.setChecked(True) def shareOnGist(self, is_public): self.code_editor_widget.shareOnGist(is_public) def hideEditor(self): self.console_widget.splitterObj.hide() self.console_widget.show_editor_action.setChecked(False) def createTempFile(self): name = tempfile.NamedTemporaryFile(delete=False).name # Need to use newline='' to avoid adding extra \r characters on Windows with open(name, "w", encoding="utf-8", newline="") as f: f.write(self.text()) return name def runScriptCode(self): autoSave = QgsSettings().value("pythonConsole/autoSaveScript", False, type=bool) filename = self.code_editor_widget.filePath() filename_override = None msgEditorBlank = QCoreApplication.translate( "PythonConsole", "Hey, type something to run!" ) if filename is None: if not self.isModified(): self.showMessage(msgEditorBlank) return deleteTempFile = False if self.syntaxCheck(): if filename and self.isModified() and autoSave: self.save(filename) elif not filename or self.isModified(): # Create a new temp file if the file isn't already saved. filename = self.createTempFile() filename_override = self.tab_widget.tabText( self.tab_widget.currentIndex() ) if filename_override.startswith("*"): filename_override = filename_override[1:] deleteTempFile = True self.console_widget.shell.runFile(filename, filename_override) if deleteTempFile: Path(filename).unlink() def runSelectedCode(self): # spellok cmd = self.selectedText() self.console_widget.shell.insertFromDropPaste(cmd) self.console_widget.shell.entered() self.setFocus() def getTextFromEditor(self): text = self.text() textList = text.split("\n") return textList def goToLine(self, objName, linenr): self.SendScintilla(QsciScintilla.SCI_GOTOLINE, linenr - 1) self.SendScintilla( QsciScintilla.SCI_SETTARGETSTART, self.SendScintilla(QsciScintilla.SCI_GETCURRENTPOS), ) self.SendScintilla(QsciScintilla.SCI_SETTARGETEND, len(self.text())) pos = self.SendScintilla( QsciScintilla.SCI_SEARCHINTARGET, len(objName), objName ) index = pos - self.SendScintilla(QsciScintilla.SCI_GETCURRENTPOS) # line, _ = self.getCursorPosition() self.setSelection(linenr - 1, index, linenr - 1, index + len(objName)) self.ensureLineVisible(linenr) self.setFocus() def syntaxCheck(self): self.code_editor_widget.clearWarnings() source = self.text().encode("utf-8") try: compile(source, "", "exec") except SyntaxError as detail: eline = detail.lineno or 1 eline -= 1 ecolumn = detail.offset or 1 edescr = detail.msg self.code_editor_widget.addWarning(eline, edescr) self.setCursorPosition(eline, ecolumn - 1) self.ensureLineVisible(eline) return False return True def loaded_external_changes(self): self.tab_widget.listObject(self.tab_widget.currentWidget()) def fileReadOnly(self): msgText = QCoreApplication.translate( "PythonConsole", 'The file "{0}" is read only, please save to different file first.', ).format(self.code_editor_widget.filePath()) self.showMessage(msgText) def save(self, filename: str | None = None): if self.isReadOnly(): return if QgsSettings().value("pythonConsole/formatOnSave", False, type=bool): self.reformatCode() index = self.tab_widget.indexOf(self.editor_tab) if not filename and not self.code_editor_widget.filePath(): saveTr = QCoreApplication.translate( "PythonConsole", "Python Console: Save file" ) folder = QgsSettings().value("pythonConsole/lastDirPath", QDir.homePath()) path, filter = QFileDialog().getSaveFileName( self, saveTr, os.path.join( folder, self.tab_widget.tabText(index).replace("*", "") + ".py" ), "Script file (*.py)", ) # If the user didn't select a file, abort the save operation if not path: self.code_editor_widget.setFilePath(None) return filename = path self.code_editor_widget.save(filename) msgText = QCoreApplication.translate( "PythonConsole", "Script was correctly saved." ) self.showMessage(msgText) # Save the new contents # Need to use newline='' to avoid adding extra \r characters on Windows with open( self.code_editor_widget.filePath(), "w", encoding="utf-8", newline="" ) as f: f.write(self.text()) self.tab_widget.setTabTitle( index, Path(self.code_editor_widget.filePath()).name ) self.tab_widget.setTabToolTip(index, self.code_editor_widget.filePath()) self.setModified(False) self.console_widget.save_file_action.setEnabled(False) self.console_widget.updateTabListScript( self.code_editor_widget.filePath(), action="append" ) self.tab_widget.listObject(self.editor_tab) QgsSettings().setValue( "pythonConsole/lastDirPath", Path(self.code_editor_widget.filePath()).parent.as_posix(), ) def event(self, e): """Used to override the Application shortcuts when the editor has focus""" if e.type() == QEvent.Type.ShortcutOverride: ctrl = e.modifiers() == Qt.KeyboardModifier.ControlModifier ctrl_shift = e.modifiers() == ( Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier ) if ( (ctrl and e.key() == Qt.Key.Key_W) or (ctrl_shift and e.key() == Qt.Key.Key_W) or (ctrl and e.key() == Qt.Key.Key_S) or (ctrl_shift and e.key() == Qt.Key.Key_S) or (ctrl and e.key() == Qt.Key.Key_T) or (ctrl and e.key() == Qt.Key.Key_Tab) ): e.accept() return True return super().event(e) def keyPressEvent(self, e): ctrl = e.modifiers() == Qt.KeyboardModifier.ControlModifier ctrl_shift = e.modifiers() == ( Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier ) # Ctrl+W: close current tab if ctrl and e.key() == Qt.Key.Key_W: self.editor_tab.close() # Ctrl+Shift+W: close all tabs if ctrl_shift and e.key() == Qt.Key.Key_W: self.tab_widget.closeAll() # Ctrl+S: save current tab if ctrl and e.key() == Qt.Key.Key_S: self.save() # Ctrl+Shift+S: save current tab as if ctrl_shift and e.key() == Qt.Key.Key_S: self.tab_widget.saveAs() # Ctrl+T: open new tab if ctrl and e.key() == Qt.Key.Key_T: self.tab_widget.newTabEditor() super().keyPressEvent(e) def showMessage( self, text: str, title: str | None = None, level=Qgis.MessageLevel.Info ): self.editor_tab.showMessage(text, level, title=title) class EditorTab(QWidget): search_bar_toggled = pyqtSignal(bool) def __init__( self, tab_widget: EditorTabWidget, console_widget: PythonConsoleWidget, filename: str | None, read_only: bool, ): super().__init__(tab_widget) self.tab_widget: EditorTabWidget = tab_widget self._editor = Editor( editor_tab=self, console_widget=console_widget, tab_widget=tab_widget ) self._editor_code_widget = QgsCodeEditorWidget(self._editor) self._editor.set_code_editor_widget(self._editor_code_widget) self._editor_code_widget.searchBarToggled.connect(self.search_bar_toggled) self._editor.trigger_find.connect(self._editor_code_widget.triggerFind) if filename: if QFileInfo(filename).exists(): self._editor_code_widget.loadFile(filename) if read_only: self._editor.setReadOnly(True) self.tabLayout = QGridLayout(self) self.tabLayout.setContentsMargins(0, 0, 0, 0) self.tabLayout.addWidget(self._editor_code_widget) def set_file_path(self, path: str): self._editor_code_widget.setFilePath(path) def file_path(self) -> str | None: return self._editor_code_widget.filePath() def open_in_external_editor(self): self._editor_code_widget.openInExternalEditor() def modified(self, modified): self.tab_widget.tabModified(self, modified) def search_bar_visible(self) -> bool: """ Returns True if the tab's search bar is visible """ return self._editor_code_widget.isSearchBarVisible() def trigger_find(self): """ Triggers a find operation using the default behavior """ self._editor_code_widget.triggerFind() def hide_search_bar(self): """ Hides the search bar """ self._editor_code_widget.hideSearchBar() def close(self): self.tab_widget._removeTab(self, tab2index=True) def __getattr__(self, name): """Forward all missing attribute requests to the editor.""" try: return super().__getattr__(name) except AttributeError: return getattr(self._editor, name) def __setattr__(self, name, value): """Forward all missing attribute requests to the editor.""" try: return super().__setattr__(name, value) except AttributeError: return setattr(self._editor, name, value) def showMessage(self, text, level=Qgis.MessageLevel.Info, timeout=-1, title=""): self._editor_code_widget.messageBar().pushMessage(title, text, level, timeout) class EditorTabWidget(QTabWidget): search_bar_toggled = pyqtSignal(bool) def __init__(self, console_widget: PythonConsoleWidget): super().__init__(parent=None) self.console_widget: PythonConsoleWidget = console_widget self.idx = -1 # Layout for top frame (restore tabs) self.layoutTopFrame = QGridLayout(self) self.layoutTopFrame.setContentsMargins(0, 0, 0, 0) spacerItem = QSpacerItem( 20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding ) self.layoutTopFrame.addItem(spacerItem, 1, 0, 1, 1) self.topFrame = QFrame(self) self.topFrame.setStyleSheet("background-color: rgb(255, 255, 230);") self.topFrame.setFrameShape(QFrame.Shape.StyledPanel) self.topFrame.setMinimumHeight(24) self.layoutTopFrame2 = QGridLayout(self.topFrame) self.layoutTopFrame2.setContentsMargins(0, 0, 0, 0) label = QCoreApplication.translate( "PythonConsole", "Click on button to restore all tabs from last session." ) self.label = QLabel(label) self.restoreTabsButton = QToolButton() toolTipRestore = QCoreApplication.translate("PythonConsole", "Restore tabs") self.restoreTabsButton.setToolTip(toolTipRestore) self.restoreTabsButton.setIcon( QgsApplication.getThemeIcon("console/iconRestoreTabsConsole.svg") ) self.restoreTabsButton.setIconSize(QSize(24, 24)) self.restoreTabsButton.setAutoRaise(True) self.restoreTabsButton.setCursor(Qt.CursorShape.PointingHandCursor) self.restoreTabsButton.setStyleSheet( "QToolButton:hover{border: none } \ QToolButton:pressed{border: none}" ) self.clButton = QToolButton() toolTipClose = QCoreApplication.translate("PythonConsole", "Close") self.clButton.setToolTip(toolTipClose) self.clButton.setIcon(QgsApplication.getThemeIcon("/mIconClose.svg")) self.clButton.setIconSize(QSize(18, 18)) self.clButton.setCursor(Qt.CursorShape.PointingHandCursor) self.clButton.setStyleSheet( "QToolButton:hover{border: none } \ QToolButton:pressed{border: none}" ) self.clButton.setAutoRaise(True) sizePolicy = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed) self.topFrame.setSizePolicy(sizePolicy) self.layoutTopFrame.addWidget(self.topFrame, 0, 0, 1, 1) self.layoutTopFrame2.addWidget(self.label, 0, 1, 1, 1) self.layoutTopFrame2.addWidget(self.restoreTabsButton, 0, 0, 1, 1) self.layoutTopFrame2.addWidget(self.clButton, 0, 2, 1, 1) self.topFrame.hide() self.restoreTabsButton.clicked.connect(self.restoreTabs) self.clButton.clicked.connect(self.closeRestore) # Fixes #7653 if sys.platform != "darwin": self.setDocumentMode(True) self.setMovable(True) self.setTabsClosable(True) self.setTabPosition(QTabWidget.TabPosition.North) # Menu button list tabs self.fileTabMenu = QMenu() self.fileTabMenu.aboutToShow.connect(self.showFileTabMenu) self.fileTabMenu.triggered.connect(self.showFileTabMenuTriggered) self.fileTabButton = QToolButton() txtToolTipMenuFile = QCoreApplication.translate( "PythonConsole", "List all tabs" ) self.fileTabButton.setToolTip(txtToolTipMenuFile) self.fileTabButton.setIcon( QgsApplication.getThemeIcon("console/iconFileTabsMenuConsole.svg") ) self.fileTabButton.setIconSize(QSize(24, 24)) self.fileTabButton.setAutoRaise(True) self.fileTabButton.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) self.fileTabButton.setMenu(self.fileTabMenu) self.setCornerWidget(self.fileTabButton, Qt.Corner.TopRightCorner) self.tabCloseRequested.connect(self._removeTab) self.currentChanged.connect(self._currentWidgetChanged) # New Editor button self.newTabButton = QToolButton() txtToolTipNewTab = QCoreApplication.translate("PythonConsole", "New Editor") self.newTabButton.setToolTip(f"{txtToolTipNewTab} (Ctrl+T)") self.newTabButton.setAutoRaise(True) self.newTabButton.setIcon( QgsApplication.getThemeIcon("console/iconNewTabEditorConsole.svg") ) self.newTabButton.setIconSize(QSize(24, 24)) self.setCornerWidget(self.newTabButton, Qt.Corner.TopLeftCorner) self.newTabButton.clicked.connect(self.newTabEditor) def _currentWidgetChanged(self, tab): if QgsSettings().value("pythonConsole/enableObjectInsp", False, type=bool): self.listObject(tab) self.changeLastDirPath(tab) self.enableSaveIfModified(tab) if self.currentWidget(): self.search_bar_toggled.emit(self.currentWidget().search_bar_visible()) def toggle_search_bar(self, visible: bool): """ Toggles whether the search bar should be visible """ if visible and not self.currentWidget().search_bar_visible(): self.currentWidget().trigger_find() elif not visible and self.currentWidget().search_bar_visible(): self.currentWidget().hide_search_bar() def contextMenuEvent(self, e): tabBar = self.tabBar() self.idx = tabBar.tabAt(e.pos()) if self.widget(self.idx): cW = self.widget(self.idx) menu = QMenu(self) menu.addSeparator() menu.addAction( QCoreApplication.translate("PythonConsole", "New Editor"), self.newTabEditor, ) menu.addSeparator() closeTabAction = menu.addAction( QCoreApplication.translate("PythonConsole", "Close Tab"), cW.close ) closeAllTabAction = menu.addAction( QCoreApplication.translate("PythonConsole", "Close All"), self.closeAll ) closeOthersTabAction = menu.addAction( QCoreApplication.translate("PythonConsole", "Close Others"), self.closeOthers, ) menu.addSeparator() saveAction = menu.addAction( QCoreApplication.translate("PythonConsole", "Save"), cW.save ) menu.addAction( QCoreApplication.translate("PythonConsole", "Save As"), self.saveAs ) closeTabAction.setEnabled(False) closeAllTabAction.setEnabled(False) closeOthersTabAction.setEnabled(False) saveAction.setEnabled(False) if self.count() > 1: closeTabAction.setEnabled(True) closeAllTabAction.setEnabled(True) closeOthersTabAction.setEnabled(True) if self.widget(self.idx).isModified(): saveAction.setEnabled(True) menu.exec(self.mapToGlobal(e.pos())) def closeOthers(self): idx = self.idx countTab = self.count() for i in list(range(countTab - 1, idx, -1)) + list(range(idx - 1, -1, -1)): self._removeTab(i) def closeAll(self): countTab = self.count() for i in range(countTab - 1, 0, -1): self._removeTab(i) self.newTabEditor( tabName=QCoreApplication.translate("PythonConsole", "Untitled-0") ) self._removeTab(0) def saveAs(self): self.console_widget.saveAsScriptFile(self.idx) def enableSaveIfModified(self, tab): tabWidget = self.widget(tab) if tabWidget: self.console_widget.save_file_action.setEnabled(tabWidget.isModified()) def enableToolBarEditor(self, enable): if self.topFrame.isVisible(): enable = False self.console_widget.toolBarEditor.setEnabled(enable) def newTabEditor(self, tabName=None, filename: str | None = None): read_only = False if filename: read_only = not QFileInfo(filename).isWritable() try: fn = codecs.open(filename, "rb", encoding="utf-8") fn.read() fn.close() except OSError as error: IOErrorTr = QCoreApplication.translate( "PythonConsole", "The file {0} could not be opened. Error: {1}\n" ).format(filename, error.strerror) print("## Error: ") sys.stderr.write(IOErrorTr) return nr = self.count() if not tabName: tabName = QCoreApplication.translate( "PythonConsole", "Untitled-{0}" ).format(nr) tab = EditorTab( tab_widget=self, console_widget=self.console_widget, filename=filename, read_only=read_only, ) self.iconTab = QgsApplication.getThemeIcon("console/iconTabEditorConsole.svg") self.addTab(tab, self.iconTab, tabName + " (ro)" if read_only else tabName) self.setCurrentWidget(tab) if filename: self.setTabToolTip(self.currentIndex(), filename) else: self.setTabToolTip(self.currentIndex(), tabName) tab.search_bar_toggled.connect(self._tab_search_bar_toggled) def _tab_search_bar_toggled(self, visible: bool): if self.sender() != self.currentWidget(): return self.search_bar_toggled.emit(visible) def tabModified(self, tab, modified): index = self.indexOf(tab) s = self.tabText(index) self.setTabTitle(index, f"*{s}" if modified else re.sub(r"^(\*)", "", s)) self.console_widget.save_file_action.setEnabled(modified) def setTabTitle(self, tab, title): self.setTabText(tab, title) def _removeTab(self, tab, tab2index=False): if tab2index: tab = self.indexOf(tab) editorTab = self.widget(tab) if editorTab.isModified(): txtSaveOnRemove = QCoreApplication.translate( "PythonConsole", "Python Console: Save File" ) txtMsgSaveOnRemove = QCoreApplication.translate( "PythonConsole", "The file '{0}' has been modified, save changes?", ).format(self.tabText(tab)) res = QMessageBox.question( self, txtSaveOnRemove, txtMsgSaveOnRemove, QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel, ) if res == QMessageBox.StandardButton.Cancel: return if res == QMessageBox.StandardButton.Save: editorTab.save() if editorTab.code_editor_widget.filePath(): self.console_widget.updateTabListScript( editorTab.code_editor_widget.filePath(), action="remove" ) self.removeTab(tab) if self.count() < 1: self.newTabEditor() else: if editorTab.code_editor_widget.filePath(): self.console_widget.updateTabListScript( editorTab.code_editor_widget.filePath(), action="remove" ) if self.count() <= 1: self.removeTab(tab) self.newTabEditor() else: self.removeTab(tab) editorTab.deleteLater() self.currentWidget()._editor.setFocus(Qt.FocusReason.TabFocusReason) def restoreTabsOrAddNew(self): """ Restore tabs if they are found in the settings. If none are found it will add a new empty tab. """ # Restore scripts from the previous session tabScripts = QgsSettings().value("pythonConsole/tabScripts", []) self.restoreTabList = tabScripts if self.restoreTabList: self.restoreTabs() else: self.newTabEditor(filename=None) def restoreTabs(self): for script in self.restoreTabList: pathFile = script if QFileInfo(pathFile).exists(): tabName = pathFile.split("/")[-1] self.newTabEditor(tabName, pathFile) else: errOnRestore = QCoreApplication.translate( "PythonConsole", "Unable to restore the file: \n{0}\n" ).format(pathFile) print("## Error: ") s = errOnRestore sys.stderr.write(s) self.console_widget.updateTabListScript(pathFile, action="remove") if self.count() < 1: self.newTabEditor(filename=None) self.topFrame.close() self.enableToolBarEditor(True) self.currentWidget()._editor.setFocus(Qt.FocusReason.TabFocusReason) def closeRestore(self): self.console_widget.updateTabListScript(None) self.topFrame.close() self.newTabEditor(filename=None) self.enableToolBarEditor(True) def showFileTabMenu(self): self.fileTabMenu.clear() for index in range(self.count()): action = self.fileTabMenu.addAction( self.tabIcon(index), self.tabText(index) ) action.setData(index) def showFileTabMenuTriggered(self, action): index = action.data() if index is not None: self.setCurrentIndex(index) def listObject(self, tab): self.console_widget.listClassMethod.clear() if isinstance(tab, EditorTab): tabWidget = self.widget(self.indexOf(tab)) else: tabWidget = self.widget(tab) if tabWidget: if tabWidget.file_path(): pathFile, file = os.path.split(tabWidget.file_path()) module, ext = os.path.splitext(file) found = False if pathFile not in sys.path: sys.path.append(pathFile) found = True try: importlib.reload(pyclbr) # NOQA dictObject = {} readModule = pyclbr.readmodule(module) readModuleFunction = pyclbr.readmodule_ex(module) for name, class_data in sorted( list(readModule.items()), key=lambda x: x[1].lineno ): if os.path.normpath(class_data.file) == os.path.normpath( tabWidget.file_path() ): superClassName = [] for superClass in class_data.super: if superClass == "object": continue if isinstance(superClass, str): superClassName.append(superClass) else: superClassName.append(superClass.name) classItem = QTreeWidgetItem() if superClassName: super = ", ".join([i for i in superClassName]) classItem.setText(0, name + " [" + super + "]") classItem.setToolTip(0, name + " [" + super + "]") else: classItem.setText(0, name) classItem.setToolTip(0, name) if sys.platform.startswith("win"): classItem.setSizeHint(0, QSize(18, 18)) classItem.setText(1, str(class_data.lineno)) iconClass = QgsApplication.getThemeIcon( "console/iconClassTreeWidgetConsole.svg" ) classItem.setIcon(0, iconClass) dictObject[name] = class_data.lineno for meth, lineno in sorted( list(class_data.methods.items()), key=itemgetter(1) ): methodItem = QTreeWidgetItem() methodItem.setText(0, meth + " ") methodItem.setText(1, str(lineno)) methodItem.setToolTip(0, meth) iconMeth = QgsApplication.getThemeIcon( "console/iconMethodTreeWidgetConsole.svg" ) methodItem.setIcon(0, iconMeth) if sys.platform.startswith("win"): methodItem.setSizeHint(0, QSize(18, 18)) classItem.addChild(methodItem) dictObject[meth] = lineno self.console_widget.listClassMethod.addTopLevelItem( classItem ) for func_name, data in sorted( list(readModuleFunction.items()), key=lambda x: x[1].lineno ): if isinstance(data, pyclbr.Function) and os.path.normpath( data.file ) == os.path.normpath(tabWidget.file_path()): funcItem = QTreeWidgetItem() funcItem.setText(0, func_name + " ") funcItem.setText(1, str(data.lineno)) funcItem.setToolTip(0, func_name) iconFunc = QgsApplication.getThemeIcon( "console/iconFunctionTreeWidgetConsole.svg" ) funcItem.setIcon(0, iconFunc) if sys.platform.startswith("win"): funcItem.setSizeHint(0, QSize(18, 18)) dictObject[func_name] = data.lineno self.console_widget.listClassMethod.addTopLevelItem( funcItem ) if found: sys.path.remove(pathFile) except: msgItem = QTreeWidgetItem() msgItem.setText( 0, QCoreApplication.translate("PythonConsole", "Check Syntax") ) msgItem.setText(1, "syntaxError") iconWarning = QgsApplication.getThemeIcon( "console/iconSyntaxErrorConsole.svg" ) msgItem.setIcon(0, iconWarning) self.console_widget.listClassMethod.addTopLevelItem(msgItem) def refreshSettingsEditor(self): objInspectorEnabled = QgsSettings().value( "pythonConsole/enableObjectInsp", False, type=bool ) if self.console_widget.listClassMethod.isVisible(): self.console_widget.object_inspector_action.setChecked(objInspectorEnabled) self.console_widget.object_inspector_action.setEnabled(objInspectorEnabled) if objInspectorEnabled: cW = self.currentWidget() if cW and not self.console_widget.listClassMethod.isVisible(): with OverrideCursor(Qt.CursorShape.WaitCursor): self.listObject(cW) def changeLastDirPath(self, tab): tabWidget = self.widget(tab) if tabWidget and tabWidget.file_path(): QgsSettings().setValue( "pythonConsole/lastDirPath", Path(tabWidget.file_path()).parent.as_posix(), ) def showMessage(self, text, level=Qgis.MessageLevel.Info, timeout=-1, title=""): currWidget = self.currentWidget() currWidget.showMessage(text, level, timeout, title)