Files
twui/lib/UIKit/TUITableView+Cell.m
Justin Spahr-Summers fd1bbba8f8 Revert TUITableViewCell changes
This reverts commit f5ade96e06, reversing changes made to 91d97d6c62.

They were causing too much breakage for us.
2012-11-12 10:33:27 -08:00

364 lines
15 KiB
Objective-C

/*
Copyright 2011 Twitter, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this work except in compliance with the License.
You may obtain a copy of the License in the LICENSE file, or at:
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "TUITableView+Cell.h"
// Dragged cells should be just above pinned headers
#define kTUITableViewDraggedCellZPosition 1001
@interface TUITableView (CellPrivate)
- (BOOL)_preLayoutCells;
- (void)_layoutSectionHeaders:(BOOL)needLayout;
- (void)_layoutCells:(BOOL)needLayout;
@end
@implementation TUITableView (Cell)
/**
* @brief Mouse down in a cell
*/
-(void)__mouseDownInCell:(TUITableViewCell *)cell offset:(CGPoint)offset event:(NSEvent *)event {
[self __beginDraggingCell:cell offset:offset location:[[cell superview] localPointForEvent:event]];
}
/**
* @brief Mouse up in a cell
*/
-(void)__mouseUpInCell:(TUITableViewCell *)cell offset:(CGPoint)offset event:(NSEvent *)event {
[self __endDraggingCell:cell offset:offset location:[[cell superview] localPointForEvent:event]];
}
/**
* @brief A cell was dragged
*
* If reordering is permitted by the table, this will begin a move operation.
*/
-(void)__mouseDraggedCell:(TUITableViewCell *)cell offset:(CGPoint)offset event:(NSEvent *)event {
[self __updateDraggingCell:cell offset:offset location:[[cell superview] localPointForEvent:event]];
}
/**
* @brief Determine if we're dragging a cell or not
*/
-(BOOL)__isDraggingCell {
return _dragToReorderCell != nil && _currentDragToReorderIndexPath != nil;
}
/**
* @brief Begin dragging a cell
*/
-(void)__beginDraggingCell:(TUITableViewCell *)cell offset:(CGPoint)offset location:(CGPoint)location {
_currentDragToReorderLocation = location;
_currentDragToReorderMouseOffset = offset;
_dragToReorderCell = cell;
_currentDragToReorderIndexPath = nil;
_previousDragToReorderIndexPath = nil;
}
/**
* @brief Update cell dragging
*/
-(void)__updateDraggingCell:(TUITableViewCell *)cell offset:(CGPoint)offset location:(CGPoint)location {
BOOL animate = TRUE;
// note the location in any event
_currentDragToReorderLocation = location;
_currentDragToReorderMouseOffset = offset;
// return if there wasn't a proper drag
if(![cell didDrag]) return;
// make sure reordering is supported by our data source (this should probably be done only once somewhere)
if(self.dataSource == nil || ![self.dataSource respondsToSelector:@selector(tableView:moveRowAtIndexPath:toIndexPath:)]){
return; // reordering is not supported by the data source
}
// determine if reordering this cell is permitted or not via our data source (this should probably be done only once somewhere)
if(self.dataSource == nil || ![self.dataSource respondsToSelector:@selector(tableView:canMoveRowAtIndexPath:)] || ![self.dataSource tableView:self canMoveRowAtIndexPath:cell.indexPath]){
return; // reordering is not permitted
}
// initialize defaults on the first drag
if(_currentDragToReorderIndexPath == nil || _previousDragToReorderIndexPath == nil){
// make sure the dragged cell is on top
_dragToReorderCell.layer.zPosition = kTUITableViewDraggedCellZPosition;
// setup index paths
_currentDragToReorderIndexPath = cell.indexPath;
_previousDragToReorderIndexPath = cell.indexPath;
return; // just initialize on the first event
}
CGRect visible = [self visibleRect];
// dragged cell destination frame
CGRect dest = CGRectMake(0, roundf(MAX(visible.origin.y, MIN(visible.origin.y + visible.size.height - cell.frame.size.height, location.y + visible.origin.y - offset.y))), self.bounds.size.width, cell.frame.size.height);
// bring to front
[[cell superview] bringSubviewToFront:cell];
// move the cell
cell.frame = dest;
// constraint the location to the viewport
location = CGPointMake(location.x, MAX(0, MIN(visible.size.height, location.y)));
// scroll content if necessary (scroll view figures out whether it's necessary or not)
[self beginContinuousScrollForDragAtPoint:location animated:TRUE];
TUITableViewInsertionMethod insertMethod = TUITableViewInsertionMethodAtIndex;
NSIndexPath *currentPath = nil;
NSInteger sectionIndex = -1;
// determine the current index path the cell is occupying
if((currentPath = [self indexPathForRowAtVerticalOffset:location.y + visible.origin.y]) == nil){
if((sectionIndex = [self indexOfSectionWithHeaderAtVerticalOffset:location.y + visible.origin.y]) > 0){
if(sectionIndex <= cell.indexPath.section){
// if we're on a section header (but not the first one, which can't move) which is above the origin
// index path we insert after the last index in the section above
NSInteger targetSectionIndex = sectionIndex - 1;
currentPath = [NSIndexPath indexPathForRow:[self numberOfRowsInSection:targetSectionIndex] - 1 inSection:targetSectionIndex];
insertMethod = TUITableViewInsertionMethodAfterIndex;
}else{
// if we're on a section header below the origin index we insert before the first index in the
// section below
NSInteger targetSectionIndex = sectionIndex;
currentPath = [NSIndexPath indexPathForRow:0 inSection:targetSectionIndex];
insertMethod = TUITableViewInsertionMethodBeforeIndex;
}
}
}
// make sure we have a valid current path before proceeding
if(currentPath == nil) return;
// allow the delegate to revise the proposed index path if it wants to
if(self.delegate != nil && [self.delegate respondsToSelector:@selector(tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath:)]){
NSIndexPath *proposedPath = currentPath;
currentPath = [self.delegate tableView:self targetIndexPathForMoveFromRowAtIndexPath:cell.indexPath toProposedIndexPath:currentPath];
// revised index paths always use the "at" insertion method
switch([currentPath compare:proposedPath]){
case NSOrderedAscending:
case NSOrderedDescending:
insertMethod = TUITableViewInsertionMethodAtIndex;
break;
case NSOrderedSame:
default:
// do nothing
break;
}
}
// note the previous path
_previousDragToReorderIndexPath = _currentDragToReorderIndexPath;
_previousDragToReorderInsertionMethod = _currentDragToReorderInsertionMethod;
// note the current path
_currentDragToReorderIndexPath = currentPath;
_currentDragToReorderInsertionMethod = insertMethod;
// determine the current drag direction
NSComparisonResult currentDragDirection = (_previousDragToReorderIndexPath != nil) ? [currentPath compare:_previousDragToReorderIndexPath] : NSOrderedSame;
// ordered index paths for enumeration
NSIndexPath *fromIndexPath = nil;
NSIndexPath *toIndexPath = nil;
if(currentDragDirection == NSOrderedAscending){
fromIndexPath = currentPath;
toIndexPath = _previousDragToReorderIndexPath;
}else if(currentDragDirection == NSOrderedDescending){
fromIndexPath = _previousDragToReorderIndexPath;
toIndexPath = currentPath;
}else if(insertMethod != _previousDragToReorderInsertionMethod){
fromIndexPath = currentPath;
toIndexPath = currentPath;
}
// we now have the final destination index path. if it's not nil, update surrounding
// cells to make room for the dragged cell
if(currentPath != nil && fromIndexPath != nil && toIndexPath != nil){
// begin animations
if(animate){
[TUIView beginAnimations:NSStringFromSelector(_cmd) context:NULL];
}
// update section headers
for(NSInteger i = fromIndexPath.section; i <= toIndexPath.section; i++){
TUIView *headerView;
if(currentPath.section < i && i <= cell.indexPath.section){
// the current index path is above this section and this section is at or
// below the origin index path; shift our header down to make room
if((headerView = [self headerViewForSection:i]) != nil){
CGRect frame = [self rectForHeaderOfSection:i];
headerView.frame = CGRectMake(frame.origin.x, frame.origin.y - cell.frame.size.height, frame.size.width, frame.size.height);
}
}else if(currentPath.section >= i && i > cell.indexPath.section){
// the current index path is at or below this section and this section is
// below the origin index path; shift our header up to make room
if((headerView = [self headerViewForSection:i]) != nil){
CGRect frame = [self rectForHeaderOfSection:i];
headerView.frame = CGRectMake(frame.origin.x, frame.origin.y + cell.frame.size.height, frame.size.width, frame.size.height);
}
}else{
// restore the header to it's normal position
if((headerView = [self headerViewForSection:i]) != nil){
headerView.frame = [self rectForHeaderOfSection:i];
}
}
}
// update rows
[self enumerateIndexPathsFromIndexPath:fromIndexPath toIndexPath:toIndexPath withOptions:0 usingBlock:^(NSIndexPath *indexPath, BOOL *stop) {
TUITableViewCell *displacedCell;
if((displacedCell = [self cellForRowAtIndexPath:indexPath]) != nil && ![displacedCell isEqual:cell]){
CGRect frame = [self rectForRowAtIndexPath:indexPath];
CGRect target;
if([indexPath isEqual:currentPath] && insertMethod == TUITableViewInsertionMethodAfterIndex){
// the visited index path is the current index path and the insertion method is "after";
// leave the cell where it is, the section header should shift out of the way instead
target = frame;
}else if([indexPath isEqual:currentPath] && insertMethod == TUITableViewInsertionMethodBeforeIndex){
// the visited index path is the current index path and the insertion method is "before";
// leave the cell where it is, the section header should shift out of the way instead
target = frame;
}else if([indexPath compare:currentPath] != NSOrderedAscending && [indexPath compare:cell.indexPath] == NSOrderedAscending){
// the visited index path is above the origin and below the current index path;
// shift the cell down by the height of the dragged cell
target = CGRectMake(frame.origin.x, frame.origin.y - cell.frame.size.height, frame.size.width, frame.size.height);
}else if([indexPath compare:currentPath] != NSOrderedDescending && [indexPath compare:cell.indexPath] == NSOrderedDescending){
// the visited index path is below the origin and above the current index path;
// shift the cell up by the height of the dragged cell
target = CGRectMake(frame.origin.x, frame.origin.y + cell.frame.size.height, frame.size.width, frame.size.height);
}else{
// the visited cell is outside the affected range and should be returned to its
// normal frame
target = frame;
}
// only animate if we actually need to
if(!CGRectEqualToRect(target, displacedCell.frame)){
displacedCell.frame = target;
}
}
}];
// commit animations
if(animate){
[TUIView commitAnimations];
}
}
}
/**
* @brief Finish dragging a cell
*/
-(void)__endDraggingCell:(TUITableViewCell *)cell offset:(CGPoint)offset location:(CGPoint)location {
BOOL animate = TRUE;
// cancel our continuous scroll
[self endContinuousScrollAnimated:TRUE];
// finalize drag to reorder if we have a drag index
if(_currentDragToReorderIndexPath != nil){
NSIndexPath *targetIndexPath;
switch(_currentDragToReorderInsertionMethod){
case TUITableViewInsertionMethodBeforeIndex:
// insert "before" is equivalent to insert "at" as subsequent indexes are shifted down to
// accommodate the insert. the distinction is only useful for presentation.
targetIndexPath = _currentDragToReorderIndexPath;
break;
case TUITableViewInsertionMethodAfterIndex:
targetIndexPath = [NSIndexPath indexPathForRow:_currentDragToReorderIndexPath.row + 1 inSection:_currentDragToReorderIndexPath.section];
break;
case TUITableViewInsertionMethodAtIndex:
default:
targetIndexPath = _currentDragToReorderIndexPath;
break;
}
// only update the data source if the drag ended on a different index path
// than it started; otherwise just clean up the view
if(![targetIndexPath isEqual:cell.indexPath]){
// notify our data source that the row will be reordered
if(self.dataSource != nil && [self.dataSource respondsToSelector:@selector(tableView:moveRowAtIndexPath:toIndexPath:)]){
[self.dataSource tableView:self moveRowAtIndexPath:cell.indexPath toIndexPath:targetIndexPath];
}
}
// compute the final cell destination frame
CGRect frame = [self rectForRowAtIndexPath:_currentDragToReorderIndexPath];
// adjust if necessary based on the insertion method
switch(_currentDragToReorderInsertionMethod){
case TUITableViewInsertionMethodBeforeIndex:
frame = CGRectMake(frame.origin.x, frame.origin.y + cell.frame.size.height, frame.size.width, frame.size.height);
break;
case TUITableViewInsertionMethodAfterIndex:
frame = CGRectMake(frame.origin.x, frame.origin.y - cell.frame.size.height, frame.size.width, frame.size.height);
break;
case TUITableViewInsertionMethodAtIndex:
default:
// do nothing. this case is just here to avoid complier complaints...
break;
}
// move the cell to its final frame and layout to make sure all the internal caching/geometry
// stuff is consistent.
if(animate && !CGRectEqualToRect(cell.frame, frame)){
// disable user interaction until the animation has completed and the table has reloaded
[self setUserInteractionEnabled:FALSE];
[TUIView animateWithDuration:0.2
animations:^ { cell.frame = frame; }
completion:^(BOOL finished) {
// reload the table when we're done (implicitly restores z-position)
if(finished) [self reloadData];
// restore user interactivity
[self setUserInteractionEnabled:TRUE];
}
];
}else{
cell.frame = frame;
cell.layer.zPosition = 0;
[self reloadData];
}
// clear state
_currentDragToReorderIndexPath = nil;
}else{
cell.layer.zPosition = 0;
}
_previousDragToReorderIndexPath = nil;
// and clean up
_dragToReorderCell = nil;
_currentDragToReorderLocation = CGPointZero;
_currentDragToReorderMouseOffset = CGPointZero;
}
@end