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 {
}
@end
 
@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];
  }
}
 
@end
  

Leave a Reply