How to implement contextual menu for NSCollectionView

One approach I've used is to not try to apply the contextual menu actions to the one specific item that was clicked on but to the selected items. And I make the clicked-on item add itself to the selection.

I used a custom view for the collection item view. The custom view class has an outlet, item, to its owning collection view item, which I connect in the NIB. It also overrides -rightMouseDown: to have the item add itself to the selection:

- (void) rightMouseDown:(NSEvent*)event
{
    NSCollectionView* parent = self.item.collectionView;
    NSUInteger index = NSNotFound;
    NSUInteger count = parent.content.count;
    for (NSUInteger i = 0; i < count; i++)
    {
        if ([parent itemAtIndex:i] == self.item)
        {
            index = i;
            break;
        }
    }

    NSMutableIndexSet* selectionIndexes = [[parent.selectionIndexes mutableCopy] autorelease];
    if (index != NSNotFound && ![selectionIndexes containsIndex:index])
    {
        [selectionIndexes addIndex:index];
        parent.selectionIndexes = selectionIndexes;
    }

    return [super rightMouseDown:event];
}

If you prefer, rather than adding the item to the selection, you can check if it's already in the selection. If it is, don't modify the selection. If it's not, replace the selection with just the item (making it the only selected item).

Alternatively, you could set a contextual menu on the item views rather than on the collection view. Then, the menu items could target either the item view or the collection view item.

Lastly, you could subclass NSCollectionView and override -menuForEvent:. You would still call through to super and return the menu it returns, but you could take the opportunity to record the event and/or the item at its location. To determine that, you'd do something like:

- (NSMenu*) menuForEvent:(NSEvent*)event
{
    _clickedItemIndex = NSNotFound;
    NSPoint point = [self convertPoint:event.locationInWindow fromView:nil];
    NSUInteger count = self.content.count;
    for (NSUInteger i = 0; i < count; i++)
    {
        NSRect itemFrame = [self frameForItemAtIndex:i];
        if (NSMouseInRect(point, itemFrame, self.isFlipped))
        {
            _clickedItemIndex = i;
            break;
        }
    }

    return [super menuForEvent:event];
}

Basically, all of our solutions are capable of addressing requirements, but I would like to make a supplement to swift3+, which I think is a complete solution.

/// 扩展NSCollectionView功能,增加常用委托
class ANCollectionView: NSCollectionView {
    // 扩展委托方式
    weak open var ANDelegate: ANCollectionViewDelegate?

    override func menu(for event: NSEvent) -> NSMenu? {
        var menu = super.menu(for: event);
        let point = self.convert(event.locationInWindow, from: nil)
        let indexPath = self.indexPathForItem(at: point);
        if ANDelegate != nil{
            menu = ANDelegate?.collectionView(self, menu: menu, at: indexPath);
        }
        return menu;
    }
}

/// 扩展NSCollectionView的委托
protocol ANCollectionViewDelegate : NSObjectProtocol {
    func collectionView(_ collectionView:NSCollectionView, menu:NSMenu?, at indexPath: IndexPath?) -> NSMenu?
}

This is what I wrote an extension, and I hope to help everyone.


Here's Ken's idea to override menuForEvent: in an NSCollectionView subclass implemented in Swift:

// MARK: - Properties

/**
The index of the item the user clicked.
*/
var clickedItemIndex: Int = NSNotFound

// MARK: - Menu override methods

override func menuForEvent(event: NSEvent) -> NSMenu?
{
    self.clickedItemIndex = NSNotFound

    let point = self.convertPoint(event.locationInWindow, fromView:nil)
    let count = self.content.count

    for index in 0 ..< count
    {
        let itemFrame = self.frameForItemAtIndex(index)
        if NSMouseInRect(point, itemFrame, self.flipped)
        {
            self.clickedItemIndex = index
            break
        }
    }

    return super.menuForEvent(event)
}