How to implement a context menu for an NSCollectionView

In my OSX application , I have a collection view that is a subclass of NSCollectionView .

Everything suits me, how things are, except for the context menu, which I still can not understand.

So what I want is:

  • right-clicking on a collection view item brings up a context menu
  • the parameters selected in the menu (deletion, editing, etc.) are applied to the element that was clicked.

I know how to do this for NSOutlineView or NSTableView , but not for representing a collection.

I can not figure out how to get the index of the element clicked.

Does anyone have any ideas how to implement this?

Any help is much appreciated!

+7
source share
5 answers

One approach that I used is to not try to apply context menu actions to one specific item that was clicked, but to the selected items. And I make a clicked-on element that adds itself to the selection.

I used a custom view to represent a collection item. The custom view class has an output, item , to its own collection view element, which I plug into the NIB. It also overrides -rightMouseDown: so that the element adds 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 an item to this selection, you can check if it is already selected. If so, do not change the selection. If this is not the case, replace the selection with only one element (making it the only selected element).

Alternatively, you can set the context menu in the element views, and not in the collection view. Then the menu items can be targeted either at the item or at the collection item.

Finally, you can subclass NSCollectionView and override -menuForEvent: You still call super and return the returned menu, but you can take the opportunity to record the event and / or element in its location. To define this, you would 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]; } 
+5
source

Here's Ken's idea of overriding menuForEvent: in a subclass of NSCollectionView 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) } 
+3
source

In principle, all our solutions are able to satisfy the requirements, but I would like to make an addition 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 the extension, and I hope to help everyone.

+2
source

Thank you for this decision. I wrapped it in a subclass of NSCollectionView:

 #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 

Thus, you can use the bindings in Interface Builder to select items with a click.

 #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 
0
source

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) } } 
0
source

Source: https://habr.com/ru/post/1203753/


All Articles