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