Wednesday, January 6, 2010

Icons in Flex MenuBar

I came across this strange behavior (I'm not sure if it qualifies to be called a bug, but it definitely is not a feature) in Flex while trying to add icons to the MenuBar control.

The icons are displayed properly if both the MenuBar and the icon-embed variables are declared in the main application class. But if they are nested inside another component, only the top level menu-items' icons are displayed - child menu items' icons are not displayed.

After some debugging I realized that only the top-level menu items are displayed using the itemToIcon method of the MenuBar class; child menu-items are displayed using the itemToIcon method of the Menu class. The problem here is that even though both of these methods use document[data[iconField]] to grab the icon-embed class, the document properties of these two objects point to different items. The document property of the MenuBar points correctly to the enclosing component (MyPanel). Whereas, for some reason that I don't understand, the document property for nested Menu points to the MainApplication class.

In short, while rendering icons of a MenuBar within a component, flex searches the component class for icons of top-level menu items, and the main-application class for icons of its nested menu items. The obvious workaround is to embed all icons to public static const in some class and to keep appropriate references to them in both application and component classes. But that doesn't sound so pretty to me (even though that's what I am doing now). Does anyone know better?

Here is example code and screenshots to demonstrate the issue:

<!--The main Application class-->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"
   creationComplete="create()" xmlns:local="*">
   <mx:Label text="MenuBar in the Application works fine"/>
   <mx:MenuBar id="menuBar" top="10" left="10"
       dataProvider="{menuBarCollection}" labelField="@label"
       iconField="@icon" />

   <local:MyPanel/>

   <mx:Script>
       <![CDATA[
           import mx.collections.XMLListCollection;

           [Bindable]
           protected var menuBarCollection:XMLListCollection;

           [Embed(source="assets/icon_one.png")]
           public var iconOne_App:Class;

           [Embed(source="assets/icon_two.png")]
           public var iconTwo_App:Class;

           private function create():void
           {
               menuBarCollection = new XMLListCollection(XMLList(
                   '<menuitem label="One" icon="iconOne_App">' +
                       '<menuitem label="Two" icon="iconTwo_App"/>' +
                       '<menuitem label="Three" icon="iconTwo_App"/>' +
                   '</menuitem>'
               ));
           }
       ]]>
   </mx:Script>
</mx:Application>

MenuBar in the Application

<!--MyPanel.mxml-->
<mx:Panel xmlns:mx="http://www.adobe.com/2006/mxml" height="144"
   creationComplete="create();" horizontalAlign="center" verticalAlign="middle">
   <mx:Text text="MenuBar inside a component doesn't display icons properly"/>
   <mx:MenuBar id="menuBar" top="10" left="10"
       dataProvider="{menuBarCollection}" labelField="@label"
       iconField="@icon" />
   <mx:Script>
       <![CDATA[
           import mx.collections.XMLListCollection;

           [Bindable]
           protected var menuBarCollection:XMLListCollection;

           [Embed(source="assets/icon_one.png")]
           public var iconOne_Panel:Class;

           [Embed(source="assets/icon_two.png")]
           public var iconTwo_Panel:Class;

           private function create():void
           {
               menuBarCollection = new XMLListCollection(XMLList(
                   //top level item referring to component
                   //this icon is displayed
                   '<menuitem label="ParentIcon_panel" icon="iconOne_Panel">' +

                       //nested item referring to component
                       //this icon is not displayed
                       '<menuitem label="ChildIcon_panel" icon="iconTwo_Panel"/>' +

                       //nested item referring to application
                       //this icon is displayed
                       '<menuitem label="ChildIcon-app" icon="iconTwo_App"/>' +
                   '</menuitem>' +

                   //top level item referring to Application
                   //this icon is not displayed
                   '<menuitem label="ParentIcon_app" icon="iconOne_App"/>'
               ));
           }
       ]]>
   </mx:Script>
</mx:Panel>

MenuBar inside a Component

10 comments:

  1. Hello Amarghosh,

    I was in the same situation as you were before.
    Your post helped me a lot.

    Thank you
    Milan Slancik, Slovakia

    ReplyDelete
  2. Amar,
    I work at ThoughtWorks, and I know we'd love to have people like you in the office :D

    If you're interested, let me know at sudhir.j@gmail.com.

    Sudhir
    http://stackoverflow.com/users/73831/sudhir-jonathan

    ReplyDelete
  3. Nice post :).
    But what if you want to put the icon aligned to the right instead? How would you do it? I tried to get the button reference and use the ButtonLabelPlacement to put it to the Left and then make it "switch" with the icon but I couldn't get the button reference :\.

    Any idea?

    ReplyDelete
  4. @Mithras

    There is a menuBarItemRenderer property in the MenuBar class. Setting this property to a custom item renderer would do the trick for top level items. The default renderer is mx.controls.menuClasses.MenuBarItem - extend it and swap the icon and label properties in it's creationComplete handler.

    As for the items in the popup menu, they have their own item renderer. The `menus` property of the MenuBar will give you a list of Menu objects - set their item renderer too. Again you can extend the default MenuItemRenderer and swap the label and icon properties to achieve this.

    ReplyDelete
  5. Thank you Amarghosh!
    It worked perfectly :)

    ReplyDelete
  6. I ran into a similar problem recently. A lot later than you guys did, I solved it in a slightly different way. Essentially using a array collection as a data provider, but in my case, the child menu Items were not constant hence I use the addItemAt method of array collection to add an object of a menu item, and set the make the children property point to a different array. something like this, http://help.adobe.com/en_US/flex/using/WS2db454920e96a9e51e63e3d11c0bf66f5a-7ff5.html

    ReplyDelete
  7. I use the addItemAt method of array collection to add an object of a menu item, and set the make the children property point to a different array. something like this,

    ReplyDelete