Using the System Tray from JavaScript with AIR

Adobe AIR is designed to be consistent across operating systems. The result is that AIR applications can also run consistently across operating systems. The reality however is that there are certainly places that operating systems differ significantly, yet present metaphors that users expect. Among those more pervasive differences is the Windows system tray. The same metaphor exists, but differently for Mac, and may not exist at all depending on your Linux distribution. Let’s take a look at what is involved to use JavaScript to interact with the system tray in a cross operating system fashion.

Note that just because you can write an AIR application to behave consistently, doesn’t mean that one cannot be written to focus on a specific operating system. If you chose to leave out support for other operating systems, then be aware that decision may come back to haunt you later.

When I set out to build this I did a little searching first and found a Flex example that originated from Sasa Radovanovic, and that was updated by Jeff Houser circa the Flex 3 Beta 3 timeline. What follows is a migration of that work from Flex to JavaScript, with a lot more verbiage around the why and how of the details. All things considered however, when you develop in AIR, you have many of the same classes available to you in both the Flash world and the JavaScript world. I encourage you to explore what’s available from both APIs.

When it comes to the system tray, it is generally common to have an icon that represents your application. This is important as your application itself won’t show up in the “start bar” when it has been minimized. On Mac, the equivalent is the “dock” and the icon that shows up there is generally the one associated with the application itself. In both cases, you can actually control at runtime how you want that icon to appear. With that in mind, let’s first start by loading our own custom icon.

var imgDock = null;
 
$( document ).ready( function() {
 
	// Loader to load the icon image
	// Use Loader not HTML image to get at bitmap data (pixels)
	var loader = new air.Loader();
 
	// Handle when the icon image is loaded
	// Load the icon image (in this case local)
	loader.contentLoaderInfo.addEventListener( air.Event.COMPLETE, doLoaderComplete );
	loader.load( new air.URLRequest( SPLASH_IMAGE ) );
 
	// Custom event handler for window closing
	// Want to prompt the user to see if they want to close or minimize
	nativeWindow.addEventListener( air.Event.CLOSING, doClosing );
 
} );

You might initially be inclined to use an IMG tag. The problem with this approach is that it doesn’t give you access to the raw pixel data that the operating system will expect. To get at this data we will use an instance of the Loader class. Like most things web, the loader works asynchronously, so we will want to register for notification when the icon is loaded. Since this is local (in this case) it should be nearly instantaneous, but still we need to wait at least that instant.

// Called when the icon image is loaded
// Setup systray/dock actions depending on operating system
function doLoaderComplete( evt )
{
	var isMac = null;
 
	// Get the bitmap data (pixels) of the icon for the system
	// The system will size and convert to the appropriate format
	imgDock = evt.target.content.bitmapData;
 
	if( air.NativeApplication.supportsSystemTrayIcon )
	{
		// Setup Windows specific system tray functionality
		air.NativeApplication.nativeApplication.icon.tooltip = SYSTRAY_TOOLTIP;
		air.NativeApplication.nativeApplication.icon.addEventListener( air.MouseEvent.CLICK, doTrayClick );		
 
		isMac = false;
	} else {
		isMac = true;
	}
 
	// Override the default minimize behavior and use our own
	nativeWindow.addEventListener( air.NativeWindowDisplayStateEvent.DISPLAY_STATE_CHANGING, doStateChange );
 
	// Setup a menu on the docked icon to restore or close
	air.NativeApplication.nativeApplication.icon.menu = createMenu( isMac );
}

Once our icon has been loaded, we will go ahead and get the pixels, referred to as bitmap data, of the image. We’ll store that in a reference for later use - specifically, we will show the icon in the system tray when the application is minimized and remove it when the window is restored. We’ll also want to put the icon back should the application window be minimized again.

What comes next is the main branch between Windows and Mac, and AIR gives you hooks to find out on which operating system your application is running. The NativeApplication class has a static property for “supportsSystemTrayIcon” and “supportsDockIcon” which represents Windows and Mac respectively. There are also many other useful properties on the NativeApplication class to include a check for menu support, and the flag to start the application when the user logs into the system.

If the application is running on a Windows operating system, then there are some additional options that we can leverage. Notably in this case are putting a tooltip on the system tray icon, and also listening for a click on the icon to restore the application window. Regardless of operating system, we want to catch a minimize gesture from the user and replace the default behavior with our own; namely minimize to the system tray and not the “start bar”.

// Called when the docked icon is clicked (Windows only)
// Calls a shared function to restore the window
function doTrayClick( evt )
{
	undock();	
}
 
