/* * 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 java.util.ArrayList; import java.util.List; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.EventTarget; 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.user.client.Command; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.DeferredCommand; 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; /** * 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> * <g:MenuBar> * <g:MenuItem>Higgledy * <g:MenuBar vertical="true"> * <g:MenuItem>able</g:MenuItem> * <g:MenuItem>baker</g:MenuItem> * <g:MenuItem>charlie</g:MenuItem> * </g:MenuBar> * </g:MenuItem> * <g:MenuItem>Piggledy * <g:MenuBar vertical="true"> * <g:MenuItem>foo</g:MenuItem> * <g:MenuItem>bar</g:MenuItem> * <g:MenuItem>baz</g:MenuItem> * </g:MenuBar> * </g:MenuItem> * <g:MenuItem><b>Pop!</b> * <g:MenuBar vertical="true"> * <g:MenuItem>uno</g:MenuItem> * <g:MenuItem>dos</g:MenuItem> * <g:MenuItem>tres</g:MenuItem> * </g:MenuBar> * </g:MenuItem> * </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, 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 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(); } /** * 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 seperator 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 seperator 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(); } } @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: 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(); } eatEvent(event); 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 (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() { MenuBar curMenu = this; while (curMenu.parentMenu != null) { curMenu.close(); curMenu = curMenu.parentMenu; } } /* * 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) { // 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. Command cmd = item.getCommand(); DeferredCommand.addCommand(cmd); // 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; } } } 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; } } // 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) { 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 seperator 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). */ private void close() { if (parentMenu != null) { parentMenu.popup.hide(); 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; 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"); } 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(); } } } /* * 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() || popupPosition == PopupPosition.LEFT) { 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) { if (items.size() > 0) { MenuItem nextItem = items.get(0); selectItem(nextItem); } 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; if (index < items.size() - 1) { itemToBeSelected = items.get(index + 1); } else { // we're at the end, loop around to the start itemToBeSelected = items.get(0); } 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; if (index > 0) { itemToBeSelected = items.get(index - 1); } else { // we're at the start, loop around to the end itemToBeSelected = items.get(items.size() - 1); } 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); } // --- customization --- private PopupPosition popupPosition = PopupPosition.AUTO; public void setPopupPosition(PopupPosition position) { this.popupPosition = position; } public enum PopupPosition { LEFT, RIGHT, AUTO; } }