Michelle Hedstrom May 17, 2005

Michelle Hedstrom
May 17, 2005
Agenda
•
•
•
•
•
•
Introduction to Dashboard/Widgets
Building your first widget
Adding on - web searching!
Scrollbars and saved searches
What’s Next
Debugging
What is Dashboard?
•
•
•
•
Part of Mac OS 10.4 (Tiger).
Lets You Run Mini-Applications (Widgets).
Pretty wrapper around Apple’s WebKit.
Virtual Layer - only comes when called.
What is a Widget?
•
•
•
•
Mini application - can run in Safari.
Stored as a bundle.
HTML, JavaScript, & CSS.
Information, application, or accessory.
 New widget object
Components of a Widget
• HTML Files, Info.plist, Default.png,
Icon.png
• Optional: JavaScript & CSS Files
Picture from http://developer.apple.com/macosx/dashboard.html
Making a Widget
• Put all required files in a folder.
• Add on to folder name .wdgt extension turns into bundle.
• Double-click .wdgt file to launch in
Dashboard.
• Optionally, move to ~/Library/Widgets.
We’re Not Talking About…
• Error checking
• Preferences
• Most design conventions
Aqua
bad
Custom
good
Tools Needed
•
•
•
•
A Mac with Tiger (OS 10.4) installed
Text editor
Graphics editor (optional)
Xcode Tools (optional)
Build Stage 1
• Create the widget itself.
• Put in the background graphic.
• Add in the search form field with results
area.
Stage 1 HTML
<html>

<head>
<link rel="stylesheet" href="YahooClass.css" type="text/css" />
<title>Yahoo Class Widget</title>
</head>
<body>
<div class="front">
<input type="search" id="searchYahoo" size="30" />
<div id="resultsArea">
</div>
</body>
</html>
front
searchYahoo
resultsArea
Stage 1 CSS
.front {

width: 290px;
height: 314px;
background-image: url(Default.png);
}
.searchYahoo {
position: absolute;
top: 60px;
left: 40px;
text-align:left;
}
#resultsArea {
background-color: white;
position: absolute;
font-size: small;
top: 90px;
left: 30px;
width: 240px;
height: 190px;
}
Stage 1 Info.plist
•CFBundleDisplayName - actual widget name displayed in Finder and widget
bar.
•CFBundleIdentifier - Uniquely identifying string in reverse domain format.
•CFBundleName - Name of widget, must match name of bundle on disk
minus the .wdgt extension.
•CFBundleVersion - version number.
•MainHTML - name of the main HTML file implementing widget.
Build Stage 2
• Run search when
user types in search
terms.
• Display results in
results box.
• Clicking on URL
opens in widget
window.
Stage 2 Info.plist
•AllowNetworkAccess - if widget requires any
network resources.
Stage 2 HTML
html>
<
<head>
<link rel="stylesheet" href="YahooClass.css" type="text/css" />
<script language="JavaScript" src="Main.js"></script>
<title>Yahoo Class Widget</title>
</head>
<body>
<div class="front">
<input type="search" id=“searchYahoo” size="30" onSearch="doSearch(this.value)" />
<div id="resultsArea">
</div>
</div>
</body>
</html>
Stage 2 JS - Vars
var baseSearchURL =

"http://api.search.yahoo.com/WebSearchService/V1/";
var searchMethod = "webSearch";
var appID = "DashboardSearch";
var numResultsReturned = "5";
var req;
•Requirements of Yahoo Search API.
•Num results returned optional - defaults to 10 if not
provided.
Stage 2 JS - doSearch()
function

doSearch(searchValue) {
var searchURL=
baseSearchURL+searchMethod+"?"+"appid="+appID+"&query="+search
Value+"&results="+numResultsReturned;
req = new XMLHttpRequest();
req.open("GET",searchURL,false);
req.send(null);
document.getElementById('resultsArea').innerHTML =
(parseXML(req.responseXML));
}
•Building the search URL:
•http://api.search.yahoo.com/WebSearchService/V1/webSearch?appid=DashboardSea
rch&query=Michelle&results=5
…Stage 2 JS - doSearch()…
function

