8

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?

www.jensolsson.se
  • 3,023
  • 2
  • 35
  • 65
  • Related question → https://stackoverflow.com/questions/65841298/swiftui-onhover-doesnt-register-mouse-leaving-the-element-if-mouse-moves-too-fa – tyirvine Jun 09 '21 at 05:11

3 Answers3

6

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)
Valentin Shergin
  • 7,166
  • 2
  • 50
  • 53
  • what if you don't have a bridging header. How do you declare those lines in Swift? – MAH Dec 30 '15 at 06:39
  • I believe you just have to have `bridging header` (why not?) because there is no another way (IMO) to import these functions in Swift code. Or you can put this code in a dynamic library (written in C or Objective-C) and then import it in Swift code. – Valentin Shergin Dec 30 '15 at 06:49
  • No reason not to have one I suppose. My app is entirely in Swift so far but I was unable to get this part of it done. The only piece of the puzzle left is getting `CGAssociateMouseAndMouseCursorPosition` to work in the background also. – MAH Dec 30 '15 at 06:52
  • My app too. I don't think that using UI methods in non-main thread is good idea... But maybe something like 'dispatch_sync' can help. – Valentin Shergin Dec 30 '15 at 07:13
  • yes I agree for the most part, but there are some exceptions where it is perfectly necessary. If your app is designed to draw on a clear window above everything else for example without appearing to affect everything else. This is doable in a non-sandboxed app with a combination of `CGEventTap` and the hack mentioned here. Otherwise you are out of luck. – MAH Dec 30 '15 at 14:36
  • 1
    Is this approach still working? Which framework or library should be included to make this work? I get linker errors for: _CGSCreateBoolean, _CGSCreateCString, _CGSReleaseObj at OS X 10.11 – Jurlie Jul 13 '16 at 03:24
4

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];
}
Wil Shipley
  • 9,343
  • 35
  • 59
  • I guess you are referring to NSTrackingArea? I have tried this now but I cannot get it to work. I am only interested in changing the cursor when the app i active but I need it to happen also on inactive NSWindows within the app. I will update my question with my findings. – www.jensolsson.se Feb 09 '14 at 11:20
  • Thnaks! Now this is very close to be working after the edit, there is just one thing left to solve. Now it properly displays the correct cursor within both my NSWindows however as soon as I click on one of the NSWindows the cursor reverts to the standard one. If I then move in and out of the window it will be correct again. But I need to fix this little glitch as well. – www.jensolsson.se Feb 09 '14 at 18:36
  • What kinds of views are in your windows? Do you still have the cursorUpdate: method on the view? – Wil Shipley Feb 09 '14 at 23:43
  • They are all subclasses of NSImageView, I removed the cursorUpdate: method. In the subclass I implement viewWillMoveToWindow (with the content of someSetup in your example), mouseEntered (exactly as in your example), mouseExited (exactly as in your example). That is all. – www.jensolsson.se Feb 10 '14 at 18:20
4

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];
}
www.jensolsson.se
  • 3,023
  • 2
  • 35
  • 65
  • I'm new to OS X but from what I've read addCursorRect defines a specific cursor for the entire rect - what if you want to change the cursor as the mouse moves into different areas? Lets assume I can't define all the rects up-front. I guess you could change self.cursor and call resetCursorRects... Overriding `windowDidBecomeKey` and `windowDidBecomeMain` to also call `[self.cursor push]` appears to be an alternate solution, although you do briefly see the cursor flash to the normal arrow when clicking the window. (do you even need the `push` using cursor rects?) – sqweek May 28 '15 at 15:23