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 }