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 }