This blog has moved to rhult.github.com
Monday, October 12, 2009
After trying out blogspot/blogger for a while, I realized it is not ideal for a code blog. Micke pointed me to the solution created by Tom Preston-Werner, which looked perfect for my needs. Simple and very flexible, and since I sit all day in front of my terminal typing git commands, the work flow is also very familiar ;)

Tom's post about the motivation for creating Jekyll (the site generator used to power the blog) is spot on: Blogging Like a Hacker.

So, I have moved over the posts from here to my new blog, and new posts will only appear there. See you!

Getting HideOnLaunch right

Tuesday, June 30, 2009
In a previous post I described how to set up an app to launch automatically on login. After posting that, I've noticed that it seems like the "Hide on launch" property often seems to be mixed up with the kLSSharedFileListItemHidden key (for an example, see this thread from 2008 on Cocoa-dev).

So in an attempt to improve chances of people finding the right answer when searching for the incorrect key kLSSharedFileListItemHidden, here is the relevant snippet again:

NSDictionary *properties =
[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES]
// NOTE, notice the key name:
forKey:@"com.apple.loginitem.HideOnLaunch"];

LSSharedFileListItemRef itemRef =
LSSharedFileListInsertItemURL(loginItemsListRef,
kLSSharedFileListItemLast,
NULL,
NULL,
(CFURLRef)bundleURL,
(CFDictionaryRef)properties,
NULL);
...

Making an NSSlider snap to tick marks

Tuesday, May 26, 2009
Have you noticed how the sliders in the energy savings panel in the system preferences behave a little differently from your average slider? They have this nice touch of resistance when you drag the slider across a tick mark, making it easy to end up exactly on a mark. I wanted the same behavior for a slider in my app, and found out that it isn't builtin in the slider. The way I solved it was by connecting an action to the slider, where I manually restrict the value a bit:

- (IBAction)sliderValueChanged:(id)sender
{
double range = [sender maxValue] - [sender minValue];
double tickInterval = range / ([sender numberOfTickMarks] - 1);

double relativeValue = [sender doubleValue] - [sender minValue];

// Get the distance to the nearest tick.
int nearestTick = round(relativeValue / tickInterval);
double distance = relativeValue - nearestTick * tickInterval;

// Change the check here depending on how much resistance you
// want, or if you don't want it to depend on the tick interval.
if (fabs(distance) < tickInterval / 8) {
[sender setDoubleValue:[sender doubleValue] - distance];
}
}


Then you have to make the slider continously perform the action when moved, instead of just when releasing it. This can be done either by checking the "Continous" check button in Interface Builder or programmatically using:

- (void)setContinuous:(BOOL)flag


on the slider instance.

There might be a better to do this, if anyone knows about it I'm all ears :)

Keyboard shortcuts in Xcode and Interface Builder

Wednesday, April 29, 2009
If you, like me, are trying to avoid using the mouse (for ergonomic reasons) as much as possible, you probably already have noticed that the Mac is quite alright when it comes to keyboard accessibility. This includes Xcode and Interface Builder, even though the latter by nature requires quite a bit of mouse wrestling. There are also some nice features that can help you having to type less.

Recently, I have been trying to collect the most useful keyboard shortcuts and really learn them so I'm not tempted to use the mouse more than necessary. Here's the list so far:

Xcode
Besides all the normal text editing shortcuts, I often use those:

(⇧ = shift, ⌘ = cmd, ^ = ctrl, ⌥ = option)


  • ⇧⌘E = "Zoom" the editor, hide the list above it

  • ⇧⌘D = Open quickly, very useful for quickly open any file, in the project our elsewhere

  • ^⌘T = Edit all in scope, this saves a lot of tedious editing

  • ^/ = Next placeholder in completions

  • ^. = Toggle between completions

  • ⌥⌘-Up = Toggle between the header/source

  • ^1 = Pop up the file history menu, useful when navigating the project files