// Called when the window is minimized
// Minimizes to systray/dock in place of traditional minimize
function doStateChange( evt )
{
	if( evt.afterDisplayState == air.NativeWindowDisplayState.MINIMIZED ) 
	{
		// Stop the default behavior
		// Call shared function to systray/dock
		evt.preventDefault();
		dock();
	}	
}

While we are at it, we will go ahead and create the menu on the system tray/dock icon for the application on the whole. I’ve made the effort here to track the operating system when I create the menu. This is subtle but important. In the case of Windows the common term to close an application is “Exit” while on a Mac the term is “Quit”. The menu items create here can call the same functionality, that part doesn’t differ per OS, but I wanted to present the user with terminology they’d expect to encounter.

// Called to create a menu on the systray/dock icon
// Takes operating system into consideration
function createMenu( isMac )
{
	var menu = new air.NativeMenu();
	var openItem = new air.NativeMenuItem( OPEN_ITEM );
	var exitItem = null;
 
	// Both operating systems have an option to open the window
	openItem.addEventListener( air.Event.SELECT, doOpenSelect );	
	menu.addItem( openItem );
 
	if( !isMac )
	{
		// Mac provides built-in "Quit" item
		// Create an "Exit" item for Windows
		exitItem = new air.NativeMenuItem( EXIT_ITEM );		
		menu.addItem( exitItem );
	}
 
	return menu;	
}

Before we talk about the actions in those specific menu items, let’s pause and talk about what the application has done so far, and is now displaying.

When the application started, it loaded a custom icon to use in the system tray/dock. Once that icon was loaded, we grabbed the bitmap data for reference, and then proceeded to create platform-specific hooks for the application when hidden; namely the menu. Now the application is running, displaying it’s main window, and expecting some fashion of user interaction. Let’s say the user is next ready to minimize the application.

Keeping in mind that we want this application to sit in the system tray/dock, not on the “start bar” we want to catch the default behavior and insert our own. We’ve already setup the application even handlers for minimize, so let’s take a closer look at the actual functionality. The first step is to prevent the actual minimize behavior, which can be done with the preventDefault() function call. Then to formally “dock” the application, we hide the main window, and associate the icon we loaded earlier with the application.

// Called to put the window on the systray/dock
// Hides window and puts icon in place
function dock()
{
	nativeWindow.visible = false;
	air.NativeApplication.nativeApplication.icon.bitmaps = [imgDock];	
}

Note that AIR can take a variety of sizes for the icon, and that they may differ depending on operating system. On Windows as an example, this icon tends to be displayed at 16×16. On a Mac it is very user-definable and may be anywhere between 16×16 and 128×128. You may want to account for this to get the best display for your icon, but when in doubt, the operating system will actually scale the icon image appropriately.

Now with the application safely tucked away, the user can continue on along with their business. At some point in the future we’ll assume that they want to bring your application back front and center however, and we’ve already laid the groundwork to make this happen. On Windows, clicking the system tray icon will “undock” the application, while selecting the “Open” context menu item on both Windows and Mac will perform the same function.

// Called to restore the window to normal mode
// Shared function called by multiple event handlers
function undock()
{
	// Show the window and bring it to the front
	nativeWindow.visible = true;
	nativeWindow.orderToFront();
 
	// Clear out the systray/dock icon (optional)
	air.NativeApplication.nativeApplication.icon.bitmaps = [];	
}

In the case of restoring the application window to it’s former glory, we’ll first want to make it visible. To make sure the user isn’t blocking our newly visible window with one of their own, we’ll also want to bring our application to the front. Finally we’ll pull any custom icon indicator from the application. On Windows this will remove the icon from the system tray while the window is visible.

You may want to keep the icon in the system tray at all times regardless of state, in which case you’ll not clear the icon. You’d also likely not wait to put the icon in place for when the user minimized the application, and make the assignment as soon as the image is loaded. The real detail here is that AIR doesn’t presume to know how your application should behave. You need to design the behavior you want, while also taking into consideration operating system differences.

Now your user can start the application, “dock” the main window to the system tray, and restore the window through some user gesture (which again is going to differ depending on operating system). How about closing your application once and for all?

// Called when the native window is starting to close
// Prompts the user to see if they want to close or minimize
function doClosing( evt )
{
	var answer = null;
 
	// Stop the default of actually closing the window
	evt.preventDefault();
 
	// Ask the user what action to take
	answer = confirm( "Select \"OK\" to exit or \"Cancel\" to minimize." );
 
	if( answer )
	{
		// If they actually want to close the application
		closeApplication();		
	} else {
		// If they really just want to minimize the window
		dock();	
	}
}

