1 /*
   2  * Copyright (c) 2010, 2016, 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 com.sun.javafx.beans.IDProperty;
  29 import javafx.collections.ObservableSet;
  30 import javafx.css.PseudoClass;
  31 import javafx.css.Styleable;
  32 import javafx.css.CssMetaData;
  33 import javafx.beans.property.BooleanProperty;
  34 import javafx.beans.property.ObjectProperty;
  35 import javafx.beans.property.ObjectPropertyBase;
  36 import javafx.beans.property.SimpleBooleanProperty;
  37 import javafx.beans.property.SimpleObjectProperty;
  38 import javafx.beans.property.SimpleStringProperty;
  39 import javafx.beans.property.StringProperty;
  40 import javafx.collections.FXCollections;
  41 import javafx.collections.ObservableList;
  42 import javafx.event.ActionEvent;
  43 import javafx.event.Event;
  44 import javafx.event.EventDispatchChain;
  45 import javafx.event.EventHandler;
  46 import javafx.event.EventTarget;
  47 import javafx.event.EventType;
  48 import javafx.scene.Node;
  49 import javafx.scene.input.KeyCombination;
  50 
  51 import com.sun.javafx.event.EventHandlerManager;
  52 import com.sun.javafx.scene.control.ContextMenuContent;
  53 import javafx.scene.control.skin.ContextMenuSkin;
  54 import java.util.Collections;
  55 import java.util.HashMap;
  56 import java.util.List;
  57 
  58 import javafx.beans.property.ReadOnlyObjectProperty;
  59 import javafx.beans.property.ReadOnlyObjectWrapper;
  60 import javafx.collections.ObservableMap;
  61 import javafx.scene.Parent;
  62 
  63 /**
  64  * <p>
  65  * MenuItem is intended to be used in conjunction with {@link Menu} to provide
  66  * options to users. MenuItem serves as the base class for the bulk of JavaFX menus
  67  * API.
  68  * It has a display {@link #getText() text} property, as well as an optional {@link #getGraphic() graphic} node
  69  * that can be set on it.
  70  * The {@link #getAccelerator() accelerator} property enables accessing the
  71  * associated action in one keystroke. Also, as with the {@link Button} control,
  72  * by using the {@link #setOnAction} method, you can have an instance of MenuItem
  73  * perform any action you wish.
  74  * <p>
  75  * <b>Note:</b> Whilst any size of graphic can be inserted into a MenuItem, the most
  76  * commonly used size in most applications is 16x16 pixels. This is
  77  * the recommended graphic dimension to use if you're using the default style provided by
  78  * JavaFX.
  79  * <p>
  80  * To create a MenuItem is simple:
  81 <pre><code>
  82 MenuItem menuItem = new MenuItem("Open");
  83 menuItem.setOnAction(new EventHandler&lt;ActionEvent&gt;() {
  84     @Override public void handle(ActionEvent e) {
  85         System.out.println("Opening Database Connection...");
  86     }
  87 });
  88 menuItem.setGraphic(new ImageView(new Image("flower.png")));
  89 </code></pre>
  90  * <p>
  91  * Refer to the {@link Menu} page to learn how to insert MenuItem into a menu
  92  * instance. Briefly however, you can insert the MenuItem from the previous
  93  * example into a Menu as such:
  94 <pre><code>
  95 final Menu menu = new Menu("File");
  96 menu.getItems().add(menuItem);
  97 </code></pre>
  98  *
  99  * @see Menu
 100  * @since JavaFX 2.0
 101  */
 102 @IDProperty("id")
 103 public class MenuItem implements EventTarget, Styleable {
 104 
 105     /***************************************************************************
 106      *                                                                         *
 107      * Constructors                                                            *
 108      *                                                                         *
 109      **************************************************************************/
 110 
 111     /**
 112      * Constructs a MenuItem with no display text.
 113      */
 114     public MenuItem() {
 115         this(null,null);
 116     }
 117 
 118     /**
 119      * Constructs a MenuItem and sets the display text with the specified text
 120      * @param text the display text
 121      * @see #setText
 122      */
 123     public MenuItem(String text) {
 124         this(text,null);
 125     }
 126 
 127     /**
 128      * Constructor s MenuItem and sets the display text with the specified text
 129      * and sets the graphic {@link Node} to the given node.
 130      * @param text the display text
 131      * @param graphic the graphic node
 132      * @see #setText
 133      * @see #setGraphic
 134      */
 135     public MenuItem(String text, Node graphic) {
 136         setText(text);
 137         setGraphic(graphic);
 138         styleClass.add(DEFAULT_STYLE_CLASS);
 139     }
 140 
 141 
 142 
 143     /***************************************************************************
 144      *                                                                         *
 145      * Instance Variables                                                      *
 146      *                                                                         *
 147      **************************************************************************/
 148 
 149     private final ObservableList<String> styleClass = FXCollections.observableArrayList();
 150 
 151     final EventHandlerManager eventHandlerManager =
 152             new EventHandlerManager(this);
 153 
 154     private Object userData;
 155     private ObservableMap<Object, Object> properties;
 156 
 157     /***************************************************************************
 158      *                                                                         *
 159      * Properties                                                              *
 160      *                                                                         *
 161      **************************************************************************/
 162 
 163     /**
 164      * The id of this MenuItem. This simple string identifier is useful for finding
 165      * a specific MenuItem within the scene graph.
 166      */
 167     private StringProperty id;
 168     public final void setId(String value) { idProperty().set(value); }
 169     @Override public final String getId() { return id == null ? null : id.get(); }
 170     public final StringProperty idProperty() {
 171         if (id == null) {
 172             id = new SimpleStringProperty(this, "id");
 173         }
 174         return id;
 175     }
 176 
 177     /**
 178      * A string representation of the CSS style associated with this specific MenuItem.
 179      * This is analogous to the "style" attribute of an HTML element. Note that,
 180      * like the HTML style attribute, this variable contains style properties and
 181      * values and not the selector portion of a style rule.
 182      */
 183     private StringProperty style;
 184     public final void setStyle(String value) { styleProperty().set(value); }
 185     @Override public final String getStyle() { return style == null ? null : style.get(); }
 186     public final StringProperty styleProperty() {
 187         if (style == null) {
 188             style = new SimpleStringProperty(this, "style");
 189         }
 190         return style;
 191     }
 192 
 193     // --- Parent Menu (useful for submenus)
 194     /**
 195      * This is the {@link Menu} in which this {@code MenuItem} exists. It is
 196      * possible for an instance of this class to not have a {@code parentMenu} -
 197      * this means that this instance is either:
 198      * <ul>
 199      * <li>Not yet associated with its {@code parentMenu}.
 200      * <li>A 'root' {@link Menu} (i.e. it is a context menu, attached directly to a
 201      * {@link MenuBar}, {@link MenuButton}, or any of the other controls that use
 202      * {@link Menu} internally.
 203      * </ul>
 204      */
 205     private ReadOnlyObjectWrapper<Menu> parentMenu;
 206 
 207     protected final void setParentMenu(Menu value) {
 208         parentMenuPropertyImpl().set(value);
 209     }
 210 
 211     public final Menu getParentMenu() {
 212         return parentMenu == null ? null : parentMenu.get();
 213     }
 214 
 215     public final ReadOnlyObjectProperty<Menu> parentMenuProperty() {
 216         return parentMenuPropertyImpl().getReadOnlyProperty();
 217     }
 218 
 219     private ReadOnlyObjectWrapper<Menu> parentMenuPropertyImpl() {
 220         if (parentMenu == null) {
 221             parentMenu = new ReadOnlyObjectWrapper<Menu>(this, "parentMenu");
 222         }
 223         return parentMenu;
 224     }
 225 
 226 
 227     // --- Parent Popup
 228      /**
 229      * This is the {@link ContextMenu} in which this {@code MenuItem} exists.
 230      */
 231     private ReadOnlyObjectWrapper<ContextMenu> parentPopup;
 232 
 233     protected final void setParentPopup(ContextMenu value) {
 234         parentPopupPropertyImpl().set(value);
 235     }
 236 
 237     public final ContextMenu getParentPopup() {
 238         return parentPopup == null ? null : parentPopup.get();
 239     }
 240 
 241     public final ReadOnlyObjectProperty<ContextMenu> parentPopupProperty() {
 242         return parentPopupPropertyImpl().getReadOnlyProperty();
 243     }
 244 
 245     private ReadOnlyObjectWrapper<ContextMenu> parentPopupPropertyImpl() {
 246         if (parentPopup == null) {
 247             parentPopup = new ReadOnlyObjectWrapper<ContextMenu>(this, "parentPopup");
 248         }
 249         return parentPopup;
 250     }
 251 
 252 
 253     // --- Text
 254     /**
 255      * The text to display in the {@code MenuItem}.
 256      */
 257     private StringProperty text;
 258 
 259     public final void setText(String value) {
 260         textProperty().set(value);
 261     }
 262 
 263     public final String getText() {
 264         return text == null ? null : text.get();
 265     }
 266 
 267     public final StringProperty textProperty() {
 268         if (text == null) {
 269             text = new SimpleStringProperty(this, "text");
 270         }
 271         return text;
 272     }
 273 
 274 
 275     // --- Graphic
 276     /**
 277      * An optional graphic for the {@code MenuItem}. This will normally be
 278      * an {@link javafx.scene.image.ImageView} node, but there is no requirement for this to be
 279      * the case.
 280      */
 281     private ObjectProperty<Node> graphic;
 282 
 283     public final void setGraphic(Node value) {
 284         graphicProperty().set(value);
 285     }
 286 
 287     public final Node getGraphic() {
 288         return graphic == null ? null : graphic.get();
 289     }
 290 
 291     public final ObjectProperty<Node> graphicProperty() {
 292         if (graphic == null) {
 293             graphic = new SimpleObjectProperty<Node>(this, "graphic");
 294         }
 295         return graphic;
 296     }
 297 
 298 
 299     // --- OnAction
 300     /**
 301      * The action, which is invoked whenever the MenuItem is fired. This
 302      * may be due to the user clicking on the button with the mouse, or by
 303      * a touch event, or by a key press, or if the developer programatically
 304      * invokes the {@link #fire()} method.
 305      */
 306     private ObjectProperty<EventHandler<ActionEvent>> onAction;
 307 
 308     public final void setOnAction(EventHandler<ActionEvent> value) {
 309         onActionProperty().set( value);
 310     }
 311 
 312     public final EventHandler<ActionEvent> getOnAction() {
 313         return onAction == null ? null : onAction.get();
 314     }
 315 
 316     public final ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
 317         if (onAction == null) {
 318             onAction = new ObjectPropertyBase<EventHandler<ActionEvent>>() {
 319                 @Override protected void invalidated() {
 320                     eventHandlerManager.setEventHandler(ActionEvent.ACTION, get());
 321                 }
 322 
 323                 @Override
 324                 public Object getBean() {
 325                     return MenuItem.this;
 326                 }
 327 
 328                 @Override
 329                 public String getName() {
 330                     return "onAction";
 331                 }
 332             };
 333         }
 334         return onAction;
 335     }
 336 
 337     /**
 338      * <p>Called when a accelerator for the Menuitem is invoked</p>
 339      * @since JavaFX 2.2
 340      */
 341     public static final EventType<Event> MENU_VALIDATION_EVENT = new EventType<Event>
 342             (Event.ANY, "MENU_VALIDATION_EVENT");
 343 
 344     /**
 345      * The event handler that is associated with invocation of an accelerator for a MenuItem. This
 346      * can happen when a key sequence for an accelerator is pressed. The event handler is also
 347      * invoked when onShowing event handler is called.
 348      * @since JavaFX 2.2
 349      */
 350     private ObjectProperty<EventHandler<Event>> onMenuValidation;
 351 
 352     public final void setOnMenuValidation(EventHandler<Event> value) {
 353         onMenuValidationProperty().set( value);
 354     }
 355 
 356     public final EventHandler<Event> getOnMenuValidation() {
 357         return onMenuValidation == null ? null : onMenuValidation.get();
 358     }
 359 
 360     public final ObjectProperty<EventHandler<Event>> onMenuValidationProperty() {
 361         if (onMenuValidation == null) {
 362             onMenuValidation = new ObjectPropertyBase<EventHandler<Event>>() {
 363                 @Override protected void invalidated() {
 364                     eventHandlerManager.setEventHandler(MENU_VALIDATION_EVENT, get());
 365                 }
 366                 @Override public Object getBean() {
 367                     return MenuItem.this;
 368                 }
 369                 @Override public String getName() {
 370                     return "onMenuValidation";
 371                 }
 372             };
 373         }
 374         return onMenuValidation;
 375     }
 376 
 377     // --- Disable
 378     /**
 379      * Sets the individual disabled state of this MenuItem.
 380      * Setting disable to true will cause this MenuItem to become disabled.
 381      */
 382     private BooleanProperty disable;
 383     public final void setDisable(boolean value) { disableProperty().set(value); }
 384     public final boolean isDisable() { return disable == null ? false : disable.get(); }
 385     public final BooleanProperty disableProperty() {
 386         if (disable == null) {
 387             disable = new SimpleBooleanProperty(this, "disable");
 388         }
 389         return disable;
 390     }
 391 
 392 
 393     // --- Visible
 394     /**
 395      * Specifies whether this MenuItem should be rendered as part of the scene graph.
 396      */
 397     private BooleanProperty visible;
 398     public final void setVisible(boolean value) { visibleProperty().set(value); }
 399     public final boolean isVisible() { return visible == null ? true : visible.get(); }
 400     public final BooleanProperty visibleProperty() {
 401         if (visible == null) {
 402             visible = new SimpleBooleanProperty(this, "visible", true);
 403         }
 404         return visible;
 405     }
 406 
 407     /**
 408      * The accelerator property enables accessing the associated action in one keystroke.
 409      * It is a convenience offered to perform quickly a given action.
 410      */
 411     private ObjectProperty<KeyCombination> accelerator;
 412     public final void setAccelerator(KeyCombination value) {
 413         acceleratorProperty().set(value);
 414     }
 415     public final KeyCombination getAccelerator() {
 416         return accelerator == null ? null : accelerator.get();
 417     }
 418     public final ObjectProperty<KeyCombination> acceleratorProperty() {
 419         if (accelerator == null) {
 420             accelerator = new SimpleObjectProperty<KeyCombination>(this, "accelerator");
 421         }
 422         return accelerator;
 423     }
 424 
 425     /**
 426      * MnemonicParsing property to enable/disable text parsing.
 427      * If this is set to true, then the MenuItem text will be
 428      * parsed to see if it contains the mnemonic parsing character '_'.
 429      * When a mnemonic is detected the key combination will
 430      * be determined based on the succeeding character, and the mnemonic
 431      * added.
 432      *
 433      * <p>
 434      * The default value for MenuItem is true.
 435      * </p>
 436      */
 437     private BooleanProperty mnemonicParsing;
 438     public final void setMnemonicParsing(boolean value) {
 439         mnemonicParsingProperty().set(value);
 440     }
 441     public final boolean isMnemonicParsing() {
 442         return mnemonicParsing == null ? true : mnemonicParsing.get();
 443     }
 444     public final BooleanProperty mnemonicParsingProperty() {
 445         if (mnemonicParsing == null) {
 446             mnemonicParsing = new SimpleBooleanProperty(this, "mnemonicParsing", true);
 447         }
 448         return mnemonicParsing;
 449     }
 450 
 451     /***************************************************************************
 452      *                                                                         *
 453      * Public API                                                              *
 454      *                                                                         *
 455      **************************************************************************/
 456 
 457     @Override public ObservableList<String> getStyleClass() {
 458         return styleClass;
 459     }
 460 
 461     /**
 462      * Fires a new ActionEvent.
 463      */
 464     public void fire() {
 465         Event.fireEvent(this, new ActionEvent(this, this));
 466     }
 467 
 468     /**
 469      * Registers an event handler to this MenuItem. The handler is called when the
 470      * menu item receives an {@code Event} of the specified type during the bubbling
 471      * phase of event delivery.
 472      *
 473      * @param <E> the specific event class of the handler
 474      * @param eventType the type of the events to receive by the handler
 475      * @param eventHandler the handler to register
 476      * @throws NullPointerException if the event type or handler is null
 477      */
 478     public <E extends Event> void addEventHandler(EventType<E> eventType, EventHandler<E> eventHandler) {
 479         eventHandlerManager.addEventHandler(eventType, eventHandler);
 480     }
 481 
 482     /**
 483      * Unregisters a previously registered event handler from this MenuItem. One
 484      * handler might have been registered for different event types, so the
 485      * caller needs to specify the particular event type from which to
 486      * unregister the handler.
 487      *
 488      * @param <E> the specific event class of the handler
 489      * @param eventType the event type from which to unregister
 490      * @param eventHandler the handler to unregister
 491      * @throws NullPointerException if the event type or handler is null
 492      */
 493     public <E extends Event> void removeEventHandler(EventType<E> eventType, EventHandler<E> eventHandler) {
 494         eventHandlerManager.removeEventHandler(eventType, eventHandler);
 495     }
 496 
 497     /** {@inheritDoc} */
 498     @Override public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
 499         // FIXME review that these are configure properly
 500         if (getParentPopup() != null) {
 501             getParentPopup().buildEventDispatchChain(tail);
 502         }
 503 
 504         if (getParentMenu() != null) {
 505             getParentMenu().buildEventDispatchChain(tail);
 506         }
 507 
 508         return tail.prepend(eventHandlerManager);
 509     }
 510 
 511     /**
 512      * Returns a previously set Object property, or null if no such property
 513      * has been set using the {@link MenuItem#setUserData(java.lang.Object)} method.
 514      *
 515      * @return The Object that was previously set, or null if no property
 516      *          has been set or if null was set.
 517      */
 518     public Object getUserData() {
 519         return userData;
 520     }
 521 
 522     /**
 523      * Convenience method for setting a single Object property that can be
 524      * retrieved at a later date. This is functionally equivalent to calling
 525      * the getProperties().put(Object key, Object value) method. This can later
 526      * be retrieved by calling {@link Node#getUserData()}.
 527      *
 528      * @param value The value to be stored - this can later be retrieved by calling
 529      *          {@link Node#getUserData()}.
 530      */
 531     public void setUserData(Object value) {
 532         this.userData = value;
 533     }
 534 
 535     /**
 536      * Returns an observable map of properties on this menu item for use primarily
 537      * by application developers.
 538      *
 539      * @return an observable map of properties on this menu item for use primarily
 540      * by application developers
 541      */
 542     public ObservableMap<Object, Object> getProperties() {
 543         if (properties == null) {
 544             properties = FXCollections.observableMap(new HashMap<Object, Object>());
 545         }
 546         return properties;
 547     }
 548 
 549     /***************************************************************************
 550      *                                                                         *
 551      * Stylesheet Handling                                                     *
 552      *                                                                         *
 553      **************************************************************************/
 554 
 555     private static final String DEFAULT_STYLE_CLASS = "menu-item";
 556 
 557     /**
 558      * {@inheritDoc}
 559      * @return "MenuItem"
 560      * @since JavaFX 8.0
 561      */
 562     @Override
 563     public String getTypeSelector() {
 564         return "MenuItem";
 565     }
 566 
 567     /**
 568      * {@inheritDoc}
 569      * @return {@code getParentMenu()}, or {@code getParentPopup()}
 570      * if {@code parentMenu} is null
 571      * @since JavaFX 8.0
 572      */
 573     @Override
 574     public Styleable getStyleableParent() {
 575 
 576         if(getParentMenu() == null) {
 577             return getParentPopup();
 578         } else {
 579             return getParentMenu();
 580         }
 581     }
 582 
 583     /**
 584      * {@inheritDoc}
 585      * @since JavaFX 8.0
 586      */
 587     @Override
 588     public final ObservableSet<PseudoClass> getPseudoClassStates() {
 589         return FXCollections.emptyObservableSet();
 590     }
 591 
 592     @Override
 593     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 594         return Collections.emptyList();
 595     }
 596 
 597     /** {@inheritDoc} */
 598     @Override public Node getStyleableNode() {
 599         // Fix for RT-20582. We dive into the visual representation
 600         // of this MenuItem so that we may return it to the caller.
 601         ContextMenu parentPopup = MenuItem.this.getParentPopup();
 602         if (parentPopup == null || ! (parentPopup.getSkin() instanceof ContextMenuSkin)) return null;
 603 
 604         ContextMenuSkin skin = (ContextMenuSkin) parentPopup.getSkin();
 605         if (! (skin.getNode() instanceof ContextMenuContent)) return null;
 606 
 607         ContextMenuContent content = (ContextMenuContent) skin.getNode();
 608         Parent nodes = content.getItemsContainer();
 609 
 610         MenuItem desiredMenuItem = MenuItem.this;
 611         List<Node> childrenNodes = nodes.getChildrenUnmodifiable();
 612         for (int i = 0; i < childrenNodes.size(); i++) {
 613             if (! (childrenNodes.get(i) instanceof ContextMenuContent.MenuItemContainer)) continue;
 614 
 615             ContextMenuContent.MenuItemContainer MenuRow =
 616                     (ContextMenuContent.MenuItemContainer) childrenNodes.get(i);
 617 
 618             if (desiredMenuItem.equals(MenuRow.getItem())) {
 619                 return MenuRow;
 620             }
 621         }
 622 
 623         return null;
 624     }
 625 
 626     @Override public String toString() {
 627         StringBuilder sbuf = new StringBuilder(getClass().getSimpleName());
 628 
 629         boolean hasId = id != null && !"".equals(getId());
 630         boolean hasStyleClass = !getStyleClass().isEmpty();
 631 
 632         if (!hasId) {
 633             sbuf.append('@');
 634             sbuf.append(Integer.toHexString(hashCode()));
 635         } else {
 636             sbuf.append("[id=");
 637             sbuf.append(getId());
 638             if (!hasStyleClass) sbuf.append("]");
 639         }
 640 
 641         if (hasStyleClass) {
 642             if (!hasId) sbuf.append('[');
 643             else sbuf.append(", ");
 644             sbuf.append("styleClass=");
 645             sbuf.append(getStyleClass());
 646             sbuf.append("]");
 647         }
 648         return sbuf.toString();
 649     }
 650 }