cocoa_frontend.py
429 lines
| 14.2 KiB
| text/x-python
|
PythonLexer
Barry Wark
|
r1263 | # encoding: utf-8 | ||
Barry Wark
|
r1291 | # -*- test-case-name: IPython.frontend.cocoa.tests.test_cocoa_frontend -*- | ||
Barry Wark
|
r1263 | |||
Barry Wark
|
r1291 | """PyObjC classes to provide a Cocoa frontend to the | ||
Barry Wark
|
r1292 | IPython.kernel.engineservice.IEngineBase. | ||
Barry Wark
|
r1263 | |||
Barry Wark
|
r1291 | To add an IPython interpreter to a cocoa app, instantiate an | ||
IPythonCocoaController in a XIB and connect its textView outlet to an | ||||
NSTextView instance in your UI. That's it. | ||||
Barry Wark
|
r1263 | |||
Barry Wark
|
r1291 | Author: Barry Wark | ||
Barry Wark
|
r1263 | """ | ||
__docformat__ = "restructuredtext en" | ||||
Barry Wark
|
r1291 | #----------------------------------------------------------------------------- | ||
# Copyright (C) 2008 The IPython Development Team | ||||
Barry Wark
|
r1263 | # | ||
# Distributed under the terms of the BSD License. The full license is in | ||||
# the file COPYING, distributed as part of this software. | ||||
Barry Wark
|
r1291 | #----------------------------------------------------------------------------- | ||
Barry Wark
|
r1263 | |||
Barry Wark
|
r1291 | #----------------------------------------------------------------------------- | ||
Barry Wark
|
r1263 | # Imports | ||
Barry Wark
|
r1291 | #----------------------------------------------------------------------------- | ||
Barry Wark
|
r1263 | |||
import objc | ||||
import uuid | ||||
from Foundation import NSObject, NSMutableArray, NSMutableDictionary,\ | ||||
NSLog, NSNotificationCenter, NSMakeRange,\ | ||||
NSLocalizedString, NSIntersectionRange | ||||
Barry Wark
|
r1279 | |||
Barry Wark
|
r1263 | from AppKit import NSApplicationWillTerminateNotification, NSBeep,\ | ||
NSTextView, NSRulerView, NSVerticalRuler | ||||
from pprint import saferepr | ||||
Barry Wark
|
r1278 | import IPython | ||
Barry Wark
|
r1292 | from IPython.kernel.engineservice import ThreadedEngineService | ||
Barry Wark
|
r1263 | from IPython.frontend.frontendbase import FrontEndBase | ||
from twisted.internet.threads import blockingCallFromThread | ||||
Barry Wark
|
r1283 | from twisted.python.failure import Failure | ||
Barry Wark
|
r1263 | |||
Barry Wark
|
r1291 | #------------------------------------------------------------------------------ | ||
Barry Wark
|
r1263 | # Classes to implement the Cocoa frontend | ||
Barry Wark
|
r1291 | #------------------------------------------------------------------------------ | ||
Barry Wark
|
r1263 | |||
# TODO: | ||||
Barry Wark
|
r1291 | # 1. use MultiEngineClient and out-of-process engine rather than | ||
# ThreadedEngineService? | ||||
Barry Wark
|
r1263 | # 2. integrate Xgrid launching of engines | ||
class IPythonCocoaController(NSObject, FrontEndBase): | ||||
userNS = objc.ivar() #mirror of engine.user_ns (key=>str(value)) | ||||
waitingForEngine = objc.ivar().bool() | ||||
textView = objc.IBOutlet() | ||||
def init(self): | ||||
self = super(IPythonCocoaController, self).init() | ||||
FrontEndBase.__init__(self, engine=ThreadedEngineService()) | ||||
if(self != None): | ||||
self._common_init() | ||||
return self | ||||
def _common_init(self): | ||||
"""_common_init""" | ||||
self.userNS = NSMutableDictionary.dictionary() | ||||
self.waitingForEngine = False | ||||
self.lines = {} | ||||
self.tabSpaces = 4 | ||||
self.tabUsesSpaces = True | ||||
Barry Wark
|
r1292 | self.currentBlockID = self.next_block_ID() | ||
Barry Wark
|
r1263 | self.blockRanges = {} # blockID=>NSRange | ||
def awakeFromNib(self): | ||||
"""awakeFromNib""" | ||||
self._common_init() | ||||
# Start the IPython engine | ||||
self.engine.startService() | ||||
NSLog('IPython engine started') | ||||
# Register for app termination | ||||
Barry Wark
|
r1292 | nc = NSNotificationCenter.defaultCenter() | ||
nc.addObserver_selector_name_object_( | ||||
self, | ||||
'appWillTerminate:', | ||||
NSApplicationWillTerminateNotification, | ||||
None) | ||||
Barry Wark
|
r1263 | |||
self.textView.setDelegate_(self) | ||||
self.textView.enclosingScrollView().setHasVerticalRuler_(True) | ||||
Barry Wark
|
r1292 | r = NSRulerView.alloc().initWithScrollView_orientation_( | ||
self.textView.enclosingScrollView(), | ||||
NSVerticalRuler) | ||||
self.verticalRulerView = r | ||||
Barry Wark
|
r1263 | self.verticalRulerView.setClientView_(self.textView) | ||
Barry Wark
|
r1292 | self._start_cli_banner() | ||
Barry Wark
|
r1263 | |||
def appWillTerminate_(self, notification): | ||||
"""appWillTerminate""" | ||||
self.engine.stopService() | ||||
def complete(self, token): | ||||
"""Complete token in engine's user_ns | ||||
Parameters | ||||
---------- | ||||
token : string | ||||
Result | ||||
------ | ||||
Barry Wark
|
r1292 | Deferred result of | ||
IPython.kernel.engineservice.IEngineBase.complete | ||||
Barry Wark
|
r1263 | """ | ||
return self.engine.complete(token) | ||||
def execute(self, block, blockID=None): | ||||
self.waitingForEngine = True | ||||
self.willChangeValueForKey_('commandHistory') | ||||
d = super(IPythonCocoaController, self).execute(block, blockID) | ||||
Barry Wark
|
r1292 | d.addBoth(self._engine_done) | ||
d.addCallback(self._update_user_ns) | ||||
Barry Wark
|
r1263 | |||
return d | ||||
Barry Wark
|
r1292 | def _engine_done(self, x): | ||
Barry Wark
|
r1263 | self.waitingForEngine = False | ||
self.didChangeValueForKey_('commandHistory') | ||||
return x | ||||
Barry Wark
|
r1292 | def _update_user_ns(self, result): | ||
Barry Wark
|
r1263 | """Update self.userNS from self.engine's namespace""" | ||
d = self.engine.keys() | ||||
Barry Wark
|
r1292 | d.addCallback(self._get_engine_namespace_values_for_keys) | ||
Barry Wark
|
r1263 | |||
return result | ||||
Barry Wark
|
r1292 | def _get_engine_namespace_values_for_keys(self, keys): | ||
Barry Wark
|
r1263 | d = self.engine.pull(keys) | ||
Barry Wark
|
r1292 | d.addCallback(self._store_engine_namespace_values, keys=keys) | ||
Barry Wark
|
r1263 | |||
Barry Wark
|
r1292 | def _store_engine_namespace_values(self, values, keys=[]): | ||
Barry Wark
|
r1263 | assert(len(values) == len(keys)) | ||
self.willChangeValueForKey_('userNS') | ||||
for (k,v) in zip(keys,values): | ||||
self.userNS[k] = saferepr(v) | ||||
self.didChangeValueForKey_('userNS') | ||||
Barry Wark
|
r1292 | def update_cell_prompt(self, result): | ||
if(isinstance(result, Failure)): | ||||
blockID = result.blockID | ||||
else: | ||||
blockID = result['blockID'] | ||||
self.insert_text(self.input_prompt(result=result), | ||||
textRange=NSMakeRange(self.blockRanges[blockID].location,0), | ||||
scrollToVisible=False | ||||
) | ||||
return result | ||||
def render_result(self, result): | ||||
blockID = result['blockID'] | ||||
inputRange = self.blockRanges[blockID] | ||||
del self.blockRanges[blockID] | ||||
#print inputRange,self.current_block_range() | ||||
self.insert_text('\n' + | ||||
self.output_prompt(result) + | ||||
result.get('display',{}).get('pprint','') + | ||||
'\n\n', | ||||
textRange=NSMakeRange(inputRange.location+inputRange.length, | ||||
0)) | ||||
return result | ||||
def render_error(self, failure): | ||||
self.insert_text('\n\n'+str(failure)+'\n\n') | ||||
self.start_new_block() | ||||
return failure | ||||
def _start_cli_banner(self): | ||||
Barry Wark
|
r1263 | """Print banner""" | ||
Barry Wark
|
r1291 | banner = """IPython1 %s -- An enhanced Interactive Python.""" % \ | ||
IPython.__version__ | ||||
Barry Wark
|
r1263 | |||
self.insert_text(banner + '\n\n') | ||||
Barry Wark
|
r1292 | |||
def start_new_block(self): | ||||
"""""" | ||||
self.currentBlockID = self.next_block_ID() | ||||
def next_block_ID(self): | ||||
return uuid.uuid4() | ||||
def current_block_range(self): | ||||
return self.blockRanges.get(self.currentBlockID, | ||||
NSMakeRange(self.textView.textStorage().length(), | ||||
0)) | ||||
Barry Wark
|
r1293 | def current_block(self): | ||
Barry Wark
|
r1292 | """The current block's text""" | ||
Barry Wark
|
r1293 | return self.text_for_range(self.current_block_range()) | ||
Barry Wark
|
r1292 | |||
Barry Wark
|
r1293 | def text_for_range(self, textRange): | ||
"""text_for_range""" | ||||
Barry Wark
|
r1292 | |||
ts = self.textView.textStorage() | ||||
return ts.string().substringWithRange_(textRange) | ||||
Barry Wark
|
r1293 | def current_line(self): | ||
block = self.text_for_range(self.current_block_range()) | ||||
Barry Wark
|
r1292 | block = block.split('\n') | ||
return block[-1] | ||||
def insert_text(self, string=None, textRange=None, scrollToVisible=True): | ||||
"""Insert text into textView at textRange, updating blockRanges | ||||
as necessary | ||||
""" | ||||
if(textRange == None): | ||||
#range for end of text | ||||
textRange = NSMakeRange(self.textView.textStorage().length(), 0) | ||||
for r in self.blockRanges.itervalues(): | ||||
intersection = NSIntersectionRange(r,textRange) | ||||
if(intersection.length == 0): #ranges don't intersect | ||||
if r.location >= textRange.location: | ||||
r.location += len(string) | ||||
else: #ranges intersect | ||||
if(r.location <= textRange.location): | ||||
assert(intersection.length == textRange.length) | ||||
r.length += textRange.length | ||||
else: | ||||
r.location += intersection.length | ||||
self.textView.replaceCharactersInRange_withString_( | ||||
textRange, string) | ||||
self.textView.setSelectedRange_( | ||||
NSMakeRange(textRange.location+len(string), 0)) | ||||
if(scrollToVisible): | ||||
self.textView.scrollRangeToVisible_(textRange) | ||||
def replace_current_block_with_string(self, textView, string): | ||||
textView.replaceCharactersInRange_withString_( | ||||
self.current_block_range(), | ||||
string) | ||||
self.current_block_range().length = len(string) | ||||
r = NSMakeRange(textView.textStorage().length(), 0) | ||||
textView.scrollRangeToVisible_(r) | ||||
textView.setSelectedRange_(r) | ||||
def current_indent_string(self): | ||||
"""returns string for indent or None if no indent""" | ||||
Barry Wark
|
r1300 | return self._indent_for_block(self.currentBlock()) | ||
def _indent_for_block(self, block): | ||||
lines = block.split('\n') | ||||
if(len(lines) > 1): | ||||
currentIndent = len(lines[-1]) - len(lines[-1].lstrip()) | ||||
Barry Wark
|
r1292 | if(currentIndent == 0): | ||
currentIndent = self.tabSpaces | ||||
if(self.tabUsesSpaces): | ||||
result = ' ' * currentIndent | ||||
else: | ||||
result = '\t' * (currentIndent/self.tabSpaces) | ||||
else: | ||||
result = None | ||||
return result | ||||
# NSTextView delegate methods... | ||||
Barry Wark
|
r1263 | def textView_doCommandBySelector_(self, textView, selector): | ||
assert(textView == self.textView) | ||||
NSLog("textView_doCommandBySelector_: "+selector) | ||||
if(selector == 'insertNewline:'): | ||||
Barry Wark
|
r1292 | indent = self.current_indent_string() | ||
Barry Wark
|
r1263 | if(indent): | ||
Barry Wark
|
r1293 | line = indent + self.current_line() | ||
Barry Wark
|
r1263 | else: | ||
Barry Wark
|
r1293 | line = self.current_line() | ||
Barry Wark
|
r1263 | |||
Barry Wark
|
r1293 | if(self.is_complete(self.current_block())): | ||
self.execute(self.current_block(), | ||||
Barry Wark
|
r1263 | blockID=self.currentBlockID) | ||
Barry Wark
|
r1292 | self.start_new_block() | ||
Barry Wark
|
r1279 | |||
Barry Wark
|
r1263 | return True | ||
return False | ||||
elif(selector == 'moveUp:'): | ||||
Barry Wark
|
r1293 | prevBlock = self.get_history_previous(self.current_block()) | ||
Barry Wark
|
r1279 | if(prevBlock != None): | ||
Barry Wark
|
r1292 | self.replace_current_block_with_string(textView, prevBlock) | ||
Barry Wark
|
r1279 | else: | ||
NSBeep() | ||||
Barry Wark
|
r1263 | return True | ||
elif(selector == 'moveDown:'): | ||||
Barry Wark
|
r1281 | nextBlock = self.get_history_next() | ||
Barry Wark
|
r1279 | if(nextBlock != None): | ||
Barry Wark
|
r1292 | self.replace_current_block_with_string(textView, nextBlock) | ||
Barry Wark
|
r1279 | else: | ||
NSBeep() | ||||
Barry Wark
|
r1263 | return True | ||
elif(selector == 'moveToBeginningOfParagraph:'): | ||||
Barry Wark
|
r1291 | textView.setSelectedRange_(NSMakeRange( | ||
Barry Wark
|
r1292 | self.current_block_range().location, | ||
Barry Wark
|
r1291 | 0)) | ||
Barry Wark
|
r1263 | return True | ||
elif(selector == 'moveToEndOfParagraph:'): | ||||
Barry Wark
|
r1291 | textView.setSelectedRange_(NSMakeRange( | ||
Barry Wark
|
r1292 | self.current_block_range().location + \ | ||
self.current_block_range().length, 0)) | ||||
Barry Wark
|
r1263 | return True | ||
elif(selector == 'deleteToEndOfParagraph:'): | ||||
Barry Wark
|
r1291 | if(textView.selectedRange().location <= \ | ||
Barry Wark
|
r1292 | self.current_block_range().location): | ||
Barry Wark
|
r1263 | # Intersect the selected range with the current line range | ||
Barry Wark
|
r1292 | if(self.current_block_range().length < 0): | ||
Barry Wark
|
r1263 | self.blockRanges[self.currentBlockID].length = 0 | ||
r = NSIntersectionRange(textView.rangesForUserTextChange()[0], | ||||
Barry Wark
|
r1292 | self.current_block_range()) | ||
Barry Wark
|
r1263 | |||
if(r.length > 0): #no intersection | ||||
textView.setSelectedRange_(r) | ||||
return False # don't actually handle the delete | ||||
elif(selector == 'insertTab:'): | ||||
Barry Wark
|
r1293 | if(len(self.current_line().strip()) == 0): #only white space | ||
Barry Wark
|
r1263 | return False | ||
else: | ||||
self.textView.complete_(self) | ||||
return True | ||||
elif(selector == 'deleteBackward:'): | ||||
#if we're at the beginning of the current block, ignore | ||||
Barry Wark
|
r1291 | if(textView.selectedRange().location == \ | ||
Barry Wark
|
r1292 | self.current_block_range().location): | ||
Barry Wark
|
r1263 | return True | ||
else: | ||||
Barry Wark
|
r1292 | self.current_block_range().length-=1 | ||
Barry Wark
|
r1263 | return False | ||
return False | ||||
Barry Wark
|
r1291 | def textView_shouldChangeTextInRanges_replacementStrings_(self, | ||
textView, ranges, replacementStrings): | ||||
Barry Wark
|
r1263 | """ | ||
Delegate method for NSTextView. | ||||
Barry Wark
|
r1291 | Refuse change text in ranges not at end, but make those changes at | ||
end. | ||||
Barry Wark
|
r1263 | """ | ||
assert(len(ranges) == len(replacementStrings)) | ||||
allow = True | ||||
for r,s in zip(ranges, replacementStrings): | ||||
r = r.rangeValue() | ||||
if(textView.textStorage().length() > 0 and | ||||
Barry Wark
|
r1292 | r.location < self.current_block_range().location): | ||
Barry Wark
|
r1263 | self.insert_text(s) | ||
allow = False | ||||
Barry Wark
|
r1291 | self.blockRanges.setdefault(self.currentBlockID, | ||
Barry Wark
|
r1292 | self.current_block_range()).length +=\ | ||
Barry Wark
|
r1291 | len(s) | ||
Barry Wark
|
r1263 | |||
return allow | ||||
Barry Wark
|
r1291 | def textView_completions_forPartialWordRange_indexOfSelectedItem_(self, | ||
textView, words, charRange, index): | ||||
Barry Wark
|
r1263 | try: | ||
Barry Wark
|
r1291 | ts = textView.textStorage() | ||
token = ts.string().substringWithRange_(charRange) | ||||
Barry Wark
|
r1263 | completions = blockingCallFromThread(self.complete, token) | ||
except: | ||||
completions = objc.nil | ||||
NSBeep() | ||||
return (completions,0) | ||||