##// END OF EJS Templates
Add better keybindings.
Gael Varoquaux -
Show More
@@ -1,406 +1,415 b''
1 1 # encoding: utf-8
2 2 """
3 3 A Wx widget to act as a console and input commands.
4 4
5 5 This widget deals with prompts and provides an edit buffer
6 6 restricted to after the last prompt.
7 7 """
8 8
9 9 __docformat__ = "restructuredtext en"
10 10
11 11 #-------------------------------------------------------------------------------
12 12 # Copyright (C) 2008 The IPython Development Team
13 13 #
14 14 # Distributed under the terms of the BSD License. The full license is
15 15 # in the file COPYING, distributed as part of this software.
16 16 #-------------------------------------------------------------------------------
17 17
18 18 #-------------------------------------------------------------------------------
19 19 # Imports
20 20 #-------------------------------------------------------------------------------
21 21
22 22 import wx
23 23 import wx.stc as stc
24 24
25 25 from wx.py import editwindow
26 26 import sys
27 27 LINESEP = '\n'
28 28 if sys.platform == 'win32':
29 29 LINESEP = '\n\r'
30 30
31 31 import re
32 32
33 33 # FIXME: Need to provide an API for non user-generated display on the
34 34 # screen: this should not be editable by the user.
35 35
36 36 _DEFAULT_SIZE = 10
37 37
38 38 _DEFAULT_STYLE = {
39 39 'stdout' : 'fore:#0000FF',
40 40 'stderr' : 'fore:#007f00',
41 41 'trace' : 'fore:#FF0000',
42 42
43 43 'default' : 'size:%d' % _DEFAULT_SIZE,
44 44 'bracegood' : 'fore:#00AA00,back:#000000,bold',
45 45 'bracebad' : 'fore:#FF0000,back:#000000,bold',
46 46
47 47 # properties for the various Python lexer styles
48 48 'comment' : 'fore:#007F00',
49 49 'number' : 'fore:#007F7F',
50 50 'string' : 'fore:#7F007F,italic',
51 51 'char' : 'fore:#7F007F,italic',
52 52 'keyword' : 'fore:#00007F,bold',
53 53 'triple' : 'fore:#7F0000',
54 54 'tripledouble' : 'fore:#7F0000',
55 55 'class' : 'fore:#0000FF,bold,underline',
56 56 'def' : 'fore:#007F7F,bold',
57 57 'operator' : 'bold'
58 58 }
59 59
60 60 # new style numbers
61 61 _STDOUT_STYLE = 15
62 62 _STDERR_STYLE = 16
63 63 _TRACE_STYLE = 17
64 64
65 65
66 66 # system colors
67 67 #SYS_COLOUR_BACKGROUND = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BACKGROUND)
68 68
69 69 #-------------------------------------------------------------------------------
70 70 # The console widget class
71 71 #-------------------------------------------------------------------------------
72 72 class ConsoleWidget(editwindow.EditWindow):
73 73 """ Specialized styled text control view for console-like workflow.
74 74
75 75 This widget is mainly interested in dealing with the prompt and
76 76 keeping the cursor inside the editing line.
77 77 """
78 78
79 79 # This is where the title captured from the ANSI escape sequences are
80 80 # stored.
81 81 title = 'Console'
82 82
83 83 # The buffer being edited.
84 84 def _set_input_buffer(self, string):
85 85 self.SetSelection(self.current_prompt_pos, self.GetLength())
86 86 self.ReplaceSelection(string)
87 87 self.GotoPos(self.GetLength())
88 88
89 89 def _get_input_buffer(self):
90 90 """ Returns the text in current edit buffer.
91 91 """
92 92 input_buffer = self.GetTextRange(self.current_prompt_pos,
93 93 self.GetLength())
94 94 input_buffer = input_buffer.replace(LINESEP, '\n')
95 95 return input_buffer
96 96
97 97 input_buffer = property(_get_input_buffer, _set_input_buffer)
98 98
99 99 style = _DEFAULT_STYLE.copy()
100 100
101 101 # Translation table from ANSI escape sequences to color. Override
102 102 # this to specify your colors.
103 103 ANSI_STYLES = {'0;30': [0, 'BLACK'], '0;31': [1, 'RED'],
104 104 '0;32': [2, 'GREEN'], '0;33': [3, 'BROWN'],
105 105 '0;34': [4, 'BLUE'], '0;35': [5, 'PURPLE'],
106 106 '0;36': [6, 'CYAN'], '0;37': [7, 'LIGHT GREY'],
107 107 '1;30': [8, 'DARK GREY'], '1;31': [9, 'RED'],
108 108 '1;32': [10, 'SEA GREEN'], '1;33': [11, 'YELLOW'],
109 109 '1;34': [12, 'LIGHT BLUE'], '1;35':
110 110 [13, 'MEDIUM VIOLET RED'],
111 111 '1;36': [14, 'LIGHT STEEL BLUE'], '1;37': [15, 'YELLOW']}
112 112
113 113 # The color of the carret (call _apply_style() after setting)
114 114 carret_color = 'BLACK'
115 115
116 116 #--------------------------------------------------------------------------
117 117 # Public API
118 118 #--------------------------------------------------------------------------
119 119
120 120 def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
121 121 size=wx.DefaultSize, style=0, ):
122 122 editwindow.EditWindow.__init__(self, parent, id, pos, size, style)
123 123 self._configure_scintilla()
124 124
125 125 self.Bind(wx.EVT_KEY_DOWN, self._on_key_down)
126 126 self.Bind(wx.EVT_KEY_UP, self._on_key_up)
127 127
128 128
129 129 def write(self, text, refresh=True):
130 130 """ Write given text to buffer, while translating the ansi escape
131 131 sequences.
132 132 """
133 133 # XXX: do not put print statements to sys.stdout/sys.stderr in
134 134 # this method, the print statements will call this method, as
135 135 # you will end up with an infinit loop
136 136 title = self.title_pat.split(text)
137 137 if len(title)>1:
138 138 self.title = title[-2]
139 139
140 140 text = self.title_pat.sub('', text)
141 141 segments = self.color_pat.split(text)
142 142 segment = segments.pop(0)
143 143 self.GotoPos(self.GetLength())
144 144 self.StartStyling(self.GetLength(), 0xFF)
145 145 try:
146 146 self.AppendText(segment)
147 147 except UnicodeDecodeError:
148 148 # XXX: Do I really want to skip the exception?
149 149 pass
150 150
151 151 if segments:
152 152 for ansi_tag, text in zip(segments[::2], segments[1::2]):
153 153 self.StartStyling(self.GetLength(), 0xFF)
154 154 try:
155 155 self.AppendText(text)
156 156 except UnicodeDecodeError:
157 157 # XXX: Do I really want to skip the exception?
158 158 pass
159 159
160 160 if ansi_tag not in self.ANSI_STYLES:
161 161 style = 0
162 162 else:
163 163 style = self.ANSI_STYLES[ansi_tag][0]
164 164
165 165 self.SetStyling(len(text), style)
166 166
167 167 self.GotoPos(self.GetLength())
168 168 if refresh:
169 169 wx.Yield()
170 170
171 171
172 172 def new_prompt(self, prompt):
173 173 """ Prints a prompt at start of line, and move the start of the
174 174 current block there.
175 175
176 176 The prompt can be given with ascii escape sequences.
177 177 """
178 178 self.write(prompt, refresh=False)
179 179 # now we update our cursor giving end of prompt
180 180 self.current_prompt_pos = self.GetLength()
181 181 self.current_prompt_line = self.GetCurrentLine()
182 182 wx.Yield()
183 183 self.EnsureCaretVisible()
184 184
185 185
186 186 def scroll_to_bottom(self):
187 187 maxrange = self.GetScrollRange(wx.VERTICAL)
188 188 self.ScrollLines(maxrange)
189 189
190 190
191 191 def pop_completion(self, possibilities, offset=0):
192 192 """ Pops up an autocompletion menu. Offset is the offset
193 193 in characters of the position at which the menu should
194 194 appear, relativ to the cursor.
195 195 """
196 196 self.AutoCompSetIgnoreCase(False)
197 197 self.AutoCompSetAutoHide(False)
198 198 self.AutoCompSetMaxHeight(len(possibilities))
199 199 self.AutoCompShow(offset, " ".join(possibilities))
200 200
201 201
202 202 def get_line_width(self):
203 203 """ Return the width of the line in characters.
204 204 """
205 205 return self.GetSize()[0]/self.GetCharWidth()
206 206
207 207
208 208 #--------------------------------------------------------------------------
209 209 # Private API
210 210 #--------------------------------------------------------------------------
211 211
212 212 def _apply_style(self):
213 213 """ Applies the colors for the different text elements and the
214 214 carret.
215 215 """
216 216 self.SetCaretForeground(self.carret_color)
217 217
218 218 #self.StyleClearAll()
219 219 self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT,
220 220 "fore:#FF0000,back:#0000FF,bold")
221 221 self.StyleSetSpec(stc.STC_STYLE_BRACEBAD,
222 222 "fore:#000000,back:#FF0000,bold")
223 223
224 224 for style in self.ANSI_STYLES.values():
225 225 self.StyleSetSpec(style[0], "bold,fore:%s" % style[1])
226 226
227 227
228 228 def _configure_scintilla(self):
229 229 self.SetEOLMode(stc.STC_EOL_LF)
230 230
231 231 # Ctrl"+" or Ctrl "-" can be used to zoomin/zoomout the text inside
232 232 # the widget
233 233 self.CmdKeyAssign(ord('+'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMIN)
234 234 self.CmdKeyAssign(ord('-'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMOUT)
235 235 # Also allow Ctrl Shift "=" for poor non US keyboard users.
236 236 self.CmdKeyAssign(ord('='), stc.STC_SCMOD_CTRL|stc.STC_SCMOD_SHIFT,
237 237 stc.STC_CMD_ZOOMIN)
238 238
239 239 # Keys: we need to clear some of the keys the that don't play
240 240 # well with a console.
241 241 self.CmdKeyClear(ord('D'), stc.STC_SCMOD_CTRL)
242 242 self.CmdKeyClear(ord('L'), stc.STC_SCMOD_CTRL)
243 243 self.CmdKeyClear(ord('T'), stc.STC_SCMOD_CTRL)
244 self.CmdKeyClear(ord('A'), stc.STC_SCMOD_CTRL)
244 245
245 246 self.SetEOLMode(stc.STC_EOL_CRLF)
246 247 self.SetWrapMode(stc.STC_WRAP_CHAR)
247 248 self.SetWrapMode(stc.STC_WRAP_WORD)
248 249 self.SetBufferedDraw(True)
249 250 self.SetUseAntiAliasing(True)
250 251 self.SetLayoutCache(stc.STC_CACHE_PAGE)
251 252 self.SetUndoCollection(False)
252 253 self.SetUseTabs(True)
253 254 self.SetIndent(4)
254 255 self.SetTabWidth(4)
255 256
256 257 # we don't want scintilla's autocompletion to choose
257 258 # automaticaly out of a single choice list, as we pop it up
258 259 # automaticaly
259 260 self.AutoCompSetChooseSingle(False)
260 261 self.AutoCompSetMaxHeight(10)
261 262 # XXX: this doesn't seem to have an effect.
262 263 self.AutoCompSetFillUps('\n')
263 264
264 265 self.SetMargins(3, 3) #text is moved away from border with 3px
265 266 # Suppressing Scintilla margins
266 267 self.SetMarginWidth(0, 0)
267 268 self.SetMarginWidth(1, 0)
268 269 self.SetMarginWidth(2, 0)
269 270
270 271 self._apply_style()
271 272
272 273 # Xterm escape sequences
273 274 self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?')
274 275 self.title_pat = re.compile('\x1b]0;(.*?)\x07')
275 276
276 277 #self.SetEdgeMode(stc.STC_EDGE_LINE)
277 278 #self.SetEdgeColumn(80)
278 279
279 280 # styles
280 281 p = self.style
281 282 self.StyleSetSpec(stc.STC_STYLE_DEFAULT, p['default'])
282 283 self.StyleClearAll()
283 284 self.StyleSetSpec(_STDOUT_STYLE, p['stdout'])
284 285 self.StyleSetSpec(_STDERR_STYLE, p['stderr'])
285 286 self.StyleSetSpec(_TRACE_STYLE, p['trace'])
286 287
287 288 self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT, p['bracegood'])
288 289 self.StyleSetSpec(stc.STC_STYLE_BRACEBAD, p['bracebad'])
289 290 self.StyleSetSpec(stc.STC_P_COMMENTLINE, p['comment'])
290 291 self.StyleSetSpec(stc.STC_P_NUMBER, p['number'])
291 292 self.StyleSetSpec(stc.STC_P_STRING, p['string'])
292 293 self.StyleSetSpec(stc.STC_P_CHARACTER, p['char'])
293 294 self.StyleSetSpec(stc.STC_P_WORD, p['keyword'])
294 295 self.StyleSetSpec(stc.STC_P_WORD2, p['keyword'])
295 296 self.StyleSetSpec(stc.STC_P_TRIPLE, p['triple'])
296 297 self.StyleSetSpec(stc.STC_P_TRIPLEDOUBLE, p['tripledouble'])
297 298 self.StyleSetSpec(stc.STC_P_CLASSNAME, p['class'])
298 299 self.StyleSetSpec(stc.STC_P_DEFNAME, p['def'])
299 300 self.StyleSetSpec(stc.STC_P_OPERATOR, p['operator'])
300 301 self.StyleSetSpec(stc.STC_P_COMMENTBLOCK, p['comment'])
301 302
302 303 def _on_key_down(self, event, skip=True):
303 304 """ Key press callback used for correcting behavior for
304 305 console-like interfaces: the cursor is constraint to be after
305 306 the last prompt.
306 307
307 308 Return True if event as been catched.
308 309 """
309 310 catched = True
310 311 # Intercept some specific keys.
311 312 if event.KeyCode == ord('L') and event.ControlDown() :
312 313 self.scroll_to_bottom()
313 314 elif event.KeyCode == ord('K') and event.ControlDown() :
314 315 self.input_buffer = ''
316 elif event.KeyCode == ord('A') and event.ControlDown() :
317 self.GotoPos(self.GetLength())
318 self.SetSelectionStart(self.current_prompt_pos)
319 self.SetSelectionEnd(self.GetCurrentPos())
320 catched = True
321 elif event.KeyCode == ord('E') and event.ControlDown() :
322 self.GotoPos(self.GetLength())
323 catched = True
315 324 elif event.KeyCode == wx.WXK_PAGEUP:
316 325 self.ScrollPages(-1)
317 326 elif event.KeyCode == wx.WXK_PAGEDOWN:
318 327 self.ScrollPages(1)
319 328 elif event.KeyCode == wx.WXK_UP and event.ShiftDown():
320 329 self.ScrollLines(-1)
321 330 elif event.KeyCode == wx.WXK_DOWN and event.ShiftDown():
322 331 self.ScrollLines(1)
323 332 else:
324 333 catched = False
325 334
326 335 if self.AutoCompActive():
327 336 event.Skip()
328 337 else:
329 338 if event.KeyCode in (13, wx.WXK_NUMPAD_ENTER) and \
330 339 event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN):
331 340 catched = True
332 341 self.CallTipCancel()
333 342 self.write('\n', refresh=False)
334 343 # Under windows scintilla seems to be doing funny stuff to the
335 344 # line returns here, but the getter for input_buffer filters
336 345 # this out.
337 346 if sys.platform == 'win32':
338 347 self.input_buffer = self.input_buffer
339 348 self._on_enter()
340 349
341 350 elif event.KeyCode == wx.WXK_HOME:
342 351 if event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN):
343 352 self.GotoPos(self.current_prompt_pos)
344 353 catched = True
345 354
346 elif event.Modifiers in (wx.MOD_SHIFT, wx.MOD_WIN) :
355 elif event.Modifiers == wx.MOD_SHIFT:
347 356 # FIXME: This behavior is not ideal: if the selection
348 357 # is already started, it will jump.
349 358 self.SetSelectionStart(self.current_prompt_pos)
350 359 self.SetSelectionEnd(self.GetCurrentPos())
351 360 catched = True
352 361
353 362 elif event.KeyCode == wx.WXK_UP:
354 363 if self.GetCurrentLine() > self.current_prompt_line:
355 364 if self.GetCurrentLine() == self.current_prompt_line + 1 \
356 365 and self.GetColumn(self.GetCurrentPos()) < \
357 366 self.GetColumn(self.current_prompt_pos):
358 367 self.GotoPos(self.current_prompt_pos)
359 368 else:
360 369 event.Skip()
361 370 catched = True
362 371
363 372 elif event.KeyCode in (wx.WXK_LEFT, wx.WXK_BACK):
364 373 if self.GetCurrentPos() > self.current_prompt_pos:
365 374 event.Skip()
366 375 catched = True
367 376
368 377 if skip and not catched:
369 378 # Put the cursor back in the edit region
370 379 if self.GetCurrentPos() < self.current_prompt_pos:
371 380 self.GotoPos(self.current_prompt_pos)
372 381 else:
373 382 event.Skip()
374 383
375 384 return catched
376 385
377 386
378 387 def _on_key_up(self, event, skip=True):
379 388 """ If cursor is outside the editing region, put it back.
380 389 """
381 390 event.Skip()
382 391 if self.GetCurrentPos() < self.current_prompt_pos:
383 392 self.GotoPos(self.current_prompt_pos)
384 393
385 394
386 395
387 396 if __name__ == '__main__':
388 397 # Some simple code to test the console widget.
389 398 class MainWindow(wx.Frame):
390 399 def __init__(self, parent, id, title):
391 400 wx.Frame.__init__(self, parent, id, title, size=(300,250))
392 401 self._sizer = wx.BoxSizer(wx.VERTICAL)
393 402 self.console_widget = ConsoleWidget(self)
394 403 self._sizer.Add(self.console_widget, 1, wx.EXPAND)
395 404 self.SetSizer(self._sizer)
396 405 self.SetAutoLayout(1)
397 406 self.Show(True)
398 407
399 408 app = wx.PySimpleApp()
400 409 w = MainWindow(None, wx.ID_ANY, 'ConsoleWidget')
401 410 w.SetSize((780, 460))
402 411 w.Show()
403 412
404 413 app.MainLoop()
405 414
406 415
General Comments 0
You need to be logged in to leave comments. Login now