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