1 /*
   2  * Copyright (c) 2012, 2017, 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;
  27 
  28 import com.sun.javafx.collections.MappingChange;
  29 import com.sun.javafx.collections.NonIterableChange;
  30 import com.sun.javafx.scene.control.Properties;
  31 import com.sun.javafx.scene.control.SelectedCellsMap;
  32 
  33 import com.sun.javafx.scene.control.behavior.TableCellBehavior;
  34 import com.sun.javafx.scene.control.behavior.TableCellBehaviorBase;
  35 import com.sun.javafx.scene.control.behavior.TreeTableCellBehavior;
  36 import javafx.beans.property.DoubleProperty;
  37 import javafx.css.CssMetaData;
  38 import javafx.css.PseudoClass;
  39 
  40 import javafx.css.converter.SizeConverter;
  41 import com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList;
  42 import com.sun.javafx.scene.control.TableColumnComparatorBase;
  43 
  44 import javafx.css.Styleable;
  45 import javafx.css.StyleableDoubleProperty;
  46 import javafx.css.StyleableProperty;
  47 import javafx.event.WeakEventHandler;
  48 
  49 import javafx.scene.control.skin.TreeTableViewSkin;
  50 
  51 import java.lang.ref.SoftReference;
  52 import java.lang.ref.WeakReference;
  53 import java.util.ArrayList;
  54 import java.util.Collection;
  55 import java.util.Collections;
  56 import java.util.Comparator;
  57 import java.util.HashMap;
  58 import java.util.LinkedHashSet;
  59 import java.util.List;
  60 import java.util.Map;
  61 import java.util.Set;
  62 import java.util.WeakHashMap;
  63 
  64 import javafx.application.Platform;
  65 import javafx.beans.DefaultProperty;
  66 import javafx.beans.InvalidationListener;
  67 import javafx.beans.WeakInvalidationListener;
  68 import javafx.beans.property.BooleanProperty;
  69 import javafx.beans.property.ObjectProperty;
  70 import javafx.beans.property.ObjectPropertyBase;
  71 import javafx.beans.property.ReadOnlyIntegerProperty;
  72 import javafx.beans.property.ReadOnlyIntegerWrapper;
  73 import javafx.beans.property.ReadOnlyObjectProperty;
  74 import javafx.beans.property.ReadOnlyObjectWrapper;
  75 import javafx.beans.property.SimpleBooleanProperty;
  76 import javafx.beans.property.SimpleObjectProperty;
  77 import javafx.beans.value.ChangeListener;
  78 import javafx.beans.value.WeakChangeListener;
  79 import javafx.beans.value.WritableValue;
  80 import javafx.collections.FXCollections;
  81 import javafx.collections.ListChangeListener;
  82 import javafx.collections.MapChangeListener;
  83 import javafx.collections.ObservableList;
  84 import javafx.collections.WeakListChangeListener;
  85 import javafx.event.Event;
  86 import javafx.event.EventHandler;
  87 import javafx.event.EventType;
  88 import javafx.scene.AccessibleAttribute;
  89 import javafx.scene.AccessibleRole;
  90 import javafx.scene.Node;
  91 import javafx.scene.layout.Region;
  92 import javafx.util.Callback;
  93 
  94 /**
  95  * The TreeTableView control is designed to visualize an unlimited number of rows
  96  * of data, broken out into columns. The TreeTableView control is conceptually
  97  * very similar to the {@link TreeView} and {@link TableView} controls,
  98  * and as you read on you'll come to see the APIs are largely the same.
  99  * However, to give a high-level overview, you'll note that the TreeTableView
 100  * uses the same {@link TreeItem} API as {@link TreeView},
 101  * and that you therefore are required to simply set the
 102  * {@link #rootProperty() root node} in the TreeTableView. Similarly, the
 103  * TreeTableView control makes use of the same TableColumn-based approach that
 104  * the {@link TableView} control uses, except instead of using the
 105  * TableView-specific {@link TableColumn} class, you should instead use the
 106  * TreeTableView-specific {@link TreeTableColumn} class instead. For an
 107  * example on how to create a TreeTableView instance, refer to the 'Creating a
 108  * TreeTableView' control section below.
 109  *
 110  * <p>As with the {@link TableView} control, the TreeTableView control has a
 111  * number of features, including:
 112  * <ul>
 113  * <li>Powerful {@link TreeTableColumn} API:
 114  *   <ul>
 115  *   <li>Support for {@link TreeTableColumn#cellFactoryProperty() cell factories} to
 116  *      easily customize {@link Cell cell} contents in both rendering and editing
 117  *      states.
 118  *   <li>Specification of {@link TreeTableColumn#minWidthProperty() minWidth}/
 119  *      {@link TreeTableColumn#prefWidthProperty() prefWidth}/
 120  *      {@link TreeTableColumn#maxWidthProperty() maxWidth},
 121  *      and also {@link TreeTableColumn#resizableProperty() fixed width columns}.
 122  *   <li>Width resizing by the user at runtime.
 123  *   <li>Column reordering by the user at runtime.
 124  *   <li>Built-in support for {@link TreeTableColumn#getColumns() column nesting}
 125  *   </ul>
 126  * <li>Different {@link #columnResizePolicyProperty() resizing policies} to
 127  *      dictate what happens when the user resizes columns.
 128  * <li>Support for {@link #getSortOrder() multiple column sorting} by clicking
 129  *      the column header (hold down Shift keyboard key whilst clicking on a
 130  *      header to sort by multiple columns).
 131  * </ul>
 132  *
 133  * <h2>Creating a TreeTableView</h2>
 134  *
 135  * <p>Creating a TreeTableView is a multi-step process, and also depends on the
 136  * underlying data model needing to be represented. For this example we'll use
 137  * the TreeTableView to visualise a file system, and will therefore make use
 138  * of an imaginary (and vastly simplified) File class as defined below:
 139  *
 140  * <pre>{@code
 141  * public class File {
 142  *     private StringProperty name;
 143  *     public void setName(String value) { nameProperty().set(value); }
 144  *     public String getName() { return nameProperty().get(); }
 145  *     public StringProperty nameProperty() {
 146  *         if (name == null) name = new SimpleStringProperty(this, "name");
 147  *         return name;
 148  *     }
 149  *
 150  *     private LongProperty lastModified;
 151  *     public void setLastModified(long value) { lastModifiedProperty().set(value); }
 152  *     public long getLastModified() { return lastModifiedProperty().get(); }
 153  *     public LongProperty lastModifiedProperty() {
 154  *         if (lastModified == null) lastModified = new SimpleLongProperty(this, "lastModified");
 155  *         return lastModified;
 156  *     }
 157  * }}</pre>
 158  *
 159  * <p>Firstly, a TreeTableView instance needs to be defined, as such:
 160  *
 161  * <pre>{@code
 162  * TreeTableView<File> treeTable = new TreeTableView<>();}</pre>
 163  *
 164  * <p>With the basic TreeTableView instantiated, we next focus on the data model.
 165  * As mentioned, for this example, we'll be representing a file system using File
 166  * instances. To do this, we need to define the root node of the tree table, as such:
 167  *
 168  * <pre>{@code
 169  * TreeItem<File> root = new TreeItem<>(new File("/"));
 170  * treeTable.setRoot(root);}</pre>
 171  *
 172  * <p>With the root set as such, the TreeTableView will automatically update whenever
 173  * the {@link TreeItem#getChildren() children} of the root changes.
 174  *
 175  * <p>At this point we now have a TreeTableView hooked up to observe the root
 176  * TreeItem instance. The missing ingredient
 177  * now is the means of splitting out the data contained within the model and
 178  * representing it in one or more {@link TreeTableColumn} instances. To
 179  * create a two-column TreeTableView to show the file name and last modified
 180  * properties, we extend the code shown above as follows:
 181  *
 182  * <pre>{@code
 183  * TreeTableColumns<File,String> fileNameCol = new TreeTableColumn<>("Filename");
 184  * TreeTableColumns<File,Long> lastModifiedCol = new TreeTableColumn<>("Size");
 185  *
 186  * table.getColumns().setAll(fileNameCol, lastModifiedCol);}</pre>
 187  *
 188  * <p>With the code shown above we have nearly fully defined the minimum properties
 189  * required to create a TreeTableView instance. The only thing missing is the
 190  * {@link javafx.scene.control.TreeTableColumn#cellValueFactoryProperty() cell value factories}
 191  * for the two columns - it is these that are responsible for determining the value
 192  * of a cell in a given row. Commonly these can be specified using the
 193  * {@link javafx.scene.control.cell.TreeItemPropertyValueFactory} class, but
 194  * failing that you can also create an anonymous inner class and do whatever is
 195  * necessary. For example, using {@link javafx.scene.control.cell.TreeItemPropertyValueFactory}
 196  * you would do the following:
 197  *
 198  * <pre>{@code
 199  * fileNameCol.setCellValueFactory(new TreeItemPropertyValueFactory("name"));
 200  * lastModifiedCol.setCellValueFactory(new TreeItemPropertyValueFactory("lastModified"));}</pre>
 201  *
 202  * Running this code (assuming the file system structure is probably built up in
 203  * memory) will result in a TreeTableView being shown with two columns for name
 204  * and lastModified. Any other properties of the File class will not be shown, as
 205  * no TreeTableColumns are defined for them.
 206  *
 207  * <h3>TreeTableView support for classes that don't contain properties</h3>
 208  *
 209  * <p>The code shown above is the shortest possible code for creating a TreeTableView
 210  * when the domain objects are designed with JavaFX properties in mind
 211  * (additionally, {@link javafx.scene.control.cell.TreeItemPropertyValueFactory} supports
 212  * normal JavaBean properties too, although there is a caveat to this, so refer
 213  * to the class documentation for more information). When this is not the case,
 214  * it is necessary to provide a custom cell value factory. More information
 215  * about cell value factories can be found in the {@link TreeTableColumn} API
 216  * documentation, but briefly, here is how a TreeTableColumns could be specified:
 217  *
 218  * <pre>{@code
 219  * firstNameCol.setCellValueFactory(new Callback<CellDataFeatures<Person, String>, ObservableValue<String>>() {
 220  *     public ObservableValue<String> call(CellDataFeatures<Person, String> p) {
 221  *         // p.getValue() returns the TreeItem<Person> instance for a particular TreeTableView row,
 222  *         // p.getValue().getValue() returns the Person instance inside the TreeItem<Person>
 223  *         return p.getValue().getValue().firstNameProperty();
 224  *     }
 225  *  });
 226  * }}</pre>
 227  *
 228  * <h3>TreeTableView Selection / Focus APIs</h3>
 229  * <p>To track selection and focus, it is necessary to become familiar with the
 230  * {@link SelectionModel} and {@link FocusModel} classes. A TreeTableView has at most
 231  * one instance of each of these classes, available from
 232  * {@link #selectionModelProperty() selectionModel} and
 233  * {@link #focusModelProperty() focusModel} properties, respectively.
 234  * Whilst it is possible to use this API to set a new selection model, in
 235  * most circumstances this is not necessary - the default selection and focus
 236  * models should work in most circumstances.
 237  *
 238  * <p>The default {@link SelectionModel} used when instantiating a TreeTableView is
 239  * an implementation of the {@link MultipleSelectionModel} abstract class.
 240  * However, as noted in the API documentation for
 241  * the {@link MultipleSelectionModel#selectionModeProperty() selectionMode}
 242  * property, the default value is {@link SelectionMode#SINGLE}. To enable
 243  * multiple selection in a default TreeTableView instance, it is therefore necessary
 244  * to do the following:
 245  *
 246  * <pre>
 247  * {@code
 248  * treeTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);}</pre>
 249  *
 250  * <h3>Customizing TreeTableView Visuals</h3>
 251  * <p>The visuals of the TreeTableView can be entirely customized by replacing the
 252  * default {@link #rowFactoryProperty() row factory}. A row factory is used to
 253  * generate {@link TreeTableRow} instances, which are used to represent an entire
 254  * row in the TreeTableView.
 255  *
 256  * <p>In many cases, this is not what is desired however, as it is more commonly
 257  * the case that cells be customized on a per-column basis, not a per-row basis.
 258  * It is therefore important to note that a {@link TreeTableRow} is not a
 259  * {@link TreeTableCell}. A  {@link TreeTableRow} is simply a container for zero or more
 260  * {@link TreeTableCell}, and in most circumstances it is more likely that you'll
 261  * want to create custom TreeTableCells, rather than TreeTableRows. The primary use case
 262  * for creating custom TreeTableRow instances would most probably be to introduce
 263  * some form of column spanning support.
 264  *
 265  * <p>You can create custom {@link TreeTableCell} instances per column by assigning
 266  * the appropriate function to the TreeTableColumns
 267  * {@link TreeTableColumn#cellFactoryProperty() cell factory} property.
 268  *
 269  * <p>See the {@link Cell} class documentation for a more complete
 270  * description of how to write custom Cells.
 271  *
 272  * <h3>Editing</h3>
 273  * <p>This control supports inline editing of values, and this section attempts to
 274  * give an overview of the available APIs and how you should use them.</p>
 275  *
 276  * <p>Firstly, cell editing most commonly requires a different user interface
 277  * than when a cell is not being edited. This is the responsibility of the
 278  * {@link Cell} implementation being used. For TreeTableView, it is highly
 279  * recommended that editing be
 280  * {@link javafx.scene.control.TreeTableColumn#cellFactoryProperty() per-TreeTableColumn},
 281  * rather than {@link #rowFactoryProperty() per row}, as more often than not
 282  * you want users to edit each column value differently, and this approach allows
 283  * for editors specific to each column. It is your choice whether the cell is
 284  * permanently in an editing state (e.g. this is common for {@link CheckBox} cells),
 285  * or to switch to a different UI when editing begins (e.g. when a double-click
 286  * is received on a cell).</p>
 287  *
 288  * <p>To know when editing has been requested on a cell,
 289  * simply override the {@link javafx.scene.control.Cell#startEdit()} method, and
 290  * update the cell {@link javafx.scene.control.Cell#textProperty() text} and
 291  * {@link javafx.scene.control.Cell#graphicProperty() graphic} properties as
 292  * appropriate (e.g. set the text to null and set the graphic to be a
 293  * {@link TextField}). Additionally, you should also override
 294  * {@link Cell#cancelEdit()} to reset the UI back to its original visual state
 295  * when the editing concludes. In both cases it is important that you also
 296  * ensure that you call the super method to have the cell perform all duties it
 297  * must do to enter or exit its editing mode.</p>
 298  *
 299  * <p>Once your cell is in an editing state, the next thing you are most probably
 300  * interested in is how to commit or cancel the editing that is taking place. This is your
 301  * responsibility as the cell factory provider. Your cell implementation will know
 302  * when the editing is over, based on the user input (e.g. when the user presses
 303  * the Enter or ESC keys on their keyboard). When this happens, it is your
 304  * responsibility to call {@link Cell#commitEdit(Object)} or
 305  * {@link Cell#cancelEdit()}, as appropriate.</p>
 306  *
 307  * <p>When you call {@link Cell#commitEdit(Object)} an event is fired to the
 308  * TreeTableView, which you can observe by adding an {@link EventHandler} via
 309  * {@link TreeTableColumn#setOnEditCommit(javafx.event.EventHandler)}. Similarly,
 310  * you can also observe edit events for
 311  * {@link TreeTableColumn#setOnEditStart(javafx.event.EventHandler) edit start}
 312  * and {@link TreeTableColumn#setOnEditCancel(javafx.event.EventHandler) edit cancel}.</p>
 313  *
 314  * <p>By default the TreeTableColumn edit commit handler is non-null, with a default
 315  * handler that attempts to overwrite the property value for the
 316  * item in the currently-being-edited row. It is able to do this as the
 317  * {@link Cell#commitEdit(Object)} method is passed in the new value, and this
 318  * is passed along to the edit commit handler via the
 319  * {@link javafx.scene.control.TreeTableColumn.CellEditEvent CellEditEvent} that is
 320  * fired. It is simply a matter of calling
 321  * {@link javafx.scene.control.TreeTableColumn.CellEditEvent#getNewValue()} to
 322  * retrieve this value.
 323  *
 324  * <p>It is very important to note that if you call
 325  * {@link TreeTableColumn#setOnEditCommit(javafx.event.EventHandler)} with your own
 326  * {@link EventHandler}, then you will be removing the default handler. Unless
 327  * you then handle the writeback to the property (or the relevant data source),
 328  * nothing will happen. You can work around this by using the
 329  * {@link TreeTableColumn#addEventHandler(javafx.event.EventType, javafx.event.EventHandler)}
 330  * method to add a {@link TreeTableColumn#EDIT_COMMIT_EVENT} {@link EventType} with
 331  * your desired {@link EventHandler} as the second argument. Using this method,
 332  * you will not replace the default implementation, but you will be notified when
 333  * an edit commit has occurred.</p>
 334  *
 335  * <p>Hopefully this summary answers some of the commonly asked questions.
 336  * Fortunately, JavaFX ships with a number of pre-built cell factories that
 337  * handle all the editing requirements on your behalf. You can find these
 338  * pre-built cell factories in the javafx.scene.control.cell package.</p>
 339  *
 340  * @see TreeTableColumn
 341  * @see TreeTablePosition
 342  * @param <S> The type of the TreeItem instances used in this TreeTableView.
 343  * @since JavaFX 8.0
 344  */
 345 @DefaultProperty("root")
 346 public class TreeTableView<S> extends Control {
 347 
 348     /***************************************************************************
 349      *                                                                         *
 350      * Constructors                                                            *
 351      *                                                                         *
 352      **************************************************************************/
 353 
 354     /**
 355      * Creates an empty TreeTableView.
 356      *
 357      * <p>Refer to the {@link TreeTableView} class documentation for details on the
 358      * default state of other properties.
 359      */
 360     public TreeTableView() {
 361         this(null);
 362     }
 363 
 364     /**
 365      * Creates a TreeTableView with the provided root node.
 366      *
 367      * <p>Refer to the {@link TreeTableView} class documentation for details on the
 368      * default state of other properties.
 369      *
 370      * @param root The node to be the root in this TreeTableView.
 371      */
 372     public TreeTableView(TreeItem<S> root) {
 373         getStyleClass().setAll(DEFAULT_STYLE_CLASS);
 374         setAccessibleRole(AccessibleRole.TREE_TABLE_VIEW);
 375 
 376         setRoot(root);
 377         updateExpandedItemCount(root);
 378 
 379         // install default selection and focus models - it's unlikely this will be changed
 380         // by many users.
 381         setSelectionModel(new TreeTableViewArrayListSelectionModel<S>(this));
 382         setFocusModel(new TreeTableViewFocusModel<S>(this));
 383 
 384         // we watch the columns list, such that when it changes we can update
 385         // the leaf columns and visible leaf columns lists (which are read-only).
 386         getColumns().addListener(weakColumnsObserver);
 387 
 388         // watch for changes to the sort order list - and when it changes run
 389         // the sort method.
 390         getSortOrder().addListener((ListChangeListener.Change<? extends TreeTableColumn<S, ?>> c) -> {
 391             doSort(TableUtil.SortEventType.SORT_ORDER_CHANGE, c);
 392         });
 393 
 394         // We're watching for changes to the content width such
 395         // that the resize policy can be run if necessary. This comes from
 396         // TreeTableViewSkin.
 397         getProperties().addListener((MapChangeListener<Object, Object>) c -> {
 398             if (c.wasAdded() && TableView.SET_CONTENT_WIDTH.equals(c.getKey())) {
 399                 if (c.getValueAdded() instanceof Number) {
 400                     setContentWidth((Double) c.getValueAdded());
 401                 }
 402                 getProperties().remove(TableView.SET_CONTENT_WIDTH);
 403             }
 404         });
 405 
 406         isInited = true;
 407     }
 408 
 409 
 410 
 411     /***************************************************************************
 412      *                                                                         *
 413      * Static properties and methods                                           *
 414      *                                                                         *
 415      **************************************************************************/
 416 
 417     /**
 418      * An EventType that indicates some edit event has occurred. It is the parent
 419      * type of all other edit events: {@link #editStartEvent},
 420      *  {@link #editCommitEvent} and {@link #editCancelEvent}.
 421      *
 422      * @param <S> The type of the TreeItem instances used in this TreeTableView
 423      * @return An EventType that indicates some edit event has occurred
 424      */
 425     @SuppressWarnings("unchecked")
 426     public static <S> EventType<TreeTableView.EditEvent<S>> editAnyEvent() {
 427         return (EventType<TreeTableView.EditEvent<S>>) EDIT_ANY_EVENT;
 428     }
 429     private static final EventType<?> EDIT_ANY_EVENT =
 430             new EventType<>(Event.ANY, "TREE_TABLE_VIEW_EDIT");
 431 
 432     /**
 433      * An EventType used to indicate that an edit event has started within the
 434      * TreeTableView upon which the event was fired.
 435      *
 436      * @param <S> The type of the TreeItem instances used in this TreeTableView
 437      * @return An EventType used to indicate that an edit event has started
 438      */
 439     @SuppressWarnings("unchecked")
 440     public static <S> EventType<TreeTableView.EditEvent<S>> editStartEvent() {
 441         return (EventType<TreeTableView.EditEvent<S>>) EDIT_START_EVENT;
 442     }
 443     private static final EventType<?> EDIT_START_EVENT =
 444             new EventType<>(editAnyEvent(), "EDIT_START");
 445 
 446     /**
 447      * An EventType used to indicate that an edit event has just been canceled
 448      * within the TreeTableView upon which the event was fired.
 449      *
 450      * @param <S> The type of the TreeItem instances used in this TreeTableView
 451      * @return An EventType used to indicate that an edit event has just been
 452      *      canceled
 453      */
 454     @SuppressWarnings("unchecked")
 455     public static <S> EventType<TreeTableView.EditEvent<S>> editCancelEvent() {
 456         return (EventType<TreeTableView.EditEvent<S>>) EDIT_CANCEL_EVENT;
 457     }
 458     private static final EventType<?> EDIT_CANCEL_EVENT =
 459             new EventType<>(editAnyEvent(), "EDIT_CANCEL");
 460 
 461     /**
 462      * An EventType that is used to indicate that an edit in a TreeTableView has been
 463      * committed. This means that user has made changes to the data of a
 464      * TreeItem, and that the UI should be updated.
 465      *
 466      * @param <S> The type of the TreeItem instances used in this TreeTableView
 467      * @return An EventType that is used to indicate that an edit in a TreeTableView
 468      *      has been committed
 469      */
 470     @SuppressWarnings("unchecked")
 471     public static <S> EventType<TreeTableView.EditEvent<S>> editCommitEvent() {
 472         return (EventType<TreeTableView.EditEvent<S>>) EDIT_COMMIT_EVENT;
 473     }
 474     private static final EventType<?> EDIT_COMMIT_EVENT =
 475             new EventType<>(editAnyEvent(), "EDIT_COMMIT");
 476 
 477     /**
 478      * Returns the number of levels of 'indentation' of the given TreeItem,
 479      * based on how many times {@link javafx.scene.control.TreeItem#getParent()}
 480      * can be recursively called. If the TreeItem does not have any parent set,
 481      * the returned value will be zero. For each time getParent() is recursively
 482      * called, the returned value is incremented by one.
 483      *
 484      * <p><strong>Important note: </strong>This method is deprecated as it does
 485      * not consider the root node. This means that this method will iterate
 486      * past the root node of the TreeTableView control, if the root node has a parent.
 487      * If this is important, call {@link TreeTableView#getTreeItemLevel(TreeItem)}
 488      * instead.
 489      *
 490      * @param node The TreeItem for which the level is needed.
 491      * @return An integer representing the number of parents above the given node,
 492      *         or -1 if the given TreeItem is null.
 493      * @deprecated This method does not correctly calculate the distance from the
 494      *          given TreeItem to the root of the TreeTableView. As of JavaFX 8.0_20,
 495      *          the proper way to do this is via
 496      *          {@link TreeTableView#getTreeItemLevel(TreeItem)}
 497      */
 498     @Deprecated(since="8u20")
 499     public static int getNodeLevel(TreeItem<?> node) {
 500         return TreeView.getNodeLevel(node);
 501     }
 502 
 503     /**
 504      * <p>Very simple resize policy that just resizes the specified column by the
 505      * provided delta and shifts all other columns (to the right of the given column)
 506      * further to the right (when the delta is positive) or to the left (when the
 507      * delta is negative).
 508      *
 509      * <p>It also handles the case where we have nested columns by sharing the new space,
 510      * or subtracting the removed space, evenly between all immediate children columns.
 511      * Of course, the immediate children may themselves be nested, and they would
 512      * then use this policy on their children.
 513      */
 514     public static final Callback<TreeTableView.ResizeFeatures, Boolean> UNCONSTRAINED_RESIZE_POLICY =
 515             new Callback<TreeTableView.ResizeFeatures, Boolean>() {
 516 
 517         @Override public String toString() {
 518             return "unconstrained-resize";
 519         }
 520 
 521         @Override public Boolean call(TreeTableView.ResizeFeatures prop) {
 522             double result = TableUtil.resize(prop.getColumn(), prop.getDelta());
 523             return Double.compare(result, 0.0) == 0;
 524         }
 525     };
 526 
 527     /**
 528      * <p>Simple policy that ensures the width of all visible leaf columns in
 529      * this table sum up to equal the width of the table itself.
 530      *
 531      * <p>When the user resizes a column width with this policy, the table automatically
 532      * adjusts the width of the right hand side columns. When the user increases a
 533      * column width, the table decreases the width of the rightmost column until it
 534      * reaches its minimum width. Then it decreases the width of the second
 535      * rightmost column until it reaches minimum width and so on. When all right
 536      * hand side columns reach minimum size, the user cannot increase the size of
 537      * resized column any more.
 538      */
 539     public static final Callback<TreeTableView.ResizeFeatures, Boolean> CONSTRAINED_RESIZE_POLICY =
 540             new Callback<TreeTableView.ResizeFeatures, Boolean>() {
 541 
 542         private boolean isFirstRun = true;
 543 
 544         @Override public String toString() {
 545             return "constrained-resize";
 546         }
 547 
 548         @Override public Boolean call(TreeTableView.ResizeFeatures prop) {
 549             TreeTableView<?> table = prop.getTable();
 550             List<? extends TableColumnBase<?,?>> visibleLeafColumns = table.getVisibleLeafColumns();
 551             Boolean result = TableUtil.constrainedResize(prop,
 552                                                isFirstRun,
 553                                                table.contentWidth,
 554                                                visibleLeafColumns);
 555             isFirstRun = ! isFirstRun ? false : ! result;
 556             return result;
 557         }
 558     };
 559 
 560     /**
 561      * The default {@link #sortPolicyProperty() sort policy} that this TreeTableView
 562      * will use if no other policy is specified. The sort policy is a simple
 563      * {@link Callback} that accepts a TreeTableView as the sole argument and expects
 564      * a Boolean response representing whether the sort succeeded or not. A Boolean
 565      * response of true represents success, and a response of false (or null) will
 566      * be considered to represent failure.
 567      */
 568     public static final Callback<TreeTableView, Boolean> DEFAULT_SORT_POLICY = new Callback<TreeTableView, Boolean>() {
 569         @Override public Boolean call(TreeTableView table) {
 570             try {
 571                 TreeItem rootItem = table.getRoot();
 572                 if (rootItem == null) return false;
 573 
 574                 TreeSortMode sortMode = table.getSortMode();
 575                 if (sortMode == null) return false;
 576 
 577                 rootItem.lastSortMode = sortMode;
 578                 rootItem.lastComparator = table.getComparator();
 579                 rootItem.sort();
 580                 return true;
 581             } catch (UnsupportedOperationException e) {
 582                 // TODO might need to support other exception types including:
 583                 // ClassCastException - if the class of the specified element prevents it from being added to this list
 584                 // NullPointerException - if the specified element is null and this list does not permit null elements
 585                 // IllegalArgumentException - if some property of this element prevents it from being added to this list
 586 
 587                 // If we are here the list does not support sorting, so we gracefully
 588                 // fail the sort request and ensure the UI is put back to its previous
 589                 // state. This is handled in the code that calls the sort policy.
 590 
 591                 return false;
 592             }
 593         }
 594     };
 595 
 596 
 597 
 598     /***************************************************************************
 599      *                                                                         *
 600      * Instance Variables                                                      *
 601      *                                                                         *
 602      **************************************************************************/
 603 
 604     // used in the tree item modification event listener. Used by the
 605     // layoutChildren method to determine whether the tree item count should
 606     // be recalculated.
 607     private boolean expandedItemCountDirty = true;
 608 
 609     // Used in the getTreeItem(int row) method to act as a cache.
 610     // See RT-26716 for the justification and performance gains.
 611     private Map<Integer, SoftReference<TreeItem<S>>> treeItemCacheMap = new HashMap<>();
 612 
 613     // this is the only publicly writable list for columns. This represents the
 614     // columns as they are given initially by the developer.
 615     private final ObservableList<TreeTableColumn<S,?>> columns = FXCollections.observableArrayList();
 616 
 617     // Finally, as convenience, we also have an observable list that contains
 618     // only the leaf columns that are currently visible.
 619     private final ObservableList<TreeTableColumn<S,?>> visibleLeafColumns = FXCollections.observableArrayList();
 620     private final ObservableList<TreeTableColumn<S,?>> unmodifiableVisibleLeafColumns = FXCollections.unmodifiableObservableList(visibleLeafColumns);
 621 
 622     // Allows for multiple column sorting based on the order of the TreeTableColumns
 623     // in this observableArrayList. Each TreeTableColumn is responsible for whether it is
 624     // sorted using ascending or descending order.
 625     private ObservableList<TreeTableColumn<S,?>> sortOrder = FXCollections.observableArrayList();
 626 
 627     // width of VirtualFlow minus the vbar width
 628     // package protected for testing only
 629     double contentWidth;
 630 
 631     // Used to minimise the amount of work performed prior to the table being
 632     // completely initialised. In particular it reduces the amount of column
 633     // resize operations that occur, which slightly improves startup time.
 634     private boolean isInited = false;
 635 
 636 
 637 
 638     /***************************************************************************
 639      *                                                                         *
 640      * Callbacks and Events                                                    *
 641      *                                                                         *
 642      **************************************************************************/
 643 
 644     // we use this to forward events that have bubbled up TreeItem instances
 645     // to the TreeTableViewSkin, to force it to recalculate teh item count and redraw
 646     // if necessary
 647     private final EventHandler<TreeItem.TreeModificationEvent<S>> rootEvent = e -> {
 648         // this forces layoutChildren at the next pulse, and therefore
 649         // updates the item count if necessary
 650         EventType<?> eventType = e.getEventType();
 651         boolean match = false;
 652         while (eventType != null) {
 653             if (eventType.equals(TreeItem.<S>expandedItemCountChangeEvent())) {
 654                 match = true;
 655                 break;
 656             }
 657             eventType = eventType.getSuperType();
 658         }
 659 
 660         if (match) {
 661             expandedItemCountDirty = true;
 662             requestLayout();
 663         }
 664     };
 665 
 666     private final ListChangeListener<TreeTableColumn<S,?>> columnsObserver = new ListChangeListener<TreeTableColumn<S,?>>() {
 667         @Override public void onChanged(ListChangeListener.Change<? extends TreeTableColumn<S,?>> c) {
 668             final List<TreeTableColumn<S,?>> columns = getColumns();
 669 
 670             // Fix for RT-39822 - don't allow the same column to be installed twice
 671             while (c.next()) {
 672                 if (c.wasAdded()) {
 673                     List<TreeTableColumn<S,?>> duplicates = new ArrayList<>();
 674                     for (TreeTableColumn<S,?> addedColumn : c.getAddedSubList()) {
 675                         if (addedColumn == null) continue;
 676 
 677                         int count = 0;
 678                         for (TreeTableColumn<S,?> column : columns) {
 679                             if (addedColumn == column) {
 680                                 count++;
 681                             }
 682                         }
 683 
 684                         if (count > 1) {
 685                             duplicates.add(addedColumn);
 686                         }
 687                     }
 688 
 689                     if (!duplicates.isEmpty()) {
 690                         String titleList = "";
 691                         for (TreeTableColumn<S,?> dupe : duplicates) {
 692                             titleList += "'" + dupe.getText() + "', ";
 693                         }
 694                         throw new IllegalStateException("Duplicate TreeTableColumns detected in TreeTableView columns list with titles " + titleList);
 695                     }
 696                 }
 697             }
 698             c.reset();
 699 
 700             // Fix for RT-15194: Need to remove removed columns from the
 701             // sortOrder list.
 702             List<TreeTableColumn<S,?>> toRemove = new ArrayList<TreeTableColumn<S,?>>();
 703             while (c.next()) {
 704                 final List<? extends TreeTableColumn<S, ?>> removed = c.getRemoved();
 705                 final List<? extends TreeTableColumn<S, ?>> added = c.getAddedSubList();
 706 
 707                 if (c.wasRemoved()) {
 708                     toRemove.addAll(removed);
 709                     for (TreeTableColumn<S,?> tc : removed) {
 710                         tc.setTreeTableView(null);
 711                     }
 712                 }
 713 
 714                 if (c.wasAdded()) {
 715                     toRemove.removeAll(added);
 716                     for (TreeTableColumn<S,?> tc : added) {
 717                         tc.setTreeTableView(TreeTableView.this);
 718                     }
 719                 }
 720 
 721                 // set up listeners
 722                 TableUtil.removeColumnsListener(removed, weakColumnsObserver);
 723                 TableUtil.addColumnsListener(added, weakColumnsObserver);
 724 
 725                 TableUtil.removeTableColumnListener(c.getRemoved(),
 726                         weakColumnVisibleObserver,
 727                         weakColumnSortableObserver,
 728                         weakColumnSortTypeObserver,
 729                         weakColumnComparatorObserver);
 730                 TableUtil.addTableColumnListener(c.getAddedSubList(),
 731                         weakColumnVisibleObserver,
 732                         weakColumnSortableObserver,
 733                         weakColumnSortTypeObserver,
 734                         weakColumnComparatorObserver);
 735             }
 736 
 737             // We don't maintain a bind for leafColumns, we simply call this update
 738             // function behind the scenes in the appropriate places.
 739             updateVisibleLeafColumns();
 740 
 741             sortOrder.removeAll(toRemove);
 742 
 743             // Fix for RT-38892.
 744             final TreeTableViewFocusModel<S> fm = getFocusModel();
 745             final TreeTableViewSelectionModel<S> sm = getSelectionModel();
 746             c.reset();
 747 
 748             // we need to collect together all removed and all added columns, because
 749             // the code below works on the actually removed columns. If we perform
 750             // the code within this while loop, we'll be deselecting columns that
 751             // should be deselected (because they have just moved place, for example).
 752             List<TreeTableColumn<S,?>> removed = new ArrayList<>();
 753             List<TreeTableColumn<S,?>> added = new ArrayList<>();
 754             while (c.next()) {
 755                 if (c.wasRemoved()) {
 756                     removed.addAll(c.getRemoved());
 757                 }
 758                 if (c.wasAdded()) {
 759                     added.addAll(c.getAddedSubList());
 760                 }
 761             }
 762             removed.removeAll(added);
 763 
 764 
 765             // Fix for focus - we simply move focus to a cell to the left
 766             // of the focused cell if the focused cell was located within
 767             // a column that has been removed.
 768             if (fm != null) {
 769                 TreeTablePosition<S, ?> focusedCell = fm.getFocusedCell();
 770                 boolean match = false;
 771                 for (TreeTableColumn<S, ?> tc : removed) {
 772                     match = focusedCell != null && focusedCell.getTableColumn() == tc;
 773                     if (match) {
 774                         break;
 775                     }
 776                 }
 777 
 778                 if (match) {
 779                     int matchingColumnIndex = lastKnownColumnIndex.getOrDefault(focusedCell.getTableColumn(), 0);
 780                     int newFocusColumnIndex =
 781                             matchingColumnIndex == 0 ? 0 :
 782                                     Math.min(getVisibleLeafColumns().size() - 1, matchingColumnIndex - 1);
 783                     fm.focus(focusedCell.getRow(), getVisibleLeafColumn(newFocusColumnIndex));
 784                 }
 785             }
 786 
 787             // Fix for selection - we remove selection from all cells that
 788             // were within the removed column.
 789             if (sm != null) {
 790                 List<TreeTablePosition> selectedCells = new ArrayList<>(sm.getSelectedCells());
 791                 for (TreeTablePosition selectedCell : selectedCells) {
 792                     boolean match = false;
 793                     for (TreeTableColumn<S, ?> tc : removed) {
 794                         match = selectedCell != null && selectedCell.getTableColumn() == tc;
 795                         if (match) break;
 796                     }
 797 
 798                     if (match) {
 799                         // we can't just use the selectedCell.getTableColumn(), as that
 800                         // column no longer exists and therefore its index is not correct.
 801                         int matchingColumnIndex = lastKnownColumnIndex.getOrDefault(selectedCell.getTableColumn(), -1);
 802                         if (matchingColumnIndex == -1) continue;
 803 
 804                         if (sm instanceof TreeTableViewArrayListSelectionModel) {
 805                             // Also, because the table column no longer exists in the columns
 806                             // list at this point, we can't just call:
 807                             // sm.clearSelection(selectedCell.getRow(), selectedCell.getTableColumn());
 808                             // as the tableColumn would map to an index of -1, which means that
 809                             // selection will not be cleared. Instead, we have to create
 810                             // a new TablePosition with a fixed column index and use that.
 811                             TreeTablePosition<S,?> fixedTablePosition =
 812                                     new TreeTablePosition<S,Object>(TreeTableView.this,
 813                                             selectedCell.getRow(),
 814                                             selectedCell.getTableColumn());
 815                             fixedTablePosition.fixedColumnIndex = matchingColumnIndex;
 816 
 817                             ((TreeTableViewArrayListSelectionModel)sm).clearSelection(fixedTablePosition);
 818                         } else {
 819                             sm.clearSelection(selectedCell.getRow(), selectedCell.getTableColumn());
 820                         }
 821                     }
 822                 }
 823             }
 824 
 825 
 826             // update the lastKnownColumnIndex map
 827             lastKnownColumnIndex.clear();
 828             for (TreeTableColumn<S,?> tc : getColumns()) {
 829                 int index = getVisibleLeafIndex(tc);
 830                 if (index > -1) {
 831                     lastKnownColumnIndex.put(tc, index);
 832                 }
 833             }
 834         }
 835     };
 836 
 837     private final WeakHashMap<TreeTableColumn<S,?>, Integer> lastKnownColumnIndex = new WeakHashMap<>();
 838 
 839     private final InvalidationListener columnVisibleObserver = valueModel -> {
 840         updateVisibleLeafColumns();
 841     };
 842 
 843     private final InvalidationListener columnSortableObserver = valueModel -> {
 844         TreeTableColumn col = (TreeTableColumn) ((BooleanProperty)valueModel).getBean();
 845         if (! getSortOrder().contains(col)) return;
 846         doSort(TableUtil.SortEventType.COLUMN_SORTABLE_CHANGE, col);
 847     };
 848 
 849     private final InvalidationListener columnSortTypeObserver = valueModel -> {
 850         TreeTableColumn col = (TreeTableColumn) ((ObjectProperty)valueModel).getBean();
 851         if (! getSortOrder().contains(col)) return;
 852         doSort(TableUtil.SortEventType.COLUMN_SORT_TYPE_CHANGE, col);
 853     };
 854 
 855     private final InvalidationListener columnComparatorObserver = valueModel -> {
 856         TreeTableColumn col = (TreeTableColumn) ((SimpleObjectProperty)valueModel).getBean();
 857         if (! getSortOrder().contains(col)) return;
 858         doSort(TableUtil.SortEventType.COLUMN_COMPARATOR_CHANGE, col);
 859     };
 860 
 861     /* proxy pseudo-class state change from selectionModel's cellSelectionEnabledProperty */
 862     private final InvalidationListener cellSelectionModelInvalidationListener = o -> {
 863         boolean isCellSelection = ((BooleanProperty)o).get();
 864         pseudoClassStateChanged(PSEUDO_CLASS_CELL_SELECTION,  isCellSelection);
 865         pseudoClassStateChanged(PSEUDO_CLASS_ROW_SELECTION,  !isCellSelection);
 866     };
 867 
 868     private WeakEventHandler<TreeItem.TreeModificationEvent<S>> weakRootEventListener;
 869 
 870     private final WeakInvalidationListener weakColumnVisibleObserver =
 871             new WeakInvalidationListener(columnVisibleObserver);
 872 
 873     private final WeakInvalidationListener weakColumnSortableObserver =
 874             new WeakInvalidationListener(columnSortableObserver);
 875 
 876     private final WeakInvalidationListener weakColumnSortTypeObserver =
 877             new WeakInvalidationListener(columnSortTypeObserver);
 878 
 879     private final WeakInvalidationListener weakColumnComparatorObserver =
 880             new WeakInvalidationListener(columnComparatorObserver);
 881 
 882     private final WeakListChangeListener<TreeTableColumn<S,?>> weakColumnsObserver =
 883             new WeakListChangeListener<TreeTableColumn<S,?>>(columnsObserver);
 884 
 885     private final WeakInvalidationListener weakCellSelectionModelInvalidationListener =
 886             new WeakInvalidationListener(cellSelectionModelInvalidationListener);
 887 
 888 
 889 
 890     /***************************************************************************
 891      *                                                                         *
 892      * Properties                                                              *
 893      *                                                                         *
 894      **************************************************************************/
 895 
 896     // --- Root
 897     private ObjectProperty<TreeItem<S>> root = new SimpleObjectProperty<TreeItem<S>>(this, "root") {
 898         private WeakReference<TreeItem<S>> weakOldItem;
 899 
 900         @Override protected void invalidated() {
 901             TreeItem<S> oldTreeItem = weakOldItem == null ? null : weakOldItem.get();
 902             if (oldTreeItem != null && weakRootEventListener != null) {
 903                 oldTreeItem.removeEventHandler(TreeItem.<S>treeNotificationEvent(), weakRootEventListener);
 904             }
 905 
 906             TreeItem<S> root = getRoot();
 907             if (root != null) {
 908                 weakRootEventListener = new WeakEventHandler<>(rootEvent);
 909                 getRoot().addEventHandler(TreeItem.<S>treeNotificationEvent(), weakRootEventListener);
 910                 weakOldItem = new WeakReference<>(root);
 911             }
 912 
 913             // Fix for RT-35763
 914             getSortOrder().clear();
 915 
 916             expandedItemCountDirty = true;
 917             updateRootExpanded();
 918         }
 919     };
 920 
 921     /**
 922      * Sets the root node in this TreeTableView. See the {@link TreeItem} class level
 923      * documentation for more details.
 924      *
 925      * @param value The {@link TreeItem} that will be placed at the root of the
 926      *      TreeTableView.
 927      */
 928     public final void setRoot(TreeItem<S> value) {
 929         rootProperty().set(value);
 930     }
 931 
 932     /**
 933      * Returns the current root node of this TreeTableView, or null if no root node
 934      * is specified.
 935      * @return The current root node, or null if no root node exists.
 936      */
 937     public final TreeItem<S> getRoot() {
 938         return root == null ? null : root.get();
 939     }
 940 
 941     /**
 942      * Property representing the root node of the TreeTableView.
 943      * @return the root property
 944      */
 945     public final ObjectProperty<TreeItem<S>> rootProperty() {
 946         return root;
 947     }
 948 
 949 
 950 
 951     // --- Show Root
 952     private BooleanProperty showRoot;
 953 
 954     /**
 955      * Specifies whether the root {@code TreeItem} should be shown within this
 956      * TreeTableView.
 957      *
 958      * @param value If true, the root TreeItem will be shown, and if false it
 959      *      will be hidden.
 960      */
 961     public final void setShowRoot(boolean value) {
 962         showRootProperty().set(value);
 963     }
 964 
 965     /**
 966      * Returns true if the root of the TreeTableView should be shown, and false if
 967      * it should not. By default, the root TreeItem is visible in the TreeTableView.
 968      * @return true if the root of the TreeTableView should be shown
 969      */
 970     public final boolean isShowRoot() {
 971         return showRoot == null ? true : showRoot.get();
 972     }
 973 
 974     /**
 975      * Property that represents whether or not the TreeTableView root node is visible.
 976      * @return the show root property
 977      */
 978     public final BooleanProperty showRootProperty() {
 979         if (showRoot == null) {
 980             showRoot = new SimpleBooleanProperty(this, "showRoot", true) {
 981                 @Override protected void invalidated() {
 982                     updateRootExpanded();
 983                     updateExpandedItemCount(getRoot());
 984                 }
 985             };
 986         }
 987         return showRoot;
 988     }
 989 
 990 
 991 
 992     // --- Tree Column
 993     private ObjectProperty<TreeTableColumn<S,?>> treeColumn;
 994     /**
 995      * Property that represents which column should have the disclosure node
 996      * shown in it (that is, the column with the arrow). By default this will be
 997      * the left-most column if this property is null, otherwise it will be the
 998      * specified column assuming it is non-null and contained within the
 999      * {@link #getVisibleLeafColumns() visible leaf columns} list.
1000      * @return the tree column property
1001      */
1002     public final ObjectProperty<TreeTableColumn<S,?>> treeColumnProperty() {
1003         if (treeColumn == null) {
1004             treeColumn = new SimpleObjectProperty<>(this, "treeColumn", null);
1005         }
1006         return treeColumn;
1007     }
1008     public final void setTreeColumn(TreeTableColumn<S,?> value) {
1009         treeColumnProperty().set(value);
1010     }
1011     public final TreeTableColumn<S,?> getTreeColumn() {
1012         return treeColumn == null ? null : treeColumn.get();
1013     }
1014 
1015 
1016 
1017     // --- Selection Model
1018     private ObjectProperty<TreeTableViewSelectionModel<S>> selectionModel;
1019 
1020     /**
1021      * Sets the {@link MultipleSelectionModel} to be used in the TreeTableView.
1022      * Despite a TreeTableView requiring a <code><b>Multiple</b>SelectionModel</code>,
1023      * it is possible to configure it to only allow single selection (see
1024      * {@link MultipleSelectionModel#setSelectionMode(javafx.scene.control.SelectionMode)}
1025      * for more information).
1026      * @param value the {@link MultipleSelectionModel} to be used
1027      */
1028     public final void setSelectionModel(TreeTableViewSelectionModel<S> value) {
1029         selectionModelProperty().set(value);
1030     }
1031 
1032     /**
1033      * Returns the currently installed selection model.
1034      * @return the currently installed selection model
1035      */
1036     public final TreeTableViewSelectionModel<S> getSelectionModel() {
1037         return selectionModel == null ? null : selectionModel.get();
1038     }
1039 
1040     /**
1041      * The SelectionModel provides the API through which it is possible
1042      * to select single or multiple items within a TreeTableView, as  well as inspect
1043      * which rows have been selected by the user. Note that it has a generic
1044      * type that must match the type of the TreeTableView itself.
1045      * @return the selection model property
1046      */
1047     public final ObjectProperty<TreeTableViewSelectionModel<S>> selectionModelProperty() {
1048         if (selectionModel == null) {
1049             selectionModel = new SimpleObjectProperty<TreeTableViewSelectionModel<S>>(this, "selectionModel") {
1050 
1051                 TreeTableViewSelectionModel<S> oldValue = null;
1052 
1053                 @Override protected void invalidated() {
1054                     // need to listen to the cellSelectionEnabledProperty
1055                     // in order to set pseudo-class state
1056                     if (oldValue != null) {
1057                         oldValue.cellSelectionEnabledProperty().removeListener(weakCellSelectionModelInvalidationListener);
1058 
1059                         if (oldValue instanceof TreeTableViewArrayListSelectionModel) {
1060                             ((TreeTableViewArrayListSelectionModel)oldValue).dispose();
1061                         }
1062                     }
1063 
1064                     oldValue = get();
1065 
1066                     if (oldValue != null) {
1067                         oldValue.cellSelectionEnabledProperty().addListener(weakCellSelectionModelInvalidationListener);
1068                         // fake invalidation to ensure updated pseudo-class states
1069                         weakCellSelectionModelInvalidationListener.invalidated(oldValue.cellSelectionEnabledProperty());
1070                     }
1071                 }
1072             };
1073         }
1074         return selectionModel;
1075     }
1076 
1077 
1078     // --- Focus Model
1079     private ObjectProperty<TreeTableViewFocusModel<S>> focusModel;
1080 
1081     /**
1082      * Sets the {@link FocusModel} to be used in the TreeTableView.
1083      * @param value the {@link FocusModel} to be used
1084      */
1085     public final void setFocusModel(TreeTableViewFocusModel<S> value) {
1086         focusModelProperty().set(value);
1087     }
1088 
1089     /**
1090      * Returns the currently installed {@link FocusModel}.
1091      * @return the currently installed {@link FocusModel}
1092      */
1093     public final TreeTableViewFocusModel<S> getFocusModel() {
1094         return focusModel == null ? null : focusModel.get();
1095     }
1096 
1097     /**
1098      * The FocusModel provides the API through which it is possible
1099      * to control focus on zero or one rows of the TreeTableView. Generally the
1100      * default implementation should be more than sufficient.
1101      * @return the focus model property
1102      */
1103     public final ObjectProperty<TreeTableViewFocusModel<S>> focusModelProperty() {
1104         if (focusModel == null) {
1105             focusModel = new SimpleObjectProperty<TreeTableViewFocusModel<S>>(this, "focusModel");
1106         }
1107         return focusModel;
1108     }
1109 
1110 
1111     // --- Tree node count
1112     /**
1113      * <p>Represents the number of tree nodes presently able to be visible in the
1114      * TreeTableView. This is essentially the count of all expanded tree items, and
1115      * their children.
1116      *
1117      * <p>For example, if just the root node is visible, the expandedItemCount will
1118      * be one. If the root had three children and the root was expanded, the value
1119      * will be four.
1120      */
1121     private ReadOnlyIntegerWrapper expandedItemCount = new ReadOnlyIntegerWrapper(this, "expandedItemCount", 0);
1122     public final ReadOnlyIntegerProperty expandedItemCountProperty() {
1123         return expandedItemCount.getReadOnlyProperty();
1124     }
1125     private void setExpandedItemCount(int value) {
1126         expandedItemCount.set(value);
1127     }
1128     public final int getExpandedItemCount() {
1129         if (expandedItemCountDirty) {
1130             updateExpandedItemCount(getRoot());
1131         }
1132         return expandedItemCount.get();
1133     }
1134 
1135 
1136     // --- Editable
1137     private BooleanProperty editable;
1138     public final void setEditable(boolean value) {
1139         editableProperty().set(value);
1140     }
1141     public final boolean isEditable() {
1142         return editable == null ? false : editable.get();
1143     }
1144     /**
1145      * Specifies whether this TreeTableView is editable - only if the TreeTableView and
1146      * the TreeCells within it are both editable will a TreeCell be able to go
1147      * into their editing state.
1148      * @return the editable property
1149      */
1150     public final BooleanProperty editableProperty() {
1151         if (editable == null) {
1152             editable = new SimpleBooleanProperty(this, "editable", false);
1153         }
1154         return editable;
1155     }
1156 
1157 
1158     // --- Editing Cell
1159     private ReadOnlyObjectWrapper<TreeTablePosition<S,?>> editingCell;
1160     private void setEditingCell(TreeTablePosition<S,?> value) {
1161         editingCellPropertyImpl().set(value);
1162     }
1163     public final TreeTablePosition<S,?> getEditingCell() {
1164         return editingCell == null ? null : editingCell.get();
1165     }
1166 
1167     /**
1168      * Represents the current cell being edited, or null if
1169      * there is no cell being edited.
1170      * @return the editing cell property
1171      */
1172     public final ReadOnlyObjectProperty<TreeTablePosition<S,?>> editingCellProperty() {
1173         return editingCellPropertyImpl().getReadOnlyProperty();
1174     }
1175 
1176     private ReadOnlyObjectWrapper<TreeTablePosition<S,?>> editingCellPropertyImpl() {
1177         if (editingCell == null) {
1178             editingCell = new ReadOnlyObjectWrapper<TreeTablePosition<S,?>>(this, "editingCell");
1179         }
1180         return editingCell;
1181     }
1182 
1183 
1184     // --- Table menu button visible
1185     private BooleanProperty tableMenuButtonVisible;
1186     /**
1187      * This controls whether a menu button is available when the user clicks
1188      * in a designated space within the TableView, within which is a radio menu
1189      * item for each TreeTableColumn in this table. This menu allows for the user to
1190      * show and hide all TreeTableColumns easily.
1191      * @return the table menu button visible property
1192      */
1193     public final BooleanProperty tableMenuButtonVisibleProperty() {
1194         if (tableMenuButtonVisible == null) {
1195             tableMenuButtonVisible = new SimpleBooleanProperty(this, "tableMenuButtonVisible");
1196         }
1197         return tableMenuButtonVisible;
1198     }
1199     public final void setTableMenuButtonVisible (boolean value) {
1200         tableMenuButtonVisibleProperty().set(value);
1201     }
1202     public final boolean isTableMenuButtonVisible() {
1203         return tableMenuButtonVisible == null ? false : tableMenuButtonVisible.get();
1204     }
1205 
1206 
1207     // --- Column Resize Policy
1208     private ObjectProperty<Callback<TreeTableView.ResizeFeatures, Boolean>> columnResizePolicy;
1209     public final void setColumnResizePolicy(Callback<TreeTableView.ResizeFeatures, Boolean> callback) {
1210         columnResizePolicyProperty().set(callback);
1211     }
1212     public final Callback<TreeTableView.ResizeFeatures, Boolean> getColumnResizePolicy() {
1213         return columnResizePolicy == null ? UNCONSTRAINED_RESIZE_POLICY : columnResizePolicy.get();
1214     }
1215 
1216     /**
1217      * This is the function called when the user completes a column-resize
1218      * operation. The two most common policies are available as static functions
1219      * in the TableView class: {@link #UNCONSTRAINED_RESIZE_POLICY} and
1220      * {@link #CONSTRAINED_RESIZE_POLICY}.
1221      * @return the column resize policy property
1222      */
1223     public final ObjectProperty<Callback<TreeTableView.ResizeFeatures, Boolean>> columnResizePolicyProperty() {
1224         if (columnResizePolicy == null) {
1225             columnResizePolicy = new SimpleObjectProperty<Callback<TreeTableView.ResizeFeatures, Boolean>>(this, "columnResizePolicy", UNCONSTRAINED_RESIZE_POLICY) {
1226                 private Callback<TreeTableView.ResizeFeatures, Boolean> oldPolicy;
1227 
1228                 @Override protected void invalidated() {
1229                     if (isInited) {
1230                         get().call(new TreeTableView.ResizeFeatures(TreeTableView.this, null, 0.0));
1231 
1232                         if (oldPolicy != null) {
1233                             PseudoClass state = PseudoClass.getPseudoClass(oldPolicy.toString());
1234                             pseudoClassStateChanged(state, false);
1235                         }
1236                         if (get() != null) {
1237                             PseudoClass state = PseudoClass.getPseudoClass(get().toString());
1238                             pseudoClassStateChanged(state, true);
1239                         }
1240                         oldPolicy = get();
1241                     }
1242                 }
1243             };
1244         }
1245         return columnResizePolicy;
1246     }
1247 
1248 
1249     // --- Row Factory
1250     private ObjectProperty<Callback<TreeTableView<S>, TreeTableRow<S>>> rowFactory;
1251 
1252     /**
1253      * A function which produces a TreeTableRow. The system is responsible for
1254      * reusing TreeTableRows. Return from this function a TreeTableRow which
1255      * might be usable for representing a single row in a TableView.
1256      * <p>
1257      * Note that a TreeTableRow is <b>not</b> a TableCell. A TreeTableRow is
1258      * simply a container for a TableCell, and in most circumstances it is more
1259      * likely that you'll want to create custom TableCells, rather than
1260      * TreeTableRows. The primary use case for creating custom TreeTableRow
1261      * instances would most probably be to introduce some form of column
1262      * spanning support.
1263      * <p>
1264      * You can create custom TableCell instances per column by assigning the
1265      * appropriate function to the cellFactory property in the TreeTableColumn class.
1266      * @return the row factory property
1267      */
1268     public final ObjectProperty<Callback<TreeTableView<S>, TreeTableRow<S>>> rowFactoryProperty() {
1269         if (rowFactory == null) {
1270             rowFactory = new SimpleObjectProperty<Callback<TreeTableView<S>, TreeTableRow<S>>>(this, "rowFactory");
1271         }
1272         return rowFactory;
1273     }
1274     public final void setRowFactory(Callback<TreeTableView<S>, TreeTableRow<S>> value) {
1275         rowFactoryProperty().set(value);
1276     }
1277     public final Callback<TreeTableView<S>, TreeTableRow<S>> getRowFactory() {
1278         return rowFactory == null ? null : rowFactory.get();
1279     }
1280 
1281 
1282     // --- Placeholder Node
1283     private ObjectProperty<Node> placeholder;
1284     /**
1285      * This Node is shown to the user when the table has no content to show.
1286      * This may be the case because the table model has no data in the first
1287      * place, that a filter has been applied to the table model, resulting
1288      * in there being nothing to show the user, or that there are no currently
1289      * visible columns.
1290      * @return the placeholder property
1291      */
1292     public final ObjectProperty<Node> placeholderProperty() {
1293         if (placeholder == null) {
1294             placeholder = new SimpleObjectProperty<Node>(this, "placeholder");
1295         }
1296         return placeholder;
1297     }
1298     public final void setPlaceholder(Node value) {
1299         placeholderProperty().set(value);
1300     }
1301     public final Node getPlaceholder() {
1302         return placeholder == null ? null : placeholder.get();
1303     }
1304 
1305 
1306     // --- Fixed cell size
1307     private DoubleProperty fixedCellSize;
1308 
1309     /**
1310      * Sets the new fixed cell size for this control. Any value greater than
1311      * zero will enable fixed cell size mode, whereas a zero or negative value
1312      * (or Region.USE_COMPUTED_SIZE) will be used to disabled fixed cell size
1313      * mode.
1314      *
1315      * @param value The new fixed cell size value, or a value less than or equal
1316      *              to zero (or Region.USE_COMPUTED_SIZE) to disable.
1317      * @since JavaFX 8.0
1318      */
1319     public final void setFixedCellSize(double value) {
1320         fixedCellSizeProperty().set(value);
1321     }
1322 
1323     /**
1324      * Returns the fixed cell size value. A value less than or equal to zero is
1325      * used to represent that fixed cell size mode is disabled, and a value
1326      * greater than zero represents the size of all cells in this control.
1327      *
1328      * @return A double representing the fixed cell size of this control, or a
1329      *      value less than or equal to zero if fixed cell size mode is disabled.
1330      * @since JavaFX 8.0
1331      */
1332     public final double getFixedCellSize() {
1333         return fixedCellSize == null ? Region.USE_COMPUTED_SIZE : fixedCellSize.get();
1334     }
1335     /**
1336      * Specifies whether this control has cells that are a fixed height (of the
1337      * specified value). If this value is less than or equal to zero,
1338      * then all cells are individually sized and positioned. This is a slow
1339      * operation. Therefore, when performance matters and developers are not
1340      * dependent on variable cell sizes it is a good idea to set the fixed cell
1341      * size value. Generally cells are around 24px, so setting a fixed cell size
1342      * of 24 is likely to result in very little difference in visuals, but a
1343      * improvement to performance.
1344      *
1345      * <p>To set this property via CSS, use the -fx-fixed-cell-size property.
1346      * This should not be confused with the -fx-cell-size property. The difference
1347      * between these two CSS properties is that -fx-cell-size will size all
1348      * cells to the specified size, but it will not enforce that this is the
1349      * only size (thus allowing for variable cell sizes, and preventing the
1350      * performance gains from being possible). Therefore, when performance matters
1351      * use -fx-fixed-cell-size, instead of -fx-cell-size. If both properties are
1352      * specified in CSS, -fx-fixed-cell-size takes precedence.</p>
1353      *
1354      * @return the fixed cell size property
1355      * @since JavaFX 8.0
1356      */
1357     public final DoubleProperty fixedCellSizeProperty() {
1358         if (fixedCellSize == null) {
1359             fixedCellSize = new StyleableDoubleProperty(Region.USE_COMPUTED_SIZE) {
1360                 @Override public CssMetaData<TreeTableView<?>,Number> getCssMetaData() {
1361                     return StyleableProperties.FIXED_CELL_SIZE;
1362                 }
1363 
1364                 @Override public Object getBean() {
1365                     return TreeTableView.this;
1366                 }
1367 
1368                 @Override public String getName() {
1369                     return "fixedCellSize";
1370                 }
1371             };
1372         }
1373         return fixedCellSize;
1374     }
1375 
1376 
1377     // --- SortMode
1378     /**
1379      * Specifies the sort mode to use when sorting the contents of this TreeTableView,
1380      * should any columns be specified in the {@link #getSortOrder() sort order}
1381      * list.
1382      */
1383     private ObjectProperty<TreeSortMode> sortMode;
1384     public final ObjectProperty<TreeSortMode> sortModeProperty() {
1385         if (sortMode == null) {
1386             sortMode = new SimpleObjectProperty<>(this, "sortMode", TreeSortMode.ALL_DESCENDANTS);
1387         }
1388         return sortMode;
1389     }
1390     public final void setSortMode(TreeSortMode value) {
1391         sortModeProperty().set(value);
1392     }
1393     public final TreeSortMode getSortMode() {
1394         return sortMode == null ? TreeSortMode.ALL_DESCENDANTS : sortMode.get();
1395     }
1396 
1397 
1398     // --- Comparator (built via sortOrder list, so read-only)
1399     /**
1400      * The comparator property is a read-only property that is representative of the
1401      * current state of the {@link #getSortOrder() sort order} list. The sort
1402      * order list contains the columns that have been added to it either programmatically
1403      * or via a user clicking on the headers themselves.
1404      */
1405     private ReadOnlyObjectWrapper<Comparator<TreeItem<S>>> comparator;
1406     private void setComparator(Comparator<TreeItem<S>> value) {
1407         comparatorPropertyImpl().set(value);
1408     }
1409     public final Comparator<TreeItem<S>> getComparator() {
1410         return comparator == null ? null : comparator.get();
1411     }
1412     public final ReadOnlyObjectProperty<Comparator<TreeItem<S>>> comparatorProperty() {
1413         return comparatorPropertyImpl().getReadOnlyProperty();
1414     }
1415     private ReadOnlyObjectWrapper<Comparator<TreeItem<S>>> comparatorPropertyImpl() {
1416         if (comparator == null) {
1417             comparator = new ReadOnlyObjectWrapper<>(this, "comparator");
1418         }
1419         return comparator;
1420     }
1421 
1422 
1423     // --- sortPolicy
1424     /**
1425      * The sort policy specifies how sorting in this TreeTableView should be performed.
1426      * For example, a basic sort policy may just recursively sort the children of
1427      * the root tree item, whereas a more advanced sort policy may call to a
1428      * database to perform the necessary sorting on the server-side.
1429      *
1430      * <p>TreeTableView ships with a {@link TableView#DEFAULT_SORT_POLICY default
1431      * sort policy} that does precisely as mentioned above: it simply attempts
1432      * to sort the tree hierarchy in-place.
1433      *
1434      * <p>It is recommended that rather than override the {@link TreeTableView#sort() sort}
1435      * method that a different sort policy be provided instead.
1436      */
1437     private ObjectProperty<Callback<TreeTableView<S>, Boolean>> sortPolicy;
1438     public final void setSortPolicy(Callback<TreeTableView<S>, Boolean> callback) {
1439         sortPolicyProperty().set(callback);
1440     }
1441     @SuppressWarnings("unchecked")
1442     public final Callback<TreeTableView<S>, Boolean> getSortPolicy() {
1443         return sortPolicy == null ?
1444                 (Callback<TreeTableView<S>, Boolean>)(Object) DEFAULT_SORT_POLICY :
1445                 sortPolicy.get();
1446     }
1447     @SuppressWarnings("unchecked")
1448     public final ObjectProperty<Callback<TreeTableView<S>, Boolean>> sortPolicyProperty() {
1449         if (sortPolicy == null) {
1450             sortPolicy = new SimpleObjectProperty<Callback<TreeTableView<S>, Boolean>>(
1451                     this, "sortPolicy", (Callback<TreeTableView<S>, Boolean>)(Object) DEFAULT_SORT_POLICY) {
1452                 @Override protected void invalidated() {
1453                     sort();
1454                 }
1455             };
1456         }
1457         return sortPolicy;
1458     }
1459 
1460 
1461     // onSort
1462     /**
1463      * Called when there's a request to sort the control.
1464      */
1465     private ObjectProperty<EventHandler<SortEvent<TreeTableView<S>>>> onSort;
1466 
1467     public void setOnSort(EventHandler<SortEvent<TreeTableView<S>>> value) {
1468         onSortProperty().set(value);
1469     }
1470 
1471     public EventHandler<SortEvent<TreeTableView<S>>> getOnSort() {
1472         if( onSort != null ) {
1473             return onSort.get();
1474         }
1475         return null;
1476     }
1477 
1478     public ObjectProperty<EventHandler<SortEvent<TreeTableView<S>>>> onSortProperty() {
1479         if( onSort == null ) {
1480             onSort = new ObjectPropertyBase<EventHandler<SortEvent<TreeTableView<S>>>>() {
1481                 @Override protected void invalidated() {
1482                     EventType<SortEvent<TreeTableView<S>>> eventType = SortEvent.sortEvent();
1483                     EventHandler<SortEvent<TreeTableView<S>>> eventHandler = get();
1484                     setEventHandler(eventType, eventHandler);
1485                 }
1486 
1487                 @Override public Object getBean() {
1488                     return TreeTableView.this;
1489                 }
1490 
1491                 @Override public String getName() {
1492                     return "onSort";
1493                 }
1494             };
1495         }
1496         return onSort;
1497     }
1498 
1499 
1500 
1501     /***************************************************************************
1502      *                                                                         *
1503      * Public API                                                              *
1504      *                                                                         *
1505      **************************************************************************/
1506 
1507     /** {@inheritDoc} */
1508     @Override protected void layoutChildren() {
1509         if (expandedItemCountDirty) {
1510             updateExpandedItemCount(getRoot());
1511         }
1512 
1513         super.layoutChildren();
1514     }
1515 
1516     /**
1517      * Scrolls the TreeTableView such that the item in the given index is visible to
1518      * the end user.
1519      *
1520      * @param index The index that should be made visible to the user, assuming
1521      *      of course that it is greater than, or equal to 0, and less than the
1522      *      number of the visible items in the TreeTableView.
1523      */
1524     public void scrollTo(int index) {
1525         ControlUtils.scrollToIndex(this, index);
1526     }
1527 
1528     /**
1529      * Called when there's a request to scroll an index into view using {@link #scrollTo(int)}
1530      */
1531     private ObjectProperty<EventHandler<ScrollToEvent<Integer>>> onScrollTo;
1532 
1533     public void setOnScrollTo(EventHandler<ScrollToEvent<Integer>> value) {
1534         onScrollToProperty().set(value);
1535     }
1536 
1537     public EventHandler<ScrollToEvent<Integer>> getOnScrollTo() {
1538         if( onScrollTo != null ) {
1539             return onScrollTo.get();
1540         }
1541         return null;
1542     }
1543 
1544     public ObjectProperty<EventHandler<ScrollToEvent<Integer>>> onScrollToProperty() {
1545         if( onScrollTo == null ) {
1546             onScrollTo = new ObjectPropertyBase<EventHandler<ScrollToEvent<Integer>>>() {
1547                 @Override protected void invalidated() {
1548                     setEventHandler(ScrollToEvent.scrollToTopIndex(), get());
1549                 }
1550 
1551                 @Override public Object getBean() {
1552                     return TreeTableView.this;
1553                 }
1554 
1555                 @Override public String getName() {
1556                     return "onScrollTo";
1557                 }
1558             };
1559         }
1560         return onScrollTo;
1561     }
1562 
1563     /**
1564      * Scrolls the TreeTableView so that the given column is visible within the viewport.
1565      * @param column The column that should be visible to the user.
1566      */
1567     public void scrollToColumn(TreeTableColumn<S, ?> column) {
1568         ControlUtils.scrollToColumn(this, column);
1569     }
1570 
1571     /**
1572      * Scrolls the TreeTableView so that the given index is visible within the viewport.
1573      * @param columnIndex The index of a column that should be visible to the user.
1574      */
1575     public void scrollToColumnIndex(int columnIndex) {
1576         if( getColumns() != null ) {
1577             ControlUtils.scrollToColumn(this, getColumns().get(columnIndex));
1578         }
1579     }
1580 
1581     /**
1582      * Called when there's a request to scroll a column into view using {@link #scrollToColumn(TreeTableColumn)}
1583      * or {@link #scrollToColumnIndex(int)}
1584      */
1585     private ObjectProperty<EventHandler<ScrollToEvent<TreeTableColumn<S, ?>>>> onScrollToColumn;
1586 
1587     public void setOnScrollToColumn(EventHandler<ScrollToEvent<TreeTableColumn<S, ?>>> value) {
1588         onScrollToColumnProperty().set(value);
1589     }
1590 
1591     public EventHandler<ScrollToEvent<TreeTableColumn<S, ?>>> getOnScrollToColumn() {
1592         if( onScrollToColumn != null ) {
1593             return onScrollToColumn.get();
1594         }
1595         return null;
1596     }
1597 
1598     public ObjectProperty<EventHandler<ScrollToEvent<TreeTableColumn<S, ?>>>> onScrollToColumnProperty() {
1599         if( onScrollToColumn == null ) {
1600             onScrollToColumn = new ObjectPropertyBase<EventHandler<ScrollToEvent<TreeTableColumn<S, ?>>>>() {
1601                 @Override
1602                 protected void invalidated() {
1603                     EventType<ScrollToEvent<TreeTableColumn<S, ?>>> type = ScrollToEvent.scrollToColumn();
1604                     setEventHandler(type, get());
1605                 }
1606                 @Override
1607                 public Object getBean() {
1608                     return TreeTableView.this;
1609                 }
1610 
1611                 @Override
1612                 public String getName() {
1613                     return "onScrollToColumn";
1614                 }
1615             };
1616         }
1617         return onScrollToColumn;
1618     }
1619 
1620     /**
1621      * Returns the index position of the given TreeItem, assuming that it is
1622      * currently accessible through the tree hierarchy (most notably, that all
1623      * parent tree items are expanded). If a parent tree item is collapsed,
1624      * the result is that this method will return -1 to indicate that the
1625      * given tree item is not accessible in the tree.
1626      *
1627      * @param item The TreeItem for which the index is sought.
1628      * @return An integer representing the location in the current TreeTableView of the
1629      *      first instance of the given TreeItem, or -1 if it is null or can not
1630      *      be found (for example, if a parent (all the way up to the root) is
1631      *      collapsed).
1632      */
1633     public int getRow(TreeItem<S> item) {
1634         return TreeUtil.getRow(item, getRoot(), expandedItemCountDirty, isShowRoot());
1635     }
1636 
1637     /**
1638      * Returns the TreeItem in the given index, or null if it is out of bounds.
1639      *
1640      * @param row The index of the TreeItem being sought.
1641      * @return The TreeItem in the given index, or null if it is out of bounds.
1642      */
1643     public TreeItem<S> getTreeItem(int row) {
1644         if (row < 0) return null;
1645 
1646         // normalize the requested row based on whether showRoot is set
1647         final int _row = isShowRoot() ? row : (row + 1);
1648 
1649         if (expandedItemCountDirty) {
1650             updateExpandedItemCount(getRoot());
1651         } else {
1652             if (treeItemCacheMap.containsKey(_row)) {
1653                 SoftReference<TreeItem<S>> treeItemRef = treeItemCacheMap.get(_row);
1654                 TreeItem<S> treeItem = treeItemRef.get();
1655                 if (treeItem != null) {
1656                     return treeItem;
1657                 }
1658             }
1659         }
1660 
1661         TreeItem<S> treeItem = TreeUtil.getItem(getRoot(), _row, expandedItemCountDirty);
1662         treeItemCacheMap.put(_row, new SoftReference<>(treeItem));
1663         return treeItem;
1664     }
1665 
1666     /**
1667      * Returns the number of levels of 'indentation' of the given TreeItem,
1668      * based on how many times getParent() can be recursively called. If the
1669      * given TreeItem is the root node of this TreeTableView, or if the TreeItem
1670      * does not have any parent set, the returned value will be zero. For each
1671      * time getParent() is recursively called, the returned value is incremented
1672      * by one.
1673      *
1674      * @param node The TreeItem for which the level is needed.
1675      * @return An integer representing the number of parents above the given node,
1676      *         or -1 if the given TreeItem is null.
1677      */
1678     public int getTreeItemLevel(TreeItem<?> node) {
1679         final TreeItem<?> root = getRoot();
1680 
1681         if (node == null) return -1;
1682         if (node == root) return 0;
1683 
1684         int level = 0;
1685         TreeItem<?> parent = node.getParent();
1686         while (parent != null) {
1687             level++;
1688 
1689             if (parent == root) {
1690                 break;
1691             }
1692 
1693             parent = parent.getParent();
1694         }
1695 
1696         return level;
1697     }
1698 
1699     /**
1700      * The TreeTableColumns that are part of this TableView. As the user reorders
1701      * the TableView columns, this list will be updated to reflect the current
1702      * visual ordering.
1703      *
1704      * <p>Note: to display any data in a TableView, there must be at least one
1705      * TreeTableColumn in this ObservableList.</p>
1706      * @return the table table column
1707      */
1708     public final ObservableList<TreeTableColumn<S,?>> getColumns() {
1709         return columns;
1710     }
1711 
1712     /**
1713      * The sortOrder list defines the order in which {@link TreeTableColumn} instances
1714      * are sorted. An empty sortOrder list means that no sorting is being applied
1715      * on the TableView. If the sortOrder list has one TreeTableColumn within it,
1716      * the TableView will be sorted using the
1717      * {@link TreeTableColumn#sortTypeProperty() sortType} and
1718      * {@link TreeTableColumn#comparatorProperty() comparator} properties of this
1719      * TreeTableColumn (assuming
1720      * {@link TreeTableColumn#sortableProperty() TreeTableColumn.sortable} is true).
1721      * If the sortOrder list contains multiple TreeTableColumn instances, then
1722      * the TableView is firstly sorted based on the properties of the first
1723      * TreeTableColumn. If two elements are considered equal, then the second
1724      * TreeTableColumn in the list is used to determine ordering. This repeats until
1725      * the results from all TreeTableColumn comparators are considered, if necessary.
1726      *
1727      * @return An ObservableList containing zero or more TreeTableColumn instances.
1728      */
1729     public final ObservableList<TreeTableColumn<S,?>> getSortOrder() {
1730         return sortOrder;
1731     }
1732 
1733     /**
1734      * Applies the currently installed resize policy against the given column,
1735      * resizing it based on the delta value provided.
1736      * @param column the column
1737      * @param delta the delta
1738      * @return true if column resizing is applied
1739      */
1740     public boolean resizeColumn(TreeTableColumn<S,?> column, double delta) {
1741         if (column == null || Double.compare(delta, 0.0) == 0) return false;
1742 
1743         boolean allowed = getColumnResizePolicy().call(new TreeTableView.ResizeFeatures<S>(TreeTableView.this, column, delta));
1744         if (!allowed) return false;
1745         return true;
1746     }
1747 
1748     /**
1749      * Causes the cell at the given row/column view indexes to switch into
1750      * its editing state, if it is not already in it, and assuming that the
1751      * TableView and column are also editable.
1752      * @param row the row
1753      * @param column the column
1754      */
1755     public void edit(int row, TreeTableColumn<S,?> column) {
1756         if (!isEditable() || (column != null && ! column.isEditable())) {
1757             return;
1758         }
1759 
1760         if (row < 0 && column == null) {
1761             setEditingCell(null);
1762         } else {
1763             setEditingCell(new TreeTablePosition<>(this, row, column));
1764         }
1765     }
1766 
1767     /**
1768      * Returns an unmodifiable list containing the currently visible leaf columns.
1769      * @return an unmodifiable list containing the currently visible leaf columns
1770      */
1771     public ObservableList<TreeTableColumn<S,?>> getVisibleLeafColumns() {
1772         return unmodifiableVisibleLeafColumns;
1773     }
1774 
1775     /**
1776      * Returns the position of the given column, relative to all other
1777      * visible leaf columns.
1778      * @param column the column
1779      * @return the position of the given column, relative to all other
1780      * visible leaf columns
1781      */
1782     public int getVisibleLeafIndex(TreeTableColumn<S,?> column) {
1783         return getVisibleLeafColumns().indexOf(column);
1784     }
1785 
1786     /**
1787      * Returns the TreeTableColumn in the given column index, relative to all other
1788      * visible leaf columns.
1789      * @param column the column
1790      * @return the TreeTableColumn in the given column index, relative to all other
1791      * visible leaf columns
1792      */
1793     public TreeTableColumn<S,?> getVisibleLeafColumn(int column) {
1794         if (column < 0 || column >= visibleLeafColumns.size()) return null;
1795         return visibleLeafColumns.get(column);
1796     }
1797 
1798     /**
1799      * The sort method forces the TreeTableView to re-run its sorting algorithm. More
1800      * often than not it is not necessary to call this method directly, as it is
1801      * automatically called when the {@link #getSortOrder() sort order},
1802      * {@link #sortPolicyProperty() sort policy}, or the state of the
1803      * TreeTableColumn {@link TreeTableColumn#sortTypeProperty() sort type} properties
1804      * change. In other words, this method should only be called directly when
1805      * something external changes and a sort is required.
1806      */
1807     public void sort() {
1808         final ObservableList<TreeTableColumn<S,?>> sortOrder = getSortOrder();
1809 
1810         // update the Comparator property
1811         final Comparator<TreeItem<S>> oldComparator = getComparator();
1812         setComparator(sortOrder.isEmpty() ? null : new TableColumnComparatorBase.TreeTableColumnComparator(sortOrder));
1813 
1814         // fire the onSort event and check if it is consumed, if
1815         // so, don't run the sort
1816         SortEvent<TreeTableView<S>> sortEvent = new SortEvent<>(TreeTableView.this, TreeTableView.this);
1817         fireEvent(sortEvent);
1818         if (sortEvent.isConsumed()) {
1819             // if the sort is consumed we could back out the last action (the code
1820             // is commented out right below), but we don't as we take it as a
1821             // sign that the developer has decided to handle the event themselves.
1822 
1823             // sortLock = true;
1824             // TableUtil.handleSortFailure(sortOrder, lastSortEventType, lastSortEventSupportInfo);
1825             // sortLock = false;
1826             return;
1827         }
1828 
1829         final List<TreeTablePosition<S,?>> prevState = new ArrayList<>(getSelectionModel().getSelectedCells());
1830         final int itemCount = prevState.size();
1831 
1832         // we set makeAtomic to true here, so that we don't fire intermediate
1833         // sort events - instead we send a single permutation event at the end
1834         // of this method.
1835         getSelectionModel().startAtomic();
1836 
1837         // get the sort policy and run it
1838         Callback<TreeTableView<S>, Boolean> sortPolicy = getSortPolicy();
1839         if (sortPolicy == null) return;
1840         Boolean success = sortPolicy.call(this);
1841 
1842         getSelectionModel().stopAtomic();
1843 
1844         if (success == null || ! success) {
1845             // the sort was a failure. Need to backout if possible
1846             sortLock = true;
1847             TableUtil.handleSortFailure(sortOrder, lastSortEventType, lastSortEventSupportInfo);
1848             setComparator(oldComparator);
1849             sortLock = false;
1850         } else {
1851             // sorting was a success, now we possibly fire an event on the
1852             // selection model that the items list has 'permutated' to a new ordering
1853 
1854             // FIXME we should support alternative selection model implementations!
1855             if (getSelectionModel() instanceof TreeTableViewArrayListSelectionModel) {
1856                 final TreeTableViewArrayListSelectionModel<S> sm = (TreeTableViewArrayListSelectionModel<S>) getSelectionModel();
1857                 final ObservableList<TreeTablePosition<S, ?>> newState = sm.getSelectedCells();
1858 
1859                 List<TreeTablePosition<S, ?>> removed = new ArrayList<>();
1860                 for (int i = 0; i < itemCount; i++) {
1861                     TreeTablePosition<S, ?> prevItem = prevState.get(i);
1862                     if (!newState.contains(prevItem)) {
1863                         removed.add(prevItem);
1864                     }
1865                 }
1866 
1867                 if (!removed.isEmpty()) {
1868                     // the sort operation effectively permutates the selectedCells list,
1869                     // but we cannot fire a permutation event as we are talking about
1870                     // TreeTablePosition's changing (which may reside in the same list
1871                     // position before and after the sort). Therefore, we need to fire
1872                     // a single add/remove event to cover the added and removed positions.
1873                     ListChangeListener.Change<TreeTablePosition<S, ?>> c = new NonIterableChange.GenericAddRemoveChange<>(0, itemCount, removed, newState);
1874                     sm.fireCustomSelectedCellsListChangeEvent(c);
1875                 }
1876             }
1877         }
1878     }
1879 
1880     /**
1881      * Calling {@code refresh()} forces the TreeTableView control to recreate and
1882      * repopulate the cells necessary to populate the visual bounds of the control.
1883      * In other words, this forces the TreeTableView to update what it is showing to
1884      * the user. This is useful in cases where the underlying data source has
1885      * changed in a way that is not observed by the TreeTableView itself.
1886      *
1887      * @since JavaFX 8u60
1888      */
1889     public void refresh() {
1890         getProperties().put(Properties.RECREATE, Boolean.TRUE);
1891     }
1892 
1893 
1894 
1895     /***************************************************************************
1896      *                                                                         *
1897      * Private Implementation                                                  *
1898      *                                                                         *
1899      **************************************************************************/
1900 
1901     private boolean sortLock = false;
1902     private TableUtil.SortEventType lastSortEventType = null;
1903     private Object[] lastSortEventSupportInfo = null;
1904 
1905     private void doSort(final TableUtil.SortEventType sortEventType, final Object... supportInfo) {
1906         if (sortLock) {
1907             return;
1908         }
1909 
1910         this.lastSortEventType = sortEventType;
1911         this.lastSortEventSupportInfo = supportInfo;
1912         sort();
1913         this.lastSortEventType = null;
1914         this.lastSortEventSupportInfo = null;
1915     }
1916 
1917     private void updateExpandedItemCount(TreeItem<S> treeItem) {
1918         setExpandedItemCount(TreeUtil.updateExpandedItemCount(treeItem, expandedItemCountDirty, isShowRoot()));
1919 
1920         if (expandedItemCountDirty) {
1921             // this is a very inefficient thing to do, but for now having a cache
1922             // is better than nothing at all...
1923             treeItemCacheMap.clear();
1924         }
1925 
1926         expandedItemCountDirty = false;
1927     }
1928 
1929     private void updateRootExpanded() {
1930         // if we aren't showing the root, and the root isn't expanded, we expand
1931         // it now so that something is shown.
1932         if (!isShowRoot() && getRoot() != null && ! getRoot().isExpanded()) {
1933             getRoot().setExpanded(true);
1934         }
1935     }
1936 
1937 
1938     // --- Content width
1939     private void setContentWidth(double contentWidth) {
1940         this.contentWidth = contentWidth;
1941         if (isInited) {
1942             // sometimes the current column resize policy will have to modify the
1943             // column width of all columns in the table if the table width changes,
1944             // so we short-circuit the resize function and just go straight there
1945             // with a null TreeTableColumn, which indicates to the resize policy function
1946             // that it shouldn't actually do anything specific to one column.
1947             getColumnResizePolicy().call(new TreeTableView.ResizeFeatures<S>(TreeTableView.this, null, 0.0));
1948         }
1949     }
1950 
1951     /**
1952      * Recomputes the currently visible leaf columns in this TableView.
1953      */
1954     private void updateVisibleLeafColumns() {
1955         // update visible leaf columns list
1956         List<TreeTableColumn<S,?>> cols = new ArrayList<TreeTableColumn<S,?>>();
1957         buildVisibleLeafColumns(getColumns(), cols);
1958         visibleLeafColumns.setAll(cols);
1959 
1960         // sometimes the current column resize policy will have to modify the
1961         // column width of all columns in the table if the table width changes,
1962         // so we short-circuit the resize function and just go straight there
1963         // with a null TreeTableColumn, which indicates to the resize policy function
1964         // that it shouldn't actually do anything specific to one column.
1965         getColumnResizePolicy().call(new TreeTableView.ResizeFeatures<S>(TreeTableView.this, null, 0.0));
1966     }
1967 
1968     private void buildVisibleLeafColumns(List<TreeTableColumn<S,?>> cols, List<TreeTableColumn<S,?>> vlc) {
1969         for (TreeTableColumn<S,?> c : cols) {
1970             if (c == null) continue;
1971 
1972             boolean hasChildren = ! c.getColumns().isEmpty();
1973 
1974             if (hasChildren) {
1975                 buildVisibleLeafColumns(c.getColumns(), vlc);
1976             } else if (c.isVisible()) {
1977                 vlc.add(c);
1978             }
1979         }
1980     }
1981 
1982 
1983 
1984     /***************************************************************************
1985      *                                                                         *
1986      * Stylesheet Handling                                                     *
1987      *                                                                         *
1988      **************************************************************************/
1989 
1990     private static final String DEFAULT_STYLE_CLASS = "tree-table-view";
1991 
1992     private static final PseudoClass PSEUDO_CLASS_CELL_SELECTION =
1993             PseudoClass.getPseudoClass("cell-selection");
1994     private static final PseudoClass PSEUDO_CLASS_ROW_SELECTION =
1995             PseudoClass.getPseudoClass("row-selection");
1996 
1997     private static class StyleableProperties {
1998         private static final CssMetaData<TreeTableView<?>,Number> FIXED_CELL_SIZE =
1999                 new CssMetaData<TreeTableView<?>,Number>("-fx-fixed-cell-size",
2000                                                      SizeConverter.getInstance(),
2001                                                      Region.USE_COMPUTED_SIZE) {
2002 
2003                     @Override public Double getInitialValue(TreeTableView<?> node) {
2004                         return node.getFixedCellSize();
2005                     }
2006 
2007                     @Override public boolean isSettable(TreeTableView<?> n) {
2008                         return n.fixedCellSize == null || !n.fixedCellSize.isBound();
2009                     }
2010 
2011                     @Override public StyleableProperty<Number> getStyleableProperty(TreeTableView<?> n) {
2012                         return (StyleableProperty<Number>)(WritableValue<Number>) n.fixedCellSizeProperty();
2013                     }
2014                 };
2015 
2016         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
2017         static {
2018             final List<CssMetaData<? extends Styleable, ?>> styleables =
2019                     new ArrayList<CssMetaData<? extends Styleable, ?>>(Control.getClassCssMetaData());
2020             styleables.add(FIXED_CELL_SIZE);
2021             STYLEABLES = Collections.unmodifiableList(styleables);
2022         }
2023     }
2024 
2025     /**
2026      * @return The CssMetaData associated with this class, which may include the
2027      * CssMetaData of its superclasses.
2028      */
2029     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
2030         return StyleableProperties.STYLEABLES;
2031     }
2032 
2033     /**
2034      * {@inheritDoc}
2035      * @since JavaFX 8.0
2036      */
2037     @Override
2038     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
2039         return getClassCssMetaData();
2040     }
2041 
2042     /** {@inheritDoc} */
2043     @Override protected Skin<?> createDefaultSkin() {
2044         return new TreeTableViewSkin<S>(this);
2045     }
2046 
2047 
2048 
2049     /***************************************************************************
2050      *                                                                         *
2051      * Accessibility handling                                                  *
2052      *                                                                         *
2053      **************************************************************************/
2054 
2055     /** {@inheritDoc} */
2056     @Override
2057     public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
2058         switch (attribute) {
2059             case ROW_COUNT: return getExpandedItemCount();
2060             case COLUMN_COUNT: return getVisibleLeafColumns().size();
2061 
2062             /*
2063              * TreeTableViewSkin returns TreeTableRows back to TreeTableView.
2064              * TreeTableRowSkin returns TreeTableCells back to TreeTableRow.
2065              */
2066             case SELECTED_ITEMS: {
2067                 @SuppressWarnings("unchecked")
2068                 ObservableList<TreeTableRow<S>> rows = (ObservableList<TreeTableRow<S>>)super.queryAccessibleAttribute(attribute, parameters);
2069                 List<Node> selection = new ArrayList<>();
2070                 for (TreeTableRow<S> row : rows) {
2071                     @SuppressWarnings("unchecked")
2072                     ObservableList<Node> cells = (ObservableList<Node>)row.queryAccessibleAttribute(attribute, parameters);
2073                     if (cells != null) selection.addAll(cells);
2074                 }
2075                 return FXCollections.observableArrayList(selection);
2076             }
2077             case FOCUS_ITEM: {
2078                 Node row = (Node)super.queryAccessibleAttribute(attribute, parameters);
2079                 if (row == null) return null;
2080                 Node cell = (Node)row.queryAccessibleAttribute(attribute, parameters);
2081                 /* cell equals to null means the row is a placeholder node */
2082                 return cell != null ?  cell : row;
2083             }
2084             case CELL_AT_ROW_COLUMN: {
2085                 @SuppressWarnings("unchecked")
2086                 TreeTableRow<S> row = (TreeTableRow<S>)super.queryAccessibleAttribute(attribute, parameters);
2087                 return row != null ? row.queryAccessibleAttribute(attribute, parameters) : null;
2088             }
2089             case MULTIPLE_SELECTION: {
2090                 TreeTableViewSelectionModel<S> sm = getSelectionModel();
2091                 return sm != null && sm.getSelectionMode() == SelectionMode.MULTIPLE;
2092             }
2093             default: return super.queryAccessibleAttribute(attribute, parameters);
2094         }
2095     }
2096 
2097     /***************************************************************************
2098      *                                                                         *
2099      * Support Classes                                                         *
2100      *                                                                         *
2101      **************************************************************************/
2102 
2103      /**
2104       * An immutable wrapper class for use in the TableView
2105      * {@link TreeTableView#columnResizePolicyProperty() column resize} functionality.
2106       * @since JavaFX 8.0
2107       */
2108      public static class ResizeFeatures<S> extends ResizeFeaturesBase<TreeItem<S>> {
2109         private TreeTableView<S> treeTable;
2110 
2111         /**
2112          * Creates an instance of this class, with the provided TreeTableView,
2113          * TreeTableColumn and delta values being set and stored in this immutable
2114          * instance.
2115          *
2116          * @param treeTable The TreeTableView upon which the resize operation is occurring.
2117          * @param column The column upon which the resize is occurring, or null
2118          *      if this ResizeFeatures instance is being created as a result of a
2119          *      TreeTableView resize operation.
2120          * @param delta The amount of horizontal space added or removed in the
2121          *      resize operation.
2122          */
2123         public ResizeFeatures(TreeTableView<S> treeTable, TreeTableColumn<S,?> column, Double delta) {
2124             super(column, delta);
2125             this.treeTable = treeTable;
2126         }
2127 
2128         /**
2129          * Returns the column upon which the resize is occurring, or null
2130          * if this ResizeFeatures instance was created as a result of a
2131          * TreeTableView resize operation.
2132          */
2133         @Override public TreeTableColumn<S,?> getColumn() {
2134             return (TreeTableColumn<S,?>) super.getColumn();
2135         }
2136 
2137         /**
2138          * Returns the TreeTableView upon which the resize operation is occurring.
2139          * @return the TreeTableView upon which the resize operation is occurring
2140          */
2141         public TreeTableView<S> getTable() { return treeTable; }
2142     }
2143 
2144 
2145 
2146     /**
2147      * An {@link Event} subclass used specifically in TreeTableView for representing
2148      * edit-related events. It provides additional API to easily access the
2149      * TreeItem that the edit event took place on, as well as the input provided
2150      * by the end user.
2151      *
2152      * @param <S> The type of the input, which is the same type as the TreeTableView
2153      *      itself.
2154      * @since JavaFX 8.0
2155      */
2156     public static class EditEvent<S> extends Event {
2157         private static final long serialVersionUID = -4437033058917528976L;
2158 
2159         /**
2160          * Common supertype for all edit event types.
2161          */
2162         public static final EventType<?> ANY = EDIT_ANY_EVENT;
2163 
2164         private final TreeTableView<S> source;
2165         private final S oldValue;
2166         private final S newValue;
2167         private transient final TreeItem<S> treeItem;
2168 
2169         /**
2170          * Creates a new EditEvent instance to represent an edit event. This
2171          * event is used for {@link #EDIT_START_EVENT},
2172          * {@link #EDIT_COMMIT_EVENT} and {@link #EDIT_CANCEL_EVENT} types.
2173          * @param source the source
2174          * @param eventType the eventType
2175          * @param treeItem the treeItem
2176          * @param oldValue the oldValue
2177          * @param newValue the newValue
2178          */
2179         public EditEvent(TreeTableView<S> source,
2180                          EventType<? extends TreeTableView.EditEvent> eventType,
2181                          TreeItem<S> treeItem, S oldValue, S newValue) {
2182             super(source, Event.NULL_SOURCE_TARGET, eventType);
2183             this.source = source;
2184             this.oldValue = oldValue;
2185             this.newValue = newValue;
2186             this.treeItem = treeItem;
2187         }
2188 
2189         /**
2190          * Returns the TreeTableView upon which the edit took place.
2191          * @return the TreeTableView upon which the edit took place
2192          */
2193         @Override public TreeTableView<S> getSource() {
2194             return source;
2195         }
2196 
2197         /**
2198          * Returns the {@link TreeItem} upon which the edit took place.
2199          * @return the {@link TreeItem} upon which the edit took place
2200          */
2201         public TreeItem<S> getTreeItem() {
2202             return treeItem;
2203         }
2204 
2205         /**
2206          * Returns the new value input into the TreeItem by the end user.
2207          * @return the new value input into the TreeItem by the end user
2208          */
2209         public S getNewValue() {
2210             return newValue;
2211         }
2212 
2213         /**
2214          * Returns the old value that existed in the TreeItem prior to the current
2215          * edit event.
2216          * @return the old value that existed in the TreeItem prior to the current
2217          * edit event
2218          */
2219         public S getOldValue() {
2220             return oldValue;
2221         }
2222     }
2223 
2224 
2225 
2226      /**
2227      * A simple extension of the {@link SelectionModel} abstract class to
2228      * allow for special support for TreeTableView controls.
2229       *
2230      * @since JavaFX 8.0
2231      */
2232     public static abstract class TreeTableViewSelectionModel<S> extends
2233             TableSelectionModel<TreeItem<S>> {
2234 
2235         /***********************************************************************
2236          *                                                                     *
2237          * Private fields                                                      *
2238          *                                                                     *
2239          **********************************************************************/
2240 
2241         private final TreeTableView<S> treeTableView;
2242 
2243 
2244         /***********************************************************************
2245          *                                                                     *
2246          * Constructors                                                        *
2247          *                                                                     *
2248          **********************************************************************/
2249 
2250         /**
2251          * Builds a default TreeTableViewSelectionModel instance with the provided
2252          * TreeTableView.
2253          * @param treeTableView The TreeTableView upon which this selection model should
2254          *      operate.
2255          * @throws NullPointerException TreeTableView can not be null.
2256          */
2257         public TreeTableViewSelectionModel(final TreeTableView<S> treeTableView) {
2258             if (treeTableView == null) {
2259                 throw new NullPointerException("TreeTableView can not be null");
2260             }
2261 
2262             this.treeTableView = treeTableView;
2263         }
2264 
2265 
2266 
2267         /***********************************************************************
2268          *                                                                     *
2269          * Abstract API                                                        *
2270          *                                                                     *
2271          **********************************************************************/
2272 
2273          /**
2274          * A read-only ObservableList representing the currently selected cells
2275          * in this TreeTableView. Rather than directly modify this list, please
2276          * use the other methods provided in the TreeTableViewSelectionModel.
2277          * @return a list of selected cells
2278          */
2279         public abstract ObservableList<TreeTablePosition<S,?>> getSelectedCells();
2280 
2281 
2282 
2283         /***********************************************************************
2284          *                                                                     *
2285          * Public API                                                          *
2286          *                                                                     *
2287          **********************************************************************/
2288 
2289          /**
2290           * Returns the TreeTableView instance that this selection model is installed in.
2291          * @return the TreeTableView instance that this selection model is installed in
2292           */
2293          public TreeTableView<S> getTreeTableView() {
2294              return treeTableView;
2295          }
2296 
2297          /** {@inheritDoc} */
2298          @Override public TreeItem<S> getModelItem(int index) {
2299              return treeTableView.getTreeItem(index);
2300          }
2301 
2302          /** {@inheritDoc} */
2303          @Override protected int getItemCount() {
2304              return treeTableView.getExpandedItemCount();
2305          }
2306 
2307          /** {@inheritDoc} */
2308          @Override public void focus(int row) {
2309              focus(row, null);
2310          }
2311 
2312          /** {@inheritDoc} */
2313          @Override public int getFocusedIndex() {
2314              return getFocusedCell().getRow();
2315          }
2316 
2317          /** {@inheritDoc} */
2318          @Override public void selectRange(int minRow, TableColumnBase<TreeItem<S>,?> minColumn,
2319                                            int maxRow, TableColumnBase<TreeItem<S>,?> maxColumn) {
2320              final int minColumnIndex = treeTableView.getVisibleLeafIndex((TreeTableColumn<S,?>)minColumn);
2321              final int maxColumnIndex = treeTableView.getVisibleLeafIndex((TreeTableColumn<S,?>)maxColumn);
2322              for (int _row = minRow; _row <= maxRow; _row++) {
2323                  for (int _col = minColumnIndex; _col <= maxColumnIndex; _col++) {
2324                      select(_row, treeTableView.getVisibleLeafColumn(_col));
2325                  }
2326              }
2327          }
2328 
2329 
2330 
2331         /***********************************************************************
2332          *                                                                     *
2333          * Private implementation                                              *
2334          *                                                                     *
2335          **********************************************************************/
2336 
2337          private void focus(int row, TreeTableColumn<S,?> column) {
2338              focus(new TreeTablePosition<>(getTreeTableView(), row, column));
2339          }
2340 
2341          private void focus(TreeTablePosition<S,?> pos) {
2342              if (getTreeTableView().getFocusModel() == null) return;
2343 
2344              getTreeTableView().getFocusModel().focus(pos.getRow(), pos.getTableColumn());
2345          }
2346 
2347          private TreeTablePosition<S,?> getFocusedCell() {
2348              if (treeTableView.getFocusModel() == null) {
2349                  return new TreeTablePosition<>(treeTableView, -1, null);
2350              }
2351              return treeTableView.getFocusModel().getFocusedCell();
2352          }
2353      }
2354 
2355 
2356 
2357     /**
2358      * A primitive selection model implementation, using a List<Integer> to store all
2359      * selected indices.
2360      */
2361     // package for testing
2362     static class TreeTableViewArrayListSelectionModel<S> extends TreeTableViewSelectionModel<S> {
2363 
2364         private final MappingChange.Map<TreeTablePosition<S,?>,Integer> cellToIndicesMap = f -> f.getRow();
2365 
2366         private TreeTableView<S> treeTableView = null;
2367 
2368         /***********************************************************************
2369          *                                                                     *
2370          * Constructors                                                        *
2371          *                                                                     *
2372          **********************************************************************/
2373 
2374         public TreeTableViewArrayListSelectionModel(final TreeTableView<S> treeTableView) {
2375             super(treeTableView);
2376             this.treeTableView = treeTableView;
2377 
2378             this.treeTableView.rootProperty().addListener(weakRootPropertyListener);
2379             this.treeTableView.showRootProperty().addListener(showRootPropertyListener);
2380             updateTreeEventListener(null, treeTableView.getRoot());
2381 
2382             selectedCellsMap = new SelectedCellsMap<TreeTablePosition<S,?>>(this::fireCustomSelectedCellsListChangeEvent) {
2383                 @Override public boolean isCellSelectionEnabled() {
2384                     return TreeTableViewArrayListSelectionModel.this.isCellSelectionEnabled();
2385                 }
2386             };
2387 
2388             selectedCellsSeq = new ReadOnlyUnbackedObservableList<TreeTablePosition<S,?>>() {
2389                 @Override public TreeTablePosition<S,?> get(int i) {
2390                     return selectedCellsMap.get(i);
2391                 }
2392 
2393                 @Override public int size() {
2394                     return selectedCellsMap.size();
2395                 }
2396             };
2397 //            selectedCellsSeq.addListener((ListChangeListener<? super TreeTablePosition<S,?>>) c -> {
2398 //                ControlUtils.updateSelectedIndices(this, c);
2399 //            });
2400 
2401             updateDefaultSelection();
2402 
2403             cellSelectionEnabledProperty().addListener(o -> {
2404                 updateDefaultSelection();
2405                 TableCellBehaviorBase.setAnchor(treeTableView, getFocusedCell(), true);
2406             });
2407         }
2408 
2409         private void dispose() {
2410             this.treeTableView.rootProperty().removeListener(weakRootPropertyListener);
2411             this.treeTableView.showRootProperty().removeListener(showRootPropertyListener);
2412 
2413             TreeItem<S> root = this.treeTableView.getRoot();
2414             if (root != null) {
2415                 root.removeEventHandler(TreeItem.<S>expandedItemCountChangeEvent(), weakTreeItemListener);
2416             }
2417         }
2418 
2419         private void updateTreeEventListener(TreeItem<S> oldRoot, TreeItem<S> newRoot) {
2420             if (oldRoot != null && weakTreeItemListener != null) {
2421                 oldRoot.removeEventHandler(TreeItem.<S>expandedItemCountChangeEvent(), weakTreeItemListener);
2422             }
2423 
2424             if (newRoot != null) {
2425                 weakTreeItemListener = new WeakEventHandler<>(treeItemListener);
2426                 newRoot.addEventHandler(TreeItem.<S>expandedItemCountChangeEvent(), weakTreeItemListener);
2427             }
2428         }
2429 
2430         private ChangeListener<TreeItem<S>> rootPropertyListener = (observable, oldValue, newValue) -> {
2431             updateDefaultSelection();
2432 
2433             updateTreeEventListener(oldValue, newValue);
2434         };
2435 
2436         private InvalidationListener showRootPropertyListener = o -> {
2437             shiftSelection(0, treeTableView.isShowRoot() ? 1 : -1, null);
2438         };
2439 
2440         private EventHandler<TreeItem.TreeModificationEvent<S>> treeItemListener = new EventHandler<>() {
2441             @Override public void handle(TreeItem.TreeModificationEvent<S> e) {
2442 
2443                 if (getSelectedIndex() == -1 && getSelectedItem() == null) return;
2444 
2445                 final TreeItem<S> treeItem = e.getTreeItem();
2446                 if (treeItem == null) return;
2447 
2448                 final int oldSelectedIndex = getSelectedIndex();
2449 
2450                 treeTableView.expandedItemCountDirty = true;
2451 
2452                 // we only shift selection from this row - everything before it
2453                 // is safe. We might change this below based on certain criteria
2454                 int startRow = treeTableView.getRow(treeItem);
2455 
2456                 int shift = 0;
2457                 ListChangeListener.Change<? extends TreeItem<?>> change = e.getChange();
2458                 if (change != null) {
2459                     change.next();
2460                 }
2461 
2462                 do {
2463                     final int addedSize = change == null ? 0 : change.getAddedSize();
2464                     final int removedSize = change == null ? 0 : change.getRemovedSize();
2465 
2466                     if (e.wasExpanded()) {
2467                         // need to shuffle selection by the number of visible children
2468                         shift += treeItem.getExpandedDescendentCount(false) - 1;
2469                         startRow++;
2470                     } else if (e.wasCollapsed()) {
2471                         // remove selection from any child treeItem, and also determine
2472                         // if any child item was selected (in which case the parent
2473                         // takes the selection on collapse)
2474                         treeItem.getExpandedDescendentCount(false);
2475                         final int count = treeItem.previousExpandedDescendentCount;
2476 
2477                         final int selectedIndex = getSelectedIndex();
2478                         final boolean wasPrimarySelectionInChild =
2479                                 selectedIndex >= (startRow + 1) &&
2480                                         selectedIndex < (startRow + count);
2481 
2482                         boolean wasAnyChildSelected = false;
2483                         final boolean isCellSelectionMode = isCellSelectionEnabled();
2484                         ObservableList<TreeTableColumn<S, ?>> columns = getTreeTableView().getVisibleLeafColumns();
2485 
2486                         selectedIndices._beginChange();
2487                         final int from = startRow + 1;
2488                         final int to = startRow + count;
2489                         final List<Integer> removed = new ArrayList<>();
2490                         TreeTableColumn<S, ?> selectedColumn = null;
2491                         for (int i = from; i < to; i++) {
2492                             // we have to handle cell selection mode differently than
2493                             // row selection mode. Refer to RT-34103 for the bug report
2494                             // that drove this change, but in short the issue was that
2495                             // when collapsing a branch that had selection, we were
2496                             // always calling isSelected(row), but that always returns
2497                             // false in cell selection mode.
2498                             if (isCellSelectionMode) {
2499                                 for (int column = 0; column < columns.size(); column++) {
2500                                     final TreeTableColumn<S, ?> col = columns.get(column);
2501                                     if (isSelected(i, col)) {
2502                                         wasAnyChildSelected = true;
2503                                         clearSelection(i, col);
2504                                         selectedColumn = col;
2505                                     }
2506                                 }
2507                             } else {
2508                                 if (isSelected(i)) {
2509                                     wasAnyChildSelected = true;
2510                                     removed.add(i);
2511                                 }
2512                             }
2513                         }
2514 
2515                         ControlUtils.reducingChange(selectedIndices, removed);
2516 
2517                         for (int index : removed) {
2518                             startAtomic();
2519                             // we pass in false here to prevent a lookup into the TreeItem, as it is unnecessary
2520                             // and results in JDK-8152396
2521                             clearSelection(new TreeTablePosition<>(treeTableView, index, null, false));
2522                             stopAtomic();
2523                         }
2524                         selectedIndices._endChange();
2525 
2526                         // put selection onto the newly-collapsed tree item
2527                         if (wasPrimarySelectionInChild && wasAnyChildSelected) {
2528                             select(startRow, selectedColumn);
2529                         }
2530 
2531                         shift += -count + 1;
2532                         startRow++;
2533                     } else if (e.wasPermutated()) {
2534                         // General approach:
2535                         //   -- detected a sort has happened
2536                         //   -- Create a permutation lookup map (1)
2537                         //   -- dump all the selected indices into a list (2)
2538                         //   -- create a list containing the new indices (3)
2539                         //   -- for each previously-selected index (4)
2540                         //     -- if index is in the permutation lookup map
2541                         //       -- add the new index to the new indices list
2542                         //   -- Perform batch selection (5)
2543 
2544                         startAtomic();
2545 
2546                         final int offset = startRow + 1;
2547 
2548                         // (1)
2549                         int length = e.getTo() - e.getFrom();
2550                         HashMap<Integer, Integer> pMap = new HashMap<> (length);
2551                         for (int i = e.getFrom(); i < e.getTo(); i++) {
2552                             pMap.put(i, e.getChange().getPermutation(i));
2553                         }
2554 
2555                         // (2)
2556                         List<TreeTablePosition<S,?>> selectedIndices = new ArrayList<>(getSelectedCells());
2557 
2558                         // (3)
2559                         List<TreeTablePosition<S,?>> newIndices = new ArrayList<>(selectedIndices.size());
2560 
2561                         // (4)
2562                         boolean selectionIndicesChanged = false;
2563                         for (int i = 0; i < selectedIndices.size(); i++) {
2564                             final TreeTablePosition<S,?> oldIndex = selectedIndices.get(i);
2565                             final int oldRow = oldIndex.getRow() - offset;
2566 
2567                             if (pMap.containsKey(oldRow)) {
2568                                 int newIndex = pMap.get(oldRow) + offset;
2569 
2570                                 selectionIndicesChanged = selectionIndicesChanged || newIndex != oldRow;
2571 
2572                                 newIndices.add(new TreeTablePosition<>(oldIndex.getTreeTableView(), newIndex, oldIndex.getTableColumn()));
2573                             }
2574 
2575                             // check if the root element of this event was selected, so that we can retain it
2576                             if (oldIndex.getRow() == startRow) {
2577                                 newIndices.add(new TreeTablePosition<>(oldIndex.getTreeTableView(), oldIndex.getRow(), oldIndex.getTableColumn()));
2578                             }
2579                         }
2580 
2581                         if (selectionIndicesChanged) {
2582                             // (5)
2583                             quietClearSelection();
2584                             stopAtomic();
2585 
2586                             selectedCellsMap.setAll(newIndices);
2587 
2588                             final int offsetOldIndex = oldSelectedIndex - offset;
2589                             if (offsetOldIndex >= 0 && offsetOldIndex < getItemCount()) {
2590                                 int newIndex = e.getChange().getPermutation(offsetOldIndex);
2591                                 setSelectedIndex(newIndex + offset);
2592                                 focus(newIndex + offset);
2593                             }
2594                         } else {
2595                             stopAtomic();
2596                         }
2597                     } else if (e.wasAdded()) {
2598                         // shuffle selection by the number of added items
2599                         shift += treeItem.isExpanded() ? addedSize : 0;
2600 
2601                         // RT-32963: We were taking the startRow from the TreeItem
2602                         // in which the children were added, rather than from the
2603                         // actual position of the new child. This led to selection
2604                         // being moved off the parent TreeItem by mistake.
2605                         // The 'if (e.getAddedSize() == 1)' condition here was
2606                         // subsequently commented out due to RT-33894.
2607                         startRow = treeTableView.getRow(e.getChange().getAddedSubList().get(0));
2608 
2609                         TreeTablePosition<S, ?> anchor = TreeTableCellBehavior.getAnchor(treeTableView, null);
2610                         if (anchor != null) {
2611                             boolean isAnchorSelected = isSelected(anchor.getRow(), anchor.getTableColumn());
2612                             if (isAnchorSelected) {
2613                                 TreeTablePosition<S, ?> newAnchor = new TreeTablePosition<>(treeTableView, anchor.getRow() + shift, anchor.getTableColumn());
2614                                 TreeTableCellBehavior.setAnchor(treeTableView, newAnchor, false);
2615                             }
2616                         }
2617                     } else if (e.wasRemoved()) {
2618                         // shuffle selection by the number of removed items
2619                         shift += treeItem.isExpanded() ? -removedSize : 0;
2620 
2621                         // the start row is incorrect - it is _not_ the index of the
2622                         // TreeItem in which the children were removed from (which is
2623                         // what it currently represents). We need to take the 'from'
2624                         // value out of the event and make use of that to understand
2625                         // what actually changed inside the children list.
2626                         startRow += e.getFrom() + 1;
2627 
2628                         // whilst we are here, we should check if the removed items
2629                         // are part of the selectedItems list - and remove them
2630                         // from selection if they are (as per RT-15446)
2631                         final List<Integer> selectedIndices = getSelectedIndices();
2632                         final List<TreeItem<S>> selectedItems = getSelectedItems();
2633                         final TreeItem<S> selectedItem = getSelectedItem();
2634                         final List<? extends TreeItem<S>> removedChildren = e.getChange().getRemoved();
2635 
2636                         for (int i = 0; i < selectedIndices.size() && !selectedItems.isEmpty(); i++) {
2637                             int index = selectedIndices.get(i);
2638                             if (index > selectedItems.size()) break;
2639 
2640                             if (removedChildren.size() == 1 &&
2641                                     selectedItems.size() == 1 &&
2642                                     selectedItem != null &&
2643                                     selectedItem.equals(removedChildren.get(0))) {
2644                                 // Bug fix for RT-28637
2645                                 if (oldSelectedIndex < getItemCount()) {
2646                                     final int previousRow = oldSelectedIndex == 0 ? 0 : oldSelectedIndex - 1;
2647                                     TreeItem<S> newSelectedItem = getModelItem(previousRow);
2648                                     if (!selectedItem.equals(newSelectedItem)) {
2649                                         clearAndSelect(previousRow);
2650                                     }
2651                                 }
2652                             }
2653                         }
2654                     }
2655                 } while (e.getChange() != null && e.getChange().next());
2656 
2657                 shiftSelection(startRow, shift, new Callback<ShiftParams, Void>() {
2658                     @Override public Void call(ShiftParams param) {
2659 
2660                         // we make the shifts atomic, as otherwise listeners to
2661                         // the items / indices lists get a lot of intermediate
2662                         // noise. They eventually get the summary event fired
2663                         // from within shiftSelection, so this is ok.
2664                         startAtomic();
2665 
2666                         final int clearIndex = param.getClearIndex();
2667                         final int setIndex = param.getSetIndex();
2668                         TreeTablePosition<S,?> oldTP = null;
2669                         if (clearIndex > -1) {
2670                             for (int i = 0; i < selectedCellsMap.size(); i++) {
2671                                 TreeTablePosition<S,?> tp = selectedCellsMap.get(i);
2672                                 if (tp.getRow() == clearIndex) {
2673                                     oldTP = tp;
2674                                     selectedCellsMap.remove(tp);
2675                                 } else if (tp.getRow() == setIndex && !param.isSelected()) {
2676                                     selectedCellsMap.remove(tp);
2677                                 }
2678                             }
2679                         }
2680 
2681                         if (oldTP != null && param.isSelected()) {
2682                             TreeTablePosition<S,?> newTP = new TreeTablePosition<>(
2683                                     treeTableView, param.getSetIndex(), oldTP.getTableColumn());
2684 
2685                             selectedCellsMap.add(newTP);
2686                         }
2687 
2688                         stopAtomic();
2689 
2690                         return null;
2691                     }
2692                 });
2693             }
2694         };
2695 
2696         private WeakChangeListener<TreeItem<S>> weakRootPropertyListener =
2697                 new WeakChangeListener<>(rootPropertyListener);
2698 
2699         private WeakEventHandler<TreeItem.TreeModificationEvent<S>> weakTreeItemListener;
2700 
2701 
2702 
2703         /***********************************************************************
2704          *                                                                     *
2705          * Observable properties (and getters/setters)                         *
2706          *                                                                     *
2707          **********************************************************************/
2708 
2709         // the only 'proper' internal data structure, selectedItems and selectedIndices
2710         // are both 'read-only and unbacked'.
2711         private final SelectedCellsMap<TreeTablePosition<S,?>> selectedCellsMap;
2712 
2713         private final ReadOnlyUnbackedObservableList<TreeTablePosition<S,?>> selectedCellsSeq;
2714         @Override public ObservableList<TreeTablePosition<S,?>> getSelectedCells() {
2715             return selectedCellsSeq;
2716         }
2717 
2718 
2719         /***********************************************************************
2720          *                                                                     *
2721          * Internal properties                                                 *
2722          *                                                                     *
2723          **********************************************************************/
2724 
2725 
2726 
2727         /***********************************************************************
2728          *                                                                     *
2729          * Public selection API                                                *
2730          *                                                                     *
2731          **********************************************************************/
2732 
2733         @Override public void clearAndSelect(int row) {
2734             clearAndSelect(row, null);
2735         }
2736 
2737         @Override public void clearAndSelect(int row, TableColumnBase<TreeItem<S>,?> column) {
2738             if (row < 0 || row >= getItemCount()) return;
2739 
2740             final TreeTablePosition<S,?> newTablePosition = new TreeTablePosition<>(getTreeTableView(), row, (TreeTableColumn<S,?>)column);
2741             final boolean isCellSelectionEnabled = isCellSelectionEnabled();
2742 
2743             // replace the anchor
2744             TreeTableCellBehavior.setAnchor(treeTableView, newTablePosition, false);
2745 
2746             // firstly we make a copy of the selection, so that we can send out
2747             // the correct details in the selection change event.
2748             List<TreeTablePosition<S,?>> previousSelection = new ArrayList<>(selectedCellsMap.getSelectedCells());
2749 
2750             // secondly we check if we can short-circuit out of here because the new selection
2751             // equals the current selection
2752             final boolean wasSelected = isSelected(row, column);
2753             if (wasSelected && previousSelection.size() == 1) {
2754                 // before we return, we double-check that the selected item
2755                 // is equal to the item in the given index
2756                 TreeTablePosition<S,?> selectedCell = getSelectedCells().get(0);
2757                 if (getSelectedItem() == getModelItem(row)) {
2758                     if (selectedCell.getRow() == row && selectedCell.getTableColumn() == column) {
2759                         return;
2760                     }
2761                 }
2762             }
2763 
2764             // RT-32411: We used to call quietClearSelection() here, but this
2765             // resulted in the selectedItems and selectedIndices lists never
2766             // reporting that they were empty.
2767             // makeAtomic toggle added to resolve RT-32618
2768             startAtomic();
2769 
2770             // then clear the current selection
2771             clearSelection();
2772 
2773             // and select the new cell
2774             select(row, column);
2775 
2776             stopAtomic();
2777 
2778             // We remove the new selection from the list seeing as it is not removed.
2779             if (isCellSelectionEnabled) {
2780                 previousSelection.remove(newTablePosition);
2781             } else {
2782                 for (TreeTablePosition<S,?> tp : previousSelection) {
2783                     if (tp.getRow() == row) {
2784                         previousSelection.remove(tp);
2785                         break;
2786                     }
2787                 }
2788             }
2789 
2790             // fire off a single add/remove/replace notification (rather than
2791             // individual remove and add notifications) - see RT-33324
2792             ListChangeListener.Change<TreeTablePosition<S, ?>> change;
2793 
2794             /*
2795              * getFrom() documentation:
2796              *   If wasAdded is true, the interval contains all the values that were added.
2797              *   If wasPermutated is true, the interval marks the values that were permutated.
2798              *   If wasRemoved is true and wasAdded is false, getFrom() and getTo() should
2799              *   return the same number - the place where the removed elements were positioned in the list.
2800              */
2801             if (wasSelected) {
2802                 change = ControlUtils.buildClearAndSelectChange(selectedCellsSeq, previousSelection, row);
2803             } else {
2804                 final int changeIndex = isCellSelectionEnabled ? 0 : Math.max(0, selectedCellsSeq.indexOf(newTablePosition));
2805                 final int changeSize = isCellSelectionEnabled ? getSelectedCells().size() : 1;
2806                 change = new NonIterableChange.GenericAddRemoveChange<>(
2807                         changeIndex, changeIndex + changeSize, previousSelection, selectedCellsSeq);
2808 //                selectedCellsSeq._beginChange();
2809 //                selectedCellsSeq._nextAdd(changeIndex, changeIndex + changeSize);
2810 //                selectedCellsSeq._nextRemove(changeIndex, previousSelection);
2811 //                selectedCellsSeq._endChange();
2812             }
2813 
2814             fireCustomSelectedCellsListChangeEvent(change);
2815         }
2816 
2817         @Override public void select(int row) {
2818             select(row, null);
2819         }
2820 
2821         @Override public void select(int row, TableColumnBase<TreeItem<S>,?> column) {
2822             // TODO we need to bring in the TreeView selection stuff here...
2823             if (row < 0 || row >= getRowCount()) return;
2824 
2825             // if I'm in cell selection mode but the column is null, select each
2826             // of the contained cells individually
2827             if (isCellSelectionEnabled() && column == null) {
2828                 List<TreeTableColumn<S,?>> columns = getTreeTableView().getVisibleLeafColumns();
2829                 for (int i = 0; i < columns.size(); i++) {
2830                     select(row, columns.get(i));
2831                 }
2832                 return;
2833             }
2834 
2835             if (TableCellBehavior.hasDefaultAnchor(treeTableView)) {
2836                 TableCellBehavior.removeAnchor(treeTableView);
2837             }
2838 
2839             if (getSelectionMode() == SelectionMode.SINGLE) {
2840                 quietClearSelection();
2841             }
2842             selectedCellsMap.add(new TreeTablePosition<>(getTreeTableView(), row, (TreeTableColumn<S,?>)column));
2843 
2844             updateSelectedIndex(row);
2845             focus(row, (TreeTableColumn<S, ?>) column);
2846         }
2847 
2848         @Override public void select(TreeItem<S> obj) {
2849             if (obj == null && getSelectionMode() == SelectionMode.SINGLE) {
2850                 clearSelection();
2851                 return;
2852             }
2853 
2854             int firstIndex = treeTableView.getRow(obj);
2855             if (firstIndex > -1) {
2856                 if (isSelected(firstIndex)) {
2857                     return;
2858                 }
2859 
2860                 if (getSelectionMode() == SelectionMode.SINGLE) {
2861                     quietClearSelection();
2862                 }
2863 
2864                 select(firstIndex);
2865             } else {
2866                 // if we are here, we did not find the item in the entire data model.
2867                 // Even still, we allow for this item to be set to the give object.
2868                 // We expect that in concrete subclasses of this class we observe the
2869                 // data model such that we check to see if the given item exists in it,
2870                 // whilst SelectedIndex == -1 && SelectedItem != null.
2871                 setSelectedIndex(-1);
2872                 setSelectedItem(obj);
2873             }
2874         }
2875 
2876         @Override public void selectIndices(int row, int... rows) {
2877             if (rows == null) {
2878                 select(row);
2879                 return;
2880             }
2881 
2882             /*
2883              * Performance optimisation - if multiple selection is disabled, only
2884              * process the end-most row index.
2885              */
2886             int rowCount = getRowCount();
2887 
2888             if (getSelectionMode() == SelectionMode.SINGLE) {
2889                 quietClearSelection();
2890 
2891                 for (int i = rows.length - 1; i >= 0; i--) {
2892                     int index = rows[i];
2893                     if (index >= 0 && index < rowCount) {
2894                         select(index);
2895                         break;
2896                     }
2897                 }
2898 
2899                 if (selectedCellsMap.isEmpty()) {
2900                     if (row > 0 && row < rowCount) {
2901                         select(row);
2902                     }
2903                 }
2904             } else {
2905                 int lastIndex = -1;
2906                 Set<TreeTablePosition<S,?>> positions = new LinkedHashSet<>();
2907 
2908                 // --- firstly, we special-case the non-varargs 'row' argument
2909                 if (row >= 0 && row < rowCount) {
2910                     // if I'm in cell selection mode, we want to select each
2911                     // of the contained cells individually
2912                     if (isCellSelectionEnabled()) {
2913                         List<TreeTableColumn<S,?>> columns = getTreeTableView().getVisibleLeafColumns();
2914                         for (int column = 0; column < columns.size(); column++) {
2915                             if (! selectedCellsMap.isSelected(row, column)) {
2916                                 positions.add(new TreeTablePosition<>(getTreeTableView(), row, columns.get(column)));
2917                             }
2918                         }
2919                     } else {
2920                         boolean match = selectedCellsMap.isSelected(row, -1);
2921                         if (!match) {
2922                             positions.add(new TreeTablePosition<>(getTreeTableView(), row, null));
2923                         }
2924                     }
2925 
2926                     lastIndex = row;
2927                 }
2928 
2929                 // --- now we iterate through all varargs values
2930                 for (int i = 0; i < rows.length; i++) {
2931                     int index = rows[i];
2932                     if (index < 0 || index >= rowCount) continue;
2933                     lastIndex = index;
2934 
2935                     if (isCellSelectionEnabled()) {
2936                         List<TreeTableColumn<S,?>> columns = getTreeTableView().getVisibleLeafColumns();
2937                         for (int column = 0; column < columns.size(); column++) {
2938                             if (! selectedCellsMap.isSelected(index, column)) {
2939                                 positions.add(new TreeTablePosition<>(getTreeTableView(), index, columns.get(column)));
2940                                 lastIndex = index;
2941                             }
2942                         }
2943                     } else {
2944                         if (! selectedCellsMap.isSelected(index, -1)) {
2945                             // if we are here then we have successfully gotten through the for-loop above
2946                             positions.add(new TreeTablePosition<>(getTreeTableView(), index, null));
2947                         }
2948                     }
2949                 }
2950 
2951                 selectedCellsMap.addAll(positions);
2952 
2953                 if (lastIndex != -1) {
2954                     select(lastIndex);
2955                 }
2956             }
2957         }
2958 
2959         @Override public void selectAll() {
2960             if (getSelectionMode() == SelectionMode.SINGLE) return;
2961 
2962             if (isCellSelectionEnabled()) {
2963                 List<TreeTablePosition<S,?>> indices = new ArrayList<>();
2964                 TreeTableColumn<S,?> column;
2965                 TreeTablePosition<S,?> tp = null;
2966                 for (int col = 0; col < getTreeTableView().getVisibleLeafColumns().size(); col++) {
2967                     column = getTreeTableView().getVisibleLeafColumns().get(col);
2968                     for (int row = 0; row < getRowCount(); row++) {
2969                         tp = new TreeTablePosition<>(getTreeTableView(), row, column);
2970                         indices.add(tp);
2971                     }
2972                 }
2973                 selectedCellsMap.setAll(indices);
2974 
2975                 if (tp != null) {
2976                     select(tp.getRow(), tp.getTableColumn());
2977                     focus(tp.getRow(), tp.getTableColumn());
2978                 }
2979             } else {
2980                 List<TreeTablePosition<S,?>> indices = new ArrayList<>();
2981                 for (int i = 0; i < getRowCount(); i++) {
2982                     indices.add(new TreeTablePosition<>(getTreeTableView(), i, null));
2983                 }
2984                 selectedCellsMap.setAll(indices);
2985 
2986                 int focusedIndex = getFocusedIndex();
2987                 if (focusedIndex == -1) {
2988                     final int itemCount = getItemCount();
2989                     if (itemCount > 0) {
2990                         select(itemCount - 1);
2991                         focus(indices.get(indices.size() - 1));
2992                     }
2993                 } else {
2994                     select(focusedIndex);
2995                     focus(focusedIndex);
2996                 }
2997             }
2998         }
2999 
3000         @Override public void selectRange(int minRow, TableColumnBase<TreeItem<S>,?> minColumn,
3001                                           int maxRow, TableColumnBase<TreeItem<S>,?> maxColumn) {
3002             if (getSelectionMode() == SelectionMode.SINGLE) {
3003                 quietClearSelection();
3004                 select(maxRow, maxColumn);
3005                 return;
3006             }
3007 
3008             startAtomic();
3009 
3010             final int itemCount = getItemCount();
3011             final boolean isCellSelectionEnabled = isCellSelectionEnabled();
3012 
3013             final int minColumnIndex = treeTableView.getVisibleLeafIndex((TreeTableColumn<S,?>)minColumn);
3014             final int maxColumnIndex = treeTableView.getVisibleLeafIndex((TreeTableColumn<S,?>)maxColumn);
3015             final int _minColumnIndex = Math.min(minColumnIndex, maxColumnIndex);
3016             final int _maxColumnIndex = Math.max(minColumnIndex, maxColumnIndex);
3017 
3018             final int _minRow = Math.min(minRow, maxRow);
3019             final int _maxRow = Math.max(minRow, maxRow);
3020 
3021             List<TreeTablePosition<S,?>> cellsToSelect = new ArrayList<>();
3022 
3023             for (int _row = _minRow; _row <= _maxRow; _row++) {
3024                 // begin copy/paste of select(int, column) method (with some
3025                 // slight modifications)
3026                 if (_row < 0 || _row >= itemCount) continue;
3027 
3028                 if (! isCellSelectionEnabled) {
3029                     cellsToSelect.add(new TreeTablePosition<>(treeTableView, _row, (TreeTableColumn<S,?>)minColumn));
3030                 } else {
3031                     for (int _col = _minColumnIndex; _col <= _maxColumnIndex; _col++) {
3032                         final TreeTableColumn<S, ?> column = treeTableView.getVisibleLeafColumn(_col);
3033 
3034                         // if I'm in cell selection mode but the column is null, I don't want
3035                         // to select the whole row instead...
3036                         if (column == null && isCellSelectionEnabled) continue;
3037 
3038                         cellsToSelect.add(new TreeTablePosition<>(treeTableView, _row, column));
3039                         // end copy/paste
3040                     }
3041                 }
3042             }
3043 
3044             // to prevent duplication we remove all currently selected cells from
3045             // our list of cells to select.
3046             cellsToSelect.removeAll(getSelectedCells());
3047 
3048             selectedCellsMap.addAll(cellsToSelect);
3049             stopAtomic();
3050 
3051             // fire off events
3052             // Note that focus and selection always goes to maxRow, not _maxRow.
3053             updateSelectedIndex(maxRow);
3054             focus(maxRow, (TreeTableColumn<S,?>)maxColumn);
3055 
3056             final TreeTableColumn<S,?> startColumn = (TreeTableColumn<S,?>)minColumn;
3057             final TreeTableColumn<S,?> endColumn = isCellSelectionEnabled ? (TreeTableColumn<S,?>)maxColumn : startColumn;
3058             final int startChangeIndex = selectedCellsMap.indexOf(new TreeTablePosition<>(treeTableView, minRow, startColumn));
3059             final int endChangeIndex = selectedCellsMap.indexOf(new TreeTablePosition<>(treeTableView, maxRow, endColumn));
3060 
3061             if (startChangeIndex > -1 && endChangeIndex > -1) {
3062                 final int startIndex = Math.min(startChangeIndex, endChangeIndex);
3063                 final int endIndex = Math.max(startChangeIndex, endChangeIndex);
3064 
3065                 ListChangeListener.Change c = new NonIterableChange.SimpleAddChange<>(startIndex, endIndex + 1, selectedCellsSeq);
3066                 fireCustomSelectedCellsListChangeEvent(c);
3067 //                selectedCellsSeq.fireChange(() -> selectedCellsSeq._nextAdd(startIndex, endIndex + 1));
3068             }
3069         }
3070 
3071         @Override public void clearSelection(int index) {
3072             clearSelection(index, null);
3073         }
3074 
3075         @Override
3076         public void clearSelection(int row, TableColumnBase<TreeItem<S>,?> column) {
3077             clearSelection(new TreeTablePosition<S,Object>(getTreeTableView(), row, (TreeTableColumn)column));
3078         }
3079 
3080         private void clearSelection(TreeTablePosition<S,?> tp) {
3081             final boolean csMode = isCellSelectionEnabled();
3082             final int row = tp.getRow();
3083             final boolean columnIsNull = tp.getTableColumn() == null;
3084 
3085             List<TreeTablePosition> toRemove = new ArrayList<>();
3086             for (TreeTablePosition pos : getSelectedCells()) {
3087                 if (!csMode) {
3088                     if (pos.getRow() == row) {
3089                         toRemove.add(pos);
3090                         break;
3091                     }
3092                 } else {
3093                     if (columnIsNull && pos.getRow() == row) {
3094                         // if we are in cell selection mode and the column is null,
3095                         // we remove all items in the row
3096                         toRemove.add(pos);
3097                     } else if (pos.equals(tp)) {
3098                         toRemove.add(tp);
3099                         break;
3100                     }
3101                 }
3102             }
3103             toRemove.stream().forEach(selectedCellsMap::remove);
3104 
3105             if (isEmpty() && ! isAtomic()) {
3106                 updateSelectedIndex(-1);
3107                 selectedCellsMap.clear();
3108             }
3109         }
3110 
3111         @Override public void clearSelection() {
3112             final List<TreeTablePosition<S,?>> removed = new ArrayList<>((Collection)getSelectedCells());
3113 
3114             quietClearSelection();
3115 
3116             if (! isAtomic()) {
3117                 updateSelectedIndex(-1);
3118                 focus(-1);
3119 
3120                 if (!removed.isEmpty()) {
3121 //                    selectedCellsSeq.fireChange(() -> selectedCellsSeq._nextRemove(0, removed));
3122                     ListChangeListener.Change<TreeTablePosition<S, ?>> c = new NonIterableChange<TreeTablePosition<S, ?>>(0, 0, selectedCellsSeq) {
3123                         @Override public List<TreeTablePosition<S, ?>> getRemoved() {
3124                             return removed;
3125                         }
3126                     };
3127                     fireCustomSelectedCellsListChangeEvent(c);
3128                 }
3129             }
3130         }
3131 
3132         private void quietClearSelection() {
3133             startAtomic();
3134             selectedCellsMap.clear();
3135             stopAtomic();
3136         }
3137 
3138         @Override public boolean isSelected(int index) {
3139             return isSelected(index, null);
3140         }
3141 
3142         @Override public boolean isSelected(int row, TableColumnBase<TreeItem<S>,?> column) {
3143             // When in cell selection mode, if the column is null, then we interpret
3144             // the users query to be asking if _all_ of the cells in the row are selected,
3145             // rather than if _any_ of the cells in the row are selected.
3146             final boolean isCellSelectionEnabled = isCellSelectionEnabled();
3147             if (isCellSelectionEnabled && column == null) {
3148                 int columnCount = treeTableView.getVisibleLeafColumns().size();
3149                 for (int col = 0; col < columnCount; col++) {
3150                     if (!selectedCellsMap.isSelected(row, col)) {
3151                         return false;
3152                     }
3153                 }
3154                 return true;
3155             } else {
3156                 int columnIndex = !isCellSelectionEnabled || column == null ? -1 : treeTableView.getVisibleLeafIndex((TreeTableColumn<S, ?>) column);
3157                 return selectedCellsMap.isSelected(row, columnIndex);
3158             }
3159         }
3160 
3161         @Override public boolean isEmpty() {
3162             return selectedCellsMap.isEmpty();
3163         }
3164 
3165         @Override public void selectPrevious() {
3166             if (isCellSelectionEnabled()) {
3167                 // in cell selection mode, we have to wrap around, going from
3168                 // right-to-left, and then wrapping to the end of the previous line
3169                 TreeTablePosition<S,?> pos = getFocusedCell();
3170                 if (pos.getColumn() - 1 >= 0) {
3171                     // go to previous row
3172                     select(pos.getRow(), getTableColumn(pos.getTableColumn(), -1));
3173                 } else if (pos.getRow() < getRowCount() - 1) {
3174                     // wrap to end of previous row
3175                     select(pos.getRow() - 1, getTableColumn(getTreeTableView().getVisibleLeafColumns().size() - 1));
3176                 }
3177             } else {
3178                 int focusIndex = getFocusedIndex();
3179                 if (focusIndex == -1) {
3180                     select(getRowCount() - 1);
3181                 } else if (focusIndex > 0) {
3182                     select(focusIndex - 1);
3183                 }
3184             }
3185         }
3186 
3187         @Override public void selectNext() {
3188             if (isCellSelectionEnabled()) {
3189                 // in cell selection mode, we have to wrap around, going from
3190                 // left-to-right, and then wrapping to the start of the next line
3191                 TreeTablePosition<S,?> pos = getFocusedCell();
3192                 if (pos.getColumn() + 1 < getTreeTableView().getVisibleLeafColumns().size()) {
3193                     // go to next column
3194                     select(pos.getRow(), getTableColumn(pos.getTableColumn(), 1));
3195                 } else if (pos.getRow() < getRowCount() - 1) {
3196                     // wrap to start of next row
3197                     select(pos.getRow() + 1, getTableColumn(0));
3198                 }
3199             } else {
3200                 int focusIndex = getFocusedIndex();
3201                 if (focusIndex == -1) {
3202                     select(0);
3203                 } else if (focusIndex < getRowCount() -1) {
3204                     select(focusIndex + 1);
3205                 }
3206             }
3207         }
3208 
3209         @Override public void selectAboveCell() {
3210             TreeTablePosition<S,?> pos = getFocusedCell();
3211             if (pos.getRow() == -1) {
3212                 select(getRowCount() - 1);
3213             } else if (pos.getRow() > 0) {
3214                 select(pos.getRow() - 1, pos.getTableColumn());
3215             }
3216         }
3217 
3218         @Override public void selectBelowCell() {
3219             TreeTablePosition<S,?> pos = getFocusedCell();
3220 
3221             if (pos.getRow() == -1) {
3222                 select(0);
3223             } else if (pos.getRow() < getRowCount() -1) {
3224                 select(pos.getRow() + 1, pos.getTableColumn());
3225             }
3226         }
3227 
3228         @Override public void selectFirst() {
3229             TreeTablePosition<S,?> focusedCell = getFocusedCell();
3230 
3231             if (getSelectionMode() == SelectionMode.SINGLE) {
3232                 quietClearSelection();
3233             }
3234 
3235             if (getRowCount() > 0) {
3236                 if (isCellSelectionEnabled()) {
3237                     select(0, focusedCell.getTableColumn());
3238                 } else {
3239                     select(0);
3240                 }
3241             }
3242         }
3243 
3244         @Override public void selectLast() {
3245             TreeTablePosition<S,?> focusedCell = getFocusedCell();
3246 
3247             if (getSelectionMode() == SelectionMode.SINGLE) {
3248                 quietClearSelection();
3249             }
3250 
3251             int numItems = getRowCount();
3252             if (numItems > 0 && getSelectedIndex() < numItems - 1) {
3253                 if (isCellSelectionEnabled()) {
3254                     select(numItems - 1, focusedCell.getTableColumn());
3255                 } else {
3256                     select(numItems - 1);
3257                 }
3258             }
3259         }
3260 
3261         @Override public void selectLeftCell() {
3262             if (! isCellSelectionEnabled()) return;
3263 
3264             TreeTablePosition<S,?> pos = getFocusedCell();
3265             if (pos.getColumn() - 1 >= 0) {
3266                 select(pos.getRow(), getTableColumn(pos.getTableColumn(), -1));
3267             }
3268         }
3269 
3270         @Override public void selectRightCell() {
3271             if (! isCellSelectionEnabled()) return;
3272 
3273             TreeTablePosition<S,?> pos = getFocusedCell();
3274             if (pos.getColumn() + 1 < getTreeTableView().getVisibleLeafColumns().size()) {
3275                 select(pos.getRow(), getTableColumn(pos.getTableColumn(), 1));
3276             }
3277         }
3278 
3279 
3280 
3281         /***********************************************************************
3282          *                                                                     *
3283          * Support code                                                        *
3284          *                                                                     *
3285          **********************************************************************/
3286 
3287         private void updateDefaultSelection() {
3288             // when the items list totally changes, we should clear out
3289             // the selection
3290             int newSelectionIndex = -1;
3291             TreeItem<S> selectedItem = getSelectedItem();
3292             if (selectedItem != null) {
3293                 newSelectionIndex = treeTableView.getRow(selectedItem);
3294             }
3295 
3296             // we put focus onto the first item, if there is at least
3297             // one item in the list
3298             int newFocusIndex = newSelectionIndex != -1 ? newSelectionIndex : treeTableView.getExpandedItemCount() > 0 ? 0 : -1;
3299 
3300             clearSelection();
3301             select(newSelectionIndex, isCellSelectionEnabled() ? getTableColumn(0) : null);
3302             focus(newFocusIndex, isCellSelectionEnabled() ? getTableColumn(0) : null);
3303         }
3304 
3305         private TreeTableColumn<S,?> getTableColumn(int pos) {
3306             return getTreeTableView().getVisibleLeafColumn(pos);
3307         }
3308 
3309         // Gets a table column to the left or right of the current one, given an offset
3310         private TreeTableColumn<S,?> getTableColumn(TreeTableColumn<S,?> column, int offset) {
3311             int columnIndex = getTreeTableView().getVisibleLeafIndex(column);
3312             int newColumnIndex = columnIndex + offset;
3313             return getTreeTableView().getVisibleLeafColumn(newColumnIndex);
3314         }
3315 
3316         private void updateSelectedIndex(int row) {
3317             setSelectedIndex(row);
3318             setSelectedItem(getModelItem(row));
3319         }
3320 
3321         @Override public void focus(int row) {
3322             focus(row, null);
3323         }
3324 
3325         private void focus(int row, TreeTableColumn<S,?> column) {
3326             focus(new TreeTablePosition<>(getTreeTableView(), row, column));
3327         }
3328 
3329         private void focus(TreeTablePosition<S,?> pos) {
3330             if (getTreeTableView().getFocusModel() == null) return;
3331 
3332             getTreeTableView().getFocusModel().focus(pos.getRow(), pos.getTableColumn());
3333             getTreeTableView().notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM);
3334         }
3335 
3336         @Override public int getFocusedIndex() {
3337             return getFocusedCell().getRow();
3338         }
3339 
3340         private TreeTablePosition<S,?> getFocusedCell() {
3341             if (treeTableView.getFocusModel() == null) {
3342                 return new TreeTablePosition<>(treeTableView, -1, null);
3343             }
3344             return treeTableView.getFocusModel().getFocusedCell();
3345         }
3346 
3347         private int getRowCount() {
3348             return treeTableView.getExpandedItemCount();
3349         }
3350 
3351         private void fireCustomSelectedCellsListChangeEvent(ListChangeListener.Change<? extends TreeTablePosition<S,?>> c) {
3352             ControlUtils.updateSelectedIndices(this, c);
3353 
3354             if (isAtomic()) {
3355                 return;
3356             }
3357 
3358             selectedCellsSeq.callObservers(new MappingChange<>(c, MappingChange.NOOP_MAP, selectedCellsSeq));
3359         }
3360     }
3361 
3362 
3363 
3364 
3365     /**
3366      * A {@link FocusModel} with additional functionality to support the requirements
3367      * of a TableView control.
3368      *
3369      * @see TableView
3370      * @since JavaFX 8.0
3371      */
3372     public static class TreeTableViewFocusModel<S> extends TableFocusModel<TreeItem<S>, TreeTableColumn<S,?>> {
3373 
3374         private final TreeTableView<S> treeTableView;
3375 
3376         private final TreeTablePosition EMPTY_CELL;
3377 
3378         /**
3379          * Creates a default TableViewFocusModel instance that will be used to
3380          * manage focus of the provided TableView control.
3381          *
3382          * @param treeTableView The tableView upon which this focus model operates.
3383          * @throws NullPointerException The TableView argument can not be null.
3384          */
3385         public TreeTableViewFocusModel(final TreeTableView<S> treeTableView) {
3386             if (treeTableView == null) {
3387                 throw new NullPointerException("TableView can not be null");
3388             }
3389 
3390             this.treeTableView = treeTableView;
3391             this.EMPTY_CELL = new TreeTablePosition<>(treeTableView, -1, null);
3392 
3393             this.treeTableView.rootProperty().addListener(weakRootPropertyListener);
3394             updateTreeEventListener(null, treeTableView.getRoot());
3395 
3396             int focusRow = getItemCount() > 0 ? 0 : -1;
3397             TreeTablePosition<S,?> pos = new TreeTablePosition<>(treeTableView, focusRow, null);
3398             setFocusedCell(pos);
3399 
3400             treeTableView.showRootProperty().addListener(o -> {
3401                 if (isFocused(0)) {
3402                     focus(-1);
3403                     focus(0);
3404                 }
3405             });
3406 
3407             focusedCellProperty().addListener(o -> {
3408                 treeTableView.notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM);
3409             });
3410         }
3411 
3412         private final ChangeListener<TreeItem<S>> rootPropertyListener = (observable, oldValue, newValue) -> {
3413             updateTreeEventListener(oldValue, newValue);
3414         };
3415 
3416         private final WeakChangeListener<TreeItem<S>> weakRootPropertyListener =
3417                 new WeakChangeListener<>(rootPropertyListener);
3418 
3419         private void updateTreeEventListener(TreeItem<S> oldRoot, TreeItem<S> newRoot) {
3420             if (oldRoot != null && weakTreeItemListener != null) {
3421                 oldRoot.removeEventHandler(TreeItem.<S>expandedItemCountChangeEvent(), weakTreeItemListener);
3422             }
3423 
3424             if (newRoot != null) {
3425                 weakTreeItemListener = new WeakEventHandler<>(treeItemListener);
3426                 newRoot.addEventHandler(TreeItem.<S>expandedItemCountChangeEvent(), weakTreeItemListener);
3427             }
3428         }
3429 
3430         private EventHandler<TreeItem.TreeModificationEvent<S>> treeItemListener = new EventHandler<TreeItem.TreeModificationEvent<S>>() {
3431             @Override public void handle(TreeItem.TreeModificationEvent<S> e) {
3432                 // don't shift focus if the event occurred on a tree item after
3433                 // the focused row, or if there is no focus index at present
3434                 if (getFocusedIndex() == -1) return;
3435 
3436                 int shift = 0;
3437                 if (e.getChange() != null) {
3438                     e.getChange().next();
3439                 }
3440 
3441                 do {
3442                     int row = treeTableView.getRow(e.getTreeItem());
3443 
3444                     if (e.wasExpanded()) {
3445                         if (row < getFocusedIndex()) {
3446                             // need to shuffle selection by the number of visible children
3447                             shift += e.getTreeItem().getExpandedDescendentCount(false) - 1;
3448                         }
3449                     } else if (e.wasCollapsed()) {
3450                         if (row < getFocusedIndex()) {
3451                             // need to shuffle selection by the number of visible children
3452                             // that were just hidden
3453                             shift += -e.getTreeItem().previousExpandedDescendentCount + 1;
3454                         }
3455                     } else if (e.wasAdded()) {
3456                         // get the TreeItem the event occurred on - we only need to
3457                         // shift if the tree item is expanded
3458                         TreeItem<S> eventTreeItem = e.getTreeItem();
3459                         if (eventTreeItem.isExpanded()) {
3460                             for (int i = 0; i < e.getAddedChildren().size(); i++) {
3461                                 // get the added item and determine the row it is in
3462                                 TreeItem<S> item = e.getAddedChildren().get(i);
3463                                 row = treeTableView.getRow(item);
3464 
3465                                 if (item != null && row <= (shift+getFocusedIndex())) {
3466                                     shift += item.getExpandedDescendentCount(false);
3467                                 }
3468                             }
3469                         }
3470                     } else if (e.wasRemoved()) {
3471                         row += e.getFrom() + 1;
3472 
3473                         for (int i = 0; i < e.getRemovedChildren().size(); i++) {
3474                             TreeItem<S> item = e.getRemovedChildren().get(i);
3475                             if (item != null && item.equals(getFocusedItem())) {
3476                                 focus(Math.max(0, getFocusedIndex() - 1));
3477                                 return;
3478                             }
3479                         }
3480 
3481                         if (row <= getFocusedIndex()) {
3482                             // shuffle selection by the number of removed items
3483                             shift += e.getTreeItem().isExpanded() ? -e.getRemovedSize() : 0;
3484                         }
3485                     }
3486                 } while (e.getChange() != null && e.getChange().next());
3487 
3488                 if (shift != 0) {
3489                     TreeTablePosition<S, ?> focusedCell = getFocusedCell();
3490                     final int newFocus = focusedCell.getRow() + shift;
3491                     if (newFocus >= 0) {
3492                         Platform.runLater(() -> focus(newFocus, focusedCell.getTableColumn()));
3493                     }
3494                 }
3495             }
3496         };
3497 
3498         private WeakEventHandler<TreeItem.TreeModificationEvent<S>> weakTreeItemListener;
3499 
3500         /** {@inheritDoc} */
3501         @Override protected int getItemCount() {
3502 //            if (tableView.getItems() == null) return -1;
3503 //            return tableView.getItems().size();
3504             return treeTableView.getExpandedItemCount();
3505         }
3506 
3507         /** {@inheritDoc} */
3508         @Override protected TreeItem<S> getModelItem(int index) {
3509             if (index < 0 || index >= getItemCount()) return null;
3510             return treeTableView.getTreeItem(index);
3511         }
3512 
3513         /**
3514          * The position of the current item in the TableView which has the focus.
3515          */
3516         private ReadOnlyObjectWrapper<TreeTablePosition<S,?>> focusedCell;
3517         public final ReadOnlyObjectProperty<TreeTablePosition<S,?>> focusedCellProperty() {
3518             return focusedCellPropertyImpl().getReadOnlyProperty();
3519         }
3520         private void setFocusedCell(TreeTablePosition<S,?> value) { focusedCellPropertyImpl().set(value);  }
3521         public final TreeTablePosition<S,?> getFocusedCell() { return focusedCell == null ? EMPTY_CELL : focusedCell.get(); }
3522 
3523         private ReadOnlyObjectWrapper<TreeTablePosition<S,?>> focusedCellPropertyImpl() {
3524             if (focusedCell == null) {
3525                 focusedCell = new ReadOnlyObjectWrapper<TreeTablePosition<S,?>>(EMPTY_CELL) {
3526                     private TreeTablePosition<S,?> old;
3527                     @Override protected void invalidated() {
3528                         if (get() == null) return;
3529 
3530                         if (old == null || !old.equals(get())) {
3531                             setFocusedIndex(get().getRow());
3532                             setFocusedItem(getModelItem(getValue().getRow()));
3533 
3534                             old = get();
3535                         }
3536                     }
3537 
3538                     @Override
3539                     public Object getBean() {
3540                         return TreeTableView.TreeTableViewFocusModel.this;
3541                     }
3542 
3543                     @Override
3544                     public String getName() {
3545                         return "focusedCell";
3546                     }
3547                 };
3548             }
3549             return focusedCell;
3550         }
3551 
3552 
3553         /**
3554          * Causes the item at the given index to receive the focus.
3555          *
3556          * @param row The row index of the item to give focus to.
3557          * @param column The column of the item to give focus to. Can be null.
3558          */
3559         @Override public void focus(int row, TreeTableColumn<S,?> column) {
3560             if (row < 0 || row >= getItemCount()) {
3561                 setFocusedCell(EMPTY_CELL);
3562             } else {
3563                 TreeTablePosition<S,?> oldFocusCell = getFocusedCell();
3564                 TreeTablePosition<S,?> newFocusCell = new TreeTablePosition<>(treeTableView, row, column);
3565                 setFocusedCell(newFocusCell);
3566 
3567                 if (newFocusCell.equals(oldFocusCell)) {
3568                     // manually update the focus properties to ensure consistency
3569                     setFocusedIndex(row);
3570                     setFocusedItem(getModelItem(row));
3571                 }
3572             }
3573         }
3574 
3575         /**
3576          * Convenience method for setting focus on a particular row or cell
3577          * using a {@link TablePosition}.
3578          *
3579          * @param pos The table position where focus should be set.
3580          */
3581         public void focus(TreeTablePosition<S,?> pos) {
3582             if (pos == null) return;
3583             focus(pos.getRow(), pos.getTableColumn());
3584         }
3585 
3586 
3587         /***********************************************************************
3588          *                                                                     *
3589          * Public API                                                          *
3590          *                                                                     *
3591          **********************************************************************/
3592 
3593         /**
3594          * Tests whether the row / cell at the given location currently has the
3595          * focus within the TableView.
3596          */
3597         @Override public boolean isFocused(int row, TreeTableColumn<S,?> column) {
3598             if (row < 0 || row >= getItemCount()) return false;
3599 
3600             TreeTablePosition<S,?> cell = getFocusedCell();
3601             boolean columnMatch = column == null || column.equals(cell.getTableColumn());
3602 
3603             return cell.getRow() == row && columnMatch;
3604         }
3605 
3606         /**
3607          * Causes the item at the given index to receive the focus. This does not
3608          * cause the current selection to change. Updates the focusedItem and
3609          * focusedIndex properties such that <code>focusedIndex = -1</code> unless
3610          * <pre><code>0 &lt;= index &lt; model size</code></pre>.
3611          *
3612          * @param index The index of the item to get focus.
3613          */
3614         @Override public void focus(int index) {
3615             if (treeTableView.expandedItemCountDirty) {
3616                 treeTableView.updateExpandedItemCount(treeTableView.getRoot());
3617             }
3618 
3619             if (index < 0 || index >= getItemCount()) {
3620                 setFocusedCell(EMPTY_CELL);
3621             } else {
3622                 setFocusedCell(new TreeTablePosition<>(treeTableView, index, null));
3623             }
3624         }
3625 
3626         /**
3627          * Attempts to move focus to the cell above the currently focused cell.
3628          */
3629         @Override public void focusAboveCell() {
3630             TreeTablePosition<S,?> cell = getFocusedCell();
3631 
3632             if (getFocusedIndex() == -1) {
3633                 focus(getItemCount() - 1, cell.getTableColumn());
3634             } else if (getFocusedIndex() > 0) {
3635                 focus(getFocusedIndex() - 1, cell.getTableColumn());
3636             }
3637         }
3638 
3639         /**
3640          * Attempts to move focus to the cell below the currently focused cell.
3641          */
3642         @Override public void focusBelowCell() {
3643             TreeTablePosition<S,?> cell = getFocusedCell();
3644             if (getFocusedIndex() == -1) {
3645                 focus(0, cell.getTableColumn());
3646             } else if (getFocusedIndex() != getItemCount() -1) {
3647                 focus(getFocusedIndex() + 1, cell.getTableColumn());
3648             }
3649         }
3650 
3651         /**
3652          * Attempts to move focus to the cell to the left of the currently focused cell.
3653          */
3654         @Override public void focusLeftCell() {
3655             TreeTablePosition<S,?> cell = getFocusedCell();
3656             if (cell.getColumn() <= 0) return;
3657             focus(cell.getRow(), getTableColumn(cell.getTableColumn(), -1));
3658         }
3659 
3660         /**
3661          * Attempts to move focus to the cell to the right of the the currently focused cell.
3662          */
3663         @Override public void focusRightCell() {
3664             TreeTablePosition<S,?> cell = getFocusedCell();
3665             if (cell.getColumn() == getColumnCount() - 1) return;
3666             focus(cell.getRow(), getTableColumn(cell.getTableColumn(), 1));
3667         }
3668 
3669         /** {@inheritDoc} */
3670         @Override public void focusPrevious() {
3671             if (getFocusedIndex() == -1) {
3672                 focus(0);
3673             } else if (getFocusedIndex() > 0) {
3674                 focusAboveCell();
3675             }
3676         }
3677 
3678         /** {@inheritDoc} */
3679         @Override public void focusNext() {
3680             if (getFocusedIndex() == -1) {
3681                 focus(0);
3682             } else if (getFocusedIndex() != getItemCount() -1) {
3683                 focusBelowCell();
3684             }
3685         }
3686 
3687 
3688 
3689          /***********************************************************************
3690          *                                                                     *
3691          * Private Implementation                                              *
3692          *                                                                     *
3693          **********************************************************************/
3694 
3695         private int getColumnCount() {
3696             return treeTableView.getVisibleLeafColumns().size();
3697         }
3698 
3699         // Gets a table column to the left or right of the current one, given an offset
3700         private TreeTableColumn<S,?> getTableColumn(TreeTableColumn<S,?> column, int offset) {
3701             int columnIndex = treeTableView.getVisibleLeafIndex(column);
3702             int newColumnIndex = columnIndex + offset;
3703             return treeTableView.getVisibleLeafColumn(newColumnIndex);
3704         }
3705     }
3706 }