Contextual menu on only certain items in a "Source List"
Asked Answered
P

4

6

I have a window with a Source List (NSOutlineView). My source list has just two levels. Level one is header and level two is data. I want to have a contextual menu on some of the data cells. Not all.

First, I try to attach a menu on the table cell view who represents the data cell -> nothing happens.

Second, I attach a menu on the Outline View in IB -> the contextual menu opens on each cells (header and data). I search for stopping the opening of the menu, but I don't find anything.

Do you have some ideas ?

Thank you

OS X 10.8.2 Lion, Xcode 4.5.2, SDK 10.8

Pinckney answered 21/1, 2013 at 20:59 Comment(4)
What if you set it on the row view?Aught
How do you set it on the row view ? Here is a view of the hierarchy in IB linkPinckney
I guess there are no row views in outline views, then. I've only done view-based table views (which have row views) and cell-based outline views, not view-based outline views. Sorry.Aught
Anyone has an idea ? I really don't know in which direction I can go...Pinckney
J
7

If you subclass NSOutlineView, you can override menuForEvent: to return a menu only if the user clicked on the correct row. Here's an example:

- (NSMenu *)menuForEvent:(NSEvent *)event;
{
    //The event has the mouse location in window space; convert it to our (the outline view's) space so we can find which row the user clicked on.
    NSPoint point = [self convertPoint:[event locationInWindow] fromView:nil];
    NSInteger row = [self rowAtPoint:point];

    //If the user did not click on a row, or is not exactly one level down from the top level of hierarchy, return nil—that is, no menu.
    if ( row == -1 || [self levelForRow:row] != 1 )
        return nil;

    //Create and populate a menu.
    NSMenu *menu = [[NSMenu alloc] init];
    NSMenuItem *delete = [menu addItemWithTitle:NSLocalizedString( @"Delete", @"" ) action:@selector(delete:) keyEquivalent:@""];

    [self selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];

    //Set the Delete menu item's represented object to the clicked-on item. If the user chooses this item, we'll retrieve its represented object so we know what to delete.
    [delete setRepresentedObject:[self itemAtRow:row]];

    return menu;
}

This assumes we're compiling with ARC, so you don't need to autorelease the menu object being created.

Josephinejosephson answered 10/2, 2013 at 20:18 Comment(1)
Also, if the questioner has both the outline view and the desired menu in the same nib, he could hook that up via the usual outlet and then use [self menu] in this method to retrieve it instead of creating a new one each time.Aught
M
2

This extension + subclass (both NSOutlineView and NSTableView) does the sensible thing of seeing whether a menu is attached to a cell view or row view. Just a general, reusable subclass!

Set the menu on the cell view in outlineView:viewForTableColumn:item:menu is a NSResponder property.

(Below is in Swift)

// An extension lets us both subclass NSTableView and NSOutlineView with the same functionality
extension NSTableView {
    // Find a cell view, or a row view, that has a menu. (e.g. NSResponder’s menu: NSMenu?)
    func burnt_menuForEventFromCellOrRowViews(event: NSEvent) -> NSMenu? {
        let point = convertPoint(event.locationInWindow, fromView: nil)
        let row = rowAtPoint(point)
        if row != -1 {
            if let rowView = rowViewAtRow(row, makeIfNecessary: true) as? NSTableRowView {
                let column = columnAtPoint(point)
                if column != -1 {
                    if let cellView = rowView.viewAtColumn(column) as? NSTableCellView {
                        if let cellMenu = cellView.menuForEvent(event) {
                            return cellMenu
                        }
                    }
                }

                if let rowMenu = rowView.menuForEvent(event) {
                    return rowMenu
                }
            }
        }

        return nil
    }
}


class OutlineView: NSOutlineView {
    override func menuForEvent(event: NSEvent) -> NSMenu? {
        // Because of weird NSTableView/NSOutlineView behaviour, must set receiver’s menu otherwise the target cannot be found
        self.menu = burnt_menuForEventFromCellOrRowViews(event)

        return super.menuForEvent(event)
    }
}

class TableView: NSTableView {
    override func menuForEvent(event: NSEvent) -> NSMenu? {
        // Because of weird NSTableView/NSOutlineView behaviour, must set receiver’s menu otherwise the target cannot be found
        self.menu = burnt_menuForEventFromCellOrRowViews(event)

        return super.menuForEvent(event)
    }
}
Mcculloch answered 3/5, 2015 at 14:56 Comment(0)
S
0

It's not clear from your question whether your outline is view based or cell based. That's important.

If you're view based, then your view instances can implement

- (NSMenu *)menuForEvent:(NSEvent *)theEvent

and return the menu appropriate to that item -- or nil f you don't want a menu at all.

If you're cell based, or if you don't want to handle this in the view class for some reason, you'll need to subclass NSOutlineView and implement - (NSMenu *)menuForEvent:(NSEvent *)theEvent there. Again, you'll figure out which cell is hit or active, and decide from that what menu you want.

Superfluid answered 14/2, 2013 at 1:0 Comment(2)
Thank you Mark. Yes, my outline view is "view based". I have a class who is a subclass of NSTableCellView. I add an implementation of "menuForEvent" in this class, I put a breakpoint inside, but it's never reached... Anything special to do ?Pinckney
Time to be systematic about this. Start out by checking that your view is receiving mouseDown events when you click on it. It’s possible some subview is intercepting the events. Once you're sure you're getting the event, you should make sure that you're not foreclosing the built-in methods by forgetting to call a superclass method somewhere.Superfluid
V
0
- (void)rightMouseDown:(NSEvent *)event

An NSView will not pass this to the next view, This method looks to see that the current class has a menuForEvent:, if it does then it is called. If it does not then it is finished and nothing else will happen. This is why you will not see an NSTableCellView respond to a menuForEvent: because the table view swallows the rightMouseDown:.

You may subclass the tableview and handle the rightMouseDown: event and call the NSTableCellView's rightMouseDown: and handle displaying your menu that you have constructed in your storyboard and hooked up to your NSTableViewCell.

Here is my solution in a subclassed NSTableView:

- (void)rightMouseDown:(NSEvent *)event
{
    for (NSTableRowView *rowView in self.subviews) {

        for (NSView *tableCellView in [rowView subviews]) {

            if (tableCellView) {
                NSPoint eventPoint = [event locationInWindow];
//            NSLog(@"Window Point:     %@", NSStringFromPoint(eventPoint));

                eventPoint = [self convertPoint:eventPoint toView:nil];
                eventPoint = [self convertPoint:eventPoint toView:self];
//            NSLog(@"Table View Point: %@", NSStringFromPoint(eventPoint));

                NSRect newRect = [tableCellView convertRect:[tableCellView bounds] toView:self];
//            NSLog(@"Rect: %@", NSStringFromRect(newRect));

                BOOL rightMouseDownInTableCellView = [tableCellView mouse:eventPoint inRect:newRect];
//            NSLog(@"Mouse in view: %hhd", mouseInView);

                if (rightMouseDownInTableCellView) {
                    if (tableCellView) {
                        // Lets be safe and make sure that the object is going to respond.
                        if ([tableCellView respondsToSelector:@selector(rightMouseDown:)]) {
                            [tableCellView rightMouseDown:event];
                        }
                    }
                }
            }
        }
    }
}    

This will find where the right mouse event occurred, check to see if we have the correct view and pass the rightMouseDown: to that view.

Please let me know if this solution works for you.

Vanya answered 14/8, 2016 at 15:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.