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