/* * CPTableView.j * AppKit * * Created by Francisco Tolmasky. * Copyright 2008, 280 North, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ @import "CPControl.j" @import "CPTableColumn.j" @import "CPColor.j" @import "CPTextField.j" #define ROW_HEIGHT(aRow) (_hasVariableHeightRows ? _rowHeights[aRow] : _rowHeight) #define ROW_MIN_Y(aRow) (_hasVariableHeightRows ? _rowMinYs[aRow] : (aRow * (_rowHeight + _intercellSpacing.height))) CPTableViewColumnDidMoveNotification = "CPTableViewColumnDidMoveNotification"; CPTableViewColumnDidResizeNotification = "CPTableViewColumnDidResizeNotification"; CPTableViewSelectionDidChangeNotification = "CPTableViewSelectionDidChangeNotification"; CPTableViewSelectionIsChangingNotification = "CPTableViewSelectionIsChangingNotification"; var _CPTableViewWillDisplayCellSelector = 1 << 0, _CPTableViewShouldSelectRowSelector = 1 << 1, _CPTableViewShouldSelectTableColumnSelector = 1 << 2, _CPTableViewSelectionShouldChangeSelector = 1 << 3, _CPTableViewShouldEditTableColumnSelector = 1 << 4, _CPTableViewSelectionIndexesForProposedSelectionSelector = 1 << 5, _CPTableViewHeightOfRowSelector = 1 << 6; /* @ingroup appkit This class is not yet stable. CPTableView is located within the AppKit framework and is used to display tables. It uses a delegate model for getting its data i.e. you give it an object that provides it with the data it should display. @ignore */ @implementation CPTableView : CPControl { // Archived: id _dataSource; id _delegate; //CPTableHeaderView _headerView; //CPView _cornerView; CPArray _tableColumns; CPIndexSet _selectedRowIndexes; //CPArray _selectedColumns; float _rowHeight; CGSize _intercellSpacing; BOOL _allowsMultipleSelection; BOOL _allowsEmptySelection; //BOOL _allowsColumnReordering; //BOOL _allowsColumnResizing; //BOOL _allowsColumnSelection; //BOOL _autoresizesAllColumnsToFit; //CPColor _gridColor; //BOOL _drawsGrid; // Not archived: int _delegateSelectorsCache; //CPScrollView _scrollView; unsigned _numberOfRows; //unsigned _numberOfColumns; BOOL _hasVariableHeightRows; // Heights float _columnHeight; // calculated CPArray _rowHeights; CPArray _rowMinYs; CPArray _tableCells; CPArray _tableColumnViews; // Caching Object _dataViewCache; CPArray _objectValueCache; CPRange _visibleRows; CPRange _visibleColumns; CPRange _populatedRows; CPRange _populatedColumns; // Selection CPIndexSet _previousSelectedRowIndexes; int _selectionStartRow; int _selectionModifier; CPIndexSet _currentlySelected; CPArray _selectionViews; CPArray _selectionViewsPool; CPDate _scrollTimer; } + (void)initialize { } - (id)initWithFrame:(CGRect)aFrame { self = [super initWithFrame:aFrame]; if (self) { [self _init]; } return self; } - (void)_init { _tableColumns = []; //_numberOfColumns = 0; _selectedRowIndexes = [CPIndexSet indexSet]; _rowHeight = 17.0; _intercellSpacing = CPSizeMake(3.0, 2.0); _allowsMultipleSelection = YES; _allowsEmptySelection = YES; _tableCells = []; _tableColumnViews = []; _dataViewCache = {}; _objectValueCache = []; _visibleRows = CPMakeRange(0, 0); _visibleColumns = CPMakeRange(0, 0); _rowHeights = []; _rowMinYs = []; } /* Returns the table's column height */ - (float)_columnHeight { return _columnHeight;//_numberOfRows * (_rowHeight + _intercellSpacing.height); } - (void)newCellForRow:(unsigned)aRowIndex column:(unsigned)aColumnIndex avoidingRows:(CPRange)rows { var dataView = [_tableColumns[aColumnIndex] _newDataViewForRow:aRowIndex avoidingRows:rows]; [dataView setFrame:CGRectMake(0.0, ROW_MIN_Y(aRowIndex), [_tableColumns[aColumnIndex] width], ROW_HEIGHT(aRowIndex))]; if ([dataView respondsToSelector:@selector(highlight:)]) [dataView highlight:[_selectedRowIndexes containsIndex:aRowIndex]]; if (!_objectValueCache[aColumnIndex]) _objectValueCache[aColumnIndex] = []; // We may be storing 0 after all! if (_objectValueCache[aColumnIndex][aRowIndex] === undefined) _objectValueCache[aColumnIndex][aRowIndex] = [_dataSource tableView:self objectValueForTableColumn:_tableColumns[aColumnIndex] row:aRowIndex]; [dataView setObjectValue:_objectValueCache[aColumnIndex][aRowIndex]]; return dataView; } - (void)clearCells { var columnEnd = CPMaxRange(_visibleColumns), rowEnd = CPMaxRange(_visibleRows); for (var column = _visibleColumns.location; column < columnEnd; column++) { var tableColumn = _tableColumns[column], tableColumnCells = _tableCells[column]; for (var row = _visibleRows.location; row < rowEnd; row++) { var cell = tableColumnCells[row]; if (cell) { tableColumnCells[row] = nil; [tableColumn _markView:cell inRow:row asPurgable:YES]; } else { CPLog.warn("Missing cell? " + row + "," + column); } } } _visibleColumns = CPMakeRange(0,0); _visibleRows = CPMakeRange(0,0); } - (void)loadTableCellsInRect:(CGRect)aRect { if (!_dataSource) return; // Determine new visible rows and columns. var rowStart = MAX(0, [self _rowAtY:CGRectGetMinY(aRect)] - 1), rowEnd = MIN(_numberOfRows, [self _rowAtY:CGRectGetMaxY(aRect)] + 1), visibleRows = CPMakeRange(rowStart, rowEnd - rowStart), columnStart = MAX(0, [self _columnAtX:CGRectGetMinX(aRect)]), columnEnd = MIN(_tableColumns.length, [self _columnAtX:CGRectGetMaxX(aRect)] + 1), visibleColumns = CPMakeRange(columnStart, columnEnd - columnStart); if (CPEqualRanges(_visibleRows, visibleRows) && CPEqualRanges(_visibleColumns, visibleColumns)) return; var unionVisibleRows = CPUnionRange(_visibleRows, visibleRows), unionVisibleColumns = CPUnionRange(_visibleColumns, visibleColumns); // Determine whether to use 2 sweeps or one. If we have lots of overlap of cells, use just one. if (unionVisibleRows.length * unionVisibleColumns.length <= (_visibleRows.length * _visibleColumns.length) + (visibleRows.length * visibleColumns.length)) { //CPLog.info("single sweep"); var cEnd = CPMaxRange(unionVisibleColumns), rEnd = CPMaxRange(unionVisibleRows), cell; for (var column = unionVisibleColumns.location; column < cEnd; ++column) { var tableColumn = _tableColumns[column], tableColumnCells = _tableCells[column], columnIsVisible = CPLocationInRange(column, visibleColumns), newCells = []; for (var row = unionVisibleRows.location; row < rEnd; ++row) { if (cell = tableColumnCells[row]) { if (!columnIsVisible || !CPLocationInRange(row, visibleRows)) { tableColumnCells[row] = nil; [tableColumn _markView:cell inRow:row asPurgable:YES]; } } else { newCells.push(row); } } while (newCells.length > 0) { var row = newCells.pop(); tableColumnCells[row] = [self newCellForRow:row column:column avoidingRows:visibleRows]; if (!tableColumnCells[row]._superview) [_tableColumnViews[column] addSubview:tableColumnCells[row]]; else if (tableColumnCells[row]._isHidden) [tableColumnCells[row] setHidden:NO]; } [tableColumn _purge]; } } else { //CPLog.info("double sweep"); // first sweep: remove old cells var cEnd = CPMaxRange(_visibleColumns), rEnd = CPMaxRange(_visibleRows), cell; for (var column = _visibleColumns.location; column < cEnd; ++column) { var tableColumn = _tableColumns[column], tableColumnCells = _tableCells[column], columnIsVisible = CPLocationInRange(column, visibleColumns); for (var row = _visibleRows.location; row < rEnd; ++row) { if (cell = tableColumnCells[row]) { if (!columnIsVisible || !CPLocationInRange(row, visibleRows)) { tableColumnCells[row] = nil; [tableColumn _markView:cell inRow:row asPurgable:YES]; } } } } // second sweep: add new cells var cEnd = CPMaxRange(visibleColumns), rEnd = CPMaxRange(visibleRows); for (var column = visibleColumns.location; column < cEnd; ++column) { var tableColumn = _tableColumns[column], tableColumnCells = _tableCells[column]; for (var row = visibleRows.location; row < rEnd; ++row) { tableColumnCells[row] = [self newCellForRow:row column:column avoidingRows:visibleRows]; if (!tableColumnCells[row]._superview) [_tableColumnViews[column] addSubview:tableColumnCells[row]]; else if (tableColumnCells[row]._isHidden) [tableColumnCells[row] setHidden:NO]; } [tableColumn _purge]; } } _visibleRows = visibleRows; _visibleColumns = visibleColumns; } // Setting display attributes /* Sets the width and height between cell. The default is (3.0, 2.0) (width, height). @param the width and height spacing */ - (void)setIntercellSpacing:(CGSize)aSize { if (_intercellSpacing.width != aSize.width) { var i = 1, delta = aSize.width - _intercellSpacing.width; total = delta; for (; i < _tableColumns.length; ++i, total += delta) { var origin = [_tableColumnViews[i] frame].origin; [_tableColumnViews[i] setFrameOrigin:CGPointMake(origin.x + total, origin.y)]; } } if (_intercellSpacing.height != aSize.height) { var i = 0; for (; i < _tableColumns.length; ++i, total += delta) { [_tableColumnViews[i] setFrameSize:CGSizeMake([_tableColumnViews[i] width], _numberOfRows * (_rowHeight + _intercellSpacing.height))]; var j = 1, y = _rowHeight + _intercellSpacing.height; for (; j < _numberOfRows; ++i, y += _rowHeight + _intercellSpacing.height) { if (!_tableCells[i][j]) continue; [_tableCells[i][j] setFrameOrigin:CPPointMake(0.0, y)]; } } // FIXME: variable height rows } _intercellSpacing = CPSizeCreateCopy(aSize); } /* Returns the width and height of the space between cells. */ - (CGSize)intercellSpacing { return _intercellSpacing; } /* Sets the height of table rows @param aRowHeight the new height of the table rows */ - (void)setRowHeight:(unsigned)aRowHeight { if (_rowHeight == aRowHeight) return; _rowHeight = aRowHeight; // don't perform adjustments if we're using variable height rows if (_hasVariableHeightRows) return; for (var row = 0; row < _numberOfRows; ++row) for (var column = 0; column < _tableColumns.length; ++column) [_tableCells[column][row] setFrameOrigin:CPPointMake(0.0, row * (_rowHeight + _intercellSpacing.height))]; } /* Returns the height of a table row */ - (unsigned)rowHeight { return _rowHeight; } /* Adds a column to the table @param aTableColumn the column to be added */ - (void)addTableColumn:(CPTableColumn)aTableColumn { var i = 0, x = _tableColumns.length ? CPRectGetMaxX([self rectOfColumn:_tableColumns.length - 1]) + _intercellSpacing.width : 0.0, tableColumnView = [[CPView alloc] initWithFrame:CPRectMake(x, 0.0, [aTableColumn width], [self _columnHeight])], tableColumnCells = []; [_tableColumns addObject:aTableColumn]; [_tableColumnViews addObject:tableColumnView]; // [tableColumnView setBackgroundColor:[CPColor greenColor]]; [self addSubview:tableColumnView]; [_tableCells addObject:tableColumnCells]; // TODO: do we really need to initialize this, or is undefined good enough? for (; i < _numberOfRows; ++i) _tableCells[_tableColumns.length-1][i] = nil; //++_numberOfColumns; } /* Removes a column from the table @param aTableColumn the column to remove */ - (void)removeTableColumn:(CPTableColumn)aTableColumn { var frame = [self frame], width = [aTableColumn width] + _intercellSpacing.width, index = [_tableColumns indexOfObjectIdenticalTo:aTableColumn]; // Remove the column view and all the cell views from the view hierarchy. [_tableColumnViews[i] removeFromSuperview]; [_tableCells removeObjectAtIndex:index]; [_tableColumns removeObjectAtIndex:index]; [_tableColumnViews removeObjectAtIndex:index]; // Shift remaining column views to the left. for (; index < _tableColumns.length; ++ index) [_tableColumnViews[index] setFrameOrigin:CPPointMake(CPRectGetMinX([_tableColumnViews[index] frame]) - width, 0.0)] // Resize ourself. [self setFrameSize:CPSizeMake(CPRectGetWidth(frame) - width, CPRectGetHeight(frame))]; } /* Not yet implemented. Changes the position of the column in the table. @param fromIndex the original index of the column @param toIndex the desired index of the column */ - (void)moveColumn:(unsigned)fromIndex toColumn:(unsinged)toIndex { if (fromIndex == toIndex) return; // FIXME: IMPLEMENT } /* Returns an array containing the table's CPTableColumns. */ - (CPArray)tableColumns { return _tableColumns; } /* Returns the first CPTableColumn equal to the given object (as determined by the "isEqual:" selector), or nil if none exists. @param anObject the object to look for */ - (CPTableColumn)tableColumnWithIdentifier:(id)anObject { for (var i = 0; i < _tableColumns.length; i++) if ([_tableColumns[i] isEqual:anObject]) return _tableColumns[i]; return nil; } // Getting the dimensions of the table /* Returns the number of columns in the table */ - (int)numberOfColumns { return _tableColumns.length; } /* Returns the number of rows in the table */ - (int)numberOfRows { return _numberOfRows; } /* Adjusts the size of the table and it's header view. */ - (void)tile { var HEIGHT = 10.0; // [_headerView setFrame:CPRectMake(0.0, 0.0, CPRectGetWidth([self bounds]), HEIGHT)]; // [_scrollView setFrame:CPRectMake(0.0, HEIGHT, CPRectGetWidth([self bounds]), CPRectGetHeight([self bounds]) - HEIGHT)]; } /* Sets the object that is used to obtain the table data. The data source must implement the following methods:
- (int)numberOfRowsInTableView:(CPTableView)aTableView
- (id)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aTableColumn row:(int)rowIndex
@param aDataSource the object with the table data @throws CPInternalInconsistencyException if aDataSource doesn't implement all the required methods */ - (void)setDataSource:(id)aDataSource { if (![aDataSource respondsToSelector:@selector(numberOfRowsInTableView:)]) [CPException raise:CPInternalInconsistencyException reason:"Data source doesn't support 'numberOfRowsInTableView:'"]; if (![aDataSource respondsToSelector:@selector(tableView:objectValueForTableColumn:row:)]) [CPException raise:CPInternalInconsistencyException reason:"Data source doesn't support 'tableView:objectValueForTableColumn:row:'"]; _dataSource = aDataSource; [self reloadData]; } /* Returns the object that has access to the table data */ - (id)dataSource { return _dataSource; } - (id)delegate { return _delegate; } /*! Sets the delegate for the tableview. */ - (void)setDelegate:(id)aDelegate { if (_delegate === aDelegate) return; var notificationCenter = [CPNotificationCenter defaultCenter]; if ([_delegate respondsToSelector:@selector(tableViewColumnDidMove:)]) [notificationCenter removeObserver:_delegate name:CPTableViewColumnDidMoveNotification object:self]; if ([_delegate respondsToSelector:@selector(tableViewColumnDidResize:)]) [notificationCenter removeObserver:_delegate name:CPTableViewColumnDidResizeNotification object:self]; if ([_delegate respondsToSelector:@selector(tableViewSelectionDidChange:)]) [notificationCenter removeObserver:_delegate name:CPTableViewSelectionDidChangeNotification object:self]; if ([_delegate respondsToSelector:@selector(tableViewSelectionIsChanging:)]) [notificationCenter removeObserver:_delegate name:CPTableViewSelectionIsChangingNotification object:self]; _delegate = aDelegate; if ([_delegate respondsToSelector:@selector(tableViewColumnDidMove:)]) [notificationCenter addObserver:_delegate selector:@selector(tableViewColumnDidMove:) name:CPTableViewColumnDidMoveNotification object:self]; if ([_delegate respondsToSelector:@selector(tableViewColumnDidResize:)]) [notificationCenter addObserver:_delegate selector:@selector(tableViewColumnDidResize:) name:CPTableViewColumnDidResizeNotification object:self]; if ([_delegate respondsToSelector:@selector(tableViewSelectionDidChange:)]) [notificationCenter addObserver:_delegate selector:@selector(tableViewSelectionDidChange:) name:CPTableViewSelectionDidChangeNotification object:self]; if ([_delegate respondsToSelector:@selector(tableViewSelectionIsChanging:)]) [notificationCenter addObserver:_delegate selector:@selector(tableViewSelectionIsChanging:) name:CPTableViewSelectionIsChangingNotification object:self]; _delegateSelectorsCache = 0; if ([_delegate respondsToSelector:@selector(tableView:willDisplayCell:forTableColumn:row:)]) _delegateSelectorsCache |= _CPTableViewWillDisplayCellSelector; if ([_delegate respondsToSelector:@selector(tableView:shouldSelectRow:)]) _delegateSelectorsCache |= _CPTableViewShouldSelectRowSelector; if ([_delegate respondsToSelector:@selector(tableView:shouldSelectTableColumn:)]) _delegateSelectorsCache |= _CPTableViewShouldSelectTableColumnSelector; if ([_delegate respondsToSelector:@selector(selectionShouldChangeInTableView:)]) _delegateSelectorsCache |= _CPTableViewSelectionShouldChangeSelector; if ([_delegate respondsToSelector:@selector(tableView:shouldEditTableColumn:row:)]) _delegateSelectorsCache |= _CPTableViewShouldEditTableColumnSelector; if ([_delegate respondsToSelector:@selector(tableView:selectionIndexesForProposedSelection:)]) _delegateSelectorsCache |= _CPTableViewSelectionIndexesForProposedSelectionSelector; if ([_delegate respondsToSelector:@selector(tableView:heightOfRow:)]) { _delegateSelectorsCache |= _CPTableViewHeightOfRowSelector; _hasVariableHeightRows = YES; } else _hasVariableHeightRows = NO; } /* Tells the table view that the number of rows in the table has changed. */ - (void)noteNumberOfRowsChanged { var numberOfRows = [_dataSource numberOfRowsInTableView:self]; if (_numberOfRows != numberOfRows) { _numberOfRows = numberOfRows; [self _recalculateColumnHeight]; } } - (void)noteHeightOfRowsWithIndexesChanged:(CPIndexSet)indexSet { // FIXME: more efficient version is possible since we know which indexes changes [self _recalculateColumnHeight]; } /* Returns the rectangle bounding the specified row. @param aRowIndex the row to obtain a rectangle for @return the bounding rectangle */ - (CGRect)rectOfRow:(int)aRowIndex { return CPRectMake(0.0, ROW_MIN_Y(aRowIndex), CPRectGetWidth([self bounds]), ROW_HEIGHT(aRowIndex)); } /* Returns the rectangle bounding the specified column @param aColumnIndex the column to obtain a rectangle for @return the bounding column */ - (CGRect)rectOfColumn:(int)aColumnIndex { return [_tableColumnViews[aColumnIndex] frame]; } /* Adjusts column widths to make them all visible at once. Same as tile. */ - (void)sizeToFit { // [self tile]; } - (void)_recalculateColumnHeight { var oldColumnHeight = _columnHeight; if (_hasVariableHeightRows) { _rowMinYs[0] = 0; for (var row = 0; row < _numberOfRows; row++) { _rowHeights[row] = [_delegate tableView:self heightOfRow:row]; _rowMinYs[row+1] = _rowMinYs[row] + _rowHeights[row] + _intercellSpacing.height; } _columnHeight = _rowMinYs[_numberOfRows]; // last index is one more than last row, and is the total column height } else _columnHeight = _numberOfRows * (_rowHeight + _intercellSpacing.height); var count = _tableColumnViews.length; while (count--) [_tableColumnViews[count] setFrameSize:CGSizeMake([_tableColumns[count] width], _columnHeight)]; [self setFrameSize:CGSizeMake(CGRectGetWidth([self frame]), _columnHeight)]; } - (CGRect)visibleRectInParent { var superview = [self superview]; if (!superview) return [self bounds]; return [self convertRect:CGRectIntersection([superview bounds], [self frame]) fromView:superview]; } /* Reloads the data from the dataSource. This is an expensive method, so use it lightly. */ - (void)reloadData { var oldNumberOfRows = _numberOfRows; _numberOfRows = [_dataSource numberOfRowsInTableView:self]; if (oldNumberOfRows != _numberOfRows) { [self _recalculateColumnHeight]; [self setFrameSize:CGSizeMake(CGRectGetWidth([self frame]), [self _columnHeight])]; } _objectValueCache = []; [self clearCells]; [self setNeedsLayout]; } - (void)layoutSubviews { [self loadTableCellsInRect:[self visibleRectInParent]]; } - (void)displaySoon { [_scrollTimer invalidate]; _scrollTimer = [CPTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(displayNow) userInfo:nil repeats:NO]; } - (void)displayNow { [self setNeedsLayout]; } - (void)viewDidMoveToSuperview { [[[self enclosingScrollView] contentView] setPostsBoundsChangedNotifications:YES]; [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(viewBoundsChanged:) name:CPViewBoundsDidChangeNotification object:[[self enclosingScrollView] contentView]]; } - (void)viewBoundsChanged:(CPNotification)aNotification { //CPLog.info(_cmd + CPStringFromRect([[[self enclosingScrollView] contentView] bounds])); //objj_debug_print_backtrace(); //[self setNeedsLayout]; [self displayNow]; } /* - (void)setAllowsColumnReordering:(BOOL)allowsColumnReordering { if (_allowsColumnReordering === _allowsColumnReordering) return; _allowsColumnReordering = allowsColumnReordering; } - (void)allowsColumnReordering { return _allowsColumnReordering; } - (void)setAllowsColumnResizing:(BOOL)allowsColumnResizing { if (_allowsColumnResizing === allowsColumnResizing) return; _allowsColumnResizing = allowsColumnResizing; } - (void)allowsColumnResizing { return _allowsColumnResizing; } - (void)setAllowsColumnSelection:(BOOL)allowsColumnSelection { if (_allowsColumnSelection === allowsColumnSelection) return; _allowsColumnSelection = allowsColumnSelection; } - (void)allowsColumnSelection { return _allowsColumnSelection; } */ - (void)setAllowsMultipleSelection:(BOOL)allowsMultipleSelection { if (_allowsMultipleSelection === allowsMultipleSelection) return; _allowsMultipleSelection = allowsMultipleSelection; // TODO: more stuff? } - (void)allowsMultipleSelection { return _allowsMultipleSelection; } - (void)setAllowsEmptySelection:(BOOL)allowsEmptySelection { if (_allowsEmptySelection === allowsEmptySelection) return; _allowsEmptySelection = allowsEmptySelection; } - (void)allowsEmptySelection { return _allowsEmptySelection; } /* Returns the index of the row at the given point, or CPNotFound (-1) if it is out of range. @param aPoint the point @return the index of the row at aPoint */ - (int)rowAtPoint:(CGPoint)aPoint { var index = [self _rowAtY:aPoint.y] if (index >= 0 && index < _numberOfRows) return index; else return CPNotFound; } - (int)columnAtPoint:(CGPoint)aPoint { var index = [self _columnAtX:aPoint.x] if (index >= 0 && index < _tableColumns.length) return index; else return CPNotFound; } /* @ignore Internal version takes a Y value, returns an index, or -1 if its beyond the min, or numberOfRows if it's beyond the max */ - (int)_rowAtY:(float)y { if (_hasVariableHeightRows) { var a = 0, b = _numberOfRows; if (y < _rowMinYs[0]) return -1; if (y >= _rowMinYs[_rowMinYs.length-1]) return _numberOfRows; // binary search while (true) { var half = a + Math.floor((b - a) / 2); if (y < _rowMinYs[half]) b = half; else if (half < _numberOfRows-1 && y >= _rowMinYs[half+1]) a = half; else return half; } } else return FLOOR(y / (_rowHeight + _intercellSpacing.height)); } /* @ignore Internal version takes a X value, returns an index, or -1 if its beyond the min, or numberOfColumns if it's beyond the max */ - (int)_columnAtX:(float)x { var a = 0, b = _tableColumns.length; var last = [_tableColumnViews[_tableColumns.length-1] frame]; if (x < [_tableColumnViews[0] frame].origin.x) return -1; if (x >= last.origin.x + last.size.width) return _tableColumns.length; // binary search while (true) { var half = a + Math.floor((b - a) / 2); if (x < [_tableColumnViews[half] frame].origin.x) b = half; else if (half < _tableColumns.length-1 && x >= [_tableColumnViews[half+1] frame].origin.x) a = half; else return half; } } /* Selects the specified row indexes, optionally adding to existing selection @param indexes the indexes to select @param extend whether or not to add to the existing selection */ - (void)selectRowIndexes:(CPIndexSet)indexes byExtendingSelection:(BOOL)extend { // FIXME: should this be subject to the delegate filters, etc? if (extend) _selectedRowIndexes = [[_selectedRowIndexes copy] addIndexes:indexes]; else if ([indexes count] > 0 || _allowsEmptySelection) _selectedRowIndexes = [indexes copy]; [self _drawSelection]; } /* Returns a CPIndexSet of the selected rows @return indexes of the selected rows */ - (CPIndexSet)selectedRowIndexes { return _selectedRowIndexes; } /* Returns the number of selected rows @return number of selected rows */ - (int)numberOfSelectedRows { return [_selectedRowIndexes count]; } /* Deselects all rows if allowsEmptySelection is true. If delegate responds to "selectionShouldChangeInTableView:", asks if it should chnage. Sends the CPTableViewSelectionDidChangeNotification on deselection. @param the sender in a target/action */ - (void)deselectAll:(id)sender { if (!_allowsEmptySelection || [_selectedRowIndexes count] === 0 || ((_delegateSelectorsCache & _CPTableViewSelectionShouldChangeSelector) && ![_delegate selectionShouldChangeInTableView:self])) return; [self selectRowIndexes:[CPIndexSet indexSet] byExtendingSelection:NO]; [[CPNotificationCenter defaultCenter] postNotificationName:CPTableViewSelectionDidChangeNotification object:self userInfo:nil]; } - (void)editColumn:(int)columnIndex row:(int)rowIndex withEvent:(CPEvent)theEvent select:(BOOL)flag { } /* @ignore */ - (void)_updateSelectionWithMouseAtRow:(int)aRow { // Make a preliminary new selection var newSelection; if (_allowsMultipleSelection) newSelection = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(MIN(aRow, _selectionStartRow), ABS(aRow-_selectionStartRow)+1)]; else if (aRow >= 0 && aRow < _numberOfRows) newSelection = [CPIndexSet indexSetWithIndex:aRow]; else newSelection = [CPIndexSet indexSet]; // If cmd/ctrl was held down XOR the old selection with the proposed selection if (_allowsMultipleSelection && _selectionModifier & (CPCommandKeyMask | CPControlKeyMask | CPAlternateKeyMask)) { // A = newSelection, B = _previousSelectedRowIndexes // (A intersection B) = (A - (A - B)) var intersection = [newSelection copy], difference = [newSelection copy]; [difference removeIndexes:_previousSelectedRowIndexes]; [intersection removeIndexes:difference] // (A xor B) = (A + B) - (A intersection B) [newSelection addIndexes:_previousSelectedRowIndexes]; [newSelection removeIndexes:intersection]; // FIXME: if multiple selection is off, and we cmd/ctrl click the previously selected row, then deselect it. } // if the new selection is different than the old selection if (![newSelection isEqualToIndexSet:_selectedRowIndexes]) { // ask the delegate if we should change the selection if ((_delegateSelectorsCache & _CPTableViewSelectionShouldChangeSelector) && ![_delegate selectionShouldChangeInTableView:self]) return; // ask the delegate which indexes can be selected. selectionIndexesForProposedSelection is faster than shouldSelectRow if (_delegateSelectorsCache & _CPTableViewSelectionIndexesForProposedSelectionSelector) newSelection = [_delegate tableView:self selectionIndexesForProposedSelection:newSelection]; else if (_delegateSelectorsCache & _CPTableViewShouldSelectRowSelector) { var indexes = []; [newSelection getIndexes:indexes maxCount:Number.MAX_VALUE inIndexRange:nil]; for (var i = 0; i < indexes.length; i++) if (![_delegate tableView:self shouldSelectRow:indexes[i]]) [newSelection removeIndex:indexes[i]]; } } // if empty selection is not allowed and the new selection has nothing selected, abort if (!_allowsEmptySelection && [newSelection count] === 0) return; // if the new selection is *still* different, and update the selection and send a notification if (![newSelection isEqualToIndexSet:_selectedRowIndexes]) { [self selectRowIndexes:newSelection byExtendingSelection:NO]; [[CPNotificationCenter defaultCenter] postNotificationName:CPTableViewSelectionIsChangingNotification object:self userInfo:nil]; } } /* @ignore */ - (void)mouseDown:(CPEvent)anEvent { [self trackSelection:anEvent]; } /* Sets the message to be sent to the target when a cell is double clicked @param aSelector the selector to be performed */ - (void)setDoubleAction:(SEL)aSelector { _doubleAction = aSelector; } - (SEL)doubleAction { return _doubleAction; } - (int)clickedColumn { return _clickedColumn; } - (int)clickedRow { return _clickedRow; } /* @ignore */ - (void)trackSelection:(CPEvent)anEvent { var type = [anEvent type], point = [self convertPoint:[anEvent locationInWindow] fromView:nil], currentRow = MAX(0, MIN(_numberOfRows-1, [self _rowAtY:point.y])); if (type == CPLeftMouseUp) { _clickedRow = [self rowAtPoint:point]; _clickedColumn = [self columnAtPoint:point]; if ([anEvent clickCount] === 2) { CPLog.warn("edit?!"); [self sendAction:_doubleAction to:_target]; } else { if (![_previousSelectedRowIndexes isEqualToIndexSet:_selectedRowIndexes]) { [[CPNotificationCenter defaultCenter] postNotificationName:CPTableViewSelectionDidChangeNotification object:self userInfo:nil]; } [self sendAction:_action to:_target]; } return; } if (type == CPLeftMouseDown) { _previousSelectedRowIndexes = _selectedRowIndexes; _selectionModifier = [anEvent modifierFlags]; if (_selectionModifier & CPShiftKeyMask) _selectionStartRow = (ABS([_previousSelectedRowIndexes firstIndex] - currentRow) < ABS([_previousSelectedRowIndexes lastIndex] - currentRow)) ? [_previousSelectedRowIndexes firstIndex] : [_previousSelectedRowIndexes lastIndex]; else _selectionStartRow = currentRow; [self _updateSelectionWithMouseAtRow:currentRow]; } else if (type == CPLeftMouseDragged) { [self _updateSelectionWithMouseAtRow:currentRow]; } [CPApp setTarget:self selector:@selector(trackSelection:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES]; } /* @ignore */ - (void)_drawSelection { if (!_currentlySelected) { _currentlySelected = [CPIndexSet indexSet]; _selectionViews = []; _selectionViewsPool = []; } // TODO: we could also remove selections that aren't visible, but then we'll need to run this on every scroll/resize? // get array of indexes we can remove var removeSet = [_currentlySelected copy], indexesToRemove = []; [removeSet removeIndexes:_selectedRowIndexes]; [removeSet getIndexes:indexesToRemove maxCount:Number.MAX_VALUE inIndexRange:nil]; // get array of indexes we need to add var addSet = [_selectedRowIndexes copy], indexesToAdd = []; [addSet removeIndexes:_currentlySelected]; [addSet getIndexes:indexesToAdd maxCount:Number.MAX_VALUE inIndexRange:nil]; for (var i = 0; i < indexesToRemove.length; i++) { var row = indexesToRemove[i]; for (var column = 0; column < _tableColumns.length; column++) if ([_tableCells[column][row] respondsToSelector:@selector(highlight:)]) [_tableCells[column][row] highlight:NO]; } for (var i = 0; i < indexesToAdd.length; i++) { var row = indexesToAdd[i]; for (var column = 0; column < _tableColumns.length; column++) if ([_tableCells[column][row] respondsToSelector:@selector(highlight:)]) [_tableCells[column][row] highlight:YES]; } // add each one we need to add, taking the selection views from removed seelctions, the pool, or new for (var i = 0; i < indexesToAdd.length; i++) { var index = indexesToAdd[i], view; if (indexesToRemove.length > 0) { view = _selectionViews[indexesToRemove.pop()]; } else if (_selectionViewsPool.length > 0) { view = _selectionViewsPool.pop(); [self addSubview:view positioned:CPWindowBelow relativeTo:nil]; } else { view = [[CPView alloc] init]; [view setBackgroundColor:[CPColor alternateSelectedControlColor]]; [self addSubview:view positioned:CPWindowBelow relativeTo:nil]; } _selectionViews[index] = view; var frame = [self rectOfRow:index]; frame.size.height += _intercellSpacing.height - 1; //frame.size.width += 500; [view setFrame:frame]; } // remove any selections that weren't already reused for (var i = 0; i < indexesToRemove.length; i++) { var row = indexesToRemove[i], view = _selectionViews[row]; [view removeFromSuperview]; _selectionViewsPool.push(view); } // update the currently selected index set _currentlySelected = [_selectedRowIndexes copy]; } @end var CPTableViewDataSourceKey = @"CPTableViewDataSourceKey", CPTableViewDelegateKey = @"CPTableViewDelegateKey", CPTableViewHeaderViewKey = @"CPTableViewHeaderViewKey", CPTableViewTableColumnsKey = @"CPTableViewTableColumnsKey", CPTableViewRowHeightKey = @"CPTableViewRowHeightKey", CPTableViewIntercellSpacingKey = @"CPTableViewIntercellSpacingKey", CPTableViewMultipleSelectionKey = @"CPTableViewMultipleSelectionKey", CPTableViewEmptySelectionKey = @"CPTableViewEmptySelectionKey"; @implementation CPTableView (CPCoding) - (id)initWithCoder:(CPCoder)aCoder { if (self = [super initWithCoder:aCoder]) { [self _init]; _dataSource = [aCoder decodeObjectForKey:CPTableViewDataSourceKey]; _delegate = [aCoder decodeObjectForKey:CPTableViewDelegateKey]; _rowHeight = [aCoder decodeFloatForKey:CPTableViewRowHeightKey]; _intercellSpacing = [aCoder decodeSizeForKey:CPTableViewIntercellSpacingKey]; _allowsMultipleSelection = [aCoder decodeBoolForKey:CPTableViewMultipleSelectionKey]; _allowsEmptySelection = [aCoder decodeBoolForKey:CPTableViewEmptySelectionKey]; var tableColumns = [aCoder decodeObjectForKey:CPTableViewTableColumnsKey]; for (var i = 0; i < tableColumns.length; i++) [self addTableColumn:tableColumns[i]]; } return self; } - (void)encodeWithCoder:(CPCoder)aCoder { [super encodeWithCoder:aCoder]; [aCoder encodeObject:_dataSource forKey:CPTableViewDataSourceKey]; [aCoder encodeObject:_delegate forKey:CPTableViewDelegateKey]; [aCoder encodeObject:_tableColumns forKey:CPTableViewTableColumnsKey]; [aCoder encodeFloat:_rowHeight forKey:CPTableViewRowHeightKey]; [aCoder encodeSize:_intercellSpacing forKey:CPTableViewIntercellSpacingKey]; [aCoder encodeBool:_allowsMultipleSelection forKey:CPTableViewMultipleSelectionKey]; [aCoder encodeBool:_allowsEmptySelection forKey:CPTableViewEmptySelectionKey]; } @end @implementation CPColor (TableView) + (CPColor)alternateSelectedControlColor { return [[CPColor alloc] _initWithRGBA:[0.22, 0.46, 0.84, 1.0]]; } + (CPColor)secondarySelectedControlColor { return [[CPColor alloc] _initWithRGBA:[0.83, 0.83, 0.83, 1.0]]; } @end