Getting mouse events from a transparent NSWindow
When a Core Animation layer is present in the root view of an NSWindow, the window loses its shadow. However, if you want to animate the entire contents of the window, this is the only choice. The problem isn't particularly difficult to solve: outset the window bounds from the actual content and add a shadow to the CALayer.
However, this raises another problem: your window will now start to gobble up mouse events that appear to be "outside" it. Initially, I tried to solve this by using +NSWindow windowNumberAtPoint:belowWindowWithWindowNumber: to retrieve the window number of the window below the clicked window, CGWindowListCopyWindowInfo to get a list of windows below the window, a loop to find the correct one, and CGEventPostToPSN to post a modified (specifically, the location and window number) event to the process owning that window.
It worked. Sort of. Occasionally.
Clearly, there had to be a better suggestion. And there was. There were a few key problems.
- Sending events across processes was the part that "sort of" worked. Within my application, all worked as expected. However, when I clicked on a background app, the window would be raised, but it did not gain key status.
- Unfortunately, there's no
NSMutableEvent, so I had to make newNSEventinstances rather than editing the old one. Also unfortunately, while "mouse down" events are really easy to make (there's a class message onNSEvent), scrolling events are... difficult. I'm sure there's a way to do it, but it involved a lot more work than it ought to.
Luckily, there was a much better option. There is a rectangular portion of my window (the non-shadow part) that should accept mouse events. The events outside of this area should go to the window below it. Windows are rectangular. Just add another window.
So, I made my primary window ignore mouse events, and added a child window on top of it, temporarily with a 50% transparent red fill so that I could ensure the frame was being set correctly. The window's -sendEvent implementation just checks if the event is a mouse event, then translates the event's -locationInWindow (which is trivial) to match the old window. Pass -sendEvent with a modified event to the other window, and we're done. It works perfectly.
Until you remove the color. Transparent NSWindow instances don't get mouse events.
The prevailing suggestion on the web to solve this issue is actually make it a little opaque. You might be able to get away with that, I guess. But it's not clear what opacity it will begin to work at. 0.1 worked. 0.01 didn't.
As it turns out, the best solution is the class that caused the issue in the first place: CALayer.
@implementation RDEventOverlayView
-(void)viewDidMoveToWindow
{
[self setWantsLayer:YES];
[CATransaction begin];
[CATransaction setDisableActions:YES];
[[self layer] setOpacity:0.0];
[CATransaction commit];
}
-(void)drawRect:(NSRect)dirtyRect
{
[[NSColor blackColor] set];
[NSBezierPath fillRect:dirtyRect];
}
@end
Just give the window's content view a CALayer with full transparency. NSWindow has no idea, and gets the events as it normally would, while the display on screen remains perfect.