1 /*
   2  * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control;
  27 
  28 import javafx.beans.property.ObjectProperty;
  29 import javafx.beans.property.ObjectPropertyBase;
  30 import javafx.collections.ListChangeListener.Change;
  31 import javafx.collections.ObservableList;
  32 import javafx.event.Event;
  33 import javafx.event.EventHandler;
  34 import javafx.event.EventType;
  35 import javafx.scene.Node;
  36 
  37 import com.sun.javafx.collections.TrackableObservableList;
  38 import com.sun.javafx.scene.control.Logging;
  39 import javafx.beans.DefaultProperty;
  40 import javafx.beans.property.ReadOnlyBooleanProperty;
  41 import javafx.beans.property.ReadOnlyBooleanWrapper;
  42 import javafx.event.EventDispatchChain;
  43 
  44 /**
  45  * <p>
  46  * A popup menu of actionable items which is displayed to the user only upon request.
  47  * When a menu is visible, in most use cases, the user can select one menu item
  48  * before the menu goes back to its hidden state. This means the menu is a good
  49  * place to put important functionality that does not necessarily need to be
  50  * visible at all times to the user.
  51  * <p>
  52  * Menus are typically placed in a {@link MenuBar}, or as a submenu of another Menu.
  53  * If the intention is to offer a context menu when the user right-clicks in a
  54  * certain area of their user interface, then this is the wrong control to use.
  55  * This is because when Menu is added to the scenegraph, it has a visual
  56  * representation that will result in it appearing on screen. Instead,
  57  * {@link ContextMenu} should be used in this circumstance.
  58  * <p>
  59  * Creating a Menu and inserting it into a MenuBar is easy, as shown below:
  60  * <pre><code>
  61  * final Menu menu1 = new Menu("File");
  62  * MenuBar menuBar = new MenuBar();
  63  * menuBar.getMenus().add(menu1);
  64  * </code></pre>
  65  * <p>
  66  * A Menu is a subclass of {@link MenuItem} which means that it can be inserted
  67  * into a Menu's {@link #items} ObservableList, resulting in a submenu being created:
  68  * <pre><code>
  69  * MenuItem menu12 = new MenuItem("Open");
  70  * menu1.getItems().add(menu12);
  71  * </code></pre>
  72  * <p>
  73  * The items ObservableList allows for any {@link MenuItem} type to be inserted,
  74  * including its subclasses {@link Menu}, {@link MenuItem}, {@link RadioMenuItem}, {@link CheckMenuItem},
  75  * {@link CustomMenuItem} and {@link SeparatorMenuItem}. In order to insert an arbitrary {@link Node} to
  76  * a Menu, a CustomMenuItem can be used. One exception to this general rule is that
  77  * {@link SeparatorMenuItem} could be used for inserting a separator.
  78  *
  79  * @see MenuBar
  80  * @see MenuItem
  81  * @since JavaFX 2.0
  82  */
  83 @DefaultProperty("items")
  84 public class Menu extends MenuItem {
  85 
  86     /**
  87      * <p>Called when the contextMenu for this menu <b>will</b> be shown. However if the
  88      * contextMenu is empty then this will not be called.
  89      * </p>
  90      */
  91     public static final EventType<Event> ON_SHOWING =
  92             new EventType<Event>(Event.ANY, "MENU_ON_SHOWING");
  93 
  94     /**
  95      * <p>Called when the contextMenu for this menu shows. However if the
  96      * contextMenu is empty then this will not be called.
  97      * </p>
  98      */
  99     public static final EventType<Event> ON_SHOWN =
 100             new EventType<Event>(Event.ANY, "MENU_ON_SHOWN");
 101 
 102     /**
 103      * <p>Called when the contextMenu for this menu <b>will</b> be hidden. However if the
 104      * contextMenu is empty then this will not be called.
 105      * </p>
 106      */
 107     public static final EventType<Event> ON_HIDING =
 108             new EventType<Event>(Event.ANY, "MENU_ON_HIDING");
 109 
 110     /**
 111      * <p>Called when the contextMenu for this menu is hidden. However if the
 112      * contextMenu is empty then this will not be called.
 113      * </p>
 114      */
 115     public static final EventType<Event> ON_HIDDEN =
 116             new EventType<Event>(Event.ANY, "MENU_ON_HIDDEN");
 117 
 118     /***************************************************************************
 119      *                                                                         *
 120      * Constructors                                                            *
 121      *                                                                         *
 122      **************************************************************************/
 123 
 124     /**
 125      * Constructs a Menu with an empty string for its display text.
 126      * @since JavaFX 2.2
 127      */
 128     public Menu() {
 129         this("");
 130     }
 131 
 132     /**
 133      * Constructs a Menu and sets the display text with the specified text.
 134      *
 135      * @param text the text to display on the menu button
 136      */
 137     public Menu(String text) {
 138         this(text,null);
 139     }
 140 
 141     /**
 142      * Constructs a Menu and sets the display text with the specified text
 143      * and sets the graphic {@link Node} to the given node.
 144      *
 145      * @param text the text to display on the menu button
 146      * @param graphic the graphic to display on the menu button
 147      */
 148     public Menu(String text, Node graphic) {
 149         this(text, graphic, (MenuItem[])null);
 150     }
 151 
 152     /**
 153      * Constructs a Menu and sets the display text with the specified text,
 154      * the graphic {@link Node} to the given node, and inserts the given items
 155      * into the {@link #getItems() items} list.
 156      *
 157      * @param text the text to display on the menu button
 158      * @param graphic the graphic to display on the menu button
 159      * @param items The items to display in the popup menu.
 160      * @since JavaFX 8u40
 161      */
 162     public Menu(String text, Node graphic, MenuItem... items) {
 163         super(text,graphic);
 164         getStyleClass().add(DEFAULT_STYLE_CLASS);
 165 
 166         if (items != null) {
 167             getItems().addAll(items);
 168         }
 169 
 170         parentPopupProperty().addListener(observable -> {
 171             for (int i = 0; i < getItems().size(); i++) {
 172                 MenuItem item = getItems().get(i);
 173                 item.setParentPopup(getParentPopup());
 174             }
 175         });
 176     }
 177 
 178 
 179 
 180      /***************************************************************************
 181      *                                                                         *
 182      * Properties                                                              *
 183      *                                                                         *
 184      **************************************************************************/
 185 
 186     /**
 187      * Indicates whether the {@link ContextMenu} is currently visible.
 188      *
 189      * @defaultValue false
 190      */
 191     private ReadOnlyBooleanWrapper showing;
 192 
 193     private void setShowing(boolean value) {
 194         if (getItems().size() == 0 || (value && isShowing())) return;
 195 
 196         // these events will not fire if the showing property is bound
 197         if (value) {
 198            if (getOnMenuValidation() != null) {
 199                 Event.fireEvent(this, new Event(MENU_VALIDATION_EVENT));
 200                 for(MenuItem m : getItems()) {
 201                     if (!(m instanceof Menu) && m.getOnMenuValidation() != null) {
 202                         Event.fireEvent(m, new Event(MenuItem.MENU_VALIDATION_EVENT));
 203                     }
 204                 }
 205            }
 206            Event.fireEvent(this, new Event(Menu.ON_SHOWING));
 207         } else {
 208            Event.fireEvent(this, new Event(Menu.ON_HIDING));
 209         }
 210         showingPropertyImpl().set(value);
 211         Event.fireEvent(this, (value) ? new Event(Menu.ON_SHOWN) :
 212             new Event(Menu.ON_HIDDEN));
 213     }
 214 
 215     public final boolean isShowing() {
 216         return showing == null ? false : showing.get();
 217     }
 218 
 219     public final ReadOnlyBooleanProperty showingProperty() {
 220         return showingPropertyImpl().getReadOnlyProperty();
 221     }
 222 
 223     private ReadOnlyBooleanWrapper showingPropertyImpl() {
 224         if (showing == null) {
 225             showing = new ReadOnlyBooleanWrapper() {
 226                 @Override protected void invalidated() {
 227                     // force validation
 228                     get();
 229 
 230                     // update the styleclass
 231                     if (isShowing()) {
 232                         getStyleClass().add(STYLE_CLASS_SHOWING);
 233                     } else {
 234                         getStyleClass().remove(STYLE_CLASS_SHOWING);
 235                     }
 236                 }
 237 
 238                 @Override
 239                 public Object getBean() {
 240                     return Menu.this;
 241                 }
 242 
 243                 @Override
 244                 public String getName() {
 245                     return "showing";
 246                 }
 247             };
 248         }
 249         return showing;
 250     }
 251 
 252 
 253     // --- On Showing
 254     /**
 255      * Called just prior to the {@code ContextMenu} being shown, even if the menu has
 256      * no items to show. Note however that this won't be called if the menu does
 257      * not have a valid anchor node.
 258      * @return the on showing property
 259      */
 260     public final ObjectProperty<EventHandler<Event>> onShowingProperty() { return onShowing; }
 261     public final void setOnShowing(EventHandler<Event> value) { onShowingProperty().set(value); }
 262     public final EventHandler<Event> getOnShowing() { return onShowingProperty().get(); }
 263     private ObjectProperty<EventHandler<Event>> onShowing = new ObjectPropertyBase<EventHandler<Event>>() {
 264         @Override protected void invalidated() {
 265             eventHandlerManager.setEventHandler(ON_SHOWING, get());
 266          }
 267 
 268          @Override
 269          public Object getBean() {
 270              return Menu.this;
 271          }
 272 
 273          @Override
 274          public String getName() {
 275              return "onShowing";
 276          }
 277      };
 278 
 279 
 280     // -- On Shown
 281     /**
 282      * Called just after the {@link ContextMenu} is shown.
 283      * @return the on shown property
 284      */
 285     public final ObjectProperty<EventHandler<Event>> onShownProperty() { return onShown; }
 286     public final void setOnShown(EventHandler<Event> value) { onShownProperty().set(value); }
 287     public final EventHandler<Event> getOnShown() { return onShownProperty().get(); }
 288     private ObjectProperty<EventHandler<Event>> onShown = new ObjectPropertyBase<EventHandler<Event>>() {
 289         @Override protected void invalidated() {
 290             eventHandlerManager.setEventHandler(ON_SHOWN, get());
 291         }
 292 
 293         @Override
 294         public Object getBean() {
 295             return Menu.this;
 296         }
 297 
 298         @Override
 299         public String getName() {
 300             return "onShown";
 301         }
 302     };
 303 
 304 
 305     // --- On Hiding
 306     /**
 307      * Called just prior to the {@link ContextMenu} being hidden.
 308      * @return the on hiding property
 309      */
 310     public final ObjectProperty<EventHandler<Event>> onHidingProperty() { return onHiding; }
 311     public final void setOnHiding(EventHandler<Event> value) { onHidingProperty().set(value); }
 312     public final EventHandler<Event> getOnHiding() { return onHidingProperty().get(); }
 313     private ObjectProperty<EventHandler<Event>> onHiding = new ObjectPropertyBase<EventHandler<Event>>() {
 314         @Override protected void invalidated() {
 315             eventHandlerManager.setEventHandler(ON_HIDING, get());
 316         }
 317 
 318         @Override
 319         public Object getBean() {
 320             return Menu.this;
 321         }
 322 
 323         @Override
 324         public String getName() {
 325             return "onHiding";
 326         }
 327     };
 328 
 329 
 330     // --- On Hidden
 331     /**
 332      * Called just after the {@link ContextMenu} has been hidden.
 333      * @return the on hidden property
 334      */
 335     public final ObjectProperty<EventHandler<Event>> onHiddenProperty() { return onHidden; }
 336     public final void setOnHidden(EventHandler<Event> value) { onHiddenProperty().set(value); }
 337     public final EventHandler<Event> getOnHidden() { return onHiddenProperty().get(); }
 338     private ObjectProperty<EventHandler<Event>> onHidden = new ObjectPropertyBase<EventHandler<Event>>() {
 339         @Override protected void invalidated() {
 340             eventHandlerManager.setEventHandler(ON_HIDDEN, get());
 341         }
 342 
 343         @Override
 344         public Object getBean() {
 345             return Menu.this;
 346         }
 347 
 348         @Override
 349         public String getName() {
 350             return "onHidden";
 351         }
 352     };
 353 
 354 
 355 
 356     /***************************************************************************
 357      *                                                                         *
 358      * Instance variables                                                      *
 359      *                                                                         *
 360      **************************************************************************/
 361 
 362     private final ObservableList<MenuItem> items = new TrackableObservableList<MenuItem>() {
 363         @Override protected void onChanged(Change<MenuItem> c) {
 364             while (c.next()) {
 365                 // remove the parent menu from all menu items that have been removed
 366                 for (MenuItem item : c.getRemoved()) {
 367                     item.setParentMenu(null);
 368                     item.setParentPopup(null);
 369                 }
 370 
 371                 // set the parent menu to be this menu for all added menu items
 372                 for (MenuItem item : c.getAddedSubList()) {
 373                     if (item.getParentMenu() != null) {
 374                         Logging.getControlsLogger().warning("Adding MenuItem " +
 375                                 item.getText() + " that has already been added to "
 376                                 + item.getParentMenu().getText());
 377                         item.getParentMenu().getItems().remove(item);
 378                     }
 379 
 380                     item.setParentMenu(Menu.this);
 381                     item.setParentPopup(getParentPopup());
 382                 }
 383             }
 384             if (getItems().size() == 0 && isShowing()) {
 385                 showingPropertyImpl().set(false);
 386             }
 387         }
 388     };
 389 
 390 
 391 
 392     /***************************************************************************
 393      *                                                                         *
 394      * Public API                                                              *
 395      *                                                                         *
 396      **************************************************************************/
 397 
 398     /**
 399      * The items to show within this menu. If this ObservableList is modified at
 400      * runtime, the Menu will update as expected.
 401      * @return the list of items
 402      */
 403     public final ObservableList<MenuItem> getItems() {
 404         return items;
 405     }
 406 
 407     /**
 408      * If the Menu is not disabled and the {@link ContextMenu} is not already showing,
 409      * then this will cause the {@link ContextMenu} to be shown.
 410      */
 411     public void show() {
 412         if (isDisable()) return;
 413         setShowing(true);
 414     }
 415 
 416     /**
 417      * Hides the {@link ContextMenu} if it was previously showing, and any showing
 418      * submenus. If this menu is not showing, then invoking this function
 419      * has no effect.
 420      */
 421     public void hide() {
 422         if (!isShowing()) return;
 423         // hide all sub menus
 424         for (MenuItem i : getItems()) {
 425             if (i instanceof Menu) {
 426                 final Menu m = (Menu) i;
 427                 m.hide();
 428             }
 429         }
 430         setShowing(false);
 431     }
 432 
 433     /** {@inheritDoc} */
 434     @Override public <E extends Event> void addEventHandler(EventType<E> eventType, EventHandler<E> eventHandler) {
 435         eventHandlerManager.addEventHandler(eventType, eventHandler);
 436     }
 437 
 438     /** {@inheritDoc} */
 439     @Override public <E extends Event> void removeEventHandler(EventType<E> eventType, EventHandler<E> eventHandler) {
 440         eventHandlerManager.removeEventHandler(eventType, eventHandler);
 441     }
 442 
 443      /** {@inheritDoc} */
 444     @Override public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
 445         return tail.prepend(eventHandlerManager);
 446     }
 447 
 448     /***************************************************************************
 449      *                                                                         *
 450      * Stylesheet Handling                                                     *
 451      *                                                                         *
 452      **************************************************************************/
 453 
 454     private static final String DEFAULT_STYLE_CLASS = "menu";
 455     private static final String STYLE_CLASS_SHOWING = "showing";
 456 }