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.skin;
  27 
  28 import com.sun.javafx.scene.NodeHelper;
  29 import com.sun.javafx.scene.control.ContextMenuContent;
  30 import com.sun.javafx.scene.control.ControlAcceleratorSupport;
  31 import com.sun.javafx.scene.control.LabeledImpl;
  32 import com.sun.javafx.scene.control.skin.Utils;
  33 import javafx.application.Platform;
  34 import javafx.collections.ListChangeListener;
  35 import javafx.event.ActionEvent;
  36 import javafx.scene.Node;
  37 import javafx.scene.control.ContextMenu;
  38 import javafx.scene.control.MenuButton;
  39 import javafx.scene.control.MenuItem;
  40 import javafx.scene.control.Skin;
  41 import javafx.scene.control.SkinBase;
  42 import javafx.scene.input.MouseEvent;
  43 import javafx.scene.layout.Region;
  44 import javafx.scene.layout.StackPane;
  45 import com.sun.javafx.scene.control.behavior.MenuButtonBehaviorBase;
  46 
  47 /**
  48  * Base class for MenuButtonSkin and SplitMenuButtonSkin. It consists of the
  49  * label, the arrowButton with its arrow shape, and the popup.
  50  *
  51  * @since 9
  52  */
  53 public class MenuButtonSkinBase<C extends MenuButton> extends SkinBase<C> {
  54 
  55     /***************************************************************************
  56      *                                                                         *
  57      * Private fields                                                          *
  58      *                                                                         *
  59      **************************************************************************/
  60 
  61     final LabeledImpl label;
  62     final StackPane arrow;
  63     final StackPane arrowButton;
  64     ContextMenu popup;
  65 
  66     /**
  67      * If true, the control should behave like a button for mouse button events.
  68      */
  69     boolean behaveLikeButton = false;
  70     private ListChangeListener<MenuItem> itemsChangedListener;
  71 
  72 
  73 
  74     /***************************************************************************
  75      *                                                                         *
  76      * Constructors                                                            *
  77      *                                                                         *
  78      **************************************************************************/
  79 
  80     /**
  81      * Creates a new instance of MenuButtonSkinBase, although note that this
  82      * instance does not handle any behavior / input mappings - this needs to be
  83      * handled appropriately by subclasses.
  84      *
  85      * @param control The control that this skin should be installed onto.
  86      */
  87     public MenuButtonSkinBase(final C control) {
  88         super(control);
  89 
  90         if (control.getOnMousePressed() == null) {
  91             control.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> {
  92                 MenuButtonBehaviorBase behavior = getBehavior();
  93                 if (behavior != null) {
  94                     behavior.mousePressed(e, behaveLikeButton);
  95                 }
  96             });
  97         }
  98 
  99         if (control.getOnMouseReleased() == null) {
 100             control.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> {
 101                 MenuButtonBehaviorBase behavior = getBehavior();
 102                 if (behavior != null) {
 103                     behavior.mouseReleased(e, behaveLikeButton);
 104                 }
 105             });
 106         }
 107 
 108         /*
 109          * Create the objects we will be displaying.
 110          */
 111         label = new MenuLabeledImpl(getSkinnable());
 112         label.setMnemonicParsing(control.isMnemonicParsing());
 113         label.setLabelFor(control);
 114 
 115         arrow = new StackPane();
 116         arrow.getStyleClass().setAll("arrow");
 117         arrow.setMaxWidth(Region.USE_PREF_SIZE);
 118         arrow.setMaxHeight(Region.USE_PREF_SIZE);
 119 
 120         arrowButton = new StackPane();
 121         arrowButton.getStyleClass().setAll("arrow-button");
 122         arrowButton.getChildren().add(arrow);
 123 
 124         popup = new ContextMenu();
 125         popup.getItems().clear();
 126         popup.getItems().addAll(getSkinnable().getItems());
 127 
 128         getChildren().clear();
 129         getChildren().addAll(label, arrowButton);
 130 
 131         getSkinnable().requestLayout();
 132 
 133         itemsChangedListener = c -> {
 134             while (c.next()) {
 135                 popup.getItems().removeAll(c.getRemoved());
 136                 popup.getItems().addAll(c.getFrom(), c.getAddedSubList());
 137             }
 138         };
 139         control.getItems().addListener(itemsChangedListener);
 140 
 141         if (getSkinnable().getScene() != null) {
 142             ControlAcceleratorSupport.addAcceleratorsIntoScene(getSkinnable().getItems(), getSkinnable());
 143         }
 144         control.sceneProperty().addListener((scene, oldValue, newValue) -> {
 145             if (getSkinnable() != null && getSkinnable().getScene() != null) {
 146                 ControlAcceleratorSupport.addAcceleratorsIntoScene(getSkinnable().getItems(), getSkinnable());
 147             }
 148         });
 149 
 150         // Register listeners
 151         registerChangeListener(control.showingProperty(), e -> {
 152             if (getSkinnable().isShowing()) {
 153                 show();
 154             } else {
 155                 hide();
 156             }
 157         });
 158         registerChangeListener(control.focusedProperty(), e -> {
 159             // Handle tabbing away from an open MenuButton
 160             if (!getSkinnable().isFocused() && getSkinnable().isShowing()) {
 161                 hide();
 162             }
 163             if (!getSkinnable().isFocused() && popup.isShowing()) {
 164                 hide();
 165             }
 166         });
 167         registerChangeListener(control.mnemonicParsingProperty(), e -> {
 168             label.setMnemonicParsing(getSkinnable().isMnemonicParsing());
 169             getSkinnable().requestLayout();
 170         });
 171         registerChangeListener(popup.showingProperty(), e -> {
 172             if (!popup.isShowing() && getSkinnable().isShowing()) {
 173                 // Popup was dismissed. Maybe user clicked outside or typed ESCAPE.
 174                 // Make sure button is in sync.
 175                 getSkinnable().hide();
 176             }
 177 
 178             if (popup.isShowing()) {
 179                 Utils.addMnemonics(popup, getSkinnable().getScene(), NodeHelper.isShowMnemonics(getSkinnable()));
 180             } else {
 181                 // we wrap this in a runLater so that mnemonics are not removed
 182                 // before all key events are fired (because KEY_PRESSED might have
 183                 // been used to hide the menu, but KEY_TYPED and KEY_RELEASED
 184                 // events are still to be fired, and shouldn't miss out on going
 185                 // through the mnemonics code (especially in case they should be
 186                 // consumed to prevent them being used elsewhere).
 187                 // See JBS-8090026 for more detail.
 188                 Platform.runLater(() -> Utils.removeMnemonics(popup, getSkinnable().getScene()));
 189             }
 190         });
 191     }
 192 
 193 
 194 
 195     /***************************************************************************
 196      *                                                                         *
 197      * Private implementation                                                  *
 198      *                                                                         *
 199      **************************************************************************/
 200 
 201     /** {@inheritDoc} */
 202     @Override public void dispose() {
 203         getSkinnable().getItems().removeListener(itemsChangedListener);
 204         super.dispose();
 205         if (popup != null ) {
 206             if (popup.getSkin() != null && popup.getSkin().getNode() != null) {
 207                 ContextMenuContent cmContent = (ContextMenuContent)popup.getSkin().getNode();
 208                 cmContent.dispose();
 209             }
 210             popup.setSkin(null);
 211             popup = null;
 212         }
 213     }
 214 
 215     /** {@inheritDoc} */
 216     @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 217         return leftInset
 218                 + label.minWidth(height)
 219                 + snapSizeX(arrowButton.minWidth(height))
 220                 + rightInset;
 221     }
 222 
 223     /** {@inheritDoc} */
 224     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 225         return topInset
 226                 + Math.max(label.minHeight(width), snapSizeY(arrowButton.minHeight(-1)))
 227                 + bottomInset;
 228     }
 229 
 230     /** {@inheritDoc} */
 231     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 232         return leftInset
 233                 + label.prefWidth(height)
 234                 + snapSizeX(arrowButton.prefWidth(height))
 235                 + rightInset;
 236     }
 237 
 238     /** {@inheritDoc} */
 239     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 240         return topInset
 241                 + Math.max(label.prefHeight(width), snapSizeY(arrowButton.prefHeight(-1)))
 242                 + bottomInset;
 243     }
 244 
 245     /** {@inheritDoc} */
 246     @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 247         return getSkinnable().prefWidth(height);
 248     }
 249 
 250     /** {@inheritDoc} */
 251     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 252         return getSkinnable().prefHeight(width);
 253     }
 254 
 255     /** {@inheritDoc} */
 256     @Override protected void layoutChildren(final double x, final double y,
 257                                             final double w, final double h) {
 258         final double arrowButtonWidth = snapSizeX(arrowButton.prefWidth(-1));
 259         label.resizeRelocate(x, y, w - arrowButtonWidth, h);
 260         arrowButton.resizeRelocate(x + (w - arrowButtonWidth), y, arrowButtonWidth, h);
 261     }
 262 
 263 
 264 
 265     /***************************************************************************
 266      *                                                                         *
 267      * Private implementation                                                  *
 268      *                                                                         *
 269      **************************************************************************/
 270 
 271     MenuButtonBehaviorBase<C> getBehavior() {
 272         return null;
 273     }
 274 
 275     private void show() {
 276         if (!popup.isShowing()) {
 277             popup.show(getSkinnable(), getSkinnable().getPopupSide(), 0, 0);
 278         }
 279     }
 280 
 281     private void hide() {
 282         if (popup.isShowing()) {
 283             popup.hide();
 284         }
 285     }
 286 
 287     boolean requestFocusOnFirstMenuItem = false;
 288     void requestFocusOnFirstMenuItem() {
 289         this.requestFocusOnFirstMenuItem = true;
 290     }
 291 
 292     void putFocusOnFirstMenuItem() {
 293         Skin<?> popupSkin = popup.getSkin();
 294         if (popupSkin instanceof ContextMenuSkin) {
 295             Node node = popupSkin.getNode();
 296             if (node instanceof ContextMenuContent) {
 297                 ((ContextMenuContent)node).requestFocusOnIndex(0);
 298             }
 299         }
 300     }
 301 
 302 
 303 
 304     /***************************************************************************
 305      *                                                                         *
 306      * Support classes                                                         *
 307      *                                                                         *
 308      **************************************************************************/
 309 
 310     private static class MenuLabeledImpl extends LabeledImpl {
 311         MenuButton button;
 312         public MenuLabeledImpl(MenuButton b) {
 313             super(b);
 314             button = b;
 315             addEventHandler(ActionEvent.ACTION, e -> {
 316                 button.fireEvent(new ActionEvent());
 317                 e.consume();
 318             });
 319         }
 320     }
 321 }