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<ActionEvent>() { 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 }