console_widget.py
625 lines
| 22.8 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 | ||
Laurent Dufrechou
|
r1835 | import string | ||
gvaroquaux
|
r1436 | 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. | ||||
Laurent Dufrechou
|
r1834 | #------------------------------------------------------------------------------- | ||
# Constants | ||||
#------------------------------------------------------------------------------- | ||||
_COMPLETE_BUFFER_MARKER = 31 | ||||
_ERROR_MARKER = 30 | ||||
_INPUT_MARKER = 29 | ||||
gvaroquaux
|
r1295 | |||
gvaroquaux
|
r1436 | _DEFAULT_SIZE = 10 | ||
gvaroquaux
|
r1502 | if sys.platform == 'darwin': | ||
Gael Varoquaux
|
r1667 | _DEFAULT_SIZE = 12 | ||
Gael Varoquaux
|
r1349 | |||
_DEFAULT_STYLE = { | ||||
Laurent Dufrechou
|
r1835 | #background definition | ||
Gael Varoquaux
|
r1349 | 'default' : 'size:%d' % _DEFAULT_SIZE, | ||
Gael Varoquaux
|
r1491 | 'bracegood' : 'fore:#00AA00,back:#000000,bold', | ||
'bracebad' : 'fore:#FF0000,back:#000000,bold', | ||||
Gael Varoquaux
|
r1349 | |||
Gael Varoquaux
|
r1893 | # Edge column: a number of None | ||
'edge_column' : -1, | ||||
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', | ||||
Gael Varoquaux
|
r1947 | 'operator' : 'bold', | ||
# Default colors | ||||
'trace' : '#FAFAF1', # Nice green | ||||
'stdout' : '#FDFFD3', # Nice yellow | ||||
'stderr' : '#FFF1F1', # Nice red | ||||
# Default scintilla settings | ||||
'antialiasing' : True, | ||||
'carret_color' : 'BLACK', | ||||
'background_color' :'WHITE', | ||||
#prompt definition | ||||
'prompt_in1' : \ | ||||
'\n\x01\x1b[0;34m\x02In [\x01\x1b[1;34m\x02$number\x01\x1b[0;34m\x02]: \x01\x1b[0m\x02', | ||||
'prompt_out': \ | ||||
'\x01\x1b[0;31m\x02Out[\x01\x1b[1;31m\x02$number\x01\x1b[0;31m\x02]: \x01\x1b[0m\x02', | ||||
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 | |||
Gael Varoquaux
|
r1893 | # Translation table from ANSI escape sequences to color. | ||
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']} | ||||
Gael Varoquaux
|
r1947 | # XXX: Maybe one day we should factor this code with ColorANSI. Right now | ||
# ColorANSI is hard to reuse and makes our code more complex. | ||||
Gael Varoquaux
|
r1893 | #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, | ||||
} | ||||
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' | ||
Gael Varoquaux
|
r1893 | # Last prompt printed | ||
last_prompt = '' | ||||
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. | ||||
Gael Varoquaux
|
r1893 | ANSI_STYLES = ANSI_STYLES.copy() | ||
gvaroquaux
|
r1295 | |||
Gael Varoquaux
|
r1893 | # Font faces | ||
faces = FACES.copy() | ||||
Laurent Dufrechou
|
r1834 | |||
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) | ||
Laurent Dufrechou
|
r1835 | self.configure_scintilla() | ||
Gael Varoquaux
|
r1893 | # Track if 'enter' key as ever been processed | ||
# This variable will only be reallowed until key goes up | ||||
self.enter_catched = False | ||||
Laurent Dufrechou
|
r1834 | self.current_prompt_pos = 0 | ||
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 | self.EnsureCaretVisible() | ||
Gael Varoquaux
|
r1893 | self.last_prompt = prompt | ||
Gael Varoquaux
|
r1361 | |||
Gael Varoquaux
|
r1893 | def continuation_prompt(self): | ||
Gael Varoquaux
|
r1894 | """ Returns the current continuation prompt. | ||
We need to implement this method here to deal with the | ||||
ascii escape sequences cleaning up. | ||||
Gael Varoquaux
|
r1893 | """ | ||
# ASCII-less prompt | ||||
ascii_less = ''.join(self.color_pat.split(self.last_prompt)[2::2]) | ||||
return "."*(len(ascii_less)-2) + ': ' | ||||
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() | ||||
Gael Varoquaux
|
r1884 | |||
Laurent Dufrechou
|
r1835 | def configure_scintilla(self): | ||
Gael Varoquaux
|
r1947 | """ Set up all the styling option of the embedded scintilla | ||
widget. | ||||
""" | ||||
p = self.style.copy() | ||||
Laurent Dufrechou
|
r1837 | |||
Laurent Dufrechou
|
r1834 | # Marker for complete buffer. | ||
self.MarkerDefine(_COMPLETE_BUFFER_MARKER, stc.STC_MARK_BACKGROUND, | ||||
Gael Varoquaux
|
r1947 | background=p['trace']) | ||
Laurent Dufrechou
|
r1835 | |||
Laurent Dufrechou
|
r1834 | # Marker for current input buffer. | ||
self.MarkerDefine(_INPUT_MARKER, stc.STC_MARK_BACKGROUND, | ||||
Gael Varoquaux
|
r1947 | background=p['stdout']) | ||
Laurent Dufrechou
|
r1834 | # Marker for tracebacks. | ||
self.MarkerDefine(_ERROR_MARKER, stc.STC_MARK_BACKGROUND, | ||||
Gael Varoquaux
|
r1947 | background=p['stderr']) | ||
gvaroquaux
|
r1455 | |||
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) | ||||
Laurent Dufrechou
|
r1836 | |||
Gael Varoquaux
|
r1947 | self.SetUseAntiAliasing(p['antialiasing']) | ||
Laurent Dufrechou
|
r1836 | |||
gvaroquaux
|
r1455 | 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) | ||||
# Xterm escape sequences | ||||
self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?') | ||||
self.title_pat = re.compile('\x1b]0;(.*?)\x07') | ||||
# styles | ||||
Laurent Dufrechou
|
r1834 | |||
Gael Varoquaux
|
r1947 | self.SetCaretForeground(p['carret_color']) | ||
Laurent Dufrechou
|
r1835 | |||
Gael Varoquaux
|
r1947 | background_color = p['background_color'] | ||
Laurent Dufrechou
|
r1835 | |||
Laurent Dufrechou
|
r1834 | if 'default' in p: | ||
Laurent Dufrechou
|
r1835 | if 'back' not in p['default']: | ||
Gael Varoquaux
|
r1893 | p['default'] += ',back:%s' % background_color | ||
Laurent Dufrechou
|
r1835 | if 'size' not in p['default']: | ||
Gael Varoquaux
|
r1893 | p['default'] += ',size:%s' % self.faces['size'] | ||
Laurent Dufrechou
|
r1835 | if 'face' not in p['default']: | ||
Gael Varoquaux
|
r1893 | p['default'] += ',face:%s' % self.faces['mono'] | ||
Laurent Dufrechou
|
r1835 | |||
Laurent Dufrechou
|
r1834 | self.StyleSetSpec(stc.STC_STYLE_DEFAULT, p['default']) | ||
else: | ||||
Gael Varoquaux
|
r1893 | self.StyleSetSpec(stc.STC_STYLE_DEFAULT, | ||
"fore:%s,back:%s,size:%d,face:%s" | ||||
% (self.ANSI_STYLES['0;30'][1], | ||||
background_color, | ||||
self.faces['size'], self.faces['mono'])) | ||||
Laurent Dufrechou
|
r1834 | |||
gvaroquaux
|
r1455 | self.StyleClearAll() | ||
Laurent Dufrechou
|
r1834 | |||
# XXX: two lines below are usefull if not using the lexer | ||||
#for style in self.ANSI_STYLES.values(): | ||||
# self.StyleSetSpec(style[0], "bold,fore:%s" % style[1]) | ||||
Gael Varoquaux
|
r1947 | # prompt definition | ||
self.prompt_in1 = p['prompt_in1'] | ||||
self.prompt_out = p['prompt_out'] | ||||
Laurent Dufrechou
|
r1835 | |||
self.output_prompt_template = string.Template(self.prompt_out) | ||||
self.input_prompt_template = string.Template(self.prompt_in1) | ||||
Gael Varoquaux
|
r1947 | 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']) | ||||
edge_column = p['edge_column'] | ||||
if edge_column is not None and edge_column > 0: | ||||
#we add a vertical line to console widget | ||||
self.SetEdgeMode(stc.STC_EDGE_LINE) | ||||
self.SetEdgeColumn(edge_column) | ||||
Gael Varoquaux
|
r1893 | |||
Gael Varoquaux
|
r1894 | |||
#-------------------------------------------------------------------------- | ||||
# 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. | ||||
""" | ||||
Laurent Dufrechou
|
r1835 | #-------------------------------------------------------------------------- | ||
# Private API | ||||
#-------------------------------------------------------------------------- | ||||
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
|
r1896 | # XXX: Would the right way to do this be to have a | ||
# dictionary at the instance level associating keys with | ||||
# callbacks? How would we deal with inheritance? And Do the | ||||
# different callbacks share local variables? | ||||
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
|
r1896 | elif event.KeyCode == wx.WXK_HOME: | ||
self.GotoPos(self.GetLength()) | ||||
elif event.KeyCode == wx.WXK_END: | ||||
self.GotoPos(self.GetLength()) | ||||
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 \ | ||
Gael Varoquaux
|
r1896 | event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN, | ||
wx.MOD_SHIFT): | ||||
Gael Varoquaux
|
r1358 | catched = True | ||
Laurent Dufrechou
|
r1835 | if not self.enter_catched: | ||
Laurent Dufrechou
|
r1834 | self.CallTipCancel() | ||
Gael Varoquaux
|
r1896 | if event.Modifiers == wx.MOD_SHIFT: | ||
# Try to force execution | ||||
self.GotoPos(self.GetLength()) | ||||
self.write('\n' + self.continuation_prompt(), | ||||
refresh=False) | ||||
self._on_enter() | ||||
else: | ||||
self._on_enter() | ||||
Laurent Dufrechou
|
r1834 | self.enter_catched = True | ||
Gael Varoquaux
|
r1358 | |||
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): | ||
Gael Varoquaux
|
r1886 | if not self._keep_cursor_in_buffer(self.GetCurrentPos() - 1): | ||
event.Skip() | ||||
catched = True | ||||
elif event.KeyCode == wx.WXK_RIGHT: | ||||
if not self._keep_cursor_in_buffer(self.GetCurrentPos() + 1): | ||||
event.Skip() | ||||
catched = True | ||||
elif event.KeyCode == wx.WXK_DELETE: | ||||
if not self._keep_cursor_in_buffer(self.GetCurrentPos() - 1): | ||||
gvaroquaux
|
r1295 | event.Skip() | ||
catched = True | ||||
if skip and not catched: | ||||
gvaroquaux
|
r1436 | # Put the cursor back in the edit region | ||
Gael Varoquaux
|
r1884 | if not self._keep_cursor_in_buffer(): | ||
Gael Varoquaux
|
r1886 | if not (self.GetCurrentPos() == self.GetLength() | ||
Gael Varoquaux
|
r1884 | and event.KeyCode == wx.WXK_DELETE): | ||
Gael Varoquaux
|
r1886 | event.Skip() | ||
catched = True | ||||
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. | ||||
""" | ||||
Gael Varoquaux
|
r1884 | if skip: | ||
event.Skip() | ||||
self._keep_cursor_in_buffer() | ||||
Gael Varoquaux
|
r1894 | # XXX: I need to avoid the problem of having an empty glass; | ||
Gael Varoquaux
|
r1886 | def _keep_cursor_in_buffer(self, pos=None): | ||
Gael Varoquaux
|
r1884 | """ Checks if the cursor is where it is allowed to be. If not, | ||
put it back. | ||||
Returns | ||||
------- | ||||
cursor_moved: Boolean | ||||
whether or not the cursor was moved by this routine. | ||||
Notes | ||||
------ | ||||
WARNING: This does proper checks only for horizontal | ||||
movements. | ||||
""" | ||||
Gael Varoquaux
|
r1886 | if pos is None: | ||
current_pos = self.GetCurrentPos() | ||||
else: | ||||
current_pos = pos | ||||
Gael Varoquaux
|
r1884 | if current_pos < self.current_prompt_pos: | ||
Gael Varoquaux
|
r1358 | self.GotoPos(self.current_prompt_pos) | ||
Gael Varoquaux
|
r1884 | return True | ||
Gael Varoquaux
|
r1886 | line_num = self.LineFromPosition(current_pos) | ||
if not current_pos > self.GetLength(): | ||||
line_pos = self.GetColumn(current_pos) | ||||
else: | ||||
line_pos = self.GetColumn(self.GetLength()) | ||||
line = self.GetLine(line_num) | ||||
Gael Varoquaux
|
r1884 | # Jump the continuation prompt | ||
continuation_prompt = self.continuation_prompt() | ||||
if ( line.startswith(continuation_prompt) | ||||
Gael Varoquaux
|
r1894 | and line_pos < len(continuation_prompt)): | ||
Gael Varoquaux
|
r1884 | if line_pos < 2: | ||
# We are at the beginning of the line, trying to move | ||||
# forward: jump forward. | ||||
self.GotoPos(current_pos + 1 + | ||||
len(continuation_prompt) - line_pos) | ||||
else: | ||||
# Jump back up | ||||
Gael Varoquaux
|
r1886 | self.GotoPos(self.GetLineEndPosition(line_num-1)) | ||
return True | ||||
elif ( current_pos > self.GetLineEndPosition(line_num) | ||||
and not current_pos == self.GetLength()): | ||||
# Jump to next line | ||||
self.GotoPos(current_pos + 1 + | ||||
len(continuation_prompt)) | ||||
Gael Varoquaux
|
r1884 | return True | ||
Gael Varoquaux
|
r1358 | |||
Gael Varoquaux
|
r1897 | # We re-allow enter event processing | ||
self.enter_catched = False | ||||
Gael Varoquaux
|
r1893 | return False | ||
Gael Varoquaux
|
r1358 | |||
gvaroquaux
|
r1295 | if __name__ == '__main__': | ||
# Some simple code to test the console widget. | ||||
class MainWindow(wx.Frame): | ||||
def __init__(self, parent, id, title): | ||||
Gael Varoquaux
|
r1893 | wx.Frame.__init__(self, parent, id, title, size=(300, 250)) | ||
gvaroquaux
|
r1295 | 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() | ||||