doSearch(searchValue) {
var searchURL=
baseSearchURL+searchMethod+"?"+"appid="+appID+"&query="+search
Value+"&results="+numResultsReturned;
req = new XMLHttpRequest();
req.open("GET",searchURL,false);
req.send(null);
document.getElementById('resultsArea').innerHTML =
(parseXML(req.responseXML));
}
•HTTP request that returns XML DOM object.
•req.open with false boolean - synchronous mode just for demo.
Asynchronous better - check for onreadystatechange event.
•req.send arg. used to transmit content for a POST request, null otherwise.
…Stage 2 JS - doSearch()…
function

doSearch(searchValue) {
var searchURL=
baseSearchURL+searchMethod+"?"+"appid="+appID+"&query="+search
Value+"&results="+numResultsReturned;
req = new XMLHttpRequest();
req.open("GET",searchURL,false);
req.send(null);
document.getElementById('resultsArea').innerHTML =
(parseXML(req.responseXML));
}
•Looks for a div called resultsArea.
•Gets the area where the content is stored for that div.
…Stage 2 JS - doSearch()
function

doSearch(searchValue) {
var searchURL=
baseSearchURL+searchMethod+"?"+"appid="+appID+"&query="+search
Value+"&results="+numResultsReturned;
req = new XMLHttpRequest();
req.open("GET",searchURL,false);
req.send(null);
document.getElementById('resultsArea').innerHTML =
(parseXML(req.responseXML));
}
•Gets results back of search as a document object.
•Passes off to parseXML function.
Stage 2 JS - parseXML()
function parseXML(xmlResults) {

var resultsStrings = "";
var indResponse = xmlResults.getElementsByTagName("Result");
for (i=0; i<indResponse.length; i++) {
var title = indResponse[i].getElementsByTagName("Title")[0].firstChild.nodeValue;
var URL = indResponse[i].getElementsByTagName("ClickUrl")[0].firstChild.nodeValue;
var summary =
indResponse[i].getElementsByTagName("Summary")[0].firstChild.nodeValue;
resultsStrings += "<A HREF=\""+URL+"\">"+title+"</a><br>"+summary+"<p>";
}
return resultsStrings;
}
•Find all nodes named Result.
•Each individual search result is one such XML node.
…Stage 2 - parseXML()
function parseXML(xmlResults) {

var resultsStrings = "";
var indResponse = xmlResults.getElementsByTagName("Result");
for (i=0; i<indResponse.length; i++) {
var title = indResponse[i].getElementsByTagName("Title")[0].firstChild.nodeValue;
var URL = indResponse[i].getElementsByTagName("ClickUrl")[0].firstChild.nodeValue;
var summary =
indResponse[i].getElementsByTagName("Summary")[0].firstChild.nodeValue;
resultsStrings += "<A HREF=\""+URL+"\">"+title+"</a><br>"+summary+"<p>";
}
return resultsStrings;
}
•Iterate through all results, from high level object to value of
each individual field we want.
•Format all results in way we want with clickable title.
Stage 2 - CSS
#resultsArea {

background-color: white;
position: absolute;
font-size: small;
top: 90px;
left: 30px;
width: 240px;
height: 190px;
overflow: hidden;
}
Without
overflow:hidden
With overflow:hidden
Build Stage 3
• Scroll bars for results.
• Clicking on URL
opens in default
browser.
• Saved keyword
search.
Scrollbar
• Custom built scrollbar - Apple code
• “Define a parent DIV with overflow CSS
property set to hidden”
#resultsArea {

background-color: white;
background-image: none;
position: absolute;
font-size: small;
top: 90px;
left: 30px;
width: 240px;
height: 190px;
overflow:hidden;
}
front
searchYahoo
resultsArea
Scrollbar Step 2
• “Place all scrollable content in a div within
the parent div: this ‘child’ div should have
an undefined overflow, or a value of
visible.”
<div id=“resultsArea”>
<div id=“mainContent”>
</div
</div>
•Changed resultsArea to
mainContent in Main.js
Scrollbar Step 3
• “Declare the Scroller divs and place them
inside the parent div.”

<div id="resultsArea">
<div id="mainContent">
</div>
<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>
</div>
Scrollbar Step 4
• “It is important that the child div start with a
CSS top property set to 0;”
#mainContent {

