How To:

Make an RSS Widget

by Jake McKenzie
November 21, 2005

Widgets are great--there is no denying it. Arranging these small, lightweight utilities on your Mac OS X Dashboard desktop puts lots of useful and fun possibilities at your fingertips and eyeballs. But when a widget you want doesn't exist, there is only one thing to do: make it.

You can download the finished MAKE widget here, but to see how it was constructed, read on!

Basics

Like many Mac users, I have a few a widgets that I use regularly, and I try out new ones fairly often. The way widgets install in OS X is simple: the system copies the widget's .wdgt file into one of two folders.

/Library/Widgets: This folder contains the standard Tiger widgets.

~/Library/Widgets: This folder contains user-installed widgets.

You can find all of your widgets in these two locations. Now that you know where they are, it's time to do some exploring. All .wdgt files are actually bundles of files acting like a single file. Ctrl-click on a .wdgt file and choose "Show Package Contents," and a new Finder window will open up with the contents of the widget package. Alternatively, notice that if you remove the .wdgt file extension from the file, the bundle becomes a regular folder.

If you look around, you will notice that every widget contains at least one .html file. This is because all widgets are basically little websites that display directly on the Dashboard, rather than in a web browser.

An example of the simplest possible widget is Apple's Hello World widget. This widget contains contains just four files, two of which are graphics (click on filenames to see):

  • Default.png is the default background image for your widget, which displays while the widget is loading.
  • Icon.png is the small image that displays in the widget management console. Apple suggests this image be 85x85 pixels, with a drop shadow.
  • Make.html is the main HTML file. This is the heart of your widget. You can name this file anything you want, but for simplicity, I will refer to it as Make.html.
  • Info.plist is the properties file that ties the pieces of your widget together. This is where you reference the filename of your html file, and give the widget a name, version number, and unique identifier. This file is an xml document with the following keys:
Key Name Datatype Purpose Required?
CFBundleName String Name of the bundle YES
CFBundleDisplayName String Localized bundle name YES
CFBundleIdentifier String Reverse DNS style identifier for bundle YES
CFBundleVersion String Widget version number YES
MainHTML String Name of main HTML file YES
Width Integer Widget with (pixels) NO
Height Integer Widget height (pixels) NO
CloseBoxInsetX Integer Horizontal inset of close box (pixels) NO
CloseBoxInsetY Integer Vertical inset of close box (pixels) NO
Plugin String Name of plugin used in widget NO
AllowNetworkAccess Bool Allow the widget access to network resources NO
AllowJava Bool Allow the widget access to Java applets NO
AllowInternetPlugins Bool Allow the widget access to Web Kit Plugins NO
AllowSystem Bool Allow the widget access to command line NO
AllowFileAccessOutsideOfWidget Bool Allow the widget access to the file system NO
AllowFullAccess Bool Equivalent to setting all other Allows true NO

In addition to the four files listed above, most widgets contain additional files such as style sheets, extra images, and Javascript documents. You include these files in the widget's .html file using the same code that you would use for a website. For instance, here's how you would include the style sheet Style.css:

<style type="text/css" title="Style">
    @import "Style.css";
</style>

And here's how you include some Javascript from the file Script.js:

<script type='text/javascript' src='Script.js' charset='utf-8'/>

Remember that all the files for a widget reside in the same dedicated folder.

