Optimized table data loading when using bindings for the cell values. A special code path in the CPTableColumn's prepareDataView:forRow: method turns a O(n) operation into O(1) where n is the number of items in the model array.

This commit is contained in:
Alexander Ljungberg committed 2010-05-26 19:42:59 -04:00
1 parent 15be647c16
commit 8b2350fb22
2 files changed
+155 -29

No files matched your search

+41 -13
View File
@@ -50,7 +50,7 @@ CPTableColumnUserResizingMask = 1 << 1;
CPSortDescriptor _sortDescriptorPrototype;
BOOL _isHidden;
CPString _headerToolTip;
BOOL _disableResizingPosting @accessors(property=disableResizingPosting);
}
@@ -72,12 +72,12 @@ CPTableColumnUserResizingMask = 1 << 1;
_maxWidth = 1000000.0;
_resizingMask = CPTableColumnAutoresizingMask | CPTableColumnUserResizingMask;
_disableResizingPosting = NO;
[self setIdentifier:anIdentifier];
var header = [[_CPTableColumnHeaderView alloc] initWithFrame:CGRectMakeZero()];
[self setHeaderView:header];
var textDataView = [CPTextField new];
[textDataView setValue:[CPColor colorWithRed:51.0 / 255.0 green:51.0 / 255.0 blue:51.0 / 255.0 alpha:1.0]
@@ -128,19 +128,19 @@ CPTableColumnUserResizingMask = 1 << 1;
{
var index = [[tableView tableColumns] indexOfObjectIdenticalTo:self],
dirtyTableColumnRangeIndex = tableView._dirtyTableColumnRangeIndex;
if (dirtyTableColumnRangeIndex < 0)
tableView._dirtyTableColumnRangeIndex = index;
else
tableView._dirtyTableColumnRangeIndex = MIN(index, tableView._dirtyTableColumnRangeIndex);
var rows = tableView._exposedRows,
columns = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(index, [tableView._exposedColumns lastIndex] - index + 1)];
columns = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(index, [tableView._exposedColumns lastIndex] - index + 1)];
// FIXME: Would be faster with some sort of -setNeedsDisplayInColumns: that updates a dirtyTableColumnForDisplay cache; then marked columns would relayout their data views at display time.
[tableView _layoutDataViewsInRows:rows columns:columns];
[tableView tile];
if (!_disableResizingPosting)
[self _postDidResizeNotificationWithOldWidth:oldWidth];
}
@@ -297,7 +297,7 @@ CPTableColumnUserResizingMask = 1 << 1;
// unarchive the data view cache
var newDataView = [CPKeyedUnarchiver unarchiveObjectWithData:_dataViewData[dataViewUID]];
newDataView.identifier = dataViewUID;
return newDataView;
}
@@ -330,7 +330,7 @@ CPTableColumnUserResizingMask = 1 << 1;
}
/*
Returns YES if the user can edit cells associated with the receiver by double-clicking the
Returns YES if the user can edit cells associated with the receiver by double-clicking the
column in the NSTableView, NO otherwise.
*/
- (BOOL)isEditable
@@ -371,7 +371,7 @@ CPTableColumnUserResizingMask = 1 << 1;
//Setting Tool Tips
/*
Sets the tooltip string that is displayed when the cursor pauses over the
Sets the tooltip string that is displayed when the cursor pauses over the
header cell of the receiver.
*/
- (void)setHeaderToolTip:(CPString)aToolTip
@@ -415,9 +415,37 @@ CPTableColumnUserResizingMask = 1 << 1;
bindingPath = [aDataView _replacementKeyPathForBinding:bindingName],
bindingInfo = [bindingsDictionary objectForKey:bindingName]._info,
destination = [bindingInfo objectForKey:CPObservedObjectKey],
keyPath = [bindingInfo objectForKey:CPObservedKeyPathKey];
keyPath = [bindingInfo objectForKey:CPObservedKeyPathKey],
dotIndex = keyPath.lastIndexOf("."),
value;
if (dotIndex === CPNotFound)
value = [[destination valueForKeyPath:keyPath] objectAtIndex:aRow];
else
{
/*
Optimize the prototypical use case where the key path describes a value
in an array. Without this optimization, we call CPArray's valueForKey
which generates as many values as objects in the array, of which we then
pick one and throw away the rest.
The optimization is to get the array and access the value directly. This
turns the operation into a single access regardless of how long the model
array is.
*/
var firstPart = keyPath.substring(0, dotIndex),
secondPart = keyPath.substring(dotIndex+1),
firstValue = [destination valueForKeyPath:firstPart];
if ([firstValue isKindOfClass:CPArray])
value = [[firstValue objectAtIndex:aRow] valueForKeyPath:secondPart];
else
value = [[firstValue valueForKeyPath:secondPart] objectAtIndex:aRow];
}
// console.log(bindingName+" : "+keyPath+" : "+aRow+" : "+[[destination valueForKeyPath:keyPath] objectAtIndex:aRow]);
[aDataView setValue:[[destination valueForKeyPath:keyPath] objectAtIndex:aRow] forKey:bindingPath];
[aDataView setValue:value forKey:bindingPath];
}
}
+114 -16
View File
@@ -1,10 +1,22 @@
@import "CPKeyValueBinding.j"
@import <AppKit/CPKeyValueBinding.j>
@import <AppKit/CPTextField.j>
// FIXME These imports are needed to prevent the following two errors:
// error=ReferenceError: Can't find variable: CPTopVerticalTextAlignment
// error=ReferenceError: Can't find variable: CPImageLeft
// CPControl uses these but in ojtest runs the dependencies don't
// get imported for some reason.
@import <AppKit/CPButton.j>
@import <AppKit/_CPImageAndTextView.j>
@import <AppKit/CPClipView.j>
@import <AppKit/CPTableView.j>
@import <AppKit/CPArrayController.j>
@implementation CPKeyValueBindingTest : OJTestCase
{
id FOO;
id FOO;
}
- (void)testExposingBindings
@@ -12,52 +24,96 @@
[BindingTester exposeBinding:@"foo"];
[BindingTester exposeBinding:@"bar"];
[CPObject exposeBinding:@"zoo"];
[self assertTrue:[[[BindingTester new] exposedBindings] isEqual:["foo", "bar", "zoo"]]];
[self assert:["foo", "bar", "zoo"] equals:[[BindingTester new] exposedBindings]];
}
- (void)testBindTo
{
var binder = [BindingTester new];
binder.cheese = "orange";
[CPKeyValueBindingTest exposeBinding:@"FOO"];
[self bind:"FOO" toObject:binder withKeyPath:"cheese" options:nil];
[binder setCheese:@"banana"];
[self assertTrue:[self valueForKey:@"FOO"]==="banana" message:"Bound value should have been updated to banana, was "+FOO];
}
/*
- (void)testControl
{
FOO = "bingo";
var binder = [BindingTester new];
binder.cheese = "yellow";
var control = [[CPTextField alloc] init];
[control setStringValue:@"brown"];
[CPControl exposeBinding:CPValueBinding];
[[self class] exposeBinding:@"FOO"];
//[control addObserver:self forKeyPath:CPValueBinding options:nil context:"testControl"];
[control bind:CPValueBinding toObject:self withKeyPath:@"FOO" options:nil];
[self bind:@"FOO" toObject:control withKeyPath:CPValueBinding options:nil];
[control setStringValue:@"banana"];
[control setStringValue:@"grapefruit"];
[self setValue:@"BAR" forKey:@"FOO"];
[self assertTrue: FOO == [control stringValue] message: "should be equal, were: "+FOO+"and: "+[control stringValue]];
[control setStringValue:@"pina colada"];
[self assertTrue: FOO == [control stringValue] message: "should be equal, were: "+FOO+"and: "+[control stringValue]];
}*/
- (void)testTableColumn
{
var tableView = [CPTableView new],
tableColumn = [[CPTableColumn alloc] initWithIdentifier:"A Column"],
arrayController = [CPArrayController new];
[tableView addTableColumn:tableColumn];
content = [
[AccessCounter counterWithValueA:"1" valueB:"2"],
[AccessCounter counterWithValueA:"3" valueB:"4"],
[AccessCounter counterWithValueA:"5" valueB:"6"],
];
[arrayController setContent:content];
[tableColumn bind:@"value" toObject:arrayController withKeyPath:@"arrangedObjects.valueA" options:nil];
// Reset these if they were read during initialization.
for(var i=0; i<[content count];i++)
[content[i] setAccesses:0];
var testView = [DataViewTester new];
[tableColumn prepareDataView:testView forRow:0];
[self assert:'1' equals:testView.lastValue];
[self assert:'value' equals:testView.lastKey];
[tableColumn prepareDataView:testView forRow:1];
[self assert:'3' equals:testView.lastValue];
[self assert:'value' equals:testView.lastKey];
// Test that CPTableColumn is optimized to only read one value per row.
[self assert:0 equals:[content[2] accesses] message:"row 2 used "+[content[2] accesses]+" accesses but was never prepared"];
[self assert:1 equals:[content[0] accesses] message:"row 0 used "+[content[0] accesses]+" accesses to prepare"];
[self assert:1 equals:[content[1] accesses] message:"row 1 used "+[content[1] accesses]+" accesses to prepare"];
// Try the case where a key path is not used.
content = ["plain", "old", "space crystals"];
[tableColumn bind:@"value" toObject:arrayController withKeyPath:@"arrangedObjects" options:nil];
[arrayController setContent:content];
[tableColumn prepareDataView:testView forRow:1];
[self assert:'old' equals:testView.lastValue];
[self assert:'value' equals:testView.lastKey];
}
- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)changes context:(id)aContext
@@ -83,3 +139,45 @@
}
@end
@implementation DataViewTester : CPObject
{
id lastValue;
id lastKey;
}
- (void)setValue:value forKey:aKey
{
lastValue = value;
lastKey = aKey;
}
@end
@implementation AccessCounter : CPObject
{
id valueA @accessors;
id valueB @accessors;
CPNumber accesses @accessors;
}
+ (AccessCounter) counterWithValueA:aValue valueB:anotherValue
{
r = [self new];
[r setValueA:aValue];
[r setValueB:anotherValue];
return r;
}
- (id)valueA
{
accesses++;
return valueA;
}
- (id)valueB
{
accesses++;
return valueB;
}
@end