ipython_view.py
915 lines
| 34.0 KiB
| text/x-python
|
PythonLexer
ville
|
r988 | #!/usr/bin/python | ||
# -*- coding: iso-8859-15 -*- | ||||
''' | ||||
Gael Varoquaux
|
r1096 | Provides IPython WX console widgets. | ||
ville
|
r988 | |||
@author: Laurent Dufrechou | ||||
laurent.dufrechou _at_ gmail.com | ||||
This WX widget is based on the original work of Eitan Isaacson | ||||
that provided the console for the GTK toolkit. | ||||
Original work from: | ||||
@author: Eitan Isaacson | ||||
@organization: IBM Corporation | ||||
@copyright: Copyright (c) 2007 IBM Corporation | ||||
@license: BSD | ||||
All rights reserved. This program and the accompanying materials are made | ||||
available under the terms of the BSD which accompanies this distribution, and | ||||
is available at U{http://www.opensource.org/licenses/bsd-license.php} | ||||
''' | ||||
ldufrechou
|
r1621 | __version__ = 0.9 | ||
ville
|
r988 | __author__ = "Laurent Dufrechou" | ||
__email__ = "laurent.dufrechou _at_ gmail.com" | ||||
__license__ = "BSD" | ||||
import wx | ||||
import wx.stc as stc | ||||
import re | ||||
from StringIO import StringIO | ||||
Cody Precord
|
r1538 | import sys | ||
import codecs | ||||
import locale | ||||
for enc in (locale.getpreferredencoding(), | ||||
sys.getfilesystemencoding(), | ||||
sys.getdefaultencoding()): | ||||
try: | ||||
codecs.lookup(enc) | ||||
ENCODING = enc | ||||
break | ||||
except LookupError: | ||||
pass | ||||
else: | ||||
ENCODING = 'utf-8' | ||||
ldufrechou
|
r1101 | from ipshell_nonblocking import NonBlockingIPShell | ||
ldufrechou
|
r1091 | |||
Gael Varoquaux
|
r1096 | class WxNonBlockingIPShell(NonBlockingIPShell): | ||
ville
|
r988 | ''' | ||
Gael Varoquaux
|
r1096 | An NonBlockingIPShell Thread that is WX dependent. | ||
ville
|
r988 | ''' | ||
Gael Varoquaux
|
r1100 | def __init__(self, parent, | ||
ldufrechou
|
r1091 | argv=[],user_ns={},user_global_ns=None, | ||
ville
|
r988 | cin=None, cout=None, cerr=None, | ||
ldufrechou
|
r1153 | ask_exit_handler=None): | ||
ville
|
r988 | |||
Laurent Dufréchou
|
r1221 | NonBlockingIPShell.__init__(self, argv, user_ns, user_global_ns, | ||
ldufrechou
|
r1099 | cin, cout, cerr, | ||
ldufrechou
|
r1153 | ask_exit_handler) | ||
ville
|
r988 | |||
Gael Varoquaux
|
r1100 | self.parent = parent | ||
ville
|
r988 | |||
Gael Varoquaux
|
r1100 | self.ask_exit_callback = ask_exit_handler | ||
laurent.dufrechou@gmail.com
|
r1816 | self._IP.exit = self._ask_exit | ||
Gael Varoquaux
|
r1100 | |||
Laurent Dufréchou
|
r1221 | def addGUIShortcut(self, text, func): | ||
Gael Varoquaux
|
r1100 | wx.CallAfter(self.parent.add_button_handler, | ||
button_info={ 'text':text, | ||||
'func':self.parent.doExecuteLine(func)}) | ||||
ldufrechou
|
r1109 | |||
laurent.dufrechou@gmail.com
|
r1816 | def _ask_exit(self): | ||
Gael Varoquaux
|
r1100 | wx.CallAfter(self.ask_exit_callback, ()) | ||
ville
|
r988 | |||
laurent.dufrechou@gmail.com
|
r1816 | def _after_execute(self): | ||
Gael Varoquaux
|
r1100 | wx.CallAfter(self.parent.evtStateExecuteDone, ()) | ||
ville
|
r988 | |||
class WxConsoleView(stc.StyledTextCtrl): | ||||
''' | ||||
Specialized styled text control view for console-like workflow. | ||||
Gael Varoquaux
|
r1107 | We use here a scintilla frontend thus it can be reused in any GUI that | ||
supports scintilla with less work. | ||||
ville
|
r988 | |||
Gael Varoquaux
|
r1107 | @cvar ANSI_COLORS_BLACK: Mapping of terminal colors to X11 names. | ||
(with Black background) | ||||
ville
|
r988 | @type ANSI_COLORS_BLACK: dictionary | ||
Gael Varoquaux
|
r1107 | @cvar ANSI_COLORS_WHITE: Mapping of terminal colors to X11 names. | ||
(with White background) | ||||
ville
|
r988 | @type ANSI_COLORS_WHITE: dictionary | ||
@ivar color_pat: Regex of terminal color pattern | ||||
@type color_pat: _sre.SRE_Pattern | ||||
''' | ||||
Laurent Dufréchou
|
r1221 | ANSI_STYLES_BLACK = {'0;30': [0, 'WHITE'], '0;31': [1, 'RED'], | ||
'0;32': [2, 'GREEN'], '0;33': [3, 'BROWN'], | ||||
'0;34': [4, 'BLUE'], '0;35': [5, 'PURPLE'], | ||||
'0;36': [6, 'CYAN'], '0;37': [7, 'LIGHT GREY'], | ||||
'1;30': [8, 'DARK GREY'], '1;31': [9, 'RED'], | ||||
'1;32': [10, 'SEA GREEN'], '1;33': [11, 'YELLOW'], | ||||
'1;34': [12, 'LIGHT BLUE'], '1;35': | ||||
[13, 'MEDIUM VIOLET RED'], | ||||
'1;36': [14, 'LIGHT STEEL BLUE'], '1;37': [15, 'YELLOW']} | ||||
ANSI_STYLES_WHITE = {'0;30': [0, 'BLACK'], '0;31': [1, 'RED'], | ||||
'0;32': [2, 'GREEN'], '0;33': [3, 'BROWN'], | ||||
'0;34': [4, 'BLUE'], '0;35': [5, 'PURPLE'], | ||||
'0;36': [6, 'CYAN'], '0;37': [7, 'LIGHT GREY'], | ||||
'1;30': [8, 'DARK GREY'], '1;31': [9, 'RED'], | ||||
'1;32': [10, 'SEA GREEN'], '1;33': [11, 'YELLOW'], | ||||
'1;34': [12, 'LIGHT BLUE'], '1;35': | ||||
[13, 'MEDIUM VIOLET RED'], | ||||
'1;36': [14, 'LIGHT STEEL BLUE'], '1;37': [15, 'YELLOW']} | ||||
def __init__(self, parent, prompt, intro="", background_color="BLACK", | ||||
Gael Varoquaux
|
r1106 | pos=wx.DefaultPosition, ID = -1, size=wx.DefaultSize, | ||
ldufrechou
|
r1159 | style=0, autocomplete_mode = 'IPYTHON'): | ||
ville
|
r988 | ''' | ||
Initialize console view. | ||||
@param parent: Parent widget | ||||
@param prompt: User specified prompt | ||||
@type intro: string | ||||
@param intro: User specified startup introduction string | ||||
@type intro: string | ||||
@param background_color: Can be BLACK or WHITE | ||||
@type background_color: string | ||||
@param other: init param of styledTextControl (can be used as-is) | ||||
ldufrechou
|
r1159 | @param autocomplete_mode: Can be 'IPYTHON' or 'STC' | ||
'IPYTHON' show autocompletion the ipython way | ||||
'STC" show it scintilla text control way | ||||
ville
|
r988 | ''' | ||
stc.StyledTextCtrl.__init__(self, parent, ID, pos, size, style) | ||||
Gael Varoquaux
|
r1106 | ####### Scintilla configuration ################################### | ||
ville
|
r988 | |||
Gael Varoquaux
|
r1106 | # Ctrl + B or Ctrl + N can be used to zoomin/zoomout the text inside | ||
# the widget | ||||
ville
|
r988 | self.CmdKeyAssign(ord('B'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMIN) | ||
self.CmdKeyAssign(ord('N'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMOUT) | ||||
ldufrechou
|
r1162 | #We draw a line at position 80 | ||
self.SetEdgeMode(stc.STC_EDGE_LINE) | ||||
self.SetEdgeColumn(80) | ||||
self.SetEdgeColour(wx.LIGHT_GREY) | ||||
#self.SetViewWhiteSpace(True) | ||||
#self.SetViewEOL(True) | ||||
self.SetEOLMode(stc.STC_EOL_CRLF) | ||||
#self.SetWrapMode(stc.STC_WRAP_CHAR) | ||||
#self.SetWrapMode(stc.STC_WRAP_WORD) | ||||
self.SetBufferedDraw(True) | ||||
#self.SetUseAntiAliasing(True) | ||||
self.SetLayoutCache(stc.STC_CACHE_PAGE) | ||||
ldufrechou
|
r1163 | self.SetUndoCollection(False) | ||
Laurent Dufréchou
|
r1221 | self.SetUseTabs(True) | ||
self.SetIndent(4) | ||||
self.SetTabWidth(4) | ||||
ldufrechou
|
r1162 | |||
self.EnsureCaretVisible() | ||||
Laurent Dufréchou
|
r1221 | self.SetMargins(3, 3) #text is moved away from border with 3px | ||
ldufrechou
|
r1162 | # Suppressing Scintilla margins | ||
Laurent Dufréchou
|
r1221 | self.SetMarginWidth(0, 0) | ||
self.SetMarginWidth(1, 0) | ||||
self.SetMarginWidth(2, 0) | ||||
ldufrechou
|
r1162 | |||
self.background_color = background_color | ||||
self.buildStyles() | ||||
self.indent = 0 | ||||
self.prompt_count = 0 | ||||
self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?') | ||||
self.write(intro) | ||||
self.setPrompt(prompt) | ||||
self.showPrompt() | ||||
self.autocomplete_mode = autocomplete_mode | ||||
self.Bind(wx.EVT_KEY_DOWN, self._onKeypress) | ||||
ldufrechou
|
r1168 | |||
ldufrechou
|
r1162 | def buildStyles(self): | ||
ville
|
r988 | #we define platform specific fonts | ||
if wx.Platform == '__WXMSW__': | ||||
Laurent Dufréchou
|
r1221 | faces = { 'times': 'Times New Roman', | ||
'mono' : 'Courier New', | ||||
'helv' : 'Arial', | ||||
'other': 'Comic Sans MS', | ||||
'size' : 10, | ||||
'size2': 8, | ||||
} | ||||
ville
|
r988 | elif wx.Platform == '__WXMAC__': | ||
faces = { 'times': 'Times New Roman', | ||||
'mono' : 'Monaco', | ||||
'helv' : 'Arial', | ||||
'other': 'Comic Sans MS', | ||||
'size' : 10, | ||||
'size2': 8, | ||||
} | ||||
else: | ||||
faces = { 'times': 'Times', | ||||
'mono' : 'Courier', | ||||
'helv' : 'Helvetica', | ||||
'other': 'new century schoolbook', | ||||
'size' : 10, | ||||
'size2': 8, | ||||
} | ||||
# make some styles | ||||
ldufrechou
|
r1162 | if self.background_color != "BLACK": | ||
ville
|
r988 | self.background_color = "WHITE" | ||
self.SetCaretForeground("BLACK") | ||||
self.ANSI_STYLES = self.ANSI_STYLES_WHITE | ||||
else: | ||||
self.SetCaretForeground("WHITE") | ||||
self.ANSI_STYLES = self.ANSI_STYLES_BLACK | ||||
Gael Varoquaux
|
r1106 | self.StyleSetSpec(stc.STC_STYLE_DEFAULT, | ||
"fore:%s,back:%s,size:%d,face:%s" | ||||
% (self.ANSI_STYLES['0;30'][1], | ||||
self.background_color, | ||||
faces['size'], faces['mono'])) | ||||
ville
|
r988 | self.StyleClearAll() | ||
Gael Varoquaux
|
r1106 | self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT, | ||
"fore:#FF0000,back:#0000FF,bold") | ||||
self.StyleSetSpec(stc.STC_STYLE_BRACEBAD, | ||||
"fore:#000000,back:#FF0000,bold") | ||||
ldufrechou
|
r1162 | |||
ville
|
r988 | for style in self.ANSI_STYLES.values(): | ||
self.StyleSetSpec(style[0], "bold,fore:%s" % style[1]) | ||||
####################################################################### | ||||
Laurent Dufréchou
|
r1221 | def setBackgroundColor(self, color): | ||
ldufrechou
|
r1162 | self.background_color = color | ||
self.buildStyles() | ||||
ldufrechou
|
r1159 | |||
Laurent Dufréchou
|
r1221 | def getBackgroundColor(self, color): | ||
ldufrechou
|
r1162 | return self.background_color | ||
ville
|
r988 | |||
ldufrechou
|
r1169 | def asyncWrite(self, text): | ||
ldufrechou
|
r1153 | ''' | ||
Write given text to buffer in an asynchroneous way. | ||||
It is used from another thread to be able to acces the GUI. | ||||
@param text: Text to append | ||||
@type text: string | ||||
''' | ||||
try: | ||||
Laurent Dufréchou
|
r1221 | wx.MutexGuiEnter() | ||
ldufrechou
|
r1153 | |||
Laurent Dufréchou
|
r1221 | #be sure not to be interrutpted before the MutexGuiLeave! | ||
self.write(text) | ||||
ldufrechou
|
r1168 | |||
ldufrechou
|
r1153 | except KeyboardInterrupt: | ||
Laurent Dufréchou
|
r1221 | wx.MutexGuiLeave() | ||
raise KeyboardInterrupt | ||||
ldufrechou
|
r1153 | wx.MutexGuiLeave() | ||
ville
|
r988 | def write(self, text): | ||
''' | ||||
Write given text to buffer. | ||||
@param text: Text to append. | ||||
@type text: string | ||||
''' | ||||
segments = self.color_pat.split(text) | ||||
segment = segments.pop(0) | ||||
Laurent Dufréchou
|
r1221 | self.StartStyling(self.getCurrentLineEnd(), 0xFF) | ||
ville
|
r988 | self.AppendText(segment) | ||
if segments: | ||||
ansi_tags = self.color_pat.findall(text) | ||||
for tag in ansi_tags: | ||||
i = segments.index(tag) | ||||
Laurent Dufréchou
|
r1221 | self.StartStyling(self.getCurrentLineEnd(), 0xFF) | ||
ville
|
r988 | self.AppendText(segments[i+1]) | ||
if tag != '0': | ||||
Laurent Dufréchou
|
r1221 | self.SetStyling(len(segments[i+1]), self.ANSI_STYLES[tag][0]) | ||
ville
|
r988 | |||
segments.pop(i) | ||||
self.moveCursor(self.getCurrentLineEnd()) | ||||
def getPromptLen(self): | ||||
''' | ||||
Return the length of current prompt | ||||
''' | ||||
return len(str(self.prompt_count)) + 7 | ||||
Laurent Dufréchou
|
r1221 | def setPrompt(self, prompt): | ||
ville
|
r988 | self.prompt = prompt | ||
Laurent Dufréchou
|
r1221 | def setIndentation(self, indentation): | ||
ville
|
r988 | self.indent = indentation | ||
Laurent Dufréchou
|
r1221 | def setPromptCount(self, count): | ||
ville
|
r988 | self.prompt_count = count | ||
def showPrompt(self): | ||||
''' | ||||
Prints prompt at start of line. | ||||
@param prompt: Prompt to print. | ||||
@type prompt: string | ||||
''' | ||||
self.write(self.prompt) | ||||
#now we update the position of end of prompt | ||||
self.current_start = self.getCurrentLineEnd() | ||||
autoindent = self.indent*' ' | ||||
autoindent = autoindent.replace(' ','\t') | ||||
self.write(autoindent) | ||||
def changeLine(self, text): | ||||
''' | ||||
Replace currently entered command line with given text. | ||||
@param text: Text to use as replacement. | ||||
@type text: string | ||||
''' | ||||
Laurent Dufréchou
|
r1221 | self.SetSelection(self.getCurrentPromptStart(), self.getCurrentLineEnd()) | ||
ville
|
r988 | self.ReplaceSelection(text) | ||
self.moveCursor(self.getCurrentLineEnd()) | ||||
def getCurrentPromptStart(self): | ||||
return self.current_start | ||||
def getCurrentLineStart(self): | ||||
return self.GotoLine(self.LineFromPosition(self.GetCurrentPos())) | ||||
def getCurrentLineEnd(self): | ||||
return self.GetLength() | ||||
def getCurrentLine(self): | ||||
''' | ||||
Get text in current command line. | ||||
@return: Text of current command line. | ||||
@rtype: string | ||||
''' | ||||
return self.GetTextRange(self.getCurrentPromptStart(), | ||||
self.getCurrentLineEnd()) | ||||
def moveCursorOnNewValidKey(self): | ||||
#If cursor is at wrong position put it at last line... | ||||
if self.GetCurrentPos() < self.getCurrentPromptStart(): | ||||
self.GotoPos(self.getCurrentPromptStart()) | ||||
Laurent Dufréchou
|
r1221 | def removeFromTo(self, from_pos, to_pos): | ||
ville
|
r988 | if from_pos < to_pos: | ||
Laurent Dufréchou
|
r1221 | self.SetSelection(from_pos, to_pos) | ||
ville
|
r988 | self.DeleteBack() | ||
def removeCurrentLine(self): | ||||
self.LineDelete() | ||||
Laurent Dufréchou
|
r1221 | def moveCursor(self, position): | ||
ville
|
r988 | self.GotoPos(position) | ||
def getCursorPos(self): | ||||
return self.GetCurrentPos() | ||||
Laurent Dufréchou
|
r1221 | def selectFromTo(self, from_pos, to_pos): | ||
ville
|
r988 | self.SetSelectionStart(from_pos) | ||
self.SetSelectionEnd(to_pos) | ||||
Laurent Dufréchou
|
r1221 | def writeHistory(self, history): | ||
self.removeFromTo(self.getCurrentPromptStart(), self.getCurrentLineEnd()) | ||||
ville
|
r988 | self.changeLine(history) | ||
ldufrechou
|
r1159 | |||
def setCompletionMethod(self, completion): | ||||
Laurent Dufréchou
|
r1221 | if completion in ['IPYTHON', 'STC']: | ||
ldufrechou
|
r1159 | self.autocomplete_mode = completion | ||
else: | ||||
raise AttributeError | ||||
def getCompletionMethod(self, completion): | ||||
return self.autocomplete_mode | ||||
ville
|
r988 | |||
def writeCompletion(self, possibilities): | ||||
ldufrechou
|
r1159 | if self.autocomplete_mode == 'IPYTHON': | ||
Laurent Dufréchou
|
r1221 | max_len = len(max(possibilities, key=len)) | ||
max_symbol = ' '*max_len | ||||
ldufrechou
|
r1159 | |||
#now we check how much symbol we can put on a line... | ||||
test_buffer = max_symbol + ' '*4 | ||||
allowed_symbols = 80/len(test_buffer) | ||||
if allowed_symbols == 0: | ||||
Laurent Dufréchou
|
r1221 | allowed_symbols = 1 | ||
ldufrechou
|
r1159 | |||
pos = 1 | ||||
buf = '' | ||||
for symbol in possibilities: | ||||
#buf += symbol+'\n'#*spaces) | ||||
Laurent Dufréchou
|
r1221 | if pos < allowed_symbols: | ||
ldufrechou
|
r1159 | spaces = max_len - len(symbol) + 4 | ||
buf += symbol+' '*spaces | ||||
pos += 1 | ||||
else: | ||||
Laurent Dufréchou
|
r1221 | buf += symbol+'\n' | ||
ldufrechou
|
r1159 | pos = 1 | ||
self.write(buf) | ||||
else: | ||||
possibilities.sort() # Python sorts are case sensitive | ||||
self.AutoCompSetIgnoreCase(False) | ||||
ldufrechou
|
r1160 | self.AutoCompSetAutoHide(False) | ||
ldufrechou
|
r1159 | #let compute the length ot last word | ||
laurent dufrechou
|
r1811 | splitter = [' ', '(', '[', '{','='] | ||
ldufrechou
|
r1159 | last_word = self.getCurrentLine() | ||
for breaker in splitter: | ||||
last_word = last_word.split(breaker)[-1] | ||||
self.AutoCompShow(len(last_word), " ".join(possibilities)) | ||||
ville
|
r988 | |||
def _onKeypress(self, event, skip=True): | ||||
''' | ||||
Key press callback used for correcting behavior for console-like | ||||
interfaces. For example 'home' should go to prompt, not to begining of | ||||
line. | ||||
@param widget: Widget that key press accored in. | ||||
@type widget: gtk.Widget | ||||
@param event: Event object | ||||
@type event: gtk.gdk.Event | ||||
@return: Return True if event as been catched. | ||||
@rtype: boolean | ||||
''' | ||||
ldufrechou
|
r1159 | if not self.AutoCompActive(): | ||
if event.GetKeyCode() == wx.WXK_HOME: | ||||
if event.Modifiers == wx.MOD_NONE: | ||||
self.moveCursorOnNewValidKey() | ||||
ville
|
r988 | self.moveCursor(self.getCurrentPromptStart()) | ||
ldufrechou
|
r1159 | return True | ||
elif event.Modifiers == wx.MOD_SHIFT: | ||||
self.moveCursorOnNewValidKey() | ||||
Laurent Dufréchou
|
r1221 | self.selectFromTo(self.getCurrentPromptStart(), self.getCursorPos()) | ||
ldufrechou
|
r1159 | return True | ||
else: | ||||
return False | ||||
elif event.GetKeyCode() == wx.WXK_LEFT: | ||||
if event.Modifiers == wx.MOD_NONE: | ||||
self.moveCursorOnNewValidKey() | ||||
self.moveCursor(self.getCursorPos()-1) | ||||
if self.getCursorPos() < self.getCurrentPromptStart(): | ||||
self.moveCursor(self.getCurrentPromptStart()) | ||||
return True | ||||
elif event.GetKeyCode() == wx.WXK_BACK: | ||||
self.moveCursorOnNewValidKey() | ||||
if self.getCursorPos() > self.getCurrentPromptStart(): | ||||
event.Skip() | ||||
ville
|
r988 | return True | ||
ldufrechou
|
r1159 | |||
if skip: | ||||
Laurent Dufréchou
|
r1221 | if event.GetKeyCode() not in [wx.WXK_PAGEUP, wx.WXK_PAGEDOWN]\ | ||
and event.Modifiers == wx.MOD_NONE: | ||||
ldufrechou
|
r1159 | self.moveCursorOnNewValidKey() | ||
ldufrechou
|
r1134 | event.Skip() | ||
ldufrechou
|
r1159 | return True | ||
return False | ||||
else: | ||||
ville
|
r988 | event.Skip() | ||
ldufrechou
|
r1159 | |||
ville
|
r988 | def OnUpdateUI(self, evt): | ||
# check for matching braces | ||||
braceAtCaret = -1 | ||||
braceOpposite = -1 | ||||
charBefore = None | ||||
caretPos = self.GetCurrentPos() | ||||
if caretPos > 0: | ||||
charBefore = self.GetCharAt(caretPos - 1) | ||||
styleBefore = self.GetStyleAt(caretPos - 1) | ||||
# check before | ||||
if charBefore and chr(charBefore) in "[]{}()" and styleBefore == stc.STC_P_OPERATOR: | ||||
braceAtCaret = caretPos - 1 | ||||
# check after | ||||
if braceAtCaret < 0: | ||||
charAfter = self.GetCharAt(caretPos) | ||||
styleAfter = self.GetStyleAt(caretPos) | ||||
if charAfter and chr(charAfter) in "[]{}()" and styleAfter == stc.STC_P_OPERATOR: | ||||
braceAtCaret = caretPos | ||||
if braceAtCaret >= 0: | ||||
braceOpposite = self.BraceMatch(braceAtCaret) | ||||
if braceAtCaret != -1 and braceOpposite == -1: | ||||
self.BraceBadLight(braceAtCaret) | ||||
else: | ||||
self.BraceHighlight(braceAtCaret, braceOpposite) | ||||
#pt = self.PointFromPosition(braceOpposite) | ||||
#self.Refresh(True, wxRect(pt.x, pt.y, 5,5)) | ||||
#print pt | ||||
#self.Refresh(False) | ||||
Gael Varoquaux
|
r1107 | class IPShellWidget(wx.Panel): | ||
ville
|
r988 | ''' | ||
This is wx.Panel that embbed the IPython Thread and the wx.StyledTextControl | ||||
Gael Varoquaux
|
r1107 | If you want to port this to any other GUI toolkit, just replace the | ||
WxConsoleView by YOURGUIConsoleView and make YOURGUIIPythonView derivate | ||||
from whatever container you want. I've choosed to derivate from a wx.Panel | ||||
because it seems to be more useful | ||||
Any idea to make it more 'generic' welcomed. | ||||
ville
|
r988 | ''' | ||
ldufrechou
|
r1133 | |||
ldufrechou
|
r1130 | def __init__(self, parent, intro=None, | ||
Gael Varoquaux
|
r1096 | background_color="BLACK", add_button_handler=None, | ||
ldufrechou
|
r1152 | wx_ip_shell=None, user_ns={},user_global_ns=None, | ||
Gael Varoquaux
|
r1096 | ): | ||
ville
|
r988 | ''' | ||
Initialize. | ||||
Instanciate an IPython thread. | ||||
Instanciate a WxConsoleView. | ||||
Redirect I/O to console. | ||||
''' | ||||
ldufrechou
|
r1158 | wx.Panel.__init__(self,parent,wx.ID_ANY) | ||
ville
|
r988 | |||
ldufrechou
|
r1168 | self.parent = parent | ||
ldufrechou
|
r1099 | ### IPython non blocking shell instanciation ### | ||
ville
|
r988 | self.cout = StringIO() | ||
ldufrechou
|
r1091 | self.add_button_handler = add_button_handler | ||
Gael Varoquaux
|
r1096 | if wx_ip_shell is not None: | ||
self.IP = wx_ip_shell | ||||
else: | ||||
self.IP = WxNonBlockingIPShell(self, | ||||
ldufrechou
|
r1134 | cout = self.cout, cerr = self.cout, | ||
ldufrechou
|
r1153 | ask_exit_handler = self.askExitCallback) | ||
ldufrechou
|
r1099 | |||
ville
|
r988 | ### IPython wx console view instanciation ### | ||
#If user didn't defined an intro text, we create one for him | ||||
ldufrechou
|
r1163 | #If you really wnat an empty intro just call wxIPythonViewPanel | ||
Gael Varoquaux
|
r1106 | #with intro='' | ||
ldufrechou
|
r1130 | if intro is None: | ||
ville
|
r988 | welcome_text = "Welcome to WxIPython Shell.\n\n" | ||
laurent.dufrechou@gmail.com
|
r1816 | welcome_text+= self.IP.get_banner() | ||
ville
|
r988 | welcome_text+= "!command -> Execute command in shell\n" | ||
welcome_text+= "TAB -> Autocompletion\n" | ||||
ldufrechou
|
r1130 | else: | ||
welcome_text = intro | ||||
ville
|
r988 | |||
self.text_ctrl = WxConsoleView(self, | ||||
laurent.dufrechou@gmail.com
|
r1816 | self.IP.get_prompt(), | ||
ville
|
r988 | intro=welcome_text, | ||
background_color=background_color) | ||||
ldufrechou
|
r1133 | |||
ldufrechou
|
r1163 | option_text = wx.StaticText(self, -1, "Options:") | ||
self.completion_option = wx.CheckBox(self, -1, "Scintilla Completion") | ||||
ldufrechou
|
r1622 | self.completion_option.SetToolTip(wx.ToolTip( | ||
"Selects the completion type:\nEither Ipython default style or Scintilla one")) | ||||
ldufrechou
|
r1168 | #self.completion_option.SetValue(False) | ||
ldufrechou
|
r1163 | self.background_option = wx.CheckBox(self, -1, "White Background") | ||
ldufrechou
|
r1622 | self.background_option.SetToolTip(wx.ToolTip( | ||
"Selects the back ground color: BLACK or WHITE")) | ||||
ldufrechou
|
r1168 | #self.background_option.SetValue(False) | ||
ldufrechou
|
r1621 | self.threading_option = wx.CheckBox(self, -1, "Execute in thread") | ||
ldufrechou
|
r1622 | self.threading_option.SetToolTip(wx.ToolTip( | ||
"Use threading: infinite loop don't freeze the GUI and commands can be breaked\nNo threading: maximum compatibility")) | ||||
ldufrechou
|
r1621 | #self.threading_option.SetValue(False) | ||
ldufrechou
|
r1168 | |||
self.options={'completion':{'value':'IPYTHON', | ||||
'checkbox':self.completion_option,'STC':True,'IPYTHON':False, | ||||
'setfunc':self.text_ctrl.setCompletionMethod}, | ||||
'background_color':{'value':'BLACK', | ||||
'checkbox':self.background_option,'WHITE':True,'BLACK':False, | ||||
'setfunc':self.text_ctrl.setBackgroundColor}, | ||||
ldufrechou
|
r1621 | 'threading':{'value':'True', | ||
'checkbox':self.threading_option,'True':True,'False':False, | ||||
laurent.dufrechou@gmail.com
|
r1816 | 'setfunc':self.IP.set_threading}, | ||
ldufrechou
|
r1168 | } | ||
ldufrechou
|
r1621 | |||
#self.cout.write dEfault option is asynchroneous because default sate is threading ON | ||||
self.cout.write = self.text_ctrl.asyncWrite | ||||
#we reloard options | ||||
ldufrechou
|
r1168 | self.reloadOptions(self.options) | ||
ville
|
r988 | |||
ldufrechou
|
r1158 | self.text_ctrl.Bind(wx.EVT_KEY_DOWN, self.keyPress) | ||
ldufrechou
|
r1162 | self.completion_option.Bind(wx.EVT_CHECKBOX, self.evtCheckOptionCompletion) | ||
self.background_option.Bind(wx.EVT_CHECKBOX, self.evtCheckOptionBackgroundColor) | ||||
ldufrechou
|
r1621 | self.threading_option.Bind(wx.EVT_CHECKBOX, self.evtCheckOptionThreading) | ||
ldufrechou
|
r1159 | |||
ville
|
r988 | ### making the layout of the panel ### | ||
sizer = wx.BoxSizer(wx.VERTICAL) | ||||
sizer.Add(self.text_ctrl, 1, wx.EXPAND) | ||||
ldufrechou
|
r1159 | option_sizer = wx.BoxSizer(wx.HORIZONTAL) | ||
ldufrechou
|
r1163 | sizer.Add(option_sizer, 0) | ||
option_sizer.AddMany([(10, 20), | ||||
(option_text, 0, wx.ALIGN_CENTER_VERTICAL), | ||||
(5, 5), | ||||
(self.completion_option, 0, wx.ALIGN_CENTER_VERTICAL), | ||||
(8, 8), | ||||
ldufrechou
|
r1621 | (self.background_option, 0, wx.ALIGN_CENTER_VERTICAL), | ||
(8, 8), | ||||
(self.threading_option, 0, wx.ALIGN_CENTER_VERTICAL) | ||||
ldufrechou
|
r1159 | ]) | ||
ville
|
r988 | self.SetAutoLayout(True) | ||
sizer.Fit(self) | ||||
sizer.SetSizeHints(self) | ||||
self.SetSizer(sizer) | ||||
#and we focus on the widget :) | ||||
self.SetFocus() | ||||
ldufrechou
|
r1099 | #widget state management (for key handling different cases) | ||
self.setCurrentState('IDLE') | ||||
Gael Varoquaux
|
r1096 | self.pager_state = 'DONE' | ||
ldufrechou
|
r1153 | self.raw_input_current_line = 0 | ||
ville
|
r988 | |||
ldufrechou
|
r1130 | def askExitCallback(self, event): | ||
self.askExitHandler(event) | ||||
Gael Varoquaux
|
r1106 | #---------------------- IPython Thread Management ------------------------ | ||
ldufrechou
|
r1091 | def stateDoExecuteLine(self): | ||
ldufrechou
|
r1138 | lines=self.text_ctrl.getCurrentLine() | ||
ldufrechou
|
r1133 | self.text_ctrl.write('\n') | ||
ldufrechou
|
r1153 | lines_to_execute = lines.replace('\t',' '*4) | ||
ldufrechou
|
r1160 | lines_to_execute = lines_to_execute.replace('\r','') | ||
laurent.dufrechou@gmail.com
|
r1816 | self.IP.do_execute(lines_to_execute.encode(ENCODING)) | ||
ldufrechou
|
r1153 | self.updateHistoryTracker(lines) | ||
ldufrechou
|
r1099 | self.setCurrentState('WAIT_END_OF_EXECUTION') | ||
ldufrechou
|
r1091 | |||
def evtStateExecuteDone(self,evt): | ||||
laurent.dufrechou@gmail.com
|
r1816 | if(self.text_ctrl.getCursorPos()!=0): | ||
self.text_ctrl.removeCurrentLine() | ||||
self.doc = self.IP.get_doc_text() | ||||
self.help = self.IP.get_help_text() | ||||
ldufrechou
|
r1091 | if self.doc: | ||
ldufrechou
|
r1099 | self.pager_lines = self.doc[7:].split('\n') | ||
ldufrechou
|
r1138 | self.pager_state = 'INIT' | ||
ldufrechou
|
r1099 | self.setCurrentState('SHOW_DOC') | ||
ville
|
r988 | self.pager(self.doc) | ||
ldufrechou
|
r1102 | elif self.help: | ||
ldufrechou
|
r1099 | self.pager_lines = self.help.split('\n') | ||
ldufrechou
|
r1138 | self.pager_state = 'INIT' | ||
ldufrechou
|
r1099 | self.setCurrentState('SHOW_DOC') | ||
ldufrechou
|
r1102 | self.pager(self.help) | ||
ldufrechou
|
r1091 | else: | ||
self.stateShowPrompt() | ||||
def stateShowPrompt(self): | ||||
ldufrechou
|
r1099 | self.setCurrentState('SHOW_PROMPT') | ||
laurent.dufrechou@gmail.com
|
r1816 | self.text_ctrl.setPrompt(self.IP.get_prompt()) | ||
self.text_ctrl.setIndentation(self.IP.get_indentation()) | ||||
self.text_ctrl.setPromptCount(self.IP.get_prompt_count()) | ||||
ldufrechou
|
r1133 | self.text_ctrl.showPrompt() | ||
laurent.dufrechou@gmail.com
|
r1816 | self.IP.init_history_index() | ||
ldufrechou
|
r1099 | self.setCurrentState('IDLE') | ||
Gael Varoquaux
|
r1106 | |||
ldufrechou
|
r1099 | def setCurrentState(self, state): | ||
self.cur_state = state | ||||
self.updateStatusTracker(self.cur_state) | ||||
ldufrechou
|
r1153 | |||
ldufrechou
|
r1134 | def pager(self,text): | ||
Gael Varoquaux
|
r1103 | |||
ldufrechou
|
r1104 | if self.pager_state == 'INIT': | ||
Laurent Dufréchou
|
r1221 | #print >>sys.__stdout__,"PAGER state:",self.pager_state | ||
self.pager_nb_lines = len(self.pager_lines) | ||||
self.pager_index = 0 | ||||
self.pager_do_remove = False | ||||
self.text_ctrl.write('\n') | ||||
self.pager_state = 'PROCESS_LINES' | ||||
ldufrechou
|
r1138 | |||
if self.pager_state == 'PROCESS_LINES': | ||||
Laurent Dufréchou
|
r1221 | #print >>sys.__stdout__,"PAGER state:",self.pager_state | ||
if self.pager_do_remove == True: | ||||
self.text_ctrl.removeCurrentLine() | ||||
self.pager_do_remove = False | ||||
if self.pager_nb_lines > 10: | ||||
#print >>sys.__stdout__,"PAGER processing 10 lines" | ||||
if self.pager_index > 0: | ||||
self.text_ctrl.write(">\x01\x1b[1;36m\x02"+self.pager_lines[self.pager_index]+'\n') | ||||
ldufrechou
|
r1138 | else: | ||
Laurent Dufréchou
|
r1221 | self.text_ctrl.write("\x01\x1b[1;36m\x02 "+self.pager_lines[self.pager_index]+'\n') | ||
for line in self.pager_lines[self.pager_index+1:self.pager_index+9]: | ||||
self.text_ctrl.write("\x01\x1b[1;36m\x02 "+line+'\n') | ||||
self.pager_index += 10 | ||||
self.pager_nb_lines -= 10 | ||||
self.text_ctrl.write("--- Push Enter to continue or 'Q' to quit---") | ||||
self.pager_do_remove = True | ||||
self.pager_state = 'WAITING' | ||||
return | ||||
else: | ||||
#print >>sys.__stdout__,"PAGER processing last lines" | ||||
if self.pager_nb_lines > 0: | ||||
if self.pager_index > 0: | ||||
self.text_ctrl.write(">\x01\x1b[1;36m\x02"+self.pager_lines[self.pager_index]+'\n') | ||||
else: | ||||
self.text_ctrl.write("\x01\x1b[1;36m\x02 "+self.pager_lines[self.pager_index]+'\n') | ||||
self.pager_index += 1 | ||||
self.pager_nb_lines -= 1 | ||||
if self.pager_nb_lines > 0: | ||||
for line in self.pager_lines[self.pager_index:]: | ||||
self.text_ctrl.write("\x01\x1b[1;36m\x02 "+line+'\n') | ||||
self.pager_nb_lines = 0 | ||||
self.pager_state = 'DONE' | ||||
self.stateShowPrompt() | ||||
Gael Varoquaux
|
r1106 | #------------------------ Key Handler ------------------------------------ | ||
ville
|
r988 | def keyPress(self, event): | ||
''' | ||||
Key press callback with plenty of shell goodness, like history, | ||||
autocompletions, etc. | ||||
''' | ||||
if event.GetKeyCode() == ord('C'): | ||||
ldufrechou
|
r1159 | if event.Modifiers == wx.MOD_CONTROL or event.Modifiers == wx.MOD_ALT: | ||
ville
|
r988 | if self.cur_state == 'WAIT_END_OF_EXECUTION': | ||
#we raise an exception inside the IPython thread container | ||||
ldufrechou
|
r1095 | self.IP.ce.raise_exc(KeyboardInterrupt) | ||
ville
|
r988 | return | ||
ldufrechou
|
r1159 | #let this before 'wx.WXK_RETURN' because we have to put 'IDLE' | ||
#mode if AutoComp has been set as inactive | ||||
if self.cur_state == 'COMPLETING': | ||||
if not self.text_ctrl.AutoCompActive(): | ||||
self.cur_state = 'IDLE' | ||||
else: | ||||
event.Skip() | ||||
ville
|
r988 | if event.KeyCode == wx.WXK_RETURN: | ||
if self.cur_state == 'IDLE': | ||||
#we change the state ot the state machine | ||||
ldufrechou
|
r1099 | self.setCurrentState('DO_EXECUTE_LINE') | ||
ldufrechou
|
r1091 | self.stateDoExecuteLine() | ||
ville
|
r988 | return | ||
ldufrechou
|
r1158 | |||
ville
|
r988 | if self.pager_state == 'WAITING': | ||
self.pager_state = 'PROCESS_LINES' | ||||
ldufrechou
|
r1091 | self.pager(self.doc) | ||
ville
|
r988 | return | ||
ldufrechou
|
r1134 | if self.cur_state == 'WAITING_USER_INPUT': | ||
line=self.text_ctrl.getCurrentLine() | ||||
self.text_ctrl.write('\n') | ||||
self.setCurrentState('WAIT_END_OF_EXECUTION') | ||||
return | ||||
ville
|
r988 | if event.GetKeyCode() in [ord('q'),ord('Q')]: | ||
if self.pager_state == 'WAITING': | ||||
self.pager_state = 'DONE' | ||||
ldufrechou
|
r1138 | self.text_ctrl.write('\n') | ||
ldufrechou
|
r1091 | self.stateShowPrompt() | ||
ville
|
r988 | return | ||
ldufrechou
|
r1133 | |||
ldufrechou
|
r1134 | if self.cur_state == 'WAITING_USER_INPUT': | ||
event.Skip() | ||||
ldufrechou
|
r1159 | |||
Gael Varoquaux
|
r1103 | if self.cur_state == 'IDLE': | ||
ville
|
r988 | if event.KeyCode == wx.WXK_UP: | ||
laurent.dufrechou@gmail.com
|
r1816 | history = self.IP.history_back() | ||
ville
|
r988 | self.text_ctrl.writeHistory(history) | ||
return | ||||
if event.KeyCode == wx.WXK_DOWN: | ||||
laurent.dufrechou@gmail.com
|
r1816 | history = self.IP.history_forward() | ||
ville
|
r988 | self.text_ctrl.writeHistory(history) | ||
return | ||||
if event.KeyCode == wx.WXK_TAB: | ||||
#if line empty we disable tab completion | ||||
if not self.text_ctrl.getCurrentLine().strip(): | ||||
self.text_ctrl.write('\t') | ||||
return | ||||
completed, possibilities = self.IP.complete(self.text_ctrl.getCurrentLine()) | ||||
if len(possibilities) > 1: | ||||
ldufrechou
|
r1159 | if self.text_ctrl.autocomplete_mode == 'IPYTHON': | ||
cur_slice = self.text_ctrl.getCurrentLine() | ||||
self.text_ctrl.write('\n') | ||||
self.text_ctrl.writeCompletion(possibilities) | ||||
self.text_ctrl.write('\n') | ||||
self.text_ctrl.showPrompt() | ||||
self.text_ctrl.write(cur_slice) | ||||
self.text_ctrl.changeLine(completed or cur_slice) | ||||
else: | ||||
self.cur_state = 'COMPLETING' | ||||
self.text_ctrl.writeCompletion(possibilities) | ||||
else: | ||||
self.text_ctrl.changeLine(completed or cur_slice) | ||||
ville
|
r988 | return | ||
event.Skip() | ||||
ldufrechou
|
r1159 | |||
#------------------------ Option Section --------------------------------- | ||||
def evtCheckOptionCompletion(self, event): | ||||
if event.IsChecked(): | ||||
ldufrechou
|
r1168 | self.options['completion']['value']='STC' | ||
ldufrechou
|
r1159 | else: | ||
ldufrechou
|
r1168 | self.options['completion']['value']='IPYTHON' | ||
self.text_ctrl.setCompletionMethod(self.options['completion']['value']) | ||||
self.updateOptionTracker('completion', | ||||
self.options['completion']['value']) | ||||
ldufrechou
|
r1159 | self.text_ctrl.SetFocus() | ||
ldufrechou
|
r1162 | |||
def evtCheckOptionBackgroundColor(self, event): | ||||
if event.IsChecked(): | ||||
ldufrechou
|
r1168 | self.options['background_color']['value']='WHITE' | ||
ldufrechou
|
r1162 | else: | ||
ldufrechou
|
r1168 | self.options['background_color']['value']='BLACK' | ||
self.text_ctrl.setBackgroundColor(self.options['background_color']['value']) | ||||
self.updateOptionTracker('background_color', | ||||
self.options['background_color']['value']) | ||||
ldufrechou
|
r1162 | self.text_ctrl.SetFocus() | ||
ldufrechou
|
r1621 | |||
def evtCheckOptionThreading(self, event): | ||||
if event.IsChecked(): | ||||
self.options['threading']['value']='True' | ||||
laurent.dufrechou@gmail.com
|
r1816 | self.IP.set_threading(True) | ||
ldufrechou
|
r1621 | self.cout.write = self.text_ctrl.asyncWrite | ||
else: | ||||
self.options['threading']['value']='False' | ||||
laurent.dufrechou@gmail.com
|
r1816 | self.IP.set_threading(False) | ||
ldufrechou
|
r1621 | self.cout.write = self.text_ctrl.write | ||
self.updateOptionTracker('threading', | ||||
self.options['threading']['value']) | ||||
self.text_ctrl.SetFocus() | ||||
ldufrechou
|
r1168 | def getOptions(self): | ||
return self.options | ||||
def reloadOptions(self,options): | ||||
self.options = options | ||||
for key in self.options.keys(): | ||||
value = self.options[key]['value'] | ||||
self.options[key]['checkbox'].SetValue(self.options[key][value]) | ||||
self.options[key]['setfunc'](value) | ||||
ldufrechou
|
r1621 | if self.options['threading']['value']=='True': | ||
laurent.dufrechou@gmail.com
|
r1816 | self.IP.set_threading(True) | ||
ldufrechou
|
r1621 | self.cout.write = self.text_ctrl.asyncWrite | ||
else: | ||||
laurent.dufrechou@gmail.com
|
r1816 | self.IP.set_threading(False) | ||
ldufrechou
|
r1621 | self.cout.write = self.text_ctrl.write | ||
Gael Varoquaux
|
r1106 | #------------------------ Hook Section ----------------------------------- | ||
ldufrechou
|
r1168 | def updateOptionTracker(self,name,value): | ||
''' | ||||
Default history tracker (does nothing) | ||||
''' | ||||
pass | ||||
def setOptionTrackerHook(self,func): | ||||
''' | ||||
Define a new history tracker | ||||
''' | ||||
self.updateOptionTracker = func | ||||
ville
|
r988 | def updateHistoryTracker(self,command_line): | ||
''' | ||||
Default history tracker (does nothing) | ||||
''' | ||||
pass | ||||
def setHistoryTrackerHook(self,func): | ||||
''' | ||||
Define a new history tracker | ||||
''' | ||||
self.updateHistoryTracker = func | ||||
Gael Varoquaux
|
r1106 | |||
ville
|
r988 | def updateStatusTracker(self,status): | ||
''' | ||||
Default status tracker (does nothing) | ||||
''' | ||||
pass | ||||
def setStatusTrackerHook(self,func): | ||||
''' | ||||
Define a new status tracker | ||||
''' | ||||
self.updateStatusTracker = func | ||||
Gael Varoquaux
|
r1107 | |||
ldufrechou
|
r1130 | def askExitHandler(self, event): | ||
''' | ||||
Default exit handler | ||||
''' | ||||
self.text_ctrl.write('\nExit callback has not been set.') | ||||
def setAskExitHandler(self, func): | ||||
''' | ||||
Define an exit handler | ||||
''' | ||||
self.askExitHandler = func | ||||
Gael Varoquaux
|
r1107 | |||
if __name__ == '__main__': | ||||
# Some simple code to test the shell widget. | ||||
class MainWindow(wx.Frame): | ||||
def __init__(self, parent, id, title): | ||||
wx.Frame.__init__(self, parent, id, title, size=(300,250)) | ||||
self._sizer = wx.BoxSizer(wx.VERTICAL) | ||||
self.shell = IPShellWidget(self) | ||||
self._sizer.Add(self.shell, 1, wx.EXPAND) | ||||
self.SetSizer(self._sizer) | ||||
self.SetAutoLayout(1) | ||||
self.Show(True) | ||||
app = wx.PySimpleApp() | ||||
frame = MainWindow(None, wx.ID_ANY, 'Ipython') | ||||
frame.SetSize((780, 460)) | ||||
shell = frame.shell | ||||
app.MainLoop() | ||||