O'Reilly Mac OS X Innovators Contest
Innovators Contest. Mac DevCenter. Mac OS X Conference.
  Contest Rules     Entry Form     Contest Prizes  

 
LinuxDevCenter.com
MacDevCenter.com
WindowsDevCenter.com
ONJava.com
ONLamp.com
OpenP2P.com
Perl.com
WebServices.XML.com
XML.com
Perl
Java
Python
C/C++
Scripting
Web
Web Services
XML
Oracle
Networking
Security
Databases
Linux/Unix
Macintosh/OS X
Windows
.NET
Open Source
Wireless
Bioinformatics
Game Development
Enterprise Development 
The Missing Manuals




Creating Sherlock Channels, Part 2
Pages: 1, 2

Back to the parsing now. Open up the XPath Finder and use the URL I gave earlier http://www.macosxhints.com/search.php?query=sherlock&type=stories&mode=search.

You would look through it trying to find a path like earlier and eventually get to the path /img/table/tr/td/table/tr/td/table/tr/td/table/tr[2]/td/br/table only to find that there are lots of trs, some of which contain the information we're looking for (links to the hints and the titles of the hints) and some of which don't. The solution to this is to use the XPath /img/table/tr/td/table/tr/td/table/tr/td/table/tr[2]/td/br/table//a which selects all of the links within the path. So putting this together we get:

let $searchGoods := for $searchItem in $htmlSearch/img/table/tr/td/table/tr/td/table/tr/td/table/tr[2]/td/br/table//a
return dictionary(
    ("description", $searchItem/b/text()/convert-html(.)),
    ("doubleClickURL", url-with-base($searchItem/@href, http-request-value($base, "ACTUAL_URL")))
)

You should be able to understand these. The only new thing here, other than the // in the XPath, is the url-with-base function, which takes a URL and a base URL and ensures that you get an absolute URL. If the first URL is already absolute, it throws out the second URL. If not, it gets the URL from $base and puts them together. The only thing left to do here is to return the values:

return dictionary(
("Internet.SearchResultsTable.selectedRows", null()),
("Internet.SearchResultsTable.dataValue", $searchGoods),
("Internet.DetailHTMLView.HTMLData", ""),
("Internet.NetworkArrows.animating", false())
)

By now you should be able to understand all of that. The only other thing to remember is that we have to stop the network busy spinner that was started earlier. Open up your channel in Sherlock and try searching for different keywords. Happily, we don't need to set up another parser to extract the hints from the pages, Sherlock will use the one we've already written.

Getting the Newest Hints button Working

One problem our users will have now is that they can't get to the newest hints after they've done a search. This is where the Newest Hints button comes into play, and I hope you now see the value of thinking through the interface. We need to put some code at the Internet.NewestHints.action path.

We could create the trigger and then paste the code from the Internet.didInstall trigger, but that would break a fundamental programming rule: "Never repeat two significant sized pieces of code". So what we'll do is change the retrieving of the newest hints to work nearly much the same way as searching does.

JavaScript code will handle the button action and Internet.didInstall and then call some XQuery code to do the parsing and displaying of the hints. Remember that any trigger that wants to be called by other triggers instead of the UI must start with DATA.

Change the path of the Internet.didInstall trigger to DATA.action.newestHints . Then copy and paste the Internet.SearchButton.action trigger and set the copy's path to Internet.didInstall . On the last line of the new Internet.didInstall trigger change DATA.action.performSearch to DATA.action.newestHints. Copy and paste our new Internet.didInstall trigger and change its path to Internet.NewestHints.action. The last change we need to make is to add these lines:

, 
("Internet.NetworkArrows.animating", false())

to the DATA.action.newestHints trigger's return statement. The reason we add the comma is because the line that used to be the last one in the return statement is now the second to last. The whole DATA.action.newestHints trigger should now look like:

let $httpRequest := http-request("http://www.macosxhints.com/backend/geeklog.rdf") 
let $rss := http-request-value($httpRequest, "DATA")
let $goods := for $item in $rss/rss/channel/item 
return dictionary( 
                ("description", $item/title/text()/convert-html(.)), 
                ("doubleClickURL", $item/link/text()/convert-html(.)) 
) 
return dictionary( 
        ("Internet.SearchResultsTable.selectedRows", null()), 
        ("Internet.DetailHTMLView.HTMLData", ""), 
        ("Internet.SearchResultsTable.dataValue", $goods),
        ("Internet.NetworkArrows.animating", false())
)

Go ahead now and test our changes in Sherlock.

Get more Sherlock Channels

