. We can immediately
* set such a list directly in to the TableView, as such:
*
*
* {@code
* ObservableList teamMembers = getTeamMembers();
* table.setItems(teamMembers);}
*
* With the items set as such, TableView will automatically update whenever
* the teamMembers
list changes. If the items list is available
* before the TableView is instantiated, it is possible to pass it directly into
* the constructor.
*
*
At this point we now have a TableView hooked up to observe the
* teamMembers
observableList. The missing ingredient
* now is the means of splitting out the data contained within the model and
* representing it in one or more {@link TableColumn TableColumn} instances. To
* create a two-column TableView to show the firstName and lastName properties,
* we extend the last code sample as follows:
*
*
* {@code
* ObservableList teamMembers = ...;
* table.setItems(teamMembers);
*
* TableColumn firstNameCol = new TableColumn("First Name");
* firstNameCol.setCellValueFactory(new PropertyValueFactory("firstName"));
* TableColumn lastNameCol = new TableColumn("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 TableView instance. Running this code (assuming the
* people ObservableList is appropriately created) will result in a TableView being
* shown with two columns for firstName and lastName. Any other properties of the
* Person class will not be shown, as no TableColumns are defined.
*
*
TableView support for classes that don't contain properties
*
* The code shown above is the shortest possible code for creating a TableView
* 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 TableColumn} API
* documentation, but briefly, here is how a TableColumn could be specified:
*
*
* {@code
* firstNameCol.setCellValueFactory(new Callback, ObservableValue>() {
* public ObservableValue call(CellDataFeatures p) {
* // p.getValue() returns the Person instance for a particular TableView row
* return p.getValue().firstNameProperty();
* }
* });
* }}
*
* TableView Selection / Focus APIs
* To track selection and focus, it is necessary to become familiar with the
* {@link SelectionModel} and {@link FocusModel} classes. A TableView 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 TableView 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 TableView instance, it is therefore necessary
* to do the following:
*
*
* {@code
* tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);}
*
* Customizing TableView Visuals
* The visuals of the TableView can be entirely customized by replacing the
* default {@link #rowFactoryProperty() row factory}. A row factory is used to
* generate {@link TableRow} instances, which are used to represent an entire
* row in the TableView.
*
*
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 TableRow} is not a
* {@link TableCell}. A {@link TableRow} is simply a container for zero or more
* {@link TableCell}, and in most circumstances it is more likely that you'll
* want to create custom TableCells, rather than TableRows. The primary use case
* for creating custom TableRow instances would most probably be to introduce
* some form of column spanning support.
*
*
You can create custom {@link TableCell} instances per column by assigning
* the appropriate function to the TableColumn
* {@link TableColumn#cellFactoryProperty() cell factory} property.
*
*
See the {@link Cell} class documentation for a more complete
* description of how to write custom Cells.
*
* @see TableColumn
* @see TablePosition
* @param The type of the objects contained within the TableView items list.
* @since JavaFX 2.0
*/
@DefaultProperty("items")
public class TableView extends Control {
/***************************************************************************
* *
* Static properties and methods *
* *
**************************************************************************/
// strings used to communicate via the TableView properties map between
// the control and the skin. Because they are private here, the strings
// are also duplicated in the TableViewSkin class - so any changes to these
// strings must also be duplicated there
static final String SET_CONTENT_WIDTH = "TableView.contentWidth";
/**
*
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(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(ResizeFeatures prop) {
TableView 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 TableView
* will use if no other policy is specified. The sort policy is a simple
* {@link Callback} that accepts a TableView 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.
* @since JavaFX 8.0
*/
public static final Callback DEFAULT_SORT_POLICY = new Callback() {
@Override public Boolean call(TableView table) {
try {
FXCollections.sort(table.getItems(), table.getComparator());
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;
}
}
};
/***************************************************************************
* *
* Constructors *
* *
**************************************************************************/
/**
* Creates a default TableView control with no content.
*
* Refer to the {@link TableView} class documentation for details on the
* default state of other properties.
*/
public TableView() {
this(FXCollections.observableArrayList());
}
/**
* Creates a TableView with the content provided in the items ObservableList.
* This also sets up an observer such that any changes to the items list
* will be immediately reflected in the TableView itself.
*
*
Refer to the {@link TableView} class documentation for details on the
* default state of other properties.
*
* @param items The items to insert into the TableView, and the list to watch
* for changes (to automatically show in the TableView).
*/
public TableView(ObservableList items) {
getStyleClass().setAll(DEFAULT_STYLE_CLASS);
// we quite happily accept items to be null here
setItems(items);
// install default selection and focus models
// it's unlikely this will be changed by many users.
setSelectionModel(new TableViewArrayListSelectionModel(this));
setFocusModel(new TableViewFocusModel(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(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
// TreeViewSkin.
getProperties().addListener(new MapChangeListener() {
@Override
public void onChanged(Change c) {
if (c.wasAdded() && SET_CONTENT_WIDTH.equals(c.getKey())) {
if (c.getValueAdded() instanceof Number) {
setContentWidth((Double) c.getValueAdded());
}
getProperties().remove(SET_CONTENT_WIDTH);
}
}
});
isInited = true;
}
/***************************************************************************
* *
* Instance Variables *
* *
**************************************************************************/
// 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 TableColumns
// in this observableArrayList. Each TableColumn 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 *
* *
**************************************************************************/
private final ListChangeListener> columnsObserver = new ListChangeListener>() {
@Override public void onChanged(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 (TableColumn tc : removed) {
tc.setTableView(null);
}
}
if (c.wasAdded()) {
toRemove.removeAll(added);
for (TableColumn tc : added) {
tc.setTableView(TableView.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) {
TableColumn col = (TableColumn) ((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) {
TableColumn col = (TableColumn) ((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) {
TableColumn col = (TableColumn) ((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) {
final boolean isCellSelection = ((BooleanProperty)o).get();
pseudoClassStateChanged(PSEUDO_CLASS_CELL_SELECTION, isCellSelection);
pseudoClassStateChanged(PSEUDO_CLASS_ROW_SELECTION, !isCellSelection);
}
};
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 *
* *
**************************************************************************/
// --- Items
/**
* The underlying data model for the TableView. Note that it has a generic
* type that must match the type of the TableView itself.
*/
public final ObjectProperty> itemsProperty() { return items; }
private ObjectProperty> items =
new SimpleObjectProperty>(this, "items") {
WeakReference> oldItemsRef;
@Override protected void invalidated() {
ObservableList oldItems = oldItemsRef == null ? null : oldItemsRef.get();
// FIXME temporary fix for RT-15793. This will need to be
// properly fixed when time permits
if (getSelectionModel() instanceof TableViewArrayListSelectionModel) {
((TableViewArrayListSelectionModel)getSelectionModel()).updateItemsObserver(oldItems, getItems());
}
if (getFocusModel() != null) {
((TableViewFocusModel)getFocusModel()).updateItemsObserver(oldItems, getItems());
}
if (getSkin() instanceof TableViewSkin) {
TableViewSkin skin = (TableViewSkin) getSkin();
skin.updateTableItems(oldItems, getItems());
}
oldItemsRef = new WeakReference>(getItems());
}
};
public final void setItems(ObservableList value) { itemsProperty().set(value); }
public final ObservableList getItems() {return items.get(); }
// --- 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 TableColumn in this table. This menu allows for the user to
* show and hide all TableColumns 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 ResizeFeatures(TableView.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, TableRow>> rowFactory;
/**
* A function which produces a TableRow. The system is responsible for
* reusing TableRows. Return from this function a TableRow which
* might be usable for representing a single row in a TableView.
*
* Note that a TableRow is not a TableCell. A TableRow 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
* TableRows. The primary use case for creating custom TableRow
* 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 TableColumn class.
*/
public final ObjectProperty, TableRow>> rowFactoryProperty() {
if (rowFactory == null) {
rowFactory = new SimpleObjectProperty, TableRow>>(this, "rowFactory");
}
return rowFactory;
}
public final void setRowFactory(Callback, TableRow> value) {
rowFactoryProperty().set(value);
}
public final Callback, TableRow> 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();
}
// --- Selection Model
private ObjectProperty> selectionModel
= new SimpleObjectProperty>(this, "selectionModel") {
TableViewSelectionModel oldValue = null;
@Override protected void invalidated() {
if (oldValue != null) {
oldValue.cellSelectionEnabledProperty().removeListener(weakCellSelectionModelInvalidationListener);
}
oldValue = get();
if (oldValue != null) {
oldValue.cellSelectionEnabledProperty().addListener(weakCellSelectionModelInvalidationListener);
// fake an invalidation to ensure updated pseudo-class state
weakCellSelectionModelInvalidationListener.invalidated(oldValue.cellSelectionEnabledProperty());
}
}
};
/**
* The SelectionModel provides the API through which it is possible
* to select single or multiple items within a TableView, as well as inspect
* which items have been selected by the user. Note that it has a generic
* type that must match the type of the TableView itself.
*/
public final ObjectProperty> selectionModelProperty() {
return selectionModel;
}
public final void setSelectionModel(TableViewSelectionModel value) {
selectionModelProperty().set(value);
}
public final TableViewSelectionModel getSelectionModel() {
return selectionModel.get();
}
// --- Focus Model
private ObjectProperty> focusModel;
public final void setFocusModel(TableViewFocusModel value) {
focusModelProperty().set(value);
}
public final TableViewFocusModel getFocusModel() {
return focusModel == null ? null : focusModel.get();
}
/**
* Represents the currently-installed {@link TableViewFocusModel} for this
* TableView. Under almost all circumstances leaving this as the default
* focus model will suffice.
*/
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();
// }
// --- 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 TableView is editable - only if the TableView, the
* TableColumn (if applicable) and the TableCells within it are both
* editable will a TableCell be able to go into their editing state.
*/
public final BooleanProperty editableProperty() {
if (editable == null) {
editable = new SimpleBooleanProperty(this, "editable", false);
}
return editable;
}
// --- 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.
* @since JavaFX 8.0
*/
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.
* @since JavaFX 8.0
*/
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.
* @since JavaFX 8.0
*/
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 TableView.this;
}
@Override public String getName() {
return "fixedCellSize";
}
};
}
return fixedCellSize;
}
// --- Editing Cell
private ReadOnlyObjectWrapper> editingCell;
private void setEditingCell(TablePosition value) {
editingCellPropertyImpl().set(value);
}
public final TablePosition 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;
}
// --- 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.
* @since JavaFX 8.0
*/
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 TableView should be performed.
* For example, a basic sort policy may just call
* {@code FXCollections.sort(tableView.getItems())}, whereas a more advanced
* sort policy may call to a database to perform the necessary sorting on the
* server-side.
*
* TableView ships with a {@link TableView#DEFAULT_SORT_POLICY default
* sort policy} that does precisely as mentioned above: it simply attempts
* to sort the items list in-place.
*
*
It is recommended that rather than override the {@link TableView#sort() sort}
* method that a different sort policy be provided instead.
* @since JavaFX 8.0
*/
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.
* @since JavaFX 8.0
*/
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 TableView.this;
}
@Override public String getName() {
return "onSort";
}
};
}
return onSort;
}
/***************************************************************************
* *
* Public API *
* *
**************************************************************************/
/**
* The TableColumns 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
* TableColumn in this ObservableList.
*/
public final ObservableList> getColumns() {
return columns;
}
/**
* The sortOrder list defines the order in which {@link TableColumn} instances
* are sorted. An empty sortOrder list means that no sorting is being applied
* on the TableView. If the sortOrder list has one TableColumn within it,
* the TableView will be sorted using the
* {@link TableColumn#sortTypeProperty() sortType} and
* {@link TableColumn#comparatorProperty() comparator} properties of this
* TableColumn (assuming
* {@link TableColumn#sortableProperty() TableColumn.sortable} is true).
* If the sortOrder list contains multiple TableColumn instances, then
* the TableView is firstly sorted based on the properties of the first
* TableColumn. If two elements are considered equal, then the second
* TableColumn in the list is used to determine ordering. This repeats until
* the results from all TableColumn comparators are considered, if necessary.
*
* @return An ObservableList containing zero or more TableColumn instances.
*/
public final ObservableList> getSortOrder() {
return sortOrder;
}
/**
* Scrolls the TableView so that the given index is visible within the viewport.
* @param index The index of an item that should be visible to the user.
*/
public void scrollTo(int index) {
ControlUtils.scrollToIndex(this, index);
}
/**
* Scrolls the TableView so that the given object is visible within the viewport.
* @param object The object that should be visible to the user.
* @since JavaFX 8.0
*/
public void scrollTo(S object) {
if( getItems() != null ) {
int idx = getItems().indexOf(object);
if( idx >= 0 ) {
ControlUtils.scrollToIndex(this, idx);
}
}
}
/**
* Called when there's a request to scroll an index into view using {@link #scrollTo(int)}
* or {@link #scrollTo(Object)}
* @since JavaFX 8.0
*/
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 TableView.this;
}
@Override
public String getName() {
return "onScrollTo";
}
};
}
return onScrollTo;
}
/**
* Scrolls the TableView so that the given column is visible within the viewport.
* @param column The column that should be visible to the user.
* @since JavaFX 8.0
*/
public void scrollToColumn(TableColumn column) {
ControlUtils.scrollToColumn(this, column);
}
/**
* Scrolls the TableView so that the given index is visible within the viewport.
* @param columnIndex The index of a column that should be visible to the user.
* @since JavaFX 8.0
*/
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)}
* @since JavaFX 8.0
*/
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 TableView.this;
}
@Override public String getName() {
return "onScrollToColumn";
}
};
}
return onScrollToColumn;
}
/**
* Applies the currently installed resize policy against the given column,
* resizing it based on the delta value provided.
*/
public boolean resizeColumn(TableColumn column, double delta) {
if (column == null || Double.compare(delta, 0.0) == 0) return false;
boolean allowed = getColumnResizePolicy().call(new ResizeFeatures(TableView.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, TableColumn column) {
if (!isEditable() || (column != null && ! column.isEditable())) return;
setEditingCell(new TablePosition(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(TableColumn column) {
return visibleLeafColumns.indexOf(column);
}
/**
* Returns the TableColumn in the given column index, relative to all other
* visible leaf columns.
*/
public TableColumn getVisibleLeafColumn(int column) {
if (column < 0 || column >= visibleLeafColumns.size()) return null;
return visibleLeafColumns.get(column);
}
/** {@inheritDoc} */
@Override protected Skin createDefaultSkin() {
return new TableViewSkin(this);
}
/**
* The sort method forces the TableView 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.
* @since JavaFX 8.0
*/
public void sort() {
final ObservableList sortOrder = getSortOrder();
// update the Comparator property
final Comparator oldComparator = getComparator();
Comparator newComparator = new TableColumnComparator(sortOrder);
setComparator(newComparator);
// if (sortOrder.isEmpty()) {
// // TODO this should eventually handle returning a SortedList back
// // to its unsorted state
// setComparator(null);
// }
// fire the onSort event and check if it is consumed, if
// so, don't run the sort
SortEvent> sortEvent = new SortEvent>(TableView.this, TableView.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;
}
/**
* 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 TableColumn, which indicates to the resize policy function
// that it shouldn't actually do anything specific to one column.
getColumnResizePolicy().call(new ResizeFeatures(TableView.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 TableColumn, which indicates to the resize policy function
// that it shouldn't actually do anything specific to one column.
getColumnResizePolicy().call(new ResizeFeatures(TableView.this, null, 0.0));
refresh();
}
private void buildVisibleLeafColumns(List> cols, List> vlc) {
for (TableColumn 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 = "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(TableView node) {
return node.getFixedCellSize();
}
@Override public boolean isSettable(TableView n) {
return n.fixedCellSize == null || !n.fixedCellSize.isBound();
}
@Override public StyleableProperty getStyleableProperty(TableView 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.
* @since JavaFX 8.0
*/
public static List> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
/**
* {@inheritDoc}
* @since JavaFX 8.0
*/
@Override
public List> getControlCssMetaData() {
return getClassCssMetaData();
}
/***************************************************************************
* *
* Support Interfaces *
* *
**************************************************************************/
/**
* An immutable wrapper class for use in the TableView
* {@link TableView#columnResizePolicyProperty() column resize} functionality.
* @since JavaFX 2.0
*/
public static class ResizeFeatures extends ResizeFeaturesBase {
private TableView table;
/**
* Creates an instance of this class, with the provided TableView,
* TableColumn and delta values being set and stored in this immutable
* instance.
*
* @param table The TableView 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
* TableView resize operation.
* @param delta The amount of horizontal space added or removed in the
* resize operation.
*/
public ResizeFeatures(TableView table, TableColumn column, Double delta) {
super(column, delta);
this.table = table;
}
/**
* Returns the column upon which the resize is occurring, or null
* if this ResizeFeatures instance was created as a result of a
* TableView resize operation.
*/
@Override public TableColumn getColumn() {
return (TableColumn) super.getColumn();
}
/**
* Returns the TableView upon which the resize operation is occurring.
*/
public TableView getTable() {
return table;
}
}
/***************************************************************************
* *
* Support Classes *
* *
**************************************************************************/
/**
* A simple extension of the {@link SelectionModel} abstract class to
* allow for special support for TableView controls.
* @since JavaFX 2.0
*/
public static abstract class TableViewSelectionModel extends TableSelectionModel> {
/***********************************************************************
* *
* Private fields *
* *
**********************************************************************/
private final TableView tableView;
/***********************************************************************
* *
* 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 TableViewSelectionModel(final TableView tableView) {
if (tableView == null) {
throw new NullPointerException("TableView can not be null");
}
this.tableView = tableView;
}
/***********************************************************************
* *
* 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 TableView getTableView() {
return tableView;
}
/**
* Convenience method that returns getTableView().getItems().
* @return The items list of the current TableView.
*/
protected ObservableList getTableModel() {
return tableView.getItems();
}
/** {@inheritDoc} */
@Override protected S getModelItem(int index) {
if (index < 0 || index > getItemCount()) return null;
return tableView.getItems().get(index);
}
/** {@inheritDoc} */
@Override protected int getItemCount() {
return getTableModel().size();
}
/** {@inheritDoc} */
@Override public void focus(int row) {
focus(row, null);
}
/** {@inheritDoc} */
@Override public int getFocusedIndex() {
return getFocusedCell().getRow();
}
/***********************************************************************
* *
* Private implementation *
* *
**********************************************************************/
void focus(int row, TableColumn column) {
focus(new TablePosition(getTableView(), row, column));
}
void focus(TablePosition pos) {
if (getTableView().getFocusModel() == null) return;
getTableView().getFocusModel().focus(pos.getRow(), pos.getTableColumn());
}
TablePosition getFocusedCell() {
if (getTableView().getFocusModel() == null) {
return new TablePosition(getTableView(), -1, null);
}
return getTableView().getFocusModel().getFocusedCell();
}
}
/**
* A primitive selection model implementation, using a List to store all
* selected indices.
*/
// package for testing
static class TableViewArrayListSelectionModel extends TableViewSelectionModel {
private int itemCount = 0;
/***********************************************************************
* *
* Constructors *
* *
**********************************************************************/
public TableViewArrayListSelectionModel(final TableView tableView) {
super(tableView);
this.tableView = tableView;
// this.selectedIndicesBitSet = new BitSet();
updateItemCount();
cellSelectionEnabledProperty().addListener(new InvalidationListener() {
@Override public void invalidated(Observable o) {
isCellSelectionEnabled();
clearSelection();
}
});
final MappingChange.Map,S> cellToItemsMap = new MappingChange.Map, S>() {
@Override public S map(TablePosition f) {
return getModelItem(f.getRow());
}
};
final MappingChange.Map,Integer> cellToIndicesMap = new MappingChange.Map, Integer>() {
@Override public Integer map(TablePosition f) {
return f.getRow();
}
};
selectedCells = FXCollections.>observableArrayList();
selectedCells.addListener(new ListChangeListener>() {
@Override public void onChanged(final 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 TablePosition 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 TablePosition 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 TablePosition 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, S>(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, TablePosition>(c, MappingChange.NOOP_MAP, selectedCellsSeq));
c.reset();
}
});
selectedItems = new ReadOnlyUnbackedObservableList() {
@Override public S get(int i) {
return getModelItem(getSelectedIndices().get(i));
}
@Override public int size() {
return getSelectedIndices().size();
}
};
selectedCellsSeq = new ReadOnlyUnbackedObservableList>() {
@Override public TablePosition get(int i) {
return selectedCells.get(i);
}
@Override public int size() {
return selectedCells.size();
}
};
/*
* The following two listeners are used in conjunction with
* SelectionModel.select(T obj) to allow for a developer to select
* an item that is not actually in the data model. When this occurs,
* we actively try to find an index that matches this object, going
* so far as to actually watch for all changes to the items list,
* rechecking each time.
*/
// watching for changes to the items list
tableView.itemsProperty().addListener(weakItemsPropertyListener);
// watching for changes to the items list content
ObservableList items = getTableModel();
if (items != null) {
items.addListener(weakItemsContentListener);
}
}
private final TableView tableView;
private ChangeListener> itemsPropertyListener = new ChangeListener>() {
@Override
public void changed(ObservableValue> observable,
ObservableList oldList, ObservableList newList) {
updateItemsObserver(oldList, newList);
}
};
private WeakChangeListener> weakItemsPropertyListener =
new WeakChangeListener>(itemsPropertyListener);
final ListChangeListener itemsContentListener = new ListChangeListener() {
@Override public void onChanged(Change c) {
updateItemCount();
List items = getTableModel();
while (c.next()) {
final S selectedItem = getSelectedItem();
final int selectedIndex = getSelectedIndex();
if (items == null || items.isEmpty()) {
clearSelection();
} else if (getSelectedIndex() == -1 && getSelectedItem() != null) {
int newIndex = items.indexOf(getSelectedItem());
if (newIndex != -1) {
setSelectedIndex(newIndex);
}
} else if (c.wasRemoved() &&
c.getRemovedSize() == 1 &&
! c.wasAdded() &&
selectedItem != null &&
selectedItem.equals(c.getRemoved().get(0))) {
// Bug fix for RT-28637
if (getSelectedIndex() < getItemCount()) {
S newSelectedItem = getModelItem(selectedIndex);
if (! selectedItem.equals(newSelectedItem)) {
setSelectedItem(newSelectedItem);
}
}
}
}
updateSelection(c);
}
};
final WeakListChangeListener weakItemsContentListener
= new WeakListChangeListener(itemsContentListener);
private void updateItemsObserver(ObservableList oldList, ObservableList newList) {
// the listview items list has changed, we need to observe
// the new list, and remove any observer we had from the old list
if (oldList != null) {
oldList.removeListener(weakItemsContentListener);
}
if (newList != null) {
newList.addListener(weakItemsContentListener);
}
updateItemCount();
// when the items list totally changes, we should clear out
// the selection
setSelectedIndex(-1);
}
/***********************************************************************
* *
* Observable properties (and getters/setters) *
* *
**********************************************************************/
// the only 'proper' internal observableArrayList, selectedItems and selectedIndices
// are both 'read-only and unbacked'.
private final ObservableList> selectedCells;
// used to represent the _row_ backing data for the selectedCells
private final ReadOnlyUnbackedObservableList selectedItems;
@Override public ObservableList getSelectedItems() {
return selectedItems;
}
private final ReadOnlyUnbackedObservableList> selectedCellsSeq;
@Override public ObservableList getSelectedCells() {
return (ObservableList)(Object)selectedCellsSeq;
}
/***********************************************************************
* *
* Internal properties *
* *
**********************************************************************/
private int previousModelSize = 0;
// Listen to changes in the tableview items list, such that when it
// changes we can update the selected indices list to refer to the
// new indices.
private void updateSelection(ListChangeListener.Change c) {
c.reset();
while (c.next()) {
if (c.wasReplaced()) {
if (c.getList().isEmpty()) {
// the entire items list was emptied - clear selection
clearSelection();
} else {
int index = getSelectedIndex();
if (previousModelSize == c.getRemovedSize()) {
// all items were removed from the model
clearSelection();
} else if (index < getItemCount() && index >= 0) {
// Fix for RT-18969: the list had setAll called on it
// Use of makeAtomic is a fix for RT-20945
makeAtomic = true;
clearSelection(index);
makeAtomic = false;
select(index);
} else {
// Fix for RT-22079
clearSelection();
}
}
} else if (c.wasAdded() || c.wasRemoved()) {
int position = c.getFrom();
int shift = c.wasAdded() ? c.getAddedSize() : -c.getRemovedSize();
if (position < 0) return;
if (shift == 0) return;
List> newIndices = new ArrayList>(selectedCells.size());
for (int i = 0; i < selectedCells.size(); i++) {
final TablePosition old = selectedCells.get(i);
final int oldRow = old.getRow();
final int newRow = oldRow < position ? oldRow : oldRow + shift;
// Special case for RT-28637 (See unit test in TableViewTest).
// Essentially the selectedItem was correct, but selectedItems
// was empty.
if (oldRow == 0 && shift == -1) {
newIndices.add(new TablePosition(getTableView(), 0, old.getTableColumn()));
continue;
}
if (newRow < 0) continue;
newIndices.add(new TablePosition(getTableView(), newRow, old.getTableColumn()));
}
quietClearSelection();
// Fix for RT-22079
for (int i = 0; i < newIndices.size(); i++) {
TablePosition tp = newIndices.get(i);
select(tp.getRow(), tp.getTableColumn());
}
} else if (c.wasPermutated()) {
// General approach:
// -- detected a sort has happened
// -- Create a permutation lookup map (1)
// -- dump all the selected indices into a list (2)
// -- clear the selected items / indexes (3)
// -- create a list containing the new indices (4)
// -- for each previously-selected index (5)
// -- if index is in the permutation lookup map
// -- add the new index to the new indices list
// -- Perform batch selection (6)
// (1)
int length = c.getTo() - c.getFrom();
HashMap pMap = new HashMap (length);
for (int i = c.getFrom(); i < c.getTo(); i++) {
pMap.put(i, c.getPermutation(i));
}
// (2)
List> selectedIndices =
new ArrayList>((ObservableList>)(Object)getSelectedCells());
// (3)
clearSelection();
// (4)
List> newIndices = new ArrayList>(getSelectedIndices().size());
// (5)
for (int i = 0; i < selectedIndices.size(); i++) {
TablePosition oldIndex = selectedIndices.get(i);
if (pMap.containsKey(oldIndex.getRow())) {
Integer newIndex = pMap.get(oldIndex.getRow());
newIndices.add(new TablePosition(oldIndex.getTableView(), newIndex, oldIndex.getTableColumn()));
}
}
// (6)
quietClearSelection();
selectedCells.setAll(newIndices);
selectedCellsSeq.callObservers(new NonIterableChange.SimpleAddChange>(0, newIndices.size(), selectedCellsSeq));
}
}
previousModelSize = getItemCount();
}
/***********************************************************************
* *
* Public selection API *
* *
**********************************************************************/
@Override public void clearAndSelect(int row) {
clearAndSelect(row, null);
}
@Override public void clearAndSelect(int row, TableColumn column) {
quietClearSelection();
select(row, column);
}
@Override public void select(int row) {
select(row, null);
}
@Override
public void select(int row, TableColumn column) {
if (row < 0 || row >= getItemCount()) return;
// if I'm in cell selection mode but the column is null, I don't want
// to select the whole row instead...
if (isCellSelectionEnabled() && column == null) return;
//
// // If I am not in cell selection mode (so I want to select rows only),
// // if a column is given, I return
// if (! isCellSelectionEnabled() && column != null) return;
TablePosition pos = new TablePosition(getTableView(), row, column);
if (getSelectionMode() == SelectionMode.SINGLE) {
quietClearSelection();
}
if (! selectedCells.contains(pos)) {
selectedCells.add(pos);
}
updateSelectedIndex(row);
focus(row, column);
}
@Override public void select(S obj) {
if (obj == null && getSelectionMode() == SelectionMode.SINGLE) {
clearSelection();
return;
}
// We have no option but to iterate through the model and select the
// first occurrence of the given object. Once we find the first one, we
// don't proceed to select any others.
S rowObj = null;
for (int i = 0; i < getItemCount(); i++) {
rowObj = getModelItem(i);
if (rowObj == null) continue;
if (rowObj.equals(obj)) {
if (isSelected(i)) {
return;
}
if (getSelectionMode() == SelectionMode.SINGLE) {
quietClearSelection();
}
select(i);
return;
}
}
// if we are here, we did not find the item in the entire data model.
// Even still, we allow for this item to be set to the give object.
// We expect that in concrete subclasses of this class we observe the
// data model such that we check to see if the given item exists in it,
// whilst SelectedIndex == -1 && SelectedItem != null.
setSelectedItem(obj);
}
@Override public void selectIndices(int row, int... rows) {
if (rows == null) {
select(row);
return;
}
/*
* Performance optimisation - if multiple selection is disabled, only
* process the end-most row index.
*/
int rowCount = getItemCount();
if (getSelectionMode() == SelectionMode.SINGLE) {
quietClearSelection();
for (int i = rows.length - 1; i >= 0; i--) {
int index = rows[i];
if (index >= 0 && index < rowCount) {
select(index);
break;
}
}
if (selectedCells.isEmpty()) {
if (row > 0 && row < rowCount) {
select(row);
}
}
} else {
int lastIndex = -1;
Set> positions = new LinkedHashSet>();
if (row >= 0 && row < rowCount) {
TablePosition tp = new TablePosition(getTableView(), row, null);
// refer to the multi-line comment below for the justification for the following
// code.
boolean match = false;
for (int j = 0; j < selectedCells.size(); j++) {
TablePosition selectedCell = selectedCells.get(j);
if (selectedCell.getRow() == row) {
match = true;
break;
}
}
if (! match) {
positions.add(tp);
lastIndex = row;
}
}
outer: for (int i = 0; i < rows.length; i++) {
int index = rows[i];
if (index < 0 || index >= rowCount) continue;
lastIndex = index;
// we need to manually check all selected cells to see whether this index is already
// selected. This is because selectIndices is inherently row-based, but there may
// be a selected cell where the column is non-null. If we were to simply do a
// selectedCells.contains(pos), then we would not find the match and duplicate the
// row selection. This leads to bugs such as RT-29930.
for (int j = 0; j < selectedCells.size(); j++) {
TablePosition selectedCell = selectedCells.get(j);
if (selectedCell.getRow() == index) continue outer;
}
// if we are here then we have successfully gotten through the for-loop above
TablePosition pos = new TablePosition(getTableView(), index, null);
positions.add(pos);
}
selectedCells.addAll(positions);
if (lastIndex != -1) {
select(lastIndex);
}
}
}
@Override public void selectAll() {
if (getSelectionMode() == SelectionMode.SINGLE) return;
quietClearSelection();
if (isCellSelectionEnabled()) {
List> indices = new ArrayList>();
TableColumn column;
TablePosition tp = null;
for (int col = 0; col < getTableView().getVisibleLeafColumns().size(); col++) {
column = getTableView().getVisibleLeafColumns().get(col);
for (int row = 0; row < getItemCount(); row++) {
tp = new TablePosition(getTableView(), row, column);
indices.add(tp);
}
}
selectedCells.setAll(indices);
if (tp != null) {
select(tp.getRow(), tp.getTableColumn());
focus(tp.getRow(), tp.getTableColumn());
}
} else {
List> indices = new ArrayList>();
for (int i = 0; i < getItemCount(); i++) {
indices.add(new TablePosition(getTableView(), i, null));
}
selectedCells.setAll(indices);
int focusedIndex = getFocusedIndex();
if (focusedIndex == -1) {
select(getItemCount() - 1);
focus(indices.get(indices.size() - 1));
} else {
select(focusedIndex);
focus(focusedIndex);
}
}
}
@Override public void clearSelection(int index) {
clearSelection(index, null);
}
@Override
public void clearSelection(int row, TableColumn column) {
TablePosition tp = new TablePosition(getTableView(), row, column);
boolean csMode = isCellSelectionEnabled();
for (TablePosition pos : getSelectedCells()) {
if ((! csMode && pos.getRow() == row) || (csMode && pos.equals(tp))) {
selectedCells.remove(pos);
// give focus to this cell index
focus(row);
return;
}
}
}
@Override public void clearSelection() {
updateSelectedIndex(-1);
focus(-1);
quietClearSelection();
}
private void quietClearSelection() {
selectedCells.clear();
}
@Override public boolean isSelected(int index) {
return isSelected(index, null);
}
@Override
public boolean isSelected(int row, TableColumn column) {
// When in cell selection mode, we currently do NOT support selecting
// entire rows, so a isSelected(row, null)
// should always return false.
if (isCellSelectionEnabled() && (column == null)) return false;
for (TablePosition tp : getSelectedCells()) {
boolean columnMatch = ! isCellSelectionEnabled() ||
(column == null && tp.getTableColumn() == null) ||
(column != null && column.equals(tp.getTableColumn()));
if (tp.getRow() == row && columnMatch) {
return true;
}
}
return false;
}
@Override public boolean isEmpty() {
return selectedCells.isEmpty();
}
@Override public void selectPrevious() {
if (isCellSelectionEnabled()) {
// in cell selection mode, we have to wrap around, going from
// right-to-left, and then wrapping to the end of the previous line
TablePosition pos = getFocusedCell();
if (pos.getColumn() - 1 >= 0) {
// go to previous row
select(pos.getRow(), getTableColumn(pos.getTableColumn(), -1));
} else if (pos.getRow() < getItemCount() - 1) {
// wrap to end of previous row
select(pos.getRow() - 1, getTableColumn(getTableView().getVisibleLeafColumns().size() - 1));
}
} else {
int focusIndex = getFocusedIndex();
if (focusIndex == -1) {
select(getItemCount() - 1);
} else if (focusIndex > 0) {
select(focusIndex - 1);
}
}
}
@Override public void selectNext() {
if (isCellSelectionEnabled()) {
// in cell selection mode, we have to wrap around, going from
// left-to-right, and then wrapping to the start of the next line
TablePosition pos = getFocusedCell();
if (pos.getColumn() + 1 < getTableView().getVisibleLeafColumns().size()) {
// go to next column
select(pos.getRow(), getTableColumn(pos.getTableColumn(), 1));
} else if (pos.getRow() < getItemCount() - 1) {
// wrap to start of next row
select(pos.getRow() + 1, getTableColumn(0));
}
} else {
int focusIndex = getFocusedIndex();
if (focusIndex == -1) {
select(0);
} else if (focusIndex < getItemCount() -1) {
select(focusIndex + 1);
}
}
}
@Override public void selectAboveCell() {
TablePosition pos = getFocusedCell();
if (pos.getRow() == -1) {
select(getItemCount() - 1);
} else if (pos.getRow() > 0) {
select(pos.getRow() - 1, pos.getTableColumn());
}
}
@Override public void selectBelowCell() {
TablePosition pos = getFocusedCell();
if (pos.getRow() == -1) {
select(0);
} else if (pos.getRow() < getItemCount() -1) {
select(pos.getRow() + 1, pos.getTableColumn());
}
}
@Override public void selectFirst() {
TablePosition focusedCell = getFocusedCell();
if (getSelectionMode() == SelectionMode.SINGLE) {
quietClearSelection();
}
if (getItemCount() > 0) {
if (isCellSelectionEnabled()) {
select(0, focusedCell.getTableColumn());
} else {
select(0);
}
}
}
@Override public void selectLast() {
TablePosition focusedCell = getFocusedCell();
if (getSelectionMode() == SelectionMode.SINGLE) {
quietClearSelection();
}
int numItems = getItemCount();
if (numItems > 0 && getSelectedIndex() < numItems - 1) {
if (isCellSelectionEnabled()) {
select(numItems - 1, focusedCell.getTableColumn());
} else {
select(numItems - 1);
}
}
}
@Override
public void selectLeftCell() {
if (! isCellSelectionEnabled()) return;
TablePosition pos = getFocusedCell();
if (pos.getColumn() - 1 >= 0) {
select(pos.getRow(), getTableColumn(pos.getTableColumn(), -1));
}
}
@Override
public void selectRightCell() {
if (! isCellSelectionEnabled()) return;
TablePosition pos = getFocusedCell();
if (pos.getColumn() + 1 < getTableView().getVisibleLeafColumns().size()) {
select(pos.getRow(), getTableColumn(pos.getTableColumn(), 1));
}
}
/***********************************************************************
* *
* Support code *
* *
**********************************************************************/
private TableColumn getTableColumn(int pos) {
return getTableView().getVisibleLeafColumn(pos);
}
// private TableColumn getTableColumn(TableColumn column) {
// return getTableColumn(column, 0);
// }
// Gets a table column to the left or right of the current one, given an offset
private TableColumn getTableColumn(TableColumn column, int offset) {
int columnIndex = getTableView().getVisibleLeafIndex(column);
int newColumnIndex = columnIndex + offset;
return getTableView().getVisibleLeafColumn(newColumnIndex);
}
private void updateSelectedIndex(int row) {
setSelectedIndex(row);
setSelectedItem(getModelItem(row));
}
/** {@inheritDoc} */
@Override protected int getItemCount() {
return itemCount;
}
private void updateItemCount() {
if (tableView == null) {
itemCount = -1;
} else {
List items = getTableModel();
itemCount = items == null ? -1 : items.size();
}
}
}
/**
* A {@link FocusModel} with additional functionality to support the requirements
* of a TableView control.
*
* @see TableView
* @since JavaFX 2.0
*/
public static class TableViewFocusModel extends TableFocusModel> {
private final TableView tableView;
private final TablePosition EMPTY_CELL;
/**
* Creates a default TableViewFocusModel instance that will be used to
* manage focus of the provided TableView control.
*
* @param tableView The tableView upon which this focus model operates.
* @throws NullPointerException The TableView argument can not be null.
*/
public TableViewFocusModel(final TableView tableView) {
if (tableView == null) {
throw new NullPointerException("TableView can not be null");
}
this.tableView = tableView;
this.tableView.itemsProperty().addListener(weakItemsPropertyListener);
if (tableView.getItems() != null) {
this.tableView.getItems().addListener(weakItemsContentListener);
}
TablePosition pos = new TablePosition(tableView, -1, null);
setFocusedCell(pos);
EMPTY_CELL = pos;
}
private ChangeListener> itemsPropertyListener = new ChangeListener>() {
@Override
public void changed(ObservableValue> observable,
ObservableList oldList, ObservableList newList) {
updateItemsObserver(oldList, newList);
}
};
private WeakChangeListener> weakItemsPropertyListener =
new WeakChangeListener>(itemsPropertyListener);
// Listen to changes in the tableview items list, such that when it
// changes we can update the focused index to refer to the new indices.
private final ListChangeListener