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