/*
* Ext GWT - Ext for GWT
* Copyright(c) 2007-2009, Ext JS, LLC.
* licensing@extjs.com
*
* http://extjs.com/license
*/
package com.extjs.gxt.ui.client.widget.menu;
import com.extjs.gxt.ui.client.GXT;
import com.extjs.gxt.ui.client.Style;
import com.extjs.gxt.ui.client.aria.FocusFrame;
import com.extjs.gxt.ui.client.core.El;
import com.extjs.gxt.ui.client.core.XDOM;
import com.extjs.gxt.ui.client.event.BaseEvent;
import com.extjs.gxt.ui.client.event.ClickRepeaterEvent;
import com.extjs.gxt.ui.client.event.ComponentEvent;
import com.extjs.gxt.ui.client.event.ContainerEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.event.MenuEvent;
import com.extjs.gxt.ui.client.event.PreviewEvent;
import com.extjs.gxt.ui.client.util.BaseEventPreview;
import com.extjs.gxt.ui.client.util.ClickRepeater;
import com.extjs.gxt.ui.client.util.KeyNav;
import com.extjs.gxt.ui.client.util.Point;
import com.extjs.gxt.ui.client.widget.Component;
import com.extjs.gxt.ui.client.widget.Container;
import com.extjs.gxt.ui.client.widget.layout.MenuLayout;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.event.dom.client.KeyCodes;
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.ui.Accessibility;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
/**
* A menu component.
*
* <dl>
* <dt><b>Events:</b></dt>
*
* <dd><b>BeforeShow</b> : MenuEvent(container)<br>
* <div>Fires before this menu is displayed. Listeners can cancel the action by
* calling {@link BaseEvent#setCancelled(boolean)}.</div>
* <ul>
* <li>container : this</li>
* </ul>
* </dd>
*
* <dd><b>Show</b> : MenuEvent(container)<br>
* <div>Fires after this menu is displayed.</div>
* <ul>
* <li>container : this</li>
* </ul>
* </dd>
*
* <dd><b>BeforeHide</b> : MenuEvent(container)<br>
* <div>Fired before the menu is hidden. Listeners can cancel the action by
* calling {@link BaseEvent#setCancelled(boolean)}.</div>
* <ul>
* <li>container : this</li>
* </ul>
* </dd>
*
* <dd><b>Hide</b> : MenuEvent(container)<br>
* <div>Fires after this menu is hidden.</div>
* <ul>
* <li>container : this</li>
* </ul>
* </dd>
*
* <dd><b>BeforeAdd</b> : MenuEvent(container, item, index)<br>
* <div>Fires before a item is added or inserted. Listeners can cancel the
* action by calling {@link BaseEvent#setCancelled(boolean)}.</div>
* <ul>
* <li>container : this</li>
* <li>item : the item being added</li>
* <li>index : the index at which the item will be added</li>
* </ul>
* </dd>
*
* <dd><b>BeforeRemove</b> : MenuEvent(container, item)<br>
* <div>Fires before a item is removed. Listeners can cancel the action by
* calling {@link BaseEvent#setCancelled(boolean)}.</div>
* <ul>
* <li>container : this</li>
* <li>item : the item being removed</li>
* </ul>
* </dd>
*
* <dd><b>Add</b> : MenuEvent(container, item, index)<br>
* <div>Fires after a item has been added or inserted.</div>
* <ul>
* <li>container : this</li>
* <li>item : the item that was added</li>
* <li>index : the index at which the item will be added</li>
* </ul>
* </dd>
*
* <dd><b>Remove</b> : MenuEvent(container, item)<br>
* <div>Fires after a item has been removed.</div>
* <ul>
* <li>container : this</li>
* <li>item : the item being removed</li>
* </ul>
* </dd>
* </dl>
*
* <dl>
* <dt>Inherited Events:</dt>
* <dd>BoxComponent Move</dd>
* <dd>BoxComponent Resize</dd>
* <dd>Component Enable</dd>
* <dd>Component Disable</dd>
* <dd>Component BeforeHide</dd>
* <dd>Component Hide</dd>
* <dd>Component BeforeShow</dd>
* <dd>Component Show</dd>
* <dd>Component Attach</dd>
* <dd>Component Detach</dd>
* <dd>Component BeforeRender</dd>
* <dd>Component Render</dd>
* <dd>Component BrowserEvent</dd>
* <dd>Component BeforeStateRestore</dd>
* <dd>Component StateRestore</dd>
* <dd>Component BeforeStateSave</dd>
* <dd>Component SaveState</dd>
* </dl>
*/
public class Menu extends Container<Component> {
protected KeyNav<ComponentEvent> keyNav;
protected Item parentItem;
protected BaseEventPreview eventPreview;
protected boolean plain;
protected boolean showSeparator = true;
protected El ul;
private String subMenuAlign = "tl-tr?";
private String defaultAlign = "tl-bl?";
private int minWidth = 120;
private Item activeItem;
private boolean showing;
private boolean constrainViewport = true;
private El focusEl;
private boolean focusOnShow = true;
private int maxHeight = Style.DEFAULT;
private boolean enableScrolling = true;
private int scrollIncrement = 24;
private int scrollerHeight = 8;
private int activeMax;
/**
* Creates a new menu.
*/
public Menu() {
baseStyle = "x-menu";
shim = true;
monitorWindowResize = true;
setShadow(true);
setLayoutOnChange(true);
enableLayout = true;
setLayout(new MenuLayout());
eventPreview = new BaseEventPreview() {
@Override
protected boolean onAutoHide(PreviewEvent pe) {
return Menu.this.onAutoHide(pe);
}
@Override
protected void onPreviewKeyPress(PreviewEvent pe) {
super.onPreviewKeyPress(pe);
if (pe.getKeyCode() == KeyCodes.KEY_ESCAPE) {
hide(true);
}
}
};
}
/**
* Adds a item to the menu.
*
* @param item the new item
*/
@Override
public boolean add(Component item) {
return super.add(item);
}
/**
* Returns the default alignment.
*
* @return the default align
*/
public String getDefaultAlign() {
return defaultAlign;
}
public El getFocusEl() {
return focusEl;
}
@Override
public El getLayoutTarget() {
return ul;
}
public int getMaxHeight() {
return maxHeight;
}
/**
* Returns the menu's minimum width.
*
* @return the width
*/
public int getMinWidth() {
return minWidth;
}
/**
* Returns the menu's parent item.
*
* @return the parent item
*/
public Item getParentItem() {
return parentItem;
}
/**
* Returns the sub menu alignment.
*
* @return the alignment
*/
public String getSubMenuAlign() {
return subMenuAlign;
}
/**
* Hides the menu.
*/
public void hide() {
hide(false);
}
/**
* Hides this menu and optionally all parent menus
*
* @param deep true to close all parent menus
* @return this
*/
public Menu hide(boolean deep) {
if (showing) {
MenuEvent me = new MenuEvent(this);
if (fireEvent(Events.BeforeHide, me)) {
if (activeItem != null) {
activeItem.deactivate();
activeItem = null;
}
onHide();
RootPanel.get().remove(this);
eventPreview.remove();
showing = false;
hidden = true;
fireEvent(Events.Hide, me);
}
if (deep && parentItem != null) {
parentItem.parentMenu.hide(true);
}
}
return this;
}
/**
* Inserts an item into the menu.
*
* @param item the item to insert
* @param index the insert location
*/
@Override
public boolean insert(Component item, int index) {
if (item instanceof Item) {
((Item) item).parentMenu = this;
}
return super.insert(item, index);
}
/**
* Returns true if constrain to viewport is enabled.
*
* @return the constrain to viewport state
*/
public boolean isConstrainViewport() {
return constrainViewport;
}
public boolean isEnableScrolling() {
return enableScrolling;
}
public boolean isFocusOnShow() {
return focusOnShow;
}
@Override
public boolean isVisible() {
return showing;
}
@Override
public void onComponentEvent(ComponentEvent ce) {
super.onComponentEvent(ce);
switch (ce.getEventTypeInt()) {
case Event.ONCLICK:
onClick(ce);
break;
case Event.ONMOUSEOVER:
onMouseOver(ce);
break;
case Event.ONMOUSEOUT:
onMouseOut(ce);
break;
case Event.ONMOUSEWHEEL:
if (enableScrolling) {
scrollMenu(ce.getEvent().getMouseWheelVelocityY() < 0);
}
}
El t = ce.getTargetEl();
if (enableScrolling && t.is(".x-menu-scroller")) {
switch (ce.getEventTypeInt()) {
case Event.ONMOUSEOVER:
deactiveActiveItem();
onScrollerIn(t);
break;
case Event.ONMOUSEOUT:
onScrollerOut(t);
break;
}
}
}
/**
* Removes a item from the menu.
*
* @param item the menu to remove
*/
@Override
public boolean remove(Component item) {
return super.remove(item);
}
/**
* Sets whether the menu should be constrained to the viewport when shown.
* Only applies when using {@link #showAt(int, int)}.
*
* @param constrainViewport true to contrain
*/
public void setConstrainViewport(boolean constrainViewport) {
this.constrainViewport = constrainViewport;
}
/**
* Sets the default {@link El#alignTo} anchor position value for this menu
* relative to its element of origin (defaults to "tl-bl?").
*
* @param defaultAlign the default align
*/
public void setDefaultAlign(String defaultAlign) {
this.defaultAlign = defaultAlign;
}
public void setEnableScrolling(boolean enableScrolling) {
this.enableScrolling = enableScrolling;
}
public void setFocusOnShow(boolean focusOnShow) {
this.focusOnShow = focusOnShow;
}
public void setMaxHeight(int maxHeight) {
this.maxHeight = maxHeight;
}
/**
* Sets he minimum width of the menu in pixels (defaults to 120).
*
* @param minWidth the min width
*/
public void setMinWidth(int minWidth) {
this.minWidth = minWidth;
}
/**
* The {@link El#alignTo} anchor position value to use for submenus of this
* menu (defaults to "tl-tr-?").
*
* @param subMenuAlign the sub alignment
*/
public void setSubMenuAlign(String subMenuAlign) {
this.subMenuAlign = subMenuAlign;
}
/**
* Displays this menu relative to another element.
*
* @param elem the element to align to
* @param pos the {@link El#alignTo} anchor position to use in aligning to the
* element (defaults to defaultAlign)
*/
public void show(Element elem, String pos) {
show(elem, pos, new int[] {0, 0});
}
/**
* Displays this menu relative to another element.
*
* @param elem the element to align to
* @param pos the {@link El#alignTo} anchor position to use in aligning to the
* element (defaults to defaultAlign)
* @param offsets the menu align offsets
*/
public void show(Element elem, String pos, int[] offsets) {
MenuEvent me = new MenuEvent(this);
if (fireEvent(Events.BeforeShow, me)) {
RootPanel.get().add(this);
showing = true;
el().makePositionable(true);
onShow();
el().updateZIndex(0);
// autoSizing when visible
layout();
el().alignTo(elem, pos, offsets);
if (enableScrolling) {
constrainScroll(el().getY());
}
el().show();
eventPreview.add();
if (focusOnShow) {
focus();
}
fireEvent(Events.Show, me);
}
}
/**
* Displays this menu relative to the widget using the default alignment.
*
* @param widget the align widget
*/
public void show(Widget widget) {
show(widget.getElement(), defaultAlign);
}
/**
* Displays this menu at a specific xy position.
*
* @param x the x coordinate
* @param y the y coordinate
*/
public void showAt(int x, int y) {
MenuEvent me = new MenuEvent(this);
if (fireEvent(Events.BeforeShow, me)) {
RootPanel.get().add(this);
showing = true;
el().makePositionable(true);
onShow();
el().updateZIndex(0);
// autoSizing when visible
layout();
if (constrainViewport) {
Point p = el().adjustForConstraints(new Point(x, y));
x = p.x;
y = p.y;
}
setPagePosition(x + XDOM.getBodyScrollLeft(), y + XDOM.getBodyScrollTop());
if (enableScrolling) {
constrainScroll(y);
}
el().show();
eventPreview.add();
if (focusOnShow) {
focus();
}
fireEvent(Events.Show, me);
}
}
protected void constrainScroll(int y) {
int full = ul.setHeight("auto").getHeight();
int max = maxHeight != Style.DEFAULT ? maxHeight : (XDOM.getViewHeight(false) - y);
if (full > max && max > 0) {
activeMax = max - 10 - scrollerHeight * 2;
ul.setHeight(activeMax, true);
createScrollers();
} else {
ul.setHeight(full, true);
NodeList<Element> nodes = el().select(".x-menu-scroller");
for (int i = 0; i < nodes.getLength(); i++) {
fly(nodes.getItem(i)).hide();
}
}
ul.setScrollTop(0);
}
@Override
protected ComponentEvent createComponentEvent(Event event) {
return new MenuEvent(this);
}
@Override
protected ContainerEvent<Menu, Component> createContainerEvent(Component item) {
return new MenuEvent(this, item);
}
protected void createScrollers() {
if (el().select(".x-menu-scroller").getLength() == 0) {
Listener<ClickRepeaterEvent> listener = new Listener<ClickRepeaterEvent>() {
public void handleEvent(ClickRepeaterEvent be) {
onScroll(be);
}
};
El scroller;
scroller = new El(DOM.createDiv());
scroller.addStyleName("x-menu-scroller", "x-menu-scroller-top");
scroller.setInnerHtml(" ");
ClickRepeater cr = new ClickRepeater(scroller);
cr.doAttach();
cr.addListener(Events.OnClick, listener);
addAttachable(cr);
el().insertFirst(scroller.dom);
scroller = new El(DOM.createDiv());
scroller.addStyleName("x-menu-scroller", "x-menu-scroller-bottom");
scroller.setInnerHtml(" ");
cr = new ClickRepeater(scroller);
cr.doAttach();
cr.addListener(Events.OnClick, listener);
addAttachable(cr);
el().appendChild(scroller.dom);
}
}
protected void deactiveActiveItem() {
if (activeItem != null) {
activeItem.deactivate();
activeItem = null;
}
}
protected boolean onAutoHide(PreviewEvent pe) {
if (pe.within(getElement()) || pe.getTarget(".x-menu", 20) != null) {
return false;
}
MenuEvent me = new MenuEvent(this);
me.setEvent(pe.getEvent());
if (fireEvent(Events.AutoHide, me)) {
hide(true);
return true;
}
return false;
}
protected void onClick(ComponentEvent ce) {
ce.stopEvent();
Component item = findItem(ce.getTarget());
if (item != null && item instanceof Item) {
((Item) item).onClick(ce);
}
}
@Override
protected void onDetach() {
super.onDetach();
if (eventPreview != null) {
eventPreview.remove();
}
}
protected void onHide() {
super.onHide();
deactiveActiveItem();
}
protected void onMouseOut(ComponentEvent ce) {
Component item = findItem(ce.getTarget());
if (item != null) {
if (item == activeItem && !ce.within(getElement()) && activeItem.shouldDeactivate(ce)) {
deactiveActiveItem();
}
} else {
if (activeItem != null && activeItem.shouldDeactivate(ce)) {
deactiveActiveItem();
}
}
}
protected void onMouseOver(ComponentEvent ce) {
Component c = findItem(ce.getTarget());
if (c != null) {
if (c instanceof Item) {
Item item = (Item) c;
if (item.canActivate && item.isEnabled()) {
setActiveItem(item, true);
}
}
}
}
protected void onRender(Element target, int index) {
setElement(DOM.createDiv(), target, index);
el().makePositionable(true);
super.onRender(target, index);
keyNav = new KeyNav<ComponentEvent>(this) {
public void onDown(ComponentEvent be) {
if (tryActivate(indexOf(activeItem) + 1, 1) == null) {
tryActivate(0, 1);
}
}
public void onEnter(ComponentEvent be) {
if (activeItem != null) {
be.cancelBubble();
activeItem.onClick(be);
}
}
public void onLeft(ComponentEvent be) {
hide();
if (parentItem != null) {
parentItem.parentMenu.focus();
if (GXT.isAriaEnabled()) {
FocusFrame.get().frame(parentItem);
}
}
}
public void onRight(ComponentEvent be) {
if (activeItem != null) {
activeItem.expandMenu(true);
}
}
public void onUp(ComponentEvent be) {
if (tryActivate(indexOf(activeItem) - 1, -1) == null) {
tryActivate(getItemCount() - 1, -1);
}
}
};
focusEl = new El(DOM.createAnchor());
focusEl.addStyleName("x-menu-focus");
focusEl.setTabIndex(-1);
getElement().appendChild(focusEl.dom);
swallowEvent(Events.OnClick, focusEl.dom, true);
ul = new El(DOM.createElement("ul"));
ul.addStyleName(baseStyle + "-list");
getElement().appendChild(ul.dom);
// add menu to ignore list
eventPreview.getIgnoreList().add(getElement());
el().setTabIndex(0);
el().setElementAttribute("hideFocus", "true");
if (GXT.isAriaEnabled()) {
Accessibility.setRole(getElement(), "menu");
Accessibility.setRole(ul.dom, "presentation");
}
if (plain) {
addStyleName("x-menu-plain");
}
if (!showSeparator) {
addStyleName("x-menu-nosep");
}
sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.KEYEVENTS | Event.ONMOUSEWHEEL);
}
protected void onScroll(ClickRepeaterEvent ce) {
El target = ce.getEl();
boolean top = target.is(".x-menu-scroller-top");
scrollMenu(top);
if (top ? ul.getScrollTop() <= 0 : ul.getScrollTop() + activeMax >= ul.dom.getPropertyInt("scrollHeight")) {
onScrollerOut(target);
}
}
// private
protected void onScrollerIn(El t) {
boolean top = t.is(".x-menu-scroller-top");
if (top ? ul.getScrollTop() > 0 : ul.getScrollTop() + this.activeMax < ul.dom.getPropertyInt("scrollHeight")) {
t.addStyleName("x-menu-item-active", "x-menu-scroller-active");
}
}
// private
protected void onScrollerOut(El t) {
t.removeStyleName("x-menu-item-active", "x-menu-scroller-active");
}
protected void onWindowResize(int width, int height) {
hide(true);
}
protected void scrollMenu(boolean top) {
ul.setScrollTop(ul.getScrollTop() + scrollIncrement * (top ? -1 : 1));
}
protected void setActiveItem(Component c, boolean autoExpand) {
if (c instanceof Item) {
Item item = (Item) c;
if (item != activeItem) {
deactiveActiveItem();
this.activeItem = item;
item.activate(autoExpand);
item.el().scrollIntoView(ul.dom, false);
focus();
if (GXT.isAriaEnabled()) {
FocusFrame.get().frame(item);
Accessibility.setState(getElement(), "aria-activedescendant", item.getId());
}
} else if (autoExpand) {
item.expandMenu(autoExpand);
}
}
}
protected Item tryActivate(int start, int step) {
for (int i = start, len = getItemCount(); i >= 0 && i < len; i += step) {
Component c = getItem(i);
if (c instanceof Item) {
Item item = (Item) c;
if (item.canActivate && item.isEnabled()) {
setActiveItem(item, false);
return item;
}
}
}
return null;
}
}