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 * @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 }