console_widget.py
370 lines
| 13.5 KiB
| text/x-python
|
PythonLexer
gvaroquaux
|
r1295 | # encoding: utf-8 | ||
""" | ||||
gvaroquaux
|
r1296 | A Wx widget to act as a console and input commands. | ||
This widget deals with prompts and provides an edit buffer | ||||
gvaroquaux
|
r1295 | restricted to after the last prompt. | ||
""" | ||||
__docformat__ = "restructuredtext en" | ||||
#------------------------------------------------------------------------------- | ||||
# Copyright (C) 2008 The IPython Development Team | ||||
# | ||||
# Distributed under the terms of the BSD License. The full license is | ||||
# in the file COPYING, distributed as part of this software. | ||||
#------------------------------------------------------------------------------- | ||||
#------------------------------------------------------------------------------- | ||||
# Imports | ||||
#------------------------------------------------------------------------------- | ||||
import wx | ||||
import wx.stc as stc | ||||
import re | ||||
# FIXME: Need to provide an API for non user-generated display on the | ||||
# screen: this should not be editable by the user. | ||||
#------------------------------------------------------------------------------- | ||||
# The console widget class | ||||
#------------------------------------------------------------------------------- | ||||
class ConsoleWidget(stc.StyledTextCtrl): | ||||
""" Specialized styled text control view for console-like workflow. | ||||
gvaroquaux
|
r1296 | |||
gvaroquaux
|
r1295 | This widget is mainly interested in dealing with the prompt and | ||
keeping the cursor inside the editing line. | ||||
""" | ||||
# Translation table from ANSI escape sequences to color. Override | ||||
# this to specify your colors. | ||||
ANSI_STYLES = {'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']} | ||||
# The color of the carret (call _apply_style() after setting) | ||||
carret_color = 'BLACK' | ||||
#-------------------------------------------------------------------------- | ||||
# Public API | ||||
#-------------------------------------------------------------------------- | ||||
def write(self, text): | ||||
""" Write given text to buffer, while translating the ansi escape | ||||
sequences. | ||||
""" | ||||
segments = self.color_pat.split(text) | ||||
segment = segments.pop(0) | ||||
self.StartStyling(self.GetLength(), 0xFF) | ||||
self.AppendText(segment) | ||||
if segments: | ||||
ansi_tags = self.color_pat.findall(text) | ||||
for tag in ansi_tags: | ||||
i = segments.index(tag) | ||||
self.StartStyling(self.GetLength(), 0xFF) | ||||
self.AppendText(segments[i+1]) | ||||
if tag != '0': | ||||
self.SetStyling(len(segments[i+1]), | ||||
self.ANSI_STYLES[tag][0]) | ||||
segments.pop(i) | ||||
self.GotoPos(self.GetLength()) | ||||
def new_prompt(self, prompt): | ||||
""" Prints a prompt at start of line, and move the start of the | ||||
current block there. | ||||
The prompt can be give with ascii escape sequences. | ||||
""" | ||||
self.write(prompt) | ||||
# now we update our cursor giving end of prompt | ||||
self.current_prompt_pos = self.GetLength() | ||||
self.current_prompt_line = self.GetCurrentLine() | ||||
autoindent = self.indent * ' ' | ||||
autoindent = autoindent.replace(' ','\t') | ||||
self.write(autoindent) | ||||
def replace_current_edit_buffer(self, text): | ||||
""" Replace currently entered command line with given text. | ||||
""" | ||||
self.SetSelection(self.current_prompt_pos, self.GetLength()) | ||||
self.ReplaceSelection(text) | ||||
self.GotoPos(self.GetLength()) | ||||
def get_current_edit_buffer(self): | ||||
""" Returns the text in current edit buffer. | ||||
""" | ||||
return self.GetTextRange(self.current_prompt_pos, | ||||
self.GetLength()) | ||||
#-------------------------------------------------------------------------- | ||||
# Private API | ||||
#-------------------------------------------------------------------------- | ||||
def __init__(self, parent, pos=wx.DefaultPosition, ID=-1, | ||||
size=wx.DefaultSize, style=0, | ||||
autocomplete_mode='IPYTHON'): | ||||
""" Autocomplete_mode: Can be 'IPYTHON' or 'STC' | ||||
'IPYTHON' show autocompletion the ipython way | ||||
'STC" show it scintilla text control way | ||||
""" | ||||
stc.StyledTextCtrl.__init__(self, parent, ID, pos, size, style) | ||||
#------ Scintilla configuration ----------------------------------- | ||||
# Ctrl"+" or Ctrl "-" can be used to zoomin/zoomout the text inside | ||||
# the widget | ||||
self.CmdKeyAssign(ord('+'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMIN) | ||||
self.CmdKeyAssign(ord('-'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMOUT) | ||||
# Also allow Ctrl Shift "=" for poor non US keyboard users. | ||||
self.CmdKeyAssign(ord('='), stc.STC_SCMOD_CTRL|stc.STC_SCMOD_SHIFT, | ||||
stc.STC_CMD_ZOOMIN) | ||||
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) | ||||
self.SetUndoCollection(False) | ||||
self.SetUseTabs(True) | ||||
self.SetIndent(4) | ||||
self.SetTabWidth(4) | ||||
self.EnsureCaretVisible() | ||||
self.SetMargins(3, 3) #text is moved away from border with 3px | ||||
# Suppressing Scintilla margins | ||||
self.SetMarginWidth(0, 0) | ||||
self.SetMarginWidth(1, 0) | ||||
self.SetMarginWidth(2, 0) | ||||
self._apply_style() | ||||
self.indent = 0 | ||||
self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?') | ||||
# FIXME: we need to retrieve this from the interpreter. | ||||
self.prompt = \ | ||||
'\n\x01\x1b[0;34m\x02In [\x01\x1b[1;34m\x026\x01\x1b[0;34m\x02]: \x01\x1b[0m\x02' | ||||
self.new_prompt(self.prompt) | ||||
self.autocomplete_mode = autocomplete_mode | ||||
self.Bind(wx.EVT_KEY_DOWN, self._onKeypress) | ||||
def _apply_style(self): | ||||
""" Applies the colors for the different text elements and the | ||||
carret. | ||||
""" | ||||
# FIXME: We need to do something for the fonts, but this is | ||||
# clearly not the right option. | ||||
#we define platform specific fonts | ||||
# if wx.Platform == '__WXMSW__': | ||||
# faces = { 'times': 'Times New Roman', | ||||
# 'mono' : 'Courier New', | ||||
# 'helv' : 'Arial', | ||||
# 'other': 'Comic Sans MS', | ||||
# 'size' : 10, | ||||
# 'size2': 8, | ||||
# } | ||||
# 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, | ||||
# } | ||||
# 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'])) | ||||
self.SetCaretForeground(self.carret_color) | ||||
self.StyleClearAll() | ||||
self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT, | ||||
"fore:#FF0000,back:#0000FF,bold") | ||||
self.StyleSetSpec(stc.STC_STYLE_BRACEBAD, | ||||
"fore:#000000,back:#FF0000,bold") | ||||
for style in self.ANSI_STYLES.values(): | ||||
self.StyleSetSpec(style[0], "bold,fore:%s" % style[1]) | ||||
def removeFromTo(self, from_pos, to_pos): | ||||
if from_pos < to_pos: | ||||
self.SetSelection(from_pos, to_pos) | ||||
self.DeleteBack() | ||||
def selectFromTo(self, from_pos, to_pos): | ||||
self.SetSelectionStart(from_pos) | ||||
self.SetSelectionEnd(to_pos) | ||||
def writeCompletion(self, possibilities): | ||||
if self.autocomplete_mode == 'IPYTHON': | ||||
max_len = len(max(possibilities, key=len)) | ||||
max_symbol = ' '*max_len | ||||
#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: | ||||
allowed_symbols = 1 | ||||
pos = 1 | ||||
buf = '' | ||||
for symbol in possibilities: | ||||
#buf += symbol+'\n'#*spaces) | ||||
if pos < allowed_symbols: | ||||
spaces = max_len - len(symbol) + 4 | ||||
buf += symbol+' '*spaces | ||||
pos += 1 | ||||
else: | ||||
buf += symbol+'\n' | ||||
pos = 1 | ||||
self.write(buf) | ||||
else: | ||||
possibilities.sort() # Python sorts are case sensitive | ||||
self.AutoCompSetIgnoreCase(False) | ||||
self.AutoCompSetAutoHide(False) | ||||
#let compute the length ot last word | ||||
splitter = [' ', '(', '[', '{'] | ||||
last_word = self.get_current_edit_buffer() | ||||
for breaker in splitter: | ||||
last_word = last_word.split(breaker)[-1] | ||||
self.AutoCompShow(len(last_word), " ".join(possibilities)) | ||||
def _onKeypress(self, event, skip=True): | ||||
""" Key press callback used for correcting behavior for | ||||
console-like interfaces: the cursor is constraint to be after | ||||
the last prompt. | ||||
Return True if event as been catched. | ||||
""" | ||||
catched = False | ||||
if self.AutoCompActive(): | ||||
event.Skip() | ||||
else: | ||||
if event.GetKeyCode() == wx.WXK_HOME: | ||||
if event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN): | ||||
self.GotoPos(self.current_prompt_pos) | ||||
catched = True | ||||
elif event.Modifiers == wx.MOD_SHIFT: | ||||
self.selectFromTo(self.current_prompt_pos, | ||||
self.GetCurrentPos()) | ||||
catched = True | ||||
elif event.GetKeyCode() == wx.WXK_UP: | ||||
if self.GetCurrentLine() > self.current_prompt_line: | ||||
if self.GetCurrentLine() == self.current_prompt_line + 1 \ | ||||
and self.GetColumn(self.GetCurrentPos()) < \ | ||||
self.GetColumn(self.current_prompt_pos): | ||||
self.GotoPos(self.current_prompt_pos) | ||||
else: | ||||
event.Skip() | ||||
catched = True | ||||
elif event.GetKeyCode() in (wx.WXK_LEFT, wx.WXK_BACK): | ||||
if self.GetCurrentPos() > self.current_prompt_pos: | ||||
event.Skip() | ||||
catched = True | ||||
if skip and not catched: | ||||
event.Skip() | ||||
if event.GetKeyCode() not in (wx.WXK_PAGEUP, wx.WXK_PAGEDOWN)\ | ||||
and event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN, | ||||
wx.MOD_SHIFT): | ||||
# If cursor is outside the editing region, put it back. | ||||
if self.GetCurrentPos() < self.current_prompt_pos: | ||||
self.GotoPos(self.current_prompt_pos) | ||||
return catched | ||||
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) | ||||
if __name__ == '__main__': | ||||
# Some simple code to test the console 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.console_widget = ConsoleWidget(self) | ||||
self._sizer.Add(self.console_widget, 1, wx.EXPAND) | ||||
self.SetSizer(self._sizer) | ||||
self.SetAutoLayout(1) | ||||
self.Show(True) | ||||
app = wx.PySimpleApp() | ||||
w = MainWindow(None, wx.ID_ANY, 'ConsoleWidget') | ||||
w.SetSize((780, 460)) | ||||
w.Show() | ||||
app.MainLoop() | ||||