How do you put an AIR App in the System Tray?

I'm being watched. I don't know exactly what that means, though. I have to admit, it's a bit weird. That feed is in my list of blogs to read, but otherwise I'm not sure what that post means. Anyone know who Iconara.net is or why they want to watch me?

Anyway, I'll be speaking this year on Code Reuse with Flex and AIR. The intent of the topic is to talk about how to write code that you can then reuse in both a Flex and AIR application. ( Dan contacted me asking for clarification ). I'll be giving this presentation next week in Connecticut at CFUGitives ( The Southwest CT Adobe User Group) and at the Hartford Adobe User Group. Next month I'll be giving it at the Boston User Group and the 360Flex conference in Atlanta. That's a busy schedule, I think.

Anyway, I'm working on my presentation, and wanted to use a moment to try put my AIR app into the system tray. AFter some search, it appears that Saskovic already did the work for me. I took his code and discovered it wouldn't compile on Beta 3. I did a quick read through of the AIR release notes and was able to convert his code to work on the latest release.

Here it is:

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="initApplication()">
<mx:Script>
   <![CDATA[
   /**
   * This example describes how to dock an AIR application to the system tray
   * and then undock it again.
   * The minimize and close actions of the WindowedApplication are caught, so
   * that we can introduce our own actions.
   *
   * A simple systray menu is presented, to show the usage of that.
   *
   * @Author: S.Radovanovic
   * @Url: http://www.saskovic.com/blog/?p=5
   * @Date: 24-11-2007
   * @Author: Jeffry Houser
   * @Date: 09-01-2007
   * Updated for AIR Beta 3
   */

   import mx.controls.Alert;
   import mx.events.CloseEvent;

   private var dockImage:BitmapData;
   
   /**
   * Initialize the application to the default values.
   * This method is called upon creationComplete from the Windowed Application
   *
   * @Author: S.Radovanovic
   */
   public function initApplication():void {
   
      //Use the loader object to load an image, which will be used for the systray       //After the image has been loaded into the object, we can prepare the application       //for docking to the system tray
      var loader:Loader = new Loader();
      loader.contentLoaderInfo.addEventListener(Event.COMPLETE, prepareForSystray);
      loader.load(new URLRequest("http://www.saskovic.com/images/systray_icon_16.png"));
      
      //Catch the closing event so that the user can decide if it wants to dock or really       //close the application
      this.addEventListener(Event.CLOSING, closingApplication);
   }

   /**
   * Check if the user wants to close the application or dock it
   *
   * @Author: S.Radovanovic
   */
   private function closingApplication(evt:Event):void {
      //Don't close, so prevent the event from happening
      evt.preventDefault();
      
      //Check what the user really want's to do       //Alert.buttonWidth = 110;
      Alert.yesLabel = "Close";
      Alert.noLabel = "Minimize";
      Alert.show("Close or minimize?", "Close?", 3, this, alertCloseHandler);
   }

   // Event handler function for displaying the selected Alert button.
   private function alertCloseHandler(event:CloseEvent):void {
      if (event.detail==Alert.YES) {
         closeApp(event);
      } else {
         dock();
      }
   }


   /**
   * Check to see if the application may be docked and set basic properties
   *
   * @Author: S.Radovanovic
   */

   public function prepareForSystray(event:Event):void {
   
      //Retrieve the image being used as the systray icon
      dockImage = event.target.content.bitmapData;
      
      //For windows systems we can set the systray props       //(there's also an implementation for mac's, it's similar and you can find it on the net... ;) )
      if (NativeApplication.supportsSystemTrayIcon){
         setSystemTrayProperties();
         
         //Set some systray menu options, so that the user can right-click and access functionality          //without needing to open the application          
SystemTrayIcon(NativeApplication.nativeApplication .icon).menu = createSystrayRootMenu();
      }
   }

   /**
   * Create a menu that can be accessed from the systray
   *
   * @Author: S.Radovanovic
   */
   private function createSystrayRootMenu():NativeMenu{
      //Add the menuitems with the corresponding actions
      var menu:NativeMenu = new NativeMenu();
      var openNativeMenuItem:NativeMenuItem = new NativeMenuItem("Open");
      var exitNativeMenuItem:NativeMenuItem = new NativeMenuItem("Exit");

      //What should happen when the user clicks on something...       

openNativeMenuItem.addEventListener(Event.SELECT, undock);

      exitNativeMenuItem.addEventListener(Event.SELECT, closeApp);

      //Add the menuitems to the menu
      menu.addItem(openNativeMenuItem);
      menu.addItem(new NativeMenuItem("",true));
//separator
      menu.addItem(exitNativeMenuItem);
      
      return menu;
   }

   /**
   * To be able to dock and undock we need to set some eventlisteners
   *
   * @Author: S.Radovanovic
   */
   private function setSystemTrayProperties():void{
      //Text to show when hovering of the docked application icon       
SystemTrayIcon(NativeApplication.nativeApplication .icon).tooltip = "Systray test application";
      
      //We want to be able to open the application after it has been docked       
SystemTrayIcon(NativeApplication.nativeApplication .icon).addEventListener(MouseEvent.CLICK, undock);
      
      //Listen to the display state changing of the window, so that we can catch the minimize       
stage.nativeWindow.addEventListener(NativeWindowDisplayStateEvent.DISPLAY_STATE_CHANGING, nwMinimized); //Catch the minimize event
   }

   /**
   * Do the appropriate actions after the windows display state has changed.
   * E.g. dock when the user clicks on minize
   *
   * @Author: S.Radovanovic
   */
   private function nwMinimized(displayStateEvent:NativeWindowDisplayStateEvent):void {
   
      //Do we have an minimize action?       //The afterDisplayState hasn't happened yet, but only describes the state the window will go to,       //so we can prevent it!
      if(displayStateEvent.afterDisplayState == NativeWindowDisplayState.MINIMIZED) {
         //Prevent the windowedapplication minimize action from happening and implement our own minimize          //The reason the windowedapplication minimize action is caught, is that if active we're not able to          //undock the application back neatly. The application doesn't become visible directly, but only after clicking          //on the taskbars application link. (Not sure yet what happens exactly with standard minimize)
         displayStateEvent.preventDefault();
         
         //Dock (our own minimize)
         dock();
      }
   }

   /**
   * Do our own 'minimize' by docking the application to the systray (showing the application icon in the systray)
   *
   * @Author: S.Radovanovic
   */
   public function dock():void {
      //Hide the applcation
      stage.nativeWindow.visible = false;
      
      //Setting the bitmaps array will show the application icon in the systray
      NativeApplication.nativeApplication .icon.bitmaps = [dockImage];
   }

   /**
   * Show the application again and remove the application icon from the systray
   *
   * @Author: S.Radovanovic
   */
   public function undock(evt:Event):void {
      //After setting the window to visible, make sure that the application is ordered to the front,       //else we'll still need to click on the application on the taskbar to make it visible
      stage.nativeWindow.visible = true;
      stage.nativeWindow.orderToFront();
      
      //Clearing the bitmaps array also clears the applcation icon from the systray
      NativeApplication.nativeApplication .icon.bitmaps = [];
   }


   /**
   * Close the application
   *
   * @Author: S.Radovanovic
   */
   private function closeApp(evt:Event):void {
      stage.nativeWindow.close();
   }
]]>
</mx:Script>
   
