/* * 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 "CPDragServer_Constants.j" @import "CGGradient.j" @import "CPCib.j" @import "CPCompatibility.j" @import "CPControl.j" @import "CPImageView.j" @import "CPScroller.j" @import "CPScrollView.j" @import "CPTableColumn.j" @import "CPTableHeaderView.j" @import "CPText.j" @import "_CPCornerView.j" @class CPButton @class CPClipView @class CPUserDefaults @class CPTableHeaderView @class _CPTableColumnHeaderView @class CPClipView @class CPButton @class _CPDropOperationDrawingView @global CPApp CPTableViewColumnDidMoveNotification = @"CPTableViewColumnDidMoveNotification"; CPTableViewColumnDidResizeNotification = @"CPTableViewColumnDidResizeNotification"; CPTableViewSelectionDidChangeNotification = @"CPTableViewSelectionDidChangeNotification"; CPTableViewSelectionIsChangingNotification = @"CPTableViewSelectionIsChangingNotification"; 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_viewForTableColumn_row_ = 1 << 1, CPTableViewDelegate_tableView_dataViewForTableColumn_row_ = 1 << 2, CPTableViewDelegate_tableView_didClickTableColumn_ = 1 << 3, CPTableViewDelegate_tableView_didDragTableColumn_ = 1 << 4, CPTableViewDelegate_tableView_heightOfRow_ = 1 << 5, CPTableViewDelegate_tableView_isGroupRow_ = 1 << 6, CPTableViewDelegate_tableView_mouseDownInHeaderOfTableColumn_ = 1 << 7, CPTableViewDelegate_tableView_nextTypeSelectMatchFromRow_toRow_forString_ = 1 << 8, CPTableViewDelegate_tableView_selectionIndexesForProposedSelection_ = 1 << 9, CPTableViewDelegate_tableView_shouldEditTableColumn_row_ = 1 << 10, CPTableViewDelegate_tableView_shouldSelectRow_ = 1 << 11, CPTableViewDelegate_tableView_shouldSelectTableColumn_ = 1 << 12, CPTableViewDelegate_tableView_shouldShowViewExpansionForTableColumn_row_ = 1 << 13, CPTableViewDelegate_tableView_shouldTrackView_forTableColumn_row_ = 1 << 14, CPTableViewDelegate_tableView_shouldTypeSelectForEvent_withCurrentSearchString_ = 1 << 15, CPTableViewDelegate_tableView_toolTipForView_rect_tableColumn_row_mouseLocation_ = 1 << 16, CPTableViewDelegate_tableView_typeSelectStringForTableColumn_row_ = 1 << 17, CPTableViewDelegate_tableView_willDisplayView_forTableColumn_row_ = 1 << 18, CPTableViewDelegate_tableView_willRemoveView_forTableColumn_row_ = 1 << 19, CPTableViewDelegate_tableViewColumnDidMove_ = 1 << 20, CPTableViewDelegate_tableViewColumnDidResize_ = 1 << 21, CPTableViewDelegate_tableViewSelectionDidChange_ = 1 << 22, CPTableViewDelegate_tableViewSelectionIsChanging_ = 1 << 23, CPTableViewDelegate_tableViewMenuForTableColumn_row_ = 1 << 24, CPTableViewDelegate_tableView_shouldReorderColumn_toColumn_ = 1 << 25; //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]; #define FULL_ROW_HEIGHT() (_rowHeight + _intercellSpacing.height) #define ROW_BOTTOM(__heightInfo) (__heightInfo.y + __heightInfo.height + _intercellSpacing.height) #define HAS_VARIABLE_ROW_HEIGHTS() (_implementedDelegateMethods & CPTableViewDelegate_tableView_heightOfRow_) @protocol CPTableViewDataSource @optional - (BOOL)tableView:(CPTableView)aTableView acceptDrop:(id )info row:(CPInteger)aRowIndex dropOperation:(CPTableViewDropOperation)operation; - (BOOL)tableView:(CPTableView)aTableView writeRowsWithIndexes:(CPIndexSet)rowIndexes toPasteboard:(CPPasteboard)pboard; - (CPArray)tableView:(CPTableView)aTableView namesOfPromisedFilesDroppedAtDestination:(CPURL)dropDestination forDraggedRowsWithIndexes:(CPIndexSet)anIndexSet; - (CPDragOperation)tableView:(CPTableView)aTableView validateDrop:(id )info proposedRow:(CPInteger)aRowIndex proposedDropOperation:(CPTableViewDropOperation)anOperation; - (CPInteger)numberOfRowsInTableView:(CPTableView)aTableView; - (id)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex; - (void)tableView:(CPTableView)aTableView setObjectValue:(id)anObjectValue forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex; - (void)tableView:(CPTableView)aTableView sortDescriptorsDidChange:(CPArray)oldDescriptors; @end @protocol CPTableViewDelegate @optional - (BOOL)selectionShouldChangeInTableView:(CPTableView)aTableView; - (BOOL)tableView:(CPTableView)aTableView isGroupRow:(CPInteger)aRowIndex; - (BOOL)tableView:(CPTableView)aTableView shouldEditTableColumn:(CPTableColumn)aTableView row:(CPInteger)aRowIndex; - (BOOL)tableView:(CPTableView)aTableView shouldReorderColumn:(CPInteger)columnIndex toColumn:(NSInteger)newColumnIndex; - (BOOL)tableView:(CPTableView)aTableView shouldSelectRow:(CPInteger)aRowIndex; - (BOOL)tableView:(CPTableView)aTableView shouldSelectTableColumn:(CPTableColumn)aTableColumn; - (BOOL)tableView:(CPTableView)aTableView shouldShowViewExpansionForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex; - (BOOL)tableView:(CPTableView)aTableView shouldTrackView:(CPView)aView forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex; - (BOOL)tableView:(CPTableView)aTableView shouldTypeSelectForEvent:(CPEvent)anEvent withCurrentSearchString:(CPString)searchString; - (CPIndexSet)tableView:(CPTableView)aTableView selectionIndexesForProposedSelection:(CPIndexSet)proposedSelectionIndexes; - (CPInteger)tableView:(CPTableView)aTableView nextTypeSelectMatchFromRow:(CPInteger)startRow toRow:(CPInteger)endRow forString:(CPString)searchString; - (CPMenu)tableViewMenuForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex; - (CPString)tableView:(CPTableView)aTableView toolTipForView:(CPView)aView rect:(CGRect)aRect tableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex mouseLocation:(CGPoint)mouseLocation; - (CPString)tableView:(CPTableView)aTableView typeSelectStringForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex; - (CPView)tableView:(CPTableView)aTableView dataViewForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex; - (CPView)tableView:(CPTableView)aTableView viewForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex; - (float)tableView:(CPTableView)aTableView heightOfRow:(CPInteger)aRowIndex; - (void)tableView:(CPTableView)aTableView didClickTableColumn:(CPTableColumn)aTableColumn; - (void)tableView:(CPTableView)aTableView didDragTableColumn:(CPTableColumn)aTableColumn; - (void)tableView:(CPTableView)aTableView mouseDownInHeaderOfTableColumn:(CPTableColumn)aTableColumn; - (void)tableView:(CPTableView)aTableView willDisplayView:(CPView)aView forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex; - (void)tableView:(CPTableView)aTableView willRemoveView:(CPView)aView forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex; - (void)tableViewSelectionDidChange:(CPNotification)aNotification; - (void)tableViewSelectionIsChanging:(CPNotification)aNotification; @end @implementation _CPTableDrawView : CPView { id _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 either set a data source which implements @ref numberofrows "numberOfRowsInTableView:" and @ref objectValueForTable "tableView:objectValueForTableColumn:row:", or alternatively to provide data through Key Value Bindings. To use a table view with Key Value Bindings, bind each column's \c CPValueBinding to an \c array.field path - typically this would be to a path in an array controller like \c arrangedObjects.X, where \c X is the name of a field. Optionally also bind the table's \c selectionIndexes and \c sortDescriptors to the array controller. All delegate and data source methods are documented in the @ref setdatasource "setDataSource:" and @ref setdelegate "setDelegate:" methods. If you want to display something other than just text in the table you should call @link CPTableColumn::setDataView: setDataView:@endlink on a CPTableColumn object. More documentation in that class including theme states. @note CPTableView does not contain its own scrollview. You should be sure you place the tableview in a CPScrollView on your own. */ @implementation CPTableView : CPControl { id _dataSource; CPInteger _implementedDataSourceMethods; id _delegate; CPInteger _implementedDelegateMethods; CPArray _tableColumns; CPArray _tableColumnRanges; CPInteger _dirtyTableColumnRangeIndex; CPInteger _numberOfHiddenColumns; BOOL _reloadAllRows; BOOL _invalidateObjectValuesCache; Object _objectValues; CGRect _exposedRect; CPIndexSet _exposedRows; CPIndexSet _exposedColumns; Object _dataViewsForRows; Object _cachedDataViews; CPDictionary _archivedDataViews; Object _unavailable_custom_cibs; //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; CPColor _unfocusedSelectionHighlightColor; CPDictionary _unfocusedSourceListSelectionColor; CPTableColumn _currentHighlightedTableColumn; unsigned _gridStyleMask; unsigned _numberOfRows; CPIndexSet _groupRows; CPArray _cachedRowHeights; // Persistence CPString _autosaveName; BOOL _autosaveTableColumns; CPTableHeaderView _headerView; _CPCornerView _cornerView; CPIndexSet _selectedColumnIndexes; CPIndexSet _selectedRowIndexes; CPInteger _selectionAnchorRow; CPInteger _lastSelectedRow; CPIndexSet _previouslySelectedRowIndexes; CGPoint _startTrackingPoint; CPDate _startTrackingTimestamp; BOOL _trackingPointMovedOutOfClickSlop; CPInteger _editingRow; CPInteger _editingColumn; _CPTableDrawView _tableDrawView; SEL _doubleAction; CPInteger _clickedRow; CPInteger _clickedColumn; unsigned _columnAutoResizingStyle; int _lastTrackedRowIndex; CGPoint _originalMouseDownPoint; BOOL _verticalMotionCanDrag; unsigned _destinationDragStyle; BOOL _isSelectingSession; CPIndexSet _draggedRowIndexes; BOOL _wasSelectionBroken; _CPDropOperationDrawingView _dropOperationFeedbackView; CPDragOperation _dragOperationDefaultMask; int _retargetedDropRow; CPDragOperation _retargetedDropOperation; BOOL _disableAutomaticResizing @accessors(property=disableAutomaticResizing); BOOL _lastColumnShouldSnap; BOOL _implementsCustomDrawRow; BOOL _isViewBased; BOOL _contentBindingExplicitlySet; SEL _viewForTableColumnRowSelector; CPInteger _draggedColumnIndex; BOOL _draggedColumnIsSelected; BOOL _needsDifferedTableColumnRemove; CPArray _differedColumnDataToRemove; Function _BlockDeselectView; Function _BlockSelectView; CPView _observedClipView; } /*! @ignore */ + (CPString)defaultThemeClass { return @"tableview"; } /*! @ignore */ + (CPDictionary)themeAttributes { return @{ @"alternating-row-colors": [CPNull null], @"grid-color": [CPNull null], @"grid-line-thickness": 1.0, @"highlighted-grid-color": [CPNull null], @"selection-color": [CPNull null], @"sourcelist-selection-color": [CPNull null], @"sort-image": [CPNull null], @"sort-image-reversed": [CPNull null], @"selection-radius": [CPNull null], @"image-generic-file": [CPNull null], @"default-row-height": 25.0, @"dropview-on-background-color": [CPNull null], @"dropview-on-border-color": [CPNull null], @"dropview-on-border-width": [CPNull null], @"dropview-on-border-radius": [CPNull null], @"dropview-on-selected-background-color": [CPNull null], @"dropview-on-selected-border-color": [CPNull null], @"dropview-on-selected-border-width": [CPNull null], @"dropview-on-selected-border-radius": [CPNull null], @"dropview-above-border-color": [CPNull null], @"dropview-above-border-width": [CPNull null], @"dropview-above-selected-border-color": [CPNull null], @"dropview-above-selected-border-width": [CPNull null] }; } - (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(3.0, 2.0); _rowHeight = [self valueForThemeAttribute:@"default-row-height"]; [self setGridColor:[CPColor colorWithHexString:@"dce0e2"]]; [self setGridStyleMask:CPTableViewGridNone]; [self setHeaderView:[[CPTableHeaderView alloc] initWithFrame:CGRectMake(0, 0, [self bounds].size.width, _rowHeight)]]; [self setCornerView:[[_CPCornerView alloc] initWithFrame:CGRectMake(0, 0, [CPScroller scrollerWidth], CGRectGetHeight([_headerView frame]))]]; _currentHighlightedTableColumn = nil; _draggedRowIndexes = [CPIndexSet indexSet]; _verticalMotionCanDrag = YES; _isSelectingSession = NO; _retargetedDropRow = nil; _retargetedDropOperation = nil; _dragOperationDefaultMask = nil; _destinationDragStyle = CPTableViewDraggingDestinationFeedbackStyleRegular; _contentBindingExplicitlySet = NO; [self setBackgroundColor:[CPColor whiteColor]]; [self _init]; } return self; } /*! @ignore FIX ME: we have a lot of redundant init stuff in initWithFrame: and initWithCoder: we should move it all into here. we should do a full audit of all the initializers before 1.0 */ - (void)_init { _lastSelectedRow = _clickedColumn = _clickedRow = -1; _selectedColumnIndexes = [CPIndexSet indexSet]; _selectedRowIndexes = [CPIndexSet indexSet]; _dropOperationFeedbackView = [[_CPDropOperationDrawingView alloc] initWithFrame:CGRectMakeZero()]; [_dropOperationFeedbackView setTableView:self]; _lastColumnShouldSnap = NO; if (!_alternatingRowBackgroundColors) _alternatingRowBackgroundColors = [[CPColor whiteColor], [CPColor colorWithHexString:@"e4e7ff"]]; _tableColumnRanges = []; _dirtyTableColumnRangeIndex = 0; _numberOfHiddenColumns = 0; _objectValues = { }; _invalidateObjectValuesCache = NO; _dataViewsForRows = { }; _numberOfRows = 0; _exposedRows = [CPIndexSet indexSet]; _exposedColumns = [CPIndexSet indexSet]; _cachedDataViews = { }; _archivedDataViews = nil; _viewForTableColumnRowSelector = nil; _unavailable_custom_cibs = { }; _cachedRowHeights = []; _groupRows = [CPIndexSet indexSet]; _tableDrawView = [[_CPTableDrawView alloc] initWithTableView:self]; [_tableDrawView setBackgroundColor:[CPColor clearColor]]; [self addSubview:_tableDrawView]; _draggedColumnIndex = -1; _draggedColumnIsSelected = NO; _editingRow = CPNotFound; _editingColumn = CPNotFound; /* //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 = []; _needsDifferedTableColumnRemove = NO; _implementsCustomDrawRow = [self implementsSelector:@selector(drawRow:clipRect:)]; if (!_sortDescriptors) _sortDescriptors = []; [self _initSubclass]; } - (void)_initSubclass { _BlockDeselectView = function(view, row, column) { [view unsetThemeState:CPThemeStateSelectedDataView]; }; _BlockSelectView = function(view, row, column) { [view setThemeState:CPThemeStateSelectedDataView]; }; } /*! @anchor setdatasource Sets the receiver's data source to a given object. The data source implements various methods for handling the tableview's data when bindings are not used. @param anObject The data source for the receiver. This object must implement numberOfRowsInTableView: and tableView:objectValueForTableColumn:row: These methods are outlined below. @section overview Overview CPTableView generally requires a datasource to run. This data source can be thought of just like a delegate, but specifically for the tableview data. Methods include: @section required Required Methods @anchor numberofrows Returns the number of rows in the tableview @code - (int)numberOfRowsInTableView:(CPTableView)aTableView; @endcode Returns the object value for each dataview. Each dataview will be sent a setObjectValue: method which will contain the object you return from this datasource method. @anchor objectValueForTable @code - (id)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aColumn row:(CPInteger)aRowIndex; @endcode @section editing Editing: Sets the data object for an item in a given row and column. This needs to be implemented if you want inline editing support @code - (void)tableView:(CPTableView)aTableView setObjectValue:(id)anObject forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)rowIndex; @endcode @section sorting Sorting: The tableview will call this method if you click the tableheader. You should sort the datasource based off of the new sort descriptors and reload the data @code - (void)tableView:(CPTableView)aTableView sortDescriptorsDidChange:(CPArray)oldDescriptors; @endcode @section draganddrop Drag and Drop: @note In order for the tableview to receive drops don't forget to first register the tableview for drag types like you do with every other view. Return the drag operation (move, copy, etc) that should be performed if a registered drag type is over the tableview The data source can retarget a drop if you want by calling
-(void)setDropRow:(CPInteger)aRow dropOperation:(CPTableViewDropOperation)anOperation;
@code - (CPDragOperation)tableView:(CPTableView)aTableView validateDrop:(CPDraggingInfo)info proposedRow:(CPInteger)row proposedDropOperation:(CPTableViewDropOperation)operation; @endcode Returns YES if the drop operation is allowed otherwise NO. This method is invoked by the tableview after a drag should begin, but before it is started. If you don't want the drag to being return NO. If you want the drag to begin you should return YES and place the drag data on the pboard. @code - (BOOL)tableView:(CPTableView)aTableView writeRowsWithIndexes:(CPIndexSet)rowIndexes toPasteboard:(CPPasteboard)pboard; @endcode Return YES if the operation was successful otherwise return NO. The data source should incorporate the data from the dragging pasteboard in this method implementation. To get this data use the draggingPasteboard method on the CPDraggingInfo object. @code - (BOOL)tableView:(CPTableView)aTableView acceptDrop:(CPDraggingInfo)info row:(CPInteger)row dropOperation:(CPTableViewDropOperation)operation; @endcode NOT YET IMPLEMENTED @code - (CPArray)tableView:(CPTableView)aTableView namesOfPromisedFilesDroppedAtDestination:(CPURL)dropDestination forDraggedRowsWithIndexes:(CPIndexSet)indexSet; @endcode */ - (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_; if ([_dataSource respondsToSelector:@selector(tableView:objectValueForTableColumn:row:)]) _implementedDataSourceMethods |= CPTableViewDataSource_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 _reloadDataViews]; } /*! Returns the object that provides the data displayed by the receiver. */ - (id)dataSource { return _dataSource; } //Loading Data /*! Reloads the data for visible data views. */ - (void)reloadData { [self _reloadDataViews]; } /*! 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]; } // Reloads the data, not the views - (void)_reloadDataForRowIndexes:(CPIndexSet)rowIndexes columnIndexes:(CPIndexSet)columnIndexes { [self _enumerateViewsInRows:rowIndexes columns:columnIndexes usingBlock:function(view, row, column, stop) { var tableColumn = [_tableColumns objectAtIndex:column]; [self _setObjectValueForTableColumn:tableColumn row:row forView:view useCache:NO]; }]; } // Reloads the views AND the data - (void)_reloadDataViews { //if (!_dataSource) // return; _reloadAllRows = YES; _objectValues = { }; _cachedRowHeights = []; // Otherwise, if we have a row marked as group with a // index greater than the new number or rows // it keeps the the graphical group style. [_groupRows removeAllIndexes]; // 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; } /*! Returns the double click action selector. */ - (SEL)doubleAction { return _doubleAction; } /* Returns the index of the the column the user clicked to trigger an action, or -1 if no column was clicked. */ - (CPInteger)clickedColumn { return _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 /*! If you want to allow the user to reorder the columns pass YES, otherwise NO. */ - (void)setAllowsColumnReordering:(BOOL)shouldAllowColumnReordering { _allowsColumnReordering = !!shouldAllowColumnReordering; } /*! Returns YES if the user is allowed to reorder the columns, otherwise NO. */ - (BOOL)allowsColumnReordering { return _allowsColumnReordering; } /*! Passing YES will allow the user to resize columns. Passing NO will keep the table columns unmovable by the user. @note The this does not affect autoresizing behavior. */ - (void)setAllowsColumnResizing:(BOOL)shouldAllowColumnResizing { _allowsColumnResizing = !!shouldAllowColumnResizing; } /*! Returns YES if the user is allowed to manually resize the columns, otherwise NO. */ - (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; } /*! Returns YES if the tableview is allowed to have multiple selections, otherwise NO. @return BOOL - YES if the tableview is allowed to have multiple selections otherwise NO. */ - (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; } /*! Returns YES if the tableview is allowed to have an unselected row or column, otherwise NO. */ - (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; } /*! Returns YES if the user is allowed to select a column by clicking it, otherwise NO. */ - (BOOL)allowsColumnSelection { return _allowsColumnSelection; } //Setting Display Attributes /*! Sets the width and height between dataviews. This value is (3.0, 2.0) by default. @param aSize a CGSize object that defines the space between the cells */ - (void)setIntercellSpacing:(CGSize)aSize { if (CGSizeEqualToSize(_intercellSpacing, aSize)) return; _intercellSpacing = CGSizeMakeCopy(aSize); _dirtyTableColumnRangeIndex = 0; // so that _recalculateTableColumnRanges will work [self _recalculateTableColumnRanges]; [_headerView setNeedsDisplay:YES]; [_headerView setNeedsLayout]; [self _reloadDataViews]; } /*! Returns the intercell spacing in a CGSize object. */ - (CGSize)intercellSpacing { return CGSizeMakeCopy(_intercellSpacing); } /*! Sets the height of each row. @note This may still used even if variable row height is being used. @param aRowHeight the height of each row */ - (void)setRowHeight:(unsigned)aRowHeight { // Accept row heights such as "0". aRowHeight = +aRowHeight; if (_rowHeight === aRowHeight) return; _rowHeight = MAX(0.0, aRowHeight); [self setNeedsLayout]; } /*! Returns the height of each row. */ - (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; } /*! Returns YES if the tableview uses alternating row background colors, otherwise NO. */ - (BOOL)usesAlternatingRowBackgroundColors { return _usesAlternatingRowBackgroundColors; } /*! Sets the colors for the rows as they alternate. The number of colors can be arbitrary. By default 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]; } /*! Returns an array of the alternating background colors */ - (CPArray)alternatingRowBackgroundColors { return [self currentValueForThemeAttribute:@"alternating-row-colors"]; } /*! Returns an enumerated value for the selection highlight style. Valid values are:
        CPTableViewSelectionHighlightStyleNone
        CPTableViewSelectionHighlightStyleRegular
        CPTableViewSelectionHighlightStyleSourceList
*/ - (unsigned)selectionHighlightStyle { return _selectionHighlightStyle; } /*! Sets the selection highlight style to an enumerated value. This value can also affect the way the tableview draws feedback when the user is dragging. Valid values are:
        CPTableViewSelectionHighlightStyleNone
        CPTableViewSelectionHighlightStyleRegular
        CPTableViewSelectionHighlightStyleSourceList
*/ - (void)setSelectionHighlightStyle:(unsigned)aSelectionHighlightStyle { _selectionHighlightStyle = aSelectionHighlightStyle; if (aSelectionHighlightStyle === CPTableViewSelectionHighlightStyleSourceList) _destinationDragStyle = CPTableViewDraggingDestinationFeedbackStyleSourceList; else _destinationDragStyle = CPTableViewDraggingDestinationFeedbackStyleRegular; [self _updateHighlightWithOldRows:[CPIndexSet indexSet] newRows:_selectedRowIndexes]; [self _updateHighlightWithOldColumns:[CPIndexSet indexSet] newColumns:_selectedColumnIndexes]; [self setNeedsDisplay:YES]; } /*! Sets the highlight color for a row or column selection. @param aColor a CPColor */ - (void)setSelectionHighlightColor:(CPColor)aColor { if ([[self selectionHighlightColor] isEqual:aColor]) return; [self setValue:aColor forThemeAttribute:@"selection-color"]; [self setNeedsDisplay:YES]; } /*! Returns the highlight color for a focused row or column selection. */ - (CPColor)selectionHighlightColor { return [self currentValueForThemeAttribute:@"selection-color"]; } /*! Returns the highlight color for an unfocused row or column selection. */ - (CPColor)unfocusedSelectionHighlightColor { if (!_unfocusedSelectionHighlightColor) _unfocusedSelectionHighlightColor = [self _unfocusedSelectionColorFromColor:[self selectionHighlightColor] saturation:0]; return _unfocusedSelectionHighlightColor; } /*! 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"]; } /*! Returns a dictionary of containing the keys:
    CPSourceListGradient
    CPSourceListTopLineColor
    CPSourceListBottomLineColor
*/ - (CPColor)unfocusedSelectionGradientColors { if (!_unfocusedSourceListSelectionColor) { var sourceListColors = [self selectionGradientColors]; _unfocusedSourceListSelectionColor = @{ CPSourceListGradient: [self _unfocusedGradientFromGradient:[sourceListColors objectForKey:CPSourceListGradient]], CPSourceListTopLineColor: [self _unfocusedSelectionColorFromColor:[sourceListColors objectForKey:CPSourceListTopLineColor] saturation:0.2], CPSourceListBottomLineColor: [self _unfocusedSelectionColorFromColor:[sourceListColors objectForKey:CPSourceListBottomLineColor] saturation:0.2] }; } return _unfocusedSourceListSelectionColor; } - (CPColor)_unfocusedSelectionColorFromColor:(CPColor)aColor saturation:(float)saturation { var hsb = [aColor hsbComponents]; return [CPColor colorWithHue:hsb[0] saturation:hsb[1] * saturation brightness:hsb[2]]; } - (CGGradient)_unfocusedGradientFromGradient:(CGGradient)aGradient { var colors = [aGradient.colors copy], count = [colors count]; while (count--) { var rgba = colors[count].components, hsb = [self _unfocusedSelectionColorFromColor:[CPColor colorWithRed:rgba[0] green:rgba[1] blue:rgba[2] alpha:rgba[3]] saturation:0.2]; colors[count] = CGColorCreate(aGradient.colorspace, [[hsb components] copy]); } return CGGradientCreateWithColors(aGradient.colorspace, colors, aGradient.locations); } /*! 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]; } /*! Returns a CPColor object of set grid color */ - (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]; } /*! Returns a grid mask */ - (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); if ([[self sortDescriptors] count] > 0) { var mainSortDescriptor = [[self sortDescriptors] objectAtIndex:0]; if (aTableColumn === [self _tableColumnForSortDescriptor:mainSortDescriptor]) { var image = [mainSortDescriptor ascending] ? [self _tableHeaderSortImage] : [self _tableHeaderReverseSortImage]; [self setIndicatorImage:image inTableColumn:aTableColumn]; } } [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 defer 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]}]; _needsDifferedTableColumnRemove = YES; [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 reloadData]; [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; } /*! @ignore Internally used to set a column that will be dragged */ - (void)_setDraggedColumn:(CPInteger)columnIndex { if (_draggedColumnIndex === columnIndex) return; // If ending a column drag, reselect the column if it was selected before the drag if (columnIndex === -1 && _draggedColumnIsSelected) [_selectedColumnIndexes addIndex:_draggedColumnIndex]; _draggedColumnIndex = columnIndex; } /* @ignore Same as moveColumn:toColumn: but doesn't trigger an autosave */ - (void)_moveColumn:(unsigned)fromIndex toColumn:(unsigned)toIndex { // Convert parameters such as "0" to 0. 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], selectedTableColumns = [_tableColumns objectsAtIndexes:_selectedColumnIndexes]; [_tableColumns removeObjectAtIndex:fromIndex]; [_tableColumns insertObject:tableColumn atIndex:toIndex]; [[self headerView] setNeedsLayout]; [[self headerView] setNeedsDisplay:YES]; var range = CPMakeRange(MIN(fromIndex, toIndex), ABS(fromIndex - toIndex) + 1), layoutColumnIndexes = [CPIndexSet indexSetWithIndexesInRange:range], selectedColumnIndexes = [CPIndexSet indexSet]; [_tableColumns enumerateObjectsUsingBlock:function(tableColumn, idx, stop) { if ([selectedTableColumns containsObjectIdenticalTo:tableColumn]) [selectedColumnIndexes addIndex:idx]; }]; if ([_selectedColumnIndexes containsIndex:fromIndex]) [selectedColumnIndexes addIndex:toIndex]; if (_draggedColumnIndex !== -1) [layoutColumnIndexes removeIndex:toIndex]; [self _layoutViewsForRowIndexes:_exposedRows columnIndexes:layoutColumnIndexes]; [self selectColumnIndexes:selectedColumnIndexes byExtendingSelection:NO]; [self setNeedsDisplay:YES]; // Notify even if programmatically moving a column as in Cocoa. // TODO Only notify when a column drag operation ends, not each time a column reaches a new slot? [[CPNotificationCenter defaultCenter] postNotificationName:CPTableViewColumnDidMoveNotification object:self userInfo:@{ @"CPOldColumn": fromIndex, @"CPNewColumn": toIndex }]; if (_implementedDelegateMethods & CPTableViewDelegate_tableViewColumnDidMove_) [_delegate tableViewColumnDidMove:[[CPNotification alloc] initWithName:CPTableViewColumnDidMoveNotification object:self userInfo:@{ @"CPOldColumn": fromIndex, @"CPNewColumn": toIndex }]]; } /*! Moves the column and heading at a given index to a new given index. @param theColumnIndex The current index of the column to move. @param theToIndex The new index for the moved column. */ - (void)moveColumn:(CPInteger)theColumnIndex toColumn:(CPInteger)theToIndex { [self _moveColumn:theColumnIndex toColumn:theToIndex]; [self _autosave]; } /*! @ignore Called when a table column changes visibility */ - (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 _layoutViewsForRowIndexes:rowIndexes columnIndexes:[CPIndexSet indexSetWithIndex:columnIndex]]; } /*! Returns an array of CPTableColumns of all the receiver's columns. */ - (CPArray)tableColumns { return _tableColumns; } /*! Returns the index of the column with the specified identifier @param anIdentifier the string value of the tablecolumn identifier @return the index of the column */ - (CPInteger)columnWithIdentifier:(CPString)anIdentifier { var index = 0, count = NUMBER_OF_COLUMNS(); for (; index < count; ++index) if ([_tableColumns[index] identifier] === anIdentifier) return index; return CPNotFound; } /*! Returns the CPTableColumn object with the given identifier string. @param anIdentifier the string value of the identifier @return a CPTableColumn object with the given identifier */ - (CPTableColumn)tableColumnWithIdentifier:(CPString)anIdentifier { var index = [self columnWithIdentifier:anIdentifier]; if (index === CPNotFound) return nil; return _tableColumns[index]; } /*! @ignore */ - (void)_didResizeTableColumn:(CPTableColumn)theColumn oldWidth:(int)oldWidth { [self _autosave]; [[CPNotificationCenter defaultCenter] postNotificationName:CPTableViewColumnDidResizeNotification object:self userInfo:@{ @"CPTableColumn": theColumn, @"CPOldWidth": oldWidth }]; if (_implementedDelegateMethods & CPTableViewDelegate_tableViewColumnDidResize_) [_delegate tableViewColumnDidResize:[[CPNotification alloc] initWithName:CPTableViewColumnDidResizeNotification object:self userInfo:@{ @"CPTableColumn": theColumn, @"CPOldWidth": oldWidth }]]; } //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] || (!shouldExtendSelection && [columns isEqualToIndexSet:_selectedColumnIndexes]) || (shouldExtendSelection && [columns count] === 0)) 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]; } /*! @ignore */ - (void)_setSelectedRowIndexes:(CPIndexSet)rows { if ([_selectedRowIndexes isEqualToIndexSet:rows]) return; var previousSelectedIndexes = _selectedRowIndexes; _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 var binderClass = [[self class] _binderClassForBinding:@"selectionIndexes"]; [[binderClass 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]) || [self numberOfColumns] <= 0) 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]; } /*! @ignore cleanup a an indexset from the indexes that cannot be selected according to the table view delegate. */ - (CPIndexSet)_cleanUpSelectionRowIndexes:(CPIndexSet)anIndexSet { if ([self _delegateRespondsToSelectionIndexesForProposedSelection]) { return [self _sendDelegateSelectionIndexesForProposedSelection:anIndexSet]; } else if ([self _delegateRespondsToShouldSelectRow]) { var indexesToRemove = [CPIndexSet new], currentIndex = [anIndexSet firstIndex]; while (currentIndex != CPNotFound) { if (![self _sendDelegateShouldSelectRow:currentIndex]) [indexesToRemove addIndex:currentIndex]; currentIndex = [anIndexSet indexGreaterThanIndex:currentIndex]; } [anIndexSet removeIndexes:indexesToRemove]; return anIndexSet; } else return anIndexSet; } /*! @ignore */ - (void)_updateHighlightWithOldRows:(CPIndexSet)oldRows newRows:(CPIndexSet)newRows { [self _enumerateViewsInRows:oldRows columns:_exposedColumns usingBlock:_BlockDeselectView]; if (_selectionHighlightStyle !== CPTableViewSelectionHighlightStyleNone) [self _enumerateViewsInRows:newRows columns:_exposedColumns usingBlock:_BlockSelectView]; } /*! @ignore */ - (void)_updateHighlightWithOldColumns:(CPIndexSet)oldColumns newColumns:(CPIndexSet)newColumns { var blockDeselectHeader = function(column, stop) { var headerView = [_tableColumns[column] headerView]; [headerView unsetThemeState:CPThemeStateSelected]; }; var showSelection = _selectionHighlightStyle !== CPTableViewSelectionHighlightStyleNone; [self _enumerateViewsInRows:_exposedRows columns:oldColumns usingBlock:_BlockDeselectView]; [oldColumns enumerateIndexesUsingBlock:blockDeselectHeader]; if (_selectionHighlightStyle !== CPTableViewSelectionHighlightStyleNone) { [self _enumerateViewsInRows:_exposedRows columns:newColumns usingBlock:_BlockSelectView]; [newColumns enumerateIndexesUsingBlock:function(column, stop) { var headerView = [_tableColumns[column] headerView]; [headerView setThemeState:CPThemeStateSelected]; }]; } } /*! Returns the index of the last selected column. */ - (int)selectedColumn { return [_selectedColumnIndexes lastIndex]; } /*! Returns an index set of all the selected columns. */ - (CPIndexSet)selectedColumnIndexes { return _selectedColumnIndexes; } /*! Returns the index of the last selected row. */ - (int)selectedRow { return _lastSelectedRow; } /*! Returns an index set with the indexes of all the selected rows. */ - (CPIndexSet)selectedRowIndexes { return [_selectedRowIndexes copy]; } /*! Deselects the column at a given index @param anIndex the index of the column to deselect */ - (void)deselectColumn:(CPInteger)anIndex { var selectedColumnIndexes = [_selectedColumnIndexes copy]; [selectedColumnIndexes removeIndex:anIndex]; [self selectColumnIndexes:selectedColumnIndexes byExtendingSelection:NO]; [self _noteSelectionDidChange]; } /*! Deselects a row at a given index @param aRow the row to deselect */ - (void)deselectRow:(CPInteger)aRow { var selectedRowIndexes = [_selectedRowIndexes copy]; [selectedRowIndexes removeIndex:aRow]; [self selectRowIndexes:selectedRowIndexes byExtendingSelection:NO]; [self _noteSelectionDidChange]; } /*! Returns the number of selected columns */ - (CPInteger)numberOfSelectedColumns { return [_selectedColumnIndexes count]; } /*! Returns the number of selected columns */ - (CPInteger)numberOfSelectedRows { return [_selectedRowIndexes count]; } /*! Returns YES if the column at a given index is selected, otherwise NO. @param anIndex the index of a column @return YES if the column is selected, otherwise NO. */ - (BOOL)isColumnSelected:(CPInteger)anIndex { return [_selectedColumnIndexes containsIndex:anIndex]; } /*! Returns YES if the row at a given index is selected, otherwise NO. @param aRow the index of a row @return YES if the row is selected, otherwise NO. */ - (BOOL)isRowSelected:(CPInteger)aRow { return [_selectedRowIndexes containsIndex:aRow]; } /*! @ignore Deselects all rows and columns */ - (void)deselectAll { [self selectRowIndexes:[CPIndexSet indexSet] byExtendingSelection:NO]; [self selectColumnIndexes:[CPIndexSet indexSet] byExtendingSelection:NO]; } - (void)selectAll:(id)sender { if (_allowsMultipleSelection) { if (![self _sendDelegateSelectionShouldChangeInTableView]) return; if ([[self selectedColumnIndexes] count]) [self selectColumnIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self numberOfColumns])] byExtendingSelection:NO]; else { var range = [self _cleanUpSelectionRowIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self numberOfRows])]]; [self selectRowIndexes:range byExtendingSelection:NO]; } } } - (void)deselectAll:(id)sender { if ([self allowsEmptySelection]) { if (![self _sendDelegateSelectionShouldChangeInTableView]) return; [self deselectAll]; } } /*! Returns the number of columns in the table */ - (int)numberOfColumns { return NUMBER_OF_COLUMNS(); } /*! Returns the number of rows in the receiver. */ - (int)numberOfRows { return _numberOfRows; } - (int)_numberOfRows { var numberOfRows, contentBindingInfo = [self infoForBinding:@"content"]; if (contentBindingInfo) { var destination = [contentBindingInfo objectForKey:CPObservedObjectKey], keyPath = [contentBindingInfo objectForKey:CPObservedKeyPathKey]; numberOfRows = [[destination valueForKeyPath:keyPath] count]; } else if (_dataSource && (_implementedDataSourceMethods & CPTableViewDataSource_numberOfRowsInTableView_)) numberOfRows = [_dataSource numberOfRowsInTableView:self] || 0; else { if (_dataSource) CPLog(@"no content binding established and data source " + [_dataSource description] + " does not implement numberOfRowsInTableView:"); numberOfRows = 0; } return numberOfRows; } /*! Returns the cornerview for the scrollview */ - (CPView)cornerView { return _cornerView; } /*! Sets the cornerview for the scrollview */ - (void)setCornerView:(CPView)aView { if (_cornerView === aView) return; _cornerView = aView; var scrollView = [self enclosingScrollView]; if ([scrollView isKindOfClass:[CPScrollView class]] && [scrollView documentView] === self) [scrollView _updateCornerAndHeaderView]; } /*! Returns the headerview for the receiver. The headerview contains column headerviews for each table column. */ - (CPView)headerView { return _headerView; } /*! Sets the headerview for the tableview. This is the container view for the table column header views. This view also handles events for resizing and dragging. If you don't want your tableview to have a headerview you should pass nil. (also see setCornerView:) If you're looking to customize the header text of a column see CPTableColumn's -(CPView)headerView; method. */ - (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]))]; } else { // If there is no header view, there should be no corner view [_cornerView removeFromSuperview]; _cornerView = nil; } var scrollView = [self enclosingScrollView]; if ([scrollView isKindOfClass:[CPScrollView class]] && [scrollView documentView] === self) [scrollView _updateCornerAndHeaderView]; [self setNeedsLayout]; } /*! @ignore */ - (void)_recalculateTableColumnRanges { // Complexity: // O(Columns) 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] + _intercellSpacing.width; _tableColumnRanges[index] = CPMakeRange(x, width); x += width; } } _tableColumnRanges.length = count; _dirtyTableColumnRangeIndex = CPNotFound; } /*! Returns a CGRect with the location and size of the column If aColumnIndex lies outside the range of the table columns a CGZeroRect is returned @param aColumnIndex the index of the column to return the rect of */ - (CGRect)rectOfColumn:(CPInteger)aColumnIndex { // Complexity: // O(1) // Coerce aColumnIndex to a number in case it is a string. aColumnIndex = +aColumnIndex; if (aColumnIndex < 0 || aColumnIndex >= NUMBER_OF_COLUMNS()) return CGRectMakeZero(); if ([[_tableColumns objectAtIndex:aColumnIndex] isHidden]) return CGRectMakeZero(); UPDATE_COLUMN_RANGES_IF_NECESSARY(); var range = _tableColumnRanges[aColumnIndex]; return CGRectMake(range.location, 0.0, range.length, CGRectGetHeight([self bounds])); } /*! @ignore Returns a CGRect with the location and size of the row @param aRowIndex the index of the row to return the rect of @param checkRange if YES this method will return a zero rect if aRowIndex is outside of the range of valid indices */ - (CGRect)_rectOfRow:(CPInteger)aRowIndex checkRange:(BOOL)checkRange { // Complexity: // O(1) var lastIndex = [self numberOfRows] - 1, validIndex = aRowIndex >= 0 && aRowIndex <= lastIndex; if (checkRange && !validIndex) return CGRectMakeZero(); var y = 0, height, fixedHeightRows = 0; if (HAS_VARIABLE_ROW_HEIGHTS()) { [self _populateRowHeightCacheIfNeeded]; // If the index is valid, we use the y and height of the given row. // If the index is invalid, we start from the bottom of the last row and use the default row height. var heightInfo; if (validIndex) { heightInfo = _cachedRowHeights[aRowIndex]; y = heightInfo.y; height = heightInfo.height + _intercellSpacing.height; } else { height = FULL_ROW_HEIGHT(); if (_numberOfRows > 0) { heightInfo = _cachedRowHeights[lastIndex]; y = ROW_BOTTOM(heightInfo); // y is now at the top of the first row beyond the last valid row. // Add the height of any rows beyond that. fixedHeightRows = aRowIndex - _numberOfRows; } } } else { fixedHeightRows = aRowIndex; height = FULL_ROW_HEIGHT(); } y += fixedHeightRows * FULL_ROW_HEIGHT(); return CGRectMake(0.0, y, CGRectGetWidth([self bounds]), height); } /*! Returns a CGRect with the location and size of the row. CGRectZero is returned if aRowIndex doesn't exist. @param aRowIndex the index of the row you want the rect of */ - (CGRect)rectOfRow:(CPInteger)aRowIndex { return [self _rectOfRow:aRowIndex checkRange:YES]; } /*! 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 { // Complexity: // O(1) // 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); } /* Return the range of rows that lie wholly or partially within aRect. If the bottom of the last real row is above the bottom of aRect, synthesized rows of the default height are added to fill aRect. */ - (CPRange)_exposedRowsInRect:(CGRect)aRect { var rowRange = [self rowsInRect:aRect], lastRealRow = CPMaxRange(rowRange) - 1, rectOfLastRealRow = [self _rectOfRow:lastRealRow checkRange:NO], bottomOfRealRows = CGRectGetMaxY(rectOfLastRealRow), rectBottom = CGRectGetMaxY(aRect); // If the bottom of the last real row is at or below the bottom of aRect, we are done if (bottomOfRealRows >= rectBottom) return rowRange; var numberOfSynthesizedRows = CEIL((rectBottom - bottomOfRealRows) / FULL_ROW_HEIGHT()); rowRange.length += numberOfSynthesizedRows; return rowRange; } /*! 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 { // Complexity: // O(log numberOfColumns) if table view contains no hidden columns // O(numberOfColumns) if table view contains hidden columns 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; } /*! Returns the index of a column at a given point. If no column is there CPNotFound is returned. @param aPoint a CGPoint */ - (CPInteger)columnAtPoint:(CGPoint)aPoint { // Complexity: // O(log numberOfColumns) if table view contains no hidden columns // O(numberOfColumns) if table view contains hidden columns 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; } /*! Returns the index of a row at a particular point. If no row exists at that point -1 is returned. @param aPoint a CGPoint */ - (CPInteger)rowAtPoint:(CGPoint)aPoint { // Complexity: // O(1) for fixed height rows or point out of bounds // O(log numberOfRows) for variable height rows // aPoint.x must be within our bounds var bounds = [self bounds]; if (aPoint.x < CGRectGetMinX(bounds) || aPoint.x >= CGRectGetMaxX(bounds)) return -1; // aPoint.x is in bounds, now we just have to check aPoint.y if (HAS_VARIABLE_ROW_HEIGHTS()) { // First make sure aPoint.y is above the bottom of the last row, otherwise we might // search the (potentially large number of) rows for nothing. var heightInfo = [_cachedRowHeights lastObject]; if (!heightInfo || aPoint.y >= ROW_BOTTOM(heightInfo)) return -1; return [_cachedRowHeights indexOfObject:aPoint inSortedRange:nil options:0 usingComparator:function(aPoint, heightInfo) { if (aPoint.y < heightInfo.y) return CPOrderedAscending; if (aPoint.y > ROW_BOTTOM(heightInfo)) return CPOrderedDescending; return CPOrderedSame; }]; } else { var row = FLOOR(aPoint.y / FULL_ROW_HEIGHT()); return row >= _numberOfRows ? -1 : row; } } /*! Returns the index of the row for the specified view. @param view The view. @return The index of the row corresponding to the view. Returns -1 if the view is not a dataView, or a subview of a dataView. @discussion This is typically needed in the action method for a CPButton (or CPControl) to find out what row (and column) the action should be performed on. The implementation is O(rxc) where r is the number of visible rows, and c is the number of visible columns, so this method should generally not be called within a loop. */ - (CPInteger)rowForView:(CPView)aView { var row; [self getColumn:nil row:@ref(row) forView:aView]; return row; } /*! Returns the index of the column for the specified view. @param view The view. @return The index of the column corresponding to the view. Returns -1 if the view is not a dataView, or a subview of a dataView. @discussion This is typically needed in the action method for a CPButton (or CPControl) to find out what row (and column) the action should be performed on. The implementation is O(rxc) where r is the number of visible rows, and c is the number of visible columns, so this method should generally not be called within a loop. */ - (CPInteger)columnForView:(CPView)aView { var column; [self getColumn:@ref(column) row:nil forView:aView]; return column; } /*! @ignore */ - (void)getColumn:(Function)columnRef row:(Function)rowRef forView:(CPView)aView { var columnResult = -1, rowResult = -1; if (aView && [aView isKindOfClass:[CPView class]] && ![aView isKindOfClass:[CPTableView class]]) { var cellView = aView, contentView = [[self window] contentView], found = NO, max_rec = 100; while (max_rec--) { if (!cellView || cellView === contentView) { found = NO; break; } else { var superview = [cellView superview]; if ([superview isKindOfClass:[CPTableView class]]) { found = YES; break; } cellView = superview; } } if (found) { [self _enumerateViewsInRows:_exposedRows columns:_exposedColumns usingBlock:function(view, row, column, stop) { if (view === cellView) { columnResult = column; rowResult = row; stop(YES); } }]; } } if (columnRef) columnRef(columnResult); if (rowRef) rowRef(rowResult); } /*! Returns a rect for the dataview / cell at the column and row given. If the column or row index is greater than the number of columns or rows a CGZeroRect is returned @param aColumn index of the column @param aRow index of the row */ - (CGRect)frameOfDataViewAtColumn:(CPInteger)aColumn row:(CPInteger)aRow { UPDATE_COLUMN_RANGES_IF_NECESSARY(); if (aColumn > [self numberOfColumns] || aRow > [self numberOfRows]) return CGRectMakeZero(); var tableColumnRange = _tableColumnRanges[aColumn], rectOfRow = [self rectOfRow:aRow], leftInset = FLOOR(_intercellSpacing.width / 2.0), topInset = FLOOR(_intercellSpacing.height / 2.0); return CGRectMake(tableColumnRange.location + leftInset, CGRectGetMinY(rectOfRow) + topInset, tableColumnRange.length - _intercellSpacing.width, CGRectGetHeight(rectOfRow) - _intercellSpacing.height); } /*! @ignore */ - (void)resizeWithOldSuperviewSize:(CGSize)aSize { [super resizeWithOldSuperviewSize:aSize]; if (_disableAutomaticResizing) return; var mask = _columnAutoResizingStyle; // should we actually do some resizing? if (!_lastColumnShouldSnap) { // did the clip view intersect the old tablesize? var superview = [self superview]; if (!superview || ![superview isKindOfClass:[CPClipView class]]) return; var superviewWidth = [superview bounds].size.width, lastColumnMaxX = CGRectGetMaxX([self rectOfColumn:[self numberOfColumns] -1]); // Fix me: this fires on the table setup at times if (lastColumnMaxX >= superviewWidth && lastColumnMaxX <= aSize.width || lastColumnMaxX <= superviewWidth && lastColumnMaxX >= aSize.width) _lastColumnShouldSnap = YES; else if (mask === CPTableViewUniformColumnAutoresizingStyle) return; } if (mask === CPTableViewUniformColumnAutoresizingStyle) [self _resizeAllColumnUniformlyWithOldSize:aSize]; else if (mask === CPTableViewLastColumnOnlyAutoresizingStyle) [self sizeLastColumnToFit]; else if (mask === CPTableViewFirstColumnOnlyAutoresizingStyle) [self _autoResizeFirstColumn]; } /*! @ignore */ - (void)_autoResizeFirstColumn { var superview = [self superview]; if (!superview) return; UPDATE_COLUMN_RANGES_IF_NECESSARY(); var count = NUMBER_OF_COLUMNS(), columnToResize = nil, totalWidth = 0, i = 0; for (; i < count; i++) { var column = _tableColumns[i]; if (![column isHidden]) { if (!columnToResize) columnToResize = column; totalWidth += [column width] + _intercellSpacing.width; } } // If there is a visible column if (columnToResize) { var superviewSize = [superview bounds].size, newWidth = superviewSize.width - totalWidth; newWidth += [columnToResize width]; [columnToResize _tryToResizeToWidth:newWidth]; } [self setNeedsLayout]; } /*! @ignore FIX ME: this can be a lot faster */ - (void)_resizeAllColumnUniformlyWithOldSize:(CGSize)oldSize { // what we care about is the superview clip rect // FIX ME: if it's not in a scrollview this doesn't really work var superview = [self superview]; if (!superview || ![superview isKindOfClass:[CPClipView class]]) return; UPDATE_COLUMN_RANGES_IF_NECESSARY(); var superviewWidth = [superview bounds].size.width, count = NUMBER_OF_COLUMNS(), resizableColumns = [CPIndexSet indexSet], remainingSpace = 0.0, i = 0; // find resizable columns // FIX ME: we could cache resizableColumns after this loop and reuse it during the resize for (; i < count; i++) { var tableColumn = _tableColumns[i]; if (![tableColumn isHidden] && ([tableColumn resizingMask] & CPTableColumnAutoresizingMask)) [resizableColumns addIndex:i]; } var maxXofColumns = CGRectGetMaxX([self rectOfColumn:[resizableColumns lastIndex]]), remainingSpace = superviewWidth - maxXofColumns, resizeableColumnsCount = [resizableColumns count], proportionate = 0; while (remainingSpace && resizeableColumnsCount) { // Divy out the space. proportionate += remainingSpace / resizeableColumnsCount; // Reset the remaining space to 0 remainingSpace = 0.0; var index = CPNotFound; while ((index = [resizableColumns indexGreaterThanIndex:index]) !== CPNotFound) { var item = _tableColumns[index], proposedWidth = [item width] + proportionate, resizeLeftovers = [item _tryToResizeToWidth:proposedWidth]; if (resizeLeftovers) { [resizableColumns removeIndex:index]; remainingSpace += resizeLeftovers; } } } // now that we've reached the end we know there are likely rounding errors // so we should size the last resized to fit // find the last visisble column while (count-- && [_tableColumns[count] isHidden]); // find the max x, but subtract a single pixel since the spacing isn't applicable here. var delta = superviewWidth - CGRectGetMaxX([self rectOfColumn:count]) - ([self intercellSpacing].width || 1), newSize = [item width] + delta; [item _tryToResizeToWidth:newSize]; } /*! Sets the column autoresizing style of the receiver to a given style. @param aStyle the column autoresizing style for the receiver. Valid values are:
        CPTableViewNoColumnAutoresizing
        CPTableViewUniformColumnAutoresizingStyle
        CPTableViewLastColumnOnlyAutoresizingStyle
        CPTableViewFirstColumnOnlyAutoresizingStyle
*/ - (void)setColumnAutoresizingStyle:(unsigned)style { //FIX ME: CPTableViewSequentialColumnAutoresizingStyle and CPTableViewReverseSequentialColumnAutoresizingStyle are not yet implemented _columnAutoResizingStyle = style; } /*! Returns the column auto resizing style of the receiver. */ - (unsigned)columnAutoresizingStyle { return _columnAutoResizingStyle; } /*! Resizes the last column if there's room so the receiver fits exactly within its enclosing clip view. */ - (void)sizeLastColumnToFit { _lastColumnShouldSnap = YES; 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 column that's not hidden while (count-- && [_tableColumns[count] isHidden]); // If the last column exists if (count >= 0) { var columnToResize = _tableColumns[count], newSize = MAX(0.0, superviewSize.width - CGRectGetMinX([self rectOfColumn:count]) - _intercellSpacing.width); [columnToResize _tryToResizeToWidth:newSize]; } [self setNeedsLayout]; } /*! Informs the receiver that the number of records in the datasource has changed. */ - (void)noteNumberOfRowsChanged { var oldNumberOfRows = _numberOfRows; _numberOfRows = [self _numberOfRows]; _cachedRowHeights = []; // this line serves two purposes // 1. it updates the _numberOfRows cache with the -numberOfRows call // 2. it updates the row height cache if needed [self _noteHeightOfRowsWithIndexesChanged:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, _numberOfRows)]]; // remove row indexes from the selection if they no longer exist var hangingSelections = oldNumberOfRows - _numberOfRows; if (hangingSelections > 0) { var previousSelectionCount = [_selectedRowIndexes count]; [_selectedRowIndexes removeIndexesInRange:CPMakeRange(_numberOfRows, hangingSelections)]; if (![_selectedRowIndexes containsIndex:[self selectedRow]]) _lastSelectedRow = CPNotFound; // For optimal performance, only send a notification if indices were actually removed. if (previousSelectionCount > [_selectedRowIndexes count]) [self _noteSelectionDidChange]; } [self tile]; } /* Populates the row height cache if necessary. */ - (void)_populateRowHeightCacheIfNeeded { if ([self numberOfRows] !== _cachedRowHeights.length) [self noteHeightOfRowsWithIndexesChanged:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, _numberOfRows)]]; } /*! Informs the receiver that the rows specified in indexSet have changed height. @param anIndexSet an index set containing the indexes of the rows which changed height */ - (void)_noteHeightOfRowsWithIndexesChanged:(CPIndexSet)anIndexSet { if (!HAS_VARIABLE_ROW_HEIGHTS()) return; // Update the height of the given rows by calling the delegate. Since the row height cache also contains // y coordinates, we have to update the y coordinates of all rows below the first valid row in the range. var i = [anIndexSet indexGreaterThanOrEqualToIndex:0]; if (i === CPNotFound) return; var y = i < _cachedRowHeights.length ? _cachedRowHeights[i].y : 0; for (var count = [self numberOfRows]; i < count; ++i) { var height; if ([anIndexSet containsIndex:i]) height = [self _sendDelegateHeightOfRow:i]; else height = _cachedRowHeights[i].height || _rowHeight; // in case the cache entry is empty _cachedRowHeights[i] = {y:y, height:height}; y += height + _intercellSpacing.height; } } /*! Informs the receiver that the rows specified in indexSet have changed height. @param anIndexSet an index set containing the indexes of the rows which changed height */ - (void)noteHeightOfRowsWithIndexesChanged:(CPIndexSet)anIndexSet { [self _noteHeightOfRowsWithIndexesChanged:anIndexSet]; [self _reloadDataViews]; } /*! Lays out the dataviews and resizes the tableview so that everything fits. */ - (void)tile { UPDATE_COLUMN_RANGES_IF_NECESSARY(); var width = _tableColumnRanges.length > 0 ? CPMaxRange([_tableColumnRanges lastObject]) : 0.0, superview = [self superview], height = 0; if (!HAS_VARIABLE_ROW_HEIGHTS()) height = FULL_ROW_HEIGHT() * _numberOfRows; else if (_numberOfRows > 0) { [self _populateRowHeightCacheIfNeeded]; var heightInfo = _cachedRowHeights[_cachedRowHeights.length - 1]; height = ROW_BOTTOM(heightInfo); } 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]; } /*! Scrolls the receiver vertically in an enclosing CPClipView so the row specified by rowIndex is visible. @param aRowIndex the index of the row to scroll to. */ - (void)scrollRowToVisible:(int)rowIndex { var visible = [self visibleRect], rowRect = [self rectOfRow:rowIndex]; visible.origin.y = rowRect.origin.y; visible.size.height = rowRect.size.height; [self scrollRectToVisible:visible]; } /*! Scrolls the receiver and header view horizontally in an enclosing CPClipView so the column specified by columnIndex is visible. @param aColumnIndex the index of the column to scroll to. */ - (void)scrollColumnToVisible:(int)columnIndex { var visible = [self visibleRect], colRect = [self rectOfColumn:columnIndex]; visible.origin.x = colRect.origin.x; visible.size.width = colRect.size.width; [self scrollRectToVisible:visible]; [_headerView scrollRectToVisible:colRect]; } /*! Set the name under which the table information is automatically saved to theAutosaveName. The table information is saved separately for each user and for each application that user uses. @note Even though a table view has an autosave name, it may not be saving table information automatically. To set whether table information is being saved automatically, use \c setAutosaveTableColumns: */ - (void)setAutosaveName:(CPString)theAutosaveName { if (_autosaveName === theAutosaveName) return; _autosaveName = theAutosaveName; [self setAutosaveTableColumns:!!theAutosaveName]; [self _restoreFromAutosave]; } /*! Returns the autosave name of the tableview. */ - (CPString)autosaveName { return _autosaveName; } /*! Sets whether the order and width of this table view's columns are automatically saved. The table information is saved separately for each user and for each application that user uses. @note that if autosaveName returns nil, this setting is ignored and table information isn't saved. */ - (void)setAutosaveTableColumns:(BOOL)shouldAutosave { _autosaveTableColumns = shouldAutosave; } /*! Returns YES the table columns should autosave, otherwise NO. */ - (BOOL)autosaveTableColumns { return _autosaveTableColumns; } /*! @ignore */ - (CPString)_columnsKeyForAutosaveName:(CPString)theAutosaveName { return @"CPTableView Columns " + theAutosaveName; } /*! @ignore */ - (BOOL)_autosaveEnabled { return [self autosaveName] && [self autosaveTableColumns]; } /*! @ignore Stores the tablecolumn setup in user defaults. I believe Apple stores the entire encoded table column, in our case that seems overkill since we need to store everything in a cookie. */ - (void)_autosave { if (![self _autosaveEnabled]) return; var userDefaults = [CPUserDefaults standardUserDefaults], autosaveName = [self autosaveName]; var columns = [self tableColumns], columnsSetup = []; for (var i = 0; i < [columns count]; i++) { var column = [columns objectAtIndex:i], metaData = @{ @"identifier": [column identifier], @"width": [column width] }; [columnsSetup addObject:metaData]; } [userDefaults setObject:columnsSetup forKey:[self _columnsKeyForAutosaveName:autosaveName]]; } /*! @ignore */ - (void)_restoreFromAutosave { if (![self _autosaveEnabled]) return; var userDefaults = [CPUserDefaults standardUserDefaults], autosaveName = [self autosaveName], tableColumns = [userDefaults objectForKey:[self _columnsKeyForAutosaveName:autosaveName]]; if ([tableColumns count] != [[self tableColumns] count]) return; for (var i = 0; i < [tableColumns count]; i++) { var metaData = [tableColumns objectAtIndex:i], columnIdentifier = [metaData objectForKey:@"identifier"], column = [self columnWithIdentifier:columnIdentifier], tableColumn = [self tableColumnWithIdentifier:columnIdentifier]; if (tableColumn && column != CPNotFound) { [self _moveColumn:column toColumn:i]; [tableColumn setWidth:[metaData objectForKey:@"width"]]; } } } /*! @anchor setdelegate Sets the delegate of the receiver. @param aDelegate the delegate object for the tableview. The delegate can provide notification for user interaction, display behaviour, contextual menus, and more. @section Providing Views for Rows and Columns: Returns the view used to display the specified row and column. @code - (CPView)tableView:(CPTableView)tableView viewForTableColumn:(CPTableColumn)tableColumn row:(CPInteger)row @endcode @param The view to display the specified column and row. Returning nil is acceptable, and a view will not be shown at that location. @discussion This method is required if you wish to use a view-based table view in Interface builder or cell-based table views without using CPTableColumn's setDataView: method. It is recommended that the implementation of this method first call the CPTableView method makeViewWithIdentifier:owner: passing, respectively, the tableColumn parameter’s identifier and self as the owner to attempt to reuse a view that is no longer visible. The frame of the view returned by this method is not important, and it will be automatically set by the table. The view's properties should be properly set up before returning the result. The delegate do not need to implement tableview:objectValueforTableColumn:row:. When using bindings, this method is optional if at least one identifier has been associated with the table view at design time. If this method is not implemented, the table will automatically call the CPTableView method makeViewWithIdentifier:owner: with the tableColumn parameter’s identifier and the table view’s delegate respectively as parameters, to attempt to reuse a previous view, or automatically unarchive a prototype associated with the table view. The autoresizingMask of the returned view will automatically be set to CPViewNotSizable if the data view was created in Interface builder in a table column. Otherwise, for example if the view is created manually in code, this method expects the data view to have a CPViewNotSizable mask. @section displayingcells Displaying Cells: Called when the tableview is about to display a dataview @code - (void)tableView:(CPTableView)aTableView willDisplayView:(id)aView forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)rowIndex; @endcode Group rows are a way to separate a groups of data in a tableview. Return YES if the given row is a group row, otherwise NO. @code - (BOOL)tableView:(CPTableView)tableView isGroupRow:(CPInteger)row; @endcode @section editingcells Editing Cells: Return YES if the dataview at a given index and column should be edited, otherwise NO. @code - (BOOL)tableView:(CPTableView)aTableView shouldEditTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)rowIndex; @endcode @section sizes Setting Row and Column Size: Return the height of the row at a given index. Only implement this if you want variable row heights. Otherwise use setRowHeight: on the tableview. @code - (float)tableView:(CPTableView)tableView heightOfRow:(CPInteger)row; @endcode @section selection Selecting in the TableView: @note These methods are only called when the user does something.@endnote Return YES if the selection of the tableview should change, otherwise NO to keep the current selection. @code - (BOOL)selectionShouldChangeInTableView:(CPTableView)aTableView; @endcode Return YES if the row at a given index should be selected, other NO to deny the selection. @code - (BOOL)tableView:(CPTableView)aTableView shouldSelectRow:(CPInteger)rowIndex; @endcode Return YES if the table column given should be selected, otherwise NO to deny the selection. @code - (BOOL)tableView:(CPTableView)aTableView shouldSelectTableColumn:(CPTableColumn)aTableColumn; @endcode Informs the delegate that the tableview is in the process of changing the selection. This usually happens when the user is dragging their mouse across rows. @code - (void)tableViewSelectionIsChanging:(CPNotification)aNotification @endcode Informs the delegate that the tableview selection has changed. @code - (void)tableViewSelectionDidChange:(CPNotification)aNotification; @endcode @section movingandresizingcolumns Moving and Resizing Columns: Return YES if the column at a given index should move to a new column index, otherwise NO. When a column is initially dragged by the user, the delegate is first called with a newColumnIndex value of -1 @code - (BOOL)tableView:(CPTableView)tableView shouldReorderColumn:(CPInteger)columnIndex toColumn:(CPInteger)newColumnIndex; @endcode Notifies the delegate that the tableview drag occurred. This is sent on mouse up. @code - (void)tableView:(CPTableView)tableView didDragTableColumn:(CPTableColumn)tableColumn; @endcode Notifies the delegate that a table column was moved by the user. @code - (void)tableViewColumnDidMove:(CPNotification)aNotification; @endcode Notifies the delegate that the user resized the table column @code - (void)tableViewColumnDidResize:(CPNotification)aNotification; @endcode @section mousevents Responding to Mouse Events: Sent when the user clicks a table column but doesn't drag. @code - (void)tableView:(CPTableView)tableView didClickTableColumn:(CPTableColumn)tableColumn; @endcode Notify the delegate that the user has clicked the table header of a column. @code - (void)tableView:(CPTableView)tableView mouseDownInHeaderOfTableColumn:(CPTableColumn)tableColumn; @endcode @section contextualmenus Contextual Menus: Called when the user right-clicks on the tableview. -1 is passed for the row or column if the user doesn't right click on a real row or column Return a CPMenu that should be displayed if the user right-clicks. If you do not implement this the tableview will call super on menuForEvent @code - (CPMenu)tableView:(CPTableView)aTableView menuForTableColumn:(CPTableColumn)aColumn row:(CPInteger)aRow; @endcode @section deletekey Delete Key Called when the user presses the delete key. Many times you will want to delete data (or prompt for deletion) when the user presses the delete key. Your delegate can implement this method to avoid subclassing the tableview to add this behaviour. @code - (void)tableViewDeleteKeyPressed:(CPTableView)aTableView; @endcode */ - (void)setDelegate:(id )aDelegate { if (_delegate === aDelegate) return; _delegate = aDelegate; _implementedDelegateMethods = 0; if ([_delegate respondsToSelector:@selector(selectionShouldChangeInTableView:)]) _implementedDelegateMethods |= CPTableViewDelegate_selectionShouldChangeInTableView_; if ([_delegate respondsToSelector:@selector(tableView:viewForTableColumn:row:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_viewForTableColumn_row_; else if ([_delegate respondsToSelector:@selector(tableView:dataViewForTableColumn:row:)]) { _implementedDelegateMethods |= CPTableViewDelegate_tableView_dataViewForTableColumn_row_; CPLog.warn("tableView:dataViewForTableColumn: is deprecated. You should use -tableView:viewForTableColumn: where you can request the view with -makeViewWithIdentifier:owner:"); } [self _updateIsViewBased]; 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(tableView:willRemoveView:forTableColumn:row:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_willRemoveView_forTableColumn_row_; if ([_delegate respondsToSelector:@selector(tableView:menuForTableColumn:row:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableViewMenuForTableColumn_row_; if ([_delegate respondsToSelector:@selector(tableView:shouldReorderColumn:toColumn:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableView_shouldReorderColumn_toColumn_; if ([_delegate respondsToSelector:@selector(tableViewColumnDidMove:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableViewColumnDidMove_; if ([_delegate respondsToSelector:@selector(tableViewColumnDidResize:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableViewColumnDidResize_; if ([_delegate respondsToSelector:@selector(tableViewSelectionDidChange:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableViewSelectionDidChange_; if ([_delegate respondsToSelector:@selector(tableViewSelectionIsChanging:)]) _implementedDelegateMethods |= CPTableViewDelegate_tableViewSelectionIsChanging_; } /*! Returns the delegate object for the table view. */ - (id)delegate { return _delegate; } /*! @ignore */ - (void)_didClickTableColumn:(CPInteger)clickedColumn modifierFlags:(unsigned)modifierFlags { [self _changeSortDescriptorsForClickOnColumn:clickedColumn]; if (_allowsColumnSelection) { if ([self _sendDelegateSelectionShouldChangeInTableView] && [self _sendDelegateShouldSelectTableColumn:clickedColumn]) { [self _noteSelectionIsChanging]; if (modifierFlags & CPPlatformActionKeyMask) { 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 _sendDelegateDidClickTableColumn:clickedColumn]; } // From GNUSTEP /*! @ignore */ - (void)_changeSortDescriptorsForClickOnColumn:(CPInteger)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]; [self setHighlightedTableColumn:tableColumn]; [self setSortDescriptors:newSortDescriptors]; } /*! Sets the indicator image of aTableColumn to anImage. The tableview will set the sort indicator images automatically; if you want a different image you can supply it here. @param anImage the image for the column @param aTableColumn the table column object for which to set the image */ - (void)setIndicatorImage:(CPImage)anImage inTableColumn:(CPTableColumn)aTableColumn { if (aTableColumn) { var headerView = [aTableColumn headerView]; if ([headerView respondsToSelector:@selector(_setIndicatorImage:)]) [headerView _setIndicatorImage:anImage]; } } /*! @ignore */ - (CPImage)_tableHeaderSortImage { return [self currentValueForThemeAttribute:@"sort-image"]; } /*! @ignore */ - (CPImage)_tableHeaderReverseSortImage { return [self currentValueForThemeAttribute:@"sort-image-reversed"]; } /*! Returns the CPTableColumn object of the highlighted table column. */ - (CPTableColumn)highlightedTableColumn { return _currentHighlightedTableColumn; } /*! Sets the table column for which the header should be highlighted. */ - (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 [rowIndexes count] > 0 && [self numberOfRows] > 0 && [self numberOfColumns] > 0; } /*! Computes and returns an image to use for dragging. This method is invoked ONLY IF dragViewForRowsWithIndexes:tableColumns:event:offset: returns nil. @param dragRows an index set with the dragged row indexes @param theTableColumns an array of the table columns which are being dragged @param dragEvent the event which initiated the drag @param offset a point at which to set the drag image to be offset from the cursor @return CPImage an image to use for the drag feedback */ - (CPImage)dragImageForRowsWithIndexes:(CPIndexSet)dragRows tableColumns:(CPArray)theTableColumns event:(CPEvent)dragEvent offset:(CGPoint)dragImageOffset { return [self valueForThemeAttribute:@"image-generic-file"]; } /*! Computes and returns a view to use for dragging. By default this is a slightly transparent copy of the dataviews which are being dragged. You can override this in a subclass to show different dragging feedback. Additionally you can return nil from this method and implement: - (CPImage)dragImageForRowsWithIndexes:tableColumns:event:offset: - if you want to return a simple image. @param dragRows an index set with the dragged row indexes @param theTableColumns an array of the table columns which are being dragged @param dragEvent the event which initiated the drag @param offset a point at which to set the drag image to be offset from the cursor @return CPView a view used as the dragging feedback */ - (CPView)dragViewForRowsWithIndexes:(CPIndexSet)theDraggedRows tableColumns:(CPArray)theTableColumns event:(CPEvent)theDragEvent offset:(CGPoint)dragViewOffset { var bounds = [self bounds], dragView = [[CPView alloc] initWithFrame:bounds]; [dragView 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) [self _enumerateViewsInRows:theDraggedRows tableColumns:theTableColumns usingBlock:function(v, row, tableColumn, stop) { var column = [_tableColumns indexOfObjectIdenticalTo:tableColumn], newDataView = [self preparedViewAtColumn:column row:row]; [dragView addSubview:newDataView]; }]; var dragPoint = [self convertPoint:[theDragEvent locationInWindow] fromView:nil]; dragViewOffset.x = CGRectGetWidth(bounds) / 2 - dragPoint.x; dragViewOffset.y = CGRectGetHeight(bounds) / 2 - dragPoint.y; return dragView; } - (CPView)_dragViewForColumn:(CPInteger)columnIndex { var headerFrame = [_headerView frame], visibleRect = [self visibleRect], visibleRows = [self rowsInRect:visibleRect], columnRect = [self rectOfColumn:columnIndex], tableColumn = [[self tableColumns] objectAtIndex:columnIndex], tableColumnUID = [tableColumn UID], columnHeaderView = [tableColumn headerView], columnHeaderFrame = [columnHeaderView frame], frame = CGRectMake(MAX(CGRectGetMinX(columnRect) - CGRectGetMinX(visibleRect), 0.0), 0.0, CGRectGetWidth(columnHeaderFrame), CGRectGetHeight(visibleRect) + CGRectGetHeight(headerFrame)); // We need a wrapper view around the header and column, this is what will be dragged var dragView = [[_CPColumnDragDrawingView alloc] initWithFrame:frame]; [dragView setTableView:self]; [dragView setColumnIndex:columnIndex]; [dragView setBackgroundColor:[CPColor clearColor]]; [dragView setAlphaValue:0.6]; // Now a view that clips the column data views, which itself is clipped to the content view var columnVisRect = CGRectIntersection(columnRect, visibleRect); frame = CGRectMake(0.0, CGRectGetHeight(headerFrame), CGRectGetWidth(columnVisRect), CGRectGetHeight(columnVisRect)); var columnClipView = [[CPView alloc] initWithFrame:frame]; [dragView addSubview:columnClipView]; [dragView setColumnClipView:columnClipView]; _draggedColumnIsSelected = [self isColumnSelected:columnIndex]; var columnLeft = CGRectGetMinX(columnRect), offset = CGPointMake(columnLeft, CGRectGetMinY(visibleRect)); [self _enumerateViewsInRows:_exposedRows columns:[CPIndexSet indexSetWithIndex:columnIndex] usingBlock:function(dataView, row, column, stop) { [self _addDraggedDataView:dataView toView:columnClipView forColumn:column row:row offset:offset]; delete (_dataViewsForRows[row][tableColumnUID]); }]; // Add the column header view columnHeaderFrame.origin = CGPointMakeZero(); var dragColumnHeaderView = [[_CPTableColumnHeaderView alloc] initWithFrame:columnHeaderFrame], image = [columnHeaderView _indicatorImage]; [dragColumnHeaderView setStringValue:[columnHeaderView stringValue]]; [dragColumnHeaderView setThemeState:[columnHeaderView themeState]]; [dragColumnHeaderView _setIndicatorImage:image]; // Give it a tag so it can be found later [dragColumnHeaderView setTag:1]; [dragView addSubview:dragColumnHeaderView]; // While dragging, the column is deselected in the table view [_selectedColumnIndexes removeIndex:columnIndex]; return dragView; } - (void)_addDraggedDataView:(CPView)aDataView toView:(CPView)aSuperview forColumn:(CPInteger)column row:(CPInteger)row offset:(CGPoint)offset { var dataViewFrame = [self frameOfDataViewAtColumn:column row:row]; dataViewFrame.origin.x -= offset.x; // Offset by table header height - scroll position dataViewFrame.origin.y -= offset.y; [aDataView setFrame:dataViewFrame]; [aSuperview addSubview:aDataView]; } /*! Sets the default operation mask for the drag behavior of the table view. @note isLocal is not implemented. */ - (void)setDraggingSourceOperationMask:(CPDragOperation)mask forLocal:(BOOL)isLocal { //ignore 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 CPTableViewDropOn or CPTableViewDropAbove, 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, 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. This style is used to determine how the tableview looks when it is the receiver of a drag and drop operation. Can be:
        CPTableViewDraggingDestinationFeedbackStyleNone
        CPTableViewDraggingDestinationFeedbackStyleRegular
        CPTableViewDraggingDestinationFeedbackStyleSourceList
*/ - (void)setDraggingDestinationFeedbackStyle:(CPTableViewDraggingDestinationFeedbackStyle)aStyle { //FIX ME: this should vary up the highlight color, currently nothing is being done with it _destinationDragStyle = aStyle; } /*! Returns the tableview dragging destination feedback style. Can be:
        CPTableViewDraggingDestinationFeedbackStyleNone
        CPTableViewDraggingDestinationFeedbackStyleRegular
        CPTableViewDraggingDestinationFeedbackStyleSourceList
*/ - (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; } /*! Returns YES if vertical motion can begin a drag of the tableview, otherwise NO. */ - (BOOL)verticalMotionCanBeginDrag { return _verticalMotionCanDrag; } - (CPTableColumn)_tableColumnForSortDescriptor:(CPSortDescriptor)theSortDescriptor { var tableColumns = [self tableColumns]; for (var i = 0; i < [tableColumns count]; i++) { var tableColumn = [tableColumns objectAtIndex:i], sortDescriptorPrototype = [tableColumn sortDescriptorPrototype]; if (!sortDescriptorPrototype) continue; if ([sortDescriptorPrototype key] === [theSortDescriptor key] && [sortDescriptorPrototype selector] === [theSortDescriptor selector]) { return tableColumn; } } return nil; } /*! Sets the table view's CPSortDescriptors objects in an array. @param sortDescriptors an array of sort descriptors. */ - (void)setSortDescriptors:(CPArray)sortDescriptors { var oldSortDescriptors = [[self sortDescriptors] copy], newSortDescriptors = [CPArray array]; if (sortDescriptors !== nil) [newSortDescriptors addObjectsFromArray:sortDescriptors]; if ([newSortDescriptors isEqual:oldSortDescriptors]) return; _sortDescriptors = newSortDescriptors; var oldColumn = nil, newColumn = nil; if ([newSortDescriptors count] > 0) { var newMainSortDescriptor = [newSortDescriptors objectAtIndex:0]; newColumn = [self _tableColumnForSortDescriptor:newMainSortDescriptor]; } if ([oldSortDescriptors count] > 0) { var oldMainSortDescriptor = [oldSortDescriptors objectAtIndex:0]; oldColumn = [self _tableColumnForSortDescriptor:oldMainSortDescriptor]; } var image = [newMainSortDescriptor ascending] ? [self _tableHeaderSortImage] : [self _tableHeaderReverseSortImage]; [self setIndicatorImage:nil inTableColumn:oldColumn]; [self setIndicatorImage:image inTableColumn:newColumn]; [self _sendDataSourceSortDescriptorsDidChange:oldSortDescriptors]; var binderClass = [[self class] _binderClassForBinding:@"sortDescriptors"]; [[binderClass getBinding:@"sortDescriptors" forObject:self] reverseSetValueFor:@"sortDescriptors"]; } /*! Returns an array of the current sort descriptors currently used by the table. */ - (CPArray)sortDescriptors { return _sortDescriptors; } /*! @ignore */ - (id)_objectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex useCache:(BOOL)useCache { var objectValue; var tableColumnUID = [aTableColumn UID], tableColumnObjectValues = _objectValues[tableColumnUID]; if (!tableColumnObjectValues) { tableColumnObjectValues = []; _objectValues[tableColumnUID] = tableColumnObjectValues; } if (useCache) objectValue = tableColumnObjectValues[aRowIndex]; // tableView:objectValueForTableColumn:row: is optional if content bindings are in place. if (objectValue === undefined) { if ([self _dataSourceRespondsToObjectValueForTableColumn]) { objectValue = [self _sendDataSourceObjectValueForTableColumn:aTableColumn row:aRowIndex]; tableColumnObjectValues[aRowIndex] = objectValue; } else if (!_isViewBased && ![self infoForBinding:@"content"]) { CPLog.warn(@"no content binding established and data source " + [_dataSource description] + " does not implement tableView:objectValueForTableColumn:row:"); } } return objectValue; } /*! Returns a CGRect of the exposed area of the tableview. */ - (CGRect)exposedRect { if (!_exposedRect) { var superview = [self superview]; // FIXME: Should we be rect intersecting in case // there are multiple views in the clip view? if ([superview isKindOfClass:[CPClipView class]]) _exposedRect = [superview bounds]; else _exposedRect = [self bounds]; } return _exposedRect; } /*! Loads all the data and dataviews for the receiver. */ - (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 _unloadDataViewsInRows:newlyExposedRows columns:newlyExposedColumns]; [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]; // if we have any columns to remove do that here if (_needsDifferedTableColumnRemove) { var removeCount = [_differedColumnDataToRemove count], removeIndexes = [CPIndexSet indexSet]; for (var i = 0; i < removeCount; i++) { var data = _differedColumnDataToRemove[i], tableColumn = data.column, columnIdx = [_tableColumns indexOfObject:tableColumn]; if (columnIdx !== CPNotFound) [removeIndexes addIndex:columnIdx]; } var rowIndexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self numberOfRows])]; [self _unloadDataViewsInRows:rowIndexes columns:removeIndexes]; [_tableColumns removeObjectsAtIndexes:removeIndexes]; _dirtyTableColumnRangeIndex = 0; [self _recalculateTableColumnRanges]; [_differedColumnDataToRemove removeAllObjects]; _needsDifferedTableColumnRemove = NO; } // 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]; } } /*! @ignore */ - (void)_unloadDataViewsInRows:(CPIndexSet)rowIndexes columns:(CPIndexSet)columnIndexes { if (![rowIndexes count] || ![columnIndexes count] || [columnIndexes lastIndex] >= [_tableColumns count]) return; // If and edited view is about to be unloaded, cleanup its state before enqueuing. if ([columnIndexes containsIndex:_editingColumn] && [rowIndexes containsIndex:_editingRow]) [self _resignEditedView]; var tableColumns = [_tableColumns objectsAtIndexes:columnIndexes]; [self _enumerateViewsInRows:rowIndexes tableColumns:tableColumns usingBlock:function(dataView, row, tableColumn, stop) { var dataViewsForRows = _dataViewsForRows[row], tableColumnUID = [tableColumn UID]; delete (dataViewsForRows[tableColumnUID]); [self _sendDelegateWillRemoveView:dataView forTableColumn:tableColumn row:row]; [self _enqueueReusableDataView:dataView]; }]; } /*! @ignore */ - (void)_loadDataViewsInRows:(CPIndexSet)rowIndexes columns:(CPIndexSet)columnIndexes { if (![rowIndexes count] || ![columnIndexes count]) return; UPDATE_COLUMN_RANGES_IF_NECESSARY(); if (_numberOfHiddenColumns > 0) columnIndexes = [columnIndexes indexesPassingTest:function(idx, stop) { return ![_tableColumns[idx] isHidden]; }]; [rowIndexes enumerateIndexesUsingBlock:function(rowIndex, stopRow) { if (!_dataViewsForRows[rowIndex]) _dataViewsForRows[rowIndex] = {}; var dataViewsForRow = _dataViewsForRows[rowIndex], isRowSelected = [self isRowSelected:rowIndex], row = rowIndex; [columnIndexes enumerateIndexesUsingBlock:function(columnIndex, stopCol) { var tableColumn = _tableColumns[columnIndex], tableColumnUID = [tableColumn UID], dataView = [self _preparedViewAtColumn:columnIndex row:row isRowSelected:isRowSelected]; if ([dataView superview] !== self) [self addSubview:dataView]; dataViewsForRow[tableColumnUID] = dataView; }]; }]; } - (CPView)preparedViewAtColumn:(CPInteger)column row:(CPInteger)row { return [self _preparedViewAtColumn:column row:row isRowSelected:[self isRowSelected:row]]; } - (CPView)_preparedViewAtColumn:(CPInteger)column row:(CPInteger)row isRowSelected:(BOOL)isRowSelected { var tableColumn = _tableColumns[column], tableColumnUID = [tableColumn UID], dataView = [self _newDataViewForRow:row tableColumn:tableColumn]; [dataView setFrame:[self frameOfDataViewAtColumn:column row:row]]; [self _setObjectValueForTableColumn:tableColumn row:row forView:dataView]; if (_selectionHighlightStyle !== CPTableViewSelectionHighlightStyleNone && (isRowSelected || [self isColumnSelected:column])) _BlockSelectView(dataView, row, column); else _BlockDeselectView(dataView, row, column); // FIX ME: for performance reasons we might consider diverging from cocoa and moving this to the reloadData method if (_implementedDelegateMethods & CPTableViewDelegate_tableView_isGroupRow_) { if ([_delegate tableView:self isGroupRow:row]) { [_groupRows addIndex:row]; [dataView setThemeState:CPThemeStateGroupRow]; } else { [_groupRows removeIndexesInRange:CPMakeRange(row, 1)]; [dataView unsetThemeState:CPThemeStateGroupRow]; } [self setNeedsDisplay:YES]; } if (!_isViewBased) [self _setEditingState:NO forView:dataView]; [self _sendDelegateWillDisplayView:dataView forTableColumn:tableColumn row:row]; return dataView; } - (void)_setObjectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow forView:(CPView)aDataView { [self _setObjectValueForTableColumn:aTableColumn row:aRow forView:aDataView useCache:!_invalidateObjectValuesCache]; } - (void)_setObjectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow forView:(CPView)aDataView useCache:(BOOL)useCache { if (_implementedDataSourceMethods & CPTableViewDataSource_tableView_objectValueForTableColumn_row_) [aDataView setObjectValue:[self _objectValueForTableColumn:aTableColumn row:aRow useCache:useCache]]; // This gives the table column an opportunity to apply its bindings. // It will override the value set above if there is a binding. if (_contentBindingExplicitlySet) [self _prepareContentBindedDataView:aDataView forRow:aRow]; else // For both cell-based and view-based [aTableColumn _prepareDataView:aDataView forRow:aRow]; } - (void)_prepareContentBindedDataView:(CPView)dataView forRow:(CPInteger)aRow { var binder = [CPTableContentBinder getBinding:@"content" forObject:self], content = [binder content], rowContent = [content objectAtIndex:aRow]; [dataView setObjectValue:rowContent]; } /*! @ignore */ - (void)_layoutViewsForRowIndexes:(CPIndexSet)rowIndexes columnIndexes:(CPIndexSet)columnIndexes { [self _enumerateViewsInRows:rowIndexes columns:columnIndexes usingBlock:function(view, row, column, stop) { [view setFrame:[self frameOfDataViewAtColumn:column row:row]]; }]; } /*! @ignore */ - (CPView)_newDataViewForRow:(CPInteger)aRow tableColumn:(CPTableColumn)aTableColumn { var view = nil; if (_viewForTableColumnRowSelector) view = self.isa.objj_msgSend2(self, _viewForTableColumnRowSelector, aTableColumn, aRow); if (!view) { // For cell-based tables, use the dataView prototype identifier as view identifier // instead of column identifier because 1/ the column identifier may be nil // and 2/ the tableColumn dataView may change after the column identifier was set. var identifier; if (_isViewBased) { identifier = [aTableColumn identifier]; view = [self makeViewWithIdentifier:identifier owner:_delegate]; } else { identifier = [[aTableColumn dataView] UID]; view = [self makeViewWithIdentifier:identifier owner:_delegate]; if (!view) view = [aTableColumn _newDataView]; } [view setIdentifier:identifier]; } return view; } /* Returns a view with the specified identifier. @param identifier The view identifier. Must not be nil. @param owner The owner of the CIB that may be loaded and instantiated to create a new view with the particular identifier. @return A view for the row. @discussion Typically identifier is associated with an external CIB and the table view will automatically instantiate the CIB with the provided owner. The owner of the CIB that may be loaded and instantiated to create a new view with the particular identifier is typically the table view’s delegate. The owner is useful in setting up outlets and target and actions from the view. This method will typically be called by the delegate in tableView:viewForTableColumn:row:, but it can also be overridden to provide custom views for the identifier. This method may also return a reused view with the same identifier that was no longer available on screen. */ - (id)makeViewWithIdentifier:(CPString)anIdentifier owner:(id)anOwner { if (!anIdentifier) return nil; var view = nil, // See if we have some reusable views available reusableViews = _cachedDataViews[anIdentifier]; if (reusableViews && reusableViews.length) view = reusableViews.pop(); // Otherwise see if we have a view in the cib with this identifier else if (_isViewBased) view = [self _unarchiveViewWithIdentifier:anIdentifier owner:anOwner]; return view; } /*! Returns an instance of CPView @param column The index of the column in the tableColumn array @param row The index of the row @param makeIfNecessary YES if a view is required, NO if you want to update properties on a view, if one is available. @discussion This method first attempts to return an available view, which is generally in the visible area. If there is no available view, and makeIfNecessary is YES, a prepared temporary view is returned. If makeIfNecessary is NO, and the view is not available, nil will be returned. An exception will be thrown if row is an invalid row index and if column is an invalid column index */ - (id)viewAtColumn:(CPInteger)column row:(CPInteger)row makeIfNecessary:(BOOL)makeIfNecessary { if (row > (_numberOfRows - 1)) [CPException raise:CPInvalidArgumentException reason:@"Row " + row + " out of row range [0-" + (_numberOfRows - 1) + "] for rowViewAtRow:createIfNeeded:"]; if (column > (NUMBER_OF_COLUMNS() - 1)) [CPException raise:CPInvalidArgumentException reason:@"Column " + column + " out of row range [0-" + (NUMBER_OF_COLUMNS ()- 1) + "] for rowViewAtRow:createIfNeeded:"]; var dataViewsForRow = _dataViewsForRows[row], tableColumn = _tableColumns[column], tableColumnUID = [tableColumn UID], view = dataViewsForRow ? dataViewsForRow[tableColumnUID] : nil; if (!makeIfNecessary) return view || nil; if (!view) { if (!dataViewsForRow) { dataViewsForRow = {} _dataViewsForRows[row] = dataViewsForRow; } // Here we will add this view to the tableView and add it to the exposedRows adn columns. // The view will be deleted if necessary during the next run loop cycle of the table view // This is how cocoa works view = [self preparedViewAtColumn:column row:row]; if ([view superview] !== self) [self addSubview:view]; dataViewsForRow[tableColumnUID] = view; [_exposedRows addIndex:row]; [_exposedColumns addIndex:column]; // Make sure to layout the tableView again [self setNeedsLayout]; } return view; } /*! @ignore */ - (CPView)_unarchiveViewWithIdentifier:(CPString)anIdentifier owner:(id)anOwner { var cib = [_archivedDataViews objectForKey:anIdentifier]; if (!cib && !_unavailable_custom_cibs[anIdentifier]) { var bundle = anOwner ? [CPBundle bundleForClass:[anOwner class]] : [CPBundle mainBundle]; cib = [[CPCib alloc] initWithCibNamed:anIdentifier bundle:bundle]; } if (!cib) { _unavailable_custom_cibs[anIdentifier] = YES; return nil; } var objects = [], load = [cib instantiateCibWithOwner:anOwner topLevelObjects:objects]; if (!load) return nil; var count = objects.length; while (count--) { var obj = objects[count]; if ([obj isKindOfClass:[CPView class]]) { [obj setIdentifier:anIdentifier]; [obj setAutoresizingMask:CPViewNotSizable]; return obj; } } return nil; } - (void)_updateIsViewBased { if ([self _delegateRespondsToViewForTableColumn]) _viewForTableColumnRowSelector = @selector(_sendDelegateViewForTableColumn:row:); else if ([self _delegateRespondsToDataViewForTableColumn]) _viewForTableColumnRowSelector = @selector(_sendDelegateDataViewForTableColumn:row:); _isViewBased = (_viewForTableColumnRowSelector !== nil || _archivedDataViews !== nil); } /*! Allows the enumeration of all the table views that are known to the table view. @param handler The function to apply to elements in the set. The function takes three arguments: @param dataView The view for the column and row. @param column The column index of the view. @param row The row index of the view. */ - (void)enumerateAvailableViewsUsingBlock:(Function/*CPView *dataView, CPInteger row, CPInteger column*, @ref stop*/)handler { [self reloadData]; [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; [self _enumerateViewsInRows:_exposedRows columns:_exposedColumns usingBlock:handler]; } /*! @ignore */ - (void)_enumerateViewsInRows:(CPIndexSet)rowIndexes columns:(CPIndexSet)columnIndexes usingBlock:(Function/*CPView dataView, CPInteger row, CPInteger column, @ref stop*/)handler { [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; [rowIndexes enumerateIndexesUsingBlock:function(rowIndex, stopRow) { var dataViewsForRow = _dataViewsForRows[rowIndex]; if (!dataViewsForRow) return; var row = rowIndex; [columnIndexes enumerateIndexesUsingBlock:function(columnIndex, stopCol) { var tableColumnUID = [_tableColumns[columnIndex] UID], view = dataViewsForRow[tableColumnUID]; if (view) handler(view, row, columnIndex, stopCol); if (stopCol()) stopRow(YES); }]; }]; } - (void)_enumerateViewsInRows:(CPIndexSet)rowIndexes tableColumns:(CPArray)tableColumns usingBlock:(Function/*CPView dataView, CPInteger row, CPtableColumn tableColumn, CPInteger column, @ref stop*/)handler { [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; [rowIndexes enumerateIndexesUsingBlock:function(rowIndex, stopRow) { var dataViewsForRow = _dataViewsForRows[rowIndex]; if (!dataViewsForRow) return; var row = rowIndex; [tableColumns enumerateObjectsUsingBlock:function(tableColumn, idx, stopCol) { var tableColumnUID = [tableColumn UID], view = dataViewsForRow[tableColumnUID]; if (view) handler(view, row, tableColumn, stopCol); if (stopCol()) stopRow(YES); }]; }]; } /*! @ignore */ - (void)_enqueueReusableDataView:(CPView)aDataView { if (!aDataView) return; // FIXME: yuck! var identifier = [aDataView identifier]; if ([aDataView hasThemeState:CPThemeStateSelectedDataView]) [aDataView unsetThemeState:CPThemeStateSelectedDataView]; if (!_cachedDataViews[identifier]) _cachedDataViews[identifier] = [aDataView]; else _cachedDataViews[identifier].push(aDataView); } /*! @ignore // we override here because we have to adjust the header */ - (void)setFrameSize:(CGSize)aSize { [super setFrameSize:aSize]; if (_headerView) [_headerView setFrameSize:CGSizeMake(CGRectGetWidth([self frame]), CGRectGetHeight([_headerView frame]))]; _exposedRect = nil; } /*! @ignore */ - (void)setFrameOrigin:(CGPoint)aPoint { [super setFrameOrigin:aPoint]; _exposedRect = nil; } /*! @ignore */ - (void)setBoundsOrigin:(CGPoint)aPoint { [super setBoundsOrigin:aPoint]; _exposedRect = nil; } /*! @ignore */ - (void)setBoundsSize:(CGSize)aSize { [super setBoundsSize:aSize]; _exposedRect = nil; } /*! @ignore */ - (void)setNeedsDisplay:(BOOL)aFlag { [super setNeedsDisplay:aFlag]; [_tableDrawView setNeedsDisplay:aFlag]; [[self headerView] setNeedsDisplay:YES]; } /*! @ignore */ - (void)setNeedsLayout { [super setNeedsLayout]; [[self headerView] setNeedsLayout]; } /*! @ignore */ - (BOOL)_isFocused { return [[self window] isKeyWindow] && ([[self window] firstResponder] === self || _editingRow !== CPNotFound); } /*! @ignore */ - (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 highlightSelectionInClipRect:exposedRect]; [self drawGridInClipRect:exposedRect]; if (_implementsCustomDrawRow) [self _drawRows:_exposedRows clipRect:exposedRect]; } /*! Draws the background in a given clip rect. This method should only be overridden if you want something other than a solid color or alternating row colors. @note this method should not be called directly, instead use \c setNeedsDisplay: */ - (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; } var exposedRows = [self _exposedRowsInRect:aRect], firstRow = FLOOR(exposedRows.location / colorCount) * colorCount, lastRow = CPMaxRange(exposedRows) - 1, colorIndex = 0, groupRowRects = []; //loop through each color so we only draw once for each color while (colorIndex < colorCount) { CGContextBeginPath(context); for (var row = firstRow + colorIndex; row <= lastRow; row += colorCount) { // if it's not a group row draw it otherwise we draw it later if (![_groupRows containsIndex:row]) CGContextAddRect(context, CGRectIntersection(aRect, [self _rectOfRow:row checkRange:NO])); else groupRowRects.push(CGRectIntersection(aRect, [self _rectOfRow:row checkRange:NO])); } CGContextClosePath(context); CGContextSetFillColor(context, rowColors[colorIndex]); CGContextFillPath(context); colorIndex++; } [self _drawGroupRowsForRects:groupRowRects]; } /*! Draws the grid for the tableview based on the set grid mask in a given clip rect. @note this method should not be called directly, instead use setNeedsDisplay: */ - (void)drawGridInClipRect:(CGRect)aRect { var context = [[CPGraphicsContext currentContext] graphicsPort], gridStyleMask = [self gridStyleMask], lineThickness = [self currentValueForThemeAttribute:@"grid-line-thickness"]; if (!(gridStyleMask & (CPTableViewSolidHorizontalGridLineMask | CPTableViewSolidVerticalGridLineMask))) return; CGContextBeginPath(context); if (gridStyleMask & CPTableViewSolidHorizontalGridLineMask) { var exposedRows = [self _exposedRowsInRect:aRect], row = exposedRows.location, lastRow = CPMaxRange(exposedRows) - 1, rowY = -lineThickness / 2, 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 checkRange:NO], rowY = CGRectGetMaxY(rowRect) - lineThickness / 2; CGContextMoveToPoint(context, minX, rowY); CGContextAddLineToPoint(context, maxX, rowY); } if (_rowHeight > 0.0) { var rowHeight = FULL_ROW_HEIGHT(), totalHeight = CGRectGetMaxY(aRect) - lineThickness / 2; 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) - lineThickness / 2; CGContextMoveToPoint(context, columnX, minY); CGContextAddLineToPoint(context, columnX, maxY); } } CGContextClosePath(context); CGContextSetStrokeColor(context, [self gridColor]); CGContextSetLineWidth(context, lineThickness); CGContextStrokePath(context); } /*! Draws the selection with the set selection highlight style in a given clip rect. You can change the highlight style to a source list style gradient in setSelectionHighlightStyle: @note this method should not be called directly, instead use \c setNeedsDisplay: */ - (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 = count = [indexes count]; if (!count) return; var drawGradient = (CPFeatureIsCompatible(CPHTMLCanvasFeature) && _selectionHighlightStyle === CPTableViewSelectionHighlightStyleSourceList && [_selectedRowIndexes count] >= 1), deltaHeight = 0.5 * (_gridStyleMask & CPTableViewSolidHorizontalGridLineMask), focused = [self _isFocused]; CGContextBeginPath(context); if (drawGradient) { var gradientCache = focused ? [self selectionGradientColors] : [self unfocusedSelectionGradientColors], topLineColor = [gradientCache objectForKey:CPSourceListTopLineColor], bottomLineColor = [gradientCache objectForKey:CPSourceListBottomLineColor], gradientColor = [gradientCache objectForKey:CPSourceListGradient]; } var normalSelectionHighlightColor = focused ? [self selectionHighlightColor] : [self unfocusedSelectionHighlightColor]; // don't do these lookups if there are no group rows if ([_groupRows count]) { var topGroupLineColor = [CPColor colorWithCalibratedWhite:212.0 / 255.0 alpha:1.0], bottomGroupLineColor = [CPColor colorWithCalibratedWhite:185.0 / 255.0 alpha:1.0], gradientGroupColor = CGGradientCreateWithColorComponents(CGColorSpaceCreateDeviceRGB(), [212.0 / 255.0, 212.0 / 255.0, 212.0 / 255.0, 1.0, 197.0 / 255.0, 197.0 / 255.0, 197.0 / 255.0, 1.0], [0, 1], 2); } while (count--) { var currentIndex = indexes[count], rowRect = CGRectIntersection(self.isa.objj_msgSend1(self, rectSelector, currentIndex), aRect); // group rows get the same highlight style as other rows if they're source list... if (!drawGradient) var shouldUseGroupGradient = [_groupRows containsIndex:currentIndex]; if (drawGradient || shouldUseGroupGradient) { var minX = CGRectGetMinX(rowRect), minY = CGRectGetMinY(rowRect), maxX = CGRectGetMaxX(rowRect), maxY = CGRectGetMaxY(rowRect) - deltaHeight; if (!drawGradient) { //If there is no source list gradient we need to close the selection path and fill it now [normalSelectionHighlightColor setFill]; CGContextClosePath(context); CGContextFillPath(context); CGContextBeginPath(context); } CGContextAddRect(context, rowRect); CGContextDrawLinearGradient(context, (shouldUseGroupGradient) ? gradientGroupColor : gradientColor, rowRect.origin, CGPointMake(minX, maxY), 0); CGContextBeginPath(context); CGContextMoveToPoint(context, minX, minY + .5); CGContextAddLineToPoint(context, maxX, minY + .5); CGContextSetStrokeColor(context, (shouldUseGroupGradient) ? topGroupLineColor : topLineColor); CGContextStrokePath(context); CGContextBeginPath(context); CGContextMoveToPoint(context, minX, maxY - .5); CGContextAddLineToPoint(context, maxX, maxY - .5); CGContextSetStrokeColor(context, (shouldUseGroupGradient) ? bottomGroupLineColor : bottomLineColor); CGContextStrokePath(context); } else { var radius = [self currentValueForThemeAttribute:@"selection-radius"]; if (radius > 0) { var minX = CGRectGetMinX(rowRect), maxX = CGRectGetMaxX(rowRect), minY = CGRectGetMinY(rowRect), maxY = CGRectGetMaxY(rowRect); CGContextMoveToPoint(context, minX + radius, minY); CGContextAddArcToPoint(context, maxX, minY, maxX, minY + radius, radius); CGContextAddArcToPoint(context, maxX, maxY, maxX - radius, maxY, radius); CGContextAddArcToPoint(context, minX, maxY, minX, maxY - radius, radius); CGContextAddArcToPoint(context, minX, minY, minX + radius, minY, radius); } else CGContextAddRect(context, rowRect); } } CGContextClosePath(context); if (!drawGradient) { [normalSelectionHighlightColor setFill]; CGContextFillPath(context); } CGContextBeginPath(context); var gridStyleMask = [self gridStyleMask]; for (var i = 0; i < count2; i++) { var rect = self.isa.objj_msgSend1(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); } /*! @ignore Draws the group rows FIX ME: this should be themed... */ - (void)_drawGroupRowsForRects:(CPArray)rects { if ((CPFeatureIsCompatible(CPHTMLCanvasFeature) && _selectionHighlightStyle === CPTableViewSelectionHighlightStyleSourceList) || !rects.length) return; var context = [[CPGraphicsContext currentContext] graphicsPort], i = rects.length; CGContextBeginPath(context); var gradientCache = [self selectionGradientColors], topLineColor = [CPColor colorWithHexString:"d3d3d3"], bottomLineColor = [CPColor colorWithHexString:"bebebd"], gradientColor = CGGradientCreateWithColorComponents(CGColorSpaceCreateDeviceRGB(), [220.0 / 255.0, 220.0 / 255.0, 220.0 / 255.0, 1.0, 199.0 / 255.0, 199.0 / 255.0, 199.0 / 255.0, 1.0], [0, 1], 2), drawGradient = YES; while (i--) { var rowRect = rects[i]; CGContextAddRect(context, rowRect); if (drawGradient) { var minX = CGRectGetMinX(rowRect), minY = CGRectGetMinY(rowRect), maxX = CGRectGetMaxX(rowRect), maxY = CGRectGetMaxY(rowRect); CGContextDrawLinearGradient(context, gradientColor, rowRect.origin, CGPointMake(minX, maxY), 0); CGContextBeginPath(context); CGContextMoveToPoint(context, minX, minY); CGContextAddLineToPoint(context, maxX, minY); CGContextSetStrokeColor(context, topLineColor); CGContextStrokePath(context); CGContextBeginPath(context); CGContextMoveToPoint(context, minX, maxY); CGContextAddLineToPoint(context, maxX, maxY - 1); CGContextSetStrokeColor(context, bottomLineColor); CGContextStrokePath(context); } } } /*! @ignore */ - (void)_drawRows:(CPIndexSet)rowsIndexes clipRect:(CGRect)clipRect { var row = [rowsIndexes firstIndex]; while (row !== CPNotFound) { [self drawRow:row clipRect:CGRectIntersection(clipRect, [self rectOfRow:row])]; row = [rowsIndexes indexGreaterThanIndex:row]; } } /*! While this method doesn't do anything in Cappuccino, subclasses can override it to customize the appearance of a row. @note \c tableView:willDisplayView:forTableColumn:row is sent to the delegate before drawing */ - (void)drawRow:(CPInteger)row clipRect:(CGRect)rect { // This method does currently nothing in cappuccino. Can be overridden by subclasses. } /*! @ignore */ - (void)layoutSubviews { [self load]; } /*! @ignore */ - (void)viewWillMoveToSuperview:(CPView)aView { if ([aView isKindOfClass:[CPClipView class]]) { _observedClipView = aView; } else { [self _stopObservingClipView]; _observedClipView = nil; } [super viewWillMoveToSuperview:aView]; } /*! @ignore */ - (void)superviewBoundsChanged:(CPNotification)aNotification { _exposedRect = nil; [self setNeedsDisplay:YES]; [self setNeedsLayout]; } /*! @ignore */ - (void)superviewFrameChanged:(CPNotification)aNotification { _exposedRect = nil; [self tile]; } /* @ignore */ - (BOOL)tracksMouseOutsideOfFrame { return YES; } /* @ignore */ - (BOOL)startTrackingAt:(CGPoint)aPoint { // Try to become the first responder, but if we can't, that's okay. [[self window] makeFirstResponder:self]; var row = [self rowAtPoint:aPoint]; _clickedRow = row; _clickedColumn = [self columnAtPoint:aPoint]; // If the user clicks outside a row then deselect everything. if (row < 0 && _allowsEmptySelection) { if ([self _sendDelegateSelectionShouldChangeInTableView]) { var indexSet = [self _sendDelegateSelectionIndexesForProposedSelection:[CPIndexSet indexSet]]; [self _noteSelectionIsChanging]; [self selectRowIndexes:indexSet byExtendingSelection:NO]; } } 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 ([self _dataSourceRespondsToSetObjectValueForTableColumnRow]) _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 && !([self _dataSourceRespondsToWriteRowsWithIndexesToPasteboard])) [self _updateSelectionWithMouseAtRow:row]; return YES; } /*! @ignore */ - (CPMenu)menuForEvent:(CPEvent)theEvent { if (!([self _delegateRespondsToMenuForTableColumnRow])) return [super menuForEvent:theEvent]; var location = [self convertPoint:[theEvent locationInWindow] fromView:nil], row = [self rowAtPoint:location], column = [self columnAtPoint:location], tableColumn = [[self tableColumns] objectAtIndex:column]; return [self _sendDelegateMenuForTableColumn:tableColumn row:row]; } /* @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]; if ([anEvent type] == CPLeftMouseUp) { _clickedRow = CPNotFound; _clickedColumn = CPNotFound; } } /* @ignore */ - (BOOL)continueTracking:(CGPoint)lastPoint at:(CGPoint)aPoint { var row = [self rowAtPoint:aPoint]; _clickedRow = row; _clickedColumn = [self columnAtPoint: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 && [self _dataSourceRespondsToWriteRowsWithIndexesToPasteboard]) { 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] && [self _sendDataSourceWriteRowsWithIndexes:_draggedRowIndexes toPasteboard:pboard]) { var currentEvent = [CPApp currentEvent], offset = CGPointMakeZero(), 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 preferred 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:CGRectMake(0, 0, [image size].width, [image size].height)]; [view setImage:image]; } var bounds = [view bounds], viewLocation = CGPointMake(aPoint.x - CGRectGetWidth(bounds) / 2 + offset.x, aPoint.y - CGRectGetHeight(bounds) / 2 + offset.y); [self dragView:view at:viewLocation offset:CGPointMakeZero() 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 ([self _dataSourceRespondsToSetObjectValueForTableColumnRow] && !_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 = -1, column, rowIndex, shouldEdit = YES; _clickedRow = [self rowAtPoint:aPoint]; _clickedColumn = [self columnAtPoint:aPoint]; if ([self _dataSourceRespondsToWriteRowsWithIndexesToPasteboard]) { rowIndex = _clickedRow; 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]; } } // Accept either tableView:setObjectValue:forTableColumn:row: delegate method, or a binding. if (!_isViewBased && mouseIsUp && !_trackingPointMovedOutOfClickSlop && ([[CPApp currentEvent] clickCount] > 1) && ([self _dataSourceRespondsToSetObjectValueForTableColumnRow] || [self infoForBinding:@"content"])) { columnIndex = _clickedColumn; if (columnIndex !== -1) { column = _tableColumns[columnIndex]; if ([column isEditable]) { rowIndex = [self rowAtPoint:aPoint]; if (rowIndex !== -1) { if ([self _sendDelegateShouldEditTableColumn:column row:rowIndex]) { [self editColumn:columnIndex row:rowIndex withEvent:nil select:YES]; return; } } } } } //end of editing conditional //double click actions if ([[CPApp currentEvent] clickCount] === 2 && _doubleAction) [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 _sendDataSourceValidateDrop:sender proposedRow:row proposedDropOperation:dropOperation]; } return CPDragOperationNone; } /* @ignore */ - (void)draggingExited:(id)sender { [_dropOperationFeedbackView removeFromSuperview]; } /* @ignore */ - (void)draggingEnded:(id)sender { [self _draggingEnded]; } /*! @ignore */ - (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 too 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 = CGRectInset(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 { var row = [self rowAtPoint:dragPoint], // 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), numberOfRows = [self numberOfRows]; if (row < 0) row = (CGRectGetMaxY(rect) < dragPoint.y) ? numberOfRows : row; else if (dragPoint.y > MAX(bottomThirty, bottomPoint - 6)) row = lowerRow; row = MIN(numberOfRows, row); return row; } /*! @ignore */ - (CGRect)_rectForDropHighlightViewOnRow:(CPInteger)theRowIndex { if (theRowIndex >= [self numberOfRows]) theRowIndex = [self numberOfRows] - 1; return [self _rectOfRow:theRowIndex checkRange:NO]; } /*! @ignore */ - (CGRect)_rectForDropHighlightViewBetweenUpperRow:(CPInteger)theUpperRowIndex andLowerRow:(CPInteger)theLowerRowIndex offset:(CGPoint)theOffset { if (theLowerRowIndex > [self numberOfRows]) theLowerRowIndex = [self numberOfRows]; return [self _rectOfRow:theLowerRowIndex checkRange:NO]; } /*! @ignore */ - (CPDragOperation)draggingUpdated:(id)sender { _retargetedDropRow = nil; _retargetedDropOperation = nil; var location = [self convertPoint:[sender draggingLocation] fromView:nil], dropOperation = [self _proposedDropOperationAtPoint:location], numberOfRows = [self numberOfRows], row = [self _proposedRowAtPoint:location], dragOperation = [self _sendDataSourceValidateDrop:sender proposedRow:row proposedDropOperation:dropOperation]; if (_retargetedDropRow !== nil) row = _retargetedDropRow; if (_retargetedDropOperation !== nil) dropOperation = _retargetedDropOperation; if (dropOperation === CPTableViewDropOn && row >= numberOfRows) row = numberOfRows - 1; var rect = CGRectMakeZero(); if (row === -1) rect = [self exposedRect]; 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 draggingUpdated: [_dropOperationFeedbackView removeFromSuperview]; return [self _dataSourceRespondsToValidateDropProposedRowProposedDropOperation]; } /* @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 [self _sendDataSourceAcceptDrop:sender row:row dropOperation:operation]; } /* @ignore */ - (void)concludeDragOperation:(id)sender { [self _reloadDataViews]; } /* @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]; } /*! @ignore */ - (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) { if (_selectionAnchorRow == CPNotFound) _selectionAnchorRow = [self numberOfRows] - 1; 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 (![self _sendDelegateSelectionShouldChangeInTableView]) return; newSelection = [self _sendDelegateSelectionIndexesForProposedSelection:newSelection]; if (![self _delegateRespondsToSelectionIndexesForProposedSelection] && [self _delegateRespondsToShouldSelectRow]) { var indexArray = []; [newSelection getIndexes:indexArray maxCount:-1 inIndexRange:nil]; var indexCount = indexArray.length; while (indexCount--) { var index = indexArray[indexCount]; if (![self _sendDelegateShouldSelectRow:index]) [newSelection removeIndex:index]; } // as per cocoa if ([newSelection count] === 0) return; } // 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 _noteSelectionIsChanging]; [self selectRowIndexes:newSelection byExtendingSelection:shouldExtendSelection]; _lastSelectedRow = [newSelection containsIndex:aRow] ? aRow : [newSelection lastIndex]; } /*! @ignore */ - (void)_noteSelectionIsChanging { [[CPNotificationCenter defaultCenter] postNotificationName:CPTableViewSelectionIsChangingNotification object:self userInfo:nil]; if (_implementedDelegateMethods & CPTableViewDelegate_tableViewSelectionIsChanging_) [_delegate tableViewSelectionIsChanging:[[CPNotification alloc] initWithName:CPTableViewSelectionIsChangingNotification object:self userInfo:nil]]; } /*! @ignore */ - (void)_noteSelectionDidChange { [[CPNotificationCenter defaultCenter] postNotificationName:CPTableViewSelectionDidChangeNotification object:self userInfo:nil]; if (_implementedDelegateMethods & CPTableViewDelegate_tableViewSelectionDidChange_) [_delegate tableViewSelectionDidChange:[[CPNotification alloc] initWithName:CPTableViewSelectionDidChangeNotification object:self userInfo:nil]]; } /*! @ignore */ - (void)becomeKeyWindow { [self setNeedsDisplay:YES]; } /*! @ignore */ - (void)resignKeyWindow { [self setNeedsDisplay:YES]; } /*! @ignore */ - (BOOL)acceptsFirstResponder { return YES; } /*! @ignore */ - (BOOL)needsPanelToBecomeKey { return YES; } /*! @ignore */ - (CPView)hitTest:(CGPoint)aPoint { var hit = [super hitTest:aPoint]; if ([[CPApp currentEvent] type] !== CPLeftMouseDown) return hit; return [self _hitTest:hit]; } - (id)_hitTest:(CPView)aView { var column, row; if ([aView acceptsFirstResponder]) { [self getColumn:@ref(column) row:@ref(row) forView:aView]; if (![self isRowSelected:row]) { if (_selectionHighlightStyle == CPTableViewSelectionHighlightStyleNone) return aView; return self; } } else if (!_isViewBased && [aView isKindOfClass:[CPControl class]] && ![aView isKindOfClass:[CPTextField class]]) { [self getColumn:@ref(column) row:@ref(row) forView:aView]; _editingColumn = column; _editingRow = row; [aView addObserver:self forKeyPath:@"objectValue" options:CPKeyValueObservingOptionOld | CPKeyValueObservingOptionNew context:"editing"]; } return aView; } - (void)_removeObservers { if (!_isObserving) return; [self _stopObservingClipView]; [super _removeObservers]; } - (void)_addObservers { if (_isObserving) return; [self _startObservingClipView]; [super _addObservers]; } /*! Called when the receiver is about to be moved to a new window. @param aWindow the window to which the receiver will be moved. */ - (void)viewWillMoveToWindow:(CPWindow)aWindow { [super viewWillMoveToWindow:aWindow]; [self _stopObservingFirstResponderForWindow:aWindow]; if (aWindow) [self _startObservingFirstResponderForWindow:aWindow]; } - (void)_startObservingClipView { if (!_observedClipView) return; var defaultCenter = [CPNotificationCenter defaultCenter]; [_observedClipView setPostsFrameChangedNotifications:YES]; [_observedClipView setPostsBoundsChangedNotifications:YES]; [defaultCenter addObserver:self selector:@selector(superviewFrameChanged:) name:CPViewFrameDidChangeNotification object:_observedClipView]; [defaultCenter addObserver:self selector:@selector(superviewBoundsChanged:) name:CPViewBoundsDidChangeNotification object:_observedClipView]; } - (void)_stopObservingClipView { if (!_observedClipView) return; var defaultCenter = [CPNotificationCenter defaultCenter]; [defaultCenter removeObserver:self name:CPViewFrameDidChangeNotification object:_observedClipView]; [defaultCenter removeObserver:self name:CPViewBoundsDidChangeNotification object:_observedClipView]; } - (void)_startObservingFirstResponderForWindow:(CPWindow)aWindow { [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_firstResponderDidChange:) name:_CPWindowDidChangeFirstResponderNotification object:aWindow]; } - (void)_stopObservingFirstResponderForWindow:(CPWindow)aWindow { [[CPNotificationCenter defaultCenter] removeObserver:self name:_CPWindowDidChangeFirstResponderNotification object:aWindow]; } - (void)_firstResponderDidChange:(CPNotification)aNotification { var responder = [[self window] firstResponder], column, row; [self getColumn:@ref(column) row:@ref(row) forView:responder]; _editingRow = row; _editingColumn = column; // We want to keep the 'First Responder' theme state for the table view as a whole, even when a subview is being edited. // This makes sure the theming effects of a focused table remain in effect even as cells are being edited in it. // Check if the firstResponder is outside the tableview: if (responder !== self && _editingRow == CPNotFound && _editingColumn == CPNotFound) [self _notifyViewDidResignFirstResponder]; else [self _notifyViewDidBecomeFirstResponder]; // This is for cell-based tables only. In view-based mode, we do not change the textfield apprearence during an edit. if (!_isViewBased && _editingRow !== CPNotFound && [responder isKindOfClass:[CPTextField class]] && [responder isEditable] && [responder superview] == self) { [responder setBezeled:YES]; [self _registerForEndEditingNote:responder]; } } - (void)_registerForEndEditingNote:(CPView)aTextField { [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_textFieldEditingDidEnd:) name:CPControlTextDidEndEditingNotification object:aTextField]; } - (void)_unregisterForEndEditingNote:(CPView)aTextField { [[CPNotificationCenter defaultCenter] removeObserver:self name:CPControlTextDidEndEditingNotification object:aTextField]; } - (void)_textFieldEditingDidEnd:(CPNotification)aNote { // FIXME: When you edit a text field and hit enter without any text modification, the CPControlTextDidEndEditingNotification // is NOT sent. This is a bug in CPTextField or CPControl according to cocoa. var textField = [aNote object]; [self _unregisterForEndEditingNote:textField]; if (!_isViewBased) { [self _setEditingState:NO forView:textField]; [self _commitDataViewObjectValue:textField]; } else [textField setBezeled:NO]; [self _resignFirstResponderWithoutSendingAction:textField]; } - (void)_resignFirstResponderWithoutSendingAction:(CPView)aView { var action = [self _disableActionIfExists:aView]; [[self window] makeFirstResponder:self]; if (action) [aView setAction:action]; } - (void)_resignEditedView { var view = [[self window] firstResponder]; if ([view respondsToSelector:@selector(selectText:)]) [view selectText:nil]; if (!_isViewBased) { [self _unregisterForEndEditingNote:view]; [self _setEditingState:NO forView:view]; } [self _resignFirstResponderWithoutSendingAction:view]; } - (SEL)_disableActionIfExists:(CPView)aView { // TODO: We disable action to prevent it from beeing sent twice when we resign the FR inside a textEndEditing notification. // Check if this is due to a bug in CPTextField. var action = nil; if ([aView respondsToSelector:@selector(action)] && (action = [aView action])) [aView setAction:nil]; return action; } /*! @ignore The action for any dataview that supports editing. This will only be called when the value was changed. The table view becomes the first responder after user is done editing a dataview. */ - (void)_commitDataViewObjectValue:(id)aDataView { var editingTableColumn = _tableColumns[_editingColumn]; if (_implementedDataSourceMethods & CPTableViewDataSource_tableView_setObjectValue_forTableColumn_row_) [_dataSource tableView:self setObjectValue:[aDataView objectValue] forTableColumn:editingTableColumn row:_editingRow]; // Allow the column binding to do a reverse set. Note that we do this even if the data source method above // is implemented. [editingTableColumn _reverseSetDataView:aDataView forRow:_editingRow]; if (_editingRow !== CPNotFound && _editingColumn !== CPNotFound) [self reloadDataForRowIndexes:[CPIndexSet indexSetWithIndex:_editingRow] columnIndexes:[CPIndexSet indexSetWithIndex:_editingColumn]]; } - (void)_setEditingState:(BOOL)editingState forView:(CPView)aView { if ([aView respondsToSelector:@selector(setEditable:)]) [aView setEditable:editingState]; if ([aView respondsToSelector:@selector(setSelectable:)]) [aView setSelectable:editingState]; if ([aView isKindOfClass:[CPTextField class]]) [aView setBezeled:editingState]; } /*! Edits the dataview at a given row and column. This method is usually invoked automatically and should rarely be invoked directly The row at supplied rowIndex must be selected otherwise an exception is thrown. @param columnIndex the index of the column to edit @param rowIndex the index of the row to edit @param theEvent the mouse event which triggers the edit, you can pass nil @param flag YES if the dataview text should be selected, otherwise NO. (NOT YET IMPLEMENTED) */ - (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]; [self reloadData]; // Process all events immediately to make sure table data views are reloaded. [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; [self scrollRowToVisible:rowIndex]; [self scrollColumnToVisible:columnIndex]; // TODO Do something with flag. _editingRow = rowIndex; _editingColumn = columnIndex; var editingTableColumnUID = [_tableColumns[_editingColumn] UID], editingView = _dataViewsForRows[_editingRow][editingTableColumnUID]; [self _setEditingState:YES forView:editingView]; [[self window] makeFirstResponder:editingView]; } - (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(void)context { if (context === "editing" && [object superview] === self) { [object removeObserver:self forKeyPath:keyPath]; [self _commitDataViewObjectValue:object]; _editingRow = CPNotFound; _editingColumn = CPNotFound; } } /*! Returns the column of the currently edited cell, or CPNotFound if none. */ - (CPInteger)editedColumn { return _editingColumn; } /*! Returns the row of the currently edited cell, or CPNotFound if none. */ - (CPInteger)editedRow { return _editingRow; } /*! @ignore */ - (void)keyDown:(CPEvent)anEvent { var character = [anEvent charactersIgnoringModifiers], modifierFlags = [anEvent modifierFlags]; // Check for the key events manually, as opposed to waiting for CPWindow to sent the actual action message // in _processKeyboardUIKey:, because we might not want to handle the arrow events. if (character === CPUpArrowFunctionKey || character === CPDownArrowFunctionKey) { // We're not interested in the arrow keys if there are no rows. // Technically we should also not be interested if we can't scroll, // but Cocoa doesn't handle that situation either. if ([self numberOfRows] !== 0) { [self _moveSelectionWithEvent:anEvent upward:(character === CPUpArrowFunctionKey)]; return; } } else if (character === CPDeleteCharacter || character === CPDeleteFunctionKey) { // Don't call super if the delegate is interested in the delete key if ([self _sendDelegateDeleteKeyPressed]) return; } [super keyDown:anEvent]; } /*! @ignore Determines if the selection is broken. A broken selection is a non-continuous selection of rows. */ - (BOOL)_selectionIsBroken { return [self selectedRowIndexes]._ranges.length !== 1; } /*! @ignore Selection behaviour depends on two things: _lastSelectedRow and the anchored selection (the last row selected by itself) */ - (void)_moveSelectionWithEvent:(CPEvent)theEvent upward:(BOOL)shouldGoUpward { if (![self _sendDelegateSelectionShouldChangeInTableView]) return; var selectedIndexes = [self selectedRowIndexes]; if ([selectedIndexes count] > 0) { var extend = (([theEvent modifierFlags] & CPShiftKeyMask) && _allowsMultipleSelection), i = [self selectedRow]; if ([self _selectionIsBroken]) { while ([selectedIndexes containsIndex:i]) { shouldGoUpward ? i-- : i++; } _wasSelectionBroken = true; } else if (_wasSelectionBroken && ((shouldGoUpward && i !== [selectedIndexes firstIndex]) || (!shouldGoUpward && i !== [selectedIndexes lastIndex]))) { shouldGoUpward ? i = [selectedIndexes firstIndex] - 1 : i = [selectedIndexes lastIndex]; _wasSelectionBroken = false; } else { shouldGoUpward ? i-- : i++; } } else { var extend = NO; //no rows are currently selected if ([self numberOfRows] > 0) var i = shouldGoUpward ? [self numberOfRows] - 1 : 0; // if we select upward select the last row, otherwise select the first row } if (i >= [self numberOfRows] || i < 0) return; if ([self _delegateRespondsToSelectionIndexesForProposedSelection] || [self _delegateRespondsToShouldSelectRow]) { var shouldSelect = !![[self _cleanUpSelectionRowIndexes:[CPIndexSet indexSetWithIndex:i]] count]; /* If shouldSelect returns NO it means this row cannot be selected. The proper behaviour is to then try to see if the next/previous row(s) can be selected, until we hit the first one that can be. */ while (!shouldSelect && (i < [self numberOfRows] && i > 0)) { shouldGoUpward ? --i : ++i; //check to see if the row can be selected. If it can't be then see if the next row can be selected. shouldSelect = !![[self _cleanUpSelectionRowIndexes:[CPIndexSet indexSetWithIndex:i]] count]; } if (!shouldSelect) return; } // If we go upward and see that this row is already selected we should deselect the row below. if (extend && [selectedIndexes containsIndex:i]) { // The row we're on is the last to be selected. var differedLastSelectedRow = i; // no remove the one before/after it shouldGoUpward ? i++ : i--; [selectedIndexes removeIndex:i]; //we're going to replace the selection extend = NO; } else if (extend) { if ([selectedIndexes containsIndex:i]) { i = shouldGoUpward ? [selectedIndexes firstIndex] -1 : [selectedIndexes lastIndex] + 1; i = MIN(MAX(i, 0), [self numberOfRows] - 1); } [selectedIndexes addIndex:i]; var differedLastSelectedRow = i; } else { selectedIndexes = [CPIndexSet indexSetWithIndex:i]; var differedLastSelectedRow = i; } selectedIndexes = [self _sendDelegateSelectionIndexesForProposedSelection:selectedIndexes]; [self selectRowIndexes:selectedIndexes byExtendingSelection:extend]; // we differ because selectRowIndexes: does its own thing which would set the wrong index _lastSelectedRow = differedLastSelectedRow; if (i !== CPNotFound) [self scrollRowToVisible:i]; } @end @implementation CPTableView (TableViewDataSource) /*! @ignore Return YES if the dataSource implements tableView:objectValueForTableColumn:row */ - (BOOL)_dataSourceRespondsToObjectValueForTableColumn { return _implementedDataSourceMethods & CPTableViewDataSource_tableView_objectValueForTableColumn_row_; } /*! @ignore Return YES if the dataSource implements tableView:writeRowsWithIndexes:toPasteboard: */ - (BOOL)_dataSourceRespondsToWriteRowsWithIndexesToPasteboard { return _implementedDataSourceMethods & CPTableViewDataSource_tableView_writeRowsWithIndexes_toPasteboard_; } /*! @ignore Return YES if the dataSource implements tableView:setObjectValue:forTableColumn:row: */ - (BOOL)_dataSourceRespondsToSetObjectValueForTableColumnRow { return CPTableViewDataSource_tableView_setObjectValue_forTableColumn_row_; } /*! @ignore Return YES if the dataSource implements tableView:validateDrop:proposedRow:proposedDropOperation:; */ - (BOOL)_dataSourceRespondsToValidateDropProposedRowProposedDropOperation { return _implementedDataSourceMethods & CPTableViewDataSource_tableView_validateDrop_proposedRow_proposedDropOperation_; } /*! @ignore Return YES if the dataSource implements numberOfRowsInTableView */ - (BOOL)_dataSourceRespondsToNumberOfRowsinTableView { return _implementedDataSourceMethods & CPTableViewDataSource_numberOfRowsInTableView_; } /*! @ignore Return the number of rows of the tableView By default return 0. */ - (int)_sendDataSourceNumberOfRowsInTableView { if (!(_implementedDataSourceMethods & CPTableViewDataSource_numberOfRowsInTableView_)) return 0; return [_dataSource numberOfRowsInTableView:self]; } /*! @ignore Return the objectValue for the given column and row. By default return nil. */ - (id)_sendDataSourceObjectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex { if (!(_implementedDataSourceMethods & CPTableViewDataSource_tableView_objectValueForTableColumn_row_)) return nil; return [_dataSource tableView:self objectValueForTableColumn:aTableColumn row:aRowIndex]; } /*! @ignore Call the method tableView:setObjectValue:ForTableColum:row: of the dataSource */ - (void)_sendDataSourceSetObjectValue:(id)anObject forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex { if (!(_implementedDataSourceMethods & CPTableViewDataSource_tableView_setObjectValue_forTableColumn_row_)) return; [_dataSource tableView:self setObjectValue:anObject forTableColumn:aTableColumn row:aRowIndex]; } /*! @ignore Call the method tableView:sortDescriptorsDidChange: of the dataSource */ - (void)_sendDataSourceSortDescriptorsDidChange:(CPArray)descriptors { if (!(_implementedDataSourceMethods & CPTableViewDataSource_tableView_sortDescriptorsDidChange_)) return; [_dataSource tableView:self sortDescriptorsDidChange:descriptors]; } /*! @ignore Return if the drop is accepted or not for the given dropOperation, info and row. By default return NO. */ - (BOOL)_sendDataSourceAcceptDrop:(id)info row:(CPInteger)aRowIndex dropOperation:(CPTableViewDropOperation)operation { if (!(_implementedDataSourceMethods & CPTableViewDataSource_tableView_acceptDrop_row_dropOperation_)) return NO; return [_dataSource tableView:self acceptDrop:info row:aRowIndex dropOperation:operation]; } /*! @ignore Return the dragOperation for the given row and dropOperation. By default return CPDragOperationNone. */ - (CPDragOperation)_sendDataSourceValidateDrop:(id)info proposedRow:(CPInteger)aRowIndex proposedDropOperation:(CPTableViewDropOperation)operation { if (!(_implementedDataSourceMethods & CPTableViewDataSource_tableView_validateDrop_proposedRow_proposedDropOperation_)) return CPDragOperationNone; return [_dataSource tableView:self validateDrop:info proposedRow:aRowIndex proposedDropOperation:operation]; } /*! @ignore Return a boolean for writeRowsWithIndexes:toPasteroard for the given pasteboard and row. By default return NO */ - (BOOL)_sendDataSourceWriteRowsWithIndexes:(CPIndexSet)rowIndexes toPasteboard:(CPPasteboard)pboard { if (!(_implementedDataSourceMethods & CPTableViewDataSource_tableView_writeRowsWithIndexes_toPasteboard_)) return NO; return [_dataSource tableView:self writeRowsWithIndexes:rowIndexes toPasteboard:pboard]; } /* This method is sent to the data source for convenience... */ - (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]; } #pragma mark - #pragma mark DataSource methods to implement /*! @ignore Not yet implemented */ - (CPArray)_sendDataSourceNamesOfPromisedFilesDroppedAtDestination:(CPURL)dropDestination forDraggedRowsWithIndexes:(CPIndexSet)indexSet { if (!(_implementedDataSourceMethods & CPTableViewDataSource_tableView_namesOfPromisedFilesDroppedAtDestination_forDraggedRowsWithIndexes_)) return []; return [_dataSource tableView:self namesOfPromisedFilesDroppedAtDestination:dropDestination forDraggedRowsWithIndexes:indexSet]; } @end @implementation CPTableView (TableViewDelegate) /*! @ignore Return YES if the delegate implements tableView:dataViewFortableColumn:row: */ - (BOOL)_delegateRespondsToDataViewForTableColumn { return _implementedDelegateMethods & CPTableViewDelegate_tableView_dataViewForTableColumn_row_; } /*! @ignore Return YES if the delegate implements tableView:viewFortableColumn:row: */ - (BOOL)_delegateRespondsToViewForTableColumn { return _implementedDelegateMethods & CPTableViewDelegate_tableView_viewForTableColumn_row_; } /*! @ignore Return YES if the delegate implements tableView:shouldSelectRow: */ - (BOOL)_delegateRespondsToShouldSelectRow { return _implementedDelegateMethods & CPTableViewDelegate_tableView_shouldSelectRow_; } /*! @ignore Return YES if the delegate implements tableView:selectionShouldChangeInTableView */ - (BOOL)_delegateRespondsToSelectionShouldChangeInTableView { return _implementedDelegateMethods & CPTableViewDelegate_selectionShouldChangeInTableView_; } /*! @ignore Return YES if the delegate implements tableView:selectionIndexesForProposedSelection */ - (BOOL)_delegateRespondsToSelectionIndexesForProposedSelection { return _implementedDelegateMethods & CPTableViewDelegate_tableView_selectionIndexesForProposedSelection_; } /*! @ignore Return YES if the delegate implements tableView:menuForTableColumn:row */ - (BOOL)_delegateRespondsToMenuForTableColumnRow { return _implementedDelegateMethods & CPTableViewDelegate_tableViewMenuForTableColumn_row_; } /*! @ignore Call the delegate didClickTableColumn with the given tableColumn */ - (void)_sendDelegateDidClickTableColumn:(CPInteger)column { if (_implementedDelegateMethods & CPTableViewDelegate_tableView_didClickTableColumn_) [_delegate tableView:self didClickTableColumn:_tableColumns[column]]; } /*! @ignore Call the delegate didDragTableColumn with the given tableColumn */ - (void)_sendDelegateDidDragTableColumn:(CPInteger)column { if (_implementedDelegateMethods & CPTableViewDelegate_tableView_didDragTableColumn_) [_delegate tableView:self didDragTableColumn:_tableColumns[column]]; } /*! @ignore Call the delegate mouseDownInHeaderOfTableColumn with the given tableColumn */ - (void)_sendDelegateMouseDownInHeaderOfTableColumn:(CPInteger)column { if (_implementedDelegateMethods & CPTableViewDelegate_tableView_mouseDownInHeaderOfTableColumn_) [_delegate tableView:self mouseDownInHeaderOfTableColumn:_tableColumns[column]]; } /* @ignore Call the delegate tableViewDeleteKeyPressed */ - (BOOL)_sendDelegateDeleteKeyPressed { if ([_delegate respondsToSelector: @selector(tableViewDeleteKeyPressed:)]) { [_delegate tableViewDeleteKeyPressed:self]; return YES; } return NO; } /*! @ignore Return if the selection should change. By default return YES */ - (BOOL)_sendDelegateSelectionShouldChangeInTableView { if (!(_implementedDelegateMethods & CPTableViewDelegate_selectionShouldChangeInTableView_)) return YES; return [_delegate selectionShouldChangeInTableView:self]; } /*! @ignore Return if the given row is a group or not. By default return NO. */ - (BOOL)_sendDelegateIsGroupRow:(CPInteger)anIndex { if (!(_implementedDelegateMethods & CPTableViewDelegate_tableView_isGroupRow_)) return NO; return [_delegate tableView:self isGroupRow:anIndex]; } /*! @ignore Return is we should select the given row. By Default return YES */ - (BOOL)_sendDelegateShouldSelectRow:(CPInteger)anIndex { if (!(_implementedDelegateMethods & CPTableViewDelegate_tableView_shouldSelectRow_)) return YES; return [_delegate tableView:self shouldSelectRow:anIndex]; } /*! @ignore Call the delegate tableView:willDisplayView:forTableColumn:row: */ - (void)_sendDelegateWillDisplayView:(id)aCell forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex { if (!(_implementedDelegateMethods & CPTableViewDelegate_tableView_willDisplayView_forTableColumn_row_)) return; [_delegate tableView:self willDisplayView:aCell forTableColumn:aTableColumn row:aRowIndex]; } /*! @ignore Call the delegate tableView:willRemoveView:forTableColumn:row: */ - (void)_sendDelegateWillRemoveView:(id)aCell forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex { if (!(_implementedDelegateMethods & CPTableViewDelegate_tableView_willRemoveView_forTableColumn_row_)) return; [_delegate tableView:self willRemoveView:aCell forTableColumn:aTableColumn row:aRowIndex]; } /*! @ignore Return a CPMenu for the given tableColumn and row. By default return the menu of super. */ - (CPMenu)_sendDelegateMenuForTableColumn:(CPTableColumn)aTableColumn row:aRowIndex { if (!(_implementedDelegateMethods & CPTableViewDelegate_tableViewMenuForTableColumn_row_)) return nil; return [_delegate tableView:self menuForTableColumn:aTableColumn row:aRowIndex]; } /* @ignore Returns YES if the column at columnIndex can be reordered. It can be possible if column reordering is allowed and if the tableview delegate also accept the reordering */ - (BOOL)_sendDelegateShouldReorderColumn:(CPInteger)columnIndex toColumn:(CPInteger)newColumnIndex { if ([self allowsColumnReordering] && _implementedDelegateMethods & CPTableViewDelegate_tableView_shouldReorderColumn_toColumn_) { return [_delegate tableView:self shouldReorderColumn:columnIndex toColumn:newColumnIndex]; } return [self allowsColumnReordering]; } /*! @ignore Return the height of the given row. By default return [self rowHeight]. */ - (float)_sendDelegateHeightOfRow:(CPInteger)anIndex { if (!(_implementedDelegateMethods & CPTableViewDelegate_tableView_heightOfRow_)) return [self rowHeight]; return [_delegate tableView:self heightOfRow:anIndex]; } /*! @ignore Return a boolean to know if we should or not edit the given row. By default return YES. */ - (BOOL)_sendDelegateShouldEditTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex { if (!(_implementedDelegateMethods & CPTableViewDelegate_tableView_shouldEditTableColumn_row_)) return YES; return [_delegate tableView:self shouldEditTableColumn:aTableColumn row:aRowIndex]; } /*! @ignore Return a new CPIndexSet instead of the proposedSelection. By default return the proposedSelection. */ - (CPIndexSet)_sendDelegateSelectionIndexesForProposedSelection:(CPIndexSet)anIndexSet { if (!(_implementedDelegateMethods & CPTableViewDelegate_tableView_selectionIndexesForProposedSelection_)) return anIndexSet; return [_delegate tableView:self selectionIndexesForProposedSelection:anIndexSet]; } /*! @ignore Return a view for the given tableColumn and row. By default return nil. */ - (CPView)_sendDelegateViewForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex { if (!(_implementedDelegateMethods & CPTableViewDelegate_tableView_viewForTableColumn_row_)) return nil; return [_delegate tableView:self viewForTableColumn:aTableColumn row:aRowIndex]; } /*! @ignore Return a view for the given tableColumn and row. By default return nil. */ - (CPView)_sendDelegateDataViewForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex { if (!(_implementedDelegateMethods & CPTableViewDelegate_tableView_dataViewForTableColumn_row_)) return nil; return [_delegate tableView:self dataViewForTableColumn:aTableColumn row:aRowIndex]; } #pragma mark - #pragma mark Delegate methods to implement /*! @ignore Not yet implemented ! */ - (BOOL)_sendDelegateShouldSelectTableColumn:(CPTableColumn)aTableColumn { if (!(_implementedDelegateMethods & CPTableViewDelegate_tableView_shouldSelectTableColumn_)) return YES; return [_delegate tableView:self shouldSelectTableColumn:aTableColumn]; } /*! @ignore Not yet implemented */ - (CPString)_sendDelegateToolTipForView:(id)aView rect:(CGRect)aRect tableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex mouseLocation:(CGPoint)aPoint { if (!(_implementedDelegateMethods & CPTableViewDelegate_tableView_toolTipForView_rect_tableColumn_row_mouseLocation_)) return nil; return [_delegate tableView:self toolTipForView:aView rect:aRect tableColumn:aTableColumn row:aRowIndex mouseLocation:aPoint]; } /*! @ignore Not yet implemented */ - (BOOL)_sendDelegateShouldTrackView:(id)aView forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex { if (!(_implementedDelegateMethods & CPTableViewDelegate_tableView_shouldTrackView_forTableColumn_row_)) return YES; return [_delegate tableView:self shouldTrackView:aView forTableColumn:aTableColumn row:aRowIndex]; } /*! @ignore Not yet implemented */ - (BOOL)_sendDelegateShouldShowViewExpansionForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex { if (!(_implementedDelegateMethods & CPTableViewDelegate_tableView_shouldShowViewExpansionForTableColumn_row_)) return YES; return [_delegate tableView:self shouldShowViewExpansionForTableColumn:aTableColumn row:aRowIndex]; } /*! @ignore Not yet implemented */ - (BOOL)_sendDelegateShouldTypeSelectForEvent:(CPEvent)anEvent withCurrentSearchString:(CPString)aString { if (!(_implementedDelegateMethods & CPTableViewDelegate_tableView_shouldTypeSelectForEvent_withCurrentSearchString_)) return NO; return [_delegate tableView:self shouldTypeSelectForEvent:anEvent withCurrentSearchString:aString]; } /*! @ignore Not yet implemented */ - (CPString)_sendDelegateTypeSelectStringForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex { if (!(_implementedDelegateMethods & CPTableViewDelegate_tableView_typeSelectStringForTableColumn_row_)) return nil; return [_delegate tableView:self typeSelectStringForTableColumn:aTableColumn row:aRowIndex]; } /*! @ignore Not yet implemented ! */ - (int)_sendDelegateNextTypeSelectMatchFromRow:(CPInteger)aRowIndex toRow:(CPInteger)aSecondRowIndex forString:(CPString)aString { if (!(_implementedDelegateMethods & CPTableViewDelegate_tableView_nextTypeSelectMatchFromRow_toRow_forString_)) return -1; return [_delegate tableView:self nextTypeSelectMatchFromRow:aRowIndex toRow:aSecondRowIndex forString:aString]; } @end @implementation CPTableView (Bindings) + (Class)_binderClassForBinding:(CPString)aBinding { if (aBinding == @"content") return [CPTableContentBinder class]; return [super _binderClassForBinding:aBinding]; } /*! @ignore */ - (CPString)_replacementKeyPathForBinding:(CPString)aBinding { if (aBinding === @"selectionIndexes") return @"selectedRowIndexes"; return [super _replacementKeyPathForBinding:aBinding]; } /*! @ignore */ - (void)_establishBindingsIfUnbound:(id)destination { if ([[self infoForBinding:@"content"] objectForKey:CPObservedObjectKey] !== destination) { [super bind:@"content" toObject:destination withKeyPath:@"arrangedObjects" options:nil]; _contentBindingExplicitlySet = NO; } // If the content binding was set manually assume the user is taking manual control of establishing bindings. if (!_contentBindingExplicitlySet) { if ([[self infoForBinding:@"selectionIndexes"] objectForKey:CPObservedObjectKey] !== destination) [self bind:@"selectionIndexes" toObject:destination withKeyPath:@"selectionIndexes" options:nil]; if ([[self infoForBinding:@"sortDescriptors"] objectForKey:CPObservedObjectKey] !== destination) [self bind:@"sortDescriptors" toObject:destination withKeyPath:@"sortDescriptors" options:nil]; } } - (void)bind:(CPString)aBinding toObject:(id)anObject withKeyPath:(CPString)aKeyPath options:(CPDictionary)options { if (aBinding == @"content") _contentBindingExplicitlySet = YES; [super bind:aBinding toObject:anObject withKeyPath:aKeyPath options:options]; } @end @implementation CPTableContentBinder : CPBinder { id _content @accessors(property=content); } - (void)setValueFor:(CPString)aBinding { var destination = [_info objectForKey:CPObservedObjectKey], keyPath = [_info objectForKey:CPObservedKeyPathKey]; _content = [destination valueForKey:keyPath]; // FIXME: reload data for all rows or just exposed rows ? [_source _reloadDataViews]; } @end var CPTableViewDataSourceKey = @"CPTableViewDataSourceKey", CPTableViewDelegateKey = @"CPTableViewDelegateKey", CPTableViewHeaderViewKey = @"CPTableViewHeaderViewKey", CPTableViewTableColumnsKey = @"CPTableViewTableColumnsKey", CPTableViewRowHeightKey = @"CPTableViewRowHeightKey", CPTableViewIntercellSpacingKey = @"CPTableViewIntercellSpacingKey", CPTableViewSelectionHighlightStyleKey = @"CPTableViewSelectionHighlightStyleKey", CPTableViewMultipleSelectionKey = @"CPTableViewMultipleSelectionKey", CPTableViewEmptySelectionKey = @"CPTableViewEmptySelectionKey", CPTableViewColumnReorderingKey = @"CPTableViewColumnReorderingKey", CPTableViewColumnResizingKey = @"CPTableViewColumnResizingKey", CPTableViewColumnSelectionKey = @"CPTableViewColumnSelectionKey", CPTableViewColumnAutoresizingStyleKey = @"CPTableViewColumnAutoresizingStyleKey", CPTableViewGridColorKey = @"CPTableViewGridColorKey", CPTableViewGridStyleMaskKey = @"CPTableViewGridStyleMaskKey", CPTableViewUsesAlternatingBackgroundKey = @"CPTableViewUsesAlternatingBackgroundKey", CPTableViewAlternatingRowColorsKey = @"CPTableViewAlternatingRowColorsKey", CPTableViewHeaderViewKey = @"CPTableViewHeaderViewKey", CPTableViewCornerViewKey = @"CPTableViewCornerViewKey", CPTableViewAutosaveNameKey = @"CPTableViewAutosaveNameKey", CPTableViewArchivedReusableViewsKey = @"CPTableViewArchivedReusableViewsKey"; @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 = [aCoder decodeIntForKey:CPTableViewSelectionHighlightStyleKey]; _columnAutoResizingStyle = [aCoder decodeIntForKey:CPTableViewColumnAutoresizingStyleKey]; _tableColumns = [aCoder decodeObjectForKey:CPTableViewTableColumnsKey] || []; [_tableColumns makeObjectsPerformSelector:@selector(setTableView:) withObject:self]; _rowHeight = [aCoder decodeFloatForKey:CPTableViewRowHeightKey] || [self valueForThemeAttribute:@"default-row-height"]; _intercellSpacing = [aCoder decodeSizeForKey:CPTableViewIntercellSpacingKey]; if (CGSizeEqualToSize(_intercellSpacing, CGSizeMakeZero())) _intercellSpacing = CGSizeMake(3.0, 2.0); [self setGridColor:[aCoder decodeObjectForKey:CPTableViewGridColorKey]]; _gridStyleMask = [aCoder decodeIntForKey:CPTableViewGridStyleMaskKey]; _usesAlternatingRowBackgroundColors = [aCoder decodeObjectForKey:CPTableViewUsesAlternatingBackgroundKey]; [self setAlternatingRowBackgroundColors:[aCoder decodeObjectForKey:CPTableViewAlternatingRowColorsKey]]; _headerView = [aCoder decodeObjectForKey:CPTableViewHeaderViewKey]; _cornerView = [aCoder decodeObjectForKey:CPTableViewCornerViewKey]; [self setDataSource:[aCoder decodeObjectForKey:CPTableViewDataSourceKey]]; [self setDelegate:[aCoder decodeObjectForKey:CPTableViewDelegateKey]]; [self _init]; if ([aCoder containsValueForKey:CPTableViewArchivedReusableViewsKey]) _archivedDataViews = [aCoder decodeObjectForKey:CPTableViewArchivedReusableViewsKey]; [self _updateIsViewBased]; [self viewWillMoveToSuperview:[self superview]]; // Do this as late as possible to make sure the tableview is fully configured [self setAutosaveName:[aCoder decodeObjectForKey:CPTableViewAutosaveNameKey]]; } return self; } - (void)encodeWithCoder:(CPCoder)aCoder { [super encodeWithCoder:aCoder]; // We do this in order to avoid encoding the _tableDrawView, which // should just automatically be created programmatically as needed. if (_tableDrawView) [_tableDrawView removeFromSuperview]; [aCoder encodeObject:_dataSource forKey:CPTableViewDataSourceKey]; [aCoder encodeObject:_delegate forKey:CPTableViewDelegateKey]; [aCoder encodeFloat:_rowHeight forKey:CPTableViewRowHeightKey]; [aCoder encodeSize:_intercellSpacing forKey:CPTableViewIntercellSpacingKey]; [aCoder encodeInt:_selectionHighlightStyle forKey:CPTableViewSelectionHighlightStyleKey]; [aCoder encodeInt:_columnAutoResizingStyle forKey:CPTableViewColumnAutoresizingStyleKey]; [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]; [aCoder encodeObject:_autosaveName forKey:CPTableViewAutosaveNameKey]; if (_archivedDataViews) [aCoder encodeObject:_archivedDataViews forKey:CPTableViewArchivedReusableViewsKey]; } @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], borderRadius, borderColor, borderWidth, backgroundColor; if (currentRow === -1) { borderColor = [tableView valueForThemeAttribute:@"dropview-on-border-color"]; borderWidth = [tableView valueForThemeAttribute:@"dropview-on-border-width"]; CGContextSetStrokeColor(context, borderColor); CGContextSetLineWidth(context, borderWidth); 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]) { borderRadius = [tableView valueForThemeAttribute:@"dropview-on-selected-border-radius"]; borderColor = [tableView valueForThemeAttribute:@"dropview-on-selected-border-color"]; borderWidth = [tableView valueForThemeAttribute:@"dropview-on-selected-border-width"]; backgroundColor = [tableView valueForThemeAttribute:@"dropview-on-selected-background-color"]; } else { borderRadius = [tableView valueForThemeAttribute:@"dropview-on-border-radius"]; borderColor = [tableView valueForThemeAttribute:@"dropview-on-border-color"]; borderWidth = [tableView valueForThemeAttribute:@"dropview-on-border-width"]; backgroundColor = [tableView valueForThemeAttribute:@"dropview-on-background-color"]; } CGContextSetStrokeColor(context, borderColor); CGContextSetLineWidth(context, borderWidth); CGContextSetFillColor(context, backgroundColor); CGContextFillRoundedRectangleInRect(context, newRect, borderRadius, YES, YES, YES, YES); CGContextStrokeRoundedRectangleInRect(context, newRect, borderRadius, 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]) { borderColor = [tableView valueForThemeAttribute:@"dropview-above-selected-border-color"]; borderWidth = [tableView valueForThemeAttribute:@"dropview-above-selected-border-width"]; } else { borderColor = [tableView valueForThemeAttribute:@"dropview-above-border-color"]; borderWidth = [tableView valueForThemeAttribute:@"dropview-above-border-width"]; } CGContextSetStrokeColor(context, borderColor); CGContextSetLineWidth(context, borderWidth); CGContextStrokeEllipseInRect(context, CGRectMake(aRect.origin.x + 4, aRect.origin.y + 4, 8, 8)); // circle CGContextBeginPath(context); CGContextMoveToPoint(context, 10, aRect.origin.y + 8); CGContextAddLineToPoint(context, aRect.size.width - aRect.origin.y - 8, aRect.origin.y + 8); CGContextStrokePath(context); } } - (void)blink { if (dropOperation !== CPTableViewDropOn) return; isBlinking = YES; var showCallback = function() { [self setHidden: NO]; isBlinking = NO; }; var hideCallback = function() { [self setHidden: YES]; isBlinking = YES; }; [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 @implementation _CPColumnDragDrawingView : CPView { CPTableView tableView @accessors; int columnIndex @accessors; CPView columnClipView @accessors; } - (void)drawRect:(CGRect)dirtyRect { var context = [[CPGraphicsContext currentContext] graphicsPort], columnRect = [tableView rectOfColumn:columnIndex], headerHeight = CGRectGetHeight([[tableView headerView] frame]), bounds = [columnClipView bounds], visibleRect = [tableView visibleRect], xScroll = CGRectGetMinX(visibleRect), yScroll = CGRectGetMinY(visibleRect); // Because we are sharing drawing code with regular table drawing, // we have to play a few tricks to fool the drawing code into thinking // our drag column is in the same place as the real column. // Shift the bounds origin to align with the column rect, and extend it vertically to ensure // it reaches the bottom of the tableView when scrolled. bounds.origin.x = CGRectGetMinX(columnRect) - xScroll; bounds.size.height += yScroll; // Fix up the CTM to account for the header and scroll CGContextTranslateCTM(context, -bounds.origin.x, headerHeight - yScroll); //[tableView drawBackgroundInClipRect:bounds]; if (tableView._draggedColumnIsSelected) { CGContextSetFillColor(context, [tableView selectionHighlightColor]); CGContextFillRect(context, bounds); } else [tableView highlightSelectionInClipRect:bounds]; //[tableView _drawHorizontalGridInClipRect:bounds]; var minX = CGRectGetMinX(bounds) + 0.5, maxX = CGRectGetMaxX(bounds) - 0.5; CGContextSetLineWidth(context, 1.0); CGContextSetAlpha(context, 1.0); CGContextSetStrokeColor(context, [tableView gridColor]); CGContextBeginPath(context); CGContextMoveToPoint(context, minX, CGRectGetMinY(bounds)); CGContextAddLineToPoint(context, minX, CGRectGetMaxY(bounds)); CGContextMoveToPoint(context, maxX, CGRectGetMinY(bounds)); CGContextAddLineToPoint(context, maxX, CGRectGetMaxY(bounds)); CGContextStrokePath(context); } @end @implementation CPTableCellView : CPView { id _objectValue @accessors(property=objectValue); CPTextField _textField @accessors(property=textField); CPImageView _imageView @accessors(property=imageView); } - (void)awakeFromCib { [super awakeFromCib]; [self setThemeState:CPThemeStateTableDataView]; } - (BOOL)setThemeState:(ThemeState)aState { [super setThemeState:aState]; [self recursivelyPerformSelector:@selector(setThemeState:) withObject:aState startingFrom:self]; } - (BOOL)unsetThemeState:(ThemeState)aState { [super unsetThemeState:aState]; [self recursivelyPerformSelector:@selector(unsetThemeState:) withObject:aState startingFrom:self]; } - (void)recursivelyPerformSelector:(SEL)selector withObject:(id)anObject startingFrom:(id)aView { [[aView subviews] enumerateObjectsUsingBlock:function(view, idx) { [view performSelector:selector withObject:anObject]; if (![view isKindOfClass:[self class]]) // Avoid infinite loop if a subview is a CPTableCellView. [self recursivelyPerformSelector:selector withObject:anObject startingFrom:view]; }]; } - (CPString)description { return "<" + [self className] + " 0x" + [CPString stringWithHash:[self UID]] + " identifier=" + [self identifier] + ">"; } @end