com.google.gwt.user.client.ui.MenuBar.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.user.client.ui.MenuBar.java

Source

/*
 * Copyright 2009 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.user.client.ui;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.HasCloseHandlers;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.resources.client.ImageResource.ImageOptions;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.ui.PopupPanel.AnimationType;

import java.util.ArrayList;
import java.util.List;

/**
 * A standard menu bar widget. A menu bar can contain any number of menu items,
 * each of which can either fire a {@link com.google.gwt.user.client.Command} or
 * open a cascaded menu bar.
 *
 * <p>
 * <img class='gallery' src='doc-files/MenuBar.png'/>
 * </p>
 *
 * <h3>CSS Style Rules</h3>
 * <dl>
 * <dt>.gwt-MenuBar</dt>
 * <dd>the menu bar itself</dd>
 * <dt>.gwt-MenuBar-horizontal</dt>
 * <dd>dependent style applied to horizontal menu bars</dd>
 * <dt>.gwt-MenuBar-vertical</dt>
 * <dd>dependent style applied to vertical menu bars</dd>
 * <dt>.gwt-MenuBar .gwt-MenuItem</dt>
 * <dd>menu items</dd>
 * <dt>.gwt-MenuBar .gwt-MenuItem-selected</dt>
 * <dd>selected menu items</dd>
 * <dt>.gwt-MenuBar .gwt-MenuItemSeparator</dt>
 * <dd>section breaks between menu items</dd>
 * <dt>.gwt-MenuBar .gwt-MenuItemSeparator .menuSeparatorInner</dt>
 * <dd>inner component of section separators</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupTopLeft</dt>
 * <dd>the top left cell</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupTopLeftInner</dt>
 * <dd>the inner element of the cell</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupTopCenter</dt>
 * <dd>the top center cell</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupTopCenterInner</dt>
 * <dd>the inner element of the cell</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupTopRight</dt>
 * <dd>the top right cell</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupTopRightInner</dt>
 * <dd>the inner element of the cell</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupMiddleLeft</dt>
 * <dd>the middle left cell</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupMiddleLeftInner</dt>
 * <dd>the inner element of the cell</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupMiddleCenter</dt>
 * <dd>the middle center cell</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupMiddleCenterInner</dt>
 * <dd>the inner element of the cell</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupMiddleRight</dt>
 * <dd>the middle right cell</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupMiddleRightInner</dt>
 * <dd>the inner element of the cell</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupBottomLeft</dt>
 * <dd>the bottom left cell</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupBottomLeftInner</dt>
 * <dd>the inner element of the cell</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupBottomCenter</dt>
 * <dd>the bottom center cell</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupBottomCenterInner</dt>
 * <dd>the inner element of the cell</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupBottomRight</dt>
 * <dd>the bottom right cell</dd>
 * <dt>.gwt-MenuBarPopup .menuPopupBottomRightInner</dt>
 * <dd>the inner element of the cell</dd>
 * </dl>
 *
 * <p>
 * <h3>Example</h3>
 * {@example com.google.gwt.examples.MenuBarExample}
 * </p>
 *
 * <h3>Use in UiBinder Templates</h3>
 * <p>
 * MenuBar elements in UiBinder template files can have a <code>vertical</code>
 * boolean attribute (which defaults to false), and may have only MenuItem
 * elements as children. MenuItems may contain HTML and MenuBars.
 * <p>
 * For example:
 *
 * <pre>
 * &lt;g:MenuBar>
 *   &lt;g:MenuItem>Higgledy
 *     &lt;g:MenuBar vertical="true">
 *       &lt;g:MenuItem>able&lt;/g:MenuItem>
 *       &lt;g:MenuItem>baker&lt;/g:MenuItem>
 *       &lt;g:MenuItem>charlie&lt;/g:MenuItem>
 *     &lt;/g:MenuBar>
 *   &lt;/g:MenuItem>
 *   &lt;g:MenuItem>Piggledy
 *     &lt;g:MenuBar vertical="true">
 *       &lt;g:MenuItem>foo&lt;/g:MenuItem>
 *       &lt;g:MenuItem>bar&lt;/g:MenuItem>
 *       &lt;g:MenuItem>baz&lt;/g:MenuItem>
 *     &lt;/g:MenuBar>
 *   &lt;/g:MenuItem>
 *   &lt;g:MenuItem>&lt;b>Pop!&lt;/b>
 *     &lt;g:MenuBar vertical="true">
 *       &lt;g:MenuItem>uno&lt;/g:MenuItem>
 *       &lt;g:MenuItem>dos&lt;/g:MenuItem>
 *       &lt;g:MenuItem>tres&lt;/g:MenuItem>
 *     &lt;/g:MenuBar>
 *   &lt;/g:MenuItem>
 * &lt;/g:MenuBar>
 * </pre>
 */
// Nothing we can do about MenuBar implementing PopupListener until next
// release.
@SuppressWarnings("deprecation")
public class MenuBar extends Widget implements PopupListener, HasAnimation, HasCloseHandlers<PopupPanel> {

