Opening a gap in NSTableView during drag and drop

牧云@^-^@ 提交于 2020-05-09 17:57:57

问题


I've got a simple, single-column, view-based NSTableView with items in it that can be dragged to reorder them. During drag and drop, I'd like to make it so that a gap for the item-to-be-dropped opens up at the location under the mouse. GarageBand does something like this when you drag to reorder tracks (video here: http://www.screencast.com/t/OmUVHcCNSl). As far as I can tell, there's no built in support for this in NSTableView.

Has anyone else tried to add this behavior to NSTableView and found a good solution? I've thought of and tried a couple approaches without much success. My first thought was to double the height of the row under the mouse during a drag by sending -noteHeightOfRowsWithIndexesChanged: in my data source's -tableView:validateDrop:... method, then returning twice the normal height in -tableView:heightOfRow:. Unfortunately, best I can tell, NSTableView doesn't update its layout during drag and drop, so despite calling noteHeightOfRowsWithIndexesChanged:, the row height isn't actually updated.

Note that I'm using a view-based NSTableView, but my rows are not so complex that I couldn't move to a cell-based table view if doing so helped accomplish this. I'm aware of the easy, built-in ability to animate a gap for the dropped item after a drag is complete. I'm looking for a way to open a gap while the drag is in progress. Also, this is for an app to be sold in the Mac App Store, so it must not use private API.

EDIT: I've just filed an enhancement request with Apple requesting built in support for this behavior: http://openradar.appspot.com/12662624. Dupe if you'd like to see it too. Update: The enhancement I requested was implemented in OS X 10.9 Mavericks, and this behavior is now available using NSTableView API. See NSTableViewDraggingDestinationFeedbackStyleGap.


回答1:


I feel bizarre for doing this, but there's an extremely thorough answer in the queue here that appears to have been deleted by its author. In it, they provided the correct links to a working solution, which I feel need to be presented as an answer for someone else to take and run with, inclusive of them if they desire to do so.

From the documentation for NSTableView, the following caveats are tucked away for row animation effects:

Row Animation Effects

Optional constant that specifies that the tableview will use a fade for row or column removal. The effect can be combined with any NSTableViewAnimationOptions constant.

enum {
    NSTableViewAnimationEffectFade = 0x1,
    NSTableViewAnimationEffectGap = 0x2,
};

Constants:

...

NSTableViewAnimationEffectGap

Creates a gap for newly inserted rows. This is useful for drag and drop animations that animate to a newly opened gap and should be used in the delegate method tableView:acceptDrop:row:dropOperation:.

Going through the example code from Apple, I find this:

- (void)_performInsertWithDragInfo:(id <NSDraggingInfo>)info parentNode:(NSTreeNode *)parentNode childIndex:(NSInteger)childIndex {
    // NSOutlineView's root is nil
    id outlineParentItem = parentNode == _rootTreeNode ? nil : parentNode;
    NSMutableArray *childNodeArray = [parentNode mutableChildNodes];
    NSInteger outlineColumnIndex = [[_outlineView tableColumns] indexOfObject:[_outlineView outlineTableColumn]];

    // Enumerate all items dropped on us and create new model objects for them    
    NSArray *classes = [NSArray arrayWithObject:[SimpleNodeData class]];
    __block NSInteger insertionIndex = childIndex;
    [info enumerateDraggingItemsWithOptions:0 forView:_outlineView classes:classes searchOptions:nil usingBlock:^(NSDraggingItem *draggingItem, NSInteger index, BOOL *stop) {
        SimpleNodeData *newNodeData = (SimpleNodeData *)draggingItem.item;
        // Wrap the model object in a tree node
        NSTreeNode *treeNode = [NSTreeNode treeNodeWithRepresentedObject:newNodeData];
        // Add it to the model
        [childNodeArray insertObject:treeNode atIndex:insertionIndex];
        [_outlineView insertItemsAtIndexes:[NSIndexSet indexSetWithIndex:insertionIndex] inParent:outlineParentItem withAnimation:NSTableViewAnimationEffectGap];
        // Update the final frame of the dragging item
        NSInteger row = [_outlineView rowForItem:treeNode];
        draggingItem.draggingFrame = [_outlineView frameOfCellAtColumn:outlineColumnIndex row:row];

        // Insert all children one after another
        insertionIndex++;
    }];

}

I'm unsure if it's really this simple, but it's at least worth inspection and outright refutal if it doesn't meet your needs.

Edit: see this answer's comments for the steps followed to the right solution. The OP has posted a more complete answer, which should be referred to by anyone looking for solutions to the same problem.




回答2:


Note: The behavior this question and answer describes are now available using built in API in NSTableView on OS X 10.9 Mavericks and later. See NSTableViewDraggingDestinationFeedbackStyleGap.

This answer may still be useful if this behavior is needed in an app targeting OS X 10.8 or earlier. Original answer below:

I've implemented this now. My basic approach looks like this:

@interface ORSGapOpeningTableView : NSTableView

@property (nonatomic) NSInteger dropTargetRow;
@property (nonatomic) CGFloat heightOfDraggedRows;

@end

@implementation ORSGapOpeningTableView

#pragma mark - Dragging

- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender
{
    NSInteger oldDropTargetRow = self.dropTargetRow;
    NSDragOperation result = [super draggingUpdated:sender];
    CGFloat imageHeight = [[sender draggedImage] size].height;
    self.heightOfDraggedRows = imageHeight;

    NSMutableIndexSet *changedRows = [NSMutableIndexSet indexSet];
    if (oldDropTargetRow > 0) [changedRows addIndex:oldDropTargetRow-1];
    if (self.dropTargetRow > 0) [changedRows addIndex:self.dropTargetRow-1];
    [self noteHeightOfRowsWithIndexesChanged:changedRows];

    return result;
}

- (void)draggingExited:(id<NSDraggingInfo>)sender
{
    self.dropTargetRow = -1;
    [self noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [self numberOfRows])]];

    [super draggingExited:sender];
}

