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