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