From dd2ad410b8c18a552194d4c8a67da331a9eeef82 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Wed, 5 Jan 2011 17:45:51 -0300 Subject: [PATCH 1/5] Cleanup. --- AppKit/CPOutlineView.j | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j index 5cffb0d25..aed70db8c 100644 --- a/AppKit/CPOutlineView.j +++ b/AppKit/CPOutlineView.j @@ -79,11 +79,11 @@ CPOutlineViewDropOnItemIndex = -1; @ingroup appkit @class CPOutlineView - CPOutlineView is a subclass of CPTableView that inherates the row and column format to display hierarchial data. + CPOutlineView is a subclass of CPTableView that inherates the row and column format to display hierarchial data. The outlineview adds the ability to expand and collapse items. This is useful for browsing a tree like structure such as directories or a filesystem. Like the tableview, an outlineview uses a data source to supply its data. For this reason you must implement a couple data source methods (documented in setDataSource:) - + */ @implementation CPOutlineView : CPTableView { @@ -151,7 +151,7 @@ CPOutlineViewDropOnItemIndex = -1; } /*! In addition to standard delegation, the outline view also supports data source delegation. This method sets the data source object. - Just like the TableView you have CPTableColumns but instead of rows you deal with items. + Just like the TableView you have CPTableColumns but instead of rows you deal with items. You must implement these data source methods: @@ -165,7 +165,7 @@ CPOutlineViewDropOnItemIndex = -1; Returns the number of child items of a given item. If item is nil you should return the number of top level (root) items. - (id)outlineView:(CPOutlineView)outlineView objectValueForTableColumn:(CPTableColumn)tableColumn byItem:(id)item; - Returns the object value of the item in a given column. + Returns the object value of the item in a given column. The following methods are optional: @@ -195,7 +195,7 @@ CPOutlineViewDropOnItemIndex = -1; Returns YES if the drop operation is allowed otherwise NO. This method is invoked by the outlineview after a drag should begin, but before it is started. If you dont want the drag to being return NO. If you want the drag to begin you should return YES and place the drag data on the pboard. - + */ - (void)setDataSource:(id)aDataSource { @@ -280,11 +280,11 @@ CPOutlineViewDropOnItemIndex = -1; } /*! - Used to find if an item is already expanded. + Used to find if an item is already expanded. @param anItem - the item you are interest in. - @return BOOL - Yes if the item is already expanded, otherwise NO. + @return BOOL - Yes if the item is already expanded, otherwise NO. */ - (BOOL)isItemExpanded:(id)anItem { @@ -500,7 +500,7 @@ CPOutlineViewDropOnItemIndex = -1; } /*! - Returns the width of an indentation level. + Returns the width of an indentation level. @return float - the width of the indentation per level. */ @@ -580,14 +580,14 @@ CPOutlineViewDropOnItemIndex = -1; /*! Returns the frame of the disclosure button for the outline column. If the item is not expandable a CGZeroRect is returned. - Subclasses can return a CGZeroRect to prevent the disclosure control from being displayed. + Subclasses can return a CGZeroRect to prevent the disclosure control from being displayed. @param aRow - The row of the reciever @return CGRect - The rect of the disclosure button at aRow. */ - (CGRect)frameOfOutlineDisclosureControlAtRow:(CPInteger)aRow { - if (![self isExpandable:[self itemAtRow:aRow]]) + if (![self isExpandable:[self itemAtRow:aRow]]) return _CGRectMakeZero(); var dataViewFrame = [self _frameOfOutlineDataViewAtRow:aRow], @@ -670,7 +670,7 @@ CPOutlineViewDropOnItemIndex = -1; Implement this to indicate whether a given item should be rendered using the group item style. Return YES if the item is a group item, otherwise NO. - @param aDelegate - the delegate object you wish to set for the reciever. + @param aDelegate - the delegate object you wish to set for the reciever. */ - (void)setDelegate:(id)aDelegate { @@ -944,7 +944,7 @@ CPOutlineViewDropOnItemIndex = -1; Retargets the drop item for the outlineview. To specify a drop on theItem, you specify item as theItem and index as CPOutlineViewDropOnItemIndex. To specify a drop between child 1 and 2 of theItem, you specify item as theItem and index as 2. - To specify a drop on an item that canŐt be expanded theItem, you specify item as someOutlineItem and index as CPOutlineViewDropOnItemIndex. + To specify a drop on an item that can't be expanded theItem, you specify item as someOutlineItem and index as CPOutlineViewDropOnItemIndex. @param theItem - The item you want to retarget the drop on. @param theIndex - The index of the child item you want to retarget the drop between. Pass CPOutlineViewDropOnItemIndex if you want to drop on theItem. From 02e72493efc047b671dfd7357c797a954c70b195 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Wed, 5 Jan 2011 20:40:30 -0300 Subject: [PATCH 2/5] Unit test outline view data loading and item collapse. --- Tests/AppKit/CPOutlineViewTest.j | 88 ++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 Tests/AppKit/CPOutlineViewTest.j diff --git a/Tests/AppKit/CPOutlineViewTest.j b/Tests/AppKit/CPOutlineViewTest.j new file mode 100644 index 000000000..ec75cc47b --- /dev/null +++ b/Tests/AppKit/CPOutlineViewTest.j @@ -0,0 +1,88 @@ +@import + +@implementation CPOutlineViewTest : OJTestCase +{ + CPOutlineView outlineView; + CPTableColumn tableColumn; + TestDataSource dataSource; +} + +- (void)setUp +{ + outlineView = [[CPOutlineView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; + + tableColumn = [[CPTableColumn alloc] initWithIdentifier:@"Foo"]; + [outlineView addTableColumn:tableColumn]; + [outlineView setOutlineTableColumn:tableColumn]; + + [outlineView setAllowsMultipleSelection:YES]; + + dataSource = [TestDataSource new]; + [dataSource setEntries:[".1", ".1.1", ".1.2", ".1.2.1", ".1.2.2", ".2", ".3", ".3.1"]]; + + [outlineView setDataSource:dataSource]; + [outlineView expandItem:nil expandChildren:YES]; +} + +- (void)testCollapse +{ + // By default all rows should be visible. + var entries = [dataSource entries]; + for (var i = 0; i < entries.length; i++) + { + var item = [outlineView itemAtRow:i]; + [self assert:entries[i] equals:item message:"item " + i + " visible, in correct order"]; + } + + // Now collapse the .1 group. + [outlineView collapseItem:".1"]; + var expected = [".1", ".2", ".3", ".3.1"] + for (var i = 0; i < expected.length; i++) + { + var item = [outlineView itemAtRow:i]; + [self assert:expected[i] equals:item message:"item " + i + " visible after collapse, in correct order"]; + } +} + +@end + +@implementation TestDataSource : CPObject +{ + CPArray entries @accessors; +} + +- (CPArray)childrenOfPrefix:(CPString)theItem +{ + if (!theItem) + theItem = ""; + + var matcher = new RegExp("^" + theItem + "\\.\\d$"), + children = []; + for (var i = 0; i < entries.length; i++) + if (matcher.exec(entries[i])) + children.push(entries[i]); + + return children; +} + +- (id)outlineView:(CPOutlineView)theOutlineView child:(int)theIndex ofItem:(id)theItem +{ + return [self childrenOfPrefix:theItem][theIndex]; +} + +- (BOOL)outlineView:(CPOutlineView)theOutlineView isItemExpandable:(id)theItem +{ + return !![[self childrenOfPrefix:theItem] count]; +} + +- (int)outlineView:(CPOutlineView)theOutlineView numberOfChildrenOfItem:(id)theItem +{ + return [[self childrenOfPrefix:theItem] count]; +} + +- (id)outlineView:(CPOutlineView)anOutlineView objectValueForTableColumn:(CPTableColumn)theColumn byItem:(id)theItem +{ + return theItem; +} + +@end From ec675a56622721fa6d85a27799df833b31aec95c Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Wed, 5 Jan 2011 20:41:26 -0300 Subject: [PATCH 3/5] Deselect outline view items when their ancestor is collapsed. --- AppKit/CPOutlineView.j | 22 ++++++++++++++++++++++ Tests/AppKit/CPOutlineViewTest.j | 19 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j index aed70db8c..58effdc05 100644 --- a/AppKit/CPOutlineView.j +++ b/AppKit/CPOutlineView.j @@ -365,7 +365,29 @@ CPOutlineViewDropOnItemIndex = -1; return; [self _noteItemWillCollapse:anItem]; + // Update selections: + // * Deselect items inside the collapsed item. + var collapseTopIndex = [self rowForItem:anItem], + topLevel = [self levelForRow:collapseTopIndex], + collapseEndIndex = collapseTopIndex; + + while (collapseEndIndex + 1 < _itemsForRows.length && [self levelForRow:collapseEndIndex + 1] > topLevel) + collapseEndIndex++; + + var collapseRange = CPMakeRange(collapseTopIndex + 1, collapseEndIndex - collapseTopIndex); + if (collapseRange.length) + { + var selection = [self selectedRowIndexes]; + if ([selection intersectsIndexesInRange:collapseRange]) + { + [self _noteSelectionIsChanging]; + [selection removeIndexesInRange:collapseRange]; + // Will call _noteSelectionDidChange + [self _setSelectedRowIndexes:selection]; + } + } itemInfo.isExpanded = NO; + [self _noteItemDidCollapse:anItem]; [self reloadItem:anItem reloadChildren:YES]; diff --git a/Tests/AppKit/CPOutlineViewTest.j b/Tests/AppKit/CPOutlineViewTest.j index ec75cc47b..fd80d0bca 100644 --- a/Tests/AppKit/CPOutlineViewTest.j +++ b/Tests/AppKit/CPOutlineViewTest.j @@ -44,6 +44,25 @@ } } +/*! + Test that when an ancestor item containing a selected item is collapsed, the item is deselected. +*/ +- (void)testCollapseDeselect +{ + var preSelection = [CPIndexSet indexSet]; + [preSelection addIndex:[outlineView rowForItem:".1.2.2"]]; + [preSelection addIndex:[outlineView rowForItem:".1.1"]]; + + [outlineView selectRowIndexes:preSelection byExtendingSelection:NO]; + + [outlineView collapseItem:".1.2"]; + + var afterSelection = [outlineView selectedRowIndexes]; + + [self assert:1 equals:[afterSelection count] message:"1 selection should remain"]; + [self assert:".1.1" equals:[outlineView itemAtRow:[outlineView selectedRow]] message:".1.1 selection should remain"]; +} + @end @implementation TestDataSource : CPObject From c836bd48e08bf9269928b8713b2f08298dc41933 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Wed, 5 Jan 2011 21:09:19 -0300 Subject: [PATCH 4/5] Preserve selection when collapsing items in an outline view. This fixes the error where if a row was selected below a certain item and that item was collapsed, the row selection would remain fixed in place even while the item it was highlighting moved upwards. --- AppKit/CPOutlineView.j | 22 ++++++++++++++++-- Tests/AppKit/CPOutlineViewTest.j | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j index 58effdc05..1c15de74c 100644 --- a/AppKit/CPOutlineView.j +++ b/AppKit/CPOutlineView.j @@ -367,6 +367,7 @@ CPOutlineViewDropOnItemIndex = -1; [self _noteItemWillCollapse:anItem]; // Update selections: // * Deselect items inside the collapsed item. + // * Shift row selections below the collapsed item so that the same logical items remain selected. var collapseTopIndex = [self rowForItem:anItem], topLevel = [self levelForRow:collapseTopIndex], collapseEndIndex = collapseTopIndex; @@ -377,14 +378,31 @@ CPOutlineViewDropOnItemIndex = -1; var collapseRange = CPMakeRange(collapseTopIndex + 1, collapseEndIndex - collapseTopIndex); if (collapseRange.length) { - var selection = [self selectedRowIndexes]; + var selection = [self selectedRowIndexes], + didChange = NO; + if ([selection intersectsIndexesInRange:collapseRange]) { - [self _noteSelectionIsChanging]; [selection removeIndexesInRange:collapseRange]; + [self _noteSelectionIsChanging]; + didChange = YES; // Will call _noteSelectionDidChange [self _setSelectedRowIndexes:selection]; } + + // Shift any selected rows below upwards. + if ([selection intersectsIndexesInRange:CPMakeRange(collapseEndIndex + 1, _itemsForRows.length)]) + { + // Notify if that wasn't already done above. + if (!didChange) + [self _noteSelectionIsChanging]; + didChange = YES; + + [selection shiftIndexesStartingAtIndex:collapseEndIndex + 1 by:-collapseRange.length]; + } + + if (didChange) + [self _setSelectedRowIndexes:selection]; } itemInfo.isExpanded = NO; diff --git a/Tests/AppKit/CPOutlineViewTest.j b/Tests/AppKit/CPOutlineViewTest.j index fd80d0bca..cfba1a27e 100644 --- a/Tests/AppKit/CPOutlineViewTest.j +++ b/Tests/AppKit/CPOutlineViewTest.j @@ -24,6 +24,9 @@ [outlineView expandItem:nil expandChildren:YES]; } +/*! + Test that entries load and hide correctly. +*/ - (void)testCollapse { // By default all rows should be visible. @@ -63,6 +66,42 @@ [self assert:".1.1" equals:[outlineView itemAtRow:[outlineView selectedRow]] message:".1.1 selection should remain"]; } +/*! + Test that the selection stays on the same item below a collapse. +*/ +- (void)testCollapseWithSelectionBelow +{ + // [".1", ".1.1", ".1.2", ".1.2.1", ".1.2.2", ".2", ".3", ".3.1"] + var preSelection = [CPIndexSet indexSet]; + [preSelection addIndex:[outlineView rowForItem:".1.1"]]; + [preSelection addIndex:[outlineView rowForItem:".3.1"]]; + + [outlineView selectRowIndexes:preSelection byExtendingSelection:NO]; + + [outlineView collapseItem:".1.2"]; + + var afterSelection = [outlineView selectedRowIndexes]; + + [self assert:2 equals:[afterSelection count] message:"selections should remain"]; + [self assert:".1.1" equals:[outlineView itemAtRow:[afterSelection firstIndex]] message:".1.1 selection should remain"]; + [self assert:".3.1" equals:[outlineView itemAtRow:[afterSelection lastIndex]] message:".3.1 selection should remain"]; + + // Collapse where one selection disappears and one shifts. + preSelection = [CPIndexSet indexSet]; + [preSelection addIndex:[outlineView rowForItem:".1.1"]]; + [preSelection addIndex:[outlineView rowForItem:".3"]]; + + [outlineView selectRowIndexes:preSelection byExtendingSelection:NO]; + + [outlineView collapseItem:".1"]; + + afterSelection = [outlineView selectedRowIndexes]; + + [self assert:1 equals:[afterSelection count] message:"1 selection should disappear"]; + + [self assert:".3" equals:[outlineView itemAtRow:[afterSelection firstIndex]] message:".3 selection should remain"]; +} + @end @implementation TestDataSource : CPObject From c53fff058b33712a7f310dd04e464ec57ddaed62 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Wed, 5 Jan 2011 21:25:08 -0300 Subject: [PATCH 5/5] Preserve selection when expanding items in an outline view. --- AppKit/CPOutlineView.j | 16 ++++++++++++++++ Tests/AppKit/CPOutlineViewTest.j | 25 ++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j index 1c15de74c..f27952646 100644 --- a/AppKit/CPOutlineView.j +++ b/AppKit/CPOutlineView.j @@ -331,6 +331,22 @@ CPOutlineViewDropOnItemIndex = -1; if (!itemInfo.isExpanded) { [self _noteItemWillExpand:anItem]; + + // Shift selection indexes below so that the same items remain selected. + var newRowCount = [_outlineViewDataSource outlineView:self numberOfChildrenOfItem:anItem]; + if (newRowCount) + { + var selection = [self selectedRowIndexes], + expandIndex = [self rowForItem:anItem] + 1; + + if ([selection intersectsIndexesInRange:CPMakeRange(expandIndex, _itemsForRows.length)]) + { + [self _noteSelectionIsChanging]; + [selection shiftIndexesStartingAtIndex:expandIndex by:newRowCount]; + [self _setSelectedRowIndexes:selection]; + } + } + itemInfo.isExpanded = YES; [self _noteItemDidExpand:anItem]; [self reloadItem:anItem reloadChildren:YES]; diff --git a/Tests/AppKit/CPOutlineViewTest.j b/Tests/AppKit/CPOutlineViewTest.j index cfba1a27e..0359f4fb5 100644 --- a/Tests/AppKit/CPOutlineViewTest.j +++ b/Tests/AppKit/CPOutlineViewTest.j @@ -39,7 +39,7 @@ // Now collapse the .1 group. [outlineView collapseItem:".1"]; - var expected = [".1", ".2", ".3", ".3.1"] + var expected = [".1", ".2", ".3", ".3.1"]; for (var i = 0; i < expected.length; i++) { var item = [outlineView itemAtRow:i]; @@ -102,6 +102,29 @@ [self assert:".3" equals:[outlineView itemAtRow:[afterSelection firstIndex]] message:".3 selection should remain"]; } +/*! + Test that the selection stays on the same item below an expand. +*/ +- (void)testExpandWithSelectionBelow +{ + // [".1", ".1.1", ".1.2", ".1.2.1", ".1.2.2", ".2", ".3", ".3.1"] + [outlineView collapseItem:".1.2"]; + + var preSelection = [CPIndexSet indexSet]; + [preSelection addIndex:[outlineView rowForItem:".1.1"]]; + [preSelection addIndex:[outlineView rowForItem:".2"]]; + + [outlineView selectRowIndexes:preSelection byExtendingSelection:NO]; + + [outlineView expandItem:".1.2"]; + afterSelection = [outlineView selectedRowIndexes]; + + [self assert:2 equals:[afterSelection count] message:"selections should remain"]; + + [self assert:".1.1" equals:[outlineView itemAtRow:[afterSelection firstIndex]] message:".1.1 selection should remain"]; + [self assert:".2" equals:[outlineView itemAtRow:[afterSelection lastIndex]] message:".2 selection should remain"]; +} + @end @implementation TestDataSource : CPObject