Not three minutes after sending out the first Monocle danger build, I got a complaint about how it doesn’t work well with Spaces. I had made a mental note earlier in the day of fixing this before sending out the build, but it had gotten swapped out. This presents a problem.
You can specify how a window is supposed to behave when using Spaces. You do this by setting a collection behavior. Your choices are:
- Default. Stay on one space like every other window.
- Join all spaces. Stay on all spaces simultaneously. (The menu bar does this and never moves.)
- Move to active space. The window will move between spaces as you switch spaces. As you switch between this window and a window technically only in the current space, you will stay in the current space.
These choices are integer constants in an enum. There’s no porting implications here, but to actually set the behavior, you have to call a method. Objective-C is dynamic and you never call methods, you “send messages”. This is all well and good, but if your project is set to treat warnings as errors (and Monocle is – keeps me honest), it becomes a problem. And if you add the methods via a category, then there’ll be more warnings when you start linking to the 10.5 SDK instead.
So, since Objective-C is so dynamic, let’s move this from compile-time (where you can get warnings thrown at you) to run-time (where the worst thing that can happen is that a method doesn’t exist). We do this by building an invocation and invoking it. Update: Or you can correctly implement what I planned yesterday at 3 in the morning but didn’t get to work. See the comments. The rest of this post isn’t going nowhere, though – it’s factually correct and shows how to construct invocations.
So let’s look at the method signature: - (void)setCollectionBehavior:(NSWindowCollectionBehavior)collectionBehavior. If we dig deeper into the NSWindowCollectionBehavior, we’ll see that it is, in fact, like all enums in 10.5, due to 64-bit considerations an anonymous enum and a typedef where the enum “name” is defined to have the type of the size of the enum – in this case NSUInteger. NSInteger and NSUInteger are two types defined with macros to be the 64-bit (unsigned) long when building 64-bit and the 32-bit (unsigned) int when building 32-bit. Because they are used in places like -[NSArray count], this means that arrays in 64-bit apps can automatically hold more items. The Foundation release notes has more on this.
The NSInteger macros are short and well-suited to directly copy and paste, so let’s do this:
#ifndef NSINTEGER_DEFINED
#if __LP64__ || NS_BUILD_32_LIKE_64
typedef long NSInteger;
typedef unsigned long NSUInteger;
#else
typedef int NSInteger;
typedef unsigned int NSUInteger;
#endif
#define NSINTEGER_DEFINED 1
#endif
Thanks to the NSINTEGER_DEFINED guard, you can even switch to the 10.5 SDK now and build and you won’t get any multiple definition issues.
To build an invocation, you need an existing selector known to the Objective-C runtime to have the same type. Therefore, we insert an innocuous noop — - (void)selectorForNSWindow_setCollectionBehavior:(NSUInteger)behavior { } — that has the same signature as the real method. Then you can make a method to simply build the invocation and invoke it:
- (void)prepareWindow:(NSWindow *)window
forSpacesUsingCollectionBehavior:(NSUInteger)mode {
BOOL allocatedBuffer = NO;
NSUInteger *bufferPtr = NULL;
@try {
NSMethodSignature *sig = [self methodSignatureForSelector:
@selector(selectorForNSWindow_setCollectionBehavior:)]
NSInvocation *setSpacesBehaviorInvocation =
[NSInvocation invocationWithMethodSignature:sig];
[setSpacesBehaviorInvocation setSelector:@selector(setCollectionBehavior:)];
[setSpacesBehaviorInvocation setTarget:window];
bufferPtr = (NSUInteger *)malloc(sizeof(NSUInteger));
allocatedBuffer = YES;
*bufferPtr = mode;
[setSpacesBehaviorInvocation setArgument:bufferPtr atIndex:2];
[setSpacesBehaviorInvocation invoke];
} @catch (NSException *ex) {
;
} @finally {
if (allocatedBuffer) {
free(bufferPtr);
}
}
}
The reason argument 2 is being set is because every Objective-C method implementation takes two arguments when being called by the runtime – id self and SEL _cmd. The exception handling is there because if this runs on 10.4, an exception will be thrown since we’re invoking something that’s not already there.
Using this method, you can then put any NSWindow in any mode you’d like.
Don’t you think it’s simpler to simply do
? To remove the warning you get when compiling this, you can do something like
@interface NSWindow (LeopardOnly) put method signature here @end
By http://thakis.myopenid.com/ · 2007.12.29 20:04
Yes, it is simpler, but I didn’t get any variation of that to compile (and believe you me that I tried a few before writing this code), since that yields a warning when I compile with the 10.5 SDK. The solution is proposed such that you can link it both to 10.4 and 10.5 and it works equally well.
By Jesper · 2007.12.29 20:23
Unwarnification:
@interface NSWindow (LeopardMethods) - (void)setCollectionBehavior:(unsigned int)collectionBehavior; @end
endif
By http://jens.ayton.se/ · 2007.12.29 21:51
Right, except that it’s
unsigned longon 10.5 when compiling 64-bit, which is why I usedNSUInteger.(In my defense, I did code this at 3 in the morning, and the code was correct, I just didn’t think of the easy way. ;))
By Jesper · 2007.12.29 22:14
Yes, but with the #if that Jens suggested, that is irrelevant as the category declaration will never be compiled when build ing 64-bit (since 64-bit Cocoa is only available in the 10.5 SDK and later)
BTW, is there any reason that you’re not just using the 10.5 SDK and setting the deployment target to 10.4?
By http://clarkcox3.livejournal.com/ · 2007.12.30 03:22
I didn’t know you could legitimately do that.
By Jesper · 2007.12.30 03:23
Also, even if, for whatever reason, the other suggestions don’t work (though I can’t see why they wouldn’t), you can simplify your code greatly by not dynamically allocating your buffer:
NSMethodSignature *sig = [self methodSignatureForSelector: @selector(selectorForNSWindow_setCollectionBehavior:)] [setSpacesBehaviorInvocation setSelector:@selector(setCollectionBehavior:)]; [setSpacesBehaviorInvocation setTarget:window]; [setSpacesBehaviorInvocation setArgument: &mode atIndex:2]; [setSpacesBehaviorInvocation invoke]; }
By http://clarkcox3.livejournal.com/ · 2007.12.30 03:36
Put simply, the SDK defines the latest API’s that you wish to use, and the deployment target defines the earliest OS that you wish to run on. Anything in the 10.5 SDK that is not available on 10.4 will be weak-linked in this situation.
So, all it would take would be a (runtime) check that you are indeed on 10.5 before calling -setCollectionBehavior:.
See How SDK Settings Affect The Build for more info.
By http://clarkcox3.livejournal.com/ · 2007.12.30 03:41
Any reason you didn’t just use [setSpacesBehaviorInvocation setArgument:&mode atIndex:2] to eschew extra memory handleage? Seems like you’re using 2 extra variables, when just one or none would do. Not to nag, just wondering if there’s something I’m missing.
By n[ate]vw · 2007.12.30 04:41
You can indeed set the deployment target earlier than the SDK. Symbols tagged
AVAILABLE_IN_MAC_OS_X_10_5_AND_LATERwill then be weak-linked (so you have to test them against NULL before use), and obviously you need to keep track of which methods may not be available and check for them if you use them. Other problems can also crop up; for instance, NS_DURING etc. are always defined in terms of @try in the 10.5 SDK, so you can’t use them if you’re targeting 10.2 (shock, horror).I personally prefer to use the older SDK and get new symbols explicitly, and methods using the category approach, but both methods are viable.
As Clark says, my code is valid because it’s only used on 32-bit systems. I should have made this clear. (What I’m doing now in new/updated projects targeting Tiger or earlier is using my own parallel number type declarations that are equivalent to NSNumber.)
By http://jens.ayton.se/ · 2007.12.30 12:14
Nate: No, not really.
By Jesper · 2007.12.30 12:28