The shortcut ^. deserves some extra attention, as it also completes text macros which can save a lot of typing. There is a whole slew of macros that you can use, just a few examples:

  • pim = expands to #import "file", highlighting file so you can change it easily

  • a = expands to the standard alloc/init combination

  • init = expands to a standard init skeleton

  • dealloc = expands to the standard dealloc skeleton



Interface Builder
Interface builder also has a few ones I often use:

  • ⌘ while resizing a window = live autoresizing

  • ^⌘ + up/down = select parent/child of selected view

  • ^⌘ + left/right = select previous/next sibling



Global Shortcuts
Finally I have a tiny list of desktop wide shortcuts (obviously in addition to the well known ones like ⌘-Tab etc) I sometimes find useful:

  • ^F2 = Focus the application menu

  • ... I said it's tiny!



I hope this can be useful for others as well.

Adding a preference to launch your app on login using the Shared File List API

Thursday, April 16, 2009
I wanted to make my app launch automatically on login, and looked around for ways to do that programmatically. I first found two ways to do it, both a little bit less than ideal: either using Apple Events to talk to System Events, or by using NSUserDefaults (or CFPreferences) to tweak values in the persistent domain "loginwindow" (as opposed to the app's own persistent domain where the preferences for the app are stored). The Apple Events way seemed a bit hackish to me, as did the NSUserDefaults way, considering it meant poking at preferences you don't "own".

Nonetheless, I decided to try the latter, so I went ahead and implemented a simple controller that exposed only one property that I could then bind to a check box button in Interface Builder using Cocoa bindings:

@property BOOL launchOnLogin;


To show more clearly what I did, here's the part that reads the current login items:

NSUserDefaults *defaults;
NSDictionary *domain;

defaults = [NSUserDefaults standardUserDefaults];

domain = [defaults persistentDomainForName:@"loginwindow"];
if (domain) {
NSArray *items = [domain objectForKey:@"AutoLaunchedApplicationDictionary"];

for (NSDictionary *entry in items) {
NSString *entryPath = [entry objectForKey:@"Path"];
// ... do something with the entry ...
}
}


The writing part is similar, getting a copy of the existing array, then adding or removing our own item and then setting as the new array:

[domain setObject:updatedItems forKey:@"AutoLaunchedApplicationDictionary"];

// Update the setting.
[defaults setPersistentDomain:domain forName:@"loginwindow"];

// Write changes to disk so the system settings will be up to date
// if opened before the changes are automatically synced.
[defaults synchronize];


While this approach works in general, there's a problem: the login items list in the system settings panel is not updated when changing the setting in my app's preferences, and the other way around. Maybe not a big problem, but it made me search some more, which led me to the following note in the release notes for the Launch Services framework in Leopard:

The Shared File List API is new to Launch Services in Mac OS X Leopard. This API provides access to several kinds of system-global and per-user persistent lists of file system objects, such as recent documents and applications, favorites, and login items. For details, see the new interface file LSSharedFileList.h.


The mentioned header file is the only documentation for this new API, but it is quite straight-forward: you create an instance of the right list, in this case the login items list for the session. Then there is a bunch of things you can do with the list, what's interesting for our use case is to get a copy of the array in the list, to add or remove an item, or to register an observer callback for a list. The callback is called when the list changes, which makes it possible for the UI to update accordingly.

Since my app already depends on Leopard, I decided to rewrite my code using LSSharedFileList. The UI was already set up to use the simple controller described above, so a quick rewrite of the controller was enough. I create the list in my init method and add an observer:

loginItemsListRef = LSSharedFileListCreate(NULL,
kLSSharedFileListSessionLoginItems,
NULL);
if (loginItemsListRef) {
// Add an observer so we can update the UI if changed externally.
LSSharedFileListAddObserver(loginItemsListRef,
CFRunLoopGetMain(),
kCFRunLoopCommonModes,
loginItemsChanged,
self);
}


The matching tearing down is done in dealloc:

if (loginItemsListRef) {
LSSharedFileListRemoveObserver(loginItemsListRef,
CFRunLoopGetMain(),
kCFRunLoopCommonModes,
loginItemsChanged,
self);
CFRelease(loginItemsListRef);
}


The callback for the observer is a plain old C funtion:

static void
loginItemsChanged(LSSharedFileListRef listRef, void *context)
{
LoginItemsController *controller = context;

// Emit change notification for the bindnings. We can't do will/did
// around the change but this will have to do.
[controller willChangeValueForKey:@"launchOnLogin"];
[controller didChangeValueForKey:@"launchOnLogin"];
}


The rest of the code just needs to check if our app bundle is listed in the login items list in the getter for the property, and add it or remove it in the setter:

// Get an NSArray with the items.
- (NSArray *)loginItems
{
CFArrayRef snapshotRef = LSSharedFileListCopySnapshot(loginItemsListRef, NULL);

// Use toll-free bridging to get an NSArray with nicer API
// and memory management.
return [NSMakeCollectable(snapshotRef) autorelease];
}

// Return a CFRetained item for the app's bundle, if there is one.
- (LSSharedFileListItemRef)mainBundleLoginItemCopy
{
NSArray *loginItems = [self loginItems];
NSURL *bundleURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];

for (id item in loginItems) {
LSSharedFileListItemRef itemRef = (LSSharedFileListItemRef)item;
CFURLRef itemURLRef;

if (LSSharedFileListItemResolve(itemRef, 0, &itemURLRef, NULL) == noErr) {
// Again, use toll-free bridging.
NSURL *itemURL = (NSURL *)[NSMakeCollectable(itemURLRef) autorelease];
if ([itemURL isEqual:bundleURL]) {
CFRetain(item);
return (LSSharedFileListItemRef)item;
}
}
}

return NULL;
}