We want the user to be able to go to a site to find more Sherlock Channels instantly, to do that, we'll add this trigger and code:

<trigger path="Internet.GetMore.action" language="JavaScript">
    System.OpenURL("http://sherlock3.homeunix.com");
    </trigger>

This simply opens the URL in whatever application or browser the user has chosen.

The Enlarge and Shrink Font buttons

One problem users with limited eye site or high resolution monitors might have is the small size of the font in the HTMLView. To help them, we want to allow the font size to changed with either of our two buttons. Amazingly, we don't have to write a single line of code to do this. Open up IB, hold down the control key, and drag the Enlarge Font button towards the HTMLView. The button won't actually move, but you'll see a line being drawn between the two. After you release the mouse button, the Connections pane for the button will open. In the column on the right side, choose "makeFontBigger:" and click the Connect button in the lower right corner. Repeat these steps for the Shrink Font button, except choose "makeFontSmaller:" in the Connections pane.

The About Button

We want users to find out more about the channel and its developer, so we put a button where they can click and have a sheet/panel come down and tell give them that information. But first we need to create the sheet in IB.

In the Windows pane of the widgets palette drag the Panel into the Channel.nib window. You need to set a path for the panel, so open it up (by double clicking on it) and open up it's Sherlock pane. For the name enter "about", you'll notice that in the Data store path it only shows about and not Internet. This is because you have a entirely separate window. Drag a button into the lower right corner, name it "OK" and give a path of ok. I would also suggest making it equivalent to return. You can now add whatever else you want, but be sure to add some text for the user to read to find out how to reach you. Add these triggers to your code:

<trigger path="Internet.about.action" language="JavaScript">
DataStore.Notify("about.beginSheet");
</trigger>

<trigger path="about.ok.action" language="JavaScript">
DataStore.Notify("about.endSheet");
</trigger>

These triggers open up the sheet when the About button and close it when the sheet's OK button is clicked.

Calling the search from a URL

One neat feature of a Sherlock is that you can have an installed channel perform a specific action from a URL. Try sherlock://com.apple.yellowPages?query=sushi. This URL calls the Yellow Pages channel with a search for sushi. We want users and Web developers to be able to offer a similar URL to search with our channel.

To do that, we need to put some code in a trigger at the URL.complete path. Remember that we should never repeat two significant pieces of code. But, thankfully, the main search code is already in a trigger prefixed with DATA, so all we need to do is write code to grab the query from the URL and call the search. Here's how:

<trigger language="JavaScript" path="URL.complete">
    query = DataStore.Get("URL.query");
    DataStore.Set("Internet.MainQueryField.objectValue", query);
      DataStore.Set("Internet.NetworkArrows.animating", true);
    DataStore.Notify("DATA.action.performSearch");
</trigger>

This code is called when someone uses a URL like sherlock://com.mac.stevej.macosxhints?query=sherlock (be sure to use your identifier). The first line in the trigger grabs the query from the URL, the second sets the search text field to it, the third starts the network arrows spinning, the fourth calls the search. Sherlock automatically un-escapes any escaped characters for you.

Getting Help

Though creating the above channel step-by-step will work fine, you'll undoubtedly want to create your own, unique channel. You'll also (most likely) run into a few problems along the way. First read through the official documentation. Though a large part of it will be review of this article, you'll probably learn some things that will be useful to you. Remember that the reference part of the documentation will be your best friend when you're trying to figure things out. In case the documentation doesn't help you, there's plenty of places to ask other developers for help:

  • Apple's sherlock-channel-development mailing list has many excellent developers on it who would, most likely, be able to answer any questions you might have about developing channels.
  • I'll be personally answering questions on the forums below, so you can certainly ask questions there

Getting your Channel to the World

After you've created your channel you need to share it with the world. The first step is to post it online. You ISP usually includes web space with your account, you can use your iDisk/.mac to host it, or you can use the Sherlock Channels free channel hosting. One thing to consider is that you want to your channel to be on a site you will own for the foreseeable future. Your don't want your users to have to deal with a channel that doesn't work, resubscribing, etc., just because you moved to a different ISP or web host.

After you post it, you'll also want to have it listed in a channel directory. I would recommend Sherlock Channels, a site devoted to listing Sherlock 3 channels.

Happy hacking!

Harold Martin is a freelance software developer and author. Visit him at his blog.


Return to the Mac Innovators Contest.


Return to the Mac DevCenter.






Copyright © 2000-2006 O’Reilly Media, Inc. All Rights Reserved.
All trademarks and registered trademarks appearing on the O'Reilly Network are the property of their respective owners.
For problems or assistance with this site, email