    /**
     * An {@link ImageBundle} that provides images for {@link MenuBar}.
     *
     * @deprecated replaced by {@link Resources}
     */
    @Deprecated
    public interface MenuBarImages extends ImageBundle {
        /**
         * An image indicating a {@link MenuItem} has an associated submenu.
         *
         * @return a prototype of this image
         */
        AbstractImagePrototype menuBarSubMenuIcon();
    }

    /**
     * A ClientBundle that contains the default resources for this widget.
     */
    public interface Resources extends ClientBundle {
        /**
         * An image indicating a {@link MenuItem} has an associated submenu.
         */
        @ImageOptions(flipRtl = true)
        ImageResource menuBarSubMenuIcon();
    }

    private static final String STYLENAME_DEFAULT = "gwt-MenuBar";

    /**
     * List of all {@link MenuItem}s and {@link MenuItemSeparator}s.
     */
    private ArrayList<UIObject> allItems = new ArrayList<UIObject>();

    /**
     * List of {@link MenuItem}s, not including {@link MenuItemSeparator}s.
     */
    private ArrayList<MenuItem> items = new ArrayList<MenuItem>();

    private Element body;

    private AbstractImagePrototype subMenuIcon = null;
    private boolean isAnimationEnabled = false;
    private MenuBar parentMenu;
    private PopupPanel popup;
    private MenuItem selectedItem;
    private MenuBar shownChildMenu;
    private boolean vertical, autoOpen;
    private boolean focusOnHover = true;

    /**
     * Creates an empty horizontal menu bar.
     */
    public MenuBar() {
        this(false);
    }

    /**
     * Creates an empty menu bar.
     *
     * @param vertical <code>true</code> to orient the menu bar vertically
     */
    public MenuBar(boolean vertical) {
        this(vertical, GWT.<Resources>create(Resources.class));
    }

    /**
     * Creates an empty menu bar that uses the specified image bundle for menu
     * images.
     *
     * @param vertical <code>true</code> to orient the menu bar vertically
     * @param images a bundle that provides images for this menu
     * @deprecated replaced by {@link #MenuBar(boolean, Resources)}
     */
    @Deprecated
    public MenuBar(boolean vertical, MenuBarImages images) {
        init(vertical, images.menuBarSubMenuIcon());
    }

    /**
     * Creates an empty menu bar that uses the specified ClientBundle for menu
     * images.
     *
     * @param vertical <code>true</code> to orient the menu bar vertically
     * @param resources a bundle that provides images for this menu
     */
    public MenuBar(boolean vertical, Resources resources) {
        init(vertical, AbstractImagePrototype.create(resources.menuBarSubMenuIcon()));
    }

    /**
     * Creates an empty horizontal menu bar that uses the specified image bundle
     * for menu images.
     *
     * @param images a bundle that provides images for this menu
     * @deprecated replaced by {@link #MenuBar(Resources)}
     */
    @Deprecated
    public MenuBar(MenuBarImages images) {
        this(false, images);
    }

    /**
     * Creates an empty horizontal menu bar that uses the specified ClientBundle
     * for menu images.
     *
     * @param resources a bundle that provides images for this menu
     */
    public MenuBar(Resources resources) {
        this(false, resources);
    }

    public HandlerRegistration addCloseHandler(CloseHandler<PopupPanel> handler) {
        return addHandler(handler, CloseEvent.getType());
    }

    /**
     * Adds a menu item to the bar.
     *
     * @param item the item to be added
     * @return the {@link MenuItem} object
     */
    public MenuItem addItem(MenuItem item) {
        return insertItem(item, allItems.size());
    }

    /**
     * Adds a menu item to the bar containing SafeHtml, that will fire the given
     * command when it is selected.
     *
     * @param html the item's html text
     * @param cmd the command to be fired
     * @return the {@link MenuItem} object created
     */
    public MenuItem addItem(SafeHtml html, Command cmd) {
        return addItem(new MenuItem(html, cmd));
    }

    /**
     * Adds a menu item to the bar, that will fire the given command when it is
     * selected.
     *
     * @param text the item's text
     * @param asHTML <code>true</code> to treat the specified text as html
     * @param cmd the command to be fired
     * @return the {@link MenuItem} object created
     */
    public MenuItem addItem(String text, boolean asHTML, Command cmd) {
        return addItem(new MenuItem(text, asHTML, cmd));
    }

    /**
     * Adds a menu item to the bar, that will open the specified menu when it is
     * selected.
     *
     * @param html the item's html text
     * @param popup the menu to be cascaded from it
     * @return the {@link MenuItem} object created
     */
    public MenuItem addItem(SafeHtml html, MenuBar popup) {
        return addItem(new MenuItem(html, popup));
    }

    /**
     * Adds a menu item to the bar, that will open the specified menu when it is
     * selected.
     *
     * @param text the item's text
     * @param asHTML <code>true</code> to treat the specified text as html
     * @param popup the menu to be cascaded from it
     * @return the {@link MenuItem} object created
     */
    public MenuItem addItem(String text, boolean asHTML, MenuBar popup) {
        return addItem(new MenuItem(text, asHTML, popup));
    }

