/*******************************************************************************
* Copyright (c) 2012-2015 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.ide.toolbar;
import org.eclipse.che.ide.api.action.Action;
import org.eclipse.che.ide.api.action.ActionEvent;
import org.eclipse.che.ide.api.action.ActionGroup;
import org.eclipse.che.ide.api.action.ActionManager;
import org.eclipse.che.ide.api.action.Presentation;
import org.eclipse.che.ide.api.action.Separator;
import org.eclipse.che.ide.api.action.ToggleAction;
import org.eclipse.che.ide.api.keybinding.KeyBindingAgent;
import org.eclipse.che.ide.collections.Array;
import org.eclipse.che.ide.collections.Collections;
import org.eclipse.che.ide.util.input.KeyMapUtil;
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.Style.Unit;
import com.google.gwt.dom.client.Style.Visibility;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.AbstractImagePrototype;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.UIObject;
import org.vectomatic.dom.svg.ui.SVGImage;
import org.vectomatic.dom.svg.ui.SVGResource;
/**
* PopupMenu is visual component represents all known Popup Menu.
*
* @author <a href="mailto:gavrikvetal@gmail.com">Vitaliy Gulyy</a>
*/
public class PopupMenu extends Composite {
private static final PopupResources POPUP_RESOURCES = GWT.create(PopupResources.class);
static {
POPUP_RESOURCES.popup().ensureInjected();
}
private final ActionGroup actionGroup;
private final ActionManager actionManager;
private final String place;
/** Working variable is needs to indicate when PopupMenu has at list one MenuItem with selected state. */
private boolean hasCheckedItems;
/** Callback uses for notify Parent Menu when menu item is selecred. */
private ActionSelectedHandler actionSelectedHandler;
/**
* Lock layer uses as root for displaying this PopupMenu and uses for locking screen and hiding menu when user just clicked outside
* menu.
*/
private MenuLockLayer lockLayer;
/** Contains opened sub Popup Menu. */
private PopupMenu openedSubPopup;
private Element subPopupAnchor;
/** Contains HTML element ( <TR> ) which is hovered for the current time. */
private Element hoveredTR;
/**
* Working variable.
* PopupMenu panel.
*/
private SimplePanel popupMenuPanel;
/** Working variable. Special table uses for handling mouse events. */
private PopupMenuTable table;
private PresentationFactory presentationFactory;
private KeyBindingAgent keyBindingAgent;
/**
* Prefix to be appended to the ID for each menu item.
* This is debug feature.
*/
private String itemIdPrefix;
private Array<Action> list;
private Timer openSubPopupTimer = new Timer() {
@Override
public void run() {
openSubPopup(hoveredTR);
}
};
private Timer closeSubPopupTimer = new Timer() {
@Override
public void run() {
if (openedSubPopup != null) {
openedSubPopup.closePopup();
openedSubPopup = null;
Element e = subPopupAnchor;
subPopupAnchor = null;
setStyleNormal(e);
}
}
};
/**
* Create PopupMenu
*
* @param lockLayer
* - lock layer, uses as rot for attaching this popup menu.
* @param actionSelectedHandler
* - callback, uses for notifying parent menu when menu item is selected.
*/
public PopupMenu(ActionGroup actionGroup, ActionManager actionManager, String place, PresentationFactory presentationFactory,
MenuLockLayer lockLayer, ActionSelectedHandler actionSelectedHandler, KeyBindingAgent keyBindingAgent,
String itemIdPrefix) {
this.actionGroup = actionGroup;
this.actionManager = actionManager;
this.place = place;
this.presentationFactory = presentationFactory;
this.keyBindingAgent = keyBindingAgent;
this.itemIdPrefix = itemIdPrefix;
list = Collections.createArray();
Utils.expandActionGroup(actionGroup, list, presentationFactory, place, actionManager);
this.lockLayer = lockLayer;
this.actionSelectedHandler = actionSelectedHandler;
popupMenuPanel = new SimplePanel();
initWidget(popupMenuPanel);
popupMenuPanel.addDomHandler(new MouseOutHandler() {
@Override
public void onMouseOut(MouseOutEvent event) {
closeSubPopupTimer.cancel();
PopupMenu.this.setStyleNormal(hoveredTR);
hoveredTR = null;
if (subPopupAnchor != null) {
setStyleHovered(subPopupAnchor);
}
}
}, MouseOutEvent.getType());
popupMenuPanel.setStyleName(POPUP_RESOURCES.popup().popupMenuMain());
hasCheckedItems = hasCheckedItems();
redraw();
}
private boolean isRowEnabled(Element tr) {
if (tr == null) {
return false;
}
String index = tr.getAttribute("item-index");
if (index == null || "".equals(index)) {
return false;
}
String enabled = tr.getAttribute("item-enabled");
if (enabled == null || "".equals(enabled) || "false".equals(enabled)) {
return false;
}
int itemIndex = Integer.parseInt(index);
Action menuItem = list.get(itemIndex);
return presentationFactory.getPresentation(menuItem).isEnabled();
}
/** Close this Popup Menu. */
public void closePopup() {
if (openedSubPopup != null) {
openedSubPopup.closePopup();
}
removeFromParent();
}
/** Render Popup Menu component. */
private void redraw() {
String idPrefix = itemIdPrefix;
if (idPrefix == null) {
idPrefix = "";
} else {
idPrefix += "/";
}
table = new PopupMenuTable();
table.setStyleName(POPUP_RESOURCES.popup().popupMenuTable());
table.setCellPadding(0);
table.setCellSpacing(0);
table.getElement().setAttribute("border", "0");
for (int i = 0; i < list.size(); i++) {
Action menuItem = list.get(i);
if (menuItem instanceof Separator) {
if (i > 0 && i < list.size() - 1) {
table.getFlexCellFormatter().setColSpan(i, 0, hasCheckedItems ? 5 : 4);
table.setHTML(i, 0, "<nobr><hr noshade=\"noshade\" size=\"1\"></nobr>");
table.getCellFormatter().setStyleName(i, 0, POPUP_RESOURCES.popup().popupMenuDelimiter());
}
} else {
Presentation presentation = presentationFactory.getPresentation(menuItem);
if (presentation.getSVGIcon() != null) {
SVGImage image = new SVGImage(presentation.getSVGIcon());
image.getElement().getStyle().setMarginTop(2, Unit.PX);
table.setWidget(i, 0, image);
} else {
Image image = null;
if (presentation.getIcon() != null) {
image = new Image(presentation.getIcon());
}
table.setWidget(i, 0, image);
}
table.getCellFormatter().setStyleName(i, 0,
presentation.isEnabled() ? POPUP_RESOURCES.popup().popupMenuIconField()
: POPUP_RESOURCES.popup().popupMenuIconFieldDisabled());
int work = 1;
if (hasCheckedItems && menuItem instanceof ToggleAction) {
String checkImage;
ToggleAction toggleAction = (ToggleAction)menuItem;
ActionEvent e = new ActionEvent(place, presentationFactory.getPresentation(toggleAction), actionManager, 0);
if (toggleAction.isSelected(e)) {
table.setHTML(i, work, AbstractImagePrototype.create(POPUP_RESOURCES.check()).getHTML());
}
table.getCellFormatter().setStyleName(i, work,
presentation.isEnabled() ? POPUP_RESOURCES.popup().popupMenuCheckField()
: POPUP_RESOURCES.popup()
.popupMenuCheckFieldDisabled());
work++;
}
table.setHTML(i, work, "<nobr id=\"" + idPrefix + presentation.getText() + "\">" + presentation.getText()
+ "</nobr>");
table.getCellFormatter().setStyleName(i, work,
presentation.isEnabled() ? POPUP_RESOURCES.popup().popupMenuTitleField()
: POPUP_RESOURCES.popup().popupMenuTitleFieldDisabled());
work++;
String hotKey = KeyMapUtil.getShortcutText(keyBindingAgent.getKeyBinding(actionManager.getId(menuItem)));
if (hotKey == null) {
hotKey = " ";
} else {
hotKey =
"<nobr> " + hotKey + " </nobr>";
}
table.setHTML(i, work, hotKey);
table.getCellFormatter().setStyleName(i, work,
presentation.isEnabled() ? POPUP_RESOURCES.popup().popupMenuHotKeyField()
: POPUP_RESOURCES.popup().popupMenuHotKeyFieldDisabled());
work++;
if (menuItem instanceof ActionGroup && !(((ActionGroup)menuItem).canBePerformed() &&
!Utils.hasVisibleChildren((ActionGroup)menuItem, presentationFactory,
actionManager,
place))) {
table.setWidget(i, work, new SVGImage(POPUP_RESOURCES.subMenu()));
table.getCellFormatter().setStyleName(i, work,
presentation.isEnabled() ? POPUP_RESOURCES.popup().popupMenuSubMenuField()
: POPUP_RESOURCES.popup()
.popupMenuSubMenuFieldDisabled());
} else {
table.getCellFormatter().setStyleName(i, work,
presentation.isEnabled() ? POPUP_RESOURCES.popup().popupMenuSubMenuField()
: POPUP_RESOURCES.popup()
.popupMenuSubMenuFieldDisabled());
}
work++;
table.getRowFormatter().getElement(i).setAttribute("item-index", Integer.toString(i));
table.getRowFormatter().getElement(i).setAttribute("item-enabled", Boolean.toString(presentation.isEnabled()));
String actionId = actionManager.getId(menuItem);
String debugId;
if (actionId == null) {
debugId = idPrefix + menuItem.getTemplatePresentation().getText();
} else {
debugId = idPrefix + actionId;
}
UIObject.ensureDebugId(table.getRowFormatter().getElement(i), debugId);
}
}
popupMenuPanel.add(table);
}
/** @return true when at list one item from list of menu items has selected state. */
private boolean hasCheckedItems() {
for (int i = 0; i < list.size(); i++) {
Action action = list.get(i);
if (action instanceof ToggleAction) {
ActionEvent e = new ActionEvent(place, presentationFactory.getPresentation(action), actionManager, 0);
if (((ToggleAction)action).isSelected(e)) {
return true;
}
}
}
return false;
}
/**
* Handling MouseOut event.
*
* @param row
* - element to be processed.
*/
protected void setStyleNormal(Element row) {
if (row == null) {
return;
}
if (hasCheckedItems) {
Element iconTD = DOM.getChild(row, 0);
Element checkTD = DOM.getChild(row, 1);
Element titleTD = DOM.getChild(row, 2);
Element hotKeyTD = DOM.getChild(row, 3);
Element submenuTD = DOM.getChild(row, 4);
iconTD.setClassName(POPUP_RESOURCES.popup().popupMenuIconField());
checkTD.setClassName(POPUP_RESOURCES.popup().popupMenuCheckField());
titleTD.setClassName(POPUP_RESOURCES.popup().popupMenuTitleField());
hotKeyTD.setClassName(POPUP_RESOURCES.popup().popupMenuHotKeyField());
submenuTD.setClassName(POPUP_RESOURCES.popup().popupMenuSubMenuField());
} else {
Element iconTD = DOM.getChild(row, 0);
Element titleTD = DOM.getChild(row, 1);
Element hotKeyTD = DOM.getChild(row, 2);
Element submenuTD = DOM.getChild(row, 3);
iconTD.setClassName(POPUP_RESOURCES.popup().popupMenuIconField());
titleTD.setClassName(POPUP_RESOURCES.popup().popupMenuTitleField());
hotKeyTD.setClassName(POPUP_RESOURCES.popup().popupMenuHotKeyField());
submenuTD.setClassName(POPUP_RESOURCES.popup().popupMenuSubMenuField());
}
}
private void setStyleHovered(Element tr) {
if (hasCheckedItems) {
Element iconTD = DOM.getChild(tr, 0);
Element checkTD = DOM.getChild(tr, 1);
Element titleTD = DOM.getChild(tr, 2);
Element hotKeyTD = DOM.getChild(tr, 3);
Element submenuTD = DOM.getChild(tr, 4);
iconTD.setClassName(POPUP_RESOURCES.popup().popupMenuIconFieldOver());
checkTD.setClassName(POPUP_RESOURCES.popup().popupMenuCheckFieldOver());
titleTD.setClassName(POPUP_RESOURCES.popup().popupMenuTitleFieldOver());
hotKeyTD.setClassName(POPUP_RESOURCES.popup().popupMenuHotKeyFieldOver());
submenuTD.setClassName(POPUP_RESOURCES.popup().popupMenuSubMenuFieldOver());
} else {
Element iconTD = DOM.getChild(tr, 0);
Element titleTD = DOM.getChild(tr, 1);
Element hotKeyTD = DOM.getChild(tr, 2);
Element submenuTD = DOM.getChild(tr, 3);
iconTD.setClassName(POPUP_RESOURCES.popup().popupMenuIconFieldOver());
titleTD.setClassName(POPUP_RESOURCES.popup().popupMenuTitleFieldOver());
hotKeyTD.setClassName(POPUP_RESOURCES.popup().popupMenuHotKeyFieldOver());
submenuTD.setClassName(POPUP_RESOURCES.popup().popupMenuSubMenuFieldOver());
}
}
/**
* Handling MouseOver event.
*
* @param tr
* - element to be processed.
*/
protected void onRowHovered(Element tr) {
if (tr == hoveredTR) {
return;
}
setStyleNormal(hoveredTR);
if (subPopupAnchor != null) {
setStyleHovered(subPopupAnchor);
}
if (!isRowEnabled(tr)) {
hoveredTR = null;
return;
}
hoveredTR = tr;
setStyleHovered(tr);
int itemIndex = Integer.parseInt(tr.getAttribute("item-index"));
Action menuItem = list.get(itemIndex);
openSubPopupTimer.cancel();
if (menuItem instanceof ActionGroup && !(((ActionGroup)menuItem).canBePerformed() &&
!Utils.hasVisibleChildren((ActionGroup)menuItem, presentationFactory, actionManager,
place))) {
openSubPopupTimer.schedule(300);
} else {
closeSubPopupTimer.cancel();
closeSubPopupTimer.schedule(200);
}
}
/**
* Handle Mouse Click
*
* @param tr
*/
protected void onRowClicked(Element tr) {
if (!isRowEnabled(tr) || tr == subPopupAnchor) {
return;
}
int itemIndex = Integer.parseInt(tr.getAttribute("item-index"));
Action menuItem = list.get(itemIndex);
if (menuItem instanceof ActionGroup && (!((ActionGroup)menuItem).canBePerformed() &&
Utils.hasVisibleChildren((ActionGroup)menuItem, presentationFactory, actionManager,
place))) {
openSubPopup(tr);
} else {
if (actionSelectedHandler != null) {
actionSelectedHandler.onActionSelected(menuItem);
}
ActionEvent e = new ActionEvent(place, presentationFactory.getPresentation(menuItem), actionManager, 0);
menuItem.actionPerformed(e);
}
}
private void openSubPopup(final Element tableRowElement) {
if (tableRowElement == null) {
return;
}
if (openedSubPopup != null) {
if (tableRowElement == subPopupAnchor) {
return;
}
openedSubPopup.closePopup();
}
if (subPopupAnchor != null) {
Element e = subPopupAnchor;
subPopupAnchor = null;
setStyleNormal(e);
}
subPopupAnchor = tableRowElement;
setStyleHovered(subPopupAnchor);
int itemIndex = Integer.parseInt(tableRowElement.getAttribute("item-index"));
Action menuItem = list.get(itemIndex);
String idPrefix = itemIdPrefix;
if (idPrefix != null) {
idPrefix += "/" + presentationFactory.getPresentation(menuItem).getText();
}
openedSubPopup =
new PopupMenu((ActionGroup)menuItem, actionManager, place, presentationFactory, lockLayer, actionSelectedHandler,
keyBindingAgent, idPrefix);
final int HORIZONTAL_OFFSET = 3;
final int VERTICAL_OFFSET = 1;
openedSubPopup.getElement().getStyle().setVisibility(Visibility.HIDDEN);
lockLayer.add(openedSubPopup, 0, 0);
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
int left = getAbsoluteLeft() + getOffsetWidth() - HORIZONTAL_OFFSET;
int top = tableRowElement.getAbsoluteTop() - lockLayer.getTopOffset() - VERTICAL_OFFSET;
if (left + openedSubPopup.getOffsetWidth() > Window.getClientWidth()) {
if (left > openedSubPopup.getOffsetWidth()) {
left = getAbsoluteLeft() - openedSubPopup.getOffsetWidth() + HORIZONTAL_OFFSET;
} else {
int diff = left + openedSubPopup.getOffsetWidth() - Window.getClientWidth();
left -= diff;
}
}
if (top + openedSubPopup.getOffsetHeight() > Window.getClientHeight()) {
if (top > openedSubPopup.getOffsetHeight()) {
top = tableRowElement.getAbsoluteTop() - openedSubPopup.getOffsetHeight() + VERTICAL_OFFSET;
} else {
int diff = top + openedSubPopup.getOffsetHeight() - Window.getClientHeight();
top -= diff;
}
}
openedSubPopup.getElement().getStyle().setLeft(left, Unit.PX);
openedSubPopup.getElement().getStyle().setTop(top, Unit.PX);
openedSubPopup.getElement().getStyle().setVisibility(Visibility.VISIBLE);
}
});
}
interface PopupResources extends ClientBundle {
@Source({"popup-menu.css", "org/eclipse/che/ide/api/ui/style.css"})
Css popup();
@Source("check.gif")
ImageResource check();
@Source("org/eclipse/che/ide/menu/submenu.svg")
SVGResource subMenu();
}
interface Css extends CssResource {
String popupMenuSubMenuFieldDisabled();
String popupMenuHotKeyFieldDisabled();
String popupMenuTitleField();
String popupMenuIconField();
String popupMenuDelimiter();
String popupMenuIconFieldDisabled();
String popupMenuIconFieldOver();
String popupMenuCheckFieldOver();
String popupMenuCheckField();
String popupMenuTable();
String popupMenuSubMenuField();
String popupMenuMain();
String popupMenuTitleFieldOver();
String popupMenuTitleFieldDisabled();
String popupMenuCheckFieldDisabled();
String popupMenuHotKeyFieldOver();
String popupMenuSubMenuFieldOver();
String popupMenuHotKeyField();
}
/** This table uses for handling mouse events. */
private class PopupMenuTable extends FlexTable {
public PopupMenuTable() {
sinkEvents(Event.ONMOUSEOVER | Event.ONCLICK);
}
@Override
public void onBrowserEvent(Event event) {
Element td = getEventTargetCell(event);
if (td == null) {
return;
}
Element tr = DOM.getParent(td);
String index = tr.getAttribute("item-index");
if (index == null || "".equals(index)) {
return;
}
switch (DOM.eventGetType(event)) {
case Event.ONMOUSEOVER:
onRowHovered(tr);
break;
case Event.ONCLICK:
onRowClicked(tr);
event.preventDefault();
event.stopPropagation();
break;
}
}
}
}