The Modal Panel

I’m still in the wonderful world of export plugins for Aperture/iPhoto.

There is one thing that took some time to figure out: the Modal Panel Conundrum.

Basically, here’s the problem: when you make a plugin for these applications, you don’t own the window. The application expects you to hand over a view and will integrate it in the window. That means it’s awfully limited in terms of the amount of data/effects/nifty things you can do, graphically.

So the decision was taken to add the optional stuff in a side “panel” (HUD window or regular panel, depending on the situation).

Problem number one: the export window is modal (normal). That means any new panel you setup and present a/ is going to be in the background and b/ won’t receive mouse/keyboard events.

Not to worry, my fellow developers! NSPanel has a method setWorksWhenModal: . If you set it to YES, the mouse and keyboard events will be forwarded to the panel. As for the background thing, it’s a window level problem. Use setLevel: to any level above NSModalPanelWindowLevel.

You’d think that’s it, right? Well unfortunately, it isn’t. While most controls do work as expected, there is ONE thing that doesn’t work. You can’t have any IBAction called in your code from the controls on the panel.

The run loop is in modal mode, therefore any event that should generate an IBAction generates a NSBeep instead. That means no button, no slider that updates a value in your code, no popup, no nothing. It should work, since it’s set to work when modal, and it does, to a point.

But fear not, you actually have a couple of possible solutions.

For sliders, popups, etc, bindings still work. Therefore if you bind the value in the control to a value in your code (and you have a custom accessor, for instance), you will detect the changes and have the correct value. A little tricky, but nothing ugly there.

For controls that have them, notifications work as well. So instead of having IBActions called in your code, you can setup notifications and react to them instead.

The only thing these two solutions doesn’t work for is unfortunately the most common control out there… the button. A button calls an IBAction. There’s nothing else it can do. So what can you do? Well you could “not use buttons”, but I agree it’s sometimes quite a tedious gymnastic.

So the solution is to subclass NSPanel, and to make your “modal panels” use that class instead. Here’s the code. As usual, feel free to comment and/or use it!

@interface NZModalPanel : NSPanel {
@implementation NZModalPanel
- (BOOL) worksWhenModal {
  return YES;
- (void)sendEvent:(NSEvent *)event {
  id clickedItem = [[self contentView] hitTest:[event locationInWindow]];
  if([clickedItem isKindOfClass:[NSButton class]]) {
    NSButton *clickedButton = (NSButton*) clickedItem;
    if([clickedButton target] != nil && [clickedButton action] != nil) {
      [[clickedButton target] performSelector:[clickedButton action] withObject:clickedButton];
  } else {
    [super sendEvent:event];


  1. I had the same problem you did with a plug-in panel that needed to operate above a modal window from the parent app. Your article was the first one I found talking about the problem, and your solution works, but it breaks a lot of things, like buttons in the panel no longer light up when you click them.

    After hours of research (finding nothing) and then tracing the cocoa calls with dtrace, I finally found the actual problem. – (BOOL) worksWhenModal was not returning YES from the owner of the NSPanel window. In other words, I loaded the Toolbar NSPanel from a NIB as follows:
    [NSBundle loadNibNamed: @”Toolbox” owner: nsToolbox];

    nsToolbox is just an instance of a class I made called NSClassToolbox, which is a subclass of NSObject. nsToolbox has a member variable called nsPanel that is set to the NSPanel that actually represents the visible panel window.

    Like you, I tried calling [nsToolbox->nsPanel setWorksWhenModal:YES] and found that my panel window came alive and some things worked, but buttons and tabs did not work.

    To get everything working, all I had to do was add this method to the NSClassToolbox class:
    – (BOOL) worksWhenModal {
    return YES;

    Frustratingly simple, I know. Hopefully the same thing will work for you.

    An even better solution would probably be to change NSClassToolbox to be a subclass of NSPanel and use it for both the NIB owner and the class of the visible window. I think it may also be a good idea to add these methods:

    – (BOOL) becomesKeyOnlyIfNeeded {
    return YES;

    – (BOOL) isFloatingPanel {
    return YES;

Leave a Reply