    /**
     * Adds a menu item to the bar, that will fire the given command when it is
     * selected.
     *
     * @param text the item's text
     * @param cmd the command to be fired
     * @return the {@link MenuItem} object created
     */
    public MenuItem addItem(String text, Command cmd) {
        return addItem(new MenuItem(text, cmd));
    }

    /**
     * Adds a menu item to the bar, that will open the specified menu when it is
     * selected.
     *
     * @param text the item's text
     * @param popup the menu to be cascaded from it
     * @return the {@link MenuItem} object created
     */
    public MenuItem addItem(String text, MenuBar popup) {
        return addItem(new MenuItem(text, popup));
    }

    /**
     * Adds a thin line to the {@link MenuBar} to separate sections of
     * {@link MenuItem}s.
     *
     * @return the {@link MenuItemSeparator} object created
     */
    public MenuItemSeparator addSeparator() {
        return addSeparator(new MenuItemSeparator());
    }

    /**
     * Adds a thin line to the {@link MenuBar} to separate sections of
     * {@link MenuItem}s.
     *
     * @param separator the {@link MenuItemSeparator} to be added
     * @return the {@link MenuItemSeparator} object
     */
    public MenuItemSeparator addSeparator(MenuItemSeparator separator) {
        return insertSeparator(separator, allItems.size());
    }

    /**
     * Removes all menu items from this menu bar.
     */
    public void clearItems() {
        // Deselect the current item
        selectItem(null);

        Element container = getItemContainerElement();
        while (DOM.getChildCount(container) > 0) {
            DOM.removeChild(container, DOM.getChild(container, 0));
        }

        // Set the parent of all items to null
        for (UIObject item : allItems) {
            setItemColSpan(item, 1);
            if (item instanceof MenuItemSeparator) {
                ((MenuItemSeparator) item).setParentMenu(null);
            } else {
                ((MenuItem) item).setParentMenu(null);
            }
        }

        // Clear out all of the items and separators
        items.clear();
        allItems.clear();
    }

    /**
     * Closes this menu and all child menu popups.
     *
     * @param focus true to move focus to the parent
     */
    public void closeAllChildren(boolean focus) {
        if (shownChildMenu != null) {
            // Hide any open submenus of this item
            shownChildMenu.onHide(focus);
            shownChildMenu = null;
            selectItem(null);
        }
        // Close the current popup
        if (popup != null) {
            popup.hide();
        }
        // If focus is true, set focus to parentMenu
        if (focus && parentMenu != null) {
            parentMenu.focus();
        }
    }

    /**
     * Give this MenuBar focus.
     */
    public void focus() {
        FocusPanel.impl.focus(getElement());
    }

    /**
     * Gets whether this menu bar's child menus will open when the mouse is moved
     * over it.
     *
     * @return <code>true</code> if child menus will auto-open
     */
    public boolean getAutoOpen() {
        return autoOpen;
    }

    /**
     * Get the index of a {@link MenuItem}.
     *
     * @return the index of the item, or -1 if it is not contained by this MenuBar
     */
    public int getItemIndex(MenuItem item) {
        return allItems.indexOf(item);
    }

    /**
     * Get the index of a {@link MenuItemSeparator}.
     *
     * @return the index of the separator, or -1 if it is not contained by this
     *         MenuBar
     */
    public int getSeparatorIndex(MenuItemSeparator item) {
        return allItems.indexOf(item);
    }

    /**
     * Adds a menu item to the bar at a specific index.
     *
     * @param item the item to be inserted
     * @param beforeIndex the index where the item should be inserted
     * @return the {@link MenuItem} object
     * @throws IndexOutOfBoundsException if <code>beforeIndex</code> is out of
     *           range
     */
    public MenuItem insertItem(MenuItem item, int beforeIndex) throws IndexOutOfBoundsException {
        // Check the bounds
        if (beforeIndex < 0 || beforeIndex > allItems.size()) {
            throw new IndexOutOfBoundsException();
        }

        // Add to the list of items
        allItems.add(beforeIndex, item);
        int itemsIndex = 0;
        for (int i = 0; i < beforeIndex; i++) {
            if (allItems.get(i) instanceof MenuItem) {
                itemsIndex++;
            }
        }
        items.add(itemsIndex, item);

        // Setup the menu item
        addItemElement(beforeIndex, item.getElement());
        item.setParentMenu(this);
        item.setSelectionStyle(false);
        updateSubmenuIcon(item);
        return item;
    }

    /**
     * Adds a thin line to the {@link MenuBar} to separate sections of
     * {@link MenuItem}s at the specified index.
     *
     * @param beforeIndex the index where the separator should be inserted
     * @return the {@link MenuItemSeparator} object
     * @throws IndexOutOfBoundsException if <code>beforeIndex</code> is out of
     *           range
     */
    public MenuItemSeparator insertSeparator(int beforeIndex) {
        return insertSeparator(new MenuItemSeparator(), beforeIndex);
    }

