/*
 * 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.aria.client.Id;
import com.google.gwt.aria.client.Roles;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
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.safehtml.shared.annotations.IsSafeHtml;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;

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.core.client.Scheduler.ScheduledCommand} 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 final class MenuPopup extends DecoratedPopupPanel {
    private boolean towardsEast = !LocaleInfo.getCurrentLocale().isRTL();

    public MenuPopup() {
      super(true, false, "menuPopup");
      setAnimationType(AnimationType.ONE_WAY_CORNER);
      setAnimationEnabled(isAnimationEnabled);
      setStyleName(STYLENAME_DEFAULT + "Popup");
      String primaryStyleName = MenuBar.this.getStylePrimaryName();
      if (!STYLENAME_DEFAULT.equals(primaryStyleName)) {
        addStyleName(primaryStyleName + "Popup");
      }
      setPreviewingAllNativeEvents(true);
    }

    @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 = MenuBar.this.getElement();
            if (parentMenuElement.isOrHasChild(Element.as(target))) {
              event.cancel();
              return;
            }
            super.onPreviewNativeEvent(event);
            if (event.isCanceled()) {
              selectItem(null);
            }
            return;
        }
      }
      super.onPreviewNativeEvent(event);
    }

    public void positionBelow(MenuItem target) {
      int top = MenuBar.this.getAbsoluteTop() + MenuBar.this.getOffsetHeight();
      int left = towardsEast ? leftOf(target) : rightOf(target) - getOffsetWidth();
      setPositionInClient(left, top);
    }

    public void positionNextTo(MenuItem target) {
      // Calculate top
      int offsetTop = target.getSubMenu().getAbsoluteTop() - getAbsoluteTop();
      int top = target.getAbsoluteTop() - offsetTop;

      // Calculate left for alternative directions
      int leftIfTowardEast = rightOf(MenuBar.this);
      int leftIfTowardWest = leftOf(MenuBar.this) - getOffsetWidth();

      // Choose direction to show
      int overflowIfTowardsEast = leftIfTowardEast + getOffsetWidth() - getClientRight();
      int overflowIfTowardsWest = getClientLeft() - leftIfTowardWest;
      selectDirection(overflowIfTowardsEast, overflowIfTowardsWest);

      int left = towardsEast ? leftIfTowardEast : leftIfTowardWest;

      setPositionInClient(left, top);
    }

    private void setPositionInClient(int left, int top) {
      // Keep the popup inside client area
      if (getOffsetWidth() < Window.getClientWidth()) {
        left = Math.min(left, getClientRight() - getOffsetWidth());
        left = Math.max(getClientLeft(), left);
      }
      setPopupPosition(left, top);
    }

    private void selectDirection(int overflowIfTowardsEast, int overflowIfTowardsWest) {
      if (overflowIfTowardsEast <= 0 && overflowIfTowardsWest <= 0) {
        // Fits both sides, use the direction from parent - if there is one
        if (parentMenu != null && parentMenu.popup != null) {
          towardsEast = parentMenu.popup.towardsEast;
        }
      } else {
        // Doesn't fit both sides, use the side with less or no overflow
        towardsEast = (overflowIfTowardsEast < overflowIfTowardsWest);
      }
    }

    private int leftOf(UIObject object) {
      return object.getAbsoluteLeft();
    }

    private int rightOf(UIObject object) {
      return object.getAbsoluteLeft() + object.getOffsetWidth();
    }

    private int getClientLeft() {
      return Window.getScrollLeft();
    }

    private int getClientRight() {
      return getClientLeft() + Window.getClientWidth();
    }
  }

  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 MenuPopup 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);
  }

  @Override
  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, ScheduledCommand 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(@IsSafeHtml String text, boolean asHTML, ScheduledCommand 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(@IsSafeHtml 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, ScheduledCommand 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) {
      container.removeChild(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;
  }

  @Override
  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, false);
        }
        break;
      }

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

      case Event.ONKEYDOWN: {
        int keyCode = event.getKeyCode();
        boolean isRtl = LocaleInfo.getCurrentLocale().isRTL();
        keyCode = KeyCodes.maybeSwapArrowKeysForRtl(keyCode, isRtl);
        switch (keyCode) {
          case KeyCodes.KEY_LEFT:
            moveToPrevItem();
            eatEvent(event);
            break;
          case KeyCodes.KEY_RIGHT:
            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
   */
  @Override
  @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();
    }

    onHide(!autoClosed && focusOnHover);
    CloseEvent.fire(MenuBar.this, sender);
    // When the menu popup closes, remember that no item is
    // currently showing a popup menu.
    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 (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);
        }
      }

      Roles.getMenubarRole().setAriaActivedescendantProperty(getElement(),
          Id.of(item.getElement()));
    }

    selectedItem = item;
  }

  @Override
  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 the command should be fired and the item has one, fire it
    if (fireCommand && item.getScheduledCommand() != null) {
      // Close this menu and all of its parents.
      closeAllParents();

      // Remove the focus from the menu
      FocusPanel.impl.blur(getElement());

      // Fire the item's command. The command must be fired in the same event
      // loop or popup blockers will prevent popups from opening.
      final ScheduledCommand cmd = item.getScheduledCommand();
      Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() {
        @Override
        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 != 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
   */
  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) {
      // Remove the submenu indicator
      if (tdCount == 2) {
        tr.removeChild(DOM.getChild(tr, 1));
      }
      setItemColSpan(item, 2);
    } else if (tdCount == 1) {
      // Show the submenu indicator
      setItemColSpan(item, 1);
      Element td = DOM.createTD();
      td.setPropertyString("vAlign", "middle");
      td.setInnerSafeHtml(subMenuIcon.getSafeHtml());
      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) {
    event.stopPropagation();
    event.preventDefault();
  }

  private MenuItem findItem(Element hItem) {
    for (MenuItem item : items) {
      if (item.getElement().isOrHasChild(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;

    Element outer = FocusPanel.impl.createFocusable();
    DOM.appendChild(outer, table);
    setElement(outer);

    Roles.getMenubarRole().set(getElement());

    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
    getElement().getStyle().setProperty("outline", "0px");

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

    // Deselect items when blurring without a child menu.
    addDomHandler(new BlurHandler() {
      @Override
      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();
      }
    }
  }

  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);
    }

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

    popup = new MenuPopup();
    popup.setWidget(shownChildMenu);
    popup.addPopupListener(this);
    popup.setPopupPositionAndShow(new PositionCallback() {
      @Override
      public void setPosition(int offsetWidth, int offsetHeight) {
        if (vertical) {
          popup.positionNextTo(item);
        } else {
          popup.positionBelow(item);
        }
      }
    });
  }

  /**
   * 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();
    container.removeChild(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) {
    item.getElement().setPropertyInt("colSpan", colspan);
  }
}
