##// END OF EJS Templates
oops... old habits die hard.
Barry Wark -
Show More
@@ -1,429 +1,429 b''
1 1 # encoding: utf-8
2 2 # -*- test-case-name: IPython.frontend.cocoa.tests.test_cocoa_frontend -*-
3 3
4 4 """PyObjC classes to provide a Cocoa frontend to the
5 5 IPython.kernel.engineservice.IEngineBase.
6 6
7 7 To add an IPython interpreter to a cocoa app, instantiate an
8 8 IPythonCocoaController in a XIB and connect its textView outlet to an
9 9 NSTextView instance in your UI. That's it.
10 10
11 11 Author: Barry Wark
12 12 """
13 13
14 14 __docformat__ = "restructuredtext en"
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Copyright (C) 2008 The IPython Development Team
18 18 #
19 19 # Distributed under the terms of the BSD License. The full license is in
20 20 # the file COPYING, distributed as part of this software.
21 21 #-----------------------------------------------------------------------------
22 22
23 23 #-----------------------------------------------------------------------------
24 24 # Imports
25 25 #-----------------------------------------------------------------------------
26 26
27 27 import objc
28 28 import uuid
29 29
30 30 from Foundation import NSObject, NSMutableArray, NSMutableDictionary,\
31 31 NSLog, NSNotificationCenter, NSMakeRange,\
32 32 NSLocalizedString, NSIntersectionRange
33 33
34 34 from AppKit import NSApplicationWillTerminateNotification, NSBeep,\
35 35 NSTextView, NSRulerView, NSVerticalRuler
36 36
37 37 from pprint import saferepr
38 38
39 39 import IPython
40 40 from IPython.kernel.engineservice import ThreadedEngineService
41 41 from IPython.frontend.frontendbase import FrontEndBase
42 42
43 43 from twisted.internet.threads import blockingCallFromThread
44 44 from twisted.python.failure import Failure
45 45
46 46 #------------------------------------------------------------------------------
47 47 # Classes to implement the Cocoa frontend
48 48 #------------------------------------------------------------------------------
49 49
50 50 # TODO:
51 51 # 1. use MultiEngineClient and out-of-process engine rather than
52 52 # ThreadedEngineService?
53 53 # 2. integrate Xgrid launching of engines
54 54
55 55
56 56
57 57
58 58 class IPythonCocoaController(NSObject, FrontEndBase):
59 59 userNS = objc.ivar() #mirror of engine.user_ns (key=>str(value))
60 60 waitingForEngine = objc.ivar().bool()
61 61 textView = objc.IBOutlet()
62 62
63 63 def init(self):
64 64 self = super(IPythonCocoaController, self).init()
65 65 FrontEndBase.__init__(self, engine=ThreadedEngineService())
66 66 if(self != None):
67 67 self._common_init()
68 68
69 69 return self
70 70
71 71 def _common_init(self):
72 72 """_common_init"""
73 73
74 74 self.userNS = NSMutableDictionary.dictionary()
75 75 self.waitingForEngine = False
76 76
77 77 self.lines = {}
78 78 self.tabSpaces = 4
79 79 self.tabUsesSpaces = True
80 80 self.currentBlockID = self.next_block_ID()
81 81 self.blockRanges = {} # blockID=>NSRange
82 82
83 83
84 84 def awakeFromNib(self):
85 85 """awakeFromNib"""
86 86
87 87 self._common_init()
88 88
89 89 # Start the IPython engine
90 90 self.engine.startService()
91 91 NSLog('IPython engine started')
92 92
93 93 # Register for app termination
94 94 nc = NSNotificationCenter.defaultCenter()
95 95 nc.addObserver_selector_name_object_(
96 96 self,
97 97 'appWillTerminate:',
98 98 NSApplicationWillTerminateNotification,
99 99 None)
100 100
101 101 self.textView.setDelegate_(self)
102 102 self.textView.enclosingScrollView().setHasVerticalRuler_(True)
103 103 r = NSRulerView.alloc().initWithScrollView_orientation_(
104 104 self.textView.enclosingScrollView(),
105 105 NSVerticalRuler)
106 106 self.verticalRulerView = r
107 107 self.verticalRulerView.setClientView_(self.textView)
108 108 self._start_cli_banner()
109 109
110 110
111 111 def appWillTerminate_(self, notification):
112 112 """appWillTerminate"""
113 113
114 114 self.engine.stopService()
115 115
116 116
117 117 def complete(self, token):
118 118 """Complete token in engine's user_ns
119 119
120 120 Parameters
121 121 ----------
122 122 token : string
123 123
124 124 Result
125 125 ------
126 126 Deferred result of
127 127 IPython.kernel.engineservice.IEngineBase.complete
128 128 """
129 129
130 130 return self.engine.complete(token)
131 131
132 132
133 133 def execute(self, block, blockID=None):
134 134 self.waitingForEngine = True
135 135 self.willChangeValueForKey_('commandHistory')
136 136 d = super(IPythonCocoaController, self).execute(block, blockID)
137 137 d.addBoth(self._engine_done)
138 138 d.addCallback(self._update_user_ns)
139 139
140 140 return d
141 141
142 142
143 143 def _engine_done(self, x):
144 144 self.waitingForEngine = False
145 145 self.didChangeValueForKey_('commandHistory')
146 146 return x
147 147
148 148 def _update_user_ns(self, result):
149 149 """Update self.userNS from self.engine's namespace"""
150 150 d = self.engine.keys()
151 151 d.addCallback(self._get_engine_namespace_values_for_keys)
152 152
153 153 return result
154 154
155 155
156 156 def _get_engine_namespace_values_for_keys(self, keys):
157 157 d = self.engine.pull(keys)
158 158 d.addCallback(self._store_engine_namespace_values, keys=keys)
159 159
160 160
161 161 def _store_engine_namespace_values(self, values, keys=[]):
162 162 assert(len(values) == len(keys))
163 163 self.willChangeValueForKey_('userNS')
164 164 for (k,v) in zip(keys,values):
165 165 self.userNS[k] = saferepr(v)
166 166 self.didChangeValueForKey_('userNS')
167 167
168 168
169 169 def update_cell_prompt(self, result):
170 170 if(isinstance(result, Failure)):
171 171 blockID = result.blockID
172 172 else:
173 173 blockID = result['blockID']
174 174
175 175
176 176 self.insert_text(self.input_prompt(result=result),
177 177 textRange=NSMakeRange(self.blockRanges[blockID].location,0),
178 178 scrollToVisible=False
179 179 )
180 180
181 181 return result
182 182
183 183
184 184 def render_result(self, result):
185 185 blockID = result['blockID']
186 186 inputRange = self.blockRanges[blockID]
187 187 del self.blockRanges[blockID]
188 188
189 189 #print inputRange,self.current_block_range()
190 190 self.insert_text('\n' +
191 191 self.output_prompt(result) +
192 192 result.get('display',{}).get('pprint','') +
193 193 '\n\n',
194 194 textRange=NSMakeRange(inputRange.location+inputRange.length,
195 195 0))
196 196 return result
197 197
198 198
199 199 def render_error(self, failure):
200 200 self.insert_text('\n\n'+str(failure)+'\n\n')
201 201 self.start_new_block()
202 202 return failure
203 203
204 204
205 205 def _start_cli_banner(self):
206 206 """Print banner"""
207 207
208 208 banner = """IPython1 %s -- An enhanced Interactive Python.""" % \
209 209 IPython.__version__
210 210
211 211 self.insert_text(banner + '\n\n')
212 212
213 213
214 214 def start_new_block(self):
215 215 """"""
216 216
217 217 self.currentBlockID = self.next_block_ID()
218 218
219 219
220 220
221 221 def next_block_ID(self):
222 222
223 223 return uuid.uuid4()
224 224
225 225 def current_block_range(self):
226 226 return self.blockRanges.get(self.currentBlockID,
227 227 NSMakeRange(self.textView.textStorage().length(),
228 228 0))
229 229
230 230 def current_block(self):
231 231 """The current block's text"""
232 232
233 233 return self.text_for_range(self.current_block_range())
234 234
235 235 def text_for_range(self, textRange):
236 236 """text_for_range"""
237 237
238 238 ts = self.textView.textStorage()
239 239 return ts.string().substringWithRange_(textRange)
240 240
241 241 def current_line(self):
242 242 block = self.text_for_range(self.current_block_range())
243 243 block = block.split('\n')
244 244 return block[-1]
245 245
246 246
247 247 def insert_text(self, string=None, textRange=None, scrollToVisible=True):
248 248 """Insert text into textView at textRange, updating blockRanges
249 249 as necessary
250 250 """
251 251
252 252 if(textRange == None):
253 253 #range for end of text
254 254 textRange = NSMakeRange(self.textView.textStorage().length(), 0)
255 255
256 256 for r in self.blockRanges.itervalues():
257 257 intersection = NSIntersectionRange(r,textRange)
258 258 if(intersection.length == 0): #ranges don't intersect
259 259 if r.location >= textRange.location:
260 260 r.location += len(string)
261 261 else: #ranges intersect
262 262 if(r.location <= textRange.location):
263 263 assert(intersection.length == textRange.length)
264 264 r.length += textRange.length
265 265 else:
266 266 r.location += intersection.length
267 267
268 268 self.textView.replaceCharactersInRange_withString_(
269 269 textRange, string)
270 270 self.textView.setSelectedRange_(
271 271 NSMakeRange(textRange.location+len(string), 0))
272 272 if(scrollToVisible):
273 273 self.textView.scrollRangeToVisible_(textRange)
274 274
275 275
276 276
277 277
278 278 def replace_current_block_with_string(self, textView, string):
279 279 textView.replaceCharactersInRange_withString_(
280 280 self.current_block_range(),
281 281 string)
282 282 self.current_block_range().length = len(string)
283 283 r = NSMakeRange(textView.textStorage().length(), 0)
284 284 textView.scrollRangeToVisible_(r)
285 285 textView.setSelectedRange_(r)
286 286
287 287
288 288 def current_indent_string(self):
289 289 """returns string for indent or None if no indent"""
290 290
291 return self._indent_for_block(self.currentBlock())
291 return self._indent_for_block(self.current_block())
292 292
293 293
294 294 def _indent_for_block(self, block):
295 295 lines = block.split('\n')
296 296 if(len(lines) > 1):
297 297 currentIndent = len(lines[-1]) - len(lines[-1].lstrip())
298 298 if(currentIndent == 0):
299 299 currentIndent = self.tabSpaces
300 300
301 301 if(self.tabUsesSpaces):
302 302 result = ' ' * currentIndent
303 303 else:
304 304 result = '\t' * (currentIndent/self.tabSpaces)
305 305 else:
306 306 result = None
307 307
308 308 return result
309 309
310 310
311 311 # NSTextView delegate methods...
312 312 def textView_doCommandBySelector_(self, textView, selector):
313 313 assert(textView == self.textView)
314 314 NSLog("textView_doCommandBySelector_: "+selector)
315 315
316 316
317 317 if(selector == 'insertNewline:'):
318 318 indent = self.current_indent_string()
319 319 if(indent):
320 320 line = indent + self.current_line()
321 321 else:
322 322 line = self.current_line()
323 323
324 324 if(self.is_complete(self.current_block())):
325 325 self.execute(self.current_block(),
326 326 blockID=self.currentBlockID)
327 327 self.start_new_block()
328 328
329 329 return True
330 330
331 331 return False
332 332
333 333 elif(selector == 'moveUp:'):
334 334 prevBlock = self.get_history_previous(self.current_block())
335 335 if(prevBlock != None):
336 336 self.replace_current_block_with_string(textView, prevBlock)
337 337 else:
338 338 NSBeep()
339 339 return True
340 340
341 341 elif(selector == 'moveDown:'):
342 342 nextBlock = self.get_history_next()
343 343 if(nextBlock != None):
344 344 self.replace_current_block_with_string(textView, nextBlock)
345 345 else:
346 346 NSBeep()
347 347 return True
348 348
349 349 elif(selector == 'moveToBeginningOfParagraph:'):
350 350 textView.setSelectedRange_(NSMakeRange(
351 351 self.current_block_range().location,
352 352 0))
353 353 return True
354 354 elif(selector == 'moveToEndOfParagraph:'):
355 355 textView.setSelectedRange_(NSMakeRange(
356 356 self.current_block_range().location + \
357 357 self.current_block_range().length, 0))
358 358 return True
359 359 elif(selector == 'deleteToEndOfParagraph:'):
360 360 if(textView.selectedRange().location <= \
361 361 self.current_block_range().location):
362 362 # Intersect the selected range with the current line range
363 363 if(self.current_block_range().length < 0):
364 364 self.blockRanges[self.currentBlockID].length = 0
365 365
366 366 r = NSIntersectionRange(textView.rangesForUserTextChange()[0],
367 367 self.current_block_range())
368 368
369 369 if(r.length > 0): #no intersection
370 370 textView.setSelectedRange_(r)
371 371
372 372 return False # don't actually handle the delete
373 373
374 374 elif(selector == 'insertTab:'):
375 375 if(len(self.current_line().strip()) == 0): #only white space
376 376 return False
377 377 else:
378 378 self.textView.complete_(self)
379 379 return True
380 380
381 381 elif(selector == 'deleteBackward:'):
382 382 #if we're at the beginning of the current block, ignore
383 383 if(textView.selectedRange().location == \
384 384 self.current_block_range().location):
385 385 return True
386 386 else:
387 387 self.current_block_range().length-=1
388 388 return False
389 389 return False
390 390
391 391
392 392 def textView_shouldChangeTextInRanges_replacementStrings_(self,
393 393 textView, ranges, replacementStrings):
394 394 """
395 395 Delegate method for NSTextView.
396 396
397 397 Refuse change text in ranges not at end, but make those changes at
398 398 end.
399 399 """
400 400
401 401 assert(len(ranges) == len(replacementStrings))
402 402 allow = True
403 403 for r,s in zip(ranges, replacementStrings):
404 404 r = r.rangeValue()
405 405 if(textView.textStorage().length() > 0 and
406 406 r.location < self.current_block_range().location):
407 407 self.insert_text(s)
408 408 allow = False
409 409
410 410
411 411 self.blockRanges.setdefault(self.currentBlockID,
412 412 self.current_block_range()).length +=\
413 413 len(s)
414 414
415 415 return allow
416 416
417 417 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self,
418 418 textView, words, charRange, index):
419 419 try:
420 420 ts = textView.textStorage()
421 421 token = ts.string().substringWithRange_(charRange)
422 422 completions = blockingCallFromThread(self.complete, token)
423 423 except:
424 424 completions = objc.nil
425 425 NSBeep()
426 426
427 427 return (completions,0)
428 428
429 429
General Comments 0
You need to be logged in to leave comments. Login now