/* * CPTableView.j * AppKit * * Created by Francisco Tolmasky. * Copyright 2009, 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 @import @import "CPControl.j" @import "CPTableColumn.j" @import "_CPCornerView.j" @import "CPScroller.j" CPTableViewColumnDidMoveNotification = @"CPTableViewColumnDidMoveNotification"; CPTableViewColumnDidResizeNotification = @"CPTableViewColumnDidResizeNotification"; CPTableViewSelectionDidChangeNotification = @"CPTableViewSelectionDidChangeNotification"; CPTableViewSelectionIsChangingNotification = @"CPTableViewSelectionIsChangingNotification"; #include "CoreGraphics/CGGeometry.h" var CPTableViewDataSource_numberOfRowsInTableView_ = 1 << 0, CPTableViewDataSource_tableView_objectValueForTableColumn_row_ = 1 << 1, CPTableViewDataSource_tableView_setObjectValue_forTableColumn_row_ = 1 << 2, CPTableViewDataSource_tableView_acceptDrop_row_dropOperation_ = 1 << 3, CPTableViewDataSource_tableView_namesOfPromisedFilesDroppedAtDestination_forDraggedRowsWithIndexes_ = 1 << 4, CPTableViewDataSource_tableView_validateDrop_proposedRow_proposedDropOperation_ = 1 << 5, CPTableViewDataSource_tableView_writeRowsWithIndexes_toPasteboard_ = 1 << 6, CPTableViewDataSource_tableView_sortDescriptorsDidChange_ = 1 << 7; var CPTableViewDelegate_selectionShouldChangeInTableView_ = 1 << 0, CPTableViewDelegate_tableView_dataViewForTableColumn_row_ = 1 << 1, CPTableViewDelegate_tableView_didClickTableColumn_ = 1 << 2, CPTableViewDelegate_tableView_didDragTableColumn_ = 1 << 3, CPTableViewDelegate_tableView_heightOfRow_ = 1 << 4, CPTableViewDelegate_tableView_isGroupRow_ = 1 << 5, CPTableViewDelegate_tableView_mouseDownInHeaderOfTableColumn_ = 1 << 6, CPTableViewDelegate_tableView_nextTypeSelectMatchFromRow_toRow_forString_ = 1 << 7, CPTableViewDelegate_tableView_selectionIndexesForProposedSelection_ = 1 << 8, CPTableViewDelegate_tableView_shouldEditTableColumn_row_ = 1 << 9, CPTableViewDelegate_tableView_shouldSelectRow_ = 1 << 10, CPTableViewDelegate_tableView_shouldSelectTableColumn_ = 1 << 11, CPTableViewDelegate_tableView_shouldShowViewExpansionForTableColumn_row_ = 1 << 12, CPTableViewDelegate_tableView_shouldTrackView_forTableColumn_row_ = 1 << 13, CPTableViewDelegate_tableView_shouldTypeSelectForEvent_withCurrentSearchString_ = 1 << 14, CPTableViewDelegate_tableView_toolTipForView_rect_tableColumn_row_mouseLocation_ = 1 << 15, CPTableViewDelegate_tableView_typeSelectStringForTableColumn_row_ = 1 << 16, CPTableViewDelegate_tableView_willDisplayView_forTableColumn_row_ = 1 << 17, CPTableViewDelegate_tableViewSelectionDidChange_ = 1 << 18, CPTableViewDelegate_tableViewSelectionIsChanging_ = 1 << 19; //CPTableViewDraggingDestinationFeedbackStyles CPTableViewDraggingDestinationFeedbackStyleNone = -1; CPTableViewDraggingDestinationFeedbackStyleRegular = 0; CPTableViewDraggingDestinationFeedbackStyleSourceList = 1; //CPTableViewDropOperations CPTableViewDropOn = 0; CPTableViewDropAbove = 1; CPSourceListGradient = "CPSourceListGradient"; CPSourceListTopLineColor = "CPSourceListTopLineColor"; CPSourceListBottomLineColor = "CPSourceListBottomLineColor"; // TODO: add docs CPTableViewSelectionHighlightStyleNone = -1; CPTableViewSelectionHighlightStyleRegular = 0; CPTableViewSelectionHighlightStyleSourceList = 1; CPTableViewGridNone = 0; CPTableViewSolidVerticalGridLineMask = 1 << 0; CPTableViewSolidHorizontalGridLineMask = 1 << 1; CPTableViewNoColumnAutoresizing = 0; CPTableViewUniformColumnAutoresizingStyle = 1; // FIX ME: This is FUBAR CPTableViewSequentialColumnAutoresizingStyle = 2; CPTableViewReverseSequentialColumnAutoresizingStyle = 3; CPTableViewLastColumnOnlyAutoresizingStyle = 4; CPTableViewFirstColumnOnlyAutoresizingStyle = 5; #define NUMBER_OF_COLUMNS() (_tableColumns.length) #define UPDATE_COLUMN_RANGES_IF_NECESSARY() if (_dirtyTableColumnRangeIndex !== CPNotFound) [self _recalculateTableColumnRanges]; @implementation _CPTableDrawView : CPView { CPTableView _tableView; } - (id)initWithTableView:(CPTableView)aTableView { self = [super init]; if (self) _tableView = aTableView; return self; } - (void)drawRect:(CGRect)aRect { var frame = [self frame], context = [[CPGraphicsContext currentContext] graphicsPort]; CGContextTranslateCTM(context, -_CGRectGetMinX(frame), -_CGRectGetMinY(frame)); [_tableView _drawRect:aRect]; } @end /*! @ingroup appkit @class CPTableView CPTableView object displays record-oriented data in a table and allows the user to edit values and resize and rearrange columns. A CPTableView requires you to set a dataSource which implements numberOfRowsInTableView: and tableView:objectValueForTableColumn:row: */ @implementation CPTableView : CPControl { id _dataSource; CPInteger _implementedDataSourceMethods; id _delegate; CPInteger _implementedDelegateMethods; CPArray _tableColumns; CPArray _tableColumnRanges; CPInteger _dirtyTableColumnRangeIndex; CPInteger _numberOfHiddenColumns; BOOL _reloadAllRows; Object _objectValues; CPIndexSet _exposedRows; CPIndexSet _exposedColumns; Object _dataViewsForTableColumns; Object _cachedDataViews; //Configuring Behavior BOOL _allowsColumnReordering; BOOL _allowsColumnResizing; BOOL _allowsColumnSelection; BOOL _allowsMultipleSelection; BOOL _allowsEmptySelection; CPArray _sortDescriptors; //Setting Display Attributes CGSize _intercellSpacing; float _rowHeight; BOOL _usesAlternatingRowBackgroundColors; CPArray _alternatingRowBackgroundColors; unsigned _selectionHighlightStyle; CPTableColumn _currentHighlightedTableColumn; unsigned _gridStyleMask; unsigned _numberOfRows; CPTableHeaderView _headerView; _CPCornerView _cornerView; CPIndexSet _selectedColumnIndexes; CPIndexSet _selectedRowIndexes; CPInteger _selectionAnchorRow; CPInteger _lastSelectedRow; CPIndexSet _previouslySelectedRowIndexes; CGPoint _startTrackingPoint; CPDate _startTrackingTimestamp; BOOL _trackingPointMovedOutOfClickSlop; CGPoint _editingCellIndex; _CPTableDrawView _tableDrawView; SEL _doubleAction; CPInteger _clickedRow; unsigned _columnAutoResizingStyle; int _lastTrackedRowIndex; CGPoint _originalMouseDownPoint; BOOL _verticalMotionCanDrag; unsigned _destinationDragStyle; BOOL _isSelectingSession; CPIndexSet _draggedRowIndexes; _CPDropOperationDrawingView _dropOperationFeedbackView; CPDragOperation _dragOperationDefaultMask; int _retargetedDropRow; CPDragOperation _retargetedDropOperation; BOOL _disableAutomaticResizing @accessors(property=disableAutomaticResizing); BOOL _lastColumnShouldSnap; int _draggedColumnIndex; CPArray _differedColumnDataToRemove; /* CPGradient _sourceListInactiveGradient; CPColor _sourceListInactiveTopLineColor; CPColor _sourceListInactiveBottomLineColor; */ } + (CPString)themeClass { return @"tableview"; } + (id)themeAttributes { return [CPDictionary dictionaryWithObjects:[[CPNull null], [CPNull null], [CPNull null], [CPNull null], [CPNull null], [CPNull null], [CPNull null]] forKeys:["alternating-row-colors", "grid-color", "highlighted-grid-color", "selection-color", "sourcelist-selection-color", "sort-image", "sort-image-reversed"]]; } - (id)initWithFrame:(CGRect)aFrame { self = [super initWithFrame:aFrame]; if (self) { //Configuring Behavior _allowsColumnReordering = YES; _allowsColumnResizing = YES; _allowsMultipleSelection = NO; _allowsEmptySelection = YES; _allowsColumnSelection = NO; _disableAutomaticResizing = NO; //Setting Display Attributes _selectionHighlightStyle = CPTableViewSelectionHighlightStyleRegular; [self setUsesAlternatingRowBackgroundColors:NO]; [self setAlternatingRowBackgroundColors: [[CPColor whiteColor], [CPColor colorWithRed:245.0 / 255.0 green:249.0 / 255.0 blue:252.0 / 255.0 alpha:1.0]]]; _tableColumns = []; _tableColumnRanges = []; _dirtyTableColumnRangeIndex = CPNotFound; _numberOfHiddenColumns = 0; _intercellSpacing = _CGSizeMake(0.0, 0.0); _rowHeight = 23.0; [self setGridColor:[CPColor colorWithHexString:@"dce0e2"]]; [self setGridStyleMask:CPTableViewGridNone]; _headerView = [[CPTableHeaderView alloc] initWithFrame:CGRectMake(0, 0, [self bounds].size.width, _rowHeight)]; [_headerView setTableView:self]; _cornerView = nil; //[[_CPCornerView alloc] initWithFrame:CGRectMake(0, 0, [CPScroller scrollerWidth], CGRectGetHeight([_headerView frame]))]; _lastSelectedRow = -1; _currentHighlightedTableColumn = nil; _sortDescriptors = [CPArray array]; _draggedRowIndexes = [CPIndexSet indexSet]; _verticalMotionCanDrag = YES; _isSelectingSession = NO; _retargetedDropRow = nil; _retargetedDropOperation = nil; _dragOperationDefaultMask = nil; _destinationDragStyle = CPTableViewDraggingDestinationFeedbackStyleRegular; [self setBackgroundColor:[CPColor whiteColor]]; [self _init]; } return self; } // FIX ME: we have a lot of redundent init stuff in initWithFrame: and initWithCoder: we should move it all into here. - (void)_init { _tableViewFlags = 0; _selectedColumnIndexes = [CPIndexSet indexSet]; _selectedRowIndexes = [CPIndexSet indexSet]; _dropOperationFeedbackView = [[_CPDropOperationDrawingView alloc] initWithFrame:_CGRectMakeZero()]; [_dropOperationFeedbackView setTableView:self]; _lastColumnShouldSnap = NO; if (!_alternatingRowBackgroundColors) _alternatingRowBackgroundColors = [[CPColor whiteColor], [CPColor colorWithHexString:@"e4e7ff"]]; _selectionHighlightColor = [CPColor colorWithHexString:@"5f83b9"]; _tableColumnRanges = []; _dirtyTableColumnRangeIndex = 0; _numberOfHiddenColumns = 0; _objectValues = { }; _dataViewsForTableColumns = { }; _dataViews= []; _numberOfRows = 0; _exposedRows = [CPIndexSet indexSet]; _exposedColumns = [CPIndexSet indexSet]; _cachedDataViews = { }; _tableDrawView = [[_CPTableDrawView alloc] initWithTableView:self]; [_tableDrawView setBackgroundColor:[CPColor clearColor]]; [self addSubview:_tableDrawView]; if (!_headerView) _headerView = [[CPTableHeaderView alloc] initWithFrame:CGRectMake(0, 0, [self bounds].size.width, _rowHeight)]; [_headerView setTableView:self]; if (!_cornerView) _cornerView = [[_CPCornerView alloc] initWithFrame:CGRectMake(0, 0, [CPScroller scrollerWidth], CGRectGetHeight([_headerView frame]))]; _draggedColumnIndex = -1; // Gradients for the source list _sourceListActiveGradient = CGGradientCreateWithColorComponents(CGColorSpaceCreateDeviceRGB(), [89.0/255.0, 153.0/255.0, 209.0/255.0,1.0, 33.0/255.0, 94.0/255.0, 208.0/255.0,1.0], [0,1], 2); _sourceListActiveTopLineColor = [CPColor colorWithCalibratedRed:(61.0/255.0) green:(123.0/255.0) blue:(218.0/255.0) alpha:1.0]; _sourceListActiveBottomLineColor = [CPColor colorWithCalibratedRed:(31.0/255.0) green:(92.0/255.0) blue:(207.0/255.0) alpha:1.0]; /* //gradients for the source list when CPTableView is NOT first responder or the window is NOT key // FIX ME: we need to actually implement this. _sourceListInactiveGradient = CGGradientCreateWithColorComponents(CGColorSpaceCreateDeviceRGB(), [168.0/255.0,183.0/255.0,205.0/255.0,1.0,157.0/255.0,174.0/255.0,199.0/255.0,1.0], [0,1], 2); _sourceListInactiveTopLineColor = [CPColor colorWithCalibratedRed:(173.0/255.0) green:(187.0/255.0) blue:(209.0/255.0) alpha:1.0]; _sourceListInactiveBottomLineColor = [CPColor colorWithCalibratedRed:(150.0/255.0) green:(161.0/255.0) blue:(183.0/255.0) alpha:1.0];*/ _differedColumnDataToRemove = [ ]; } /*! Sets the receiver's data source to a given object. @param anObject The data source for the receiver. The object must implement the appropriate methods. */ - (void)setDataSource:(id)aDataSource { if (_dataSource === aDataSource) return; _dataSource = aDataSource; _implementedDataSourceMethods = 0; if (!_dataSource) return; var hasContentBinding = !![self infoForBinding:@"content"]; if ([_dataSource respondsToSelector:@selector(numberOfRowsInTableView:)]) _implementedDataSourceMethods |= CPTableViewDataSource_numberOfRowsInTableView_; else if (!hasContentBinding) [CPException raise:CPInternalInconsistencyException reason:[aDataSource description] + " does not implement numberOfRowsInTableView:."]; if ([_dataSource respondsToSelector:@selector(tableView:objectValueForTableColumn:row:)]) _implementedDataSourceMethods |= CPTableViewDataSource_tableView_objectValueForTableColumn_row_; else if (!hasContentBinding) [CPException raise:CPInternalInconsistencyException reason:[aDataSource description] + " does not implement tableView:objectValueForTableColumn:row:"]; if ([_dataSource respondsToSelector:@selector(tableView:setObjectValue:forTableColumn:row:)]) _implementedDataSourceMethods |= CPTableViewDataSource_tableView_setObjectValue_forTableColumn_row_; if ([_dataSource respondsToSelector:@selector(tableView:acceptDrop:row:dropOperation:)]) _implementedDataSourceMethods |= CPTableViewDataSource_tableView_acceptDrop_row_dropOperation_; if ([_dataSource respondsToSelector:@selector(tableView:namesOfPromisedFilesDroppedAtDestination:forDraggedRowsWithIndexes:)]) _implementedDataSourceMethods |= CPTableViewDataSource_tableView_namesOfPromisedFilesDroppedAtDestination_forDraggedRowsWithIndexes_; if ([_dataSource respondsToSelector:@selector(tableView:validateDrop:proposedRow:proposedDropOperation:)]) _implementedDataSourceMethods |= CPTableViewDataSource_tableView_validateDrop_proposedRow_proposedDropOperation_; if ([_dataSource respondsToSelector:@selector(tableView:writeRowsWithIndexes:toPasteboard:)]) _implementedDataSourceMethods |= CPTableViewDataSource_tableView_writeRowsWithIndexes_toPasteboard_; if ([_dataSource respondsToSelector:@selector(tableView:sortDescriptorsDidChange:)]) _implementedDataSourceMethods |= CPTableViewDataSource_tableView_sortDescriptorsDidChange_; [self reloadData]; } /*! Returns the object that provides the data displayed by the receiver. */ - (id)dataSource { return _dataSource; } //Loading Data /*! Reloads the data for only the specified rows and columns. @param rowIndexes The indexes of the rows to update. @param columnIndexes The indexes of the columns to update. */ - (void)reloadDataForRowIndexes:(CPIndexSet)rowIndexes columnIndexes:(CPIndexSet)columnIndexes { [self reloadData]; // [_previouslyExposedRows removeIndexes:rowIndexes]; // [_previouslyExposedColumns removeIndexes:columnIndexes]; } /*! Reloads the data for all rows and columns. */ - (void)reloadData { //if (!_dataSource) // return; _reloadAllRows = YES; _objectValues = { }; // This updates the size too. [self noteNumberOfRowsChanged]; [self setNeedsLayout]; [self setNeedsDisplay:YES]; } //Target-action Behavior /*! Sets the message sent to the target when the user double-clicks an uneditable cell or a column header to a given selector. @param aSelector The message the receiver sends to its target when the user double-clicks an uneditable cell or a column header. */ - (void)setDoubleAction:(SEL)anAction { _doubleAction = anAction; } - (SEL)doubleAction { return _doubleAction; } /* * - clickedColumn */ /*! Returns the index of the the row the user clicked to trigger an action, or -1 if no row was clicked. */ - (CPInteger)clickedRow { return _clickedRow; } //Configuring Behavior - (void)setAllowsColumnReordering:(BOOL)shouldAllowColumnReordering { _allowsColumnReordering = !!shouldAllowColumnReordering; } - (BOOL)allowsColumnReordering { return _allowsColumnReordering; } - (void)setAllowsColumnResizing:(BOOL)shouldAllowColumnResizing { _allowsColumnResizing = !!shouldAllowColumnResizing; } - (BOOL)allowsColumnResizing { return _allowsColumnResizing; } /*! Controls whether the user can select more than one row or column at a time. @param aFlag YES to allow the user to select multiple rows or columns, otherwise NO. */ - (void)setAllowsMultipleSelection:(BOOL)shouldAllowMultipleSelection { _allowsMultipleSelection = !!shouldAllowMultipleSelection; } - (BOOL)allowsMultipleSelection { return _allowsMultipleSelection; } /*! Controls whether the receiver allows zero rows or columns to be selected. @param aFlag YES if an empty selection is allowed, otherwise NO. */ - (void)setAllowsEmptySelection:(BOOL)shouldAllowEmptySelection { _allowsEmptySelection = !!shouldAllowEmptySelection; } - (BOOL)allowsEmptySelection { return _allowsEmptySelection; } /*! Controls whether the user can select an entire column by clicking its header. @param aFlag YES to allow the user to select columns, otherwise NO. */ - (void)setAllowsColumnSelection:(BOOL)shouldAllowColumnSelection { _allowsColumnSelection = !!shouldAllowColumnSelection; } - (BOOL)allowsColumnSelection { return _allowsColumnSelection; } //Setting Display Attributes - (void)setIntercellSpacing:(CGSize)aSize { if (_CGSizeEqualToSize(_intercellSpacing, aSize)) return; _intercellSpacing = _CGSizeMakeCopy(aSize); [self setNeedsLayout]; } - (void)setThemeState:(int)astae { } - (CGSize)intercellSpacing { return _CGSizeMakeCopy(_intercellSpacing); } - (void)setRowHeight:(unsigned)aRowHeight { aRowHeight = +aRowHeight; if (_rowHeight === aRowHeight) return; _rowHeight = MAX(0.0, aRowHeight); [self setNeedsLayout]; } - (unsigned)rowHeight { return _rowHeight; } /*! Sets whether the receiver uses the standard alternating row colors for its background. @param aFlag YES to specify standard alternating row colors for the background, NO to specify a solid color. */ - (void)setUsesAlternatingRowBackgroundColors:(BOOL)shouldUseAlternatingRowBackgroundColors { _usesAlternatingRowBackgroundColors = shouldUseAlternatingRowBackgroundColors; } - (BOOL)usesAlternatingRowBackgroundColors { return _usesAlternatingRowBackgroundColors; } /*! Sets the colors for the rows as they alternate. The number of colors can be arbitrary. By deafult these colors are white and light blue. @param anArray an array of CPColors */ - (void)setAlternatingRowBackgroundColors:(CPArray)alternatingRowBackgroundColors { [self setValue:alternatingRowBackgroundColors forThemeAttribute:"alternating-row-colors"]; [self setNeedsDisplay:YES]; } - (CPArray)alternatingRowBackgroundColors { return [self currentValueForThemeAttribute:@"alternating-row-colors"]; } - (unsigned)selectionHighlightStyle { return _selectionHighlightStyle; } - (void)setSelectionHighlightStyle:(unsigned)aSelectionHighlightStyle { //early return for IE. if (aSelectionHighlightStyle == CPTableViewSelectionHighlightStyleSourceList && !CPFeatureIsCompatible(CPHTMLCanvasFeature)) return; _selectionHighlightStyle = aSelectionHighlightStyle; [self setNeedsDisplay:YES]; if (aSelectionHighlightStyle === CPTableViewSelectionHighlightStyleSourceList) _destinationDragStyle = CPTableViewDraggingDestinationFeedbackStyleSourceList; else _destinationDragStyle = CPTableViewDraggingDestinationFeedbackStyleRegular; } /*! Sets the highlight color for a row or column selection @param aColor a CPColor */ - (void)setSelectionHighlightColor:(CPColor)aColor { [self setValue:aColor forThemeAttribute:"selection-color"]; [self setNeedsDisplay:YES]; } /*! Returns the highlight color for a row or column selection. */ - (CPColor)selectionHighlightColor { return [self currentValueForThemeAttribute:@"selection-color"]; } /*! Sets the highlight gradient for a row or column selection This is specific to the @param aDictionary a CPDictionary expects three keys to be set: CPSourceListGradient which is a CGGradient CPSourceListTopLineColor which is a CPColor CPSourceListBottomLineColor which is a CPColor */ - (void)setSelectionGradientColors:(CPDictionary)aDictionary { [self setValue:aDictionary forThemeAttribute:"sourcelist-selection-color"]; [self setNeedsDisplay:YES]; } /*! Returns a dictionary of containing the keys: CPSourceListGradient CPSourceListTopLineColor CPSourceListBottomLineColor */ - (CPDictionary)selectionGradientColors { return [self currentValueForThemeAttribute:@"sourcelist-selection-color"]; } /*! Sets the grid color in the non highlighted state. @param aColor a CPColor */ - (void)setGridColor:(CPColor)aColor { [self setValue:aColor forThemeAttribute:"grid-color"]; [self setNeedsDisplay:YES]; } - (CPColor)gridColor { return [self currentValueForThemeAttribute:@"grid-color"];; } /*! Sets the grid style mask to specify if no grid lines, vertical grid lines, or horizontal grid lines should be displayed. @param gridType The grid style mask. CPTableViewGridNone, CPTableViewSolidVerticalGridLineMask, CPTableViewSolidHorizontalGridLineMask */ - (void)setGridStyleMask:(unsigned)aGrideStyleMask { if (_gridStyleMask === aGrideStyleMask) return; _gridStyleMask = aGrideStyleMask; [self setNeedsDisplay:YES]; } - (unsigned)gridStyleMask { return _gridStyleMask; } //Column Management /*! Adds a given column as the last column of the receiver. @param aColumn The column to add to the receiver. */ - (void)addTableColumn:(CPTableColumn)aTableColumn { [_tableColumns addObject:aTableColumn]; [aTableColumn setTableView:self]; if (_dirtyTableColumnRangeIndex < 0) _dirtyTableColumnRangeIndex = NUMBER_OF_COLUMNS() - 1; else _dirtyTableColumnRangeIndex = MIN(NUMBER_OF_COLUMNS() - 1, _dirtyTableColumnRangeIndex); [self tile]; [self setNeedsLayout]; } /*! Removes a given column from the receiver. @param aTableColumn The column to remove from the receiver. */ - (void)removeTableColumn:(CPTableColumn)aTableColumn { if ([aTableColumn tableView] !== self) return; var index = [_tableColumns indexOfObjectIdenticalTo:aTableColumn]; if (index === CPNotFound) return; // we differ the actual removal until the end of the runloop in order to keep a reference to the column. [_differedColumnDataToRemove addObject:{"column":aTableColumn, "shouldBeHidden": [aTableColumn isHidden]}]; [aTableColumn setHidden:YES]; [aTableColumn setTableView:nil]; var tableColumnUID = [aTableColumn UID]; if (_objectValues[tableColumnUID]) _objectValues[tableColumnUID] = nil; if (_dirtyTableColumnRangeIndex < 0) _dirtyTableColumnRangeIndex = index; else _dirtyTableColumnRangeIndex = MIN(index, _dirtyTableColumnRangeIndex); [self setNeedsLayout]; } - (void)_setDraggedColumn:(int)aColumnIndex { if (_draggedColumnIndex === aColumnIndex) return; _draggedColumnIndex = aColumnIndex; [self reloadDataForRowIndexes:_exposedRows columnIndexes:[CPIndexSet indexSetWithIndex:aColumnIndex]]; } /*! Moves the column and heading at a given index to a new given index. @param columnIndex The current index of the column to move. @param newIndex The new index for the moved column. */ - (void)moveColumn:(unsigned)fromIndex toColumn:(unsigned)toIndex { fromIndex = +fromIndex; toIndex = +toIndex; if (fromIndex === toIndex) return; if (_dirtyTableColumnRangeIndex < 0) _dirtyTableColumnRangeIndex = MIN(fromIndex, toIndex); else _dirtyTableColumnRangeIndex = MIN(fromIndex, toIndex, _dirtyTableColumnRangeIndex); var tableColumn = _tableColumns[fromIndex]; [_tableColumns removeObjectAtIndex:fromIndex]; [_tableColumns insertObject:tableColumn atIndex:toIndex]; [[self headerView] setNeedsLayout]; [[self headerView] setNeedsDisplay:YES]; var rowIndexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self numberOfRows])], columnIndexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(fromIndex, toIndex)]; [self reloadDataForRowIndexes:rowIndexes columnIndexes:columnIndexes]; } /*! @ignore */ - (void)_tableColumnVisibilityDidChange:(CPTableColumn)aColumn { var columnIndex = [[self tableColumns] indexOfObjectIdenticalTo:aColumn]; if (_dirtyTableColumnRangeIndex < 0) _dirtyTableColumnRangeIndex = columnIndex; else _dirtyTableColumnRangeIndex = MIN(columnIndex, _dirtyTableColumnRangeIndex); [[self headerView] setNeedsLayout]; [[self headerView] setNeedsDisplay:YES]; var rowIndexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self numberOfRows])]; [self reloadDataForRowIndexes:rowIndexes columnIndexes:[CPIndexSet indexSetWithIndex:columnIndex]]; } - (CPArray)tableColumns { return _tableColumns; } - (CPInteger)columnWithIdentifier:(CPString)anIdentifier { var index = 0, count = NUMBER_OF_COLUMNS(); for (; index < count; ++index) if ([_tableColumns[index] identifier] === anIdentifier) return index; return CPNotFound; } - (CPTableColumn)tableColumnWithIdentifier:(CPString)anIdentifier { var index = [self columnWithIdentifier:anIdentifier]; if (index === CPNotFound) return nil; return _tableColumns[index]; } //Selecting Columns and Rows /*! Sets the column selection using indexes. @param columns a CPIndexSet of columns to select @param aFlag should extend the selection thereby retaining the previous selection */ - (void)selectColumnIndexes:(CPIndexSet)columns byExtendingSelection:(BOOL)shouldExtendSelection { // If we're out of range, just return if (([columns firstIndex] != CPNotFound && [columns firstIndex] < 0) || [columns lastIndex] >= [self numberOfColumns]) return; // We deselect all rows when selecting columns. if ([_selectedRowIndexes count] > 0) { [self _updateHighlightWithOldRows:_selectedRowIndexes newRows:[CPIndexSet indexSet]]; _selectedRowIndexes = [CPIndexSet indexSet]; } var previousSelectedIndexes = [_selectedColumnIndexes copy]; if (shouldExtendSelection) [_selectedColumnIndexes addIndexes:columns]; else _selectedColumnIndexes = [columns copy]; [self _updateHighlightWithOldColumns:previousSelectedIndexes newColumns:_selectedColumnIndexes]; [self setNeedsDisplay:YES]; // FIXME: should be setNeedsDisplayInRect:enclosing rect of new (de)selected columns // but currently -drawRect: is not implemented here if (_headerView) [_headerView setNeedsDisplay:YES]; [self _noteSelectionDidChange]; } - (void)_setSelectedRowIndexes:(CPIndexSet)rows { var previousSelectedIndexes = [_selectedRowIndexes copy]; _lastSelectedRow = ([rows count] > 0) ? [rows lastIndex] : -1; _selectedRowIndexes = [rows copy]; [self _updateHighlightWithOldRows:previousSelectedIndexes newRows:_selectedRowIndexes]; [self setNeedsDisplay:YES]; // FIXME: should be setNeedsDisplayInRect:enclosing rect of new (de)selected rows // but currently -drawRect: is not implemented here [[CPKeyValueBinding getBinding:@"selectionIndexes" forObject:self] reverseSetValueFor:@"selectedRowIndexes"]; [self _noteSelectionDidChange]; } /*! Sets the row selection using indexes. @param rows a CPIndexSet of rows to select @param aFlag should extend the selection thereby retaining the previous selection */ - (void)selectRowIndexes:(CPIndexSet)rows byExtendingSelection:(BOOL)shouldExtendSelection { if ([rows isEqualToIndexSet:_selectedRowIndexes] || (([rows firstIndex] != CPNotFound && [rows firstIndex] < 0) || [rows lastIndex] >= [self numberOfRows])) return; // We deselect all columns when selecting rows. if ([_selectedColumnIndexes count] > 0) { [self _updateHighlightWithOldColumns:_selectedColumnIndexes newColumns:[CPIndexSet indexSet]]; _selectedColumnIndexes = [CPIndexSet indexSet]; if (_headerView) [_headerView setNeedsDisplay:YES]; } var newSelectedIndexes; if (shouldExtendSelection) { newSelectedIndexes = [_selectedRowIndexes copy]; [newSelectedIndexes addIndexes:rows]; } else newSelectedIndexes = [rows copy]; [self _setSelectedRowIndexes:newSelectedIndexes]; } - (void)_updateHighlightWithOldRows:(CPIndexSet)oldRows newRows:(CPIndexSet)newRows { var firstExposedRow = [_exposedRows firstIndex], exposedLength = [_exposedRows lastIndex] - firstExposedRow + 1, deselectRows = [], selectRows = [], deselectRowIndexes = [oldRows copy], selectRowIndexes = [newRows copy]; [deselectRowIndexes removeMatches:selectRowIndexes]; [deselectRowIndexes getIndexes:deselectRows maxCount:-1 inIndexRange:CPMakeRange(firstExposedRow, exposedLength)]; [selectRowIndexes getIndexes:selectRows maxCount:-1 inIndexRange:CPMakeRange(firstExposedRow, exposedLength)]; for (var identifier in _dataViewsForTableColumns) { var dataViewsInTableColumn = _dataViewsForTableColumns[identifier]; var count = deselectRows.length; while (count--) [self _performSelection:NO forRow:deselectRows[count] context:dataViewsInTableColumn]; count = selectRows.length; while (count--) [self _performSelection:YES forRow:selectRows[count] context:dataViewsInTableColumn]; } } - (void)_performSelection:(BOOL)select forRow:(CPInteger)rowIndex context:(id)context { var view = context[rowIndex], selector = select ? @"setThemeState:" : @"unsetThemeState:"; [view performSelector:CPSelectorFromString(selector) withObject:CPThemeStateSelectedDataView]; } - (void)_updateHighlightWithOldColumns:(CPIndexSet)oldColumns newColumns:(CPIndexSet)newColumns { var firstExposedColumn = [_exposedColumns firstIndex], exposedLength = [_exposedColumns lastIndex] - firstExposedColumn +1, deselectColumns = [], selectColumns = [], deselectColumnIndexes = [oldColumns copy], selectColumnIndexes = [newColumns copy], selectRows = []; [deselectColumnIndexes removeMatches:selectColumnIndexes]; [deselectColumnIndexes getIndexes:deselectColumns maxCount:-1 inIndexRange:CPMakeRange(firstExposedColumn, exposedLength)]; [selectColumnIndexes getIndexes:selectColumns maxCount:-1 inIndexRange:CPMakeRange(firstExposedColumn, exposedLength)]; [_exposedRows getIndexes:selectRows maxCount:-1 inIndexRange:nil]; var rowsCount = selectRows.length, count = deselectColumns.length; while (count--) { var columnIndex = deselectColumns[count], identifier = [_tableColumns[columnIndex] UID], dataViewsInTableColumn = _dataViewsForTableColumns[identifier]; for (var i = 0; i < rowsCount; i++) { var rowIndex = selectRows[i], dataView = dataViewsInTableColumn[rowIndex]; [dataView unsetThemeState:CPThemeStateSelectedDataView]; } if (_headerView) { var headerView = [_tableColumns[columnIndex] headerView]; [headerView unsetThemeState:CPThemeStateSelected]; } } count = selectColumns.length; while (count--) { var columnIndex = selectColumns[count], identifier = [_tableColumns[columnIndex] UID], dataViewsInTableColumn = _dataViewsForTableColumns[identifier]; for (var i = 0; i < rowsCount; i++) { var rowIndex = selectRows[i], dataView = dataViewsInTableColumn[rowIndex]; [dataView setThemeState:CPThemeStateSelectedDataView]; } if (_headerView) { var headerView = [_tableColumns[columnIndex] headerView]; [headerView setThemeState:CPThemeStateSelected]; } } } - (int)selectedColumn { [_selectedColumnIndexes lastIndex]; } - (CPIndexSet)selectedColumnIndexes { return _selectedColumnIndexes; } - (int)selectedRow { return _lastSelectedRow; } - (CPIndexSet)selectedRowIndexes { return [_selectedRowIndexes copy]; } - (void)deselectColumn:(CPInteger)aColumn { var selectedColumnIndexes = [_selectedColumnIndexes copy]; [selectedColumnIndexes removeIndex:aColumn]; [self selectColumnIndexes:selectedColumnIndexes byExtendingSelection:NO]; [self _noteSelectionDidChange]; } - (void)deselectRow:(CPInteger)aRow { var selectedRowIndexes = [_selectedRowIndexes copy]; [selectedRowIndexes removeIndex:aRow]; [self selectRowIndexes:selectedRowIndexes byExtendingSelection:NO]; [self _noteSelectionDidChange]; } - (CPInteger)numberOfSelectedColumns { return [_selectedColumnIndexes count]; } - (CPInteger)numberOfSelectedRows { return [_selectedRowIndexes count]; } /* - (CPInteger)selectedColumn * - selectedRow */ - (BOOL)isColumnSelected:(CPInteger)aColumn { return [_selectedColumnIndexes containsIndex:aColumn]; } - (BOOL)isRowSelected:(CPInteger)aRow { return [_selectedRowIndexes containsIndex:aRow]; } /* - (void)selectAll: * - deselectAll: * - allowsTypeSelect * - setAllowsTypeSelect: */ /*! Deselects all rows */ - (void)deselectAll { [self selectRowIndexes:[CPIndexSet indexSet] byExtendingSelection:NO]; [self selectColumnIndexes:[CPIndexSet indexSet] byExtendingSelection:NO]; } //Table Dimensions - (int)numberOfColumns { return NUMBER_OF_COLUMNS(); } /* Returns the number of rows in the receiver. */ - (int)numberOfRows { if (_numberOfRows) return _numberOfRows; var contentBindingInfo = [self infoForBinding:@"content"]; if (contentBindingInfo) { var destination = [contentBindingInfo objectForKey:CPObservedObjectKey], keyPath = [contentBindingInfo objectForKey:CPObservedKeyPathKey]; return [[destination valueForKeyPath:keyPath] count]; } else if (_dataSource) return [_dataSource numberOfRowsInTableView:self]; return 0; } //Displaying Cell /* * - preparedCellAtColumn:row: */ //Editing Cells /*! Edits the indicated row. */ - (void)editColumn:(CPInteger)columnIndex row:(CPInteger)rowIndex withEvent:(CPEvent)theEvent select:(BOOL)flag { if (![self isRowSelected:rowIndex]) [[CPException exceptionWithName:@"Error" reason:@"Attempt to edit row="+rowIndex+" when not selected." userInfo:nil] raise]; // TODO Do something with flag. _editingCellIndex = CGPointMake(columnIndex, rowIndex); [self reloadDataForRowIndexes:[CPIndexSet indexSetWithIndex:rowIndex] columnIndexes:[CPIndexSet indexSetWithIndex:columnIndex]]; } /*! Returns the column of the currently edited cell, or -1 if none. */ - (CPInteger)editedColumn { if (!_editingCellIndex) return -1; return _editingCellIndex.x; } /*! Returns the row of the currently edited cell, or -1 if none. */ - (CPInteger)editedRow { if (!_editingCellIndex) return -1; return _editingCellIndex.x; } //Setting Auxiliary Views /* * - setHeaderView: * - headerView * - setCornerView: * - cornerView */ - (CPView)cornerView { return _cornerView; } - (void)setCornerView:(CPView)aView { if (_cornerView === aView) return; _cornerView = aView; var scrollView = [[self superview] superview]; if ([scrollView isKindOfClass:[CPScrollView class]] && [scrollView documentView] === self) [scrollView _updateCornerAndHeaderView]; } - (CPView)headerView { return _headerView; } - (void)setHeaderView:(CPView)aHeaderView { if (_headerView === aHeaderView) return; [_headerView setTableView:nil]; _headerView = aHeaderView; if (_headerView) { [_headerView setTableView:self]; [_headerView setFrameSize:_CGSizeMake(_CGRectGetWidth([self frame]), _CGRectGetHeight([_headerView frame]))]; } var scrollView = [[self superview] superview]; if ([scrollView isKindOfClass:[CPScrollView class]] && [scrollView documentView] === self) [scrollView _updateCornerAndHeaderView]; } //Layout Support // Complexity: // O(Columns) - (void)_recalculateTableColumnRanges { if (_dirtyTableColumnRangeIndex < 0) return; _numberOfHiddenColumns = 0; var index = _dirtyTableColumnRangeIndex, count = NUMBER_OF_COLUMNS(), x = index === 0 ? 0.0 : CPMaxRange(_tableColumnRanges[index - 1]); for (; index < count; ++index) { var tableColumn = _tableColumns[index]; if ([tableColumn isHidden]) { _numberOfHiddenColumns += 1; _tableColumnRanges[index] = CPMakeRange(x, 0.0); } else { var width = [_tableColumns[index] width]; _tableColumnRanges[index] = CPMakeRange(x, width); x += width; } } _tableColumnRanges.length = count; _dirtyTableColumnRangeIndex = CPNotFound; } // Complexity: // O(1) - (CGRect)rectOfColumn:(CPInteger)aColumnIndex { aColumnIndex = +aColumnIndex; var column = [[self tableColumns] objectAtIndex:aColumnIndex]; if ([column isHidden] || aColumnIndex < 0 || aColumnIndex >= NUMBER_OF_COLUMNS()) return _CGRectMakeZero(); UPDATE_COLUMN_RANGES_IF_NECESSARY(); var range = _tableColumnRanges[aColumnIndex]; return _CGRectMake(range.location, 0.0, range.length, CGRectGetHeight([self bounds])); } - (CGRect)rectOfRow:(CPInteger)aRowIndex { if (NO) return NULL; // FIXME: WRONG: ASK TABLE COLUMN RANGE return _CGRectMake(0.0, (aRowIndex * (_rowHeight + _intercellSpacing.height)), _CGRectGetWidth([self bounds]), _rowHeight); } // Complexity: // O(1) /*! Returns a range of indices for the rows that lie wholly or partially within the vertical boundaries of a given rectangle. @param aRect A rectangle in the coordinate system of the receiver. */ - (CPRange)rowsInRect:(CGRect)aRect { // If we have no rows, then we won't intersect anything. if (_numberOfRows <= 0) return CPMakeRange(0, 0); var bounds = [self bounds]; // No rows if the rect doesn't even intersect us. if (!CGRectIntersectsRect(aRect, bounds)) return CPMakeRange(0, 0); var firstRow = [self rowAtPoint:aRect.origin]; // first row has to be undershot, because if not we wouldn't be intersecting. if (firstRow < 0) firstRow = 0; var lastRow = [self rowAtPoint:_CGPointMake(0.0, _CGRectGetMaxY(aRect))]; // last row has to be overshot, because if not we wouldn't be intersecting. if (lastRow < 0) lastRow = _numberOfRows - 1; return CPMakeRange(firstRow, lastRow - firstRow + 1); } // Complexity: // O(lg Columns) if table view contains no hidden columns // O(Columns) if table view contains hidden columns /*! Returns the indexes of the receiverŐs columns that intersect the specified rectangle. @param aRect A rectangle in the coordinate system of the receiver. */ - (CPIndexSet)columnIndexesInRect:(CGRect)aRect { var column = MAX(0, [self columnAtPoint:_CGPointMake(aRect.origin.x, 0.0)]), lastColumn = [self columnAtPoint:_CGPointMake(_CGRectGetMaxX(aRect), 0.0)]; if (lastColumn === CPNotFound) lastColumn = NUMBER_OF_COLUMNS() - 1; // Don't bother doing the expensive removal of hidden indexes if we have no hidden columns. if (_numberOfHiddenColumns <= 0) return [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(column, lastColumn - column + 1)]; // var indexSet = [CPIndexSet indexSet]; for (; column <= lastColumn; ++column) { var tableColumn = _tableColumns[column]; if (![tableColumn isHidden]) [indexSet addIndex:column]; } return indexSet; } // Complexity: // O(lg Columns) if table view contains now hidden columns // O(Columns) if table view contains hidden columns - (CPInteger)columnAtPoint:(CGPoint)aPoint { var bounds = [self bounds]; if (!_CGRectContainsPoint(bounds, aPoint)) return CPNotFound; UPDATE_COLUMN_RANGES_IF_NECESSARY(); var x = aPoint.x, low = 0, high = _tableColumnRanges.length - 1; while (low <= high) { var middle = FLOOR(low + (high - low) / 2), range = _tableColumnRanges[middle]; if (x < range.location) high = middle - 1; else if (x >= CPMaxRange(range)) low = middle + 1; else { var numberOfColumns = _tableColumnRanges.length; while (middle < numberOfColumns && [_tableColumns[middle] isHidden]) ++middle; if (middle < numberOfColumns) return middle; return CPNotFound; } } return CPNotFound; } - (CPInteger)rowAtPoint:(CGPoint)aPoint { var y = aPoint.y; var row = FLOOR(y / (_rowHeight + _intercellSpacing.height)); if (row >= _numberOfRows) return -1; return row; } - (CGRect)frameOfDataViewAtColumn:(CPInteger)aColumn row:(CPInteger)aRow { UPDATE_COLUMN_RANGES_IF_NECESSARY(); var tableColumnRange = _tableColumnRanges[aColumn], rectOfRow = [self rectOfRow:aRow]; return _CGRectMake(tableColumnRange.location, _CGRectGetMinY(rectOfRow), tableColumnRange.length, _CGRectGetHeight(rectOfRow)); } - (void)resizeWithOldSuperviewSize:(CGSize)aSize { [super resizeWithOldSuperviewSize:aSize]; if (_disableAutomaticResizing) return; var mask = _columnAutoResizingStyle; if(mask === CPTableViewUniformColumnAutoresizingStyle) [self _resizeAllColumnUniformlyWithOldSize:aSize]; else if(mask === CPTableViewLastColumnOnlyAutoresizingStyle) [self sizeLastColumnToFit]; else if(mask === CPTableViewFirstColumnOnlyAutoresizingStyle) [self _autoResizeFirstColumn]; } - (void)_autoResizeFirstColumn { var superview = [self superview]; if (!superview) return; var superviewSize = [superview bounds].size; UPDATE_COLUMN_RANGES_IF_NECESSARY(); var count = NUMBER_OF_COLUMNS(), visColumns = [[CPArray alloc] init], totalWidth = 0, i = 0; for(; i < count; i++) { if(![_tableColumns[i] isHidden]) { [visColumns addObject:i]; totalWidth += [_tableColumns[i] width]; } } count = [visColumns count]; //if there are rows if (count > 0) { var columnToResize = _tableColumns[visColumns[0]]; var newWidth = superviewSize.width - totalWidth;// - [columnToResize width]; newWidth += [columnToResize width]; newWidth = (newWidth < [columnToResize minWidth]) ? [columnToResize minWidth] : newWidth; newWidth = (newWidth > [columnToResize maxWidth]) ? [columnToResize maxWidth] : newWidth; [columnToResize setWidth:FLOOR(newWidth)]; } [self setNeedsLayout]; } - (void)_resizeAllColumnUniformlyWithOldSize:(CGSize)oldSize { var superview = [self superview]; if (!superview) return; var superviewSize = [superview bounds].size; if (_dirtyTableColumnRangeIndex !== CPNotFound) [self _recalculateTableColumnRanges];//UPDATE_COLUMN_RANGES_IF_NECESSARY(); var count = _tableColumns.length,//NUMBER_OF_COLUMNS(), visColumns = [[CPArray alloc] init], buffer = 0.0; // Fixme: cache resizable columns because they won't changes betwwen two calls to this method. for(var i=0; i < count; i++) { var tableColumn = _tableColumns[i]; if(![tableColumn isHidden] && ([tableColumn resizingMask] & CPTableColumnAutoresizingMask)) [visColumns addObject:i]; } // redefine count count = [visColumns count]; //if there are columns if (count > 0) { var maxXofColumns = CGRectGetMaxX([self rectOfColumn:visColumns[count - 1]]); // If the x value of the end of the last column is between the current bounds and the previous bounds we should snap. if (!_lastColumnShouldSnap && (maxXofColumns >= superviewSize.width && maxXofColumns <= oldSize.width || maxXofColumns <= superviewSize.width && maxXofColumns >= oldSize.width)) { //set the snap mask _lastColumnShouldSnap = YES; //then we need to make sure everything is set correctly. [self _resizeAllColumnUniformlyWithOldSize:CGSizeMake(maxXofColumns, 0)]; } if(!_lastColumnShouldSnap) return; // FIX ME: This is wrong because this should continue to resize all columns // If the last column reaches it's max/min it will simply stop resizing, // correct behavior is to resize all columns until they reach their min/max for (var i = 0; i < count; i++) { var column = visColumns[i]; columnToResize = _tableColumns[column], currentBuffer = buffer / (count - i), realNewWidth = ([columnToResize width] / oldSize.width * [superview bounds].size.width) + currentBuffer , newWidth = MAX([columnToResize minWidth], realNewWidth); newWidth = MIN([columnToResize maxWidth], realNewWidth); buffer -= currentBuffer; // the buffer takes into account the min/max width of the column buffer += realNewWidth - newWidth; [columnToResize setWidth:newWidth]; } // if there is space left over that means column resize was too long or too short if(buffer !== 0) _lastColumnShouldSnap = NO; } [self setNeedsLayout]; } /*! Sets the column autoresizing style of the receiver to a given style. @param aStyle The column autoresizing style for the receiver. CPTableViewNoColumnAutoresizing, CPTableViewUniformColumnAutoresizingStyle, CPTableViewLastColumnOnlyAutoresizingStyle, CPTableViewFirstColumnOnlyAutoresizingStyle */ - (void)setColumnAutoresizingStyle:(unsigned)style { //FIX ME: CPTableViewSequentialColumnAutoresizingStyle and CPTableViewReverseSequentialColumnAutoresizingStyle are not yet implemented _columnAutoResizingStyle = style; } - (unsigned)columnAutoresizingStyle { return _columnAutoResizingStyle; } /*! Resizes the last column if thereŐs room so the receiver fits exactly within its enclosing clip view. */ - (void)sizeLastColumnToFit { var superview = [self superview]; if (!superview) return; var superviewSize = [superview bounds].size; UPDATE_COLUMN_RANGES_IF_NECESSARY(); var count = NUMBER_OF_COLUMNS(); //decrement the counter until we get to the last row that's not hidden while (count-- && [_tableColumns[count] isHidden]) ; //if the last row exists if (count >= 0) { var columnToResize = _tableColumns[count]; var newSize = MAX(0.0, superviewSize.width - CGRectGetMinX([self rectOfColumn:count])); if (newSize > 0) { newSize = MAX([columnToResize minWidth], newSize); newSize = MIN([columnToResize maxWidth], newSize); [columnToResize setWidth:newSize]; } } [self setNeedsLayout]; } - (void)noteNumberOfRowsChanged { _numberOfRows = nil; _numberOfRows = [self numberOfRows]; var oldNumberOfRows = _numberOfRows; // remove row indexes from the selection if they no longer exist var hangingSelections = oldNumberOfRows - _numberOfRows; if (hangingSelections > 0) { [_selectedRowIndexes removeIndexesInRange:CPMakeRange(_numberOfRows, hangingSelections)]; [self _noteSelectionDidChange]; } [self tile]; } - (void)tile { UPDATE_COLUMN_RANGES_IF_NECESSARY(); // FIXME: variable row heights. var width = _tableColumnRanges.length > 0 ? CPMaxRange([_tableColumnRanges lastObject]) : 0.0, height = (_rowHeight + _intercellSpacing.height) * _numberOfRows, superview = [self superview]; if ([superview isKindOfClass:[CPClipView class]]) { var superviewSize = [superview bounds].size; width = MAX(superviewSize.width, width); height = MAX(superviewSize.height, height); } [self setFrameSize:_CGSizeMake(width, height)]; [self setNeedsLayout]; [self setNeedsDisplay:YES]; } /* * - tile * - sizeToFit * - noteHeightOfRowsWithIndexesChanged: */ //Scrolling /* * - scrollRowToVisible: * - scrollColumnToVisible: */ /*! Scrolls the receiver vertically in an enclosing NSClipView so the row specified by rowIndex is visible. @param aRowIndex the index of the row to scroll to. */ - (void)scrollRowToVisible:(int)rowIndex { [self scrollRectToVisible:[self rectOfRow:rowIndex]]; } /*! Scrolls the receiver and header view horizontally in an enclosing NSClipView so the column specified by columnIndex is visible. @param aColumnIndex the index of the column to scroll to. */ - (void)scrollColumnToVisible:(int)columnIndex { [self scrollRectToVisible:[self rectOfColumn:columnIndex]]; /*FIX ME: tableview header isn't rendered until you click the horizontal scroller (or scroll)*/ } //Persistence /* * - autosaveName * - autosaveTableColumns * - setAutosaveName: * - setAutosaveTableColumns: */ //Setting the Delegate:(id)aDelegate - (void)setDelegate:(id)aDelegate { if (_delegate === aDelegate) return; var defaultCenter = [CPNotificationCenter defaultCenter]; if (_delegate) { if ([_delegate respondsToSelector:@selector(tableViewColumnDidMove:)]) [defaultCenter removeObserver:_delegate name:CPTableViewColumnDidMoveNotification object:self]; if ([_delegate respondsToSelector:@selector(tableViewColumnDidResize:)]) [defaultCenter removeObserver:_delegate name:CPTableViewColumnDidResizeNotification object:self]; if ([_delegate respondsToSelector:@selector(tableViewSelectionDidChange:)]) [defaultCenter removeObserver:_delegate name:CPTableViewSelectionDidChangeNotification object:self]; if ([_delegate respondsToSelector:@selector(tableViewSelectionIsChanging:)]) [defaultCenter removeObserver:_delegate name:CPTableViewSelectionIsChangingNotification object:self]; } _delegate = aDelegate; _implementedDelegateMethods = 0; if ([_delegate respondsToSelector:@selector(selectionShouldChangeInTableView:)]) _implementedDelegateMethods |= CPTableViewDelegate_selectionShouldChangeInTableView_; if ([_delegate respondsToSelector:@selector(tableView:dataViewForTableColumn:row:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_dataViewForTableColumn_row_; if ([_delegate respondsToSelector:@selector(tableView:didClickTableColumn:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_didClickTableColumn_; if ([_delegate respondsToSelector:@selector(tableView:didDragTableColumn:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_didDragTableColumn_; if ([_delegate respondsToSelector:@selector(tableView:heightOfRow:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_heightOfRow_; if ([_delegate respondsToSelector:@selector(tableView:isGroupRow:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_isGroupRow_; if ([_delegate respondsToSelector:@selector(tableView:mouseDownInHeaderOfTableColumn:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_mouseDownInHeaderOfTableColumn_; if ([_delegate respondsToSelector:@selector(tableView:nextTypeSelectMatchFromRow:toRow:forString:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_nextTypeSelectMatchFromRow_toRow_forString_; if ([_delegate respondsToSelector:@selector(tableView:selectionIndexesForProposedSelection:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_selectionIndexesForProposedSelection_; if ([_delegate respondsToSelector:@selector(tableView:shouldEditTableColumn:row:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_shouldEditTableColumn_row_; if ([_delegate respondsToSelector:@selector(tableView:shouldSelectRow:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_shouldSelectRow_; if ([_delegate respondsToSelector:@selector(tableView:shouldSelectTableColumn:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_shouldSelectTableColumn_; if ([_delegate respondsToSelector:@selector(tableView:shouldShowViewExpansionForTableColumn:row:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_shouldShowViewExpansionForTableColumn_row_; if ([_delegate respondsToSelector:@selector(tableView:shouldTrackView:forTableColumn:row:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_shouldTrackView_forTableColumn_row_; if ([_delegate respondsToSelector:@selector(tableView:shouldTypeSelectForEvent:withCurrentSearchString:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_shouldTypeSelectForEvent_withCurrentSearchString_; if ([_delegate respondsToSelector:@selector(tableView:toolTipForView:rect:tableColumn:row:mouseLocation:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_toolTipForView_rect_tableColumn_row_mouseLocation_; if ([_delegate respondsToSelector:@selector(tableView:typeSelectStringForTableColumn:row:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_typeSelectStringForTableColumn_row_; if ([_delegate respondsToSelector:@selector(tableView:willDisplayView:forTableColumn:row:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_willDisplayView_forTableColumn_row_; if ([_delegate respondsToSelector:@selector(tableViewColumnDidMove:)]) [defaultCenter addObserver:_delegate selector:@selector(tableViewColumnDidMove:) name:CPTableViewColumnDidMoveNotification object:self]; if ([_delegate respondsToSelector:@selector(tableViewColumnDidResize:)]) [defaultCenter addObserver:_delegate selector:@selector(tableViewColumnDidResize:) name:CPTableViewColumnDidResizeNotification object:self]; if ([_delegate respondsToSelector:@selector(tableViewSelectionDidChange:)]) [defaultCenter addObserver:_delegate selector:@selector(tableViewSelectionDidChange:) name:CPTableViewSelectionDidChangeNotification object:self]; if ([_delegate respondsToSelector:@selector(tableViewSelectionIsChanging:)]) [defaultCenter addObserver:_delegate selector:@selector(tableViewSelectionIsChanging:) name:CPTableViewSelectionIsChangingNotification object:self]; } - (id)delegate { return _delegate; } - (void)_sendDelegateDidClickColumn:(int)column { if (_implementedDelegateMethods & CPTableViewDelegate_tableView_didClickTableColumn_) [_delegate tableView:self didClickTableColumn:_tableColumns[column]]; } - (void)_sendDelegateDidDragColumn:(int)column { if (_implementedDelegateMethods & CPTableViewDelegate_tableView_didDragTableColumn_) [_delegate tableView:self didDragTableColumn:_tableColumns[column]]; } - (void)_sendDelegateDidMouseDownInHeader:(int)column { if (_implementedDelegateMethods & CPTableViewDelegate_tableView_mouseDownInHeaderOfTableColumn_) [_delegate tableView:self mouseDownInHeaderOfTableColumn:_tableColumns[column]]; } - (void)_sendDataSourceSortDescriptorsDidChange:(CPArray)oldDescriptors { if (_implementedDataSourceMethods & CPTableViewDataSource_tableView_sortDescriptorsDidChange_) [_dataSource tableView:self sortDescriptorsDidChange:oldDescriptors]; } - (void)_didClickTableColumn:(int)clickedColumn modifierFlags:(unsigned)modifierFlags { [self _sendDelegateDidClickColumn:clickedColumn]; if (_allowsColumnSelection) { [self _noteSelectionIsChanging]; if (modifierFlags & CPCommandKeyMask) { if ([self isColumnSelected:clickedColumn]) [self deselectColumn:clickedColumn]; else if ([self allowsMultipleSelection] == YES) [self selectColumnIndexes:[CPIndexSet indexSetWithIndex:clickedColumn] byExtendingSelection:YES]; return; } else if (modifierFlags & CPShiftKeyMask) { // should be from clickedColumn to lastClickedColum with extending:(direction == previous selection) var startColumn = MIN(clickedColumn, [_selectedColumnIndexes lastIndex]), endColumn = MAX(clickedColumn, [_selectedColumnIndexes firstIndex]); [self selectColumnIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(startColumn, endColumn - startColumn + 1)] byExtendingSelection:YES]; return; } else [self selectColumnIndexes:[CPIndexSet indexSetWithIndex:clickedColumn] byExtendingSelection:NO]; } [self _changeSortDescriptorsForClickOnColumn:clickedColumn]; } // From GNUSTEP - (void)_changeSortDescriptorsForClickOnColumn:(int)column { var tableColumn = [_tableColumns objectAtIndex:column], newMainSortDescriptor = [tableColumn sortDescriptorPrototype]; if (!newMainSortDescriptor) return; var oldMainSortDescriptor = nil, oldSortDescriptors = [self sortDescriptors], newSortDescriptors = [CPArray arrayWithArray:oldSortDescriptors], e = [newSortDescriptors objectEnumerator], descriptor = nil, outdatedDescriptors = [CPArray array]; if ([_sortDescriptors count] > 0) oldMainSortDescriptor = [[self sortDescriptors] objectAtIndex: 0]; // Remove every main descriptor equivalents (normally only one) while ((descriptor = [e nextObject]) != nil) { if ([[descriptor key] isEqual: [newMainSortDescriptor key]]) [outdatedDescriptors addObject:descriptor]; } // Invert the sort direction when the same column header is clicked twice if ([[newMainSortDescriptor key] isEqual:[oldMainSortDescriptor key]]) newMainSortDescriptor = [oldMainSortDescriptor reversedSortDescriptor]; [newSortDescriptors removeObjectsInArray:outdatedDescriptors]; [newSortDescriptors insertObject:newMainSortDescriptor atIndex:0]; // Update indicator image & highlighted column before var image = [newMainSortDescriptor ascending] ? [self _tableHeaderSortImage] : [self _tableHeaderReverseSortImage]; [self setIndicatorImage:nil inTableColumn:_currentHighlightedTableColumn]; [self setIndicatorImage:image inTableColumn:tableColumn]; [self setHighlightedTableColumn:tableColumn]; [self setSortDescriptors:newSortDescriptors]; } - (void)setIndicatorImage:(CPImage)anImage inTableColumn:(CPTableColumn)aTableColumn { if (aTableColumn) [[aTableColumn headerView] _setIndicatorImage:anImage]; } - (CPImage)_tableHeaderSortImage { return [self currentValueForThemeAttribute:"sort-image"]; } - (CPImage)_tableHeaderReverseSortImage { return [self currentValueForThemeAttribute:"sort-image-reversed"]; } //Highlightable Column Headers - (CPTableColumn)highlightedTableColumn { return _currentHighlightedTableColumn; } - (void)setHighlightedTableColumn:(CPTableColumn)aTableColumn { if (_currentHighlightedTableColumn == aTableColumn) return; if (_headerView) { if (_currentHighlightedTableColumn != nil) [[_currentHighlightedTableColumn headerView] unsetThemeState:CPThemeStateSelected]; if (aTableColumn != nil) [[aTableColumn headerView] setThemeState:CPThemeStateSelected]; } _currentHighlightedTableColumn = aTableColumn; } /*! Returns whether the receiver allows dragging the rows at rowIndexes with a drag initiated at mousedDownPoint. @param rowIndexes an index set of rows to be dragged @param aPoint the point at which the mouse was clicked. */ - (BOOL)canDragRowsWithIndexes:(CPIndexSet)rowIndexes atPoint:(CGPoint)mouseDownPoint { return YES; } - (CPImage)dragImageForRowsWithIndexes:(CPIndexSet)dragRows tableColumns:(CPArray)theTableColumns event:(CPEvent)dragEvent offset:(CPPointPointer)dragImageOffset { return [[CPImage alloc] initWithContentsOfFile:@"Frameworks/AppKit/Resources/GenericFile.png" size:CGSizeMake(32,32)]; } - (CPView)dragViewForRowsWithIndexes:(CPIndexSet)theDraggedRows tableColumns:(CPArray)theTableColumns event:(CPEvent)theDragEvent offset:(CPPointPointer)dragViewOffset { var bounds = [self bounds], view = [[CPView alloc] initWithFrame:bounds]; [view setAlphaValue:0.7]; // We have to fetch all the data views for the selected rows and columns // After that we can copy these add them to a transparent drag view and use that drag view // to make it appear we are dragging images of those rows (as you would do in regular Cocoa) var columnIndex = [theTableColumns count]; while (columnIndex--) { var tableColumn = [theTableColumns objectAtIndex:columnIndex], row = [theDraggedRows firstIndex]; while (row !== CPNotFound) { var dataView = [self _newDataViewForRow:row tableColumn:tableColumn]; [dataView setFrame:[self frameOfDataViewAtColumn:columnIndex row:row]]; [dataView setObjectValue:[self _objectValueForTableColumn:tableColumn row:row]]; // If the column uses content bindings, allow them to override the objectValueForTableColumn. [tableColumn prepareDataView:dataView forRow:row]; [view addSubview:dataView]; row = [theDraggedRows indexGreaterThanIndex:row]; } } var dragPoint = [self convertPoint:[theDragEvent locationInWindow] fromView:nil]; dragViewOffset.x = CGRectGetWidth(bounds)/2 - dragPoint.x; dragViewOffset.y = CGRectGetHeight(bounds)/2 - dragPoint.y; return view; } /*! @ignore // Fetches all the data views (from the datasource) for the column and it's visible rows // Copy the dataviews add them to a transparent drag view and use that drag view // to make it appear we are dragging images of those rows (as you would do in regular Cocoa) */ - (CPView)_dragViewForColumn:(int)theColumnIndex event:(CPEvent)theDragEvent offset:(CPPointPointer)theDragViewOffset { var dragView = [[CPView alloc] initWithFrame:CPRectMakeZero()]; tableColumn = [[self tableColumns] objectAtIndex:theColumnIndex], bounds = CPRectMake(0.0, 0.0, [tableColumn width], CPRectGetHeight([self _exposedRect]) + 23.0), columnRect = [self rectOfColumn:theColumnIndex], headerView = [tableColumn headerView]; row = [_exposedRows firstIndex]; while (row !== CPNotFound) { var dataView = [self _newDataViewForRow:row tableColumn:tableColumn], dataViewFrame = [self frameOfDataViewAtColumn:theColumnIndex row:row]; // Only one column is ever dragged so we just place the view at dataViewFrame.origin.x = 0.0; // Offset by table header height - scroll position dataViewFrame.origin.y = ( CPRectGetMinY(dataViewFrame) - CPRectGetMinY([self _exposedRect]) ) + 23.0; [dataView setFrame:dataViewFrame]; [dataView setObjectValue:[self _objectValueForTableColumn:tableColumn row:row]]; [dragView addSubview:dataView]; row = [_exposedRows indexGreaterThanIndex:row]; } // Add the column header view var headerFrame = [headerView frame]; headerFrame.origin = CPPointMakeZero(); columnHeaderView = [[_CPTableColumnHeaderView alloc] initWithFrame:headerFrame]; [columnHeaderView setStringValue:[headerView stringValue]]; [columnHeaderView setThemeState:[headerView themeState]]; [dragView addSubview:columnHeaderView]; [dragView setBackgroundColor:[CPColor whiteColor]]; [dragView setAlphaValue:0.7]; [dragView setFrame:bounds]; return dragView; } - (void)setDraggingSourceOperationMask:(CPDragOperation)mask forLocal:(BOOL)isLocal { //ignoral local for the time being since only one capp app can run at a time... _dragOperationDefaultMask = mask; } /*! This should be called inside tableView:validateDrop:... method either drop on or above, specify the row as -1 to select the whole table for drop on */ - (void)setDropRow:(CPInteger)row dropOperation:(CPTableViewDropOperation)operation { if(row > [self numberOfRows] && operation === CPTableViewDropOn) { var numberOfRows = [self numberOfRows] + 1; var reason = @"Attempt to set dropRow=" + row + " dropOperation=CPTableViewDropOn when [0 - " + numberOfRows + "] is valid range of rows." [[CPException exceptionWithName:@"Error" reason:reason userInfo:nil] raise]; } _retargetedDropRow = row; _retargetedDropOperation = operation; } /*! sets the feedback style for when the table is the destination of a drag operation Can be: None Regular Source List */ - (void)setDraggingDestinationFeedbackStyle:(CPTableViewDraggingDestinationFeedbackStyle)aStyle { //FIX ME: this should vary up the highlight color, currently nothing is being done with it _destinationDragStyle = aStyle; } - (CPTableViewDraggingDestinationFeedbackStyle)draggingDestinationFeedbackStyle { return _destinationDragStyle; } /*! Sets whether vertical motion is treated as a drag or selection change to flag. @param aFlag If flag is NO then vertical motion will not start a drag. The default is YES. */ - (void)setVerticalMotionCanBeginDrag:(BOOL)aFlag { _verticalMotionCanDrag = aFlag; } - (BOOL)verticalMotionCanBeginDrag { return _verticalMotionCanDrag; } - (void)setSortDescriptors:(CPArray)sortDescriptors { var oldSortDescriptors = [self sortDescriptors], newSortDescriptors = nil; if (sortDescriptors == nil) newSortDescriptors = [CPArray array]; else newSortDescriptors = [CPArray arrayWithArray:sortDescriptors]; if ([newSortDescriptors isEqual:oldSortDescriptors]) return; _sortDescriptors = newSortDescriptors; [self _sendDataSourceSortDescriptorsDidChange:oldSortDescriptors]; } - (CPArray)sortDescriptors { return _sortDescriptors; } //Text Delegate Methods /* * - textShouldBeginEditing: * - textDidBeginEditing: * - textDidChange: * - textShouldEndEditing: * - textDidEndEditing: */ - (id)_objectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex { var tableColumnUID = [aTableColumn UID], tableColumnObjectValues = _objectValues[tableColumnUID]; if (!tableColumnObjectValues) { tableColumnObjectValues = []; _objectValues[tableColumnUID] = tableColumnObjectValues; } var objectValue = tableColumnObjectValues[aRowIndex]; // tableView:objectValueForTableColumn:row: is optional if content bindings are in place. if (objectValue === undefined && (_implementedDataSourceMethods & CPTableViewDataSource_tableView_objectValueForTableColumn_row_)) { objectValue = [_dataSource tableView:self objectValueForTableColumn:aTableColumn row:aRowIndex]; tableColumnObjectValues[aRowIndex] = objectValue; } return objectValue; } - (CGRect)_exposedRect { var superview = [self superview]; if (![superview isKindOfClass:[CPClipView class]]) return [self bounds]; return [self convertRect:CGRectIntersection([superview bounds], [self frame]) fromView:superview]; } - (void)load { if (_reloadAllRows) { [self _unloadDataViewsInRows:_exposedRows columns:_exposedColumns]; _exposedRows = [CPIndexSet indexSet]; _exposedColumns = [CPIndexSet indexSet]; _reloadAllRows = NO; } var exposedRect = [self _exposedRect], exposedRows = [CPIndexSet indexSetWithIndexesInRange:[self rowsInRect:exposedRect]], exposedColumns = [self columnIndexesInRect:exposedRect], obscuredRows = [_exposedRows copy], obscuredColumns = [_exposedColumns copy]; [obscuredRows removeIndexes:exposedRows]; [obscuredColumns removeIndexes:exposedColumns]; var newlyExposedRows = [exposedRows copy], newlyExposedColumns = [exposedColumns copy]; [newlyExposedRows removeIndexes:_exposedRows]; [newlyExposedColumns removeIndexes:_exposedColumns]; var previouslyExposedRows = [exposedRows copy], previouslyExposedColumns = [exposedColumns copy]; [previouslyExposedRows removeIndexes:newlyExposedRows]; [previouslyExposedColumns removeIndexes:newlyExposedColumns]; [self _unloadDataViewsInRows:previouslyExposedRows columns:obscuredColumns]; [self _unloadDataViewsInRows:obscuredRows columns:previouslyExposedColumns]; [self _unloadDataViewsInRows:obscuredRows columns:obscuredColumns]; [self _loadDataViewsInRows:previouslyExposedRows columns:newlyExposedColumns]; [self _loadDataViewsInRows:newlyExposedRows columns:previouslyExposedColumns]; [self _loadDataViewsInRows:newlyExposedRows columns:newlyExposedColumns]; _exposedRows = exposedRows; _exposedColumns = exposedColumns; [_tableDrawView setFrame:exposedRect]; [self setNeedsDisplay:YES]; // Now clear all the leftovers // FIXME: this could be faster! for (var identifier in _cachedDataViews) { var dataViews = _cachedDataViews[identifier], count = dataViews.length; while (count--) [dataViews[count] removeFromSuperview]; } // if we have any columns to remove do that here if ([_differedColumnDataToRemove count]) { for (var i = 0; i < _differedColumnDataToRemove.length; i++) { var data = _differedColumnDataToRemove[i], column = data.column; [column setHidden:data.shouldBeHidden]; [_tableColumns removeObject:column]; } [_differedColumnDataToRemove removeAllObjects]; } } - (void)_unloadDataViewsInRows:(CPIndexSet)rows columns:(CPIndexSet)columns { if (![rows count] || ![columns count]) return; var rowArray = [], columnArray = []; [rows getIndexes:rowArray maxCount:-1 inIndexRange:nil]; [columns getIndexes:columnArray maxCount:-1 inIndexRange:nil]; var columnIndex = 0, columnsCount = columnArray.length; for (; columnIndex < columnsCount; ++columnIndex) { var column = columnArray[columnIndex], tableColumn = _tableColumns[column], tableColumnUID = [tableColumn UID]; var rowIndex = 0, rowsCount = rowArray.length; for (; rowIndex < rowsCount; ++rowIndex) { var row = rowArray[rowIndex], dataView = _dataViewsForTableColumns[tableColumnUID][row]; _dataViewsForTableColumns[tableColumnUID][row] = nil; [self _enqueueReusableDataView:dataView]; } } } - (void)_loadDataViewsInRows:(CPIndexSet)rows columns:(CPIndexSet)columns { if (![rows count] || ![columns count]) return; var rowArray = [], rowRects = [], columnArray = []; [rows getIndexes:rowArray maxCount:-1 inIndexRange:nil]; [columns getIndexes:columnArray maxCount:-1 inIndexRange:nil]; UPDATE_COLUMN_RANGES_IF_NECESSARY(); var columnIndex = 0, columnsCount = columnArray.length; for (; columnIndex < columnsCount; ++columnIndex) { var column = columnArray[columnIndex], tableColumn = _tableColumns[column]; if ([tableColumn isHidden] || columnIndex === _draggedColumnIndex) continue; var tableColumnUID = [tableColumn UID]; if (!_dataViewsForTableColumns[tableColumnUID]) _dataViewsForTableColumns[tableColumnUID] = []; var rowIndex = 0, rowsCount = rowArray.length; var isColumnSelected = [_selectedColumnIndexes containsIndex:column]; for (; rowIndex < rowsCount; ++rowIndex) { var row = rowArray[rowIndex], dataView = [self _newDataViewForRow:row tableColumn:tableColumn], isButton = [dataView isKindOfClass:[CPButton class]], isTextField = [dataView isKindOfClass:[CPTextField class]]; [dataView setFrame:[self frameOfDataViewAtColumn:column row:row]]; [dataView setObjectValue:[self _objectValueForTableColumn:tableColumn row:row]]; //This gives the table column an opportunity to apply the bindings. //It will override the value set in the data source, if there is a data source. //It will do nothing if there is no value binding set. [tableColumn prepareDataView:dataView forRow:row]; if (isColumnSelected || [self isRowSelected:row]) [dataView setThemeState:CPThemeStateSelectedDataView]; else [dataView unsetThemeState:CPThemeStateSelectedDataView]; if (_implementedDelegateMethods & CPTableViewDelegate_tableView_willDisplayView_forTableColumn_row_) [_delegate tableView:self willDisplayView:dataView forTableColumn:tableColumn row:row]; if ([dataView superview] !== self) [self addSubview:dataView]; _dataViewsForTableColumns[tableColumnUID][row] = dataView; if (isButton || (_editingCellIndex && _editingCellIndex.x === column && _editingCellIndex.y === row)) { if (!isButton) _editingCellIndex = undefined; if (isTextField) { [dataView setEditable:YES]; [dataView setSendsActionOnEndEditing:YES]; [dataView setSelectable:YES]; [dataView selectText:nil]; // Doesn't seem to actually work (yet?). } [dataView setTarget:self]; [dataView setAction:@selector(_commitDataViewObjectValue:)]; dataView.tableViewEditedColumnObj = tableColumn; dataView.tableViewEditedRowIndex = row; } else if (isTextField) { [dataView setEditable:NO]; [dataView setSelectable:NO]; } } } } - (void)_layoutDataViewsInRows:(CPIndexSet)rows columns:(CPIndexSet)columns { var rowArray = [], columnArray = []; [rows getIndexes:rowArray maxCount:-1 inIndexRange:nil]; [columns getIndexes:columnArray maxCount:-1 inIndexRange:nil]; var columnIndex = 0, columnsCount = columnArray.length; for (; columnIndex < columnsCount; ++columnIndex) { var column = columnArray[columnIndex], tableColumn = _tableColumns[column], tableColumnUID = [tableColumn UID], dataViewsForTableColumn = _dataViewsForTableColumns[tableColumnUID], columnRange = _tableColumnRanges[column]; var rowIndex = 0, rowsCount = rowArray.length; for (; rowIndex < rowsCount; ++rowIndex) { var row = rowArray[rowIndex], dataView = dataViewsForTableColumn[row]; [dataView setFrame:[self frameOfDataViewAtColumn:column row:row]]; } } } - (void)_commitDataViewObjectValue:(id)sender { [_dataSource tableView:self setObjectValue:[sender objectValue] forTableColumn:sender.tableViewEditedColumnObj row:sender.tableViewEditedRowIndex]; if ([sender respondsToSelector:@selector(setEditable:)]) [sender setEditable:NO]; } - (CPView)_newDataViewForRow:(CPInteger)aRow tableColumn:(CPTableColumn)aTableColumn { if ((_implementedDelegateMethods & CPTableViewDelegate_tableView_dataViewForTableColumn_row_)) { var dataView = [_delegate tableView:self dataViewForTableColumn:aTableColumn row:aRow]; [aTableColumn setDataView:dataView]; } return [aTableColumn _newDataViewForRow:aRow]; } - (void)_enqueueReusableDataView:(CPView)aDataView { if (!aDataView) return; // FIXME: yuck! var identifier = aDataView.identifier; if (!_cachedDataViews[identifier]) _cachedDataViews[identifier] = [aDataView]; else _cachedDataViews[identifier].push(aDataView); } - (void)setFrameSize:(CGSize)aSize { [super setFrameSize:aSize]; if (_headerView) [_headerView setFrameSize:_CGSizeMake(_CGRectGetWidth([self frame]), _CGRectGetHeight([_headerView frame]))]; } - (CGRect)exposedClipRect { var superview = [self superview]; if (![superview isKindOfClass:[CPClipView class]]) return [self bounds]; return [self convertRect:CGRectIntersection([superview bounds], [self frame]) fromView:superview]; } - (void)setNeedsDisplay:(BOOL)aFlag { [super setNeedsDisplay:aFlag]; [_tableDrawView setNeedsDisplay:aFlag]; } - (void)_drawRect:(CGRect)aRect { // FIX ME: All three of these methods will likely need to be rewritten for 1.0 // We've got grid drawing in highlightSelection and crap everywhere. var exposedRect = [self _exposedRect]; [self drawBackgroundInClipRect:exposedRect]; [self drawGridInClipRect:exposedRect]; [self highlightSelectionInClipRect:exposedRect]; if (_draggedColumnIndex === -1) return; var context = [[CPGraphicsContext currentContext] graphicsPort], columnRect = [self rectOfColumn:_draggedColumnIndex]; CGContextSetFillColor(context, [CPColor grayColor]); CGContextFillRect(context, columnRect); } - (void)drawBackgroundInClipRect:(CGRect)aRect { if (!_usesAlternatingRowBackgroundColors) return; var rowColors = [self alternatingRowBackgroundColors], colorCount = [rowColors count]; if (colorCount === 0) return; var context = [[CPGraphicsContext currentContext] graphicsPort]; if (colorCount === 1) { CGContextSetFillColor(context, rowColors[0]); CGContextFillRect(context, aRect); return; } // CGContextFillRect(context, CGRectIntersection(aRect, fillRect)); // console.profile("row-paint"); var exposedRows = [self rowsInRect:aRect], firstRow = exposedRows.location, lastRow = CPMaxRange(exposedRows) - 1, colorIndex = MIN(exposedRows.length, colorCount), heightFilled = 0.0; while (colorIndex--) { var row = firstRow - firstRow % colorCount + colorIndex, fillRect = nil; CGContextBeginPath(context); for (; row <= lastRow; row += colorCount) if (row >= firstRow) CGContextAddRect(context, CGRectIntersection(aRect, fillRect = [self rectOfRow:row])); if (row - colorCount === lastRow) heightFilled = _CGRectGetMaxY(fillRect); CGContextClosePath(context); CGContextSetFillColor(context, rowColors[colorIndex]); CGContextFillPath(context); } // console.profileEnd("row-paint"); var totalHeight = _CGRectGetMaxY(aRect); if (heightFilled >= totalHeight || _rowHeight <= 0.0) return; var rowHeight = _rowHeight + _intercellSpacing.height, fillRect = _CGRectMake(_CGRectGetMinX(aRect), _CGRectGetMinY(aRect) + heightFilled, _CGRectGetWidth(aRect), rowHeight); for (row = lastRow + 1; heightFilled < totalHeight; ++row) { CGContextSetFillColor(context, rowColors[row % colorCount]); CGContextFillRect(context, fillRect); heightFilled += rowHeight; fillRect.origin.y += rowHeight; } } - (void)drawGridInClipRect:(CGRect)aRect { var context = [[CPGraphicsContext currentContext] graphicsPort], gridStyleMask = [self gridStyleMask]; if (!(gridStyleMask & (CPTableViewSolidHorizontalGridLineMask | CPTableViewSolidVerticalGridLineMask))) return; CGContextBeginPath(context); if (gridStyleMask & CPTableViewSolidHorizontalGridLineMask) { var exposedRows = [self rowsInRect:aRect]; row = exposedRows.location, lastRow = CPMaxRange(exposedRows) - 1, rowY = 0.0, minX = _CGRectGetMinX(aRect), maxX = _CGRectGetMaxX(aRect); for (; row <= lastRow; ++row) { // grab each row rect and add the top and bottom lines var rowRect = [self rectOfRow:row], rowY = _CGRectGetMaxY(rowRect) - 0.5; CGContextMoveToPoint(context, minX, rowY); CGContextAddLineToPoint(context, maxX, rowY); } if (_rowHeight > 0.0) { var rowHeight = _rowHeight + _intercellSpacing.height, totalHeight = _CGRectGetMaxY(aRect); while (rowY < totalHeight) { rowY += rowHeight; CGContextMoveToPoint(context, minX, rowY); CGContextAddLineToPoint(context, maxX, rowY); } } } if (gridStyleMask & CPTableViewSolidVerticalGridLineMask) { var exposedColumnIndexes = [self columnIndexesInRect:aRect], columnsArray = []; [exposedColumnIndexes getIndexes:columnsArray maxCount:-1 inIndexRange:nil]; var columnArrayIndex = 0, columnArrayCount = columnsArray.length, minY = _CGRectGetMinY(aRect), maxY = _CGRectGetMaxY(aRect); for (; columnArrayIndex < columnArrayCount; ++columnArrayIndex) { var columnRect = [self rectOfColumn:columnsArray[columnArrayIndex]], columnX = _CGRectGetMaxX(columnRect) + 0.5; CGContextMoveToPoint(context, columnX, minY); CGContextAddLineToPoint(context, columnX, maxY); } } CGContextClosePath(context); CGContextSetStrokeColor(context, [self gridColor]); CGContextStrokePath(context); } - (void)highlightSelectionInClipRect:(CGRect)aRect { if (_selectionHighlightStyle === CPTableViewSelectionHighlightStyleNone) return; var context = [[CPGraphicsContext currentContext] graphicsPort], indexes = [], rectSelector = @selector(rectOfRow:); if ([_selectedRowIndexes count] >= 1) { var exposedRows = [CPIndexSet indexSetWithIndexesInRange:[self rowsInRect:aRect]], firstRow = [exposedRows firstIndex], exposedRange = CPMakeRange(firstRow, [exposedRows lastIndex] - firstRow + 1); [_selectedRowIndexes getIndexes:indexes maxCount:-1 inIndexRange:exposedRange]; } else if ([_selectedColumnIndexes count] >= 1) { rectSelector = @selector(rectOfColumn:); var exposedColumns = [self columnIndexesInRect:aRect], firstColumn = [exposedColumns firstIndex], exposedRange = CPMakeRange(firstColumn, [exposedColumns lastIndex] - firstColumn + 1); [_selectedColumnIndexes getIndexes:indexes maxCount:-1 inIndexRange:exposedRange]; } var count = count2 = [indexes count]; if (!count) return; var drawGradient = (_selectionHighlightStyle === CPTableViewSelectionHighlightStyleSourceList && [_selectedRowIndexes count] >= 1), deltaHeight = 0.5 * (_gridStyleMask & CPTableViewSolidHorizontalGridLineMask); CGContextBeginPath(context); var gradientCache = [self selectionGradientColors], topLineColor = [gradientCache objectForKey:CPSourceListTopLineColor], bottomLineColor = [gradientCache objectForKey:CPSourceListBottomLineColor], gradientColor = [gradientCache objectForKey:CPSourceListGradient]; while (count--) { var rowRect = CGRectIntersection(objj_msgSend(self, rectSelector, indexes[count]), aRect); CGContextAddRect(context, rowRect); if (drawGradient) { var minX = _CGRectGetMinX(rowRect), minY = _CGRectGetMinY(rowRect), maxX = _CGRectGetMaxX(rowRect), maxY = _CGRectGetMaxY(rowRect) - deltaHeight; CGContextDrawLinearGradient(context, gradientColor, rowRect.origin, CGPointMake(minX, maxY), 0); CGContextClosePath(context); CGContextBeginPath(context); CGContextMoveToPoint(context, minX, minY); CGContextAddLineToPoint(context, maxX, minY); CGContextClosePath(context); CGContextSetStrokeColor(context, topLineColor); CGContextStrokePath(context); CGContextBeginPath(context); CGContextMoveToPoint(context, minX, maxY); CGContextAddLineToPoint(context, maxX, maxY - 1); CGContextClosePath(context); CGContextSetStrokeColor(context, bottomLineColor); CGContextStrokePath(context); } } CGContextClosePath(context); if (!drawGradient) { [[self selectionHighlightColor] setFill]; CGContextFillPath(context); } CGContextBeginPath(context); gridStyleMask = [self gridStyleMask]; for(var i=0; i < count2; i++) { var rect = objj_msgSend(self, rectSelector, indexes[i]), minX = CGRectGetMinX(rect) - 0.5, maxX = CGRectGetMaxX(rect) - 0.5, minY = CGRectGetMinY(rect) - 0.5, maxY = CGRectGetMaxY(rect) - 0.5; if ([_selectedRowIndexes count] >= 1 && gridStyleMask & CPTableViewSolidVerticalGridLineMask) { var exposedColumns = [self columnIndexesInRect:aRect], exposedColumnIndexes = [], firstExposedColumn = [exposedColumns firstIndex], exposedRange = CPMakeRange(firstExposedColumn, [exposedColumns lastIndex] - firstExposedColumn + 1); [exposedColumns getIndexes:exposedColumnIndexes maxCount:-1 inIndexRange:exposedRange]; var exposedColumnCount = [exposedColumnIndexes count]; for(var c = firstExposedColumn; c < exposedColumnCount; c++) { var colRect = [self rectOfColumn:exposedColumnIndexes[c]], colX = CGRectGetMaxX(colRect) + 0.5; CGContextMoveToPoint(context, colX, minY); CGContextAddLineToPoint(context, colX, maxY); } } //if the row after the current row is not selected then there is no need to draw the bottom grid line white. if([indexes containsObject:indexes[i]+1]) { CGContextMoveToPoint(context, minX, maxY); CGContextAddLineToPoint(context, maxX, maxY); } } CGContextClosePath(context); CGContextSetStrokeColor(context, [self currentValueForThemeAttribute:"highlighted-grid-color"]); CGContextStrokePath(context); } - (void)layoutSubviews { [self load]; } - (void)viewWillMoveToSuperview:(CPView)aView { var superview = [self superview], defaultCenter = [CPNotificationCenter defaultCenter]; if (superview) { [defaultCenter removeObserver:self name:CPViewFrameDidChangeNotification object:superview]; [defaultCenter removeObserver:self name:CPViewBoundsDidChangeNotification object:superview]; } if (aView) { [aView setPostsFrameChangedNotifications:YES]; [aView setPostsBoundsChangedNotifications:YES]; [defaultCenter addObserver:self selector:@selector(superviewFrameChanged:) name:CPViewFrameDidChangeNotification object:aView]; [defaultCenter addObserver:self selector:@selector(superviewBoundsChanged:) name:CPViewBoundsDidChangeNotification object:aView]; } } - (void)superviewBoundsChanged:(CPNotification)aNotification { [self setNeedsDisplay:YES]; [self setNeedsLayout]; } - (void)superviewFrameChanged:(CPNotification)aNotification { [self tile]; } /* @ignore */ - (BOOL)tracksMouseOutsideOfFrame { return YES; } /* @ignore */ - (BOOL)startTrackingAt:(CGPoint)aPoint { var row = [self rowAtPoint:aPoint]; //if the user clicks outside a row then deslect everything if (row < 0 && _allowsEmptySelection) [self selectRowIndexes:[CPIndexSet indexSet] byExtendingSelection:NO]; [self _noteSelectionIsChanging]; if ([self mouseDownFlags] & CPShiftKeyMask) _selectionAnchorRow = (ABS([_selectedRowIndexes firstIndex] - row) < ABS([_selectedRowIndexes lastIndex] - row)) ? [_selectedRowIndexes firstIndex] : [_selectedRowIndexes lastIndex]; else _selectionAnchorRow = row; //set ivars for startTrackingPoint and time... _startTrackingPoint = aPoint; _startTrackingTimestamp = new Date(); if (_implementedDataSourceMethods & CPTableViewDataSource_tableView_setObjectValue_forTableColumn_row_) _trackingPointMovedOutOfClickSlop = NO; // if the table has drag support then we use mouseUp to select a single row. // otherwise it uses mouse down. if (row >=0 && !(_implementedDataSourceMethods & CPTableViewDataSource_tableView_writeRowsWithIndexes_toPasteboard_)) [self _updateSelectionWithMouseAtRow:row]; [[self window] makeFirstResponder:self]; return YES; } /* @ignore */ - (void)trackMouse:(CPEvent)anEvent { // Prevent CPControl from eating the mouse events when we are in a drag session if (![_draggedRowIndexes count]) { [self autoscroll:anEvent]; [super trackMouse:anEvent]; } else [CPApp sendEvent:anEvent]; } /* @ignore */ - (BOOL)continueTracking:(CGPoint)lastPoint at:(CGPoint)aPoint { var row = [self rowAtPoint:aPoint]; // begin the drag is the datasource lets us, we've move at least +-3px vertical or horizontal, // or we're dragging from selected rows and we haven't begun a drag session if(!_isSelectingSession && _implementedDataSourceMethods & CPTableViewDataSource_tableView_writeRowsWithIndexes_toPasteboard_) { if (row >= 0 && (ABS(_startTrackingPoint.x - aPoint.x) > 3 || (_verticalMotionCanDrag && ABS(_startTrackingPoint.y - aPoint.y) > 3)) || ([_selectedRowIndexes containsIndex:row])) { if ([_selectedRowIndexes containsIndex:row]) _draggedRowIndexes = [[CPIndexSet alloc] initWithIndexSet:_selectedRowIndexes]; else _draggedRowIndexes = [CPIndexSet indexSetWithIndex:row]; //ask the datasource for the data var pboard = [CPPasteboard pasteboardWithName:CPDragPboard]; if ([self canDragRowsWithIndexes:_draggedRowIndexes atPoint:aPoint] && [_dataSource tableView:self writeRowsWithIndexes:_draggedRowIndexes toPasteboard:pboard]) { var currentEvent = [CPApp currentEvent], offset = CPPointMakeZero(), tableColumns = [_tableColumns objectsAtIndexes:_exposedColumns]; // We deviate from the default Cocoa implementation here by asking for a view in stead of an image // We support both, but the view prefered over the image because we can mimic the rows we are dragging // by re-creating the data views for the dragged rows var view = [self dragViewForRowsWithIndexes:_draggedRowIndexes tableColumns:tableColumns event:currentEvent offset:offset]; if (!view) { var image = [self dragImageForRowsWithIndexes:_draggedRowIndexes tableColumns:tableColumns event:currentEvent offset:offset]; view = [[CPImageView alloc] initWithFrame:CPMakeRect(0, 0, [image size].width, [image size].height)]; [view setImage:image]; } var bounds = [view bounds]; var viewLocation = CPPointMake(aPoint.x - CGRectGetWidth(bounds)/2 + offset.x, aPoint.y - CGRectGetHeight(bounds)/2 + offset.y); [self dragView:view at:viewLocation offset:CPPointMakeZero() event:[CPApp currentEvent] pasteboard:pboard source:self slideBack:YES]; _startTrackingPoint = nil; return NO; } // The delegate disallowed the drag so clear the dragged row indexes _draggedRowIndexes = [CPIndexSet indexSet]; } else if (ABS(_startTrackingPoint.x - aPoint.x) < 5 && ABS(_startTrackingPoint.y - aPoint.y) < 5) return YES; } _isSelectingSession = YES; if(row >= 0 && row !== _lastTrackedRowIndex) { _lastTrackedRowIndex = row; [self _updateSelectionWithMouseAtRow:row]; } if ((_implementedDataSourceMethods & CPTableViewDataSource_tableView_setObjectValue_forTableColumn_row_) && !_trackingPointMovedOutOfClickSlop) { var CLICK_SPACE_DELTA = 5.0; // Stolen from AppKit/Platform/DOM/CPPlatformWindow+DOM.j if (ABS(aPoint.x - _startTrackingPoint.x) > CLICK_SPACE_DELTA || ABS(aPoint.y - _startTrackingPoint.y) > CLICK_SPACE_DELTA) { _trackingPointMovedOutOfClickSlop = YES; } } return YES; } /*! @ignore */ - (void)stopTracking:(CGPoint)lastPoint at:(CGPoint)aPoint mouseIsUp:(BOOL)mouseIsUp { _isSelectingSession = NO; var CLICK_TIME_DELTA = 1000, columnIndex, column, rowIndex, shouldEdit = YES; if(_implementedDataSourceMethods & CPTableViewDataSource_tableView_writeRowsWithIndexes_toPasteboard_) { rowIndex = [self rowAtPoint:aPoint]; if (rowIndex !== -1) { if ([_draggedRowIndexes count] > 0) { _draggedRowIndexes = [CPIndexSet indexSet]; return; } // if the table has drag support then we use mouseUp to select a single row. _previouslySelectedRowIndexes = [_selectedRowIndexes copy]; [self _updateSelectionWithMouseAtRow:rowIndex]; } } if (mouseIsUp && (_implementedDataSourceMethods & CPTableViewDataSource_tableView_setObjectValue_forTableColumn_row_) && !_trackingPointMovedOutOfClickSlop && ([[CPApp currentEvent] clickCount] > 1)) { columnIndex = [self columnAtPoint:lastPoint]; if (columnIndex !== -1) { column = _tableColumns[columnIndex]; if ([column isEditable]) { rowIndex = [self rowAtPoint:aPoint]; if (rowIndex !== -1) { if (_implementedDelegateMethods & CPTableViewDelegate_tableView_shouldEditTableColumn_row_) shouldEdit = [_delegate tableView:self shouldEditTableColumn:column row:rowIndex]; if (shouldEdit) { [self editColumn:columnIndex row:rowIndex withEvent:nil select:YES]; return; } } } } } //end of editing conditional //double click actions if([[CPApp currentEvent] clickCount] === 2 && _doubleAction) { _clickedRow = [self rowAtPoint:aPoint]; [self sendAction:_doubleAction to:_target]; } } /* @ignore */ - (CPDragOperation)draggingEntered:(id)sender { var location = [self convertPoint:[sender draggingLocation] fromView:nil], dropOperation = [self _proposedDropOperationAtPoint:location], row = [self _proposedRowAtPoint:location]; if(_retargetedDropRow !== nil) row = _retargetedDropRow; var draggedTypes = [self registeredDraggedTypes], count = [draggedTypes count], i = 0; for (; i < count; i++) { if ([[[sender draggingPasteboard] types] containsObject:[draggedTypes objectAtIndex: i]]) return [self _validateDrop:sender proposedRow:row proposedDropOperation:dropOperation]; } return CPDragOperationNone; } /* @ignore */ - (void)draggingExited:(id)sender { [_dropOperationFeedbackView removeFromSuperview]; } /* @ignore */ - (void)draggingEnded:(id)sender { [self _draggingEnded]; } - (void)_draggingEnded { _retargetedDropOperation = nil; _retargetedDropRow = nil; _draggedRowIndexes = [CPIndexSet indexSet]; [_dropOperationFeedbackView removeFromSuperview]; } /* @ignore */ - (BOOL)wantsPeriodicDraggingUpdates { return YES; } /* @ignore */ - (CPTableViewDropOperation)_proposedDropOperationAtPoint:(CGPoint)theDragPoint { if(_retargetedDropOperation !== nil) return _retargetedDropOperation; var row = [self _proposedRowAtPoint:theDragPoint], rowRect = [self rectOfRow:row]; // If there is no (the default) or to little inter cell spacing we create some room for the CPTableViewDropAbove indicator // This probably doesn't work if the row height is smaller than or around 5.0 if ([self intercellSpacing].height < 5.0) rowRect = CPRectInset(rowRect, 0.0, 5.0 - [self intercellSpacing].height); // If the altered row rect contains the drag point we show the drop on // We don't show the drop on indicator if we are dragging below the last row // in that case we always want to show the drop above indicator if (CGRectContainsPoint(rowRect, theDragPoint) && row < _numberOfRows) return CPTableViewDropOn; return CPTableViewDropAbove; } /* @ignore */ - (CPInteger)_proposedRowAtPoint:(CGPoint)dragPoint { // We don't use rowAtPoint here because the drag indicator can appear below the last row // and rowAtPoint doesn't return rows that are larger than numberOfRows // FIX ME: this is going to break when we implement variable row heights... var row = FLOOR(dragPoint.y / ( _rowHeight + _intercellSpacing.height )), // Determine if the mouse is currently closer to this row or the row below it lowerRow = row + 1, rect = [self rectOfRow:row], bottomPoint = CGRectGetMaxY(rect), bottomThirty = bottomPoint - ((bottomPoint - CGRectGetMinY(rect)) * 0.3); if (dragPoint.y > MAX(bottomThirty, bottomPoint - 6)) row = lowerRow; if (row >= [self numberOfRows]) row = [self numberOfRows]; return row; } - (void)_validateDrop:(id)info proposedRow:(CPInteger)row proposedDropOperation:(CPTableViewDropOperation)dropOperation { if(_implementedDataSourceMethods & CPTableViewDataSource_tableView_validateDrop_proposedRow_proposedDropOperation_) return [_dataSource tableView:self validateDrop:info proposedRow:row proposedDropOperation:dropOperation]; return CPDragOperationNone; } - (CPRect)_rectForDropHighlightViewOnRow:(int)theRowIndex { if (theRowIndex >= [self numberOfRows]) theRowIndex = [self numberOfRows] - 1; return [self rectOfRow:theRowIndex]; } - (CPRect)_rectForDropHighlightViewBetweenUpperRow:(int)theUpperRowIndex andLowerRow:(int)theLowerRowIndex offset:(CPPoint)theOffset { if (theLowerRowIndex > [self numberOfRows]) theLowerRowIndex = [self numberOfRows]; return [self rectOfRow:theLowerRowIndex]; } - (CPDragOperation)draggingUpdated:(id)sender { var location = [self convertPoint:[sender draggingLocation] fromView:nil], dropOperation = [self _proposedDropOperationAtPoint:location], numberOfRows = [self numberOfRows]; var row = [self _proposedRowAtPoint:location], dragOperation = [self _validateDrop:sender proposedRow:row proposedDropOperation:dropOperation]; exposedClipRect = [self exposedClipRect]; if(_retargetedDropRow !== nil) row = _retargetedDropRow; if (dropOperation === CPTableViewDropOn && row >= [self numberOfRows]) row = [self numberOfRows] - 1; var rect = CPRectMakeZero(); if (row === -1) rect = exposedClipRect; else if (dropOperation === CPTableViewDropAbove) rect = [self _rectForDropHighlightViewBetweenUpperRow:row - 1 andLowerRow:row offset:location]; else rect = [self _rectForDropHighlightViewOnRow:row]; [_dropOperationFeedbackView setDropOperation:row !== -1 ? dropOperation : CPDragOperationNone]; [_dropOperationFeedbackView setHidden:(dragOperation == CPDragOperationNone)]; [_dropOperationFeedbackView setFrame:rect]; [_dropOperationFeedbackView setCurrentRow:row]; [self addSubview:_dropOperationFeedbackView]; return dragOperation; } /* @ignore */ - (BOOL)prepareForDragOperation:(id)sender { // FIX ME: is there anything else that needs to happen here? // actual validation is called in dragginUpdated: [_dropOperationFeedbackView removeFromSuperview]; return (_implementedDataSourceMethods & CPTableViewDataSource_tableView_validateDrop_proposedRow_proposedDropOperation_); } /* @ignore */ - (BOOL)performDragOperation:(id)sender { var location = [self convertPoint:[sender draggingLocation] fromView:nil]; operation = [self _proposedDropOperationAtPoint:location], row = _retargetedDropRow; if(row === nil) var row = [self _proposedRowAtPoint:location]; return [_dataSource tableView:self acceptDrop:sender row:row dropOperation:operation]; } /* @ignore */ - (void)concludeDragOperation:(id)sender { [self reloadData]; } /* //this method is sent to the data source for conviences... */ - (void)draggedImage:(CPImage)anImage endedAt:(CGPoint)aLocation operation:(CPDragOperation)anOperation { if([_dataSource respondsToSelector:@selector(tableView:didEndDraggedImage:atPosition:operation:)]) [_dataSource tableView:self didEndDraggedImage:anImage atPosition:aLocation operation:anOperation]; } /* @ignore we're using this because we drag views instead of images so we can get the rows themselves to actually drag */ - (void)draggedView:(CPImage)aView endedAt:(CGPoint)aLocation operation:(CPDragOperation)anOperation { [self _draggingEnded]; [self draggedImage:aView endedAt:aLocation operation:anOperation]; } - (void)_updateSelectionWithMouseAtRow:(CPInteger)aRow { //check to make sure the row exists if(aRow < 0) return; var newSelection, shouldExtendSelection = NO; // If cmd/ctrl was held down XOR the old selection with the proposed selection if ([self mouseDownFlags] & (CPCommandKeyMask | CPControlKeyMask | CPAlternateKeyMask)) { if ([_selectedRowIndexes containsIndex:aRow]) { newSelection = [_selectedRowIndexes copy]; [newSelection removeIndex:aRow]; } else if (_allowsMultipleSelection) { newSelection = [_selectedRowIndexes copy]; [newSelection addIndex:aRow]; } else newSelection = [CPIndexSet indexSetWithIndex:aRow]; } else if (_allowsMultipleSelection) { newSelection = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(MIN(aRow, _selectionAnchorRow), ABS(aRow - _selectionAnchorRow) + 1)]; shouldExtendSelection = [self mouseDownFlags] & CPShiftKeyMask && ((_lastSelectedRow == [_selectedRowIndexes lastIndex] && aRow > _lastSelectedRow) || (_lastSelectedRow == [_selectedRowIndexes firstIndex] && aRow < _lastSelectedRow)); } else if (aRow >= 0 && aRow < _numberOfRows) newSelection = [CPIndexSet indexSetWithIndex:aRow]; else newSelection = [CPIndexSet indexSet]; if ([newSelection isEqualToIndexSet:_selectedRowIndexes]) return; if (_implementedDelegateMethods & CPTableViewDelegate_selectionShouldChangeInTableView_ && ![_delegate selectionShouldChangeInTableView:self]) return; if (_implementedDelegateMethods & CPTableViewDelegate_tableView_selectionIndexesForProposedSelection_) newSelection = [_delegate tableView:self selectionIndexesForProposedSelection:newSelection]; if (_implementedDelegateMethods & CPTableViewDelegate_tableView_shouldSelectRow_) { var indexArray = []; [newSelection getIndexes:indexArray maxCount:-1 inIndexRange:nil]; var indexCount = indexArray.length; while (indexCount--) { var index = indexArray[indexCount]; if (![_delegate tableView:self shouldSelectRow:index]) [newSelection removeIndex:index]; } } // if empty selection is not allowed and the new selection has nothing selected, abort if (!_allowsEmptySelection && [newSelection count] === 0) return; if ([newSelection isEqualToIndexSet:_selectedRowIndexes]) return; [self selectRowIndexes:newSelection byExtendingSelection:shouldExtendSelection]; } - (void)_noteSelectionIsChanging { [[CPNotificationCenter defaultCenter] postNotificationName:CPTableViewSelectionIsChangingNotification object:self userInfo:nil]; } - (void)_noteSelectionDidChange { [[CPNotificationCenter defaultCenter] postNotificationName:CPTableViewSelectionDidChangeNotification object:self userInfo:nil]; } - (BOOL)becomeFirstResponder { return YES; } - (BOOL)acceptsFirstResponder { return YES; } - (void)keyDown:(CPEvent)anEvent { [self interpretKeyEvents:[anEvent]]; } - (void)moveDown:(id)sender { if (_implementedDelegateMethods & CPTableViewDelegate_selectionShouldChangeInTableView_ && ![_delegate selectionShouldChangeInTableView:self]) return; var anEvent = [CPApp currentEvent]; if([[self selectedRowIndexes] count] > 0) { var extend = NO; if(([anEvent modifierFlags] & CPShiftKeyMask) && _allowsMultipleSelection) extend = YES; var i = [[self selectedRowIndexes] lastIndex]; if(i<[self numberOfRows] - 1) i++; //set index to the next row after the last row selected } else { var extend = NO; //no rows are currently selected if([self numberOfRows] > 0) var i = 0; //select the first row } if(_implementedDelegateMethods & CPTableViewDelegate_tableView_shouldSelectRow_) { while((![_delegate tableView:self shouldSelectRow:i]) && i<[self numberOfRows]) { //check to see if the row can be selected if it can't be then see if the next row can be selected i++; } //if the index still can be selected after the loop then just return if(![_delegate tableView:self shouldSelectRow:i]) return; } [self selectRowIndexes:[CPIndexSet indexSetWithIndex:i] byExtendingSelection:extend]; if(i >= 0) [self scrollRowToVisible:i]; } - (void)moveDownAndModifySelection:(id)sender { [self moveDown:sender]; } - (void)moveUp:(id)sender { if (_implementedDelegateMethods & CPTableViewDelegate_selectionShouldChangeInTableView_ && ![_delegate selectionShouldChangeInTableView:self]) return; var anEvent = [CPApp currentEvent]; if([[self selectedRowIndexes] count] > 0) { var extend = NO; if(([anEvent modifierFlags] & CPShiftKeyMask) && _allowsMultipleSelection) extend = YES; var i = [[self selectedRowIndexes] firstIndex]; if(i > 0) i--; //set index to the prev row before the first row selected } else { var extend = NO; //no rows are currently selected if([self numberOfRows] > 0) var i = [self numberOfRows] - 1; //select the first row } if(_implementedDelegateMethods & CPTableViewDelegate_tableView_shouldSelectRow_) { while((![_delegate tableView:self shouldSelectRow:i]) && i > 0) { //check to see if the row can be selected if it can't be then see if the prev row can be selected i--; } //if the index still can be selected after the loop then just return if(![_delegate tableView:self shouldSelectRow:i]) return; } [self selectRowIndexes:[CPIndexSet indexSetWithIndex:i] byExtendingSelection:extend]; if(i >= 0) [self scrollRowToVisible:i]; } - (void)moveUpAndModifySelection:(id)sender { [self moveUp:sender]; } - (void)deleteBackward:(id)sender { if([_delegate respondsToSelector: @selector(tableViewDeleteKeyPressed:)]) [_delegate tableViewDeleteKeyPressed:self]; } @end @implementation CPTableView (Bindings) - (CPString)_replacementKeyPathForBinding:(CPString)aBinding { if (aBinding === @"selectionIndexes") return @"selectedRowIndexes"; return [super _replacementKeyPathForBinding:aBinding]; } - (void)_establishBindingsIfUnbound:(id)destination { if ([[self infoForBinding:@"content"] objectForKey:CPObservedObjectKey] !== destination) [self bind:@"content" toObject:destination withKeyPath:@"arrangedObjects" options:nil]; if ([[self infoForBinding:@"selectionIndexes"] objectForKey:CPObservedObjectKey] !== destination) [self bind:@"selectionIndexes" toObject:destination withKeyPath:@"selectionIndexes" options:nil]; //[self bind:@"sortDescriptors" toObject:destination withKeyPath:@"sortDescriptors" options:nil]; } - (void)setContent:(CPArray)content { [self reloadData]; } @end var CPTableViewDataSourceKey = @"CPTableViewDataSourceKey", CPTableViewDelegateKey = @"CPTableViewDelegateKey", CPTableViewHeaderViewKey = @"CPTableViewHeaderViewKey", CPTableViewTableColumnsKey = @"CPTableViewTableColumnsKey", CPTableViewRowHeightKey = @"CPTableViewRowHeightKey", CPTableViewIntercellSpacingKey = @"CPTableViewIntercellSpacingKey", CPTableViewMultipleSelectionKey = @"CPTableViewMultipleSelectionKey", CPTableViewEmptySelectionKey = @"CPTableViewEmptySelectionKey", CPTableViewColumnReorderingKey = @"CPTableViewColumnReorderingKey", CPTableViewColumnResizingKey = @"CPTableViewColumnResizingKey", CPTableViewColumnSelectionKey = @"CPTableViewColumnSelectionKey", CPTableViewGridColorKey = @"CPTableViewGridColorKey", CPTableViewGridStyleMaskKey = @"CPTableViewGridStyleMaskKey", CPTableViewUsesAlternatingBackgroundKey = @"CPTableViewUsesAlternatingBackgroundKey", CPTableViewAlternatingRowColorsKey = @"CPTableViewAlternatingRowColorsKey", CPTableViewHeaderViewKey = @"CPTableViewHeaderViewKey", CPTableViewCornerViewKey = @"CPTableViewCornerViewKey"; @implementation CPTableView (CPCoding) - (id)initWithCoder:(CPCoder)aCoder { self = [super initWithCoder:aCoder]; if (self) { //Configuring Behavior _allowsColumnReordering = [aCoder decodeBoolForKey:CPTableViewColumnReorderingKey]; _allowsColumnResizing = [aCoder decodeBoolForKey:CPTableViewColumnResizingKey]; _allowsMultipleSelection = [aCoder decodeBoolForKey:CPTableViewMultipleSelectionKey]; _allowsEmptySelection = [aCoder decodeBoolForKey:CPTableViewEmptySelectionKey]; _allowsColumnSelection = [aCoder decodeBoolForKey:CPTableViewColumnSelectionKey]; //Setting Display Attributes _selectionHighlightStyle = CPTableViewSelectionHighlightStyleRegular; _tableColumns = [aCoder decodeObjectForKey:CPTableViewTableColumnsKey] || []; [_tableColumns makeObjectsPerformSelector:@selector(setTableView:) withObject:self]; if ([aCoder containsValueForKey:CPTableViewRowHeightKey]) _rowHeight = [aCoder decodeFloatForKey:CPTableViewRowHeightKey]; else _rowHeight = 23.0; _intercellSpacing = [aCoder decodeSizeForKey:CPTableViewIntercellSpacingKey] || _CGSizeMake(0.0, 0.0); [self setGridColor:[aCoder decodeObjectForKey:CPTableViewGridColorKey] || [CPColor grayColor]]; _gridStyleMask = [aCoder decodeIntForKey:CPTableViewGridStyleMaskKey] || CPTableViewGridNone; _usesAlternatingRowBackgroundColors = [aCoder decodeObjectForKey:CPTableViewUsesAlternatingBackgroundKey]; [self setAlternatingRowBackgroundColors:[aCoder decodeObjectForKey:CPTableViewAlternatingRowColorsKey]]; _headerView = [aCoder decodeObjectForKey:CPTableViewHeaderViewKey]; _cornerView = [aCoder decodeObjectForKey:CPTableViewCornerViewKey]; _dataSource = [aCoder decodeObjectForKey:CPTableViewDataSourceKey]; _delegate = [aCoder decodeObjectForKey:CPTableViewDelegateKey]; [self _init]; [self viewWillMoveToSuperview:[self superview]]; } return self; } - (void)encodeWithCoder:(CPCoder)aCoder { [super encodeWithCoder:aCoder]; [aCoder encodeObject:_dataSource forKey:CPTableViewDataSourceKey]; [aCoder encodeObject:_delegate forKey:CPTableViewDelegateKey]; [aCoder encodeFloat:_rowHeight forKey:CPTableViewRowHeightKey]; [aCoder encodeSize:_intercellSpacing forKey:CPTableViewIntercellSpacingKey]; [aCoder encodeBool:_allowsMultipleSelection forKey:CPTableViewMultipleSelectionKey]; [aCoder encodeBool:_allowsEmptySelection forKey:CPTableViewEmptySelectionKey]; [aCoder encodeBool:_allowsColumnReordering forKey:CPTableViewColumnReorderingKey]; [aCoder encodeBool:_allowsColumnResizing forKey:CPTableViewColumnResizingKey]; [aCoder encodeBool:_allowsColumnSelection forKey:CPTableViewColumnSelectionKey]; [aCoder encodeObject:_tableColumns forKey:CPTableViewTableColumnsKey]; [aCoder encodeObject:[self gridColor] forKey:CPTableViewGridColorKey]; [aCoder encodeInt:_gridStyleMask forKey:CPTableViewGridStyleMaskKey]; [aCoder encodeBool:_usesAlternatingRowBackgroundColors forKey:CPTableViewUsesAlternatingBackgroundKey]; [aCoder encodeObject:[self alternatingRowBackgroundColors] forKey:CPTableViewAlternatingRowColorsKey] [aCoder encodeObject:_cornerView forKey:CPTableViewCornerViewKey]; [aCoder encodeObject:_headerView forKey:CPTableViewHeaderViewKey]; } @end @implementation CPIndexSet (tableview) - (void)removeMatches:otherSet { var firstindex = [self firstIndex]; var index = MIN(firstindex,[otherSet firstIndex]); var switchFlag = (index == firstindex); while(index != CPNotFound) { var indexSet = (switchFlag) ? otherSet : self; otherIndex = [indexSet indexGreaterThanOrEqualToIndex:index]; if (otherIndex == index) { [self removeIndex:index]; [otherSet removeIndex:index]; } index = otherIndex; switchFlag = !switchFlag; } } @end @implementation _CPDropOperationDrawingView : CPView { unsigned dropOperation @accessors; CPTableView tableView @accessors; int currentRow @accessors; BOOL isBlinking @accessors; } - (void)drawRect:(CGRect)aRect { if(tableView._destinationDragStyle === CPTableViewDraggingDestinationFeedbackStyleNone || isBlinking) return; var context = [[CPGraphicsContext currentContext] graphicsPort]; CGContextSetStrokeColor(context, [CPColor colorWithHexString:@"4886ca"]); CGContextSetLineWidth(context, 3); if (currentRow === -1) { CGContextStrokeRect(context, [self bounds]); } else if (dropOperation === CPTableViewDropOn) { //if row is selected don't fill and stroke white var selectedRows = [tableView selectedRowIndexes], newRect = _CGRectMake(aRect.origin.x + 2, aRect.origin.y + 2, aRect.size.width - 4, aRect.size.height - 5); if([selectedRows containsIndex:currentRow]) { CGContextSetLineWidth(context, 2); CGContextSetStrokeColor(context, [CPColor whiteColor]); } else { CGContextSetFillColor(context, [CPColor colorWithRed:72/255 green:134/255 blue:202/255 alpha:0.25]); CGContextFillRoundedRectangleInRect(context, newRect, 8, YES, YES, YES, YES); } CGContextStrokeRoundedRectangleInRect(context, newRect, 8, YES, YES, YES, YES); } else if (dropOperation === CPTableViewDropAbove) { //reposition the view up a tad [self setFrameOrigin:CGPointMake(_frame.origin.x, _frame.origin.y - 8)]; var selectedRows = [tableView selectedRowIndexes]; if([selectedRows containsIndex:currentRow - 1] || [selectedRows containsIndex:currentRow]) { CGContextSetStrokeColor(context, [CPColor whiteColor]); CGContextSetLineWidth(context, 4); //draw the circle thing CGContextStrokeEllipseInRect(context, _CGRectMake(aRect.origin.x + 4, aRect.origin.y + 4, 8, 8)); //then draw the line CGContextBeginPath(context); CGContextMoveToPoint(context, 10, aRect.origin.y + 8); CGContextAddLineToPoint(context, aRect.size.width - aRect.origin.y - 8, aRect.origin.y + 8); CGContextClosePath(context); CGContextStrokePath(context); CGContextSetStrokeColor(context, [CPColor colorWithHexString:@"4886ca"]); CGContextSetLineWidth(context, 3); } //draw the circle thing CGContextStrokeEllipseInRect(context, _CGRectMake(aRect.origin.x + 4, aRect.origin.y + 4, 8, 8)); //then draw the line CGContextBeginPath(context); CGContextMoveToPoint(context, 10, aRect.origin.y + 8); CGContextAddLineToPoint(context, aRect.size.width - aRect.origin.y - 8, aRect.origin.y + 8); CGContextClosePath(context); CGContextStrokePath(context); //CGContextStrokeLineSegments(context, [aRect.origin.x + 8, aRect.origin.y + 8, 300 , aRect.origin.y + 8]); } } - (void)blink { if (dropOperation !== CPTableViewDropOn) return; isBlinking = YES; var showCallback = function() { objj_msgSend(self, "setHidden:", NO) isBlinking = NO; } var hideCallback = function() { objj_msgSend(self, "setHidden:", YES) isBlinking = YES; } objj_msgSend(self, "setHidden:", YES); [CPTimer scheduledTimerWithTimeInterval:0.1 callback:showCallback repeats:NO]; [CPTimer scheduledTimerWithTimeInterval:0.19 callback:hideCallback repeats:NO]; [CPTimer scheduledTimerWithTimeInterval:0.27 callback:showCallback repeats:NO]; } @end