MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Extending the Dashboard Virtual Earth Widget

by Luke Burton
03/01/2006

Previously on Mac DevCenter, we explored the creation of a Virtual Earth Dashboard Widget from the ground up.

While it's pretty cool to integrate MSN Virtual Earth into a widget so easily, there's plenty of room to improve our widget. For starters, it looks like it was designed by Fred Flintstone in Photoshop. Secondly, there is no reason we should arbitrarily constrain it to a particular size. Thirdly, it's a bit mysterious; there isn't even an About pane, let alone any preferences.

To give you an idea of what we're shooting for, here's a sneak peek of the finished product. I think you'll agree it's quite an improvement from the version in our first article!

Go ahead and download a copy of the completed widget here. You can install this widget and walk through its source code as we progress through this tutorial.

So, without further delay, let's discover how we can make some of these changes to our widget and really get it looking professional.

figure 1

Back to the Old Drawing Board

It's time to hit Photoshop again, and create something a little less Neanderthal-looking for our widget to live in. Our widget definitely needs a new look for 2006.

In the first version of our widget, we took the easy way out. The frame that surrounded the actual Virtual Earth control was a simple static image. An image like this is not going to help us when it comes to resizing. We need a container more like what you'd find on a web page, a dynamic, resizable container implemented with CSS.

figure 2

Now that looks a bit more modern. In Photoshop, I created a box with rounded edges. I then subtracted a square out of the middle. On top of this I applied some layer effects: an inner shadow for the inner box, and a subtle drop shadow for the outside. I also applied a slight bevel to the frame itself. Finally, I used the slice tool to subdivide the image for display in the browser.

It's important to note what slices you'll need. Because your widget will be fully resizable, you will need each corner of the box as its own image. You'll then need a 1-pixel-wide slice for the top, left, right, and bottom frames. You can read about similar techniques at places like A List Apart. When it comes time to save your images, make sure they're all 24-bit PNGs, so that our transparencies are preserved.

Remember, a widget is for all intents and purposes a web page, so you can apply any technique that would normally work on a web page. Except, joyously, you can strip out the IE-specific workarounds.

Google Maps Hacks

Related Reading

Google Maps Hacks
By Rich Gibson, Schuyler Erle

Laying Out the HTML

We'll be introducing quite a few new ideas in our revised HTML. But it's small enough that we can examine it as a whole. So let's see:

<html>
<head>

<link href="VirtualEarth.css" type="text/css" rel="stylesheet"/>
<link href="http://dev.virtualearth.net/commercial/v1/VE_MapSearchControl.css" type="text/css" rel="stylesheet"/>
<script type='text/javascript' src="http://dev.virtualearth.net/commercial/v1/VE_MapSearchControl.js"/>
<script type='text/javascript' src='AppleClasses/AppleInfoButton.js' charset='utf-8'/>
<script type='text/javascript' src='AppleClasses/AppleAnimator.js' charset='utf-8'/>
<script type='text/javascript' src='AppleClasses/AppleButton.js' charset='utf-8'/>
<script type='text/javascript' src='VirtualEarth.js' charset='utf-8'/>

</head>

<body onload="setup();">
        <div id="front">
            <div id="top">
                <div id="top_left"></div>
                <div id="top_inner"></div>
                <div id="top_right"></div>
            </div>

            <div id="inner_contents"></div>
            <div id="inner_left"></div>
            <div id="inner_right"></div>

            <div id="bottom">
                <div id="bottom_left"></div>
                <div id="bottom_inner"></div>
                <div id="bottom_right"></div>
            </div>
            <img id='resize' src='/System/Library/WidgetResources/resize.png' onmousedown='mouseDown(event);'/>
            <div id="infoButton"></div>
        </div>
        
        <div id="back">
            <div id="back_contents">
                VirtualEarth Dashboard Widget v1.5<br/>
                Luke Burton, 2006<br/>
                <span class="link" onclick="widget.openURL('mailto:luke@burton.echidna.id.au');">luke@burton.echidna.id.au</span><br/>
                <br/>
                Visit <span class="link" onclick="widget.openURL('http://www.hagus.net/taxonomy/term/15')">Luke's website</span> for more info and new versions.<br/>
            </div>
            <div id="doneButton"></div>
        </div>