- (void)addMainBundleToLoginItems
{
// We use the URL to the app itself (i.e. the main bundle).
NSURL *bundleURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];

// Ask to be hidden on launch. The key name to use was a bit hard to find, but can
// be found by inspecting the plist ~/Library/Preferences/com.apple.loginwindow.plist
// and looking at some existing entries. Thanks to Anders for the hint!
NSDictionary *properties;
properties = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES]
forKey:@"com.apple.loginitem.HideOnLaunch"];

LSSharedFileListItemRef itemRef;
itemRef = LSSharedFileListInsertItemURL(loginItemsListRef,
kLSSharedFileListItemLast,
NULL,
NULL,
(CFURLRef)bundleURL,
(CFDictionaryRef)properties,
NULL);
if (itemRef) {
CFRelease(itemRef);
}
}

- (void)removeMainBundleFromLoginItems
{
// Try to get the item corresponding to the main bundle URL.
LSSharedFileListItemRef itemRef = [self mainBundleLoginItemCopy];
if (!itemRef)
return;

LSSharedFileListItemRemove(loginItemsListRef, itemRef);

CFRelease(itemRef);
}

#pragma mark Property accessor methods
- (BOOL)launchOnLogin
{
if (!loginItemsListRef)
return NO;

LSSharedFileListItemRef itemRef = [self mainBundleLoginItemCopy];
if (!itemRef)
return NO;

CFRelease(itemRef);
return YES;
}

- (void)setLaunchOnLogin:(BOOL)value
{
if (!loginItemsListRef)
return;

if (!value) {
[self removeMainBundleFromLoginItems];
} else {
[self addMainBundleToLoginItems];
}
}


That's all! Not only does this give you synchronization between your preferences panel and the system settings, but also feels more correct than poking at a different application's or subsystem's persistent domain.

Note: There is also a "seed" for each list that can be used to check if the list has changed. Using that you can make sure that any change notification is not emitted unless necessary (see LSSharedFileListGetSeedValue()). The example above doesn't do that, so there will be notification sent out when toggling the setting from the app's preferences.