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