API  0.9.6
 All Classes Files Functions Variables Macros Groups Pages
CPArrayController.j
Go to the documentation of this file.
1 /*
2  * CPArrayController.j
3  * AppKit
4  *
5  * Adapted from Cocotron, by Johannes Fortmann
6  *
7  * Created by Ross Boucher.
8  * Copyright 2009, 280 North, Inc.
9  *
10  * This library is free software; you can redistribute it and/or
11  * modify it under the terms of the GNU Lesser General Public
12  * License as published by the Free Software Foundation; either
13  * version 2.1 of the License, or (at your option) any later version.
14  *
15  * This library is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18  * Lesser General Public License for more details.
19  *
20  * You should have received a copy of the GNU Lesser General Public
21  * License along with this library; if not, write to the Free Software
22  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
23  */
24 
25 
26 
35 @implementation CPArrayController : CPObjectController
36 {
37  BOOL _avoidsEmptySelection;
38  BOOL _clearsFilterPredicateOnInsertion;
39  BOOL _filterRestrictsInsertion;
40  BOOL _preservesSelection;
41  BOOL _selectsInsertedObjects;
42  BOOL _alwaysUsesMultipleValuesMarker;
43 
44  BOOL _automaticallyRearrangesObjects; // FIXME: Not in use
45 
46  CPIndexSet _selectionIndexes;
47  CPArray _sortDescriptors;
48  CPPredicate _filterPredicate;
49  CPArray _arrangedObjects;
50 
51  BOOL _disableSetContent;
52 }
53 
54 + (void)initialize
55 {
56  if (self !== [CPArrayController class])
57  return;
58 
59  [self exposeBinding:@"contentArray"];
60  [self exposeBinding:@"contentSet"];
61 }
62 
63 + (CPSet)keyPathsForValuesAffectingContentArray
64 {
65  return [CPSet setWithObjects:"content"];
66 }
67 
68 + (CPSet)keyPathsForValuesAffectingArrangedObjects
69 {
70  // Also depends on "filterPredicate" but we'll handle that manually.
71  return [CPSet setWithObjects:"content", "sortDescriptors"];
72 }
73 
74 + (CPSet)keyPathsForValuesAffectingSelection
75 {
76  return [CPSet setWithObjects:"selectionIndexes"];
77 }
78 
79 + (CPSet)keyPathsForValuesAffectingSelectionIndex
80 {
81  return [CPSet setWithObjects:"selectionIndexes"];
82 }
83 
84 + (CPSet)keyPathsForValuesAffectingSelectionIndexes
85 {
86  // When the arranged objects change, selection preservation may cause the indexes
87  // to change.
88  return [CPSet setWithObjects:"arrangedObjects"];
89 }
90 
91 + (CPSet)keyPathsForValuesAffectingSelectedObjects
92 {
93  // Don't need to depend on arrangedObjects here because selectionIndexes already does.
94  return [CPSet setWithObjects:"selectionIndexes"];
95 }
96 
97 + (CPSet)keyPathsForValuesAffectingCanRemove
98 {
99  return [CPSet setWithObjects:"selectionIndexes"];
100 }
101 
102 + (CPSet)keyPathsForValuesAffectingCanSelectNext
103 {
104  return [CPSet setWithObjects:"selectionIndexes"];
105 }
106 
107 + (CPSet)keyPathsForValuesAffectingCanSelectPrevious
108 {
109  return [CPSet setWithObjects:"selectionIndexes"];
110 }
111 
112 
113 - (id)init
114 {
115  self = [super init];
116 
117  if (self)
118  {
119  _preservesSelection = YES;
120  _selectsInsertedObjects = YES;
121  _avoidsEmptySelection = YES;
122  _clearsFilterPredicateOnInsertion = YES;
123  _alwaysUsesMultipleValuesMarker = NO;
124  _automaticallyRearrangesObjects = NO;
125 
126  _filterRestrictsInsertion = YES; // FIXME: Not in use
127 
128  [self _init];
129  }
130 
131  return self;
132 }
133 
134 - (void)_init
135 {
136  _sortDescriptors = [CPArray array];
137  _filterPredicate = nil;
138  _selectionIndexes = [CPIndexSet indexSet];
139  [self __setArrangedObjects:[CPArray array]];
140 }
141 
142 - (void)prepareContent
143 {
144  [self _setContentArray:[[self newObject]]];
145 }
150 - (BOOL)preservesSelection
151 {
152  return _preservesSelection;
153 }
154 
160 - (void)setPreservesSelection:(BOOL)value
161 {
162  _preservesSelection = value;
163 }
164 
168 - (BOOL)selectsInsertedObjects
169 {
170  return _selectsInsertedObjects;
171 }
172 
177 - (void)setSelectsInsertedObjects:(BOOL)value
178 {
179  _selectsInsertedObjects = value;
180 }
181 
185 - (BOOL)avoidsEmptySelection
186 {
187  return _avoidsEmptySelection;
188 }
189 
194 - (void)setAvoidsEmptySelection:(BOOL)value
195 {
196  _avoidsEmptySelection = value;
197 }
198 
204 - (BOOL)clearsFilterPredicateOnInsertion
205 {
206  return _clearsFilterPredicateOnInsertion;
207 }
208 
214 - (void)setClearsFilterPredicateOnInsertion:(BOOL)aFlag
215 {
216  _clearsFilterPredicateOnInsertion = aFlag;
217 }
218 
225 - (BOOL)alwaysUsesMultipleValuesMarker
226 {
227  return _alwaysUsesMultipleValuesMarker;
228 }
229 
236 - (void)setAlwaysUsesMultipleValuesMarker:(BOOL)aFlag
237 {
238  _alwaysUsesMultipleValuesMarker = aFlag;
239 }
240 
250 - (BOOL)automaticallyRearrangesObjects
251 {
252  return _automaticallyRearrangesObjects;
253 }
254 
264 - (void)setAutomaticallyRearrangesObjects:(BOOL)aFlag
265 {
266  _automaticallyRearrangesObjects = aFlag;
267 }
268 
274 - (void)setContent:(id)value
275 {
276  // This is used to ignore expected setContent: calls caused by a binding to our content
277  // object when we are the ones modifying the content object and can deal with the update
278  // faster directly in the code in charge of the modification.
279  if (_disableSetContent)
280  return;
281 
282  if (value === nil)
283  value = [];
284 
285  if (![value isKindOfClass:[CPArray class]])
286  value = [value];
287 
288  var oldSelectedObjects = nil,
289  oldSelectionIndexes = nil;
290 
291  if ([self preservesSelection])
292  oldSelectedObjects = [self selectedObjects];
293  else
294  oldSelectionIndexes = [self selectionIndexes];
295 
296  /*
297  When the contents are changed, the selected indexes may no longer refer to the
298  same items. This would cause problems when setSelectedObjects is called below.
299  Any KVO observation would try to retrieve the 'before' value which could be
300  wrong or even throw an exception for no longer existing indexes.
301 
302  To avoid that, use the internal __setSelectedObjects which fires no notifications.
303  The selectionIndexes notifications will fire later since they depend on the
304  content key. This pattern is also applied for many other methods throughout this
305  class.
306  */
307 
308  if (_clearsFilterPredicateOnInsertion)
309  [self willChangeValueForKey:@"filterPredicate"];
310 
311  // Don't use [super setContent:] as that would fire the contentObject change.
312  // We need to be in control of when notifications fire.
313  // Note that if we have a contentArray binding, setting the content does /not/
314  // cause a reverse binding set.
315  _contentObject = value;
316 
317  if (_clearsFilterPredicateOnInsertion && _filterPredicate != nil)
318  [self __setFilterPredicate:nil]; // Causes a _rearrangeObjects.
319  else
320  [self _rearrangeObjects];
321 
322  if ([self preservesSelection])
323  [self __setSelectedObjects:oldSelectedObjects];
324  else
325  [self __setSelectionIndexes:oldSelectionIndexes];
326 
327  if (_clearsFilterPredicateOnInsertion)
328  [self didChangeValueForKey:@"filterPredicate"];
329 }
330 
334 - (void)_setContentArray:(id)anArray
335 {
336  [self setContent:anArray];
337 }
338 
342 - (void)_setContentSet:(id)aSet
343 {
344  [self setContent:[aSet allObjects]];
345 }
346 
351 - (id)contentArray
352 {
353  return [self content];
354 }
355 
361 - (id)contentSet
362 {
363  return [CPSet setWithArray:[self content]];
364 }
365 
372 - (CPArray)arrangeObjects:(CPArray)objects
373 {
374  var filterPredicate = [self filterPredicate],
375  sortDescriptors = [self sortDescriptors];
376 
377  if (filterPredicate && [sortDescriptors count] > 0)
378  {
379  var sortedObjects = [objects filteredArrayUsingPredicate:filterPredicate];
380  [sortedObjects sortUsingDescriptors:sortDescriptors];
381  return sortedObjects;
382  }
383  else if (filterPredicate)
384  return [objects filteredArrayUsingPredicate:filterPredicate];
385  else if ([sortDescriptors count] > 0)
386  return [objects sortedArrayUsingDescriptors:sortDescriptors];
387 
388  return [objects copy];
389 }
390 
394 - (void)rearrangeObjects
395 {
396  [self willChangeValueForKey:@"arrangedObjects"];
397  [self _rearrangeObjects];
398  [self didChangeValueForKey:@"arrangedObjects"];
399 }
400 
401 /*
402  Like rearrangeObjects but don't fire any change notifications.
403  @ignore
404 */
405 - (void)_rearrangeObjects
406 {
407  /*
408  Rearranging reapplies the selection criteria and may cause objects to disappear,
409  so take care of the selection.
410  */
411  var oldSelectedObjects = nil,
412  oldSelectionIndexes = nil;
413 
414  if ([self preservesSelection])
415  oldSelectedObjects = [self selectedObjects];
416  else
417  oldSelectionIndexes = [self selectionIndexes];
418 
419  [self __setArrangedObjects:[self arrangeObjects:[self contentArray]]];
420 
421  if ([self preservesSelection])
422  [self __setSelectedObjects:oldSelectedObjects];
423  else
424  [self __setSelectionIndexes:oldSelectionIndexes];
425 }
426 
430 - (void)__setArrangedObjects:(id)value
431 {
432  if (_arrangedObjects === value)
433  return;
434 
435  _arrangedObjects = [[_CPObservableArray alloc] initWithArray:value];
436 }
437 
442 - (id)arrangedObjects
443 {
444  return _arrangedObjects;
445 }
446 
451 - (CPArray)sortDescriptors
452 {
453  return _sortDescriptors;
454 }
455 
461 - (void)setSortDescriptors:(CPArray)value
462 {
463  if (_sortDescriptors === value)
464  return;
465 
466  _sortDescriptors = [value copy];
467  // Use the non-notification version since arrangedObjects already depends
468  // on sortDescriptors.
469  [self _rearrangeObjects];
470 }
471 
478 - (CPPredicate)filterPredicate
479 {
480  return _filterPredicate;
481 }
482 
489 - (void)setFilterPredicate:(CPPredicate)value
490 {
491  if (_filterPredicate === value)
492  return;
493 
494  // __setFilterPredicate will call _rearrangeObjects without
495  // sending notifications, so we must send them instead.
496  [self willChangeValueForKey:@"arrangedObjects"];
497  [self __setFilterPredicate:value];
498  [self didChangeValueForKey:@"arrangedObjects"];
499 }
500 
501 /*
502  Like setFilterPredicate but don't fire any change notifications.
503  @ignore
504 */
505 - (void)__setFilterPredicate:(CPPredicate)value
506 {
507  if (_filterPredicate === value)
508  return;
509 
510  _filterPredicate = value;
511  // Use the non-notification version.
512  [self _rearrangeObjects];
513 }
514 
519 - (BOOL)alwaysUsesMultipleValuesMarker
520 {
521  return _alwaysUsesMultipleValuesMarker;
522 }
523 
524 //Selection
529 - (unsigned)selectionIndex
530 {
531  return [_selectionIndexes firstIndex];
532 }
533 
540 - (BOOL)setSelectionIndex:(unsigned)index
541 {
542  return [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]];
543 }
544 
550 - (CPIndexSet)selectionIndexes
551 {
552  return _selectionIndexes;
553 }
554 
561 - (BOOL)setSelectionIndexes:(CPIndexSet)indexes
562 {
563  [self _selectionWillChange]
564  var r = [self __setSelectionIndexes:indexes avoidEmpty:NO];
565  [self _selectionDidChange];
566  return r;
567 }
568 
569 /*
570  Like setSelectionIndex but don't fire any change notifications.
571  @ignore
572 */
573 - (BOOL)__setSelectionIndex:(int)theIndex
574 {
575  return [self __setSelectionIndexes:[CPIndexSet indexSetWithIndex:theIndex]];
576 }
577 
578 /*
579  Like setSelectionIndexes but don't fire any change notifications.
580  @ignore
581 */
582 - (BOOL)__setSelectionIndexes:(CPIndexSet)indexes
583 {
584  return [self __setSelectionIndexes:indexes avoidEmpty:_avoidsEmptySelection];
585 }
586 
587 - (BOOL)__setSelectionIndexes:(CPIndexSet)indexes avoidEmpty:(BOOL)avoidEmpty
588 {
589  var newIndexes = indexes;
590 
591  if (!newIndexes)
592  newIndexes = [CPIndexSet indexSet];
593 
594  if (![newIndexes count])
595  {
596  if (avoidEmpty && [[self arrangedObjects] count])
597  newIndexes = [CPIndexSet indexSetWithIndex:0];
598  }
599  else
600  {
601  var objectsCount = [[self arrangedObjects] count];
602 
603  // Don't trash the input - the caller might depend on it or we might have been
604  // given _selectionIndexes as the input in which case the equality test below
605  // would always succeed despite our change below.
606  newIndexes = [newIndexes copy];
607 
608  // Remove out of bounds indexes.
609  [newIndexes removeIndexesInRange:CPMakeRange(objectsCount, [newIndexes lastIndex] + 1)];
610  // When avoiding empty selection and the deleted selection was at the bottom, select the last item.
611  if (![newIndexes count] && avoidEmpty && objectsCount)
612  newIndexes = [CPIndexSet indexSetWithIndex:objectsCount - 1];
613  }
614 
615  if ([_selectionIndexes isEqualToIndexSet:newIndexes])
616  return NO;
617 
618  // If we haven't already created our own index instance, make sure to copy it here so that
619  // the copy the user sent in is decoupled from our internal copy.
620  _selectionIndexes = indexes === newIndexes ? [indexes copy] : newIndexes;
621 
622  // Push back the new selection to the model for selectionIndexes if we have one.
623  // There won't be an infinite loop because of the equality check above.
624  var binderClass = [[self class] _binderClassForBinding:@"selectionIndexes"];
625  [[binderClass getBinding:@"selectionIndexes" forObject:self] reverseSetValueFor:@"selectionIndexes"];
626 
627  return YES;
628 }
629 
634 - (CPArray)selectedObjects
635 {
636  var objects = [[self arrangedObjects] objectsAtIndexes:[self selectionIndexes]];
637 
638  return [_CPObservableArray arrayWithArray:(objects || [])];
639 }
640 
647 - (BOOL)setSelectedObjects:(CPArray)objects
648 {
649  [self willChangeValueForKey:@"selectionIndexes"];
650  [self _selectionWillChange];
651 
652  var r = [self __setSelectedObjects:objects avoidEmpty:NO];
653 
654  [self didChangeValueForKey:@"selectionIndexes"];
655  [self _selectionDidChange];
656  return r;
657 }
658 
659 /*
660  Like setSelectedObjects but don't fire any change notifications.
661  @ignore
662 */
663 - (BOOL)__setSelectedObjects:(CPArray)objects
664 {
665  [self __setSelectedObjects:objects avoidEmpty:_avoidsEmptySelection];
666 }
667 
668 - (BOOL)__setSelectedObjects:(CPArray)objects avoidEmpty:(BOOL)avoidEmpty
669 {
670  var set = [CPIndexSet indexSet],
671  count = [objects count],
672  arrangedObjects = [self arrangedObjects];
673 
674  for (var i = 0; i < count; i++)
675  {
676  var index = [arrangedObjects indexOfObject:[objects objectAtIndex:i]];
677 
678  if (index !== CPNotFound)
679  [set addIndex:index];
680  }
681 
682  [self __setSelectionIndexes:set avoidEmpty:avoidEmpty];
683  return YES;
684 }
685 
686 //Moving selection
692 - (BOOL)canSelectPrevious
693 {
694  return [[self selectionIndexes] firstIndex] > 0
695 }
696 
701 - (void)selectPrevious:(id)sender
702 {
703  var index = [[self selectionIndexes] firstIndex] - 1;
704 
705  if (index >= 0)
706  [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]];
707 }
708 
714 - (BOOL)canSelectNext
715 {
716  return [[self selectionIndexes] firstIndex] < [[self arrangedObjects] count] - 1;
717 }
718 
723 - (void)selectNext:(id)sender
724 {
725  var index = [[self selectionIndexes] firstIndex] + 1;
726 
727  if (index < [[self arrangedObjects] count])
728  [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]];
729 }
730 
731 //Add/Remove
732 
738 - (void)addObject:(id)object
739 {
740  if (![self canAdd])
741  return;
742 
743  var willClearPredicate = NO;
744  if (_clearsFilterPredicateOnInsertion && _filterPredicate)
745  {
746  [self willChangeValueForKey:@"filterPredicate"];
747  willClearPredicate = YES;
748  }
749 
750  [self willChangeValueForKey:@"content"];
751 
752  /*
753  If the content array is bound then our addObject: message below will cause the observed
754  array to change. The binding will call setContent:_contentObject on this array
755  controller to let it know about the change. We want to ignore that message since we
756  A) already have the right _contentObject and B) properly update _arrangedObjects
757  by hand below.
758  */
759  _disableSetContent = YES;
760  [_contentObject addObject:object];
761 
762  // Allow handlesContentAsCompoundValue reverse sets to trigger.
763  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
764 
765  _disableSetContent = NO;
766 
767  if (willClearPredicate)
768  {
769  // Full rearrange needed due to changed filter.
770  _filterPredicate = nil;
771  [self _rearrangeObjects];
772  }
773  else if (_filterPredicate === nil || [_filterPredicate evaluateWithObject:object])
774  {
775  // Insert directly into the array.
776  var pos = [_arrangedObjects insertObject:object inArraySortedByDescriptors:_sortDescriptors];
777 
778  // selectionIndexes change notification will be fired as a result of the
779  // content change. Don't fire manually.
780  if (_selectsInsertedObjects)
781  [self __setSelectionIndex:pos];
782  else
783  [_selectionIndexes shiftIndexesStartingAtIndex:pos by:1];
784  }
785  /*
786  else if (_filterPredicate !== nil)
787  ...
788  // Implies _filterPredicate && ![_filterPredicate evaluateWithObject:object], so the new object does
789  // not appear in arrangedObjects and we do not have to update at all.
790  */
791 
792  // This will also send notificaitons for arrangedObjects.
793  [self didChangeValueForKey:@"content"];
794  if (willClearPredicate)
795  [self didChangeValueForKey:@"filterPredicate"];
796 }
797 
805 - (void)insertObject:(id)anObject atArrangedObjectIndex:(int)anIndex
806 {
807  if (![self canAdd])
808  return;
809 
810  var willClearPredicate = NO;
811  if (_clearsFilterPredicateOnInsertion && _filterPredicate)
812  {
813  [self willChangeValueForKey:@"filterPredicate"];
814  willClearPredicate = YES;
815  }
816 
817  [self willChangeValueForKey:@"content"];
818 
819  /*
820  See _disableSetContent explanation in addObject:.
821  */
822  _disableSetContent = YES;
823 
824  // The atArrangedObjectIndex: part of this method's name only refers to where the
825  // object goes in arrangedObjects, not in the content array. So use addObject:,
826  // not insertObject:atIndex: here for speed.
827  [_contentObject addObject:anObject];
828  // Allow handlesContentAsCompoundValue reverse sets to trigger.
829  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
830 
831  _disableSetContent = NO;
832 
833  if (willClearPredicate)
834  [self __setFilterPredicate:nil];
835 
836  [[self arrangedObjects] insertObject:anObject atIndex:anIndex];
837 
838  // selectionIndexes change notification will be fired as a result of the
839  // content change. Don't fire manually.
840  if ([self selectsInsertedObjects])
841  [self __setSelectionIndex:anIndex];
842  else
844 
845  if ([self avoidsEmptySelection] && [[self selectionIndexes] count] <= 0 && [_contentObject count] > 0)
846  [self __setSelectionIndexes:[CPIndexSet indexSetWithIndex:0]];
847 
848  [self didChangeValueForKey:@"content"];
849  if (willClearPredicate)
850  [self didChangeValueForKey:@"filterPredicate"];
851 }
852 
858 - (void)removeObject:(id)object
859 {
860  [self willChangeValueForKey:@"content"];
861 
862  // See _disableSetContent explanation in addObject:.
863  _disableSetContent = YES;
864 
865  [_contentObject removeObject:object];
866  // Allow handlesContentAsCompoundValue reverse sets to trigger.
867  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
868 
869  _disableSetContent = NO;
870 
871  if (_filterPredicate === nil || [_filterPredicate evaluateWithObject:object])
872  {
873  // selectionIndexes change notification will be fired as a result of the
874  // content change. Don't fire manually.
875  var pos = [_arrangedObjects indexOfObject:object];
876 
877  [_arrangedObjects removeObjectAtIndex:pos];
878  [_selectionIndexes shiftIndexesStartingAtIndex:pos by:-1];
879 
880  // This will automatically handle the avoidsEmptySelection case.
881  [self __setSelectionIndexes:_selectionIndexes];
882  }
883 
884  [self didChangeValueForKey:@"content"];
885 }
886 
892 - (void)add:(id)sender
893 {
894  if (![self canAdd])
895  return;
896 
897  [self insert:sender];
898 }
899 
904 - (void)insert:(id)sender
905 {
906  if (![self canInsert])
907  return;
908 
909  var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject];
910 
911  [self addObject:newObject];
912 }
913 
918 - (void)remove:(id)sender
919 {
920  [self removeObjectsAtArrangedObjectIndexes:_selectionIndexes];
921 }
922 
927 - (void)removeObjectAtArrangedObjectIndex:(int)index
928 {
930 }
931 
936 - (void)removeObjectsAtArrangedObjectIndexes:(CPIndexSet)anIndexSet
937 {
938  [self willChangeValueForKey:@"content"];
939 
940  /*
941  See _disableSetContent explanation in addObject:.
942  */
943  _disableSetContent = YES;
944 
945  var arrangedObjects = [self arrangedObjects],
946  position = CPNotFound,
947  newSelectionIndexes = [_selectionIndexes copy];
948 
949  // First try the simple case which should work if there are no sort descriptors.// Since we don't have a reverse mapping between the sorted order and the// unsorted one, we'll just simply have to remove an arbitrary pointer. It might// be the 'wrong' one - as in not the one the user selected - but the wrong// one is still just another pointer to the same object, so the user will not// be able to see any difference.[anIndexSet enumerateIndexesWithOptions:CPEnumerationReverse
950  usingBlock:function(anIndex)
951  {
952  var object = [arrangedObjects objectAtIndex:anIndex];
953 
954 
955  if ([_contentObject objectAtIndex:anIndex] === object)
956  [_contentObject removeObjectAtIndex:anIndex];
957  else
958  {
959 
960 
961 
962 
963 
964  contentIndex = [_contentObject indexOfObjectIdenticalTo:object];
965  [_contentObject removeObjectAtIndex:contentIndex];
966  }
967  [arrangedObjects removeObjectAtIndex:anIndex];
968 
969  if (!_avoidsEmptySelection || [newSelectionIndexes count] > 1)
970  {
971  [newSelectionIndexes removeIndex:anIndex];
972  [newSelectionIndexes shiftIndexesStartingAtIndex:anIndex by:-1];
973  }
974  else if ([newSelectionIndexes lastIndex] !== anIndex)
975  [newSelectionIndexes shiftIndexesStartingAtIndex:anIndex by:-1];
976  }];
977 
978  // Allow handlesContentAsCompoundValue reverse sets to trigger.
979  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
980  _disableSetContent = NO;
981 
982  // This will automatically handle the avoidsEmptySelection case.
983  [self __setSelectionIndexes:newSelectionIndexes];
984 
985  [self didChangeValueForKey:@"content"];
986 }
987 
993 - (void)addObjects:(CPArray)objects
994 {
995  if (![self canAdd])
996  return;
997 
998  var contentArray = [self contentArray],
999  count = [objects count];
1000 
1001  for (var i = 0; i < count; i++)
1002  [contentArray addObject:[objects objectAtIndex:i]];
1003 
1004  [self setContent:contentArray];
1005  // Allow handlesContentAsCompoundValue reverse sets to trigger.
1006  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
1007 }
1008 
1013 - (void)removeObjects:(CPArray)objects
1014 {
1015  [self _removeObjects:objects];
1016 }
1017 
1021 - (void)_removeObjects:(CPArray)objects
1022 {
1023  [self willChangeValueForKey:@"content"];
1024 
1025  // See _disableSetContent explanation in addObject:.
1026  _disableSetContent = YES;
1027 
1028  [_contentObject removeObjectsInArray:objects];
1029  // Allow handlesContentAsCompoundValue reverse sets to trigger.
1030  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
1031 
1032  _disableSetContent = NO;
1033 
1034  var arrangedObjects = [self arrangedObjects],
1035  position = [arrangedObjects indexOfObject:[objects objectAtIndex:0]];
1036 
1037  [arrangedObjects removeObjectsInArray:objects];
1038 
1039  var objectsCount = [arrangedObjects count],
1040  selectionIndexes = [CPIndexSet indexSet];
1041 
1042  if ([self preservesSelection] || [self avoidsEmptySelection])
1043  {
1044  selectionIndexes = [CPIndexSet indexSetWithIndex:position];
1045 
1046  // Remove the selection if there are no objects
1047  if (objectsCount <= 0)
1048  selectionIndexes = [CPIndexSet indexSet];
1049 
1050  // Shift selection to last object if position is out of bounds
1051  else if (position >= objectsCount)
1052  selectionIndexes = [CPIndexSet indexSetWithIndex:objectsCount - 1];
1053  }
1054 
1055  _selectionIndexes = selectionIndexes;
1056 
1057  [self didChangeValueForKey:@"content"];
1058 }
1059 
1064 - (BOOL)canInsert
1065 {
1066  return [self isEditable];
1067 }
1068 
1069 @end
1070 
1072 
1073 + (Class)_binderClassForBinding:(CPString)theBinding
1074 {
1075  if (theBinding == @"contentArray")
1076  return [_CPArrayControllerContentBinder class];
1077 
1078  return [super _binderClassForBinding:theBinding];
1079 }
1080 
1081 @end
1082 @implementation _CPArrayControllerContentBinder : CPBinder
1083 {
1084  id __doxygen__;
1085 }
1086 
1087 - (void)setValueFor:(CPString)aBinding
1088 {
1089  var destination = [_info objectForKey:CPObservedObjectKey],
1090  keyPath = [_info objectForKey:CPObservedKeyPathKey],
1091  options = [_info objectForKey:CPOptionsKey],
1092  isCompound = [self handlesContentAsCompoundValue],
1093  dotIndex = keyPath.lastIndexOf("."),
1094  firstPart = dotIndex !== CPNotFound ? keyPath.substring(0, dotIndex) : nil,
1095  isSelectionProxy = firstPart && [[destination valueForKeyPath:firstPart] isKindOfClass:CPControllerSelectionProxy];
1096 
1097  if (!isCompound && !isSelectionProxy)
1098  {
1099  newValue = [destination mutableArrayValueForKeyPath:keyPath];
1100  }
1101  else
1102  {
1103  // 1. If handlesContentAsCompoundValue we cannot just set up a proxy.
1104  // Every read and every write must go through transformValue and
1105  // reverseTransformValue, and the resulting object cannot be described by
1106  // a key path.
1107 
1108  // 2. If isSelectionProxy, we don't want to proxy a proxy - that's bad
1109  // for performance and won't work with markers.
1110 
1111  newValue = [destination valueForKeyPath:keyPath];
1112  }
1113 
1114  var isPlaceholder = CPIsControllerMarker(newValue);
1115  if (isPlaceholder)
1116  {
1117  if (newValue === CPNotApplicableMarker && [options objectForKey:CPRaisesForNotApplicableKeysBindingOption])
1118  {
1119  [CPException raise:CPGenericException
1120  reason:@"can't transform non applicable key on: " + _source + " value: " + newValue];
1121  }
1122 
1123  newValue = [self _placeholderForMarker:newValue];
1124 
1125  // This seems to be what Cocoa does.
1126  if (!newValue)
1127  newValue = [CPMutableArray array];
1128  }
1129  else
1130  newValue = [self transformValue:newValue withOptions:options];
1131 
1132  if (isCompound)
1133  {
1134  // Make sure we can edit our copy of the content. TODO In Cocoa, this copy
1135  // appears to be deferred until the array actually needs to be edited.
1136  newValue = [newValue mutableCopy];
1137  }
1138 
1139  [_source setValue:newValue forKey:aBinding];
1140 }
1141 
1142 - (void)_contentArrayDidChange
1143 {
1144  // When handlesContentAsCompoundValue == YES, it is not sufficient to modify the content object
1145  // in place because what we are holding is an array 'unwrapped' from a compound value by
1146  // a value transformer. So when we modify it we need a reverse set and transform to create
1147  // a new compound value.
1148  //
1149  // (The Cocoa documentation on the subject is not very clear but after substantial
1150  // experimentation this seems both reasonable and compliant.)
1151  if ([self handlesContentAsCompoundValue])
1152  {
1153  var destination = [_info objectForKey:CPObservedObjectKey],
1154  keyPath = [_info objectForKey:CPObservedKeyPathKey];
1155 
1156  [self suppressSpecificNotificationFromObject:destination keyPath:keyPath];
1157  [self reverseSetValueFor:@"contentArray"];
1158  [self unsuppressSpecificNotificationFromObject:destination keyPath:keyPath];
1159  }
1160 }
1161 
1162 @end
1163 
1164 var CPArrayControllerAvoidsEmptySelection = @"CPArrayControllerAvoidsEmptySelection",
1165  CPArrayControllerClearsFilterPredicateOnInsertion = @"CPArrayControllerClearsFilterPredicateOnInsertion",
1166  CPArrayControllerFilterRestrictsInsertion = @"CPArrayControllerFilterRestrictsInsertion",
1167  CPArrayControllerPreservesSelection = @"CPArrayControllerPreservesSelection",
1168  CPArrayControllerSelectsInsertedObjects = @"CPArrayControllerSelectsInsertedObjects",
1169  CPArrayControllerAlwaysUsesMultipleValuesMarker = @"CPArrayControllerAlwaysUsesMultipleValuesMarker",
1170  CPArrayControllerAutomaticallyRearrangesObjects = @"CPArrayControllerAutomaticallyRearrangesObjects";
1171 
1173 
1174 - (id)initWithCoder:(CPCoder)aCoder
1175 {
1176  self = [super initWithCoder:aCoder];
1177 
1178  if (self)
1179  {
1180  _avoidsEmptySelection = [aCoder decodeBoolForKey:CPArrayControllerAvoidsEmptySelection];
1181  _clearsFilterPredicateOnInsertion = [aCoder decodeBoolForKey:CPArrayControllerClearsFilterPredicateOnInsertion];
1182  _filterRestrictsInsertion = [aCoder decodeBoolForKey:CPArrayControllerFilterRestrictsInsertion];
1183  _preservesSelection = [aCoder decodeBoolForKey:CPArrayControllerPreservesSelection];
1184  _selectsInsertedObjects = [aCoder decodeBoolForKey:CPArrayControllerSelectsInsertedObjects];
1185  _alwaysUsesMultipleValuesMarker = [aCoder decodeBoolForKey:CPArrayControllerAlwaysUsesMultipleValuesMarker];
1186  _automaticallyRearrangesObjects = [aCoder decodeBoolForKey:CPArrayControllerAutomaticallyRearrangesObjects];
1187  _sortDescriptors = [CPArray array];
1188 
1189  if (![self content] && [self automaticallyPreparesContent])
1190  [self prepareContent];
1191  else if (![self content])
1192  [self _setContentArray:[]];
1193  }
1194 
1195  return self;
1196 }
1197 
1198 - (void)encodeWithCoder:(CPCoder)aCoder
1199 {
1200  [super encodeWithCoder:aCoder];
1201 
1202  [aCoder encodeBool:_avoidsEmptySelection forKey:CPArrayControllerAvoidsEmptySelection];
1203  [aCoder encodeBool:_clearsFilterPredicateOnInsertion forKey:CPArrayControllerClearsFilterPredicateOnInsertion];
1204  [aCoder encodeBool:_filterRestrictsInsertion forKey:CPArrayControllerFilterRestrictsInsertion];
1205  [aCoder encodeBool:_preservesSelection forKey:CPArrayControllerPreservesSelection];
1206  [aCoder encodeBool:_selectsInsertedObjects forKey:CPArrayControllerSelectsInsertedObjects];
1207  [aCoder encodeBool:_alwaysUsesMultipleValuesMarker forKey:CPArrayControllerAlwaysUsesMultipleValuesMarker];
1208  [aCoder encodeBool:_automaticallyRearrangesObjects forKey:CPArrayControllerAutomaticallyRearrangesObjects];
1209 }
1210 
1211 - (void)awakeFromCib
1212 {
1213  [self _selectionWillChange];
1214  [self _selectionDidChange];
1215 }
1216 
1217 @end