</mx:WindowedApplication>

I'm not sure if the Blog formatting may have added extra line breaks or not in the comments, so beware of that.

Over the next 3-6 months, I want to blog about the process I use in creating an application, from conception to completion. My plan is to build an "RSS Watcher". Something that can sit in my sustem tray, watch an RSS Feed, and show a pop-up window whenever an RSS Feed changes. I have a preliminary version of this that I hacked together as an example in the presentation. IT uses Saskovic's code along with code from the Adobe example app Wordup.

Comments
Dan's Gravatar Thanks for the props! And yes, you're aren't just being paranoid; it IS weird that someone is "watching" you...
# Posted By Dan | 1/8/08 9:41 AM
Tom Cornilliac's Gravatar Wow! you know you've arrived in the Coldfusion/Flex communities when you have your very own stalker ;-) I'm not sure who Iconara is but I think what he/she was trying to point out is that your name is showing up in their Google Analytics data. Probably your network name or machine name. Either way it's kinda a creepy thing to post to your blog, twitter maybe, blog creepy.
# Posted By Tom Cornilliac | 1/8/08 11:09 AM
Jeffry Houser's Gravatar Funny thing; that isn't the name of my network or any of my machines. At least not that I know of. I do pay for a static IP Address Block, so I wonder if that info comes from my Internet Service Provider somehow.
# Posted By Jeffry Houser | 1/8/08 11:42 AM
todd sharp's Gravatar Yeah dude - that's definitely a 9 on the weirdo/creepy meter...
# Posted By todd sharp | 1/8/08 12:14 PM
Tom Cornilliac's Gravatar Probably the reverse dns lookup on your static ip
# Posted By Tom Cornilliac | 1/8/08 9:52 PM
Theo's Gravatar Sorry about the post if it freaked you out Jeff. I thought most would recognize the Network Location view from Google Analytics and think it a bit odd and funny that something so specific would turn up there, but I might have been wrong about that.

