Alternative Menu Items in NSMenu - cocoa

I have an NSMenu that contains NSMenuItems with custom views. I want it so that when the alt button is pressed, the menu items would change part of their look (through their view). I found setAlternative in the NSMenuItem docs, however, in practice I could only get it to work with NSMenuItems without custom views. As soon as I set a custom view, all of the menu items would be displayed.
Also, I tried getting keypress events while the menu was open. Due to the other run loop, NSApplication's sendEvent: doesn't receive events until after the menu is closed. Therefore, I can't just intercept the event coming in.
Does anyone know how I can get notified, whether through delegation or subclassing, of when the alt key is pressed when a menu is opened?

You should set an object as the delegate of your menu and then implement the delegate method -menu:updateItem:atIndex:shouldCancel:.
This will allow you to change the state of your custom view before the menu item is displayed, based on the current modifier state.
You can get the current modifiers by asking for [[NSApp currentEvent] modifierFlags].
If you need to be notified if the modifier flags change while your menu is open, implement the -flagsChanged: method in your custom view:
- (void)flagsChanged:(NSEvent*)event
{
if ([event modifierFlags] & NSAlternateKeyMask) // check for option key
{
//do something
}
else
{
//do something else
}
}

Related

How to create an NSMenu containing an NSMenuItem which only appears while holding a keyboard modifier key?

I'd like to create an NSMenu containing an NSMenuItem which is hidden by default, and only appears while the user is holding a keyboard modifier key.
Basically, I'm looking for the same behaviour as the 'Library' option in the Finder's 'Go' Menu:
Without holding Option (⌥):
While holding Option (⌥):
I already tried installing a key listener using [NSEvent addGlobalMonitorForEventsMatchingMask: handler:] to hide and unhide the NSMenuItem programmatically by setting it's hidden property. This kind of worked, but the problem is that the hiding/unhiding wouldn't work while the NSMenu was open. Apparently an NSMenu completely takes over the event processing loop while it's open, preventing the key listener from working.
I could probably use a CGEventTap to still receive events while the NSMenu is open, but that seems like complete overkill.
Another thing I discovered which does a similar thing to what I want is the 'alternate' mechanism of NSMenu. But I could only get it to switch out NSMenuItems, not hide/unhide them.
Any help would be greatly appreciated. Thanks!
Let's say your option-only menu item's action is (in Swift) performOptionOnlyMenuItem(_:) and its target is your AppDelegate.
The first thing you need to do is make sure AppDelegate conforms to the NSMenuItemValidation protocol.
The second thing you need to do is implement the validateMenuItem(_:) method, and have it check whether the menu item sends the performOptionOnlyMenuItem(_:) action. If so, set the item's isHidden property based on whether the option key is currently pressed.
If you don't need to validate any other menu items, the code can look like this:
extension AppDelegate: NSMenuItemValidation {
func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
switch menuItem.action {
case #selector(performOptionOnlyMenuItem(_:)):
let flags = NSApp.currentEvent?.modifierFlags ?? []
menuItem.isHidden = !flags.contains(.option)
return true
default:
return true
}
}
}
If the action is sent to some other target, you need to implement the validation (including the protocol conformance) on that target. Each menu item is validated only by the item's target.
I found a solution that behaves perfectly!
On the NSMenuItem you want hidable, set the alternate property to YES, and set the keyEquivalentModifierMask property to the keyboard modifiers which you want to unhide the item.
In your NSMenu, right before the NSMenuItem which you want to be hideable, insert another NSMenuItem that has height 0.
In Objc, you can create an NSMenuItem with height 0 like this:
NSMenuItem *i = [[NSMenuItem alloc] init];
i.view = [[NSView alloc] initWithFrame:NSZeroRect];
The hideable NSMenuItem will now be 'alternate' to the zero-height NSMenuItem preceding it. The zero-height item will display by default, but while you hold the keyboard modifier(s) you specified, the zero-height item will be swapped out with the hideable item. Because the zero-height item is invisible, this has the effect of unhiding the hideable item.

Rich, Window-like Context Menu (like Interface Builder)

I need a context menu that is similar in capabilities to what Interface Builder presents when right(control)-clicking on a view, view controller etc.:
At first sight, it looks like an NSPanel with its style attribute set to "HUD Panel", containing some sort of outline view.
The window itself shouldn't be difficult to implement, but the usual way of presenting a context menu on right(control)-click is by overriding the method:
func menu(for event: NSEvent) -> NSMenu?
...which takes an NSMenu as the return value; can't pass an NSWindow / NSPanel instead.
Perhaps I could do something like this:
override func menu(for event: NSEvent) -> NSMenu? {
// Create our popup window (HUD Panel) and present it
// at the location of event:
// (...actual code omitted...)
// Prevent any actual menu from being displayed:
return nil
}
...but it feels like a hack; I am tricking the system into giving away the timing of the right(control)-click event by pretending to care about presenting an actual NSMenu (i.e., overriding a method explicitly intended for that), but using that timing to do something different.
I would also need to place some logic to dismiss the window when the user clicks somewhere else (context menues have this functionality built in).
I don't think that subclassing NSMenu and NSMenuItem to obtain the above behaviour and appearance is feasible either...
Is there a better way?
Does anybody know (or is able to guess) what Interface Builder actually does?
EDIT: As pointed out in the comment by #Willeke, the conditions for the panel to be shown are not exactly the same as a context menu, so it most surely is not one. This means that hijacking the method above in order to display the window is not just unelegant, but wrong. The question stands as to How to Display the Window (i.e., detect static, non-dragging right click).
You'll want to override rightMouseDown(with:) from your view subclass and use that to trigger showing the panel. NSView's implementation of that method is what calls menu(for:) and presents the returned menu, so your custom subclass can use it to show its custom menu panel instead of calling super.
For full behavioral consistency with both standard context menus and the menus in Interface Builder, you'll also want to handle ctrl-left-clicks by overriding mouseDown(with:) and check for if the event's modifierFlags includes .control.

