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