AddThis Social Bookmark Button

Print

Let's Build Another Dashboard Widget

by Andrew Anderson
06/07/2005

As we found out with my first article on writing widgets, the Dashboard environment can be a tricky place to develop code. While certain features may not work exactly the same between the beta and full release (as with widget.system), Dashboard is still an excellent environment to run mini-applications.

Even though no real development environment exists now, there are some tricks and techniques that can be used to make widget development easier. This article will explore some of the tricks and techniques that I've found, and then present a widget that uses JavaScript's XMLHttpRequest method to retrieve spelling suggestions from Google.

Developing and Debugging Widgets

Since widgets are basically complex JavaScript-driven web pages, developing widgets is a lot like developing web pages. The major difference between a widget and a web page is in the role of the JavaScript code. Widgets, because they are hosted on a local machine, depend more on the ability of JavaScript to get work done and less on a remote server. This means that widgets often have more lines of complex JavaScript code than a traditional web page.

Like all programming languages, the more complex JavaScript code is, the more difficult it is to debug. Since Dashboard does not include a development environment with a debugger, as widgets get more complex, they get much harder to debug.

Another frustration that I have run into with Dashboard is having to reload widgets when code has changed. This either leaves dozens of older versions of widgets on the Dashboard desktop, or makes the code update cycle awkward; i.e.:

  • Write code

  • Load Dashboard

  • Click the "+"

  • Close previous version of widget

  • Load new version of widget

  • Test new version of widget

Depending on which route you opt for, this is either very inconvenient or leads to a very cluttered Dashboard screen. In a similar way, deploying widgets is a pain for developers because they need to put together a package with several pieces and files before they get the chance to develop any code.

There are some things that can be done to get around these issues, though. The first is to develop widgets as HTML files and test them in Safari. (Apple makes a similar suggestion in their "Developing Dashboard Widgets" article.) While certain features (specifically, the widget object) are not available, it is still the perfect environment to test the UI and any web-based functionality. Since a lot of widgets that people will create focus on web-based functionality, using Safari as a development environment is an easy way to go.

Unfortunately, Safari is set up by default to not include a JavaScript console. For most web pages, this is a minor annoyance, but since the more information you can get from your code the easier it is to develop, a JavaScript console is a big help. Setting up Safari (at least as early as version 1.3) to include a number of debug features, including the JavaScript console, can be done by setting the IncludeDebugMenu property to 1 in the Safari properties file. The easiest way to do this is run the following from a shell:

 defaults write com.apple.Safari IncludeDebugMenu 1

Related Reading

Learning Unix for Mac OS X Tiger
By Dave Taylor

If you have Safari running, quit it and then restart it and you will see a new drop-down menu named Debug. The Debug menu includes lots of interesting features, including a JavaScript console and the ability to log JavaScript exceptions, both of which are very helpful when debugging widgets.

At some point, as we are developing a widget and not a web page, we will need to turn the HTML file into a proper widget. Since the widget already works, it is fairly straightforward to set up the Info.plist and default graphics attributes. But you still may need to debug the code, either because some functionality needs the widget object, or because Dashboard is not behaving exactly the same way Safari did.

To debug in this scenario, I use JavaScript alerts, which instead of being pop-ups, are printed to the console log in /Library/Logs/Console/<user id>. They can also be accessed with the Console application, located in /Applications/Utilities/Console.app. While this is kind of a primitive way to debug a program, it at least gives some insight into what is going on. One way to avoid a cluttered Dashboard screen is to set the AllowMultipleInstances property to false in the widget's Info.plist file. While this does not eliminate the awkward development cycle, it may help you keep your sanity.

Other Sources of Information

Clearly there is a lot more to explore in terms of developing widgets. I have run into a couple of sites that are pretty good, but Apple's documentation is, not surprisingly, really the best and most comprehensive. Here are the sites that I have found are useful:

Build the SpellingSuggestion Widget

Now on to actually creating the SpellingSuggestion widget. The idea behind this widget is pretty simple: it uses JavaScript to retrieve information from the Google Web APIs that gives you a suggested spelling for a misspelled word. In order to get the widget to work, you need to have a Google license key (which anyone can get free of charge)--check out the Google Web APIs home page for more information.

The Google Web APIs uses SOAP XML requests and responses to process requests for information. We will need to create a SOAP XML request that contains the input data that Google needs, and then we will have to parse out the data that we want from the SOAP response to display in our widget. To send the SOAP requests to Google, we will use JavaScript's XMLHttpRequest object. This object allows JavaScript methods to send a request to an XML-based web service and retrieve the XML response in a DOM-based document object. Once the DOM object is returned, the data can then be used anywhere in the widget.

