console_widget.py
437 lines
| 16.1 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 | ||||
Gael Varoquaux
|
r1349 | from wx.py import editwindow | ||
Gael Varoquaux
|
r1618 | import time | ||
gvaroquaux
|
r1436 | import sys | ||
LINESEP = '\n' | ||||
if sys.platform == 'win32': | ||||
LINESEP = '\n\r' | ||||
Gael Varoquaux
|
r1349 | |||
gvaroquaux
|
r1295 | import re | ||
# FIXME: Need to provide an API for non user-generated display on the | ||||
# screen: this should not be editable by the user. | ||||
gvaroquaux
|
r1436 | _DEFAULT_SIZE = 10 | ||
gvaroquaux
|
r1502 | if sys.platform == 'darwin': | ||
_DEFAULT_STYLE = 12 | ||||
Gael Varoquaux
|
r1349 | |||
_DEFAULT_STYLE = { | ||||
'stdout' : 'fore:#0000FF', | ||||
'stderr' : 'fore:#007f00', | ||||
'trace' : 'fore:#FF0000', | ||||
'default' : 'size:%d' % _DEFAULT_SIZE, | ||||
Gael Varoquaux
|
r1491 | 'bracegood' : 'fore:#00AA00,back:#000000,bold', | ||
'bracebad' : 'fore:#FF0000,back:#000000,bold', | ||||
Gael Varoquaux
|
r1349 | |||
# properties for the various Python lexer styles | ||||
Gael Varoquaux
|
r1374 | 'comment' : 'fore:#007F00', | ||
'number' : 'fore:#007F7F', | ||||
'string' : 'fore:#7F007F,italic', | ||||
'char' : 'fore:#7F007F,italic', | ||||
'keyword' : 'fore:#00007F,bold', | ||||
'triple' : 'fore:#7F0000', | ||||
'tripledouble' : 'fore:#7F0000', | ||||
'class' : 'fore:#0000FF,bold,underline', | ||||
'def' : 'fore:#007F7F,bold', | ||||
'operator' : 'bold' | ||||
Gael Varoquaux
|
r1349 | } | ||
# new style numbers | ||||
_STDOUT_STYLE = 15 | ||||
_STDERR_STYLE = 16 | ||||
_TRACE_STYLE = 17 | ||||
Gael Varoquaux
|
r1373 | # system colors | ||
Gael Varoquaux
|
r1380 | #SYS_COLOUR_BACKGROUND = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BACKGROUND) | ||
Gael Varoquaux
|
r1373 | |||
gvaroquaux
|
r1295 | #------------------------------------------------------------------------------- | ||
# The console widget class | ||||
#------------------------------------------------------------------------------- | ||||
Gael Varoquaux
|
r1349 | class ConsoleWidget(editwindow.EditWindow): | ||
gvaroquaux
|
r1295 | """ 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. | ||||
""" | ||||
gvaroquaux
|
r1455 | # This is where the title captured from the ANSI escape sequences are | ||
# stored. | ||||
Gael Varoquaux
|
r1359 | title = 'Console' | ||
gvaroquaux
|
r1462 | # The buffer being edited. | ||
def _set_input_buffer(self, string): | ||||
self.SetSelection(self.current_prompt_pos, self.GetLength()) | ||||
self.ReplaceSelection(string) | ||||
self.GotoPos(self.GetLength()) | ||||
def _get_input_buffer(self): | ||||
""" Returns the text in current edit buffer. | ||||
""" | ||||
input_buffer = self.GetTextRange(self.current_prompt_pos, | ||||
self.GetLength()) | ||||
input_buffer = input_buffer.replace(LINESEP, '\n') | ||||
return input_buffer | ||||
input_buffer = property(_get_input_buffer, _set_input_buffer) | ||||
Gael Varoquaux
|
r1349 | style = _DEFAULT_STYLE.copy() | ||
gvaroquaux
|
r1295 | # 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' | ||||
Gael Varoquaux
|
r1618 | # Store the last time a refresh was done | ||
_last_refresh_time = 0 | ||||
gvaroquaux
|
r1295 | #-------------------------------------------------------------------------- | ||
# Public API | ||||
#-------------------------------------------------------------------------- | ||||
Gael Varoquaux
|
r1349 | def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, | ||
gvaroquaux
|
r1630 | size=wx.DefaultSize, style=wx.WANTS_CHARS, ): | ||
Gael Varoquaux
|
r1349 | editwindow.EditWindow.__init__(self, parent, id, pos, size, style) | ||
gvaroquaux
|
r1455 | self._configure_scintilla() | ||
Gael Varoquaux
|
r1349 | |||
self.Bind(wx.EVT_KEY_DOWN, self._on_key_down) | ||||
Gael Varoquaux
|
r1358 | self.Bind(wx.EVT_KEY_UP, self._on_key_up) | ||
Gael Varoquaux
|
r1349 | |||
gvaroquaux
|
r1449 | def write(self, text, refresh=True): | ||
gvaroquaux
|
r1295 | """ Write given text to buffer, while translating the ansi escape | ||
sequences. | ||||
""" | ||||
Gael Varoquaux
|
r1437 | # XXX: do not put print statements to sys.stdout/sys.stderr in | ||
# this method, the print statements will call this method, as | ||||
# you will end up with an infinit loop | ||||
Gael Varoquaux
|
r1359 | title = self.title_pat.split(text) | ||
Gael Varoquaux
|
r1393 | if len(title)>1: | ||
self.title = title[-2] | ||||
Gael Varoquaux
|
r1359 | |||
text = self.title_pat.sub('', text) | ||||
gvaroquaux
|
r1295 | segments = self.color_pat.split(text) | ||
segment = segments.pop(0) | ||||
gvaroquaux
|
r1436 | self.GotoPos(self.GetLength()) | ||
gvaroquaux
|
r1295 | self.StartStyling(self.GetLength(), 0xFF) | ||
Gael Varoquaux
|
r1451 | try: | ||
self.AppendText(segment) | ||||
except UnicodeDecodeError: | ||||
# XXX: Do I really want to skip the exception? | ||||
pass | ||||
Gael Varoquaux
|
r1359 | |||
gvaroquaux
|
r1295 | if segments: | ||
Gael Varoquaux
|
r1386 | for ansi_tag, text in zip(segments[::2], segments[1::2]): | ||
gvaroquaux
|
r1295 | self.StartStyling(self.GetLength(), 0xFF) | ||
Gael Varoquaux
|
r1451 | try: | ||
self.AppendText(text) | ||||
except UnicodeDecodeError: | ||||
# XXX: Do I really want to skip the exception? | ||||
pass | ||||
gvaroquaux
|
r1295 | |||
Gael Varoquaux
|
r1424 | if ansi_tag not in self.ANSI_STYLES: | ||
Gael Varoquaux
|
r1386 | style = 0 | ||
else: | ||||
style = self.ANSI_STYLES[ansi_tag][0] | ||||
gvaroquaux
|
r1295 | |||
Gael Varoquaux
|
r1386 | self.SetStyling(len(text), style) | ||
gvaroquaux
|
r1295 | |||
self.GotoPos(self.GetLength()) | ||||
gvaroquaux
|
r1449 | if refresh: | ||
Gael Varoquaux
|
r1618 | current_time = time.time() | ||
if current_time - self._last_refresh_time > 0.03: | ||||
gvaroquaux
|
r1653 | if sys.platform == 'win32': | ||
wx.SafeYield() | ||||
else: | ||||
wx.Yield() | ||||
gvaroquaux
|
r1651 | # self.ProcessEvent(wx.PaintEvent()) | ||
Gael Varoquaux
|
r1618 | self._last_refresh_time = current_time | ||
gvaroquaux
|
r1436 | |||
Gael Varoquaux
|
r1388 | |||
gvaroquaux
|
r1295 | def new_prompt(self, prompt): | ||
""" Prints a prompt at start of line, and move the start of the | ||||
current block there. | ||||
gvaroquaux
|
r1465 | The prompt can be given with ascii escape sequences. | ||
gvaroquaux
|
r1295 | """ | ||
Gael Varoquaux
|
r1495 | self.write(prompt, refresh=False) | ||
gvaroquaux
|
r1295 | # now we update our cursor giving end of prompt | ||
self.current_prompt_pos = self.GetLength() | ||||
self.current_prompt_line = self.GetCurrentLine() | ||||
Gael Varoquaux
|
r1361 | wx.Yield() | ||
self.EnsureCaretVisible() | ||||
gvaroquaux
|
r1295 | |||
gvaroquaux
|
r1455 | def scroll_to_bottom(self): | ||
maxrange = self.GetScrollRange(wx.VERTICAL) | ||||
self.ScrollLines(maxrange) | ||||
gvaroquaux
|
r1295 | |||
gvaroquaux
|
r1455 | def pop_completion(self, possibilities, offset=0): | ||
""" Pops up an autocompletion menu. Offset is the offset | ||||
in characters of the position at which the menu should | ||||
appear, relativ to the cursor. | ||||
""" | ||||
self.AutoCompSetIgnoreCase(False) | ||||
self.AutoCompSetAutoHide(False) | ||||
self.AutoCompSetMaxHeight(len(possibilities)) | ||||
self.AutoCompShow(offset, " ".join(possibilities)) | ||||
gvaroquaux
|
r1295 | |||
gvaroquaux
|
r1463 | def get_line_width(self): | ||
""" Return the width of the line in characters. | ||||
""" | ||||
return self.GetSize()[0]/self.GetCharWidth() | ||||
gvaroquaux
|
r1503 | #-------------------------------------------------------------------------- | ||
# EditWindow API | ||||
#-------------------------------------------------------------------------- | ||||
def OnUpdateUI(self, event): | ||||
""" Override the OnUpdateUI of the EditWindow class, to prevent | ||||
syntax highlighting both for faster redraw, and for more | ||||
consistent look and feel. | ||||
""" | ||||
Gael Varoquaux
|
r1380 | |||
gvaroquaux
|
r1455 | #-------------------------------------------------------------------------- | ||
# Private API | ||||
#-------------------------------------------------------------------------- | ||||
def _apply_style(self): | ||||
""" Applies the colors for the different text elements and the | ||||
carret. | ||||
Gael Varoquaux
|
r1380 | """ | ||
gvaroquaux
|
r1455 | self.SetCaretForeground(self.carret_color) | ||
gvaroquaux
|
r1295 | |||
gvaroquaux
|
r1455 | #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 _configure_scintilla(self): | ||||
self.SetEOLMode(stc.STC_EOL_LF) | ||||
# 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) | ||||
# Keys: we need to clear some of the keys the that don't play | ||||
# well with a console. | ||||
self.CmdKeyClear(ord('D'), stc.STC_SCMOD_CTRL) | ||||
self.CmdKeyClear(ord('L'), stc.STC_SCMOD_CTRL) | ||||
self.CmdKeyClear(ord('T'), stc.STC_SCMOD_CTRL) | ||||
Gael Varoquaux
|
r1498 | self.CmdKeyClear(ord('A'), stc.STC_SCMOD_CTRL) | ||
gvaroquaux
|
r1455 | |||
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) | ||||
# we don't want scintilla's autocompletion to choose | ||||
# automaticaly out of a single choice list, as we pop it up | ||||
# automaticaly | ||||
self.AutoCompSetChooseSingle(False) | ||||
self.AutoCompSetMaxHeight(10) | ||||
Gael Varoquaux
|
r1493 | # XXX: this doesn't seem to have an effect. | ||
self.AutoCompSetFillUps('\n') | ||||
gvaroquaux
|
r1455 | |||
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() | ||||
# Xterm escape sequences | ||||
self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?') | ||||
self.title_pat = re.compile('\x1b]0;(.*?)\x07') | ||||
#self.SetEdgeMode(stc.STC_EDGE_LINE) | ||||
#self.SetEdgeColumn(80) | ||||
# styles | ||||
p = self.style | ||||
self.StyleSetSpec(stc.STC_STYLE_DEFAULT, p['default']) | ||||
self.StyleClearAll() | ||||
self.StyleSetSpec(_STDOUT_STYLE, p['stdout']) | ||||
self.StyleSetSpec(_STDERR_STYLE, p['stderr']) | ||||
self.StyleSetSpec(_TRACE_STYLE, p['trace']) | ||||
self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT, p['bracegood']) | ||||
self.StyleSetSpec(stc.STC_STYLE_BRACEBAD, p['bracebad']) | ||||
self.StyleSetSpec(stc.STC_P_COMMENTLINE, p['comment']) | ||||
self.StyleSetSpec(stc.STC_P_NUMBER, p['number']) | ||||
self.StyleSetSpec(stc.STC_P_STRING, p['string']) | ||||
self.StyleSetSpec(stc.STC_P_CHARACTER, p['char']) | ||||
self.StyleSetSpec(stc.STC_P_WORD, p['keyword']) | ||||
self.StyleSetSpec(stc.STC_P_WORD2, p['keyword']) | ||||
self.StyleSetSpec(stc.STC_P_TRIPLE, p['triple']) | ||||
self.StyleSetSpec(stc.STC_P_TRIPLEDOUBLE, p['tripledouble']) | ||||
self.StyleSetSpec(stc.STC_P_CLASSNAME, p['class']) | ||||
self.StyleSetSpec(stc.STC_P_DEFNAME, p['def']) | ||||
self.StyleSetSpec(stc.STC_P_OPERATOR, p['operator']) | ||||
self.StyleSetSpec(stc.STC_P_COMMENTBLOCK, p['comment']) | ||||
Gael Varoquaux
|
r1349 | |||
def _on_key_down(self, event, skip=True): | ||||
gvaroquaux
|
r1295 | """ 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. | ||||
""" | ||||
Gael Varoquaux
|
r1363 | catched = True | ||
Gael Varoquaux
|
r1358 | # Intercept some specific keys. | ||
Gael Varoquaux
|
r1349 | if event.KeyCode == ord('L') and event.ControlDown() : | ||
self.scroll_to_bottom() | ||||
Gael Varoquaux
|
r1371 | elif event.KeyCode == ord('K') and event.ControlDown() : | ||
gvaroquaux
|
r1462 | self.input_buffer = '' | ||
Gael Varoquaux
|
r1498 | elif event.KeyCode == ord('A') and event.ControlDown() : | ||
self.GotoPos(self.GetLength()) | ||||
self.SetSelectionStart(self.current_prompt_pos) | ||||
self.SetSelectionEnd(self.GetCurrentPos()) | ||||
catched = True | ||||
elif event.KeyCode == ord('E') and event.ControlDown() : | ||||
self.GotoPos(self.GetLength()) | ||||
catched = True | ||||
Gael Varoquaux
|
r1490 | elif event.KeyCode == wx.WXK_PAGEUP: | ||
Gael Varoquaux
|
r1358 | self.ScrollPages(-1) | ||
Gael Varoquaux
|
r1490 | elif event.KeyCode == wx.WXK_PAGEDOWN: | ||
Gael Varoquaux
|
r1358 | self.ScrollPages(1) | ||
Gael Varoquaux
|
r1380 | elif event.KeyCode == wx.WXK_UP and event.ShiftDown(): | ||
self.ScrollLines(-1) | ||||
elif event.KeyCode == wx.WXK_DOWN and event.ShiftDown(): | ||||
Gael Varoquaux
|
r1389 | self.ScrollLines(1) | ||
Gael Varoquaux
|
r1363 | else: | ||
catched = False | ||||
Gael Varoquaux
|
r1349 | |||
gvaroquaux
|
r1295 | if self.AutoCompActive(): | ||
event.Skip() | ||||
else: | ||||
Gael Varoquaux
|
r1358 | if event.KeyCode in (13, wx.WXK_NUMPAD_ENTER) and \ | ||
event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN): | ||||
catched = True | ||||
Gael Varoquaux
|
r1383 | self.CallTipCancel() | ||
gvaroquaux
|
r1479 | self.write('\n', refresh=False) | ||
gvaroquaux
|
r1436 | # Under windows scintilla seems to be doing funny stuff to the | ||
gvaroquaux
|
r1462 | # line returns here, but the getter for input_buffer filters | ||
# this out. | ||||
gvaroquaux
|
r1436 | if sys.platform == 'win32': | ||
gvaroquaux
|
r1462 | self.input_buffer = self.input_buffer | ||
Gael Varoquaux
|
r1358 | self._on_enter() | ||
elif event.KeyCode == wx.WXK_HOME: | ||||
gvaroquaux
|
r1295 | if event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN): | ||
self.GotoPos(self.current_prompt_pos) | ||||
catched = True | ||||
Gael Varoquaux
|
r1498 | elif event.Modifiers == wx.MOD_SHIFT: | ||
Gael Varoquaux
|
r1358 | # FIXME: This behavior is not ideal: if the selection | ||
# is already started, it will jump. | ||||
self.SetSelectionStart(self.current_prompt_pos) | ||||
self.SetSelectionEnd(self.GetCurrentPos()) | ||||
gvaroquaux
|
r1295 | catched = True | ||
Gael Varoquaux
|
r1349 | elif event.KeyCode == wx.WXK_UP: | ||
gvaroquaux
|
r1295 | 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 | ||||
Gael Varoquaux
|
r1349 | elif event.KeyCode in (wx.WXK_LEFT, wx.WXK_BACK): | ||
gvaroquaux
|
r1295 | if self.GetCurrentPos() > self.current_prompt_pos: | ||
event.Skip() | ||||
catched = True | ||||
if skip and not catched: | ||||
gvaroquaux
|
r1436 | # Put the cursor back in the edit region | ||
if self.GetCurrentPos() < self.current_prompt_pos: | ||||
self.GotoPos(self.current_prompt_pos) | ||||
else: | ||||
event.Skip() | ||||
gvaroquaux
|
r1295 | |||
return catched | ||||
Gael Varoquaux
|
r1358 | def _on_key_up(self, event, skip=True): | ||
""" If cursor is outside the editing region, put it back. | ||||
""" | ||||
event.Skip() | ||||
if self.GetCurrentPos() < self.current_prompt_pos: | ||||
self.GotoPos(self.current_prompt_pos) | ||||
gvaroquaux
|
r1295 | 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() | ||||