You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

VMenuBar.java 45KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446
  1. /*
  2. @VaadinApache2LicenseForJavaFiles@
  3. */
  4. package com.vaadin.terminal.gwt.client.ui.menubar;
  5. import java.util.ArrayList;
  6. import java.util.List;
  7. import com.google.gwt.core.client.GWT;
  8. import com.google.gwt.core.client.Scheduler;
  9. import com.google.gwt.core.client.Scheduler.ScheduledCommand;
  10. import com.google.gwt.dom.client.Style;
  11. import com.google.gwt.dom.client.Style.Overflow;
  12. import com.google.gwt.dom.client.Style.Unit;
  13. import com.google.gwt.event.dom.client.FocusEvent;
  14. import com.google.gwt.event.dom.client.FocusHandler;
  15. import com.google.gwt.event.dom.client.KeyCodes;
  16. import com.google.gwt.event.dom.client.KeyDownEvent;
  17. import com.google.gwt.event.dom.client.KeyDownHandler;
  18. import com.google.gwt.event.dom.client.KeyPressEvent;
  19. import com.google.gwt.event.dom.client.KeyPressHandler;
  20. import com.google.gwt.event.logical.shared.CloseEvent;
  21. import com.google.gwt.event.logical.shared.CloseHandler;
  22. import com.google.gwt.user.client.Command;
  23. import com.google.gwt.user.client.DOM;
  24. import com.google.gwt.user.client.Element;
  25. import com.google.gwt.user.client.Event;
  26. import com.google.gwt.user.client.Timer;
  27. import com.google.gwt.user.client.ui.HasHTML;
  28. import com.google.gwt.user.client.ui.PopupPanel;
  29. import com.google.gwt.user.client.ui.RootPanel;
  30. import com.google.gwt.user.client.ui.Widget;
  31. import com.vaadin.terminal.gwt.client.ApplicationConnection;
  32. import com.vaadin.terminal.gwt.client.BrowserInfo;
  33. import com.vaadin.terminal.gwt.client.LayoutManager;
  34. import com.vaadin.terminal.gwt.client.TooltipInfo;
  35. import com.vaadin.terminal.gwt.client.UIDL;
  36. import com.vaadin.terminal.gwt.client.Util;
  37. import com.vaadin.terminal.gwt.client.ui.Icon;
  38. import com.vaadin.terminal.gwt.client.ui.SimpleFocusablePanel;
  39. import com.vaadin.terminal.gwt.client.ui.SubPartAware;
  40. import com.vaadin.terminal.gwt.client.ui.VLazyExecutor;
  41. import com.vaadin.terminal.gwt.client.ui.VOverlay;
  42. public class VMenuBar extends SimpleFocusablePanel implements
  43. CloseHandler<PopupPanel>, KeyPressHandler, KeyDownHandler,
  44. FocusHandler, SubPartAware {
  45. // The hierarchy of VMenuBar is a bit weird as VMenuBar is the Paintable,
  46. // used for the root menu but also used for the sub menus.
  47. /** Set the CSS class name to allow styling. */
  48. public static final String CLASSNAME = "v-menubar";
  49. /** For server connections **/
  50. protected String uidlId;
  51. protected ApplicationConnection client;
  52. protected final VMenuBar hostReference = this;
  53. protected CustomMenuItem moreItem = null;
  54. // Only used by the root menu bar
  55. protected VMenuBar collapsedRootItems;
  56. // Construct an empty command to be used when the item has no command
  57. // associated
  58. protected static final Command emptyCommand = null;
  59. public static final String OPEN_ROOT_MENU_ON_HOWER = "ormoh";
  60. public static final String ATTRIBUTE_CHECKED = "checked";
  61. public static final String ATTRIBUTE_ITEM_DESCRIPTION = "description";
  62. public static final String ATTRIBUTE_ITEM_ICON = "icon";
  63. public static final String ATTRIBUTE_ITEM_DISABLED = "disabled";
  64. public static final String ATTRIBUTE_ITEM_STYLE = "style";
  65. public static final String HTML_CONTENT_ALLOWED = "usehtml";
  66. /** Widget fields **/
  67. protected boolean subMenu;
  68. protected ArrayList<CustomMenuItem> items;
  69. protected Element containerElement;
  70. protected VOverlay popup;
  71. protected VMenuBar visibleChildMenu;
  72. protected boolean menuVisible = false;
  73. protected VMenuBar parentMenu;
  74. protected CustomMenuItem selected;
  75. boolean enabled = true;
  76. private VLazyExecutor iconLoadedExecutioner = new VLazyExecutor(100,
  77. new ScheduledCommand() {
  78. public void execute() {
  79. iLayout(true);
  80. }
  81. });
  82. boolean openRootOnHover;
  83. boolean htmlContentAllowed;
  84. public VMenuBar() {
  85. // Create an empty horizontal menubar
  86. this(false, null);
  87. // Navigation is only handled by the root bar
  88. addFocusHandler(this);
  89. /*
  90. * Firefox auto-repeat works correctly only if we use a key press
  91. * handler, other browsers handle it correctly when using a key down
  92. * handler
  93. */
  94. if (BrowserInfo.get().isGecko()) {
  95. addKeyPressHandler(this);
  96. } else {
  97. addKeyDownHandler(this);
  98. }
  99. }
  100. public VMenuBar(boolean subMenu, VMenuBar parentMenu) {
  101. items = new ArrayList<CustomMenuItem>();
  102. popup = null;
  103. visibleChildMenu = null;
  104. containerElement = getElement();
  105. if (!subMenu) {
  106. setStyleName(CLASSNAME);
  107. } else {
  108. setStyleName(CLASSNAME + "-submenu");
  109. this.parentMenu = parentMenu;
  110. }
  111. this.subMenu = subMenu;
  112. sinkEvents(Event.ONCLICK | Event.ONMOUSEOVER | Event.ONMOUSEOUT
  113. | Event.ONLOAD);
  114. }
  115. @Override
  116. protected void onDetach() {
  117. super.onDetach();
  118. if (!subMenu) {
  119. setSelected(null);
  120. hideChildren();
  121. menuVisible = false;
  122. }
  123. }
  124. void updateSize() {
  125. // Take from setWidth
  126. if (!subMenu) {
  127. // Only needed for root level menu
  128. hideChildren();
  129. setSelected(null);
  130. menuVisible = false;
  131. }
  132. }
  133. /**
  134. * Build the HTML content for a menu item.
  135. *
  136. * @param item
  137. * @return
  138. */
  139. protected String buildItemHTML(UIDL item) {
  140. // Construct html from the text and the optional icon
  141. StringBuffer itemHTML = new StringBuffer();
  142. if (item.hasAttribute("separator")) {
  143. itemHTML.append("<span>---</span>");
  144. } else {
  145. // Add submenu indicator
  146. if (item.getChildCount() > 0) {
  147. String bgStyle = "";
  148. itemHTML.append("<span class=\"" + CLASSNAME
  149. + "-submenu-indicator\"" + bgStyle + ">&#x25BA;</span>");
  150. }
  151. itemHTML.append("<span class=\"" + CLASSNAME
  152. + "-menuitem-caption\">");
  153. if (item.hasAttribute("icon")) {
  154. itemHTML.append("<img src=\""
  155. + Util.escapeAttribute(client.translateVaadinUri(item
  156. .getStringAttribute("icon"))) + "\" class=\""
  157. + Icon.CLASSNAME + "\" alt=\"\" />");
  158. }
  159. String itemText = item.getStringAttribute("text");
  160. if (!htmlContentAllowed) {
  161. itemText = Util.escapeHTML(itemText);
  162. }
  163. itemHTML.append(itemText);
  164. itemHTML.append("</span>");
  165. }
  166. return itemHTML.toString();
  167. }
  168. /**
  169. * This is called by the items in the menu and it communicates the
  170. * information to the server
  171. *
  172. * @param clickedItemId
  173. * id of the item that was clicked
  174. */
  175. public void onMenuClick(int clickedItemId) {
  176. // Updating the state to the server can not be done before
  177. // the server connection is known, i.e., before updateFromUIDL()
  178. // has been called.
  179. if (uidlId != null && client != null) {
  180. // Communicate the user interaction parameters to server. This call
  181. // will initiate an AJAX request to the server.
  182. client.updateVariable(uidlId, "clickedId", clickedItemId, true);
  183. }
  184. }
  185. /** Widget methods **/
  186. /**
  187. * Returns a list of items in this menu
  188. */
  189. public List<CustomMenuItem> getItems() {
  190. return items;
  191. }
  192. /**
  193. * Remove all the items in this menu
  194. */
  195. public void clearItems() {
  196. Element e = getContainerElement();
  197. while (DOM.getChildCount(e) > 0) {
  198. DOM.removeChild(e, DOM.getChild(e, 0));
  199. }
  200. items.clear();
  201. }
  202. /**
  203. * Returns the containing element of the menu
  204. *
  205. * @return
  206. */
  207. @Override
  208. public Element getContainerElement() {
  209. return containerElement;
  210. }
  211. /**
  212. * Add a new item to this menu
  213. *
  214. * @param html
  215. * items text
  216. * @param cmd
  217. * items command
  218. * @return the item created
  219. */
  220. public CustomMenuItem addItem(String html, Command cmd) {
  221. CustomMenuItem item = GWT.create(CustomMenuItem.class);
  222. item.setHTML(html);
  223. item.setCommand(cmd);
  224. addItem(item);
  225. return item;
  226. }
  227. /**
  228. * Add a new item to this menu
  229. *
  230. * @param item
  231. */
  232. public void addItem(CustomMenuItem item) {
  233. if (items.contains(item)) {
  234. return;
  235. }
  236. DOM.appendChild(getContainerElement(), item.getElement());
  237. item.setParentMenu(this);
  238. item.setSelected(false);
  239. items.add(item);
  240. }
  241. public void addItem(CustomMenuItem item, int index) {
  242. if (items.contains(item)) {
  243. return;
  244. }
  245. DOM.insertChild(getContainerElement(), item.getElement(), index);
  246. item.setParentMenu(this);
  247. item.setSelected(false);
  248. items.add(index, item);
  249. }
  250. /**
  251. * Remove the given item from this menu
  252. *
  253. * @param item
  254. */
  255. public void removeItem(CustomMenuItem item) {
  256. if (items.contains(item)) {
  257. int index = items.indexOf(item);
  258. DOM.removeChild(getContainerElement(),
  259. DOM.getChild(getContainerElement(), index));
  260. items.remove(index);
  261. }
  262. }
  263. /*
  264. * @see
  265. * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user
  266. * .client.Event)
  267. */
  268. @Override
  269. public void onBrowserEvent(Event e) {
  270. super.onBrowserEvent(e);
  271. // Handle onload events (icon loaded, size changes)
  272. if (DOM.eventGetType(e) == Event.ONLOAD) {
  273. VMenuBar parent = getParentMenu();
  274. if (parent != null) {
  275. // The onload event for an image in a popup should be sent to
  276. // the parent, which owns the popup
  277. parent.iconLoaded();
  278. } else {
  279. // Onload events for images in the root menu are handled by the
  280. // root menu itself
  281. iconLoaded();
  282. }
  283. return;
  284. }
  285. Element targetElement = DOM.eventGetTarget(e);
  286. CustomMenuItem targetItem = null;
  287. for (int i = 0; i < items.size(); i++) {
  288. CustomMenuItem item = items.get(i);
  289. if (DOM.isOrHasChild(item.getElement(), targetElement)) {
  290. targetItem = item;
  291. }
  292. }
  293. if (targetItem != null) {
  294. switch (DOM.eventGetType(e)) {
  295. case Event.ONCLICK:
  296. if (isEnabled() && targetItem.isEnabled()) {
  297. itemClick(targetItem);
  298. }
  299. if (subMenu) {
  300. // Prevent moving keyboard focus to child menus
  301. VMenuBar parent = parentMenu;
  302. while (parent.getParentMenu() != null) {
  303. parent = parent.getParentMenu();
  304. }
  305. parent.setFocus(true);
  306. }
  307. break;
  308. case Event.ONMOUSEOVER:
  309. LazyCloser.cancelClosing();
  310. if (isEnabled() && targetItem.isEnabled()) {
  311. itemOver(targetItem);
  312. }
  313. break;
  314. case Event.ONMOUSEOUT:
  315. itemOut(targetItem);
  316. LazyCloser.schedule();
  317. break;
  318. }
  319. } else if (subMenu && DOM.eventGetType(e) == Event.ONCLICK && subMenu) {
  320. // Prevent moving keyboard focus to child menus
  321. VMenuBar parent = parentMenu;
  322. while (parent.getParentMenu() != null) {
  323. parent = parent.getParentMenu();
  324. }
  325. parent.setFocus(true);
  326. }
  327. }
  328. private boolean isEnabled() {
  329. return enabled;
  330. }
  331. private void iconLoaded() {
  332. iconLoadedExecutioner.trigger();
  333. }
  334. /**
  335. * When an item is clicked
  336. *
  337. * @param item
  338. */
  339. public void itemClick(CustomMenuItem item) {
  340. if (item.getCommand() != null) {
  341. setSelected(null);
  342. if (visibleChildMenu != null) {
  343. visibleChildMenu.hideChildren();
  344. }
  345. hideParents(true);
  346. menuVisible = false;
  347. Scheduler.get().scheduleDeferred(item.getCommand());
  348. } else {
  349. if (item.getSubMenu() != null
  350. && item.getSubMenu() != visibleChildMenu) {
  351. setSelected(item);
  352. showChildMenu(item);
  353. menuVisible = true;
  354. } else if (!subMenu) {
  355. setSelected(null);
  356. hideChildren();
  357. menuVisible = false;
  358. }
  359. }
  360. }
  361. /**
  362. * When the user hovers the mouse over the item
  363. *
  364. * @param item
  365. */
  366. public void itemOver(CustomMenuItem item) {
  367. if ((openRootOnHover || subMenu || menuVisible) && !item.isSeparator()) {
  368. setSelected(item);
  369. if (!subMenu && openRootOnHover && !menuVisible) {
  370. menuVisible = true; // start opening menus
  371. LazyCloser.prepare(this);
  372. }
  373. }
  374. if (menuVisible && visibleChildMenu != item.getSubMenu()
  375. && popup != null) {
  376. popup.hide();
  377. }
  378. if (menuVisible && item.getSubMenu() != null
  379. && visibleChildMenu != item.getSubMenu()) {
  380. showChildMenu(item);
  381. }
  382. }
  383. /**
  384. * When the mouse is moved away from an item
  385. *
  386. * @param item
  387. */
  388. public void itemOut(CustomMenuItem item) {
  389. if (visibleChildMenu != item.getSubMenu()) {
  390. hideChildMenu(item);
  391. setSelected(null);
  392. } else if (visibleChildMenu == null) {
  393. setSelected(null);
  394. }
  395. }
  396. /**
  397. * Used to autoclose submenus when they the menu is in a mode which opens
  398. * root menus on mouse hover.
  399. */
  400. private static class LazyCloser extends Timer {
  401. static LazyCloser INSTANCE;
  402. private VMenuBar activeRoot;
  403. @Override
  404. public void run() {
  405. activeRoot.hideChildren();
  406. activeRoot.setSelected(null);
  407. activeRoot.menuVisible = false;
  408. activeRoot = null;
  409. }
  410. public static void cancelClosing() {
  411. if (INSTANCE != null) {
  412. INSTANCE.cancel();
  413. }
  414. }
  415. public static void prepare(VMenuBar vMenuBar) {
  416. if (INSTANCE == null) {
  417. INSTANCE = new LazyCloser();
  418. }
  419. if (INSTANCE.activeRoot == vMenuBar) {
  420. INSTANCE.cancel();
  421. } else if (INSTANCE.activeRoot != null) {
  422. INSTANCE.cancel();
  423. INSTANCE.run();
  424. }
  425. INSTANCE.activeRoot = vMenuBar;
  426. }
  427. public static void schedule() {
  428. if (INSTANCE != null && INSTANCE.activeRoot != null) {
  429. INSTANCE.schedule(750);
  430. }
  431. }
  432. }
  433. /**
  434. * Shows the child menu of an item. The caller must ensure that the item has
  435. * a submenu.
  436. *
  437. * @param item
  438. */
  439. public void showChildMenu(CustomMenuItem item) {
  440. int left = 0;
  441. int top = 0;
  442. if (subMenu) {
  443. left = item.getParentMenu().getAbsoluteLeft()
  444. + item.getParentMenu().getOffsetWidth();
  445. top = item.getAbsoluteTop();
  446. } else {
  447. left = item.getAbsoluteLeft();
  448. top = item.getParentMenu().getAbsoluteTop()
  449. + item.getParentMenu().getOffsetHeight();
  450. }
  451. showChildMenuAt(item, top, left);
  452. }
  453. protected void showChildMenuAt(CustomMenuItem item, int top, int left) {
  454. final int shadowSpace = 10;
  455. popup = new VOverlay(true, false, true);
  456. // Setting owner and handlers to support tooltips. Needed for tooltip
  457. // handling of overlay widgets (will direct queries to parent menu)
  458. if (parentMenu == null) {
  459. popup.setOwner(this);
  460. } else {
  461. VMenuBar parent = parentMenu;
  462. while (parent.getParentMenu() != null) {
  463. parent = parent.getParentMenu();
  464. }
  465. popup.setOwner(parent);
  466. }
  467. if (client != null) {
  468. client.getVTooltip().connectHandlersToWidget(popup);
  469. }
  470. popup.setStyleName(CLASSNAME + "-popup");
  471. popup.setWidget(item.getSubMenu());
  472. popup.addCloseHandler(this);
  473. popup.addAutoHidePartner(item.getElement());
  474. // at 0,0 because otherwise IE7 add extra scrollbars (#5547)
  475. popup.setPopupPosition(0, 0);
  476. item.getSubMenu().onShow();
  477. visibleChildMenu = item.getSubMenu();
  478. item.getSubMenu().setParentMenu(this);
  479. popup.show();
  480. if (left + popup.getOffsetWidth() >= RootPanel.getBodyElement()
  481. .getOffsetWidth() - shadowSpace) {
  482. if (subMenu) {
  483. left = item.getParentMenu().getAbsoluteLeft()
  484. - popup.getOffsetWidth() - shadowSpace;
  485. } else {
  486. left = RootPanel.getBodyElement().getOffsetWidth()
  487. - popup.getOffsetWidth() - shadowSpace;
  488. }
  489. // Accommodate space for shadow
  490. if (left < shadowSpace) {
  491. left = shadowSpace;
  492. }
  493. }
  494. top = adjustPopupHeight(top, shadowSpace);
  495. popup.setPopupPosition(left, top);
  496. }
  497. private int adjustPopupHeight(int top, final int shadowSpace) {
  498. // Check that the popup will fit the screen
  499. int availableHeight = RootPanel.getBodyElement().getOffsetHeight()
  500. - top - shadowSpace;
  501. int missingHeight = popup.getOffsetHeight() - availableHeight;
  502. if (missingHeight > 0) {
  503. // First move the top of the popup to get more space
  504. // Don't move above top of screen, don't move more than needed
  505. int moveUpBy = Math.min(top - shadowSpace, missingHeight);
  506. // Update state
  507. top -= moveUpBy;
  508. missingHeight -= moveUpBy;
  509. availableHeight += moveUpBy;
  510. if (missingHeight > 0) {
  511. int contentWidth = visibleChildMenu.getOffsetWidth();
  512. // If there's still not enough room, limit height to fit and add
  513. // a scroll bar
  514. Style style = popup.getElement().getStyle();
  515. style.setHeight(availableHeight, Unit.PX);
  516. style.setOverflowY(Overflow.SCROLL);
  517. // Make room for the scroll bar by adjusting the width of the
  518. // popup
  519. style.setWidth(contentWidth + Util.getNativeScrollbarSize(),
  520. Unit.PX);
  521. popup.updateShadowSizeAndPosition();
  522. }
  523. }
  524. return top;
  525. }
  526. /**
  527. * Hides the submenu of an item
  528. *
  529. * @param item
  530. */
  531. public void hideChildMenu(CustomMenuItem item) {
  532. if (visibleChildMenu != null
  533. && !(visibleChildMenu == item.getSubMenu())) {
  534. popup.hide();
  535. }
  536. }
  537. /**
  538. * When the menu is shown.
  539. */
  540. public void onShow() {
  541. // remove possible previous selection
  542. if (selected != null) {
  543. selected.setSelected(false);
  544. selected = null;
  545. }
  546. menuVisible = true;
  547. }
  548. /**
  549. * Listener method, fired when this menu is closed
  550. */
  551. public void onClose(CloseEvent<PopupPanel> event) {
  552. hideChildren();
  553. if (event.isAutoClosed()) {
  554. hideParents(true);
  555. menuVisible = false;
  556. }
  557. visibleChildMenu = null;
  558. popup = null;
  559. }
  560. /**
  561. * Recursively hide all child menus
  562. */
  563. public void hideChildren() {
  564. if (visibleChildMenu != null) {
  565. visibleChildMenu.hideChildren();
  566. popup.hide();
  567. }
  568. }
  569. /**
  570. * Recursively hide all parent menus
  571. */
  572. public void hideParents(boolean autoClosed) {
  573. if (visibleChildMenu != null) {
  574. popup.hide();
  575. setSelected(null);
  576. menuVisible = !autoClosed;
  577. }
  578. if (getParentMenu() != null) {
  579. getParentMenu().hideParents(autoClosed);
  580. }
  581. }
  582. /**
  583. * Returns the parent menu of this menu, or null if this is the top-level
  584. * menu
  585. *
  586. * @return
  587. */
  588. public VMenuBar getParentMenu() {
  589. return parentMenu;
  590. }
  591. /**
  592. * Set the parent menu of this menu
  593. *
  594. * @param parent
  595. */
  596. public void setParentMenu(VMenuBar parent) {
  597. parentMenu = parent;
  598. }
  599. /**
  600. * Returns the currently selected item of this menu, or null if nothing is
  601. * selected
  602. *
  603. * @return
  604. */
  605. public CustomMenuItem getSelected() {
  606. return selected;
  607. }
  608. /**
  609. * Set the currently selected item of this menu
  610. *
  611. * @param item
  612. */
  613. public void setSelected(CustomMenuItem item) {
  614. // If we had something selected, unselect
  615. if (item != selected && selected != null) {
  616. selected.setSelected(false);
  617. }
  618. // If we have a valid selection, select it
  619. if (item != null) {
  620. item.setSelected(true);
  621. }
  622. selected = item;
  623. }
  624. /**
  625. *
  626. * A class to hold information on menu items
  627. *
  628. */
  629. public static class CustomMenuItem extends Widget implements HasHTML {
  630. protected String html = null;
  631. protected Command command = null;
  632. protected VMenuBar subMenu = null;
  633. protected VMenuBar parentMenu = null;
  634. protected boolean enabled = true;
  635. protected boolean isSeparator = false;
  636. protected boolean checkable = false;
  637. protected boolean checked = false;
  638. protected String description = null;
  639. /**
  640. * Default menu item {@link Widget} constructor for GWT.create().
  641. *
  642. * Use {@link #setHTML(String)} and {@link #setCommand(Command)} after
  643. * constructing a menu item.
  644. */
  645. public CustomMenuItem() {
  646. this("", null);
  647. }
  648. /**
  649. * Creates a menu item {@link Widget}.
  650. *
  651. * @param html
  652. * @param cmd
  653. * @deprecated use the default constructor and {@link #setHTML(String)}
  654. * and {@link #setCommand(Command)} instead
  655. */
  656. @Deprecated
  657. public CustomMenuItem(String html, Command cmd) {
  658. // We need spans to allow inline-block in IE
  659. setElement(DOM.createSpan());
  660. setHTML(html);
  661. setCommand(cmd);
  662. setSelected(false);
  663. setStyleName(CLASSNAME + "-menuitem");
  664. }
  665. public void setSelected(boolean selected) {
  666. if (selected && isSelectable()) {
  667. addStyleDependentName("selected");
  668. // needed for IE6 to have a single style name to match for an
  669. // element
  670. // TODO Can be optimized now that IE6 is not supported any more
  671. if (checkable) {
  672. if (checked) {
  673. removeStyleDependentName("selected-unchecked");
  674. addStyleDependentName("selected-checked");
  675. } else {
  676. removeStyleDependentName("selected-checked");
  677. addStyleDependentName("selected-unchecked");
  678. }
  679. }
  680. } else {
  681. removeStyleDependentName("selected");
  682. // needed for IE6 to have a single style name to match for an
  683. // element
  684. removeStyleDependentName("selected-checked");
  685. removeStyleDependentName("selected-unchecked");
  686. }
  687. }
  688. public void setChecked(boolean checked) {
  689. if (checkable && !isSeparator) {
  690. this.checked = checked;
  691. if (checked) {
  692. addStyleDependentName("checked");
  693. removeStyleDependentName("unchecked");
  694. } else {
  695. addStyleDependentName("unchecked");
  696. removeStyleDependentName("checked");
  697. }
  698. } else {
  699. this.checked = false;
  700. }
  701. }
  702. public boolean isChecked() {
  703. return checked;
  704. }
  705. public void setCheckable(boolean checkable) {
  706. if (checkable && !isSeparator) {
  707. this.checkable = true;
  708. } else {
  709. setChecked(false);
  710. this.checkable = false;
  711. }
  712. }
  713. public boolean isCheckable() {
  714. return checkable;
  715. }
  716. /*
  717. * setters and getters for the fields
  718. */
  719. public void setSubMenu(VMenuBar subMenu) {
  720. this.subMenu = subMenu;
  721. }
  722. public VMenuBar getSubMenu() {
  723. return subMenu;
  724. }
  725. public void setParentMenu(VMenuBar parentMenu) {
  726. this.parentMenu = parentMenu;
  727. }
  728. public VMenuBar getParentMenu() {
  729. return parentMenu;
  730. }
  731. public void setCommand(Command command) {
  732. this.command = command;
  733. }
  734. public Command getCommand() {
  735. return command;
  736. }
  737. public String getHTML() {
  738. return html;
  739. }
  740. public void setHTML(String html) {
  741. this.html = html;
  742. DOM.setInnerHTML(getElement(), html);
  743. // Sink the onload event for any icons. The onload
  744. // events are handled by the parent VMenuBar.
  745. Util.sinkOnloadForImages(getElement());
  746. }
  747. public String getText() {
  748. return html;
  749. }
  750. public void setText(String text) {
  751. setHTML(Util.escapeHTML(text));
  752. }
  753. public void setEnabled(boolean enabled) {
  754. this.enabled = enabled;
  755. if (enabled) {
  756. removeStyleDependentName("disabled");
  757. } else {
  758. addStyleDependentName("disabled");
  759. }
  760. }
  761. public boolean isEnabled() {
  762. return enabled;
  763. }
  764. private void setSeparator(boolean separator) {
  765. isSeparator = separator;
  766. if (separator) {
  767. setStyleName(CLASSNAME + "-separator");
  768. } else {
  769. setStyleName(CLASSNAME + "-menuitem");
  770. setEnabled(enabled);
  771. }
  772. }
  773. public boolean isSeparator() {
  774. return isSeparator;
  775. }
  776. public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
  777. setSeparator(uidl.hasAttribute("separator"));
  778. setEnabled(!uidl.hasAttribute(ATTRIBUTE_ITEM_DISABLED));
  779. if (!isSeparator() && uidl.hasAttribute(ATTRIBUTE_CHECKED)) {
  780. // if the selected attribute is present (either true or false),
  781. // the item is selectable
  782. setCheckable(true);
  783. setChecked(uidl.getBooleanAttribute(ATTRIBUTE_CHECKED));
  784. } else {
  785. setCheckable(false);
  786. }
  787. if (uidl.hasAttribute(ATTRIBUTE_ITEM_STYLE)) {
  788. String itemStyle = uidl
  789. .getStringAttribute(ATTRIBUTE_ITEM_STYLE);
  790. addStyleDependentName(itemStyle);
  791. }
  792. if (uidl.hasAttribute(ATTRIBUTE_ITEM_DESCRIPTION)) {
  793. description = uidl
  794. .getStringAttribute(ATTRIBUTE_ITEM_DESCRIPTION);
  795. }
  796. }
  797. public TooltipInfo getTooltip() {
  798. if (description == null) {
  799. return null;
  800. }
  801. return new TooltipInfo(description);
  802. }
  803. /**
  804. * Checks if the item can be selected.
  805. *
  806. * @return true if it is possible to select this item, false otherwise
  807. */
  808. public boolean isSelectable() {
  809. return !isSeparator() && isEnabled();
  810. }
  811. }
  812. /**
  813. * @author Jouni Koivuviita / Vaadin Ltd.
  814. */
  815. public void iLayout() {
  816. iLayout(false);
  817. updateSize();
  818. }
  819. public void iLayout(boolean iconLoadEvent) {
  820. // Only collapse if there is more than one item in the root menu and the
  821. // menu has an explicit size
  822. if ((getItems().size() > 1 || (collapsedRootItems != null && collapsedRootItems
  823. .getItems().size() > 0))
  824. && getElement().getStyle().getProperty("width") != null
  825. && moreItem != null) {
  826. // Measure the width of the "more" item
  827. final boolean morePresent = getItems().contains(moreItem);
  828. addItem(moreItem);
  829. final int moreItemWidth = moreItem.getOffsetWidth();
  830. if (!morePresent) {
  831. removeItem(moreItem);
  832. }
  833. int availableWidth = LayoutManager.get(client).getInnerWidth(
  834. getElement());
  835. // Used width includes the "more" item if present
  836. int usedWidth = getConsumedWidth();
  837. int diff = availableWidth - usedWidth;
  838. removeItem(moreItem);
  839. if (diff < 0) {
  840. // Too many items: collapse last items from root menu
  841. int widthNeeded = usedWidth - availableWidth;
  842. if (!morePresent) {
  843. widthNeeded += moreItemWidth;
  844. }
  845. int widthReduced = 0;
  846. while (widthReduced < widthNeeded && getItems().size() > 0) {
  847. // Move last root menu item to collapsed menu
  848. CustomMenuItem collapse = getItems().get(
  849. getItems().size() - 1);
  850. widthReduced += collapse.getOffsetWidth();
  851. removeItem(collapse);
  852. collapsedRootItems.addItem(collapse, 0);
  853. }
  854. } else if (collapsedRootItems.getItems().size() > 0) {
  855. // Space available for items: expand first items from collapsed
  856. // menu
  857. int widthAvailable = diff + moreItemWidth;
  858. int widthGrowth = 0;
  859. while (widthAvailable > widthGrowth
  860. && collapsedRootItems.getItems().size() > 0) {
  861. // Move first item from collapsed menu to the root menu
  862. CustomMenuItem expand = collapsedRootItems.getItems()
  863. .get(0);
  864. collapsedRootItems.removeItem(expand);
  865. addItem(expand);
  866. widthGrowth += expand.getOffsetWidth();
  867. if (collapsedRootItems.getItems().size() > 0) {
  868. widthAvailable -= moreItemWidth;
  869. }
  870. if (widthGrowth > widthAvailable) {
  871. removeItem(expand);
  872. collapsedRootItems.addItem(expand, 0);
  873. } else {
  874. widthAvailable = diff + moreItemWidth;
  875. }
  876. }
  877. }
  878. if (collapsedRootItems.getItems().size() > 0) {
  879. addItem(moreItem);
  880. }
  881. }
  882. // If a popup is open we might need to adjust the shadow as well if an
  883. // icon shown in that popup was loaded
  884. if (popup != null) {
  885. // Forces a recalculation of the shadow size
  886. popup.show();
  887. }
  888. if (iconLoadEvent) {
  889. // Size have changed if the width is undefined
  890. Util.notifyParentOfSizeChange(this, false);
  891. }
  892. }
  893. private int getConsumedWidth() {
  894. int w = 0;
  895. for (CustomMenuItem item : getItems()) {
  896. if (!collapsedRootItems.getItems().contains(item)) {
  897. w += item.getOffsetWidth();
  898. }
  899. }
  900. return w;
  901. }
  902. /*
  903. * (non-Javadoc)
  904. *
  905. * @see
  906. * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google
  907. * .gwt.event.dom.client.KeyPressEvent)
  908. */
  909. public void onKeyPress(KeyPressEvent event) {
  910. if (handleNavigation(event.getNativeEvent().getKeyCode(),
  911. event.isControlKeyDown() || event.isMetaKeyDown(),
  912. event.isShiftKeyDown())) {
  913. event.preventDefault();
  914. }
  915. }
  916. /*
  917. * (non-Javadoc)
  918. *
  919. * @see
  920. * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt
  921. * .event.dom.client.KeyDownEvent)
  922. */
  923. public void onKeyDown(KeyDownEvent event) {
  924. if (handleNavigation(event.getNativeEvent().getKeyCode(),
  925. event.isControlKeyDown() || event.isMetaKeyDown(),
  926. event.isShiftKeyDown())) {
  927. event.preventDefault();
  928. }
  929. }
  930. /**
  931. * Get the key that moves the selection upwards. By default it is the up
  932. * arrow key but by overriding this you can change the key to whatever you
  933. * want.
  934. *
  935. * @return The keycode of the key
  936. */
  937. protected int getNavigationUpKey() {
  938. return KeyCodes.KEY_UP;
  939. }
  940. /**
  941. * Get the key that moves the selection downwards. By default it is the down
  942. * arrow key but by overriding this you can change the key to whatever you
  943. * want.
  944. *
  945. * @return The keycode of the key
  946. */
  947. protected int getNavigationDownKey() {
  948. return KeyCodes.KEY_DOWN;
  949. }
  950. /**
  951. * Get the key that moves the selection left. By default it is the left
  952. * arrow key but by overriding this you can change the key to whatever you
  953. * want.
  954. *
  955. * @return The keycode of the key
  956. */
  957. protected int getNavigationLeftKey() {
  958. return KeyCodes.KEY_LEFT;
  959. }
  960. /**
  961. * Get the key that moves the selection right. By default it is the right
  962. * arrow key but by overriding this you can change the key to whatever you
  963. * want.
  964. *
  965. * @return The keycode of the key
  966. */
  967. protected int getNavigationRightKey() {
  968. return KeyCodes.KEY_RIGHT;
  969. }
  970. /**
  971. * Get the key that selects a menu item. By default it is the Enter key but
  972. * by overriding this you can change the key to whatever you want.
  973. *
  974. * @return
  975. */
  976. protected int getNavigationSelectKey() {
  977. return KeyCodes.KEY_ENTER;
  978. }
  979. /**
  980. * Get the key that closes the menu. By default it is the escape key but by
  981. * overriding this yoy can change the key to whatever you want.
  982. *
  983. * @return
  984. */
  985. protected int getCloseMenuKey() {
  986. return KeyCodes.KEY_ESCAPE;
  987. }
  988. /**
  989. * Handles the keyboard events handled by the MenuBar
  990. *
  991. * @param event
  992. * The keyboard event received
  993. * @return true iff the navigation event was handled
  994. */
  995. public boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
  996. // If tab or shift+tab close menus
  997. if (keycode == KeyCodes.KEY_TAB) {
  998. setSelected(null);
  999. hideChildren();
  1000. menuVisible = false;
  1001. return false;
  1002. }
  1003. if (ctrl || shift || !isEnabled()) {
  1004. // Do not handle tab key, nor ctrl keys
  1005. return false;
  1006. }
  1007. if (keycode == getNavigationLeftKey()) {
  1008. if (getSelected() == null) {
  1009. // If nothing is selected then select the last item
  1010. setSelected(items.get(items.size() - 1));
  1011. if (!getSelected().isSelectable()) {
  1012. handleNavigation(keycode, ctrl, shift);
  1013. }
  1014. } else if (visibleChildMenu == null && getParentMenu() == null) {
  1015. // If this is the root menu then move to the left
  1016. int idx = items.indexOf(getSelected());
  1017. if (idx > 0) {
  1018. setSelected(items.get(idx - 1));
  1019. } else {
  1020. setSelected(items.get(items.size() - 1));
  1021. }
  1022. if (!getSelected().isSelectable()) {
  1023. handleNavigation(keycode, ctrl, shift);
  1024. }
  1025. } else if (visibleChildMenu != null) {
  1026. // Redirect all navigation to the submenu
  1027. visibleChildMenu.handleNavigation(keycode, ctrl, shift);
  1028. } else if (getParentMenu().getParentMenu() == null) {
  1029. // Inside a sub menu, whose parent is a root menu item
  1030. VMenuBar root = getParentMenu();
  1031. root.getSelected().getSubMenu().setSelected(null);
  1032. root.hideChildren();
  1033. // Get the root menus items and select the previous one
  1034. int idx = root.getItems().indexOf(root.getSelected());
  1035. idx = idx > 0 ? idx : root.getItems().size();
  1036. CustomMenuItem selected = root.getItems().get(--idx);
  1037. while (selected.isSeparator() || !selected.isEnabled()) {
  1038. idx = idx > 0 ? idx : root.getItems().size();
  1039. selected = root.getItems().get(--idx);
  1040. }
  1041. root.setSelected(selected);
  1042. openMenuAndFocusFirstIfPossible(selected);
  1043. } else {
  1044. getParentMenu().getSelected().getSubMenu().setSelected(null);
  1045. getParentMenu().hideChildren();
  1046. }
  1047. return true;
  1048. } else if (keycode == getNavigationRightKey()) {
  1049. if (getSelected() == null) {
  1050. // If nothing is selected then select the first item
  1051. setSelected(items.get(0));
  1052. if (!getSelected().isSelectable()) {
  1053. handleNavigation(keycode, ctrl, shift);
  1054. }
  1055. } else if (visibleChildMenu == null && getParentMenu() == null) {
  1056. // If this is the root menu then move to the right
  1057. int idx = items.indexOf(getSelected());
  1058. if (idx < items.size() - 1) {
  1059. setSelected(items.get(idx + 1));
  1060. } else {
  1061. setSelected(items.get(0));
  1062. }
  1063. if (!getSelected().isSelectable()) {
  1064. handleNavigation(keycode, ctrl, shift);
  1065. }
  1066. } else if (visibleChildMenu == null
  1067. && getSelected().getSubMenu() != null) {
  1068. // If the item has a submenu then show it and move the selection
  1069. // there
  1070. showChildMenu(getSelected());
  1071. menuVisible = true;
  1072. visibleChildMenu.handleNavigation(keycode, ctrl, shift);
  1073. } else if (visibleChildMenu == null) {
  1074. // Get the root menu
  1075. VMenuBar root = getParentMenu();
  1076. while (root.getParentMenu() != null) {
  1077. root = root.getParentMenu();
  1078. }
  1079. // Hide the submenu
  1080. root.hideChildren();
  1081. // Get the root menus items and select the next one
  1082. int idx = root.getItems().indexOf(root.getSelected());
  1083. idx = idx < root.getItems().size() - 1 ? idx : -1;
  1084. CustomMenuItem selected = root.getItems().get(++idx);
  1085. while (selected.isSeparator() || !selected.isEnabled()) {
  1086. idx = idx < root.getItems().size() - 1 ? idx : -1;
  1087. selected = root.getItems().get(++idx);
  1088. }
  1089. root.setSelected(selected);
  1090. openMenuAndFocusFirstIfPossible(selected);
  1091. } else if (visibleChildMenu != null) {
  1092. // Redirect all navigation to the submenu
  1093. visibleChildMenu.handleNavigation(keycode, ctrl, shift);
  1094. }
  1095. return true;
  1096. } else if (keycode == getNavigationUpKey()) {
  1097. if (getSelected() == null) {
  1098. // If nothing is selected then select the last item
  1099. setSelected(items.get(items.size() - 1));
  1100. if (!getSelected().isSelectable()) {
  1101. handleNavigation(keycode, ctrl, shift);
  1102. }
  1103. } else if (visibleChildMenu != null) {
  1104. // Redirect all navigation to the submenu
  1105. visibleChildMenu.handleNavigation(keycode, ctrl, shift);
  1106. } else {
  1107. // Select the previous item if possible or loop to the last item
  1108. int idx = items.indexOf(getSelected());
  1109. if (idx > 0) {
  1110. setSelected(items.get(idx - 1));
  1111. } else {
  1112. setSelected(items.get(items.size() - 1));
  1113. }
  1114. if (!getSelected().isSelectable()) {
  1115. handleNavigation(keycode, ctrl, shift);
  1116. }
  1117. }
  1118. return true;
  1119. } else if (keycode == getNavigationDownKey()) {
  1120. if (getSelected() == null) {
  1121. // If nothing is selected then select the first item
  1122. selectFirstItem();
  1123. } else if (visibleChildMenu == null && getParentMenu() == null) {
  1124. // If this is the root menu the show the child menu with arrow
  1125. // down, if there is a child menu
  1126. openMenuAndFocusFirstIfPossible(getSelected());
  1127. } else if (visibleChildMenu != null) {
  1128. // Redirect all navigation to the submenu
  1129. visibleChildMenu.handleNavigation(keycode, ctrl, shift);
  1130. } else {
  1131. // Select the next item if possible or loop to the first item
  1132. int idx = items.indexOf(getSelected());
  1133. if (idx < items.size() - 1) {
  1134. setSelected(items.get(idx + 1));
  1135. } else {
  1136. setSelected(items.get(0));
  1137. }
  1138. if (!getSelected().isSelectable()) {
  1139. handleNavigation(keycode, ctrl, shift);
  1140. }
  1141. }
  1142. return true;
  1143. } else if (keycode == getCloseMenuKey()) {
  1144. setSelected(null);
  1145. hideChildren();
  1146. menuVisible = false;
  1147. } else if (keycode == getNavigationSelectKey()) {
  1148. if (getSelected() == null) {
  1149. // If nothing is selected then select the first item
  1150. selectFirstItem();
  1151. } else if (visibleChildMenu != null) {
  1152. // Redirect all navigation to the submenu
  1153. visibleChildMenu.handleNavigation(keycode, ctrl, shift);
  1154. menuVisible = false;
  1155. } else if (visibleChildMenu == null
  1156. && getSelected().getSubMenu() != null) {
  1157. // If the item has a sub menu then show it and move the
  1158. // selection there
  1159. openMenuAndFocusFirstIfPossible(getSelected());
  1160. } else {
  1161. Command command = getSelected().getCommand();
  1162. if (command != null) {
  1163. command.execute();
  1164. }
  1165. setSelected(null);
  1166. hideParents(true);
  1167. }
  1168. }
  1169. return false;
  1170. }
  1171. private void selectFirstItem() {
  1172. for (int i = 0; i < items.size(); i++) {
  1173. CustomMenuItem item = items.get(i);
  1174. if (item.isSelectable()) {
  1175. setSelected(item);
  1176. break;
  1177. }
  1178. }
  1179. }
  1180. private void openMenuAndFocusFirstIfPossible(CustomMenuItem menuItem) {
  1181. VMenuBar subMenu = menuItem.getSubMenu();
  1182. if (subMenu == null) {
  1183. // No child menu? Nothing to do
  1184. return;
  1185. }
  1186. VMenuBar parentMenu = menuItem.getParentMenu();
  1187. parentMenu.showChildMenu(menuItem);
  1188. menuVisible = true;
  1189. // Select the first item in the newly open submenu
  1190. subMenu.selectFirstItem();
  1191. }
  1192. /*
  1193. * (non-Javadoc)
  1194. *
  1195. * @see
  1196. * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
  1197. * .dom.client.FocusEvent)
  1198. */
  1199. public void onFocus(FocusEvent event) {
  1200. }
  1201. private final String SUBPART_PREFIX = "item";
  1202. public Element getSubPartElement(String subPart) {
  1203. int index = Integer
  1204. .parseInt(subPart.substring(SUBPART_PREFIX.length()));
  1205. CustomMenuItem item = getItems().get(index);
  1206. return item.getElement();
  1207. }
  1208. public String getSubPartName(Element subElement) {
  1209. if (!getElement().isOrHasChild(subElement)) {
  1210. return null;
  1211. }
  1212. Element menuItemRoot = subElement;
  1213. while (menuItemRoot != null && menuItemRoot.getParentElement() != null
  1214. && menuItemRoot.getParentElement() != getElement()) {
  1215. menuItemRoot = menuItemRoot.getParentElement().cast();
  1216. }
  1217. // "menuItemRoot" is now the root of the menu item
  1218. final int itemCount = getItems().size();
  1219. for (int i = 0; i < itemCount; i++) {
  1220. if (getItems().get(i).getElement() == menuItemRoot) {
  1221. String name = SUBPART_PREFIX + i;
  1222. return name;
  1223. }
  1224. }
  1225. return null;
  1226. }
  1227. /**
  1228. * Get menu item with given DOM element
  1229. *
  1230. * @param element
  1231. * Element used in search
  1232. * @return Menu item or null if not found
  1233. */
  1234. public CustomMenuItem getMenuItemWithElement(Element element) {
  1235. for (int i = 0; i < items.size(); i++) {
  1236. CustomMenuItem item = items.get(i);
  1237. if (DOM.isOrHasChild(item.getElement(), element)) {
  1238. return item;
  1239. }
  1240. if (item.getSubMenu() != null) {
  1241. item = item.getSubMenu().getMenuItemWithElement(element);
  1242. if (item != null) {
  1243. return item;
  1244. }
  1245. }
  1246. }
  1247. return null;
  1248. }
  1249. }