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      */
 443     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 444         return StyleableProperties.STYLEABLES;
 445     }
 446 
 447     /**
 448      * {@inheritDoc}
 449      */
 450     @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 451         return getClassCssMetaData();
 452     }
 453 
 454 
 455     /** {@inheritDoc} */
 456     @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 457         final TreeTableView<T> treeTableView = getSkinnable().getTreeTableView();
 458         switch (attribute) {
 459             case SELECTED_ITEMS: {
 460                 // FIXME this could be optimised to iterate over cellsMap only
 461                 // (selectedCells could be big, cellsMap is much smaller)
 462                 List<Node> selection = new ArrayList<>();
 463                 int index = getSkinnable().getIndex();
 464                 for (TreeTablePosition<T,?> pos : treeTableView.getSelectionModel().getSelectedCells()) {
 465                     if (pos.getRow() == index) {
 466                         TreeTableColumn<T,?> column = pos.getTableColumn();
 467                         if (column == null) {
 468                             /* This is the row-based case */
 469                             column = treeTableView.getVisibleLeafColumn(0);
 470                         }
 471                         TreeTableCell<T,?> cell = cellsMap.get(column).get();
 472                         if (cell != null) selection.add(cell);
 473                     }
 474                     return FXCollections.observableArrayList(selection);
 475                 }
 476             }
 477             case CELL_AT_ROW_COLUMN: {
 478                 int colIndex = (Integer)parameters[1];
 479                 TreeTableColumn<T,?> column = treeTableView.getVisibleLeafColumn(colIndex);
 480                 if (cellsMap.containsKey(column)) {
 481                     return cellsMap.get(column).get();
 482                 }
 483                 return null;
 484             }
 485             case FOCUS_ITEM: {
 486                 TreeTableView.TreeTableViewFocusModel<T> fm = treeTableView.getFocusModel();
 487                 TreeTablePosition<T,?> focusedCell = fm.getFocusedCell();
 488                 TreeTableColumn<T,?> column = focusedCell.getTableColumn();
 489                 if (column == null) {
 490                     /* This is the row-based case */
 491                     column = treeTableView.getVisibleLeafColumn(0);
 492                 }
 493                 if (cellsMap.containsKey(column)) {
 494                     return cellsMap.get(column).get();
 495                 }
 496                 return null;
 497             }
 498             default: return super.queryAccessibleAttribute(attribute, parameters);
 499         }
 500     }
 501 }