The getSuggestion function below shows how we use the XMLHttpRequest object in the SpellingSuggestion widget:

function getSuggestion() {
  var word = document.getElementById('inputWord').value;
  xmlhttp = new XMLHttpRequest();

  xmlhttp.open("POST",
               "http://api.google.com/search/beta2",
               true);
  xmlhttp.onreadystatechange=function() {
    if (xmlhttp.readyState==4) {
      var returns = 
         xmlhttp.responseXML.getElementsByTagName("return");
      var node = returns[0].firstChild;

      document.getElementById('outputWord').value = node.data;
    }
    else if (xmlhttp.readyState == 3){
      document.getElementById('outputWord').value =
                                        "(no suggestion )" ;
    }
    else {
      document.getElementById('outputWord').value = 
                                         "(checking ... )" ;
    }
}

xmlhttp.setRequestHeader("Content-Type", "text/xml");
var request = "<?xml version='1.0' encoding='UTF-8'?>" + 
 '<SOAP-ENV:Envelope xmlns:SOAP-ENV='+
 '"http://schemas.xmlsoap.org/soap/envelope/" ' + 
 'xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" ' +
 'xmlns:xsd="http://www.w3.org/1999/XMLSchema">' + 
 '<SOAP-ENV:Body>' +
 '<ns1:doSpellingSuggestion xmlns:ns1="urn:GoogleSearch" '+
 ' SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' +
 '<key xsi:type="xsd:string">'+googleId+'</key>' +
 '<phrase xsi:type="xsd:string">'+word+'</phrase>' +
 '</ns1:doSpellingSuggestion>' +
 '</SOAP-ENV:Body>' +
 '</SOAP-ENV:Envelope>';

	xmlhttp.send(request);
}

