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