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