From 4de3a34a3ee7e45dc5846ebe060e094e990829e3 Mon Sep 17 00:00:00 2001 From: Ross Boucher Date: Mon, 16 Feb 2009 00:37:50 -0800 Subject: [PATCH] CPObjectController now works, as do bindings and CPControls. --- AppKit/CPArrayController.j | 428 +++++++++++++++++++ AppKit/CPControl.j | 18 +- AppKit/CPKeyValueBinding.j | 11 +- Foundation/CPArray.j | 29 +- Foundation/CPValueTransformer.j | 176 ++++++++ Tests/AppKit/SimpleBindings2/AppController.j | 13 +- 6 files changed, 662 insertions(+), 13 deletions(-) create mode 100644 AppKit/CPArrayController.j create mode 100644 Foundation/CPValueTransformer.j diff --git a/AppKit/CPArrayController.j b/AppKit/CPArrayController.j new file mode 100644 index 000000000..2dfda77f2 --- /dev/null +++ b/AppKit/CPArrayController.j @@ -0,0 +1,428 @@ + +/* + * CPArrayController.j + * AppKit + * + * Adapted from Cocotron, by Johannes Fortmann + * + * Created by Ross Boucher. + * Copyright 2009, 280 North, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +@import +@import + + +@implementation CPArrayController : CPObjectController +{ + BOOL _avoidsEmptySelection; + BOOL _clearsFilterPredicateOnInsertion; + BOOL _filterRestrictsInsertion; + BOOL _preservesSelection; + BOOL _selectsInsertedObjects; + BOOL _alwaysUsesMultipleValuesMarker; + + id _selectionIndexes; + id _sortDescriptors; + id _filterPredicate; + id _arrangedObjects; +} + ++ (CPSet)keyPathsForValuesAffectingValueForContentArray +{ + return [CPSet setWithObjects:"content"]; +} + ++ (CPSet)keyPathsForValuesAffectingValueForSelection +{ + return [CPSet setWithObjects:"content", "contentArray", "selectionIndexes"]; +} + ++ (CPSet)keyPathsForValuesAffectingValueForSelectionIndex +{ + return [CPSet setWithObjects:"content", "contentArray", "selectionIndexes", "selection"]; +} + ++ (CPSet)keyPathsForValuesAffectingValueForSelectedObjects +{ + return [CPSet setWithObjects:"content", "contentArray", "selectionIndexes", "selection"]; +} + ++ (CPSet)keyPathsForValuesAffectingValueForCanRemove +{ + return [CPSet setWithObjects:"selectionIndexes"]; +} + ++ (CPSet)keyPathsForValuesAffectingValueForCanSelectNext +{ + return [CPSet setWithObjects:"selectionIndexes"]; +} + ++ (CPSet)keyPathsForValuesAffectingValueForCanSelectPrevious +{ + return [CPSet setWithObjects:"selectionIndexes"]; +} + +-(void)prepareContent +{ + [self _setContentArray:[[self newObject]]]; +} + +- (BOOL)preservesSelection +{ + return _preservesSelection; +} + +- (void)setPreservesSelection:(BOOL)value +{ + _preservesSelection = value; +} + +- (void)setContent:(id)value +{ + if(![value isKindOfClass:[CPArray class]]) + value = [value]; + + var oldSelection = nil, + oldSelectionIndexes = [[self selectionIndexes] copy]; + + if ([self preservesSelection]) + oldSelection = [self selectedObjects]; + + //FIXME: mutable copy? + [super setContent:[value copy]]; + + if(_clearsFilterPredicateOnInsertion) + [self setFilterPredicate:nil]; + + [self rearrangeObjects]; + + if (oldSelection) + [self setSelectedObjects:oldSelection]; + else + [self setSelectionIndexes:oldSelectionIndexes]; +} + +- (void)_setContentArray:(id)value +{ + [self setContent:value]; +} + +- (id)contentArray +{ + return [self content]; +} + +- (CPArray)arrangeObjects:(CPArray)objects +{ + var sortedObjects = objects; + + if ([self filterPredicate]) + sortedObjects = [sortedObjects filteredArrayUsingPredicate:[self filterPredicate]]; + if ([self sortDescriptors]) + sortedObjects = [sortedObjects sortedArrayUsingDescriptors:[self sortDescriptors]]; + + return sortedObjects; +} + +- (void)rearrangeObjects +{ + [self _setArrangedObjects:[self arrangeObjects:[self contentArray]]]; +} + +- (void)_setArrangedObjects:(id)value +{ + if (_arrangedObjects === value) + return; + + _arrangedObjects = [[_CPObservableArray alloc] initWithArray:value]; +} + +- (id)arrangedObjects +{ + return _arrangedObjects; +} + + +- (CPArray)sortDescriptors +{ + return _sortDescriptors; +} + +- (void)setSortDescriptors:(CPArray)value +{ + if (_sortDescriptors === value) + return; + + _sortDescriptors = [value copy]; + [self rearrangeObjects]; +} + +- (CPPredicate)filterPredicate +{ + return _filterPredicate; +} + +- (void)setFilterPredicate:(CPPredicate)value +{ + if (_filterPredicate === value) + return; + + _filterPredicate = value; + [self rearrangeObjects]; +} + +- (BOOL)alwaysUsesMultipleValuesMarker +{ + return _alwaysUsesMultipleValuesMarker; +} + +//Selection + +- (unsigned)selectionIndex +{ + return [_selectionIndexes firstIndex]; +} + +- (BOOL)setSelectionIndex:(unsigned)index +{ + return [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]]; +} + +- (CPIndexSet)selectionIndexes +{ + return _selectionIndexes; +} + +- (BOOL)setSelectionIndexes:(CPIndexSet)indexes +{ + if ([_selectionIndexes isEqual:indexes]) + return NO; + + if(![indexes count] && _avoidsEmptySelection && [[self arrangedObjects] count]) + indexes = [CPIndexSet indexSetWithIndex:0]; + + [indexes removeIndexesInRange:CPMakeRange([[self arrangedObjects] count]+1, CPNotFound)]; + + [self willChangeValueForKey:@"selectionIndexes"]; + [self _selectionWillChange]; + + _selectionIndexes = [indexes copy]; + + [self _selectionDidChange]; + [self didChangeValueForKey:@"selectionIndexes"]; + + return YES; +} + +- (CPArray)selectedObjects +{ + var objects = [_CPObservableArray arrayWithArray:[[self arrangedObjects] objectsAtIndexes:[self selectionIndexes]]]; + + return objects || [_CPObservableArray array]; +} + +- (BOOL)setSelectedObjects:(CPArray)objects +{ + var set = [CPIndexSet indexSet], + count = [objects count], + arrangedObjects = [self arrangedObjects]; + + for (var i=0; i 0 +} + +-(BOOL)canSelectNext +{ + return [[self selectionIndexes] firstIndex] < [[self arrangedObjects] count] -1; +} + +-(void)selectNext:(id)sender +{ + var index = [[self selectionIndexes] firstIndex] + 1 || 0; + + if (index < [[self arrangedObjects] count]) + [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]]; +} + +-(void)selectPrevious:(id)sender +{ + var index = [[self selectionIndexes] firstIndex] - 1 || [[self arrangedObjects] count] - 1; + + if (index >= 0) + [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]]; +} + +//Add/Remove + +- (void)addObject:(id)object +{ + if (![self canAdd]) + return; + + [self willChangeValueForKey:@"content"]; + [_content addObject:object]; + [self didChangeValueForKey:@"content"]; + + if (_clearsFilterPredicateOnInsertion) + [self setFilterPredicate:nil]; + + if ([_filterPredicate evaluateWithObject:object]) + { + [self willChangeValueForKey:@"selectionIndexes"]; + var pos = [_arrangedObjects insertObject:object inArraySortedByDescriptors:_sortDescriptors]; + + [_selectionIndexes shiftIndexesStartingAtIndex:pos by:1]; + [self didChangeValueForKey:@"selectionIndexes"]; + } +} + + +- (void)removeObject:(id)object +{ + if (![self canRemove]) + return; + + [self willChangeValueForKey:@"content"]; + [_content removeObject:object]; + [self didChangeValueForKey:@"content"]; + + if ([_filterPredicate evaluateWithObject:object]) + { + [self willChangeValueForKey:@"selectionIndexes"]; + var pos = [_arrangedObjects indexOfObject:object]; + + [_selectionIndexes shiftIndexesStartingAtIndex:pos by:-1]; + [self didChangeValueForKey:@"selectionIndexes"]; + } +} + +-(void)add:(id)sender +{ + if(![self canAdd]) + return; + + [self insert:sender]; +} + +- (void)insert:(id)sender +{ + if(![self canInsert]) + return; + + var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject]; + + [self addObject:newObject]; +} + +- (void)remove:(id)sender +{ + [self removeObjects:[[self contentArray] objectsAtIndexes:[self selectionIndexes]]]; +} + +- (void)removeObjectsAtArrangedObjectIndexes:(CPIndexSet)indexes +{ + [self _removeObjects:[[self contentArray] objectsAtIndexes:indexes]]; +} + + +- (void)addObjects:(CPArray)objects +{ + if(![self canAdd]) + return; + + var contentArray = [self contentArray], + count = [objects count]; + + for (var i=0; iCPNotFound if it was not found. */ - (unsigned)indexOfObject:(id)anObject sortedByFunction:(Function)aFunction context:(id)aContext +{ + var result = [self _indexOfObject:anObject sortedByFunction:aFunction context:aContext]; + return result >= 0 ? result : CPNotFound; +} + +- (unsigned)_indexOfObject:(id)anObject sortedByFunction:(Function)aFunction context:(id)aContext { if (!aFunction || anObject === undefined) return CPNotFound; @@ -483,7 +489,7 @@ } } - return CPNotFound; + return -mid-1; } /*! @@ -510,6 +516,27 @@ }]; } +- (unsigned)insertObject:(id)anObject inArraySortedByDescriptors:(CPArray)descriptors +{ + var index = [self _insertObject:anObject sortedByFunction:function(lhs, rhs) + { + var i = 0, + count = [descriptors count], + result = CPOrderedSame; + + while (i < count) + if((result = [descriptors[i++] compareObject:lhs withObject:rhs]) != CPOrderedSame) + return result; + + return result; + } context:nil]; + + if (index < 0) + index = -result-1; + + [self insertObject:anObject atIndex:index]; +} + /*! Returns the last object in the array. If the array is empty, returns nil/ */ diff --git a/Foundation/CPValueTransformer.j b/Foundation/CPValueTransformer.j new file mode 100644 index 000000000..29bfcf6f8 --- /dev/null +++ b/Foundation/CPValueTransformer.j @@ -0,0 +1,176 @@ +/* + * CPKeyValueBinding.j + * Foundation + * + * Created by Ross Boucher 2/15/09 + * Copyright 280 North, Inc. + * + * Adapted from GNUStep + * Released under the LGPL. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +@import +@import + + +var transformerMap = [CPDictionary dictionary]; + + +@implementation CPValueTransformer : CPObject +{ + +} + ++ (void)setValueTransformer:(CPValueTransformer)transformer forName:(CPString)name +{ + [transformerMap setObject:transformer forKey:name]; +} + ++ (CPValueTransformer)valueTransformerForName:(CPString)name +{ + return [transformerMap objectForKey:name]; +} + ++ (CPArray)valueTransformerNames +{ + return [transformerMap allKeys]; +} + ++ (BOOL)allowsReverseTransformation +{ + return NO; +} + ++ (Class)transformedValueClass +{ + return [CPObject class]; +} + +- (id)reverseTransformedValue:(id)value +{ + if ([[self class] allowsReverseTransformation]) + { + [CPException raise:CPInvalidArgumentException reason:(self+" is not reversible.")]; + } + + return [self transformedValue:value]; +} + +- (id)transformedValue:(id)value +{ + return nil; +} + +@end + +// builtin transformers + +@implementation CPNegateBooleanTransformer + ++ (BOOL)allowsReverseTransformation +{ + return YES; +} + ++ (Class)transformedValueClass +{ + return [CPNumber class]; +} + +- (id)reverseTransformedValue:(id)value +{ + return ![value boolValue]; +} + +- (id)transformedValue:(id)value +{ + return ![value boolValue]; +} + +@end + +@implementation CPIsNilTransformer + ++ (BOOL)allowsReverseTransformation +{ + return NO; +} + ++ (Class)transformedValueClass +{ + return [CPNumber class]; +} + +- (id)transformedValue:(id)value +{ + return value === nil || value === undefined; +} + +@end + +@implementation CPIsNotNilTransformer + ++ (BOOL)allowsReverseTransformation +{ + return NO; +} + ++ (Class)transformedValueClass +{ + return [CPNumber class]; +} + +- (id)transformedValue:(id)value +{ + return value !== nil && value !== undefined; +} + +@end + +@implementation CPUnarchiveFromDataTransformer + ++ (BOOL)allowsReverseTransformation +{ + return YES; +} + ++ (Class)transformedValueClass +{ + return [CPData class]; +} + +- (id)reverseTransformedValue:(id)value +{ + return [CPKeyedArchiver archivedDataWithRootObject:value]; +} + +- (id)transformedValue:(id)value +{ + return [CPKeyedUnarchiver unarchiveObjectWithData:value]; +} + +@end + +CPNegateBooleanTransformerName = @"CPNegateBooleanTransformerName"; +CPIsNilTransformerName = @"CPIsNilTransformerName"; +CPIsNotNilTransformerName = @"CPIsNotNilTransformerName"; +CPUnarchiveFromDataTransformerName = @"CPUnarchiveFromDataTransformerName"; + +[CPValueTransformer setValueTransformer:[CPNegateBooleanTransformer new] forName:CPNegateBooleanTransformerName]; +[CPValueTransformer setValueTransformer:[CPIsNilTransformer new] forName:CPIsNilTransformerName]; +[CPValueTransformer setValueTransformer:[CPIsNotNilTransformer new] forName:CPIsNotNilTransformerName]; +[CPValueTransformer setValueTransformer:[CPUnarchiveFromDataTransformer new] forName:CPUnarchiveFromDataTransformerName]; diff --git a/Tests/AppKit/SimpleBindings2/AppController.j b/Tests/AppKit/SimpleBindings2/AppController.j index df49d1a52..301739ea3 100644 --- a/Tests/AppKit/SimpleBindings2/AppController.j +++ b/Tests/AppKit/SimpleBindings2/AppController.j @@ -40,13 +40,16 @@ CPLogRegister(CPLogConsole); slider = [[CPSlider alloc] initWithFrame:CGRectMake(CGRectGetMinX(textFrame), CGRectGetMaxY(textFrame)+10, CGRectGetWidth(textFrame), 12)]; - [slider setContinuous:NO]; + [slider setContinuous:YES]; [contentView addSubview:slider]; var button = [[CPButton alloc] initWithFrame:CGRectMake(CGRectGetMinX(textFrame)+25, CGRectGetMaxY(textFrame)+40, 100, 18)]; [button setTitle:@"mute"]; + + [button setTarget:self]; + [button setAction:@selector(muteTrack:)]; [contentView addSubview:button]; @@ -60,13 +63,10 @@ CPLogRegister(CPLogConsole); var controller = [[CPObjectController alloc] init]; [controller bind:@"contentObject" toObject:self withKeyPath:@"track" options:nil]; - [textField bind:@"objectValue" toObject:controller withKeyPath:@"selection.volume" options:nil]; - [slider bind:@"objectValue" toObject:controller withKeyPath:@"selection.volume" options:nil]; + [textField bind:CPValueBinding toObject:controller withKeyPath:@"selection.volume" options:nil]; + [slider bind:CPValueBinding toObject:controller withKeyPath:@"selection.volume" options:nil]; [theWindow orderFront:self]; - - // Uncomment the following line to turn on the standard menu bar. - //[CPMenu setMenuBarVisible:YES]; } - (IBAction)muteTrack:(id)sender @@ -74,6 +74,7 @@ CPLogRegister(CPLogConsole); [track setVolume:0.0]; } + @end