NSView control-click quirks
Yesterday I came across an interesting AppKit issue, reproducible in OS X 10.10 (but this has been around since at least 10.7). On the Mac, most users expect a control-click to behave identically to a right-click, and display a contextual menu. This is true in the Finder and most Cocoa apps. Within AppKit the primary hook for providing contextual menus in NSView is:
The default NSView handling for -rightMouseDown: passes the message to its superview if -menuForEvent: returns nil. This gives right-clicks a chance to work their way up the view hierarchy until they hit the top-level container view, which does have a menu and shows it as the default behavior. There's no parallel to this for a control-click, however. This is a problem since both of these actions should behave the same.
- (NSMenu *)menuForEvent:(NSEvent *)theEventYour view returns a menu, and it's automatically shown by NSView. The problem occurs when your view wraps one or more subviews and the click lands within a subview. (This is a common scenario, e.g. custom UI components that wrap multiple subviews but provide a top-level contextual menu).
You'd expect that NSView would display the contextual menu for your top-level container view for both right and control clicks, but it doesn't. A right click anywhere in the parent view (even if on a subview) shows the contextual menu as expected, whereas control-clicking within the view only shows the contextual menu if the click does not occur on one of the subviews.
The default NSView handling for -rightMouseDown: passes the message to its superview if -menuForEvent: returns nil. This gives right-clicks a chance to work their way up the view hierarchy until they hit the top-level container view, which does have a menu and shows it as the default behavior. There's no parallel to this for a control-click, however. This is a problem since both of these actions should behave the same.
So what to do? There are a number of possible workarounds:
- Traverse the subview tree, explicitly calling -setMenu: for all subviews
- Subclass/override all subview classes as needed to explicitly return e.g. [self.superview menuForEvent:]
- Override -hitTest: in the top-level NSView
The best solution I've found is to display the menu for control-clicks yourself. While this isn't ideal, it incurs the fewest side-effects, and provides the expected behavior, without any significant changes to existing code: