# -*- python -*-
#
# OpenAlea.Visualea: OpenAlea graphical user interface
#
# Copyright 2006-2009 - 2008 INRIA - CIRAD - INRA
#
# File author(s): Samuel Dufour-Kowalski <samuel.dufour@sophia.inria.fr>
# Christophe Pradal <christophe.prada@cirad.fr>
#
# Distributed under the CeCILL v2 License.
# See accompanying file LICENSE.txt or copy at
# http://www.cecill.info/licences/Licence_CeCILL_V2-en.html
#
# OpenAlea WebSite : http://openalea.gforge.inria.fr
#
################################################################################
"""This module implements a QT4 python interpreter widget."""
__license__ = "CeCILL V2"
__revision__ = " $Id: scishell.py 3672 2012-12-05 12:28:19Z jcoste $"
import os, sys
from openalea.vpltk.qt import QtCore, QtGui
#from openalea.vpltk.qt.QtCore import Qt
Qt = QtCore.Qt
from PyQt4.Qsci import QsciScintilla, QsciLexerPython, QsciAPIs
from streamredirection import *
[docs]class SciShell(QsciScintilla,GraphicalStreamRedirection):
"""
SciShell is a Python shell based in QScintilla.
It is inspired by PyCute (pycute.py) : http://gerard.vermeulen.free.fr (GPL)
and Eric4 shell (shell.py) : http://www.die-offenbachs.de/eric/index.html (GPL)
"""
def __init__(self, interpreter, message="", log='', parent=None):
"""Constructor.
@param interpreter : InteractiveInterpreter in which
the code will be executed
@param message : welcome message string
@param 'parent' : specifies the parent widget.
If no parent widget has been specified, it is possible to
exit the interpreter by Ctrl-D.
"""
QsciScintilla.__init__(self, parent)
GraphicalStreamRedirection.__init__(self)
self.interpreter = interpreter
# user interface setup
self.setAutoIndent(True)
self.setAutoCompletionThreshold(4)
self.setAutoCompletionSource(QsciScintilla.AcsDocument)
# Lexer
self.setLexer(QsciLexerPython(self))
# Search
self.incrementalSearchString = ""
self.incrementalSearchActive = False
self.inRawMode = False
self.echoInput = True
# Initialize history
self.historyLists = {}
self.maxHistoryEntries = 30
self.history = []
self.histidx = -1
self.reading = 0
# interpreter prompt.
try:
sys.ps1
except AttributeError:
sys.ps1 = ">>> "
try:
sys.ps2
except AttributeError:
sys.ps2 = "... "
#self.completionText = ""
# Excecution Status
self.more = False
# Multi line execution Buffer
self.execlines = []
# interpreter banner
self.write('The shell running Python %s on %s.\n' %
(sys.version, sys.platform))
self.write('Type "copyright", "credits" or "license"'
' for more information on Python.\n')
self.write(message+'\n')
#self.write("help -> Python help system.\n")
self.write(" object? -> Print details about 'object'\n\n")
self.write(sys.ps1)
#self.standardCommands().clearKeys()
self.keymap = {
Qt.Key_Backspace : self.__QScintillaDeleteBack,
Qt.Key_Delete : self.__QScintillaDelete,
Qt.Key_Return : self.__QScintillaNewline,
Qt.Key_Enter : self.__QScintillaNewline,
Qt.Key_Tab : self.__QScintillaTab,
Qt.Key_Left : self.__QScintillaCharLeft,
Qt.Key_Right : self.__QScintillaCharRight,
Qt.Key_Up : self.__QScintillaLineUp,
Qt.Key_Down : self.__QScintillaLineDown,
Qt.Key_Home : self.__QScintillaVCHome,
Qt.Key_End : self.__QScintillaLineEnd,
}
self.connect(self, QtCore.SIGNAL('userListActivated(int)'),
self.__completionListSelected)
self.setFocus()
[docs] def customEvent(self,event):
GraphicalStreamRedirection.customEvent(self,event)
QsciScintilla.customEvent(self,event)
[docs] def clear(self):
""" Clear shell """
QsciScintilla.clear(self)
[docs] def get_interpreter(self):
""" Return the interpreter object """
return self.interpreter
[docs] def flush(self):
"""
Simulate stdin, stdout, and stderr.
"""
pass
[docs] def isatty(self):
"""
Simulate stdin, stdout, and stderr.
"""
return 1
[docs] def readline(self):
"""
Simulate stdin, stdout, and stderr.
"""
self.reading = 1
line, col = self.__getEndPos()
self.setCursorPosition(line, col)
buf = ""
if len(buf) == 0:
return '\n'
else:
return buf
[docs] def write(self, s):
"""
Simulate stdin, stdout, and stderr.
"""
line, col = self.__getEndPos()
self.setCursorPosition(line, col)
self.insert(s)
line, col = self.__getEndPos()
self.setCursorPosition(line, col)
self.prline, self.prcol = self.getCursorPosition()
self.ensureCursorVisible()
self.ensureLineVisible(line)
###########################################
def __getEndPos(self):
"""
Private method to return the line and column of the last character.
@return tuple of two values (int, int) giving the line and column
"""
line = self.lines() - 1
return (line, self.lineLength(line))
[docs] def paste(self):
"""
Reimplemented slot to handle the paste action.
"""
lines = unicode(QtGui.QApplication.clipboard().text())
self.__executeLines(lines)
def __middleMouseButton(self):
"""
Private method to handle the middle mouse button press.
"""
lines = unicode(QtGui.QApplication.clipboard().text(
QtGui.QClipboard.Selection))
self.__executeLines(lines)
def __executeLines(self, lines):
"""
Private method to execute a set of lines as multiple commands.
:param lines: multiple lines of text to be executed as single
commands (string)
"""
for line in lines.splitlines(True):
if line.endswith("\r\n"):
fullline = True
cmd = line[:-2]
elif line.endswith("\r") or line.endswith("\n"):
fullline = True
cmd = line[:-1]
else:
fullline = False
self.__insertTextAtEnd(line)
if fullline:
self.__executeCommand(cmd)
def __executeCommand(self, cmd):
"""
Private slot to execute a command.
@param cmd command to be executed by debug client (string)
"""
if not cmd: cmd = ''
else:
if len(self.history) == self.maxHistoryEntries:
del self.history[0]
self.history.append(str(cmd))
self.histidx = -1
if(cmd.endswith('?')):
self.__showHelp(cmd)
return
# Execute command
self.execlines.append(str(cmd))
source = '\n'.join(self.execlines)
self.more = self.interpreter.runsource(source)
if self.more:
self.write(sys.ps2)
else:
self.write(sys.ps1)
self.execlines = []
def __insertText(self, s):
"""
Insert text at the current cursor position.
"""
line, col = self.getCursorPosition()
self.insertAt(s, line, col)
self.setCursorPosition(line, col + len(str(s)))
def __insertTextAtEnd(self, s):
"""
Private method to insert some text at the end of the command line.
@param s text to be inserted (string)
"""
line, col = self.__getEndPos()
self.setCursorPosition(line, col)
self.insert(s)
self.prline, self.prcol = self.__getEndPos()
self.setCursorPosition(self.prline, self.prcol)
def __isCursorOnLastLine(self):
"""
Private method to check, if the cursor is on the last line.
"""
cline, ccol = self.getCursorPosition()
return cline == self.lines() - 1
[docs] def mousePressEvent(self, event):
"""
Protected method to handle the mouse press event.
@param event the mouse press event (QMouseEvent)
"""
self.setFocus()
if event.button() == Qt.MidButton:
self.__middleMouseButton()
else:
QsciScintilla.mousePressEvent(self, event)
[docs] def keyPressEvent(self, ev):
"""
Re-implemented to handle the user input a key at a time.
@param ev key event (QKeyEvent)
"""
txt = ev.text()
key = ev.key()
ctrl = ev.modifiers() & Qt.ControlModifier
shift = ev.modifiers() & Qt.ShiftModifier
# See it is text to insert.
if(self.keymap.has_key(key) and not shift and not ctrl):
self.keymap[key]()
elif ev == QtGui.QKeySequence.Paste:
self.paste()
elif self.__isCursorOnLastLine() and len(txt) :
QsciScintilla.keyPressEvent(self, ev)
self.incrementalSearchActive = True
if(txt == '.'):
self.__showDynCompletion()
elif(ctrl or shift):
QsciScintilla.keyPressEvent(self, ev)
else:
ev.ignore()
def __QScintillaTab(self):
"""
Private method to handle the Tab key.
"""
if self.isListActive():
self.SendScintilla(QsciScintilla.SCI_TAB)
elif self.__isCursorOnLastLine():
line, index = self.getCursorPosition()
buf = unicode(self.text(line)).replace(sys.ps1, "").replace(sys.ps2, "")
if self.more and not buf[:index-len(sys.ps2)].strip():
self.SendScintilla(QsciScintilla.SCI_TAB)
def __QScintillaDeleteBack(self):
"""
Private method to handle the Backspace key.
"""
if self.__isCursorOnLastLine():
line, col = self.getCursorPosition()
ac = self.isListActive()
oldLength = len(self.text(line))
if self.text(line).startswith(sys.ps1):
if col > len(sys.ps1):
self.SendScintilla(QsciScintilla.SCI_DELETEBACK)
elif self.text(line).startswith(sys.ps2):
if col > len(sys.ps2):
self.SendScintilla(QsciScintilla.SCI_DELETEBACK)
elif col > 0:
self.SendScintilla(QsciScintilla.SCI_DELETEBACK)
def __QScintillaDelete(self):
"""
Private method to handle the delete command.
"""
if self.__isCursorOnLastLine():
if self.hasSelectedText():
lineFrom, indexFrom, lineTo, indexTo = self.getSelection()
if self.text(lineFrom).startswith(sys.ps1):
if indexFrom >= len(sys.ps1):
self.SendScintilla(QsciScintilla.SCI_CLEAR)
elif self.text(lineFrom).startswith(sys.ps2):
if indexFrom >= len(sys.ps2):
self.SendScintilla(QsciScintilla.SCI_CLEAR)
elif indexFrom >= 0:
self.SendScintilla(QsciScintilla.SCI_CLEAR)
self.setSelection(lineTo, indexTo, lineTo, indexTo)
else:
self.SendScintilla(QsciScintilla.SCI_CLEAR)
def __QScintillaNewline(self):
"""
Private method to handle the Return key.
"""
if self.__isCursorOnLastLine():
if self.isListActive():
self.SendScintilla(QsciScintilla.SCI_NEWLINE)
elif self.reading:
self.reading = 0
else:
self.incrementalSearchString = ""
self.incrementalSearchActive = False
line, col = self.__getEndPos()
self.setCursorPosition(line,col)
buf = unicode(self.text(line)).replace(sys.ps1, "").replace(sys.ps2, "")
self.insert('\n')
self.__executeCommand(buf)
# add and run selection
else:
s= self.selectedText()
self.__insertTextAtEnd(s)
def __QScintillaCharLeft(self, allLinesAllowed = False):
"""
Private method to handle the Cursor Left command.
"""
if self.__isCursorOnLastLine() or allLinesAllowed:
line, col = self.getCursorPosition()
if self.text(line).startswith(sys.ps1):
if col > len(sys.ps1):
self.SendScintilla(QsciScintilla.SCI_CHARLEFT)
elif self.text(line).startswith(sys.ps2):
if col > len(sys.ps2):
self.SendScintilla(QsciScintilla.SCI_CHARLEFT)
elif col > 0:
self.SendScintilla(QsciScintilla.SCI_CHARLEFT)
def __QScintillaCharRight(self):
"""
Private method to handle the Cursor Right command.
"""
if self.__isCursorOnLastLine():
self.SendScintilla(QsciScintilla.SCI_CHARRIGHT)
def __QScintillaVCHome(self):
"""
Private method to handle the Home key.
"""
if self.isListActive():
self.SendScintilla(QsciScintilla.SCI_VCHOME)
elif self.__isCursorOnLastLine():
line, col = self.getCursorPosition()
if self.text(line).startswith(sys.ps1):
col = len(sys.ps1)
elif self.text(line).startswith(sys.ps2):
col = len(sys.ps2)
else:
col = 0
self.setCursorPosition(line, col)
def __QScintillaLineEnd(self):
"""
Private method to handle the End key.
"""
if self.isListActive():
self.SendScintilla(QsciScintilla.SCI_LINEEND)
elif self.__isCursorOnLastLine():
self.SendScintilla(QsciScintilla.SCI_LINEEND)
def __QScintillaLineUp(self):
"""
Private method to handle the Up key.
"""
if self.isListActive():
self.SendScintilla(QsciScintilla.SCI_LINEUP)
else:
line, col = self.__getEndPos()
buf = unicode(self.text(line)).replace(sys.ps1, "").replace(sys.ps2, "")
if buf and self.incrementalSearchActive:
if self.incrementalSearchString:
idx = self.__rsearchHistory(self.incrementalSearchString,
self.histidx)
if idx >= 0:
self.histidx = idx
self.__useHistory()
else:
idx = self.__rsearchHistory(buf)
if idx >= 0:
self.histidx = idx
self.incrementalSearchString = buf
self.__useHistory()
else:
if self.histidx < 0:
self.histidx = len(self.history)
if self.histidx > 0:
self.histidx = self.histidx - 1
self.__useHistory()
def __QScintillaLineDown(self):
"""
Private method to handle the Down key.
"""
if self.isListActive():
self.SendScintilla(QsciScintilla.SCI_LINEDOWN)
else:
line, col = self.__getEndPos()
buf = unicode(self.text(line)).replace(sys.ps1, "").replace(sys.ps2, "")
if buf and self.incrementalSearchActive:
if self.incrementalSearchString:
idx = self.__searchHistory(self.incrementalSearchString, self.histidx)
if idx >= 0:
self.histidx = idx
self.__useHistory()
else:
idx = self.__searchHistory(buf)
if idx >= 0:
self.histidx = idx
self.incrementalSearchString = buf
self.__useHistory()
else:
if self.histidx >= 0 and self.histidx < len(self.history):
self.histidx += 1
self.__useHistory()
def __useHistory(self):
"""
Private method to display a command from the history.
"""
if self.histidx < len(self.history):
cmd = self.history[self.histidx]
else:
cmd = str()
self.incrementalSearchString = ""
self.incrementalSearchActive = False
self.setCursorPosition(self.prline, self.prcol + len(self.more and sys.ps1 or sys.ps2))
self.setSelection(self.prline,self.prcol,\
self.prline,self.lineLength(self.prline))
self.removeSelectedText()
self.__insertText(cmd)
def __searchHistory(self, txt, startIdx = -1):
"""
Private method used to search the history.
@param txt text to match at the beginning (string)
@param startIdx index to start search from (integer)
@return index of
"""
if startIdx == -1:
idx = 0
else:
idx = startIdx + 1
while idx < len(self.history) and \
not self.history[idx].startswith(txt):
idx += 1
return idx
def __rsearchHistory(self, txt, startIdx = -1):
"""
Private method used to reverse search the history.
@param txt text to match at the beginning (string)
@param startIdx index to start search from (integer)
@return index of
"""
if startIdx == -1:
idx = len(self.history) - 1
else:
idx = startIdx - 1
while idx >= 0 and \
not self.history[idx].startswith(txt):
idx -= 1
return idx
[docs] def focusNextPrevChild(self, next):
"""
Reimplemented to stop Tab moving to the next window.
While the user is entering a multi-line command, the movement to
the next window by the Tab key being pressed is suppressed.
@param next next window
@return flag indicating the movement
"""
if next and self.more:
return False
return QsciScintilla.focusNextPrevChild(self,next)
def __get_current_line(self):
""" Return the current line """
line, col = self.__getEndPos()
self.setCursorPosition(line,col)
buf = unicode(self.text(line)).replace(sys.ps1, "").replace(sys.ps2, "")
text = buf.split()[-1][:-1]
return text
def __showHelp(self, text):
#text = self.__get_current_line()
self.__executeCommand('help(%s)'%(text[:-1],))
#self.__QScintillaNewline()
#self.__insertTextAtEnd(text)
def __showDynCompletion(self):
"""
Display a completion list based on the last token
"""
text = self.__get_current_line()
try:
locals = self.interpreter.locals
obj = eval(text, globals(), self.interpreter.locals)
l = dir(obj)
#l = filter(lambda x : not x.startswith('__'), l)
self.__showCompletions(l, text)
except : pass
def __showCompletions(self, completions, text):
"""
Private method to display the possible completions.
"""
if len(completions) == 0:
return
if len(completions) > 1:
completions.sort()
comps = list()
for comp in completions:
comps.append(comp)
self.showUserList(1, comps)
#self.completionText = text
else:
txt = completions[0]
if text != "":
txt = txt.replace(text, "")
self.__insertText(txt)
#self.completionText = ""
def __completionListSelected(self, id, txt):
"""
Private slot to handle the selection from the completion list.
@param id the ID of the user list (should be 1) (integer)
@param txt the selected text (str)
"""
# Remove already written characters
line, col = self.__getEndPos()
self.setCursorPosition(line,col)
buf = unicode(self.text(line))
ind = len(buf) - buf.rfind(".") - 1
if id == 1:
txt = unicode(txt[ind:])
#if self.completionText != "":
# txt = txt.replace(self.completionText, "")
self.__insertText(txt)
#self.completionText = ""
# Drag and Drop support
[docs] def dragEnterEvent(self, event):
event.setAccepted(event.mimeData().hasFormat("text/plain"))
[docs] def dragMoveEvent(self, event):
if (event.mimeData().hasFormat("text/plain")):
event.setDropAction(QtCore.Qt.MoveAction)
event.accept()
else:
event.ignore()
[docs] def dropEvent(self, event):
if(event.mimeData().hasFormat("text/plain")):
line = event.mimeData().text()
self.__insertTextAtEnd(line)
self.setFocus()
event.setDropAction(QtCore.Qt.MoveAction)
event.accept()
else:
event.ignore()