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 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 }