##// END OF EJS Templates
Add color feedback for multiline entry.
Gael Varoquaux -
Show More
@@ -1,465 +1,479 b''
1 1 # encoding: utf-8 -*- test-case-name:
2 2 # FIXME: Need to add tests.
3 3 # ipython1.frontend.wx.tests.test_wx_frontend -*-
4 4
5 5 """Classes to provide a Wx frontend to the
6 6 IPython.kernel.core.interpreter.
7 7
8 8 This class inherits from ConsoleWidget, that provides a console-like
9 9 widget to provide a text-rendering widget suitable for a terminal.
10 10 """
11 11
12 12 __docformat__ = "restructuredtext en"
13 13
14 14 #-------------------------------------------------------------------------------
15 15 # Copyright (C) 2008 The IPython Development Team
16 16 #
17 17 # Distributed under the terms of the BSD License. The full license is in
18 18 # the file COPYING, distributed as part of this software.
19 19 #-------------------------------------------------------------------------------
20 20
21 21 #-------------------------------------------------------------------------------
22 22 # Imports
23 23 #-------------------------------------------------------------------------------
24 24
25 25 # Major library imports
26 26 import re
27 27 import __builtin__
28 28 from time import sleep
29 29 import sys
30 30 from threading import Lock
31 31 import string
32 32
33 33 import wx
34 34 from wx import stc
35 35
36 36 # Ipython-specific imports.
37 37 from IPython.frontend._process import PipedProcess
38 38 from console_widget import ConsoleWidget
39 39 from IPython.frontend.prefilterfrontend import PrefilterFrontEnd
40 40
41 41 #-------------------------------------------------------------------------------
42 42 # Constants
43 43 #-------------------------------------------------------------------------------
44 44
45 45 _COMPLETE_BUFFER_BG = '#FAFAF1' # Nice green
46 46 _INPUT_BUFFER_BG = '#FDFFD3' # Nice yellow
47 47 _ERROR_BG = '#FFF1F1' # Nice red
48 48
49 49 _COMPLETE_BUFFER_MARKER = 31
50 50 _ERROR_MARKER = 30
51 51 _INPUT_MARKER = 29
52 52
53 53 prompt_in1 = \
54 54 '\n\x01\x1b[0;34m\x02In [\x01\x1b[1;34m\x02$number\x01\x1b[0;34m\x02]: \x01\x1b[0m\x02'
55 55
56 56 prompt_out = \
57 57 '\x01\x1b[0;31m\x02Out[\x01\x1b[1;31m\x02$number\x01\x1b[0;31m\x02]: \x01\x1b[0m\x02'
58 58
59 59 #-------------------------------------------------------------------------------
60 60 # Classes to implement the Wx frontend
61 61 #-------------------------------------------------------------------------------
62 62 class WxController(ConsoleWidget, PrefilterFrontEnd):
63 63 """Classes to provide a Wx frontend to the
64 64 IPython.kernel.core.interpreter.
65 65
66 66 This class inherits from ConsoleWidget, that provides a console-like
67 67 widget to provide a text-rendering widget suitable for a terminal.
68 68 """
69 69
70 70 output_prompt_template = string.Template(prompt_out)
71 71
72 72 input_prompt_template = string.Template(prompt_in1)
73 73
74 74 # Print debug info on what is happening to the console.
75 75 debug = True
76 76
77 77 # The title of the terminal, as captured through the ANSI escape
78 78 # sequences.
79 79 def _set_title(self, title):
80 80 return self.Parent.SetTitle(title)
81 81
82 82 def _get_title(self):
83 83 return self.Parent.GetTitle()
84 84
85 85 title = property(_get_title, _set_title)
86 86
87 87
88 88 # The buffer being edited.
89 # We are duplicating the defination here because of multiple
89 # We are duplicating the definition here because of multiple
90 90 # inheritence
91 91 def _set_input_buffer(self, string):
92 return ConsoleWidget._set_input_buffer(self, string)
92 ConsoleWidget._set_input_buffer(self, string)
93 wx.Yield()
94 self._colorize_input_buffer()
93 95
94 96 def _get_input_buffer(self):
95 97 """ Returns the text in current edit buffer.
96 98 """
97 99 return ConsoleWidget._get_input_buffer(self)
98 100
99 101 input_buffer = property(_get_input_buffer, _set_input_buffer)
100 102
101 103
102 104 #--------------------------------------------------------------------------
103 105 # Private Attributes
104 106 #--------------------------------------------------------------------------
105 107
106 108 # A flag governing the behavior of the input. Can be:
107 109 #
108 110 # 'readline' for readline-like behavior with a prompt
109 111 # and an edit buffer.
110 112 # 'subprocess' for sending the raw input directly to a
111 113 # subprocess.
112 114 # 'buffering' for buffering of the input, that will be used
113 115 # when the input state switches back to another state.
114 116 _input_state = 'readline'
115 117
116 118 # Attribute to store reference to the pipes of a subprocess, if we
117 119 # are running any.
118 120 _running_process = False
119 121
120 122 # A queue for writing fast streams to the screen without flooding the
121 123 # event loop
122 124 _out_buffer = []
123 125
124 126 # A lock to lock the _out_buffer to make sure we don't empty it
125 127 # while it is being swapped
126 128 _out_buffer_lock = Lock()
127 129
128 130 _markers = dict()
129 131
130 132 #--------------------------------------------------------------------------
131 133 # Public API
132 134 #--------------------------------------------------------------------------
133 135
134 136 def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
135 137 size=wx.DefaultSize, style=wx.CLIP_CHILDREN,
136 138 *args, **kwds):
137 139 """ Create Shell instance.
138 140 """
139 141 ConsoleWidget.__init__(self, parent, id, pos, size, style)
140 142 PrefilterFrontEnd.__init__(self)
141 143
142 144 # Marker for complete buffer.
143 145 self.MarkerDefine(_COMPLETE_BUFFER_MARKER, stc.STC_MARK_BACKGROUND,
144 146 background=_COMPLETE_BUFFER_BG)
145 147 # Marker for current input buffer.
146 148 self.MarkerDefine(_INPUT_MARKER, stc.STC_MARK_BACKGROUND,
147 149 background=_INPUT_BUFFER_BG)
148 150 # Marker for tracebacks.
149 151 self.MarkerDefine(_ERROR_MARKER, stc.STC_MARK_BACKGROUND,
150 152 background=_ERROR_BG)
151 153
152 154 # A time for flushing the write buffer
153 155 BUFFER_FLUSH_TIMER_ID = 100
154 156 self._buffer_flush_timer = wx.Timer(self, BUFFER_FLUSH_TIMER_ID)
155 157 wx.EVT_TIMER(self, BUFFER_FLUSH_TIMER_ID, self._buffer_flush)
156 158
157 159 # Inject self in namespace, for debug
158 160 if self.debug:
159 161 self.shell.user_ns['self'] = self
160 162
161 163
162 164 def raw_input(self, prompt):
163 165 """ A replacement from python's raw_input.
164 166 """
165 167 self.new_prompt(prompt)
166 168 self.waiting = True
167 169 self.__old_on_enter = self._on_enter
168 170 def my_on_enter():
169 171 self.waiting = False
170 172 self._on_enter = my_on_enter
171 173 # XXX: Busy waiting, ugly.
172 174 while self.waiting:
173 175 wx.Yield()
174 176 sleep(0.1)
175 177 self._on_enter = self.__old_on_enter
176 178 self._input_state = 'buffering'
177 179 return self.input_buffer.rstrip('\n')
178 180
179 181
180 182 def system_call(self, command_string):
181 183 self._input_state = 'subprocess'
182 184 self._running_process = PipedProcess(command_string,
183 185 out_callback=self.buffered_write,
184 186 end_callback = self._end_system_call)
185 187 self._running_process.start()
186 188 # XXX: another one of these polling loops to have a blocking
187 189 # call
188 190 wx.Yield()
189 191 while self._running_process:
190 192 wx.Yield()
191 193 sleep(0.1)
192 194 # Be sure to flush the buffer.
193 195 self._buffer_flush(event=None)
194 196
195 197
196 198 def do_calltip(self):
197 199 """ Analyse current and displays useful calltip for it.
198 200 """
199 201 if self.debug:
200 202 print >>sys.__stdout__, "do_calltip"
201 203 separators = re.compile('[\s\{\}\[\]\(\)\= ,:]')
202 204 symbol = self.input_buffer
203 205 symbol_string = separators.split(symbol)[-1]
204 206 base_symbol_string = symbol_string.split('.')[0]
205 207 if base_symbol_string in self.shell.user_ns:
206 208 symbol = self.shell.user_ns[base_symbol_string]
207 209 elif base_symbol_string in self.shell.user_global_ns:
208 210 symbol = self.shell.user_global_ns[base_symbol_string]
209 211 elif base_symbol_string in __builtin__.__dict__:
210 212 symbol = __builtin__.__dict__[base_symbol_string]
211 213 else:
212 214 return False
213 215 for name in symbol_string.split('.')[1:] + ['__doc__']:
214 216 symbol = getattr(symbol, name)
215 217 try:
216 218 self.AutoCompCancel()
217 219 wx.Yield()
218 220 self.CallTipShow(self.GetCurrentPos(), symbol)
219 221 except:
220 222 # The retrieve symbol couldn't be converted to a string
221 223 pass
222 224
223 225
224 226 def _popup_completion(self, create=False):
225 227 """ Updates the popup completion menu if it exists. If create is
226 228 true, open the menu.
227 229 """
228 230 if self.debug:
229 231 print >>sys.__stdout__, "_popup_completion",
230 232 line = self.input_buffer
231 233 if (self.AutoCompActive() and not line[-1] == '.') \
232 234 or create==True:
233 235 suggestion, completions = self.complete(line)
234 236 offset=0
235 237 if completions:
236 238 complete_sep = re.compile('[\s\{\}\[\]\(\)\= ,:]')
237 239 residual = complete_sep.split(line)[-1]
238 240 offset = len(residual)
239 241 self.pop_completion(completions, offset=offset)
240 242 if self.debug:
241 243 print >>sys.__stdout__, completions
242 244
243 245
244 246 def buffered_write(self, text):
245 247 """ A write method for streams, that caches the stream in order
246 248 to avoid flooding the event loop.
247 249
248 250 This can be called outside of the main loop, in separate
249 251 threads.
250 252 """
251 253 self._out_buffer_lock.acquire()
252 254 self._out_buffer.append(text)
253 255 self._out_buffer_lock.release()
254 256 if not self._buffer_flush_timer.IsRunning():
255 257 wx.CallAfter(self._buffer_flush_timer.Start, 100) # milliseconds
256 258
257 259
258 260 #--------------------------------------------------------------------------
259 261 # LineFrontEnd interface
260 262 #--------------------------------------------------------------------------
261 263
262 264 def execute(self, python_string, raw_string=None):
263 265 self._input_state = 'buffering'
264 266 self.CallTipCancel()
265 267 self._cursor = wx.BusyCursor()
266 268 if raw_string is None:
267 269 raw_string = python_string
268 270 end_line = self.current_prompt_line \
269 271 + max(1, len(raw_string.split('\n'))-1)
270 272 for i in range(self.current_prompt_line, end_line):
271 273 if i in self._markers:
272 274 self.MarkerDeleteHandle(self._markers[i])
273 275 self._markers[i] = self.MarkerAdd(i, _COMPLETE_BUFFER_MARKER)
274 276 # Update the display:
275 277 wx.Yield()
276 278 self.GotoPos(self.GetLength())
277 279 PrefilterFrontEnd.execute(self, python_string, raw_string=raw_string)
278 280
281 def save_output_hooks(self):
282 self.__old_raw_input = __builtin__.raw_input
283 PrefilterFrontEnd.save_output_hooks(self)
279 284
280 285 def capture_output(self):
281 self.__old_raw_input = __builtin__.raw_input
282 286 __builtin__.raw_input = self.raw_input
283 287 PrefilterFrontEnd.capture_output(self)
284 288
285 289
286 290 def release_output(self):
287 291 __builtin__.raw_input = self.__old_raw_input
288 292 PrefilterFrontEnd.release_output(self)
289 293
290 294
291 295 def after_execute(self):
292 296 PrefilterFrontEnd.after_execute(self)
293 297 # Clear the wait cursor
294 298 if hasattr(self, '_cursor'):
295 299 del self._cursor
296 300
297 301
298 302 def show_traceback(self):
299 303 start_line = self.GetCurrentLine()
300 304 PrefilterFrontEnd.show_traceback(self)
301 305 wx.Yield()
302 306 for i in range(start_line, self.GetCurrentLine()):
303 307 self._markers[i] = self.MarkerAdd(i, _ERROR_MARKER)
304 308
305 309
306 310 #--------------------------------------------------------------------------
307 311 # ConsoleWidget interface
308 312 #--------------------------------------------------------------------------
309 313
310 314 def new_prompt(self, prompt):
311 315 """ Display a new prompt, and start a new input buffer.
312 316 """
313 317 self._input_state = 'readline'
314 318 ConsoleWidget.new_prompt(self, prompt)
319 i = self.current_prompt_line
320 self._markers[i] = self.MarkerAdd(i, _INPUT_MARKER)
315 321
316 322
317 323 def write(self, *args, **kwargs):
318 324 # Avoid multiple inheritence, be explicit about which
319 325 # parent method class gets called
320 326 ConsoleWidget.write(self, *args, **kwargs)
321 327
322 328
323 329 def _on_key_down(self, event, skip=True):
324 330 """ Capture the character events, let the parent
325 331 widget handle them, and put our logic afterward.
326 332 """
327 333 # FIXME: This method needs to be broken down in smaller ones.
328 334 current_line_number = self.GetCurrentLine()
329 335 if event.KeyCode in (ord('c'), ord('C')) and event.ControlDown():
330 336 # Capture Control-C
331 337 if self._input_state == 'subprocess':
332 338 if self.debug:
333 339 print >>sys.__stderr__, 'Killing running process'
334 340 self._running_process.process.kill()
335 341 elif self._input_state == 'buffering':
336 342 if self.debug:
337 343 print >>sys.__stderr__, 'Raising KeyboardInterrupt'
338 344 raise KeyboardInterrupt
339 345 # XXX: We need to make really sure we
340 346 # get back to a prompt.
341 347 elif self._input_state == 'subprocess' and (
342 348 ( event.KeyCode<256 and
343 349 not event.ControlDown() )
344 350 or
345 351 ( event.KeyCode in (ord('d'), ord('D')) and
346 352 event.ControlDown())):
347 353 # We are running a process, we redirect keys.
348 354 ConsoleWidget._on_key_down(self, event, skip=skip)
349 355 char = chr(event.KeyCode)
350 356 # Deal with some inconsistency in wx keycodes:
351 357 if char == '\r':
352 358 char = '\n'
353 359 elif not event.ShiftDown():
354 360 char = char.lower()
355 361 if event.ControlDown() and event.KeyCode in (ord('d'), ord('D')):
356 362 char = '\04'
357 363 self._running_process.process.stdin.write(char)
358 364 self._running_process.process.stdin.flush()
359 365 elif event.KeyCode in (ord('('), 57):
360 366 # Calltips
361 367 event.Skip()
362 368 self.do_calltip()
363 369 elif self.AutoCompActive():
364 370 event.Skip()
365 371 if event.KeyCode in (wx.WXK_BACK, wx.WXK_DELETE):
366 372 wx.CallAfter(self._popup_completion, create=True)
367 373 elif not event.KeyCode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT,
368 374 wx.WXK_RIGHT):
369 375 wx.CallAfter(self._popup_completion)
370 376 else:
371 377 # Up history
372 378 if event.KeyCode == wx.WXK_UP and (
373 379 ( current_line_number == self.current_prompt_line and
374 380 event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN) )
375 381 or event.ControlDown() ):
376 382 new_buffer = self.get_history_previous(
377 383 self.input_buffer)
378 384 if new_buffer is not None:
379 385 self.input_buffer = new_buffer
380 386 if self.GetCurrentLine() > self.current_prompt_line:
381 387 # Go to first line, for seemless history up.
382 388 self.GotoPos(self.current_prompt_pos)
383 389 # Down history
384 390 elif event.KeyCode == wx.WXK_DOWN and (
385 391 ( current_line_number == self.LineCount -1 and
386 392 event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN) )
387 393 or event.ControlDown() ):
388 394 new_buffer = self.get_history_next()
389 395 if new_buffer is not None:
390 396 self.input_buffer = new_buffer
391 397 # Tab-completion
392 398 elif event.KeyCode == ord('\t'):
393 399 last_line = self.input_buffer.split('\n')[-1]
394 400 if not re.match(r'^\s*$', last_line):
395 401 self.complete_current_input()
396 402 else:
397 403 event.Skip()
398 404 else:
399 405 ConsoleWidget._on_key_down(self, event, skip=skip)
400 406
401 407
402 408 def _on_key_up(self, event, skip=True):
403 409 """ Called when any key is released.
404 410 """
405 411 if event.KeyCode in (59, ord('.')):
406 412 # Intercepting '.'
407 413 event.Skip()
408 414 self._popup_completion(create=True)
409 415 else:
410 416 ConsoleWidget._on_key_up(self, event, skip=skip)
411 417
412 418
413 419 def _on_enter(self):
414 420 """ Called on return key down, in readline input_state.
415 421 """
416 422 if self.debug:
417 423 print >>sys.__stdout__, repr(self.input_buffer)
418 i = self.GetLineCount()
419 self._markers[i] = self.MarkerAdd(i, _INPUT_MARKER)
420 424 PrefilterFrontEnd._on_enter(self)
425 #self._colorize_input_buffer()
421 426
422 427
423 428 #--------------------------------------------------------------------------
424 429 # Private API
425 430 #--------------------------------------------------------------------------
426 431
427 432 def _end_system_call(self):
428 433 """ Called at the end of a system call.
429 434 """
430 435 self._input_state = 'buffering'
431 436 self._running_process = False
432 437
433 438
434 439 def _buffer_flush(self, event):
435 440 """ Called by the timer to flush the write buffer.
436 441
437 442 This is always called in the mainloop, by the wx timer.
438 443 """
439 444 self._out_buffer_lock.acquire()
440 445 _out_buffer = self._out_buffer
441 446 self._out_buffer = []
442 447 self._out_buffer_lock.release()
443 448 self.write(''.join(_out_buffer), refresh=False)
444 449 self._buffer_flush_timer.Stop()
445 450
451 def _colorize_input_buffer(self):
452 """ Keep the input buffer lines at a bright color.
453 """
454 end_line = self.GetCurrentLine() + 1
455 for i in range(self.current_prompt_line, end_line):
456 if i in self._markers:
457 self.MarkerDeleteHandle(self._markers[i])
458 self._markers[i] = self.MarkerAdd(i, _INPUT_MARKER)
459
446 460
447 461 if __name__ == '__main__':
448 462 class MainWindow(wx.Frame):
449 463 def __init__(self, parent, id, title):
450 464 wx.Frame.__init__(self, parent, id, title, size=(300,250))
451 465 self._sizer = wx.BoxSizer(wx.VERTICAL)
452 466 self.shell = WxController(self)
453 467 self._sizer.Add(self.shell, 1, wx.EXPAND)
454 468 self.SetSizer(self._sizer)
455 469 self.SetAutoLayout(1)
456 470 self.Show(True)
457 471
458 472 app = wx.PySimpleApp()
459 473 frame = MainWindow(None, wx.ID_ANY, 'Ipython')
460 474 frame.shell.SetFocus()
461 475 frame.SetSize((680, 460))
462 476 self = frame.shell
463 477
464 478 app.MainLoop()
465 479
General Comments 0
You need to be logged in to leave comments. Login now