Skip to content

Instantly share code, notes, and snippets.

@JonGretar
Forked from anonymous/gist:111048
Created May 19, 2009 09:02
Show Gist options
  • Save JonGretar/113994 to your computer and use it in GitHub Desktop.
Save JonGretar/113994 to your computer and use it in GitHub Desktop.
/*
* Copyright 2009 agile42 GmbH - All rights reserved
*
* Authors:
* - Martin Häcker <martin.haecker@agile42.com>
*/
// TODO: minimize this so only the actual dependencies are included
@import <AppKit/AppKit.j>
/*! @class CPBrowser
// TODO: description
@par Delegate Methods
@delegate -(void)browser:(CPBrowser)sender createRowsForColumn:(CPNumber)aColumn inCollectionView:(CPCollectionView)aCollectionView
Called when the browser needs a row to be filled from the delegate
*/
@implementation CPBrowser : CPControl
{
id _delegate;
BOOL _isLoaded;
// REFACT: consider adding another array for the not yet loaded views
CPMutableArray _columns;
float _minimumColumnWidth;
BOOL _allowsEmptySelection;
BOOL _shouldAcceptArrowKeys;
}
// Lifecycle .........................
- (id) initWithFrame:(CGRect)aRect
{
if ( ! [super initWithFrame:aRect])
return nil;
_columns = [CPMutableArray array];
// TODO: minimum width, something like scrollbar + the borders if they are to be drawn
_minimumColumnWidth = 100;
_allowsEmptySelection = YES;
_shouldAcceptArrowKeys = YES;
return self;
}
// Configuring a Browser .............
/*! Returns the NSBrowser's delegate.
@see -setDelegate: */
- (id) delegate
{
return _delegate;
}
/*! Sets the delegate of the receiver.
If not nil, the delegate must either be passive and respond to
-browser:numberOfRowsInColumn: or be active and respond to
-browser:createRowsForColumn:inCollectionView: but not both.
If the delegate is passive it must also respond to
-browser:willDisplayCell:atRow:column:.
If the delegate is not nil but does not meet these conditions,
an NSBrowserIllegalDelegateException will be raised.
@see -delegate */
- (void) setDelegate:(id)anObject
{
// Neither active nor passive
if (! [self _isActiveDelegate:anObject]
&& ! [self _isPassiveDelegate:anObject])
[CPException raise:CPInternalInconsistencyException
reason:"Delegate " + _delegate + " needs to be either passive and implement "
+ "-browser:numberOfRowsInColumn: "
+ "and -browser:willDisplayCell:atRow:column: "
+ " or active and implement -browser:createRowsForColumn:inCollectionView:"];
if ([self _isActiveDelegate:anObject] && [self _isPassiveDelegate:anObject])
[CPException raise:CPInternalInconsistencyException
reason:"Delegate can't implement both the active and passive delegate interface!"];
if ([self _isPassiveDelegate:anObject])
[CPException raise:CPInternalInconsistencyException
reason:"Passive Delegate not yet implemented. Please send patches!"];
_delegate = anObject;
}
- (CPNumber) minColumnWidth
{
return _minimumColumnWidth;
}
/*! Returns whether the arrow keys are enabled. By default YES.
@see -setAcceptsArrowKeys: */
- (BOOL) acceptsArrowKeys
{
return _shouldAcceptArrowKeys;
}
/*! Enables or disables the arrow keys as used for navigating within
and between browsers. By default YES.
@see -acceptsArrowKeys */
- (void) setAcceptsArrowKeys:(BOOL)aBool
{
_shouldAcceptArrowKeys = aBool;
}
// Managing Columns ............
/*! Returns wether column zero is loaded. */
- (BOOL) isLoaded
{
return _isLoaded;
}
/*! Loads Column 0; unloads previously loaded columns. */
- (void) loadColumnZero
{
// reset everything
[self setLastColumn:-1];
[self addColumn];
// TODO: check if I need [self setNeedsDisplay:YES] here?
}
/*! Sets the last column to aNumber.
Call with -1 to unload everything.
@see -lastColumn */
- (void) setLastColumn:(int)aNumber
{
// -1 case is what Cocoa and GnuStep do.
if (-1 === aNumber) {
[_columns removeAllObjects];
_isLoaded = NO;
return;
}
if (aNumber >= [_columns count])
return;
var rangeToRemove = CPMakeRange(aNumber + 1, [_columns count] - aNumber - 1);
var each, objectEnumerator = [[_columns subarrayWithRange:rangeToRemove] objectEnumerator];
while (each = [objectEnumerator nextObject])
[[each scrollView] removeFromSuperview];
[_columns removeObjectsInRange:rangeToRemove];
}
/*! Returns the index of the last column loaded.
Returns -1 if no column is loaded yet.
@see -setLastColumn: */
- (CPNumber) lastColumn
{
// -1 if not loaded is what Cocoa and GnuStep do.
return [_columns count] - 1;
}
/*! Adds a column to the right of the last column, adjusts subviews and
scrolls to make the new column visible if needed. */
- (void) addColumn
{
// REFACT: consider using -setLastColumn: somewhere here
var newColumn = [[CPBrowserColumn alloc] initWithBrowser:self];
[_columns addObject:newColumn];
var lastLoadedColumnIndex = [_columns indexOfObject:newColumn];
[newColumn setColumnIndex:lastLoadedColumnIndex]
[self reloadColumn:lastLoadedColumnIndex];
// REFACT: most other objects seem to have a -tile method that re-layouts everything
[self addSubview:[newColumn scrollView]];
_isLoaded = YES;
// TODO: scroll the new column into visibility
}
/*! Reloads column if it is loaded; sets it as the last column.
Reselects previously selected cells, if they remain. */
- (void) reloadColumn:(CPNumber)aColumn
{
// only reload columns that are currently loaded
if ([_columns count] <= aColumn)
return;
[_delegate browser:self createRowsForColumn:aColumn inCollectionView:[self _collectionViewInColumn:aColumn]];
// TODO: check and implement different delegate types
// TODO: handle preserving selection
}
// Selection management ......................
/*! Returns whether there can be nothing selected. By default YES.
@see -setAllowsEmptySelection: */
- (BOOL) allowsEmptySelection { return _allowsEmptySelection; }
/*! Sets whether there can be nothing selected.
@see -allowsEmptySelection */
- (void) setAllowsEmptySelection:(BOOL)aBool { _allowsEmptySelection = aBool; }
/*! Returns the last (rightmost and lowest) selected view. Returns nil if
no view is selected
@see -selectedCells, -selectedCellInColumn:
*/
- (id) selectedCell
{
var each, reverseEnumerator = [_columns reverseObjectEnumerator];
while (each = [reverseEnumerator nextObject])
if ([each selectedCell])
return [each selectedCell];
return nil;
}
/*! Selects the views at aRowIndex in the column identified by
aColumnIndex. If the delegate method -browser:selectRow:inColumn:
is implemented, this is its responsability to select the cell.
Non existing indexes will be ignored.
@see -loadedCellAtRow:column:, -browser:selectRow:inColumn: */
- (void) selectRow:(unsigned)aRowIndex inColumn:(unsigned)aColumnIndex
{
// TODO: external selection management is still completely unsupported / untested
// if ([_delegate respondsToSelector:@selector(browser:selectRow:inColumn:)])
// return [_delegate browser:self selectRow:aRowIndex inColumn:aColumnIndex];
if ( ! [self isLoaded])
[self loadColumnZero];
if (aColumnIndex < 0 || aColumnIndex >= [_columns count])
return;
if (aRowIndex < 0 || aRowIndex >= [_columns[aColumnIndex] numberOfRows])
return;
var indexes = [CPIndexSet indexSetWithIndex:aRowIndex];
[[self _collectionViewInColumn:aColumnIndex] setSelectionIndexes:indexes];
}
/** Returns the row index of the selected view in the column specified by
aColumnIndex. Returns -1 if no view is selected
@see -selectedCellInColumn: */
- (int) selectedRowInColumn:(unsigned)aColumnIndex
{
if ( ! [_columns[aColumnIndex] selectedCell])
return -1;
return [_columns[aColumnIndex] selectedCellIndex];
}
/** Returns the last (lowest) CPBrowserView that's selected in aColumnIndex.
Returns nil if no view is selected
@see -selectedCell, -selectedCells */
- (id) selectedCellInColumn:(unsigned)aColumnIndex
{
if ( ! [_columns[aColumnIndex] selectedCell])
return nil;
return [_columns[aColumnIndex] selectedCell];
}
/*! Returns the index of the last column with a selected item
or -1 if nothing is selected */
- (int) selectedColumn
{
for (var i = [_columns count] - 1; i >= 0; i--)
if ([_columns[i] selectedCell])
return i;
return -1;
}
// Managing Actions ...............
/*! Sends the action message to the target. Returns YES upon success,
NO if no target for the message could be found. */
- (BOOL) sendAction
{
return [self sendAction:[self action] to:[self target]];
}
// Private methods ................
- (CPCollectionView) _collectionViewInColumn:(CPNumber)aNumber
{
if ([_columns count] <= aNumber)
[CPException raise:CPInternalInconsistencyException
reason:"you asked for a non existing column: " + aNumber];
return [[_columns objectAtIndex:aNumber] collectionView];
}
- (BOOL) _isActiveDelegate:(id)aDelegate
{
return [aDelegate respondsToSelector:@selector(browser:createRowsForColumn:inCollectionView:)];
}
- (BOOL) _isPassiveDelegate:(id)aDelegate
{
return [aDelegate respondsToSelector:@selector(browser:numberOfRowsInColumn:)]
&& [aDelegate respondsToSelector:@selector(browser:willDisplayCell:atRow:column:)];
}
- (void) _sendActionFromColumn:(unsigned)aColumnIndex
{
[self sendAction];
[self setLastColumn:aColumnIndex];
if ([[self selectedCellInColumn:aColumnIndex] isLeaf])
return;
[self addColumn];
}
// Overides .........................
- (void) drawRect:(CGRect)dirtyRect
{
if ( ! _isLoaded)
[self loadColumnZero];
[super drawRect:dirtyRect];
}
- (BOOL) acceptsFirstResponder
{
return YES;
}
- (void)keyDown:(CPEvent)event
{
[self interpretKeyEvents:[event]];
}
- (void) moveDown:(id)sender
{
if ( ! [self acceptsArrowKeys])
return;
var selectedRow = [self selectedRowInColumn:[self selectedColumn]];
[self selectRow:selectedRow + 1 inColumn:[self selectedColumn]];
}
- (void) moveUp:(id)sender
{
if ( ! [self acceptsArrowKeys])
return;
var selectedRow = [self selectedRowInColumn:[self selectedColumn]];
[self selectRow:selectedRow - 1 inColumn:[self selectedColumn]];
}
- (void) moveBackward:(id)sender
{
if ( ! [self acceptsArrowKeys])
return;
var selectedRowInPreviousColumn = [self selectedRowInColumn:[self selectedColumn] - 1];
[self selectRow:selectedRowInPreviousColumn inColumn:[self selectedColumn] - 1];
}
- (void) moveForward:(id)sender
{
if ( ! [self acceptsArrowKeys])
return;
[self selectRow:0 inColumn:[self selectedColumn] + 1];
}
@end
/// Private implementation helper class @ignore
@implementation CPBrowserColumn : CPObject
{
CPBrowser _browser;
CPScrollView _scrollView;
CPCollectionView _collectionView;
unsigned _columnIndex;
}
- initWithBrowser:(CPBrowser)aBrowser
{
if ( ! [super init])
return nil;
_browser = aBrowser;
[self buildColumn];
return self;
}
- (void) buildColumn
{
// TODO: restrict to a single row
var frame = [_browser bounds];
frame.size.width = [_browser minColumnWidth];
_scrollView = [[CPScrollView alloc] initWithFrame:frame];
[_scrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[_scrollView setAutohidesScrollers:YES];
_collectionView = [[CPCollectionView alloc] initWithFrame:[_scrollView bounds]];
[_collectionView setAutoresizingMask:CPViewHeightSizable | CPViewWidthSizable];
// TODO: use sensible defaults here
[_collectionView setDelegate:self];
[_collectionView setMinItemSize:CGSizeMake(100,22)];
[_collectionView setMaxItemSize:CGSizeMake(100,2)];
[_collectionView setVerticalMargin:0.0];
[_collectionView setAllowsEmptySelection:[_browser allowsEmptySelection]];
var itemPrototype = [[CPCollectionViewItem alloc] init];
// TODO: need to use the default height and width here
[itemPrototype setView:[[CPBrowserView alloc] initWithFrame:CGRectMake(0,0, 100,22)]];
[_collectionView setItemPrototype:itemPrototype];
[_scrollView setDocumentView:_collectionView];
[[_scrollView contentView] setBackgroundColor:[CPColor whiteColor]];
[_browser addSubview:_scrollView];
}
- (CPScrollView) scrollView
{
return _scrollView;
}
- (CPCollectionView) collectionView
{
return _collectionView;
}
// REFACT: move this into the constructor.
- (void) setColumnIndex:(CPNumber)anIndex
{
_columnIndex = anIndex;
var origin = [[self scrollView] frame].origin;
origin.x = anIndex * [_browser minColumnWidth];
[[self scrollView] setFrameOrigin:origin]
}
- (int) selectedCellIndex
{
var selectedCellIndexes = [[self collectionView] selectionIndexes];
if (-1 === [selectedCellIndexes count])
return nil;
return [selectedCellIndexes lastIndex];
}
- (CPBrowserView) selectedCell
{
if (-1 === [self selectedCellIndex])
return nil;
return [[[[self collectionView] items] objectAtIndex:[self selectedCellIndex]] view];
}
- (unsigned) numberOfRows
{
return [[[self collectionView] content] count];
}
// CollecitonView delegates ................
- (void) collectionViewDidChangeSelection:(CPCollectionView)aCollectionView
{
// TODO: find a way to prevent the selection if the delegate doesn't want it
// && ! [[_browser delegate] browser:self selectRow:aRowIndex inColumn:aColumnIndex])
// This probably means expanding the delegate protocol of CPCollectionView
// TODO: of course the action method should not be sent when the selection is changed programmaticly
// only if ther is still a selection?
//var rowIndex = [[[self collectionView] items] indexOfObject:[self selectedCell]];
// REFACT: research if an action on the CollectionView can be used for this too
[_browser _sendActionFromColumn:_columnIndex];
[[_browser window] makeFirstResponder:_browser];
}
@end
// REFACT: As a public class, this should go into it's own file
@implementation CPBrowserView : CPView
{
CPTextField _label;
// TODO: CPImageView _arrow;
BOOL _isLeaf @accessors(getter=isLeaf, setter=setLeaf:);
id _representedObject;
}
// REFACT: this is still not good. Doing it like this means that the delegate has
// to provide his own subclass of the CPBrowserView so that it can now how to ask
// the provided object if it is a leaf and how to get the data from it.
- (void) setRepresentedObject:(id)anObject
{
_representedObject = anObject;
if ( ! _label)
{
_label = [[CPTextField alloc] initWithFrame:CGRectInset([self bounds], 0, 2)];
[self addSubview:_label];
}
[_label setStringValue:[anObject description]];
}
- (id) representedObject { return _representedObject; }
- (void) setSelected:(BOOL)isSelected
{
if (isSelected) {
[self setBackgroundColor:[CPColor selectedControlColor]];
[_label setTextColor:[CPColor selectedControlTextColor]];
// when not in focus use secondarySelectedControlColor
}
else {
[self setBackgroundColor:[CPColor clearColor]];
[_label setTextColor:[CPColor textColor]];
}
}
@end
// REFACT: these should either go into CPColor, or come from the themes somehow.
@implementation CPColor (CPBrowser)
+ (CPColor) selectedControlColor { return [self alternateSelectedControlColor]; }
+ (CPColor) selectedControlTextColor { return [self whiteColor]; }
+ (CPColor) textColor { return [self blackColor]; }
@end
// TODO: needs CPCoding support
// TODO: specify the delegate protocoll somehow (category on CPObject? seems like a bad idea)
/*
* Copyright 2009 agile42 GmbH - All rights reserved
*
* Authors:
* - Martin Häcker <martin.haecker@agile42.com>
*/
@import "CPBrowser.j"
// REFACT: consider grouping all the delegate tests together for ease of maintenance
/// Only used to test if the delegate check works
@implementation CPBrowserDelegateWhichIsActiveAndPassive : CPObject
- browser:aBrowser numberOfRowsInColumn:aNumber {}
- browser:aBrowser willDisplayCell:aCell atRow:aRowIndex column:aColumnIndex {}
- browser:aBrowser createRowsForColumn:aNumber inCollectionView:aCollectionView {}
@end
@implementation CPBrowserTest : OJTestCase
{
CPBrowser browser;
CPMutableArray reloadedColumns;
CPCollectionView lastCollectionView;
BOOL didCallClickCallback;
}
// CPBrowser Delegate methods and test helpers .....
- (BOOL) didReloadColumn:(CPNumber)aColumn
{
if (aColumn >= [reloadedColumns count])
return NO;
return [reloadedColumns objectAtIndex:aColumn];
}
// Delegate methods .......................
- (void)browser:(CPBrowser)sender createRowsForColumn:(unsigned)aColumn inCollectionView:(CPCollectionView)aCollectionView
{
[reloadedColumns replaceObjectAtIndex:aColumn withObject:YES];
lastCollectionView = aCollectionView;
[aCollectionView setContent:["first", "second", "third"]];
}
// Aditional asserts ....................
// REFACT: consider moving this into OJUnit
- assert:(CPString)aRegex matches:(CPString)aString
{
[self assertTrue:aString.match(RegExp(aRegex))
message:"string <" + aString + "> should be matched by regex /" + aRegex + "/"];
}
- (BOOL) hasView:(CPView)aView subview:(CPView)aSubview
{
if ([[aView subviews] containsObject:aSubview])
return YES;
var each, enumerator = [[aView subviews] objectEnumerator];
while (each = [enumerator nextObject]) {
if ([self hasView:each subview:aSubview])
return YES;
}
return NO;
}
// Testing helpers .....................
- clickEventAtPoint:(CGPoint)aPoint
{
return [CPEvent mouseEventWithType:CPLeftMouseDown location:aPoint modifierFlags:0
timestamp:0.0 windowNumber:0 context:nil eventNumber:0 clickCount:1 pressure:1.0];
}
// Test methods ........................
- setUp
{
// This is purely needed to initialize CPApp, which is used all over the place in AppKit as a shorthand...
[CPApplication sharedApplication];
browser = [[CPBrowser alloc] initWithFrame:CGRectMake(0,0, 123,234)];
[browser setDelegate:self];
reloadedColumns = [];
}
- testSmoke
{
var browser = [[CPBrowser alloc] initWithFrame:CGRectMakeZero()];
[self assertNotNull:browser];
[self assertFalse:[browser isLoaded]];
}
- testBrowserRetainsDelegate
{
var browser = [[CPBrowser alloc] initWithFrame:CGRectMakeZero()];
[browser setDelegate:self];
[self assert:self same:[browser delegate]];
}
- testBrowserOnlyAcceptsDelegateWhoSatisfiesTheDelegateConstraints
{
var exception = nil;
try { [browser setDelegate:[CPObject new]]; }
catch (ex) { exception = ex; }
[self assert:"-browser:willDisplayCell:atRow:column:" matches:[exception reason]];
[self assert:"-browser:createRowsForColumn:inCollectionView:" matches:[exception reason]];
var activeAndPassiveDelegate = [CPBrowserDelegateWhichIsActiveAndPassive new];
[self assertTrue:[browser _isActiveDelegate:activeAndPassiveDelegate] message:"active"];
[self assertTrue:[browser _isPassiveDelegate:activeAndPassiveDelegate] message:"passive"];
exception = nil;
try { [browser setDelegate:activeAndPassiveDelegate]; }
catch (ex) { exception = ex; }
[self assertNotNull:exception message:"should have thrown"];
[self assert:"both the active and passive delegate interface" matches:[exception reason]];
}
- testCanLoadColumnZero
{
[browser loadColumnZero];
[self assertTrue:[self didReloadColumn:0]]
[self assertNotNull:lastCollectionView];
}
- testBrowserIsLoadedAfterColumnZeroIsLoaded
{
[self assertFalse:[browser isLoaded]];
[browser loadColumnZero];
[self assertTrue:[browser isLoaded]];
}
- testCanOnlyReloadColunsThatHaveBeenLoadedBefore
{
[browser reloadColumn:0];
[self assertNull:lastCollectionView];
[self assertFalse:[self didReloadColumn:0]];
[self assertFalse:[browser isLoaded]];
[browser loadColumnZero];
[self assertNotNull:lastCollectionView];
[self assertTrue:[self didReloadColumn:0]];
[self assertTrue:[browser isLoaded]];
reloadedColumns = [];
lastCollectionView = nil;
[browser reloadColumn:0];
[self assertNotNull:lastCollectionView];
[self assertTrue:[self didReloadColumn:0]];
reloadedColumns = [];
lastCollectionView = nil;
[browser reloadColumn:3];
[self assertNull:lastCollectionView];
[self assertFalse:[self didReloadColumn:0]];
[self assertFalse:[self didReloadColumn:3]];
}
- testLastColumnReturnsMinusOneWhenNotLoaded
{
[self assert:-1 equals:[browser lastColumn]];
}
- testSettingLastColumnToMinusOneRemovesAllColumnsAndMarksTheBrowserAsUnloaded
{
[browser loadColumnZero];
[self assertTrue:[browser isLoaded]];
[self assert:0 equals:[browser lastColumn]];
[browser setLastColumn:-1];
[self assertFalse:[browser isLoaded] message:"should be unloaded now"];
[self assert:-1 equals:[browser lastColumn]];
}
- testSetLastColumn
{
// shouldn't do anyting
[browser setLastColumn:100];
[browser selectRow:1 inColumn:0];
[self assert:2 equals:[browser._columns count] message:"selected and next row"];
[self assert:1 equals:[browser selectedRowInColumn:0]];
var lastColumn = browser._columns[1];
[browser setLastColumn:0];
[self assertFalse:[self hasView:browser subview:[lastColumn scrollView]]];
[self assert:1 equals:[browser._columns count] message:"next row should be gone"];
[self assert:1 equals:[browser selectedRowInColumn:0]];
[self assert:[browser selectedCellInColumn:0] same:[browser selectedCell]];
[browser selectRow:1 inColumn:0];
[browser selectRow:1 inColumn:1];
[browser selectRow:1 inColumn:2];
[browser setLastColumn:0];
[self assert:1 equals:[browser._columns count] message:"after adding 3 columns"];
}
@end
@implementation CPBrowserTest (ColumnViewLayoutTests)
- testBrowserHandsOutCollectionViewInCallback
{
[browser loadColumnZero];
[self assertTrue:[lastCollectionView isKindOfClass:CPCollectionView]];
}
- testFirstColumnIsLayoutedAllToTheLeft
{
[browser loadColumnZero];
[self assertTrue:CGPointEqualToPoint(CGPointMake(0,0), [lastCollectionView frame].origin)];
[self assert:[browser frame].size.height equals:[[browser._columns[0] scrollView] frame].size.height];
[self assertTrue:[self hasView:browser subview:lastCollectionView] message:"subview not found"];
}
- testFirstColumnHasDefaultMinimumWidth
{
[self assert:100 equals:[browser minColumnWidth] message:"arbitrarily chosen default value"];
[browser loadColumnZero];
[self assert:100 equals:[lastCollectionView frame].size.width];
}
- testColumnCollectionViewHasCPBrowserViewAsView
{
[browser loadColumnZero];
[self assert:CPBrowserView equals:[[[lastCollectionView itemPrototype] view] class]];
}
- testSecondColumnHasTheRightOffset
{
[self assert:100 equals:[browser minColumnWidth] message:"arbitrarily chosen default value"];
[browser addColumn];
[browser addColumn];
// Fishy to call through the implementation like this. Is there a better way?
[self assert:100 equals:[[browser._columns[1] scrollView] frame].origin.x]
}
- testBrowserHasColumnsAsSubviews
{
[browser addColumn];
[browser addColumn];
[self assertTrue:[self hasView:browser subview:[browser._columns[0] scrollView]]];
[self assertTrue:[self hasView:browser subview:[browser._columns[1] scrollView]]];
}
- testDrawRectLoadsColumnZeroIfNeccessary
{
[self assertFalse:[self didReloadColumn:0]];
[browser drawRect:CGRectMakeZero()];
[self assertTrue:[self didReloadColumn:0]];
}
// TODO: actually the whole scroll view is still missing. :)
- testAddColumnScrollsToMakeTheLastColumnVisible {}
/*
TODO: the cell that is created from the default prototype doesn't really get nice dimensions.
It should probably get them on setting it up, so it has them after decoding.
Also -setFrameSize is called on it only after it has been -setRepresentedObject:'ed
(so creating layout there doesn't really work). On another note, the frameSize is set to zero there...
These are probably bugs that should be fixed in a separate session on CPCollectionView
*/
@end
@implementation CPBrowserTest (MultipleColumns)
- testBrowserViewCanHaveMultipleColumns
{
[browser addColumn];
[self assertTrue:[self didReloadColumn:0] message:"0"];
[self assert:0 equals:[browser lastColumn]];
[browser addColumn];
[self assertTrue:[self didReloadColumn:1] message:"1"];
[self assert:1 equals:[browser lastColumn]];
}
- testWhenABranchIsSelectedOrClickedTheNextColumnIsAutomaticlyLoaded
{
[browser selectRow:0 inColumn:0];
[self assertFalse:[[browser selectedCell] isLeaf]];
[self assert:2 equals:[browser._columns count]];
var event = [self clickEventAtPoint:CGPointMakeZero()];
[[browser _collectionViewInColumn:1] mouseDown:event];
[self assert:3 equals:[browser._columns count]];
}
- testWhenALeafIsSelectedOrClickedOnTheNextColumnIsNotLoaded
{
[browser loadColumnZero];
var cell = [[[browser _collectionViewInColumn:0] items][0] view];
[cell setLeaf:YES];
[[browser _collectionViewInColumn:0] mouseDown:[self clickEventAtPoint:CGPointMakeZero()]];
[self assert:0 equals:[browser selectedRowInColumn:0]];
[self assert:1 equals:[browser._columns count]];
}
- testProgrammaticallySelectingSometingCanLoadFurtherColumns
{
[browser selectRow:1 inColumn:0];
[browser selectRow:1 inColumn:1];
[self assert:3 equals:[browser._columns count]];
[self assert:[browser._columns[1] selectedCell] same:[browser selectedCell]];
}
- testWhenAViewInAPrevieousColumnIsSelectedAllFurtherColumnsAreRemoved
{
[browser selectRow:1 inColumn:0];
[browser selectRow:1 inColumn:1];
[browser selectRow:1 inColumn:0];
[self assert:2 equals:[browser._columns count]];
}
// TODO: - testWhenANewColumnIsAddedItIsScrolledIntoVisibility {}
// default browser view
//- testReloadingSetsNeedsDisplay
// not sure how to do this yet - it should definitely only set that on the loaded column - not the ones in front of it
@end
@implementation CPBrowserTest (SelectionManagementTest)
- testAllowsEmptySelectionByDefault
{
[self assertTrue:[browser allowsEmptySelection]];
[browser setAllowsEmptySelection:NO];
[self assertFalse:[browser allowsEmptySelection]];
[browser setAllowsEmptySelection:YES];
[self assertTrue:[browser allowsEmptySelection]];
}
- testNoCellIsSelectedAfterLoadingWhenEmptySelectionIsEnabled
{
[self assertNull:[browser selectedCell]];
[browser loadColumnZero];
[self assertNotNull:[browser _collectionViewInColumn:0]];
[self assertTrue:[[browser _collectionViewInColumn:0] allowsEmptySelection] message:"browser should allow"];
[self assertNull:[browser selectedCell]];
}
- testNothingIsSelectedAfterLoadingWhenEmptySelectionsAreDisallowed
{
// Even though empty selection is disallowed initially nothing is selected
// This makes sense, as the user first has to decide what he selects and also
// so that each row can first come up empty. Otherwise the current selected
// cell would always have to be the first cell on the column one to the right
// of the one clicked (if the clicked cell was a branch)
// TODO: switch these two lines! (hint: it will break :)
[browser setAllowsEmptySelection:NO];
[browser loadColumnZero];
[self assertFalse:[[browser _collectionViewInColumn:0] allowsEmptySelection] message:"browser should deny"];
[self assert:-1 equals:[[[browser _collectionViewInColumn:0] selectionIndexes] firstIndex]];
[self assertNull:[browser selectedCell]];
}
- testSelectedCellCanBeFoundViaSelectedCell
{
[browser loadColumnZero];
[[browser _collectionViewInColumn:0] setSelectionIndexes:[CPIndexSet indexSetWithIndex:0]];
[self assert:1 equals:[[[browser _collectionViewInColumn:0] selectionIndexes] count]];
[self assert:0 equals:[[[browser _collectionViewInColumn:0] selectionIndexes] firstIndex]];
[self assertNotNull:[browser selectedCellInColumn:0]];
[self assert:CPBrowserView same:[[browser selectedCellInColumn:0] class]];
[self assertNotNull:[browser selectedCell]];
[self assert:[[[browser _collectionViewInColumn:0] items][0] view] same:[browser selectedCell]];
[self assert:CPBrowserView same:[[browser selectedCell] class]];
}
- testCanSelectACellProgrammaticly
{
[browser loadColumnZero];
[browser selectRow:0 inColumn:0];
[self assert:0 equals:[browser selectedRowInColumn:0]];
// TODO: also check the other selection modes
// TODO: make sure it can also select on another column.
}
- testSelectingSomethingProgrammaticlyLoadsTheBrowserIfNeccessary
{
[self assertFalse:[browser isLoaded]];
[browser selectRow:0 inColumn:0];
[self assertTrue:[browser isLoaded]];
[self assert:0 equals:[browser selectedRowInColumn:0]];
}
- (IBAction) clickCallback:(CPBrowser)sender { didCallClickCallback = YES; }
- testActionGetsCalledWhenARowIsSelected
{
// Interestingly the action is NOT sent when -selectRow:inColumn: is used to
// programmaticly select a an entry in Cocoa. So we have to use other means.
[browser loadColumnZero];
[browser setAction:@selector(clickCallback:)];
[browser setTarget:self];
// This should call alarm when my assumption that the browser not being in a
// window and therefore don't touching the point becomes wrong
var convertedPoint = [[browser _collectionViewInColumn:0] convertPoint:CGPointMake(23,42) fromView:nil];
[self assert:23 equals:convertedPoint.x];
[self assert:42 equals:convertedPoint.y];
var clickEvent = [self clickEventAtPoint:CGPointMakeZero()];
[self assertFalse:didCallClickCallback];
[[browser _collectionViewInColumn:0] mouseDown:clickEvent];
[self assertTrue:[browser isLoaded]];
[self assertTrue:didCallClickCallback];
}
- testSelectedRowInColumn
{
[self assert:-1 equals:[browser selectedRowInColumn:0]];
[browser selectRow:0 inColumn:0];
[self assert:0 equals:[browser selectedRowInColumn:0] message:"row zero might be interpreted in a bool context"];
[browser selectRow:1 inColumn:0];
[self assert:1 equals:[browser selectedRowInColumn:0]];
// TODO: test with multiple columns
}
- testSelectedCellInColumn
{
[self assertNull:[browser selectedCellInColumn:0]];
[browser selectRow:0 inColumn:0];
[self assert:[browser._columns[0] selectedCell] equals:[browser selectedCellInColumn:0]
message:"row zero might be interpreted in a bool context"];
[browser selectRow:1 inColumn:0];
[self assert:[browser._columns[0] selectedCell] equals:[browser selectedCellInColumn:0]];
}
- testSelectedColumn
{
[self assert:-1 equals:[browser selectedColumn]];
[browser selectRow:1 inColumn:0];
[self assert:0 equals:[browser selectedColumn]];
[browser selectRow:1 inColumn:1];
[self assert:1 equals:[browser selectedColumn]];
}
- testSelectingRowsProgrammaticlyWhichDontExistDoesntChangeTheSelection
{
[browser loadColumnZero];
[browser selectRow:1 inColumn:0];
[browser selectRow:-1 inColumn:0];
[browser selectRow:11 inColumn:0];
[browser selectRow:1 inColumn:3];
[browser selectRow:1 inColumn:-1];
[self assert:0 equals:[browser selectedColumn]];
[self assert:1 equals:[browser selectedRowInColumn:0]];
}
- testCanNavigateBrowserWithArrowKeys
{
// -moveDown:
[self assertTrue:[browser acceptsArrowKeys]];
[browser selectRow:0 inColumn:0];
[browser moveDown:nil];
[self assert:1 equals:[browser selectedRowInColumn:0]];
[self assert:0 equals:[browser selectedColumn]];
[browser selectRow:2 inColumn:0];
[browser moveDown:nil];
[self assert:2 equals:[browser selectedRowInColumn:0]];
// -moveUp:
[browser moveUp:nil];
[self assert:1 equals:[browser selectedRowInColumn:0]];
[self assert:0 equals:[browser selectedColumn]];
[browser selectRow:0 inColumn:0];
[browser moveUp:nil];
[self assert:0 equals:[browser selectedRowInColumn:0]];
// -moveBackward:
[browser moveBackward:nil];
[self assert:0 equals:[browser selectedColumn]];
[self assert:0 equals:[browser selectedRowInColumn:0]];
[browser selectRow:0 inColumn:1];
[browser moveBackward:nil];
[self assert:0 equals:[browser selectedColumn]];
[self assert:0 equals:[browser selectedRowInColumn:0]];
// -moveForward:
[browser moveForward:nil];
[self assert:1 equals:[browser selectedColumn]];
[self assert:0 equals:[browser selectedRowInColumn:1]];
[self assert:0 equals:[browser selectedRowInColumn:0] message:"selection in row 0 should stay"];
[browser selectRow:0 inColumn:1];
[browser setLastColumn:1];
[browser moveForward:nil];
[self assert:1 equals:[browser selectedColumn]];
[self assert:0 equals:[browser selectedRowInColumn:1] message:""];
[self assert:0 equals:[browser selectedRowInColumn:0] message:"selection in row 0 should stay"];
}
- testCanIgnoreArrowKeyNavigation
{
[browser setAcceptsArrowKeys:NO];
[browser selectRow:1 inColumn:0];
[browser selectRow:1 inColumn:1];
[self assert:1 equals:[browser selectedColumn]];
[self assert:1 equals:[browser selectedRowInColumn:1]];
[browser moveDown:nil];
[self assert:1 equals:[browser selectedColumn]];
[self assert:1 equals:[browser selectedRowInColumn:1]];
[browser moveUp:nil];
[self assert:1 equals:[browser selectedColumn]];
[self assert:1 equals:[browser selectedRowInColumn:1]];
[browser moveBackward:nil];
[self assert:1 equals:[browser selectedColumn]];
[self assert:1 equals:[browser selectedRowInColumn:1]];
[browser moveForward:nil];
[self assert:1 equals:[browser selectedColumn]];
[self assert:1 equals:[browser selectedRowInColumn:1]];
}
- testClickingOntheBrowserMakesItTheFirstResponder
{
var window = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:null];
[self assertNotNull:window];
[window setContentView:browser];
[browser loadColumnZero];
[[browser _collectionViewInColumn:0] mouseDown:[self clickEventAtPoint:CGPointMakeZero()]];
[self assert:browser same:[window firstResponder]];
// REFACT: GnuStep make the NSMatrix first responder (in -becomeFirstResponder)
// therefore consider making the CPCollectionView first responder
}
// Perhaps send a shift-click event to the cell in question?
// Cocoa does not use shift-click to unselect, but only command-click. Dunno why. May be a bug in Cocoa.
- testCantDeselectSomethingAfterItHasBeenSelectedIfEmtpySelectionsAreDisallowed {}
@end
@implementation CPBrowserTest (CPBrowserColumnTest)
- testBrowserColumnSmoke
{
var column = [[CPBrowserColumn alloc] initWithBrowser:browser];
[self assertNotNull:column];
}
- testColumnCanAccessScrollAndCollectionView
{
var column = [[CPBrowserColumn alloc] initWithBrowser:browser];
[self assertNotNull:[column scrollView]];
[self assertNotNull:[column collectionView]];
}
@end
@implementation CPBrowserTest (CPBrowserViewTest)
- testViewsAreBranchesByDefault
{
var view = [[CPBrowserView alloc] initWithFrame:CGRectMakeZero()];
[self assertFalse:[view isLeaf]];
[view setLeaf:YES];
[self assertTrue:[view isLeaf]];
}
- testViewRetainsRepresentedObject
{
var view = [[CPBrowserView alloc] initWithFrame:CGRectMakeZero()];
[self assertNull:[view representedObject]];
[view setRepresentedObject:self];
[self assert:self same:[view representedObject]];
}
@end
/*
Plan of attack:
- Get one collumn working with a collection or table view
- Start with the collection view as it is a smaller object with less "baggage"
- if it doesn't work out, go with the table view instead
*/
// TODO: browser should test suitability of delegate
// TODO: Implement different types of delegates
// TODO: ensure setNeedsDisplay: is set by all calls that need to set it
// TODO: test -lastColumn
// TODO: test setting the lastColumn with more columns
// TODO: Focus handling - only the column with keyboard focus has the active highlight - the rest is different
// TODO: browser should render empty columns for stuff not needed
// TODO: browser should be able to resize
// ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment