mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-11-14 18:53:50 +00:00
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:
1 parent
15be647c16
commit
8b2350fb22
2 files changed
+155
-29
No files matched your search
+41
-13
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user