position:absolute;
left:0;
top: 0;
right:35px;
}
Scrollbar Step 5
• Scrollbar CSS from Apple
myScrollBar {
#
/*
border-style:solid; border-color:yellow; */
position:absolute;
top:6px;
bottom:14px;
right:0px;
width:19px;
display:block;
-apple-dashboard-region:dashboard-region(control rectangle);
}
/*
Scroller track
*/
.scrollTrackTop {
/*
border-style:solid; border-color:red; */
position:absolute;
top:0px;
width:19px;
height:18px;
background:url(Images/top_scroll_track.png) no-repeat top left;
}
…
apple-dashboard-region
#myScrollBar {
/* border-style:solid; border-color:yellow; */
position:absolute;
top:6px;
bottom:14px;
right:0px;
width:19px;
display:block;
-apple-dashboard-region:dashboard-region(control rectangle);
}
•Region used for specific purpose.
•1st param - type of region defined.
•2nd param - shape of region
Stage 3 HTML

…
<head>
<link rel="stylesheet" href="YahooClass.css" type="text/css" />
<script language="JavaScript" src="Main.js"></script>
<script language="JavaScript" src="Scroller.js"></script>
<title>Yahoo Class Widget</title>
</head>
<body onload='setup();'>
…
•Use the JavaScript file provided by Apple.
•Call a setup function after load.
Stage 3 JS - setup()
function setup() {

scrollerInit(document.getElementById("myScrollBar"),
document.getElementById("myScrollTrack"),
document.getElementById("myScrollThumb"));
document.getElementById('mainContent').innerHTML = "";
calculateAndShowThumb(document.getElementById('mainContent'));
}
•Call Apple defined init function.
•Set our content area to empty.
•Figure out the height of the view, and make
the thumb proportional (Apple defined func.).
Stage 3 JS - doSearch()
function doSearch(searchValue) {

var searchURL= baseSearchURL+searchMethod+"?"+"appid="+appID+”
&query="+searchValue+"&results="+numResultsReturned;
req = new XMLHttpRequest();
req.open("GET",searchURL,false);
req.send(null);
document.getElementById('mainContent').innerHTML =
(parseXML(req.responseXML));
calculateAndShowThumb(document.getElementById('mainContent'));
}
•Resize the scrollbar for the current amount of
text in the area.
Saved Keyword Search
Change:
<input type="search" id="searchYahoo” size="30"
onSearch="doSearch(this.value)" />
To:
<input type="search" id="searchYahoo" size="30" results="5"
onSearch="doSearch(this.value)" />
•Results is number of past searches saved.
Open URL in Default Browser
function parseXML(xmlResults) {

var resultsStrings = "";
indResponse = xmlResults.getElementsByTagName("Result");
for (var i=0; i<numResultsReturned; i++) {
var title =
indResponse[i].getElementsByTagName("Title")[0].firstChild.nodeValue;
var URL =
indResponse[i].getElementsByTagName("ClickUrl")[0].firstChild.nodeValue;
var summary =
indResponse[i].getElementsByTagName("Summary")[0].firstChild.nodeValue;
if (window.widget)

{
resultsStrings += "<A HREF=\"javascript:widget.openURL('"+URL+"')\">";
}
else {
resultsStrings += "<A HREF=\""+URL+"\">";
}
resultsStrings += title+"</a><br>"+summary+"<p>";
}
return resultsStrings;
}
What’s Next?
• Truncate summary or build summary box
at bottom of screen.
• Preferences to support language, type of
search, num results returned.
Debugging
• Safari JavaScript Console
 Open Terminal window, type:
• defaults write com.apple.Safari IncludeDebugMenu 1
 Relaunch Safari, check “Log JavaScript Exceptions” in Debug Menu
 Choose “Show JavaScript Console” in Debug menu.
• Write directly to JS Console
 window.console.log(“Problem if you get here")
 Shows up in dark green
• Create Debug div
• Good ‘ol alerts
Resources
• Developing Dashboard Widgets http://developer.apple.com/macosx/dashboard.html
• Apple Dashboard Documentation http://developer.apple.com/documentation/AppleApplications/Dashboarddate.html
•
•
•
•
•
Yahoo Search API - http://developer.yahoo.net/
/Library/WidgetResources for premade graphics/buttons
/Developer/Examples/Dashboard
/Developer/Applications/Utilities/Property List Editor
Me! - [email protected]