8
votes

I have subclassed NSWindow and I have a MYWindow class implementing the following method:

-(void)resetCursorRects {
    NSImage *image = [NSImage imageNamed:@"cursor.png"];
    [image setSize:NSMakeSize(32, 32)];
    NSCursor *cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSMakePoint(1, 1)];
    [super resetCursorRects];    
    [self addCursorRect:[self bounds] cursor:cursor];
}

This will change the cursor for the whole window and I will see cursor.png instead of the default mouse pointer. The problem is that this only works if the MYWindow is set to the key window which is of course non trivial to make it.

In the beginning of my project I just have one main window but now I need to have two different MYWindow. The problem with two windows it is not possible to set both as the key window and hence the custom mouse pointer is only displayed on the active window. I need to click the other window to make the cursor appear.

Is there any way around this? So I get a custom cursor on both windows?

Edit: Tried NSTrackingArea

I added this to my content view's init method:

self.trackingArea = [[NSTrackingArea alloc] initWithRect:[self frame] options: (NSTrackingCursorUpdate | NSTrackingActiveAlways | NSTrackingMouseMoved) owner:self userInfo:nil];
[self addTrackingArea:self.trackingArea];

Then I overrided cursorUpdate: like this:

-(void)cursorUpdate:(NSEvent *)event {
    NSLog(@"event : %@", event);
    [[NSCursor crosshairCursor] set];
}

This makes the crosshairCursor show when the NSWindow that contains the NSImageView derived class is key window. But if I make another NSWindow within the app the key window, the cursor returns to the standard cursor again. Am I doing something wrong?

3

3 Answers

6
votes

I struggled with this problem for a long period of time and I think there is only one way to change mouse cursor over inactive application (over non-foreground window). This is hacky and magic way.

Before calling pretty standard:

[[NSCursor pointingHandCursor] push];

You have to call:

void CGSSetConnectionProperty(int, int, int, int);
int CGSCreateCString(char *);
int CGSCreateBoolean(BOOL);
int _CGSDefaultConnection();
void CGSReleaseObj(int);
int propertyString, boolVal;

propertyString = CGSCreateCString("SetsCursorInBackground");
boolVal = CGSCreateBoolean(TRUE);
CGSSetConnectionProperty(_CGSDefaultConnection(), _CGSDefaultConnection(), propertyString, boolVal);
CGSReleaseObj(propertyString);
CGSReleaseObj(boolVal);

Or if you are using Swift:

Put this in your YourApp-Bridging-Header.h:

typedef int CGSConnectionID;
CGError CGSSetConnectionProperty(CGSConnectionID cid, CGSConnectionID targetCID, CFStringRef key, CFTypeRef value);
int _CGSDefaultConnection();

And then call:

let propertyString = CFStringCreateWithCString(kCFAllocatorDefault, "SetsCursorInBackground", 0)
CGSSetConnectionProperty(_CGSDefaultConnection(), _CGSDefaultConnection(), propertyString, kCFBooleanTrue)
4
votes

You should be able to add an NSTrackingArea that changes the cursor, as long as you don’t want it to also change when the app is inactive (that is essentially impossible).


Edit:

I was able to get this working with the following code:

- (vod)someSetup;
{
    NSTrackingArea *const trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect options: (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingInVisibleRect) owner:self userInfo:nil];
    [self.view addTrackingArea:trackingArea];
}

- (void)mouseEntered:(NSEvent *)theEvent;
{
    [[NSCursor IBeamCursor] push];
}

- (void)mouseExited:(NSEvent *)theEvent;
{
    [[NSCursor IBeamCursor] pop];
}
4
votes

Now I finally found a solution that works. I don't know if this will bite me in the tail in the future but at least this seem to work when testing.

Thanks Wil for the example it got me half way there. But it was only when I finally combined it with resetCursorRects and also defined a cursor rect in each view with the specific cursor. This took me a long time to figure out and I don't know if the solution is optimal (suggestions of improvement are welcome)

Below is the full example that made it work for me in the end (self.cursor is an instance of the cursor)

- (void)viewWillMoveToWindow:(NSWindow *)newWindow {
    NSTrackingArea *const trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect) owner:self userInfo:nil];
    [self addTrackingArea:trackingArea];
    [self.window invalidateCursorRectsForView:self];
}

- (void)resetCursorRects {
    [super resetCursorRects];
    [self addCursorRect:self.bounds cursor:self.cursor];
}

- (void)mouseEntered:(NSEvent *)theEvent {
    [super mouseEntered:theEvent];
    [self.cursor push];
}

- (void)mouseExited:(NSEvent *)theEvent {
    [super mouseExited:theEvent];
    [self.cursor pop];
}