Index: python/console.py =================================================================== --- python/console.py (revision 15043) +++ python/console.py (working copy) @@ -15,10 +15,10 @@ - supports expressions that span through more lines - has command history, accessible using up/down keys - supports pasting of commands +- python syntax highlighing (portions of code from http://diotavelli.net/PyQtWiki/Python%20syntax%20highlighting) TODO: - configuration - init commands, font, ... -- python code highlighting """ @@ -29,160 +29,340 @@ import traceback import code +EDIT_LINE, ERROR, OUTPUT, INIT = range(4) _init_commands = ["from qgis.core import *", "import qgis.utils"] _console = None +def format(color, style = ''): + """Return a QTextCharFormat with the given attributes. + """ + _color = QColor() + _color.setNamedColor(color) + + _format = QTextCharFormat() + _format.setForeground(_color) + if 'bold' in style: + _format.setFontWeight(QFont.Bold) + if 'italic' in style: + _format.setFontItalic(True) + + return _format + +# Syntax styles that can be shared by all languages +STYLES = { + 'keyword': format('blue'), + 'operator': format('red'), + 'brace': format('darkGray'), + 'defclass': format('black', 'bold'), + 'string': format('magenta'), + 'string2': format('darkMagenta'), + 'comment': format('darkGreen', 'italic'), + 'self': format('black', 'italic'), + 'numbers': format('brown'), +} + def show_console(): - """ called from QGIS to open the console """ - global _console - if _console is None: - _console = PythonConsole(iface.mainWindow()) - _console.show() # force show even if it was restored as hidden - else: - _console.setVisible(not _console.isVisible()) - # set focus to the edit box so the user can start typing - if _console.isVisible(): - _console.activateWindow() - _console.edit.setFocus() + """ called from QGIS to open the console """ + global _console + if _console is None: + _console = PythonConsole(iface.mainWindow()) + _console.show() # force show even if it was restored as hidden + else: + _console.setVisible(not _console.isVisible()) + # set focus to the edit box so the user can start typing + if _console.isVisible(): + _console.activateWindow() + _console.edit.setFocus() - + _old_stdout = sys.stdout _console_output = None def clearConsole(): - global _console - if _console is None: - return - _console.edit.clearConsole() + global _console + if _console is None: + return + _console.edit.clearConsole() - # hook for python console so all output will be redirected # and then shown in console def console_displayhook(obj): - global _console_output - _console_output = obj + global _console_output + _console_output = obj class QgisOutputCatcher: - def __init__(self): - self.data = '' - def write(self, stuff): - self.data += stuff - def get_and_clean_data(self): - tmp = self.data - self.data = '' - return tmp - def flush(self): - pass + def __init__(self): + self.data = '' + def write(self, stuff): + self.data += stuff + def get_and_clean_data(self): + tmp = self.data + self.data = '' + return tmp + def flush(self): + pass sys.stdout = QgisOutputCatcher() class PythonConsole(QDockWidget): - def __init__(self, parent=None): - QDockWidget.__init__(self, parent) - self.setObjectName("Python Console") - self.setAllowedAreas(Qt.BottomDockWidgetArea) - self.widget = QWidget() - self.l = QVBoxLayout(self.widget) - self.edit = PythonEdit() - self.l.addWidget(self.edit) - self.setWidget(self.widget) - self.setWindowTitle(QCoreApplication.translate("PythonConsole", "Python Console")) - # try to restore position from stored main window state - if not iface.mainWindow().restoreDockWidget(self): - iface.mainWindow().addDockWidget(Qt.BottomDockWidgetArea, self) + def __init__(self, parent = None): + QDockWidget.__init__(self, parent) + self.setObjectName("Python Console") + self.setAllowedAreas(Qt.AllDockWidgetAreas) + + self.widget = QWidget() + self.splitter = QSplitter(Qt.Vertical) + + self.layout = QVBoxLayout(self.widget) + self.edit = PythonEdit() + self.editarea = PythonTextArea() + + self.highlight = PythonHighlighter(self.editarea.document()) + self.edithighlight = PythonHighlighter(self.edit.document()) + + self.splitter.addWidget(self.edit) + self.splitter.addWidget(self.editarea) + self.layout.addWidget(self.splitter) + self.setWidget(self.widget) + + self.setWindowTitle(QCoreApplication.translate("PythonConsole", "Python Console")) + # try to restore position from stored main window state + if not iface.mainWindow().restoreDockWidget(self): + iface.mainWindow().addDockWidget(Qt.BottomDockWidgetArea, self) + + + def sizeHint(self): + return QSize(500, 300) + + def closeEvent(self, event): + QWidget.closeEvent(self, event) + +class PythonTextArea(QTextEdit): + def __init_(self, parent = None): + QTextEdit.__init__(self, parent) + + def keyPressEvent(self, e): + if e.modifiers() & Qt.ControlModifier: + if e.key() == Qt.Key_Return: + lines = QStringList() + lines = self.toPlainText().split("\n") + #run each command in the edit area + for line in lines: + _console.edit.insertPlainText(line) + _console.edit.runCommand(unicode(line)) + else: + QTextEdit.keyPressEvent(self, e) + elif e.key() == Qt.Key_Tab: + self.insertPlainText(" " * 4) + else: + QTextEdit.keyPressEvent(self, e) + + + +class PythonHighlighter (QSyntaxHighlighter): + #Syntax highlighter for the Python language. + # Python keywords + keywords = [ + 'and', 'assert', 'break', 'class', 'continue', 'def', + 'del', 'elif', 'else', 'except', 'exec', 'finally', + 'for', 'from', 'global', 'if', 'import', 'in', + 'is', 'lambda', 'not', 'or', 'pass', 'print', + 'raise', 'return', 'try', 'while', 'yield', + 'None', 'True', 'False', + ] - def sizeHint(self): - return QSize(500,300) + # Python operators + operators = [ + '=', + # Comparison + '==', '!=', '<', '<=', '>', '>=', + # Arithmetic + '\+', '-', '\*', '/', '//', '\%', '\*\*', + # In-place + '\+=', '-=', '\*=', '/=', '\%=', + # Bitwise + '\^', '\|', '\&', '\~', '>>', '<<', + ] - def closeEvent(self, event): - QWidget.closeEvent(self, event) + # Python braces + braces = [ + '\{', '\}', '\(', '\)', '\[', '\]', + ] + def __init__(self, document): + QSyntaxHighlighter.__init__(self, document) -class ConsoleHighlighter(QSyntaxHighlighter): - EDIT_LINE, ERROR, OUTPUT, INIT = range(4) - def __init__(self, doc): - QSyntaxHighlighter.__init__(self,doc) - formats = { self.OUTPUT : Qt.black, - self.ERROR : Qt.red, - self.EDIT_LINE : Qt.darkGreen, - self.INIT : Qt.gray } - self.f = {} - for tag, color in formats.iteritems(): - self.f[tag] = QTextCharFormat() - self.f[tag].setForeground(color) + # Multi-line strings (expression, flag, style) + # FIXME: The triple-quotes in these two lines will mess up the + # syntax highlighting from this point onward + self.tri_single = (QRegExp("'''"), 1, STYLES['string2']) + self.tri_double = (QRegExp('"""'), 2, STYLES['string2']) + + rules = [] - def highlightBlock(self, txt): - size = txt.length() - state = self.currentBlockState() - if state == self.OUTPUT or state == self.ERROR or state == self.INIT: - self.setFormat(0,size, self.f[state]) - # highlight prompt only - if state == self.EDIT_LINE: - self.setFormat(0,3, self.f[self.EDIT_LINE]) + # Keyword, operator, and brace rules + rules += [(r'\b%s\b' % w, 0, STYLES['keyword']) + for w in PythonHighlighter.keywords] + + rules += [(r'%s' % o, 0, STYLES['operator']) + for o in PythonHighlighter.operators] + + rules += [(r'%s' % b, 0, STYLES['brace']) + for b in PythonHighlighter.braces] + # All other rules + rules += [ + # 'self' + (r'\bself\b', 0, STYLES['self']), + # Double-quoted string, possibly containing escape sequences + (r'"[^"\\]*(\\.[^"\\]*)*"', 0, STYLES['string']), + # Single-quoted string, possibly containing escape sequences + (r"'[^'\\]*(\\.[^'\\]*)*'", 0, STYLES['string']), + + # 'def' followed by an identifier + (r'\bdef\b\s*(\w+)', 1, STYLES['defclass']), + # 'class' followed by an identifier + (r'\bclass\b\s*(\w+)', 1, STYLES['defclass']), + + # From '#' until a newline + (r'#[^\n]*', 0, STYLES['comment']), + + # Numeric literals + (r'\b[+-]?[0-9]+[lL]?\b', 0, STYLES['numbers']), + (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, STYLES['numbers']), + (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, STYLES['numbers']), + ] + + # Build a QRegExp for each pattern + self.rules = [(QRegExp(pat), index, fmt) + for (pat, index, fmt) in rules] + + + def highlightBlock(self, text): + """Apply syntax highlighting to the given block of text. + """ + + # Do other syntax formatting + for expression, nth, format in self.rules: + index = expression.indexIn(text, 0) + + while index >= 0: + # We actually want the index of the nth match + index = expression.pos(nth) + length = expression.cap(nth).length() + self.setFormat(index, length, format) + index = expression.indexIn(text, index + length) + + self.setCurrentBlockState(0) + + # Do multi-line strings + in_multiline = self.match_multiline(text, *self.tri_single) + if not in_multiline: + in_multiline = self.match_multiline(text, *self.tri_double) + + + def match_multiline(self, text, delimiter, in_state, style): + """Do highlighting of multi-line strings. ``delimiter`` should be a + ``QRegExp`` for triple-single-quotes or triple-double-quotes, and + ``in_state`` should be a unique integer to represent the corresponding + state changes when inside those strings. Returns True if we're still + inside a multi-line string when this function is finished. + """ + # If inside triple-single quotes, start at 0 + if self.previousBlockState() == in_state: + start = 0 + add = 0 + # Otherwise, look for the delimiter on this line + else: + start = delimiter.indexIn(text) + # Move past this match + add = delimiter.matchedLength() + + # As long as there's a delimiter match on this line... + while start >= 0: + # Look for the ending delimiter + end = delimiter.indexIn(text, start + add) + # Ending delimiter on this line? + if end >= add: + length = end - start + add + delimiter.matchedLength() + self.setCurrentBlockState(0) + # No; multi-line string + else: + self.setCurrentBlockState(in_state) + length = text.length() - start + add + # Apply formatting + self.setFormat(start, length, style) + # Look for the next match + start = delimiter.indexIn(text, start + length) + + # Return True if still inside a multi-line string, False otherwise + if self.currentBlockState() == in_state: + return True + else: + return False + + + class PythonEdit(QTextEdit, code.InteractiveInterpreter): + def __init__(self, parent = None): + QTextEdit.__init__(self, parent) + code.InteractiveInterpreter.__init__(self, locals = None) - def __init__(self,parent=None): - QTextEdit.__init__(self, parent) - code.InteractiveInterpreter.__init__(self, locals=None) + self.setTextInteractionFlags(Qt.TextEditorInteraction) + self.setAcceptDrops(False) + self.setMinimumSize(30, 30) + self.setUndoRedoEnabled(False) + self.setAcceptRichText(False) + monofont = QFont("Monospace") + monofont.setStyleHint(QFont.TypeWriter) + self.setFont(monofont) - self.setTextInteractionFlags(Qt.TextEditorInteraction) - self.setAcceptDrops(False) - self.setMinimumSize(30, 30) - self.setUndoRedoEnabled(False) - self.setAcceptRichText(False) - monofont = QFont("Monospace") - monofont.setStyleHint(QFont.TypeWriter) - self.setFont(monofont) + self.buffer = [] - self.buffer = [] - - self.insertInitText() + self.insertInitText() - for line in _init_commands: - self.runsource(line) + for line in _init_commands: + self.runsource(line) - self.displayPrompt(False) + self.displayPrompt(False) - self.history = QStringList() - self.historyIndex = 0 + self.history = QStringList() + self.historyIndex = 0 - self.high = ConsoleHighlighter(self) - - def insertInitText(self): - self.insertTaggedText(QCoreApplication.translate("PythonConsole", "To access Quantum GIS environment from this console\n" - "use qgis.utils.iface object (instance of QgisInterface class).\n\n"), - ConsoleHighlighter.INIT) + def insertInitText(self): + self.insertTaggedText(QCoreApplication.translate("PythonConsole", "#To access Quantum GIS environment from this console\n" + "#use qgis.utils.iface object (instance of QgisInterface class).\n\n"), + INIT) - def clearConsole(self): - self.clear() - self.insertInitText() + def clearConsole(self): + self.clear() + self.insertInitText() - def displayPrompt(self, more=False): - self.currentPrompt = "... " if more else ">>> " - self.currentPromptLength = len(self.currentPrompt) - self.insertTaggedLine(self.currentPrompt, ConsoleHighlighter.EDIT_LINE) - self.moveCursor(QTextCursor.End, QTextCursor.MoveAnchor) + def displayPrompt(self, more = False): + self.currentPrompt = "... " if more else ">>> " + self.currentPromptLength = len(self.currentPrompt) + self.insertTaggedLine(self.currentPrompt, EDIT_LINE) + self.moveCursor(QTextCursor.End, QTextCursor.MoveAnchor) - def isCursorInEditionZone(self): - cursor = self.textCursor() - pos = cursor.position() - block = self.document().lastBlock() - last = block.position() + self.currentPromptLength - return pos >= last + def isCursorInEditionZone(self): + cursor = self.textCursor() + pos = cursor.position() + block = self.document().lastBlock() + last = block.position() + self.currentPromptLength + return pos >= last - def currentCommand(self): - block = self.cursor.block() - text = block.text() - return text.right(text.length()-self.currentPromptLength) + def currentCommand(self): + block = self.cursor.block() + text = block.text() + return text.right(text.length() - self.currentPromptLength) - def showPrevious(self): + def showPrevious(self): if self.historyIndex < len(self.history) and not self.history.isEmpty(): self.cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.MoveAnchor) self.cursor.movePosition(QTextCursor.StartOfBlock, QTextCursor.KeepAnchor) @@ -194,7 +374,7 @@ else: self.insertPlainText(self.history[self.historyIndex]) - def showNext(self): + def showNext(self): if self.historyIndex > 0 and not self.history.isEmpty(): self.cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.MoveAnchor) self.cursor.movePosition(QTextCursor.StartOfBlock, QTextCursor.KeepAnchor) @@ -206,7 +386,7 @@ else: self.insertPlainText(self.history[self.historyIndex]) - def updateHistory(self, command): + def updateHistory(self, command): if isinstance(command, QStringList): for line in command: self.history.append(line) @@ -216,7 +396,7 @@ self.history.append(command) self.historyIndex = len(self.history) - def keyPressEvent(self, e): + def keyPressEvent(self, e): self.cursor = self.textCursor() # if the cursor isn't in the edition zone, don't do anything except Ctrl+C if not self.isCursorInEditionZone(): @@ -259,13 +439,15 @@ elif e.key() == Qt.Key_End: anchor = QTextCursor.KeepAnchor if e.modifiers() & Qt.ShiftModifier else QTextCursor.MoveAnchor self.cursor.movePosition(QTextCursor.EndOfBlock, anchor, 1) + elif e.key() == Qt.Key_Tab: + self.insertPlainText(" " * 4) # use normal operation for all remaining keys else: QTextEdit.keyPressEvent(self, e) self.setTextCursor(self.cursor) self.ensureCursorVisible() - def insertFromMimeData(self, source): + def insertFromMimeData(self, source): self.cursor = self.textCursor() self.cursor.movePosition(QTextCursor.End, QTextCursor.MoveAnchor, 1) self.setTextCursor(self.cursor) @@ -273,54 +455,54 @@ pasteList = QStringList() pasteList = source.text().split("\n") for line in pasteList: - self.insertPlainText(line) - self.runCommand(unicode(line)) + self.insertPlainText(line) + self.runCommand(unicode(line)) - def entered(self): - self.cursor.movePosition(QTextCursor.End, QTextCursor.MoveAnchor) - self.setTextCursor(self.cursor) - self.runCommand( unicode(self.currentCommand()) ) + def entered(self): + self.cursor.movePosition(QTextCursor.End, QTextCursor.MoveAnchor) + self.setTextCursor(self.cursor) + self.runCommand(unicode(self.currentCommand())) - def insertTaggedText(self, txt, tag): + def insertTaggedText(self, txt, tag): - if len(txt) > 0 and txt[-1] == '\n': # remove trailing newline to avoid one more empty line - txt = txt[0:-1] + if len(txt) > 0 and txt[-1] == '\n': # remove trailing newline to avoid one more empty line + txt = txt[0:-1] - c = self.textCursor() - for line in txt.split('\n'): - b = c.block() - b.setUserState(tag) - c.insertText(line) - c.insertBlock() + c = self.textCursor() + for line in txt.split('\n'): + b = c.block() + b.setUserState(tag) + c.insertText(line) + c.insertBlock() - def insertTaggedLine(self, txt, tag): - c = self.textCursor() - b = c.block() - b.setUserState(tag) - c.insertText(txt) + def insertTaggedLine(self, txt, tag): + c = self.textCursor() + b = c.block() + b.setUserState(tag) + c.insertText(txt) - def runCommand(self, cmd): + def runCommand(self, cmd): - self.updateHistory(cmd) + self.updateHistory(cmd) - self.insertPlainText("\n") + self.insertPlainText("\n") - self.buffer.append(cmd) - src = "\n".join(self.buffer) - more = self.runsource(src, "") - if not more: - self.buffer = [] + 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.insertTaggedText(output, ConsoleHighlighter.OUTPUT) - self.displayPrompt(more) + output = sys.stdout.get_and_clean_data() + if output: + self.insertTaggedText(output, OUTPUT) + self.displayPrompt(more) - def write(self, txt): - """ reimplementation from code.InteractiveInterpreter """ - self.insertTaggedText(txt, ConsoleHighlighter.ERROR) + def write(self, txt): + """ reimplementation from code.InteractiveInterpreter """ + self.insertTaggedText(txt, ERROR) if __name__ == '__main__': - a = QApplication(sys.argv) - show_console() - a.exec_() + a = QApplication(sys.argv) + show_console() + a.exec_()