API  0.9.10
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 
33 @implementation CPArrayController : CPObjectController
34 {
35  BOOL _avoidsEmptySelection;
36  BOOL _clearsFilterPredicateOnInsertion;
37  BOOL _filterRestrictsInsertion;
38  BOOL _preservesSelection;
39  BOOL _selectsInsertedObjects;
40  BOOL _alwaysUsesMultipleValuesMarker;
41 
42  BOOL _automaticallyRearrangesObjects; // FIXME: Not in use
43 
44  CPIndexSet _selectionIndexes;
45  CPArray _sortDescriptors;
46  CPPredicate _filterPredicate;
47  CPArray _arrangedObjects;
48 
49  BOOL _disableSetContent;
50 }
51 
52 + (void)initialize
53 {
54  if (self !== [CPArrayController class])
55  return;
56 
57  [self exposeBinding:@"contentArray"];
58  [self exposeBinding:@"contentSet"];
59 }
60 
62 {
63  return [CPSet setWithObjects:@"content"];
64 }
65 
67 {
68  // Also depends on "filterPredicate" but we'll handle that manually.
69  return [CPSet setWithObjects:@"content", @"sortDescriptors"];
70 }
71 
73 {
74  return [CPSet setWithObjects:@"selectionIndexes"];
75 }
76 
78 {
79  return [CPSet setWithObjects:@"selectionIndexes"];
80 }
81 
83 {
84  // When the arranged objects change, selection preservation may cause the indexes
85  // to change.
86  return [CPSet setWithObjects:@"arrangedObjects"];
87 }
88 
90 {
91  // Don't need to depend on arrangedObjects here because selectionIndexes already does.
92  return [CPSet setWithObjects:@"selectionIndexes"];
93 }
94 
96 {
97  return [CPSet setWithObjects:@"selectionIndexes"];
98 }
99 
101 {
102  return [CPSet setWithObjects:@"selectionIndexes"];
103 }
104 
106 {
107  return [CPSet setWithObjects:@"selectionIndexes"];
108 }
109 
110 
111 - (id)init
112 {
113  self = [super init];
114 
115  if (self)
116  {
117  _preservesSelection = YES;
118  _selectsInsertedObjects = YES;
119  _avoidsEmptySelection = YES;
120  _clearsFilterPredicateOnInsertion = YES;
121  _alwaysUsesMultipleValuesMarker = NO;
122  _automaticallyRearrangesObjects = NO;
123 
124  _filterRestrictsInsertion = YES; // FIXME: Not in use
125 
126  [self _init];
127  }
128 
129  return self;
130 }
131 
132 - (void)_init
133 {
134  _sortDescriptors = [CPArray array];
135  _filterPredicate = nil;
136  _selectionIndexes = [CPIndexSet indexSet];
137  [self __setArrangedObjects:[CPArray array]];
138 }
139 
141 {
142  [self _setContentArray:[[self newObject]]];
143 }
149 {
150  return _preservesSelection;
151 }
152 
158 - (void)setPreservesSelection:(BOOL)value
159 {
160  _preservesSelection = value;
161 }
162 
167 {
168  return _selectsInsertedObjects;
169 }
170 
175 - (void)setSelectsInsertedObjects:(BOOL)value
176 {
177  _selectsInsertedObjects = value;
178 }
179 
184 {
185  return _avoidsEmptySelection;
186 }
187 
192 - (void)setAvoidsEmptySelection:(BOOL)value
193 {
194  _avoidsEmptySelection = value;
195 }
196 
203 {
204  return _clearsFilterPredicateOnInsertion;
205 }
206 
212 - (void)setClearsFilterPredicateOnInsertion:(BOOL)aFlag
213 {
214  _clearsFilterPredicateOnInsertion = aFlag;
215 }
216 
224 {
225  return _alwaysUsesMultipleValuesMarker;
226 }
227 
234 - (void)setAlwaysUsesMultipleValuesMarker:(BOOL)aFlag
235 {
236  _alwaysUsesMultipleValuesMarker = aFlag;
237 }
238 
249 {
250  return _automaticallyRearrangesObjects;
251 }
252 
262 - (void)setAutomaticallyRearrangesObjects:(BOOL)aFlag
263 {
264  _automaticallyRearrangesObjects = aFlag;
265 }
266 
272 - (void)setContent:(id)value
273 {
274  // This is used to ignore expected setContent: calls caused by a binding to our content
275  // object when we are the ones modifying the content object and can deal with the update
276  // faster directly in the code in charge of the modification.
277  if (_disableSetContent)
278  return;
279 
280  if (value === nil)
281  value = [];
282 
283  if (![value isKindOfClass:[CPArray class]])
284  value = [value];
285 
286  var oldSelectedObjects = nil,
287  oldSelectionIndexes = nil;
288 
289  if ([self preservesSelection])
290  oldSelectedObjects = [self selectedObjects];
291  else
292  oldSelectionIndexes = [self selectionIndexes];
293 
294  /*
295  When the contents are changed, the selected indexes may no longer refer to the
296  same items. This would cause problems when setSelectedObjects is called below.
297  Any KVO observation would try to retrieve the 'before' value which could be
298  wrong or even throw an exception for no longer existing indexes.
299 
300  To avoid that, use the internal __setSelectedObjects which fires no notifications.
301  The selectionIndexes notifications will fire later since they depend on the
302  content key. This pattern is also applied for many other methods throughout this
303  class.
304  */
305 
306  if (_clearsFilterPredicateOnInsertion)
307  [self willChangeValueForKey:@"filterPredicate"];
308 
309  // Don't use [super setContent:] as that would fire the contentObject change.
310  // We need to be in control of when notifications fire.
311  // Note that if we have a contentArray binding, setting the content does /not/
312  // cause a reverse binding set.
313  _contentObject = value;
314 
315  if (_clearsFilterPredicateOnInsertion && _filterPredicate != nil)
316  [self __setFilterPredicate:nil]; // Causes a _rearrangeObjects.
317  else
318  [self _rearrangeObjects];
319 
320  if ([self preservesSelection])
321  [self __setSelectedObjects:oldSelectedObjects];
322  else
323  [self __setSelectionIndexes:oldSelectionIndexes];
324 
325  if (_clearsFilterPredicateOnInsertion)
326  [self didChangeValueForKey:@"filterPredicate"];
327 }
328 
332 - (void)_setContentArray:(id)anArray
333 {
334  [self setContent:anArray];
335 }
336 
340 - (void)_setContentSet:(id)aSet
341 {
342  [self setContent:[aSet allObjects]];
343 }
344 
350 {
351  return [self content];
352 }
353 
360 {
361  return [CPSet setWithArray:[self content]];
362 }
363 
370 - (CPArray)arrangeObjects:(CPArray)objects
371 {
372  var filterPredicate = [self filterPredicate],
374 
375  if (filterPredicate && [sortDescriptors count] > 0)
376  {
377  var sortedObjects = [objects filteredArrayUsingPredicate:filterPredicate];
378  [sortedObjects sortUsingDescriptors:sortDescriptors];
379  return sortedObjects;
380  }
381  else if (filterPredicate)
382  return [objects filteredArrayUsingPredicate:filterPredicate];
383  else if ([sortDescriptors count] > 0)
384  return [objects sortedArrayUsingDescriptors:sortDescriptors];
385 
386  return [objects copy];
387 }
388 
393 {
394  [self willChangeValueForKey:@"arrangedObjects"];
395  [self _rearrangeObjects];
396  [self didChangeValueForKey:@"arrangedObjects"];
397 }
398 
399 /*
400  Like rearrangeObjects but don't fire any change notifications.
401  @ignore
402 */
403 - (void)_rearrangeObjects
404 {
405  /*
406  Rearranging reapplies the selection criteria and may cause objects to disappear,
407  so take care of the selection.
408  */
409  var oldSelectedObjects = nil,
410  oldSelectionIndexes = nil;
411 
412  if ([self preservesSelection])
413  oldSelectedObjects = [self selectedObjects];
414  else
415  oldSelectionIndexes = [self selectionIndexes];
416 
417  [self __setArrangedObjects:[self arrangeObjects:[self contentArray]]];
418 
419  if ([self preservesSelection])
420  [self __setSelectedObjects:oldSelectedObjects];
421  else
422  [self __setSelectionIndexes:oldSelectionIndexes];
423 }
424 
428 - (void)__setArrangedObjects:(id)value
429 {
430  if (_arrangedObjects === value)
431  return;
432 
433  _arrangedObjects = [[_CPObservableArray alloc] initWithArray:value];
434 }
435 
441 {
442  return _arrangedObjects;
443 }
444 
449 - (CPArray)sortDescriptors
450 {
451  return _sortDescriptors;
452 }
453 
459 - (void)setSortDescriptors:(CPArray)value
460 {
461  if (_sortDescriptors === value)
462  return;
463 
464  _sortDescriptors = [value copy];
465  // Use the non-notification version since arrangedObjects already depends
466  // on sortDescriptors.
467  [self _rearrangeObjects];
468 }
469 
476 - (CPPredicate)filterPredicate
477 {
478  return _filterPredicate;
479 }
480 
487 - (void)setFilterPredicate:(CPPredicate)value
488 {
489  if (_filterPredicate === value)
490  return;
491 
492  // __setFilterPredicate will call _rearrangeObjects without
493  // sending notifications, so we must send them instead.
494  [self willChangeValueForKey:@"arrangedObjects"];
495  [self __setFilterPredicate:value];
496  [self didChangeValueForKey:@"arrangedObjects"];
497 }
498 
499 /*
500  Like setFilterPredicate but don't fire any change notifications.
501  @ignore
502 */
503 - (void)__setFilterPredicate:(CPPredicate)value
504 {
505  if (_filterPredicate === value)
506  return;
507 
508  _filterPredicate = value;
509  // Use the non-notification version.
510  [self _rearrangeObjects];
511 }
512 
518 {
519  return _alwaysUsesMultipleValuesMarker;
520 }
521 
522 //Selection
527 - (unsigned)selectionIndex
528 {
529  return [_selectionIndexes firstIndex];
530 }
531 
538 - (BOOL)setSelectionIndex:(unsigned)index
539 {
540  return [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]];
541 }
542 
549 {
550  return _selectionIndexes;
551 }
552 
559 - (BOOL)setSelectionIndexes:(CPIndexSet)indexes
560 {
561  [self _selectionWillChange]
562  var r = [self __setSelectionIndexes:indexes avoidEmpty:NO];
563  [self _selectionDidChange];
564  return r;
565 }
566 
567 /*
568  Like setSelectionIndex but don't fire any change notifications.
569  @ignore
570 */
571 - (BOOL)__setSelectionIndex:(int)theIndex
572 {
573  return [self __setSelectionIndexes:[CPIndexSet indexSetWithIndex:theIndex]];
574 }
575 
576 /*
577  Like setSelectionIndexes but don't fire any change notifications.
578  @ignore
579 */
580 - (BOOL)__setSelectionIndexes:(CPIndexSet)indexes
581 {
582  return [self __setSelectionIndexes:indexes avoidEmpty:_avoidsEmptySelection];
583 }
584 
585 - (BOOL)__setSelectionIndexes:(CPIndexSet)indexes avoidEmpty:(BOOL)avoidEmpty
586 {
587  var newIndexes = indexes;
588 
589  if (!newIndexes)
590  newIndexes = [CPIndexSet indexSet];
591 
592  if (![newIndexes count])
593  {
594  if (avoidEmpty && [[self arrangedObjects] count])
595  newIndexes = [CPIndexSet indexSetWithIndex:0];
596  }
597  else
598  {
599  var objectsCount = [[self arrangedObjects] count];
600 
601  // Don't trash the input - the caller might depend on it or we might have been
602  // given _selectionIndexes as the input in which case the equality test below
603  // would always succeed despite our change below.
604  newIndexes = [newIndexes copy];
605 
606  // Remove out of bounds indexes.
607  [newIndexes removeIndexesInRange:CPMakeRange(objectsCount, [newIndexes lastIndex] + 1)];
608  // When avoiding empty selection and the deleted selection was at the bottom, select the last item.
609  if (![newIndexes count] && avoidEmpty && objectsCount)
610  newIndexes = [CPIndexSet indexSetWithIndex:objectsCount - 1];
611  }
612 
613  if ([_selectionIndexes isEqualToIndexSet:newIndexes])
614  return NO;
615 
616  // If we haven't already created our own index instance, make sure to copy it here so that
617  // the copy the user sent in is decoupled from our internal copy.
618  _selectionIndexes = indexes === newIndexes ? [indexes copy] : newIndexes;
619 
620  // Push back the new selection to the model for selectionIndexes if we have one.
621  // There won't be an infinite loop because of the equality check above.
622  var binderClass = [[self class] _binderClassForBinding:@"selectionIndexes"];
623  [[binderClass getBinding:@"selectionIndexes" forObject:self] reverseSetValueFor:@"selectionIndexes"];
624 
625  return YES;
626 }
627 
632 - (CPArray)selectedObjects
633 {
634  var objects = [[self arrangedObjects] objectsAtIndexes:[self selectionIndexes]];
635 
636  return [_CPObservableArray arrayWithArray:(objects || [])];
637 }
638 
645 - (BOOL)setSelectedObjects:(CPArray)objects
646 {
647  [self willChangeValueForKey:@"selectionIndexes"];
648  [self _selectionWillChange];
649 
650  var r = [self __setSelectedObjects:objects avoidEmpty:NO];
651 
652  [self didChangeValueForKey:@"selectionIndexes"];
653  [self _selectionDidChange];
654  return r;
655 }
656 
657 /*
658  Like setSelectedObjects but don't fire any change notifications.
659  @ignore
660 */
661 - (BOOL)__setSelectedObjects:(CPArray)objects
662 {
663  [self __setSelectedObjects:objects avoidEmpty:_avoidsEmptySelection];
664 }
665 
666 - (BOOL)__setSelectedObjects:(CPArray)objects avoidEmpty:(BOOL)avoidEmpty
667 {
668  var set = [CPIndexSet indexSet],
669  count = [objects count],
671 
672  for (var i = 0; i < count; i++)
673  {
674  var index = [arrangedObjects indexOfObject:[objects objectAtIndex:i]];
675 
676  if (index !== CPNotFound)
677  [set addIndex:index];
678  }
679 
680  [self __setSelectionIndexes:set avoidEmpty:avoidEmpty];
681  return YES;
682 }
683 
684 //Moving selection
691 {
692  return [[self selectionIndexes] firstIndex] > 0
693 }
694 
699 - (void)selectPrevious:(id)sender
700 {
701  var index = [[self selectionIndexes] firstIndex] - 1;
702 
703  if (index >= 0)
705 }
706 
713 {
714  return [[self selectionIndexes] firstIndex] < [[self arrangedObjects] count] - 1;
715 }
716 
721 - (void)selectNext:(id)sender
722 {
723  var index = [[self selectionIndexes] firstIndex] + 1;
724 
725  if (index < [[self arrangedObjects] count])
727 }
728 
729 //Add/Remove
730 
736 - (void)addObject:(id)object
737 {
738  if (![self canAdd])
739  return;
740 
741  var willClearPredicate = NO;
742 
743  if (_clearsFilterPredicateOnInsertion && _filterPredicate)
744  {
745  [self willChangeValueForKey:@"filterPredicate"];
746  willClearPredicate = YES;
747  }
748 
749  [self willChangeValueForKey:@"content"];
750 
751  /*
752  If the content array is bound then our addObject: message below will cause the observed
753  array to change. The binding will call setContent:_contentObject on this array
754  controller to let it know about the change. We want to ignore that message since we
755  A) already have the right _contentObject and B) properly update _arrangedObjects
756  by hand below.
757  */
758  _disableSetContent = YES;
759  [_contentObject addObject:object];
760 
761  // Allow handlesContentAsCompoundValue reverse sets to trigger.
762  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
763 
764  _disableSetContent = NO;
765 
766  if (willClearPredicate)
767  {
768  // Full rearrange needed due to changed filter.
769  _filterPredicate = nil;
770  [self _rearrangeObjects];
771  }
772  else if (_filterPredicate === nil || [_filterPredicate evaluateWithObject:object])
773  {
774  // Insert directly into the array.
775  var pos = [_arrangedObjects insertObject:object inArraySortedByDescriptors:_sortDescriptors];
776 
777  // selectionIndexes change notification will be fired as a result of the
778  // content change. Don't fire manually.
779  if (_selectsInsertedObjects)
780  [self __setSelectionIndex:pos];
781  else
782  [_selectionIndexes shiftIndexesStartingAtIndex:pos by:1];
783  }
784  /*
785  else if (_filterPredicate !== nil)
786  ...
787  // Implies _filterPredicate && ![_filterPredicate evaluateWithObject:object], so the new object does
788  // not appear in arrangedObjects and we do not have to update at all.
789  */
790 
791  // TODO: Remove these lines when granular notifications are implemented
792  var proxy = [_CPKVOProxy proxyForObject:self];
793  [proxy setAdding:YES];
794 
795  // This will also send notifications for arrangedObjects.
796  [self didChangeValueForKey:@"content"];
797 
798  if (willClearPredicate)
799  [self didChangeValueForKey:@"filterPredicate"];
800 
801  // TODO: Remove this line when granular notifications are implemented
802  [proxy setAdding:NO];
803 }
804 
812 - (void)insertObject:(id)anObject atArrangedObjectIndex:(int)anIndex
813 {
814  if (![self canAdd])
815  return;
816 
817  var willClearPredicate = NO;
818 
819  if (_clearsFilterPredicateOnInsertion && _filterPredicate)
820  {
821  [self willChangeValueForKey:@"filterPredicate"];
822  willClearPredicate = YES;
823  }
824 
825  [self willChangeValueForKey:@"content"];
826 
827  /*
828  See _disableSetContent explanation in addObject:.
829  */
830  _disableSetContent = YES;
831 
832  // The atArrangedObjectIndex: part of this method's name only refers to where the
833  // object goes in arrangedObjects, not in the content array. So use addObject:,
834  // not insertObject:atIndex: here for speed.
835  [_contentObject addObject:anObject];
836  // Allow handlesContentAsCompoundValue reverse sets to trigger.
837  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
838 
839  _disableSetContent = NO;
840 
841  if (willClearPredicate)
842  [self __setFilterPredicate:nil];
843 
844  [[self arrangedObjects] insertObject:anObject atIndex:anIndex];
845 
846  // selectionIndexes change notification will be fired as a result of the
847  // content change. Don't fire manually.
848  if ([self selectsInsertedObjects])
849  [self __setSelectionIndex:anIndex];
850  else
852 
853  if ([self avoidsEmptySelection] && [[self selectionIndexes] count] <= 0 && [_contentObject count] > 0)
854  [self __setSelectionIndexes:[CPIndexSet indexSetWithIndex:0]];
855 
856  var proxy = [_CPKVOProxy proxyForObject:self];
857  [proxy setAdding:YES];
858 
859  [self didChangeValueForKey:@"content"];
860 
861  if (willClearPredicate)
862  [self didChangeValueForKey:@"filterPredicate"];
863 
864  [proxy setAdding:NO];
865 }
866 
872 - (void)removeObject:(id)object
873 {
874  [self willChangeValueForKey:@"content"];
875 
876  // See _disableSetContent explanation in addObject:.
877  _disableSetContent = YES;
878 
879  [_contentObject removeObject:object];
880  // Allow handlesContentAsCompoundValue reverse sets to trigger.
881  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
882 
883  _disableSetContent = NO;
884 
885  if (_filterPredicate === nil || [_filterPredicate evaluateWithObject:object])
886  {
887  // selectionIndexes change notification will be fired as a result of the
888  // content change. Don't fire manually.
889  var pos = [_arrangedObjects indexOfObject:object];
890 
891  [_arrangedObjects removeObjectAtIndex:pos];
892  [_selectionIndexes shiftIndexesStartingAtIndex:pos by:-1];
893 
894  // This will automatically handle the avoidsEmptySelection case.
895  [self __setSelectionIndexes:_selectionIndexes];
896  }
897 
898  [self didChangeValueForKey:@"content"];
899 }
900 
906 - (void)add:(id)sender
907 {
908  if (![self canAdd])
909  return;
910 
911  var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject];
912 
913  [self addObject:newObject];
914 }
915 
920 - (void)insert:(id)sender
921 {
922  if (![self canInsert])
923  return;
924 
925  var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject],
926  lastSelectedIndex = [_selectionIndexes lastIndex];
927 
928  if (lastSelectedIndex !== CPNotFound)
929  [self insertObject:newObject atArrangedObjectIndex:lastSelectedIndex];
930  else
931  [self addObject:newObject];
932 }
933 
938 - (void)remove:(id)sender
939 {
940  [self removeObjectsAtArrangedObjectIndexes:_selectionIndexes];
941 }
942 
947 - (void)removeObjectAtArrangedObjectIndex:(int)index
948 {
950 }
951 
956 - (void)removeObjectsAtArrangedObjectIndexes:(CPIndexSet)anIndexSet
957 {
958  [self willChangeValueForKey:@"content"];
959 
960  /*
961  See _disableSetContent explanation in addObject:.
962  */
963  _disableSetContent = YES;
964 
965  var arrangedObjects = [self arrangedObjects],
967  newSelectionIndexes = [_selectionIndexes copy];
968 
969  [anIndexSet enumerateIndexesWithOptions:CPEnumerationReverse
970  usingBlock:function(anIndex)
971  {
972  var object = [arrangedObjects objectAtIndex:anIndex];
973 
974  // First try the simple case which should work if there are no sort descriptors.
975  if ([_contentObject objectAtIndex:anIndex] === object)
976  [_contentObject removeObjectAtIndex:anIndex];
977  else
978  {
979  // Since we don't have a reverse mapping between the sorted order and the
980  // unsorted one, we'll just simply have to remove an arbitrary pointer. It might
981  // be the 'wrong' one - as in not the one the user selected - but the wrong
982  // one is still just another pointer to the same object, so the user will not
983  // be able to see any difference.
984  var contentIndex = [_contentObject indexOfObjectIdenticalTo:object];
985  [_contentObject removeObjectAtIndex:contentIndex];
986  }
987  [arrangedObjects removeObjectAtIndex:anIndex];
988 
989  if (!_avoidsEmptySelection || [newSelectionIndexes count] > 1)
990  {
991  [newSelectionIndexes removeIndex:anIndex];
992  [newSelectionIndexes shiftIndexesStartingAtIndex:anIndex by:-1];
993  }
994  else if ([newSelectionIndexes lastIndex] !== anIndex)
995  [newSelectionIndexes shiftIndexesStartingAtIndex:anIndex by:-1];
996  }];
997 
998  // Allow handlesContentAsCompoundValue reverse sets to trigger.
999  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
1000  _disableSetContent = NO;
1001 
1002  // This will automatically handle the avoidsEmptySelection case.
1003  [self __setSelectionIndexes:newSelectionIndexes];
1004 
1005  [self didChangeValueForKey:@"content"];
1006 }
1007 
1013 - (void)addObjects:(CPArray)objects
1014 {
1015  if (![self canAdd])
1016  return;
1017 
1018  var contentArray = [self contentArray],
1019  count = [objects count];
1020 
1021  for (var i = 0; i < count; i++)
1022  [contentArray addObject:[objects objectAtIndex:i]];
1023 
1024  [self setContent:contentArray];
1025  // Allow handlesContentAsCompoundValue reverse sets to trigger.
1026  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
1027 }
1028 
1033 - (void)removeObjects:(CPArray)objects
1034 {
1035  [self _removeObjects:objects];
1036 }
1037 
1041 - (void)_removeObjects:(CPArray)objects
1042 {
1043  [self willChangeValueForKey:@"content"];
1044 
1045  // See _disableSetContent explanation in addObject:.
1046  _disableSetContent = YES;
1047 
1048  [_contentObject removeObjectsInArray:objects];
1049  // Allow handlesContentAsCompoundValue reverse sets to trigger.
1050  [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
1051 
1052  _disableSetContent = NO;
1053 
1054  var arrangedObjects = [self arrangedObjects],
1055  position = [arrangedObjects indexOfObject:[objects objectAtIndex:0]];
1056 
1057  [arrangedObjects removeObjectsInArray:objects];
1058 
1059  var objectsCount = [arrangedObjects count],
1061 
1062  if ([self preservesSelection] || [self avoidsEmptySelection])
1063  {
1065 
1066  // Remove the selection if there are no objects
1067  if (objectsCount <= 0)
1069 
1070  // Shift selection to last object if position is out of bounds
1071  else if (position >= objectsCount)
1072  selectionIndexes = [CPIndexSet indexSetWithIndex:objectsCount - 1];
1073  }
1074 
1075  _selectionIndexes = selectionIndexes;
1076 
1077  [self didChangeValueForKey:@"content"];
1078 }
1079 
1084 - (BOOL)canInsert
1085 {
1086  return [self isEditable];
1087 }
1088 
1089 @end
1090 
1092 
1093 + (Class)_binderClassForBinding:(CPString)aBinding
1094 {
1095  if (aBinding == @"contentArray")
1096  return [_CPArrayControllerContentBinder class];
1097 
1098  return [super _binderClassForBinding:aBinding];
1099 }
1100 
1101 @end
1102 @implementation _CPArrayControllerContentBinder : CPBinder
1103 {
1104  id __doxygen__;
1105 }
1106 
1107 - (void)setValueFor:(CPString)aBinding
1108 {
1109  var destination = [_info objectForKey:CPObservedObjectKey],
1110  keyPath = [_info objectForKey:CPObservedKeyPathKey],
1111  options = [_info objectForKey:CPOptionsKey],
1112  isCompound = [self handlesContentAsCompoundValue],
1113  dotIndex = keyPath.lastIndexOf("."),
1114  firstPart = dotIndex !== CPNotFound ? keyPath.substring(0, dotIndex) : nil,
1115  isSelectionProxy = firstPart && [[destination valueForKeyPath:firstPart] isKindOfClass:CPControllerSelectionProxy],
1116  newValue;
1117 
1118  if (!isCompound && !isSelectionProxy)
1119  {
1120  newValue = [destination mutableArrayValueForKeyPath:keyPath];
1121  }
1122  else
1123  {
1124  // 1. If handlesContentAsCompoundValue we cannot just set up a proxy.
1125  // Every read and every write must go through transformValue and
1126  // reverseTransformValue, and the resulting object cannot be described by
1127  // a key path.
1128 
1129  // 2. If isSelectionProxy, we don't want to proxy a proxy - that's bad
1130  // for performance and won't work with markers.
1131 
1132  newValue = [destination valueForKeyPath:keyPath];
1133  }
1134 
1135  var isPlaceholder = CPIsControllerMarker(newValue);
1136 
1137  if (isPlaceholder)
1138  {
1139  if (newValue === CPNotApplicableMarker && [options objectForKey:CPRaisesForNotApplicableKeysBindingOption])
1140  {
1141  [CPException raise:CPGenericException
1142  reason:@"can't transform non applicable key on: " + _source + " value: " + newValue];
1143  }
1144 
1145  newValue = [self _placeholderForMarker:newValue];
1146 
1147  // This seems to be what Cocoa does.
1148  if (!newValue)
1149  newValue = [CPMutableArray array];
1150  }
1151  else
1152  newValue = [self transformValue:newValue withOptions:options];
1153 
1154  if (isCompound)
1155  {
1156  // Make sure we can edit our copy of the content. TODO In Cocoa, this copy
1157  // appears to be deferred until the array actually needs to be edited.
1158  newValue = [newValue mutableCopy];
1159  }
1160 
1161  [_source setValue:newValue forKey:aBinding];
1162 }
1163 
1164 - (void)_contentArrayDidChange
1165 {
1166  // When handlesContentAsCompoundValue == YES, it is not sufficient to modify the content object
1167  // in place because what we are holding is an array 'unwrapped' from a compound value by
1168  // a value transformer. So when we modify it we need a reverse set and transform to create
1169  // a new compound value.
1170  //
1171  // (The Cocoa documentation on the subject is not very clear but after substantial
1172  // experimentation this seems both reasonable and compliant.)
1173  if ([self handlesContentAsCompoundValue])
1174  {
1175  var destination = [_info objectForKey:CPObservedObjectKey],
1176  keyPath = [_info objectForKey:CPObservedKeyPathKey];
1177 
1178  [self suppressSpecificNotificationFromObject:destination keyPath:keyPath];
1179  [self reverseSetValueFor:@"contentArray"];
1180  [self unsuppressSpecificNotificationFromObject:destination keyPath:keyPath];
1181  }
1182 }
1183 
1184 @end
1185 
1186 var CPArrayControllerAvoidsEmptySelection = @"CPArrayControllerAvoidsEmptySelection",
1187  CPArrayControllerClearsFilterPredicateOnInsertion = @"CPArrayControllerClearsFilterPredicateOnInsertion",
1188  CPArrayControllerFilterRestrictsInsertion = @"CPArrayControllerFilterRestrictsInsertion",
1189  CPArrayControllerPreservesSelection = @"CPArrayControllerPreservesSelection",
1190  CPArrayControllerSelectsInsertedObjects = @"CPArrayControllerSelectsInsertedObjects",
1191  CPArrayControllerAlwaysUsesMultipleValuesMarker = @"CPArrayControllerAlwaysUsesMultipleValuesMarker",
1192  CPArrayControllerAutomaticallyRearrangesObjects = @"CPArrayControllerAutomaticallyRearrangesObjects";
1193 
1195 
1196 - (id)initWithCoder:(CPCoder)aCoder
1197 {
1198  self = [super initWithCoder:aCoder];
1199 
1200  if (self)
1201  {
1202  _avoidsEmptySelection = [aCoder decodeBoolForKey:CPArrayControllerAvoidsEmptySelection];
1203  _clearsFilterPredicateOnInsertion = [aCoder decodeBoolForKey:CPArrayControllerClearsFilterPredicateOnInsertion];
1204  _filterRestrictsInsertion = [aCoder decodeBoolForKey:CPArrayControllerFilterRestrictsInsertion];
1205  _preservesSelection = [aCoder decodeBoolForKey:CPArrayControllerPreservesSelection];
1206  _selectsInsertedObjects = [aCoder decodeBoolForKey:CPArrayControllerSelectsInsertedObjects];
1207  _alwaysUsesMultipleValuesMarker = [aCoder decodeBoolForKey:CPArrayControllerAlwaysUsesMultipleValuesMarker];
1208  _automaticallyRearrangesObjects = [aCoder decodeBoolForKey:CPArrayControllerAutomaticallyRearrangesObjects];
1209  _sortDescriptors = [CPArray array];
1210 
1211  if (![self content] && [self automaticallyPreparesContent])
1212  [self prepareContent];
1213  else if (![self content])
1214  [self _setContentArray:[]];
1215  }
1216 
1217  return self;
1218 }
1219 
1220 - (void)encodeWithCoder:(CPCoder)aCoder
1221 {
1222  [super encodeWithCoder:aCoder];
1223 
1224  [aCoder encodeBool:_avoidsEmptySelection forKey:CPArrayControllerAvoidsEmptySelection];
1225  [aCoder encodeBool:_clearsFilterPredicateOnInsertion forKey:CPArrayControllerClearsFilterPredicateOnInsertion];
1226  [aCoder encodeBool:_filterRestrictsInsertion forKey:CPArrayControllerFilterRestrictsInsertion];
1227  [aCoder encodeBool:_preservesSelection forKey:CPArrayControllerPreservesSelection];
1228  [aCoder encodeBool:_selectsInsertedObjects forKey:CPArrayControllerSelectsInsertedObjects];
1229  [aCoder encodeBool:_alwaysUsesMultipleValuesMarker forKey:CPArrayControllerAlwaysUsesMultipleValuesMarker];
1230  [aCoder encodeBool:_automaticallyRearrangesObjects forKey:CPArrayControllerAutomaticallyRearrangesObjects];
1231 }
1232 
1234 {
1235  [self _selectionWillChange];
1236  [self _selectionDidChange];
1237 }
1238 
1239 @end
Used to implement exception handling (creating & raising).
Definition: CPException.h:2
CPArray arrangeObjects:(CPArray objects)
CPSet keyPathsForValuesAffectingSelectionIndexes()
void willChangeValueForKey:(CPString aKey)
CGPoint position()
Definition: CALayer.j:225
CPSet keyPathsForValuesAffectingArrangedObjects()
CPRaisesForNotApplicableKeysBindingOption
CPIsControllerMarker
CPSet keyPathsForValuesAffectingCanSelectPrevious()
void raise:reason:(CPString aName, [reason] CPString aReason)
Definition: CPException.j:66
A collection of unique integers.
Definition: CPIndexSet.h:2
CPInteger firstIndex()
Definition: CPIndexSet.j:278
CPSet keyPathsForValuesAffectingSelectedObjects()
var CPArrayControllerAvoidsEmptySelection
void setContent:(id value)
id initWithCoder:(CPCoder aCoder)
CPSet keyPathsForValuesAffectingSelectionIndex()
An immutable string (collection of characters).
Definition: CPString.h:2
CPSet keyPathsForValuesAffectingCanRemove()
void encodeWithCoder:(CPCoder aCoder)
void shiftIndexesStartingAtIndex:by:(CPInteger anIndex, [by] int aDelta)
Definition: CPIndexSet.j:921
void removeObjectsAtArrangedObjectIndexes:(CPIndexSet anIndexSet)
void insertObject:atArrangedObjectIndex:(id anObject, [atArrangedObjectIndex] int anIndex)
CPPredicate filterPredicate()
CPBinder getBinding:forObject:(CPString aBinding, [forObject] id anObject)
BOOL clearsFilterPredicateOnInsertion()
CPNotApplicableMarker
var CPArrayControllerSelectsInsertedObjects
var CPArrayControllerFilterRestrictsInsertion
void exposeBinding:(CPString aBinding)
void didChangeValueForKey:(CPString aKey)
Defines methods for use when archiving & restoring (enc/decoding).
Definition: CPCoder.h:2
CPNotFound
Definition: CPObjJRuntime.j:62
CPSet keyPathsForValuesAffectingSelection()
CPSet keyPathsForValuesAffectingCanSelectNext()
Class class()
Definition: CPObject.j:179
BOOL setSelectionIndexes:(CPIndexSet indexes)
var CPArrayControllerAlwaysUsesMultipleValuesMarker
CPSet keyPathsForValuesAffectingContentArray()
var CPArrayControllerPreservesSelection
id indexSetWithIndex:(int anIndex)
Definition: CPIndexSet.j:51
void enumerateIndexesWithOptions:usingBlock:(CPEnumerationOptions options, [usingBlock] Function/*(int idx, @ref BOOL stop) */aFunction)
Definition: CPIndexSet.j:492
void addObject:(id object)
CPIndexSet selectionIndexes()
id indexSet()
Definition: CPIndexSet.j:43
var CPArrayControllerAutomaticallyRearrangesObjects
var CPArrayControllerClearsFilterPredicateOnInsertion