You are probably one of the only people in the community (and the only one I have ever seen) to have your IP listed under your own name like this. Usually IP:s, even static ones, are listed under the name of the service provider, or under the company name for larger companies.

Going through the Network Location view in GA is good fun, most of the names are just ISP:s, but once in a while you see that someone from Adobe, Apple, Sony or IRS (or rather your local equivalent) has been surfing your site. A couple of days ago I was surprised to find your name.

I would be a little freaked out if I were you, not by my post (which really, really wasn't meant that way), but by the fact that any site that you visit will be able to track you, see that it's you, and see your name. It's true that I can be tracked to, but not over the course of several days (let alone months) since my IP will change and my cookies may be deleted. You announce your name all the time. I can go back and see that the first time you visited my site was May 9 2007, that is creepy.

I've bookmarked the graph that shows your visits to my site, so I'm still watching you (in all friendliness).
# Posted By Theo | 1/9/08 5:41 AM
Jeffry Houser's Gravatar Theo,

I did figure it was a Google analytics graph, and was able to look up stats on this site. The "Jeff Houser" account visited my own site 219 times, starting on May 8th. There are no visits this year.

The value in the Google Analytics is not the same as a reverse DNS lookup of any of my static IPs.

I guess I should call my provider to see what they say. Where does this issues show up in the "call tree" used by the non-english-speaking tech support?
# Posted By Jeffry Houser | 1/9/08 8:52 AM
Ken's Gravatar HI Jeffry,

This sounds fantastic in principle, but it doesn't appear to work. I've reformatted the code and tried various options but to no avail. Any ideas? Is there something in the app.xml file I need to do?

Note: Flex also throws an error because it doesn't know what 'refreshNativeMenuItem' is on this line:

menu.addItem(refreshNativeMenuItem);

Many thanks,

Ken
# Posted By Ken | 1/9/08 3:10 PM
Jeffry Houser's Gravatar Ken,

If I had to guess, you didn't put it in a WindowedApplication with a creationcomplete that runs "initApplication", did you? Would that be correct?

I updated the code to include a full application and hopefully improved formatting. It looked the cut and paste from Flex Builder removed a lot of line breaks, thus commenting out code.
# Posted By Jeffry Houser | 1/9/08 5:37 PM
Ken's Gravatar Hi Jeffry,

You're totally correct, I hadn't included the creationComplete at all... My Flex knowledge is absolute zero (as you might have guessed!).

Thanks for updating the code. It is much appreciated.

Is this code likely to work in the final release of AIR? I'm looking into working on a project where an AIR application should be open at all times and the user cannot quit it (yes, it does sound evil). By putting the app in the systray it seems to be pretty unquittable - removing the NativeMenuItems of course.

Mucho thanks,

Ken
# Posted By Ken | 1/10/08 3:32 AM
Jeffry Houser's Gravatar Ken,

I believe that Air Beta 3 is in a "API Locked" state; so I would expect this code to work on the final 1.0 release of AIR. That said, until 1.0 is released we won't actually know.

Making an application that can't be exited sounds dangerous.
# Posted By Jeffry Houser | 1/10/08 7:58 AM
Ken's Gravatar I totally agree. The one way to quit it will be through Task Manager's Processes, which is fine by me.

How might you modify your code above to invoke minimizing (or sending to the systray) on start-up? I've tried playing with NativeWindow.minimize() to no avail, and calling dock() in the init function doesn't do nuthin'. Again, I've highlighted my newbieness....

Thanks, Ken
# Posted By Ken | 1/10/08 12:12 PM
Jeffry Houser's Gravatar I haven't tested, but try calling "Dock" in the init method.
# Posted By Jeffry Houser | 1/10/08 2:58 PM
WebGyver's Gravatar Awesome! Thank you very much!

Thanks to your excellent example code, I'm beginning to understand a few more things about AIR desktop application development in Flex 3. I greatly appreciate your willingness to share your code with others. Thanks again.
# Posted By WebGyver | 2/16/08 4:52 PM
Jeffry Houser's Gravatar WebGyver,

Thanks for reading! I'm glad to be of service.
# Posted By Jeffry Houser | 2/16/08 5:21 PM
Francisco Echeverria's Gravatar Muchas Gracias, Realmente Sirve :) , Thank you very much
# Posted By Francisco Echeverria | 2/29/08 8:56 AM
Saskovic's Gravatar I've finally updated my blog-code (http://www.saskovic.com/blog/?p=5) with yours.

Thanks again for the effort and nice to see it's of use for many of us around! :)
# Posted By Saskovic | 3/4/08 5:34 AM
All Content Copyright 2005, 2006, 2007 Jeffry Houser. May not be reused without permission
BlogCFC was created by Raymond Camden. This blog is running version 5.8.