</body>
</html>

Setting Up the Apple Classes

The first thing you'll notice in our <head> section is the heap of inclusions we make from something called the Apple Classes. These classes are a convenience for developers who want to include custom widget stuff like sliders, scroll bars, glass buttons, info buttons, and more. This all helps us attain that genuine Widget look and feel.

You can read more in-depth documentation on the Apple Classes here. For now, it suffices for you to copy the entire AppleClasses directory into your project:

cd /path/to/my/widget.wdgt
cp -r /System/Library/WidgetResources/AppleClasses .

In our case, we then include the AppleInfoButton, AppleAnimator, and AppleButton in our widget's HTML source file.

Divs Inside Divs Inside Divs ...

I'll be honest and admit that when it comes to CSS layout, I employ a lot of trial and error. I browse through other people's code, looking for that magic configuration of nested divs and CSS that renders correctly. In this case I'm pretty pleased with the result; the code is quite clean.

We start by defining two major divs. The first is front and the second is back. These will correspond to the front and back of the widget itself. We'll cover the back part of the widget, and how it is displayed, in more detail later. For now we'll concentrate on the front.

The front starts with a top div, inside of which are top_left, top_inner, and top_right divs. As you might guess from how we sliced our image in Photoshop, I intend to put one image in each of these divs.

The middle three divs, after much consternation, hair pulling, and rude words, ended up being configured without a container div. I found this the easiest configuration to guarantee proper resizing. The inner_contents div will contain the actual Virtual Earth object, while inner_left and inner_right will have the left and right image slices, respectively.

The bottom div works exactly like the top one.

We have two final points of interest in the front HTML. One is the resize button, which is declared here as an image. Note that we also associate a mouseDown event here, which will be covered when we come to the JavaScript. We also include a div for the infoButton, which is the little "i" symbol that flashes up on some widgets when you mouse over them, indicating they have a preference or information pane.

CSS Layout

Our goal with the CSS layout was to produce a container that, when resized, would scale gracefully. I started off by designing most of the layout in a test HTML file that I opened with Safari. Once it looked reasonable in there, I moved everything into the widget directory.

The CSS is quite lengthy, and for the most part repetitive, but you can see it here.

Rather than examine it line by line, I shall extract the major lessons I learned when writing it.

First, everything must be position: absolute for maximum control. There is no real need to use relative placement, or even floats. If you want to have something stuck to the left of your widget, you set left: 0px. If you want it on the right, right: 0px. Absolute positioning was definitely easier. You can sometimes fine-tune the positions using negative margins.

The disadvantage of absolute positioning is that it's difficult to go back and change the size of your original images. Say you weren't happy with how you sliced it up in Photoshop; going back and changing all the per-pixel sizes in your CSS file will be a pain. So try to settle on good image slices early on.

Then, use background images against your divs. Give them a height, a width, and a background image. It's important to specify the width and height, otherwise your divs won't be visible. To control the layering of these divs, you must use z-index. For example, I set all of my divs to a z-index of 1, but the inner_contents div to z-index: 0. This enabled the pleasing inner shadow effect we designed in Photoshop earlier.

Next, your widget should scale relative to one major div. In my case, it's the inner_contents div. If I change its height and width, I want all the other divs to organize themselves around it automatically. This is important for later when we look at the JavaScript that controls resizing--we would like to make as few calculations and change as few divs as possible.

Finally, remember to place the resize handle and info buttons. The resize handle will be marked as an -apple-dashboard-region like our main Virtual Earth canvas. The resize handle should of course always be placed in the lower right-hand corner of the widget.

