So, I’ve been issued a challenge at SitePoint to develop a piece of software in order to write an article about the experience. I can honestly say it’s so far been a real revalation – an experience that has made me betray all my deeply held web standards principles. Did I forget all about semantic markup? Yes. Did I completely ignore cross-browser compatibility? Yes. Did I even consider using HTML tables for layout? Yes. Did I feel unclean but excited all at the same time? YES!
I built a desktop application with Adobe AIR … and I liked it!
Yep, using free software and HTML, CSS, and jQuery, I made my own cross-platform desktop application using Adobe AIR. Well, the beginnings of one at least. Pay attention when you read this article because there’ll be a test afterwards. Answer the quiz, and you could win one of 100 free copies of Getting Started With Adobe Flex 3.
The challenge was to build a useful application (rather than another to-do list, thankfully) to make use of the new flippa.com web site – it’s a marketplace for buying and selling web sites. So here’s the idea. Imagine you’re interested in buying a web site about photography with a forum attached. You open this application, fill in the custom search form, and see a list of matching auctions. The app will let you pick the auctions you want to watch and it’ll notify you every time there’s a new bid. Also, it will keep polling the site to see if there are any new auctions that match your search query, using nothing but HTML, CSS, jQuery, and the publicly available Atom feeds from flippa.com.
And, I want to call it Harpoon.
Development Environment
AIR uses the WebKit rendering engine also used in Safari and Chrome, so even though you’re using standard web development technology you’re only developing for one rendering engine. All it took to make the application up to this point was a single HTML (ui.html) and CSS file (styles.css), and some supporting JavaScript files.
There is an application.xml
configuration file but you can rely on Aptana to do that for you. Aptana has an AIR plugin that makes development push-button simple. You can read about how to set it up in the article Learn Adobe AIR, Part I: Build A Note Storage App by Akash Mehta. Other than having a handy run button that lets you test-run your AIR app and debug, it presents the application.xml file in a tabbed form for easy editing.
Other than using Aptana, you can edit the files in whichever editor you like. Akash has also written an article that describes this process called Walk on AIR: Create a To-do List in Five Minutes.
Window Layout
One of the cool tricks you can do with AIR is make an app without window chrome and a transparent body. This enables you to style your app any way you like. My demo is fairly conservative but I imagine that designers with a greater talent for graphics will be able to go crazy and create all sorts of imaginative application layouts.
Here’s my amateur effort:
The plan was to create the following: a title bar for the app title, minimize controls, quit controls, a footer that will house the controls to switch application modes, and the bottom-corner window-resize handle. In between the title bar and the footer is the main application area.
In the Application XML Window Settings you need to set Window Style to Custom Chrome (transparent) as pictured previously. I’ll also enable Minimizable and Resizable in Sizing Options.
After messing around for a while with positioning and trying to make the element heights right – it’s actually quite tricky using CSS to make a fixed header and footer with a scrollable content area and still allow for window resizing – here’s the HTML I came up with:
<body>
<div id="wrapper">
<div id="app">
...
</div> <!-- app -->
<div id="head">
...
</div>
<div id="foot">
...
</div>
</div> <!-- wrapper -->
</body>
The body
element is transparent and has some padding applied in order for the background image to be seen outside of our custom window chrome.
Here’s the CSS for our body element:
body {
font-family: Helvetica, Tahoma, Arial, sans-serif;
font-size:14px;
padding: 50px 0 20px 55px;
margin: 0;
background: transparent url(icons/harpoon.png) no-repeat fixed;
}
The application interface is wrapped inside the (imaginatively named) wrapper
element. This element has a height
and max-height
set to 100%. We do this so that the wrapper
element always fills the viewport but never expands to a size greater than the viewport, which would cause ugly scrollbars to appear and ruin the layout. The wrapper
element also needs to be relatively positioned to provide a positioning context for the head
and foot
elements.
Here’s the CSS:
#wrapper {
position: relative;
padding: 0;
margin: 0;
height: 100%;
max-height: 100%;
}
The head
and foot
elements are both absolutely positioned (relative to the wrapper element): the head element with top: 0;
and bottom: 0;
for the foot
element. There’s more to consider though, because while I want these elements to be 100% wide, I also want them to have a rounded border. So, what’s the problem? Well, if you specify a width of 100% and add borders, according to the rules of the box model the border increases the width of the box, making them wider than 100%. This will cause all sorts of ugly problems.
The answer is the -webkit-box-sizing
property. The box-sizing
property is a CSS3 property that switches the box model used for that element. WebKit supports it in a custom vendor-extension. If we set it to border-box
, the borders (and padding) will be included in the specified width (just like the old IE5.x box model!). You know, I honestly thought I’d never find a need to use that property. Here it lets me specify a width of 100% and then apply padding and borders without worrying that it’ll break the layout.
It’s also worth noting I’ve used the WebKit custom border-radius
extension to make nice rounded borders. Much has been written about this property so I’ll leave out my two cents worth here. You’ll also notice I specify the move
cursor for the head element. That’s because it’ll eventually become the grab handle for moving our window around.
Here’s the CSS:
#head, #foot {
position: absolute;
-webkit-box-sizing: border-box;
border: 6px solid #3378be;
width: 100%;
padding: 0 10px;
background-color: #3378be;
}
#head {
top: 0;
height: 35px;
-webkit-border-top-left-radius: 6px;
-webkit-border-top-right-radius: 6px;
cursor: move;
}
#foot {
bottom: 0;
height: 50px;
-webkit-border-bottom-left-radius: 6px;
-webkit-border-bottom-right-radius: 6px;
}
Now we come to the fun part. The app
element needs to grow and shrink according to window size but also be scrollable. To make the head and foot elements fixed while allowing the app
element to scroll requires the use of the position: fixed;
CSS property value. A fixed positioning means the element is positioned in relation to the viewport. You probably forgot that fixed
was even valid for the position
property. Lack of IE6 support has historically kept it in the dark. But there’s no need to worry about that here; WebKit supports it fine. Actually, IE7 and above supports it now too, so you can probably start considering it when designing web layouts.
After applying position: fixed;
to the app
element we also specify the top
, bottom
, left
, and right
properties. By doing so there’s no need to specify height
or width
; the element will be automatically resized to suit. We’ve also added a min-height
and overflow: auto;
to cause the scrollbar to appear, if necessary. Here’s the CSS with the figure below displaying the result:
#app {
position: fixed;
top: 80px;
bottom: 70px;
left: 55px;
right: 0;
overflow: auto;
min-height: 300px;
...
}
There you have it, our layout is complete. And just to show you how flexible it is, here’s the application displaying a list of search results while having been resized:
Window Controls
Ignoring the application area in the middle for the moment, the next step will be to wire up all the window controls, like the minimize and quit buttons, window dragging, and the resizing handle. Here’s the plan:
As it turns out adding custom window controls is very easy.
The minimize and quit buttons are simply a couple of HTML buttons with some CSS added. Here’s the markup:
<div id="window_control">
<button id="minimise_control">-</button>
<button id="quit_control">x</button>
</div>
The CSS removes the borders and applies a background-color
, and a few other properties:
#window_control button {
border: none;
background-color: #3378be;
color: #dae6f3;
font-weight: bold;
cursor: pointer;
}
The effect of the CSS can be seen in the figure below, with the finished button on the right:
While we can use jQuery to wire up the events that drive the button functions, we also need an AIR JavaScript file for the window API. AIR provides a file called AIRAliases.js
that simply exposes a lot of AIR API functions. I’ve added the following to the HTML head of my ui.html
file:
<script type="text/javascript" src="lib/air/AIRAliases.js"></script>
<script type="text/javascript" src="lib/jquery/jquery-1.3.2.min.js"></script>
<script type="text/javascript" src="scripts/harpoon.js"></script>
The harpoon.js
file is where I keep all the custom JavaScript that drives the app.
First we create a function that’s called when the application starts. Within this function we add event listeners to the window control buttons that will call the appropriate AIR functions. I like to keep all my custom JavaScript neatly namespaced so that I can see where the code calls a custom function instead of a jQuery or AIR function. Actually, I do it mostly out of habit, but it’s a good habit to have. So here’s the Harpoon
object and a function called init
that will be called when the application starts; the standard jQuery method works perfectly for this:
var Harpoon = {
init : function() {
// application setup happens here
}
}
$(document).ready(function() {
Harpoon.init();
});
We then create a function called setupWindow
and call it from our init
function:
var Harpoon = {
init : function() {
Harpoon.setupWindow();
},
setupWindow : function() {
}
}
Now we’re ready to add the event listeners. In our setupWindow
function we bind event listeners for the click
event to each button. These will in turn call our custom functions: Harpoon.minimize
and Harpoon.quit
:
setupWindow : function() {
$('#minimise_control').bind('click', function(event){
event.preventDefault();
Harpoon.minimize();
});
$('#quit_control').bind('click', function(event){
event.preventDefault();
Harpoon.quit();
});
}
The custom functions call the approriate AIR functions. We use the nativeWindow.minimize
function for the minimize control :
minimize : function() {
nativeWindow.minimize();
}
That’s all we need to do for the minimize button (did you think this was going to be hard?). The function for the quit button is a little more complex, but only a little:
quit : function() {
var exitingEvent = new air.Event(air.Event.EXITING, false, true);
air.NativeApplication.nativeApplication.dispatchEvent(exitingEvent);
if (!exitingEvent.isDefaultPrevented()) {
air.NativeApplication.nativeApplication.exit();
}
}
This is the standard approach recommended by Adobe. The air.NativeApplication.nativeApplication.exit
function is the one that actually closes the application, but the risk of calling this function is that you may have unsaved data you can lose. So instead Adobe recommend that you first create a new event called an exiting event:
var exitingEvent = new air.Event(air.Event.EXITING, false, true);
Then you dispatch this event to your application:
air.NativeApplication.nativeApplication.dispatchEvent(exitingEvent);
In your application, if there’s any data you risk losing when the application quits, you create an event listener and listen for the exiting event. This event listener should cancel the exiting event and save any relevant data before the application exits. This is what our quit function is testing. If the exiting event is not cancelled then it’s okay to exit the application:
if(!exitingEvent.isDefaultPrevented()) {
air.NativeApplication.nativeApplication.exit();
}
We’ll make use of this facility in our application later on.
The window resize handle is a transparent PNG image placed in the bottom-right corner of the foot
element:
<img id="resize_control" src="icons/resize.png">
#resize_control {
position: absolute;
right: -3px;
bottom: -3px;
width: 20px;
height: 20px;
}
Here’s the transparent PNG image used and its final appearance in the application:
It’s wired up in exactly the same way as the minimize and quit buttons. We add some code to our setupWindow
function, this time listening for the mousedown
event instead of the click
event:
$('#resize_control').bind('mousedown', function(event){
event.preventDefault();
Harpoon.resize();
});
Then we add the custom function to the Harpoon
object:
resize : function() {
nativeWindow.startResize(air.NativeWindowResize.BOTTOM_RIGHT);
}
You can add resize controls that resize the window from various directions. We’ve specified the bottom-right direction here.
Making our window draggable is just as easy as creating the other window controls. We’ll use the head
element as the handle, as that seems to be the most intuitive for users. Once again we add some code for our event listener (again the mousedown
event):
$('#head').bind('mousedown', function(event){
event.preventDefault();
Harpoon.move();
});
And the custom function:
move : function() {
nativeWindow.startMove();
}
Our custom JavaScript file now looks like this:
var Harpoon = {
init : function() {
Harpoon.setupWindow();
},
minimize : function() {
nativeWindow.minimize();
},
resize : function() {
nativeWindow.startResize(air.NativeWindowResize.BOTTOM_RIGHT);
},
move : function() {
nativeWindow.startMove();
},
quit : function() {
var exitingEvent = new air.Event(air.Event.EXITING, false, true);
air.NativeApplication.nativeApplication.dispatchEvent(exitingEvent);
if (!exitingEvent.isDefaultPrevented()) {
air.NativeApplication.nativeApplication.exit();
}
},
setupWindow : function() {
$('#minimise_control').bind('click', function(event){
event.preventDefault();
Harpoon.minimize();
});
$('#quit_control').bind('click', function(event){
event.preventDefault();
Harpoon.quit();
});
$('#resize_control').bind('mousedown', function(event){
event.preventDefault();
Harpoon.resize();
});
$('#head').bind('mousedown', function(event){
event.preventDefault();
Harpoon.move();
});
}
}
$(document).ready(function() {
Harpoon.init();
});
For all our efforts, we now have custom-designed and completely functional window chrome for our application.
As far as Harpoon is concerned there’s still a long way to go. The window design is unfinished and the application is functional but hardly complete. At the moment all you can do is perform a search and it’ll use the custom search feeds from flippa.com for the results. Some of the challenges ahead include storing data in a database, making use of the auction bid feeds on flippa.com, refreshing the results automatically, and alerting the user. I’ll keep you posted!
You can download all the Harpoon files and have a look yourself. Download Aptana and start your own AIR project; you can use the Harpoon window as a starting point if you like. It’s a lot of fun, once you’ve started.
If you’d like to install Harpoon, first grab Adobe AIR, and then download Harpoon.
Remember to have a crack at the quiz and win a great prize!
iOS Developer, sometimes web developer and Technical Editor.