API  0.9.7
 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 
745  if (_clearsFilterPredicateOnInsertion && _filterPredicate)
746  {
747  [self willChangeValueForKey:@"filterPredicate"];
748  willClearPredicate = YES;
749  }
750 
751  [self willChangeValueForKey:@"content"];
752 
753  /*
754  If the content array is bound then our addObject: message below will cause the observed
755  array to change. The binding will call setContent:_contentObject on this array
756  controller to let it know about the change. We want to ignore that message since we
757  A) already have the right _contentObject and B) properly update _arrangedObjects
758  by hand below.
759  */
760  _disableSetContent = YES;
761  [_contentObject addObject:object];
762 
763  // Allow handlesContentAsCompoundValue reverse sets to trigger.
764  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
765 
766  _disableSetContent = NO;
767 
768  if (willClearPredicate)
769  {
770  // Full rearrange needed due to changed filter.
771  _filterPredicate = nil;
772  [self _rearrangeObjects];
773  }
774  else if (_filterPredicate === nil || [_filterPredicate evaluateWithObject:object])
775  {
776  // Insert directly into the array.
777  var pos = [_arrangedObjects insertObject:object inArraySortedByDescriptors:_sortDescriptors];
778 
779  // selectionIndexes change notification will be fired as a result of the
780  // content change. Don't fire manually.
781  if (_selectsInsertedObjects)
782  [self __setSelectionIndex:pos];
783  else
784  [_selectionIndexes shiftIndexesStartingAtIndex:pos by:1];
785  }
786  /*
787  else if (_filterPredicate !== nil)
788  ...
789  // Implies _filterPredicate && ![_filterPredicate evaluateWithObject:object], so the new object does
790  // not appear in arrangedObjects and we do not have to update at all.
791  */
792 
793  // TODO: Remove these lines when granular notifications are implemented
794  var proxy = [_CPKVOProxy proxyForObject:self];
795  [proxy setAdding:YES];
796 
797  // This will also send notifications for arrangedObjects.
798  [self didChangeValueForKey:@"content"];
799 
800  if (willClearPredicate)
801  [self didChangeValueForKey:@"filterPredicate"];
802 
803  // TODO: Remove this line when granular notifications are implemented
804  [proxy setAdding:NO];
805 }
806 
814 - (void)insertObject:(id)anObject atArrangedObjectIndex:(int)anIndex
815 {
816  if (![self canAdd])
817  return;
818 
819  var willClearPredicate = NO;
820 
821  if (_clearsFilterPredicateOnInsertion && _filterPredicate)
822  {
823  [self willChangeValueForKey:@"filterPredicate"];
824  willClearPredicate = YES;
825  }
826 
827  [self willChangeValueForKey:@"content"];
828 
829  /*
830  See _disableSetContent explanation in addObject:.
831  */
832  _disableSetContent = YES;
833 
834  // The atArrangedObjectIndex: part of this method's name only refers to where the
835  // object goes in arrangedObjects, not in the content array. So use addObject:,
836  // not insertObject:atIndex: here for speed.
837  [_contentObject addObject:anObject];
838  // Allow handlesContentAsCompoundValue reverse sets to trigger.
839  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
840 
841  _disableSetContent = NO;
842 
843  if (willClearPredicate)
844  [self __setFilterPredicate:nil];
845 
846  [[self arrangedObjects] insertObject:anObject atIndex:anIndex];
847 
848  // selectionIndexes change notification will be fired as a result of the
849  // content change. Don't fire manually.
850  if ([self selectsInsertedObjects])
851  [self __setSelectionIndex:anIndex];
852  else
854 
855  if ([self avoidsEmptySelection] && [[self selectionIndexes] count] <= 0 && [_contentObject count] > 0)
856  [self __setSelectionIndexes:[CPIndexSet indexSetWithIndex:0]];
857 
858  var proxy = [_CPKVOProxy proxyForObject:self];
859  [proxy setAdding:YES];
860 
861  [self didChangeValueForKey:@"content"];
862 
863  if (willClearPredicate)
864  [self didChangeValueForKey:@"filterPredicate"];
865 
866  [proxy setAdding:NO];
867 }
868 
874 - (void)removeObject:(id)object
875 {
876  [self willChangeValueForKey:@"content"];
877 
878  // See _disableSetContent explanation in addObject:.
879  _disableSetContent = YES;
880 
881  [_contentObject removeObject:object];
882  // Allow handlesContentAsCompoundValue reverse sets to trigger.
883  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
884 
885  _disableSetContent = NO;
886 
887  if (_filterPredicate === nil || [_filterPredicate evaluateWithObject:object])
888  {
889  // selectionIndexes change notification will be fired as a result of the
890  // content change. Don't fire manually.
891  var pos = [_arrangedObjects indexOfObject:object];
892 
893  [_arrangedObjects removeObjectAtIndex:pos];
894  [_selectionIndexes shiftIndexesStartingAtIndex:pos by:-1];
895 
896  // This will automatically handle the avoidsEmptySelection case.
897  [self __setSelectionIndexes:_selectionIndexes];
898  }
899 
900  [self didChangeValueForKey:@"content"];
901 }
902 
908 - (void)add:(id)sender
909 {
910  if (![self canAdd])
911  return;
912 
913  var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject];
914 
915  [self addObject:newObject];
916 }
917 
922 - (void)insert:(id)sender
923 {
924  if (![self canInsert])
925  return;
926 
927  var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject],
928  lastSelectedIndex = [_selectionIndexes lastIndex];
929 
930  if (lastSelectedIndex !== CPNotFound)
931  [self insertObject:newObject atArrangedObjectIndex:lastSelectedIndex];
932  else
933  [self addObject:newObject];
934 }
935 
940 - (void)remove:(id)sender
941 {
942  [self removeObjectsAtArrangedObjectIndexes:_selectionIndexes];
943 }
944 
949 - (void)removeObjectAtArrangedObjectIndex:(int)index
950 {
952 }
953 
958 - (void)removeObjectsAtArrangedObjectIndexes:(CPIndexSet)anIndexSet
959 {
960  [self willChangeValueForKey:@"content"];
961 
962  /*
963  See _disableSetContent explanation in addObject:.
964  */
965  _disableSetContent = YES;
966 
967  var arrangedObjects = [self arrangedObjects],
968  position = CPNotFound,
969  newSelectionIndexes = [_selectionIndexes copy];
970 
971  // 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
972  usingBlock:function(anIndex)
973  {
974  var object = [arrangedObjects objectAtIndex:anIndex];
975 
976 
977  if ([_contentObject objectAtIndex:anIndex] === object)
978  [_contentObject removeObjectAtIndex:anIndex];
979  else
980  {
981 
982 
983 
984 
985 
986  var contentIndex = [_contentObject indexOfObjectIdenticalTo:object];
987  [_contentObject removeObjectAtIndex:contentIndex];
988  }
989  [arrangedObjects removeObjectAtIndex:anIndex];
990 
991  if (!_avoidsEmptySelection || [newSelectionIndexes count] > 1)
992  {
993  [newSelectionIndexes removeIndex:anIndex];
994  [newSelectionIndexes shiftIndexesStartingAtIndex:anIndex by:-1];
995  }
996  else if ([newSelectionIndexes lastIndex] !== anIndex)
997  [newSelectionIndexes shiftIndexesStartingAtIndex:anIndex by:-1];
998  }];
999 
1000  // Allow handlesContentAsCompoundValue reverse sets to trigger.
1001  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
1002  _disableSetContent = NO;
1003 
1004  // This will automatically handle the avoidsEmptySelection case.
1005  [self __setSelectionIndexes:newSelectionIndexes];
1006 
1007  [self didChangeValueForKey:@"content"];
1008 }
1009 
1015 - (void)addObjects:(CPArray)objects
1016 {
1017  if (![self canAdd])
1018  return;
1019 
1020  var contentArray = [self contentArray],
1021  count = [objects count];
1022 
1023  for (var i = 0; i < count; i++)
1024  [contentArray addObject:[objects objectAtIndex:i]];
1025 
1026  [self setContent:contentArray];
1027  // Allow handlesContentAsCompoundValue reverse sets to trigger.
1028  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
1029 }
1030 
1035 - (void)removeObjects:(CPArray)objects
1036 {
1037  [self _removeObjects:objects];
1038 }
1039 
1043 - (void)_removeObjects:(CPArray)objects
1044 {
1045  [self willChangeValueForKey:@"content"];
1046 
1047  // See _disableSetContent explanation in addObject:.
1048  _disableSetContent = YES;
1049 
1050  [_contentObject removeObjectsInArray:objects];
1051  // Allow handlesContentAsCompoundValue reverse sets to trigger.
1052  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
1053 
1054  _disableSetContent = NO;
1055 
1056  var arrangedObjects = [self arrangedObjects],
1057  position = [arrangedObjects indexOfObject:[objects objectAtIndex:0]];
1058 
1059  [arrangedObjects removeObjectsInArray:objects];
1060 
1061  var objectsCount = [arrangedObjects count],
1062  selectionIndexes = [CPIndexSet indexSet];
1063 
1064  if ([self preservesSelection] || [self avoidsEmptySelection])
1065  {
1066  selectionIndexes = [CPIndexSet indexSetWithIndex:position];
1067 
1068  // Remove the selection if there are no objects
1069  if (objectsCount <= 0)
1070  selectionIndexes = [CPIndexSet indexSet];
1071 
1072  // Shift selection to last object if position is out of bounds
1073  else if (position >= objectsCount)
1074  selectionIndexes = [CPIndexSet indexSetWithIndex:objectsCount - 1];
1075  }
1076 
1077  _selectionIndexes = selectionIndexes;
1078 
1079  [self didChangeValueForKey:@"content"];
1080 }
1081 
1086 - (BOOL)canInsert
1087 {
1088  return [self isEditable];
1089 }
1090 
1091 @end
1092 
1094 
1095 + (Class)_binderClassForBinding:(CPString)aBinding
1096 {
1097  if (aBinding == @"contentArray")
1098  return [_CPArrayControllerContentBinder class];
1099 
1100  return [super _binderClassForBinding:aBinding];
1101 }
1102 
1103 @end
1104 @implementation _CPArrayControllerContentBinder : CPBinder
1105 {
1106  id __doxygen__;
1107 }
1108 
1109 - (void)setValueFor:(CPString)aBinding
1110 {
1111  var destination = [_info objectForKey:CPObservedObjectKey],
1112  keyPath = [_info objectForKey:CPObservedKeyPathKey],
1113  options = [_info objectForKey:CPOptionsKey],
1114  isCompound = [self handlesContentAsCompoundValue],
1115  dotIndex = keyPath.lastIndexOf("."),
1116  firstPart = dotIndex !== CPNotFound ? keyPath.substring(0, dotIndex) : nil,
1117  isSelectionProxy = firstPart && [[destination valueForKeyPath:firstPart] isKindOfClass:CPControllerSelectionProxy],
1118  newValue;
1119 
1120  if (!isCompound && !isSelectionProxy)
1121  {
1122  newValue = [destination mutableArrayValueForKeyPath:keyPath];
1123  }
1124  else
1125  {
1126  // 1. If handlesContentAsCompoundValue we cannot just set up a proxy.
1127  // Every read and every write must go through transformValue and
1128  // reverseTransformValue, and the resulting object cannot be described by
1129  // a key path.
1130 
1131  // 2. If isSelectionProxy, we don't want to proxy a proxy - that's bad
1132  // for performance and won't work with markers.
1133 
1134  newValue = [destination valueForKeyPath:keyPath];
1135  }
1136 
1137  var isPlaceholder = CPIsControllerMarker(newValue);
1138 
1139  if (isPlaceholder)
1140  {
1141  if (newValue === CPNotApplicableMarker && [options objectForKey:CPRaisesForNotApplicableKeysBindingOption])
1142  {
1143  [CPException raise:CPGenericException
1144  reason:@"can't transform non applicable key on: " + _source + " value: " + newValue];
1145  }
1146 
1147  newValue = [self _placeholderForMarker:newValue];
1148 
1149  // This seems to be what Cocoa does.
1150  if (!newValue)
1151  newValue = [CPMutableArray array];
1152  }
1153  else
1154  newValue = [self transformValue:newValue withOptions:options];
1155 
1156  if (isCompound)
1157  {
1158  // Make sure we can edit our copy of the content. TODO In Cocoa, this copy
1159  // appears to be deferred until the array actually needs to be edited.
1160  newValue = [newValue mutableCopy];
1161  }
1162 
1163  [_source setValue:newValue forKey:aBinding];
1164 }
1165 
1166 - (void)_contentArrayDidChange
1167 {
1168  // When handlesContentAsCompoundValue == YES, it is not sufficient to modify the content object
1169  // in place because what we are holding is an array 'unwrapped' from a compound value by
1170  // a value transformer. So when we modify it we need a reverse set and transform to create
1171  // a new compound value.
1172  //
1173  // (The Cocoa documentation on the subject is not very clear but after substantial
1174  // experimentation this seems both reasonable and compliant.)
1175  if ([self handlesContentAsCompoundValue])
1176  {
1177  var destination = [_info objectForKey:CPObservedObjectKey],
1178  keyPath = [_info objectForKey:CPObservedKeyPathKey];
1179 
1180  [self suppressSpecificNotificationFromObject:destination keyPath:keyPath];
1181  [self reverseSetValueFor:@"contentArray"];
1182  [self unsuppressSpecificNotificationFromObject:destination keyPath:keyPath];
1183  }
1184 }
1185 
1186 @end
1187 
1188 var CPArrayControllerAvoidsEmptySelection = @"CPArrayControllerAvoidsEmptySelection",
1189  CPArrayControllerClearsFilterPredicateOnInsertion = @"CPArrayControllerClearsFilterPredicateOnInsertion",
1190  CPArrayControllerFilterRestrictsInsertion = @"CPArrayControllerFilterRestrictsInsertion",
1191  CPArrayControllerPreservesSelection = @"CPArrayControllerPreservesSelection",
1192  CPArrayControllerSelectsInsertedObjects = @"CPArrayControllerSelectsInsertedObjects",
1193  CPArrayControllerAlwaysUsesMultipleValuesMarker = @"CPArrayControllerAlwaysUsesMultipleValuesMarker",
1194  CPArrayControllerAutomaticallyRearrangesObjects = @"CPArrayControllerAutomaticallyRearrangesObjects";
1195 
1197 
1198 - (id)initWithCoder:(CPCoder)aCoder
1199 {
1200  self = [super initWithCoder:aCoder];
1201 
1202  if (self)
1203  {
1204  _avoidsEmptySelection = [aCoder decodeBoolForKey:CPArrayControllerAvoidsEmptySelection];
1205  _clearsFilterPredicateOnInsertion = [aCoder decodeBoolForKey:CPArrayControllerClearsFilterPredicateOnInsertion];
1206  _filterRestrictsInsertion = [aCoder decodeBoolForKey:CPArrayControllerFilterRestrictsInsertion];
1207  _preservesSelection = [aCoder decodeBoolForKey:CPArrayControllerPreservesSelection];
1208  _selectsInsertedObjects = [aCoder decodeBoolForKey:CPArrayControllerSelectsInsertedObjects];
1209  _alwaysUsesMultipleValuesMarker = [aCoder decodeBoolForKey:CPArrayControllerAlwaysUsesMultipleValuesMarker];
1210  _automaticallyRearrangesObjects = [aCoder decodeBoolForKey:CPArrayControllerAutomaticallyRearrangesObjects];
1211  _sortDescriptors = [CPArray array];
1212 
1213  if (![self content] && [self automaticallyPreparesContent])
1214  [self prepareContent];
1215  else if (![self content])
1216  [self _setContentArray:[]];
1217  }
1218 
1219  return self;
1220 }
1221 
1222 - (void)encodeWithCoder:(CPCoder)aCoder
1223 {
1224  [super encodeWithCoder:aCoder];
1225 
1226  [aCoder encodeBool:_avoidsEmptySelection forKey:CPArrayControllerAvoidsEmptySelection];
1227  [aCoder encodeBool:_clearsFilterPredicateOnInsertion forKey:CPArrayControllerClearsFilterPredicateOnInsertion];
1228  [aCoder encodeBool:_filterRestrictsInsertion forKey:CPArrayControllerFilterRestrictsInsertion];
1229  [aCoder encodeBool:_preservesSelection forKey:CPArrayControllerPreservesSelection];
1230  [aCoder encodeBool:_selectsInsertedObjects forKey:CPArrayControllerSelectsInsertedObjects];
1231  [aCoder encodeBool:_alwaysUsesMultipleValuesMarker forKey:CPArrayControllerAlwaysUsesMultipleValuesMarker];
1232  [aCoder encodeBool:_automaticallyRearrangesObjects forKey:CPArrayControllerAutomaticallyRearrangesObjects];
1233 }
1234 
1235 - (void)awakeFromCib
1236 {
1237  [self _selectionWillChange];
1238  [self _selectionDidChange];
1239 }
1240 
1241 @end