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.pngis the default background image for your widget, which displays while the widget is loading.Icon.pngis the small image that displays in the widget management console. Apple suggests this image be 85x85 pixels, with a drop shadow.Make.htmlis 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.plistis 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:
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.
Discussion
You must be logged in to post a talkback.
[ Display main threads only] [ Oldest First]Showing messages 1 through 3 of 3.
- Excellent Article!
You must be logged in to reply.
Thanks so much for sharing this info. I was just about to roll up my sleeves and try to decipher Apple's documentation when I found yours. I've built my own modified version of your widget to monitor one of the categories on my blog-based site. It works like charm. I'm looking forward to getting "under the hood" to further customize it.Posted by mlanger on March 28, 2007 at 16:02:14 Pacific Time
- but Safari is NOT a reliable widget test environment
You must be logged in to reply.
Widget can appears to work fine in Safari, but fail nevertheless in the real widget environment. I know because I've got this problem now. Techniques extra to Safari are required, that no one will spell out in terms that are sufficiently simple, to develop a widget.
For example, a widget using an HTML "img" tag to display an image, shows the img perfectly in Safari, reloading the image each time. But the widget does not reload the image each time the widget is launched in the widget environment.
Posted by JeffJeff on November 12, 2006 at 05:31:15 Pacific Time
- compatability
You must be logged in to reply.
is this compatiable with konfabulator/yahoo widgets? or is there a mac widget app for panther on older macs.. (G4 ibook)
Posted by !sam! on December 27, 2005 at 13:07:02 Pacific Time
|
Showing messages 1 through 3 of 3. |










