Friday, April 29, 2011

Flex: Custom context menu

Background

I never really liked the context menus supplied by the flash player that you are bound to use when developing flex applications. Mostly because of the non-customizable look and feel and the fact that your application specific menu items will be in the same list as the built in items (you can't hide all of them). Since we are using context menus in many places in our applications I have been looking at the following solution for a while to replace the built in menus:

http://code.google.com/p/custom-context-menu/

This solution uses javascript to capture the right click event over the SWF to prevent the flash player from opening its built-in context menu. The event is then passed on to a function that communicates with Flash over the ExternalInterface. The function called in action script is then free to do whatever it likes to act upon the performed right click.

The drawback of the solution is that it relies on using wmode='opaque' for the flash object and this does not work reliably across different browsers and operating systems.

Anyway, when running into the following bug with the latest Safari browser (5.0) on Mac:

http://bugs.adobe.com/jira/browse/FP-4825

I decided to go ahead and implement custom context menus, at least for our Mac users.

Implementation

The implementation relies on the solution mentioned above for handling the right click events and passing them on to the Flex application. An action script function then traverses the components currently located below the mouse pointer until it finds one that has a context menu defined. The found ContextMenu object then gets wrapped in a CustomContextMenu wrapper that uses the original context menu to create and show a regular flex menu.

The CustomContextMenu wrapper also makes sure that the enabled and visible statuses of the context menu items are also reflected in the items of the generated menu. Last but not least the wrapper makes sure that any user actions on the generated menu are dispatched as events on the original context menu so that the behavior of the original context menu is preserved in the new menu.

That's it. Enough talking. Let's look at the code, feel free to re-use it in any way you deem fit:

This is the code for the CustomContextMenu wrapper:

CustomContextMenu.as

package com.flexceptional.components
{
    import flash.events.ContextMenuEvent;
    import flash.ui.ContextMenu;
    import flash.ui.ContextMenuItem;
    
    import mx.controls.Menu;
    import mx.events.MenuEvent;
    
    public class CustomContextMenu
    {
        private static var SEPARATOR:Object = {type: "separator"};
        
        private var _menu:Menu;
        private var _contextMenu:ContextMenu;
        
        public function CustomContextMenu(contextMenu:ContextMenu)
        {   
            _contextMenu = contextMenu;
            
            // Create menu
            _menu = Menu.createMenu(null, null);
            
            // Add event listeners
            _menu.addEventListener(MenuEvent.MENU_SHOW, menuShowHandler);
            _menu.addEventListener(MenuEvent.ITEM_CLICK, itemClickHandler);
            
            // Set properties
            _menu.setStyle("openDuration", 0);
            _menu.variableRowHeight = true;
            _menu.id = "contextMenu";
            
            // Create custom menu items
            var menuItems:Array = [];
            for each (var item:ContextMenuItem in contextMenu.customItems)
            {
                if (item.separatorBefore)
                {
                    menuItems.push(CustomContextMenu.SEPARATOR);
                }
                menuItems.push(new CustomContextMenuItem(item));
            }
            _menu.dataProvider = menuItems;
        }
        
        private function menuShowHandler(event:MenuEvent):void
        {
            _contextMenu.dispatchEvent(new ContextMenuEvent(ContextMenuEvent.MENU_SELECT));
        }
        
        private function itemClickHandler(event:MenuEvent):void
        {
            event.item.dispatchEvent(new ContextMenuEvent(ContextMenuEvent.MENU_ITEM_SELECT));
        }
        
        public function show(xShow:Object = null, yShow:Object = null):void
        {
            _menu.show(xShow, yShow);
        }
        
        public function hide():void
        {
            _menu.hide();
        }
    }
}

import flash.events.ContextMenuEvent;
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.ui.ContextMenuItem;

// ContextMenuItem wrapper
class CustomContextMenuItem extends EventDispatcher
{   
    private var _contextMenuItem:ContextMenuItem;
    
    public function CustomContextMenuItem(contextMenuItem:ContextMenuItem)
    {
        _contextMenuItem = contextMenuItem;
        
        // Dispatch event on the proxied ContextMenuItem
        addEventListener(ContextMenuEvent.MENU_ITEM_SELECT, function(event:Event):void
        {
            _contextMenuItem.dispatchEvent(event);
        });
    }
    
    public function get label():String
    {
        return _contextMenuItem.caption;
    }
    
    public function get enabled():Boolean
    {
        return _contextMenuItem.enabled;
    }
    
    public function get visible():Boolean
    {
        return _contextMenuItem.visible;
    }
}

This is the function invoked by the java script, place it in your main application (the optional x and y arguments can be used for automated testing):

private var customContextMenu:CustomContextMenu;

private function rightClickHandler(clickedX:Number = NaN, clickedY:Number = NaN):void {
    // Close previous context menu
    if (customContextMenu)
    {
        customContextMenu.hide();
    }
    
    clickedX = isNaN(clickedX) ? mouseX : clickedX;
    clickedY = isNaN(clickedY) ? mouseY : clickedY;
    
    // Get objects under clicked point
    var objects:Array = systemManager.getObjectsUnderPoint(new Point(clickedX, clickedY));
    if (objects.length > 0)
    {
        // Get the top most object clicked
        var object:Object = objects[objects.length-1];
        
        // Check if any of the parent objects have
        // a context menu defined
        while (object)
        {
            if (object.hasOwnProperty("contextMenu") && object.contextMenu)
            {
                customContextMenu = new CustomContextMenu(object.contextMenu);
                customContextMenu.show(clickedX, clickedY);
                break;
            }
            object = object.parent;
        }
    }
}

The code snippet for making the above function available through the ExternalInterface, place it in the initializeHandler of your main application file:

// Enable custom right click functionality
ExternalInterface.addCallback("rightClick", rightClickHandler);

We use the following javascript function to initialize the right click handling only for Mac users running Safari 5, place it in your html template and call it on the onload event on the body tag:

function initRightClick() {
    if (BrowserDetect.OS == "Mac" &&
        BrowserDetect.browser == "Safari" &&
        BrowserDetect.version == "5") {
        RightClick.init();
    }
}

The BrowserDetect object referenced in the above function comes from a file created using the code posted on this page:

http://www.quirksmode.org/js/detect.html

So, big post, feel free to comment on it if you have any questions or doubts. The solution provides a nice workaround for the Mac Safari 5 context menu bug and since our context menus can now be invoked by calling a function from javascript it also allows for us to do automated testing of our context menus using our integration test setup. More on testing in a later post.

Friday, April 15, 2011

Install stand alone Flash player on 64 bit Ubuntu

We are running our Flex unit tests on a 64 bit Ubuntu machine as part of our continuos integration environment. In order to run the tests we need to have the stand alone version of the Flash player installed. Unfortunately there is not yet a 64 bit version of the flash player projector for linux so to run our tests we need to use the 32 bit version.

These are the steps needed to install the 32 bit stand alone flash player under a 64 bit Ubuntu:

  1. Download the 32 bit Flash player projector from http://www.adobe.com/support/flashplayer/downloads.html

    > wget http://download.macromedia.com/pub/flashplayer/updaters/10/flashplayer_10_sa.tar.gz

  2. Unpack the flash player executable and make sure you put it on your path.

    > tar -xzvf flashplayer_10_sa.tar.gz

    Now, if you try to run the Flash player, chances are you will get an error message similar to the following:

    > flashplayer
    flashplayer: error while loading shared libraries: libX11.so.6: cannot open shared object file: No such file or directory

    This is because the 32 bit Flash player also requires 32 bit versions of certain libraries to be installed. If you run ldd (print shared library dependencies) on the flash player executable you will be presented with a list of the required libraries:

    > ldd flashplayer

  3. Install required 32 bit libraries. Luckily, most of these libraries can be installed by installing a single package using apt-get:

    > apt-get install ia32-libs

    If you now run ldd on the flash player again you should see that the library dependencies have been resolved and, if you are lucky, the flash player will now launch.

Good luck!

Note that these were the steps required for our installation, another platform might require additional steps or libraries.