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