One of the challenges of developing Mac software, particularly for an audience that is generally more technical than the average user, is that your users compare your application to others that are out there on the basis of how they utilize the rich set of technologies available on Mac OS X. And I'm not just talking competitors here, but any Mac application. This can be both a blessing and a curse, as it can inform you of and inspire you to add great new features, but can also cast a seemingly bad light on your own product if you are unable or unwilling to implement certain functionality. Users often don't care why you might not want to or can't implement features which are, in your own estimation, not the "right" way of doing something -- at least not right now. Other apps are doing it, why can't you?
I'm hoping to provide some insight into my thought process on making these judgement calls and talk about a specific issue that I've been working on in this department. I'll get a bit technical, but I'll try to explain along the way for the non-programmers in the room. And I'll mention a couple of other apps by name that handle the same issue in different ways. I don't mean to call out or embarrass anyone, and all the apps that I mention are ones that I use (and love) on a daily basis, but I want to show what other developers are doing. Lastly, I have an overriding goal here to be a somewhat exhaustive resource for this particular issue for other programmers to find, too.
What's up, dock?
A big feature request that's been hanging around for some time for Pukka is the ability to run without a dock icon. For a utility with a small footprint like Pukka, this makes a lot of sense for a number of reasons:
- As of Pukka 1.6, a status bar menu provides omnipresence for all of the app's "background features" -- looking up bookmarks and visiting your links online.
- Pukka is not visible when you are not using its "foreground features" -- posting bookmarks -- anyway. Some users would like to not have to think about it by seeing it in the dock and task switcher.
- Pukka has long supported a "quit after posting" feature which quits the app entirely after a post is made. Since you can call Pukka via your RSS reader or the browser bookmarklet and since it's lightweight and starts quickly, Pukka can come and go only when you need it. People who like an uncluttered dock really appreciate this, though some people would rather have Pukka running all of the time, but still not in the dock. After all, lingering invisibly in the background is faster than starting up on demand.
- "All the other apps are doing it!" :-) Apps that I use that have a user-configurable dockless mode include Twitterrific, MenuCalendarClock, Knox, BluePhoneElite, SSHKeychain, and iShowU. There are many more that do as well.
I should make a distinction here between applications like Lighthouse, Macaroni, and Growl (which I also use) which always run dockless, since these apps have alternate interfaces -- typically robust status bar menus or preference panes -- that allow use of the application. I'm talking about applications that let you choose on a whim whether you'd like to run without a dock icon or not -- defaulting, of course, like normal applications, to running with a dock icon.
Users who are used to dockless apps know the downsides -- no application switching, the app doesn't show up in the Force Quit menu, and sometimes there are window focus issues -- and are willing to accept them as limitations in Mac OS X. But there are some issues on the developer side that make this feature more difficult than one might think at first pass.
How do you work this thing?
The only real way* to make an application run dockless is to change a setting in a file inside of its application bundle -- though a variation on this exists, as I'll describe in a little bit.
* I do know about TransformProcessType, a Carbon call, which can go from dockless mode to having a dock icon, but this has to happen too early in the launch process to read a user's preference setting, plus it's only a one-way transition.
A Mac application is actually a folder named <application name>.app containing a number of files and folders, one of which is called Info.plist. This property list file contains entries such as the application's name, version, copyright info, and other info used by the system to query what the application is all about.
If you add an entry to an application's Info.plist called LSUIElement
and set its value to a string containing the number one, relaunching the application will make it go dockless. There are even applications like Dockless and Dock Dodger that will do this for you for a given application. Most of the applications that I mention above use this method to go dockless.
However, I've long preferred not to do this for a couple reasons:
- It's bad form for an application to modify itself -- though I realize it's not the end of the world.
- If the user is not an admin or if the application is located someplace like a network-mounted share, the user will not be able to -- and shouldn't be able to -- modify the app bundle that is shared by other users or even other computers.
- Lastly -- and this is the big one -- Apple introduced code signing in Leopard and, according to the documentation, it's only a matter of time before this method of altering the Info.plist will render the application unusable.
With code signing and Info.plist modification on the fly, the writing is on the wall:
A seal, which is a collection of checksums or hashes of the various parts of the program, such as the identifier, the Info.plist, the main executable, the resource files, and so on. The seal can be used to detect alterations to the code and to the program identifier. #
Various components of the application bundle (such as the Info.plist file, if there is one) are also signed. #
Your code must be immutable once signed. After signing, do not attempt to change executable code (including symbol tables), the Info.plist, or your program’s resource files. Do all that before signing. If you make modifications after signing, the signature may be invalid. #
And according to Apple, you should be signing code now. Not doing so can interfere with the keychain user experience, Parental Controls, firewall, and other integrated systems in Leopard that rely on code signing.
So, where does that leave us?
When I talked to Marko Karppinen, whose company makes the excellent Knox security application, at last summer's C4 in Chicago, he let me in on how he accomplished a user-specified dockless setting in that application.
If you look at the application bundle, it contains another copy of the application within a subfolder of the app bundle itself -- sort of a Russian Doll. But if you look closer, you'll see that all of the components of this "inside" application are actually symbolic links to the parent application's version, except for the Info.plist file. And -- you guessed it -- the internal Info.plist has the LSUIElement
set. For the curious, here is a shell script that I came up with to be used as an Xcode build phase to accomplish this.
I'm actually using this method successfully with Meerkat, my next application (which is currently in private beta) and it's working ok for now. On launch, the application checks to see if if the user's preference setting matches the setting of the version being launched and if not, it launches the other one. Pretty sweet, right?
It is, except for one thing in Pukka's case -- Apple Event targeting. Besides being launched normally, Pukka can be launched by a number of external forces, including:
- AppleScripts that interact with Pukka
- Posted information from RSS readers like NetNewsWire using its author's External Weblog Editor Interface
- Pukka's bookmarklet, which actually uses a custom pukka:// protocol to pass information
- Opening http:// and https:// URLs with Pukka like you would with an alternate web browser
- Opening a supported file, such as a .webloc on the desktop, a Pukka license file, or one of the Spotlight files that Pukka generates for easy access to your bookmarks via system searching
On launch, not only do I have to figure out if the right version is running, but if not, I have to pass whatever information actually launched the application to a new process. I've actually managed to accomplish this for everything but AppleScript, but it's still non-ideal. It comes down to the fact that it's just generally bad practice to have more than one copy of the same application on the same hard drive because Mac OS X's Launch Services can get confused as to which one it should be targeting.
On top of that, since Pukka uses Sparkle for auto-updating, some issues needed to be resolved in order to update the outer, docked application and not the internal, dockless one, even if the internal one is the one running the update. Overwhelmed yet?
Now what?
So right now, I'm at a loss as to how to proceed. Apple doesn't provide a tried-and-true way to let the user toggle an application's dock mode and although many applications implement this, it seems to me that no way is fool-proof nor future-proof. The last thing I'd want is to release the feature but have to revoke it later from paying customers because it's untenable.
Cocoa developers, anything obvious that I'm missing? Have you tackled these challenges? Are you thinking about adding the same feature to your application?
Trackback URL for this post:
- Login to post comments
offer two downloads for your app. one that is displayed in the dock, the other that does not.
@Lee: Thanks for the post. I've considered this too, but it opens up the possibility of differences creeping up between the versions that could complicate the support of Pukka more than the feature is worth (either intended because certain things wouldn't be possible on both versions, or unintended because of bugs). Also, it would mean that I have to package separate versions for Sparkle to use as well and keep that distinction between the two versions. Lastly, people would read about the feature on the site but perhaps wouldn't download the right version and that would also complicate things. It's not out of the question, but it seems to me a non-ideal solution as well.
I had thought you could set LSUIElement using "defaults write..." but it seems I was mistaken.
The question of Apple Events, etc, probably depends on how you launch the hidden pseudo-app bundle. Higher-level approaches like telling NSWorkspace to open it probably mean you need to pass everything on yourself. A lower-level approach like execve(), targeting the binary directly, might mean you could just wait until after execve() to read all that information, and it'd all just arrive. This is an untested idea, though.
@Tom: You can use
defaults
to write to the app bundle (as I do in the Xcode build script) but not to the user defaults. Or, rather, it doesn't have any effect. I wish it could go into the defaults :-/I'm pretty open in how I target the binary. I should mention that it's kind of unpredictable as to which version gets opened, though -- it's not always the outer one. It's kind of up to the whim of Launch Services. Regardless, though, the launched version can figure out which one it is and launch the other. I'm currently using NSTask to launch the binary directly, but I could use NSWorkspace,
/usr/bin/open
,execve()
, etc. Regardless, I'm still not seeing how to get the context of the current Apple Event to pass it along. For file opening and the NNW posting protocol, I have methods that are called, so that's been easy, but for AppleScript, either my NSScriptHandler subclass'sperformDefaultImplementation
is called (for commands) or my app delegate'sapplication:delegateHandlesKey:
is consulted (for getters/setters) but I'm not able to grasp the event to pass it along. Even if I could, I've tried passing dummy events and the Script Editor running the script seems to get confused and loses the connection with the app when the other version is launched.To tell you the truth, I have avoided buying Pukka because it does not run dockless. I try a new version out every few months in the hope that it's added, but to me having a docked Pukka is a plague. I have absolutely no interest in it wasting space on my dock. I absolutely hate seeing it there. I never swap to it anyway - it's default window is just a blank entry screen - who would want to swap to that....
So once you figure out how to go dockless, I'll buy the app! There are so many dockless apps out there. I understand from your post that the issue isn't as straightforward as you would wish, but don't stop me from going dockless now because in the future Apple might change something... Or because of Applescript. I'd like to use Pukka in two ways: a) from netnewswire, b) as a bookmarklet. I'm sure 95% of your users do the same. Save us all from tearing our hair out :-) Please!
@Jon: Thanks for your feedback. One thing that you can do right now to run Pukka dockless is to quit the app, copy and paste the following into a Terminal window, hit Return, then relaunch Pukka:
defaults write /Applications/Pukka.app/Contents/Info LSUIElement 1
If you were to then want to go back to dock mode, you'd use this:
defaults write /Applications/Pukka.app/Contents/Info LSUIElement 0
i.e., the same thing but with a zero instead of a one.
This is the method mentioned above of altering the app bundle itself, which is the method that some apps use to go dockless, but the one that will eventually mess with code signing.
Thanks Justin! I'm buying the app right now...
Very interesting article! I believe I have succeeded in overcoming all the difficulties that you describe (and created a new one, but it seems to be a very minor one, see below). The trick is to set LSUIElement=1 in the Info.plist, then if on initialization the app wants to become an ordinary foreground application, use TransformProcessType(), which causes the dock icon to display, followed by a call to SetSystemUIMode(kUIModeNormal, 0), which re-enables the menu bar. This almost solves the problem completely. The only remaining difficulty is that the menu bar is still not visible until you switch to a different app and then switch back. In the code I wrote I overcame this by switching to the dock and back, resulting in the following somewhat ugly hack:
#import
// this should be called from the application delegate's applicationDidFinishLaunching
// method or from some controller object's awakeFromNib method
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"LaunchAsAgentApp"]) {
ProcessSerialNumber psn = { 0, kCurrentProcess };
// display dock icon
TransformProcessType(&psn, kProcessTransformToForegroundApplication);
// enable menu bar
SetSystemUIMode(kUIModeNormal, 0);
// switch to Dock.app
[[NSWorkspace sharedWorkspace] launchAppWithBundleIdentifier:@"com.apple.dock" options:NSWorkspaceLaunchDefault additionalEventParamDescriptor:nil launchIdentifier:nil];
// switch back
[[NSApplication sharedApplication] activateIgnoringOtherApps:TRUE];
}
As far as I can tell, beyond the code's ugliness its only shortcoming is that it might produce a strange result for users who have disabled the Dock application and use other window management programs like Desktop Manager. Sounds like a small price to pay, though, considering the problems with the other approaches you describe in the article.
@Dan -- this is an interesting approach; I'll have a look at it! But I suspect it might be too much for me, as I really want Pukka to launch quickly. I'll have to see what the speed of it is like.
2 apps
Perhaps you could build 2 apps -- 1 dockless app with status bar icon, and 1 "normal" app. The dockless app would launch the normal one when a GUI is required.
@sreitshamer: I had
@sreitshamer: I had considered that, and in fact am doing something like that with the current betas of Meerkat. I've got the normal process, either docked or dockless, and a small bundled application inside called Meerkat Scripting. This application is the true AppleScript-enabled one, and uses distributed objects to talk to whichever main process is running (launching it if necessary) to dispatch the commands and requests for information.
So far it's working ok, but would likely run into issues in an environment that needs fast performance available to scripting (which Meerkat does not). But I figure any such sort of application user base will rather opt for performance than a dockless mode :-)
some examples
Actually I've been looking for examples of this, and found 2: Mozy and iTunes.
Mozy.app contains 4 other .app packages within its Contents/Resources directory, including "Mozy Status.app" which is a dockless app with status menu item. Selecting "Configure Mozy" from the dockless app's menu launches Mozy.app, a docked app. Seems to work pretty smoothly.
iTunes does something similar, hiding its iTunesHelper.app within its Contents/Resources directory.
Cool, I will check out Mozy.
Cool, I will check out Mozy. And I believe that for iTunes, all that the helper program does is handle auto-launches for iPhones and iPods and the like.
Doesn't work for me
The com.apple.dock hack doesn't seem to work for me. Actually, activating the dock seems to help in so far as afterwards the user only has to switch to your app (instead of switching away THEN switching back again). But the activateIgnoringOtherApps doesn't actually make it front.
I tried a bunch of other stuff too: SetFrontProcess, and various manner of other things, sleeping in various places and stuff, but I can't get this to work. Any tips?
BTW, this code below seems to do the same thing as the actual com.apple.dock line of code, as far as switching the front process away, and doesn't rely on the dock, which seems preferable:
ProcessSerialNumber psnx = { 0, kNoProcess };
GetNextProcess(&psnx);
SetFrontProcess(&psnx);
Maybe this?
Ahh, to answer my own question, this incantation seems to work for me:
-(void)setFront; {
ProcessSerialNumber psn = { 0, kCurrentProcess };
SetFrontProcess(&psn);
}
ProcessSerialNumber psn = { 0, kCurrentProcess };
OSStatus returnCode = TransformProcessType(& psn,
kProcessTransformToForegroundApplication);
ProcessSerialNumber psnx = { 0, kNoProcess };
GetNextProcess(&psnx);
SetFrontProcess(&psnx);
[self performSelector:@selector(setFront) withObject:nil afterDelay:0.0];
@Dan: Excellent solution
@Dan:
Excellent solution. It works for me. Thanks!
And for other readers' convenience. The first line code should be:
#import <Carbon/Carbon.h>
surrounded by less than symbol and greater than symbol, but they are eaten by the browser after posting, and in Xcode, project should add Carbon.framework.
Workaround for menu bar
I was trying out this solution and found some issues where the dock application wasn't updating the menu bar until I had clicked on the dock icon. It turns out that I'm not the only one. here is a reference that might be useful.
The code from that page seems to work best when inserted into the setFront method (called after the delay). I found that we needed both the AppleScript and the setFrontProcess.
-(void)setFront;
{
// Work around rdar 5599887 http://www.mail-archive.com/cocoa-dev@lists.apple.com/msg14367.html
NSAppleScript *script = [[[NSAppleScript alloc] initWithSource:@"tell application \"System Events\" to name of first process whose frontmost is true"
] autorelease];
// Run the script
NSDictionary *errorInfo = nil;
NSAppleEventDescriptor *scriptResult = [script executeAndReturnError:&errorInfo];
if (errorInfo) {
NSLog(@"%@: %@", NSStringFromSelector(_cmd), errorInfo);
}
if (scriptResult && [scriptResult descriptorType] != typeNull)
{
NSString *currentApp = [scriptResult stringValue];
ProcessSerialNumber psn = { 0, kCurrentProcess };
OSStatus returnCode = TransformProcessType(&psn, kProcessTransformToForegroundApplication);
if( 0 == returnCode)
{
[[NSWorkspace sharedWorkspace] launchApplication:currentApp];
// this step is necessary - if left out the menubar is not updated
script = [[[NSAppleScript alloc] initWithSource:
[NSString stringWithFormat:@"tell application \"%@\" to activate", currentApp]
] autorelease];
(void) [script executeAndReturnError:&errorInfo];
// [NOTE: SetFrontProcess() doesn't work]
if (errorInfo) {
NSLog(@"%@: %@", NSStringFromSelector(_cmd), errorInfo);
}
}
}
ProcessSerialNumber psn = { 0, kCurrentProcess };
SetFrontProcess(&psn);
}