1 /*
   2  * Copyright (c) 2012, 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 com.sun.javafx.collections.NonIterableChange;
  29 import com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList;
  30 
  31 import javafx.event.WeakEventHandler;
  32 import javafx.scene.control.*;
  33 
  34 import com.sun.javafx.scene.control.behavior.TreeTableViewBehavior;
  35 
  36 import java.lang.ref.WeakReference;
  37 import java.util.ArrayList;
  38 import java.util.List;
  39 
  40 import javafx.beans.property.BooleanProperty;
  41 import javafx.beans.property.ObjectProperty;
  42 import javafx.beans.property.SimpleObjectProperty;
  43 import javafx.collections.FXCollections;
  44 import javafx.collections.ObservableList;
  45 import javafx.event.EventHandler;
  46 import javafx.event.EventType;
  47 import javafx.scene.AccessibleAction;
  48 import javafx.scene.AccessibleAttribute;
  49 import javafx.scene.Node;
  50 import javafx.scene.control.TreeItem.TreeModificationEvent;
  51 import javafx.scene.input.MouseEvent;
  52 import javafx.scene.layout.Region;
  53 import javafx.scene.layout.StackPane;
  54 import javafx.util.Callback;
  55 
  56 public class TreeTableViewSkin<S> extends TableViewSkinBase<S, TreeItem<S>, TreeTableView<S>, TreeTableViewBehavior<S>, TreeTableRow<S>, TreeTableColumn<S,?>> {
  57 
  58     public TreeTableViewSkin(final TreeTableView<S> treeTableView) {
  59         super(treeTableView, new TreeTableViewBehavior<S>(treeTableView));
  60 
  61         this.treeTableView = treeTableView;
  62         this.tableBackingList = new TreeTableViewBackingList<S>(treeTableView);
  63         this.tableBackingListProperty = new SimpleObjectProperty<ObservableList<TreeItem<S>>>(tableBackingList);
  64 
  65         flow.setFixedCellSize(treeTableView.getFixedCellSize());
  66 
  67         super.init(treeTableView);
  68 
  69         setRoot(getSkinnable().getRoot());
  70 
  71         EventHandler<MouseEvent> ml = event -> {
  72             // RT-15127: cancel editing on scroll. This is a bit extreme
  73             // (we are cancelling editing on touching the scrollbars).
  74             // This can be improved at a later date.
  75             if (treeTableView.getEditingCell() != null) {
  76                 treeTableView.edit(-1, null);
  77             }
  78 
  79             // This ensures that the table maintains the focus, even when the vbar
  80             // and hbar controls inside the flow are clicked. Without this, the
  81             // focus border will not be shown when the user interacts with the
  82             // scrollbars, and more importantly, keyboard navigation won't be
  83             // available to the user.
  84             if (treeTableView.isFocusTraversable()) {
  85                 treeTableView.requestFocus();
  86             }
  87         };
  88         flow.getVbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml);
  89         flow.getHbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml);
  90 
  91         // init the behavior 'closures'
  92         TreeTableViewBehavior<S> behavior = getBehavior();
  93         behavior.setOnFocusPreviousRow(() -> { onFocusPreviousCell(); });
  94         behavior.setOnFocusNextRow(() -> { onFocusNextCell(); });
  95         behavior.setOnMoveToFirstCell(() -> { onMoveToFirstCell(); });
  96         behavior.setOnMoveToLastCell(() -> { onMoveToLastCell(); });
  97         behavior.setOnScrollPageDown(isFocusDriven -> onScrollPageDown(isFocusDriven));
  98         behavior.setOnScrollPageUp(isFocusDriven -> onScrollPageUp(isFocusDriven));
  99         behavior.setOnSelectPreviousRow(() -> { onSelectPreviousCell(); });
 100         behavior.setOnSelectNextRow(() -> { onSelectNextCell(); });
 101         behavior.setOnSelectLeftCell(() -> { onSelectLeftCell(); });
 102         behavior.setOnSelectRightCell(() -> { onSelectRightCell(); });
 103 
 104         registerChangeListener(treeTableView.rootProperty(), "ROOT");
 105         registerChangeListener(treeTableView.showRootProperty(), "SHOW_ROOT");
 106         registerChangeListener(treeTableView.rowFactoryProperty(), "ROW_FACTORY");
 107         registerChangeListener(treeTableView.expandedItemCountProperty(), "TREE_ITEM_COUNT");
 108         registerChangeListener(treeTableView.fixedCellSizeProperty(), "FIXED_CELL_SIZE");
 109     }
 110 
 111     @Override protected void handleControlPropertyChanged(String p) {
 112         super.handleControlPropertyChanged(p);
 113 
 114         if ("ROOT".equals(p)) {
 115             // fix for RT-37853
 116             getSkinnable().edit(-1, null);
 117 
 118             setRoot(getSkinnable().getRoot());
 119         } else if ("SHOW_ROOT".equals(p)) {
 120             // if we turn off showing the root, then we must ensure the root
 121             // is expanded - otherwise we end up with no visible items in
 122             // the tree.
 123             if (! getSkinnable().isShowRoot() && getRoot() != null) {
 124                  getRoot().setExpanded(true);
 125             }
 126             // update the item count in the flow and behavior instances
 127             updateRowCount();
 128         } else if ("ROW_FACTORY".equals(p)) {
 129             flow.recreateCells();
 130         } else if ("TREE_ITEM_COUNT".equals(p)) {
 131             rowCountDirty = true;
 132         } else if ("FIXED_CELL_SIZE".equals(p)) {
 133             flow.setFixedCellSize(getSkinnable().getFixedCellSize());
 134         }
 135     }
 136 
 137     /***************************************************************************
 138      *                                                                         *
 139      * Listeners                                                               *
 140      *                                                                         *
 141      **************************************************************************/
 142 
 143 
 144 
 145     /***************************************************************************
 146      *                                                                         *
 147      * Internal Fields                                                         *
 148      *                                                                         *
 149      **************************************************************************/
 150 
 151     private TreeTableViewBackingList<S> tableBackingList;
 152     private ObjectProperty<ObservableList<TreeItem<S>>> tableBackingListProperty;
 153     private TreeTableView<S> treeTableView;
 154     private WeakReference<TreeItem<S>> weakRootRef;
 155 
 156     private EventHandler<TreeItem.TreeModificationEvent<S>> rootListener = e -> {
 157         if (e.wasAdded() && e.wasRemoved() && e.getAddedSize() == e.getRemovedSize()) {
 158             // Fix for RT-14842, where the children of a TreeItem were changing,
 159             // but because the overall item count was staying the same, there was
 160             // no event being fired to the skin to be informed that the items
 161             // had changed. So, here we just watch for the case where the number
 162             // of items being added is equal to the number of items being removed.
 163             rowCountDirty = true;
 164             getSkinnable().requestLayout();
 165         } else if (e.getEventType().equals(TreeItem.valueChangedEvent())) {
 166             // Fix for RT-14971 and RT-15338.
 167             needCellsRebuilt = true;
 168             getSkinnable().requestLayout();
 169         } else {
 170             // Fix for RT-20090. We are checking to see if the event coming
 171             // from the TreeItem root is an event where the count has changed.
 172             EventType<?> eventType = e.getEventType();
 173             while (eventType != null) {
 174                 if (eventType.equals(TreeItem.<S>expandedItemCountChangeEvent())) {
 175                     rowCountDirty = true;
 176                     getSkinnable().requestLayout();
 177                     break;
 178                 }
 179                 eventType = eventType.getSuperType();
 180             }
 181         }
 182 
 183         // fix for RT-37853
 184         getSkinnable().edit(-1, null);
 185     };
 186 
 187     private WeakEventHandler<TreeModificationEvent<S>> weakRootListener;
 188 
 189 
 190 //    private WeakReference<TreeItem> weakRoot;
 191     private TreeItem<S> getRoot() {
 192         return weakRootRef == null ? null : weakRootRef.get();
 193     }
 194     private void setRoot(TreeItem<S> newRoot) {
 195         if (getRoot() != null && weakRootListener != null) {
 196             getRoot().removeEventHandler(TreeItem.<S>treeNotificationEvent(), weakRootListener);
 197         }
 198         weakRootRef = new WeakReference<>(newRoot);
 199         if (getRoot() != null) {
 200             weakRootListener = new WeakEventHandler<>(rootListener);
 201             getRoot().addEventHandler(TreeItem.<S>treeNotificationEvent(), weakRootListener);
 202         }
 203 
 204         updateRowCount();
 205     }
 206 
 207 
 208     /***************************************************************************
 209      *                                                                         *
 210      * Public API                                                              *
 211      *                                                                         *
 212      **************************************************************************/
 213 
 214     /** {@inheritDoc} */
 215     @Override protected ObservableList<TreeTableColumn<S, ?>> getVisibleLeafColumns() {
 216         return treeTableView.getVisibleLeafColumns();
 217     }
 218 
 219     @Override protected int getVisibleLeafIndex(TreeTableColumn<S,?> tc) {
 220         return treeTableView.getVisibleLeafIndex(tc);
 221     }
 222 
 223     @Override protected TreeTableColumn<S,?> getVisibleLeafColumn(int col) {
 224         return treeTableView.getVisibleLeafColumn(col);
 225     }
 226 
 227     /** {@inheritDoc} */
 228     @Override protected TreeTableView.TreeTableViewFocusModel<S> getFocusModel() {
 229         return treeTableView.getFocusModel();
 230     }
 231 
 232     /** {@inheritDoc} */
 233     @Override protected TreeTablePosition<S, ?> getFocusedCell() {
 234         return treeTableView.getFocusModel().getFocusedCell();
 235     }
 236 
 237     /** {@inheritDoc} */
 238     @Override protected TableSelectionModel<TreeItem<S>> getSelectionModel() {
 239         return treeTableView.getSelectionModel();
 240     }
 241 
 242     /** {@inheritDoc} */
 243     @Override protected ObjectProperty<Callback<TreeTableView<S>, TreeTableRow<S>>> rowFactoryProperty() {
 244         return treeTableView.rowFactoryProperty();
 245     }
 246 
 247     /** {@inheritDoc} */
 248     @Override protected ObjectProperty<Node> placeholderProperty() {
 249         return treeTableView.placeholderProperty();
 250     }
 251 
 252     /** {@inheritDoc} */
 253     @Override protected ObjectProperty<ObservableList<TreeItem<S>>> itemsProperty() {
 254         return tableBackingListProperty;
 255     }
 256 
 257     /** {@inheritDoc} */
 258     @Override protected ObservableList<TreeTableColumn<S,?>> getColumns() {
 259         return treeTableView.getColumns();
 260     }
 261 
 262     /** {@inheritDoc} */
 263     @Override protected BooleanProperty tableMenuButtonVisibleProperty() {
 264         return treeTableView.tableMenuButtonVisibleProperty();
 265     }
 266 
 267     /** {@inheritDoc} */
 268     @Override protected ObjectProperty<Callback<ResizeFeaturesBase, Boolean>> columnResizePolicyProperty() {
 269         // TODO Ugly!
 270         return (ObjectProperty<Callback<ResizeFeaturesBase, Boolean>>) (Object) treeTableView.columnResizePolicyProperty();
 271     }
 272 
 273     /** {@inheritDoc} */
 274     @Override protected ObservableList<TreeTableColumn<S,?>> getSortOrder() {
 275         return treeTableView.getSortOrder();
 276     }
 277 
 278     @Override protected boolean resizeColumn(TreeTableColumn<S,?> tc, double delta) {
 279         return treeTableView.resizeColumn(tc, delta);
 280     }
 281 
 282     @Override protected void edit(int index, TreeTableColumn<S, ?> column) {
 283         treeTableView.edit(index, column);
 284     }
 285 
 286     /*
 287      * FIXME: Naive implementation ahead
 288      * Attempts to resize column based on the pref width of all items contained
 289      * in this column. This can be potentially very expensive if the number of
 290      * rows is large.
 291      */
 292     @Override protected void resizeColumnToFitContent(TreeTableColumn<S,?> tc, int maxRows) {
 293         final TreeTableColumn col = tc;
 294         List<?> items = itemsProperty().get();
 295         if (items == null || items.isEmpty()) return;
 296 
 297         Callback cellFactory = col.getCellFactory();
 298         if (cellFactory == null) return;
 299 
 300         TreeTableCell<S,?> cell = (TreeTableCell) cellFactory.call(col);
 301         if (cell == null) return;
 302 
 303         // set this property to tell the TableCell we want to know its actual
 304         // preferred width, not the width of the associated TableColumnBase
 305         cell.getProperties().put(TableCellSkin.DEFER_TO_PARENT_PREF_WIDTH, Boolean.TRUE);
 306 
 307         // determine cell padding
 308         double padding = 10;
 309         Node n = cell.getSkin() == null ? null : cell.getSkin().getNode();
 310         if (n instanceof Region) {
 311             Region r = (Region) n;
 312             padding = r.snappedLeftInset() + r.snappedRightInset();
 313         }
 314 
 315         TreeTableRow<S> treeTableRow = new TreeTableRow<>();
 316         treeTableRow.updateTreeTableView(treeTableView);
 317 
 318         int rows = maxRows == -1 ? items.size() : Math.min(items.size(), maxRows);
 319         double maxWidth = 0;
 320         for (int row = 0; row < rows; row++) {
 321             treeTableRow.updateIndex(row);
 322             treeTableRow.updateTreeItem(treeTableView.getTreeItem(row));
 323 
 324             cell.updateTreeTableColumn(col);
 325             cell.updateTreeTableView(treeTableView);
 326             cell.updateTreeTableRow(treeTableRow);
 327             cell.updateIndex(row);
 328 
 329             if ((cell.getText() != null && !cell.getText().isEmpty()) || cell.getGraphic() != null) {
 330                 getChildren().add(cell);
 331                 cell.applyCss();
 332 
 333                 double w = cell.prefWidth(-1);
 334 
 335                 maxWidth = Math.max(maxWidth, w);
 336                 getChildren().remove(cell);
 337             }
 338         }
 339 
 340         // dispose of the cell to prevent it retaining listeners (see RT-31015)
 341         cell.updateIndex(-1);
 342 
 343         // RT-36855 - take into account the column header text / graphic widths.
 344         // Magic 10 is to allow for sort arrow to appear without text truncation.
 345         TableColumnHeader header = getTableHeaderRow().getColumnHeaderFor(tc);
 346         double headerTextWidth = Utils.computeTextWidth(header.label.getFont(), tc.getText(), -1);
 347         Node graphic = header.label.getGraphic();
 348         double headerGraphicWidth = graphic == null ? 0 : graphic.prefWidth(-1) + header.label.getGraphicTextGap();
 349         double headerWidth = headerTextWidth + headerGraphicWidth + 10 + header.snappedLeftInset() + header.snappedRightInset();
 350         maxWidth = Math.max(maxWidth, headerWidth);
 351 
 352         // RT-23486
 353         maxWidth += padding;
 354         if(treeTableView.getColumnResizePolicy() == TreeTableView.CONSTRAINED_RESIZE_POLICY) {
 355             maxWidth = Math.max(maxWidth, col.getWidth());
 356         }
 357 
 358         col.impl_setWidth(maxWidth);
 359     }
 360 
 361     /** {@inheritDoc} */
 362     @Override public int getItemCount() {
 363         return treeTableView.getExpandedItemCount();
 364     }
 365 
 366     /** {@inheritDoc} */
 367     @Override public TreeTableRow<S> createCell() {
 368         TreeTableRow<S> cell;
 369 
 370         if (treeTableView.getRowFactory() != null) {
 371             cell = treeTableView.getRowFactory().call(treeTableView);
 372         } else {
 373             cell = new TreeTableRow<S>();
 374         }
 375 
 376         // If there is no disclosure node, then add one of my own
 377         if (cell.getDisclosureNode() == null) {
 378             final StackPane disclosureNode = new StackPane();
 379             disclosureNode.getStyleClass().setAll("tree-disclosure-node");
 380             disclosureNode.setMouseTransparent(true);
 381 
 382             final StackPane disclosureNodeArrow = new StackPane();
 383             disclosureNodeArrow.getStyleClass().setAll("arrow");
 384             disclosureNode.getChildren().add(disclosureNodeArrow);
 385 
 386             cell.setDisclosureNode(disclosureNode);
 387         }
 388 
 389         cell.updateTreeTableView(treeTableView);
 390         return cell;
 391     }
 392 
 393     @Override protected void horizontalScroll() {
 394         super.horizontalScroll();
 395         if (getSkinnable().getFixedCellSize() > 0) {
 396             flow.requestCellLayout();
 397         }
 398     }
 399 
 400     @Override
 401     protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 402         switch (attribute) {
 403             case ROW_AT_INDEX: {
 404                 final int rowIndex = (Integer)parameters[0];
 405                 return rowIndex < 0 ? null : flow.getPrivateCell(rowIndex);
 406             }
 407             case SELECTED_ITEMS: {
 408                 List<Node> selection = new ArrayList<>();
 409                 TreeTableView.TreeTableViewSelectionModel<S> sm = getSkinnable().getSelectionModel();
 410                 for (TreeTablePosition<S,?> pos : sm.getSelectedCells()) {
 411                     TreeTableRow<S> row = flow.getPrivateCell(pos.getRow());
 412                     if (row != null) selection.add(row);
 413                 }
 414                 return FXCollections.observableArrayList(selection);
 415             }
 416             case FOCUS_ITEM: // TableViewSkinBase
 417             case CELL_AT_ROW_COLUMN: // TableViewSkinBase
 418             case COLUMN_AT_INDEX: // TableViewSkinBase
 419             case HEADER: // TableViewSkinBase
 420             case VERTICAL_SCROLLBAR: // TableViewSkinBase
 421             case HORIZONTAL_SCROLLBAR: // TableViewSkinBase
 422             default: return super.queryAccessibleAttribute(attribute, parameters);
 423         }
 424     }
 425 
 426     @Override
 427     protected void executeAccessibleAction(AccessibleAction action, Object... parameters) {
 428         switch (action) {
 429             case SHOW_ITEM: {
 430                 Node item = (Node)parameters[0];
 431                 if (item instanceof TreeTableCell) {
 432                     @SuppressWarnings("unchecked")
 433                     TreeTableCell<S, ?> cell = (TreeTableCell<S, ?>)item;
 434                     flow.show(cell.getIndex());
 435                 }
 436                 break;
 437             }
 438             case SET_SELECTED_ITEMS: {
 439                 @SuppressWarnings("unchecked")
 440                 ObservableList<Node> items = (ObservableList<Node>)parameters[0];
 441                 if (items != null) {
 442                     TreeTableView.TreeTableViewSelectionModel<S> sm = getSkinnable().getSelectionModel();
 443                     if (sm != null) {
 444                         sm.clearSelection();
 445                         for (Node item : items) {
 446                             if (item instanceof TreeTableCell) {
 447                                 @SuppressWarnings("unchecked")
 448                                 TreeTableCell<S, ?> cell = (TreeTableCell<S, ?>)item;
 449                                 sm.select(cell.getIndex(), cell.getTableColumn());
 450                             }
 451                         }
 452                     }
 453                 }
 454                 break;
 455             }
 456             default: super.executeAccessibleAction(action, parameters);
 457         }
 458     }
 459 
 460     /***************************************************************************
 461      *                                                                         *
 462      * Layout                                                                  *
 463      *                                                                         *
 464      **************************************************************************/
 465 
 466 
 467 
 468 
 469     /***************************************************************************
 470      *                                                                         *
 471      * Private methods                                                         *
 472      *                                                                         *
 473      **************************************************************************/
 474 
 475     @Override protected void updateRowCount() {
 476         updatePlaceholderRegionVisibility();
 477 
 478         tableBackingList.resetSize();
 479 
 480         int oldCount = flow.getCellCount();
 481         int newCount = getItemCount();
 482 
 483         // if this is not called even when the count is the same, we get a
 484         // memory leak in VirtualFlow.sheet.children. This can probably be
 485         // optimised in the future when time permits.
 486         flow.setCellCount(newCount);
 487 
 488         if (forceCellRecreate) {
 489             needCellsRecreated = true;
 490             forceCellRecreate = false;
 491         } else if (newCount != oldCount) {
 492             needCellsRebuilt = true;
 493         } else {
 494             needCellsReconfigured = true;
 495         }
 496     }
 497 
 498     /**
 499      * A simple read only list structure that maps into the TreeTableView tree
 500      * structure.
 501      */
 502     private static class TreeTableViewBackingList<S> extends ReadOnlyUnbackedObservableList<TreeItem<S>> {
 503         private final TreeTableView<S> treeTable;
 504 
 505         private int size = -1;
 506 
 507         TreeTableViewBackingList(TreeTableView<S> treeTable) {
 508             this.treeTable = treeTable;
 509         }
 510 
 511         void resetSize() {
 512             int oldSize = size;
 513             size = -1;
 514 
 515             // TODO we can certainly make this better....but it may not really matter
 516             callObservers(new NonIterableChange.GenericAddRemoveChange<TreeItem<S>>(
 517                     0, oldSize, FXCollections.<TreeItem<S>>emptyObservableList(), this));
 518         }
 519 
 520         @Override public TreeItem<S> get(int i) {
 521             return treeTable.getTreeItem(i);
 522         }
 523 
 524         @Override public int size() {
 525             if (size == -1) {
 526                 size = treeTable.getExpandedItemCount();
 527             }
 528             return size;
 529         }
 530     }
 531 }