1 /* 2 * Copyright (c) 2010, 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.control; 27 28 import javafx.css.PseudoClass; 29 import javafx.beans.property.ObjectProperty; 30 import javafx.beans.property.ObjectPropertyBase; 31 import javafx.collections.FXCollections; 32 import javafx.collections.ObservableList; 33 import javafx.event.ActionEvent; 34 import javafx.event.Event; 35 import javafx.event.EventHandler; 36 import javafx.event.EventType; 37 import javafx.geometry.Side; 38 import javafx.scene.AccessibleAction; 39 import javafx.scene.AccessibleRole; 40 import javafx.scene.Node; 41 import javafx.scene.control.skin.MenuButtonSkin; 42 import javafx.beans.property.ReadOnlyBooleanProperty; 43 import javafx.beans.property.ReadOnlyBooleanWrapper; 44 45 /** 46 * MenuButton is a button which, when clicked or pressed, will show a 47 * {@link ContextMenu}. A MenuButton shares a very similar API to the {@link Menu} 48 * control, insofar that you set the items that should be shown in the 49 * {@link #getItems() items} ObservableList, and there is a {@link #textProperty() text} property to specify the 50 * label shown within the MenuButton. 51 * <p> 52 * As mentioned, like the Menu API itself, you'll find an {@link #getItems() items} ObservableList 53 * within which you can provide anything that extends from {@link MenuItem}. 54 * There are several useful subclasses of {@link MenuItem} including 55 * {@link RadioMenuItem}, {@link CheckMenuItem}, {@link Menu}, 56 * {@link SeparatorMenuItem} and {@link CustomMenuItem}. 57 * <p> 58 * A MenuButton can be set to show its menu on any side of the button. This is 59 * specified using the {@link #popupSideProperty() popupSide} property. By default 60 * the menu appears below the button. However, regardless of the popupSide specified, 61 * if there is not enough room, the {@link ContextMenu} will be 62 * smartly repositioned, most probably to be on the opposite side of the 63 * MenuButton. 64 * 65 * <p>Example:</p> 66 * <pre> 67 * MenuButton m = new MenuButton("Eats"); 68 * m.getItems().addAll(new MenuItem("Burger"), new MenuItem("Hot Dog")); 69 * </pre> 70 * 71 * <p> 72 * MnemonicParsing is enabled by default for MenuButton. 73 * </p> 74 * 75 * @see MenuItem 76 * @see Menu 77 * @see SplitMenuButton 78 * @since JavaFX 2.0 79 */ 80 public class MenuButton extends ButtonBase { 81 82 83 /*************************************************************************** 84 * * 85 * Static properties and methods * 86 * * 87 **************************************************************************/ 88 89 /** 90 * Called prior to the MenuButton showing its popup after the user 91 * has clicked or otherwise interacted with the MenuButton. 92 * @since JavaFX 8u60 93 */ 94 public static final EventType<Event> ON_SHOWING = 95 new EventType<Event>(Event.ANY, "MENU_BUTTON_ON_SHOWING"); 96 97 /** 98 * Called after the MenuButton has shown its popup. 99 * @since JavaFX 8u60 100 */ 101 public static final EventType<Event> ON_SHOWN = 102 new EventType<Event>(Event.ANY, "MENU_BUTTON_ON_SHOWN"); 103 104 /** 105 * Called when the MenuButton popup <b>will</b> be hidden. 106 * @since JavaFX 8u60 107 */ 108 public static final EventType<Event> ON_HIDING = 109 new EventType<Event>(Event.ANY, "MENU_BUTTON_ON_HIDING"); 110 111 /** 112 * Called when the MenuButton popup has been hidden. 113 * @since JavaFX 8u60 114 */ 115 public static final EventType<Event> ON_HIDDEN = 116 new EventType<Event>(Event.ANY, "MENU_BUTTON_ON_HIDDEN"); 117 118 119 /*************************************************************************** 120 * * 121 * Constructors * 122 * * 123 **************************************************************************/ 124 125 /** 126 * Creates a new empty menu button. Use {@link #setText(String)}, 127 * {@link #setGraphic(Node)} and {@link #getItems()} to set the content. 128 */ 129 public MenuButton() { 130 this(null, null); 131 } 132 133 /** 134 * Creates a new empty menu button with the given text to display on the 135 * button. Use {@link #setGraphic(Node)} and {@link #getItems()} to set the 136 * content. 137 * 138 * @param text the text to display on the menu button 139 */ 140 public MenuButton(String text) { 141 this(text, null); 142 } 143 144 /** 145 * Creates a new empty menu button with the given text and graphic to 146 * display on the button. Use {@link #getItems()} to set the content. 147 * 148 * @param text the text to display on the menu button 149 * @param graphic the graphic to display on the menu button 150 */ 151 public MenuButton(String text, Node graphic) { 152 this(text, graphic, (MenuItem[])null); 153 } 154 155 /** 156 * Creates a new menu button with the given text and graphic to 157 * display on the button, and inserts the given items 158 * into the {@link #getItems() items} list. 159 * 160 * @param text the text to display on the menu button 161 * @param graphic the graphic to display on the menu button 162 * @param items The items to display in the popup menu. 163 * @since JavaFX 8u40 164 */ 165 public MenuButton(String text, Node graphic, MenuItem... items) { 166 if (text != null) { 167 setText(text); 168 } 169 if (graphic != null) { 170 setGraphic(graphic); 171 } 172 if (items != null) { 173 getItems().addAll(items); 174 } 175 176 getStyleClass().setAll(DEFAULT_STYLE_CLASS); 177 setAccessibleRole(AccessibleRole.MENU_BUTTON); 178 setMnemonicParsing(true); // enable mnemonic auto-parsing by default 179 // the default value for popupSide = Side.BOTTOM therefor 180 // PSEUDO_CLASS_OPENVERTICALLY should be set from the start. 181 pseudoClassStateChanged(PSEUDO_CLASS_OPENVERTICALLY, true); 182 } 183 184 /*************************************************************************** 185 * * 186 * Properties * 187 * * 188 **************************************************************************/ 189 private final ObservableList<MenuItem> items = FXCollections.<MenuItem>observableArrayList(); 190 191 /** 192 * The items to show within this buttons menu. If this ObservableList is modified 193 * at runtime, the Menu will update as expected. 194 * <p> 195 * Commonly used controls include including {@code MenuItem}, 196 * {@code CheckMenuItem}, {@code RadioMenuItem}, 197 * and of course {@code Menu}, which if added to a menu, will become a sub 198 * menu. {@link SeparatorMenuItem} is another commonly used Node in the Menu's items 199 * ObservableList. 200 * @return the list of menu items within this buttons menu 201 */ 202 public final ObservableList<MenuItem> getItems() { 203 return items; 204 } 205 206 // --- Showing 207 /** 208 * Indicates whether the {@link ContextMenu} is currently visible. 209 */ 210 private ReadOnlyBooleanWrapper showing = new ReadOnlyBooleanWrapper(this, "showing", false) { 211 @Override protected void invalidated() { 212 pseudoClassStateChanged(PSEUDO_CLASS_SHOWING, get()); 213 super.invalidated(); 214 } 215 }; 216 private void setShowing(boolean value) { 217 // these events will not fire if the showing property is bound 218 Event.fireEvent(this, value ? new Event(ON_SHOWING) : 219 new Event(ON_HIDING)); 220 showing.set(value); 221 Event.fireEvent(this, value ? new Event(ON_SHOWN) : 222 new Event(ON_HIDDEN)); 223 } 224 public final boolean isShowing() { return showing.get(); } 225 public final ReadOnlyBooleanProperty showingProperty() { return showing.getReadOnlyProperty(); } 226 227 228 229 /** 230 * Indicates on which side the {@link ContextMenu} should open in 231 * relation to the MenuButton. Menu items are generally laid 232 * out vertically in either case. 233 * For example, if the menu button were in a vertical toolbar on the left 234 * edge of the application, you might change {@link #popupSideProperty() popupSide} 235 * to {@code Side.RIGHT} so that the popup will appear to the right of the MenuButton. 236 * 237 * @defaultValue {@code Side.BOTTOM} 238 */ 239 // TODO expose via CSS 240 private ObjectProperty<Side> popupSide; 241 242 public final void setPopupSide(Side value) { 243 popupSideProperty().set(value); 244 } 245 246 public final Side getPopupSide() { 247 return popupSide == null ? Side.BOTTOM : popupSide.get(); 248 } 249 250 public final ObjectProperty<Side> popupSideProperty() { 251 if (popupSide == null) { 252 popupSide = new ObjectPropertyBase<Side>(Side.BOTTOM) { 253 @Override protected void invalidated() { 254 final Side side = get(); 255 final boolean active = (side == Side.TOP) || (side == Side.BOTTOM); 256 pseudoClassStateChanged(PSEUDO_CLASS_OPENVERTICALLY, active); 257 } 258 259 @Override 260 public Object getBean() { 261 return MenuButton.this; 262 } 263 264 @Override 265 public String getName() { 266 return "popupSide"; 267 } 268 }; 269 } 270 return popupSide; 271 } 272 273 /** 274 * Called just prior to the {@code ContextMenu} being shown. 275 * @return the on showing property 276 * @since 10 277 */ 278 public final ObjectProperty<EventHandler<Event>> onShowingProperty() { return onShowing; } 279 public final void setOnShowing(EventHandler<Event> value) { onShowingProperty().set(value); } 280 public final EventHandler<Event> getOnShowing() { return onShowingProperty().get(); } 281 private ObjectProperty<EventHandler<Event>> onShowing = new ObjectPropertyBase<EventHandler<Event>>() { 282 @Override protected void invalidated() { 283 setEventHandler(ON_SHOWING, get()); 284 } 285 286 @Override public Object getBean() { 287 return MenuButton.this; 288 } 289 290 @Override public String getName() { 291 return "onShowing"; 292 } 293 }; 294 295 /** 296 * Called just after the {@code ContextMenu} is shown. 297 * @return the on shown property 298 * @since 10 299 */ 300 public final ObjectProperty<EventHandler<Event>> onShownProperty() { return onShown; } 301 public final void setOnShown(EventHandler<Event> value) { onShownProperty().set(value); } 302 public final EventHandler<Event> getOnShown() { return onShownProperty().get(); } 303 private ObjectProperty<EventHandler<Event>> onShown = new ObjectPropertyBase<EventHandler<Event>>() { 304 @Override protected void invalidated() { 305 setEventHandler(ON_SHOWN, get()); 306 } 307 308 @Override public Object getBean() { 309 return MenuButton.this; 310 } 311 312 @Override public String getName() { 313 return "onShown"; 314 } 315 }; 316 317 /** 318 * Called just prior to the {@code ContextMenu} being hidden. 319 * @return the on hiding property 320 * @since 10 321 */ 322 public final ObjectProperty<EventHandler<Event>> onHidingProperty() { return onHiding; } 323 public final void setOnHiding(EventHandler<Event> value) { onHidingProperty().set(value); } 324 public final EventHandler<Event> getOnHiding() { return onHidingProperty().get(); } 325 private ObjectProperty<EventHandler<Event>> onHiding = new ObjectPropertyBase<EventHandler<Event>>() { 326 @Override protected void invalidated() { 327 setEventHandler(ON_HIDING, get()); 328 } 329 330 @Override public Object getBean() { 331 return MenuButton.this; 332 } 333 334 @Override public String getName() { 335 return "onHiding"; 336 } 337 }; 338 339 /** 340 * Called just after the {@code ContextMenu} has been hidden. 341 * @return the on hidden property 342 * @since 10 343 */ 344 public final ObjectProperty<EventHandler<Event>> onHiddenProperty() { return onHidden; } 345 public final void setOnHidden(EventHandler<Event> value) { onHiddenProperty().set(value); } 346 public final EventHandler<Event> getOnHidden() { return onHiddenProperty().get(); } 347 private ObjectProperty<EventHandler<Event>> onHidden = new ObjectPropertyBase<EventHandler<Event>>() { 348 @Override protected void invalidated() { 349 setEventHandler(ON_HIDDEN, get()); 350 } 351 352 @Override public Object getBean() { 353 return MenuButton.this; 354 } 355 356 @Override public String getName() { 357 return "onHidden"; 358 } 359 }; 360 361 362 /*************************************************************************** 363 * * 364 * Control methods * 365 * * 366 **************************************************************************/ 367 368 /** 369 * Shows the {@link ContextMenu}, assuming this MenuButton is not disabled. 370 * 371 * @see #isDisabled() 372 * @see #isShowing() 373 */ 374 public void show() { 375 // TODO: isBound check is probably unnecessary here 376 if (!isDisabled() && !showing.isBound()) { 377 setShowing(true); 378 } 379 } 380 381 /** 382 * Hides the {@link ContextMenu}. 383 * 384 * @see #isShowing() 385 */ 386 public void hide() { 387 // TODO: isBound check is probably unnecessary here 388 if (!showing.isBound()) { 389 setShowing(false); 390 } 391 } 392 393 /** 394 * This has no impact. 395 */ 396 @Override 397 public void fire() { 398 if (!isDisabled()) { 399 fireEvent(new ActionEvent()); 400 } 401 } 402 403 /** {@inheritDoc} */ 404 @Override protected Skin<?> createDefaultSkin() { 405 return new MenuButtonSkin(this); 406 } 407 408 /*************************************************************************** 409 * * 410 * Stylesheet Handling * 411 * * 412 **************************************************************************/ 413 414 private static final String DEFAULT_STYLE_CLASS = "menu-button"; 415 private static final PseudoClass PSEUDO_CLASS_OPENVERTICALLY = 416 PseudoClass.getPseudoClass("openvertically"); 417 private static final PseudoClass PSEUDO_CLASS_SHOWING = 418 PseudoClass.getPseudoClass("showing"); 419 420 /*************************************************************************** 421 * * 422 * Accessibility handling * 423 * * 424 **************************************************************************/ 425 426 /** {@inheritDoc} */ 427 @Override 428 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 429 switch (action) { 430 case FIRE: 431 if (isShowing()) { 432 hide(); 433 } else { 434 show(); 435 } 436 break; 437 default: super.executeAccessibleAction(action); 438 } 439 } 440 }