1 /* 2 * Copyright (c) 2010, 2015, 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.EventType; 36 import javafx.geometry.Side; 37 import javafx.scene.AccessibleAction; 38 import javafx.scene.AccessibleRole; 39 import javafx.scene.Node; 40 import javafx.scene.control.skin.MenuButtonSkin; 41 import javafx.beans.property.ReadOnlyBooleanProperty; 42 import javafx.beans.property.ReadOnlyBooleanWrapper; 43 44 /** 45 * MenuButton is a button which, when clicked or pressed, will show a 46 * {@link ContextMenu}. A MenuButton shares a very similar API to the {@link Menu} 47 * control, insofar that you set the items that should be shown in the 48 * {@link #items} ObservableList, and there is a {@link #text} property to specify the 49 * label shown within the MenuButton. 50 * <p> 51 * As mentioned, like the Menu API itself, you'll find an {@link #items} ObservableList 52 * within which you can provide anything that extends from {@link MenuItem}. 53 * There are several useful subclasses of {@link MenuItem} including 54 * {@link RadioMenuItem}, {@link CheckMenuItem}, {@link Menu}, 55 * {@link SeparatorMenuItem} and {@link CustomMenuItem}. 56 * <p> 57 * A MenuButton can be set to show its menu on any side of the button. This is 58 * specified using the {@link #popupSideProperty() popupSide} property. By default 59 * the menu appears below the button. However, regardless of the popupSide specified, 60 * if there is not enough room, the {@link ContextMenu} will be 61 * smartly repositioned, most probably to be on the opposite side of the 62 * MenuButton. 63 * 64 * <p>Example:</p> 65 * <pre> 66 * MenuButton m = new MenuButton("Eats"); 67 * m.getItems().addAll(new MenuItem("Burger"), new MenuItem("Hot Dog")); 68 * </pre> 69 * 70 * <p> 71 * MnemonicParsing is enabled by default for MenuButton. 72 * </p> 73 * 74 * @see MenuItem 75 * @see Menu 76 * @see SplitMenuButton 77 * @since JavaFX 2.0 78 */ 79 public class MenuButton extends ButtonBase { 80 81 82 /*************************************************************************** 83 * * 84 * Static properties and methods * 85 * * 86 **************************************************************************/ 87 88 /** 89 * Called prior to the MenuButton showing its popup after the user 90 * has clicked or otherwise interacted with the MenuButton. 91 * @since JavaFX 8u60 92 */ 93 public static final EventType<Event> ON_SHOWING = 94 new EventType<Event>(Event.ANY, "MENU_BUTTON_ON_SHOWING"); 95 96 /** 97 * Called after the MenuButton has shown its popup. 98 * @since JavaFX 8u60 99 */ 100 public static final EventType<Event> ON_SHOWN = 101 new EventType<Event>(Event.ANY, "MENU_BUTTON_ON_SHOWN"); 102 103 /** 104 * Called when the MenuButton popup <b>will</b> be hidden. 105 * @since JavaFX 8u60 106 */ 107 public static final EventType<Event> ON_HIDING = 108 new EventType<Event>(Event.ANY, "MENU_BUTTON_ON_HIDING"); 109 110 /** 111 * Called when the MenuButton popup has been hidden. 112 * @since JavaFX 8u60 113 */ 114 public static final EventType<Event> ON_HIDDEN = 115 new EventType<Event>(Event.ANY, "MENU_BUTTON_ON_HIDDEN"); 116 117 118 /*************************************************************************** 119 * * 120 * Constructors * 121 * * 122 **************************************************************************/ 123 124 /** 125 * Creates a new empty menu button. Use {@link #setText(String)}, 126 * {@link #setGraphic(Node)} and {@link #getItems()} to set the content. 127 */ 128 public MenuButton() { 129 this(null, null); 130 } 131 132 /** 133 * Creates a new empty menu button with the given text to display on the 134 * button. Use {@link #setGraphic(Node)} and {@link #getItems()} to set the 135 * content. 136 * 137 * @param text the text to display on the menu button 138 */ 139 public MenuButton(String text) { 140 this(text, null); 141 } 142 143 /** 144 * Creates a new empty menu button with the given text and graphic to 145 * display on the button. Use {@link #getItems()} to set the content. 146 * 147 * @param text the text to display on the menu button 148 * @param graphic the graphic to display on the menu button 149 */ 150 public MenuButton(String text, Node graphic) { 151 this(text, graphic, (MenuItem[])null); 152 } 153 154 /** 155 * Creates a new menu button with the given text and graphic to 156 * display on the button, and inserts the given items 157 * into the {@link #getItems() items} list. 158 * 159 * @param text the text to display on the menu button 160 * @param graphic the graphic to display on the menu button 161 * @param items The items to display in the popup menu. 162 * @since JavaFX 8u40 163 */ 164 public MenuButton(String text, Node graphic, MenuItem... items) { 165 if (text != null) { 166 setText(text); 167 } 168 if (graphic != null) { 169 setGraphic(graphic); 170 } 171 if (items != null) { 172 getItems().addAll(items); 173 } 174 175 getStyleClass().setAll(DEFAULT_STYLE_CLASS); 176 setAccessibleRole(AccessibleRole.MENU_BUTTON); 177 setMnemonicParsing(true); // enable mnemonic auto-parsing by default 178 // the default value for popupSide = Side.BOTTOM therefor 179 // PSEUDO_CLASS_OPENVERTICALLY should be set from the start. 180 pseudoClassStateChanged(PSEUDO_CLASS_OPENVERTICALLY, true); 181 } 182 183 /*************************************************************************** 184 * * 185 * Properties * 186 * * 187 **************************************************************************/ 188 private final ObservableList<MenuItem> items = FXCollections.<MenuItem>observableArrayList(); 189 190 /** 191 * The items to show within this buttons menu. If this ObservableList is modified 192 * at runtime, the Menu will update as expected. 193 * <p> 194 * Commonly used controls include including {@code MenuItem}, 195 * {@code CheckMenuItem}, {@code RadioMenuItem}, 196 * and of course {@code Menu}, which if added to a menu, will become a sub 197 * menu. {@link SeparatorMenuItem} is another commonly used Node in the Menu's items 198 * ObservableList. 199 */ 200 public final ObservableList<MenuItem> getItems() { 201 return items; 202 } 203 204 // --- Showing 205 /** 206 * Indicates whether the {@link ContextMenu} is currently visible. 207 */ 208 private ReadOnlyBooleanWrapper showing = new ReadOnlyBooleanWrapper(this, "showing", false) { 209 @Override protected void invalidated() { 210 pseudoClassStateChanged(PSEUDO_CLASS_SHOWING, get()); 211 super.invalidated(); 212 } 213 }; 214 private void setShowing(boolean value) { 215 // these events will not fire if the showing property is bound 216 Event.fireEvent(this, value ? new Event(ComboBoxBase.ON_SHOWING) : 217 new Event(ComboBoxBase.ON_HIDING)); 218 showing.set(value); 219 Event.fireEvent(this, value ? new Event(ComboBoxBase.ON_SHOWN) : 220 new Event(ComboBoxBase.ON_HIDDEN)); 221 } 222 public final boolean isShowing() { return showing.get(); } 223 public final ReadOnlyBooleanProperty showingProperty() { return showing.getReadOnlyProperty(); } 224 225 226 227 /** 228 * Indicates on which side the {@link ContextMenu} should open in 229 * relation to the MenuButton. Menu items are generally laid 230 * out vertically in either case. 231 * For example, if the menu button were in a vertical toolbar on the left 232 * edge of the application, you might change {@link #popupSide} to {@code Side.RIGHT} so that 233 * the popup will appear to the right of the MenuButton. 234 * 235 * @defaultValue {@code Side.BOTTOM} 236 */ 237 // TODO expose via CSS 238 private ObjectProperty<Side> popupSide; 239 240 public final void setPopupSide(Side value) { 241 popupSideProperty().set(value); 242 } 243 244 public final Side getPopupSide() { 245 return popupSide == null ? Side.BOTTOM : popupSide.get(); 246 } 247 248 public final ObjectProperty<Side> popupSideProperty() { 249 if (popupSide == null) { 250 popupSide = new ObjectPropertyBase<Side>(Side.BOTTOM) { 251 @Override protected void invalidated() { 252 final Side side = get(); 253 final boolean active = (side == Side.TOP) || (side == Side.BOTTOM); 254 pseudoClassStateChanged(PSEUDO_CLASS_OPENVERTICALLY, active); 255 } 256 257 @Override 258 public Object getBean() { 259 return MenuButton.this; 260 } 261 262 @Override 263 public String getName() { 264 return "popupSide"; 265 } 266 }; 267 } 268 return popupSide; 269 } 270 271 /*************************************************************************** 272 * * 273 * Control methods * 274 * * 275 **************************************************************************/ 276 277 /** 278 * Shows the {@link ContextMenu}, assuming this MenuButton is not disabled. 279 * 280 * @see #isDisabled() 281 * @see #isShowing() 282 */ 283 public void show() { 284 // TODO: isBound check is probably unnecessary here 285 if (!isDisabled() && !showing.isBound()) { 286 setShowing(true); 287 } 288 } 289 290 /** 291 * Hides the {@link ContextMenu}. 292 * 293 * @see #isShowing() 294 */ 295 public void hide() { 296 // TODO: isBound check is probably unnecessary here 297 if (!showing.isBound()) { 298 setShowing(false); 299 } 300 } 301 302 /** 303 * This has no impact. 304 */ 305 @Override 306 public void fire() { 307 if (!isDisabled()) { 308 fireEvent(new ActionEvent()); 309 } 310 } 311 312 /** {@inheritDoc} */ 313 @Override protected Skin<?> createDefaultSkin() { 314 return new MenuButtonSkin(this); 315 } 316 317 /*************************************************************************** 318 * * 319 * Stylesheet Handling * 320 * * 321 **************************************************************************/ 322 323 private static final String DEFAULT_STYLE_CLASS = "menu-button"; 324 private static final PseudoClass PSEUDO_CLASS_OPENVERTICALLY = 325 PseudoClass.getPseudoClass("openvertically"); 326 private static final PseudoClass PSEUDO_CLASS_SHOWING = 327 PseudoClass.getPseudoClass("showing"); 328 329 /*************************************************************************** 330 * * 331 * Accessibility handling * 332 * * 333 **************************************************************************/ 334 335 @Override 336 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 337 switch (action) { 338 case FIRE: 339 if (isShowing()) { 340 hide(); 341 } else { 342 show(); 343 } 344 break; 345 default: super.executeAccessibleAction(action); 346 } 347 } 348 }