One of the lines of code we didn’t talk about up front was that we added a listener to the native window to catch the closing event. This happens before the window is actually closed, and gives us a chance to more formally handle the event. In this case, we will ask the user if they are sure they want to close. This is a good practice since the user may not know what gesture to provide to actually put the application into the system tray/dock.

Very much like the minimize event, we can prevent the default behavior by calling preventDefault(). Next we can use the good old JavaScript confirm() method to ask the user about their real intentions. If they really want to close the application, then we’ll go ahead and make that call at the application level. If they actually want to put the application into the system tray/dock, then we’ll call the “dock” method we discussed earlier during the minimize operation.

// Called to close the window
// Shared by multiple event handlers
function closeApplication()
{
	nativeWindow.close();	
}

As a summary, we first need to manage a custom icon, potentially at different sizes based on operating system, create the appropriate context menus for the application, and stop some of the default behavior and inject our own.

While that’s it in a nutshell, the variables get involved in how you want your application to behave when running on different operating systems. When does the icon get generated or put into the system tray? What should the context menu say to the user? How should the prompt to stop closing be worded depending on OS? These are all decision you will need to consider as you implement your own approach to docking. I’ve included a sample application with the above behaviors for your use as a template.

7 Responses to “Using the System Tray from JavaScript with AIR”

  1. Mike Ellis Says:

    Hi Kevin

    Thanks for this (and for your email..!).

    I’ve tried running this, and find that it works except when trying to restore the window - ie. it minimises ok to the system tray (I’m on Win XP) but a click on the icon doesn’t do anything at all.

    I’ve noticed an error when previewing - “can’t find variable: MouseEvent” which I guess is related to the line

    air.NativeApplication.nativeApplication.icon.addEventListener(air.MouseEvent.CLICK, doTrayClick);

    Can you help with this?

    Thanks in advance

    Mike

  2. Mike Ellis Says:

    I think I got it - I replaced this line with

    air.NativeApplication.nativeApplication.icon.addEventListener(’click’, doTrayClick);

    and seems to work…

    :-)

  3. Abhinav Singh Says:

    Thanks, worked like a charm :D

  4. Abhinav Singh Says:

    BTW why is this function in thr?

    function doExitSelect(evt) {
    closeApplication();
    }

    I dnt see it being called from anywhere.

  5. sujith subramanian Says:

    Hi,

    Tried using your code.

    The second alternative of air.NativeApplication.nativeApplication.icon.addEventListener(’click’, doTrayClick); gave an error and when use your original code, I cant do anything with the icon (I click on the icon and nothing happens).

    Could you please guide me on this??

    Thanks in advance,

    Sujith

  6. Charlie Says:

    Hi Mike Ellis,

    Your piece of code worked for me. Thanks a lot.

    Charlie
    http://www.twitter.com/haicharlie143

  7. Leslie Bertels Says:

    Hi,

    is it possible to override the build-in “Quit” taskbar item on Mac.
    I’ve been trying to find any info on that but without any luck.

    Any help is appreciated.

    Regards,
    Les.

Leave a Reply

You must be logged in to post a comment.


order cialis in canada clomid without prescription lasix for sale synthroid prescription discount cialis overnight delivery buy generic propecia order no rx viagra buy viagra low price buy viagra online viagra sale cheapest viagra buy cialis from india buy cheap acomplia online buy clomid cheap purchase clomid order discount viagra online where to buy viagra price of lasix price of propecia soma without prescription purchase clomid online find viagra no prescription required buy generic zithromax synthroid online stores price of synthroid purchase lasix cialis approved cheapest generic viagra online find viagra cialis pharmacy online best price viagra buy cheapest cialis on line cheapest viagra price buying cialis lasix generic order cheap cialis find viagra online buy cialis lowest price best price for viagra purchase zithromax lowest price soma cheapest generic cialis order cialis online cialis free delivery lowest price viagra purchase viagra no rx order cheap cialis online viagra australia discount clomid cheap synthroid tablets cheap cialis pharmacy online zithromax online synthroid buy viagra on internet levitra prescription viagra tablets sale cialis cialis price buy cheap clomid online cheap viagra in canada buy clomid online buy generic viagra cheap viagra from canada cialis in bangkok discount viagra online cialis australia acomplia for sale buy cialis no rx buy levitra without prescription viagra online stores buy cheap viagra online viagra cheapest price viagra rx