View Javadoc
1   package com.vaadin.contextmenu;
2   
3   /*
4    * Copyright 2000-2014 Vaadin Ltd.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
7    * use this file except in compliance with the License. You may obtain a copy of
8    * the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15   * License for the specific language governing permissions and limitations under
16   * the License.
17   */
18  
19  import java.util.ArrayList;
20  import java.util.Collection;
21  import java.util.Iterator;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Stack;
25  
26  import org.jsoup.nodes.Attributes;
27  import org.jsoup.nodes.Element;
28  import org.jsoup.nodes.Node;
29  import org.jsoup.parser.Tag;
30  
31  import com.vaadin.server.PaintException;
32  import com.vaadin.server.PaintTarget;
33  import com.vaadin.server.Resource;
34  import com.vaadin.shared.ui.menubar.MenuBarConstants;
35  import com.vaadin.shared.ui.menubar.MenuBarState;
36  import com.vaadin.ui.AbstractComponent;
37  import com.vaadin.ui.Component.Focusable;
38  import com.vaadin.ui.LegacyComponent;
39  import com.vaadin.ui.declarative.DesignAttributeHandler;
40  import com.vaadin.ui.declarative.DesignContext;
41  
42  /**
43   * <p>
44   * A class representing a horizontal menu bar. The menu can contain MenuItem
45   * objects, which in turn can contain more MenuBars. These sub-level MenuBars
46   * are represented as vertical menu.
47   * </p>
48   */
49  @SuppressWarnings({ "serial", "deprecation" })
50  public class MenuBar extends AbstractComponent
51          implements Menu, LegacyComponent, Focusable {
52  
53      private MenuItem moreItem;
54  
55      private boolean openRootOnHover;
56  
57      @Override
58      protected MenuBarState getState() {
59          return (MenuBarState) super.getState();
60      }
61  
62      @Override
63      protected MenuBarState getState(boolean markAsDirty) {
64          return (MenuBarState) super.getState(markAsDirty);
65      }
66  
67      /** Paint (serialise) the component for the client. */
68      @Override
69      public void paintContent(PaintTarget target) throws PaintException {
70          target.addAttribute(MenuBarConstants.OPEN_ROOT_MENU_ON_HOWER,
71                  openRootOnHover);
72  
73          if (isHtmlContentAllowed()) {
74              target.addAttribute(MenuBarConstants.HTML_CONTENT_ALLOWED, true);
75          }
76  
77          target.startTag("options");
78  
79          if (getWidth() > -1) {
80              target.startTag("moreItem");
81              target.addAttribute("text", moreItem.getText());
82              if (moreItem.getIcon() != null) {
83                  target.addAttribute("icon", moreItem.getIcon());
84              }
85              target.endTag("moreItem");
86          }
87  
88          target.endTag("options");
89          target.startTag("items");
90  
91          // This generates the tree from the contents of the menu
92          for (MenuItem item : getItems()) {
93              paintItem(target, item);
94          }
95  
96          target.endTag("items");
97      }
98  
99      private void paintItem(PaintTarget target, MenuItem item)
100             throws PaintException {
101         if (!item.isVisible()) {
102             return;
103         }
104 
105         target.startTag("item");
106 
107         target.addAttribute("id", item.getId());
108 
109         if (item.getStyleName() != null) {
110             target.addAttribute(MenuBarConstants.ATTRIBUTE_ITEM_STYLE,
111                     item.getStyleName());
112         }
113 
114         if (item.isSeparator()) {
115             target.addAttribute("separator", true);
116         } else {
117             target.addAttribute("text", item.getText());
118 
119             Command command = item.getCommand();
120             if (command != null) {
121                 target.addAttribute("command", true);
122             }
123 
124             Resource icon = item.getIcon();
125             if (icon != null) {
126                 target.addAttribute(MenuBarConstants.ATTRIBUTE_ITEM_ICON, icon);
127             }
128 
129             if (!item.isEnabled()) {
130                 target.addAttribute(MenuBarConstants.ATTRIBUTE_ITEM_DISABLED,
131                         true);
132             }
133 
134             String description = item.getDescription();
135             if (description != null && description.length() > 0) {
136                 target.addAttribute(MenuBarConstants.ATTRIBUTE_ITEM_DESCRIPTION,
137                         description);
138             }
139             if (item.isCheckable()) {
140                 // if the "checked" attribute is present (either true or false),
141                 // the item is checkable
142                 target.addAttribute(MenuBarConstants.ATTRIBUTE_CHECKED,
143                         item.isChecked());
144             }
145             if (item.hasChildren()) {
146                 for (MenuItem child : item.getChildren()) {
147                     paintItem(target, child);
148                 }
149             }
150 
151         }
152 
153         target.endTag("item");
154     }
155 
156     /** Deserialize changes received from client. */
157     @Override
158     public void changeVariables(Object source, Map<String, Object> variables) {
159         Stack<MenuItem> items = new Stack<MenuItem>();
160         boolean found = false;
161 
162         if (variables.containsKey("clickedId")) {
163 
164             Integer clickedId = (Integer) variables.get("clickedId");
165             Iterator<MenuItem> itr = getItems().iterator();
166             while (itr.hasNext()) {
167                 items.push(itr.next());
168             }
169 
170             MenuItem tmpItem = null;
171 
172             // Go through all the items in the menu
173             while (!found && !items.empty()) {
174                 tmpItem = items.pop();
175                 found = (clickedId.intValue() == tmpItem.getId());
176 
177                 if (tmpItem.hasChildren()) {
178                     itr = tmpItem.getChildren().iterator();
179                     while (itr.hasNext()) {
180                         items.push(itr.next());
181                     }
182                 }
183 
184             } // while
185 
186             // If we got the clicked item, launch the command.
187             if (found && tmpItem.isEnabled()) {
188                 if (tmpItem.isCheckable()) {
189                     tmpItem.setChecked(!tmpItem.isChecked());
190                 }
191                 if (null != tmpItem.getCommand()) {
192                     tmpItem.getCommand().menuSelected(tmpItem);
193                 }
194             }
195         } // if
196     }// changeVariables
197 
198     /**
199      * Constructs an empty, horizontal menu
200      */
201     public MenuBar() {
202         setMoreMenuItem(null);
203     }
204 
205     @Override
206     public int getTabIndex() {
207         return getState(false).tabIndex;
208     }
209 
210     /*
211      * (non-Javadoc)
212      *
213      * @see com.vaadin.ui.Component.Focusable#setTabIndex(int)
214      */
215     @Override
216     public void setTabIndex(int tabIndex) {
217         getState().tabIndex = tabIndex;
218     }
219 
220     @Override
221     public void focus() {
222         // Overridden only to make public
223         super.focus();
224     }
225 
226     @Override
227     public void writeDesign(Element design, DesignContext designContext) {
228         super.writeDesign(design, designContext);
229         for (MenuItem item : getItems()) {
230             design.appendChild(createMenuElement(item, designContext));
231         }
232 
233         // in many cases there seems to be an empty more menu item
234         if (getMoreMenuItem() != null
235                 && !getMoreMenuItem().getText().isEmpty()) {
236             Element moreMenu = createMenuElement(getMoreMenuItem(),
237                     designContext);
238             moreMenu.attr("more", "");
239             design.appendChild(moreMenu);
240         }
241 
242         if (!isHtmlContentAllowed()) {
243             design.attr(DESIGN_ATTR_PLAIN_TEXT, "");
244         }
245     }
246 
247     protected Element createMenuElement(MenuItem item,
248             DesignContext designContext) {
249         Element menuElement = new Element(Tag.valueOf("menu"), "");
250         // Defaults
251         MenuItem def = new MenuItemImpl("", null, null);
252 
253         Attributes attr = menuElement.attributes();
254         DesignAttributeHandler.writeAttribute("icon", attr, item.getIcon(),
255                 def.getIcon(), Resource.class, designContext);
256         DesignAttributeHandler.writeAttribute("disabled", attr,
257                 !item.isEnabled(), !def.isEnabled(), boolean.class,
258                 designContext);
259         DesignAttributeHandler.writeAttribute("visible", attr, item.isVisible(),
260                 def.isVisible(), boolean.class, designContext);
261         DesignAttributeHandler.writeAttribute("separator", attr,
262                 item.isSeparator(), def.isSeparator(), boolean.class,
263                 designContext);
264         DesignAttributeHandler.writeAttribute("checkable", attr,
265                 item.isCheckable(), def.isCheckable(), boolean.class,
266                 designContext);
267         DesignAttributeHandler.writeAttribute("checked", attr, item.isChecked(),
268                 def.isChecked(), boolean.class, designContext);
269         DesignAttributeHandler.writeAttribute("description", attr,
270                 item.getDescription(), def.getDescription(), String.class,
271                 designContext);
272         DesignAttributeHandler.writeAttribute("style-name", attr,
273                 item.getStyleName(), def.getStyleName(), String.class,
274                 designContext);
275 
276         menuElement.append(item.getText());
277 
278         if (item.hasChildren()) {
279             for (MenuItem subMenu : item.getChildren()) {
280                 menuElement
281                         .appendChild(createMenuElement(subMenu, designContext));
282             }
283         }
284 
285         return menuElement;
286     }
287 
288     protected MenuItem readMenuElement(Element menuElement, MenuItem parent) {
289         Resource icon = null;
290         if (menuElement.hasAttr("icon")) {
291             icon = DesignAttributeHandler.getFormatter()
292                     .parse(menuElement.attr("icon"), Resource.class);
293         }
294 
295         String caption = "";
296         List<Element> subMenus = new ArrayList<Element>();
297         for (Node node : menuElement.childNodes()) {
298             if (node instanceof Element
299                     && ((Element) node).tagName().equals("menu")) {
300                 subMenus.add((Element) node);
301             } else {
302                 caption += node.toString();
303             }
304         }
305 
306         MenuItemImpl menu = new MenuItemImpl(parent, caption.trim(), icon,
307                 null);
308 
309         Attributes attr = menuElement.attributes();
310         if (menuElement.hasAttr("icon")) {
311             menu.setIcon(DesignAttributeHandler.readAttribute("icon", attr,
312                     Resource.class));
313         }
314         if (menuElement.hasAttr("disabled")) {
315             menu.setEnabled(!DesignAttributeHandler.readAttribute("disabled",
316                     attr, boolean.class));
317         }
318         if (menuElement.hasAttr("visible")) {
319             menu.setVisible(DesignAttributeHandler.readAttribute("visible",
320                     attr, boolean.class));
321         }
322         if (menuElement.hasAttr("separator")) {
323             menu.setSeparator(DesignAttributeHandler.readAttribute("separator",
324                     attr, boolean.class));
325         }
326         if (menuElement.hasAttr("checkable")) {
327             menu.setCheckable(DesignAttributeHandler.readAttribute("checkable",
328                     attr, boolean.class));
329         }
330         if (menuElement.hasAttr("checked")) {
331             menu.setChecked(DesignAttributeHandler.readAttribute("checked",
332                     attr, boolean.class));
333         }
334         if (menuElement.hasAttr("description")) {
335             menu.setDescription(DesignAttributeHandler
336                     .readAttribute("description", attr, String.class));
337         }
338         if (menuElement.hasAttr("style-name")) {
339             menu.setStyleName(DesignAttributeHandler.readAttribute("style-name",
340                     attr, String.class));
341         }
342 
343         if (!subMenus.isEmpty()) {
344             menu.setChildren(new ArrayList<MenuItem>());
345         }
346 
347         for (Element subMenu : subMenus) {
348             MenuItem newItem = readMenuElement(subMenu, menu);
349 
350             menu.getChildren().add(newItem);
351         }
352 
353         return menu;
354     }
355 
356     /**
357      * Set the item that is used when collapsing the top level menu. All
358      * "overflowing" items will be added below this. The item command will be
359      * ignored. If set to null, the default item with a downwards arrow is used.
360      *
361      * The item command (if specified) is ignored.
362      *
363      * @param item
364      */
365     public void setMoreMenuItem(MenuItem item) {
366         if (item != null) {
367             moreItem = item;
368         } else {
369             moreItem = new MenuItemImpl("", null, null);
370         }
371         markAsDirty();
372     }
373 
374     /**
375      * Get the MenuItem used as the collapse menu item.
376      *
377      * @return
378      */
379     public MenuItem getMoreMenuItem() {
380         return moreItem;
381     }
382 
383     /**
384      * Using this method menubar can be put into a special mode where top level
385      * menus opens without clicking on the menu, but automatically when mouse
386      * cursor is moved over the menu. In this mode the menu also closes itself
387      * if the mouse is moved out of the opened menu.
388      * <p>
389      * Note, that on touch devices the menu still opens on a click event.
390      *
391      * @param autoOpenTopLevelMenu
392      *            true if menus should be opened without click, the default is
393      *            false
394      */
395     public void setAutoOpen(boolean autoOpenTopLevelMenu) {
396         if (autoOpenTopLevelMenu != openRootOnHover) {
397             openRootOnHover = autoOpenTopLevelMenu;
398             markAsDirty();
399         }
400     }
401 
402     /**
403      * Detects whether the menubar is in a mode where top level menus are
404      * automatically opened when the mouse cursor is moved over the menu.
405      * Normally root menu opens only by clicking on the menu. Submenus always
406      * open automatically.
407      *
408      * @return true if the root menus open without click, the default is false
409      */
410     public boolean isAutoOpen() {
411         return openRootOnHover;
412     }
413 
414     @Override
415     public void readDesign(Element design, DesignContext designContext) {
416         super.readDesign(design, designContext);
417 
418         for (Element itemElement : design.children()) {
419             if (itemElement.tagName().equals("menu")) {
420                 MenuItem menuItem = readMenuElement(itemElement, null);
421                 if (itemElement.hasAttr("more")) {
422                     setMoreMenuItem(menuItem);
423                 } else {
424                     getItems().add(menuItem);
425                 }
426             }
427         }
428 
429         setHtmlContentAllowed(!design.hasAttr(DESIGN_ATTR_PLAIN_TEXT));
430     }
431 
432     @Override
433     protected Collection<String> getCustomAttributes() {
434         Collection<String> result = super.getCustomAttributes();
435         result.add(DESIGN_ATTR_PLAIN_TEXT);
436         result.add("html-content-allowed");
437         return result;
438     }
439 
440     /**** Delegates to AbstractMenu ****/
441 
442     private Menu menu = new AbstractMenu(this);
443 
444     @Override
445     public MenuItem addItem(String caption, Command command) {
446         return menu.addItem(caption, command);
447     }
448 
449     @Override
450     public MenuItem addItem(String caption, Resource icon, Command command) {
451         return menu.addItem(caption, icon, command);
452     }
453 
454     @Override
455     public MenuItem addItemBefore(String caption, Resource icon,
456             Command command, MenuItem itemToAddBefore) {
457         return menu.addItemBefore(caption, icon, command, itemToAddBefore);
458     }
459 
460     @Override
461     public List<MenuItem> getItems() {
462         return menu.getItems();
463     }
464 
465     @Override
466     public void removeItem(MenuItem item) {
467         menu.removeItem(item);
468     }
469 
470     @Override
471     public void removeItems() {
472         menu.removeItems();
473     }
474 
475     @Override
476     public int getSize() {
477         return menu.getSize();
478     }
479 
480     @Override
481     public void setHtmlContentAllowed(boolean htmlContentAllowed) {
482         menu.setHtmlContentAllowed(htmlContentAllowed);
483     }
484 
485     @Override
486     public boolean isHtmlContentAllowed() {
487         return menu.isHtmlContentAllowed();
488     }
489 
490     /**** End of deletates to AbstractMenu ****/
491 
492     // public class MenuItem extends MenuItemImpl implements Serializable {
493     // public MenuItem(String caption, Resource icon, Command command) {
494     // super(caption, icon, command);
495     // // Auto-generated constructor stub
496     // }
497     // }
498 }// class MenuBar