8

In my OSX app I have a collection view which is a subclass of NSCollectionView.

I'm all satisfied with how the things are except the contextual menu, which I can't figure out yet.

So what I want is:

  • right-click on a collection view item brings up the contextual menu
  • the options picked from the menu (delete, edit, etc) are applied to the item that the click was performed on.

I know how to do it for NSOutlineView or NSTableView, but not for collection view.

I can't figure out how to get the index of the item clicked.

Does anyone have any ideas on how I can implement this?

Any kind of help is highly appreciated!

Andriy
  • 2,767
  • 2
  • 21
  • 29
Eugene Gordin
  • 4,047
  • 3
  • 47
  • 80

5 Answers5

6

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.

Simon
  • 438
  • 4
  • 14
  • As NSMouseInRect isnt available in Xamarin bindings, that is the only solution that worked for me - and it works very well - thanks! – tipa Feb 11 '20 at 12:51
5

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];
}
Ken Thomases
  • 88,520
  • 7
  • 116
  • 154
  • 1
    That's way too complicated. Have you tried setting the menu outlet of your collection view to a menu from your nib file? Set your controller object as delegate for the menu. Each time the menu is invoked, update your menu according to the selection in the collection view. – iljawascoding Feb 02 '15 at 13:57
  • I really like the `NSCollectionView` subclass approach — gives a good consistency of behaviour with `NSTableView`. – Luke Rogers Jul 06 '15 at 09:34
4

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)
}
Community
  • 1
  • 1
Luke Rogers
  • 2,369
  • 21
  • 28
1

In Swift 5, you can use

class ClickedCollectionView: NSCollectionView {
    var clickedIndex: Int?

    override func menu(for event: NSEvent) -> NSMenu? {
        clickedIndex = nil

        let point = convert(event.locationInWindow, from: nil)
        for index in 0..<numberOfItems(inSection: 0) {
            let frame = frameForItem(at: index)
            if NSMouseInRect(point, frame, isFlipped) {
                clickedIndex = index
                break
            }
        }

        return super.menu(for: event)
    }
}
onmyway133
  • 45,645
  • 31
  • 257
  • 263
0

Thanks for this solution. I wrapped it into a NSCollectionView subclass:

#import <Cocoa/Cocoa.h>

@interface TAClickableCollectionViewItem : NSCollectionViewItem
@property (nonatomic, assign) BOOL isClicked;
@end



@interface TAClickableCollectionView : NSCollectionView <NSMenuDelegate>
@property (nonatomic, readonly) id clickedObject;
@property (nonatomic, readonly) TAClickableCollectionViewItem *clickedItem;
@end

So you can use bindings in Interface Builder to highlight clicked items as well.

#import "TAClickableCollectionView.h"

@implementation TAClickableCollectionViewItem
@end



@implementation TAClickableCollectionView

- (NSMenu*) menuForEvent:(NSEvent*)event
{
    NSInteger _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;
            }
        }

    if(_clickedItemIndex < self.content.count) {
        id obj = [self.content objectAtIndex:_clickedItemIndex];
        TAClickableCollectionViewItem *item = (TAClickableCollectionViewItem *)[self itemAtIndex:_clickedItemIndex];

        if(item != _clickedItem) {
            [self willChangeValueForKey:@"clickedObject"];
            _clickedItem.isClicked = NO;
            _clickedItem = item;
            [self didChangeValueForKey:@"clickedObject"];
        }

        item.isClicked = YES;

        if(obj != _clickedObject) {
            [self willChangeValueForKey:@"clickedObject"];
            _clickedObject = obj;
            [self didChangeValueForKey:@"clickedObject"];
        }
    }

    return [super menuForEvent:event];
}

- (void)menuDidClose:(NSMenu *)menu {
    _clickedItem.isClicked = NO;
}
@end