001
002package org.opencms.ui.contextmenu;
003
004import org.opencms.ui.CmsVaadinUtils;
005import org.opencms.ui.shared.CmsContextMenuState;
006import org.opencms.ui.shared.CmsContextMenuState.ContextMenuItemState;
007import org.opencms.ui.shared.rpc.I_CmsContextMenuClientRpc;
008import org.opencms.ui.shared.rpc.I_CmsContextMenuServerRpc;
009
010import java.io.Serializable;
011import java.lang.reflect.Method;
012import java.util.ArrayList;
013import java.util.Collection;
014import java.util.EventListener;
015import java.util.EventObject;
016import java.util.HashMap;
017import java.util.HashSet;
018import java.util.List;
019import java.util.Locale;
020import java.util.Map;
021import java.util.Set;
022import java.util.StringTokenizer;
023import java.util.UUID;
024
025import com.vaadin.v7.event.ItemClickEvent;
026import com.vaadin.event.MouseEvents.ClickEvent;
027import com.vaadin.server.AbstractClientConnector;
028import com.vaadin.server.AbstractExtension;
029import com.vaadin.server.Resource;
030import com.vaadin.ui.Component;
031import com.vaadin.v7.ui.Table;
032import com.vaadin.v7.ui.Tree;
033import com.vaadin.ui.UI;
034import com.vaadin.util.ReflectTools;
035
036/**
037 * ContextMenu is an extension which can be attached to any Vaadin component to
038 * display a popup context menu. Most useful the menu is when attached for
039 * example to Tree or Table which support item and property based context menu
040 * detection.<p>
041 *
042 * Adapted from ContextMenu by Peter Lehto / Vaadin Ltd.<p>
043 */
044public class CmsContextMenu extends AbstractExtension {
045
046    /**
047     * ContextMenuClosedEvent is an event fired by the context menu when it's
048     * closed.
049     */
050    public static class ContextMenuClosedEvent extends EventObject {
051
052        /** The serial version id. */
053        private static final long serialVersionUID = -5705205542849351984L;
054
055        /** The context menu. */
056        private final CmsContextMenu m_contextMenu;
057
058        /**
059         * Constructor.<p>
060         *
061         * @param contextMenu the context menu
062         */
063        public ContextMenuClosedEvent(CmsContextMenu contextMenu) {
064            super(contextMenu);
065            m_contextMenu = contextMenu;
066        }
067
068        /**
069         * Returns the context menu.<p>
070         *
071         * @return the menu
072         */
073        public CmsContextMenu getContextMenu() {
074
075            return m_contextMenu;
076        }
077    }
078
079    /**
080     * ContextMenuClosedListener is used to listen for the event that the
081     * context menu is closed, either when a item is clicked or when the popup
082     * is canceled.<p>
083     */
084    public interface ContextMenuClosedListener extends EventListener {
085
086        /** The menu closed method. */
087        public static final Method MENU_CLOSED = ReflectTools.findMethod(
088            ContextMenuClosedListener.class,
089            "onContextMenuClosed",
090            ContextMenuClosedEvent.class);
091
092        /**
093         * Called when the context menu is closed.<p>
094         *
095         * @param event the event
096         */
097        public void onContextMenuClosed(ContextMenuClosedEvent event);
098    }
099
100    /**
101     * ContextMenuItem represents one clickable item in the context menu. Item may have sub items.<p>
102     */
103    public class ContextMenuItem implements Serializable {
104
105        /** The serial version id. */
106        private static final long serialVersionUID = -6514832427611690050L;
107
108        /** The item state. */
109        final ContextMenuItemState m_state;
110
111        /** The item click listeners. */
112        private final List<CmsContextMenu.ContextMenuItemClickListener> m_clickListeners;
113
114        /** The item data. */
115        private Object m_data;
116
117        /** The parent item. */
118        private ContextMenuItem m_parent;
119
120        /**
121         * Constructor.<p>
122         *
123         * @param parent the parent item
124         * @param itemState the item state
125         */
126        protected ContextMenuItem(ContextMenuItem parent, ContextMenuItemState itemState) {
127            m_parent = parent;
128
129            if (itemState == null) {
130                throw new NullPointerException("Context menu item state must not be null");
131            }
132
133            m_clickListeners = new ArrayList<CmsContextMenu.ContextMenuItemClickListener>();
134            m_state = itemState;
135        }
136
137        /**
138         * Adds new item as this item's sub item with given icon.<p>
139         *
140         * @param icon the icon
141         *
142         * @return reference to newly added item
143         */
144        public ContextMenuItem addItem(Resource icon) {
145
146            ContextMenuItem item = this.addItem("");
147            item.setIcon(icon);
148
149            return item;
150        }
151
152        /**
153         * Adds new item as this item's sub item with given caption.<p>
154         *
155         * @param caption the caption
156         * @return reference to newly created item.
157         */
158        public ContextMenuItem addItem(String caption) {
159
160            ContextMenuItemState childItemState = m_state.addChild(caption, getNextId());
161            ContextMenuItem item = new ContextMenuItem(this, childItemState);
162
163            m_items.put(childItemState.getId(), item);
164            markAsDirty();
165            return item;
166        }
167
168        /**
169         * Adds new item as this item's sub item with given caption and icon.<p>
170         *
171         * @param caption the caption
172         * @param icon the icon
173         * @return reference to newly added item
174         */
175        public ContextMenuItem addItem(String caption, Resource icon) {
176
177            ContextMenuItem item = this.addItem(caption);
178            item.setIcon(icon);
179
180            return item;
181        }
182
183        /**
184         * Adds context menu item click listener only to this item. This
185         * listener will be invoked only when this item is clicked.<p>
186         *
187         * @param clickListener the click listener
188         */
189        public void addItemClickListener(CmsContextMenu.ContextMenuItemClickListener clickListener) {
190
191            this.m_clickListeners.add(clickListener);
192        }
193
194        /**
195         * Add a new style to the menu item. This method is following the same
196         * semantics as {@link Component#addStyleName(String)}.<p>
197         *
198         * @param style
199         *            the new style to be added to the component
200         */
201        public void addStyleName(String style) {
202
203            if ((style == null) || style.isEmpty()) {
204                return;
205            }
206            if (style.contains(" ")) {
207                // Split space separated style names and add them one by one.
208                StringTokenizer tokenizer = new StringTokenizer(style, " ");
209                while (tokenizer.hasMoreTokens()) {
210                    addStyleName(tokenizer.nextToken());
211                }
212                return;
213            }
214
215            m_state.getStyles().add(style);
216            markAsDirty();
217        }
218
219        /**
220         * @see java.lang.Object#equals(java.lang.Object)
221         */
222        @Override
223        public boolean equals(Object other) {
224
225            if (this == other) {
226                return true;
227            }
228
229            if (other instanceof ContextMenuItem) {
230                return m_state.getId().equals(((ContextMenuItem)other).m_state.getId());
231            }
232
233            return false;
234        }
235
236        /**
237         * Returns the item data.<p>
238         *
239         * @return Object associated with ContextMenuItem.
240         */
241        public Object getData() {
242
243            return m_data;
244        }
245
246        /**
247         * Returns the item description.<p>
248         *
249         * @return the description
250         */
251        public String getDescription() {
252
253            return m_state.getDescription();
254        }
255
256        /**
257         * Returns the icon.<p>
258         *
259         * @return current icon
260         */
261        public Resource getIcon() {
262
263            return getResource(m_state.getId());
264        }
265
266        /**
267         * @see java.lang.Object#hashCode()
268         */
269        @Override
270        public int hashCode() {
271
272            return m_state.getId().hashCode();
273        }
274
275        /**
276         * Returns whether the item has a separator.<p>
277         *
278         * @return <code>true</code> if separator line is visible after this item
279         */
280        public boolean hasSeparator() {
281
282            return m_state.isSeparator();
283        }
284
285        /**
286         * Returns whether the item has a sub menu.<p>
287         *
288         * @return <code>true</code> if this menu item has a sub menu
289         */
290        public boolean hasSubMenu() {
291
292            return m_state.getChildren().size() > 0;
293        }
294
295        /**
296         * Returns if the item is enabled.<p>
297         * @return <code>true</code> if menu item is enabled
298         */
299        public boolean isEnabled() {
300
301            return m_state.isEnabled();
302        }
303
304        /**
305         * Returns whether this item is the root item.<p>
306         *
307         * @return <code>true</code> if this item is root item
308         */
309        public boolean isRootItem() {
310
311            return m_parent == null;
312        }
313
314        /**
315         * Removes given click listener from this item. Removing listener
316         * affects only this context menu item.<p>
317         *
318         * @param clickListener the click listener to remove
319         */
320        public void removeItemClickListener(CmsContextMenu.ContextMenuItemClickListener clickListener) {
321
322            this.m_clickListeners.remove(clickListener);
323        }
324
325        /**
326         * Remove a style name from this menu item. This method is following the
327         * same semantics as {@link Component#removeStyleName(String)}.<p>
328         *
329         * @param style the style name or style names to be removed
330         */
331        public void removeStyleName(String style) {
332
333            if (m_state.getStyles().isEmpty()) {
334                return;
335            }
336
337            StringTokenizer tokenizer = new StringTokenizer(style, " ");
338            while (tokenizer.hasMoreTokens()) {
339                m_state.getStyles().remove(tokenizer.nextToken());
340            }
341        }
342
343        /**
344         * Changes the caption of the menu item.<p>
345         *
346         * @param newCaption the caption
347         */
348        public void setCaption(String newCaption) {
349
350            m_state.setCaption(newCaption);
351            markAsDirty();
352        }
353
354        /**
355         * Associates given object with this menu item. Given object can be
356         * whatever application specific if necessary.<p>
357         *
358         * @param data the data
359         */
360        public void setData(Object data) {
361
362            m_data = data;
363        }
364
365        /**
366         * Sets the item description used as tool-tip.<p>
367         *
368         * @param description the description
369         */
370        public void setDescription(String description) {
371
372            m_state.setDescription(description);
373            markAsDirty();
374        }
375
376        /**
377         * Enables or disables this menu item.<p>
378         *
379         * @param enabled the enabled flag
380         */
381        public void setEnabled(boolean enabled) {
382
383            m_state.setEnabled(enabled);
384            markAsDirty();
385        }
386
387        /**
388         * Sets given resource as icon of this menu item.<p>
389         *
390         * @param icon the icon
391         */
392        public void setIcon(Resource icon) {
393
394            setResource(m_state.getId(), icon);
395        }
396
397        /**
398         * Sets or disables separator line under this item.<p>
399         *
400         * @param separatorVisible the visibility flag
401         */
402        public void setSeparatorVisible(boolean separatorVisible) {
403
404            m_state.setSeparator(separatorVisible);
405            markAsDirty();
406        }
407
408        /**
409         * Returns the item children.<p>
410         *
411         * @return the children
412         */
413        protected Set<ContextMenuItem> getAllChildren() {
414
415            Set<ContextMenuItem> children = new HashSet<CmsContextMenu.ContextMenuItem>();
416
417            for (ContextMenuItemState childState : m_state.getChildren()) {
418                ContextMenuItem child = m_items.get(childState.getId());
419                children.add(child);
420                children.addAll(child.getAllChildren());
421            }
422
423            return children;
424        }
425
426        /**
427         * Returns the parent item.<p>
428         *
429         * @return parent item of this menu item. Null if this item is a root item.
430         */
431        protected ContextMenuItem getParent() {
432
433            return m_parent;
434        }
435
436        /**
437         * Notifies all click listeners.<p>
438         */
439        protected void notifyClickListeners() {
440
441            for (CmsContextMenu.ContextMenuItemClickListener clickListener : m_clickListeners) {
442                clickListener.contextMenuItemClicked(new ContextMenuItemClickEvent(this));
443            }
444        }
445    }
446
447    /**
448     * ContextMenuItemClickEvent is an event produced by the context menu item
449     * when it is clicked. Event contains method for retrieving the clicked item
450     * and menu from which the click event originated.
451     */
452    public static class ContextMenuItemClickEvent extends EventObject {
453
454        /** The serial version id. */
455        private static final long serialVersionUID = -3301204853129409248L;
456
457        /**
458         * Constructor.<p>
459         *
460         * @param component the component
461         */
462        public ContextMenuItemClickEvent(Object component) {
463            super(component);
464        }
465    }
466
467    /**
468     * ContextMenuItemClickListener is listener for context menu items wanting
469     * to notify listeners about item click
470     */
471    public interface ContextMenuItemClickListener extends EventListener {
472
473        /** The item click method. */
474        public static final Method ITEM_CLICK_METHOD = ReflectTools.findMethod(
475            ContextMenuItemClickListener.class,
476            "contextMenuItemClicked",
477            ContextMenuItemClickEvent.class);
478
479        /**
480         * Called by the context menu item when it's clicked
481         *
482         * @param event
483         *            containing the information of which item was clicked
484         */
485        public void contextMenuItemClicked(ContextMenuItemClickEvent event);
486    }
487
488    /**
489     * ContextMenuOpenedListener is used to modify the content of context menu
490     * based on what was clicked. For example TableListener can be used to
491     * modify context menu based on certain table component clicks.<p>
492     */
493    public interface ContextMenuOpenedListener extends EventListener {
494
495        /**
496         * ComponentListener is used when context menu is extending a component
497         * and works in mode where auto opening is disabled. For example if
498         * ContextMenu is assigned to a Layout and layout is right clicked when
499         * auto open feature is disabled, the open listener would be called
500         * instead of menu opening automatically. Example usage is for example
501         * as follows:<p>
502         *
503         * event.getContextMenu().open(event.getRequestSourceComponent());<p>
504         */
505        public interface ComponentListener extends ContextMenuOpenedListener {
506
507            /** The open menu method. */
508            public static final Method MENU_OPENED_FROM_COMPONENT = ReflectTools.findMethod(
509                ContextMenuOpenedListener.ComponentListener.class,
510                "onContextMenuOpenFromComponent",
511                ContextMenuOpenedOnComponentEvent.class);
512
513            /**
514             * Called by the context menu when it's opened by clicking on
515             * component.<p>
516             *
517             * @param event the event
518             */
519            public void onContextMenuOpenFromComponent(ContextMenuOpenedOnComponentEvent event);
520        }
521
522        /**
523         * ContextMenuOpenedListener.TableListener sub interface for table
524         * related context menus.<p>
525         */
526        public interface TableListener extends ContextMenuOpenedListener {
527
528            /** Opened from method. */
529            public static final Method MENU_OPENED_FROM_TABLE_FOOTER_METHOD = ReflectTools.findMethod(
530                ContextMenuOpenedListener.TableListener.class,
531                "onContextMenuOpenFromFooter",
532                ContextMenuOpenedOnTableFooterEvent.class);
533
534            /** Opened from method. */
535            public static final Method MENU_OPENED_FROM_TABLE_HEADER_METHOD = ReflectTools.findMethod(
536                ContextMenuOpenedListener.TableListener.class,
537                "onContextMenuOpenFromHeader",
538                ContextMenuOpenedOnTableHeaderEvent.class);
539
540            /** Opened from method. */
541            public static final Method MENU_OPENED_FROM_TABLE_ROW_METHOD = ReflectTools.findMethod(
542                ContextMenuOpenedListener.TableListener.class,
543                "onContextMenuOpenFromRow",
544                ContextMenuOpenedOnTableRowEvent.class);
545
546            /**
547             * Called by the context menu when it's opened by clicking table
548             * component's footer.<p>
549             *
550             * @param event the event
551             */
552            public void onContextMenuOpenFromFooter(ContextMenuOpenedOnTableFooterEvent event);
553
554            /**
555             * Called by the context menu when it's opened by clicking table
556             * component's header.<p>
557             *
558             * @param event the event
559             */
560            public void onContextMenuOpenFromHeader(ContextMenuOpenedOnTableHeaderEvent event);
561
562            /**
563             * Called by the context menu when it's opened by clicking table
564             * component's row.<p>
565             *
566             * @param event the event
567             */
568            public void onContextMenuOpenFromRow(ContextMenuOpenedOnTableRowEvent event);
569        }
570
571        /**
572         * Tree listener interface.<p>
573         */
574        public interface TreeListener extends ContextMenuOpenedListener {
575
576            /** Opened from method. */
577            public static final Method MENU_OPENED_FROM_TREE_ITEM_METHOD = ReflectTools.findMethod(
578                ContextMenuOpenedListener.TreeListener.class,
579                "onContextMenuOpenFromTreeItem",
580                ContextMenuOpenedOnTreeItemEvent.class);
581
582            /**
583             * Called by the context menu when it's opened by clicking item on a
584             * tree.<p>
585             *
586             * @param event the event
587             */
588            public void onContextMenuOpenFromTreeItem(ContextMenuOpenedOnTreeItemEvent event);
589        }
590
591    }
592
593    /**
594     * ContextMenuOpenedOnComponentEvent is an event fired by the context menu
595     * when it's opened from a component.<p>
596     *
597     */
598    public static class ContextMenuOpenedOnComponentEvent extends EventObject {
599
600        /** The serial version id. */
601        private static final long serialVersionUID = 947108059398706966L;
602
603        /** The context menu. */
604        private final CmsContextMenu m_contextMenu;
605
606        /** The client x position. */
607        private final int m_x;
608
609        /** The client y position. */
610        private final int m_y;
611
612        /**
613         * Constructor.<p>
614         *
615         * @param contextMenu the context menu
616         * @param x the client x position
617         * @param y the client y position
618         * @param component the component
619         */
620        public ContextMenuOpenedOnComponentEvent(CmsContextMenu contextMenu, int x, int y, Component component) {
621            super(component);
622
623            m_contextMenu = contextMenu;
624            m_x = x;
625            m_y = y;
626        }
627
628        /**
629         * Returns the context menu.<p>
630         *
631         * @return ContextMenu that was opened.
632         */
633        public CmsContextMenu getContextMenu() {
634
635            return m_contextMenu;
636        }
637
638        /**
639         * Returns the source component.<p>
640         *
641         * @return Component which initiated the context menu open request.
642         */
643        public Component getRequestSourceComponent() {
644
645            return (Component)getSource();
646        }
647
648        /**
649         * Returns the client x position.<p>
650         *
651         * @return x-coordinate of open position.
652         */
653        public int getX() {
654
655            return m_x;
656        }
657
658        /**
659         * Returns the client y position.<p>
660         *
661         * @return y-coordinate of open position.
662         */
663        public int getY() {
664
665            return m_y;
666        }
667    }
668
669    /**
670     * ContextMenuOpenedOnTableFooterEvent is an event that is fired by the
671     * context menu when it's opened by clicking on table footer
672     */
673    public static class ContextMenuOpenedOnTableFooterEvent extends EventObject {
674
675        /** The serial version id. */
676        private static final long serialVersionUID = 1999781663913723438L;
677
678        /** The context menu. */
679        private final CmsContextMenu m_contextMenu;
680
681        /** The property id. */
682        private final Object m_propertyId;
683
684        /**
685         * Constructor.<p>
686         *
687         * @param contextMenu the context menu
688         * @param sourceTable the source table
689         * @param propertyId the property id
690         */
691        public ContextMenuOpenedOnTableFooterEvent(CmsContextMenu contextMenu, Table sourceTable, Object propertyId) {
692            super(sourceTable);
693
694            m_contextMenu = contextMenu;
695            m_propertyId = propertyId;
696        }
697
698        /**
699         * Returns the context menu.<p>
700         *
701         * @return the context menu
702         */
703        public CmsContextMenu getContextMenu() {
704
705            return m_contextMenu;
706        }
707
708        /**
709         * Returns the property id.<p>
710         *
711         * @return the property id
712         */
713        public Object getPropertyId() {
714
715            return m_propertyId;
716        }
717    }
718
719    /**
720     * ContextMenuOpenedOnTableHeaderEvent is an event fired by the context menu
721     * when it's opened by clicking on table header row.<p>
722     */
723    public static class ContextMenuOpenedOnTableHeaderEvent extends EventObject {
724
725        /** The serial version id. */
726        private static final long serialVersionUID = -1220618848356241248L;
727
728        /** The context menu. */
729        private final CmsContextMenu m_contextMenu;
730
731        /** The property id. */
732        private final Object m_propertyId;
733
734        /**
735         * Constructor.<p>
736         *
737         * @param contextMenu the context menu
738         * @param sourceTable the source
739         * @param propertyId the property id
740         */
741        public ContextMenuOpenedOnTableHeaderEvent(CmsContextMenu contextMenu, Table sourceTable, Object propertyId) {
742            super(sourceTable);
743
744            m_contextMenu = contextMenu;
745            m_propertyId = propertyId;
746        }
747
748        /**
749         * Returns the context menu.<p>
750         *
751         * @return the context menu
752         */
753        public CmsContextMenu getContextMenu() {
754
755            return m_contextMenu;
756        }
757
758        /**
759         * Returns the property id.<p>
760         *
761         * @return the property id
762         */
763        public Object getPropertyId() {
764
765            return m_propertyId;
766        }
767    }
768
769    /**
770     * ContextMenuOpenedOnTableRowEvent is an event that is fired when context
771     * menu is opened by clicking on table row.<p>
772     */
773    public static class ContextMenuOpenedOnTableRowEvent extends EventObject {
774
775        /** The serial version id. */
776        private static final long serialVersionUID = -470218301318358912L;
777
778        /** The context menu. */
779        private final CmsContextMenu m_contextMenu;
780
781        /** The item id. */
782        private final Object m_itemId;
783
784        /** The property id. */
785        private final Object m_propertyId;
786
787        /**
788         * Constructor.<p>
789         *
790         * @param contextMenu the context menu
791         * @param table the table
792         * @param itemId the item id
793         * @param propertyId the property id
794         */
795        public ContextMenuOpenedOnTableRowEvent(
796            CmsContextMenu contextMenu,
797            Table table,
798            Object itemId,
799            Object propertyId) {
800            super(table);
801
802            m_contextMenu = contextMenu;
803            m_itemId = itemId;
804            m_propertyId = propertyId;
805        }
806
807        /**
808         * Returns the context menu.<p>
809         *
810         * @return the context menu
811         */
812        public CmsContextMenu getContextMenu() {
813
814            return m_contextMenu;
815        }
816
817        /**
818         * Returns the item id.<p>
819         *
820         * @return the item id
821         */
822        public Object getItemId() {
823
824            return m_itemId;
825        }
826
827        /**
828         * Returns the property id.<p>
829         *
830         * @return the property id
831         */
832        public Object getPropertyId() {
833
834            return m_propertyId;
835        }
836    }
837
838    /**
839     * ContextMenuOpenedOnTreeItemEvent is an event fired by the context menu
840     * when it's opened by clicking on tree item.<p>
841     */
842    public static class ContextMenuOpenedOnTreeItemEvent extends EventObject {
843
844        /** The serial version id. */
845        private static final long serialVersionUID = -7705205542849351984L;
846
847        /** The context menu. */
848        private final CmsContextMenu m_contextMenu;
849
850        /** The item id. */
851        private final Object m_itemId;
852
853        /**
854         * Constructor.<p>
855         *
856         * @param contextMenu the context menu
857         * @param tree the tree
858         * @param itemId the item id
859         */
860        public ContextMenuOpenedOnTreeItemEvent(CmsContextMenu contextMenu, Tree tree, Object itemId) {
861            super(tree);
862
863            m_contextMenu = contextMenu;
864            m_itemId = itemId;
865        }
866
867        /**
868         * Returns the context menu.<p>
869         *
870         * @return the context menu
871         */
872        public CmsContextMenu getContextMenu() {
873
874            return m_contextMenu;
875        }
876
877        /**
878         * Returns the item id.<p>
879         *
880         * @return the item id
881         */
882        public Object getItemId() {
883
884            return m_itemId;
885        }
886    }
887
888    /** The serial version id. */
889    private static final long serialVersionUID = 4275181115413786498L;
890
891    /** The items. */
892    final Map<String, ContextMenuItem> m_items;
893
894    /** The server RPC. */
895    private final I_CmsContextMenuServerRpc m_serverRPC = new I_CmsContextMenuServerRpc() {
896
897        /** The serial version id. */
898        private static final long serialVersionUID = 5622864428554337992L;
899
900        @Override
901        public void contextMenuClosed() {
902
903            fireEvent(new ContextMenuClosedEvent(CmsContextMenu.this));
904        }
905
906        @Override
907        public void itemClicked(String itemId, boolean menuClosed) {
908
909            ContextMenuItem item = m_items.get(itemId);
910            if (item == null) {
911                return;
912            }
913
914            item.notifyClickListeners();
915            fireEvent(new ContextMenuItemClickEvent(item));
916        }
917
918        @Override
919        public void onContextMenuOpenRequested(int x, int y, String connectorId) {
920
921            fireEvent(
922                new ContextMenuOpenedOnComponentEvent(
923                    CmsContextMenu.this,
924                    x,
925                    y,
926                    (Component)UI.getCurrent().getConnectorTracker().getConnector(connectorId)));
927        }
928    };
929
930    /**
931     * Constructor.<p>
932     */
933    public CmsContextMenu() {
934        registerRpc(m_serverRPC);
935
936        m_items = new HashMap<String, CmsContextMenu.ContextMenuItem>();
937
938        setOpenAutomatically(true);
939        setHideAutomatically(true);
940    }
941
942    /**
943     * Adds listener that will be invoked when context menu is closed.<p>
944     *
945     * @param contextMenuClosedListener menu close listener
946     */
947    public void addContextMenuCloseListener(ContextMenuClosedListener contextMenuClosedListener) {
948
949        addListener(ContextMenuClosedEvent.class, contextMenuClosedListener, ContextMenuClosedListener.MENU_CLOSED);
950    }
951
952    /**
953     * Adds listener that will be invoked when context menu is opened from the
954     * component to which it's assigned to.<p>
955     *
956     * @param contextMenuComponentListener the component listener
957     */
958    public void addContextMenuComponentListener(
959        CmsContextMenu.ContextMenuOpenedListener.ComponentListener contextMenuComponentListener) {
960
961        addListener(
962            ContextMenuOpenedOnComponentEvent.class,
963            contextMenuComponentListener,
964            ContextMenuOpenedListener.ComponentListener.MENU_OPENED_FROM_COMPONENT);
965    }
966
967    /**
968     * Adds listener that will be invoked when context menu is opened from
969     * com.vaadin.ui.Table component.<p>
970     *
971     * @param contextMenuTableListener the table listener
972     */
973    public void addContextMenuTableListener(
974        CmsContextMenu.ContextMenuOpenedListener.TableListener contextMenuTableListener) {
975
976        addListener(
977            ContextMenuOpenedOnTableRowEvent.class,
978            contextMenuTableListener,
979            ContextMenuOpenedListener.TableListener.MENU_OPENED_FROM_TABLE_ROW_METHOD);
980        addListener(
981            ContextMenuOpenedOnTableHeaderEvent.class,
982            contextMenuTableListener,
983            ContextMenuOpenedListener.TableListener.MENU_OPENED_FROM_TABLE_HEADER_METHOD);
984        addListener(
985            ContextMenuOpenedOnTableFooterEvent.class,
986            contextMenuTableListener,
987            ContextMenuOpenedListener.TableListener.MENU_OPENED_FROM_TABLE_FOOTER_METHOD);
988    }
989
990    /**
991     * Adds listener that will be invoked when context menu is opened from
992     * com.vaadin.ui.Tree component.<p>
993     *
994     * @param contextMenuTreeListener the menu tree listener
995     */
996    public void addContextMenuTreeListener(
997        CmsContextMenu.ContextMenuOpenedListener.TreeListener contextMenuTreeListener) {
998
999        addListener(
1000            ContextMenuOpenedOnTreeItemEvent.class,
1001            contextMenuTreeListener,
1002            ContextMenuOpenedListener.TreeListener.MENU_OPENED_FROM_TREE_ITEM_METHOD);
1003    }
1004
1005    /**
1006     * Adds new item to context menu root with given icon without caption.<p>
1007     *
1008     * @param icon the icon
1009     * @return reference to newly added item
1010     */
1011    public ContextMenuItem addItem(Resource icon) {
1012
1013        ContextMenuItem item = addItem("");
1014        item.setIcon(icon);
1015        return item;
1016    }
1017
1018    /**
1019     * Adds new item to context menu root with given caption.<p>
1020     *
1021     * @param caption the caption
1022     * @return reference to newly added item
1023     */
1024    public ContextMenuItem addItem(String caption) {
1025
1026        ContextMenuItemState itemState = getState().addChild(caption, getNextId());
1027
1028        ContextMenuItem item = new ContextMenuItem(null, itemState);
1029        m_items.put(itemState.getId(), item);
1030
1031        return item;
1032    }
1033
1034    /**
1035     * Adds new item to context menu root with given caption and icon.<p>
1036     *
1037     * @param caption the caption
1038     * @param icon the icon
1039     * @return reference to newly added item
1040     */
1041    public ContextMenuItem addItem(String caption, Resource icon) {
1042
1043        ContextMenuItem item = addItem(caption);
1044        item.setIcon(icon);
1045        return item;
1046    }
1047
1048    /**
1049     * Adds click listener to context menu. This listener will be invoked when
1050     * any of the menu items in this menu are clicked.<p>
1051     *
1052     * @param clickListener the click listener
1053     */
1054    public void addItemClickListener(CmsContextMenu.ContextMenuItemClickListener clickListener) {
1055
1056        addListener(ContextMenuItemClickEvent.class, clickListener, ContextMenuItemClickListener.ITEM_CLICK_METHOD);
1057    }
1058
1059    /**
1060     * @see com.vaadin.server.AbstractExtension#extend(com.vaadin.server.AbstractClientConnector)
1061     */
1062    @Override
1063    public void extend(AbstractClientConnector target) {
1064
1065        super.extend(target);
1066    }
1067
1068    /**
1069     * Closes the context menu from server side.<p>
1070     */
1071    public void hide() {
1072
1073        getRpcProxy(I_CmsContextMenuClientRpc.class).hide();
1074    }
1075
1076    /**
1077     * Returns if the menu is set to hide automatically.<p>
1078     *
1079     * @return <code>true</code> if context menu is hiding automatically after clicks
1080     */
1081    public boolean isHideAutomatically() {
1082
1083        return getState().isHideAutomatically();
1084    }
1085
1086    /**
1087     * Returns if the menu is set to open automatically.<p>
1088     *
1089     * @return <code>true</code> if open automatically is on. If open automatically is on, it
1090     *         means that context menu will always be opened when it's host
1091     *         component is right clicked. If automatic opening is turned off,
1092     *         context menu will only open when server side open(x, y) is
1093     *         called. Automatic opening avoid having to make server roundtrip
1094     *         whereas "manual" opening allows to have logic in menu before
1095     *         opening it.
1096     */
1097    public boolean isOpenAutomatically() {
1098
1099        return getState().isOpenAutomatically();
1100    }
1101
1102    /**
1103     * Opens the menu for the given component.<p>
1104     *
1105     * @param component the component
1106     */
1107    public void open(Component component) {
1108
1109        getRpcProxy(I_CmsContextMenuClientRpc.class).showContextMenuRelativeTo(component.getConnectorId());
1110    }
1111
1112    /**
1113     * Opens the context menu to given coordinates. ContextMenu must extend
1114     * component before calling this method. This method is only intended for
1115     * opening the context menu from server side when using
1116     * {@link #ContextMenuOpenedListener.ComponentListener}.<p>
1117     *
1118     * @param x the client x position
1119     * @param y the client y position
1120     */
1121    @SuppressWarnings("javadoc")
1122    public void open(int x, int y) {
1123
1124        getRpcProxy(I_CmsContextMenuClientRpc.class).showContextMenu(x, y);
1125    }
1126
1127    /**
1128     * Opens the context menu of the given table.<p>
1129     *
1130     * @param event the click event
1131     * @param itemId of clicked item
1132     * @param propertyId of clicked item
1133     * @param table the table
1134     */
1135    public void openForTable(ClickEvent event, Object itemId, Object propertyId, Table table) {
1136
1137        fireEvent(new ContextMenuOpenedOnTableRowEvent(this, table, itemId, propertyId));
1138        open(event.getClientX(), event.getClientY());
1139
1140    }
1141
1142    /**
1143     * Opens the context menu of the given table.<p>
1144     *
1145     * @param event the click event
1146     * @param table the table
1147     */
1148    public void openForTable(ItemClickEvent event, Table table) {
1149
1150        fireEvent(new ContextMenuOpenedOnTableRowEvent(this, table, event.getItemId(), event.getPropertyId()));
1151        open(event.getClientX(), event.getClientY());
1152
1153    }
1154
1155    /**
1156     * Opens the context menu of the given tree.<p>
1157     *
1158     * @param event the click event
1159     * @param tree the tree
1160     */
1161    public void openForTree(ItemClickEvent event, Tree tree) {
1162
1163        fireEvent(new ContextMenuOpenedOnTreeItemEvent(this, tree, event.getItemId()));
1164        open(event.getClientX(), event.getClientY());
1165    }
1166
1167    /**
1168     * Removes all items from the context menu.<p>
1169     */
1170    public void removeAllItems() {
1171
1172        m_items.clear();
1173        getState().getRootItems().clear();
1174    }
1175
1176    /**
1177     * Removes given context menu item from the context menu. The given item can
1178     * be a root item or leaf item or anything in between. If given given is not
1179     * found from the context menu structure, this method has no effect.<p>
1180     *
1181     * @param contextMenuItem the menu item
1182     */
1183    public void removeItem(ContextMenuItem contextMenuItem) {
1184
1185        if (!hasMenuItem(contextMenuItem)) {
1186            return;
1187        }
1188
1189        if (contextMenuItem.isRootItem()) {
1190            getState().getRootItems().remove(contextMenuItem.m_state);
1191        } else {
1192            ContextMenuItem parent = contextMenuItem.getParent();
1193            parent.m_state.getChildren().remove(contextMenuItem.m_state);
1194        }
1195
1196        Set<ContextMenuItem> children = contextMenuItem.getAllChildren();
1197
1198        m_items.remove(contextMenuItem.m_state.getId());
1199
1200        for (ContextMenuItem child : children) {
1201            m_items.remove(child.m_state.getId());
1202        }
1203
1204        markAsDirty();
1205    }
1206
1207    /**
1208     * Assigns this as context menu of given component which will react to right
1209     * mouse button click.<p>
1210     *
1211     * @param component the component
1212     */
1213    public void setAsContextMenuOf(AbstractClientConnector component) {
1214
1215        if (component instanceof Table) {
1216            setAsTableContextMenu((Table)component);
1217        } else if (component instanceof Tree) {
1218            setAsTreeContextMenu((Tree)component);
1219        } else {
1220            super.extend(component);
1221        }
1222    }
1223
1224    /**
1225     * Assigns this as the context menu of given table.<p>
1226     *
1227     * @param table the table
1228     */
1229    public void setAsTableContextMenu(final Table table) {
1230
1231        extend(table);
1232        setOpenAutomatically(false);
1233    }
1234
1235    /**
1236     * Assigns this as context menu of given tree.<p>
1237     *
1238     * @param tree the tree
1239     */
1240    public void setAsTreeContextMenu(final Tree tree) {
1241
1242        extend(tree);
1243        setOpenAutomatically(false);
1244    }
1245
1246    /**
1247     * Sets the context menu entries. Removes all previously present entries.<p>
1248     *
1249     * @param entries the entries
1250     * @param data the context data
1251     */
1252    public <T> void setEntries(Collection<I_CmsSimpleContextMenuEntry<T>> entries, T data) {
1253
1254        removeAllItems();
1255        Locale locale = UI.getCurrent().getLocale();
1256        for (final I_CmsSimpleContextMenuEntry<T> entry : entries) {
1257            CmsMenuItemVisibilityMode visibility = entry.getVisibility(data);
1258            if (!visibility.isInVisible()) {
1259                ContextMenuItem item = addItem(entry.getTitle(locale));
1260                if (visibility.isInActive()) {
1261                    item.setEnabled(false);
1262                    if (visibility.getMessageKey() != null) {
1263                        item.setDescription(CmsVaadinUtils.getMessageText(visibility.getMessageKey()));
1264                    }
1265                } else {
1266                    item.setData(data);
1267                    item.addItemClickListener(new ContextMenuItemClickListener() {
1268
1269                        @SuppressWarnings("unchecked")
1270                        public void contextMenuItemClicked(ContextMenuItemClickEvent event) {
1271
1272                            entry.executeAction((T)((ContextMenuItem)event.getSource()).getData());
1273                        }
1274                    });
1275                }
1276                if (entry instanceof I_CmsSimpleContextMenuEntry.I_HasCssStyles) {
1277                    item.addStyleName(((I_CmsSimpleContextMenuEntry.I_HasCssStyles)entry).getStyles());
1278                }
1279            }
1280        }
1281    }
1282
1283    /**
1284     * Sets menu to hide automatically after mouse cliks on menu items or area
1285     * off the menu. If automatic hiding is disabled menu will stay open as long
1286     * as hide is called from the server side.<p>
1287     *
1288     * @param hideAutomatically whether to hide automatically
1289     */
1290    public void setHideAutomatically(boolean hideAutomatically) {
1291
1292        getState().setHideAutomatically(hideAutomatically);
1293    }
1294
1295    /**
1296     * Enables or disables open automatically feature. If open automatically is
1297     * on, it means that context menu will always be opened when it's host
1298     * component is right clicked. This will happen on client side without
1299     * server round trip. If automatic opening is turned off, context menu will
1300     * only open when server side open(x, y) is called. If automatic opening is
1301     * disabled you will need a listener implementation for context menu that is
1302     * called upon client side click event. Another option is to extend context
1303     * menu and handle the right clicking internally with case specific listener
1304     * implementation and inside it call open(x, y) method.
1305     *
1306     * @param openAutomatically whether to open automatically
1307     */
1308    public void setOpenAutomatically(boolean openAutomatically) {
1309
1310        getState().setOpenAutomatically(openAutomatically);
1311    }
1312
1313    /**
1314     * Added to increase method visibility.<p>
1315     *
1316     * @see com.vaadin.server.AbstractClientConnector#fireEvent(java.util.EventObject)
1317     */
1318    @Override
1319    protected void fireEvent(EventObject event) {
1320
1321        super.fireEvent(event);
1322    }
1323
1324    /**
1325     * Returns a new UUID.<p>
1326     *
1327     * @return a new UUID
1328     */
1329    protected String getNextId() {
1330
1331        return UUID.randomUUID().toString();
1332    }
1333
1334    /**
1335     * Added to increase visibility.<p>
1336     *
1337     * @see com.vaadin.server.AbstractClientConnector#getResource(java.lang.String)
1338     */
1339    @Override
1340    protected Resource getResource(String key) {
1341
1342        return super.getResource(key);
1343    }
1344
1345    /**
1346     * @see com.vaadin.server.AbstractClientConnector#getState()
1347     */
1348    @Override
1349    protected CmsContextMenuState getState() {
1350
1351        return (CmsContextMenuState)super.getState();
1352    }
1353
1354    /**
1355     * Added to increase visibility.<p>
1356     *
1357     * @see com.vaadin.server.AbstractClientConnector#setResource(java.lang.String, com.vaadin.server.Resource)
1358     */
1359    @Override
1360    protected void setResource(String key, Resource resource) {
1361
1362        super.setResource(key, resource);
1363    }
1364
1365    /**
1366     * Returns whether the menu has a specific item.<p>
1367     *
1368     * @param contextMenuItem the item to look for
1369     *
1370     * @return <code>true</code> if the item is present
1371     */
1372    private boolean hasMenuItem(ContextMenuItem contextMenuItem) {
1373
1374        return m_items.containsKey(contextMenuItem.m_state.getId());
1375    }
1376
1377}