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