    /**
     * Adds a thin line to the {@link MenuBar} to separate sections of
     * {@link MenuItem}s at the specified index.
     *
     * @param separator the {@link MenuItemSeparator} to be inserted
     * @param beforeIndex the index where the separator should be inserted
     * @return the {@link MenuItemSeparator} object
     * @throws IndexOutOfBoundsException if <code>beforeIndex</code> is out of
     *           range
     */
    public MenuItemSeparator insertSeparator(MenuItemSeparator separator, int beforeIndex)
            throws IndexOutOfBoundsException {
        // Check the bounds
        if (beforeIndex < 0 || beforeIndex > allItems.size()) {
            throw new IndexOutOfBoundsException();
        }

        if (vertical) {
            setItemColSpan(separator, 2);
        }
        addItemElement(beforeIndex, separator.getElement());
        separator.setParentMenu(this);
        allItems.add(beforeIndex, separator);
        return separator;
    }

    public boolean isAnimationEnabled() {
        return isAnimationEnabled;
    }

    /**
     * Check whether or not this widget will steal keyboard focus when the mouse
     * hovers over it.
     *
     * @return true if enabled, false if disabled
     */
    public boolean isFocusOnHoverEnabled() {
        return focusOnHover;
    }

    /**
     * Moves the menu selection down to the next item. If there is no selection,
     * selects the first item. If there are no items at all, does nothing.
     */
    public void moveSelectionDown() {
        if (selectFirstItemIfNoneSelected()) {
            return;
        }

        if (vertical) {
            selectNextItem();
        } else {
            if (selectedItem.getSubMenu() != null && !selectedItem.getSubMenu().getItems().isEmpty()
                    && (shownChildMenu == null || shownChildMenu.getSelectedItem() == null)) {
                if (shownChildMenu == null) {
                    doItemAction(selectedItem, false, true);
                }
                selectedItem.getSubMenu().focus();
            } else if (parentMenu != null) {
                if (parentMenu.vertical) {
                    parentMenu.selectNextItem();
                } else {
                    parentMenu.moveSelectionDown();
                }
            }
        }
    }

    /**
     * Moves the menu selection up to the previous item. If there is no selection,
     * selects the first item. If there are no items at all, does nothing.
     */
    public void moveSelectionUp() {
        if (selectFirstItemIfNoneSelected()) {
            return;
        }

        if ((shownChildMenu == null) && vertical) {
            selectPrevItem();
        } else if ((parentMenu != null) && parentMenu.vertical) {
            parentMenu.selectPrevItem();
        } else {
            close(true);
        }
    }

    @Override
    public void onBrowserEvent(Event event) {
        MenuItem item = findItem(DOM.eventGetTarget(event));
        switch (DOM.eventGetType(event)) {
        case Event.ONCLICK: {
            FocusPanel.impl.focus(getElement());
            // Fire an item's command when the user clicks on it.
            if (item != null) {
                doItemAction(item, true, true);
            }
            break;
        }

        case Event.ONMOUSEOVER: {
            if (item != null) {
                itemOver(item, true);
            }
            break;
        }

        case Event.ONMOUSEOUT: {
            if (item != null) {
                itemOver(null, true);
            }
            break;
        }

        case Event.ONFOCUS: {
            selectFirstItemIfNoneSelected();
            break;
        }

        case Event.ONKEYDOWN: {
            int keyCode = DOM.eventGetKeyCode(event);
            switch (keyCode) {
            case KeyCodes.KEY_LEFT:
                if (LocaleInfo.getCurrentLocale().isRTL()) {
                    moveToNextItem();
                } else {
                    moveToPrevItem();
                }
                eatEvent(event);
                break;
            case KeyCodes.KEY_RIGHT:
                if (LocaleInfo.getCurrentLocale().isRTL()) {
                    moveToPrevItem();
                } else {
                    moveToNextItem();
                }
                eatEvent(event);
                break;
            case KeyCodes.KEY_UP:
                moveSelectionUp();
                eatEvent(event);
                break;
            case KeyCodes.KEY_DOWN:
                moveSelectionDown();
                eatEvent(event);
                break;
            case KeyCodes.KEY_ESCAPE:
                closeAllParentsAndChildren();
                eatEvent(event);
                break;
            case KeyCodes.KEY_TAB:
                closeAllParentsAndChildren();
                break;
            case KeyCodes.KEY_ENTER:
                if (!selectFirstItemIfNoneSelected()) {
                    doItemAction(selectedItem, true, true);
                    eatEvent(event);
                }
                break;
            } // end switch(keyCode)

            break;
        } // end case Event.ONKEYDOWN
        } // end switch (DOM.eventGetType(event))
        super.onBrowserEvent(event);
    }

