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