Question

The iTunes mini-player (to give just one example) supports click-through where the application isn't brought to the front when the play/pause and volume controls are used.

How is this done?

I've been looking through Apple's documentation and have a little to go on, in Cocoa Event-Handling Guide, Event Dispatch it states:

Some events, many of which are defined by the Application Kit (type NSAppKitDefined), have to do with actions controlled by a window or the application object itself. Examples of these events are those related to activating, deactivating, hiding, and showing the application. NSApp filters out these events early in its dispatch routine and handles them itself.

So, from my limited understanding (How an Event Enters a Cocoa, Application) subclassing NSApplication and overriding - (void)sendEvent:(NSEvent *)theEvent should trap every mouse and keyboard event, but still, the window is raised on click. So either the window is raised before the event is seen by NSApplication or I'm missing something else.

I've looked at Matt Gallagher's Demystifying NSApplication by recreating it, unfortunately Matt didn't cover the event queue, so other than that, I'm stumped.

Any help would be appreciated, thanks.

Edited to add: Found a post at Lloyd's Lounge in which he talks about the same problem and links to a post at CocoaBuilder, capture first right mouse down. I'm currently trying out the code supplied there, after some fiddling around and reactivating the NSLog for [theEvent type], the left mouse button activity is being caught.

Now, left clicking on the window to bring it forward produces a sequence of event types, 13, 1, 13, these are NSAppKitDefined, NSLeftMouseDown and NSAppKitDefined again. Can I filter these out or find where they are going?

Was it helpful?

Solution 2

This isn't quite the answer I was looking for, but for now it works sufficiently well. By subclassing NSView and implementing the following methods, that view can then act as a button.

- (void)mouseDown:(NSEvent *)theEvent {
    [NSApp preventWindowOrdering];
//  [self setNeedsDisplay:YES];
}

- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent {
    return YES;
}

- (BOOL)shouldDelayWindowOrderingForEvent:(NSEvent *)theEvent {
    return YES;
}

Nothing else is required, that's it. Of course this bypasses all the NSButton goodness that I wanted to keep, I have to design and then take care of drawing and updating the button states, but I'm just glad to have an answer.

OTHER TIPS

This can be accomplished with an NSPanel that's been set to not hide on deactivate and to become key only as needed. This also assumes that the controls you'll be using return YES to -acceptsFirstMouse:; NSButtons do so by default.

You can turn off the hide on deactivate flag through IB for the panel but you'll need to issue the -setBecomesKeyOnlyIfNeeded:YES message to the panel to keep it and the app from coming forward on button clicks.

It's possible to make an NSButton respond to click-through events without changing its behaviour when the app is active:

Interface (ClickThroughButton.h):

#import <AppKit/AppKit.h>

@interface ClickThroughButton : NSButton

@end

Implementation (ClickThroughButton.m):

#import "ClickThroughButton.h"

@implementation ClickThroughButton

- (BOOL)shouldDelayWindowOrderingForEvent:(NSEvent *)theEvent {
    return ![NSApp isActive];    
}

- (void)mouseDown:(NSEvent *)theEvent {    
    if (![NSApp isActive]) {
        [NSApp preventWindowOrdering];        

        [self highlight:YES];

        NSEvent *mouseUpEvent = [[self window] nextEventMatchingMask:NSLeftMouseUpMask
                                                  untilDate:[NSDate distantFuture]
                                                     inMode:NSEventTrackingRunLoopMode
                                                    dequeue:YES];
        NSPoint mouseLocation = [self convertPoint:[mouseUpEvent locationInWindow] fromView:nil];
        BOOL mouseUpInside = [self mouse:mouseLocation inRect:[self bounds]];

        if (mouseUpInside) {
            if ([self target])
                [[self target] performSelector:[self action] withObject:self]; 
        }

        [self highlight:NO];            

    } else {
        [super mouseDown:theEvent];        
    }
}

@end

Have you checked out acceptsFirstMouse:? Any NSView can return YES to this event to say that the view should handle the event rather than bringing the window to the foreground. Presumably, by intercepting the event, it won't be used to bring the window to the foreground, but I can't vouch for that.

You'll obviously need to subclass any control you want to have this behavior to override acceptsFirstMouse:.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top