    /**
     * Closes the menu bar.
     *
     * @deprecated Use {@link #addCloseHandler(CloseHandler)} instead
     */
    @Deprecated
    public void onPopupClosed(PopupPanel sender, boolean autoClosed) {
        // If the menu popup was auto-closed, close all of its parents as well.
        if (autoClosed) {
            closeAllParents();
        }

        // When the menu popup closes, remember that no item is
        // currently showing a popup menu.
        onHide(!autoClosed);
        CloseEvent.fire(MenuBar.this, sender);
        shownChildMenu = null;
        popup = null;
        if (parentMenu != null && parentMenu.popup != null) {
            parentMenu.popup.setPreviewingAllNativeEvents(true);
        }
    }

    /**
     * Removes the specified menu item from the bar.
     *
     * @param item the item to be removed
     */
    public void removeItem(MenuItem item) {
        // Unselect if the item is currently selected
        if (selectedItem == item) {
            selectItem(null);
        }

        if (removeItemElement(item)) {
            setItemColSpan(item, 1);
            items.remove(item);
            item.setParentMenu(null);
        }
    }

    /**
     * Removes the specified {@link MenuItemSeparator} from the bar.
     *
     * @param separator the separator to be removed
     */
    public void removeSeparator(MenuItemSeparator separator) {
        if (removeItemElement(separator)) {
            separator.setParentMenu(null);
        }
    }

    /**
     * Select the given MenuItem, which must be a direct child of this MenuBar.
     *
     * @param item the MenuItem to select, or null to clear selection
     */
    public void selectItem(MenuItem item) {
        assert item == null || item.getParentMenu() == this;

        if (item == selectedItem) {
            return;
        }

        if (selectedItem != null) {
            selectedItem.setSelectionStyle(false);
            // Set the style of the submenu indicator
            if (vertical) {
                Element tr = DOM.getParent(selectedItem.getElement());
                if (DOM.getChildCount(tr) == 2) {
                    Element td = DOM.getChild(tr, 1);
                    setStyleName(td, "subMenuIcon-selected", false);
                }
            }

            if (vertical && shownChildMenu != null && shownChildMenu == selectedItem.getSubMenu()) {
                shownChildMenu.onHide(false);
                popup.hide();
                shownChildMenu = null;
            }
        }

        if (item != null) {
            item.setSelectionStyle(true);

            // Set the style of the submenu indicator
            if (vertical) {
                Element tr = DOM.getParent(item.getElement());
                if (DOM.getChildCount(tr) == 2) {
                    Element td = DOM.getChild(tr, 1);
                    setStyleName(td, "subMenuIcon-selected", true);
                }
            }

            Accessibility.setState(getElement(), Accessibility.STATE_ACTIVEDESCENDANT,
                    DOM.getElementAttribute(item.getElement(), "id"));
        }

        selectedItem = item;
    }

    public void setAnimationEnabled(boolean enable) {
        isAnimationEnabled = enable;
    }

    /**
     * Sets whether this menu bar's child menus will open when the mouse is moved
     * over it.
     *
     * @param autoOpen <code>true</code> to cause child menus to auto-open
     */
    public void setAutoOpen(boolean autoOpen) {
        this.autoOpen = autoOpen;
    }

    /**
     * Enable or disable auto focus when the mouse hovers over the MenuBar. This
     * allows the MenuBar to respond to keyboard events without the user having to
     * click on it, but it will steal focus from other elements on the page.
     * Enabled by default.
     *
     * @param enabled true to enable, false to disable
     */
    public void setFocusOnHoverEnabled(boolean enabled) {
        focusOnHover = enabled;
    }

    /**
     * Returns a list containing the <code>MenuItem</code> objects in the menu
     * bar. If there are no items in the menu bar, then an empty <code>List</code>
     * object will be returned.
     *
     * @return a list containing the <code>MenuItem</code> objects in the menu bar
     */
    protected List<MenuItem> getItems() {
        return this.items;
    }

    /**
     * Returns the <code>MenuItem</code> that is currently selected (highlighted)
     * by the user. If none of the items in the menu are currently selected, then
     * <code>null</code> will be returned.
     *
     * @return the <code>MenuItem</code> that is currently selected, or
     *         <code>null</code> if no items are currently selected
     */
    protected MenuItem getSelectedItem() {
        return this.selectedItem;
    }

    @Override
    protected void onDetach() {
        // When the menu is detached, make sure to close all of its children.
        if (popup != null) {
            popup.hide();
        }

        super.onDetach();
    }

    /**
     * <b>Affected Elements:</b>
     * <ul>
     * <li>-item# = the {@link MenuItem} at the specified index.</li>
     * </ul>
     *
     * @see UIObject#onEnsureDebugId(String)
     */
    @Override
    protected void onEnsureDebugId(String baseID) {
        super.onEnsureDebugId(baseID);
        setMenuItemDebugIds(baseID);
    }

    /*
     * Closes all parent menu popups.
     */
    void closeAllParents() {
        if (parentMenu != null) {
            // The parent menu will recursively call closeAllParents.
            close(false);
        } else {
            // If this is the top most menu, deselect the current item.
            selectItem(null);
        }
    }

