Building Custom OS X Dashboard Widgets

Multithreaded JavaScript has been published with O'Reilly!

This guide is meant to supplement the comprehensive (although limited) Apple Dashboard Reference page, which provides a lot of technical details, but lacks some of the important “between the lines” stuff. This guide will cover some tools to use, shortcomings, gotcha's, you name it. I've picked up these tips while developing the NeoInvoice Time Tracking Widget, along with development of some other smaller widgets.

The coolest thing about Dashboard Widgets is that they are built using HTML and JavaScript, which means that any web developer can build a widget. Each widget in the OS X Dashboard contains it's own DOM, supports most of the existing events you would expect, and is rendered using WebKit (or, more specifically, Safari based WebKit, I believe there are some Safari specific features you can use). A widget can have a predefined (and resizable), width and height. The background of the DOM is transparent by default, and using semi-transparent PNG's show the desktop as expected. AJAX works better than expected (no cross domain limitations), and you can even run command line applications (which can be distributed within the widget).

Tip #1: Developing in Browser

While developing your widget, you will want to build it in a web browser. Since the widget will be rendered using WebKit, you will want to use Google Chrome or Apple Safari to develop. Don't bother using Firefox, or trying to make the HTML render the same in multiple browsers. The built-in web developer tools (Cmd + Opt + I) will help you out a lot. Apple creates an object available to your Widget, appropriately named “widget“, which allows you to do some magic you can't normally do in a browser (e.g. execute commands, rotate widget animations, run an app or open a website, and save preferences [an alternative to cookies]).

This widget object, of course, is NOT available to your browser. You will need to keep this in mind while developing your Widget. You can use the following line of code to determine if you are in the browser or Widget mode, and either do or do not run functionality:

var isWidget = (typeof(widget) != "undefined");

Tip #2: Getting around Cross Site Request restrictions in your browser

A feature of modern browsers is that AJAX (XHR) requests are not able to access data in a separate domain. This prevents all sorts of bad things from happening. A Widget, however, doesn't have a domain and (if it's AJAX powered) will need to talk to a web service to get updates and send new data for a website. Since an author has a bit more control of a Widget than a website, it makes sense that the Widget doesn't require the same restrictions that a browser places on a website. Luckily for us, WebKit based browsers have a command line argument you can pass in to disable this feature.

Run one of the following commands in your Terminal (depending on which browser you prefer to develop on). Keep in mind that if you run other websites in the browser while developing, there are security risks involved.

/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --disable-web-security
/Applications/Safari.app/Contents/MacOS/Safari --disable-web-security

Tip #3: Info.plist caching

The XML document which controls a bunch of your Widget's attributes Info.plist, is cached rather aggressively by OS X. Or more specifically, certain pieces of it are (e.g. security settings). If you run your Widget once, and make a change to the plist file, it will remain in memory until a reboot (there should be a cached file to delete or a Terminal command to run to fix this, but I couldn't find it). So, try to set your options right the first time or be prepared for some hair pulling slowness.

Settings other than the security items would properly be reloaded each time a Widget was removed from the Dashboard and added again, such as the X and Y position of the close icon.

Tip #4: Running Command Line Scripts

As a Widget developer, you are allowed to package executable files within your .wdgt directory, which can be executed with the widget.system() function. These can do some of the heavy lifting which JavaScript isn't capable of, and your Widget can consume the output of the script. One caveat with this, though, is that widgets are usually distributed as .zip files (you can't distribute the raw Widget since it is actually just a folder containing several files). By distributing a .zip file, certain data such as the executable bit is not transmitted, so simply executing a script called test.php won't necessarily get run by the PHP interpreter, even if you prefix it with the proper script shebang. For this reason, you would either want to distribute the Widget as a .tar.gz file (which does store permissions), or better yet, execute the full command required to execute the file, such as /usr/bin/php -q test.php.

Tip #5: Using the Widget Transition animation

Widgets have access to the flip animation (made popular by iOS), which allows you to show a back and front side of your Widget. This is great for flipping the Widget over when you want to show the config side of the screen, and flipping it back when you want to show the user the normal interface again. There are two functions involved when doing this. The first is widget.prepareForTransition(ToFront | ToBack). What this function does is take a screenshot of the Widget, and pause any updates being made. Once you run this function, you will want to change the DOM around (hiding the front screen items and showing the back screen items). Once you've done that, you are ready to show the other side by running widget.performTransition(). This function takes a screenshot of the new DOM, calculates and displays the animation to go between the two sides, and un-freezes the DOM.

Here's the pitfall: If pause the widget, hide and show elements, and perform the transition, the transition may run too quickly and the DOM elements wouldn't have updated in time. This is a race condition which would happen 100% of the time in my situation. I've got the newest MacBook Pro, but am using the jQuery library in my widget which may explain why the animation isn't instantaneous. The easy way to fix this is to make use of JavaScript's setTimeout() function, which allows for asynchronous code execution. This combined with the Widget check code mentioned earlier looks like so:

if (isWidget) setTimeout("widget.performTransition()", 10);

Conclusion

Widgets run a lot like standard HTML documents, but there are a few pitfalls. Keep these tips in mind, and memorize the Apple Dashboard guide, and you will be on your way to creating an awesome web app experience in no time.

Tags: #macos
Thomas Hunter II Avatar

Thomas has contributed to dozens of enterprise Node.js services and has worked for a company dedicated to securing Node.js. He has spoken at several conferences on Node.js and JavaScript and is an O'Reilly published author.