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 }