Java tutorial
/* * Copyright (c) 2008-2016 Haulmont. * * 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.haulmont.cuba.web.toolkit.ui.client.verticalmenu; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.SpanElement; import com.google.gwt.event.dom.client.*; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.*; import com.vaadin.client.BrowserInfo; import com.vaadin.client.WidgetUtil; import com.vaadin.client.ui.Icon; import elemental.json.JsonArray; import elemental.json.JsonObject; import java.util.*; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; public class CubaSideMenuWidget extends FocusableFlowPanel implements KeyPressHandler, KeyDownHandler, FocusHandler, HasEnabled, BlurHandler { protected static final String CLASS_NAME = "c-sidemenu"; protected boolean enabled = true; protected boolean focused = true; protected MenuItemWidget focusedItem; protected MenuItemWidget selectedItem; public Consumer<String> menuItemClickHandler; public BiConsumer<String, Boolean> headerItemExpandHandler; public Function<String, Icon> menuItemIconSupplier; public boolean selectOnTrigger; public boolean singleExpandedMenu; public CubaSideMenuWidget() { setStylePrimaryName(CLASS_NAME); sinkEvents(Event.ONMOUSEOVER | Event.ONMOUSEOUT); // Navigation is only handled by the root bar addFocusHandler(this); addBlurHandler(this); /* * Firefox auto-repeat works correctly only if we use a key press * handler, other browsers handle it correctly when using a key down handler */ if (BrowserInfo.get().isGecko()) { addKeyPressHandler(this); } else { addKeyDownHandler(this); } } public void setFocusedItem(MenuItemWidget focusedItem) { if (focusedItem != this.focusedItem && this.focusedItem != null) { this.focusedItem.setFocused(false); } if (focusedItem != null) { focusedItem.setFocused(true); } this.focusedItem = focusedItem; } public MenuItemWidget getFocusedItem() { return focusedItem; } public MenuItemWidget getSelectedItem() { return selectedItem; } public void setSelectedItem(MenuItemWidget selectedItem) { if (selectedItem != this.selectedItem && this.selectedItem != null) { this.selectedItem.setSelected(false); } if (selectedItem != null) { selectedItem.setSelected(true); } this.selectedItem = selectedItem; } @Override public void onBrowserEvent(Event event) { super.onBrowserEvent(event); if (isEnabled()) { switch (DOM.eventGetType(event)) { case Event.ONMOUSEOVER: Element targetElement = DOM.eventGetTarget(event); Object targetWidget = WidgetUtil.findWidget(targetElement, null); if (targetWidget instanceof MenuItemWidget) { setFocusedItem((MenuItemWidget) targetWidget); } break; case Event.ONMOUSEOUT: if (!focused) { setFocusedItem(null); } break; } } } @Override public void onFocus(FocusEvent event) { if (isEnabled() && getFocusedItem() == null) { if (getSelectedItem() != null) { setFocusedItem(getSelectedItem()); } else if (getWidgetCount() > 0) { setFocusedItem((MenuItemWidget) getWidget(0)); } } this.focused = true; } @Override public void onBlur(BlurEvent event) { setFocusedItem(null); this.focused = false; } @Override public void onKeyDown(KeyDownEvent event) { int keyCode = event.getNativeEvent().getKeyCode(); if (keyCode == 0) { keyCode = event.getNativeEvent().getCharCode(); } if (handleNavigation(keyCode, event.isControlKeyDown() || event.isMetaKeyDown(), event.isShiftKeyDown())) { event.preventDefault(); event.stopPropagation(); } } @Override public void onKeyPress(KeyPressEvent event) { int keyCode = event.getNativeEvent().getKeyCode(); if (keyCode == 0) { keyCode = event.getNativeEvent().getCharCode(); } if (handleNavigation(keyCode, event.isControlKeyDown() || event.isMetaKeyDown(), event.isShiftKeyDown())) { event.preventDefault(); event.stopPropagation(); } } @Override public boolean isEnabled() { return enabled; } @Override public void setEnabled(boolean enabled) { this.enabled = enabled; } public void buildMenu(JsonArray itemsJson) { setFocusedItem(null); setSelectedItem(null); Iterator<Widget> iterator = this.iterator(); while (iterator.hasNext()) { iterator.next(); iterator.remove(); } addItems(itemsJson, this); } public String getTooltip(Element element) { Object widget = WidgetUtil.findWidget(element, null); if (widget instanceof MenuItemWidget) { return ((MenuItemWidget) widget).getDescription(); } return null; } public void selectItem(String itemId) { if (itemId == null) { setSelectedItem(null); } else { walkItems(this, menuItemWidget -> { if (itemId.equals(menuItemWidget.getId())) { setSelectedItem(menuItemWidget); return true; } return false; }); } } public void updateBadges(Map<String, String> badgeUpdates) { if (!badgeUpdates.isEmpty()) { Map<String, String> remainingUpdates = new HashMap<>(badgeUpdates); walkItems(this, menuItemWidget -> { String newBadgeText = remainingUpdates.remove(menuItemWidget.getId()); if (newBadgeText != null) { menuItemWidget.setBadgeText(newBadgeText); } return remainingUpdates.isEmpty(); }); } } protected boolean handleNavigation(int keyCode, boolean ctrl, boolean shift) { if (keyCode == KeyCodes.KEY_TAB) { setFocusedItem(null); return false; } if (ctrl || shift || !isEnabled()) { // Do not handle neither shift key, nor ctrl keys return false; } switch (keyCode) { case KeyCodes.KEY_UP: if (getFocusedItem() == null && getSelectedItem() != null) { setFocusedItem(getSelectedItem()); } if (getFocusedItem() == null && getChildren().size() > 0) { MenuItemWidget widget = (MenuItemWidget) getChildren().get(0); setFocusedItem(widget); } else { // find previous sibling menu item widget MenuItemWidget targetNextSibling = findPreviousMenuItem(getFocusedItem()); if (targetNextSibling != null) { setFocusedItem(targetNextSibling); } } // move up return true; case KeyCodes.KEY_DOWN: if (getFocusedItem() == null && getSelectedItem() != null) { setFocusedItem(getSelectedItem()); } if (getFocusedItem() == null) { MenuItemWidget widget = (MenuItemWidget) getChildren().get(0); setFocusedItem(widget); } else { // find next sibling menu item widget MenuItemWidget targetNextSibling = findNextMenuItem(getFocusedItem()); if (targetNextSibling != null) { setFocusedItem(targetNextSibling); } } // move down return true; case KeyCodes.KEY_ESCAPE: // collapse parent menu if (getFocusedItem() != null) { Widget parent = getFocusedItem().getParent(); if (parent instanceof MenuContainerWidget) { MenuItemWidget parentMenuItem = ((MenuContainerWidget) parent).getMenuItemWidget(); parentMenuItem.collapse(); setFocusedItem(parentMenuItem); } } return true; case KeyCodes.KEY_LEFT: // collapse sub menu if (getFocusedItem() != null) { getFocusedItem().collapse(); } return true; case KeyCodes.KEY_RIGHT: // expand submenu if (getFocusedItem() != null) { getFocusedItem().expand(); } return true; case KeyCodes.KEY_SPACE: case KeyCodes.KEY_ENTER: // expand submenu if (getFocusedItem() != null) { getFocusedItem().expandOrTrigger(); } return true; default: return false; } } protected MenuItemWidget findPreviousMenuItem(MenuItemWidget currentItem) { // convert tree to flat list, find previous item List<MenuTreeNode> menuTree = buildVisibleTree(this); List<MenuItemWidget> menuItemWidgets = menuTreeToList(menuTree); int menuItemFlatIndex = menuItemWidgets.indexOf(currentItem); int nextMenuItemFlatIndex = menuItemFlatIndex - 1; if (nextMenuItemFlatIndex < menuItemWidgets.size()) { return menuItemWidgets.get(nextMenuItemFlatIndex); } return null; } protected MenuItemWidget findNextMenuItem(MenuItemWidget currentItem) { // convert tree to flat list, find next item List<MenuTreeNode> menuTree = buildVisibleTree(this); List<MenuItemWidget> menuItemWidgets = menuTreeToList(menuTree); int menuItemFlatIndex = menuItemWidgets.indexOf(currentItem); int previousMenuItemFlatIndex = menuItemFlatIndex + 1; if (previousMenuItemFlatIndex >= 0) { return menuItemWidgets.get(previousMenuItemFlatIndex); } return null; } protected void onMenuItemTriggered(MenuItemWidget item) { if (selectOnTrigger) { setSelectedItem(item); } if (menuItemClickHandler != null) { menuItemClickHandler.accept(item.getId()); } } protected void onHeaderItemExpandChanged(MenuItemWidget item) { if (headerItemExpandHandler != null) { headerItemExpandHandler.accept(item.getId(), item.getSubMenu().isExpanded()); } if (singleExpandedMenu && item.getSubMenu().isExpanded()) { List<MenuTreeNode> menuTree = buildVisibleTree(this); List<MenuItemWidget> menuItemWidgets = menuTreeToList(menuTree); for (MenuItemWidget itemWidget : menuItemWidgets) { if (itemWidget != item && itemWidget.getParent().equals(item.getParent())) { itemWidget.collapse(); } } } } protected void addItems(JsonArray items, HasWidgets container) { for (int i = 0; i < items.length(); i++) { JsonObject itemJson = items.getObject(i); Icon icon = null; String iconId = itemJson.getString("icon"); if (menuItemIconSupplier != null && iconId != null) { icon = menuItemIconSupplier.apply(iconId); } boolean captionAsHtml = false; if (itemJson.hasKey("captionAsHtml")) { captionAsHtml = itemJson.getBoolean("captionAsHtml"); } MenuItemWidget menuItemWidget = new MenuItemWidget(this, itemJson.getString("id"), icon, itemJson.getString("styleName"), itemJson.getString("caption"), captionAsHtml); menuItemWidget.setDescription(itemJson.getString("description")); menuItemWidget.setCubaId(itemJson.getString("cubaId")); menuItemWidget.setBadgeText(itemJson.getString("badgeText")); container.add(menuItemWidget); JsonArray childrenItemsJson = itemJson.getArray("children"); if (childrenItemsJson != null) { MenuContainerWidget menuContainerWidget = new MenuContainerWidget(this, menuItemWidget); addItems(childrenItemsJson, menuContainerWidget); container.add(menuContainerWidget); menuItemWidget.setSubMenu(menuContainerWidget); if (itemJson.hasKey("expanded") && itemJson.getBoolean("expanded")) { menuContainerWidget.setExpanded(true); } } } } protected boolean walkItems(ComplexPanel container, Function<MenuItemWidget, Boolean> walker) { for (Widget widget : container) { if (widget instanceof MenuItemWidget) { Boolean stopFlag = walker.apply((MenuItemWidget) widget); if (Boolean.TRUE.equals(stopFlag)) { return true; } } else { MenuContainerWidget containerWidget = (MenuContainerWidget) widget; Boolean stopFlag = walkItems(containerWidget, walker); if (Boolean.TRUE.equals(stopFlag)) { return true; } } } return false; } protected List<MenuTreeNode> buildVisibleTree(ComplexPanel container) { List<MenuTreeNode> nodes = new ArrayList<>(); for (Widget subWidget : container) { if (subWidget instanceof MenuItemWidget) { MenuTreeNode node = new MenuTreeNode(); node.widget = (MenuItemWidget) subWidget; MenuContainerWidget subMenu = node.widget.getSubMenu(); if (subMenu != null && subMenu.isExpanded()) { node.children = buildVisibleTree(subMenu); } else { node.children = Collections.emptyList(); } nodes.add(node); } } return nodes; } protected List<MenuItemWidget> menuTreeToList(List<MenuTreeNode> menuTree) { List<MenuItemWidget> list = new ArrayList<>(); for (MenuTreeNode rootNode : menuTree) { menuTreeCollect(rootNode, list); } return list; } protected void menuTreeCollect(MenuTreeNode element, List<MenuItemWidget> list) { list.add(element.widget); for (MenuTreeNode data : element.children) { menuTreeCollect(data, list); } } public static class MenuItemWidget extends Widget implements ClickHandler { protected CubaSideMenuWidget menu; protected String id; protected String description; protected boolean focused; protected boolean selected; protected SpanElement badgeElement; protected SpanElement captionElement; protected MenuContainerWidget subMenu; public MenuItemWidget(CubaSideMenuWidget menu, String id, Icon icon, String styleName, String caption, boolean captionAsHtml) { this.menu = menu; this.id = id; setElement(Document.get().createDivElement()); setStylePrimaryName(menu.getStylePrimaryName() + "-item"); addStyleDependentName("action"); if (styleName != null) { for (String style : styleName.split(" ")) { if (!style.isEmpty()) { addStyleName(style); } } } SpanElement wrapElement = Document.get().createSpanElement(); wrapElement.setClassName(getStylePrimaryName() + "-wrap"); if (icon != null) { wrapElement.appendChild(icon.getElement()); } captionElement = Document.get().createSpanElement(); captionElement.setClassName(getStylePrimaryName() + "-caption"); if (caption != null) { if (captionAsHtml) { captionElement.setInnerHTML(caption); } else { captionElement.setInnerText(caption); } } badgeElement = Document.get().createSpanElement(); badgeElement.setClassName(getStylePrimaryName() + "-badge"); wrapElement.appendChild(captionElement); getElement().appendChild(wrapElement); addDomHandler(this, ClickEvent.getType()); } public void setFocused(boolean focused) { this.focused = focused; if (focused) { addStyleDependentName("focused"); } else { removeStyleDependentName("focused"); } } public void setSelected(boolean selected) { this.selected = selected; if (selected) { addStyleDependentName("selected"); } else { removeStyleDependentName("selected"); } } public MenuContainerWidget getSubMenu() { return subMenu; } public void setSubMenu(MenuContainerWidget subMenu) { this.subMenu = subMenu; if (this.subMenu != null) { addStyleDependentName("header"); removeStyleDependentName("action"); } else { removeStyleDependentName("header"); addStyleDependentName("action"); } } public void setBadgeText(String badgeText) { if (badgeText == null || badgeText.isEmpty()) { if (badgeElement.getParentElement() != null) { badgeElement.removeFromParent(); } } else { if (badgeElement.getParentElement() == null) { captionElement.appendChild(badgeElement); } badgeElement.setInnerText(badgeText); } } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public void setCubaId(String cubaId) { if (cubaId != null && !cubaId.isEmpty()) { getElement().setAttribute("cuba-id", cubaId); } } public String getId() { return id; } @Override public void onClick(ClickEvent event) { if (menu.isEnabled()) { if (subMenu == null) { menu.setFocusedItem(null); menu.onMenuItemTriggered(this); } else { menu.setFocusedItem(this); subMenu.triggerExpand(); menu.onHeaderItemExpandChanged(this); } } } public void expand() { if (subMenu != null) { subMenu.setExpanded(true); menu.onHeaderItemExpandChanged(this); } } public void collapse() { if (subMenu != null) { subMenu.setExpanded(false); menu.onHeaderItemExpandChanged(this); } } public void expandOrTrigger() { if (subMenu == null) { menu.onMenuItemTriggered(this); } else { subMenu.triggerExpand(); menu.onHeaderItemExpandChanged(this); } } } public static class MenuContainerWidget extends FlowPanel { protected CubaSideMenuWidget menu; protected MenuItemWidget menuItemWidget; protected boolean expanded = false; public MenuContainerWidget(CubaSideMenuWidget menu, MenuItemWidget menuItemWidget) { this.menu = menu; this.menuItemWidget = menuItemWidget; setStylePrimaryName(menu.getStylePrimaryName() + "-submenu"); } public boolean isExpanded() { return expanded; } public void setExpanded(boolean expanded) { if (this.expanded != expanded) { this.expanded = expanded; if (expanded) { addStyleDependentName("open"); menuItemWidget.addStyleDependentName("header-open"); } else { removeStyleDependentName("open"); menuItemWidget.removeStyleDependentName("header-open"); } } } public void triggerExpand() { setExpanded(!isExpanded()); } public MenuItemWidget getMenuItemWidget() { return menuItemWidget; } } public static class MenuTreeNode { public MenuItemWidget widget; public List<MenuTreeNode> children; } }