# -*- coding:utf-8 -*- """ /*************************************************************************** Python Conosle for QGIS ------------------- begin : 2012-09-xx 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 PyQt4.QtCore import * from PyQt4.QtGui import * from PyQt4.Qsci import * from PyQt4.Qsci import QsciScintilla, QsciScintillaBase, QsciLexerPython #from qgis.utils import iface import sys import os import traceback import code _init_commands = ["from qgis.core import *", "import qgis.utils"] _console = None _old_stdout = sys.stdout _console_output = None class PythonEdit(QsciScintilla, code.InteractiveInterpreter): def __init__(self, parent=None): #QsciScintilla.__init__(self, parent) super(PythonEdit,self).__init__(parent) code.InteractiveInterpreter.__init__(self, locals=None) self.current_prompt_pos = None self.new_input_line = True self.setMarginWidth(0, 0) self.setMarginWidth(1, 0) self.setMarginWidth(2, 0) self.buffer = [] self.insertInitText() self.setCursorPosition(4,4) self.displayPrompt(False) for line in _init_commands: self.runsource(line) self.history = QStringList() self.historyIndex = 0 # Brace matching: enable for a brace immediately before or after # the current position self.setBraceMatching(QsciScintilla.SloppyBraceMatch) #self.moveToMatchingBrace() #self.selectToMatchingBrace() # Current line visible with special background color self.setCaretLineVisible(True) self.setCaretLineBackgroundColor(QColor("#ffe4e4")) self.setCaretWidth(2) # Set Python lexer # Set style for Python comments (style number 1) to a fixed-width # courier. self.setLexers(True) # Indentation #self.setAutoIndent(True) #self.setIndentationsUseTabs(False) #self.setIndentationWidth(4) #self.setTabIndents(True) #self.setBackspaceUnindents(True) #self.setTabWidth(4) self.setAutoCompletionThreshold(2) self.setAutoCompletionSource(self.AcsAPIs) # Don't want to see the horizontal scrollbar at all # Use raw message to Scintilla here (all messages are documented # here: http://www.scintilla.org/ScintillaDoc.html) self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0) # not too small #self.setMinimumSize(500, 300) self.SendScintilla(QsciScintilla.SCI_SETWRAPMODE, 1) self.SendScintilla(QsciScintilla.SCI_EMPTYUNDOBUFFER) ## 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('Z')+ ctrl) self.SendScintilla(QsciScintilla.SCI_CLEARCMDKEY, ord('Y')+ 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.newShortcutCAS = QShortcut(QKeySequence(Qt.CTRL + Qt.ALT + Qt.Key_Space), self) self.newShortcutCS.activated.connect(self.autoComplete) self.newShortcutCAS.activated.connect(self.autoComplete) def autoComplete(self): self.autoCompleteFromAll() def clearConsole(self): """Clear the contents of the console.""" self.setText('') self.insertInitText() self.displayPrompt(False) self.setFocus() def commandConsole(self, command): line, pos = self.getCurLine() selCmd= self.text(line).length() self.setSelection(line, 4, line, selCmd) self.removeSelectedText() if command == "iface": """Import QgisInterface class""" self.append('from qgis.utils import iface') self.move_cursor_to_end() elif command == "cLayer": """Retrive current Layer from map camvas""" self.append('cLayer = iface.mapCanvas().currentLayer()') self.move_cursor_to_end() self.setFocus() def setLexers(self, lexer): if lexer: font = QFont() font.setFamily('Courier New') ## Courier New font.setFixedPitch(True) font.setPointSize(13) self.setFont(font) self.setMarginsFont(font) self.lexer = QsciLexerPython() self.lexer.setDefaultFont(font) #self.lexer.setDefaultFont(QFont('Mono', 10, 0, False)) #self.lexer.setDefaultColor(Qt.darkGray) self.lexer.setColor(Qt.red, 1) self.lexer.setColor(Qt.darkGreen, 5) self.lexer.setColor(Qt.darkBlue, 15) self.lexer.setFont(font, 1) self.lexer.setFont(font, 3) self.lexer.setFont(font, 4) self.api = QsciAPIs(self.lexer) self.api.loadPrepared(QString(os.path.dirname(__file__) + "/api/pyqgis_master.pap")) # self.api.load(os.path.dirname(__file__) + "/api/PyQGIS_1.8.api") # self.api.load(os.path.dirname(__file__) + "/api/osgeo_gdal-ogr_1.9.1-1.api") # self.api.load("qgis.networkanalysis.api") # self.api.load("qgis.gui.api") # self.api.load("qgis.core.api") # self.api.load("qgis.analysis.api") # self.api.prepare() # self.lexer.setAPIs(self.api) self.setLexer(self.lexer) ## TODO: show completion list for file and directory # def show_completion_list(self, completions, text): # """Private method to display the possible completions""" # if len(completions) == 0: # return # if len(completions) > 1: # self.showUserList(1, QStringList(sorted(completions))) # self.completion_chars = 1 # else: # txt = completions[0] # if text != "": # txt = txt.replace(text, "") # self.insert(txt) # self.completion_chars = 0 # # def show_file_completion(self): # """Display a completion list for files and directories""" # cwd = os.getcwdu() # self.show_completion_list(self.listdir_fullpath('/'), cwd) # # def listdir_fullpath(self, d): # return [os.path.join(d, f) for f in os.listdir(d)] def insertInitText(self): #self.setLexers(False) txtInit = ("## To access Quantum GIS environment from this console\n" "## use qgis.utils.iface object (instance of QgisInterface class).\n\n") initText = self.setText(txtInit) def getCurrentPos(self): """ Get the position (as an int) of the cursor. getCursorPosition() returns a (linenr, index) tuple. """ return self.SendScintilla(self.SCI_GETCURRENTPOS) def getText(self): """ Get the text as a unicode string. """ value = self.getBytes().decode('utf-8') # print (value) printing can give an error because the console font # may not have all unicode characters return value def getBytes(self): """ Get the text as bytes (utf-8 encoded). This is how the data is stored internally. """ len = self.SendScintilla(self.SCI_GETLENGTH)+1 bb = QByteArray(len,'0') N = self.SendScintilla(self.SCI_GETTEXT, len, bb) return bytes(bb)[:-1] def getTextLength(self): return self.SendScintilla(QsciScintilla.SCI_GETLENGTH) def getLine(self, linenr): """ Get the bytes on the given line number. """ len = self.SendScintilla(QsciScintilla.SCI_LINELENGTH)+1 bb = QByteArray(len,'0') N = self.SendScintilla(QsciScintilla.SCI_GETLINE, len, bb) return bytes(bb)[:-1] def getCurLine(self): """ Get the current line (as a string) and the position of the cursor in it. """ linenr, index = self.getCursorPosition() #line = self.getLine(linenr) #.decode('utf-8') return linenr, index def get_end_pos(self): """Return (line, index) position of the last character""" line = self.lines() - 1 return (line, self.text(line).length()) def is_cursor_at_end(self): """Return True if cursor is at the end of text""" cline, cindex = self.getCursorPosition() return (cline, cindex) == self.get_end_pos() def move_cursor_to_end(self): """Move cursor to end of text""" line, index = self.get_end_pos() self.setCursorPosition(line, index) self.ensureCursorVisible() def on_new_line(self): """On new input line""" self.move_cursor_to_end() self.current_prompt_pos = self.getCursorPosition() self.new_input_line = False def is_cursor_on_last_line(self): """Return True if cursor is on the last line""" cline, _ = self.getCursorPosition() return cline == self.lines() - 1 def new_prompt(self, prompt): """ Print a new prompt and save its (line, index) position """ self.write(prompt, prompt=True) # now we update our cursor giving end of prompt self.current_prompt_pos = self.getCursorPosition() self.ensureCursorVisible() def check_selection(self): """ Check if selected text is r/w, otherwise remove read-only parts of selection """ if self.current_prompt_pos is None: self.move_cursor_to_end() return line_from, index_from, line_to, index_to = self.getSelection() pline, pindex = self.current_prompt_pos if line_from < pline or \ (line_from == pline and index_from < pindex): self.setSelection(pline, pindex, line_to, index_to) def displayPrompt(self, more=False): self.append("... ") if more else self.append(">>> ") self.move_cursor_to_end() def updateHistory(self, command): if isinstance(command, QStringList): for line in command: self.history.append(line) elif not command == "": if len(self.history) <= 0 or \ not command == self.history[-1]: self.history.append(command) self.historyIndex = len(self.history) def showPrevious(self): if self.historyIndex < len(self.history) and not self.history.isEmpty(): line, pos = self.getCurLine() selCmd= self.text(line).length() self.setSelection(line, 4, line, selCmd) self.removeSelectedText() self.historyIndex += 1 if self.historyIndex == len(self.history): self.insert("") pass else: self.insert(self.history[self.historyIndex]) self.move_cursor_to_end() #self.SendScintilla(QsciScintilla.SCI_DELETEBACK) def showNext(self): if self.historyIndex > 0 and not self.history.isEmpty(): line, pos = self.getCurLine() selCmd = self.text(line).length() self.setSelection(line, 4, line, selCmd) self.removeSelectedText() self.historyIndex -= 1 if self.historyIndex == len(self.history): self.insert("") else: self.insert(self.history[self.historyIndex]) self.move_cursor_to_end() #self.SendScintilla(QsciScintilla.SCI_DELETEBACK) def keyPressEvent(self, e): linenr, index = self.getCurLine() if not self.is_cursor_on_last_line() or index < 4: if e.modifiers() & Qt.ControlModifier or e.modifiers() & Qt.MetaModifier: if e.key() == Qt.Key_C or e.key() == Qt.Key_A: QsciScintilla.keyPressEvent(self, e) else: # all other keystrokes get sent to the input line self.move_cursor_to_end() #pass else: if (e.key() == Qt.Key_Return or e.key() == Qt.Key_Enter) and not self.isListActive(): self.entered() elif e.key() == Qt.Key_Backspace: curPos, pos = self.getCursorPosition() line = self.lines() -1 if curPos < line -1 or pos < 5: return #else: #self.move_cursor_to_end() QsciScintilla.keyPressEvent(self, e) elif e.key() == Qt.Key_Delete: if self.hasSelectedText(): self.check_selection() self.removeSelectedText() elif self.is_cursor_on_last_line(): self.SendScintilla(QsciScintilla.SCI_CLEAR) e.accept() elif e.key() == Qt.Key_Home: self.setCursorPosition(linenr,4) self.ensureCursorVisible() elif e.key() == Qt.Key_Down and not self.isListActive(): self.showPrevious() elif e.key() == Qt.Key_Up and not self.isListActive(): self.showNext() elif e.key() == Qt.Key_Left: e.accept() if e.modifiers() & Qt.ShiftModifier: if e.modifiers() & Qt.ControlModifier: self.SendScintilla(QsciScintilla.SCI_WORDLEFTEXTEND) else: self.SendScintilla(QsciScintilla.SCI_CHARLEFTEXTEND) else: if e.modifiers() & Qt.ControlModifier: self.SendScintilla(QsciScintilla.SCI_WORDLEFT) else: self.SendScintilla(QsciScintilla.SCI_CHARLEFT) elif e.key() == Qt.Key_Right: e.accept() if e.modifiers() & Qt.ShiftModifier: if e.modifiers() & Qt.ControlModifier: self.SendScintilla(QsciScintilla.SCI_WORDRIGHTEXTEND) else: self.SendScintilla(QsciScintilla.SCI_CHARRIGHTEXTEND) else: if e.modifiers() & Qt.ControlModifier: self.SendScintilla(QsciScintilla.SCI_WORDRIGHT) else: self.SendScintilla(QsciScintilla.SCI_CHARRIGHT) ## TODO: press event for auto-completion file directory #elif e.key() == Qt.Key_Tab: #self.show_file_completion() #else: #self.on_new_line() else: QsciScintilla.keyPressEvent(self, e) def paste(self): """Reimplement QScintilla method""" # Moving cursor to the end of the last line self.move_cursor_to_end() #QsciScintilla.paste(self) QMessageBox.warning(self, "Python Console", "Currently the action paste in console is not supported!") return ## Drag and drop def dragEnterEvent(self, e): if e.mimeData().hasFormat('text/plain'): e.accept() else: e.ignore() def dropEvent(self, e): stringDrag = e.mimeData().text() pasteList = QStringList() pasteList = stringDrag.split("\n") for line in pasteList[:-1]: self.append(line) self.move_cursor_to_end() #self.SendScintilla(QsciScintilla.SCI_DELETEBACK) self.runCommand(unicode(self.currentCommand())) self.append(unicode(pasteList[-1])) self.move_cursor_to_end() def getTextFromEditor(self): text = self.text() textList = QStringList() textList = text.split("\n") return textList def insertTextFromFile(self, listOpenFile): for line in listOpenFile[:-1]: self.append(line) self.move_cursor_to_end() self.SendScintilla(QsciScintilla.SCI_DELETEBACK) self.runCommand(unicode(self.currentCommand())) self.append(unicode(listOpenFile[-1])) self.move_cursor_to_end() self.SendScintilla(QsciScintilla.SCI_DELETEBACK) def entered(self): self.move_cursor_to_end() self.runCommand( unicode(self.currentCommand()) ) self.setFocus() #self.SendScintilla(QsciScintilla.SCI_EMPTYUNDOBUFFER) def currentCommand(self): linenr, index = self.getCurLine() #for i in range(0, linenr): txtLength = self.text(linenr).length() string = self.text() cmdLine = string.right(txtLength - 4) cmd = str(cmdLine) return cmd def runCommand(self, cmd): self.updateHistory(cmd) self.SendScintilla(QsciScintilla.SCI_NEWLINE) self.buffer.append(cmd) src = "\n".join(self.buffer) more = self.runsource(src, "") if not more: self.buffer = [] output = sys.stdout.get_and_clean_data() if output: self.append(output) self.move_cursor_to_end() self.displayPrompt(more) def write(self, txt): self.SendScintilla(QsciScintilla.SCI_SETSTYLING, len(txt), 1) self.append(txt) self.SendScintilla(QsciScintilla.SCI_SETSTYLING, len(txt), 1)