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>
resultsArea
searchYahoo
front
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="+searchValue+"&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=DashboardSearch&query=Michelle&results=5
…Stage 2 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('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="+searchValue+"&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="+searchValue+"&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
Without overflow:hidden
With overflow:hidden
#resultsArea { background-color: white; position: absolute; font-size: small; top: 90px; left: 30px; width: 240px; height: 190px; 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
searchYahoo
front
#resultsArea { background-color: white; background-image: none; position: absolute; font-size: small; top: 90px; left: 30px; width: 240px; height: 190px; overflow:hidden;}
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
<input type="search" id="searchYahoo” size="30" onSearch="doSearch(this.value)" />
Change:
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 Browserfunction 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/Dashboard-date.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]