]>
Commit | Line | Data |
---|---|---|
d14a1e28 RD |
1 | """Shell is an interactive text control in which a user types in |
2 | commands to be sent to the interpreter. This particular shell is | |
3 | based on wxPython's wxStyledTextCtrl. | |
1fded56b | 4 | |
d14a1e28 | 5 | Sponsored by Orbtech - Your source for Python programming expertise.""" |
1fded56b | 6 | |
d14a1e28 RD |
7 | __author__ = "Patrick K. O'Brien <pobrien@orbtech.com>" |
8 | __cvsid__ = "$Id$" | |
9 | __revision__ = "$Revision$"[11:-2] | |
10 | ||
d14a1e28 RD |
11 | import wx |
12 | from wx import stc | |
13 | ||
14 | import keyword | |
15 | import os | |
16 | import sys | |
17 | import time | |
18 | ||
19 | from buffer import Buffer | |
20 | import dispatcher | |
21 | import editwindow | |
22 | import frame | |
23 | from pseudo import PseudoFileIn | |
24 | from pseudo import PseudoFileOut | |
25 | from pseudo import PseudoFileErr | |
26 | from version import VERSION | |
27 | ||
d14a1e28 RD |
28 | sys.ps3 = '<-- ' # Input prompt. |
29 | ||
30 | NAVKEYS = (wx.WXK_END, wx.WXK_LEFT, wx.WXK_RIGHT, | |
31 | wx.WXK_UP, wx.WXK_DOWN, wx.WXK_PRIOR, wx.WXK_NEXT) | |
32 | ||
33 | ||
02b800ce | 34 | class ShellFrame(frame.Frame, frame.ShellFrameMixin): |
d14a1e28 RD |
35 | """Frame containing the shell component.""" |
36 | ||
37 | name = 'Shell Frame' | |
38 | revision = __revision__ | |
39 | ||
40 | def __init__(self, parent=None, id=-1, title='PyShell', | |
41 | pos=wx.DefaultPosition, size=wx.DefaultSize, | |
42 | style=wx.DEFAULT_FRAME_STYLE, locals=None, | |
02b800ce RD |
43 | InterpClass=None, |
44 | config=None, dataDir=None, | |
45 | *args, **kwds): | |
d14a1e28 RD |
46 | """Create ShellFrame instance.""" |
47 | frame.Frame.__init__(self, parent, id, title, pos, size, style) | |
02b800ce RD |
48 | frame.ShellFrameMixin.__init__(self, config, dataDir) |
49 | ||
50 | if size == wx.DefaultSize: | |
51 | self.SetSize((750, 525)) | |
52 | ||
d14a1e28 | 53 | intro = 'PyShell %s - The Flakiest Python Shell' % VERSION |
d14a1e28 RD |
54 | self.SetStatusText(intro.replace('\n', ', ')) |
55 | self.shell = Shell(parent=self, id=-1, introText=intro, | |
56 | locals=locals, InterpClass=InterpClass, | |
02b800ce RD |
57 | startupScript=self.startupScript, |
58 | execStartupScript=self.execStartupScript, | |
d14a1e28 | 59 | *args, **kwds) |
02b800ce | 60 | |
d14a1e28 RD |
61 | # Override the shell so that status messages go to the status bar. |
62 | self.shell.setStatusText = self.SetStatusText | |
63 | ||
02b800ce RD |
64 | self.shell.SetFocus() |
65 | self.LoadSettings() | |
66 | ||
67 | ||
d14a1e28 RD |
68 | def OnClose(self, event): |
69 | """Event handler for closing.""" | |
70 | # This isn't working the way I want, but I'll leave it for now. | |
71 | if self.shell.waiting: | |
72 | if event.CanVeto(): | |
73 | event.Veto(True) | |
74 | else: | |
02b800ce | 75 | self.SaveSettings() |
d14a1e28 RD |
76 | self.shell.destroy() |
77 | self.Destroy() | |
78 | ||
79 | def OnAbout(self, event): | |
80 | """Display an About window.""" | |
81 | title = 'About PyShell' | |
82 | text = 'PyShell %s\n\n' % VERSION + \ | |
83 | 'Yet another Python shell, only flakier.\n\n' + \ | |
84 | 'Half-baked by Patrick K. O\'Brien,\n' + \ | |
85 | 'the other half is still in the oven.\n\n' + \ | |
86 | 'Shell Revision: %s\n' % self.shell.revision + \ | |
87 | 'Interpreter Revision: %s\n\n' % self.shell.interp.revision + \ | |
f2f8a5fc | 88 | 'Platform: %s\n' % sys.platform + \ |
d14a1e28 RD |
89 | 'Python Version: %s\n' % sys.version.split()[0] + \ |
90 | 'wxPython Version: %s\n' % wx.VERSION_STRING + \ | |
f2f8a5fc | 91 | ('\t(%s)\n' % ", ".join(wx.PlatformInfo[1:])) |
d14a1e28 RD |
92 | dialog = wx.MessageDialog(self, text, title, |
93 | wx.OK | wx.ICON_INFORMATION) | |
94 | dialog.ShowModal() | |
95 | dialog.Destroy() | |
96 | ||
97 | ||
4617be08 RD |
98 | def OnHelp(self, event): |
99 | """Show a help dialog.""" | |
100 | frame.ShellFrameMixin.OnHelp(self, event) | |
101 | ||
102 | ||
02b800ce RD |
103 | def LoadSettings(self): |
104 | if self.config is not None: | |
105 | frame.ShellFrameMixin.LoadSettings(self) | |
106 | frame.Frame.LoadSettings(self, self.config) | |
107 | self.shell.LoadSettings(self.config) | |
d14a1e28 | 108 | |
c0ab3f7f | 109 | def SaveSettings(self, force=False): |
02b800ce RD |
110 | if self.config is not None: |
111 | frame.ShellFrameMixin.SaveSettings(self) | |
c0ab3f7f | 112 | if self.autoSaveSettings or force: |
02b800ce RD |
113 | frame.Frame.SaveSettings(self, self.config) |
114 | self.shell.SaveSettings(self.config) | |
d14a1e28 | 115 | |
02b800ce RD |
116 | def DoSaveSettings(self): |
117 | if self.config is not None: | |
c0ab3f7f | 118 | self.SaveSettings(force=True) |
02b800ce RD |
119 | self.config.Flush() |
120 | ||
d14a1e28 | 121 | |
02b800ce RD |
122 | |
123 | ||
124 | HELP_TEXT = """\ | |
d14a1e28 RD |
125 | * Key bindings: |
126 | Home Go to the beginning of the command or line. | |
127 | Shift+Home Select to the beginning of the command or line. | |
128 | Shift+End Select to the end of the line. | |
129 | End Go to the end of the line. | |
130 | Ctrl+C Copy selected text, removing prompts. | |
131 | Ctrl+Shift+C Copy selected text, retaining prompts. | |
02b800ce | 132 | Alt+C Copy to the clipboard, including prefixed prompts. |
d14a1e28 RD |
133 | Ctrl+X Cut selected text. |
134 | Ctrl+V Paste from clipboard. | |
135 | Ctrl+Shift+V Paste and run multiple commands from clipboard. | |
136 | Ctrl+Up Arrow Retrieve Previous History item. | |
137 | Alt+P Retrieve Previous History item. | |
138 | Ctrl+Down Arrow Retrieve Next History item. | |
139 | Alt+N Retrieve Next History item. | |
140 | Shift+Up Arrow Insert Previous History item. | |
141 | Shift+Down Arrow Insert Next History item. | |
142 | F8 Command-completion of History item. | |
143 | (Type a few characters of a previous command and press F8.) | |
144 | Ctrl+Enter Insert new line into multiline command. | |
145 | Ctrl+] Increase font size. | |
146 | Ctrl+[ Decrease font size. | |
147 | Ctrl+= Default font size. | |
02b800ce RD |
148 | Ctrl-Space Show Auto Completion. |
149 | Ctrl-Alt-Space Show Call Tip. | |
02b800ce | 150 | Shift+Enter Complete Text from History. |
e773f79b RD |
151 | Ctrl+F Search |
152 | F3 Search next | |
02b800ce RD |
153 | Ctrl+H "hide" lines containing selection / "unhide" |
154 | F12 on/off "free-edit" mode | |
d14a1e28 RD |
155 | """ |
156 | ||
02b800ce RD |
157 | class ShellFacade: |
158 | """Simplified interface to all shell-related functionality. | |
159 | ||
160 | This is a semi-transparent facade, in that all attributes of other | |
161 | are accessible, even though only some are visible to the user.""" | |
162 | ||
163 | name = 'Shell Interface' | |
164 | revision = __revision__ | |
165 | ||
166 | def __init__(self, other): | |
167 | """Create a ShellFacade instance.""" | |
168 | d = self.__dict__ | |
169 | d['other'] = other | |
170 | d['helpText'] = HELP_TEXT | |
c15cc56f | 171 | d['this'] = other.this |
02b800ce | 172 | |
d14a1e28 RD |
173 | def help(self): |
174 | """Display some useful information about how to use the shell.""" | |
175 | self.write(self.helpText) | |
176 | ||
177 | def __getattr__(self, name): | |
178 | if hasattr(self.other, name): | |
179 | return getattr(self.other, name) | |
180 | else: | |
181 | raise AttributeError, name | |
182 | ||
183 | def __setattr__(self, name, value): | |
184 | if self.__dict__.has_key(name): | |
185 | self.__dict__[name] = value | |
186 | elif hasattr(self.other, name): | |
187 | setattr(self.other, name, value) | |
188 | else: | |
189 | raise AttributeError, name | |
190 | ||
191 | def _getAttributeNames(self): | |
192 | """Return list of magic attributes to extend introspection.""" | |
193 | list = [ | |
194 | 'about', | |
195 | 'ask', | |
196 | 'autoCallTip', | |
197 | 'autoComplete', | |
d351525a | 198 | 'autoCompleteAutoHide', |
d14a1e28 RD |
199 | 'autoCompleteCaseInsensitive', |
200 | 'autoCompleteIncludeDouble', | |
201 | 'autoCompleteIncludeMagic', | |
202 | 'autoCompleteIncludeSingle', | |
02b800ce | 203 | 'callTipInsert', |
d14a1e28 RD |
204 | 'clear', |
205 | 'pause', | |
206 | 'prompt', | |
207 | 'quit', | |
208 | 'redirectStderr', | |
209 | 'redirectStdin', | |
210 | 'redirectStdout', | |
211 | 'run', | |
212 | 'runfile', | |
213 | 'wrap', | |
214 | 'zoom', | |
215 | ] | |
216 | list.sort() | |
217 | return list | |
218 | ||
219 | ||
02b800ce | 220 | |
d14a1e28 RD |
221 | class Shell(editwindow.EditWindow): |
222 | """Shell based on StyledTextCtrl.""" | |
223 | ||
224 | name = 'Shell' | |
225 | revision = __revision__ | |
226 | ||
227 | def __init__(self, parent, id=-1, pos=wx.DefaultPosition, | |
228 | size=wx.DefaultSize, style=wx.CLIP_CHILDREN, | |
02b800ce RD |
229 | introText='', locals=None, InterpClass=None, |
230 | startupScript=None, execStartupScript=True, | |
231 | *args, **kwds): | |
d14a1e28 RD |
232 | """Create Shell instance.""" |
233 | editwindow.EditWindow.__init__(self, parent, id, pos, size, style) | |
234 | self.wrap() | |
235 | if locals is None: | |
a47c63ba PB |
236 | import __main__ |
237 | locals = __main__.__dict__ | |
02b800ce | 238 | |
d14a1e28 RD |
239 | # Grab these so they can be restored by self.redirect* methods. |
240 | self.stdin = sys.stdin | |
241 | self.stdout = sys.stdout | |
242 | self.stderr = sys.stderr | |
02b800ce | 243 | |
d14a1e28 RD |
244 | # Import a default interpreter class if one isn't provided. |
245 | if InterpClass == None: | |
246 | from interpreter import Interpreter | |
247 | else: | |
248 | Interpreter = InterpClass | |
02b800ce | 249 | |
d14a1e28 RD |
250 | # Create a replacement for stdin. |
251 | self.reader = PseudoFileIn(self.readline, self.readlines) | |
252 | self.reader.input = '' | |
253 | self.reader.isreading = False | |
02b800ce | 254 | |
d14a1e28 RD |
255 | # Set up the interpreter. |
256 | self.interp = Interpreter(locals=locals, | |
257 | rawin=self.raw_input, | |
258 | stdin=self.reader, | |
259 | stdout=PseudoFileOut(self.writeOut), | |
260 | stderr=PseudoFileErr(self.writeErr), | |
261 | *args, **kwds) | |
02b800ce | 262 | |
d14a1e28 RD |
263 | # Set up the buffer. |
264 | self.buffer = Buffer() | |
02b800ce | 265 | |
d14a1e28 RD |
266 | # Find out for which keycodes the interpreter will autocomplete. |
267 | self.autoCompleteKeys = self.interp.getAutoCompleteKeys() | |
02b800ce | 268 | |
d14a1e28 RD |
269 | # Keep track of the last non-continuation prompt positions. |
270 | self.promptPosStart = 0 | |
271 | self.promptPosEnd = 0 | |
02b800ce | 272 | |
d14a1e28 RD |
273 | # Keep track of multi-line commands. |
274 | self.more = False | |
02b800ce | 275 | |
d14a1e28 RD |
276 | # Create the command history. Commands are added into the |
277 | # front of the list (ie. at index 0) as they are entered. | |
278 | # self.historyIndex is the current position in the history; it | |
279 | # gets incremented as you retrieve the previous command, | |
280 | # decremented as you retrieve the next, and reset when you hit | |
281 | # Enter. self.historyIndex == -1 means you're on the current | |
282 | # command, not in the history. | |
283 | self.history = [] | |
284 | self.historyIndex = -1 | |
02b800ce RD |
285 | |
286 | #seb add mode for "free edit" | |
287 | self.noteMode = 0 | |
288 | self.MarkerDefine(0,stc.STC_MARK_ROUNDRECT) # marker for hidden | |
289 | self.searchTxt = "" | |
290 | ||
d14a1e28 | 291 | # Assign handlers for keyboard events. |
02b800ce RD |
292 | self.Bind(wx.EVT_CHAR, self.OnChar) |
293 | self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) | |
294 | ||
d14a1e28 RD |
295 | # Assign handler for idle time. |
296 | self.waiting = False | |
02b800ce RD |
297 | self.Bind(wx.EVT_IDLE, self.OnIdle) |
298 | ||
d14a1e28 RD |
299 | # Display the introductory banner information. |
300 | self.showIntro(introText) | |
02b800ce | 301 | |
d14a1e28 RD |
302 | # Assign some pseudo keywords to the interpreter's namespace. |
303 | self.setBuiltinKeywords() | |
02b800ce | 304 | |
d14a1e28 RD |
305 | # Add 'shell' to the interpreter's local namespace. |
306 | self.setLocalShell() | |
02b800ce RD |
307 | |
308 | ## NOTE: See note at bottom of this file... | |
309 | ## #seb: File drag and drop | |
310 | ## self.SetDropTarget( FileDropTarget(self) ) | |
311 | ||
d14a1e28 RD |
312 | # Do this last so the user has complete control over their |
313 | # environment. They can override anything they want. | |
02b800ce RD |
314 | if execStartupScript: |
315 | if startupScript is None: | |
316 | startupScript = os.environ.get('PYTHONSTARTUP') | |
317 | self.execStartupScript(startupScript) | |
318 | else: | |
319 | self.prompt() | |
320 | ||
d14a1e28 RD |
321 | wx.CallAfter(self.ScrollToLine, 0) |
322 | ||
02b800ce | 323 | |
e773f79b RD |
324 | def clearHistory(self): |
325 | self.history = [] | |
326 | self.historyIndex = -1 | |
327 | dispatcher.send(signal="Shell.clearHistory") | |
328 | ||
02b800ce | 329 | |
d14a1e28 RD |
330 | def destroy(self): |
331 | del self.interp | |
332 | ||
333 | def setFocus(self): | |
334 | """Set focus to the shell.""" | |
335 | self.SetFocus() | |
336 | ||
337 | def OnIdle(self, event): | |
338 | """Free the CPU to do other things.""" | |
339 | if self.waiting: | |
340 | time.sleep(0.05) | |
341 | event.Skip() | |
342 | ||
343 | def showIntro(self, text=''): | |
344 | """Display introductory text in the shell.""" | |
345 | if text: | |
346 | if not text.endswith(os.linesep): | |
347 | text += os.linesep | |
348 | self.write(text) | |
349 | try: | |
350 | self.write(self.interp.introText) | |
351 | except AttributeError: | |
352 | pass | |
353 | ||
354 | def setBuiltinKeywords(self): | |
355 | """Create pseudo keywords as part of builtins. | |
356 | ||
9f4cc34f | 357 | This sets "close", "exit" and "quit" to a helpful string. |
d14a1e28 RD |
358 | """ |
359 | import __builtin__ | |
360 | __builtin__.close = __builtin__.exit = __builtin__.quit = \ | |
361 | 'Click on the close button to leave the application.' | |
362 | ||
02b800ce | 363 | |
d14a1e28 RD |
364 | def quit(self): |
365 | """Quit the application.""" | |
d14a1e28 | 366 | # XXX Good enough for now but later we want to send a close event. |
d14a1e28 RD |
367 | # In the close event handler we can make sure they want to |
368 | # quit. Other applications, like PythonCard, may choose to | |
369 | # hide rather than quit so we should just post the event and | |
370 | # let the surrounding app decide what it wants to do. | |
371 | self.write('Click on the close button to leave the application.') | |
372 | ||
02b800ce | 373 | |
d14a1e28 RD |
374 | def setLocalShell(self): |
375 | """Add 'shell' to locals as reference to ShellFacade instance.""" | |
376 | self.interp.locals['shell'] = ShellFacade(other=self) | |
377 | ||
02b800ce | 378 | |
d14a1e28 RD |
379 | def execStartupScript(self, startupScript): |
380 | """Execute the user's PYTHONSTARTUP script if they have one.""" | |
381 | if startupScript and os.path.isfile(startupScript): | |
382 | text = 'Startup script executed: ' + startupScript | |
383 | self.push('print %r; execfile(%r)' % (text, startupScript)) | |
02b800ce | 384 | self.interp.startupScript = startupScript |
d14a1e28 RD |
385 | else: |
386 | self.push('') | |
387 | ||
02b800ce | 388 | |
d14a1e28 RD |
389 | def about(self): |
390 | """Display information about Py.""" | |
391 | text = """ | |
392 | Author: %r | |
393 | Py Version: %s | |
394 | Py Shell Revision: %s | |
395 | Py Interpreter Revision: %s | |
396 | Python Version: %s | |
397 | wxPython Version: %s | |
02b800ce | 398 | wxPython PlatformInfo: %s |
d14a1e28 RD |
399 | Platform: %s""" % \ |
400 | (__author__, VERSION, self.revision, self.interp.revision, | |
02b800ce RD |
401 | sys.version.split()[0], wx.VERSION_STRING, str(wx.PlatformInfo), |
402 | sys.platform) | |
d14a1e28 RD |
403 | self.write(text.strip()) |
404 | ||
02b800ce | 405 | |
d14a1e28 RD |
406 | def OnChar(self, event): |
407 | """Keypress event handler. | |
408 | ||
409 | Only receives an event if OnKeyDown calls event.Skip() for the | |
410 | corresponding event.""" | |
411 | ||
02b800ce RD |
412 | if self.noteMode: |
413 | event.Skip() | |
414 | return | |
415 | ||
d14a1e28 RD |
416 | # Prevent modification of previously submitted |
417 | # commands/responses. | |
418 | if not self.CanEdit(): | |
419 | return | |
a3cee65e | 420 | key = event.GetKeyCode() |
d14a1e28 RD |
421 | currpos = self.GetCurrentPos() |
422 | stoppos = self.promptPosEnd | |
423 | # Return (Enter) needs to be ignored in this handler. | |
e773f79b | 424 | if key in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER]: |
d14a1e28 RD |
425 | pass |
426 | elif key in self.autoCompleteKeys: | |
427 | # Usually the dot (period) key activates auto completion. | |
428 | # Get the command between the prompt and the cursor. Add | |
429 | # the autocomplete character to the end of the command. | |
430 | if self.AutoCompActive(): | |
431 | self.AutoCompCancel() | |
432 | command = self.GetTextRange(stoppos, currpos) + chr(key) | |
433 | self.write(chr(key)) | |
434 | if self.autoComplete: | |
435 | self.autoCompleteShow(command) | |
436 | elif key == ord('('): | |
437 | # The left paren activates a call tip and cancels an | |
438 | # active auto completion. | |
439 | if self.AutoCompActive(): | |
440 | self.AutoCompCancel() | |
441 | # Get the command between the prompt and the cursor. Add | |
442 | # the '(' to the end of the command. | |
443 | self.ReplaceSelection('') | |
444 | command = self.GetTextRange(stoppos, currpos) + '(' | |
445 | self.write('(') | |
02b800ce | 446 | self.autoCallTipShow(command, self.GetCurrentPos() == self.GetTextLength()) |
d14a1e28 RD |
447 | else: |
448 | # Allow the normal event handling to take place. | |
449 | event.Skip() | |
450 | ||
02b800ce | 451 | |
d14a1e28 RD |
452 | def OnKeyDown(self, event): |
453 | """Key down event handler.""" | |
454 | ||
a3cee65e | 455 | key = event.GetKeyCode() |
d14a1e28 RD |
456 | # If the auto-complete window is up let it do its thing. |
457 | if self.AutoCompActive(): | |
458 | event.Skip() | |
459 | return | |
486afba9 | 460 | |
d14a1e28 RD |
461 | # Prevent modification of previously submitted |
462 | # commands/responses. | |
463 | controlDown = event.ControlDown() | |
464 | altDown = event.AltDown() | |
465 | shiftDown = event.ShiftDown() | |
466 | currpos = self.GetCurrentPos() | |
467 | endpos = self.GetTextLength() | |
468 | selecting = self.GetSelectionStart() != self.GetSelectionEnd() | |
02b800ce | 469 | |
486afba9 | 470 | if controlDown and shiftDown and key in (ord('F'), ord('f')): |
02b800ce RD |
471 | li = self.GetCurrentLine() |
472 | m = self.MarkerGet(li) | |
473 | if m & 1<<0: | |
474 | startP = self.PositionFromLine(li) | |
475 | self.MarkerDelete(li, 0) | |
476 | maxli = self.GetLineCount() | |
477 | li += 1 # li stayed visible as header-line | |
478 | li0 = li | |
479 | while li<maxli and self.GetLineVisible(li) == 0: | |
480 | li += 1 | |
481 | endP = self.GetLineEndPosition(li-1) | |
482 | self.ShowLines(li0, li-1) | |
483 | self.SetSelection( startP, endP ) # select reappearing text to allow "hide again" | |
484 | return | |
485 | startP,endP = self.GetSelection() | |
486 | endP-=1 | |
487 | startL,endL = self.LineFromPosition(startP), self.LineFromPosition(endP) | |
488 | ||
489 | if endL == self.LineFromPosition(self.promptPosEnd): # never hide last prompt | |
490 | endL -= 1 | |
491 | ||
492 | m = self.MarkerGet(startL) | |
493 | self.MarkerAdd(startL, 0) | |
494 | self.HideLines(startL+1,endL) | |
495 | self.SetCurrentPos( startP ) # to ensure caret stays visible ! | |
496 | ||
497 | if key == wx.WXK_F12: #seb | |
498 | if self.noteMode: | |
499 | # self.promptPosStart not used anyway - or ? | |
500 | self.promptPosEnd = self.PositionFromLine( self.GetLineCount()-1 ) + len(str(sys.ps1)) | |
501 | self.GotoLine(self.GetLineCount()) | |
502 | self.GotoPos(self.promptPosEnd) | |
503 | self.prompt() #make sure we have a prompt | |
504 | self.SetCaretForeground("black") | |
505 | self.SetCaretWidth(1) #default | |
506 | self.SetCaretPeriod(500) #default | |
507 | else: | |
508 | self.SetCaretForeground("red") | |
509 | self.SetCaretWidth(4) | |
510 | self.SetCaretPeriod(0) #steady | |
511 | ||
512 | self.noteMode = not self.noteMode | |
513 | return | |
514 | if self.noteMode: | |
515 | event.Skip() | |
516 | return | |
517 | ||
d14a1e28 RD |
518 | # Return (Enter) is used to submit a command to the |
519 | # interpreter. | |
e773f79b | 520 | if (not controlDown and not shiftDown and not altDown) and key in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER]: |
d14a1e28 RD |
521 | if self.CallTipActive(): |
522 | self.CallTipCancel() | |
523 | self.processLine() | |
486afba9 RD |
524 | |
525 | # Complete Text (from already typed words) | |
e773f79b | 526 | elif shiftDown and key in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER]: |
02b800ce | 527 | self.OnShowCompHistory() |
486afba9 RD |
528 | |
529 | # Ctrl+Return (Ctrl+Enter) is used to insert a line break. | |
e773f79b | 530 | elif controlDown and key in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER]: |
d14a1e28 RD |
531 | if self.CallTipActive(): |
532 | self.CallTipCancel() | |
533 | if currpos == endpos: | |
534 | self.processLine() | |
535 | else: | |
536 | self.insertLineBreak() | |
486afba9 | 537 | |
d14a1e28 RD |
538 | # Let Ctrl-Alt-* get handled normally. |
539 | elif controlDown and altDown: | |
540 | event.Skip() | |
486afba9 | 541 | |
d14a1e28 RD |
542 | # Clear the current, unexecuted command. |
543 | elif key == wx.WXK_ESCAPE: | |
544 | if self.CallTipActive(): | |
545 | event.Skip() | |
546 | else: | |
547 | self.clearCommand() | |
486afba9 | 548 | |
1ad0de84 RD |
549 | # Clear the current command |
550 | elif key == wx.WXK_BACK and controlDown and shiftDown: | |
551 | self.clearCommand() | |
552 | ||
d14a1e28 | 553 | # Increase font size. |
486afba9 | 554 | elif controlDown and key in (ord(']'), wx.WXK_NUMPAD_ADD): |
d14a1e28 | 555 | dispatcher.send(signal='FontIncrease') |
486afba9 | 556 | |
d14a1e28 | 557 | # Decrease font size. |
486afba9 | 558 | elif controlDown and key in (ord('['), wx.WXK_NUMPAD_SUBTRACT): |
d14a1e28 | 559 | dispatcher.send(signal='FontDecrease') |
486afba9 | 560 | |
d14a1e28 | 561 | # Default font size. |
486afba9 | 562 | elif controlDown and key in (ord('='), wx.WXK_NUMPAD_DIVIDE): |
d14a1e28 | 563 | dispatcher.send(signal='FontDefault') |
486afba9 | 564 | |
d14a1e28 RD |
565 | # Cut to the clipboard. |
566 | elif (controlDown and key in (ord('X'), ord('x'))) \ | |
486afba9 | 567 | or (shiftDown and key == wx.WXK_DELETE): |
d14a1e28 | 568 | self.Cut() |
486afba9 | 569 | |
d14a1e28 RD |
570 | # Copy to the clipboard. |
571 | elif controlDown and not shiftDown \ | |
486afba9 | 572 | and key in (ord('C'), ord('c'), wx.WXK_INSERT): |
d14a1e28 | 573 | self.Copy() |
486afba9 | 574 | |
d14a1e28 RD |
575 | # Copy to the clipboard, including prompts. |
576 | elif controlDown and shiftDown \ | |
486afba9 | 577 | and key in (ord('C'), ord('c'), wx.WXK_INSERT): |
d14a1e28 | 578 | self.CopyWithPrompts() |
486afba9 | 579 | |
d14a1e28 RD |
580 | # Copy to the clipboard, including prefixed prompts. |
581 | elif altDown and not controlDown \ | |
486afba9 | 582 | and key in (ord('C'), ord('c'), wx.WXK_INSERT): |
d14a1e28 | 583 | self.CopyWithPromptsPrefixed() |
486afba9 | 584 | |
d14a1e28 RD |
585 | # Home needs to be aware of the prompt. |
586 | elif key == wx.WXK_HOME: | |
587 | home = self.promptPosEnd | |
588 | if currpos > home: | |
589 | self.SetCurrentPos(home) | |
590 | if not selecting and not shiftDown: | |
591 | self.SetAnchor(home) | |
592 | self.EnsureCaretVisible() | |
593 | else: | |
594 | event.Skip() | |
486afba9 | 595 | |
d14a1e28 RD |
596 | # |
597 | # The following handlers modify text, so we need to see if | |
598 | # there is a selection that includes text prior to the prompt. | |
599 | # | |
600 | # Don't modify a selection with text prior to the prompt. | |
601 | elif selecting and key not in NAVKEYS and not self.CanEdit(): | |
602 | pass | |
486afba9 | 603 | |
d14a1e28 RD |
604 | # Paste from the clipboard. |
605 | elif (controlDown and not shiftDown and key in (ord('V'), ord('v'))) \ | |
606 | or (shiftDown and not controlDown and key == wx.WXK_INSERT): | |
607 | self.Paste() | |
486afba9 RD |
608 | |
609 | # manually invoke AutoComplete and Calltips | |
02b800ce | 610 | elif controlDown and key == wx.WXK_SPACE: |
486afba9 RD |
611 | self.OnCallTipAutoCompleteManually(shiftDown) |
612 | ||
d14a1e28 RD |
613 | # Paste from the clipboard, run commands. |
614 | elif controlDown and shiftDown and key in (ord('V'), ord('v')): | |
615 | self.PasteAndRun() | |
486afba9 | 616 | |
d14a1e28 RD |
617 | # Replace with the previous command from the history buffer. |
618 | elif (controlDown and key == wx.WXK_UP) \ | |
619 | or (altDown and key in (ord('P'), ord('p'))): | |
620 | self.OnHistoryReplace(step=+1) | |
486afba9 | 621 | |
d14a1e28 RD |
622 | # Replace with the next command from the history buffer. |
623 | elif (controlDown and key == wx.WXK_DOWN) \ | |
624 | or (altDown and key in (ord('N'), ord('n'))): | |
625 | self.OnHistoryReplace(step=-1) | |
486afba9 | 626 | |
d14a1e28 RD |
627 | # Insert the previous command from the history buffer. |
628 | elif (shiftDown and key == wx.WXK_UP) and self.CanEdit(): | |
629 | self.OnHistoryInsert(step=+1) | |
486afba9 | 630 | |
d14a1e28 RD |
631 | # Insert the next command from the history buffer. |
632 | elif (shiftDown and key == wx.WXK_DOWN) and self.CanEdit(): | |
633 | self.OnHistoryInsert(step=-1) | |
486afba9 | 634 | |
d14a1e28 RD |
635 | # Search up the history for the text in front of the cursor. |
636 | elif key == wx.WXK_F8: | |
637 | self.OnHistorySearch() | |
486afba9 | 638 | |
d14a1e28 RD |
639 | # Don't backspace over the latest non-continuation prompt. |
640 | elif key == wx.WXK_BACK: | |
641 | if selecting and self.CanEdit(): | |
642 | event.Skip() | |
643 | elif currpos > self.promptPosEnd: | |
644 | event.Skip() | |
486afba9 | 645 | |
d14a1e28 RD |
646 | # Only allow these keys after the latest prompt. |
647 | elif key in (wx.WXK_TAB, wx.WXK_DELETE): | |
648 | if self.CanEdit(): | |
649 | event.Skip() | |
486afba9 | 650 | |
d14a1e28 RD |
651 | # Don't toggle between insert mode and overwrite mode. |
652 | elif key == wx.WXK_INSERT: | |
653 | pass | |
486afba9 | 654 | |
d14a1e28 RD |
655 | # Don't allow line deletion. |
656 | elif controlDown and key in (ord('L'), ord('l')): | |
657 | pass | |
486afba9 | 658 | |
d14a1e28 RD |
659 | # Don't allow line transposition. |
660 | elif controlDown and key in (ord('T'), ord('t')): | |
661 | pass | |
486afba9 | 662 | |
d14a1e28 RD |
663 | # Basic navigation keys should work anywhere. |
664 | elif key in NAVKEYS: | |
665 | event.Skip() | |
486afba9 | 666 | |
d14a1e28 RD |
667 | # Protect the readonly portion of the shell. |
668 | elif not self.CanEdit(): | |
669 | pass | |
486afba9 | 670 | |
d14a1e28 RD |
671 | else: |
672 | event.Skip() | |
673 | ||
486afba9 | 674 | |
02b800ce RD |
675 | def OnShowCompHistory(self): |
676 | """Show possible autocompletion Words from already typed words.""" | |
677 | ||
678 | #copy from history | |
679 | his = self.history[:] | |
680 | ||
681 | #put together in one string | |
682 | joined = " ".join (his) | |
683 | import re | |
684 | ||
685 | #sort out only "good" words | |
686 | newlist = re.split("[ \.\[\]=}(\)\,0-9\"]", joined) | |
687 | ||
688 | #length > 1 (mix out "trash") | |
689 | thlist = [] | |
690 | for i in newlist: | |
691 | if len (i) > 1: | |
692 | thlist.append (i) | |
693 | ||
694 | #unique (no duplicate words | |
695 | #oneliner from german python forum => unique list | |
696 | unlist = [thlist[i] for i in xrange(len(thlist)) if thlist[i] not in thlist[:i]] | |
697 | ||
698 | #sort lowercase | |
699 | unlist.sort(lambda a, b: cmp(a.lower(), b.lower())) | |
700 | ||
701 | #this is more convenient, isn't it? | |
702 | self.AutoCompSetIgnoreCase(True) | |
703 | ||
704 | #join again together in a string | |
705 | stringlist = " ".join(unlist) | |
706 | ||
707 | #pos von 0 noch ausrechnen | |
708 | ||
709 | #how big is the offset? | |
710 | cpos = self.GetCurrentPos() - 1 | |
711 | while chr (self.GetCharAt (cpos)).isalnum(): | |
712 | cpos -= 1 | |
713 | ||
714 | #the most important part | |
715 | self.AutoCompShow(self.GetCurrentPos() - cpos -1, stringlist) | |
716 | ||
717 | ||
d14a1e28 RD |
718 | def clearCommand(self): |
719 | """Delete the current, unexecuted command.""" | |
720 | startpos = self.promptPosEnd | |
721 | endpos = self.GetTextLength() | |
722 | self.SetSelection(startpos, endpos) | |
723 | self.ReplaceSelection('') | |
724 | self.more = False | |
725 | ||
726 | def OnHistoryReplace(self, step): | |
727 | """Replace with the previous/next command from the history buffer.""" | |
728 | self.clearCommand() | |
729 | self.replaceFromHistory(step) | |
730 | ||
731 | def replaceFromHistory(self, step): | |
732 | """Replace selection with command from the history buffer.""" | |
733 | ps2 = str(sys.ps2) | |
734 | self.ReplaceSelection('') | |
735 | newindex = self.historyIndex + step | |
736 | if -1 <= newindex <= len(self.history): | |
737 | self.historyIndex = newindex | |
738 | if 0 <= newindex <= len(self.history)-1: | |
739 | command = self.history[self.historyIndex] | |
740 | command = command.replace('\n', os.linesep + ps2) | |
741 | self.ReplaceSelection(command) | |
742 | ||
743 | def OnHistoryInsert(self, step): | |
744 | """Insert the previous/next command from the history buffer.""" | |
745 | if not self.CanEdit(): | |
746 | return | |
747 | startpos = self.GetCurrentPos() | |
748 | self.replaceFromHistory(step) | |
749 | endpos = self.GetCurrentPos() | |
750 | self.SetSelection(endpos, startpos) | |
751 | ||
752 | def OnHistorySearch(self): | |
753 | """Search up the history buffer for the text in front of the cursor.""" | |
754 | if not self.CanEdit(): | |
755 | return | |
756 | startpos = self.GetCurrentPos() | |
757 | # The text up to the cursor is what we search for. | |
758 | numCharsAfterCursor = self.GetTextLength() - startpos | |
759 | searchText = self.getCommand(rstrip=False) | |
760 | if numCharsAfterCursor > 0: | |
761 | searchText = searchText[:-numCharsAfterCursor] | |
762 | if not searchText: | |
763 | return | |
764 | # Search upwards from the current history position and loop | |
765 | # back to the beginning if we don't find anything. | |
766 | if (self.historyIndex <= -1) \ | |
767 | or (self.historyIndex >= len(self.history)-2): | |
768 | searchOrder = range(len(self.history)) | |
769 | else: | |
770 | searchOrder = range(self.historyIndex+1, len(self.history)) + \ | |
771 | range(self.historyIndex) | |
772 | for i in searchOrder: | |
773 | command = self.history[i] | |
774 | if command[:len(searchText)] == searchText: | |
775 | # Replace the current selection with the one we found. | |
776 | self.ReplaceSelection(command[len(searchText):]) | |
777 | endpos = self.GetCurrentPos() | |
778 | self.SetSelection(endpos, startpos) | |
779 | # We've now warped into middle of the history. | |
780 | self.historyIndex = i | |
781 | break | |
782 | ||
783 | def setStatusText(self, text): | |
784 | """Display status information.""" | |
785 | ||
786 | # This method will likely be replaced by the enclosing app to | |
787 | # do something more interesting, like write to a status bar. | |
788 | print text | |
789 | ||
790 | def insertLineBreak(self): | |
791 | """Insert a new line break.""" | |
792 | if self.CanEdit(): | |
793 | self.write(os.linesep) | |
794 | self.more = True | |
795 | self.prompt() | |
796 | ||
797 | def processLine(self): | |
798 | """Process the line of text at which the user hit Enter.""" | |
799 | ||
800 | # The user hit ENTER and we need to decide what to do. They | |
801 | # could be sitting on any line in the shell. | |
802 | ||
803 | thepos = self.GetCurrentPos() | |
804 | startpos = self.promptPosEnd | |
805 | endpos = self.GetTextLength() | |
806 | ps2 = str(sys.ps2) | |
807 | # If they hit RETURN inside the current command, execute the | |
808 | # command. | |
809 | if self.CanEdit(): | |
810 | self.SetCurrentPos(endpos) | |
811 | self.interp.more = False | |
812 | command = self.GetTextRange(startpos, endpos) | |
813 | lines = command.split(os.linesep + ps2) | |
814 | lines = [line.rstrip() for line in lines] | |
815 | command = '\n'.join(lines) | |
816 | if self.reader.isreading: | |
817 | if not command: | |
818 | # Match the behavior of the standard Python shell | |
819 | # when the user hits return without entering a | |
820 | # value. | |
821 | command = '\n' | |
822 | self.reader.input = command | |
823 | self.write(os.linesep) | |
824 | else: | |
825 | self.push(command) | |
02b800ce | 826 | wx.FutureCall(1, self.EnsureCaretVisible) |
d14a1e28 RD |
827 | # Or replace the current command with the other command. |
828 | else: | |
829 | # If the line contains a command (even an invalid one). | |
830 | if self.getCommand(rstrip=False): | |
831 | command = self.getMultilineCommand() | |
832 | self.clearCommand() | |
833 | self.write(command) | |
834 | # Otherwise, put the cursor back where we started. | |
835 | else: | |
836 | self.SetCurrentPos(thepos) | |
837 | self.SetAnchor(thepos) | |
838 | ||
839 | def getMultilineCommand(self, rstrip=True): | |
840 | """Extract a multi-line command from the editor. | |
841 | ||
842 | The command may not necessarily be valid Python syntax.""" | |
843 | # XXX Need to extract real prompts here. Need to keep track of | |
844 | # the prompt every time a command is issued. | |
845 | ps1 = str(sys.ps1) | |
846 | ps1size = len(ps1) | |
847 | ps2 = str(sys.ps2) | |
848 | ps2size = len(ps2) | |
849 | # This is a total hack job, but it works. | |
850 | text = self.GetCurLine()[0] | |
851 | line = self.GetCurrentLine() | |
852 | while text[:ps2size] == ps2 and line > 0: | |
853 | line -= 1 | |
854 | self.GotoLine(line) | |
855 | text = self.GetCurLine()[0] | |
856 | if text[:ps1size] == ps1: | |
857 | line = self.GetCurrentLine() | |
858 | self.GotoLine(line) | |
859 | startpos = self.GetCurrentPos() + ps1size | |
860 | line += 1 | |
861 | self.GotoLine(line) | |
862 | while self.GetCurLine()[0][:ps2size] == ps2: | |
863 | line += 1 | |
864 | self.GotoLine(line) | |
865 | stoppos = self.GetCurrentPos() | |
866 | command = self.GetTextRange(startpos, stoppos) | |
867 | command = command.replace(os.linesep + ps2, '\n') | |
868 | command = command.rstrip() | |
869 | command = command.replace('\n', os.linesep + ps2) | |
870 | else: | |
871 | command = '' | |
872 | if rstrip: | |
873 | command = command.rstrip() | |
874 | return command | |
875 | ||
876 | def getCommand(self, text=None, rstrip=True): | |
877 | """Extract a command from text which may include a shell prompt. | |
878 | ||
879 | The command may not necessarily be valid Python syntax.""" | |
880 | if not text: | |
881 | text = self.GetCurLine()[0] | |
882 | # Strip the prompt off the front leaving just the command. | |
883 | command = self.lstripPrompt(text) | |
884 | if command == text: | |
885 | command = '' # Real commands have prompts. | |
886 | if rstrip: | |
887 | command = command.rstrip() | |
888 | return command | |
889 | ||
890 | def lstripPrompt(self, text): | |
891 | """Return text without a leading prompt.""" | |
892 | ps1 = str(sys.ps1) | |
893 | ps1size = len(ps1) | |
894 | ps2 = str(sys.ps2) | |
895 | ps2size = len(ps2) | |
896 | # Strip the prompt off the front of text. | |
897 | if text[:ps1size] == ps1: | |
898 | text = text[ps1size:] | |
899 | elif text[:ps2size] == ps2: | |
900 | text = text[ps2size:] | |
901 | return text | |
902 | ||
02b800ce | 903 | def push(self, command, silent = False): |
d14a1e28 | 904 | """Send command to the interpreter for execution.""" |
02b800ce RD |
905 | if not silent: |
906 | self.write(os.linesep) | |
d14a1e28 RD |
907 | busy = wx.BusyCursor() |
908 | self.waiting = True | |
909 | self.more = self.interp.push(command) | |
910 | self.waiting = False | |
911 | del busy | |
912 | if not self.more: | |
913 | self.addHistory(command.rstrip()) | |
02b800ce RD |
914 | if not silent: |
915 | self.prompt() | |
d14a1e28 RD |
916 | |
917 | def addHistory(self, command): | |
918 | """Add command to the command history.""" | |
919 | # Reset the history position. | |
920 | self.historyIndex = -1 | |
921 | # Insert this command into the history, unless it's a blank | |
922 | # line or the same as the last command. | |
923 | if command != '' \ | |
924 | and (len(self.history) == 0 or command != self.history[0]): | |
925 | self.history.insert(0, command) | |
e773f79b | 926 | dispatcher.send(signal="Shell.addHistory", command=command) |
d14a1e28 RD |
927 | |
928 | def write(self, text): | |
929 | """Display text in the shell. | |
930 | ||
931 | Replace line endings with OS-specific endings.""" | |
932 | text = self.fixLineEndings(text) | |
933 | self.AddText(text) | |
934 | self.EnsureCaretVisible() | |
935 | ||
936 | def fixLineEndings(self, text): | |
937 | """Return text with line endings replaced by OS-specific endings.""" | |
938 | lines = text.split('\r\n') | |
939 | for l in range(len(lines)): | |
940 | chunks = lines[l].split('\r') | |
941 | for c in range(len(chunks)): | |
942 | chunks[c] = os.linesep.join(chunks[c].split('\n')) | |
943 | lines[l] = os.linesep.join(chunks) | |
944 | text = os.linesep.join(lines) | |
945 | return text | |
946 | ||
947 | def prompt(self): | |
948 | """Display proper prompt for the context: ps1, ps2 or ps3. | |
949 | ||
950 | If this is a continuation line, autoindent as necessary.""" | |
951 | isreading = self.reader.isreading | |
952 | skip = False | |
953 | if isreading: | |
954 | prompt = str(sys.ps3) | |
955 | elif self.more: | |
956 | prompt = str(sys.ps2) | |
957 | else: | |
958 | prompt = str(sys.ps1) | |
959 | pos = self.GetCurLine()[1] | |
960 | if pos > 0: | |
961 | if isreading: | |
962 | skip = True | |
963 | else: | |
964 | self.write(os.linesep) | |
965 | if not self.more: | |
966 | self.promptPosStart = self.GetCurrentPos() | |
967 | if not skip: | |
968 | self.write(prompt) | |
969 | if not self.more: | |
970 | self.promptPosEnd = self.GetCurrentPos() | |
971 | # Keep the undo feature from undoing previous responses. | |
972 | self.EmptyUndoBuffer() | |
973 | # XXX Add some autoindent magic here if more. | |
974 | if self.more: | |
975 | self.write(' '*4) # Temporary hack indentation. | |
976 | self.EnsureCaretVisible() | |
977 | self.ScrollToColumn(0) | |
978 | ||
979 | def readline(self): | |
980 | """Replacement for stdin.readline().""" | |
981 | input = '' | |
982 | reader = self.reader | |
983 | reader.isreading = True | |
984 | self.prompt() | |
985 | try: | |
986 | while not reader.input: | |
987 | wx.YieldIfNeeded() | |
988 | input = reader.input | |
989 | finally: | |
990 | reader.input = '' | |
991 | reader.isreading = False | |
992 | input = str(input) # In case of Unicode. | |
993 | return input | |
994 | ||
995 | def readlines(self): | |
996 | """Replacement for stdin.readlines().""" | |
997 | lines = [] | |
998 | while lines[-1:] != ['\n']: | |
999 | lines.append(self.readline()) | |
1000 | return lines | |
1001 | ||
1002 | def raw_input(self, prompt=''): | |
1003 | """Return string based on user input.""" | |
1004 | if prompt: | |
1005 | self.write(prompt) | |
1006 | return self.readline() | |
1007 | ||
1008 | def ask(self, prompt='Please enter your response:'): | |
1009 | """Get response from the user using a dialog box.""" | |
1010 | dialog = wx.TextEntryDialog(None, prompt, | |
1011 | 'Input Dialog (Raw)', '') | |
1012 | try: | |
1013 | if dialog.ShowModal() == wx.ID_OK: | |
1014 | text = dialog.GetValue() | |
1015 | return text | |
1016 | finally: | |
1017 | dialog.Destroy() | |
1018 | return '' | |
1019 | ||
1020 | def pause(self): | |
1021 | """Halt execution pending a response from the user.""" | |
1022 | self.ask('Press enter to continue:') | |
1023 | ||
1024 | def clear(self): | |
1025 | """Delete all text from the shell.""" | |
1026 | self.ClearAll() | |
1027 | ||
1028 | def run(self, command, prompt=True, verbose=True): | |
1029 | """Execute command as if it was typed in directly. | |
1030 | >>> shell.run('print "this"') | |
1031 | >>> print "this" | |
1032 | this | |
1033 | >>> | |
1034 | """ | |
1035 | # Go to the very bottom of the text. | |
1036 | endpos = self.GetTextLength() | |
1037 | self.SetCurrentPos(endpos) | |
1038 | command = command.rstrip() | |
1039 | if prompt: self.prompt() | |
1040 | if verbose: self.write(command) | |
1041 | self.push(command) | |
1042 | ||
1043 | def runfile(self, filename): | |
1044 | """Execute all commands in file as if they were typed into the | |
1045 | shell.""" | |
1046 | file = open(filename) | |
1047 | try: | |
1048 | self.prompt() | |
1049 | for command in file.readlines(): | |
1050 | if command[:6] == 'shell.': | |
1051 | # Run shell methods silently. | |
1052 | self.run(command, prompt=False, verbose=False) | |
1053 | else: | |
1054 | self.run(command, prompt=False, verbose=True) | |
1055 | finally: | |
1056 | file.close() | |
1057 | ||
02b800ce | 1058 | def autoCompleteShow(self, command, offset = 0): |
d14a1e28 | 1059 | """Display auto-completion popup list.""" |
d351525a PB |
1060 | self.AutoCompSetAutoHide(self.autoCompleteAutoHide) |
1061 | self.AutoCompSetIgnoreCase(self.autoCompleteCaseInsensitive) | |
d14a1e28 RD |
1062 | list = self.interp.getAutoCompleteList(command, |
1063 | includeMagic=self.autoCompleteIncludeMagic, | |
1064 | includeSingle=self.autoCompleteIncludeSingle, | |
1065 | includeDouble=self.autoCompleteIncludeDouble) | |
1066 | if list: | |
1067 | options = ' '.join(list) | |
02b800ce | 1068 | #offset = 0 |
d14a1e28 RD |
1069 | self.AutoCompShow(offset, options) |
1070 | ||
02b800ce | 1071 | def autoCallTipShow(self, command, insertcalltip = True, forceCallTip = False): |
d14a1e28 RD |
1072 | """Display argument spec and docstring in a popup window.""" |
1073 | if self.CallTipActive(): | |
1074 | self.CallTipCancel() | |
1075 | (name, argspec, tip) = self.interp.getCallTip(command) | |
1076 | if tip: | |
1077 | dispatcher.send(signal='Shell.calltip', sender=self, calltip=tip) | |
02b800ce | 1078 | if not self.autoCallTip and not forceCallTip: |
d14a1e28 | 1079 | return |
02b800ce | 1080 | if argspec and insertcalltip and self.callTipInsert: |
d14a1e28 RD |
1081 | startpos = self.GetCurrentPos() |
1082 | self.write(argspec + ')') | |
1083 | endpos = self.GetCurrentPos() | |
1084 | self.SetSelection(endpos, startpos) | |
1085 | if tip: | |
1086 | curpos = self.GetCurrentPos() | |
1087 | tippos = curpos - (len(name) + 1) | |
1088 | fallback = curpos - self.GetColumn(curpos) | |
1089 | # In case there isn't enough room, only go back to the | |
1090 | # fallback. | |
1091 | tippos = max(tippos, fallback) | |
1092 | self.CallTipShow(tippos, tip) | |
02b800ce RD |
1093 | |
1094 | def OnCallTipAutoCompleteManually (self, shiftDown): | |
1095 | """AutoComplete and Calltips manually.""" | |
1096 | if self.AutoCompActive(): | |
1097 | self.AutoCompCancel() | |
1098 | currpos = self.GetCurrentPos() | |
1099 | stoppos = self.promptPosEnd | |
1100 | ||
1101 | cpos = currpos | |
1102 | #go back until '.' is found | |
1103 | pointavailpos = -1 | |
1104 | while cpos >= stoppos: | |
1105 | if self.GetCharAt(cpos) == ord ('.'): | |
1106 | pointavailpos = cpos | |
1107 | break | |
1108 | cpos -= 1 | |
1109 | ||
1110 | #word from non whitespace until '.' | |
1111 | if pointavailpos != -1: | |
1112 | #look backward for first whitespace char | |
1113 | textbehind = self.GetTextRange (pointavailpos + 1, currpos) | |
1114 | pointavailpos += 1 | |
1115 | ||
1116 | if not shiftDown: | |
1117 | #call AutoComplete | |
1118 | stoppos = self.promptPosEnd | |
1119 | textbefore = self.GetTextRange(stoppos, pointavailpos) | |
1120 | self.autoCompleteShow(textbefore, len (textbehind)) | |
1121 | else: | |
1122 | #call CallTips | |
1123 | cpos = pointavailpos | |
1124 | begpos = -1 | |
1125 | while cpos > stoppos: | |
1126 | if chr(self.GetCharAt(cpos)).isspace(): | |
1127 | begpos = cpos | |
1128 | break | |
1129 | cpos -= 1 | |
1130 | if begpos == -1: | |
1131 | begpos = cpos | |
1132 | ctips = self.GetTextRange (begpos, currpos) | |
1133 | ctindex = ctips.find ('(') | |
1134 | if ctindex != -1 and not self.CallTipActive(): | |
1135 | #insert calltip, if current pos is '(', otherwise show it only | |
095315e2 RD |
1136 | self.autoCallTipShow(ctips[:ctindex + 1], |
1137 | self.GetCharAt(currpos - 1) == ord('(') and self.GetCurrentPos() == self.GetTextLength(), | |
02b800ce RD |
1138 | True) |
1139 | ||
d14a1e28 RD |
1140 | |
1141 | def writeOut(self, text): | |
1142 | """Replacement for stdout.""" | |
1143 | self.write(text) | |
1144 | ||
1145 | def writeErr(self, text): | |
1146 | """Replacement for stderr.""" | |
1147 | self.write(text) | |
1148 | ||
1149 | def redirectStdin(self, redirect=True): | |
1150 | """If redirect is true then sys.stdin will come from the shell.""" | |
1151 | if redirect: | |
1152 | sys.stdin = self.reader | |
1153 | else: | |
1154 | sys.stdin = self.stdin | |
1155 | ||
1156 | def redirectStdout(self, redirect=True): | |
1157 | """If redirect is true then sys.stdout will go to the shell.""" | |
1158 | if redirect: | |
1159 | sys.stdout = PseudoFileOut(self.writeOut) | |
1160 | else: | |
1161 | sys.stdout = self.stdout | |
1162 | ||
1163 | def redirectStderr(self, redirect=True): | |
1164 | """If redirect is true then sys.stderr will go to the shell.""" | |
1165 | if redirect: | |
1166 | sys.stderr = PseudoFileErr(self.writeErr) | |
1167 | else: | |
1168 | sys.stderr = self.stderr | |
1169 | ||
1170 | def CanCut(self): | |
1171 | """Return true if text is selected and can be cut.""" | |
1172 | if self.GetSelectionStart() != self.GetSelectionEnd() \ | |
1173 | and self.GetSelectionStart() >= self.promptPosEnd \ | |
1174 | and self.GetSelectionEnd() >= self.promptPosEnd: | |
1175 | return True | |
1176 | else: | |
1177 | return False | |
1178 | ||
1179 | def CanPaste(self): | |
1180 | """Return true if a paste should succeed.""" | |
1181 | if self.CanEdit() and editwindow.EditWindow.CanPaste(self): | |
1182 | return True | |
1183 | else: | |
1184 | return False | |
1185 | ||
1186 | def CanEdit(self): | |
1187 | """Return true if editing should succeed.""" | |
1188 | if self.GetSelectionStart() != self.GetSelectionEnd(): | |
1189 | if self.GetSelectionStart() >= self.promptPosEnd \ | |
1190 | and self.GetSelectionEnd() >= self.promptPosEnd: | |
1191 | return True | |
1192 | else: | |
1193 | return False | |
1194 | else: | |
1195 | return self.GetCurrentPos() >= self.promptPosEnd | |
1196 | ||
1197 | def Cut(self): | |
1198 | """Remove selection and place it on the clipboard.""" | |
1199 | if self.CanCut() and self.CanCopy(): | |
1200 | if self.AutoCompActive(): | |
1201 | self.AutoCompCancel() | |
1202 | if self.CallTipActive(): | |
1203 | self.CallTipCancel() | |
1204 | self.Copy() | |
1205 | self.ReplaceSelection('') | |
1206 | ||
1207 | def Copy(self): | |
1208 | """Copy selection and place it on the clipboard.""" | |
1209 | if self.CanCopy(): | |
1210 | ps1 = str(sys.ps1) | |
1211 | ps2 = str(sys.ps2) | |
1212 | command = self.GetSelectedText() | |
1213 | command = command.replace(os.linesep + ps2, os.linesep) | |
1214 | command = command.replace(os.linesep + ps1, os.linesep) | |
1215 | command = self.lstripPrompt(text=command) | |
1216 | data = wx.TextDataObject(command) | |
1217 | self._clip(data) | |
1218 | ||
1219 | def CopyWithPrompts(self): | |
1220 | """Copy selection, including prompts, and place it on the clipboard.""" | |
1221 | if self.CanCopy(): | |
1222 | command = self.GetSelectedText() | |
1223 | data = wx.TextDataObject(command) | |
1224 | self._clip(data) | |
1225 | ||
1226 | def CopyWithPromptsPrefixed(self): | |
1227 | """Copy selection, including prompts prefixed with four | |
1228 | spaces, and place it on the clipboard.""" | |
1229 | if self.CanCopy(): | |
1230 | command = self.GetSelectedText() | |
1231 | spaces = ' ' * 4 | |
1232 | command = spaces + command.replace(os.linesep, | |
1233 | os.linesep + spaces) | |
1234 | data = wx.TextDataObject(command) | |
1235 | self._clip(data) | |
1236 | ||
1237 | def _clip(self, data): | |
1238 | if wx.TheClipboard.Open(): | |
1239 | wx.TheClipboard.UsePrimarySelection(False) | |
1240 | wx.TheClipboard.SetData(data) | |
1241 | wx.TheClipboard.Flush() | |
1242 | wx.TheClipboard.Close() | |
1243 | ||
1244 | def Paste(self): | |
1245 | """Replace selection with clipboard contents.""" | |
1246 | if self.CanPaste() and wx.TheClipboard.Open(): | |
1247 | ps2 = str(sys.ps2) | |
1248 | if wx.TheClipboard.IsSupported(wx.DataFormat(wx.DF_TEXT)): | |
1249 | data = wx.TextDataObject() | |
1250 | if wx.TheClipboard.GetData(data): | |
1251 | self.ReplaceSelection('') | |
1252 | command = data.GetText() | |
1253 | command = command.rstrip() | |
1254 | command = self.fixLineEndings(command) | |
1255 | command = self.lstripPrompt(text=command) | |
1256 | command = command.replace(os.linesep + ps2, '\n') | |
1257 | command = command.replace(os.linesep, '\n') | |
1258 | command = command.replace('\n', os.linesep + ps2) | |
1259 | self.write(command) | |
1260 | wx.TheClipboard.Close() | |
1261 | ||
095315e2 | 1262 | |
d14a1e28 RD |
1263 | def PasteAndRun(self): |
1264 | """Replace selection with clipboard contents, run commands.""" | |
095315e2 | 1265 | text = '' |
d14a1e28 | 1266 | if wx.TheClipboard.Open(): |
d14a1e28 RD |
1267 | if wx.TheClipboard.IsSupported(wx.DataFormat(wx.DF_TEXT)): |
1268 | data = wx.TextDataObject() | |
1269 | if wx.TheClipboard.GetData(data): | |
d14a1e28 | 1270 | text = data.GetText() |
d14a1e28 | 1271 | wx.TheClipboard.Close() |
095315e2 RD |
1272 | if text: |
1273 | self.Execute(text) | |
1274 | ||
1275 | ||
1276 | def Execute(self, text): | |
1277 | """Replace selection with text and run commands.""" | |
1278 | ps1 = str(sys.ps1) | |
1279 | ps2 = str(sys.ps2) | |
1280 | endpos = self.GetTextLength() | |
1281 | self.SetCurrentPos(endpos) | |
1282 | startpos = self.promptPosEnd | |
1283 | self.SetSelection(startpos, endpos) | |
1284 | self.ReplaceSelection('') | |
1285 | text = text.lstrip() | |
1286 | text = self.fixLineEndings(text) | |
1287 | text = self.lstripPrompt(text) | |
1288 | text = text.replace(os.linesep + ps1, '\n') | |
1289 | text = text.replace(os.linesep + ps2, '\n') | |
1290 | text = text.replace(os.linesep, '\n') | |
1291 | lines = text.split('\n') | |
1292 | commands = [] | |
1293 | command = '' | |
1294 | for line in lines: | |
1295 | if line.strip() == ps2.strip(): | |
1296 | # If we are pasting from something like a | |
1297 | # web page that drops the trailing space | |
1298 | # from the ps2 prompt of a blank line. | |
1299 | line = '' | |
1300 | lstrip = line.lstrip() | |
1301 | if line.strip() != '' and lstrip == line and \ | |
1302 | lstrip[:4] not in ['else','elif'] and \ | |
1303 | lstrip[:6] != 'except': | |
1304 | # New command. | |
1305 | if command: | |
1306 | # Add the previous command to the list. | |
1307 | commands.append(command) | |
1308 | # Start a new command, which may be multiline. | |
1309 | command = line | |
1310 | else: | |
1311 | # Multiline command. Add to the command. | |
1312 | command += '\n' | |
1313 | command += line | |
1314 | commands.append(command) | |
1315 | for command in commands: | |
1316 | command = command.replace('\n', os.linesep + ps2) | |
1317 | self.write(command) | |
1318 | self.processLine() | |
1319 | ||
d14a1e28 RD |
1320 | |
1321 | def wrap(self, wrap=True): | |
1322 | """Sets whether text is word wrapped.""" | |
1323 | try: | |
1324 | self.SetWrapMode(wrap) | |
1325 | except AttributeError: | |
1326 | return 'Wrapping is not available in this version.' | |
1327 | ||
1328 | def zoom(self, points=0): | |
1329 | """Set the zoom level. | |
1330 | ||
1331 | This number of points is added to the size of all fonts. It | |
1332 | may be positive to magnify or negative to reduce.""" | |
1333 | self.SetZoom(points) | |
02b800ce RD |
1334 | |
1335 | ||
1336 | ||
1337 | def LoadSettings(self, config): | |
1338 | self.autoComplete = config.ReadBool('Options/AutoComplete', True) | |
1339 | self.autoCompleteIncludeMagic = config.ReadBool('Options/AutoCompleteIncludeMagic', True) | |
1340 | self.autoCompleteIncludeSingle = config.ReadBool('Options/AutoCompleteIncludeSingle', True) | |
1341 | self.autoCompleteIncludeDouble = config.ReadBool('Options/AutoCompleteIncludeDouble', True) | |
1342 | ||
1343 | self.autoCallTip = config.ReadBool('Options/AutoCallTip', True) | |
1344 | self.callTipInsert = config.ReadBool('Options/CallTipInsert', True) | |
1345 | self.SetWrapMode(config.ReadBool('View/WrapMode', True)) | |
1346 | ||
1347 | useAA = config.ReadBool('Options/UseAntiAliasing', self.GetUseAntiAliasing()) | |
1348 | self.SetUseAntiAliasing(useAA) | |
1349 | self.lineNumbers = config.ReadBool('View/ShowLineNumbers', True) | |
1350 | self.setDisplayLineNumbers (self.lineNumbers) | |
1351 | zoom = config.ReadInt('View/Zoom/Shell', -99) | |
1352 | if zoom != -99: | |
1353 | self.SetZoom(zoom) | |
1354 | ||
1355 | ||
1356 | ||
1357 | def SaveSettings(self, config): | |
1358 | config.WriteBool('Options/AutoComplete', self.autoComplete) | |
1359 | config.WriteBool('Options/AutoCompleteIncludeMagic', self.autoCompleteIncludeMagic) | |
1360 | config.WriteBool('Options/AutoCompleteIncludeSingle', self.autoCompleteIncludeSingle) | |
1361 | config.WriteBool('Options/AutoCompleteIncludeDouble', self.autoCompleteIncludeDouble) | |
1362 | config.WriteBool('Options/AutoCallTip', self.autoCallTip) | |
1363 | config.WriteBool('Options/CallTipInsert', self.callTipInsert) | |
1364 | config.WriteBool('Options/UseAntiAliasing', self.GetUseAntiAliasing()) | |
1365 | config.WriteBool('View/WrapMode', self.GetWrapMode()) | |
1366 | config.WriteBool('View/ShowLineNumbers', self.lineNumbers) | |
1367 | config.WriteInt('View/Zoom/Shell', self.GetZoom()) | |
1368 | ||
1369 | ||
1370 | ||
1371 | ## NOTE: The DnD of file names is disabled until I can figure out how | |
1372 | ## best to still allow DnD of text. | |
1373 | ||
1374 | ||
1375 | ## #seb : File drag and drop | |
1376 | ## class FileDropTarget(wx.FileDropTarget): | |
1377 | ## def __init__(self, obj): | |
1378 | ## wx.FileDropTarget.__init__(self) | |
1379 | ## self.obj = obj | |
1380 | ## def OnDropFiles(self, x, y, filenames): | |
1381 | ## if len(filenames) == 1: | |
1382 | ## txt = 'r\"%s\"' % filenames[0] | |
1383 | ## else: | |
1384 | ## txt = '( ' | |
1385 | ## for f in filenames: | |
1386 | ## txt += 'r\"%s\" , ' % f | |
1387 | ## txt += ')' | |
1388 | ## self.obj.AppendText(txt) | |
1389 | ## pos = self.obj.GetCurrentPos() | |
1390 | ## self.obj.SetCurrentPos( pos ) | |
1391 | ## self.obj.SetSelection( pos, pos ) | |
1392 | ||
1393 | ||
1394 | ||
1395 | ## class TextAndFileDropTarget(wx.DropTarget): | |
1396 | ## def __init__(self, shell): | |
1397 | ## wx.DropTarget.__init__(self) | |
1398 | ## self.shell = shell | |
1399 | ## self.compdo = wx.DataObjectComposite() | |
1400 | ## self.textdo = wx.TextDataObject() | |
1401 | ## self.filedo = wx.FileDataObject() | |
1402 | ## self.compdo.Add(self.textdo) | |
1403 | ## self.compdo.Add(self.filedo, True) | |
1404 | ||
1405 | ## self.SetDataObject(self.compdo) | |
1406 | ||
1407 | ## def OnDrop(self, x, y): | |
1408 | ## return True | |
1409 | ||
1410 | ## def OnData(self, x, y, result): | |
1411 | ## self.GetData() | |
1412 | ## if self.textdo.GetTextLength() > 1: | |
1413 | ## text = self.textdo.GetText() | |
1414 | ## # *** Do somethign with the dragged text here... | |
1415 | ## self.textdo.SetText('') | |
1416 | ## else: | |
1417 | ## filenames = str(self.filename.GetFilenames()) | |
1418 | ## if len(filenames) == 1: | |
1419 | ## txt = 'r\"%s\"' % filenames[0] | |
1420 | ## else: | |
1421 | ## txt = '( ' | |
1422 | ## for f in filenames: | |
1423 | ## txt += 'r\"%s\" , ' % f | |
1424 | ## txt += ')' | |
1425 | ## self.shell.AppendText(txt) | |
1426 | ## pos = self.shell.GetCurrentPos() | |
1427 | ## self.shell.SetCurrentPos( pos ) | |
1428 | ## self.shell.SetSelection( pos, pos ) | |
1429 | ||
1430 | ## return result | |
1431 |