/*
* Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javafx.scene.control;
import com.sun.javafx.collections.MappingChange;
import com.sun.javafx.collections.NonIterableChange;
import com.sun.javafx.collections.annotations.ReturnsUnmodifiableCollection;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.css.CssMetaData;
import javafx.css.PseudoClass;
import com.sun.javafx.css.converters.SizeConverter;
import com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList;
import com.sun.javafx.scene.control.TableColumnComparatorBase;
import com.sun.javafx.scene.control.skin.TableViewSkinBase;
import javafx.css.Styleable;
import javafx.css.StyleableDoubleProperty;
import javafx.css.StyleableProperty;
import javafx.event.WeakEventHandler;
import com.sun.javafx.scene.control.skin.TreeTableViewSkin;
import com.sun.javafx.scene.control.skin.VirtualContainerBase;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javafx.application.Platform;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.collections.MapChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.control.MultipleSelectionModelBase.ShiftParams;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Region;
import javafx.util.Callback;
/**
* The TreeTableView control is designed to visualize an unlimited number of rows
* of data, broken out into columns. A TreeTableView is therefore very similar to the
* {@link ListView} and {@link TableView} controls. For an
* example on how to create a TreeTableView, refer to the 'Creating a TreeTableView'
* control section below.
*
* The TreeTableView control has a number of features, including:
*
* Powerful {@link TreeTableColumn} API:
*
* Support for {@link TreeTableColumn#cellFactoryProperty() cell factories} to
* easily customize {@link Cell cell} contents in both rendering and editing
* states.
* Specification of {@link #minWidthProperty() minWidth}/
* {@link #prefWidthProperty() prefWidth}/{@link #maxWidthProperty() maxWidth},
* and also {@link TreeTableColumn#resizableProperty() fixed width columns}.
* Width resizing by the user at runtime.
* Column reordering by the user at runtime.
* Built-in support for {@link TreeTableColumn#getColumns() column nesting}
*
* Different {@link #columnResizePolicyProperty() resizing policies} to
* dictate what happens when the user resizes columns.
* Support for {@link #getSortOrder() multiple column sorting} by clicking
* the column header (hold down Shift keyboard key whilst clicking on a
* header to sort by multiple columns).
*
*
*
* Note that TreeTableView is intended to be used to visualize data - it is not
* intended to be used for laying out your user interface. If you want to lay
* your user interface out in a grid-like fashion, consider the
* {@link GridPane} layout.
*
* Creating a TreeTableView
*
* TODO update to a relevant example
*
* Creating a TreeTableView is a multi-step process, and also depends on the
* underlying data model needing to be represented. For this example we'll use
* the TreeTableView to visualise a file system, and will therefore make use
* of an imaginary (and vastly simplified) File class as defined below:
*
*
* {@code
* public class File {
* private StringProperty name;
* public void setName(String value) { nameProperty().set(value); }
* public String getName() { return nameProperty().get(); }
* public StringProperty nameProperty() {
* if (name == null) name = new SimpleStringProperty(this, "name");
* return name;
* }
*
* private DoubleProperty lastModified;
* public void setLastModified(Double value) { lastModifiedProperty().set(value); }
* public DoubleProperty getLastModified() { return lastModifiedProperty().get(); }
* public DoubleProperty lastModifiedProperty() {
* if (lastModified == null) lastModified = new SimpleDoubleProperty(this, "lastModified");
* return lastModified;
* }
* }}
*
* Firstly, a TreeTableView instance needs to be defined, as such:
*
*
* {@code
* TreeTableView treeTable = new TreeTableView();}
*
* With the basic tree table defined, we next focus on the data model. As mentioned,
* for this example, we'll be representing a file system using File instances. To
* do this, we need to define the root node of the tree table, as such:
*
*
* {@code
* TreeItem root = new TreeItem(new File("/"));
* treeTable.setRoot(root);}
*
* With the root set as such, the TreeTableView will automatically update whenever
* the {@link TreeItem#getChildren() children} of the root changes.
*
*
At this point we now have a TreeTableView hooked up to observe the root
* TreeItem instance. The missing ingredient
* now is the means of splitting out the data contained within the model and
* representing it in one or more {@link TreeTableColumn} instances. To
* create a two-column TreeTableView to show the file name and last modified
* properties, we extend the code shown above as follows:
*
*
* {@code
* TreeItem root = new TreeItem(new File("/"));
* treeTable.setRoot(root);
*
* // TODO this is not valid TreeTableView code
* TreeTableColumns firstNameCol = new TreeTableColumns("First Name");
* firstNameCol.setCellValueFactory(new PropertyValueFactory("firstName"));
* TreeTableColumns lastNameCol = new TreeTableColumns("Last Name");
* lastNameCol.setCellValueFactory(new PropertyValueFactory("lastName"));
*
* table.getColumns().setAll(firstNameCol, lastNameCol);}
*
* With the code shown above we have fully defined the minimum properties
* required to create a TreeTableView instance. Running this code (assuming the
* file system structure is probably built up in memory) will result in a TreeTableView being
* shown with two columns for name and lastModified. Any other properties of the
* File class will not be shown, as no TreeTableColumnss are defined for them.
*
*
TreeTableView support for classes that don't contain properties
*
* // TODO update - this is not correct for TreeTableView
*
* The code shown above is the shortest possible code for creating a TreeTableView
* when the domain objects are designed with JavaFX properties in mind
* (additionally, {@link javafx.scene.control.cell.PropertyValueFactory} supports
* normal JavaBean properties too, although there is a caveat to this, so refer
* to the class documentation for more information). When this is not the case,
* it is necessary to provide a custom cell value factory. More information
* about cell value factories can be found in the {@link TreeTableColumns} API
* documentation, but briefly, here is how a TreeTableColumns could be specified:
*
*
* {@code
* firstNameCol.setCellValueFactory(new Callback, ObservableValue>() {
* public ObservableValue call(CellDataFeatures p) {
* // p.getValue() returns the Person instance for a particular TreeTableView row
* return p.getValue().firstNameProperty();
* }
* });
* }}
*
* TreeTableView Selection / Focus APIs
* To track selection and focus, it is necessary to become familiar with the
* {@link SelectionModel} and {@link FocusModel} classes. A TreeTableView has at most
* one instance of each of these classes, available from
* {@link #selectionModelProperty() selectionModel} and
* {@link #focusModelProperty() focusModel} properties respectively.
* Whilst it is possible to use this API to set a new selection model, in
* most circumstances this is not necessary - the default selection and focus
* models should work in most circumstances.
*
*
The default {@link SelectionModel} used when instantiating a TreeTableView is
* an implementation of the {@link MultipleSelectionModel} abstract class.
* However, as noted in the API documentation for
* the {@link MultipleSelectionModel#selectionModeProperty() selectionMode}
* property, the default value is {@link SelectionMode#SINGLE}. To enable
* multiple selection in a default TreeTableView instance, it is therefore necessary
* to do the following:
*
*
* {@code
* treeTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);}
*
* Customizing TreeTableView Visuals
* The visuals of the TreeTableView can be entirely customized by replacing the
* default {@link #rowFactoryProperty() row factory}. A row factory is used to
* generate {@link TreeTableRow} instances, which are used to represent an entire
* row in the TreeTableView.
*
*
In many cases, this is not what is desired however, as it is more commonly
* the case that cells be customized on a per-column basis, not a per-row basis.
* It is therefore important to note that a {@link TreeTableRow} is not a
* {@link TreeTableCell}. A {@link TreeTableRow} is simply a container for zero or more
* {@link TreeTableCell}, and in most circumstances it is more likely that you'll
* want to create custom TreeTableCells, rather than TreeTableRows. The primary use case
* for creating custom TreeTableRow instances would most probably be to introduce
* some form of column spanning support.
*
*
You can create custom {@link TreeTableCell} instances per column by assigning
* the appropriate function to the TreeTableColumns
* {@link TreeTableColumns#cellFactoryProperty() cell factory} property.
*
*
See the {@link Cell} class documentation for a more complete
* description of how to write custom Cells.
*
* @see TreeTableColumn
* @see TreeTablePosition
* @param The type of the TreeItem instances used in this TreeTableView.
* @since JavaFX 8.0
*/
@DefaultProperty("root")
public class TreeTableView extends Control {
/***************************************************************************
* *
* Constructors *
* *
**************************************************************************/
/**
* Creates an empty TreeTableView.
*
*
Refer to the {@link TreeTableView} class documentation for details on the
* default state of other properties.
*/
public TreeTableView() {
this(null);
}
/**
* Creates a TreeTableView with the provided root node.
*
*
Refer to the {@link TreeTableView} class documentation for details on the
* default state of other properties.
*
* @param root The node to be the root in this TreeTableView.
*/
public TreeTableView(TreeItem root) {
getStyleClass().setAll(DEFAULT_STYLE_CLASS);
setRoot(root);
updateExpandedItemCount(root);
// install default selection and focus models - it's unlikely this will be changed
// by many users.
setSelectionModel(new TreeTableViewArrayListSelectionModel(this));
setFocusModel(new TreeTableViewFocusModel(this));
// we watch the columns list, such that when it changes we can update
// the leaf columns and visible leaf columns lists (which are read-only).
getColumns().addListener(weakColumnsObserver);
// watch for changes to the sort order list - and when it changes run
// the sort method.
getSortOrder().addListener(new ListChangeListener>() {
@Override public void onChanged(ListChangeListener.Change> c) {
doSort(TableUtil.SortEventType.SORT_ORDER_CHANGE, c);
}
});
// We're watching for changes to the content width such
// that the resize policy can be run if necessary. This comes from
// TreeTableViewSkin.
getProperties().addListener(new MapChangeListener() {
@Override
public void onChanged(Change c) {
if (c.wasAdded() && TableView.SET_CONTENT_WIDTH.equals(c.getKey())) {
if (c.getValueAdded() instanceof Number) {
setContentWidth((Double) c.getValueAdded());
}
getProperties().remove(TableView.SET_CONTENT_WIDTH);
}
}
});
isInited = true;
}
/***************************************************************************
* *
* Static properties and methods *
* *
**************************************************************************/
/**
* An EventType that indicates some edit event has occurred. It is the parent
* type of all other edit events: {@link #editStartEvent},
* {@link #editCommitEvent} and {@link #editCancelEvent}.
*
* @return An EventType that indicates some edit event has occurred.
*/
@SuppressWarnings("unchecked")
public static EventType> editAnyEvent() {
return (EventType>) EDIT_ANY_EVENT;
}
private static final EventType EDIT_ANY_EVENT =
new EventType(Event.ANY, "TREE_TABLE_VIEW_EDIT");
/**
* An EventType used to indicate that an edit event has started within the
* TreeTableView upon which the event was fired.
*
* @return An EventType used to indicate that an edit event has started.
*/
@SuppressWarnings("unchecked")
public static EventType> editStartEvent() {
return (EventType>) EDIT_START_EVENT;
}
private static final EventType EDIT_START_EVENT =
new EventType(editAnyEvent(), "EDIT_START");
/**
* An EventType used to indicate that an edit event has just been canceled
* within the TreeTableView upon which the event was fired.
*
* @return An EventType used to indicate that an edit event has just been
* canceled.
*/
@SuppressWarnings("unchecked")
public static EventType> editCancelEvent() {
return (EventType>) EDIT_CANCEL_EVENT;
}
private static final EventType EDIT_CANCEL_EVENT =
new EventType(editAnyEvent(), "EDIT_CANCEL");
/**
* An EventType that is used to indicate that an edit in a TreeTableView has been
* committed. This means that user has made changes to the data of a
* TreeItem, and that the UI should be updated.
*
* @return An EventType that is used to indicate that an edit in a TreeTableView
* has been committed.
*/
@SuppressWarnings("unchecked")
public static EventType> editCommitEvent() {
return (EventType>) EDIT_COMMIT_EVENT;
}
private static final EventType EDIT_COMMIT_EVENT =
new EventType(editAnyEvent(), "EDIT_COMMIT");
/**
* Returns the number of levels of 'indentation' of the given TreeItem,
* based on how many times getParent() can be recursively called. If the
* given TreeItem is the root node, or if the TreeItem does not have any
* parent set, the returned value will be zero. For each time getParent() is
* recursively called, the returned value is incremented by one.
*
* @param node The TreeItem for which the level is needed.
* @return An integer representing the number of parents above the given node,
* or -1 if the given TreeItem is null.
*/
public static int getNodeLevel(TreeItem node) {
return TreeView.getNodeLevel(node);
}
/**
* Very simple resize policy that just resizes the specified column by the
* provided delta and shifts all other columns (to the right of the given column)
* further to the right (when the delta is positive) or to the left (when the
* delta is negative).
*
*
It also handles the case where we have nested columns by sharing the new space,
* or subtracting the removed space, evenly between all immediate children columns.
* Of course, the immediate children may themselves be nested, and they would
* then use this policy on their children.
*/
public static final Callback UNCONSTRAINED_RESIZE_POLICY =
new Callback() {
@Override public String toString() {
return "unconstrained-resize";
}
@Override public Boolean call(TreeTableView.ResizeFeatures prop) {
double result = TableUtil.resize(prop.getColumn(), prop.getDelta());
return Double.compare(result, 0.0) == 0;
}
};
/**
* Simple policy that ensures the width of all visible leaf columns in
* this table sum up to equal the width of the table itself.
*
*
When the user resizes a column width with this policy, the table automatically
* adjusts the width of the right hand side columns. When the user increases a
* column width, the table decreases the width of the rightmost column until it
* reaches its minimum width. Then it decreases the width of the second
* rightmost column until it reaches minimum width and so on. When all right
* hand side columns reach minimum size, the user cannot increase the size of
* resized column any more.
*/
public static final Callback CONSTRAINED_RESIZE_POLICY =
new Callback() {
private boolean isFirstRun = true;
@Override public String toString() {
return "constrained-resize";
}
@Override public Boolean call(TreeTableView.ResizeFeatures prop) {
TreeTableView table = prop.getTable();
List> visibleLeafColumns = table.getVisibleLeafColumns();
Boolean result = TableUtil.constrainedResize(prop,
isFirstRun,
table.contentWidth,
visibleLeafColumns);
isFirstRun = false;
return result;
}
};
/**
* The default {@link #sortPolicyProperty() sort policy} that this TreeTableView
* will use if no other policy is specified. The sort policy is a simple
* {@link Callback} that accepts a TreeTableView as the sole argument and expects
* a Boolean response representing whether the sort succeeded or not. A Boolean
* response of true represents success, and a response of false (or null) will
* be considered to represent failure.
*/
public static final Callback DEFAULT_SORT_POLICY = new Callback() {
@Override public Boolean call(TreeTableView table) {
try {
TreeItem rootItem = table.getRoot();
if (rootItem == null) return false;
TreeSortMode sortMode = table.getSortMode();
if (sortMode == null) return false;
rootItem.lastSortMode = sortMode;
rootItem.lastComparator = table.getComparator();
rootItem.sort();
return true;
} catch (UnsupportedOperationException e) {
// TODO might need to support other exception types including:
// ClassCastException - if the class of the specified element prevents it from being added to this list
// NullPointerException - if the specified element is null and this list does not permit null elements
// IllegalArgumentException - if some property of this element prevents it from being added to this list
// If we are here the list does not support sorting, so we gracefully
// fail the sort request and ensure the UI is put back to its previous
// state. This is handled in the code that calls the sort policy.
return false;
}
}
};
/***************************************************************************
* *
* Instance Variables *
* *
**************************************************************************/
// used in the tree item modification event listener. Used by the
// layoutChildren method to determine whether the tree item count should
// be recalculated.
private boolean expandedItemCountDirty = true;
// this is the only publicly writable list for columns. This represents the
// columns as they are given initially by the developer.
private final ObservableList> columns = FXCollections.observableArrayList();
// Finally, as convenience, we also have an observable list that contains
// only the leaf columns that are currently visible.
private final ObservableList> visibleLeafColumns = FXCollections.observableArrayList();
private final ObservableList> unmodifiableVisibleLeafColumns = FXCollections.unmodifiableObservableList(visibleLeafColumns);
// Allows for multiple column sorting based on the order of the TreeTableColumns
// in this observableArrayList. Each TreeTableColumn is responsible for whether it is
// sorted using ascending or descending order.
private ObservableList> sortOrder = FXCollections.observableArrayList();
// width of VirtualFlow minus the vbar width
private double contentWidth;
// Used to minimise the amount of work performed prior to the table being
// completely initialised. In particular it reduces the amount of column
// resize operations that occur, which slightly improves startup time.
private boolean isInited = false;
/***************************************************************************
* *
* Callbacks and Events *
* *
**************************************************************************/
// we use this to forward events that have bubbled up TreeItem instances
// to the TreeTableViewSkin, to force it to recalculate teh item count and redraw
// if necessary
private final EventHandler> rootEvent = new EventHandler>() {
@Override public void handle(TreeItem.TreeModificationEvent e) {
// this forces layoutChildren at the next pulse, and therefore
// updates the item count if necessary
EventType eventType = e.getEventType();
boolean match = false;
while (eventType != null) {
if (eventType.equals(TreeItem.expandedItemCountChangeEvent())) {
match = true;
break;
}
eventType = eventType.getSuperType();
}
if (match) {
expandedItemCountDirty = true;
requestLayout();
}
}
};
private final ListChangeListener> columnsObserver = new ListChangeListener>() {
@Override public void onChanged(ListChangeListener.Change> c) {
// We don't maintain a bind for leafColumns, we simply call this update
// function behind the scenes in the appropriate places.
updateVisibleLeafColumns();
// Fix for RT-15194: Need to remove removed columns from the
// sortOrder list.
List> toRemove = new ArrayList>();
while (c.next()) {
final List> removed = c.getRemoved();
final List> added = c.getAddedSubList();
if (c.wasRemoved()) {
toRemove.addAll(removed);
for (TreeTableColumn tc : removed) {
tc.setTreeTableView(null);
}
}
if (c.wasAdded()) {
toRemove.removeAll(added);
for (TreeTableColumn tc : added) {
tc.setTreeTableView(TreeTableView.this);
}
}
// set up listeners
TableUtil.removeColumnsListener(removed, weakColumnsObserver);
TableUtil.addColumnsListener(added, weakColumnsObserver);
TableUtil.removeTableColumnListener(c.getRemoved(),
weakColumnVisibleObserver,
weakColumnSortableObserver,
weakColumnSortTypeObserver,
weakColumnComparatorObserver);
TableUtil.addTableColumnListener(c.getAddedSubList(),
weakColumnVisibleObserver,
weakColumnSortableObserver,
weakColumnSortTypeObserver,
weakColumnComparatorObserver);
}
sortOrder.removeAll(toRemove);
}
};
private final InvalidationListener columnVisibleObserver = new InvalidationListener() {
@Override public void invalidated(Observable valueModel) {
updateVisibleLeafColumns();
}
};
private final InvalidationListener columnSortableObserver = new InvalidationListener() {
@Override public void invalidated(Observable valueModel) {
TreeTableColumn col = (TreeTableColumn) ((BooleanProperty)valueModel).getBean();
if (! getSortOrder().contains(col)) return;
doSort(TableUtil.SortEventType.COLUMN_SORTABLE_CHANGE, col);
}
};
private final InvalidationListener columnSortTypeObserver = new InvalidationListener() {
@Override public void invalidated(Observable valueModel) {
TreeTableColumn col = (TreeTableColumn) ((ObjectProperty)valueModel).getBean();
if (! getSortOrder().contains(col)) return;
doSort(TableUtil.SortEventType.COLUMN_SORT_TYPE_CHANGE, col);
}
};
private final InvalidationListener columnComparatorObserver = new InvalidationListener() {
@Override public void invalidated(Observable valueModel) {
TreeTableColumn col = (TreeTableColumn) ((SimpleObjectProperty)valueModel).getBean();
if (! getSortOrder().contains(col)) return;
doSort(TableUtil.SortEventType.COLUMN_COMPARATOR_CHANGE, col);
}
};
/* proxy pseudo-class state change from selectionModel's cellSelectionEnabledProperty */
private final InvalidationListener cellSelectionModelInvalidationListener = new InvalidationListener() {
@Override public void invalidated(Observable o) {
boolean isCellSelection = ((BooleanProperty)o).get();
pseudoClassStateChanged(PSEUDO_CLASS_CELL_SELECTION, isCellSelection);
pseudoClassStateChanged(PSEUDO_CLASS_ROW_SELECTION, !isCellSelection);
}
};
private WeakEventHandler weakRootEventListener;
private final WeakInvalidationListener weakColumnVisibleObserver =
new WeakInvalidationListener(columnVisibleObserver);
private final WeakInvalidationListener weakColumnSortableObserver =
new WeakInvalidationListener(columnSortableObserver);
private final WeakInvalidationListener weakColumnSortTypeObserver =
new WeakInvalidationListener(columnSortTypeObserver);
private final WeakInvalidationListener weakColumnComparatorObserver =
new WeakInvalidationListener(columnComparatorObserver);
private final WeakListChangeListener> weakColumnsObserver =
new WeakListChangeListener>(columnsObserver);
private final WeakInvalidationListener weakCellSelectionModelInvalidationListener =
new WeakInvalidationListener(cellSelectionModelInvalidationListener);
/***************************************************************************
* *
* Properties *
* *
**************************************************************************/
// --- Root
private ObjectProperty> root = new SimpleObjectProperty>(this, "root") {
private WeakReference> weakOldItem;
@Override protected void invalidated() {
TreeItem oldTreeItem = weakOldItem == null ? null : weakOldItem.get();
if (oldTreeItem != null && weakRootEventListener != null) {
oldTreeItem.removeEventHandler(TreeItem.treeNotificationEvent(), weakRootEventListener);
}
TreeItem root = getRoot();
if (root != null) {
weakRootEventListener = new WeakEventHandler(rootEvent);
getRoot().addEventHandler(TreeItem.treeNotificationEvent(), weakRootEventListener);
weakOldItem = new WeakReference>(root);
}
expandedItemCountDirty = true;
updateRootExpanded();
}
};
/**
* Sets the root node in this TreeTableView. See the {@link TreeItem} class level
* documentation for more details.
*
* @param value The {@link TreeItem} that will be placed at the root of the
* TreeTableView.
*/
public final void setRoot(TreeItem value) {
rootProperty().set(value);
}
/**
* Returns the current root node of this TreeTableView, or null if no root node
* is specified.
* @return The current root node, or null if no root node exists.
*/
public final TreeItem getRoot() {
return root == null ? null : root.get();
}
/**
* Property representing the root node of the TreeTableView.
*/
public final ObjectProperty> rootProperty() {
return root;
}
// --- Show Root
private BooleanProperty showRoot;
/**
* Specifies whether the root {@code TreeItem} should be shown within this
* TreeTableView.
*
* @param value If true, the root TreeItem will be shown, and if false it
* will be hidden.
*/
public final void setShowRoot(boolean value) {
showRootProperty().set(value);
}
/**
* Returns true if the root of the TreeTableView should be shown, and false if
* it should not. By default, the root TreeItem is visible in the TreeTableView.
*/
public final boolean isShowRoot() {
return showRoot == null ? true : showRoot.get();
}
/**
* Property that represents whether or not the TreeTableView root node is visible.
*/
public final BooleanProperty showRootProperty() {
if (showRoot == null) {
showRoot = new SimpleBooleanProperty(this, "showRoot", true) {
@Override protected void invalidated() {
updateRootExpanded();
updateExpandedItemCount(getRoot());
}
};
}
return showRoot;
}
// --- Tree Column
private ObjectProperty> treeColumn;
/**
* Property that represents which column should have the disclosure node
* shown in it (that is, the column with the arrow). By default this will be
* the left-most column if this property is null, otherwise it will be the
* specified column assuming it is non-null and contained within the
* {@link #getVisibleLeafColumns() visible leaf columns} list.
*/
public final ObjectProperty> treeColumnProperty() {
if (treeColumn == null) {
treeColumn = new SimpleObjectProperty>(this, "treeColumn", null);
}
return treeColumn;
}
public final void setTreeColumn(TreeTableColumn value) {
treeColumnProperty().set(value);
}
public final TreeTableColumn getTreeColumn() {
return treeColumn == null ? null : treeColumn.get();
}
// --- Selection Model
private ObjectProperty> selectionModel;
/**
* Sets the {@link MultipleSelectionModel} to be used in the TreeTableView.
* Despite a TreeTableView requiring a Multiple SelectionModel
,
* it is possible to configure it to only allow single selection (see
* {@link MultipleSelectionModel#setSelectionMode(javafx.scene.control.SelectionMode)}
* for more information).
*/
public final void setSelectionModel(TreeTableViewSelectionModel value) {
selectionModelProperty().set(value);
}
/**
* Returns the currently installed selection model.
*/
public final TreeTableViewSelectionModel getSelectionModel() {
return selectionModel == null ? null : selectionModel.get();
}
/**
* The SelectionModel provides the API through which it is possible
* to select single or multiple items within a TreeTableView, as well as inspect
* which rows have been selected by the user. Note that it has a generic
* type that must match the type of the TreeTableView itself.
*/
public final ObjectProperty> selectionModelProperty() {
if (selectionModel == null) {
selectionModel = new SimpleObjectProperty>(this, "selectionModel") {
TreeTableViewSelectionModel oldValue = null;
@Override protected void invalidated() {
// need to listen to the cellSelectionEnabledProperty
// in order to set pseudo-class state
if (oldValue != null) {
oldValue.cellSelectionEnabledProperty().removeListener(weakCellSelectionModelInvalidationListener);
}
oldValue = get();
if (oldValue != null) {
oldValue.cellSelectionEnabledProperty().addListener(weakCellSelectionModelInvalidationListener);
// fake invalidation to ensure updated pseudo-class states
weakCellSelectionModelInvalidationListener.invalidated(oldValue.cellSelectionEnabledProperty());
}
}
};
}
return selectionModel;
}
// --- Focus Model
private ObjectProperty> focusModel;
/**
* Sets the {@link FocusModel} to be used in the TreeTableView.
*/
public final void setFocusModel(TreeTableViewFocusModel value) {
focusModelProperty().set(value);
}
/**
* Returns the currently installed {@link FocusModel}.
*/
public final TreeTableViewFocusModel getFocusModel() {
return focusModel == null ? null : focusModel.get();
}
/**
* The FocusModel provides the API through which it is possible
* to control focus on zero or one rows of the TreeTableView. Generally the
* default implementation should be more than sufficient.
*/
public final ObjectProperty> focusModelProperty() {
if (focusModel == null) {
focusModel = new SimpleObjectProperty>(this, "focusModel");
}
return focusModel;
}
// // --- Span Model
// private ObjectProperty>> spanModel
// = new SimpleObjectProperty>>(this, "spanModel") {
//
// @Override protected void invalidated() {
// ObservableList styleClass = getStyleClass();
// if (getSpanModel() == null) {
// styleClass.remove(CELL_SPAN_TABLE_VIEW_STYLE_CLASS);
// } else if (! styleClass.contains(CELL_SPAN_TABLE_VIEW_STYLE_CLASS)) {
// styleClass.add(CELL_SPAN_TABLE_VIEW_STYLE_CLASS);
// }
// }
// };
//
// public final ObjectProperty>> spanModelProperty() {
// return spanModel;
// }
// public final void setSpanModel(SpanModel> value) {
// spanModelProperty().set(value);
// }
//
// public final SpanModel> getSpanModel() {
// return spanModel.get();
// }
// --- Tree node count
/**
* Represents the number of tree nodes presently able to be visible in the
* TreeTableView. This is essentially the count of all expanded tree items, and
* their children.
*
*
For example, if just the root node is visible, the expandedItemCount will
* be one. If the root had three children and the root was expanded, the value
* will be four.
*/
private ReadOnlyIntegerWrapper expandedItemCount = new ReadOnlyIntegerWrapper(this, "expandedItemCount", 0);
public final ReadOnlyIntegerProperty expandedItemCountProperty() {
return expandedItemCount.getReadOnlyProperty();
}
private void setExpandedItemCount(int value) {
expandedItemCount.set(value);
}
public final int getExpandedItemCount() {
if (expandedItemCountDirty) {
updateExpandedItemCount(getRoot());
}
return expandedItemCount.get();
}
// --- Editable
private BooleanProperty editable;
public final void setEditable(boolean value) {
editableProperty().set(value);
}
public final boolean isEditable() {
return editable == null ? false : editable.get();
}
/**
* Specifies whether this TreeTableView is editable - only if the TreeTableView and
* the TreeCells within it are both editable will a TreeCell be able to go
* into their editing state.
*/
public final BooleanProperty editableProperty() {
if (editable == null) {
editable = new SimpleBooleanProperty(this, "editable", false);
}
return editable;
}
// --- Editing Item
private ReadOnlyObjectWrapper> editingItem;
private void setEditingItem(TreeItem value) {
editingItemPropertyImpl().set(value);
}
/**
* Returns the TreeItem that is currently being edited in the TreeTableView,
* or null if no item is being edited.
*/
public final TreeItem getEditingItem() {
return editingItem == null ? null : editingItem.get();
}
/**
* A property used to represent the TreeItem currently being edited
* in the TreeTableView, if editing is taking place, or -1 if no item is being edited.
*
*
It is not possible to set the editing item, instead it is required that
* you call {@link #edit(javafx.scene.control.TreeItem)}.
*/
public final ReadOnlyObjectProperty> editingItemProperty() {
return editingItemPropertyImpl().getReadOnlyProperty();
}
private ReadOnlyObjectWrapper> editingItemPropertyImpl() {
if (editingItem == null) {
editingItem = new ReadOnlyObjectWrapper>(this, "editingItem");
}
return editingItem;
}
// --- On Edit Start
private ObjectProperty>> onEditStart;
/**
* Sets the {@link EventHandler} that will be called when the user begins
* an edit.
*/
public final void setOnEditStart(EventHandler> value) {
onEditStartProperty().set(value);
}
/**
* Returns the {@link EventHandler} that will be called when the user begins
* an edit.
*/
public final EventHandler> getOnEditStart() {
return onEditStart == null ? null : onEditStart.get();
}
/**
* This event handler will be fired when the user successfully initiates
* editing.
*/
public final ObjectProperty>> onEditStartProperty() {
if (onEditStart == null) {
onEditStart = new SimpleObjectProperty>>(this, "onEditStart") {
@Override protected void invalidated() {
setEventHandler(TreeTableView.editStartEvent(), get());
}
};
}
return onEditStart;
}
// --- On Edit Commit
private ObjectProperty>> onEditCommit;
/**
* Sets the {@link EventHandler} that will be called when the user commits
* an edit.
*/
public final void setOnEditCommit(EventHandler> value) {
onEditCommitProperty().set(value);
}
/**
* Returns the {@link EventHandler} that will be called when the user commits
* an edit.
*/
public final EventHandler> getOnEditCommit() {
return onEditCommit == null ? null : onEditCommit.get();
}
/**
* This property is used when the user performs an action that should
* result in their editing input being persisted.
*
* The EventHandler in this property should not be called directly -
* instead call {@link TreeCell#commitEdit(java.lang.Object)} from within
* your custom TreeCell. This will handle firing this event, updating the
* view, and switching out of the editing state.
*/
public final ObjectProperty>> onEditCommitProperty() {
if (onEditCommit == null) {
onEditCommit = new SimpleObjectProperty>>(this, "onEditCommit") {
@Override protected void invalidated() {
setEventHandler(TreeTableView.editCommitEvent(), get());
}
};
}
return onEditCommit;
}
// --- On Edit Cancel
private ObjectProperty>> onEditCancel;
/**
* Sets the {@link EventHandler} that will be called when the user cancels
* an edit.
*/
public final void setOnEditCancel(EventHandler> value) {
onEditCancelProperty().set(value);
}
/**
* Returns the {@link EventHandler} that will be called when the user cancels
* an edit.
*/
public final EventHandler> getOnEditCancel() {
return onEditCancel == null ? null : onEditCancel.get();
}
/**
* This event handler will be fired when the user cancels editing a cell.
*/
public final ObjectProperty>> onEditCancelProperty() {
if (onEditCancel == null) {
onEditCancel = new SimpleObjectProperty>>(this, "onEditCancel") {
@Override protected void invalidated() {
setEventHandler(TreeTableView.editCancelEvent(), get());
}
};
}
return onEditCancel;
}
// --- Table menu button visible
private BooleanProperty tableMenuButtonVisible;
/**
* This controls whether a menu button is available when the user clicks
* in a designated space within the TableView, within which is a radio menu
* item for each TreeTableColumn in this table. This menu allows for the user to
* show and hide all TreeTableColumns easily.
*/
public final BooleanProperty tableMenuButtonVisibleProperty() {
if (tableMenuButtonVisible == null) {
tableMenuButtonVisible = new SimpleBooleanProperty(this, "tableMenuButtonVisible");
}
return tableMenuButtonVisible;
}
public final void setTableMenuButtonVisible (boolean value) {
tableMenuButtonVisibleProperty().set(value);
}
public final boolean isTableMenuButtonVisible() {
return tableMenuButtonVisible == null ? false : tableMenuButtonVisible.get();
}
// --- Column Resize Policy
private ObjectProperty> columnResizePolicy;
public final void setColumnResizePolicy(Callback callback) {
columnResizePolicyProperty().set(callback);
}
public final Callback getColumnResizePolicy() {
return columnResizePolicy == null ? UNCONSTRAINED_RESIZE_POLICY : columnResizePolicy.get();
}
/**
* This is the function called when the user completes a column-resize
* operation. The two most common policies are available as static functions
* in the TableView class: {@link #UNCONSTRAINED_RESIZE_POLICY} and
* {@link #CONSTRAINED_RESIZE_POLICY}.
*/
public final ObjectProperty> columnResizePolicyProperty() {
if (columnResizePolicy == null) {
columnResizePolicy = new SimpleObjectProperty>(this, "columnResizePolicy", UNCONSTRAINED_RESIZE_POLICY) {
private Callback oldPolicy;
@Override protected void invalidated() {
if (isInited) {
get().call(new TreeTableView.ResizeFeatures(TreeTableView.this, null, 0.0));
refresh();
if (oldPolicy != null) {
PseudoClass state = PseudoClass.getPseudoClass(oldPolicy.toString());
pseudoClassStateChanged(state, false);
}
if (get() != null) {
PseudoClass state = PseudoClass.getPseudoClass(get().toString());
pseudoClassStateChanged(state, true);
}
oldPolicy = get();
}
}
};
}
return columnResizePolicy;
}
// --- Row Factory
private ObjectProperty, TreeTableRow>> rowFactory;
/**
* A function which produces a TreeTableRow. The system is responsible for
* reusing TreeTableRows. Return from this function a TreeTableRow which
* might be usable for representing a single row in a TableView.
*
* Note that a TreeTableRow is not a TableCell. A TreeTableRow is
* simply a container for a TableCell, and in most circumstances it is more
* likely that you'll want to create custom TableCells, rather than
* TreeTableRows. The primary use case for creating custom TreeTableRow
* instances would most probably be to introduce some form of column
* spanning support.
*
* You can create custom TableCell instances per column by assigning the
* appropriate function to the cellFactory property in the TreeTableColumn class.
*/
public final ObjectProperty, TreeTableRow>> rowFactoryProperty() {
if (rowFactory == null) {
rowFactory = new SimpleObjectProperty, TreeTableRow>>(this, "rowFactory");
}
return rowFactory;
}
public final void setRowFactory(Callback, TreeTableRow> value) {
rowFactoryProperty().set(value);
}
public final Callback, TreeTableRow> getRowFactory() {
return rowFactory == null ? null : rowFactory.get();
}
// --- Placeholder Node
private ObjectProperty placeholder;
/**
* This Node is shown to the user when the table has no content to show.
* This may be the case because the table model has no data in the first
* place, that a filter has been applied to the table model, resulting
* in there being nothing to show the user, or that there are no currently
* visible columns.
*/
public final ObjectProperty placeholderProperty() {
if (placeholder == null) {
placeholder = new SimpleObjectProperty(this, "placeholder");
}
return placeholder;
}
public final void setPlaceholder(Node value) {
placeholderProperty().set(value);
}
public final Node getPlaceholder() {
return placeholder == null ? null : placeholder.get();
}
// --- Fixed cell size
private DoubleProperty fixedCellSize;
/**
* Sets the new fixed cell size for this control. Any value greater than
* zero will enable fixed cell size mode, whereas a zero or negative value
* (or Region.USE_COMPUTED_SIZE) will be used to disabled fixed cell size
* mode.
*
* @param value The new fixed cell size value, or -1 (or Region.USE_COMPUTED_SIZE)
* to disable.
*/
public final void setFixedCellSize(double value) {
fixedCellSizeProperty().set(value);
}
/**
* Returns the fixed cell size value, which may be -1 to represent fixed cell
* size mode is disabled, or a value greater than zero to represent the size
* of all cells in this control.
*
* @return A double representing the fixed cell size of this control, or -1
* if fixed cell size mode is disabled.
*/
public final double getFixedCellSize() {
return fixedCellSize == null ? Region.USE_COMPUTED_SIZE : fixedCellSize.get();
}
/**
* Specifies whether this control has cells that are a fixed height (of the
* specified value). If this value is -1 (i.e. {@link Region#USE_COMPUTED_SIZE}),
* then all cells are individually sized and positioned. This is a slow
* operation. Therefore, when performance matters and developers are not
* dependent on variable cell sizes it is a good idea to set the fixed cell
* size value. Generally cells are around 24px, so setting a fixed cell size
* of 24 is likely to result in very little difference in visuals, but a
* improvement to performance.
*
* To set this property via CSS, use the -fx-fixed-cell-size property.
* This should not be confused with the -fx-cell-size property. The difference
* between these two CSS properties is that -fx-cell-size will size all
* cells to the specified size, but it will not enforce that this is the
* only size (thus allowing for variable cell sizes, and preventing the
* performance gains from being possible). Therefore, when performance matters
* use -fx-fixed-cell-size, instead of -fx-cell-size. If both properties are
* specified in CSS, -fx-fixed-cell-size takes precedence.
*/
public final DoubleProperty fixedCellSizeProperty() {
if (fixedCellSize == null) {
fixedCellSize = new StyleableDoubleProperty(Region.USE_COMPUTED_SIZE) {
@Override public CssMetaData,Number> getCssMetaData() {
return StyleableProperties.FIXED_CELL_SIZE;
}
@Override public Object getBean() {
return TreeTableView.this;
}
@Override public String getName() {
return "fixedCellSize";
}
};
}
return fixedCellSize;
}
// --- Editing Cell
private ReadOnlyObjectWrapper> editingCell;
private void setEditingCell(TreeTablePosition value) {
editingCellPropertyImpl().set(value);
}
public final TreeTablePosition getEditingCell() {
return editingCell == null ? null : editingCell.get();
}
/**
* Represents the current cell being edited, or null if
* there is no cell being edited.
*/
public final ReadOnlyObjectProperty> editingCellProperty() {
return editingCellPropertyImpl().getReadOnlyProperty();
}
private ReadOnlyObjectWrapper> editingCellPropertyImpl() {
if (editingCell == null) {
editingCell = new ReadOnlyObjectWrapper>(this, "editingCell");
}
return editingCell;
}
// --- SortMode
/**
* Specifies the sort mode to use when sorting the contents of this TreeTableView,
* should any columns be specified in the {@link #getSortOrder() sort order}
* list.
*/
private ObjectProperty sortMode;
public final ObjectProperty sortModeProperty() {
if (sortMode == null) {
sortMode = new SimpleObjectProperty(this, "sortMode", TreeSortMode.ALL_DESCENDANTS);
}
return sortMode;
}
public final void setSortMode(TreeSortMode value) {
sortModeProperty().set(value);
}
public final TreeSortMode getSortMode() {
return sortMode == null ? TreeSortMode.ALL_DESCENDANTS : sortMode.get();
}
// --- Comparator (built via sortOrder list, so read-only)
/**
* The comparator property is a read-only property that is representative of the
* current state of the {@link #getSortOrder() sort order} list. The sort
* order list contains the columns that have been added to it either programmatically
* or via a user clicking on the headers themselves.
*/
private ReadOnlyObjectWrapper> comparator;
private void setComparator(Comparator value) {
comparatorPropertyImpl().set(value);
}
public final Comparator getComparator() {
return comparator == null ? null : comparator.get();
}
public final ReadOnlyObjectProperty> comparatorProperty() {
return comparatorPropertyImpl().getReadOnlyProperty();
}
private ReadOnlyObjectWrapper> comparatorPropertyImpl() {
if (comparator == null) {
comparator = new ReadOnlyObjectWrapper>(this, "comparator");
}
return comparator;
}
// --- sortPolicy
/**
* The sort policy specifies how sorting in this TreeTableView should be performed.
* For example, a basic sort policy may just recursively sort the children of
* the root tree item, whereas a more advanced sort policy may call to a
* database to perform the necessary sorting on the server-side.
*
* TreeTableView ships with a {@link TableView#DEFAULT_SORT_POLICY default
* sort policy} that does precisely as mentioned above: it simply attempts
* to sort the tree hierarchy in-place.
*
*
It is recommended that rather than override the {@link TreeTableView#sort() sort}
* method that a different sort policy be provided instead.
*/
private ObjectProperty, Boolean>> sortPolicy;
public final void setSortPolicy(Callback, Boolean> callback) {
sortPolicyProperty().set(callback);
}
@SuppressWarnings("unchecked")
public final Callback, Boolean> getSortPolicy() {
return sortPolicy == null ?
(Callback, Boolean>)(Object) DEFAULT_SORT_POLICY :
sortPolicy.get();
}
@SuppressWarnings("unchecked")
public final ObjectProperty, Boolean>> sortPolicyProperty() {
if (sortPolicy == null) {
sortPolicy = new SimpleObjectProperty, Boolean>>(
this, "sortPolicy", (Callback, Boolean>)(Object) DEFAULT_SORT_POLICY) {
@Override protected void invalidated() {
sort();
}
};
}
return sortPolicy;
}
// onSort
/**
* Called when there's a request to sort the control.
*/
private ObjectProperty>>> onSort;
public void setOnSort(EventHandler>> value) {
onSortProperty().set(value);
}
public EventHandler>> getOnSort() {
if( onSort != null ) {
return onSort.get();
}
return null;
}
public ObjectProperty>>> onSortProperty() {
if( onSort == null ) {
onSort = new ObjectPropertyBase>>>() {
@Override protected void invalidated() {
EventType>> eventType = SortEvent.sortEvent();
EventHandler>> eventHandler = get();
setEventHandler(eventType, eventHandler);
}
@Override public Object getBean() {
return TreeTableView.this;
}
@Override public String getName() {
return "onSort";
}
};
}
return onSort;
}
/***************************************************************************
* *
* Public API *
* *
**************************************************************************/
/** {@inheritDoc} */
@Override protected void layoutChildren() {
if (expandedItemCountDirty) {
updateExpandedItemCount(getRoot());
}
super.layoutChildren();
}
/**
* Instructs the TreeTableView to begin editing the given TreeItem, if
* the TreeTableView is {@link #editableProperty() editable}. Once
* this method is called, if the current
* {@link #cellFactoryProperty() cell factory} is set up to support editing,
* the Cell will switch its visual state to enable the user input to take place.
*
* @param item The TreeItem in the TreeTableView that should be edited.
*/
public void edit(TreeItem item) {
if (!isEditable()) return;
setEditingItem(item);
}
/**
* Scrolls the TreeTableView such that the item in the given index is visible to
* the end user.
*
* @param index The index that should be made visible to the user, assuming
* of course that it is greater than, or equal to 0, and less than the
* number of the visible items in the TreeTableView.
*/
public void scrollTo(int index) {
ControlUtils.scrollToIndex(this, index);
}
/**
* Called when there's a request to scroll an index into view using {@link #scrollTo(int)}
*/
private ObjectProperty>> onScrollTo;
public void setOnScrollTo(EventHandler> value) {
onScrollToProperty().set(value);
}
public EventHandler> getOnScrollTo() {
if( onScrollTo != null ) {
return onScrollTo.get();
}
return null;
}
public ObjectProperty>> onScrollToProperty() {
if( onScrollTo == null ) {
onScrollTo = new ObjectPropertyBase>>() {
@Override protected void invalidated() {
setEventHandler(ScrollToEvent.scrollToTopIndex(), get());
}
@Override public Object getBean() {
return TreeTableView.this;
}
@Override public String getName() {
return "onScrollTo";
}
};
}
return onScrollTo;
}
/**
* Scrolls the TreeTableView so that the given column is visible within the viewport.
* @param column The column that should be visible to the user.
*/
public void scrollToColumn(TableColumn column) {
ControlUtils.scrollToColumn(this, column);
}
/**
* Scrolls the TreeTableView so that the given index is visible within the viewport.
* @param columnIndex The index of a column that should be visible to the user.
*/
public void scrollToColumnIndex(int columnIndex) {
if( getColumns() != null ) {
ControlUtils.scrollToColumn(this, getColumns().get(columnIndex));
}
}
/**
* Called when there's a request to scroll a column into view using {@link #scrollToColumn(TableColumn)}
* or {@link #scrollToColumnIndex(int)}
*/
private ObjectProperty>>> onScrollToColumn;
public void setOnScrollToColumn(EventHandler>> value) {
onScrollToColumnProperty().set(value);
}
public EventHandler>> getOnScrollToColumn() {
if( onScrollToColumn != null ) {
return onScrollToColumn.get();
}
return null;
}
public ObjectProperty>>> onScrollToColumnProperty() {
if( onScrollToColumn == null ) {
onScrollToColumn = new ObjectPropertyBase>>>() {
@Override
protected void invalidated() {
EventType>> type = ScrollToEvent.scrollToColumn();
setEventHandler(type, get());
}
@Override
public Object getBean() {
return TreeTableView.this;
}
@Override
public String getName() {
return "onScrollToColumn";
}
};
}
return onScrollToColumn;
}
/**
* Returns the index position of the given TreeItem, taking into account the
* current state of each TreeItem (i.e. whether or not it is expanded).
*
* @param item The TreeItem for which the index is sought.
* @return An integer representing the location in the current TreeTableView of the
* first instance of the given TreeItem, or -1 if it is null or can not
* be found.
*/
public int getRow(TreeItem item) {
return TreeUtil.getRow(item, getRoot(), expandedItemCountDirty, isShowRoot());
}
/**
* Returns the TreeItem in the given index, or null if it is out of bounds.
*
* @param row The index of the TreeItem being sought.
* @return The TreeItem in the given index, or null if it is out of bounds.
*/
public TreeItem getTreeItem(int row) {
// normalize the requested row based on whether showRoot is set
int r = isShowRoot() ? row : (row + 1);
return TreeUtil.getItem(getRoot(), r, expandedItemCountDirty);
}
/**
* The TreeTableColumns that are part of this TableView. As the user reorders
* the TableView columns, this list will be updated to reflect the current
* visual ordering.
*
* Note: to display any data in a TableView, there must be at least one
* TreeTableColumn in this ObservableList.
*/
public final ObservableList> getColumns() {
return columns;
}
/**
* The sortOrder list defines the order in which {@link TreeTableColumn} instances
* are sorted. An empty sortOrder list means that no sorting is being applied
* on the TableView. If the sortOrder list has one TreeTableColumn within it,
* the TableView will be sorted using the
* {@link TreeTableColumn#sortTypeProperty() sortType} and
* {@link TreeTableColumn#comparatorProperty() comparator} properties of this
* TreeTableColumn (assuming
* {@link TreeTableColumn#sortableProperty() TreeTableColumn.sortable} is true).
* If the sortOrder list contains multiple TreeTableColumn instances, then
* the TableView is firstly sorted based on the properties of the first
* TreeTableColumn. If two elements are considered equal, then the second
* TreeTableColumn in the list is used to determine ordering. This repeats until
* the results from all TreeTableColumn comparators are considered, if necessary.
*
* @return An ObservableList containing zero or more TreeTableColumn instances.
*/
public final ObservableList> getSortOrder() {
return sortOrder;
}
/**
* Applies the currently installed resize policy against the given column,
* resizing it based on the delta value provided.
*/
public boolean resizeColumn(TreeTableColumn column, double delta) {
if (column == null || Double.compare(delta, 0.0) == 0) return false;
boolean allowed = getColumnResizePolicy().call(new TreeTableView.ResizeFeatures(TreeTableView.this, column, delta));
if (!allowed) return false;
// This fixes the issue where if the column width is reduced and the
// table width is also reduced, horizontal scrollbars will begin to
// appear at the old width. This forces the VirtualFlow.maxPrefBreadth
// value to be reset to -1 and subsequently recalculated. Of course
// ideally we'd just refreshView, but for the time-being no such function
// exists.
refresh();
return true;
}
/**
* Causes the cell at the given row/column view indexes to switch into
* its editing state, if it is not already in it, and assuming that the
* TableView and column are also editable.
*/
public void edit(int row, TreeTableColumn column) {
if (!isEditable() || (column != null && ! column.isEditable())) return;
setEditingCell(new TreeTablePosition(this, row, column));
}
/**
* Returns an unmodifiable list containing the currently visible leaf columns.
*/
@ReturnsUnmodifiableCollection
public ObservableList> getVisibleLeafColumns() {
return unmodifiableVisibleLeafColumns;
}
/**
* Returns the position of the given column, relative to all other
* visible leaf columns.
*/
public int getVisibleLeafIndex(TreeTableColumn column) {
return getVisibleLeafColumns().indexOf(column);
}
/**
* Returns the TableColumn in the given column index, relative to all other
* visible leaf columns.
*/
public TreeTableColumn getVisibleLeafColumn(int column) {
if (column < 0 || column >= visibleLeafColumns.size()) return null;
return visibleLeafColumns.get(column);
}
/**
* The sort method forces the TreeTableView to re-run its sorting algorithm. More
* often than not it is not necessary to call this method directly, as it is
* automatically called when the {@link #getSortOrder() sort order},
* {@link #sortPolicyProperty() sort policy}, or the state of the
* TableColumn {@link TableColumn#sortTypeProperty() sort type} properties
* change. In other words, this method should only be called directly when
* something external changes and a sort is required.
*/
public void sort() {
final ObservableList> sortOrder = getSortOrder();
// update the Comparator property
final Comparator oldComparator = getComparator();
if (sortOrder.isEmpty()) {
setComparator(null);
} else {
Comparator newComparator = new TableColumnComparatorBase.TreeTableColumnComparator(sortOrder);
setComparator(newComparator);
}
// fire the onSort event and check if it is consumed, if
// so, don't run the sort
SortEvent> sortEvent = new SortEvent>(TreeTableView.this, TreeTableView.this);
fireEvent(sortEvent);
if (sortEvent.isConsumed()) {
// if the sort is consumed we could back out the last action (the code
// is commented out right below), but we don't as we take it as a
// sign that the developer has decided to handle the event themselves.
// sortLock = true;
// TableUtil.handleSortFailure(sortOrder, lastSortEventType, lastSortEventSupportInfo);
// sortLock = false;
return;
}
// get the sort policy and run it
Callback, Boolean> sortPolicy = getSortPolicy();
if (sortPolicy == null) return;
Boolean success = sortPolicy.call(this);
if (success == null || ! success) {
// the sort was a failure. Need to backout if possible
sortLock = true;
TableUtil.handleSortFailure(sortOrder, lastSortEventType, lastSortEventSupportInfo);
setComparator(oldComparator);
sortLock = false;
}
}
/***************************************************************************
* *
* Private Implementation *
* *
**************************************************************************/
private boolean sortLock = false;
private TableUtil.SortEventType lastSortEventType = null;
private Object[] lastSortEventSupportInfo = null;
private void doSort(final TableUtil.SortEventType sortEventType, final Object... supportInfo) {
if (sortLock) {
return;
}
this.lastSortEventType = sortEventType;
this.lastSortEventSupportInfo = supportInfo;
sort();
this.lastSortEventType = null;
this.lastSortEventSupportInfo = null;
}
private void updateExpandedItemCount(TreeItem treeItem) {
setExpandedItemCount(TreeUtil.updateExpandedItemCount(treeItem, expandedItemCountDirty, isShowRoot()));
expandedItemCountDirty = false;
}
private void updateRootExpanded() {
// if we aren't showing the root, and the root isn't expanded, we expand
// it now so that something is shown.
if (!isShowRoot() && getRoot() != null && ! getRoot().isExpanded()) {
getRoot().setExpanded(true);
}
}
/**
* Call this function to force the TableView to re-evaluate itself. This is
* useful when the underlying data model is provided by a TableModel, and
* you know that the data model has changed. This will force the TableView
* to go back to the dataProvider and get the row count, as well as update
* the view to ensure all sorting is still correct based on any changes to
* the data model.
*/
private void refresh() {
getProperties().put(TableViewSkinBase.REFRESH, Boolean.TRUE);
}
// --- Content width
private void setContentWidth(double contentWidth) {
this.contentWidth = contentWidth;
if (isInited) {
// sometimes the current column resize policy will have to modify the
// column width of all columns in the table if the table width changes,
// so we short-circuit the resize function and just go straight there
// with a null TreeTableColumn, which indicates to the resize policy function
// that it shouldn't actually do anything specific to one column.
getColumnResizePolicy().call(new TreeTableView.ResizeFeatures(TreeTableView.this, null, 0.0));
refresh();
}
}
/**
* Recomputes the currently visible leaf columns in this TableView.
*/
private void updateVisibleLeafColumns() {
// update visible leaf columns list
List> cols = new ArrayList>();
buildVisibleLeafColumns(getColumns(), cols);
visibleLeafColumns.setAll(cols);
// sometimes the current column resize policy will have to modify the
// column width of all columns in the table if the table width changes,
// so we short-circuit the resize function and just go straight there
// with a null TreeTableColumn, which indicates to the resize policy function
// that it shouldn't actually do anything specific to one column.
getColumnResizePolicy().call(new TreeTableView.ResizeFeatures(TreeTableView.this, null, 0.0));
refresh();
}
private void buildVisibleLeafColumns(List> cols, List> vlc) {
for (TreeTableColumn c : cols) {
if (c == null) continue;
boolean hasChildren = ! c.getColumns().isEmpty();
if (hasChildren) {
buildVisibleLeafColumns(c.getColumns(), vlc);
} else if (c.isVisible()) {
vlc.add(c);
}
}
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
private static final String DEFAULT_STYLE_CLASS = "tree-table-view";
private static final PseudoClass PSEUDO_CLASS_CELL_SELECTION =
PseudoClass.getPseudoClass("cell-selection");
private static final PseudoClass PSEUDO_CLASS_ROW_SELECTION =
PseudoClass.getPseudoClass("row-selection");
/** @treatAsPrivate */
private static class StyleableProperties {
private static final CssMetaData,Number> FIXED_CELL_SIZE =
new CssMetaData,Number>("-fx-fixed-cell-size",
SizeConverter.getInstance(),
Region.USE_COMPUTED_SIZE) {
@Override public Double getInitialValue(TreeTableView node) {
return node.getFixedCellSize();
}
@Override public boolean isSettable(TreeTableView n) {
return n.fixedCellSize == null || !n.fixedCellSize.isBound();
}
@Override public StyleableProperty getStyleableProperty(TreeTableView n) {
return (StyleableProperty) n.fixedCellSizeProperty();
}
};
private static final List> STYLEABLES;
static {
final List> styleables =
new ArrayList>(Control.getClassCssMetaData());
styleables.add(FIXED_CELL_SIZE);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
/**
* @return The CssMetaData associated with this class, which may include the
* CssMetaData of its super classes.
*/
public static List> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
/**
* {@inheritDoc}
*/
@Override
public List> getControlCssMetaData() {
return getClassCssMetaData();
}
/** {@inheritDoc} */
@Override protected Skin createDefaultSkin() {
return new TreeTableViewSkin(this);
}
/***************************************************************************
* *
* Support Classes *
* *
**************************************************************************/
/**
* An immutable wrapper class for use in the TableView
* {@link TreeTableView#columnResizePolicyProperty() column resize} functionality.
* @since JavaFX 8.0
*/
public static class ResizeFeatures extends ResizeFeaturesBase> {
private TreeTableView treeTable;
/**
* Creates an instance of this class, with the provided TreeTableView,
* TreeTableColumn and delta values being set and stored in this immutable
* instance.
*
* @param table The TreeTableView upon which the resize operation is occurring.
* @param column The column upon which the resize is occurring, or null
* if this ResizeFeatures instance is being created as a result of a
* TreeTableView resize operation.
* @param delta The amount of horizontal space added or removed in the
* resize operation.
*/
public ResizeFeatures(TreeTableView treeTable, TreeTableColumn column, Double delta) {
super(column, delta);
this.treeTable = treeTable;
}
/**
* Returns the column upon which the resize is occurring, or null
* if this ResizeFeatures instance was created as a result of a
* TreeTableView resize operation.
*/
@Override public TreeTableColumn getColumn() {
return (TreeTableColumn) super.getColumn();
}
/**
* Returns the TreeTableView upon which the resize operation is occurring.
*/
public TreeTableView getTable() { return treeTable; }
}
/**
* An {@link Event} subclass used specifically in TreeTableView for representing
* edit-related events. It provides additional API to easily access the
* TreeItem that the edit event took place on, as well as the input provided
* by the end user.
*
* @param The type of the input, which is the same type as the TreeTableView
* itself.
* @since JavaFX 8.0
*/
public static class EditEvent extends Event {
private static final long serialVersionUID = -4437033058917528976L;
/**
* Common supertype for all edit event types.
*/
public static final EventType ANY = EDIT_ANY_EVENT;
private final S oldValue;
private final S newValue;
private transient final TreeItem treeItem;
/**
* Creates a new EditEvent instance to represent an edit event. This
* event is used for {@link #EDIT_START_EVENT},
* {@link #EDIT_COMMIT_EVENT} and {@link #EDIT_CANCEL_EVENT} types.
*/
public EditEvent(TreeTableView source,
EventType eventType,
TreeItem treeItem, S oldValue, S newValue) {
super(source, Event.NULL_SOURCE_TARGET, eventType);
this.oldValue = oldValue;
this.newValue = newValue;
this.treeItem = treeItem;
}
/**
* Returns the TreeTableView upon which the edit took place.
*/
@Override public TreeTableView getSource() {
return (TreeTableView) super.getSource();
}
/**
* Returns the {@link TreeItem} upon which the edit took place.
*/
public TreeItem getTreeItem() {
return treeItem;
}
/**
* Returns the new value input into the TreeItem by the end user.
*/
public S getNewValue() {
return newValue;
}
/**
* Returns the old value that existed in the TreeItem prior to the current
* edit event.
*/
public S getOldValue() {
return oldValue;
}
}
/**
* A simple extension of the {@link SelectionModel} abstract class to
* allow for special support for TableView controls.
* @since JavaFX 8.0
*/
public static abstract class TreeTableViewSelectionModel extends
TableSelectionModel, TreeTableColumn> {
/***********************************************************************
* *
* Private fields *
* *
**********************************************************************/
private final TreeTableView treeTableView;
/***********************************************************************
* *
* Constructors *
* *
**********************************************************************/
/**
* Builds a default TableViewSelectionModel instance with the provided
* TableView.
* @param tableView The TableView upon which this selection model should
* operate.
* @throws NullPointerException TableView can not be null.
*/
public TreeTableViewSelectionModel(final TreeTableView treeTableView) {
if (treeTableView == null) {
throw new NullPointerException("TreeTableView can not be null");
}
this.treeTableView = treeTableView;
cellSelectionEnabledProperty().addListener(new InvalidationListener() {
@Override public void invalidated(Observable o) {
isCellSelectionEnabled();
clearSelection();
}
});
}
/***********************************************************************
* *
* Abstract API *
* *
**********************************************************************/
/**
* A read-only ObservableList representing the currently selected cells
* in this TableView. Rather than directly modify this list, please
* use the other methods provided in the TableViewSelectionModel.
*/
public abstract ObservableList> getSelectedCells();
/***********************************************************************
* *
* Public API *
* *
**********************************************************************/
/**
* Returns the TableView instance that this selection model is installed in.
*/
public TreeTableView getTreeTableView() {
return treeTableView;
}
/** {@inheritDoc} */
@Override public TreeItem getModelItem(int index) {
return treeTableView.getTreeItem(index);
}
/** {@inheritDoc} */
@Override protected int getItemCount() {
return treeTableView.getExpandedItemCount();
}
/** {@inheritDoc} */
@Override public void focus(int row) {
focus(row, null);
}
/** {@inheritDoc} */
@Override public int getFocusedIndex() {
return getFocusedCell().getRow();
}
/***********************************************************************
* *
* Private implementation *
* *
**********************************************************************/
private void focus(int row, TreeTableColumn column) {
focus(new TreeTablePosition(getTreeTableView(), row, column));
}
private void focus(TreeTablePosition pos) {
if (getTreeTableView().getFocusModel() == null) return;
getTreeTableView().getFocusModel().focus(pos.getRow(), pos.getTableColumn());
}
private TreeTablePosition getFocusedCell() {
if (treeTableView.getFocusModel() == null) {
return new TreeTablePosition(treeTableView, -1, null);
}
return treeTableView.getFocusModel().getFocusedCell();
}
}
/**
* A primitive selection model implementation, using a List to store all
* selected indices.
*/
// package for testing
static class TreeTableViewArrayListSelectionModel extends TreeTableViewSelectionModel {
/***********************************************************************
* *
* Constructors *
* *
**********************************************************************/
public TreeTableViewArrayListSelectionModel(final TreeTableView treeTableView) {
super(treeTableView);
this.treeTableView = treeTableView;
this.treeTableView.rootProperty().addListener(weakRootPropertyListener);
updateTreeEventListener(null, treeTableView.getRoot());
final MappingChange.Map,TreeItem> cellToItemsMap = new MappingChange.Map, TreeItem>() {
@Override public TreeItem map(TreeTablePosition f) {
return getModelItem(f.getRow());
}
};
final MappingChange.Map,Integer> cellToIndicesMap = new MappingChange.Map, Integer>() {
@Override public Integer map(TreeTablePosition f) {
return f.getRow();
}
};
selectedCells = FXCollections.>observableArrayList();
selectedCells.addListener(new ListChangeListener>() {
@Override
public void onChanged(final ListChangeListener.Change> c) {
// RT-29313: because selectedIndices and selectedItems represent
// row-based selection, we need to update the
// selectedIndicesBitSet when the selectedCells changes to
// ensure that selectedIndices and selectedItems return only
// the correct values (and only once). The issue identified
// by RT-29313 is that the size and contents of selectedIndices
// and selectedItems can not simply defer to the
// selectedCells as selectedCells may be representing
// multiple cells from one row (e.g. selectedCells of
// [(0,1), (1,1), (1,2), (1,3)] should result in
// selectedIndices of [0,1], not [0,1,1,1]).
// An inefficient solution would rebuild the selectedIndicesBitSet
// every time the change happens, but we can do better than
// that. Inefficient solution:
//
// selectedIndicesBitSet.clear();
// for (int i = 0; i < selectedCells.size(); i++) {
// final TreeTablePosition tp = selectedCells.get(i);
// final int row = tp.getRow();
// selectedIndicesBitSet.set(row);
// }
//
// A more efficient solution:
final List newlySelectedRows = new ArrayList();
final List newlyUnselectedRows = new ArrayList();
while (c.next()) {
if (c.wasRemoved()) {
List> removed = c.getRemoved();
for (int i = 0; i < removed.size(); i++) {
final TreeTablePosition tp = removed.get(i);
final int row = tp.getRow();
if (selectedIndices.get(row)) {
selectedIndices.clear(row);
newlySelectedRows.add(row);
}
}
}
if (c.wasAdded()) {
List> added = c.getAddedSubList();
for (int i = 0; i < added.size(); i++) {
final TreeTablePosition tp = added.get(i);
final int row = tp.getRow();
if (! selectedIndices.get(row)) {
selectedIndices.set(row);
newlySelectedRows.add(row);
}
}
}
}
c.reset();
// when the selectedCells observableArrayList changes, we manually call
// the observers of the selectedItems, selectedIndices and
// selectedCells lists.
// create an on-demand list of the removed objects contained in the
// given rows
selectedItems.callObservers(new MappingChange, TreeItem>(c, cellToItemsMap, selectedItems));
c.reset();
final ReadOnlyUnbackedObservableList selectedIndicesSeq =
(ReadOnlyUnbackedObservableList)getSelectedIndices();
if (! newlySelectedRows.isEmpty() && newlyUnselectedRows.isEmpty()) {
// need to come up with ranges based on the actualSelectedRows, and
// then fire the appropriate number of changes. We also need to
// translate from a desired row to select to where that row is
// represented in the selectedIndices list. For example,
// we may have requested to select row 5, and the selectedIndices
// list may therefore have the following: [1,4,5], meaning row 5
// is in position 2 of the selectedIndices list
Change change = createRangeChange(selectedIndicesSeq, newlySelectedRows);
selectedIndicesSeq.callObservers(change);
} else {
selectedIndicesSeq.callObservers(new MappingChange, Integer>(c, cellToIndicesMap, selectedIndicesSeq));
c.reset();
}
selectedCellsSeq.callObservers(new MappingChange, TreeTablePosition>(c, MappingChange.NOOP_MAP, selectedCellsSeq));
c.reset();
}
});
selectedItems = new ReadOnlyUnbackedObservableList>() {
@Override public TreeItem get(int i) {
return getModelItem(getSelectedIndices().get(i));
}
@Override public int size() {
return getSelectedIndices().size();
}
};
selectedCellsSeq = new ReadOnlyUnbackedObservableList>() {
@Override public TreeTablePosition get(int i) {
return selectedCells.get(i);
}
@Override public int size() {
return selectedCells.size();
}
};
}
private final TreeTableView treeTableView;
private void updateTreeEventListener(TreeItem oldRoot, TreeItem