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