Klauxs
Klauxs

Reputation: 1

How track a NSWindow's onActiveSpace property

I would like find a way to track changes to the onActiveSpace property of an NSWindow.

I used the KVO method to monitor this property, but did not receive the property change notification. Does this property not support KVO? If so, is there any way to confirm whether a certain attribute supports KVO? Here is my code:

- (instancetype)initWithWidgetSpaceTracker:(WidgetSpaceTracker*)owner {
  if ((self = [super init])) {
    DCHECK(owner);
    _owner = owner;
    NSWindow* window = _owner->GetNSWindow();

    if (window) {
      [window addObserver:self
               forKeyPath:@"onActiveSpace"
                  options:NSKeyValueObservingOptionNew
                  context:nil];
    }
  }

  return self;
}
- (void)observeValueForKeyPath:(NSString*)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey, id>*)change
                       context:(void*)context {
  DCHECK(_owner);
  if ([keyPath isEqual:@"onActiveSpace"]) {
    _owner->OnSpaceActiveChanged([change[NSKeyValueChangeNewKey] boolValue]);
  } else {
    [super observeValueForKeyPath:keyPath
                         ofObject:object
                           change:change
                          context:context];
  }
}

Upvotes: 0

Views: 56

Answers (1)

Jinwoo Kim
Jinwoo Kim

Reputation: 621

onActiveSpace can change under the following circumstances:

  • When the user changes the stage they are currently viewing. For example, assume that in Mission Control there are Stage 1 and Stage 2, and the window is on Stage 1 while the user is viewing Stage 1. In this case, onActiveSpace is YES; however, if the user switches to Stage 2, onActiveSpace will change to NO. This stage change can be detected via the NSWorkspaceActiveSpaceDidChangeNotification.

  • When the window moves to another stage. This cannot be achieved using public APIs. Instead, you can use the SkyLight API as shown below:

#import <Cocoa/Cocoa.h>

NS_ASSUME_NONNULL_BEGIN

APPKIT_EXTERN NSNotificationName const MA_NSWindowActiveSpaceDidChangeNotification;
APPKIT_EXTERN void startWindowActiveSpaceObservation(void);

NS_ASSUME_NONNULL_END

/* ——————————— */

#import <objc/message.h>
#import <objc/runtime.h>
#include <dlfcn.h>

NSNotificationName const MA_NSWindowActiveSpaceDidChangeNotification = @"MA_NSWindowActiveSpaceDidChangeNotification";

void _ma_NSWindow_Category_didReceiveEvent(unsigned int type, const void *data, size_t length, const void *context, unsigned int cid) {
    long windowNumber = *(long *)data;
    NSWindow *window = reinterpret_cast<id (*)(id, SEL, long)>(objc_msgSend)(NSApp, sel_registerName("windowWithWindowNumber:"), windowNumber);
    if (window == nil) return;
    
    [NSNotificationCenter.defaultCenter postNotificationName:MA_NSWindowActiveSpaceDidChangeNotification object:window];
}

void startWindowActiveSpaceObservation(void) {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        void *handle = dlopen("/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLight", RTLD_NOW);
        void *symbol = dlsym(handle, "SLSRegisterConnectionNotifyProc");
        assert(symbol != NULL);
        
        __kindof NSNotificationCenter *notificationCenter = NSWorkspace.sharedWorkspace.notificationCenter;
        unsigned int connectionID = reinterpret_cast<unsigned int (*)(id, SEL)>(objc_msgSend)(notificationCenter, sel_registerName("connectionID"));

        // ____NSDoOneTimeWindowNotificationRegistration_block_invoke
        reinterpret_cast<SInt16 (*)(unsigned int, void *, unsigned int, void *)>(symbol)(connectionID, (void *)_ma_NSWindow_Category_didReceiveEvent, 0x33b, NULL);
    });
}

Then, you can use it like this:

// invoke from `-applicationDidFinishLaunching:`
startWindowActiveSpaceObservation();

[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(_didChangeWindowActiveSpace:) name:MA_NSWindowActiveSpaceDidChangeNotification object:nil];

Thus, when notifications for NSWorkspaceActiveSpaceDidChangeNotification and MA_NSWindowActiveSpaceDidChangeNotification are received, the onActiveSpace value will change.

Upvotes: 0

Related Questions