1 /* 2 * Copyright (c) 2010, 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.scene.control.Properties; 29 import com.sun.javafx.scene.control.behavior.BehaviorBase; 30 import javafx.beans.InvalidationListener; 31 import javafx.beans.Observable; 32 import javafx.beans.WeakInvalidationListener; 33 import javafx.collections.FXCollections; 34 import javafx.collections.MapChangeListener; 35 import javafx.collections.ObservableList; 36 import javafx.collections.ObservableMap; 37 import javafx.event.EventHandler; 38 import javafx.event.EventType; 39 import javafx.event.WeakEventHandler; 40 import javafx.scene.AccessibleAction; 41 import javafx.scene.AccessibleAttribute; 42 import javafx.scene.Node; 43 import javafx.scene.control.*; 44 import javafx.scene.control.TreeItem.TreeModificationEvent; 45 import javafx.scene.input.MouseEvent; 46 import javafx.scene.layout.HBox; 47 import javafx.scene.layout.StackPane; 48 import java.lang.ref.WeakReference; 49 import java.security.AccessController; 50 import java.security.PrivilegedAction; 51 import java.util.ArrayList; 52 import java.util.List; 53 54 import com.sun.javafx.scene.control.behavior.TreeViewBehavior; 55 56 /** 57 * Default skin implementation for the {@link TreeView} control. 58 * 59 * @see TreeView 60 * @since 9 61 */ 62 public class TreeViewSkin<T> extends VirtualContainerBase<TreeView<T>, TreeCell<T>> { 63 64 /*************************************************************************** 65 * * 66 * Static fields * 67 * * 68 **************************************************************************/ 69 70 // RT-34744 : IS_PANNABLE will be false unless 71 // javafx.scene.control.skin.TreeViewSkin.pannable 72 // is set to true. This is done in order to make TreeView functional 73 // on embedded systems with touch screens which do not generate scroll 74 // events for touch drag gestures. 75 private static final boolean IS_PANNABLE = 76 AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> Boolean.getBoolean("javafx.scene.control.skin.TreeViewSkin.pannable")); 77 78 79 80 /*************************************************************************** 81 * * 82 * Private fields * 83 * * 84 **************************************************************************/ 85 86 private final VirtualFlow<TreeCell<T>> flow; 87 private WeakReference<TreeItem<T>> weakRoot; 88 private final TreeViewBehavior<T> behavior; 89 90 // private boolean needItemCountUpdate = false; 91 private boolean needCellsRebuilt = true; 92 private boolean needCellsReconfigured = false; 93 94 95 96 /*************************************************************************** 97 * * 98 * Listeners * 99 * * 100 **************************************************************************/ 101 102 private MapChangeListener<Object, Object> propertiesMapListener = c -> { 103 if (! c.wasAdded()) return; 104 if (Properties.RECREATE.equals(c.getKey())) { 105 needCellsRebuilt = true; 106 getSkinnable().requestLayout(); 107 getSkinnable().getProperties().remove(Properties.RECREATE); 108 } 109 }; 110 111 private EventHandler<TreeModificationEvent<T>> rootListener = e -> { 112 if (e.wasAdded() && e.wasRemoved() && e.getAddedSize() == e.getRemovedSize()) { 113 // Fix for RT-14842, where the children of a TreeItem were changing, 114 // but because the overall item count was staying the same, there was 115 // no event being fired to the skin to be informed that the items 116 // had changed. So, here we just watch for the case where the number 117 // of items being added is equal to the number of items being removed. 118 rowCountDirty = true; 119 getSkinnable().requestLayout(); 120 } else if (e.getEventType().equals(TreeItem.valueChangedEvent())) { 121 // Fix for RT-14971 and RT-15338. 122 needCellsRebuilt = true; 123 getSkinnable().requestLayout(); 124 } else { 125 // Fix for RT-20090. We are checking to see if the event coming 126 // from the TreeItem root is an event where the count has changed. 127 EventType<?> eventType = e.getEventType(); 128 while (eventType != null) { 129 if (eventType.equals(TreeItem.<T>expandedItemCountChangeEvent())) { 130 rowCountDirty = true; 131 getSkinnable().requestLayout(); 132 break; 133 } 134 eventType = eventType.getSuperType(); 135 } 136 } 137 138 // fix for RT-37853 139 getSkinnable().edit(null); 140 }; 141 142 private WeakEventHandler<TreeModificationEvent<T>> weakRootListener; 143 144 145 146 /*************************************************************************** 147 * * 148 * Constructors * 149 * * 150 **************************************************************************/ 151 152 /** 153 * Creates a new TreeViewSkin instance, installing the necessary child 154 * nodes into the Control {@link Control#getChildren() children} list, as 155 * well as the necessary input mappings for handling key, mouse, etc events. 156 * 157 * @param control The control that this skin should be installed onto. 158 */ 159 public TreeViewSkin(final TreeView control) { 160 super(control); 161 162 // install default input map for the TreeView control 163 behavior = new TreeViewBehavior<>(control); 164 // control.setInputMap(behavior.getInputMap()); 165 166 // init the VirtualFlow 167 flow = getVirtualFlow(); 168 flow.setPannable(IS_PANNABLE); 169 flow.setCellFactory(flow1 -> createCell()); 170 flow.setFixedCellSize(control.getFixedCellSize()); 171 getChildren().add(flow); 172 173 setRoot(getSkinnable().getRoot()); 174 175 EventHandler<MouseEvent> ml = event -> { 176 // RT-15127: cancel editing on scroll. This is a bit extreme 177 // (we are cancelling editing on touching the scrollbars). 178 // This can be improved at a later date. 179 if (control.getEditingItem() != null) { 180 control.edit(null); 181 } 182 183 // This ensures that the tree maintains the focus, even when the vbar 184 // and hbar controls inside the flow are clicked. Without this, the 185 // focus border will not be shown when the user interacts with the 186 // scrollbars, and more importantly, keyboard navigation won't be 187 // available to the user. 188 if (control.isFocusTraversable()) { 189 control.requestFocus(); 190 } 191 }; 192 flow.getVbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml); 193 flow.getHbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml); 194 195 final ObservableMap<Object, Object> properties = control.getProperties(); 196 properties.remove(Properties.RECREATE); 197 properties.addListener(propertiesMapListener); 198 199 // init the behavior 'closures' 200 behavior.setOnFocusPreviousRow(() -> { onFocusPreviousCell(); }); 201 behavior.setOnFocusNextRow(() -> { onFocusNextCell(); }); 202 behavior.setOnMoveToFirstCell(() -> { onMoveToFirstCell(); }); 203 behavior.setOnMoveToLastCell(() -> { onMoveToLastCell(); }); 204 behavior.setOnScrollPageDown(this::onScrollPageDown); 205 behavior.setOnScrollPageUp(this::onScrollPageUp); 206 behavior.setOnSelectPreviousRow(() -> { onSelectPreviousCell(); }); 207 behavior.setOnSelectNextRow(() -> { onSelectNextCell(); }); 208 209 registerChangeListener(control.rootProperty(), e -> setRoot(getSkinnable().getRoot())); 210 registerChangeListener(control.showRootProperty(), e -> { 211 // if we turn off showing the root, then we must ensure the root 212 // is expanded - otherwise we end up with no visible items in 213 // the tree. 214 if (! getSkinnable().isShowRoot() && getRoot() != null) { 215 getRoot().setExpanded(true); 216 } 217 // update the item count in the flow and behavior instances 218 updateRowCount(); 219 }); 220 registerChangeListener(control.cellFactoryProperty(), e -> flow.recreateCells()); 221 registerChangeListener(control.fixedCellSizeProperty(), e -> flow.setFixedCellSize(getSkinnable().getFixedCellSize())); 222 223 updateRowCount(); 224 } 225 226 227 228 /*************************************************************************** 229 * * 230 * Public API * 231 * * 232 **************************************************************************/ 233 234 /** {@inheritDoc} */ 235 @Override public void dispose() { 236 super.dispose(); 237 238 if (behavior != null) { 239 behavior.dispose(); 240 } 241 } 242 243 /** {@inheritDoc} */ 244 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 245 return computePrefHeight(-1, topInset, rightInset, bottomInset, leftInset) * 0.618033987; 246 } 247 248 /** {@inheritDoc} */ 249 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 250 return 400; 251 } 252 253 /** {@inheritDoc} */ 254 @Override protected void layoutChildren(final double x, final double y, 255 final double w, final double h) { 256 super.layoutChildren(x, y, w, h); 257 258 if (needCellsRebuilt) { 259 flow.rebuildCells(); 260 } else if (needCellsReconfigured) { 261 flow.reconfigureCells(); 262 } 263 264 needCellsRebuilt = false; 265 needCellsReconfigured = false; 266 267 flow.resizeRelocate(x, y, w, h); 268 } 269 270 /** {@inheritDoc} */ 271 @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 272 switch (attribute) { 273 case FOCUS_ITEM: { 274 FocusModel<?> fm = getSkinnable().getFocusModel(); 275 int focusedIndex = fm.getFocusedIndex(); 276 if (focusedIndex == -1) { 277 if (getItemCount() > 0) { 278 focusedIndex = 0; 279 } else { 280 return null; 281 } 282 } 283 return flow.getPrivateCell(focusedIndex); 284 } 285 case ROW_AT_INDEX: { 286 final int rowIndex = (Integer)parameters[0]; 287 return rowIndex < 0 ? null : flow.getPrivateCell(rowIndex); 288 } 289 case SELECTED_ITEMS: { 290 MultipleSelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel(); 291 ObservableList<Integer> indices = sm.getSelectedIndices(); 292 List<Node> selection = new ArrayList<>(indices.size()); 293 for (int i : indices) { 294 TreeCell<T> row = flow.getPrivateCell(i); 295 if (row != null) selection.add(row); 296 } 297 return FXCollections.observableArrayList(selection); 298 } 299 case VERTICAL_SCROLLBAR: return flow.getVbar(); 300 case HORIZONTAL_SCROLLBAR: return flow.getHbar(); 301 default: return super.queryAccessibleAttribute(attribute, parameters); 302 } 303 } 304 305 /** {@inheritDoc} */ 306 @Override protected void executeAccessibleAction(AccessibleAction action, Object... parameters) { 307 switch (action) { 308 case SHOW_ITEM: { 309 Node item = (Node)parameters[0]; 310 if (item instanceof TreeCell) { 311 @SuppressWarnings("unchecked") 312 TreeCell<T> cell = (TreeCell<T>)item; 313 flow.scrollTo(cell.getIndex()); 314 } 315 break; 316 } 317 case SET_SELECTED_ITEMS: { 318 @SuppressWarnings("unchecked") 319 ObservableList<Node> items = (ObservableList<Node>)parameters[0]; 320 if (items != null) { 321 MultipleSelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel(); 322 if (sm != null) { 323 sm.clearSelection(); 324 for (Node item : items) { 325 if (item instanceof TreeCell) { 326 @SuppressWarnings("unchecked") 327 TreeCell<T> cell = (TreeCell<T>)item; 328 sm.select(cell.getIndex()); 329 } 330 } 331 } 332 } 333 break; 334 } 335 default: super.executeAccessibleAction(action, parameters); 336 } 337 } 338 339 340 /*************************************************************************** 341 * * 342 * Private implementation * 343 * * 344 **************************************************************************/ 345 346 /** {@inheritDoc} */ 347 private TreeCell<T> createCell() { 348 final TreeCell<T> cell; 349 if (getSkinnable().getCellFactory() != null) { 350 cell = getSkinnable().getCellFactory().call(getSkinnable()); 351 } else { 352 cell = createDefaultCellImpl(); 353 } 354 355 // If there is no disclosure node, then add one of my own 356 if (cell.getDisclosureNode() == null) { 357 final StackPane disclosureNode = new StackPane(); 358 359 /* This code is intentionally commented. 360 * Currently as it stands it does provided any functionality and interferes 361 * with TreeView. The VO cursor move over the DISCLOSURE_NODE instead of the 362 * tree item itself. This is possibly caused by the order of item's children 363 * (the Labeled and the disclosure node). 364 */ 365 // final StackPane disclosureNode = new StackPane() { 366 // @Override protected Object accGetAttribute(Attribute attribute, Object... parameters) { 367 // switch (attribute) { 368 // case ROLE: return Role.DISCLOSURE_NODE; 369 // default: return super.accGetAttribute(attribute, parameters); 370 // } 371 // } 372 // }; 373 disclosureNode.getStyleClass().setAll("tree-disclosure-node"); 374 375 final StackPane disclosureNodeArrow = new StackPane(); 376 disclosureNodeArrow.getStyleClass().setAll("arrow"); 377 disclosureNode.getChildren().add(disclosureNodeArrow); 378 379 cell.setDisclosureNode(disclosureNode); 380 } 381 382 cell.updateTreeView(getSkinnable()); 383 384 return cell; 385 } 386 387 private TreeItem<T> getRoot() { 388 return weakRoot == null ? null : weakRoot.get(); 389 } 390 private void setRoot(TreeItem<T> newRoot) { 391 if (getRoot() != null && weakRootListener != null) { 392 getRoot().removeEventHandler(TreeItem.<T>treeNotificationEvent(), weakRootListener); 393 } 394 weakRoot = new WeakReference<>(newRoot); 395 if (getRoot() != null) { 396 weakRootListener = new WeakEventHandler<>(rootListener); 397 getRoot().addEventHandler(TreeItem.<T>treeNotificationEvent(), weakRootListener); 398 } 399 400 updateRowCount(); 401 } 402 403 /** {@inheritDoc} */ 404 @Override int getItemCount() { 405 return getSkinnable().getExpandedItemCount(); 406 } 407 408 /** {@inheritDoc} */ 409 @Override void updateRowCount() { 410 // int oldCount = flow.getCellCount(); 411 int newCount = getItemCount(); 412 413 // if this is not called even when the count is the same, we get a 414 // memory leak in VirtualFlow.sheet.children. This can probably be 415 // optimised in the future when time permits. 416 flow.setCellCount(newCount); 417 418 // Ideally we would be more nuanced here, toggling a cheaper needs* 419 // field, but if we do we hit issues such as those identified in 420 // RT-27852, where the expended item count of the new root equals the 421 // EIC of the old root, which would lead to the visuals not updating 422 // properly. 423 needCellsRebuilt = true; 424 getSkinnable().requestLayout(); 425 } 426 427 // Note: This is a copy/paste of javafx.scene.control.cell.DefaultTreeCell, 428 // which is package-protected 429 private TreeCell<T> createDefaultCellImpl() { 430 return new TreeCell<T>() { 431 private HBox hbox; 432 433 private WeakReference<TreeItem<T>> treeItemRef; 434 435 private InvalidationListener treeItemGraphicListener = observable -> { 436 updateDisplay(getItem(), isEmpty()); 437 }; 438 439 private InvalidationListener treeItemListener = new InvalidationListener() { 440 @Override public void invalidated(Observable observable) { 441 TreeItem<T> oldTreeItem = treeItemRef == null ? null : treeItemRef.get(); 442 if (oldTreeItem != null) { 443 oldTreeItem.graphicProperty().removeListener(weakTreeItemGraphicListener); 444 } 445 446 TreeItem<T> newTreeItem = getTreeItem(); 447 if (newTreeItem != null) { 448 newTreeItem.graphicProperty().addListener(weakTreeItemGraphicListener); 449 treeItemRef = new WeakReference<TreeItem<T>>(newTreeItem); 450 } 451 } 452 }; 453 454 private WeakInvalidationListener weakTreeItemGraphicListener = 455 new WeakInvalidationListener(treeItemGraphicListener); 456 457 private WeakInvalidationListener weakTreeItemListener = 458 new WeakInvalidationListener(treeItemListener); 459 460 { 461 treeItemProperty().addListener(weakTreeItemListener); 462 463 if (getTreeItem() != null) { 464 getTreeItem().graphicProperty().addListener(weakTreeItemGraphicListener); 465 } 466 } 467 468 private void updateDisplay(T item, boolean empty) { 469 if (item == null || empty) { 470 hbox = null; 471 setText(null); 472 setGraphic(null); 473 } else { 474 // update the graphic if one is set in the TreeItem 475 TreeItem<T> treeItem = getTreeItem(); 476 Node graphic = treeItem == null ? null : treeItem.getGraphic(); 477 if (graphic != null) { 478 if (item instanceof Node) { 479 setText(null); 480 481 // the item is a Node, and the graphic exists, so 482 // we must insert both into an HBox and present that 483 // to the user (see RT-15910) 484 if (hbox == null) { 485 hbox = new HBox(3); 486 } 487 hbox.getChildren().setAll(graphic, (Node)item); 488 setGraphic(hbox); 489 } else { 490 hbox = null; 491 setText(item.toString()); 492 setGraphic(graphic); 493 } 494 } else { 495 hbox = null; 496 if (item instanceof Node) { 497 setText(null); 498 setGraphic((Node)item); 499 } else { 500 setText(item.toString()); 501 setGraphic(null); 502 } 503 } 504 } 505 } 506 507 @Override public void updateItem(T item, boolean empty) { 508 super.updateItem(item, empty); 509 updateDisplay(item, empty); 510 } 511 }; 512 } 513 514 private void onFocusPreviousCell() { 515 FocusModel<TreeItem<T>> fm = getSkinnable().getFocusModel(); 516 if (fm == null) return; 517 flow.scrollTo(fm.getFocusedIndex()); 518 } 519 520 private void onFocusNextCell() { 521 FocusModel<TreeItem<T>> fm = getSkinnable().getFocusModel(); 522 if (fm == null) return; 523 flow.scrollTo(fm.getFocusedIndex()); 524 } 525 526 private void onSelectPreviousCell() { 527 int row = getSkinnable().getSelectionModel().getSelectedIndex(); 528 flow.scrollTo(row); 529 } 530 531 private void onSelectNextCell() { 532 int row = getSkinnable().getSelectionModel().getSelectedIndex(); 533 flow.scrollTo(row); 534 } 535 536 private void onMoveToFirstCell() { 537 flow.scrollTo(0); 538 flow.setPosition(0); 539 } 540 541 private void onMoveToLastCell() { 542 flow.scrollTo(getItemCount()); 543 flow.setPosition(1); 544 } 545 546 /** 547 * Function used to scroll the container down by one 'page'. 548 */ 549 private int onScrollPageDown(boolean isFocusDriven) { 550 TreeCell<T> lastVisibleCell = flow.getLastVisibleCellWithinViewPort(); 551 if (lastVisibleCell == null) return -1; 552 553 final SelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel(); 554 final FocusModel<TreeItem<T>> fm = getSkinnable().getFocusModel(); 555 if (sm == null || fm == null) return -1; 556 557 int lastVisibleCellIndex = lastVisibleCell.getIndex(); 558 559 // isSelected represents focus OR selection 560 boolean isSelected = false; 561 if (isFocusDriven) { 562 isSelected = lastVisibleCell.isFocused() || fm.isFocused(lastVisibleCellIndex); 563 } else { 564 isSelected = lastVisibleCell.isSelected() || sm.isSelected(lastVisibleCellIndex); 565 } 566 567 if (isSelected) { 568 boolean isLeadIndex = (isFocusDriven && fm.getFocusedIndex() == lastVisibleCellIndex) 569 || (! isFocusDriven && sm.getSelectedIndex() == lastVisibleCellIndex); 570 571 if (isLeadIndex) { 572 // if the last visible cell is selected, we want to shift that cell up 573 // to be the top-most cell, or at least as far to the top as we can go. 574 flow.scrollToTop(lastVisibleCell); 575 576 TreeCell<T> newLastVisibleCell = flow.getLastVisibleCellWithinViewPort(); 577 lastVisibleCell = newLastVisibleCell == null ? lastVisibleCell : newLastVisibleCell; 578 } 579 } else { 580 // if the selection is not on the 'bottom' most cell, we firstly move 581 // the selection down to that, without scrolling the contents, so 582 // this is a no-op 583 } 584 585 int newSelectionIndex = lastVisibleCell.getIndex(); 586 flow.scrollTo(lastVisibleCell); 587 return newSelectionIndex; 588 } 589 590 /** 591 * Function used to scroll the container up by one 'page'. 592 */ 593 private int onScrollPageUp(boolean isFocusDriven) { 594 TreeCell<T> firstVisibleCell = flow.getFirstVisibleCellWithinViewPort(); 595 if (firstVisibleCell == null) return -1; 596 597 final SelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel(); 598 final FocusModel<TreeItem<T>> fm = getSkinnable().getFocusModel(); 599 if (sm == null || fm == null) return -1; 600 601 int firstVisibleCellIndex = firstVisibleCell.getIndex(); 602 603 // isSelected represents focus OR selection 604 boolean isSelected = false; 605 if (isFocusDriven) { 606 isSelected = firstVisibleCell.isFocused() || fm.isFocused(firstVisibleCellIndex); 607 } else { 608 isSelected = firstVisibleCell.isSelected() || sm.isSelected(firstVisibleCellIndex); 609 } 610 611 if (isSelected) { 612 boolean isLeadIndex = (isFocusDriven && fm.getFocusedIndex() == firstVisibleCellIndex) 613 || (! isFocusDriven && sm.getSelectedIndex() == firstVisibleCellIndex); 614 615 if (isLeadIndex) { 616 // if the first visible cell is selected, we want to shift that cell down 617 // to be the bottom-most cell, or at least as far to the bottom as we can go. 618 flow.scrollToBottom(firstVisibleCell); 619 620 TreeCell<T> newFirstVisibleCell = flow.getFirstVisibleCellWithinViewPort(); 621 firstVisibleCell = newFirstVisibleCell == null ? firstVisibleCell : newFirstVisibleCell; 622 } 623 } else { 624 // if the selection is not on the 'top' most cell, we firstly move 625 // the selection up to that, without scrolling the contents, so 626 // this is a no-op 627 } 628 629 int newSelectionIndex = firstVisibleCell.getIndex(); 630 flow.scrollTo(firstVisibleCell); 631 return newSelectionIndex; 632 } 633 }