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