cocoa_frontend.py
369 lines
| 13.1 KiB
| text/x-python
|
PythonLexer
Barry Wark
|
r1263 | # encoding: utf-8 | ||
# -*- test-case-name: ipython1.frontend.cocoa.tests.test_cocoa_frontend -*- | ||||
"""PyObjC classes to provide a Cocoa frontend to the ipython1.kernel.engineservice.EngineService. | ||||
The Cocoa frontend is divided into two classes: | ||||
- IPythonCocoaController | ||||
- IPythonCLITextViewDelegate | ||||
To add an IPython interpreter to a cocoa app, instantiate both of these classes in an XIB...[FINISH] | ||||
""" | ||||
__docformat__ = "restructuredtext en" | ||||
#------------------------------------------------------------------------------- | ||||
# Copyright (C) 2008 Barry Wark <barrywark@gmail.com> | ||||
# | ||||
# Distributed under the terms of the BSD License. The full license is in | ||||
# the file COPYING, distributed as part of this software. | ||||
#------------------------------------------------------------------------------- | ||||
#------------------------------------------------------------------------------- | ||||
# Imports | ||||
#------------------------------------------------------------------------------- | ||||
import objc | ||||
import uuid | ||||
from Foundation import NSObject, NSMutableArray, NSMutableDictionary,\ | ||||
NSLog, NSNotificationCenter, NSMakeRange,\ | ||||
NSLocalizedString, NSIntersectionRange | ||||
from AppKit import NSApplicationWillTerminateNotification, NSBeep,\ | ||||
NSTextView, NSRulerView, NSVerticalRuler | ||||
from pprint import saferepr | ||||
from IPython.kernel.engineservice import EngineService, ThreadedEngineService | ||||
from IPython.frontend.frontendbase import FrontEndBase | ||||
from twisted.internet.threads import blockingCallFromThread | ||||
#------------------------------------------------------------------------------- | ||||
# Classes to implement the Cocoa frontend | ||||
#------------------------------------------------------------------------------- | ||||
# TODO: | ||||
# 1. use MultiEngineClient and out-of-process engine rather than ThreadedEngineService? | ||||
# 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 | ||||
self.currentBlockID = self.nextBlockID() | ||||
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 | ||||
NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, | ||||
'appWillTerminate:', | ||||
NSApplicationWillTerminateNotification, | ||||
None) | ||||
self.textView.setDelegate_(self) | ||||
self.textView.enclosingScrollView().setHasVerticalRuler_(True) | ||||
self.verticalRulerView = NSRulerView.alloc().initWithScrollView_orientation_( | ||||
self.textView.enclosingScrollView(), | ||||
NSVerticalRuler) | ||||
self.verticalRulerView.setClientView_(self.textView) | ||||
self.startCLIForTextView() | ||||
def appWillTerminate_(self, notification): | ||||
"""appWillTerminate""" | ||||
self.engine.stopService() | ||||
def complete(self, token): | ||||
"""Complete token in engine's user_ns | ||||
Parameters | ||||
---------- | ||||
token : string | ||||
Result | ||||
------ | ||||
Deferred result of ipython1.kernel.engineservice.IEngineInteractive.complete | ||||
""" | ||||
return self.engine.complete(token) | ||||
def execute(self, block, blockID=None): | ||||
self.waitingForEngine = True | ||||
self.willChangeValueForKey_('commandHistory') | ||||
d = super(IPythonCocoaController, self).execute(block, blockID) | ||||
d.addBoth(self._engineDone) | ||||
d.addCallback(self._updateUserNS) | ||||
return d | ||||
def _engineDone(self, x): | ||||
self.waitingForEngine = False | ||||
self.didChangeValueForKey_('commandHistory') | ||||
return x | ||||
def _updateUserNS(self, result): | ||||
"""Update self.userNS from self.engine's namespace""" | ||||
d = self.engine.keys() | ||||
d.addCallback(self._getEngineNamepsaceValuesForKeys) | ||||
return result | ||||
def _getEngineNamepsaceValuesForKeys(self, keys): | ||||
d = self.engine.pull(keys) | ||||
d.addCallback(self._storeEngineNamespaceValues, keys=keys) | ||||
def _storeEngineNamespaceValues(self, values, keys=[]): | ||||
assert(len(values) == len(keys)) | ||||
self.willChangeValueForKey_('userNS') | ||||
for (k,v) in zip(keys,values): | ||||
self.userNS[k] = saferepr(v) | ||||
self.didChangeValueForKey_('userNS') | ||||
def startCLIForTextView(self): | ||||
"""Print banner""" | ||||
banner = """IPython1 0.X -- An enhanced Interactive Python.""" | ||||
self.insert_text(banner + '\n\n') | ||||
# NSTextView/IPythonTextView delegate methods | ||||
def textView_doCommandBySelector_(self, textView, selector): | ||||
assert(textView == self.textView) | ||||
NSLog("textView_doCommandBySelector_: "+selector) | ||||
if(selector == 'insertNewline:'): | ||||
indent = self.currentIndentString() | ||||
if(indent): | ||||
line = indent + self.currentLine() | ||||
else: | ||||
line = self.currentLine() | ||||
if(self.is_complete(self.currentBlock())): | ||||
self.execute(self.currentBlock(), | ||||
blockID=self.currentBlockID) | ||||
self.currentBlockID = self.nextBlockID() | ||||
return True | ||||
return False | ||||
elif(selector == 'moveUp:'): | ||||
self.replaceCurrentBlockWithString(textView, self.get_history_item_previous(self.currentBlock())) | ||||
return True | ||||
elif(selector == 'moveDown:'): | ||||
self.replaceCurrentBlockWithString(textView, self.get_history_item_next(self.currentBlock())) | ||||
return True | ||||
elif(selector == 'moveToBeginningOfParagraph:'): | ||||
textView.setSelectedRange_(NSMakeRange(self.currentBlockRange().location, 0)) | ||||
return True | ||||
elif(selector == 'moveToEndOfParagraph:'): | ||||
textView.setSelectedRange_(NSMakeRange(self.currentBlockRange().location + \ | ||||
self.currentBlockRange().length, 0)) | ||||
return True | ||||
elif(selector == 'deleteToEndOfParagraph:'): | ||||
if(textView.selectedRange().location <= self.currentBlockRange().location): | ||||
# Intersect the selected range with the current line range | ||||
if(self.currentBlockRange().length < 0): | ||||
self.blockRanges[self.currentBlockID].length = 0 | ||||
r = NSIntersectionRange(textView.rangesForUserTextChange()[0], | ||||
self.currentBlockRange()) | ||||
if(r.length > 0): #no intersection | ||||
textView.setSelectedRange_(r) | ||||
return False # don't actually handle the delete | ||||
elif(selector == 'insertTab:'): | ||||
if(len(self.currentLine().strip()) == 0): #only white space | ||||
return False | ||||
else: | ||||
self.textView.complete_(self) | ||||
return True | ||||
elif(selector == 'deleteBackward:'): | ||||
#if we're at the beginning of the current block, ignore | ||||
if(textView.selectedRange().location == self.currentBlockRange().location): | ||||
return True | ||||
else: | ||||
self.currentBlockRange().length-=1 | ||||
return False | ||||
return False | ||||
def textView_shouldChangeTextInRanges_replacementStrings_(self, textView, ranges, replacementStrings): | ||||
""" | ||||
Delegate method for NSTextView. | ||||
Refuse change text in ranges not at end, but make those changes at end. | ||||
""" | ||||
#print 'textView_shouldChangeTextInRanges_replacementStrings_:',ranges,replacementStrings | ||||
assert(len(ranges) == len(replacementStrings)) | ||||
allow = True | ||||
for r,s in zip(ranges, replacementStrings): | ||||
r = r.rangeValue() | ||||
if(textView.textStorage().length() > 0 and | ||||
r.location < self.currentBlockRange().location): | ||||
self.insert_text(s) | ||||
allow = False | ||||
self.blockRanges.setdefault(self.currentBlockID, self.currentBlockRange()).length += len(s) | ||||
return allow | ||||
def textView_completions_forPartialWordRange_indexOfSelectedItem_(self, textView, words, charRange, index): | ||||
try: | ||||
token = textView.textStorage().string().substringWithRange_(charRange) | ||||
completions = blockingCallFromThread(self.complete, token) | ||||
except: | ||||
completions = objc.nil | ||||
NSBeep() | ||||
return (completions,0) | ||||
def nextBlockID(self): | ||||
return uuid.uuid4() | ||||
def currentBlockRange(self): | ||||
return self.blockRanges.get(self.currentBlockID, NSMakeRange(self.textView.textStorage().length(), 0)) | ||||
def currentBlock(self): | ||||
"""The current block's text""" | ||||
return self.textForRange(self.currentBlockRange()) | ||||
def textForRange(self, textRange): | ||||
"""textForRange""" | ||||
return self.textView.textStorage().string().substringWithRange_(textRange) | ||||
def currentLine(self): | ||||
block = self.textForRange(self.currentBlockRange()) | ||||
block = block.split('\n') | ||||
return block[-1] | ||||
def update_cell_prompt(self, result): | ||||
blockID = result['blockID'] | ||||
self.insert_text(self.inputPrompt(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.currentBlockRange() | ||||
self.insert_text('\n' + | ||||
self.outputPrompt(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(str(failure)) | ||||
return failure | ||||
def insert_text(self, string=None, textRange=None, scrollToVisible=True): | ||||
"""Insert text into textView at textRange, updating blockRanges as necessary""" | ||||
if(textRange == None): | ||||
textRange = NSMakeRange(self.textView.textStorage().length(), 0) #range for end of text | ||||
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) #textStorage().string() | ||||
self.textView.setSelectedRange_(NSMakeRange(textRange.location+len(string), 0)) | ||||
if(scrollToVisible): | ||||
self.textView.scrollRangeToVisible_(textRange) | ||||
def replaceCurrentBlockWithString(self, textView, string): | ||||
textView.replaceCharactersInRange_withString_(self.currentBlockRange(), | ||||
string) | ||||
r = NSMakeRange(textView.textStorage().length(), 0) | ||||
textView.scrollRangeToVisible_(r) | ||||
textView.setSelectedRange_(r) | ||||
def currentIndentString(self): | ||||
"""returns string for indent or None if no indent""" | ||||
if(len(self.currentBlock()) > 0): | ||||
lines = self.currentBlock().split('\n') | ||||
currentIndent = len(lines[-1]) - len(lines[-1]) | ||||
if(currentIndent == 0): | ||||
currentIndent = self.tabSpaces | ||||
if(self.tabUsesSpaces): | ||||
result = ' ' * currentIndent | ||||
else: | ||||
result = '\t' * (currentIndent/self.tabSpaces) | ||||
else: | ||||
result = None | ||||
return result | ||||