""" /*************************************************************************** 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, QgsMessageBar 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.runScut = QShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_E), self) self.runScut.setContext(Qt.ShortcutContext.WidgetShortcut) self.runScut.activated.connect(self.runSelectedCode) # spellok 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) runSelected = QAction( QgsApplication.getThemeIcon("console/mIconRunConsole.svg"), # spellok QCoreApplication.translate("PythonConsole", "Run Selected"), menu, ) runSelected.triggered.connect(self.runSelectedCode) # spellok runSelected.setShortcut("Ctrl+E") # spellok menu.addAction(runSelected) # spellok 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("F1") menu.addAction(context_help_action) start_action = QAction( QgsApplication.getThemeIcon("mActionStart.svg"), QCoreApplication.translate("PythonConsole", "Run Script"), menu, ) start_action.triggered.connect(self.runScriptCode) start_action.setShortcut("Ctrl+Shift+E") 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) 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( QgsApplication.getThemeIcon( "console/iconCommentEditorConsole.svg", self.palette().color(QPalette.ColorRole.WindowText), ), QCoreApplication.translate("PythonConsole", "Toggle Comment"), menu, ) toggle_comment_action.triggered.connect(self.toggleComment) toggle_comment_action.setShortcut("Ctrl+:") menu.addAction(toggle_comment_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) runSelected.setEnabled(False) # spellok copyAction.setEnabled(False) selectAllAction.setEnabled(False) undoAction.setEnabled(False) redoAction.setEnabled(False) showCodeInspection.setEnabled(False) if self.hasSelectedText(): runSelected.setEnabled(True) # spellok 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.objectListButton.setChecked(False) else: listObj.show() self.console_widget.objectListButton.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.showEditorButton.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.saveFileButton.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(txtToolTipNewTab) 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.saveFileButton.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.saveFileButton.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 ) listObj = self.console_widget.objectListButton if self.console_widget.listClassMethod.isVisible(): listObj.setChecked(objInspectorEnabled) listObj.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)