# -*- coding:utf-8 -*- """ /*************************************************************************** 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 qgis.PyQt.QtCore import Qt, QObject, QEvent, QCoreApplication, QFileInfo, QSize, QDir, QByteArray, QJsonDocument, QUrl from qgis.PyQt.QtGui import QFont, QColor, QKeySequence from qgis.PyQt.QtNetwork import QNetworkRequest from qgis.PyQt.QtWidgets import QShortcut, QMenu, QApplication, QWidget, QGridLayout, QSpacerItem, QSizePolicy, QFileDialog, QTabWidget, QTreeWidgetItem, QFrame, QLabel, QToolButton, QMessageBox from qgis.PyQt.Qsci import QsciScintilla, QsciStyle from qgis.core import ( Qgis, QgsApplication, QgsSettings, QgsBlockingNetworkRequest, QgsFileUtils ) from qgis.gui import QgsMessageBar, QgsCodeEditorPython from qgis.utils import OverrideCursor import sys import os import subprocess import datetime import pyclbr from operator import itemgetter import traceback import codecs import re import importlib from functools import partial class KeyFilter(QObject): SHORTCUTS = { ("Control", "T"): lambda w, t: w.newTabEditor(), ("Control", "M"): lambda w, t: t.save(), ("Control", "W"): lambda w, t: t.close() } def __init__(self, window, tab, *args): QObject.__init__(self, *args) self.window = window self.tab = tab self._handlers = {} for shortcut, handler in list(KeyFilter.SHORTCUTS.items()): modifiers = shortcut[0] if not isinstance(modifiers, list): modifiers = [modifiers] qt_mod_code = Qt.NoModifier for each in modifiers: qt_mod_code |= getattr(Qt, each + "Modifier") qt_keycode = getattr(Qt, "Key_" + shortcut[1].upper()) handlers = self._handlers.get(qt_keycode, []) handlers.append((qt_mod_code, handler)) self._handlers[qt_keycode] = handlers def get_handler(self, key, modifier): if self.window.count() > 1: for modifiers, handler in self._handlers.get(key, []): if modifiers == modifier: return handler return None def eventFilter(self, obj, event): if event.type() == QEvent.KeyPress and event.key() < 256: handler = self.get_handler(event.key(), event.modifiers()) if handler: handler(self.window, self.tab) return QObject.eventFilter(self, obj, event) class Editor(QgsCodeEditorPython): def __init__(self, parent=None): super().__init__(parent) self.parent = parent # recent modification time self.lastModified = 0 self.opening = ['(', '{', '[', "'", '"'] self.closing = [')', '}', ']', "'", '"'] self.settings = QgsSettings() self.setMinimumHeight(120) self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) # Disable command key ctrl, shift = self.SCMOD_CTRL << 16, self.SCMOD_SHIFT << 16 self.SendScintilla(QsciScintilla.SCI_CLEARCMDKEY, ord('L') + ctrl) self.SendScintilla(QsciScintilla.SCI_CLEARCMDKEY, ord('T') + ctrl) self.SendScintilla(QsciScintilla.SCI_CLEARCMDKEY, ord('D') + ctrl) self.SendScintilla(QsciScintilla.SCI_CLEARCMDKEY, ord('L') + ctrl + shift) # New QShortcut = ctrl+space/ctrl+alt+space for Autocomplete self.newShortcutCS = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Space), self) self.newShortcutCS.setContext(Qt.WidgetShortcut) self.redoScut = QShortcut(QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_Z), self) self.redoScut.setContext(Qt.WidgetShortcut) self.redoScut.activated.connect(self.redo) self.newShortcutCS.activated.connect(self.autoComplete) self.runScut = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_E), self) self.runScut.setContext(Qt.WidgetShortcut) self.runScut.activated.connect(self.runSelectedCode) # spellok self.runScriptScut = QShortcut(QKeySequence(Qt.SHIFT + Qt.CTRL + Qt.Key_E), self) self.runScriptScut.setContext(Qt.WidgetShortcut) self.runScriptScut.activated.connect(self.runScriptCode) self.syntaxCheckScut = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_4), self) self.syntaxCheckScut.setContext(Qt.WidgetShortcut) self.syntaxCheckScut.activated.connect(self.syntaxCheck) self.commentScut = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_3), self) self.commentScut.setContext(Qt.WidgetShortcut) self.commentScut.activated.connect(self.parent.pc.commentCode) self.uncommentScut = QShortcut(QKeySequence(Qt.SHIFT + Qt.CTRL + Qt.Key_3), self) self.uncommentScut.setContext(Qt.WidgetShortcut) self.uncommentScut.activated.connect(self.parent.pc.uncommentCode) self.modificationChanged.connect(self.parent.modified) self.modificationAttempted.connect(self.fileReadOnly) def settingsEditor(self): # Set Python lexer self.initializeLexer() def move_cursor_to_end(self): """Move cursor to end of text""" line, index = self.get_end_pos() self.setCursorPosition(line, index) self.ensureCursorVisible() self.ensureLineVisible(line) def get_end_pos(self): """Return (line, index) position of the last character""" line = self.lines() - 1 return (line, len(self.text(line))) def contextMenuEvent(self, e): menu = QMenu(self) menu.addAction( QCoreApplication.translate("PythonConsole", "Hide Editor"), self.hideEditor) menu.addSeparator() # ------------------------------ syntaxCheckAction = menu.addAction(QgsApplication.getThemeIcon("console/iconSyntaxErrorConsole.svg"), QCoreApplication.translate("PythonConsole", "Check Syntax"), self.syntaxCheck, 'Ctrl+4') runSelected = menu.addAction(QgsApplication.getThemeIcon("console/mIconRunConsole.svg"), # spellok QCoreApplication.translate("PythonConsole", "Run Selected"), self.runSelectedCode, 'Ctrl+E') # spellok pyQGISHelpAction = menu.addAction(QgsApplication.getThemeIcon("console/iconHelpConsole.svg"), QCoreApplication.translate("PythonConsole", "Search Selected in PyQGIS docs"), self.searchSelectedTextInPyQGISDocs) menu.addAction(QgsApplication.getThemeIcon("mActionStart.svg"), QCoreApplication.translate("PythonConsole", "Run Script"), self.runScriptCode, 'Shift+Ctrl+E') menu.addSeparator() undoAction = menu.addAction(QgsApplication.getThemeIcon("mActionUndo.svg"), QCoreApplication.translate("PythonConsole", "Undo"), self.undo, QKeySequence.Undo) redoAction = menu.addAction(QgsApplication.getThemeIcon("mActionRedo.svg"), QCoreApplication.translate("PythonConsole", "Redo"), self.redo, 'Ctrl+Shift+Z') menu.addSeparator() menu.addAction(QgsApplication.getThemeIcon("console/iconSearchEditorConsole.svg"), QCoreApplication.translate("PythonConsole", "Find Text"), self.openFindWidget) cutAction = menu.addAction(QgsApplication.getThemeIcon("mActionEditCut.svg"), QCoreApplication.translate("PythonConsole", "Cut"), self.cut, QKeySequence.Cut) copyAction = menu.addAction(QgsApplication.getThemeIcon("mActionEditCopy.svg"), QCoreApplication.translate("PythonConsole", "Copy"), self.copy, QKeySequence.Copy) pasteAction = menu.addAction(QgsApplication.getThemeIcon("mActionEditPaste.svg"), QCoreApplication.translate("PythonConsole", "Paste"), self.paste, QKeySequence.Paste) selectAllAction = menu.addAction( QCoreApplication.translate("PythonConsole", "Select All"), self.selectAll, QKeySequence.SelectAll) menu.addSeparator() menu.addAction(QgsApplication.getThemeIcon("console/iconCommentEditorConsole.svg"), QCoreApplication.translate("PythonConsole", "Comment"), self.parent.pc.commentCode, 'Ctrl+3') menu.addAction(QgsApplication.getThemeIcon("console/iconUncommentEditorConsole.svg"), QCoreApplication.translate("PythonConsole", "Uncomment"), self.parent.pc.uncommentCode, 'Shift+Ctrl+3') 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.parent.pc.openSettings) syntaxCheckAction.setEnabled(False) pasteAction.setEnabled(False) pyQGISHelpAction.setEnabled(False) gist_menu.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) gist_menu.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 self.settings.value("pythonConsole/enableObjectInsp", False, type=bool): showCodeInspection.setEnabled(True) menu.exec_(self.mapToGlobal(e.pos())) def findText(self, forward, showMessage=True, findFirst=False): lineFrom, indexFrom, lineTo, indexTo = self.getSelection() if findFirst: line = 0 index = 0 else: line, index = self.getCursorPosition() text = self.parent.pc.lineEditFind.text() re = False wrap = self.parent.pc.wrapAround.isChecked() cs = self.parent.pc.caseSensitive.isChecked() wo = self.parent.pc.wholeWord.isChecked() notFound = False if text: if not forward: line = lineFrom index = indexFrom # findFirst(QString(), re bool, cs bool, wo bool, wrap, bool, forward=True) # re = Regular Expression, cs = Case Sensitive, wo = Whole Word, wrap = Wrap Around if not self.findFirst(text, re, cs, wo, wrap, forward, line, index): notFound = True if notFound: styleError = 'QLineEdit {background-color: #d65253; \ color: #ffffff;}' if showMessage: msgText = QCoreApplication.translate('PythonConsole', '"{0}" was not found.').format(text) self.parent.pc.callWidgetMessageBarEditor(msgText, 0, True) else: styleError = '' self.parent.pc.lineEditFind.setStyleSheet(styleError) def findNext(self): self.findText(True) def findPrevious(self): self.findText(False) def objectListEditor(self): listObj = self.parent.pc.listClassMethod if listObj.isVisible(): listObj.hide() self.parent.pc.objectListButton.setChecked(False) else: listObj.show() self.parent.pc.objectListButton.setChecked(True) def shareOnGist(self, is_public): ACCESS_TOKEN = self.settings.value("pythonConsole/accessTokenGithub", '', type=QByteArray) if not ACCESS_TOKEN: msg_text = QCoreApplication.translate( 'PythonConsole', 'GitHub personal access token must be generated (see Console Options)') self.parent.pc.callWidgetMessageBarEditor(msg_text, 1, True) return URL = "https://api.github.com/gists" path = self.parent.tw.currentWidget().path filename = os.path.basename(path) if path else None filename = filename if filename else "pyqgis_snippet.py" selected_text = self.selectedText() data = {"description": "Gist created by PyQGIS Console", "public": is_public, "files": {filename: {"content": selected_text}}} request = QgsBlockingNetworkRequest() net_req = QNetworkRequest() url = QUrl(URL) net_req.setUrl(url) net_req.setRawHeader(b"Authorization", b"token %s" % ACCESS_TOKEN) err = request.post(net_req, QJsonDocument(data).toJson()) if not err: response = request.reply().content() json_doc = QJsonDocument() _json = json_doc.fromJson(response) link = _json.object()['html_url'].toString() QApplication.clipboard().setText(link) msg = QCoreApplication.translate('PythonConsole', 'URL copied to clipboard.') self.parent.pc.callWidgetMessageBarEditor(msg, 0, True) else: msg = QCoreApplication.translate('PythonConsole', 'Connection error: ') self.parent.pc.callWidgetMessageBarEditor(msg + request.erroMessage(), 0, True) def hideEditor(self): self.parent.pc.splitterObj.hide() self.parent.pc.showEditorButton.setChecked(False) def openFindWidget(self): wF = self.parent.pc.widgetFind wF.show() if self.hasSelectedText(): self.parent.pc.lineEditFind.setText(self.selectedText().strip()) self.parent.pc.lineEditFind.setFocus() self.parent.pc.findTextButton.setChecked(True) def closeFindWidget(self): wF = self.parent.pc.widgetFind wF.hide() self.parent.pc.findTextButton.setChecked(False) def toggleFindWidget(self): wF = self.parent.pc.widgetFind if wF.isVisible(): self.closeFindWidget() else: self.openFindWidget() def commentEditorCode(self, commentCheck): self.beginUndoAction() if self.hasSelectedText(): startLine, _, endLine, _ = self.getSelection() for line in range(startLine, endLine + 1): if commentCheck: self.insertAt('#', line, 0) else: if not self.text(line).strip().startswith('#'): continue self.setSelection(line, self.indentation(line), line, self.indentation(line) + 1) self.removeSelectedText() else: line, pos = self.getCursorPosition() if commentCheck: self.insertAt('#', line, 0) else: if not self.text(line).strip().startswith('#'): return self.setSelection(line, self.indentation(line), line, self.indentation(line) + 1) self.removeSelectedText() self.endUndoAction() def createTempFile(self): import tempfile fd, path = tempfile.mkstemp() tmpFileName = path + '.py' with codecs.open(path, "w", encoding='utf-8') as f: f.write(self.text()) os.close(fd) os.rename(path, tmpFileName) return tmpFileName def _runSubProcess(self, filename, tmp=False): dir = QFileInfo(filename).path() file = QFileInfo(filename).fileName() name = QFileInfo(filename).baseName() if dir not in sys.path: sys.path.append(dir) if name in sys.modules: importlib.reload(sys.modules[name]) # NOQA try: # set creationflags for running command without shell window if sys.platform.startswith('win'): p = subprocess.Popen(['python3', filename], shell=False, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE, creationflags=0x08000000) else: p = subprocess.Popen(['python3', filename], shell=False, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) out, _traceback = p.communicate() # Fix interrupted system call on OSX if sys.platform == 'darwin': status = None while status is None: try: status = p.wait() except OSError as e: if e.errno == 4: pass else: raise e if tmp: tmpFileTr = QCoreApplication.translate('PythonConsole', ' [Temporary file saved in {0}]').format(dir) file = file + tmpFileTr if _traceback: msgTraceTr = QCoreApplication.translate('PythonConsole', '## Script error: {0}').format(file) print("## {}".format(datetime.datetime.now())) print(msgTraceTr) sys.stderr.write(_traceback) p.stderr.close() else: msgSuccessTr = QCoreApplication.translate('PythonConsole', '## Script executed successfully: {0}').format(file) print("## {}".format(datetime.datetime.now())) print(msgSuccessTr) sys.stdout.write(out) p.stdout.close() del p if tmp: os.remove(filename) except IOError as error: IOErrorTr = QCoreApplication.translate('PythonConsole', 'Cannot execute file {0}. Error: {1}\n').format(filename, error.strerror) print('## Error: ' + IOErrorTr) except: s = traceback.format_exc() print('## Error: ') sys.stderr.write(s) def runScriptCode(self): autoSave = self.settings.value("pythonConsole/autoSaveScript", False, type=bool) tabWidget = self.parent.tw.currentWidget() filename = tabWidget.path msgEditorBlank = QCoreApplication.translate('PythonConsole', 'Hey, type something to run!') if filename is None: if not self.isModified(): self.parent.pc.callWidgetMessageBarEditor(msgEditorBlank, 0, True) return if self.syntaxCheck(): if filename and self.isModified() and autoSave: self.parent.save(filename) elif not filename or self.isModified(): # Create a new temp file if the file isn't already saved. tmpFile = self.createTempFile() filename = tmpFile self.parent.pc.shell.runCommand("exec(Path('{0}').read_text())" .format(filename.replace("\\", "/"))) def runSelectedCode(self): # spellok cmd = self.selectedText() self.parent.pc.shell.insertFromDropPaste(cmd) self.parent.pc.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): source = self.text() self.clearWarnings() try: filename = self.parent.tw.currentWidget().path if not filename: tmpFile = self.createTempFile() filename = tmpFile if isinstance(source, type("")): source = source.encode('utf-8') if isinstance(filename, type("")): filename = filename.encode('utf-8') if filename: compile(source, filename, 'exec') except SyntaxError as detail: eline = detail.lineno and detail.lineno or 1 eline -= 1 ecolumn = detail.offset and detail.offset or 1 edescr = detail.msg self.addWarning(eline, edescr) self.setCursorPosition(eline, ecolumn - 1) self.ensureLineVisible(eline) return False return True def keyPressEvent(self, e): t = e.text() startLine, _, endLine, endPos = self.getSelection() line, pos = self.getCursorPosition() self.autoCloseBracket = self.settings.value("pythonConsole/autoCloseBracket", False, type=bool) self.autoImport = self.settings.value("pythonConsole/autoInsertionImport", True, type=bool) txt = self.text(line)[:pos] # Close bracket automatically if t in self.opening and self.autoCloseBracket: self.beginUndoAction() i = self.opening.index(t) if self.hasSelectedText(): selText = self.selectedText() self.removeSelectedText() if startLine == endLine: self.insert(self.opening[i] + selText + self.closing[i]) self.setCursorPosition(endLine, endPos + 2) self.endUndoAction() return elif startLine < endLine and self.opening[i] in ("'", '"'): self.insert("'''" + selText + "'''") self.setCursorPosition(endLine, endPos + 3) self.endUndoAction() return elif t == '(' and (re.match(r'^[ \t]*def \w+$', txt) or re.match(r'^[ \t]*class \w+$', txt)): self.insert('):') else: self.insert(self.closing[i]) self.endUndoAction() # FIXES #8392 (automatically removes the redundant char # when autoclosing brackets option is enabled) elif t in [')', ']', '}'] and self.autoCloseBracket: txt = self.text(line) try: if txt[pos - 1] in self.opening and t == txt[pos]: self.setCursorPosition(line, pos + 1) self.SendScintilla(QsciScintilla.SCI_DELETEBACK) except IndexError: pass elif t == ' ' and self.autoImport: ptrn = r'^[ \t]*from [\w.]+$' if re.match(ptrn, txt): self.insert(' import') self.setCursorPosition(line, pos + 7) QsciScintilla.keyPressEvent(self, e) def focusInEvent(self, e): pathfile = self.parent.path if pathfile: if not QFileInfo(pathfile).exists(): msgText = QCoreApplication.translate('PythonConsole', 'The file "{0}" has been deleted or is not accessible').format(pathfile) self.parent.pc.callWidgetMessageBarEditor(msgText, 2, False) return if pathfile and self.lastModified != QFileInfo(pathfile).lastModified(): self.beginUndoAction() self.selectAll() # fileReplaced = self.selectedText() self.removeSelectedText() file = open(pathfile, "r") fileLines = file.readlines() file.close() with OverrideCursor(Qt.WaitCursor): for line in reversed(fileLines): self.insert(line) self.setModified(False) self.endUndoAction() self.parent.tw.listObject(self.parent.tw.currentWidget()) self.lastModified = QFileInfo(pathfile).lastModified() QsciScintilla.focusInEvent(self, e) def fileReadOnly(self): tabWidget = self.parent.tw.currentWidget() msgText = QCoreApplication.translate('PythonConsole', 'The file "{0}" is read only, please save to different file first.').format(tabWidget.path) self.parent.pc.callWidgetMessageBarEditor(msgText, 1, False) class EditorTab(QWidget): def __init__(self, parent, parentConsole, filename, readOnly): super(EditorTab, self).__init__(parent) self.tw = parent self.pc = parentConsole self.path = None self.readOnly = readOnly self.fileExecuteList = {} self.fileExecuteList = dict() self.newEditor = Editor(self) if filename: self.path = filename if QFileInfo(filename).exists(): self.loadFile(filename, False) # Creates layout for message bar self.layout = QGridLayout(self.newEditor) self.layout.setContentsMargins(0, 0, 0, 0) spacerItem = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) self.layout.addItem(spacerItem, 1, 0, 1, 1) # messageBar instance self.infoBar = QgsMessageBar() sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) self.infoBar.setSizePolicy(sizePolicy) self.layout.addWidget(self.infoBar, 0, 0, 1, 1) self.tabLayout = QGridLayout(self) self.tabLayout.setContentsMargins(0, 0, 0, 0) self.tabLayout.addWidget(self.newEditor) self.keyFilter = KeyFilter(parent, self) self.setEventFilter(self.keyFilter) def loadFile(self, filename, modified): self.newEditor.lastModified = QFileInfo(filename).lastModified() fn = codecs.open(filename, "rb", encoding='utf-8') txt = fn.read() fn.close() with OverrideCursor(Qt.WaitCursor): self.newEditor.setText(txt) if self.readOnly: self.newEditor.setReadOnly(self.readOnly) self.newEditor.setModified(modified) self.newEditor.recolor() def save(self, fileName=None): index = self.tw.indexOf(self) if fileName: self.path = fileName if self.path is None: saveTr = QCoreApplication.translate('PythonConsole', 'Python Console: Save file') folder = self.pc.settings.value("pythonConsole/lastDirPath", QDir.homePath()) self.path, filter = QFileDialog().getSaveFileName(self, saveTr, os.path.join(folder, self.tw.tabText(index).replace('*', '') + '.py'), "Script file (*.py)") # If the user didn't select a file, abort the save operation if len(self.path) == 0: self.path = None return self.path = QgsFileUtils.ensureFileNameHasExtension(self.path, ['py']) self.tw.setCurrentWidget(self) msgText = QCoreApplication.translate('PythonConsole', 'Script was correctly saved.') self.pc.callWidgetMessageBarEditor(msgText, 0, True) # Rename the original file, if it exists path = self.path overwrite = QFileInfo(path).exists() if overwrite: try: permis = os.stat(path).st_mode # self.newEditor.lastModified = QFileInfo(path).lastModified() os.chmod(path, permis) except: raise temp_path = path + "~" if QFileInfo(temp_path).exists(): os.remove(temp_path) os.rename(path, temp_path) # Save the new contents with codecs.open(path, "w", encoding='utf-8') as f: f.write(self.newEditor.text()) if overwrite: os.remove(temp_path) if self.newEditor.isReadOnly(): self.newEditor.setReadOnly(False) fN = path.split('/')[-1] self.tw.setTabTitle(index, fN) self.tw.setTabToolTip(index, path) self.newEditor.setModified(False) self.pc.saveFileButton.setEnabled(False) self.newEditor.lastModified = QFileInfo(path).lastModified() self.pc.updateTabListScript(path, action='append') self.tw.listObject(self) lastDirPath = QFileInfo(path).path() self.pc.settings.setValue("pythonConsole/lastDirPath", lastDirPath) def modified(self, modified): self.tw.tabModified(self, modified) def close(self): self.tw._removeTab(self, tab2index=True) def setEventFilter(self, filter): self.newEditor.installEventFilter(filter) def newTab(self): self.tw.newTabEditor() class EditorTabWidget(QTabWidget): def __init__(self, parent): QTabWidget.__init__(self, parent=None) self.parent = parent self.settings = QgsSettings() 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.Minimum, QSizePolicy.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.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.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.PointingHandCursor) self.clButton.setStyleSheet('QToolButton:hover{border: none } \ QToolButton:pressed{border: none}') self.clButton.setAutoRaise(True) sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.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.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.InstantPopup) self.fileTabButton.setMenu(self.fileTabMenu) self.setCornerWidget(self.fileTabButton, Qt.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.TopLeftCorner) self.newTabButton.clicked.connect(self.newTabEditor) def _currentWidgetChanged(self, tab): if self.settings.value("pythonConsole/enableObjectInsp", False, type=bool): self.listObject(tab) self.changeLastDirPath(tab) self.enableSaveIfModified(tab) 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).newEditor.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='Untitled-0') self._removeTab(0) def saveAs(self): idx = self.idx self.parent.saveAsScriptFile(idx) self.setCurrentWidget(self.widget(idx)) def enableSaveIfModified(self, tab): tabWidget = self.widget(tab) if tabWidget: self.parent.saveFileButton.setEnabled(tabWidget.newEditor.isModified()) def enableToolBarEditor(self, enable): if self.topFrame.isVisible(): enable = False self.parent.toolBarEditor.setEnabled(enable) def newTabEditor(self, tabName=None, filename=None): readOnly = False if filename: readOnly = 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) self.tab = EditorTab(self, self.parent, filename, readOnly) self.iconTab = QgsApplication.getThemeIcon('console/iconTabEditorConsole.svg') self.addTab(self.tab, self.iconTab, tabName + ' (ro)' if readOnly else tabName) self.setCurrentWidget(self.tab) if filename: self.setTabToolTip(self.currentIndex(), filename) else: self.setTabToolTip(self.currentIndex(), tabName) 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.parent.saveFileButton.setEnabled(modified) def closeTab(self, tab): if self.count() < 2: self.removeTab(self.indexOf(tab)) self.newTabEditor() else: self.removeTab(self.indexOf(tab)) self.currentWidget().setFocus(Qt.TabFocusReason) def setTabTitle(self, tab, title): self.setTabText(tab, title) def _removeTab(self, tab, tab2index=False): if tab2index: tab = self.indexOf(tab) tabWidget = self.widget(tab) if tabWidget.newEditor.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.Save | QMessageBox.Discard | QMessageBox.Cancel) if res == QMessageBox.Save: tabWidget.save() elif res == QMessageBox.Cancel: return if tabWidget.path: self.parent.updateTabListScript(tabWidget.path, action='remove') self.removeTab(tab) if self.count() < 1: self.newTabEditor() else: if tabWidget.path: self.parent.updateTabListScript(tabWidget.path, action='remove') if self.count() <= 1: self.removeTab(tab) self.newTabEditor() else: self.removeTab(tab) self.currentWidget().newEditor.setFocus(Qt.TabFocusReason) def buttonClosePressed(self): self.closeCurrentWidget() def closeCurrentWidget(self): currWidget = self.currentWidget() if currWidget and currWidget.close(): self.removeTab(self.currentIndex()) currWidget = self.currentWidget() if currWidget: currWidget.setFocus(Qt.TabFocusReason) if currWidget.path in self.restoreTabList: self.parent.updateTabListScript(currWidget.path, action='remove') def restoreTabsOrAddNew(self): """ Restore tabs if they are found in the settings. If none are found it will add a new empty tab. """ # Restore script of the previuos session tabScripts = self.settings.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.parent.updateTabListScript(pathFile, action='remove') if self.count() < 1: self.newTabEditor(filename=None) self.topFrame.close() self.enableToolBarEditor(True) self.currentWidget().newEditor.setFocus(Qt.TabFocusReason) def closeRestore(self): self.parent.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.parent.listClassMethod.clear() if isinstance(tab, EditorTab): tabWidget = self.widget(self.indexOf(tab)) else: tabWidget = self.widget(tab) if tabWidget: if tabWidget.path: pathFile, file = os.path.split(tabWidget.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.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.parent.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.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.parent.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.parent.listClassMethod.addTopLevelItem(msgItem) # s = traceback.format_exc() # print('## Error: ') # sys.stderr.write(s) # pass def refreshSettingsEditor(self): objInspectorEnabled = self.settings.value("pythonConsole/enableObjectInsp", False, type=bool) listObj = self.parent.objectListButton if self.parent.listClassMethod.isVisible(): listObj.setChecked(objInspectorEnabled) listObj.setEnabled(objInspectorEnabled) if objInspectorEnabled: cW = self.currentWidget() if cW and not self.parent.listClassMethod.isVisible(): with OverrideCursor(Qt.WaitCursor): self.listObject(cW) def changeLastDirPath(self, tab): tabWidget = self.widget(tab) if tabWidget and tabWidget.path: self.settings.setValue("pythonConsole/lastDirPath", tabWidget.path) def widgetMessageBar(self, iface, text, level, timed=True): messageLevel = [Qgis.Info, Qgis.Warning, Qgis.Critical] if timed: timeout = iface.messageTimeout() else: timeout = 0 currWidget = self.currentWidget() currWidget.infoBar.pushMessage(text, messageLevel[level], timeout)