##// END OF EJS Templates
Corrected behaviour of raw_input under windows. Recursive wx.yield is ...
laurent.dufrechou@gmail.com -
Show More
This diff has been collapsed as it changes many lines, (1884 lines changed) Show them Hide them
@@ -1,940 +1,944 b''
1 #!/usr/bin/python
2 # -*- coding: iso-8859-15 -*-
3 '''
4 Provides IPython WX console widgets.
5
6 @author: Laurent Dufrechou
7 laurent.dufrechou _at_ gmail.com
8 This WX widget is based on the original work of Eitan Isaacson
9 that provided the console for the GTK toolkit.
10
11 Original work from:
12 @author: Eitan Isaacson
13 @organization: IBM Corporation
14 @copyright: Copyright (c) 2007 IBM Corporation
15 @license: BSD
16
17 All rights reserved. This program and the accompanying materials are made
18 available under the terms of the BSD which accompanies this distribution, and
19 is available at U{http://www.opensource.org/licenses/bsd-license.php}
20 '''
21
22 __version__ = 0.9
23 __author__ = "Laurent Dufrechou"
24 __email__ = "laurent.dufrechou _at_ gmail.com"
25 __license__ = "BSD"
26
27 import wx
28 import wx.stc as stc
29
30 import re
31 from StringIO import StringIO
32
33 import sys
34 import codecs
35 import locale
36 import time
37
38 for enc in (locale.getpreferredencoding(),
39 sys.getfilesystemencoding(),
40 sys.getdefaultencoding()):
41 try:
42 codecs.lookup(enc)
43 ENCODING = enc
44 break
45 except LookupError:
46 pass
47 else:
48 ENCODING = 'utf-8'
49
50 from ipshell_nonblocking import NonBlockingIPShell
51
52 class WxNonBlockingIPShell(NonBlockingIPShell):
53 '''
54 An NonBlockingIPShell Thread that is WX dependent.
55 '''
56 def __init__(self, parent,
57 argv=[],user_ns={},user_global_ns=None,
58 cin=None, cout=None, cerr=None,
59 ask_exit_handler=None):
60
61 NonBlockingIPShell.__init__(self, argv, user_ns, user_global_ns,
62 cin, cout, cerr,
63 ask_exit_handler)
64
65 self.parent = parent
66
67 self.ask_exit_callback = ask_exit_handler
68 self._IP.exit = self._ask_exit
69
70 def addGUIShortcut(self, text, func):
71 wx.CallAfter(self.parent.add_button_handler,
72 button_info={ 'text':text,
73 'func':self.parent.doExecuteLine(func)})
74
75 def _raw_input(self, prompt=''):
76 """ A replacement from python's raw_input.
77 """
78 self.answer = None
79 wx.CallAfter(self._yesNoBox, prompt)
80 while self.answer is None:
81 wx.Yield()
82 time.sleep(.1)
83 return self.answer
84
85 def _yesNoBox(self, prompt):
86 """ yes/no box managed with wx.CallAfter jsut in case caler is executed in a thread"""
87 dlg = wx.TextEntryDialog(
88 self.parent, prompt,
89 'Input requested', 'Python')
90 dlg.SetValue("")
91
92 answer = ''
93 if dlg.ShowModal() == wx.ID_OK:
94 answer = dlg.GetValue()
95
96 dlg.Destroy()
97 self.answer = answer
98
99 def _ask_exit(self):
100 wx.CallAfter(self.ask_exit_callback, ())
101
102 def _after_execute(self):
103 wx.CallAfter(self.parent.evtStateExecuteDone, ())
104
105
106 class WxConsoleView(stc.StyledTextCtrl):
107 '''
108 Specialized styled text control view for console-like workflow.
109 We use here a scintilla frontend thus it can be reused in any GUI that
110 supports scintilla with less work.
111
112 @cvar ANSI_COLORS_BLACK: Mapping of terminal colors to X11 names.
113 (with Black background)
114 @type ANSI_COLORS_BLACK: dictionary
115
116 @cvar ANSI_COLORS_WHITE: Mapping of terminal colors to X11 names.
117 (with White background)
118 @type ANSI_COLORS_WHITE: dictionary
119
120 @ivar color_pat: Regex of terminal color pattern
121 @type color_pat: _sre.SRE_Pattern
122 '''
123 ANSI_STYLES_BLACK = {'0;30': [0, 'WHITE'], '0;31': [1, 'RED'],
124 '0;32': [2, 'GREEN'], '0;33': [3, 'BROWN'],
125 '0;34': [4, 'BLUE'], '0;35': [5, 'PURPLE'],
126 '0;36': [6, 'CYAN'], '0;37': [7, 'LIGHT GREY'],
127 '1;30': [8, 'DARK GREY'], '1;31': [9, 'RED'],
128 '1;32': [10, 'SEA GREEN'], '1;33': [11, 'YELLOW'],
129 '1;34': [12, 'LIGHT BLUE'], '1;35':
130 [13, 'MEDIUM VIOLET RED'],
131 '1;36': [14, 'LIGHT STEEL BLUE'], '1;37': [15, 'YELLOW']}
132
133 ANSI_STYLES_WHITE = {'0;30': [0, 'BLACK'], '0;31': [1, 'RED'],
134 '0;32': [2, 'GREEN'], '0;33': [3, 'BROWN'],
135 '0;34': [4, 'BLUE'], '0;35': [5, 'PURPLE'],
136 '0;36': [6, 'CYAN'], '0;37': [7, 'LIGHT GREY'],
137 '1;30': [8, 'DARK GREY'], '1;31': [9, 'RED'],
138 '1;32': [10, 'SEA GREEN'], '1;33': [11, 'YELLOW'],
139 '1;34': [12, 'LIGHT BLUE'], '1;35':
140 [13, 'MEDIUM VIOLET RED'],
141 '1;36': [14, 'LIGHT STEEL BLUE'], '1;37': [15, 'YELLOW']}
142
143 def __init__(self, parent, prompt, intro="", background_color="BLACK",
144 pos=wx.DefaultPosition, ID = -1, size=wx.DefaultSize,
145 style=0, autocomplete_mode = 'IPYTHON'):
146 '''
147 Initialize console view.
148
149 @param parent: Parent widget
150 @param prompt: User specified prompt
151 @type intro: string
152 @param intro: User specified startup introduction string
153 @type intro: string
154 @param background_color: Can be BLACK or WHITE
155 @type background_color: string
156 @param other: init param of styledTextControl (can be used as-is)
157 @param autocomplete_mode: Can be 'IPYTHON' or 'STC'
158 'IPYTHON' show autocompletion the ipython way
159 'STC" show it scintilla text control way
160 '''
161 stc.StyledTextCtrl.__init__(self, parent, ID, pos, size, style)
162
163 ####### Scintilla configuration ###################################
164
165 # Ctrl + B or Ctrl + N can be used to zoomin/zoomout the text inside
166 # the widget
167 self.CmdKeyAssign(ord('B'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMIN)
168 self.CmdKeyAssign(ord('N'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMOUT)
169
170 #We draw a line at position 80
171 self.SetEdgeMode(stc.STC_EDGE_LINE)
172 self.SetEdgeColumn(80)
173 self.SetEdgeColour(wx.LIGHT_GREY)
174
175 #self.SetViewWhiteSpace(True)
176 #self.SetViewEOL(True)
177 self.SetEOLMode(stc.STC_EOL_CRLF)
178 #self.SetWrapMode(stc.STC_WRAP_CHAR)
179 #self.SetWrapMode(stc.STC_WRAP_WORD)
180 self.SetBufferedDraw(True)
181 #self.SetUseAntiAliasing(True)
182 self.SetLayoutCache(stc.STC_CACHE_PAGE)
183 self.SetUndoCollection(False)
184 self.SetUseTabs(True)
185 self.SetIndent(4)
186 self.SetTabWidth(4)
187
188 self.EnsureCaretVisible()
189
190 self.SetMargins(3, 3) #text is moved away from border with 3px
191 # Suppressing Scintilla margins
192 self.SetMarginWidth(0, 0)
193 self.SetMarginWidth(1, 0)
194 self.SetMarginWidth(2, 0)
195
196 self.background_color = background_color
197 self.buildStyles()
198
199 self.indent = 0
200 self.prompt_count = 0
201 self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?')
202
203 self.write(intro)
204 self.setPrompt(prompt)
205 self.showPrompt()
206
207 self.autocomplete_mode = autocomplete_mode
208
209 self.Bind(wx.EVT_KEY_DOWN, self._onKeypress)
210
211 def buildStyles(self):
212 #we define platform specific fonts
213 if wx.Platform == '__WXMSW__':
214 faces = { 'times': 'Times New Roman',
215 'mono' : 'Courier New',
216 'helv' : 'Arial',
217 'other': 'Comic Sans MS',
218 'size' : 10,
219 'size2': 8,
220 }
221 elif wx.Platform == '__WXMAC__':
222 faces = { 'times': 'Times New Roman',
223 'mono' : 'Monaco',
224 'helv' : 'Arial',
225 'other': 'Comic Sans MS',
226 'size' : 10,
227 'size2': 8,
228 }
229 else:
230 faces = { 'times': 'Times',
231 'mono' : 'Courier',
232 'helv' : 'Helvetica',
233 'other': 'new century schoolbook',
234 'size' : 10,
235 'size2': 8,
236 }
237
238 # make some styles
239 if self.background_color != "BLACK":
240 self.background_color = "WHITE"
241 self.SetCaretForeground("BLACK")
242 self.ANSI_STYLES = self.ANSI_STYLES_WHITE
243 else:
244 self.SetCaretForeground("WHITE")
245 self.ANSI_STYLES = self.ANSI_STYLES_BLACK
246
247 self.StyleSetSpec(stc.STC_STYLE_DEFAULT,
248 "fore:%s,back:%s,size:%d,face:%s"
249 % (self.ANSI_STYLES['0;30'][1],
250 self.background_color,
251 faces['size'], faces['mono']))
252 self.StyleClearAll()
253 self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT,
254 "fore:#FF0000,back:#0000FF,bold")
255 self.StyleSetSpec(stc.STC_STYLE_BRACEBAD,
256 "fore:#000000,back:#FF0000,bold")
257
258 for style in self.ANSI_STYLES.values():
259 self.StyleSetSpec(style[0], "bold,fore:%s" % style[1])
260
261 #######################################################################
262
263 def setBackgroundColor(self, color):
264 self.background_color = color
265 self.buildStyles()
266
267 def getBackgroundColor(self, color):
268 return self.background_color
269
270 def asyncWrite(self, text):
271 '''
272 Write given text to buffer in an asynchroneous way.
273 It is used from another thread to be able to acces the GUI.
274 @param text: Text to append
275 @type text: string
276 '''
277 try:
278 wx.MutexGuiEnter()
279
280 #be sure not to be interrutpted before the MutexGuiLeave!
281 self.write(text)
282
283 except KeyboardInterrupt:
284 wx.MutexGuiLeave()
285 raise KeyboardInterrupt
286 wx.MutexGuiLeave()
287
288
289 def write(self, text):
290 '''
291 Write given text to buffer.
292
293 @param text: Text to append.
294 @type text: string
295 '''
296 segments = self.color_pat.split(text)
297 segment = segments.pop(0)
298 self.StartStyling(self.getCurrentLineEnd(), 0xFF)
299 self.AppendText(segment)
300
301 if segments:
302 ansi_tags = self.color_pat.findall(text)
303
304 for tag in ansi_tags:
305 i = segments.index(tag)
306 self.StartStyling(self.getCurrentLineEnd(), 0xFF)
307 self.AppendText(segments[i+1])
308
309 if tag != '0':
310 self.SetStyling(len(segments[i+1]), self.ANSI_STYLES[tag][0])
311
312 segments.pop(i)
313
314 self.moveCursor(self.getCurrentLineEnd())
315
316 def getPromptLen(self):
317 '''
318 Return the length of current prompt
319 '''
320 return len(str(self.prompt_count)) + 7
321
322 def setPrompt(self, prompt):
323 self.prompt = prompt
324
325 def setIndentation(self, indentation):
326 self.indent = indentation
327
328 def setPromptCount(self, count):
329 self.prompt_count = count
330
331 def showPrompt(self):
332 '''
333 Prints prompt at start of line.
334
335 @param prompt: Prompt to print.
336 @type prompt: string
337 '''
338 self.write(self.prompt)
339 #now we update the position of end of prompt
340 self.current_start = self.getCurrentLineEnd()
341
342 autoindent = self.indent*' '
343 autoindent = autoindent.replace(' ','\t')
344 self.write(autoindent)
345
346 def changeLine(self, text):
347 '''
348 Replace currently entered command line with given text.
349
350 @param text: Text to use as replacement.
351 @type text: string
352 '''
353 self.SetSelection(self.getCurrentPromptStart(), self.getCurrentLineEnd())
354 self.ReplaceSelection(text)
355 self.moveCursor(self.getCurrentLineEnd())
356
357 def getCurrentPromptStart(self):
358 return self.current_start
359
360 def getCurrentLineStart(self):
361 return self.GotoLine(self.LineFromPosition(self.GetCurrentPos()))
362
363 def getCurrentLineEnd(self):
364 return self.GetLength()
365
366 def getCurrentLine(self):
367 '''
368 Get text in current command line.
369
370 @return: Text of current command line.
371 @rtype: string
372 '''
373 return self.GetTextRange(self.getCurrentPromptStart(),
374 self.getCurrentLineEnd())
375
376 def moveCursorOnNewValidKey(self):
377 #If cursor is at wrong position put it at last line...
378 if self.GetCurrentPos() < self.getCurrentPromptStart():
379 self.GotoPos(self.getCurrentPromptStart())
380
381 def removeFromTo(self, from_pos, to_pos):
382 if from_pos < to_pos:
383 self.SetSelection(from_pos, to_pos)
384 self.DeleteBack()
385
386 def removeCurrentLine(self):
387 self.LineDelete()
388
389 def moveCursor(self, position):
390 self.GotoPos(position)
391
392 def getCursorPos(self):
393 return self.GetCurrentPos()
394
395 def selectFromTo(self, from_pos, to_pos):
396 self.SetSelectionStart(from_pos)
397 self.SetSelectionEnd(to_pos)
398
399 def writeHistory(self, history):
400 self.removeFromTo(self.getCurrentPromptStart(), self.getCurrentLineEnd())
401 self.changeLine(history)
402
403 def setCompletionMethod(self, completion):
404 if completion in ['IPYTHON', 'STC']:
405 self.autocomplete_mode = completion
406 else:
407 raise AttributeError
408
409 def getCompletionMethod(self, completion):
410 return self.autocomplete_mode
411
412 def writeCompletion(self, possibilities):
413 if self.autocomplete_mode == 'IPYTHON':
414 max_len = len(max(possibilities, key=len))
415 max_symbol = ' '*max_len
416
417 #now we check how much symbol we can put on a line...
418 test_buffer = max_symbol + ' '*4
419
420 allowed_symbols = 80/len(test_buffer)
421 if allowed_symbols == 0:
422 allowed_symbols = 1
423
424 pos = 1
425 buf = ''
426 for symbol in possibilities:
427 #buf += symbol+'\n'#*spaces)
428 if pos < allowed_symbols:
429 spaces = max_len - len(symbol) + 4
430 buf += symbol+' '*spaces
431 pos += 1
432 else:
433 buf += symbol+'\n'
434 pos = 1
435 self.write(buf)
436 else:
437 possibilities.sort() # Python sorts are case sensitive
438 self.AutoCompSetIgnoreCase(False)
439 self.AutoCompSetAutoHide(False)
440 #let compute the length ot last word
441 splitter = [' ', '(', '[', '{','=']
442 last_word = self.getCurrentLine()
443 for breaker in splitter:
444 last_word = last_word.split(breaker)[-1]
445 self.AutoCompShow(len(last_word), " ".join(possibilities))
446
447 def _onKeypress(self, event, skip=True):
448 '''
449 Key press callback used for correcting behavior for console-like
450 interfaces. For example 'home' should go to prompt, not to begining of
451 line.
452
453 @param widget: Widget that key press accored in.
454 @type widget: gtk.Widget
455 @param event: Event object
456 @type event: gtk.gdk.Event
457
458 @return: Return True if event as been catched.
459 @rtype: boolean
460 '''
461 if not self.AutoCompActive():
462 if event.GetKeyCode() == wx.WXK_HOME:
463 if event.Modifiers == wx.MOD_NONE:
464 self.moveCursorOnNewValidKey()
465 self.moveCursor(self.getCurrentPromptStart())
466 return True
467 elif event.Modifiers == wx.MOD_SHIFT:
468 self.moveCursorOnNewValidKey()
469 self.selectFromTo(self.getCurrentPromptStart(), self.getCursorPos())
470 return True
471 else:
472 return False
473
474 elif event.GetKeyCode() == wx.WXK_LEFT:
475 if event.Modifiers == wx.MOD_NONE:
476 self.moveCursorOnNewValidKey()
477
478 self.moveCursor(self.getCursorPos()-1)
479 if self.getCursorPos() < self.getCurrentPromptStart():
480 self.moveCursor(self.getCurrentPromptStart())
481 return True
482
483 elif event.GetKeyCode() == wx.WXK_BACK:
484 self.moveCursorOnNewValidKey()
485 if self.getCursorPos() > self.getCurrentPromptStart():
486 event.Skip()
487 return True
488
489 if skip:
490 if event.GetKeyCode() not in [wx.WXK_PAGEUP, wx.WXK_PAGEDOWN]\
491 and event.Modifiers == wx.MOD_NONE:
492 self.moveCursorOnNewValidKey()
493
494 event.Skip()
495 return True
496 return False
497 else:
498 event.Skip()
499
500 def OnUpdateUI(self, evt):
501 # check for matching braces
502 braceAtCaret = -1
503 braceOpposite = -1
504 charBefore = None
505 caretPos = self.GetCurrentPos()
506
507 if caretPos > 0:
508 charBefore = self.GetCharAt(caretPos - 1)
509 styleBefore = self.GetStyleAt(caretPos - 1)
510
511 # check before
512 if charBefore and chr(charBefore) in "[]{}()" and styleBefore == stc.STC_P_OPERATOR:
513 braceAtCaret = caretPos - 1
514
515 # check after
516 if braceAtCaret < 0:
517 charAfter = self.GetCharAt(caretPos)
518 styleAfter = self.GetStyleAt(caretPos)
519
520 if charAfter and chr(charAfter) in "[]{}()" and styleAfter == stc.STC_P_OPERATOR:
521 braceAtCaret = caretPos
522
523 if braceAtCaret >= 0:
524 braceOpposite = self.BraceMatch(braceAtCaret)
525
526 if braceAtCaret != -1 and braceOpposite == -1:
527 self.BraceBadLight(braceAtCaret)
528 else:
529 self.BraceHighlight(braceAtCaret, braceOpposite)
530 #pt = self.PointFromPosition(braceOpposite)
531 #self.Refresh(True, wxRect(pt.x, pt.y, 5,5))
532 #print pt
533 #self.Refresh(False)
534
535 class IPShellWidget(wx.Panel):
536 '''
537 This is wx.Panel that embbed the IPython Thread and the wx.StyledTextControl
538 If you want to port this to any other GUI toolkit, just replace the
539 WxConsoleView by YOURGUIConsoleView and make YOURGUIIPythonView derivate
540 from whatever container you want. I've choosed to derivate from a wx.Panel
541 because it seems to be more useful
542 Any idea to make it more 'generic' welcomed.
543 '''
544
545 def __init__(self, parent, intro=None,
546 background_color="BLACK", add_button_handler=None,
547 wx_ip_shell=None, user_ns={},user_global_ns=None,
548 ):
549 '''
550 Initialize.
551 Instanciate an IPython thread.
552 Instanciate a WxConsoleView.
553 Redirect I/O to console.
554 '''
555 wx.Panel.__init__(self,parent,wx.ID_ANY)
556
557 self.parent = parent
558 ### IPython non blocking shell instanciation ###
559 self.cout = StringIO()
560 self.add_button_handler = add_button_handler
561
562 if wx_ip_shell is not None:
563 self.IP = wx_ip_shell
564 else:
565 self.IP = WxNonBlockingIPShell(self,
566 cout = self.cout, cerr = self.cout,
567 ask_exit_handler = self.askExitCallback)
568
569 ### IPython wx console view instanciation ###
570 #If user didn't defined an intro text, we create one for him
571 #If you really wnat an empty intro just call wxIPythonViewPanel
572 #with intro=''
573 if intro is None:
574 welcome_text = "Welcome to WxIPython Shell.\n\n"
575 welcome_text+= self.IP.get_banner()
576 welcome_text+= "!command -> Execute command in shell\n"
577 welcome_text+= "TAB -> Autocompletion\n"
578 else:
579 welcome_text = intro
580
581 self.text_ctrl = WxConsoleView(self,
582 self.IP.get_prompt(),
583 intro=welcome_text,
584 background_color=background_color)
585
586 option_text = wx.StaticText(self, -1, "Options:")
587 self.completion_option = wx.CheckBox(self, -1, "Scintilla Completion")
588 self.completion_option.SetToolTip(wx.ToolTip(
589 "Selects the completion type:\nEither Ipython default style or Scintilla one"))
590 #self.completion_option.SetValue(False)
591 self.background_option = wx.CheckBox(self, -1, "White Background")
592 self.background_option.SetToolTip(wx.ToolTip(
593 "Selects the back ground color: BLACK or WHITE"))
594 #self.background_option.SetValue(False)
595 self.threading_option = wx.CheckBox(self, -1, "Execute in thread")
596 self.threading_option.SetToolTip(wx.ToolTip(
597 "Use threading: infinite loop don't freeze the GUI and commands can be breaked\nNo threading: maximum compatibility"))
598 #self.threading_option.SetValue(False)
599
600 self.options={'completion':{'value':'IPYTHON',
601 'checkbox':self.completion_option,'STC':True,'IPYTHON':False,
602 'setfunc':self.text_ctrl.setCompletionMethod},
603 'background_color':{'value':'BLACK',
604 'checkbox':self.background_option,'WHITE':True,'BLACK':False,
605 'setfunc':self.text_ctrl.setBackgroundColor},
606 'threading':{'value':'True',
607 'checkbox':self.threading_option,'True':True,'False':False,
608 'setfunc':self.IP.set_threading},
609 }
610
611 #self.cout.write dEfault option is asynchroneous because default sate is threading ON
612 self.cout.write = self.text_ctrl.asyncWrite
613 #we reloard options
614 self.reloadOptions(self.options)
615
616 self.text_ctrl.Bind(wx.EVT_KEY_DOWN, self.keyPress)
617 self.completion_option.Bind(wx.EVT_CHECKBOX, self.evtCheckOptionCompletion)
618 self.background_option.Bind(wx.EVT_CHECKBOX, self.evtCheckOptionBackgroundColor)
619 self.threading_option.Bind(wx.EVT_CHECKBOX, self.evtCheckOptionThreading)
620
621 ### making the layout of the panel ###
622 sizer = wx.BoxSizer(wx.VERTICAL)
623 sizer.Add(self.text_ctrl, 1, wx.EXPAND)
624 option_sizer = wx.BoxSizer(wx.HORIZONTAL)
625 sizer.Add(option_sizer, 0)
626 option_sizer.AddMany([(10, 20),
627 (option_text, 0, wx.ALIGN_CENTER_VERTICAL),
628 (5, 5),
629 (self.completion_option, 0, wx.ALIGN_CENTER_VERTICAL),
630 (8, 8),
631 (self.background_option, 0, wx.ALIGN_CENTER_VERTICAL),
632 (8, 8),
633 (self.threading_option, 0, wx.ALIGN_CENTER_VERTICAL)
634 ])
635 self.SetAutoLayout(True)
636 sizer.Fit(self)
637 sizer.SetSizeHints(self)
638 self.SetSizer(sizer)
639 #and we focus on the widget :)
640 self.SetFocus()
641
642 #widget state management (for key handling different cases)
643 self.setCurrentState('IDLE')
644 self.pager_state = 'DONE'
645 self.raw_input_current_line = 0
646
647 def askExitCallback(self, event):
648 self.askExitHandler(event)
649
650 #---------------------- IPython Thread Management ------------------------
651 def stateDoExecuteLine(self):
652 lines=self.text_ctrl.getCurrentLine()
653 self.text_ctrl.write('\n')
654 lines_to_execute = lines.replace('\t',' '*4)
655 lines_to_execute = lines_to_execute.replace('\r','')
656 self.IP.do_execute(lines_to_execute.encode(ENCODING))
657 self.updateHistoryTracker(lines)
658 self.setCurrentState('WAIT_END_OF_EXECUTION')
659
660 def evtStateExecuteDone(self,evt):
661 self.doc = self.IP.get_doc_text()
662 self.help = self.IP.get_help_text()
663 if self.doc:
664 self.pager_lines = self.doc[7:].split('\n')
665 self.pager_state = 'INIT'
666 self.setCurrentState('SHOW_DOC')
667 self.pager(self.doc)
668 elif self.help:
669 self.pager_lines = self.help.split('\n')
670 self.pager_state = 'INIT'
671 self.setCurrentState('SHOW_DOC')
672 self.pager(self.help)
673 else:
674 if(self.text_ctrl.getCursorPos()!=0):
675 self.text_ctrl.removeCurrentLine()
676 self.stateShowPrompt()
677
678 def stateShowPrompt(self):
679 self.setCurrentState('SHOW_PROMPT')
680 self.text_ctrl.setPrompt(self.IP.get_prompt())
681 self.text_ctrl.setIndentation(self.IP.get_indentation())
682 self.text_ctrl.setPromptCount(self.IP.get_prompt_count())
683 self.text_ctrl.showPrompt()
684 self.IP.init_history_index()
685 self.setCurrentState('IDLE')
686
687 def setCurrentState(self, state):
688 self.cur_state = state
689 self.updateStatusTracker(self.cur_state)
690
691 def pager(self,text):
692
693 if self.pager_state == 'INIT':
694 #print >>sys.__stdout__,"PAGER state:",self.pager_state
695 self.pager_nb_lines = len(self.pager_lines)
696 self.pager_index = 0
697 self.pager_do_remove = False
698 self.text_ctrl.write('\n')
699 self.pager_state = 'PROCESS_LINES'
700
701 if self.pager_state == 'PROCESS_LINES':
702 #print >>sys.__stdout__,"PAGER state:",self.pager_state
703 if self.pager_do_remove == True:
704 self.text_ctrl.removeCurrentLine()
705 self.pager_do_remove = False
706
707 if self.pager_nb_lines > 10:
708 #print >>sys.__stdout__,"PAGER processing 10 lines"
709 if self.pager_index > 0:
710 self.text_ctrl.write(">\x01\x1b[1;36m\x02"+self.pager_lines[self.pager_index]+'\n')
711 else:
712 self.text_ctrl.write("\x01\x1b[1;36m\x02 "+self.pager_lines[self.pager_index]+'\n')
713
714 for line in self.pager_lines[self.pager_index+1:self.pager_index+9]:
715 self.text_ctrl.write("\x01\x1b[1;36m\x02 "+line+'\n')
716 self.pager_index += 10
717 self.pager_nb_lines -= 10
718 self.text_ctrl.write("--- Push Enter to continue or 'Q' to quit---")
719 self.pager_do_remove = True
720 self.pager_state = 'WAITING'
721 return
722 else:
723 #print >>sys.__stdout__,"PAGER processing last lines"
724 if self.pager_nb_lines > 0:
725 if self.pager_index > 0:
726 self.text_ctrl.write(">\x01\x1b[1;36m\x02"+self.pager_lines[self.pager_index]+'\n')
727 else:
728 self.text_ctrl.write("\x01\x1b[1;36m\x02 "+self.pager_lines[self.pager_index]+'\n')
729
730 self.pager_index += 1
731 self.pager_nb_lines -= 1
732 if self.pager_nb_lines > 0:
733 for line in self.pager_lines[self.pager_index:]:
734 self.text_ctrl.write("\x01\x1b[1;36m\x02 "+line+'\n')
735 self.pager_nb_lines = 0
736 self.pager_state = 'DONE'
737 self.stateShowPrompt()
738
739 #------------------------ Key Handler ------------------------------------
740 def keyPress(self, event):
741 '''
742 Key press callback with plenty of shell goodness, like history,
743 autocompletions, etc.
744 '''
745 if event.GetKeyCode() == ord('C'):
746 if event.Modifiers == wx.MOD_CONTROL or event.Modifiers == wx.MOD_ALT:
747 if self.cur_state == 'WAIT_END_OF_EXECUTION':
748 #we raise an exception inside the IPython thread container
749 self.IP.ce.raise_exc(KeyboardInterrupt)
750 return
751
752 #let this before 'wx.WXK_RETURN' because we have to put 'IDLE'
753 #mode if AutoComp has been set as inactive
754 if self.cur_state == 'COMPLETING':
755 if not self.text_ctrl.AutoCompActive():
756 self.cur_state = 'IDLE'
757 else:
758 event.Skip()
759
760 if event.KeyCode == wx.WXK_RETURN:
761 if self.cur_state == 'IDLE':
762 #we change the state ot the state machine
763 self.setCurrentState('DO_EXECUTE_LINE')
764 self.stateDoExecuteLine()
765 return
766
767 if self.pager_state == 'WAITING':
768 self.pager_state = 'PROCESS_LINES'
769 self.pager(self.doc)
770 return
771
772 if self.cur_state == 'WAITING_USER_INPUT':
773 line=self.text_ctrl.getCurrentLine()
774 self.text_ctrl.write('\n')
775 self.setCurrentState('WAIT_END_OF_EXECUTION')
776 return
777
778 if event.GetKeyCode() in [ord('q'),ord('Q')]:
779 if self.pager_state == 'WAITING':
780 self.pager_state = 'DONE'
781 self.text_ctrl.write('\n')
782 self.stateShowPrompt()
783 return
784
785 if self.cur_state == 'WAITING_USER_INPUT':
786 event.Skip()
787
788 if self.cur_state == 'IDLE':
789 if event.KeyCode == wx.WXK_UP:
790 history = self.IP.history_back()
791 self.text_ctrl.writeHistory(history)
792 return
793 if event.KeyCode == wx.WXK_DOWN:
794 history = self.IP.history_forward()
795 self.text_ctrl.writeHistory(history)
796 return
797 if event.KeyCode == wx.WXK_TAB:
798 #if line empty we disable tab completion
799 if not self.text_ctrl.getCurrentLine().strip():
800 self.text_ctrl.write('\t')
801 return
802 completed, possibilities = self.IP.complete(self.text_ctrl.getCurrentLine())
803 if len(possibilities) > 1:
804 if self.text_ctrl.autocomplete_mode == 'IPYTHON':
805 cur_slice = self.text_ctrl.getCurrentLine()
806 self.text_ctrl.write('\n')
807 self.text_ctrl.writeCompletion(possibilities)
808 self.text_ctrl.write('\n')
809
810 self.text_ctrl.showPrompt()
811 self.text_ctrl.write(cur_slice)
812 self.text_ctrl.changeLine(completed or cur_slice)
813 else:
814 self.cur_state = 'COMPLETING'
815 self.text_ctrl.writeCompletion(possibilities)
816 else:
817 self.text_ctrl.changeLine(completed or cur_slice)
818 return
819 event.Skip()
820
821 #------------------------ Option Section ---------------------------------
822 def evtCheckOptionCompletion(self, event):
823 if event.IsChecked():
824 self.options['completion']['value']='STC'
825 else:
826 self.options['completion']['value']='IPYTHON'
827 self.text_ctrl.setCompletionMethod(self.options['completion']['value'])
828 self.updateOptionTracker('completion',
829 self.options['completion']['value'])
830 self.text_ctrl.SetFocus()
831
832 def evtCheckOptionBackgroundColor(self, event):
833 if event.IsChecked():
834 self.options['background_color']['value']='WHITE'
835 else:
836 self.options['background_color']['value']='BLACK'
837 self.text_ctrl.setBackgroundColor(self.options['background_color']['value'])
838 self.updateOptionTracker('background_color',
839 self.options['background_color']['value'])
840 self.text_ctrl.SetFocus()
841
842 def evtCheckOptionThreading(self, event):
843 if event.IsChecked():
844 self.options['threading']['value']='True'
845 self.IP.set_threading(True)
846 self.cout.write = self.text_ctrl.asyncWrite
847 else:
848 self.options['threading']['value']='False'
849 self.IP.set_threading(False)
850 self.cout.write = self.text_ctrl.write
851 self.updateOptionTracker('threading',
852 self.options['threading']['value'])
853 self.text_ctrl.SetFocus()
854
855 def getOptions(self):
856 return self.options
857
858 def reloadOptions(self,options):
859 self.options = options
860 for key in self.options.keys():
861 value = self.options[key]['value']
862 self.options[key]['checkbox'].SetValue(self.options[key][value])
863 self.options[key]['setfunc'](value)
864
865 if self.options['threading']['value']=='True':
866 self.IP.set_threading(True)
867 self.cout.write = self.text_ctrl.asyncWrite
868 else:
869 self.IP.set_threading(False)
870 self.cout.write = self.text_ctrl.write
871
872 #------------------------ Hook Section -----------------------------------
873 def updateOptionTracker(self,name,value):
874 '''
875 Default history tracker (does nothing)
876 '''
877 pass
878
879 def setOptionTrackerHook(self,func):
880 '''
881 Define a new history tracker
882 '''
883 self.updateOptionTracker = func
884
885 def updateHistoryTracker(self,command_line):
886 '''
887 Default history tracker (does nothing)
888 '''
889 pass
890
891 def setHistoryTrackerHook(self,func):
892 '''
893 Define a new history tracker
894 '''
895 self.updateHistoryTracker = func
896
897 def updateStatusTracker(self,status):
898 '''
899 Default status tracker (does nothing)
900 '''
901 pass
902
903 def setStatusTrackerHook(self,func):
904 '''
905 Define a new status tracker
906 '''
907 self.updateStatusTracker = func
908
909 def askExitHandler(self, event):
910 '''
911 Default exit handler
912 '''
913 self.text_ctrl.write('\nExit callback has not been set.')
914
915 def setAskExitHandler(self, func):
916 '''
917 Define an exit handler
918 '''
919 self.askExitHandler = func
920
921 if __name__ == '__main__':
922 # Some simple code to test the shell widget.
923 class MainWindow(wx.Frame):
924 def __init__(self, parent, id, title):
925 wx.Frame.__init__(self, parent, id, title, size=(300,250))
926 self._sizer = wx.BoxSizer(wx.VERTICAL)
927 self.shell = IPShellWidget(self)
928 self._sizer.Add(self.shell, 1, wx.EXPAND)
929 self.SetSizer(self._sizer)
930 self.SetAutoLayout(1)
931 self.Show(True)
932
933 app = wx.PySimpleApp()
934 frame = MainWindow(None, wx.ID_ANY, 'Ipython')
935 frame.SetSize((780, 460))
936 shell = frame.shell
937
938 app.MainLoop()
939
940
1 #!/usr/bin/python
2 # -*- coding: iso-8859-15 -*-
3 '''
4 Provides IPython WX console widgets.
5
6 @author: Laurent Dufrechou
7 laurent.dufrechou _at_ gmail.com
8 This WX widget is based on the original work of Eitan Isaacson
9 that provided the console for the GTK toolkit.
10
11 Original work from:
12 @author: Eitan Isaacson
13 @organization: IBM Corporation
14 @copyright: Copyright (c) 2007 IBM Corporation
15 @license: BSD
16
17 All rights reserved. This program and the accompanying materials are made
18 available under the terms of the BSD which accompanies this distribution, and
19 is available at U{http://www.opensource.org/licenses/bsd-license.php}
20 '''
21
22 __version__ = 0.9
23 __author__ = "Laurent Dufrechou"
24 __email__ = "laurent.dufrechou _at_ gmail.com"
25 __license__ = "BSD"
26
27 import wx
28 import wx.stc as stc
29
30 import re
31 from StringIO import StringIO
32
33 import sys
34 import codecs
35 import locale
36 import time
37
38 for enc in (locale.getpreferredencoding(),
39 sys.getfilesystemencoding(),
40 sys.getdefaultencoding()):
41 try:
42 codecs.lookup(enc)
43 ENCODING = enc
44 break
45 except LookupError:
46 pass
47 else:
48 ENCODING = 'utf-8'
49
50 from ipshell_nonblocking import NonBlockingIPShell
51
52 class WxNonBlockingIPShell(NonBlockingIPShell):
53 '''
54 An NonBlockingIPShell Thread that is WX dependent.
55 '''
56 def __init__(self, parent,
57 argv=[],user_ns={},user_global_ns=None,
58 cin=None, cout=None, cerr=None,
59 ask_exit_handler=None):
60
61 NonBlockingIPShell.__init__(self, argv, user_ns, user_global_ns,
62 cin, cout, cerr,
63 ask_exit_handler)
64
65 self.parent = parent
66
67 self.ask_exit_callback = ask_exit_handler
68 self._IP.exit = self._ask_exit
69
70 def addGUIShortcut(self, text, func):
71 wx.CallAfter(self.parent.add_button_handler,
72 button_info={ 'text':text,
73 'func':self.parent.doExecuteLine(func)})
74
75 def _raw_input(self, prompt=''):
76 """ A replacement from python's raw_input.
77 """
78 self.answer = None
79 if(self._threading == True):
80 wx.CallAfter(self._yesNoBox, prompt)
81 while self.answer is None:
82 time.sleep(.1)
83 else:
84 self._yesNoBox(prompt)
85 return self.answer
86
87 def _yesNoBox(self, prompt):
88 """ yes/no box managed with wx.CallAfter jsut in case caler is executed in a thread"""
89 dlg = wx.TextEntryDialog(
90 self.parent, prompt,
91 'Input requested', 'Python')
92 dlg.SetValue("")
93
94 answer = ''
95 if dlg.ShowModal() == wx.ID_OK:
96 answer = dlg.GetValue()
97
98 dlg.Destroy()
99 self.answer = answer
100
101 def _ask_exit(self):
102 wx.CallAfter(self.ask_exit_callback, ())
103
104 def _after_execute(self):
105 wx.CallAfter(self.parent.evtStateExecuteDone, ())
106
107
108 class WxConsoleView(stc.StyledTextCtrl):
109 '''
110 Specialized styled text control view for console-like workflow.
111 We use here a scintilla frontend thus it can be reused in any GUI that
112 supports scintilla with less work.
113
114 @cvar ANSI_COLORS_BLACK: Mapping of terminal colors to X11 names.
115 (with Black background)
116 @type ANSI_COLORS_BLACK: dictionary
117
118 @cvar ANSI_COLORS_WHITE: Mapping of terminal colors to X11 names.
119 (with White background)
120 @type ANSI_COLORS_WHITE: dictionary
121
122 @ivar color_pat: Regex of terminal color pattern
123 @type color_pat: _sre.SRE_Pattern
124 '''
125 ANSI_STYLES_BLACK = {'0;30': [0, 'WHITE'], '0;31': [1, 'RED'],
126 '0;32': [2, 'GREEN'], '0;33': [3, 'BROWN'],
127 '0;34': [4, 'BLUE'], '0;35': [5, 'PURPLE'],
128 '0;36': [6, 'CYAN'], '0;37': [7, 'LIGHT GREY'],
129 '1;30': [8, 'DARK GREY'], '1;31': [9, 'RED'],
130 '1;32': [10, 'SEA GREEN'], '1;33': [11, 'YELLOW'],
131 '1;34': [12, 'LIGHT BLUE'], '1;35':
132 [13, 'MEDIUM VIOLET RED'],
133 '1;36': [14, 'LIGHT STEEL BLUE'], '1;37': [15, 'YELLOW']}
134
135 ANSI_STYLES_WHITE = {'0;30': [0, 'BLACK'], '0;31': [1, 'RED'],
136 '0;32': [2, 'GREEN'], '0;33': [3, 'BROWN'],
137 '0;34': [4, 'BLUE'], '0;35': [5, 'PURPLE'],
138 '0;36': [6, 'CYAN'], '0;37': [7, 'LIGHT GREY'],
139 '1;30': [8, 'DARK GREY'], '1;31': [9, 'RED'],
140 '1;32': [10, 'SEA GREEN'], '1;33': [11, 'YELLOW'],
141 '1;34': [12, 'LIGHT BLUE'], '1;35':
142 [13, 'MEDIUM VIOLET RED'],
143 '1;36': [14, 'LIGHT STEEL BLUE'], '1;37': [15, 'YELLOW']}
144
145 def __init__(self, parent, prompt, intro="", background_color="BLACK",
146 pos=wx.DefaultPosition, ID = -1, size=wx.DefaultSize,
147 style=0, autocomplete_mode = 'IPYTHON'):
148 '''
149 Initialize console view.
150
151 @param parent: Parent widget
152 @param prompt: User specified prompt
153 @type intro: string
154 @param intro: User specified startup introduction string
155 @type intro: string
156 @param background_color: Can be BLACK or WHITE
157 @type background_color: string
158 @param other: init param of styledTextControl (can be used as-is)
159 @param autocomplete_mode: Can be 'IPYTHON' or 'STC'
160 'IPYTHON' show autocompletion the ipython way
161 'STC" show it scintilla text control way
162 '''
163 stc.StyledTextCtrl.__init__(self, parent, ID, pos, size, style)
164
165 ####### Scintilla configuration ###################################
166
167 # Ctrl + B or Ctrl + N can be used to zoomin/zoomout the text inside
168 # the widget
169 self.CmdKeyAssign(ord('B'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMIN)
170 self.CmdKeyAssign(ord('N'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMOUT)
171
172 #We draw a line at position 80
173 self.SetEdgeMode(stc.STC_EDGE_LINE)
174 self.SetEdgeColumn(80)
175 self.SetEdgeColour(wx.LIGHT_GREY)
176
177 #self.SetViewWhiteSpace(True)
178 #self.SetViewEOL(True)
179 self.SetEOLMode(stc.STC_EOL_CRLF)
180 #self.SetWrapMode(stc.STC_WRAP_CHAR)
181 #self.SetWrapMode(stc.STC_WRAP_WORD)
182 self.SetBufferedDraw(True)
183 #self.SetUseAntiAliasing(True)
184 self.SetLayoutCache(stc.STC_CACHE_PAGE)
185 self.SetUndoCollection(False)
186 self.SetUseTabs(True)
187 self.SetIndent(4)
188 self.SetTabWidth(4)
189
190 self.EnsureCaretVisible()
191
192 self.SetMargins(3, 3) #text is moved away from border with 3px
193 # Suppressing Scintilla margins
194 self.SetMarginWidth(0, 0)
195 self.SetMarginWidth(1, 0)
196 self.SetMarginWidth(2, 0)
197
198 self.background_color = background_color
199 self.buildStyles()
200
201 self.indent = 0
202 self.prompt_count = 0
203 self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?')
204
205 self.write(intro)
206 self.setPrompt(prompt)
207 self.showPrompt()
208
209 self.autocomplete_mode = autocomplete_mode
210
211 self.Bind(wx.EVT_KEY_DOWN, self._onKeypress)
212
213 def buildStyles(self):
214 #we define platform specific fonts
215 if wx.Platform == '__WXMSW__':
216 faces = { 'times': 'Times New Roman',
217 'mono' : 'Courier New',
218 'helv' : 'Arial',
219 'other': 'Comic Sans MS',
220 'size' : 10,
221 'size2': 8,
222 }
223 elif wx.Platform == '__WXMAC__':
224 faces = { 'times': 'Times New Roman',
225 'mono' : 'Monaco',
226 'helv' : 'Arial',
227 'other': 'Comic Sans MS',
228 'size' : 10,
229 'size2': 8,
230 }
231 else:
232 faces = { 'times': 'Times',
233 'mono' : 'Courier',
234 'helv' : 'Helvetica',
235 'other': 'new century schoolbook',
236 'size' : 10,
237 'size2': 8,
238 }
239
240 # make some styles
241 if self.background_color != "BLACK":
242 self.background_color = "WHITE"
243 self.SetCaretForeground("BLACK")
244 self.ANSI_STYLES = self.ANSI_STYLES_WHITE
245 else:
246 self.SetCaretForeground("WHITE")
247 self.ANSI_STYLES = self.ANSI_STYLES_BLACK
248
249 self.StyleSetSpec(stc.STC_STYLE_DEFAULT,
250 "fore:%s,back:%s,size:%d,face:%s"
251 % (self.ANSI_STYLES['0;30'][1],
252 self.background_color,
253 faces['size'], faces['mono']))
254 self.StyleClearAll()
255 self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT,
256 "fore:#FF0000,back:#0000FF,bold")
257 self.StyleSetSpec(stc.STC_STYLE_BRACEBAD,
258 "fore:#000000,back:#FF0000,bold")
259
260 for style in self.ANSI_STYLES.values():
261 self.StyleSetSpec(style[0], "bold,fore:%s" % style[1])
262
263 #######################################################################
264
265 def setBackgroundColor(self, color):
266 self.background_color = color
267 self.buildStyles()
268
269 def getBackgroundColor(self, color):
270 return self.background_color
271
272 def asyncWrite(self, text):
273 '''
274 Write given text to buffer in an asynchroneous way.
275 It is used from another thread to be able to acces the GUI.
276 @param text: Text to append
277 @type text: string
278 '''
279 try:
280 wx.MutexGuiEnter()
281
282 #be sure not to be interrutpted before the MutexGuiLeave!
283 self.write(text)
284
285 except KeyboardInterrupt:
286 wx.MutexGuiLeave()
287 raise KeyboardInterrupt
288 wx.MutexGuiLeave()
289
290
291 def write(self, text):
292 '''
293 Write given text to buffer.
294
295 @param text: Text to append.
296 @type text: string
297 '''
298 segments = self.color_pat.split(text)
299 segment = segments.pop(0)
300 self.StartStyling(self.getCurrentLineEnd(), 0xFF)
301 self.AppendText(segment)
302
303 if segments:
304 ansi_tags = self.color_pat.findall(text)
305
306 for tag in ansi_tags:
307 i = segments.index(tag)
308 self.StartStyling(self.getCurrentLineEnd(), 0xFF)
309 self.AppendText(segments[i+1])
310
311 if tag != '0':
312 self.SetStyling(len(segments[i+1]), self.ANSI_STYLES[tag][0])
313
314 segments.pop(i)
315
316 self.moveCursor(self.getCurrentLineEnd())
317
318 def getPromptLen(self):
319 '''
320 Return the length of current prompt
321 '''
322 return len(str(self.prompt_count)) + 7
323
324 def setPrompt(self, prompt):
325 self.prompt = prompt
326
327 def setIndentation(self, indentation):
328 self.indent = indentation
329
330 def setPromptCount(self, count):
331 self.prompt_count = count
332
333 def showPrompt(self):
334 '''
335 Prints prompt at start of line.
336
337 @param prompt: Prompt to print.
338 @type prompt: string
339 '''
340 self.write(self.prompt)
341 #now we update the position of end of prompt
342 self.current_start = self.getCurrentLineEnd()
343
344 autoindent = self.indent*' '
345 autoindent = autoindent.replace(' ','\t')
346 self.write(autoindent)
347
348 def changeLine(self, text):
349 '''
350 Replace currently entered command line with given text.
351
352 @param text: Text to use as replacement.
353 @type text: string
354 '''
355 self.SetSelection(self.getCurrentPromptStart(), self.getCurrentLineEnd())
356 self.ReplaceSelection(text)
357 self.moveCursor(self.getCurrentLineEnd())
358
359 def getCurrentPromptStart(self):
360 return self.current_start
361
362 def getCurrentLineStart(self):
363 return self.GotoLine(self.LineFromPosition(self.GetCurrentPos()))
364
365 def getCurrentLineEnd(self):
366 return self.GetLength()
367
368 def getCurrentLine(self):
369 '''
370 Get text in current command line.
371
372 @return: Text of current command line.
373 @rtype: string
374 '''
375 return self.GetTextRange(self.getCurrentPromptStart(),
376 self.getCurrentLineEnd())
377
378 def moveCursorOnNewValidKey(self):
379 #If cursor is at wrong position put it at last line...
380 if self.GetCurrentPos() < self.getCurrentPromptStart():
381 self.GotoPos(self.getCurrentPromptStart())
382
383 def removeFromTo(self, from_pos, to_pos):
384 if from_pos < to_pos:
385 self.SetSelection(from_pos, to_pos)
386 self.DeleteBack()
387
388 def removeCurrentLine(self):
389 self.LineDelete()
390
391 def moveCursor(self, position):
392 self.GotoPos(position)
393
394 def getCursorPos(self):
395 return self.GetCurrentPos()
396
397 def selectFromTo(self, from_pos, to_pos):
398 self.SetSelectionStart(from_pos)
399 self.SetSelectionEnd(to_pos)
400
401 def writeHistory(self, history):
402 self.removeFromTo(self.getCurrentPromptStart(), self.getCurrentLineEnd())
403 self.changeLine(history)
404
405 def setCompletionMethod(self, completion):
406 if completion in ['IPYTHON', 'STC']:
407 self.autocomplete_mode = completion
408 else:
409 raise AttributeError
410
411 def getCompletionMethod(self, completion):
412 return self.autocomplete_mode
413
414 def writeCompletion(self, possibilities):
415 if self.autocomplete_mode == 'IPYTHON':
416 max_len = len(max(possibilities, key=len))
417 max_symbol = ' '*max_len
418
419 #now we check how much symbol we can put on a line...
420 test_buffer = max_symbol + ' '*4
421
422 allowed_symbols = 80/len(test_buffer)
423 if allowed_symbols == 0:
424 allowed_symbols = 1
425
426 pos = 1
427 buf = ''
428 for symbol in possibilities:
429 #buf += symbol+'\n'#*spaces)
430 if pos < allowed_symbols:
431 spaces = max_len - len(symbol) + 4
432 buf += symbol+' '*spaces
433 pos += 1
434 else:
435 buf += symbol+'\n'
436 pos = 1
437 self.write(buf)
438 else:
439 possibilities.sort() # Python sorts are case sensitive
440 self.AutoCompSetIgnoreCase(False)
441 self.AutoCompSetAutoHide(False)
442 #let compute the length ot last word
443 splitter = [' ', '(', '[', '{','=']
444 last_word = self.getCurrentLine()
445 for breaker in splitter:
446 last_word = last_word.split(breaker)[-1]
447 self.AutoCompShow(len(last_word), " ".join(possibilities))
448
449 def _onKeypress(self, event, skip=True):
450 '''
451 Key press callback used for correcting behavior for console-like
452 interfaces. For example 'home' should go to prompt, not to begining of
453 line.
454
455 @param widget: Widget that key press accored in.
456 @type widget: gtk.Widget
457 @param event: Event object
458 @type event: gtk.gdk.Event
459
460 @return: Return True if event as been catched.
461 @rtype: boolean
462 '''
463 if not self.AutoCompActive():
464 if event.GetKeyCode() == wx.WXK_HOME:
465 if event.Modifiers == wx.MOD_NONE:
466 self.moveCursorOnNewValidKey()
467 self.moveCursor(self.getCurrentPromptStart())
468 return True
469 elif event.Modifiers == wx.MOD_SHIFT:
470 self.moveCursorOnNewValidKey()
471 self.selectFromTo(self.getCurrentPromptStart(), self.getCursorPos())
472 return True
473 else:
474 return False
475
476 elif event.GetKeyCode() == wx.WXK_LEFT:
477 if event.Modifiers == wx.MOD_NONE:
478 self.moveCursorOnNewValidKey()
479
480 self.moveCursor(self.getCursorPos()-1)
481 if self.getCursorPos() < self.getCurrentPromptStart():
482 self.moveCursor(self.getCurrentPromptStart())
483 return True
484
485 elif event.GetKeyCode() == wx.WXK_BACK:
486 self.moveCursorOnNewValidKey()
487 if self.getCursorPos() > self.getCurrentPromptStart():
488 event.Skip()
489 return True
490
491 if skip:
492 if event.GetKeyCode() not in [wx.WXK_PAGEUP, wx.WXK_PAGEDOWN]\
493 and event.Modifiers == wx.MOD_NONE:
494 self.moveCursorOnNewValidKey()
495
496 event.Skip()
497 return True
498 return False
499 else:
500 event.Skip()
501
502 def OnUpdateUI(self, evt):
503 # check for matching braces
504 braceAtCaret = -1
505 braceOpposite = -1
506 charBefore = None
507 caretPos = self.GetCurrentPos()
508
509 if caretPos > 0:
510 charBefore = self.GetCharAt(caretPos - 1)
511 styleBefore = self.GetStyleAt(caretPos - 1)
512
513 # check before
514 if charBefore and chr(charBefore) in "[]{}()" and styleBefore == stc.STC_P_OPERATOR:
515 braceAtCaret = caretPos - 1
516
517 # check after
518 if braceAtCaret < 0:
519 charAfter = self.GetCharAt(caretPos)
520 styleAfter = self.GetStyleAt(caretPos)
521
522 if charAfter and chr(charAfter) in "[]{}()" and styleAfter == stc.STC_P_OPERATOR:
523 braceAtCaret = caretPos
524
525 if braceAtCaret >= 0:
526 braceOpposite = self.BraceMatch(braceAtCaret)
527
528 if braceAtCaret != -1 and braceOpposite == -1:
529 self.BraceBadLight(braceAtCaret)
530 else:
531 self.BraceHighlight(braceAtCaret, braceOpposite)
532 #pt = self.PointFromPosition(braceOpposite)
533 #self.Refresh(True, wxRect(pt.x, pt.y, 5,5))
534 #print pt
535 #self.Refresh(False)
536
537 class IPShellWidget(wx.Panel):
538 '''
539 This is wx.Panel that embbed the IPython Thread and the wx.StyledTextControl
540 If you want to port this to any other GUI toolkit, just replace the
541 WxConsoleView by YOURGUIConsoleView and make YOURGUIIPythonView derivate
542 from whatever container you want. I've choosed to derivate from a wx.Panel
543 because it seems to be more useful
544 Any idea to make it more 'generic' welcomed.
545 '''
546
547 def __init__(self, parent, intro=None,
548 background_color="BLACK", add_button_handler=None,
549 wx_ip_shell=None, user_ns={},user_global_ns=None,
550 ):
551 '''
552 Initialize.
553 Instanciate an IPython thread.
554 Instanciate a WxConsoleView.
555 Redirect I/O to console.
556 '''
557 wx.Panel.__init__(self,parent,wx.ID_ANY)
558
559 self.parent = parent
560 ### IPython non blocking shell instanciation ###
561 self.cout = StringIO()
562 self.add_button_handler = add_button_handler
563
564 if wx_ip_shell is not None:
565 self.IP = wx_ip_shell
566 else:
567 self.IP = WxNonBlockingIPShell(self,
568 cout = self.cout, cerr = self.cout,
569 ask_exit_handler = self.askExitCallback)
570
571 ### IPython wx console view instanciation ###
572 #If user didn't defined an intro text, we create one for him
573 #If you really wnat an empty intro just call wxIPythonViewPanel
574 #with intro=''
575 if intro is None:
576 welcome_text = "Welcome to WxIPython Shell.\n\n"
577 welcome_text+= self.IP.get_banner()
578 welcome_text+= "!command -> Execute command in shell\n"
579 welcome_text+= "TAB -> Autocompletion\n"
580 else:
581 welcome_text = intro
582
583 self.text_ctrl = WxConsoleView(self,
584 self.IP.get_prompt(),
585 intro=welcome_text,
586 background_color=background_color)
587
588 option_text = wx.StaticText(self, -1, "Options:")
589 self.completion_option = wx.CheckBox(self, -1, "Scintilla Completion")
590 self.completion_option.SetToolTip(wx.ToolTip(
591 "Selects the completion type:\nEither Ipython default style or Scintilla one"))
592 #self.completion_option.SetValue(False)
593 self.background_option = wx.CheckBox(self, -1, "White Background")
594 self.background_option.SetToolTip(wx.ToolTip(
595 "Selects the back ground color: BLACK or WHITE"))
596 #self.background_option.SetValue(False)
597 self.threading_option = wx.CheckBox(self, -1, "Execute in thread")
598 self.threading_option.SetToolTip(wx.ToolTip(
599 "Use threading: infinite loop don't freeze the GUI and commands can be breaked\nNo threading: maximum compatibility"))
600 #self.threading_option.SetValue(False)
601
602 self.options={'completion':{'value':'IPYTHON',
603 'checkbox':self.completion_option,'STC':True,'IPYTHON':False,
604 'setfunc':self.text_ctrl.setCompletionMethod},
605 'background_color':{'value':'BLACK',
606 'checkbox':self.background_option,'WHITE':True,'BLACK':False,
607 'setfunc':self.text_ctrl.setBackgroundColor},
608 'threading':{'value':'True',
609 'checkbox':self.threading_option,'True':True,'False':False,
610 'setfunc':self.IP.set_threading},
611 }
612
613 #self.cout.write dEfault option is asynchroneous because default sate is threading ON
614 self.cout.write = self.text_ctrl.asyncWrite
615 #we reloard options
616 self.reloadOptions(self.options)
617
618 self.text_ctrl.Bind(wx.EVT_KEY_DOWN, self.keyPress)
619 self.completion_option.Bind(wx.EVT_CHECKBOX, self.evtCheckOptionCompletion)
620 self.background_option.Bind(wx.EVT_CHECKBOX, self.evtCheckOptionBackgroundColor)
621 self.threading_option.Bind(wx.EVT_CHECKBOX, self.evtCheckOptionThreading)
622
623 ### making the layout of the panel ###
624 sizer = wx.BoxSizer(wx.VERTICAL)
625 sizer.Add(self.text_ctrl, 1, wx.EXPAND)
626 option_sizer = wx.BoxSizer(wx.HORIZONTAL)
627 sizer.Add(option_sizer, 0)
628 option_sizer.AddMany([(10, 20),
629 (option_text, 0, wx.ALIGN_CENTER_VERTICAL),
630 (5, 5),
631 (self.completion_option, 0, wx.ALIGN_CENTER_VERTICAL),
632 (8, 8),
633 (self.background_option, 0, wx.ALIGN_CENTER_VERTICAL),
634 (8, 8),
635 (self.threading_option, 0, wx.ALIGN_CENTER_VERTICAL)
636 ])
637 self.SetAutoLayout(True)
638 sizer.Fit(self)
639 sizer.SetSizeHints(self)
640 self.SetSizer(sizer)
641 #and we focus on the widget :)
642 self.SetFocus()
643
644 #widget state management (for key handling different cases)
645 self.setCurrentState('IDLE')
646 self.pager_state = 'DONE'
647 self.raw_input_current_line = 0
648
649 def askExitCallback(self, event):
650 self.askExitHandler(event)
651
652 #---------------------- IPython Thread Management ------------------------
653 def stateDoExecuteLine(self):
654 lines=self.text_ctrl.getCurrentLine()
655 self.text_ctrl.write('\n')
656 lines_to_execute = lines.replace('\t',' '*4)
657 lines_to_execute = lines_to_execute.replace('\r','')
658 self.IP.do_execute(lines_to_execute.encode(ENCODING))
659 self.updateHistoryTracker(lines)
660 if(self.text_ctrl.getCursorPos()!=0):
661 self.text_ctrl.removeCurrentLine()
662 self.setCurrentState('WAIT_END_OF_EXECUTION')
663
664 def evtStateExecuteDone(self,evt):
665 self.doc = self.IP.get_doc_text()
666 self.help = self.IP.get_help_text()
667 if self.doc:
668 self.pager_lines = self.doc[7:].split('\n')
669 self.pager_state = 'INIT'
670 self.setCurrentState('SHOW_DOC')
671 self.pager(self.doc)
672 elif self.help:
673 self.pager_lines = self.help.split('\n')
674 self.pager_state = 'INIT'
675 self.setCurrentState('SHOW_DOC')
676 self.pager(self.help)
677 else:
678 if(self.text_ctrl.getCursorPos()!=0):
679 self.text_ctrl.removeCurrentLine()
680 self.stateShowPrompt()
681
682 def stateShowPrompt(self):
683 self.setCurrentState('SHOW_PROMPT')
684 self.text_ctrl.setPrompt(self.IP.get_prompt())
685 self.text_ctrl.setIndentation(self.IP.get_indentation())
686 self.text_ctrl.setPromptCount(self.IP.get_prompt_count())
687 self.text_ctrl.showPrompt()
688 self.IP.init_history_index()
689 self.setCurrentState('IDLE')
690
691 def setCurrentState(self, state):
692 self.cur_state = state
693 self.updateStatusTracker(self.cur_state)
694
695 def pager(self,text):
696
697 if self.pager_state == 'INIT':
698 #print >>sys.__stdout__,"PAGER state:",self.pager_state
699 self.pager_nb_lines = len(self.pager_lines)
700 self.pager_index = 0
701 self.pager_do_remove = False
702 self.text_ctrl.write('\n')
703 self.pager_state = 'PROCESS_LINES'
704
705 if self.pager_state == 'PROCESS_LINES':
706 #print >>sys.__stdout__,"PAGER state:",self.pager_state
707 if self.pager_do_remove == True:
708 self.text_ctrl.removeCurrentLine()
709 self.pager_do_remove = False
710
711 if self.pager_nb_lines > 10:
712 #print >>sys.__stdout__,"PAGER processing 10 lines"
713 if self.pager_index > 0:
714 self.text_ctrl.write(">\x01\x1b[1;36m\x02"+self.pager_lines[self.pager_index]+'\n')
715 else:
716 self.text_ctrl.write("\x01\x1b[1;36m\x02 "+self.pager_lines[self.pager_index]+'\n')
717
718 for line in self.pager_lines[self.pager_index+1:self.pager_index+9]:
719 self.text_ctrl.write("\x01\x1b[1;36m\x02 "+line+'\n')
720 self.pager_index += 10
721 self.pager_nb_lines -= 10
722 self.text_ctrl.write("--- Push Enter to continue or 'Q' to quit---")
723 self.pager_do_remove = True
724 self.pager_state = 'WAITING'
725 return
726 else:
727 #print >>sys.__stdout__,"PAGER processing last lines"
728 if self.pager_nb_lines > 0:
729 if self.pager_index > 0:
730 self.text_ctrl.write(">\x01\x1b[1;36m\x02"+self.pager_lines[self.pager_index]+'\n')
731 else:
732 self.text_ctrl.write("\x01\x1b[1;36m\x02 "+self.pager_lines[self.pager_index]+'\n')
733
734 self.pager_index += 1
735 self.pager_nb_lines -= 1
736 if self.pager_nb_lines > 0:
737 for line in self.pager_lines[self.pager_index:]:
738 self.text_ctrl.write("\x01\x1b[1;36m\x02 "+line+'\n')
739 self.pager_nb_lines = 0
740 self.pager_state = 'DONE'
741 self.stateShowPrompt()
742
743 #------------------------ Key Handler ------------------------------------
744 def keyPress(self, event):
745 '''
746 Key press callback with plenty of shell goodness, like history,
747 autocompletions, etc.
748 '''
749 if event.GetKeyCode() == ord('C'):
750 if event.Modifiers == wx.MOD_CONTROL or event.Modifiers == wx.MOD_ALT:
751 if self.cur_state == 'WAIT_END_OF_EXECUTION':
752 #we raise an exception inside the IPython thread container
753 self.IP.ce.raise_exc(KeyboardInterrupt)
754 return
755
756 #let this before 'wx.WXK_RETURN' because we have to put 'IDLE'
757 #mode if AutoComp has been set as inactive
758 if self.cur_state == 'COMPLETING':
759 if not self.text_ctrl.AutoCompActive():
760 self.cur_state = 'IDLE'
761 else:
762 event.Skip()
763
764 if event.KeyCode == wx.WXK_RETURN:
765 if self.cur_state == 'IDLE':
766 #we change the state ot the state machine
767 self.setCurrentState('DO_EXECUTE_LINE')
768 self.stateDoExecuteLine()
769 return
770
771 if self.pager_state == 'WAITING':
772 self.pager_state = 'PROCESS_LINES'
773 self.pager(self.doc)
774 return
775
776 if self.cur_state == 'WAITING_USER_INPUT':
777 line=self.text_ctrl.getCurrentLine()
778 self.text_ctrl.write('\n')
779 self.setCurrentState('WAIT_END_OF_EXECUTION')
780 return
781
782 if event.GetKeyCode() in [ord('q'),ord('Q')]:
783 if self.pager_state == 'WAITING':
784 self.pager_state = 'DONE'
785 self.text_ctrl.write('\n')
786 self.stateShowPrompt()
787 return
788
789 if self.cur_state == 'WAITING_USER_INPUT':
790 event.Skip()
791
792 if self.cur_state == 'IDLE':
793 if event.KeyCode == wx.WXK_UP:
794 history = self.IP.history_back()
795 self.text_ctrl.writeHistory(history)
796 return
797 if event.KeyCode == wx.WXK_DOWN:
798 history = self.IP.history_forward()
799 self.text_ctrl.writeHistory(history)
800 return
801 if event.KeyCode == wx.WXK_TAB:
802 #if line empty we disable tab completion
803 if not self.text_ctrl.getCurrentLine().strip():
804 self.text_ctrl.write('\t')
805 return
806 completed, possibilities = self.IP.complete(self.text_ctrl.getCurrentLine())
807 if len(possibilities) > 1:
808 if self.text_ctrl.autocomplete_mode == 'IPYTHON':
809 cur_slice = self.text_ctrl.getCurrentLine()
810 self.text_ctrl.write('\n')
811 self.text_ctrl.writeCompletion(possibilities)
812 self.text_ctrl.write('\n')
813
814 self.text_ctrl.showPrompt()
815 self.text_ctrl.write(cur_slice)
816 self.text_ctrl.changeLine(completed or cur_slice)
817 else:
818 self.cur_state = 'COMPLETING'
819 self.text_ctrl.writeCompletion(possibilities)
820 else:
821 self.text_ctrl.changeLine(completed or cur_slice)
822 return
823 event.Skip()
824
825 #------------------------ Option Section ---------------------------------
826 def evtCheckOptionCompletion(self, event):
827 if event.IsChecked():
828 self.options['completion']['value']='STC'
829 else:
830 self.options['completion']['value']='IPYTHON'
831 self.text_ctrl.setCompletionMethod(self.options['completion']['value'])
832 self.updateOptionTracker('completion',
833 self.options['completion']['value'])
834 self.text_ctrl.SetFocus()
835
836 def evtCheckOptionBackgroundColor(self, event):
837 if event.IsChecked():
838 self.options['background_color']['value']='WHITE'
839 else:
840 self.options['background_color']['value']='BLACK'
841 self.text_ctrl.setBackgroundColor(self.options['background_color']['value'])
842 self.updateOptionTracker('background_color',
843 self.options['background_color']['value'])
844 self.text_ctrl.SetFocus()
845
846 def evtCheckOptionThreading(self, event):
847 if event.IsChecked():
848 self.options['threading']['value']='True'
849 self.IP.set_threading(True)
850 self.cout.write = self.text_ctrl.asyncWrite
851 else:
852 self.options['threading']['value']='False'
853 self.IP.set_threading(False)
854 self.cout.write = self.text_ctrl.write
855 self.updateOptionTracker('threading',
856 self.options['threading']['value'])
857 self.text_ctrl.SetFocus()
858
859 def getOptions(self):
860 return self.options
861
862 def reloadOptions(self,options):
863 self.options = options
864 for key in self.options.keys():
865 value = self.options[key]['value']
866 self.options[key]['checkbox'].SetValue(self.options[key][value])
867 self.options[key]['setfunc'](value)
868
869 if self.options['threading']['value']=='True':
870 self.IP.set_threading(True)
871 self.cout.write = self.text_ctrl.asyncWrite
872 else:
873 self.IP.set_threading(False)
874 self.cout.write = self.text_ctrl.write
875
876 #------------------------ Hook Section -----------------------------------
877 def updateOptionTracker(self,name,value):
878 '''
879 Default history tracker (does nothing)
880 '''
881 pass
882
883 def setOptionTrackerHook(self,func):
884 '''
885 Define a new history tracker
886 '''
887 self.updateOptionTracker = func
888
889 def updateHistoryTracker(self,command_line):
890 '''
891 Default history tracker (does nothing)
892 '''
893 pass
894
895 def setHistoryTrackerHook(self,func):
896 '''
897 Define a new history tracker
898 '''
899 self.updateHistoryTracker = func
900
901 def updateStatusTracker(self,status):
902 '''
903 Default status tracker (does nothing)
904 '''
905 pass
906
907 def setStatusTrackerHook(self,func):
908 '''
909 Define a new status tracker
910 '''
911 self.updateStatusTracker = func
912
913 def askExitHandler(self, event):
914 '''
915 Default exit handler
916 '''
917 self.text_ctrl.write('\nExit callback has not been set.')
918
919 def setAskExitHandler(self, func):
920 '''
921 Define an exit handler
922 '''
923 self.askExitHandler = func
924
925 if __name__ == '__main__':
926 # Some simple code to test the shell widget.
927 class MainWindow(wx.Frame):
928 def __init__(self, parent, id, title):
929 wx.Frame.__init__(self, parent, id, title, size=(300,250))
930 self._sizer = wx.BoxSizer(wx.VERTICAL)
931 self.shell = IPShellWidget(self)
932 self._sizer.Add(self.shell, 1, wx.EXPAND)
933 self.SetSizer(self._sizer)
934 self.SetAutoLayout(1)
935 self.Show(True)
936
937 app = wx.PySimpleApp()
938 frame = MainWindow(None, wx.ID_ANY, 'Ipython')
939 frame.SetSize((780, 460))
940 shell = frame.shell
941
942 app.MainLoop()
943
944
General Comments 0
You need to be logged in to leave comments. Login now