Click-through buttons and not raising the window
Asked Answered
O

4

6

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?

Oliveolivegreen answered 30/9, 2009 at 14:48 Comment(0)
O
3

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.

Oliveolivegreen answered 23/10, 2009 at 14:20 Comment(0)
C
3

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.

Cacia answered 30/9, 2009 at 20:30 Comment(4)
I want to believe this is the answer, but reading the NSView class reference (where the acceptsFirstMouse: method is defined) says that it's set to NO. And finally subclassing NSView makes no difference anyway.Oliveolivegreen
Just found a brief discussion on CocoaDev, cocoadev.com/index.pl?PreventWindowOrdering, this does what I want, it seems like a bad way to go about it though.Oliveolivegreen
The docs for acceptsFirstMouse: specifically calls out NSButton as returning YES: "Many control objects, however, such as instances of NSButton and NSSlider, do accept them, so the user can immediately manipulate the control without having to release the mouse button."Cacia
If you can't use an NSPanel for the window where you want this behavior the method described on CocoaDev seems fine to me.Cacia
O
3

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.

Oliveolivegreen answered 23/10, 2009 at 14:20 Comment(0)
A
2

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
Antivenin answered 16/11, 2011 at 18:4 Comment(1)
It works, but how to keep dialog on top? I am developping an application like Keyboard Viewer.Inhaler
W
1

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:.

Wallaby answered 30/9, 2009 at 19:17 Comment(1)
acceptsFirstMouse: and acceptsFirstResponder: have no effect, it was (and I should have noted) my first attempt at this problem. Thanks though.Oliveolivegreen

© 2022 - 2024 — McMap. All rights reserved.