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 javafx.event.EventHandler;
  29 import javafx.scene.control.SkinBase;
  30 import com.sun.javafx.scene.control.behavior.ComboBoxBaseBehavior;
  31 import javafx.geometry.HPos;
  32 import javafx.geometry.VPos;
  33 import javafx.scene.Node;
  34 import javafx.scene.control.ComboBoxBase;
  35 import javafx.scene.input.MouseEvent;
  36 import javafx.scene.layout.Region;
  37 import javafx.scene.layout.StackPane;
  38 
  39 import java.util.List;
  40 
  41 /**
  42  * An abstract class intended to be used as the base skin for ComboBox-like
  43  * controls that are based on {@link ComboBoxBase}. Most users of this skin class
  44  * would be well-advised to also look at {@link ComboBoxPopupControl} for
  45  * additional useful API.
  46  *
  47  * @since 9
  48  * @param <T> The type of the ComboBox-like control.
  49  * @see ComboBoxBase
  50  * @see ComboBoxPopupControl
  51  */
  52 public abstract class ComboBoxBaseSkin<T> extends SkinBase<ComboBoxBase<T>> {
  53 
  54     /***************************************************************************
  55      *                                                                         *
  56      * Private Fields                                                          *
  57      *                                                                         *
  58      **************************************************************************/
  59 
  60     private Node displayNode; // this is normally either label or textField
  61 
  62     StackPane arrowButton;
  63     Region arrow;
  64 
  65     /** The mode in which this control will be represented. */
  66     private ComboBoxMode mode = ComboBoxMode.COMBOBOX;
  67     final ComboBoxMode getMode() { return mode; }
  68     final void setMode(ComboBoxMode value) { mode = value; }
  69 
  70     private final EventHandler<MouseEvent> mouseEnteredEventHandler  = e ->   getBehavior().mouseEntered(e);
  71     private final EventHandler<MouseEvent> mousePressedEventHandler  = e -> { getBehavior().mousePressed(e);  e.consume(); };
  72     private final EventHandler<MouseEvent> mouseReleasedEventHandler = e -> { getBehavior().mouseReleased(e); e.consume(); };
  73     private final EventHandler<MouseEvent> mouseExitedEventHandler   = e ->   getBehavior().mouseExited(e);
  74 
  75 
  76 
  77     /***************************************************************************
  78      *                                                                         *
  79      * Constructors                                                            *
  80      *                                                                         *
  81      **************************************************************************/
  82 
  83     /**
  84      * Creates a new instance of ComboBoxBaseSkin, although note that this
  85      * instance does not handle any behavior / input mappings - this needs to be
  86      * handled appropriately by subclasses.
  87      *
  88      * @param control The control that this skin should be installed onto.
  89      */
  90     public ComboBoxBaseSkin(final ComboBoxBase<T> control) {
  91         // Call the super method with the ComboBox we were just given in the constructor
  92         super(control);
  93 
  94         getChildren().clear();
  95 
  96         // open button / arrow
  97         arrow = new Region();
  98         arrow.setFocusTraversable(false);
  99         arrow.getStyleClass().setAll("arrow");
 100         arrow.setId("arrow");
 101         arrow.setMaxWidth(Region.USE_PREF_SIZE);
 102         arrow.setMaxHeight(Region.USE_PREF_SIZE);
 103         arrow.setMouseTransparent(true);
 104 
 105         arrowButton = new StackPane();
 106         arrowButton.setFocusTraversable(false);
 107         arrowButton.setId("arrow-button");
 108         arrowButton.getStyleClass().setAll("arrow-button");
 109         arrowButton.getChildren().add(arrow);
 110 
 111         getChildren().add(arrowButton);
 112 
 113         // When ComboBoxBase focus shifts to another node, it should hide.
 114         getSkinnable().focusedProperty().addListener((observable, oldValue, newValue) -> {
 115             if (!newValue) {
 116                 focusLost();
 117             }
 118         });
 119 
 120         // Register listeners
 121         updateArrowButtonListeners();
 122         registerChangeListener(control.editableProperty(), e -> {
 123             updateArrowButtonListeners();
 124             updateDisplayArea();
 125         });
 126         registerChangeListener(control.showingProperty(), e -> {
 127             if (getSkinnable().isShowing()) {
 128                 show();
 129             } else {
 130                 hide();
 131             }
 132         });
 133         registerChangeListener(control.valueProperty(), e -> updateDisplayArea());
 134     }
 135 
 136 
 137 
 138     /***************************************************************************
 139      *                                                                         *
 140      * Public API                                                              *
 141      *                                                                         *
 142      **************************************************************************/
 143 
 144     /**
 145      * This method should return a Node that will be positioned within the
 146      * ComboBox 'button' area.
 147      */
 148     public abstract Node getDisplayNode();
 149 
 150     /**
 151      * This method will be called when the ComboBox popup should be displayed.
 152      * It is up to specific skin implementations to determine how this is handled.
 153      */
 154     public abstract void show();
 155 
 156     /**
 157      * This method will be called when the ComboBox popup should be hidden.
 158      * It is up to specific skin implementations to determine how this is handled.
 159      */
 160     public abstract void hide();
 161 
 162     /** {@inheritDoc} */
 163     @Override protected void layoutChildren(final double x, final double y,
 164             final double w, final double h) {
 165         if (displayNode == null) {
 166             updateDisplayArea();
 167         }
 168 
 169         final double arrowWidth = snapSizeX(arrow.prefWidth(-1));
 170         final double arrowButtonWidth = (isButton()) ? 0 :
 171                 arrowButton.snappedLeftInset() + arrowWidth +
 172                 arrowButton.snappedRightInset();
 173 
 174         if (displayNode != null) {
 175             displayNode.resizeRelocate(x, y, w - arrowButtonWidth, h);
 176         }
 177 
 178         arrowButton.setVisible(! isButton());
 179         if (! isButton()) {
 180             arrowButton.resize(arrowButtonWidth, h);
 181             positionInArea(arrowButton, (x + w) - arrowButtonWidth, y,
 182                     arrowButtonWidth, h, 0, HPos.CENTER, VPos.CENTER);
 183         }
 184     }
 185 
 186     /** {@inheritDoc} */
 187     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 188         if (displayNode == null) {
 189             updateDisplayArea();
 190         }
 191 
 192         final double arrowWidth = snapSizeX(arrow.prefWidth(-1));
 193         final double arrowButtonWidth = isButton() ? 0 :
 194                                         arrowButton.snappedLeftInset() +
 195                                         arrowWidth +
 196                                         arrowButton.snappedRightInset();
 197         final double displayNodeWidth = displayNode == null ? 0 : displayNode.prefWidth(height);
 198 
 199         final double totalWidth = displayNodeWidth + arrowButtonWidth;
 200         return leftInset + totalWidth + rightInset;
 201     }
 202 
 203     /** {@inheritDoc} */
 204     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 205         if (displayNode == null) {
 206             updateDisplayArea();
 207         }
 208 
 209         double ph;
 210         if (displayNode == null) {
 211             final int DEFAULT_HEIGHT = 21;
 212             double arrowHeight = (isButton()) ? 0 :
 213                     (arrowButton.snappedTopInset() + arrow.prefHeight(-1) + arrowButton.snappedBottomInset());
 214             ph = Math.max(DEFAULT_HEIGHT, arrowHeight);
 215         } else {
 216             ph = displayNode.prefHeight(width);
 217         }
 218 
 219         return topInset+ ph + bottomInset;
 220     }
 221 
 222     /** {@inheritDoc} */
 223     @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 224         return getSkinnable().prefWidth(height);
 225     }
 226 
 227     /** {@inheritDoc} */
 228     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 229         return getSkinnable().prefHeight(width);
 230     }
 231 
 232     // Overridden so that we use the displayNode as the baseline, rather than the arrow.
 233     // See RT-30754 for more information.
 234     /** {@inheritDoc} */
 235     @Override protected double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
 236         if (displayNode == null) {
 237             updateDisplayArea();
 238         }
 239 
 240         if (displayNode != null) {
 241             return displayNode.getLayoutBounds().getMinY() + displayNode.getLayoutY() + displayNode.getBaselineOffset();
 242         }
 243 
 244         return super.computeBaselineOffset(topInset, rightInset, bottomInset, leftInset);
 245     }
 246 
 247 
 248 
 249     /***************************************************************************
 250      *                                                                         *
 251      * Private implementation                                                  *
 252      *                                                                         *
 253      **************************************************************************/
 254 
 255     ComboBoxBaseBehavior getBehavior() {
 256         return null;
 257     }
 258 
 259     void focusLost() {
 260         getSkinnable().hide();
 261     }
 262 
 263     private boolean isButton() {
 264         return getMode() == ComboBoxMode.BUTTON;
 265     }
 266 
 267     private void updateArrowButtonListeners() {
 268         if (getSkinnable().isEditable()) {
 269             //
 270             // arrowButton behaves like a button.
 271             // This is strongly tied to the implementation in ComboBoxBaseBehavior.
 272             //
 273             arrowButton.addEventHandler(MouseEvent.MOUSE_ENTERED,  mouseEnteredEventHandler);
 274             arrowButton.addEventHandler(MouseEvent.MOUSE_PRESSED,  mousePressedEventHandler);
 275             arrowButton.addEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleasedEventHandler);
 276             arrowButton.addEventHandler(MouseEvent.MOUSE_EXITED,   mouseExitedEventHandler);
 277         } else {
 278             arrowButton.removeEventHandler(MouseEvent.MOUSE_ENTERED,  mouseEnteredEventHandler);
 279             arrowButton.removeEventHandler(MouseEvent.MOUSE_PRESSED,  mousePressedEventHandler);
 280             arrowButton.removeEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleasedEventHandler);
 281             arrowButton.removeEventHandler(MouseEvent.MOUSE_EXITED,   mouseExitedEventHandler);
 282         }
 283     }
 284 
 285     void updateDisplayArea() {
 286         final List<Node> children = getChildren();
 287         final Node oldDisplayNode = displayNode;
 288         displayNode = getDisplayNode();
 289 
 290         // don't remove displayNode if it hasn't changed.
 291         if (oldDisplayNode != null && oldDisplayNode != displayNode) {
 292             children.remove(oldDisplayNode);
 293         }
 294 
 295         if (displayNode != null && !children.contains(displayNode)) {
 296             children.add(displayNode);
 297             displayNode.applyCss();
 298         }
 299     }
 300 }