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 com.sun.javafx.scene.control.skin; 27 28 import javafx.beans.value.ObservableValue; 29 import javafx.css.Styleable; 30 import javafx.geometry.*; 31 import javafx.scene.control.*; 32 import com.sun.javafx.scene.control.behavior.ComboBoxBaseBehavior; 33 import javafx.beans.InvalidationListener; 34 import javafx.scene.AccessibleAttribute; 35 import javafx.scene.Node; 36 import javafx.scene.input.MouseEvent; 37 import javafx.scene.layout.Region; 38 import javafx.stage.WindowEvent; 39 40 public abstract class ComboBoxPopupControl<T> extends ComboBoxBaseSkin<T> { 41 42 protected PopupControl popup; 43 public static final String COMBO_BOX_STYLE_CLASS = "combo-box-popup"; 44 45 private boolean popupNeedsReconfiguring = true; 46 47 public ComboBoxPopupControl(ComboBoxBase<T> comboBox, final ComboBoxBaseBehavior<T> behavior) { 48 super(comboBox, behavior); 49 } 50 51 /** 52 * This method should return the Node that will be displayed when the user 53 * clicks on the ComboBox 'button' area. 54 */ 55 protected abstract Node getPopupContent(); 56 57 protected PopupControl getPopup() { 58 if (popup == null) { 59 createPopup(); 60 } 61 return popup; 62 } 63 64 @Override public void show() { 65 if (getSkinnable() == null) { 66 throw new IllegalStateException("ComboBox is null"); 67 } 68 69 Node content = getPopupContent(); 70 if (content == null) { 71 throw new IllegalStateException("Popup node is null"); 72 } 73 74 if (getPopup().isShowing()) return; 75 76 positionAndShowPopup(); 77 } 78 79 @Override public void hide() { 80 if (popup != null && popup.isShowing()) { 81 popup.hide(); 82 } 83 } 84 85 private Point2D getPrefPopupPosition() { 86 return com.sun.javafx.util.Utils.pointRelativeTo(getSkinnable(), getPopupContent(), HPos.CENTER, VPos.BOTTOM, 0, 0, false); 87 } 88 89 private void positionAndShowPopup() { 90 final PopupControl _popup = getPopup(); 91 _popup.getScene().setNodeOrientation(getSkinnable().getEffectiveNodeOrientation()); 92 93 94 final Node popupContent = getPopupContent(); 95 sizePopup(); 96 97 Point2D p = getPrefPopupPosition(); 98 99 popupNeedsReconfiguring = true; 100 reconfigurePopup(); 101 102 final ComboBoxBase<T> comboBoxBase = getSkinnable(); 103 _popup.show(comboBoxBase.getScene().getWindow(), 104 snapPosition(p.getX()), 105 snapPosition(p.getY())); 106 107 popupContent.requestFocus(); 108 109 // second call to sizePopup here to enable proper sizing _after_ the popup 110 // has been displayed. See RT-37622 for more detail. 111 sizePopup(); 112 } 113 114 private void sizePopup() { 115 final Node popupContent = getPopupContent(); 116 117 if (popupContent instanceof Region) { 118 // snap to pixel 119 final Region r = (Region) popupContent; 120 121 final double prefWidth = r.prefWidth(-1); 122 final double minWidth = r.minWidth(-1); 123 final double maxWidth = r.maxWidth(-1); 124 final double w = snapSize(Math.min(Math.max(prefWidth, minWidth), Math.max(minWidth, maxWidth))); 125 126 final double prefHeight = r.prefHeight(w); 127 final double minHeight = r.minHeight(w); 128 final double maxHeight = r.maxHeight(w); 129 final double h = snapSize(Math.min(Math.max(prefHeight, minHeight), Math.max(minHeight, maxHeight))); 130 131 popupContent.resize(w, h); 132 } else { 133 popupContent.autosize(); 134 } 135 } 136 137 private void createPopup() { 138 popup = new PopupControl() { 139 140 @Override public Styleable getStyleableParent() { 141 return ComboBoxPopupControl.this.getSkinnable(); 142 } 143 { 144 setSkin(new Skin<Skinnable>() { 145 @Override public Skinnable getSkinnable() { return ComboBoxPopupControl.this.getSkinnable(); } 146 @Override public Node getNode() { return getPopupContent(); } 147 @Override public void dispose() { } 148 }); 149 } 150 151 }; 152 popup.getStyleClass().add(COMBO_BOX_STYLE_CLASS); 153 popup.setConsumeAutoHidingEvents(false); 154 popup.setAutoHide(true); 155 popup.setAutoFix(true); 156 popup.setHideOnEscape(true); 157 popup.setOnAutoHide(e -> { 158 getBehavior().onAutoHide(); 159 }); 160 popup.addEventHandler(MouseEvent.MOUSE_CLICKED, t -> { 161 // RT-18529: We listen to mouse input that is received by the popup 162 // but that is not consumed, and assume that this is due to the mouse 163 // clicking outside of the node, but in areas such as the 164 // dropshadow. 165 getBehavior().onAutoHide(); 166 }); 167 popup.addEventHandler(WindowEvent.WINDOW_HIDDEN, t -> { 168 // Make sure the accessibility focus returns to the combo box 169 // after the window closes. 170 getSkinnable().notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_NODE); 171 }); 172 173 // Fix for RT-21207 174 InvalidationListener layoutPosListener = o -> { 175 popupNeedsReconfiguring = true; 176 reconfigurePopup(); 177 }; 178 getSkinnable().layoutXProperty().addListener(layoutPosListener); 179 getSkinnable().layoutYProperty().addListener(layoutPosListener); 180 getSkinnable().widthProperty().addListener(layoutPosListener); 181 getSkinnable().heightProperty().addListener(layoutPosListener); 182 183 // RT-36966 - if skinnable's scene becomes null, ensure popup is closed 184 getSkinnable().sceneProperty().addListener(o -> { 185 if (((ObservableValue)o).getValue() == null) { 186 hide(); 187 } 188 }); 189 190 } 191 192 void reconfigurePopup() { 193 // RT-26861. Don't call getPopup() here because it may cause the popup 194 // to be created too early, which leads to memory leaks like those noted 195 // in RT-32827. 196 if (popup == null) return; 197 198 final boolean isShowing = popup.isShowing(); 199 if (! isShowing) return; 200 201 if (! popupNeedsReconfiguring) return; 202 popupNeedsReconfiguring = false; 203 204 final Point2D p = getPrefPopupPosition(); 205 206 final Node popupContent = getPopupContent(); 207 final double minWidth = popupContent.prefWidth(Region.USE_COMPUTED_SIZE); 208 final double minHeight = popupContent.prefHeight(Region.USE_COMPUTED_SIZE); 209 210 if (p.getX() > -1) popup.setAnchorX(p.getX()); 211 if (p.getY() > -1) popup.setAnchorY(p.getY()); 212 if (minWidth > -1) popup.setMinWidth(minWidth); 213 if (minHeight > -1) popup.setMinHeight(minHeight); 214 215 final Bounds b = popupContent.getLayoutBounds(); 216 final double currentWidth = b.getWidth(); 217 final double currentHeight = b.getHeight(); 218 final double newWidth = currentWidth < minWidth ? minWidth : currentWidth; 219 final double newHeight = currentHeight < minHeight ? minHeight : currentHeight; 220 221 if (newWidth != currentWidth || newHeight != currentHeight) { 222 // Resizing content to resolve issues such as RT-32582 and RT-33700 223 // (where RT-33700 was introduced due to a previous fix for RT-32582) 224 popupContent.resize(newWidth, newHeight); 225 if (popupContent instanceof Region) { 226 ((Region)popupContent).setMinSize(newWidth, newHeight); 227 ((Region)popupContent).setPrefSize(newWidth, newHeight); 228 } 229 } 230 } 231 }