##// END OF EJS Templates
oops
oops

File last commit:

r1263:67f5e0c2
r1276:10c38e4d
Show More
cocoa_frontend.py
369 lines | 13.1 KiB | text/x-python | PythonLexer
Barry Wark
moved frontend from ipython1-dev. Got engineservice.ThreadedEngineService running, but does nto correctly propagate errors during execute()
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