- (void)draggingEnded:(id<NSDraggingInfo>)sender
{
    self.dropTargetRow = -1;
    self.heightOfDraggedRows = 0.0;
    self.draggedRows = nil;
    [self noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [self numberOfRows])]];
}

- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender
{
    self.dropTargetRow = -1;
    self.heightOfDraggedRows = 0.0;
    self.draggedRows = nil;
    [self noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [self numberOfRows])]];

    return [super performDragOperation:sender];
}

// In my delegate and data source:

- (NSDragOperation)tableView:(NSTableView *)tableView validateDrop:(id<NSDraggingInfo>)info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)dropOperation
{
    if (dropOperation == NSTableViewDropOn) 
    {
        dropOperation = NSTableViewDropAbove;
        [self.tableView setDropRow:++row dropOperation:dropOperation];
    }

    NSDragOperation result = [self.realDataSource tableView:tableView validateDrop:info proposedRow:row proposedDropOperation:dropOperation];
    if (result != NSDragOperationNone) 
    {
        self.tableView.dropTargetRow = row;
    } 
    else 
    {
        self.tableView.dropTargetRow = -1; // Don't open a gap
    }
    return result;
}

- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
{
    CGFloat result = [tableView rowHeight];

    if (row == self.tableView.dropTargetRow - 1 && row > -1)
    {
        result += self.tableView.heightOfDraggedRows;
    }

    return result;
}

Note that this is simplified code, not a verbatim copy/paste from my program. I actually ended up making this all contained within an NSTableView subclass that uses proxy delegate and data source objects so the code in data source/delegate methods above is actually inside the proxies' intercept of the calls to the real delegate and data source. That way, the real data source and delegate don't have to do anything special to get the gap opening behavior. Also, there's sometimes a little flakiness with the table view animations, and this doesn't work for drags above the first row (no gap is opened since there's no row to make taller). All in all, despite the room for further improvement, this approach works reasonably well.

I'd still like to try a similar approach, but insert a blank row (as Caleb suggested) instead of changing the row height.




回答3:


As of Mac OS X 10.9 (Mavericks), there's a much easier solution to animating drag & drop in a NSTableView:

[aTableView setDraggingDestinationFeedbackStyle:NSTableViewDraggingDestinationFeedbackStyleGap];

The table view will automatically insert gaps with animation as a row is dragged which is much nicer than the old blue line insertion point method.




回答4:


One way to accomplish what you're asking is to insert an empty row at the proposed drop point (that is, between the two nearest rows). It sounds like you've been looking at using NSTableViewAnimationEffectGap, which as you note is really meant for animating the insertion when the drop is accepted in -tableView:acceptDrop:row:dropOperation:.

Since you want to open up the gap before the user releases the mouse button to actually do the drop, you could instead insert a blank row using -insertRowsAtIndexes:withAnimation: from your table's -draggingUpdate: method and at the same time delete any blank row you previously inserted for this drag using -removeRowsAtIndexes:withAnimation:. Use NSTableViewAnimationSlideUp and NSTableViewAnimationSlideDown as the animations for these operations, as appropriate.



来源:https://stackoverflow.com/questions/10142675/opening-a-gap-in-nstableview-during-drag-and-drop

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!