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