Here is a line-by-line explanation of what the function does:

  • var word = document.getElementById('inputWord').value; gets the data from the HTML page.

  • xmlhttp = new XMLHttpRequest(); creates the XMLHttpRequest object.

  • xmlhttp.open("POST", "http://api.google.com/search/beta2",true); specifies that it is a POST request and the URL to which request is being sent.

  • xmlhttp.onreadystatechange=function() {
      if (xmlhttp.readyState==4) {
        var returns =
          xmlhttp.responseXML.getElementsByTagName("return");
        var node = returns[0].firstChild;
    	
        document.getElementById('outputWord').value = 
                                                   node.data;
      }
      else if (xmlhttp.readyState == 3){
       document.getElementById('outputWord').value = 
                                          "(no suggestion )";
      }
      else {
        document.getElementById('outputWord').value =
                                          "(checking ... )" ;
      }
    }

    sets up the function that XMLHttpRequest will call when it receives a response from the web server. This function checks out the XML readystate code to determine what to do. A readystate code of 4 means that the response has completed and that Google had a spelling suggestion for the word we put in. The function then pulls the value out of the DOM object and then sets it into a text field on the HTML page. A readystate code of 3 means "interactive," which in this case means that Google had no spelling suggestion for us, so we set the text field to say "(no suggestion)". Any other response code means that we are still waiting around for an answer, so we set the response field to say "(checking...)".

  • xmlhttp.setRequestHeader("Content-Type", "text/xml"); sets the content type of the request to text/xml, because we are sending a SOAP request.

  • var request = "<?xml version='1.0' encoding='UTF-8'?>" +
     '<SOAP-ENV:Envelope xmlns:SOAP-ENV='+
     '"http://schemas.xmlsoap.org/soap/envelope/" ' + 
     'xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" '+
     'xmlns:xsd="http://www.w3.org/1999/XMLSchema">' + 
     '<SOAP-ENV:Body>' +
     '<ns1:doSpellingSuggestion xmlns:ns1="urn:GoogleSearch" '+
     ' SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">'+
     '<key xsi:type="xsd:string">'+googleId+'</key>' +
     '<phrase xsi:type="xsd:string">'+word+'</phrase>' +
     '</ns1:doSpellingSuggestion>' +
     '</SOAP-ENV:Body>' +
     '</SOAP-ENV:Envelope>';

    sets up the SOAP request. Most of this request is configuration information. The important sections are the section that specifies the API we would like to use: <ns1:doSpellingSuggestion ..., the line that sets our Google ID (which we will get to later: '<key xsi:type="xsd:string">'+googleId+'</key>' +, and the line that sets the word that we want the suggestion for: <phrase xsi:type="xsd:string">'+word+'</phrase>.

  • xmlhttp.send(request); sends the request and gets the whole process moving.

Now that we have the getSuggestion function, we can put all the other pieces of the widget together. The first step is the 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>AllowFullAccess</key>
	<true/>
	<key>AllowMultipleInstances</key>
	<false/>
	<key>CFBundleIdentifier</key>
	<string>com.oreilly.widget.ManPage</string>
	<key>CFBundleName</key>
	<string>Man Page</string>
	<key>DefaultImage</key>
	<string>Default</string>
	<key>Height</key>
	<string>55</string>
	<key>MainHTML</key>
	<string>SpellSuggest.html</string>
	<key>Width</key>
	<integer>550</integer>
</dict>
</plist>

Next is the Default.png file, which is here:

Default.png

Finally we have the HTML file, which includes getSuggestion and all of the CSS and HTML that we need:

<html>
  <head>
    <script LANGUAGE="JavaScript1.2" TYPE="text/javascript">
    <!--
    var googleId = "abc123" ;
    
    function getSuggestion() {
    var word = document.getElementById('inputWord').value;
    xmlhttp = new XMLHttpRequest();

    xmlhttp.open("POST",
                 "http://api.google.com/search/beta2",
                 true);
    xmlhttp.onreadystatechange=function() {
    if (xmlhttp.readyState==4) {
    var returns = 
    xmlhttp.responseXML.getElementsByTagName("return");
    var node = returns[0].firstChild;
    document.getElementById('outputWord').value = 
                                             node.data;
    }
    else if (xmlhttp.readyState == 3){
      document.getElementById('outputWord').value = 
                                    "(no suggestion )";
    }
    else {
      document.getElementById('outputWord').value = 
                                     "(checking ... )";
    }
  }

xmlhttp.setRequestHeader("Content-Type", "text/xml");
var request =	"<?xml version='1.0' encoding='UTF-8'?>" + 
  '<SOAP-ENV:Envelope xmlns:SOAP-ENV='+
  '"http://schemas.xmlsoap.org/soap/envelope/" ' + 
  'xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" '+
  'xmlns:xsd="http://www.w3.org/1999/XMLSchema">' + 
  '<SOAP-ENV:Body>' +
  '<ns1:doSpellingSuggestion xmlns:ns1="urn:GoogleSearch" '+
  ' SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' +
  '<key xsi:type="xsd:string">'+googleId+'</key>' +
  '<phrase xsi:type="xsd:string">'+word+'</phrase>' +
  '</ns1:doSpellingSuggestion>' +
   '</SOAP-ENV:Body>' +
  '</SOAP-ENV:Envelope>';

	xmlhttp.send(request);
}
-->
</script>

<style type="text/css">

<!--
.textStyle {
	font-family: Lucida Grande, Arial, Helvetica, sans-serif;
	font-size: 12px;
	color: #ffffff;
}

.backgroundStyle {
	position:absolute; 
	width:550px; 
	height:55px; 
	left:0px; 
	top:0px; 
	z-index:0; 
	visibility: visible;
}

.ioStyle{
        position:absolute; 
	width:550px;
	height:55px;
        left: 0px; 
        top: 8px; 
	z-index:1;
        visibility: visible;
}
-->
</style>

</head>

<body>

<body>
<div id="BackgroundLayer" class="backgroundStyle">
	<img src="Default.png" width="550px" height="55px">
  </img>  
</div> 

<div id="IOLayer" class="ioStyle">
    <div class="textStyle" align="center" valign="top">
      Spelling Suggestions
    </div>
        <div align="center" class="textStyle">
        Word: <input tabindex="1" name="inputWord"
	id ="inputWord"></input>
        <input name="getSuggestion" type="submit"
	value="Get Suggestion" onClick="getSuggestion()"></input>
        <input notab readonly name="outputWord" id="outputWord">
        </input>
    </div>
</div>


</body>


</html>

A couple of notes about the widget:

  • You will need to change "abc123" in

    var googleId = "abc123" ; // put your Google Id here

    to your Google ID.

  • If Google thinks a word is spelled correctly, or it has no guess because the word is so badly butchered, then you will get the "Interactive" response, where readystate=3.

  • The widget may seem redundant, as Tiger comes with a dictionary widget already, but Google's spelling suggestions work on things that a standard spell checker does not work with, including famous people's names, spelling based on context, and short phrases; for instance, if I entered "Duke Coach Kryzeski" and got back "Duke Coach Krzyzewski."

Final Thoughts

Thanks for sticking around with this widget; I realize that this last one was a bit of a letdown. If you figure out any other tricks to help develop widgets, please post them. I'm sure I'm not the only one who will be interested.

Andrew Anderson is a software developer and consultant in Chicago who's been using and programming on the Mac as long as he can remember.


Return to MacDevCenter.com.