Hooking Up the Resize Code

We have everything in place now for a nice-looking widget, but we need to write a small amount of code to actually make our resize handle function. Recall that when we declared the resize image in HTML, we hooked up the onMouseDown handler to the mouseDown function ...

function mouseDown(event)
{
    document.addEventListener("mousemove", mouseMove, true);
    document.addEventListener("mouseup", mouseUp, true);

    growboxInset = {x:(window.innerWidth - event.x), y:(window.innerHeight - event.y)};

    event.stopPropagation();
    event.preventDefault();
}

This function is pretty straightforward, and is lifted pretty much directly from Apple's Dashboard Developer documentation. First we declare two handlers for mouseMove and mouseUp events. These correspond to a drag event and releasing the mouse button, respectively. We then initialize growboxInset, which will capture the current size of the window. Finally, some cleanup code to stop event propagation.

Naturally, what will happen next is a drag event as the user moves the resize handle. At this point, our mouseMove event is invoked:

function mouseMove(event)
{
    var x = event.x + growboxInset.x;
    var y = event.y + growboxInset.y;

    if (x < 555) x = 555;
    if (y < 450) y = 450;

    // set the attributes for the front of the widget
    document.getElementById("front").style.width = x;
    document.getElementById("front").style.height = y;

    // set the attributes for the inner contents div, minus 
    // the width of the border
    document.getElementById("inner_contents").style.width = x-40;
    document.getElementById("inner_contents").style.height = y-40;

    // resize the main widget window
    window.resizeTo(x,y);

    // resize the virtual earth control
    map.Resize(x-40, y-40);

    event.stopPropagation();
    event.preventDefault();
}

This function is again lifted mostly from the Dashboard Developer documentation, but we've had to make a few changes to customize it. We start off by calculating the size of the resized box in variables x and y. We then have a small amount of code to guarantee the box does not size any smaller than 555x450. These are more or less arbitrary numbers that take into account the minimum size of the Virtual Earth control.

Now there are several elements that we must change to ensure the widget resizes smoothly. First, we resize the front div, making its dimensions equal to the new size. Then we resize the inner_contents div, which corresponds to the actual Virtual Earth control. When we adjust its dimensions, we pay attention to the thickness of the borders too.

Finally, we resize the entire widget window itself, followed by the actual map control. This final step is important so that the internal Virtual Earth control code is aware that its dimensions have changed; it isn't aware of a resize event just because its parent div has changed size. Again, we consider the border thickness of the widget by subtracting 40 pixels.

To conclude, we perform some important cleanup when the user releases the mouse button. Namely, we destroy the two event handlers that have been calling our functions.

// when we release the resize handle ...
function mouseUp(event)
{
    document.removeEventListener("mousemove", mouseMove, true);
    document.removeEventListener("mouseup", mouseUp, true); 

    event.stopPropagation();
    event.preventDefault();
}

And now our widget resizes correctly!

Coding the Preference Pane

Quite a bit of the code for the preference pane is shoveled out of Apple's developer documentation, but we shall walk through the implementation I have used here.

Recall that we declared a new div with id="back" earlier on. This div represents what you'll see displayed when the widget is rotated. Let's take a quick look at the HTML again:

<div id="back">
    <div id="back_contents">
        VirtualEarth Dashboard Widget v1.5<br/>
        Luke Burton, 2006<br/>
        <span class="link" onclick="widget.openURL('mailto:luke@burton.echidna.id.au');">luke@burton.echidna.id.au</span><br/>
        <br/>
        Visit <span class="link" onclick="widget.openURL('http://www.hagus.net/taxonomy/term/15')">Luke's website</span> for more info and new versions.<br/>
    </div>
    <div id="doneButton"></div>
</div>

There really isn't much unusual going on here, we're just declaring what we'd like the back of our widget to look like. Inside a div called back_contents, I declare a bit of text and some hyperlinks to inform people about my widget. I then add a doneButton div which I'll explain right now.

