Detecting when the active Space changes

OK, so there’s been a discussion about this on the cocoa-​dev list of late, and no-​one answered the question — I figure I’ll jump into the fray given that this is exactly what I’ve been working on with Hyperspaces.

There are two things you have to understand up front:

There is a notification “com.apple.switchSpaces” posted when a user chooses to switch spaces via the menu extra. This notification can also be (ab)used to switch spaces using your app as well — however…

There is no other notification that your active space has changed, and it is only posted by the menu extra. You need to check for changes from the CoreGraphicsServer — the easiest way to do that is as follows. It’s up to you to write the code that uses this, but attaching it to a simple NSTimer would probably work just fine.

-(NSNumber*)CGSSpaceNumber {
  int spaceNumber = -1;
  CGSGetWorkspace(_CGSDefaultConnection(), &spaceNumber);
  return [NSNumber numberWithInt: (spaceNumber - 1)];
}

I use this method in Hyperspaces, and it works just fine for my purposes (with the requisite delay associated with my application having to scan for changes). Apple’s Eric Schlegel explains the reasoning behind why there is no notification on the cocoa-​dev list. Long story short, it sounds like Apple’s not quite done developing Spaces yet, and they don’t want people relying on APIs that aren’t finalised.

Comments

Gravatar for Steve Voida.

Actu­ally, there is a way to do this a bit more ele­gantly (i.e., no polling), but it’s yet another layer of undoc­u­mented API hack­ery. Then again, that’s what’s got­ten us this far, isn’t it?

The Spaces menu extra uses an inter­nal call­back hook to ask the win­dow man­ager to poke it when some­thing hap­pens with Spaces. I haven’t puz­zled out all the details about what’s being sent back and forth, but this proof-​of-​concept code shows how you can at least receive auto­matic noti­fi­ca­tions with­out hav­ing to con­tin­u­ally ask for them.

--------------------
SpacesListener.h
--------------------

// Internal CoreGraphics typedefs

/* Connection ID between the app and the window server (from CGSPrivate.h). */
typedef int CGSConnection;

/* Prototype for the Spaces change notification callback.
 *
 * data1 -- returns whatever value is passed to data1 parameter in CGSRegisterConnectionNotifyProc
 * data2 -- indeterminate (always a large negative integer; seems to be limited to a small set of values)
 * data3 -- indeterminate (always returns the number '4' for me)
 * userParameter -- returns whatever value is passed to userParameter in CGSRegisterConnectionNotifyProc
 */
typedef void (*CGConnectionNotifyProc)(int data1, int data2, int data3, void* userParameter);


// Internal CoreGraphics functions

/* Get the default connection for the current process (from CGSPrivate.h). */
extern CGSConnection _CGSDefaultConnection(void);

/* Retrieve the workspace number associated with the workspace currently
 * being shown (from CGSPrivate.h). 
 *
 * cid -- Current connection
 * workspace -- Pointer to int value to be set to workspace number
 */
extern OSStatus CGSGetWorkspace(const CGSConnection cid, int *workspace);

/* Register a callback function to receive notifications about when the current Space is changing.
 *
 * cid -- Current connection
 * function -- A pointer to the intended callback function (must be in C; cannot be an Objective-C selector)
 * data1 -- indeterminate (this is hard-coded to 0x579 in Spaces.menu...perhpas some kind of event filter code?)
 * userParameter -- pointer to user-defined auxiliary information structure; passed directly to callback proc
 */
extern CGError CGSRegisterConnectionNotifyProc(const CGSConnection cid, CGConnectionNotifyProc function, int data1, void* userParameter);


// Demonstration class declaration

@interface SpacesListener : NSObject {
}
- (void)displayCurrentSpace;
@end


--------------------
SpacesListener.m
--------------------

#import "SpacesListener.h"

// C-language callback function

/* Callback function used to notify app when the Space is changing.
 *
 * data1 -- returns whatever value is passed to data1 parameter in CGSRegisterConnectionNotifyProc
 * data2 -- indeterminate (always a large negative integer; seems to be limited to a small set of values)
 * data3 -- indeterminate (always returns the number '4' for me)
 * userParameter -- returns whatever value is passed to userParameter in CGSRegisterConnectionNotifyProc
 */ 
void spacesCallback(int data1, int data2, int data3, void* userParameter) {
    // Re-construct an instance of the calling class instance and call back into it
    SpacesListener* listenerProxy = (SpacesListener*)userParameter;
    [listenerProxy displayCurrentSpace];
}


// Demonstration class implementation

@implementation SpacesListener

- (id)init {
    self = [super init];
    if (self) {
        // Register our Spaces callback. Not sure why '1401', but that's what Spaces.menu does;
        // userParameter is used to pass along a pointer to this SpacesListener class instance.
        CGSRegisterConnectionNotifyProc(_CGSDefaultConnection(), spacesCallback, 1401, (void*)self);
    }
    return self;
}

- (void)displayCurrentSpace {
    // Get the current space ID
    int spaceID = 0;
    CGSGetWorkspace(_CGSDefaultConnection(), &spaceID);

    // Display it to the console
    if (spaceID == 65538)
        NSLog(@"transitioning (or in overview)");
    else
        NSLog(@"switched to space number %d", spaceID);
}

@end

Thanks for all your awe­some work on VirtueDesk­tops – I wouldn’t have been able to com­plete my dis­ser­ta­tion research with­out it! I’m also look­ing for­ward to see­ing what comes of the new Hyper­Spaces project. Any chance you’d be will­ing to share your new code­base for some aca­d­e­mic research & development?

Cheers,
Steve Voida
Uni­ver­sity of Calgary

Posted by Steve Voida on

Gravatar for Tony Arnold.

Hi Steve — whether or not that works, you’ve earned your­self free licenses for Hyper­spaces for life. Thanks for point­ing out that API — I wouldn’t have even looked for it to be honest!

I’m going to be sell­ing Hyper­spaces as a commercial/​shareware prod­uct, but I’m happy to talk about my imple­men­ta­tion — we can prob­a­bly fig­ure some­thing out. Shoot me an e-​mail pri­vately and we’ll talk further.

Posted by Tony Arnold on

Gravatar for Brian Dono­van.

Any chance of beta-​testing Hyper­spaces? I ran into this post after search­ing for “cocoa space change noti­fi­ca­tion”, since that’s the only way I could think to sanely imple­ment a “labels for spaces” fea­ture, which seems to be exactly what you’re doing with Hyperspaces.

Posted by Brian Dono­van on

Gravatar for Tony Arnold.

Hi Brian — I’m actu­ally only a few days from releas­ing ver­sion 1.0 pub­licly, so there wouldn’t be much point right now. Thanks for the offer though — make sure you sign up over at <http://​projects​.the​co​coabots​.com/> to be involved in future beta releases.

Posted by Tony Arnold on

Gravatar for Paul.

In 10.6, NSWorkspace now has the following notification: NSWorkspaceActiveSpaceDidChangeNotification.

Posted by Paul on

Sorry, this conversation has finished.

This post is a bit old now, so I've closed the conversation. If you're keen to keep talking about it, please email me directly.