fd1bbba8f8
This reverts commitf5ade96e06, reversing changes made to91d97d6c62. They were causing too much breakage for us.
364 lines
15 KiB
Objective-C
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
|
|
|