    /**
     * Closes all parent and child menu popups.
     */
    void closeAllParentsAndChildren() {
        closeAllParents();
        // Ensure the popup is closed even if it has not been enetered
        // with the mouse or key navigation
        if (parentMenu == null && popup != null) {
            popup.hide();
        }
    }

    /*
     * Performs the action associated with the given menu item. If the item has a
     * popup associated with it, the popup will be shown. If it has a command
     * associated with it, and 'fireCommand' is true, then the command will be
     * fired. Popups associated with other items will be hidden.
     *
     * @param item the item whose popup is to be shown. @param fireCommand
     * <code>true</code> if the item's command should be fired, <code>false</code>
     * otherwise.
     */
    void doItemAction(final MenuItem item, boolean fireCommand, boolean focus) {
        // Should not perform any action if the item is disabled
        if (!item.isEnabled()) {
            return;
        }

        // Ensure that the item is selected.
        selectItem(item);

        if (item != null) {
            // if the command should be fired and the item has one, fire it
            if (fireCommand && item.getCommand() != null) {
                // Close this menu and all of its parents.
                closeAllParents();

                // Fire the item's command. The command must be fired in the same event
                // loop or popup blockers will prevent popups from opening.
                final Command cmd = item.getCommand();
                Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() {
                    public void execute() {
                        cmd.execute();
                    }
                });

                // hide any open submenus of this item
                if (shownChildMenu != null) {
                    shownChildMenu.onHide(focus);
                    popup.hide();
                    shownChildMenu = null;
                    selectItem(null);
                }
            } else if (item.getSubMenu() != null) {
                if (shownChildMenu == null) {
                    // open this submenu
                    openPopup(item);
                } else if (item.getSubMenu() != shownChildMenu) {
                    // close the other submenu and open this one
                    shownChildMenu.onHide(focus);
                    popup.hide();
                    openPopup(item);
                } else if (fireCommand && !autoOpen) {
                    // close this submenu
                    shownChildMenu.onHide(focus);
                    popup.hide();
                    shownChildMenu = null;
                    selectItem(item);
                }
            } else if (autoOpen && shownChildMenu != null) {
                // close submenu
                shownChildMenu.onHide(focus);
                popup.hide();
                shownChildMenu = null;
            }
        }
    }

    /**
     * Visible for testing.
     */
    PopupPanel getPopup() {
        return popup;
    }

    void itemOver(MenuItem item, boolean focus) {
        if (item == null) {
            // Don't clear selection if the currently selected item's menu is showing.
            if ((selectedItem != null) && (shownChildMenu == selectedItem.getSubMenu())) {
                return;
            }
        }

        if (item != null && !item.isEnabled()) {
            return;
        }

        // Style the item selected when the mouse enters.
        selectItem(item);
        if (focus && focusOnHover) {
            focus();
        }

        // If child menus are being shown, or this menu is itself
        // a child menu, automatically show an item's child menu
        // when the mouse enters.
        if (item != null) {
            if ((shownChildMenu != null) || (parentMenu != null) || autoOpen) {
                doItemAction(item, false, focusOnHover);
            }
        }
    }

    /**
     * Set the IDs of the menu items.
     *
     * @param baseID the base ID
     */
    void setMenuItemDebugIds(String baseID) {
        int itemCount = 0;
        for (MenuItem item : items) {
            item.ensureDebugId(baseID + "-item" + itemCount);
            itemCount++;
        }
    }

    /**
     * Show or hide the icon used for items with a submenu.
     *
     * @param item the item with or without a submenu
     */
    protected void updateSubmenuIcon(MenuItem item) {
        // The submenu icon only applies to vertical menus
        if (!vertical) {
            return;
        }

        // Get the index of the MenuItem
        int idx = allItems.indexOf(item);
        if (idx == -1) {
            return;
        }

        Element container = getItemContainerElement();
        Element tr = DOM.getChild(container, idx);
        int tdCount = DOM.getChildCount(tr);
        MenuBar submenu = item.getSubMenu();
        if (submenu == null || !item.isVisible()) {
            // Remove the submenu indicator
            if (tdCount == 2) {
                DOM.removeChild(tr, DOM.getChild(tr, 1));
            }
            setItemColSpan(item, 2);
        } else if (tdCount == 1) {
            // Show the submenu indicator
            setItemColSpan(item, 1);
            Element td = DOM.createTD();
            DOM.setElementProperty(td, "vAlign", "middle");
            DOM.setInnerHTML(td, subMenuIcon.getHTML());
            setStyleName(td, "subMenuIcon");
            DOM.appendChild(tr, td);
        }
    }

    /**
     * Physically add the td element of a {@link MenuItem} or
     * {@link MenuItemSeparator} to this {@link MenuBar}.
     *
     * @param beforeIndex the index where the separator should be inserted
     * @param tdElem the td element to be added
     */
    private void addItemElement(int beforeIndex, Element tdElem) {
        if (vertical) {
            Element tr = DOM.createTR();
            DOM.insertChild(body, tr, beforeIndex);
            DOM.appendChild(tr, tdElem);
        } else {
            Element tr = DOM.getChild(body, 0);
            DOM.insertChild(tr, tdElem, beforeIndex);
        }
    }

    /**
     * Closes this menu (if it is a popup).
     *
     * @param focus true to move focus to the parent
     */
    private void close(boolean focus) {
        if (parentMenu != null) {
            parentMenu.popup.hide(!focus);
            if (focus) {
                parentMenu.focus();
            }
        }
    }

    private void eatEvent(Event event) {
        DOM.eventCancelBubble(event, true);
        DOM.eventPreventDefault(event);
    }

    private MenuItem findItem(Element hItem) {
        for (MenuItem item : items) {
            if (DOM.isOrHasChild(item.getElement(), hItem)) {
                return item;
            }
        }
        return null;
    }

    private Element getItemContainerElement() {
        if (vertical) {
            return body;
        } else {
            return DOM.getChild(body, 0);
        }
    }

    private void init(boolean vertical, AbstractImagePrototype subMenuIcon) {
        this.subMenuIcon = subMenuIcon;

        Element table = DOM.createTable();
        body = DOM.createTBody();
        DOM.appendChild(table, body);

        if (!vertical) {
            Element tr = DOM.createTR();
            DOM.appendChild(body, tr);
        }

        this.vertical = vertical;

        com.google.gwt.dom.client.Element outer = FocusPanel.impl.createFocusable();
        DOM.appendChild(outer, table);
        setElement(outer);

        Accessibility.setRole(getElement(), Accessibility.ROLE_MENUBAR);

        sinkEvents(Event.ONCLICK | Event.ONMOUSEOVER | Event.ONMOUSEOUT | Event.ONFOCUS | Event.ONKEYDOWN);

        setStyleName(STYLENAME_DEFAULT);
        if (vertical) {
            addStyleDependentName("vertical");
        } else {
            addStyleDependentName("horizontal");
        }

        // Hide focus outline in Mozilla/Webkit/Opera
        DOM.setStyleAttribute(getElement(), "outline", "0px");

        // Hide focus outline in IE 6/7
        DOM.setElementAttribute(getElement(), "hideFocus", "true");

        // Deselect items when blurring without a child menu.
        addDomHandler(new BlurHandler() {
            public void onBlur(BlurEvent event) {
                if (shownChildMenu == null) {
                    selectItem(null);
                }
            }
        }, BlurEvent.getType());
    }

    private void moveToNextItem() {
        if (selectFirstItemIfNoneSelected()) {
            return;
        }

        if (!vertical) {
            selectNextItem();
        } else {
            if (selectedItem.getSubMenu() != null && !selectedItem.getSubMenu().getItems().isEmpty()
                    && (shownChildMenu == null || shownChildMenu.getSelectedItem() == null)) {
                if (shownChildMenu == null) {
                    doItemAction(selectedItem, false, true);
                }
                selectedItem.getSubMenu().focus();
            } else if (parentMenu != null) {
                if (!parentMenu.vertical) {
                    parentMenu.selectNextItem();
                } else {
                    parentMenu.moveToNextItem();
                }
            }
        }
    }

    private void moveToPrevItem() {
        if (selectFirstItemIfNoneSelected()) {
            return;
        }

        if (!vertical) {
            selectPrevItem();
        } else {
            if ((parentMenu != null) && (!parentMenu.vertical)) {
                parentMenu.selectPrevItem();
            } else {
                close(true);
            }
        }
    }

    /*
     * This method is called when a menu bar is hidden, so that it can hide any
     * child popups that are currently being shown.
     */
    private void onHide(boolean focus) {
        if (shownChildMenu != null) {
            shownChildMenu.onHide(focus);
            popup.hide();
            if (focus) {
                focus();
            }
        }
    }

    /*
     * This method is called when a menu bar is shown.
     */
    private void onShow() {
        // clear the selection; a keyboard user can cursor down to the first item
        selectItem(null);
    }

    private void openPopup(final MenuItem item) {
        // Only the last popup to be opened should preview all event
        if (parentMenu != null && parentMenu.popup != null) {
            parentMenu.popup.setPreviewingAllNativeEvents(false);
        }

        // Create a new popup for this item, and position it next to
        // the item (below if this is a horizontal menu bar, to the
        // right if it's a vertical bar).
        popup = new DecoratedPopupPanel(true, false, "menuPopup") {
            {
                setWidget(item.getSubMenu());
                setPreviewingAllNativeEvents(true);
                item.getSubMenu().onShow();
            }

            @Override
            protected void onPreviewNativeEvent(NativePreviewEvent event) {
                // Hook the popup panel's event preview. We use this to keep it from
                // auto-hiding when the parent menu is clicked.
                if (!event.isCanceled()) {

                    switch (event.getTypeInt()) {
                    case Event.ONMOUSEDOWN:
                        // If the event target is part of the parent menu, suppress the
                        // event altogether.
                        EventTarget target = event.getNativeEvent().getEventTarget();
                        Element parentMenuElement = item.getParentMenu().getElement();
                        if (parentMenuElement.isOrHasChild(Element.as(target))) {
                            event.cancel();
                            return;
                        }
                        super.onPreviewNativeEvent(event);
                        if (event.isCanceled()) {
                            selectItem(null);
                        }
                        return;
                    }
                }
                super.onPreviewNativeEvent(event);
            }
        };
        popup.setAnimationType(AnimationType.ONE_WAY_CORNER);
        popup.setAnimationEnabled(isAnimationEnabled);
        popup.setStyleName(STYLENAME_DEFAULT + "Popup");
        String primaryStyleName = getStylePrimaryName();
        if (!STYLENAME_DEFAULT.equals(primaryStyleName)) {
            popup.addStyleName(primaryStyleName + "Popup");
        }
        popup.addPopupListener(this);

        shownChildMenu = item.getSubMenu();
        item.getSubMenu().parentMenu = this;

        // Show the popup, ensuring that the menubar's event preview remains on top
        // of the popup's.
        popup.setPopupPositionAndShow(new PopupPanel.PositionCallback() {

            public void setPosition(int offsetWidth, int offsetHeight) {

                // depending on the bidi direction position a menu on the left or right
                // of its base item
                if (LocaleInfo.getCurrentLocale().isRTL()) {
                    if (vertical) {
                        popup.setPopupPosition(MenuBar.this.getAbsoluteLeft() - offsetWidth + 1,
                                item.getAbsoluteTop());
                    } else {
                        popup.setPopupPosition(item.getAbsoluteLeft() + item.getOffsetWidth() - offsetWidth,
                                MenuBar.this.getAbsoluteTop() + MenuBar.this.getOffsetHeight() - 1);
                    }
                } else {
                    if (vertical) {
                        popup.setPopupPosition(MenuBar.this.getAbsoluteLeft() + MenuBar.this.getOffsetWidth() - 1,
                                item.getAbsoluteTop());
                    } else {
                        popup.setPopupPosition(item.getAbsoluteLeft(),
                                MenuBar.this.getAbsoluteTop() + MenuBar.this.getOffsetHeight() - 1);
                    }
                }
            }
        });
    }

    /**
     * Removes the specified item from the {@link MenuBar} and the physical DOM
     * structure.
     *
     * @param item the item to be removed
     * @return true if the item was removed
     */
    private boolean removeItemElement(UIObject item) {
        int idx = allItems.indexOf(item);
        if (idx == -1) {
            return false;
        }

        Element container = getItemContainerElement();
        DOM.removeChild(container, DOM.getChild(container, idx));
        allItems.remove(idx);
        return true;
    }

    /**
     * Selects the first item in the menu if no items are currently selected. Has
     * no effect if there are no items.
     *
     * @return true if no item was previously selected, false otherwise
     */
    private boolean selectFirstItemIfNoneSelected() {
        if (selectedItem == null) {
            for (MenuItem nextItem : items) {
                if (nextItem.isEnabled()) {
                    selectItem(nextItem);
                    break;
                }
            }
            return true;
        }
        return false;
    }

    private void selectNextItem() {
        if (selectedItem == null) {
            return;
        }

        int index = items.indexOf(selectedItem);
        // We know that selectedItem is set to an item that is contained in the
        // items collection.
        // Therefore, we know that index can never be -1.
        assert (index != -1);

        MenuItem itemToBeSelected;

        int firstIndex = index;
        while (true) {
            index = index + 1;
            if (index == items.size()) {
                // we're at the end, loop around to the start
                index = 0;
            }
            if (index == firstIndex) {
                itemToBeSelected = items.get(firstIndex);
                break;
            } else {
                itemToBeSelected = items.get(index);
                if (itemToBeSelected.isEnabled()) {
                    break;
                }
            }
        }

        selectItem(itemToBeSelected);
        if (shownChildMenu != null) {
            doItemAction(itemToBeSelected, false, true);
        }
    }

    private void selectPrevItem() {
        if (selectedItem == null) {
            return;
        }

        int index = items.indexOf(selectedItem);
        // We know that selectedItem is set to an item that is contained in the
        // items collection.
        // Therefore, we know that index can never be -1.
        assert (index != -1);

        MenuItem itemToBeSelected;

        int firstIndex = index;
        while (true) {
            index = index - 1;
            if (index < 0) {
                // we're at the start, loop around to the end
                index = items.size() - 1;
            }
            if (index == firstIndex) {
                itemToBeSelected = items.get(firstIndex);
                break;
            } else {
                itemToBeSelected = items.get(index);
                if (itemToBeSelected.isEnabled()) {
                    break;
                }
            }
        }

        selectItem(itemToBeSelected);
        if (shownChildMenu != null) {
            doItemAction(itemToBeSelected, false, true);
        }
    }

    /**
     * Set the colspan of a {@link MenuItem} or {@link MenuItemSeparator}.
     *
     * @param item the {@link MenuItem} or {@link MenuItemSeparator}
     * @param colspan the colspan
     */
    private void setItemColSpan(UIObject item, int colspan) {
        DOM.setElementPropertyInt(item.getElement(), "colSpan", colspan);
    }
}