API  0.9.6
 All Classes Files Functions Variables Macros Groups Pages
CPObjectController.j
Go to the documentation of this file.
1 /*
2  * CPObjectController.j
3  * AppKit
4  *
5  * Created by Ross Boucher.
6  * Copyright 2009, 280 North, Inc.
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Lesser General Public
10  * License as published by the Free Software Foundation; either
11  * version 2.1 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16  * Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public
19  * License along with this library; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21  */
22 
23 
24 
35 @implementation CPObjectController : CPController
36 {
37  id _contentObject;
38  id _selection;
39 
40  Class _objectClass;
41  CPString _objectClassName;
42 
43  BOOL _isEditable;
44  BOOL _automaticallyPreparesContent;
45 
46  CPCountedSet _observedKeys;
47 }
48 
49 + (id)initialize
50 {
51  if (self !== [CPObjectController class])
52  return;
53 
54  [self exposeBinding:@"editable"];
55  [self exposeBinding:@"contentObject"];
56 }
57 
58 + (CPSet)keyPathsForValuesAffectingContentObject
59 {
60  return [CPSet setWithObjects:"content"];
61 }
62 
63 + (BOOL)automaticallyNotifiesObserversForKey:(CPString)aKey
64 {
65  if (aKey === @"contentObject")
66  return NO;
67 
68  return YES;
69 }
70 
71 + (CPSet)keyPathsForValuesAffectingCanAdd
72 {
73  return [CPSet setWithObject:"editable"];
74 }
75 
76 + (CPSet)keyPathsForValuesAffectingCanInsert
77 {
78  return [CPSet setWithObject:"editable"];
79 }
80 
81 + (CPSet)keyPathsForValuesAffectingCanRemove
82 {
83  return [CPSet setWithObjects:"editable", "selection"];
84 }
85 
89 - (id)init
90 {
91  return [self initWithContent:nil];
92 }
93 
100 - (id)initWithContent:(id)aContent
101 {
102  if (self = [super init])
103  {
104  [self setContent:aContent];
105  [self setEditable:YES];
107 
108  _observedKeys = [[CPCountedSet alloc] init];
109  }
110 
111  return self;
112 }
113 
118 - (id)content
119 {
120  return _contentObject;
121 }
122 
127 - (void)setContent:(id)aContent
128 {
129  [self willChangeValueForKey:@"contentObject"];
130  [self _selectionWillChange];
131 
132  _contentObject = aContent;
133 
134  [self _selectionDidChange];
135  [self didChangeValueForKey:@"contentObject"];
136 }
137 
141 - (void)_setContentObject:(id)aContent
142 {
143  [self setContent:aContent];
144 }
145 
149 - (id)_contentObject
150 {
151  return [self content];
152 }
153 
161 - (void)setAutomaticallyPreparesContent:(BOOL)shouldAutomaticallyPrepareContent
162 {
163  _automaticallyPreparesContent = shouldAutomaticallyPrepareContent;
164 }
165 
170 - (BOOL)automaticallyPreparesContent
171 {
172  return _automaticallyPreparesContent;
173 }
174 
178 - (void)prepareContent
179 {
180  [self setContent:[self newObject]];
181 }
182 
187 - (void)setObjectClass:(Class)aClass
188 {
189  _objectClass = aClass;
190 }
191 
197 - (Class)objectClass
198 {
199  return _objectClass;
200 }
201 
205 - (id)_defaultNewObject
206 {
207  return [[[self objectClass] alloc] init];
208 }
209 
214 - (id)newObject
215 {
216  return [self _defaultNewObject];
217 }
218 
223 - (void)addObject:(id)anObject
224 {
225  [self setContent:anObject];
226 
227  var binderClass = [[self class] _binderClassForBinding:@"contentObject"];
228  [[binderClass getBinding:@"contentObject" forObject:self] reverseSetValueFor:@"contentObject"];
229 }
230 
235 - (void)removeObject:(id)anObject
236 {
237  if ([self content] === anObject)
238  [self setContent:nil];
239 
240  var binderClass = [[self class] _binderClassForBinding:@"contentObject"];
241  [[binderClass getBinding:@"contentObject" forObject:self] reverseSetValueFor:@"contentObject"];
242 }
243 
248 - (void)add:(id)aSender
249 {
250  // FIXME: This should happen on the next run loop?
251  [self addObject:[self newObject]];
252 }
253 
257 - (BOOL)canAdd
258 {
259  return [self isEditable];
260 }
261 
266 - (void)remove:(id)aSender
267 {
268  // FIXME: This should happen on the next run loop?
269  [self removeObject:[self content]];
270 }
271 
275 - (BOOL)canRemove
276 {
277  return [self isEditable] && [[self selectedObjects] count];
278 }
279 
284 - (void)setEditable:(BOOL)shouldBeEditable
285 {
286  _isEditable = shouldBeEditable;
287 }
288 
292 - (BOOL)isEditable
293 {
294  return _isEditable;
295 }
296 
300 - (CPArray)selectedObjects
301 {
302  return [[_CPObservableArray alloc] initWithArray:[_contentObject]];
303 }
304 
308 - (id)selection
309 {
310  return _selection;
311 }
312 
316 - (void)_selectionWillChange
317 {
318  [_selection controllerWillChange];
319  [self willChangeValueForKey:@"selection"];
320 }
321 
325 - (void)_selectionDidChange
326 {
327  if (_selection === undefined || _selection === nil)
328  _selection = [[CPControllerSelectionProxy alloc] initWithController:self];
329 
330  [_selection controllerDidChange];
331  [self didChangeValueForKey:@"selection"];
332 }
333 
337 - (id)observedKeys
338 {
339  return _observedKeys;
340 }
341 
342 - (void)addObserver:(id)anObserver forKeyPath:(CPString)aKeyPath options:(CPKeyValueObservingOptions)options context:(id)context
343 {
344  [_observedKeys addObject:aKeyPath];
345  [super addObserver:anObserver forKeyPath:aKeyPath options:options context:context];
346 }
347 
348 - (void)removeObserver:(id)anObserver forKeyPath:(CPString)aKeyPath
349 {
350  [_observedKeys removeObject:aKeyPath];
351  [super removeObserver:anObserver forKeyPath:aKeyPath];
352 }
353 
354 @end
355 
356 var CPObjectControllerContentKey = @"CPObjectControllerContentKey",
357  CPObjectControllerObjectClassNameKey = @"CPObjectControllerObjectClassNameKey",
358  CPObjectControllerIsEditableKey = @"CPObjectControllerIsEditableKey",
359  CPObjectControllerAutomaticallyPreparesContentKey = @"CPObjectControllerAutomaticallyPreparesContentKey";
360 
362 
363 - (id)initWithCoder:(CPCoder)aCoder
364 {
365  self = [super init];
366 
367  if (self)
368  {
369  var objectClassName = [aCoder decodeObjectForKey:CPObjectControllerObjectClassNameKey],
370  objectClass = CPClassFromString(objectClassName);
371 
372  [self setObjectClass:objectClass || [CPMutableDictionary class]];
373  [self setEditable:[aCoder decodeBoolForKey:CPObjectControllerIsEditableKey]];
374  [self setAutomaticallyPreparesContent:[aCoder decodeBoolForKey:CPObjectControllerAutomaticallyPreparesContentKey]];
375  [self setContent:[aCoder decodeObjectForKey:CPObjectControllerContentKey]];
376 
377  _observedKeys = [[CPCountedSet alloc] init];
378  }
379 
380  return self;
381 }
382 
383 - (void)encodeWithCoder:(CPCoder)aCoder
384 {
385  [aCoder encodeObject:[self content] forKey:CPObjectControllerContentKey];
386 
387  if (_objectClass)
388  [aCoder encodeObject:CPStringFromClass(_objectClass) forKey:CPObjectControllerObjectClassNameKey];
389  else if (_objectClassName)
390  [aCoder encodeObject:_objectClassName forKey:CPObjectControllerObjectClassNameKey];
391 
392  [aCoder encodeBool:[self isEditable] forKey:CPObjectControllerIsEditableKey];
393  [aCoder encodeBool:[self automaticallyPreparesContent] forKey:CPObjectControllerAutomaticallyPreparesContentKey];
394 }
395 
396 - (void)awakeFromCib
397 {
398  if (![self content] && [self automaticallyPreparesContent])
399  [self prepareContent];
400 }
401 
402 @end
403 
404 @implementation _CPObservationProxy : CPObject
405 {
406  id _keyPath;
407  id _observer;
408  id _object;
409 
410  BOOL _notifyObject;
411 
412  id _context;
413  int _options;
414 }
415 
416 - (id)initWithKeyPath:(id)aKeyPath observer:(id)anObserver object:(id)anObject
417 {
418  if (self = [super init])
419  {
420  _keyPath = aKeyPath;
421  _observer = anObserver;
422  _object = anObject;
423  }
424 
425  return self;
426 }
427 
428 - (id)observer
429 {
430  return _observer;
431 }
432 
433 - (id)keyPath
434 {
435  return _keyPath;
436 }
437 
438 - (id)context
439 {
440  return _context;
441 }
442 
443 - (int)options
444 {
445  return _options;
446 }
447 
448 - (void)setNotifyObject:(BOOL)notify
449 {
450  _notifyObject = notify;
451 }
452 
453 - (BOOL)isEqual:(id)anObject
454 {
455  if ([anObject class] === [self class])
456  {
457  if (anObject._observer === _observer && [anObject._keyPath isEqual:_keyPath] && [anObject._object isEqual:_object])
458  return YES;
459  }
460 
461  return NO;
462 }
463 
464 - (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)change context:(id)context
465 {
466  if (_notifyObject)
467  [_object observeValueForKeyPath:aKeyPath ofObject:_object change:change context:context];
468 
469  [_observer observeValueForKeyPath:aKeyPath ofObject:_object change:change context:context];
470 }
471 
472 - (CPString)description
473 {
474  return [super description] + [CPString stringWithFormat:@"observation proxy for %@ on key path %@", _observer, _keyPath];
475 }
476 
477 @end
478 
479 // FIXME: This should subclass CPMutableArray not _CPJavaScriptArray
480 @implementation _CPObservableArray : _CPJavaScriptArray
481 {
482  CPArray _observationProxies;
483 }
484 
485 + (id)alloc
486 {
487  var a = [];
488  a.isa = self;
489 
490  var ivars = class_copyIvarList(self),
491  count = ivars.length;
492 
493  while (count--)
494  a[ivar_getName(ivars[count])] = nil;
495 
496  return a;
497 }
498 
499 - (CPString)description
500 {
501  return "<_CPObservableArray: " + [super description] + " >";
502 }
503 
504 - (id)initWithArray:(CPArray)anArray
505 {
506  self = [super initWithArray:anArray];
507 
508  self.isa = [_CPObservableArray class];
509  self._observationProxies = [];
510 
511  return self;
512 }
513 
514 - (void)addObserver:(id)anObserver forKeyPath:(CPString)aKeyPath options:(CPKeyValueObservingOptions)options context:(id)context
515 {
516  if (aKeyPath.indexOf("@") === 0)
517  {
518  var proxy = [[_CPObservationProxy alloc] initWithKeyPath:aKeyPath observer:anObserver object:self];
519 
520  proxy._options = options;
521  proxy._context = context;
522 
523  [_observationProxies addObject:proxy];
524 
525  var dotIndex = aKeyPath.indexOf("."),
526  remaining = aKeyPath.substring(dotIndex + 1),
527  indexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self count])];
528 
529  [self addObserver:proxy toObjectsAtIndexes:indexes forKeyPath:remaining options:options context:context];
530  }
531  else
532  {
533  var indexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self count])];
534  [self addObserver:anObserver toObjectsAtIndexes:indexes forKeyPath:aKeyPath options:options context:context];
535  }
536 }
537 
538 - (void)removeObserver:(id)anObserver forKeyPath:(CPString)aKeyPath
539 {
540  if (aKeyPath.indexOf("@") === 0)
541  {
542  var proxy = [[_CPObservationProxy alloc] initWithKeyPath:aKeyPath observer:anObserver object:self],
543  index = [_observationProxies indexOfObject:proxy];
544 
545  proxy = [_observationProxies objectAtIndex:index];
546 
547  var dotIndex = aKeyPath.indexOf("."),
548  remaining = aKeyPath.substring(dotIndex + 1),
549  indexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self count])];
550 
551  [self removeObserver:proxy fromObjectsAtIndexes:indexes forKeyPath:remaining];
552  }
553  else
554  {
555  var indexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self count])];
556  [self removeObserver:anObserver fromObjectsAtIndexes:indexes forKeyPath:aKeyPath];
557  }
558 }
559 
560 - (void)insertObject:(id)anObject atIndex:(unsigned)anIndex
561 {
562  for (var i = 0, count = [_observationProxies count]; i < count; i++)
563  {
564  var proxy = [_observationProxies objectAtIndex:i],
565  keyPath = [proxy keyPath],
566  operator = keyPath.indexOf(".") === 0;
567 
568  if (operator)
569  [self willChangeValueForKey:keyPath];
570 
571  [anObject addObserver:proxy forKeyPath:keyPath options:[proxy options] context:[proxy context]];
572 
573  if (operator)
574  [self didChangeValueForKey:keyPath];
575  }
576 
577  [super insertObject:anObject atIndex:anIndex];
578 }
579 
580 - (void)removeObjectAtIndex:(unsigned)anIndex
581 {
582  var currentObject = [self objectAtIndex:anIndex];
583 
584  for (var i = 0, count = [_observationProxies count]; i < count; i++)
585  {
586  var proxy = [_observationProxies objectAtIndex:i],
587  keyPath = [proxy keyPath],
588  operator = keyPath.indexOf(".") === 0;
589 
590  if (operator)
591  [self willChangeValueForKey:keyPath];
592 
593  [currentObject removeObserver:proxy forKeyPath:keyPath];
594 
595  if (operator)
596  [self didChangeValueForKey:keyPath];
597  }
598 
599  [super removeObjectAtIndex:anIndex];
600 }
601 
602 - (_CPObservableArray)objectsAtIndexes:(CPIndexSet)theIndexes
603 {
604  return [_CPObservableArray arrayWithArray:[super objectsAtIndexes:theIndexes]];
605 }
606 
607 - (void)addObject:(id)anObject
608 {
609  [self insertObject:anObject atIndex:[self count]];
610 }
611 
612 - (void)removeLastObject
613 {
614  [self removeObjectAtIndex:[self count]];
615 }
616 
617 - (void)replaceObjectAtIndex:(unsigned)anIndex withObject:(id)anObject
618 {
619  var currentObject = [self objectAtIndex:anIndex];
620 
621  for (var i = 0, count = [_observationProxies count]; i < count; i++)
622  {
623  var proxy = [_observationProxies objectAtIndex:i],
624  keyPath = [proxy keyPath],
625  operator = keyPath.indexOf(".") === 0;
626 
627  if (operator)
628  [self willChangeValueForKey:keyPath];
629 
630  [currentObject removeObserver:proxy forKeyPath:keyPath];
631  [anObject addObserver:proxy forKeyPath:keyPath options:[proxy options] context:[proxy context]];
632 
633  if (operator)
634  [self didChangeValueForKey:keyPath];
635  }
636 
637  [super replaceObjectAtIndex:anIndex withObject:anObject];
638 }
639 
640 @end
641 
642 @implementation CPControllerSelectionProxy : CPObject
643 {
644  id _controller;
645  id _keys;
646 
647  CPDictionary _cachedValues;
648  CPArray _observationProxies;
649 
650  Object _observedObjectsByKeyPath;
651 }
652 
653 - (id)initWithController:(id)aController
654 {
655  if (self = [super init])
656  {
657  _cachedValues = [CPDictionary dictionary];
658  _observationProxies = [CPArray array];
659  _controller = aController;
660  _observedObjectsByKeyPath = {};
661  }
662 
663  return self;
664 }
665 
666 - (id)_controllerMarkerForValues:(CPArray)theValues
667 {
668  var count = [theValues count],
669  value;
670 
671  if (!count)
672  value = CPNoSelectionMarker;
673  else if (count === 1)
674  value = [theValues objectAtIndex:0];
675  else
676  {
677  if ([_controller alwaysUsesMultipleValuesMarker])
678  value = CPMultipleValuesMarker;
679  else
680  {
681  value = [theValues objectAtIndex:0];
682 
683  for (var i = 0, count = [theValues count]; i < count && value != CPMultipleValuesMarker; i++)
684  {
685  if (![value isEqual:[theValues objectAtIndex:i]])
686  value = CPMultipleValuesMarker;
687  }
688  }
689  }
690 
691  if (value === nil || value.isa && [value isEqual:[CPNull null]])
692  value = CPNullMarker;
693 
694  return value;
695 }
696 
697 - (id)valueForKeyPath:(CPString)theKeyPath
698 {
699  var values = [[_controller selectedObjects] valueForKeyPath:theKeyPath],
700  value = [self _controllerMarkerForValues:values];
701 
702  [_cachedValues setObject:value forKey:theKeyPath];
703 
704  return value;
705 }
706 
707 - (id)valueForKey:(CPString)theKeyPath
708 {
709  return [self valueForKeyPath:theKeyPath];
710 }
711 
712 - (void)setValue:(id)theValue forKeyPath:(CPString)theKeyPath
713 {
714  [[_controller selectedObjects] setValue:theValue forKeyPath:theKeyPath];
715  [_cachedValues removeObjectForKey:theKeyPath];
716 
717  // Allow handlesContentAsCompoundValue to work, based on observation of Cocoa's
718  // NSArrayController - when handlesContentAsCompoundValue and setValue:forKey:@"selection.X"
719  // is called, the array controller causes the compound value to be rewritten if
720  // handlesContentAsCompoundValue == YES. Note that
721  // A) this doesn't use observation (observe: X is not visible in backtraces)
722  // B) it only happens through the selection proxy and not on arrangedObject.X, content.X
723  // or even selectedObjects.X.
724  // FIXME The main code for this should somehow be in CPArrayController and also work
725  // for table based row edits.
726  [[CPBinder getBinding:@"contentArray" forObject:_controller] _contentArrayDidChange];
727 }
728 
729 - (void)setValue:(id)theValue forKey:(CPString)theKeyPath
730 {
731  [self setValue:theKeyPath forKeyPath:theKeyPath];
732 }
733 
734 - (unsigned)count
735 {
736  return [_cachedValues count];
737 }
738 
739 - (id)keyEnumerator
740 {
741  return [_cachedValues keyEnumerator];
742 }
743 
744 - (void)controllerWillChange
745 {
746  _keys = [_cachedValues allKeys];
747 
748  if (!_keys)
749  return;
750 
751  for (var i = 0, count = _keys.length; i < count; i++)
752  [self willChangeValueForKey:_keys[i]];
753 
754  [_cachedValues removeAllObjects];
755 }
756 
757 - (void)controllerDidChange
758 {
759  [_cachedValues removeAllObjects];
760 
761  if (!_keys)
762  return;
763 
764  for (var i = 0, count = _keys.length; i < count; i++)
765  [self didChangeValueForKey:_keys[i]];
766 
767  _keys = nil;
768 }
769 
770 - (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)change context:(id)context
771 {
772  [_cachedValues removeObjectForKey:aKeyPath];
773 }
774 
775 - (void)addObserver:(id)anObject forKeyPath:(CPString)aKeyPath options:(CPKeyValueObservingOptions)options context:(id)context
776 {
777  var proxy = [[_CPObservationProxy alloc] initWithKeyPath:aKeyPath observer:anObject object:self];
778 
779  [proxy setNotifyObject:YES];
780  [_observationProxies addObject:proxy];
781 
782  // We keep a reference to the observed objects because removeObserver: will be called after the selection changes.
783  var observedObjects = [_controller selectedObjects];
784  _observedObjectsByKeyPath[aKeyPath] = observedObjects;
785  [observedObjects addObserver:proxy forKeyPath:aKeyPath options:options context:context];
786 }
787 
788 - (void)removeObserver:(id)anObject forKeyPath:(CPString)aKeyPath
789 {
790  var proxy = [[_CPObservationProxy alloc] initWithKeyPath:aKeyPath observer:anObject object:self],
791  index = [_observationProxies indexOfObject:proxy];
792 
793  var observedObjects = _observedObjectsByKeyPath[aKeyPath];
794  [observedObjects removeObserver:[_observationProxies objectAtIndex:index] forKeyPath:aKeyPath];
795 
796  [_observationProxies removeObjectAtIndex:index];
797 
798  _observedObjects = nil;
799 }
800 
801 @end