##// END OF EJS Templates
BUG: Correct some trailing wx2.6 incompatibilities
Gael Varoquaux -
Show More
@@ -1,602 +1,602 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 import sys
29 29 from threading import Lock
30 30
31 31 import wx
32 32 from wx import stc
33 33
34 34 # Ipython-specific imports.
35 35 from IPython.frontend.process import PipedProcess
36 36 from console_widget import ConsoleWidget, _COMPLETE_BUFFER_MARKER, \
37 37 _ERROR_MARKER, _INPUT_MARKER
38 38 from IPython.frontend.prefilterfrontend import PrefilterFrontEnd
39 39
40 40 #-------------------------------------------------------------------------------
41 41 # Classes to implement the Wx frontend
42 42 #-------------------------------------------------------------------------------
43 43 class WxController(ConsoleWidget, PrefilterFrontEnd):
44 44 """Classes to provide a Wx frontend to the
45 45 IPython.kernel.core.interpreter.
46 46
47 47 This class inherits from ConsoleWidget, that provides a console-like
48 48 widget to provide a text-rendering widget suitable for a terminal.
49 49 """
50 50
51 51 # Print debug info on what is happening to the console.
52 52 debug = False
53 53
54 54 # The title of the terminal, as captured through the ANSI escape
55 55 # sequences.
56 56 def _set_title(self, title):
57 57 return self.Parent.SetTitle(title)
58 58
59 59 def _get_title(self):
60 60 return self.Parent.GetTitle()
61 61
62 62 title = property(_get_title, _set_title)
63 63
64 64
65 65 # The buffer being edited.
66 66 # We are duplicating the definition here because of multiple
67 67 # inheritence
68 68 def _set_input_buffer(self, string):
69 69 ConsoleWidget._set_input_buffer(self, string)
70 70 self._colorize_input_buffer()
71 71
72 72 def _get_input_buffer(self):
73 73 """ Returns the text in current edit buffer.
74 74 """
75 75 return ConsoleWidget._get_input_buffer(self)
76 76
77 77 input_buffer = property(_get_input_buffer, _set_input_buffer)
78 78
79 79
80 80 #--------------------------------------------------------------------------
81 81 # Private Attributes
82 82 #--------------------------------------------------------------------------
83 83
84 84 # A flag governing the behavior of the input. Can be:
85 85 #
86 86 # 'readline' for readline-like behavior with a prompt
87 87 # and an edit buffer.
88 88 # 'raw_input' similar to readline, but triggered by a raw-input
89 89 # call. Can be used by subclasses to act differently.
90 90 # 'subprocess' for sending the raw input directly to a
91 91 # subprocess.
92 92 # 'buffering' for buffering of the input, that will be used
93 93 # when the input state switches back to another state.
94 94 _input_state = 'readline'
95 95
96 96 # Attribute to store reference to the pipes of a subprocess, if we
97 97 # are running any.
98 98 _running_process = False
99 99
100 100 # A queue for writing fast streams to the screen without flooding the
101 101 # event loop
102 102 _out_buffer = []
103 103
104 104 # A lock to lock the _out_buffer to make sure we don't empty it
105 105 # while it is being swapped
106 106 _out_buffer_lock = Lock()
107 107
108 108 # The different line markers used to higlight the prompts.
109 109 _markers = dict()
110 110
111 111 #--------------------------------------------------------------------------
112 112 # Public API
113 113 #--------------------------------------------------------------------------
114 114
115 115 def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
116 116 size=wx.DefaultSize,
117 117 style=wx.CLIP_CHILDREN|wx.WANTS_CHARS,
118 118 styledef=None,
119 119 *args, **kwds):
120 120 """ Create Shell instance.
121 121
122 122 Parameters
123 123 -----------
124 124 styledef : dict, optional
125 125 styledef is the dictionary of options used to define the
126 126 style.
127 127 """
128 128 if styledef is not None:
129 129 self.style = styledef
130 130 ConsoleWidget.__init__(self, parent, id, pos, size, style)
131 131 PrefilterFrontEnd.__init__(self, **kwds)
132 132
133 133 # Stick in our own raw_input:
134 134 self.ipython0.raw_input = self.raw_input
135 135
136 136 # A time for flushing the write buffer
137 137 BUFFER_FLUSH_TIMER_ID = 100
138 138 self._buffer_flush_timer = wx.Timer(self, BUFFER_FLUSH_TIMER_ID)
139 139 wx.EVT_TIMER(self, BUFFER_FLUSH_TIMER_ID, self._buffer_flush)
140 140
141 141 if 'debug' in kwds:
142 142 self.debug = kwds['debug']
143 143 kwds.pop('debug')
144 144
145 145 # Inject self in namespace, for debug
146 146 if self.debug:
147 147 self.shell.user_ns['self'] = self
148 148 # Inject our own raw_input in namespace
149 149 self.shell.user_ns['raw_input'] = self.raw_input
150 150
151 151 def raw_input(self, prompt=''):
152 152 """ A replacement from python's raw_input.
153 153 """
154 154 self.new_prompt(prompt)
155 155 self._input_state = 'raw_input'
156 156 if hasattr(self, '_cursor'):
157 157 del self._cursor
158 158 self.SetCursor(wx.StockCursor(wx.CURSOR_CROSS))
159 159 self.__old_on_enter = self._on_enter
160 160 event_loop = wx.EventLoop()
161 161 def my_on_enter():
162 162 event_loop.Exit()
163 163 self._on_enter = my_on_enter
164 164 # XXX: Running a separate event_loop. Ugly.
165 165 event_loop.Run()
166 166 self._on_enter = self.__old_on_enter
167 167 self._input_state = 'buffering'
168 168 self._cursor = wx.BusyCursor()
169 169 return self.input_buffer.rstrip('\n')
170 170
171 171
172 172 def system_call(self, command_string):
173 173 self._input_state = 'subprocess'
174 174 event_loop = wx.EventLoop()
175 175 def _end_system_call():
176 176 self._input_state = 'buffering'
177 177 self._running_process = False
178 178 event_loop.Exit()
179 179
180 180 self._running_process = PipedProcess(command_string,
181 181 out_callback=self.buffered_write,
182 182 end_callback = _end_system_call)
183 183 self._running_process.start()
184 184 # XXX: Running a separate event_loop. Ugly.
185 185 event_loop.Run()
186 186 # Be sure to flush the buffer.
187 187 self._buffer_flush(event=None)
188 188
189 189
190 190 def do_calltip(self):
191 191 """ Analyse current and displays useful calltip for it.
192 192 """
193 193 if self.debug:
194 194 print >>sys.__stdout__, "do_calltip"
195 195 separators = re.compile('[\s\{\}\[\]\(\)\= ,:]')
196 196 symbol = self.input_buffer
197 197 symbol_string = separators.split(symbol)[-1]
198 198 base_symbol_string = symbol_string.split('.')[0]
199 199 if base_symbol_string in self.shell.user_ns:
200 200 symbol = self.shell.user_ns[base_symbol_string]
201 201 elif base_symbol_string in self.shell.user_global_ns:
202 202 symbol = self.shell.user_global_ns[base_symbol_string]
203 203 elif base_symbol_string in __builtin__.__dict__:
204 204 symbol = __builtin__.__dict__[base_symbol_string]
205 205 else:
206 206 return False
207 207 try:
208 208 for name in symbol_string.split('.')[1:] + ['__doc__']:
209 209 symbol = getattr(symbol, name)
210 210 self.AutoCompCancel()
211 211 # Check that the symbol can indeed be converted to a string:
212 212 symbol += ''
213 213 wx.CallAfter(self.CallTipShow, self.GetCurrentPos(), symbol)
214 214 except:
215 215 # The retrieve symbol couldn't be converted to a string
216 216 pass
217 217
218 218
219 219 def _popup_completion(self, create=False):
220 220 """ Updates the popup completion menu if it exists. If create is
221 221 true, open the menu.
222 222 """
223 223 if self.debug:
224 224 print >>sys.__stdout__, "_popup_completion"
225 225 line = self.input_buffer
226 226 if (self.AutoCompActive() and line and not line[-1] == '.') \
227 227 or create==True:
228 228 suggestion, completions = self.complete(line)
229 229 if completions:
230 230 offset = len(self._get_completion_text(line))
231 231 self.pop_completion(completions, offset=offset)
232 232 if self.debug:
233 233 print >>sys.__stdout__, completions
234 234
235 235
236 236 def buffered_write(self, text):
237 237 """ A write method for streams, that caches the stream in order
238 238 to avoid flooding the event loop.
239 239
240 240 This can be called outside of the main loop, in separate
241 241 threads.
242 242 """
243 243 self._out_buffer_lock.acquire()
244 244 self._out_buffer.append(text)
245 245 self._out_buffer_lock.release()
246 246 if not self._buffer_flush_timer.IsRunning():
247 247 wx.CallAfter(self._buffer_flush_timer.Start,
248 248 milliseconds=100, oneShot=True)
249 249
250 250
251 251 def clear_screen(self):
252 252 """ Empty completely the widget.
253 253 """
254 254 self.ClearAll()
255 255 self.new_prompt(self.input_prompt_template.substitute(
256 256 number=(self.last_result['number'] + 1)))
257 257
258 258
259 259 #--------------------------------------------------------------------------
260 260 # LineFrontEnd interface
261 261 #--------------------------------------------------------------------------
262 262
263 263 def execute(self, python_string, raw_string=None):
264 264 self._input_state = 'buffering'
265 265 self.CallTipCancel()
266 266 self._cursor = wx.BusyCursor()
267 267 if raw_string is None:
268 268 raw_string = python_string
269 269 end_line = self.current_prompt_line \
270 270 + max(1, len(raw_string.split('\n'))-1)
271 271 for i in range(self.current_prompt_line, end_line):
272 272 if i in self._markers:
273 273 self.MarkerDeleteHandle(self._markers[i])
274 274 self._markers[i] = self.MarkerAdd(i, _COMPLETE_BUFFER_MARKER)
275 275 # Use a callafter to update the display robustly under windows
276 276 def callback():
277 277 self.GotoPos(self.GetLength())
278 278 PrefilterFrontEnd.execute(self, python_string,
279 279 raw_string=raw_string)
280 280 wx.CallAfter(callback)
281 281
282 282
283 283 def execute_command(self, command, hidden=False):
284 284 """ Execute a command, not only in the model, but also in the
285 285 view.
286 286 """
287 287 # XXX: This method needs to be integrated in the base fronted
288 288 # interface
289 289 if hidden:
290 290 return self.shell.execute(command)
291 291 else:
292 292 # XXX: we are not storing the input buffer previous to the
293 293 # execution, as this forces us to run the execution
294 294 # input_buffer a yield, which is not good.
295 295 ##current_buffer = self.shell.control.input_buffer
296 296 command = command.rstrip()
297 297 if len(command.split('\n')) > 1:
298 298 # The input command is several lines long, we need to
299 299 # force the execution to happen
300 300 command += '\n'
301 301 cleaned_command = self.prefilter_input(command)
302 302 self.input_buffer = command
303 303 # Do not use wx.Yield() (aka GUI.process_events()) to avoid
304 304 # recursive yields.
305 305 self.ProcessEvent(wx.PaintEvent())
306 306 self.write('\n')
307 307 if not self.is_complete(cleaned_command + '\n'):
308 308 self._colorize_input_buffer()
309 309 self.render_error('Incomplete or invalid input')
310 310 self.new_prompt(self.input_prompt_template.substitute(
311 311 number=(self.last_result['number'] + 1)))
312 312 return False
313 313 self._on_enter()
314 314 return True
315 315
316 316
317 317 def save_output_hooks(self):
318 318 self.__old_raw_input = __builtin__.raw_input
319 319 PrefilterFrontEnd.save_output_hooks(self)
320 320
321 321 def capture_output(self):
322 322 self.SetLexer(stc.STC_LEX_NULL)
323 323 PrefilterFrontEnd.capture_output(self)
324 324 __builtin__.raw_input = self.raw_input
325 325
326 326
327 327 def release_output(self):
328 328 __builtin__.raw_input = self.__old_raw_input
329 329 PrefilterFrontEnd.release_output(self)
330 330 self.SetLexer(stc.STC_LEX_PYTHON)
331 331
332 332
333 333 def after_execute(self):
334 334 PrefilterFrontEnd.after_execute(self)
335 335 # Clear the wait cursor
336 336 if hasattr(self, '_cursor'):
337 337 del self._cursor
338 338 self.SetCursor(wx.StockCursor(wx.CURSOR_CHAR))
339 339
340 340
341 341 def show_traceback(self):
342 342 start_line = self.GetCurrentLine()
343 343 PrefilterFrontEnd.show_traceback(self)
344 344 self.ProcessEvent(wx.PaintEvent())
345 345 #wx.Yield()
346 346 for i in range(start_line, self.GetCurrentLine()):
347 347 self._markers[i] = self.MarkerAdd(i, _ERROR_MARKER)
348 348
349 349
350 350 #--------------------------------------------------------------------------
351 351 # FrontEndBase interface
352 352 #--------------------------------------------------------------------------
353 353
354 354 def render_error(self, e):
355 355 start_line = self.GetCurrentLine()
356 356 self.write('\n' + e + '\n')
357 357 for i in range(start_line, self.GetCurrentLine()):
358 358 self._markers[i] = self.MarkerAdd(i, _ERROR_MARKER)
359 359
360 360
361 361 #--------------------------------------------------------------------------
362 362 # ConsoleWidget interface
363 363 #--------------------------------------------------------------------------
364 364
365 365 def new_prompt(self, prompt):
366 366 """ Display a new prompt, and start a new input buffer.
367 367 """
368 368 self._input_state = 'readline'
369 369 ConsoleWidget.new_prompt(self, prompt)
370 370 i = self.current_prompt_line
371 371 self._markers[i] = self.MarkerAdd(i, _INPUT_MARKER)
372 372
373 373
374 374 def continuation_prompt(self, *args, **kwargs):
375 375 # Avoid multiple inheritence, be explicit about which
376 376 # parent method class gets called
377 377 return ConsoleWidget.continuation_prompt(self, *args, **kwargs)
378 378
379 379
380 380 def write(self, *args, **kwargs):
381 381 # Avoid multiple inheritence, be explicit about which
382 382 # parent method class gets called
383 383 return ConsoleWidget.write(self, *args, **kwargs)
384 384
385 385
386 386 def _on_key_down(self, event, skip=True):
387 387 """ Capture the character events, let the parent
388 388 widget handle them, and put our logic afterward.
389 389 """
390 390 # FIXME: This method needs to be broken down in smaller ones.
391 391 current_line_num = self.GetCurrentLine()
392 392 key_code = event.GetKeyCode()
393 393 if key_code in (ord('c'), ord('C')) and event.ControlDown():
394 394 # Capture Control-C
395 395 if self._input_state == 'subprocess':
396 396 if self.debug:
397 397 print >>sys.__stderr__, 'Killing running process'
398 398 if hasattr(self._running_process, 'process'):
399 399 self._running_process.process.kill()
400 400 elif self._input_state == 'buffering':
401 401 if self.debug:
402 402 print >>sys.__stderr__, 'Raising KeyboardInterrupt'
403 403 raise KeyboardInterrupt
404 404 # XXX: We need to make really sure we
405 405 # get back to a prompt.
406 406 elif self._input_state == 'subprocess' and (
407 407 ( key_code <256 and not event.ControlDown() )
408 408 or
409 409 ( key_code in (ord('d'), ord('D')) and
410 410 event.ControlDown())):
411 411 # We are running a process, we redirect keys.
412 412 ConsoleWidget._on_key_down(self, event, skip=skip)
413 413 char = chr(key_code)
414 414 # Deal with some inconsistency in wx keycodes:
415 415 if char == '\r':
416 416 char = '\n'
417 417 elif not event.ShiftDown():
418 418 char = char.lower()
419 419 if event.ControlDown() and key_code in (ord('d'), ord('D')):
420 420 char = '\04'
421 421 self._running_process.process.stdin.write(char)
422 422 self._running_process.process.stdin.flush()
423 423 elif key_code in (ord('('), 57, 53):
424 424 # Calltips
425 425 event.Skip()
426 426 self.do_calltip()
427 427 elif self.AutoCompActive() and not key_code == ord('\t'):
428 428 event.Skip()
429 429 if key_code in (wx.WXK_BACK, wx.WXK_DELETE):
430 430 wx.CallAfter(self._popup_completion, create=True)
431 431 elif not key_code in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT,
432 432 wx.WXK_RIGHT, wx.WXK_ESCAPE):
433 433 wx.CallAfter(self._popup_completion)
434 434 else:
435 435 # Up history
436 436 if key_code == wx.WXK_UP and (
437 437 event.ControlDown() or
438 438 current_line_num == self.current_prompt_line
439 439 ):
440 440 new_buffer = self.get_history_previous(
441 441 self.input_buffer)
442 442 if new_buffer is not None:
443 443 self.input_buffer = new_buffer
444 444 if self.GetCurrentLine() > self.current_prompt_line:
445 445 # Go to first line, for seemless history up.
446 446 self.GotoPos(self.current_prompt_pos)
447 447 # Down history
448 elif event.KeyCode == wx.WXK_DOWN and (
448 elif key_code == wx.WXK_DOWN and (
449 449 event.ControlDown() or
450 450 current_line_num == self.LineCount -1
451 451 ):
452 452 new_buffer = self.get_history_next()
453 453 if new_buffer is not None:
454 454 self.input_buffer = new_buffer
455 455 # Tab-completion
456 elif event.KeyCode == ord('\t'):
456 elif key_code == ord('\t'):
457 457 current_line, current_line_num = self.CurLine
458 458 if not re.match(r'^%s\s*$' % self.continuation_prompt(),
459 459 current_line):
460 460 self.complete_current_input()
461 461 if self.AutoCompActive():
462 462 wx.CallAfter(self._popup_completion, create=True)
463 463 else:
464 464 event.Skip()
465 elif event.KeyCode == wx.WXK_BACK:
465 elif key_code == wx.WXK_BACK:
466 466 # If characters where erased, check if we have to
467 467 # remove a line.
468 468 # XXX: What about DEL?
469 469 # FIXME: This logics should be in ConsoleWidget, as it is
470 470 # independant of IPython
471 471 current_line, _ = self.CurLine
472 472 current_pos = self.GetCurrentPos()
473 473 current_line_num = self.LineFromPosition(current_pos)
474 474 current_col = self.GetColumn(current_pos)
475 475 len_prompt = len(self.continuation_prompt())
476 476 if ( current_line.startswith(self.continuation_prompt())
477 477 and current_col == len_prompt):
478 478 new_lines = []
479 479 for line_num, line in enumerate(
480 480 self.input_buffer.split('\n')):
481 481 if (line_num + self.current_prompt_line ==
482 482 current_line_num):
483 483 new_lines.append(line[len_prompt:])
484 484 else:
485 485 new_lines.append('\n'+line)
486 486 # The first character is '\n', due to the above
487 487 # code:
488 488 self.input_buffer = ''.join(new_lines)[1:]
489 489 self.GotoPos(current_pos - 1 - len_prompt)
490 490 else:
491 491 ConsoleWidget._on_key_down(self, event, skip=skip)
492 492 else:
493 493 ConsoleWidget._on_key_down(self, event, skip=skip)
494 494
495 495
496 496
497 497 def _on_key_up(self, event, skip=True):
498 498 """ Called when any key is released.
499 499 """
500 if event.KeyCode in (59, ord('.')):
500 if event.GetKeyCode() in (59, ord('.')):
501 501 # Intercepting '.'
502 502 event.Skip()
503 503 wx.CallAfter(self._popup_completion, create=True)
504 504 else:
505 505 ConsoleWidget._on_key_up(self, event, skip=skip)
506 506 # Make sure the continuation_prompts are always followed by a
507 507 # whitespace
508 508 new_lines = []
509 509 if self._input_state == 'readline':
510 510 position = self.GetCurrentPos()
511 511 continuation_prompt = self.continuation_prompt()[:-1]
512 512 for line in self.input_buffer.split('\n'):
513 513 if not line == continuation_prompt:
514 514 new_lines.append(line)
515 515 self.input_buffer = '\n'.join(new_lines)
516 516 self.GotoPos(position)
517 517
518 518
519 519 def _on_enter(self):
520 520 """ Called on return key down, in readline input_state.
521 521 """
522 522 last_line_num = self.LineFromPosition(self.GetLength())
523 523 current_line_num = self.LineFromPosition(self.GetCurrentPos())
524 524 new_line_pos = (last_line_num - current_line_num)
525 525 if self.debug:
526 526 print >>sys.__stdout__, repr(self.input_buffer)
527 527 self.write('\n', refresh=False)
528 528 # Under windows scintilla seems to be doing funny
529 529 # stuff to the line returns here, but the getter for
530 530 # input_buffer filters this out.
531 531 if sys.platform == 'win32':
532 532 self.input_buffer = self.input_buffer
533 533 old_prompt_num = self.current_prompt_pos
534 534 has_executed = PrefilterFrontEnd._on_enter(self,
535 535 new_line_pos=new_line_pos)
536 536 if old_prompt_num == self.current_prompt_pos:
537 537 # No execution has happened
538 538 self.GotoPos(self.GetLineEndPosition(current_line_num + 1))
539 539 return has_executed
540 540
541 541
542 542 #--------------------------------------------------------------------------
543 543 # EditWindow API
544 544 #--------------------------------------------------------------------------
545 545
546 546 def OnUpdateUI(self, event):
547 547 """ Override the OnUpdateUI of the EditWindow class, to prevent
548 548 syntax highlighting both for faster redraw, and for more
549 549 consistent look and feel.
550 550 """
551 551 if not self._input_state == 'readline':
552 552 ConsoleWidget.OnUpdateUI(self, event)
553 553
554 554 #--------------------------------------------------------------------------
555 555 # Private API
556 556 #--------------------------------------------------------------------------
557 557
558 558 def _buffer_flush(self, event):
559 559 """ Called by the timer to flush the write buffer.
560 560
561 561 This is always called in the mainloop, by the wx timer.
562 562 """
563 563 self._out_buffer_lock.acquire()
564 564 _out_buffer = self._out_buffer
565 565 self._out_buffer = []
566 566 self._out_buffer_lock.release()
567 567 self.write(''.join(_out_buffer), refresh=False)
568 568
569 569
570 570 def _colorize_input_buffer(self):
571 571 """ Keep the input buffer lines at a bright color.
572 572 """
573 573 if not self._input_state in ('readline', 'raw_input'):
574 574 return
575 575 end_line = self.GetCurrentLine()
576 576 if not sys.platform == 'win32':
577 577 end_line += 1
578 578 for i in range(self.current_prompt_line, end_line):
579 579 if i in self._markers:
580 580 self.MarkerDeleteHandle(self._markers[i])
581 581 self._markers[i] = self.MarkerAdd(i, _INPUT_MARKER)
582 582
583 583
584 584 if __name__ == '__main__':
585 585 class MainWindow(wx.Frame):
586 586 def __init__(self, parent, id, title):
587 587 wx.Frame.__init__(self, parent, id, title, size=(300,250))
588 588 self._sizer = wx.BoxSizer(wx.VERTICAL)
589 589 self.shell = WxController(self)
590 590 self._sizer.Add(self.shell, 1, wx.EXPAND)
591 591 self.SetSizer(self._sizer)
592 592 self.SetAutoLayout(1)
593 593 self.Show(True)
594 594
595 595 app = wx.PySimpleApp()
596 596 frame = MainWindow(None, wx.ID_ANY, 'Ipython')
597 597 frame.shell.SetFocus()
598 598 frame.SetSize((680, 460))
599 599 self = frame.shell
600 600
601 601 app.MainLoop()
602 602
General Comments 0
You need to be logged in to leave comments. Login now