QGIS/python/console.py
Martin Dobias 2fcf608acb Python console: consider also Enter on keypad. Fixes #3855.
Contributed by Steven Mizuno
2011-07-05 10:32:38 +02:00

332 lines
12 KiB
Python
Executable File

# -*- coding: utf-8 -*-
# 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 managerR plugin.
"""
Implementation of interactive Python console widget for QGIS.
Has +- the same behaviour as command-line interactive console:
- runs commands one by one
- supports expressions that span through more lines
- has command history, accessible using up/down keys
- supports pasting of commands
TODO:
- configuration - init commands, font, ...
- python code highlighting
"""
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from qgis.utils import iface
import sys
import traceback
import code
_init_commands = ["from qgis.core import *", "import qgis.utils"]
_console = None
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()
_old_stdout = sys.stdout
_console_output = None
def 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
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
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 sizeHint(self):
return QSize(500,300)
def closeEvent(self, event):
QWidget.closeEvent(self, event)
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)
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])
class PythonEdit(QTextEdit, code.InteractiveInterpreter):
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.buffer = []
self.insertInitText()
for line in _init_commands:
self.runsource(line)
self.displayPrompt(False)
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 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 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 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)
self.cursor.removeSelectedText()
self.cursor.insertText(self.currentPrompt)
self.historyIndex += 1
if self.historyIndex == len(self.history):
self.insertPlainText("")
else:
self.insertPlainText(self.history[self.historyIndex])
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)
self.cursor.removeSelectedText()
self.cursor.insertText(self.currentPrompt)
self.historyIndex -= 1
if self.historyIndex == len(self.history):
self.insertPlainText("")
else:
self.insertPlainText(self.history[self.historyIndex])
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 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():
if e.modifiers() & Qt.ControlModifier or e.modifiers() & Qt.MetaModifier:
if e.key() == Qt.Key_C or e.key() == Qt.Key_A:
QTextEdit.keyPressEvent(self, e)
else:
# all other keystrokes get sent to the input line
self.cursor.movePosition(QTextCursor.End, QTextCursor.MoveAnchor)
else:
# if Return is pressed, then perform the commands
if e.key() == Qt.Key_Return or e.key() == Qt.Key_Enter:
self.entered()
# if Up or Down is pressed
elif e.key() == Qt.Key_Down:
self.showPrevious()
elif e.key() == Qt.Key_Up:
self.showNext()
# if backspace is pressed, delete until we get to the prompt
elif e.key() == Qt.Key_Backspace:
if not self.cursor.hasSelection() and self.cursor.columnNumber() == self.currentPromptLength:
return
QTextEdit.keyPressEvent(self, e)
# if the left key is pressed, move left until we get to the prompt
elif e.key() == Qt.Key_Left and self.cursor.position() > self.document().lastBlock().position() + self.currentPromptLength:
anchor = QTextCursor.KeepAnchor if e.modifiers() & Qt.ShiftModifier else QTextCursor.MoveAnchor
move = QTextCursor.WordLeft if e.modifiers() & Qt.ControlModifier or e.modifiers() & Qt.MetaModifier else QTextCursor.Left
self.cursor.movePosition(move, anchor)
# use normal operation for right key
elif e.key() == Qt.Key_Right:
anchor = QTextCursor.KeepAnchor if e.modifiers() & Qt.ShiftModifier else QTextCursor.MoveAnchor
move = QTextCursor.WordRight if e.modifiers() & Qt.ControlModifier or e.modifiers() & Qt.MetaModifier else QTextCursor.Right
self.cursor.movePosition(move, anchor)
# if home is pressed, move cursor to right of prompt
elif e.key() == Qt.Key_Home:
anchor = QTextCursor.KeepAnchor if e.modifiers() & Qt.ShiftModifier else QTextCursor.MoveAnchor
self.cursor.movePosition(QTextCursor.StartOfBlock, anchor, 1)
self.cursor.movePosition(QTextCursor.Right, anchor, self.currentPromptLength)
# use normal operation for end key
elif e.key() == Qt.Key_End:
anchor = QTextCursor.KeepAnchor if e.modifiers() & Qt.ShiftModifier else QTextCursor.MoveAnchor
self.cursor.movePosition(QTextCursor.EndOfBlock, anchor, 1)
# use normal operation for all remaining keys
else:
QTextEdit.keyPressEvent(self, e)
self.setTextCursor(self.cursor)
self.ensureCursorVisible()
def insertFromMimeData(self, source):
self.cursor = self.textCursor()
if source.hasText():
pasteList = QStringList()
pasteList = source.text().split("\n")
# move the cursor to the end only if the text is multi-line or is going to be pasted not into the last line
if (len(pasteList) > 1) or (not self.isCursorInEditionZone()):
self.cursor.movePosition(QTextCursor.End, QTextCursor.MoveAnchor, 1)
self.setTextCursor(self.cursor)
# with multi-line text also run the commands
for line in pasteList[:-1]:
self.insertPlainText(line)
self.runCommand(unicode(self.currentCommand()))
# last line: only paste the text, do not run it
self.insertPlainText(unicode(pasteList[-1]))
def entered(self):
self.cursor.movePosition(QTextCursor.End, QTextCursor.MoveAnchor)
self.setTextCursor(self.cursor)
self.runCommand( unicode(self.currentCommand()) )
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]
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 runCommand(self, cmd):
self.updateHistory(cmd)
self.insertPlainText("\n")
self.buffer.append(cmd)
src = "\n".join(self.buffer)
more = self.runsource(src, "<input>")
if not more:
self.buffer = []
output = sys.stdout.get_and_clean_data()
if output:
self.insertTaggedText(output, ConsoleHighlighter.OUTPUT)
self.displayPrompt(more)
def write(self, txt):
""" reimplementation from code.InteractiveInterpreter """
self.insertTaggedText(txt, ConsoleHighlighter.ERROR)
if __name__ == '__main__':
a = QApplication(sys.argv)
show_console()
a.exec_()