Using NSMenu in a LSUIElement

While developing ShelfMenu, I have encountered two strange behaviors (aka bugs...) in Cocoa; I have not been able to find any documents or post on these, and I have spent long hours to find acceptable workarounds. I will summarize here the issues that I have found, and the solutions, hopefully this will save you some time.

The worst problem by far is related to manually invoking menus. Both NSMenu and NSStatusItem have methods to popup a menu programmatically (this is useful if you want to assign a keyboard shortcut to the menu).

[sourcecode lang="objc" gutter="false"]
// NSMenu
- (BOOL)popUpMenuPositioningItem:(NSMenuItem *)item atLocation:(NSPoint)location inView:(NSView *)view

//NSStatusItem
- (void)popUpStatusItemMenu:(NSMenu *)menu
[/sourcecode]

These methods are synchronous (i.e. they only return when the menu is dismissed), but in a couple of very specific cases they do not return, ever... Your application is not crashing, but it's stuck until the user does some actions which will make the system realize that the menu is not there any more. The test case to reproduce this bug is simple:

  • Set LSUIElement to true in Info.plist (if LSUIElement is false the system behaves normally);
  • Invoke the menu programmatically;
  • [tooltip trigger="Activate"]See the code below, line 3[/tooltip] your application;
  • Without dismissing the menu, switch to a different application (with ⌘-TAB or by clicking on another application);
  • Your application is now unresponsive, and if you add a couple of NSLog you will realize that the popUpMenu (or popUpStatusItemMenu) method has not returned.

If the user by chance clicks on some status item, or invokes exposé, your method will return, otherwise you will most likely get a 1-star review...

[note_box]Update: I have sent this but to Apple, feel free to reference it: rdar://9277191[/note_box]

This problem is actually quite simple to solve. My best guess is that the application activation and the menu invocation must not be in the same event cycle. The solution is to activate your application in a method triggered by a timer. Doing this allows the system to complete the current event cycle before the timer is triggered. Here is some sample code showing how to do that:

[sourcecode lang="objc" wraplines="true" highlight="3,4"]
- (void) someMethod {
// Some code
[NSApp activateIgnoringOtherApps:YES];
[NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(showMenu) userInfo:nil repeats:NO];
//Some other code
}

- (void) showMenu {
// This will show the menu at the current mouse position
[aMenu popUpMenuPositioningItem:[mainMenu itemAtIndex:0] atLocation:[NSEvent mouseLocation] inView:nil];
}
[/sourcecode]

The second problem is less critical, but it is equally annoying if you plan to make your application completely keyboard-driven. If you use a custom NSView in your menu, and if you hover the mouse over the view (without clicking) before dismissing the menu by clicking somewhere else, you won't be able to navigate the menu using the arrow keys again. This remains true till the application is restarted, or till the user invokes the menu again, hover the mouse over the view again, but this time clicks on the view, and then dismisses the menu.
For this second problem the workaround is simply to activate the application. So, you will have this problem with any LSUIElement applications using an NSView in an NSMenu, and the workaround to this will expose you the the first (and worst) issue. But now you have a workaround to that as well.

Probably the best workaround would be not to use NSMenu and to design a custom NSView in a borderless window; I will think about that for the next version.