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_()