Can I open an NSMenu programatically?

Like the title says, I want to automatically open the menu at certain intervals. Preferably from within the program itself, as I am sure I could do something manually with applescripts+cron, which I don't want to use.
I am talking about a menu/button/whatever from the OSX menubar. Just to be clear. No custom views, just a plain nsmenu.
For opening an NSStatusItem menu, I found it easier to call the performClick method on the button belonging to the NSStatusItem. That way I didn't have to worry about properly passing an event or view to popUpContextMenu. Something like this:
statusItem.button?.performClick(nil)
You can programatically show an NSMenu by calling:
[NSMenu popUpContextMenu:YOUR_MENU withEvent:MOUSE_EVENT forView:YOUR_VIEW]
To construct the event you can use NSEvent's +mouseEventWithType:location:modifierFlags:timestamp:windowNumber:context:eventNumber:clickCount:pressure:
Note: if your menu is an NSStatusItem menu, you will have to set the item's view to be able to popUp the menu.

Which NSTableView receives NSMenuItem?

I have a custom NSViewController with two NSTableViews side-by-side, something like a split-view setup where the selection on the left tableView changes the right tableView's list. I'm not sure how to handle NSMenuItem events in this case. For e.g. if I press the Delete button, how do I distinguish between whether it's the left tableview or the right tableview that was highlighted when the Delete button was pressed? All I get is the delete: selector called with the NSMenuItem as the sender.
First, some background:
In Cocoa terminology, the "active" view or control is known as the "first responder." For instance, when you're entering text into a text field, the text field is considered the "first responder" because it's the object that's first to respond to keyboard input. An NSTableView can also receive first responder status (you can control the selected row by using the arrow keys).
You can ask the window for it's first responder like so:
// it's not necessarily a sure thing that the first responder is a TableView.
id myFirstResponder = [_parentWindow firstResponder];
if (myFirstResponder == _leftTableView) {
// left tableview is selected
} else if (myFirstResponder == _rightTableView) {
// right tableview is selected
}

Cannot seem to setEnabled:NO on NSMenuItem

I have subclassed NSMenu and connected a bunch of NSMenuItem's via Interface Builder. I have tested via the debugger to see that they really get initialized.
The menu is set to not auto enable items. Still when I set any of my NSMenuItem's to [myMenuItem setEnabled:NO] they continue to be enabled. Even if I call [self update] from within the NSMenu subclass.
What am I missing?
Had the same issue, so I thought I'd post my solution. NSMenu auto enables NSMenuButtons, so we have to override that.
In IB:
Or programmatically:
// Disable auto enable
[myMenu setAutoenablesItems:NO];
// Test it
[myMenuButton setEnabled:NO];
[myMenuButton setEnabled:YES];
I solved it with the help of a colleague, so I post it here for others that experience the same issue.
You should set your NSMenu-sublass to auto-enable items (default behaviour) and then implement this method in the NSMenu-sublass.
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem {
return [menuItem isEnabled];
}
You should uncheck Auto Enables Items on the closest parent NSMenu
You can solve this problem without subclassing.
If only you need is standard menu + some custom NSMenuItems that can be disabled on your control, than you can just:
Add one more menu node - just drag and drop Submenu Menu Item from Object library to your menu.
Add all NSMenuItems you want to manage to this new Menu node.
Open Attributes inspector for your New Menu node, and switch off Auto Enables Items option:
configure any other options of your menu & items.
Now you can write a code like:
#property (weak) IBOutlet NSMenuItem *hidePlateMenuItem;
...
[self.hidePlateMenuItem setEnabled:NO];
and it will works well.
Adding to the response of itsdavyh, if the menu item is located inside one or more submenus you only have to uncheck the 'Auto enables items'-property on the submenu of the menu item and not on any other parentmenus.
I tried all these solution but finally i found the real problem that also make more sense to me.
It also is the simplest way to handle disabled nsmenuitem that no needs to subclass or do code.
The nsmenuitem before to be child of menu itself is child of the main item for example "Save as..." is child of "File". Just select the parent item (in this example is File) and set off "auto enable menu items" in the menu ispector panel, and here you go!
Swift 3 answer:
I have a submenu item under the standard "View" menu called "Enable System Setup On Launch". I use the function below to enable or disable the menu item. Note: the view menu does need the "Auto Enable Items" in IB to be turned off.
func enableSystemSetupMenuItem(enabled:Bool) {
//set view menu item to enabled: value
//requires "Auto Enable Items" of "View" menu item to be turned off in IB
//because "View" menu is now turned off for "Auto Enable" we have to handle all
//of the "View" menu items ourselves
//just to avoid operating on menu separators I set all other menu items to TAG = -1
let main = NSApplication.shared().menu?.item(withTitle: "View")
let subMenuItems = main?.submenu?.items
for item in subMenuItems! {
if item.title == "Enable System Setup On Launch" {
item.isEnabled = enabled
} else if item.tag == -1 {
item.isEnabled = true
}
}
}
Try calling [myMenuItem setEnabled:NO] from a different place and making sure that it happens after the menu-containing nib is loaded. Maybe do it right in the subclassed NSMenu's awakeFromNib.

Resources