To get the widget rotating, we'll need to set up a button to flip from front to back, and another button to do the reverse. In Widget terminology, the button that shows you the preference pane (the back) is called the Info button, and the button that does the opposite is called the Done button. You'll see these on most widgets as a little glowing "i" on the front and a glassy-looking Done button on the back. Hence we have placeholders for both these buttons in our HTML code: divs called doneButton andinfoButton.

Let's take a look at our revised setup() function to see how these buttons are declared in JavaScript. Both buttons are of types declared in the Apple Classes included previously.

Note that the first thing we do is declare some global variables. Along with map and growboxInset which we've already encountered, we have two variables representing the Done and Info buttons.

var growboxInset;
var map;
var gDoneButton;
var gInfoButton;

Our setup code has a few small additions:

function setup()
{
    // set up the map object
    map = VE_MapSearchControl.Create(37.3189139456153,-122.029211560580, 
            12, 'r', "absolute", 0, 0, 515, 410, 
            "http://local.live.com/search.ashx", "http://local.live.com/Ads.ashx");

    // attach it to the correct div
    document.getElementById("inner_contents").appendChild(map.element); 

    // set our default window size
    window.resizeTo(555, 450);

    // organise some button objects
    gDoneButton = new AppleGlassButton(document.getElementById("doneButton"), "Done", hideBack);
    gInfoButton = new AppleInfoButton(document.getElementById("infoButton"), document.getElementById("front"), "black", "black", showBack);

}

After declaring the map control and appending it to the inner_contents div, we then set a starting size for our widget. I found this necessary to avoid an unusual flicker when the resize handle is first grabbed. We then declare the JavaScript objects that will take care of rendering our buttons.

The Apple Classes documentation explains these constructors.

But briefly, for the Done button we pass in our target div, the label we'd like on the button, and the JavaScript function to be called when the button is clicked. For the Info button, we pass the target div, the id of the front of our widget, two attributes detailing the color scheme for the glowing "i" button, and the JavaScript function we'd like called upon a click.

Let's now examine the JavaScript functions that are invoked when you show the back, and hide the back.

function showBack()
{
    var front = document.getElementById("front");
    var back = document.getElementById("back");

    if (window.widget)
        widget.prepareForTransition("ToBack");

    front.style.display="none";
        back.style.display="block";

    if (window.widget)
        setTimeout ('widget.performTransition();', 0);  
}

function hideBack()
{
    var front = document.getElementById("front");
    var back = document.getElementById("back");

    if (window.widget)
        widget.prepareForTransition("ToFront");

    back.style.display="none";
    front.style.display="block";

    if (window.widget)
        setTimeout ('widget.performTransition();', 0);
}

These two functions are going to be pretty much the same in almost all widgets, unless you have some setup routines you'd like to perform before displaying the preference pane.

The general gist is that we grab handles to our front and back divs. We then prepare the widget for transition with widget.prepareForTransition, but only if we're running as a widget (as opposed to running in Safari, for instance). We then switch the display styles of our front and back appropriately. Finally, we call widget.performTransition. This is called in a special manner using setTimeout, as the Apple docs say, to ensure the front and back display styles have already been swapped before the transition occurs.

What Have We Covered?

Well, it feels like we've come a long way, but my explanation of how it happened is far longer than the code itself. In summary, we:

But once again, we have barely scratched the surface of what's possible with Dashboard Widgets. Your imagination really is your only limit with this technology. You can even include your own Quartz effects designed in Quartz Composer for stunning visual effects. Hmm, I wonder what kind of gratuitous visual effects we could include in our Virtual Earth Widget ...

Luke Burton chipped his teeth on C++, and has lately sought refuge in the beautiful world of scripting languages like Ruby and Perl.


Return to the Mac DevCenter

Copyright © 2009 O'Reilly Media, Inc.