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 }