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 javafx.scene.control.skin;
  27 
  28 import java.util.ArrayList;
  29 import java.util.Collections;
  30 import java.util.List;
  31 import java.util.Map;
  32 import java.util.WeakHashMap;
  33 
  34 import com.sun.javafx.scene.control.behavior.BehaviorBase;
  35 import javafx.beans.InvalidationListener;
  36 import javafx.beans.property.DoubleProperty;
  37 import javafx.beans.value.WritableValue;
  38 import javafx.geometry.HPos;
  39 import javafx.geometry.VPos;
  40 import javafx.scene.Node;
  41 import javafx.scene.control.Control;
  42 import javafx.scene.control.TreeCell;
  43 import javafx.scene.control.TreeItem;
  44 import javafx.scene.control.TreeView;
  45 import javafx.css.StyleableDoubleProperty;
  46 import javafx.css.StyleableProperty;
  47 import javafx.css.CssMetaData;
  48 
  49 import javafx.css.converter.SizeConverter;
  50 import com.sun.javafx.scene.control.behavior.TreeCellBehavior;
  51 
  52 import javafx.css.Styleable;
  53 
  54 /**
  55  * Default skin implementation for the {@link TreeCell} control.
  56  *
  57  * @see TreeCell
  58  * @since 9
  59  */
  60 public class TreeCellSkin<T> extends CellSkinBase<TreeCell<T>> {
  61 
  62     /***************************************************************************
  63      *                                                                         *
  64      * Static fields                                                           *
  65      *                                                                         *
  66      **************************************************************************/
  67 
  68     /*
  69      * This is rather hacky - but it is a quick workaround to resolve the
  70      * issue that we don't know maximum width of a disclosure node for a given
  71      * TreeView. If we don't know the maximum width, we have no way to ensure
  72      * consistent indentation for a given TreeView.
  73      *
  74      * To work around this, we create a single WeakHashMap to store a max
  75      * disclosureNode width per TreeView. We use WeakHashMap to help prevent
  76      * any memory leaks.
  77      * 
  78      * RT-19656 identifies a related issue, which is that we may not provide
  79      * indentation to any TreeItems because we have not yet encountered a cell
  80      * which has a disclosureNode. Once we scroll and encounter one, indentation
  81      * happens in a displeasing way.
  82      */
  83     private static final Map<TreeView<?>, Double> maxDisclosureWidthMap = new WeakHashMap<TreeView<?>, Double>();
  84 
  85 
  86 
  87     /***************************************************************************
  88      *                                                                         *
  89      * Private fields                                                          *
  90      *                                                                         *
  91      **************************************************************************/
  92     
  93     private boolean disclosureNodeDirty = true;
  94     private TreeItem<?> treeItem;
  95     private final BehaviorBase<TreeCell<T>> behavior;
  96 
  97     private double fixedCellSize;
  98     private boolean fixedCellSizeEnabled;
  99     
 100     private InvalidationListener expandedListener = o -> updateDisclosureNodeRotation(true);
 101 
 102 
 103 
 104     /***************************************************************************
 105      *                                                                         *
 106      * Constructors                                                            *
 107      *                                                                         *
 108      **************************************************************************/
 109 
 110     /**
 111      * Creates a new TreeCellSkin instance, installing the necessary child
 112      * nodes into the Control {@link Control#getChildren() children} list, as
 113      * well as the necessary input mappings for handling key, mouse, etc events.
 114      *
 115      * @param control The control that this skin should be installed onto.
 116      */
 117     public TreeCellSkin(TreeCell<T> control) {
 118         super(control);
 119 
 120         // install default input map for the TreeCell control
 121         behavior = new TreeCellBehavior<>(control);
 122 //        control.setInputMap(behavior.getInputMap());
 123 
 124         this.fixedCellSize = control.getTreeView().getFixedCellSize();
 125         this.fixedCellSizeEnabled = fixedCellSize > 0;
 126         
 127         updateTreeItem();
 128         updateDisclosureNodeRotation(false);
 129         
 130         registerChangeListener(control.treeItemProperty(), e -> {
 131             updateTreeItem();
 132             disclosureNodeDirty = true;
 133             getSkinnable().requestLayout();
 134         });
 135         registerChangeListener(control.textProperty(), e -> getSkinnable().requestLayout());
 136         registerChangeListener(control.getTreeView().fixedCellSizeProperty(), e -> {
 137             this.fixedCellSize = getSkinnable().getTreeView().getFixedCellSize();
 138             this.fixedCellSizeEnabled = fixedCellSize > 0;
 139         });
 140     }
 141 
 142 
 143 
 144     /***************************************************************************
 145      *                                                                         *
 146      * Properties                                                              *
 147      *                                                                         *
 148      **************************************************************************/
 149 
 150     /**
 151      * The amount of space to multiply by the treeItem.level to get the left
 152      * margin for this tree cell. This is settable from CSS
 153      */
 154     private DoubleProperty indent = null;
 155     public final void setIndent(double value) { indentProperty().set(value); }
 156     public final double getIndent() { return indent == null ? 10.0 : indent.get(); }
 157     public final DoubleProperty indentProperty() {
 158         if (indent == null) {
 159             indent = new StyleableDoubleProperty(10.0) {
 160                 @Override public Object getBean() {
 161                     return TreeCellSkin.this;
 162                 }
 163 
 164                 @Override public String getName() {
 165                     return "indent";
 166                 }
 167 
 168                 @Override public CssMetaData<TreeCell<?>,Number> getCssMetaData() {
 169                     return StyleableProperties.INDENT;
 170                 }
 171             };
 172         }
 173         return indent;
 174     }
 175 
 176 
 177 
 178     /***************************************************************************
 179      *                                                                         *
 180      * Public API                                                              *
 181      *                                                                         *
 182      **************************************************************************/
 183 
 184     /** {@inheritDoc} */
 185     @Override public void dispose() {
 186         super.dispose();
 187 
 188         if (behavior != null) {
 189             behavior.dispose();
 190         }
 191     }
 192 
 193     /** {@inheritDoc} */
 194     @Override protected void updateChildren() {
 195         super.updateChildren();
 196         updateDisclosureNode();
 197     }
 198 
 199     /** {@inheritDoc} */
 200     @Override protected void layoutChildren(double x, final double y,
 201                                             double w, final double h) {
 202         // RT-25876: can not null-check here as this prevents empty rows from
 203         // being cleaned out.
 204         // if (treeItem == null) return;
 205 
 206         TreeView<T> tree = getSkinnable().getTreeView();
 207         if (tree == null) return;
 208 
 209         if (disclosureNodeDirty) {
 210             updateDisclosureNode();
 211             disclosureNodeDirty = false;
 212         }
 213 
 214         Node disclosureNode = getSkinnable().getDisclosureNode();
 215 
 216         int level = tree.getTreeItemLevel(treeItem);
 217         if (! tree.isShowRoot()) level--;
 218         double leftMargin = getIndent() * level;
 219 
 220         x += leftMargin;
 221 
 222         // position the disclosure node so that it is at the proper indent
 223         boolean disclosureVisible = disclosureNode != null && treeItem != null && ! treeItem.isLeaf();
 224 
 225         final double defaultDisclosureWidth = maxDisclosureWidthMap.containsKey(tree) ?
 226                 maxDisclosureWidthMap.get(tree) : 18;   // RT-19656: default width of default disclosure node
 227         double disclosureWidth = defaultDisclosureWidth;
 228 
 229         if (disclosureVisible) {
 230             if (disclosureNode == null || disclosureNode.getScene() == null) {
 231                 updateChildren();
 232             }
 233 
 234             if (disclosureNode != null) {
 235                 disclosureWidth = disclosureNode.prefWidth(h);
 236                 if (disclosureWidth > defaultDisclosureWidth) {
 237                     maxDisclosureWidthMap.put(tree, disclosureWidth);
 238                 }
 239 
 240                 double ph = disclosureNode.prefHeight(disclosureWidth);
 241 
 242                 disclosureNode.resize(disclosureWidth, ph);
 243                 positionInArea(disclosureNode, x, y,
 244                         disclosureWidth, ph, /*baseline ignored*/0,
 245                         HPos.CENTER, VPos.CENTER);
 246             }
 247         }
 248 
 249         // determine starting point of the graphic or cell node, and the
 250         // remaining width available to them
 251         final int padding = treeItem != null && treeItem.getGraphic() == null ? 0 : 3;
 252         x += disclosureWidth + padding;
 253         w -= (leftMargin + disclosureWidth + padding);
 254 
 255         // Rather ugly fix for RT-38519, where graphics are disappearing in
 256         // certain circumstances
 257         Node graphic = getSkinnable().getGraphic();
 258         if (graphic != null && !getChildren().contains(graphic)) {
 259             getChildren().add(graphic);
 260         }
 261 
 262         layoutLabelInArea(x, y, w, h);
 263     }
 264 
 265     /** {@inheritDoc} */
 266     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 267         if (fixedCellSizeEnabled) {
 268             return fixedCellSize;
 269         }
 270 
 271         double pref = super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
 272         Node d = getSkinnable().getDisclosureNode();
 273         return (d == null) ? pref : Math.max(d.minHeight(-1), pref);
 274     }
 275 
 276     /** {@inheritDoc} */
 277     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 278         if (fixedCellSizeEnabled) {
 279             return fixedCellSize;
 280         }
 281 
 282         final TreeCell<T> cell = getSkinnable();
 283 
 284         final double pref = super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
 285         final Node d = cell.getDisclosureNode();
 286         final double prefHeight = (d == null) ? pref : Math.max(d.prefHeight(-1), pref);
 287 
 288         // RT-30212: TreeCell does not honor minSize of cells.
 289         // snapSize for RT-36460
 290         return snapSize(Math.max(cell.getMinHeight(), prefHeight));
 291     }
 292 
 293     /** {@inheritDoc} */
 294     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 295         if (fixedCellSizeEnabled) {
 296             return fixedCellSize;
 297         }
 298 
 299         return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
 300     }
 301 
 302     /** {@inheritDoc} */
 303     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 304         double labelWidth = super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset);
 305 
 306         double pw = snappedLeftInset() + snappedRightInset();
 307 
 308         TreeView<T> tree = getSkinnable().getTreeView();
 309         if (tree == null) return pw;
 310 
 311         if (treeItem == null) return pw;
 312 
 313         pw = labelWidth;
 314 
 315         // determine the amount of indentation
 316         int level = tree.getTreeItemLevel(treeItem);
 317         if (! tree.isShowRoot()) level--;
 318         pw += getIndent() * level;
 319 
 320         // include the disclosure node width
 321         Node disclosureNode = getSkinnable().getDisclosureNode();
 322         double disclosureNodePrefWidth = disclosureNode == null ? 0 : disclosureNode.prefWidth(-1);
 323         final double defaultDisclosureWidth = maxDisclosureWidthMap.containsKey(tree) ?
 324                 maxDisclosureWidthMap.get(tree) : 0;
 325         pw += Math.max(defaultDisclosureWidth, disclosureNodePrefWidth);
 326 
 327         return pw;
 328     }
 329 
 330 
 331 
 332     /***************************************************************************
 333      *                                                                         *
 334      * Private implementation                                                  *
 335      *                                                                         *
 336      **************************************************************************/
 337 
 338     private void updateDisclosureNodeRotation(boolean animate) {
 339         // no-op, this is now handled in CSS (although we no longer animate)
 340 //        if (treeItem == null || treeItem.isLeaf()) return;
 341 //        
 342 //        Node disclosureNode = getSkinnable().getDisclosureNode();
 343 //        if (disclosureNode == null) return;
 344 //        
 345 //        final boolean isExpanded = treeItem.isExpanded();
 346 //        int fromAngle = isExpanded ? 0 : 90;
 347 //        int toAngle = isExpanded ? 90 : 0;
 348 // 
 349 //        if (animate) {
 350 //            RotateTransition rt = new RotateTransition(Duration.millis(200), disclosureNode);
 351 //            rt.setFromAngle(fromAngle);
 352 //            rt.setToAngle(toAngle);
 353 //            rt.play();
 354 //        } else {
 355 //            disclosureNode.setRotate(toAngle);
 356 //        }
 357     }
 358     
 359     private void updateTreeItem() {
 360         if (treeItem != null) {
 361             treeItem.expandedProperty().removeListener(expandedListener);
 362         }
 363         treeItem = getSkinnable().getTreeItem();
 364         if (treeItem != null) {
 365             treeItem.expandedProperty().addListener(expandedListener);
 366         }
 367 
 368         updateDisclosureNodeRotation(false);
 369     }
 370     
 371     private void updateDisclosureNode() {
 372         if (getSkinnable().isEmpty()) return;
 373 
 374         Node disclosureNode = getSkinnable().getDisclosureNode();
 375         if (disclosureNode == null) return;
 376         
 377         boolean disclosureVisible = treeItem != null && ! treeItem.isLeaf();
 378         disclosureNode.setVisible(disclosureVisible);
 379             
 380         if (! disclosureVisible) {
 381             getChildren().remove(disclosureNode);
 382         } else if (disclosureNode.getParent() == null) {
 383             getChildren().add(disclosureNode);
 384             disclosureNode.toFront();
 385         } else {
 386             disclosureNode.toBack();
 387         }
 388         
 389         // RT-26625: [TreeView, TreeTableView] can lose arrows while scrolling
 390         // RT-28668: Ensemble tree arrow disappears
 391         if (disclosureNode.getScene() != null) {
 392             disclosureNode.applyCss();
 393         }
 394     }
 395 
 396 
 397 
 398     /***************************************************************************
 399      *                                                                         *
 400      *                         Stylesheet Handling                             *
 401      *                                                                         *
 402      **************************************************************************/
 403 
 404     /** @treatAsPrivate */
 405     private static class StyleableProperties {
 406         
 407         private static final CssMetaData<TreeCell<?>,Number> INDENT = 
 408             new CssMetaData<TreeCell<?>,Number>("-fx-indent",
 409                 SizeConverter.getInstance(), 10.0) {
 410                     
 411             @Override public boolean isSettable(TreeCell<?> n) {
 412                 DoubleProperty p = ((TreeCellSkin<?>) n.getSkin()).indentProperty();
 413                 return p == null || !p.isBound();
 414             }
 415 
 416             @Override public StyleableProperty<Number> getStyleableProperty(TreeCell<?> n) {
 417                 final TreeCellSkin<?> skin = (TreeCellSkin<?>) n.getSkin();
 418                 return (StyleableProperty<Number>)(WritableValue<Number>)skin.indentProperty();
 419             }
 420         };
 421         
 422         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 423         static {
 424             final List<CssMetaData<? extends Styleable, ?>> styleables =
 425                 new ArrayList<CssMetaData<? extends Styleable, ?>>(CellSkinBase.getClassCssMetaData());
 426             styleables.add(INDENT);
 427             STYLEABLES = Collections.unmodifiableList(styleables);
 428         }
 429     }
 430 
 431     /**
 432      * Returns the CssMetaData associated with this class, which may include the
 433      * CssMetaData of its super classes.
 434      */
 435     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 436         return StyleableProperties.STYLEABLES;
 437     }
 438 
 439     /**
 440      * {@inheritDoc}
 441      */
 442     @Override
 443     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 444         return getClassCssMetaData();
 445     }
 446 }