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