package org.vaadin.peter.contextmenu; import java.io.Serializable; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.vaadin.peter.contextmenu.client.ui.VContextMenu; import com.vaadin.terminal.PaintException; import com.vaadin.terminal.PaintTarget; import com.vaadin.terminal.Resource; import com.vaadin.ui.AbstractComponent; import com.vaadin.ui.ClientWidget; import com.vaadin.ui.Component; /** * ContextMenu is a popup menu that can be placed to an arbitrary location * inside Vaadin application's bounds. * * Items can be added to ContextMenu by calling ContextMenu.addItem(String). * Returned value represents ContextMenuItem that is attached to added item. If * ContextMenu.ClickListener is attached to component, * ContextMenu.ClickListener.getClickedItem() can be used to retrieve the * clicked item. * * It is also possible to set ContextMenuItem disabled or invisible using * ContextMenuItem.setEnabled(boolean) and ContextMenuItem.setVisible(boolean) * methods. * * @author Peter Lehto / Vaadin Ltd */ @ClientWidget(VContextMenu.class) public class ContextMenu extends AbstractComponent { private static final long serialVersionUID = 1729861951779648682L; private boolean visible; private int x; private int y; private final List<ContextMenuItem> items; private String selectedComponentId = ""; private final Map<Integer, ContextMenuItem> itemIds; private int itemIdIndex; private boolean enabled; /** * Creates new empty context menu */ public ContextMenu() { this.items = new LinkedList<ContextMenuItem>(); this.itemIds = new HashMap<Integer, ContextMenuItem>(); setVisible(false); setEnabled(true); } @Override public void paintContent(PaintTarget target) throws PaintException { super.paintContent(target); this.renderInnerItems(this.items, target); if (visible) { target.addAttribute("show", true); target.addAttribute("component_locator", this.selectedComponentId); target.addAttribute("left", x); target.addAttribute("top", y); } else { target.addAttribute("show", false); } this.visible = false; } @Override public void changeVariables(Object source, Map<String, Object> variables) { super.changeVariables(source, variables); if (variables.containsKey("itemId")) { int itemId = (Integer) variables.get("itemId"); if (this.itemIds.containsKey(itemId)) { fireClick(itemIds.get(itemId)); } } } /** * Pops up context menu in given coordinates * * @param left * - pixels from application's left border * @param top * - pixels from application's top border */ public void show(int left, int top) { if (!this.isEnabled()) { return; } this.setVisible(true); this.visible = true; this.x = left; this.y = top; this.selectedComponentId = ""; this.requestRepaint(); } /** * Pops up context menu next to given component * * @param component * @throws IllegalArgumentException * if given component doesn't have debug id specified */ public void show(Component component) { if (component.getDebugId() == null) { throw new IllegalArgumentException( "Given component must have debug id specified"); } if (!this.isEnabled()) { return; } if (component.getDebugId() != null) { this.selectedComponentId = component.getDebugId(); this.setVisible(true); this.visible = true; this.requestRepaint(); } } /** * Hides context menu */ public void hide() { visible = false; requestRepaint(); } /** * Adds new item to context menu using given name. Returns corresponding * ContextMenuItem object that can be used for click event handling and * settings added item visible or enabled. * * @param name * @return ContextMenuItem */ public ContextMenuItem addItem(String name) { ContextMenuItem item = new ContextMenuItem(name); this.items.add(item); requestRepaint(); return item; } /** * Removes given context menu item from the root level's context menu. If * you want to remove menu item from a sub menu, call removeItem for that * ContextMenuItem * * @param item * to remove */ public void removeItem(ContextMenuItem item) { if (!items.contains(item)) { return; } this.itemIds.remove(item.getItemId()); this.items.remove(item); requestRepaint(); } /** * Enables or disabled context menu component. If component is disabled it * will not popup when show methods are called. */ @Override public void setEnabled(boolean enabled) { this.enabled = enabled; } /** * @return true if this component is enabled, false otherwise */ @Override public boolean isEnabled() { return this.enabled; } @Override public void setReadOnly(boolean readOnly) { throw new UnsupportedOperationException( "Context menu does not support read only"); } @Override public boolean isReadOnly() { return false; } /** * Renders the internal structure of context menu to given paint target * * @param items * @param target * @throws PaintException */ private void renderInnerItems(List<ContextMenuItem> items, PaintTarget target) throws PaintException { target.startTag("items"); for (ContextMenuItem item : items) { if (!item.isVisible()) { // let's not draw hidden items continue; } target.startTag("item"); target.addAttribute("id", item.getItemId()); target.addAttribute("name", item.getName()); target.addAttribute("enabled", item.isEnabled()); target.addAttribute("style", item.getStyleName()); target.addAttribute("separator", item.hasSeparator()); if (item.hasIcon()) { target.addAttribute("icon", item.getIcon()); } // Draw all the sub items if (item.hasSubMenus()) { renderInnerItems(item.getChildren(), target); } target.endTag("item"); } target.endTag("items"); } /** * ContextMenu's ClickListener that is called when an item from context menu * is clicked. * * @author Peter Lehto / IT Mill Oy Ltd */ public interface ClickListener extends Serializable { /** * Called when ContextMenuItem is clicked from ContextMenu * * @param event */ public void contextItemClick(ClickEvent event); } /** * Adds the context item click listener. * * @param listener * the Listener to be added. */ public void addListener(ClickListener listener) { addListener(ClickEvent.class, listener, BUTTON_CLICK_METHOD); } /** * Removes the context item click listener. * * @param listener * the Listener to be removed. */ public void removeListener(ClickListener listener) { removeListener(ClickEvent.class, listener, BUTTON_CLICK_METHOD); } protected void fireClick(ContextMenuItem contextMenuItem) { fireEvent(new ContextMenu.ClickEvent(this, contextMenuItem)); } private void setItemIdFor(ContextMenuItem item) { int index = itemIdIndex++; item.setItemId(index); this.itemIds.put(index, item); } private static final Method BUTTON_CLICK_METHOD; static { try { BUTTON_CLICK_METHOD = ClickListener.class.getDeclaredMethod( "contextItemClick", new Class[] { ClickEvent.class }); } catch (final java.lang.NoSuchMethodException e) { throw new java.lang.RuntimeException( "Internal error finding methods in ContextMenu"); } } /** * ClickEvent is fired when context item is clicked from ContextMenu * * @author Peter Lehto / IT Mill Oy Ltd */ public class ClickEvent extends Component.Event { private static final long serialVersionUID = -7705638357488426038L; private final ContextMenuItem clickedItem; /** * New instance of context item click event. * * @param source * the Source of the event. */ public ClickEvent(Component source, ContextMenuItem clickedItem) { super(source); this.clickedItem = clickedItem; } /** * Gets the ContextMenu where the event occurred. * * @return the Source of the event. */ public ContextMenu getContextMenu() { return (ContextMenu) getSource(); } /** * @return Item that was clicked */ public ContextMenuItem getClickedItem() { return clickedItem; } } /** * ContextMenuItem is POJO that can be used to access items in ContextMenu. * ContextMenuItem has methods for hiding and disabling * * @author Peter Lehto / IT Mill Oy Ltd */ public class ContextMenuItem implements Serializable { private static final long serialVersionUID = 3828334687114823216L; private Integer itemId; private final String name; private boolean enabled; private boolean visible; private Resource icon; private final List<String> styleNames; private final List<ContextMenuItem> children; private boolean separator; private ContextMenuItem(String name) { if (name == null) { throw new IllegalArgumentException( "Menu item name cannot be null"); } this.styleNames = new LinkedList<String>(); this.children = new LinkedList<ContextMenuItem>(); this.name = name; this.visible = true; this.enabled = true; ContextMenu.this.setItemIdFor(this); } /** * Adds given css style name to this context menu item * * @param styleName */ public void addStyleName(String styleName) { if (styleName == null || styleName.equals("")) { return; } this.styleNames.add(styleName); } /** * Sets given css style name to this context menu item * * @param styleName */ public void setStyleName(String styleName) { this.styleNames.clear(); this.addStyleName(styleName); } /** * Removes given style name from this context menu item * * @param styleName */ public void removeStyleName(String styleName) { if (styleName == null || styleName.equals("")) { return; } this.styleNames.remove(styleName); } /** * @return Name of this context menu item */ public String getName() { return name; } /** * Enables or disables this menu item * * @param enabled */ public void setEnabled(boolean enabled) { this.enabled = enabled; requestRepaint(); } /** * @return true if menu item is enabled, false otherwise */ public boolean isEnabled() { return enabled; } /** * Sets visible or hides this menu item * * @param visible */ public void setVisible(boolean visible) { this.visible = visible; requestRepaint(); } /** * @return true if menu item is visible, false otherwise */ public boolean isVisible() { return visible; } /** * Sets icon for this context menu item. Default and recommended icon * size is 16x16 pixels. * * @param icon */ public void setIcon(Resource icon) { this.icon = icon; } /** * @return current icon for this context menu item */ public Resource getIcon() { return this.icon; } /** * Adds new item to context menu as this menu's sub menu. Returns * corresponding ContextMenuItem object that can be used for click event * handling and settings added item visible or enabled. * * @param name * @return ContextMenuItem */ public ContextMenuItem addItem(String name) { ContextMenuItem item = new ContextMenuItem(name); this.children.add(item); requestRepaint(); return item; } /** * Removes given context menu item from this sub menu. If you want to * remove menu item from a sub menu, call removeItem for that * ContextMenuItem * * @param item * to remove */ public void removeItem(ContextMenuItem child) { if (!children.contains(child)) { return; } children.remove(child); ContextMenu.this.itemIds.remove(child.getItemId()); } /** * Sets or disables separator line under this item * * @param visible */ public void setSeparatorVisible(boolean visible) { this.separator = visible; } /** * @return true if separator line is visible after this item, false * otherwise */ public boolean hasSeparator() { return this.separator; } @Override public boolean equals(Object other) { if (this == other) { return true; } if (other instanceof ContextMenuItem) { return this.itemId.equals(((ContextMenuItem) other).itemId); } return false; } @Override public int hashCode() { return this.itemId.hashCode(); } /** * @return list of submenus */ List<ContextMenuItem> getChildren() { return this.children; } /** * @return true if this menu has sub menus */ boolean hasSubMenus() { return this.children.size() > 0; } /** * Sets itemId to this context menu item * * @param itemId */ void setItemId(int itemId) { this.itemId = itemId; } /** * @return itemId */ int getItemId() { return this.itemId; } /** * @return true if this item has an icon, false otherwise */ boolean hasIcon() { return this.icon != null; } /** * @return combined style name */ String getStyleName() { String styleName = ""; Iterator<String> iterator = styleNames.iterator(); while (iterator.hasNext()) { styleName += iterator.next(); if (iterator.hasNext()) { styleName += " "; } } return styleName; } } }