Created
May 13, 2009 14:27
-
-
Save anonymous/111048 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2009 agile42 GmbH - All rights reserved | |
* | |
* Authors: | |
* - Martin Häcker <[email protected]> | |
*/ | |
// 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2009 agile42 GmbH - All rights reserved | |
* | |
* Authors: | |
* - Martin Häcker <[email protected]> | |
*/ | |
@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