CPObjectController now works, as do bindings and CPControls.

This commit is contained in:
Ross Boucher
2009-02-16 00:37:50 -08:00
parent c028425f14
commit 4de3a34a3e
6 changed files with 662 additions and 13 deletions
+428
View File
@@ -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 <AppKit/CPObjectController.j>
@import <AppKit/CPKeyValueBinding.j>
@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<count; i++)
{
var index = [arrangedObjects indexOfObject:[objects objectAtIndex:i]];
if (index !== CPNotFound)
[set addIndex:index];
}
[self setSelectionIndexes:set];
return YES;
}
//Moving selection
-(BOOL)canSelectPrevious
{
return [[self selectionIndexes] firstIndex] > 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; i<count; i++)
[contentArray addObject:[objects objectAtIndex:i]];
[self setContent:contentArray];
}
- (void)removeObjects:(CPArray)objects
{
if(![self canRemove])
return;
[self _removeObjects:objects];
}
- (void)_removeObjects:(CPArray)objects
{
var contentArray = [self contentArray],
count = [objects count];
for (var i=0; i<count; i++)
[contentArray removeObject:[objects objectAtIndex:i]];
[self setContent:contentArray];
}
- (BOOL)canInsert
{
return [self isEditable];
}
@end
@implementation CPArrayController (CPCoding)
- (id)initWithCoder:(CPCoder)aCoder
{
self = [super initWithCoder:coder];
if (self)
{
_avoidsEmptySelection = [coder decodeBoolForKey:@"CPArrayControllerAvoidsEmptySelection"];
_clearsFilterPredicateOnInsertion = [coder decodeBoolForKey:@"CPClearsFilterPredicateOnInsertion"];
_filterRestrictsInsertion = [coder decodeBoolForKey:@"CPArrayControllerFilterRestrictsInsertion"];
_preservesSelection = [coder decodeBoolForKey:@"CPArrayControllerPreservesSelection"];
_selectsInsertedObjects = [coder decodeBoolForKey:@"CPArrayControllerSelectsInsertedObjects"];
_alwaysUsesMultipleValuesMarker = [coder decodeBoolForKey:@"CPArrayControllerAlwaysUsesMultipleValuesMarker"];
if ([self automaticallyPreparesContent])
[self prepareContent];
else
[self _setContentArray:[]];
}
return self;
}
- (void)awakeFromCib
{
[self _selectionWillChange];
[self _selectionDidChange];
}
@end
+17 -1
View File
@@ -121,7 +121,7 @@ var CPControlBlackColor = [CPColor blackColor];
+ (void)initialize
{
[self exposeBinding:"objectValue", "value"];
[self exposeBinding:"value"];
}
- (id)initWithFrame:(CGRect)aFrame
@@ -146,6 +146,17 @@ var CPControlBlackColor = [CPColor blackColor];
return self;
}
- (void)bind:(CPString)binding toObject:(id)anObject withKeyPath:(CPString)keyPath options:(CPDictionary)options
{
if ([binding isEqual: CPValueBinding])
{
[self unbind: binding];
[[CPKeyValueBinding alloc] initWithBinding: @"objectValue" name:CPValueBinding to:anObject keyPath:keyPath options:options from:self];
}
else
[super bind:binding toObject:anObject withKeyPath:keyPath options:options];
}
/*!
Sets whether the receiver responds to mouse events.
@param isEnabled whether the receiver will respond to mouse events
@@ -438,6 +449,11 @@ var CPControlBlackColor = [CPColor blackColor];
*/
- (void)sendAction:(SEL)anAction to:(id)anObject
{
var theBinding = [CPKeyValueBinding getBinding:CPValueBinding forObject:self];
if (theBinding)
[theBinding reverseSetValueFor:@"objectValue"];
[CPApp sendAction:anAction to:anObject from:self];
}
+6 -5
View File
@@ -55,7 +55,7 @@ var CPBindingOperationAnd = 0,
+ (CPKeyValueBinding)getBinding:(CPString)aBinding forObject:(id)anObject
{
return [[bindingsMap objectForKey:anObject] objectForKey:aBinding];
return [[bindingsMap objectForKey:[anObject hash]] objectForKey:aBinding];
}
+ (CPDictionary)infoForBinding:(CPString)aBinding forObject:(id)anObject
@@ -160,7 +160,7 @@ var CPBindingOperationAnd = 0,
{
if (!changes)
return;
CPLog.fatal("HERE");
var options = [_info objectForKey:CPOptionsKey],
newValue = [changes objectForKey:CPKeyValueChangeNewKey];
@@ -176,7 +176,7 @@ var CPBindingOperationAnd = 0,
valueTransformer,
placeholder;
switch (aValue || @"")
switch (aValue)
{
case CPMultipleValuesMarker: return [options objectForKey:CPMultipleValuesPlaceholderBindingOption] || @"Multiple Values";
@@ -187,7 +187,8 @@ var CPBindingOperationAnd = 0,
return [options objectForKey:CPNotApplicablePlaceholderBindingOption] || @"Not Applicable";
case @"": return [options objectForKey:CPNullPlaceholderBindingOption] || @"";
case nil:
case undefined: return [options objectForKey:CPNullPlaceholderBindingOption] || @"";
}
var valueTransformerName = [options objectForKey:CPValueTransformerNameBindingOption],
@@ -204,7 +205,7 @@ var CPBindingOperationAnd = 0,
return aValue;
}
- (id)reverseTransformValue: (id)value withOptions: (NSDictionary *)options
- (id)reverseTransformValue:(id)aValue withOptions: (CPDictionary)options
{
var valueTransformerName = [options objectForKey:CPValueTransformerNameBindingOption],
valueTransformer;
+28 -1
View File
@@ -460,6 +460,12 @@
@return the index of the object, or <code>CPNotFound</code> 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 <code>nil</code>/
*/
+176
View File
@@ -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 <Foundation/CPObject.j>
@import <Foundation/CPDictionary.j>
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];
+7 -6
View File
@@ -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