modules/controls/src/main/java/javafx/scene/control/skin/TreeViewSkin.java

Print this page
rev 9240 : 8076423: JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization


   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();


 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 }


   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();


 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 }