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