""" /*************************************************************************** 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 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: Optional[QgsCodeEditorWidget] = 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 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 pyQGISHelpAction = QAction(QgsApplication.getThemeIcon("console/iconHelpConsole.svg"), QCoreApplication.translate("PythonConsole", "Search Selection in PyQGIS Documentation"), menu) pyQGISHelpAction.triggered.connect(self.searchSelectedTextInPyQGISDocs) menu.addAction(pyQGISHelpAction) 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"), 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) pyQGISHelpAction.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) pyQGISHelpAction.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: Optional[str] = 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: Optional[str] = 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: Optional[str], 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) -> Optional[str]: 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: Optional[str] = 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 IOError 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, '*{}'.format(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)