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 com.sun.javafx.scene.control.behavior.BehaviorBase; 29 import javafx.beans.InvalidationListener; 30 import javafx.beans.Observable; 31 import javafx.collections.FXCollections; 32 import javafx.scene.control.Control; 33 import javafx.scene.control.TableColumnBase; 34 import javafx.scene.control.TreeItem; 35 import javafx.scene.control.TreeTableCell; 36 import javafx.scene.control.TreeTableColumn; 37 import javafx.scene.control.TreeTablePosition; 38 import javafx.scene.control.TreeTableRow; 39 import javafx.scene.control.TreeTableView; 40 41 import java.util.ArrayList; 42 import java.util.Collections; 43 import java.util.List; 44 45 import javafx.beans.property.DoubleProperty; 46 import javafx.scene.AccessibleAttribute; 47 import javafx.scene.Node; 48 import javafx.css.StyleableDoubleProperty; 49 import javafx.css.CssMetaData; 50 51 import javafx.css.converter.SizeConverter; 52 import com.sun.javafx.scene.control.behavior.TreeTableRowBehavior; 53 54 import javafx.beans.property.ObjectProperty; 55 import javafx.beans.value.WritableValue; 56 import javafx.collections.ObservableList; 57 import javafx.css.Styleable; 58 import javafx.css.StyleableProperty; 59 import javafx.scene.control.TreeView; 60 61 /** 62 * Default skin implementation for the {@link TreeTableRow} control. 63 * 64 * @see TreeTableRow 65 * @since 9 66 */ 67 public class TreeTableRowSkin<T> extends TableRowSkinBase<TreeItem<T>, TreeTableRow<T>, TreeTableCell<T,?>> { 68 69 /*************************************************************************** 70 * * 71 * Private Fields * 72 * * 73 **************************************************************************/ 74 75 // maps into the TreeTableViewSkin items property via 76 // TreeTableViewSkin.treeItemToListMap 77 private TreeItem<?> treeItem; 78 private boolean disclosureNodeDirty = true; 79 private Node graphic; 80 private final BehaviorBase<TreeTableRow<T>> behavior; 81 82 private TreeTableViewSkin treeTableViewSkin; 83 84 private boolean childrenDirty = false; 85 86 87 88 /*************************************************************************** 89 * * 90 * Constructors * 91 * * 92 **************************************************************************/ 93 94 /** 95 * Creates a new TreeTableRowSkin instance, installing the necessary child 96 * nodes into the Control {@link Control#getChildren() children} list, as 97 * well as the necessary input mappings for handling key, mouse, etc events. 98 * 99 * @param control The control that this skin should be installed onto. 100 */ 101 public TreeTableRowSkin(TreeTableRow<T> control) { 102 super(control); 103 104 // install default input map for the TreeTableRow control 105 behavior = new TreeTableRowBehavior<>(control); 106 // control.setInputMap(behavior.getInputMap()); 107 108 updateTreeItem(); 109 updateTableViewSkin(); 110 111 registerChangeListener(control.treeTableViewProperty(), e -> updateTableViewSkin()); 112 registerChangeListener(control.indexProperty(), e -> updateCells = true); 113 registerChangeListener(control.treeItemProperty(), e -> { 114 updateTreeItem(); 115 // There used to be an isDirty = true statement here, but this was 116 // determined to be unnecessary and led to performance issues such as 117 // those detailed in JDK-8143266 118 }); 119 120 setupTreeTableViewListeners(); 121 } 122 123 private void setupTreeTableViewListeners() { 124 TreeTableView<T> treeTableView = getSkinnable().getTreeTableView(); 125 if (treeTableView == null) { 126 getSkinnable().treeTableViewProperty().addListener(new InvalidationListener() { 127 @Override public void invalidated(Observable observable) { 128 getSkinnable().treeTableViewProperty().removeListener(this); 129 setupTreeTableViewListeners(); 130 } 131 }); 132 } else { 133 registerChangeListener(treeTableView.treeColumnProperty(), e -> { 134 // Fix for RT-27782: Need to set isDirty to true, rather than the 135 // cheaper updateCells, as otherwise the text indentation will not 136 // be recalculated in TreeTableCellSkin.leftLabelPadding() 137 isDirty = true; 138 getSkinnable().requestLayout(); 139 }); 140 141 DoubleProperty fixedCellSizeProperty = getTreeTableView().fixedCellSizeProperty(); 142 if (fixedCellSizeProperty != null) { 143 registerChangeListener(fixedCellSizeProperty, e -> { 144 fixedCellSize = fixedCellSizeProperty.get(); 145 fixedCellSizeEnabled = fixedCellSize > 0; 146 }); 147 fixedCellSize = fixedCellSizeProperty.get(); 148 fixedCellSizeEnabled = fixedCellSize > 0; 149 150 // JDK-8144500: 151 // When in fixed cell size mode, we must listen to the width of the virtual flow, so 152 // that when it changes, we can appropriately add / remove cells that may or may not 153 // be required (because we remove all cells that are not visible). 154 registerChangeListener(getVirtualFlow().widthProperty(), e -> treeTableView.requestLayout()); 155 } 156 } 157 } 158 159 160 /*************************************************************************** 161 * * 162 * Listeners * 163 * * 164 **************************************************************************/ 165 166 private final InvalidationListener graphicListener = o -> { 167 disclosureNodeDirty = true; 168 getSkinnable().requestLayout(); 169 }; 170 171 172 /*************************************************************************** 173 * * 174 * Properties * 175 * * 176 **************************************************************************/ 177 178 /** 179 * The amount of space to multiply by the treeItem.level to get the left 180 * margin for this tree cell. This is settable from CSS 181 */ 182 private DoubleProperty indent = null; 183 public final void setIndent(double value) { indentProperty().set(value); } 184 public final double getIndent() { return indent == null ? 10.0 : indent.get(); } 185 public final DoubleProperty indentProperty() { 186 if (indent == null) { 187 indent = new StyleableDoubleProperty(10.0) { 188 @Override public Object getBean() { 189 return TreeTableRowSkin.this; 190 } 191 192 @Override public String getName() { 193 return "indent"; 194 } 195 196 @Override public CssMetaData<TreeTableRow<?>,Number> getCssMetaData() { 197 return TreeTableRowSkin.StyleableProperties.INDENT; 198 } 199 }; 200 } 201 return indent; 202 } 203 204 205 206 /*************************************************************************** 207 * * 208 * Public API * 209 * * 210 **************************************************************************/ 211 212 /** {@inheritDoc} */ 213 @Override public void dispose() { 214 super.dispose(); 215 216 if (behavior != null) { 217 behavior.dispose(); 218 } 219 } 220 221 /** {@inheritDoc} */ 222 @Override protected void updateChildren() { 223 super.updateChildren(); 224 225 updateDisclosureNodeAndGraphic(); 226 227 if (childrenDirty) { 228 childrenDirty = false; 229 if (cells.isEmpty()) { 230 getChildren().clear(); 231 } else { 232 // TODO we can optimise this by only showing cells that are 233 // visible based on the table width and the amount of horizontal 234 // scrolling. 235 getChildren().addAll(cells); 236 } 237 } 238 } 239 240 /** {@inheritDoc} */ 241 @Override protected void layoutChildren(double x, double y, double w, double h) { 242 if (disclosureNodeDirty) { 243 updateDisclosureNodeAndGraphic(); 244 disclosureNodeDirty = false; 245 } 246 247 Node disclosureNode = getDisclosureNode(); 248 if (disclosureNode != null && disclosureNode.getScene() == null) { 249 updateDisclosureNodeAndGraphic(); 250 } 251 252 super.layoutChildren(x, y, w, h); 253 } 254 255 256 257 /*************************************************************************** 258 * * 259 * Private Implementation * 260 * * 261 **************************************************************************/ 262 263 /** {@inheritDoc} */ 264 @Override protected TreeTableCell<T, ?> createCell(TableColumnBase tcb) { 265 TreeTableColumn tableColumn = (TreeTableColumn<T,?>) tcb; 266 TreeTableCell cell = (TreeTableCell) tableColumn.getCellFactory().call(tableColumn); 267 268 cell.updateTreeTableColumn(tableColumn); 269 cell.updateTreeTableView(tableColumn.getTreeTableView()); 270 271 return cell; 272 } 273 274 /** {@inheritDoc} */ 275 @Override void updateCells(boolean resetChildren) { 276 super.updateCells(resetChildren); 277 278 if (resetChildren) { 279 childrenDirty = true; 280 updateChildren(); 281 } 282 } 283 284 /** {@inheritDoc} */ 285 @Override boolean isIndentationRequired() { 286 return true; 287 } 288 289 /** {@inheritDoc} */ 290 @Override TableColumnBase getTreeColumn() { 291 return getTreeTableView().getTreeColumn(); 292 } 293 294 /** {@inheritDoc} */ 295 @Override int getIndentationLevel(TreeTableRow<T> control) { 296 return getTreeTableView().getTreeItemLevel(control.getTreeItem()); 297 } 298 299 /** {@inheritDoc} */ 300 @Override double getIndentationPerLevel() { 301 return getIndent(); 302 } 303 304 /** {@inheritDoc} */ 305 @Override Node getDisclosureNode() { 306 return getSkinnable().getDisclosureNode(); 307 } 308 309 @Override boolean isDisclosureNodeVisible() { 310 return getDisclosureNode() != null && treeItem != null && ! treeItem.isLeaf(); 311 } 312 313 @Override boolean isShowRoot() { 314 return getTreeTableView().isShowRoot(); 315 } 316 317 /** {@inheritDoc} */ 318 @Override protected ObservableList<TreeTableColumn<T, ?>> getVisibleLeafColumns() { 319 return getTreeTableView() == null ? FXCollections.emptyObservableList() : getTreeTableView().getVisibleLeafColumns(); 320 } 321 322 /** {@inheritDoc} */ 323 @Override protected void updateCell(TreeTableCell<T, ?> cell, TreeTableRow<T> row) { 324 cell.updateTreeTableRow(row); 325 } 326 327 /** {@inheritDoc} */ 328 @Override protected TreeTableColumn<T, ?> getTableColumn(TreeTableCell cell) { 329 return cell.getTableColumn(); 330 } 331 332 /** {@inheritDoc} */ 333 @Override protected ObjectProperty<Node> graphicProperty() { 334 TreeTableRow<T> treeTableRow = getSkinnable(); 335 if (treeTableRow == null) return null; 336 if (treeItem == null) return null; 337 338 return treeItem.graphicProperty(); 339 } 340 341 private void updateTreeItem() { 342 if (treeItem != null) { 343 treeItem.graphicProperty().removeListener(graphicListener); 344 } 345 treeItem = getSkinnable().getTreeItem(); 346 if (treeItem != null) { 347 treeItem.graphicProperty().addListener(graphicListener); 348 } 349 } 350 351 private TreeTableView<T> getTreeTableView() { 352 return getSkinnable().getTreeTableView(); 353 } 354 355 private void updateDisclosureNodeAndGraphic() { 356 if (getSkinnable().isEmpty()) { 357 getChildren().remove(graphic); 358 return; 359 } 360 361 // check for graphic missing 362 ObjectProperty<Node> graphicProperty = graphicProperty(); 363 Node newGraphic = graphicProperty == null ? null : graphicProperty.get(); 364 if (newGraphic != null) { 365 // RT-30466: remove the old graphic 366 if (newGraphic != graphic) { 367 getChildren().remove(graphic); 368 } 369 370 if (! getChildren().contains(newGraphic)) { 371 getChildren().add(newGraphic); 372 graphic = newGraphic; 373 } 374 } 375 376 // check disclosure node 377 Node disclosureNode = getSkinnable().getDisclosureNode(); 378 if (disclosureNode != null) { 379 boolean disclosureVisible = treeItem != null && ! treeItem.isLeaf(); 380 disclosureNode.setVisible(disclosureVisible); 381 382 if (! disclosureVisible) { 383 getChildren().remove(disclosureNode); 384 } else if (disclosureNode.getParent() == null) { 385 getChildren().add(disclosureNode); 386 disclosureNode.toFront(); 387 } else { 388 disclosureNode.toBack(); 389 } 390 391 // RT-26625: [TreeView, TreeTableView] can lose arrows while scrolling 392 // RT-28668: Ensemble tree arrow disappears 393 if (disclosureNode.getScene() != null) { 394 disclosureNode.applyCss(); 395 } 396 } 397 } 398 399 private void updateTableViewSkin() { 400 TreeTableView<T> tableView = getSkinnable().getTreeTableView(); 401 if (tableView != null && tableView.getSkin() instanceof TreeTableViewSkin) { 402 treeTableViewSkin = (TreeTableViewSkin)tableView.getSkin(); 403 } 404 } 405 406 407 /*************************************************************************** 408 * * 409 * Stylesheet Handling * 410 * * 411 **************************************************************************/ 412 413 private static class StyleableProperties { 414 415 private static final CssMetaData<TreeTableRow<?>,Number> INDENT = 416 new CssMetaData<TreeTableRow<?>,Number>("-fx-indent", 417 SizeConverter.getInstance(), 10.0) { 418 419 @Override public boolean isSettable(TreeTableRow<?> n) { 420 DoubleProperty p = ((TreeTableRowSkin<?>) n.getSkin()).indentProperty(); 421 return p == null || !p.isBound(); 422 } 423 424 @Override public StyleableProperty<Number> getStyleableProperty(TreeTableRow<?> n) { 425 final TreeTableRowSkin<?> skin = (TreeTableRowSkin<?>) n.getSkin(); 426 return (StyleableProperty<Number>)(WritableValue<Number>)skin.indentProperty(); 427 } 428 }; 429 430 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 431 static { 432 final List<CssMetaData<? extends Styleable, ?>> styleables = 433 new ArrayList<CssMetaData<? extends Styleable, ?>>(CellSkinBase.getClassCssMetaData()); 434 styleables.add(INDENT); 435 STYLEABLES = Collections.unmodifiableList(styleables); 436 } 437 } 438 439 /** 440 * Returns the CssMetaData associated with this class, which may include the 441 * CssMetaData of its superclasses. 442 * @return the CssMetaData associated with this class, which may include the 443 * CssMetaData of its superclasses 444 */ 445 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 446 return StyleableProperties.STYLEABLES; 447 } 448 449 /** 450 * {@inheritDoc} 451 */ 452 @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 453 return getClassCssMetaData(); 454 } 455 456 457 /** {@inheritDoc} */ 458 @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 459 final TreeTableView<T> treeTableView = getSkinnable().getTreeTableView(); 460 switch (attribute) { 461 case SELECTED_ITEMS: { 462 // FIXME this could be optimised to iterate over cellsMap only 463 // (selectedCells could be big, cellsMap is much smaller) 464 List<Node> selection = new ArrayList<>(); 465 int index = getSkinnable().getIndex(); 466 for (TreeTablePosition<T,?> pos : treeTableView.getSelectionModel().getSelectedCells()) { 467 if (pos.getRow() == index) { 468 TreeTableColumn<T,?> column = pos.getTableColumn(); 469 if (column == null) { 470 /* This is the row-based case */ 471 column = treeTableView.getVisibleLeafColumn(0); 472 } 473 TreeTableCell<T,?> cell = cellsMap.get(column).get(); 474 if (cell != null) selection.add(cell); 475 } 476 return FXCollections.observableArrayList(selection); 477 } 478 } 479 case CELL_AT_ROW_COLUMN: { 480 int colIndex = (Integer)parameters[1]; 481 TreeTableColumn<T,?> column = treeTableView.getVisibleLeafColumn(colIndex); 482 if (cellsMap.containsKey(column)) { 483 return cellsMap.get(column).get(); 484 } 485 return null; 486 } 487 case FOCUS_ITEM: { 488 TreeTableView.TreeTableViewFocusModel<T> fm = treeTableView.getFocusModel(); 489 TreeTablePosition<T,?> focusedCell = fm.getFocusedCell(); 490 TreeTableColumn<T,?> column = focusedCell.getTableColumn(); 491 if (column == null) { 492 /* This is the row-based case */ 493 column = treeTableView.getVisibleLeafColumn(0); 494 } 495 if (cellsMap.containsKey(column)) { 496 return cellsMap.get(column).get(); 497 } 498 return null; 499 } 500 default: return super.queryAccessibleAttribute(attribute, parameters); 501 } 502 } 503 }