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 }