With a bit of code finished, it's time to test out your widget. A quick and easy way to do this is to open your widget's .html file with Safari (other browsers won't work). If it looks right and you are satisfied with the results, you can "build" the widget by simply naming the folder to give it a .wdgt extension. For terminal users, a simple cp -r "Content" MyWidget.wdgt/ works great when your files are in a folder called Content. Once you have your widget "file," copy the folder over into ~/Library/Widgets.

The Make RSS Widget

If you want to get started on a new widget, the first thing to check out is Apple's sample code. There you can find great sample widgets that will give you a good foundation to build upon.

For my Make widget, I wanted to display a live RSS feed from the MAKE blog, one clickable line per blog entry. To do this, I lifted and modified the code for Apple's SampleRSS. The completed Make widget contains the following files:

/
/Make.html
/Make.css    
/Make.js
/Scroller.js
/default.png
/Icon.png
/Info.plist
/Images/
/Images/background.png
/Images/well.png
/Images/dark.png
/Images/light.png
/Images/scrollControl_bottom.png
/Images/scrollControl_middle.png
/Images/scrollControl_top.png
/Images/Links/
/Images/Links/delicious_links.gif
/Images/Links/flickr_pool.gif
/Images/Links/podcastmp3.gif

Let's start by taking a look at the widget's property list, in Info.plist. It's a fairly standard-looking xml file that contains seven key/value pairs. For your own RSS widget code, you can modify all of these seven values, except for the first one, AllowNetworkAccess, which tells the widget to monitor the RSS feed.

Although it's not necessary to implicitly define the width and height of a widget (the last two key/value pairs), it seems like a good idea. If values are not given, the width and height are taken from default.png.

Here's the contents of our widget's Info.plist file:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" 
		"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>AllowNetworkAccess</key>       //This key and value is required 
for RSS widgets
    <true/>
    <key>CFBundleIdentifier</key>       //The bundle identifier should be 
unique to your widget, 
    									//reverse DNS style names are reccomended
    <string>com.makezine.rsswidget</string>
    <key>CFBundleName</key>
    <string>Make:RSS</string>
    <key>CFBundleVersion</key>
    <string>1.0</string>
    <key>MainHTML</key>
    <string>Make.html</string>
    <key>Width</key>	
    <integer>280</integer>
    <key>Height</key>
    <integer>369</integer>
</dict>
</plist>

According to this plist, the MainHTML file is called Make.html. Let's examine that file next; here are its contents:



<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    //Include Stylesheet
    <style type="text/css" title="MakeStyle">
        @import "Make.css";
    </style>
    //Include Javascript docs
    <script type='text/javascript' src='Make.js' charset='utf-8'/>
    <script type='text/javascript' src='Scroller.js' charset='utf-8'/>
</head>
<body onload='load();'> //Initialize scrollbar and feed display
<div id='front'>
    <img src='Images/background.png'>
    //Make the logo a link
    <div id='feed' onclick="widget.openURL('http://www.makezine.com');"></div>
        <div id='container'>
            <div id='contents'></div> //This is the div where the 
articles are displayed
        </div>
  	//Display the scrollbar
        <div id='myScrollBar'>
        <div id='myScrollTrack'> <!-- onmousedown='mouseDownTrack(event);' 
        								onmouseup='mouseUpTrack(event);'-->	
            <div class='scrollTrackTop' id='myScrollTrackTop'></div>
            <div class='scrollTrackMid' id='myScrollTrackMid'></div>
            <div class='scrollTrackBot' id='myScrollTrackBot'></div>-->
        </div>
        <div id='myScrollThumb'><!-- onmousedown='mouseDownScrollThumb(event);'-->
            <div class='scrollThumbTop' id='myScrollThumbTop'></div>
            <div class='scrollThumbMid' id='myScrollThumbMid'></div>
            <div class='scrollThumbBot' id='myScrollThumbBot'></div>
        </div>
    </div>
    //Display the mini-links	
    <div id='links'>
        <span onclick="widget.openURL('http://flickr.com/groups/make/pool/');">
			<img src="Images/Links/flickr_pool.gif" alt="make flickr pool"></span>
        <span onclick="widget.openURL('http://www.makezine.com/blog/archive/
			make_podcast/index.html');"><img src="Images/Links/podcastmp3.gif" alt="make:audio
			podcast
        <span onclick="widget.openURL('http://del.icio.us/makemagazine');">
			<img src="Images/Links/delicious_links.gif" alt="make: links on del.ico.us"></sp
    </div>
</div>
</body>
</html>


One interesting thing about this file is its use of Javascript's "widget" object. This is a special object that allows widgets to access the shell, call other applications, access user preferences, and respond to Dashboard activation events. Here, the method widget.openURL is used to open the default browser containing the page we want displayed. This method of opening pages is preferable to using html <a> tags because it allows for more flexible formatting. By letting this html file refer to the style sheet Make.css for all of its formatting, we can greatly change the look of the widget without modifying the display code.

So far, we have covered how the widget's RSS information is displayed, but not where it comes from. Receiving this information is the job of the Javascript file Make.js, which is really the backbone of the widget. This file contains functions for downloading new feed items, formatting dates, and passing all of this info over to Make.html for displaying.

The magic happens in the Make.js file's createRow function, which creates HTML div blocks to contain the information that is obtained by the show function. The function xml_loaded then parses the information into one-line rows and injects them into the div blocks created to hold the content, which is how they mysteriously make their way to their proper location in the widget. Here's what's in Make.js:



var feed = {title:"MAKE", url:"http://www.makezine.com/blog/index.xml"};


function load ()
{
 	scrollerInit(document.getElementById("myScrollBar"), document.getElementById("myScrollTrack"), 
		document.getElementById("myScrollThumb"));
 	calculateAndShowThumb(document.getElementById("contents"));

 	if (!window.widget)
 	{
  		show ();
 	}
}


var last_updated = 0;
var xml_request = null;

//----------------------------------------------------------------------------------------------
//
// show - post a request to get RSS feed when showing the widget. Space requests by
//    	at least 15 minutes to avoid hitting the server too often. Also, note that you should
//    	cancel any outstanding request before posting the new one.
//
//----------------------------------------------------------------------------------------------
function show ()
{
 	DEBUG("show");
 	var now = (new Date).getTime();

 	if ((now - last_updated) > 900000)
 	{
  		if (xml_request != null)
  		{
   			xml_request.abort();
   			xml_request = null;
  		}
  		xml_request = new XMLHttpRequest();

  		xml_request.onload = function(e) {xml_loaded(e, xml_request);}
  		xml_request.overrideMimeType("text/xml");
  		xml_request.open("GET", feed.url);
  		xml_request.setRequestHeader("Cache-Control", "no-cache");
  		xml_request.send(null);
    	}
    	DEBUG("/show");
}

if (window.widget)
{
 	widget.onshow = show;
}


//---------------------------------------------------------------------------------------------
//
// findChild - scan the children of a given DOM element for a node matching nodeName; much more
//    	efficient than the standard DOM methods (getElementsByTagName, etc) if you know
//    	what you're looking for
//
//----------------------------------------------------------------------------------------------
function findChild (element, nodeName)
{
 	var child;

 	for (child = element.firstChild; child != null; child = child.nextSibling)
 	{
  		if (child.nodeName == nodeName)
   			return child;
 	}
 	
	return null;
}

//---------------------------------------------------------------------------------------------
//
// xml_loaded - extract the content of RSS feed and place the items data into a
//       	results array: extract the title, link and publication date for each item.
//
//----------------------------------------------------------------------------------------------
function xml_loaded (e, request)
{
 	xml_request = null;
 	if (request.responseXML)
 	{
  		var contents = document.getElementById('contents');
  		while (contents.hasChildNodes())
  		{
   			contents.removeChild(contents.firstChild);
  		}

  		// Get the top level <rss> element
  		var rss = findChild(request.responseXML, 'rss');
  		if (!rss) {alert("no <rss> element"); return;}

  		// Get single subordinate channel element
  		var channel = findChild( rss, 'channel');
  		if (!channel) {alert("no <channel> element"); return;}

  		var results = new Array;

  		// Get all item elements subordinate to the channel element
  		// For each element, get title, link and publication date.
  		// Note that all elements of an item are optional.
  		for( var item = channel.firstChild; item != null; item = item.nextSibling)
  		{
   			if( item.nodeName == 'item' )
   			{
    				var title = findChild (item, 'title');

    				// we have to have the title to include the item in the list

    				if( title != null )
    				{
     					var link = findChild (item, 'link');
     					var pubDate = findChild (item, 'pubDate');
     					results[results.length] = {title:title.firstChild.data,
      						link:(link != null ? link.firstChild.data : null),
      						date:new Date(Date.parse(pubDate.firstChild.data))
       				    };
    				}
   			}
  		}

  		// sort by date
  		results.sort (compFunc);

  		// copy title and date into rows for display. Store link so it can be used when user
  		// clicks on title
  		//nItems = results.length;
  		nItems = 20;  // limit results to 20 most recent items
  		var even = true;

  		for (var i = 0; i < nItems; ++i)
  		{
   			var item = results[i];
   			var row = createRow (item.title, item.link, item.date, even);
   			even = !even;
   			// insert the new row into the contents div
   			contents.appendChild (row);
  		}

  		// update the scrollbar so scrollbar matches new data
  		calculateAndShowThumb(document.getElementById("contents"));

  		// set last_updated to the current time to keep track of the last time a request was posted
  		last_updated = (new Date).getTime();
 	}
}

//---------------------------------------------------------------------------------------------
//
// sortFunc - compare function used for sorting dates
//
//----------------------------------------------------------------------------------------------
function compFunc (a, b)
{
 	if (a.date < b.date)
  		return 1;
 	else if (a.date > b.date)
  		return -1;
 	else
  		return 0;
}


//---------------------------------------------------------------------------------------------
//
// createRow - add data to the next row in the widget body. Rows have alternating (light and
//      	dark backgound). The title and date as displayed for each item. The link is used
//      	when the user clicks on a RSS title.
//
//----------------------------------------------------------------------------------------------
function createRow (title, link, date, even)
{
 	var row = document.createElement ('div');
 	row.setAttribute ('class', 'row ' + (even ? 'dark' : 'light'));
 	
	var title_div = document.createElement ('div');
 	title_div.innerText = title;
 	title_div.setAttribute ('class', 'title');
 	if (link != null)
 	{
  		title_div.setAttribute ('the_link', link);
  		title_div.setAttribute ('onclick', 'clickOnTitle (event, this);');
 	}
 	row.appendChild (title_div);
 
	if (date != null)
 	{
  		var date_div = document.createElement ('div');
  		date_div.setAttribute ('class', 'date');
  		date_div.innerText = createDateStr (date);
  		
		row.appendChild (date_div);
 	}
 
	return row;
}

function createDateStr (date)
{
 	var month;
 	switch (date.getMonth())
 	{
  		case 0: month = 'Jan'; break;
  		case 1: month = 'Feb'; break;
  		case 2: month = 'Mar'; break;
  		case 3: month = 'Apr'; break;
  		case 4: month = 'May'; break;
  		case 5: month = 'Jun'; break;
  		case 6: month = 'Jul'; break;
  		case 7: month = 'Aug'; break;
  		case 8: month = 'Sep'; break;
  		case 9: month = 'Oct'; break;
  		case 10: month = 'Nov'; break;
  		case 11: month = 'Dec'; break;
 	}
 	return month + ' ' + date.getDate();
}


//---------------------------------------------------------------------------------------------
//
// clickOnTitle - take the user to the RSS link when they click on an article's title.
//
//----------------------------------------------------------------------------------------------
function clickOnTitle (event, div)
{
 	if (window.widget)
 	{
  		widget.openURL (div.the_link);
 	} else document.location = div.the_link;
}


And that's a wrap! Most of the rest of the files in the Make widget are just graphics. There's also some standard scrolling-display code, Scroller.js, and the style sheet, Make.css. You can see all the files by downloading the finished widget

.

Here's what the finished product looks like in action:

Finished

Now that the magic behind the creation of widgets has been uncovered, it's time to put your newly acquired knowledge to good use...

Possibilities

In this article, I've covered Apple's Web Kit and some basic customizations of its RSS widget object. But this is just a start, and there's a lot more you can do. For instance, you can make a widget that accesses the shell by using the widget.system() method. This lets you run fancy commands or shell scripts with one click of a button. Using the canvas tag, you can render ultra-fast graphics and video on your widgets, via Apple's Quartz Extreme architecture (which is built on the popular OpenGL graphics library). For more information on these topics, check out Apple's Dashboard Programming Guide.

References

Dashboard Programming Guide: The number one source for info about writing widgets.

Dashboard Reference: Information on the widget object and other objects available to widgets.


Advertise here with FM.

Why advertise on MAKE?
Read what folks are saying about us!

Click here to advertise on MAKE!

Subscribe to MAKE Magazine!
Important please read

Search the pages of MAKE

Raves for MAKE!

“Now we've got geek DIY (do it yourself) porn. Just as would-be Emerils pore over lushly illustrated cookbooks with recipes involving hard-to-find morels and complicated instructions for roux, Tom Swift wanna-bes are devouring MAKE.”
— Steven Levy, Newsweek

“...O'Reilly Media recently launched what has already become the bible of this new movement, a magazine called MAKE.”
— Daniel Roth, FORTUNE

“If you're the type who views the warnings not to pry open your computer as more a challenge than admonition, MAKE is for you.”
— Rolling Stone

“One of the most innovative magazines I've seen in a long time.”
— Steve Riggio, CEO Barnes & Noble

“The kind of magazine that would impress MacGyver”
— Marcus Chan, San Francisco Chronicle

More Raves for MAKE

Subscribe


Advertise here.
Why advertise on MAKE?
Read what folks are saying about us!

Click here to advertise on MAKE!
Subscribe to MAKE Magazine!