javafx.scene.control.TableView Maven / Gradle / Ivy
/*
* Copyright (c) 2011, 2024, 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 java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.function.Function;
import java.util.function.IntPredicate;
import com.sun.javafx.collections.MappingChange;
import com.sun.javafx.collections.NonIterableChange;
import com.sun.javafx.logging.PlatformLogger.Level;
import com.sun.javafx.scene.control.ConstrainedColumnResize;
import com.sun.javafx.scene.control.Logging;
import com.sun.javafx.scene.control.Properties;
import com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList;
import com.sun.javafx.scene.control.SelectedCellsMap;
import com.sun.javafx.scene.control.TableColumnComparatorBase.TableColumnComparator;
import com.sun.javafx.scene.control.behavior.TableCellBehavior;
import com.sun.javafx.scene.control.behavior.TableCellBehaviorBase;
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.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;
import javafx.collections.transformation.SortedList;
import javafx.css.CssMetaData;
import javafx.css.PseudoClass;
import javafx.css.Styleable;
import javafx.css.StyleableDoubleProperty;
import javafx.css.StyleableProperty;
import javafx.css.converter.SizeConverter;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.AccessibleAttribute;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import javafx.scene.control.skin.TableViewSkin;
import javafx.scene.layout.Region;
import javafx.util.Callback;
/**
* The TableView control is designed to visualize an unlimited number of rows
* of data, broken out into columns. A TableView is therefore very similar to the
* {@link ListView} control, with the addition of support for columns. For an
* example on how to create a TableView, refer to the 'Creating a TableView'
* control section below.
*
* The TableView control has a number of features, including:
*
* - Powerful {@link TableColumn} API:
*
* - Support for {@link TableColumn#cellFactoryProperty() cell factories} to
* easily customize {@link Cell cell} contents in both rendering and editing
* states.
*
- Specification of {@link TableColumn#minWidthProperty() minWidth}/
* {@link TableColumn#prefWidthProperty() prefWidth}/
* {@link TableColumn#maxWidthProperty() maxWidth},
* and also {@link TableColumn#resizableProperty() fixed width columns}.
*
- Width resizing by the user at runtime.
*
- Column reordering by the user at runtime.
*
- Built-in support for {@link TableColumn#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 TableView 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 javafx.scene.layout.GridPane} layout instead.
*
* Creating a TableView
*
*
* Creating a TableView is a multi-step process, and also depends on the
* underlying data model needing to be represented. For this example we'll use
* an {@literal ObservableList}, as it is the simplest way of showing data in a
* TableView. The {@code Person} class will consist of a first
* name and last name properties. That is:
*
* {@code public class Person {
* private StringProperty firstName;
* public void setFirstName(String value) { firstNameProperty().set(value); }
* public String getFirstName() { return firstNameProperty().get(); }
* public StringProperty firstNameProperty() {
* if (firstName == null) firstName = new SimpleStringProperty(this, "firstName");
* return firstName;
* }
*
* private StringProperty lastName;
* public void setLastName(String value) { lastNameProperty().set(value); }
* public String getLastName() { return lastNameProperty().get(); }
* public StringProperty lastNameProperty() {
* if (lastName == null) lastName = new SimpleStringProperty(this, "lastName");
* return lastName;
* }
*
* public Person(String firstName, String lastName) {
* setFirstName(firstName);
* setLastName(lastName);
* }
* }}
*
* The data we will use for this example is:
*
*
{@code List members = List.of(
* new Person("William", "Reed"),
* new Person("James", "Michaelson"),
* new Person("Julius", "Dean"));}
*
* Firstly, we need to create a data model. As mentioned,
* for this example, we'll be using an {@literal ObservableList}:
*
* {@code ObservableList teamMembers = FXCollections.observableArrayList(members);}
*
* Then we create a TableView instance:
*
*
{@code TableView table = new TableView<>();
* 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:
*
*
{@code TableView table = new TableView<>(teamMembers);}
*
* 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 TableColumn firstNameCol = new TableColumn<>("First Name");
* firstNameCol.setCellValueFactory(new PropertyValueFactory<>(members.get(0).firstNameProperty().getName())));
* TableColumn lastNameCol = new TableColumn<>("Last Name");
* lastNameCol.setCellValueFactory(new PropertyValueFactory<>(members.get(0).lastNameProperty().getName())));
*
* 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 will result in the
* 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();
* }
* });
*
* // or with a lambda expression:
* firstNameCol.setCellValueFactory(p -> 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.
*
*
Warning: Nodes should not be inserted directly into the TableView cells
* {@code TableView} allows for it's cells to contain elements of any type, including
* {@link Node} instances. Putting nodes into
* the TableView cells is strongly discouraged, as it can
* lead to unexpected results.
*
* Important points to note:
*
* - Avoid inserting {@code Node} instances directly into the {@code TableView} cells or its data model.
* - The recommended approach is to put the relevant information into the items list, and
* provide a custom {@link TableColumn#cellFactoryProperty() cell factory} to create the nodes for a
* given cell and update them on demand using the data stored in the item for that cell.
* - Avoid creating new {@code Node}s in the {@code updateItem} method of a custom {@link TableColumn#cellFactoryProperty() cell factory}.
*
* The following minimal example shows how to create a custom cell factory for {@code TableView} containing {@code Node}s:
*
{@code
* class CustomColor {
* private SimpleObjectProperty color;
*
* CustomColor(Color col) {
* this.color = new SimpleObjectProperty(col);
* }
* public Color getColor() { return color.getValue(); }
* public void setColor(Color c) { color.setValue(c); }
* public SimpleObjectProperty colorProperty() { return color; }
* }
*
* TableView tableview = new TableView();
*
* ObservableList colorList = FXCollections.observableArrayList();
* colorList.addAll(
* new CustomColor(Color.RED),
* new CustomColor(Color.GREEN),
* new CustomColor(Color.BLUE));
*
* TableColumn col = new TableColumn("Color");
* col.setCellValueFactory(data -> data.getValue().colorProperty());
*
* col.setCellFactory(p -> {
* return new TableCell () {
* private final Rectangle rectangle;
* {
* setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
* rectangle = new Rectangle(10, 10);
* }
*
* @Override
* protected void updateItem(Color item, boolean empty) {
* super.updateItem(item, empty);
*
* if (item == null || empty) {
* setGraphic(null);
* } else {
* rectangle.setFill(item);
* setGraphic(rectangle);
* }
* }
* };
* });
*
* tableview.getColumns().add(col);
* tableview.setItems(colorList); }
*
* This example has an anonymous custom {@code TableCell} class in the custom cell factory.
* Note that the {@code Rectangle} ({@code Node}) object needs to be created in the instance initialization block
* or the constructor of the custom {@code TableCell} class and updated/used in its {@code updateItem} method.
*
*
Sorting
* Prior to JavaFX 8.0, the TableView control would treat the
* {@link #getItems() items} list as the view model, meaning that any changes to
* the list would be immediately reflected visually. TableView would also modify
* the order of this list directly when a user initiated a sort. This meant that
* (again, prior to JavaFX 8.0) it was not possible to have the TableView return
* to an unsorted state (after iterating through ascending and descending
* orders).
*
* Starting with JavaFX 8.0 (and the introduction of {@link SortedList}), it
* is now possible to have the collection return to the unsorted state when
* there are no columns as part of the TableView
* {@link #getSortOrder() sort order}. To do this, you must create a SortedList
* instance, and bind its
* {@link javafx.collections.transformation.SortedList#comparatorProperty() comparator}
* property to the TableView {@link #comparatorProperty() comparator} property,
* list so:
*
* {@code // create a SortedList based on the provided ObservableList
* SortedList sortedList = new SortedList(FXCollections.observableArrayList(2, 1, 3));
*
* // create a TableView with the sorted list set as the items it will show
* final TableView tableView = new TableView<>(sortedList);
*
* // bind the sortedList comparator to the TableView comparator
* sortedList.comparatorProperty().bind(tableView.comparatorProperty());
*
* // Don't forget to define columns!}
*
* Editing
* This control supports inline editing of values, and this section attempts to
* give an overview of the available APIs and how you should use them.
*
* Firstly, cell editing most commonly requires a different user interface
* than when a cell is not being edited. This is the responsibility of the
* {@link Cell} implementation being used. For TableView, it is highly
* recommended that editing be
* {@link javafx.scene.control.TableColumn#cellFactoryProperty() per-TableColumn},
* rather than {@link #rowFactoryProperty() per row}, as more often than not
* you want users to edit each column value differently, and this approach allows
* for editors specific to each column. It is your choice whether the cell is
* permanently in an editing state (e.g. this is common for {@link CheckBox} cells),
* or to switch to a different UI when editing begins (e.g. when a double-click
* is received on a cell).
*
* To know when editing has been requested on a cell,
* simply override the {@link javafx.scene.control.Cell#startEdit()} method, and
* update the cell {@link javafx.scene.control.Cell#textProperty() text} and
* {@link javafx.scene.control.Cell#graphicProperty() graphic} properties as
* appropriate (e.g. set the text to null and set the graphic to be a
* {@link TextField}). Additionally, you should also override
* {@link Cell#cancelEdit()} to reset the UI back to its original visual state
* when the editing concludes. In both cases it is important that you also
* ensure that you call the super method to have the cell perform all duties it
* must do to enter or exit its editing mode.
*
* Once your cell is in an editing state, the next thing you are most probably
* interested in is how to commit or cancel the editing that is taking place. This is your
* responsibility as the cell factory provider. Your cell implementation will know
* when the editing is over, based on the user input (e.g. when the user presses
* the Enter or ESC keys on their keyboard). When this happens, it is your
* responsibility to call {@link Cell#commitEdit(Object)} or
* {@link Cell#cancelEdit()}, as appropriate.
*
* When you call {@link Cell#commitEdit(Object)} an event is fired to the
* TableView, which you can observe by adding an {@link EventHandler} via
* {@link TableColumn#setOnEditCommit(javafx.event.EventHandler)}. Similarly,
* you can also observe edit events for
* {@link TableColumn#setOnEditStart(javafx.event.EventHandler) edit start}
* and {@link TableColumn#setOnEditCancel(javafx.event.EventHandler) edit cancel}.
*
* By default the TableColumn edit commit handler is non-null, with a default
* handler that attempts to overwrite the property value for the
* item in the currently-being-edited row. It is able to do this as the
* {@link Cell#commitEdit(Object)} method is passed in the new value, and this
* is passed along to the edit commit handler via the
* {@link javafx.scene.control.TableColumn.CellEditEvent CellEditEvent} that is
* fired. It is simply a matter of calling
* {@link javafx.scene.control.TableColumn.CellEditEvent#getNewValue()} to
* retrieve this value.
*
*
It is very important to note that if you call
* {@link TableColumn#setOnEditCommit(javafx.event.EventHandler)} with your own
* {@link EventHandler}, then you will be removing the default handler. Unless
* you then handle the writeback to the property (or the relevant data source),
* nothing will happen. You can work around this by using the
* {@link TableColumn#addEventHandler(javafx.event.EventType, javafx.event.EventHandler)}
* method to add a {@link TableColumn#editCommitEvent()} {@link EventType} with
* your desired {@link EventHandler} as the second argument. Using this method,
* you will not replace the default implementation, but you will be notified when
* an edit commit has occurred.
*
* Hopefully this summary answers some of the commonly asked questions.
* Fortunately, JavaFX ships with a number of pre-built cell factories that
* handle all the editing requirements on your behalf. You can find these
* pre-built cell factories in the javafx.scene.control.cell package.
*
* @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;
}
};
/**
* A resize policy that adjusts other columns in order to fit the table width.
* During UI adjustment, proportionately resizes all columns to preserve the total width.
*
* When column constraints make it impossible to fit all the columns into the allowed area,
* the columns are either clipped, or an empty space appears. This policy disables the horizontal
* scroll bar.
*
* @since 20
*/
public static final Callback CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS =
ConstrainedColumnResize.forTable(ConstrainedColumnResize.ResizeMode.AUTO_RESIZE_ALL_COLUMNS);
/**
* A resize policy that adjusts the last column in order to fit the table width.
* During UI adjustment, resizes the last column only to preserve the total width.
*
* When column constraints make it impossible to fit all the columns into the allowed area,
* the columns are either clipped, or an empty space appears. This policy disables the horizontal
* scroll bar.
*
* @since 20
*/
public static final Callback CONSTRAINED_RESIZE_POLICY_LAST_COLUMN =
ConstrainedColumnResize.forTable(ConstrainedColumnResize.ResizeMode.AUTO_RESIZE_LAST_COLUMN);
/**
* A resize policy that adjusts the next column in order to fit the table width.
* During UI adjustment, resizes the next column the opposite way.
*
* When column constraints make it impossible to fit all the columns into the allowed area,
* the columns are either clipped, or an empty space appears. This policy disables the horizontal
* scroll bar.
*
* @since 20
*/
public static final Callback CONSTRAINED_RESIZE_POLICY_NEXT_COLUMN =
ConstrainedColumnResize.forTable(ConstrainedColumnResize.ResizeMode.AUTO_RESIZE_NEXT_COLUMN);
/**
* A resize policy that adjusts subsequent columns in order to fit the table width.
* During UI adjustment, proportionally resizes subsequent columns to preserve the total width.
*
* When column constraints make it impossible to fit all the columns into the allowed area,
* the columns are either clipped, or an empty space appears. This policy disables the horizontal
* scroll bar.
*
* @since 20
*/
public static final Callback CONSTRAINED_RESIZE_POLICY_SUBSEQUENT_COLUMNS =
ConstrainedColumnResize.forTable(ConstrainedColumnResize.ResizeMode.AUTO_RESIZE_SUBSEQUENT_COLUMNS);
/**
* A resize policy that adjusts columns, starting with the next one, in order to fit the table width.
* During UI adjustment, resizes the next column to preserve the total width. When the next column
* cannot be further resized due to a constraint, the following column gets resized, and so on.
*
* When column constraints make it impossible to fit all the columns into the allowed area,
* the columns are either clipped, or an empty space appears. This policy disables the horizontal
* scroll bar.
*
* @since 20
*/
public static final Callback CONSTRAINED_RESIZE_POLICY_FLEX_NEXT_COLUMN =
ConstrainedColumnResize.forTable(ConstrainedColumnResize.ResizeMode.AUTO_RESIZE_FLEX_HEAD);
/**
* A resize policy that adjusts columns, starting with the last one, in order to fit the table width.
* During UI adjustment, resizes the last column to preserve the total width. When the last column
* cannot be further resized due to a constraint, the column preceding the last one gets resized, and so on.
*
* When column constraints make it impossible to fit all the columns into the allowed area,
* the columns are either clipped, or an empty space appears. This policy disables the horizontal
* scroll bar.
*
* @since 20
*/
public static final Callback CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN =
ConstrainedColumnResize.forTable(ConstrainedColumnResize.ResizeMode.AUTO_RESIZE_FLEX_TAIL);
/**
* 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.
*
* @deprecated Use {@link #CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN} instead.
*/
@Deprecated(since="20")
public static final Callback CONSTRAINED_RESIZE_POLICY =
CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN;
/**
* 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 {
ObservableList> itemsList = table.getItems();
if (itemsList instanceof SortedList) {
// it is the responsibility of the SortedList to bind to the
// comparator provided by the TableView. However, we don't
// want to fail the sort (which would put the UI in an
// inconsistent state), so we return true here, but only if
// the SortedList has its comparator bound to the TableView
// comparator property.
SortedList sortedList = (SortedList) itemsList;
boolean comparatorsBound = sortedList.comparatorProperty().
isEqualTo(table.comparatorProperty()).get();
if (! comparatorsBound) {
// this isn't a good situation to be in, so lets log it
// out in case the developer is unaware
if (Logging.getControlsLogger().isLoggable(Level.INFO)) {
String s = "TableView items list is a SortedList, but the SortedList " +
"comparator should be bound to the TableView comparator for " +
"sorting to be enabled (e.g. " +
"sortedList.comparatorProperty().bind(tableView.comparatorProperty());).";
Logging.getControlsLogger().info(s);
}
}
return comparatorsBound;
} else {
if (itemsList == null || itemsList.isEmpty()) {
// sorting is not supported on null or empty lists
return true;
}
Comparator comparator = table.getComparator();
if (comparator == null) {
return true;
}
// otherwise we attempt to do a manual sort, and if successful
// we return true
FXCollections.sort(itemsList, comparator);
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);
setAccessibleRole(AccessibleRole.TABLE_VIEW);
// 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((ListChangeListener>) 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 extends Object, ? extends Object> c) {
if (c.wasAdded() && SET_CONTENT_WIDTH.equals(c.getKey())) {
if (c.getValueAdded() instanceof Number) {
setContentWidth((Double) c.getValueAdded());
}
getProperties().remove(SET_CONTENT_WIDTH);
}
}
});
pseudoClassStateChanged(PseudoClass.getPseudoClass(getColumnResizePolicy().toString()), true);
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 extends TableColumn> c) {
final List> columns = getColumns();
// Fix for RT-39822 - don't allow the same column to be installed twice
while (c.next()) {
if (c.wasAdded()) {
List> duplicates = new ArrayList<>();
for (TableColumn addedColumn : c.getAddedSubList()) {
if (addedColumn == null) continue;
int count = 0;
for (TableColumn column : columns) {
if (addedColumn == column) {
count++;
}
}
if (count > 1) {
duplicates.add(addedColumn);
}
}
if (!duplicates.isEmpty()) {
String titleList = "";
for (TableColumn dupe : duplicates) {
titleList += "'" + dupe.getText() + "', ";
}
throw new IllegalStateException("Duplicate TableColumns detected in TableView columns list with titles " + titleList);
}
}
}
c.reset();
// Fix for RT-15194: Need to remove removed columns from the
// sortOrder list.
List> toRemove = new ArrayList<>();
while (c.next()) {
final List extends TableColumn> removed = c.getRemoved();
final List extends TableColumn> 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);
}
// We don't maintain a bind for leafColumns, we simply call this update
// function behind the scenes in the appropriate places.
updateVisibleLeafColumns();
sortOrder.removeAll(toRemove);
// Fix for RT-38892.
final TableViewFocusModel fm = getFocusModel();
final TableViewSelectionModel sm = getSelectionModel();
c.reset();
// we need to collect together all removed and all added columns, because
// the code below works on the actually removed columns. If we perform
// the code within this while loop, we'll be deselecting columns that
// should be deselected (because they have just moved place, for example).
List> removed = new ArrayList<>();
List> added = new ArrayList<>();
while (c.next()) {
if (c.wasRemoved()) {
removed.addAll(c.getRemoved());
}
if (c.wasAdded()) {
added.addAll(c.getAddedSubList());
}
}
removed.removeAll(added);
// Fix for focus - we simply move focus to a cell to the left
// of the focused cell if the focused cell was located within
// a column that has been removed.
if (fm != null) {
TablePosition focusedCell = fm.getFocusedCell();
boolean match = false;
for (TableColumn tc : removed) {
match = focusedCell != null && focusedCell.getTableColumn() == tc;
if (match) {
break;
}
}
if (match) {
int matchingColumnIndex = lastKnownColumnIndex.getOrDefault(focusedCell.getTableColumn(), 0);
int newFocusColumnIndex =
matchingColumnIndex == 0 ? 0 :
Math.min(getVisibleLeafColumns().size() - 1, matchingColumnIndex - 1);
fm.focus(focusedCell.getRow(), getVisibleLeafColumn(newFocusColumnIndex));
}
}
// Fix for selection - we remove selection from all cells that
// were within the removed column.
if (sm != null) {
List selectedCells = new ArrayList<>(sm.getSelectedCells());
for (TablePosition selectedCell : selectedCells) {
boolean match = false;
for (TableColumn tc : removed) {
match = selectedCell != null && selectedCell.getTableColumn() == tc;
if (match) break;
}
if (match) {
// we can't just use the selectedCell.getTableColumn(), as that
// column no longer exists and therefore its index is not correct.
int matchingColumnIndex = lastKnownColumnIndex.getOrDefault(selectedCell.getTableColumn(), -1);
if (matchingColumnIndex == -1) continue;
if (sm instanceof TableViewArrayListSelectionModel) {
// Also, because the table column no longer exists in the columns
// list at this point, we can't just call:
// sm.clearSelection(selectedCell.getRow(), selectedCell.getTableColumn());
// as the tableColumn would map to an index of -1, which means that
// selection will not be cleared. Instead, we have to create
// a new TablePosition with a fixed column index and use that.
TablePosition fixedTablePosition =
new TablePosition<>(TableView.this,
selectedCell.getRow(),
selectedCell.getTableColumn());
fixedTablePosition.fixedColumnIndex = matchingColumnIndex;
((TableViewArrayListSelectionModel)sm).clearSelection(fixedTablePosition);
} else {
sm.clearSelection(selectedCell.getRow(), selectedCell.getTableColumn());
}
}
}
}
// update the lastKnownColumnIndex map
lastKnownColumnIndex.clear();
for (TableColumn tc : getColumns()) {
int index = getVisibleLeafIndex(tc);
if (index > -1) {
lastKnownColumnIndex.put(tc, index);
}
}
}
};
private final WeakHashMap, Integer> lastKnownColumnIndex = new WeakHashMap<>();
private final InvalidationListener columnVisibleObserver = valueModel -> {
updateVisibleLeafColumns();
};
private final InvalidationListener columnSortableObserver = valueModel -> {
Object col = ((Property>)valueModel).getBean();
if (! getSortOrder().contains(col)) return;
doSort(TableUtil.SortEventType.COLUMN_SORTABLE_CHANGE, col);
};
private final InvalidationListener columnSortTypeObserver = valueModel -> {
Object col = ((Property>)valueModel).getBean();
if (! getSortOrder().contains(col)) return;
doSort(TableUtil.SortEventType.COLUMN_SORT_TYPE_CHANGE, col);
};
private final InvalidationListener columnComparatorObserver = valueModel -> {
Object col = ((Property>)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 = 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.
* @return the items property
*/
public final ObjectProperty> itemsProperty() { return items; }
private ObjectProperty> items =
new SimpleObjectProperty<>(this, "items") {
WeakReference> oldItemsRef;
@Override protected void invalidated() {
final ObservableList oldItems = oldItemsRef == null ? null : oldItemsRef.get();
final ObservableList newItems = getItems();
// Fix for RT-36425
if (newItems != null && newItems == oldItems) {
return;
}
// Fix for RT-35763
if (! (newItems instanceof SortedList)) {
getSortOrder().clear();
}
oldItemsRef = new WeakReference<>(newItems);
}
};
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.
* @return the tableMenuButtonVisible property
*/
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();
}
/**
* 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}.
* @return columnResizePolicy property
*/
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));
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.
* @return rowFactory property
*/
public final ObjectProperty, TableRow>> rowFactoryProperty() {
if (rowFactory == null) {
rowFactory = new SimpleObjectProperty<>(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.
* @return placeholder property
*/
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.clearSelection();
oldValue.cellSelectionEnabledProperty().removeListener(weakCellSelectionModelInvalidationListener);
if (oldValue instanceof TableViewArrayListSelectionModel) {
((TableViewArrayListSelectionModel)oldValue).dispose();
}
}
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.
* @return selectionModel property
*/
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.
* @return focusModel property
*/
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.
* @return the editable property
*/
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 a value less than or equal
* to zero (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. A value less than or equal to zero is
* used to represent that fixed cell size mode is disabled, and a value
* greater than zero represents the size of all cells in this control.
*
* @return A double representing the fixed cell size of this control, or a
* value less than or equal to zero 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 less than or equal to zero,
* 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.
*
* @return fixedCellSize property
* @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.
* @return the editingCell property
*/
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<>(
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 final void setOnSort(EventHandler>> value) {
onSortProperty().set(value);
}
public final EventHandler>> getOnSort() {
if( onSort != null ) {
return onSort.get();
}
return null;
}
public final 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.
* @return the columns
*/
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 final void setOnScrollTo(EventHandler> value) {
onScrollToProperty().set(value);
}
public final EventHandler> getOnScrollTo() {
if( onScrollTo != null ) {
return onScrollTo.get();
}
return null;
}
public final 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 final void setOnScrollToColumn(EventHandler>> value) {
onScrollToColumnProperty().set(value);
}
public final EventHandler>> getOnScrollToColumn() {
if( onScrollToColumn != null ) {
return onScrollToColumn.get();
}
return null;
}
public final 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.
* @param column the column
* @param delta the delta
* @return true if column resize is allowed
*/
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;
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.
*
* Note: This method will cancel editing if the given row
* value is less than zero and the given column is null.
* @param row the row
* @param column the column
*/
public void edit(int row, TableColumn column) {
if (!isEditable() || (column != null && ! column.isEditable())) {
return;
}
if (row < 0 && column == null) {
setEditingCell(null);
} else {
setEditingCell(new TablePosition<>(this, row, column));
}
}
/**
* Returns an unmodifiable list containing the currently visible leaf columns.
* @return an unmodifiable list containing the currently visible leaf columns
*/
public ObservableList> getVisibleLeafColumns() {
return unmodifiableVisibleLeafColumns;
}
/**
* Returns the position of the given column, relative to all other
* visible leaf columns.
* @param column the column
* @return 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.
* @param column the column
* @return 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 extends TableColumnBase> sortOrder = getSortOrder();
// update the Comparator property
final Comparator oldComparator = getComparator();
setComparator(sortOrder.isEmpty() ? null : new TableColumnComparator(sortOrder));
// 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;
}
TableViewSelectionModel selectionModel = getSelectionModel();
final List prevState = selectionModel == null ?
null :
new ArrayList<>(selectionModel.getSelectedCells());
// we set makeAtomic to true here, so that we don't fire intermediate
// sort events - instead we send a single permutation event at the end
// of this method.
if (selectionModel != null) {
selectionModel.startAtomic();
}
// get the sort policy and run it
Callback, Boolean> sortPolicy = getSortPolicy();
if (sortPolicy == null) return;
Boolean success = sortPolicy.call(this);
if (selectionModel != null) {
selectionModel.stopAtomic();
}
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;
} else {
// sorting was a success, now we possibly fire an event on the
// selection model that the items list has 'permutated' to a new ordering
// FIXME we should support alternative selection model implementations!
if (selectionModel instanceof TableViewArrayListSelectionModel) {
final TableViewArrayListSelectionModel sm = (TableViewArrayListSelectionModel)selectionModel;
final ObservableList> newState = (ObservableList>)(Object)sm.getSelectedCells();
List> removed = new ArrayList<>();
if (prevState != null) {
for (TablePosition prevItem : prevState) {
if (!newState.contains(prevItem)) {
removed.add(prevItem);
}
}
}
if (!removed.isEmpty()) {
// the sort operation effectively permutates the selectedCells list,
// but we cannot fire a permutation event as we are talking about
// TablePosition's changing (which may reside in the same list
// position before and after the sort). Therefore, we need to fire
// a single add/remove event to cover the added and removed positions.
int itemCount = prevState == null ? 0 : prevState.size();
ListChangeListener.Change> c = new NonIterableChange.GenericAddRemoveChange<>(0, itemCount, removed, newState);
sm.fireCustomSelectedCellsListChangeEvent(c);
}
}
}
}
/**
* Calling {@code refresh()} forces the TableView control to recreate and
* repopulate the cells necessary to populate the visual bounds of the control.
* In other words, this forces the TableView to update what it is showing to
* the user. This is useful in cases where the underlying data source has
* changed in a way that is not observed by the TableView itself.
*
* @since JavaFX 8u60
*/
public void refresh() {
getProperties().put(Properties.RECREATE, Boolean.TRUE);
}
/* *************************************************************************
* *
* 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;
}
// --- 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));
}
}
/**
* 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));
}
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");
private static class StyleableProperties {
private static final CssMetaData,Number> FIXED_CELL_SIZE =
new CssMetaData<>("-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);
}
}
/**
* Gets the {@code CssMetaData} associated with this class, which may include the
* {@code CssMetaData} of its superclasses.
* @return the {@code CssMetaData}
* @since JavaFX 8.0
*/
public static List> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
/**
* {@inheritDoc}
* @since JavaFX 8.0
*/
@Override
public List> getControlCssMetaData() {
return getClassCssMetaData();
}
/* *************************************************************************
* *
* Accessibility handling *
* *
**************************************************************************/
/** {@inheritDoc} */
@Override
public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
switch (attribute) {
case COLUMN_COUNT: return getVisibleLeafColumns().size();
case ROW_COUNT: return getItems() != null ? getItems().size() : 0;
case SELECTED_ITEMS: {
// TableViewSkin returns TableRows back to TableView.
// TableRowSkin returns TableCells back to TableRow.
@SuppressWarnings("unchecked")
ObservableList> rows = (ObservableList>)super.queryAccessibleAttribute(attribute, parameters);
List selection = new ArrayList<>();
if (rows != null) {
for (TableRow row: rows) {
@SuppressWarnings("unchecked")
ObservableList cells =
(ObservableList)row.queryAccessibleAttribute(attribute, parameters);
if (cells != null) {
selection.addAll(cells);
}
}
}
return FXCollections.observableArrayList(selection);
}
case FOCUS_ITEM: {
Node row = (Node)super.queryAccessibleAttribute(attribute, parameters);
if (row == null) return null;
Node cell = (Node)row.queryAccessibleAttribute(attribute, parameters);
/* cell equals to null means the row is a placeholder node */
return cell != null ? cell : row;
}
case CELL_AT_ROW_COLUMN: {
@SuppressWarnings("unchecked")
TableRow row = (TableRow)super.queryAccessibleAttribute(attribute, parameters);
return row != null ? row.queryAccessibleAttribute(attribute, parameters) : null;
}
case MULTIPLE_SELECTION: {
MultipleSelectionModel sm = getSelectionModel();
return sm != null && sm.getSelectionMode() == SelectionMode.MULTIPLE;
}
default: return super.queryAccessibleAttribute(attribute, parameters);
}
}
/* *************************************************************************
* *
* Support Interfaces *
* *
**************************************************************************/
/**
* An immutable wrapper class for use in the TableView
* {@link TableView#columnResizePolicyProperty() column resize} functionality.
*
* @param the type of the objects contained within the TableView items list
* @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.
* @return the TableView
*/
public TableView getTable() {
return table;
}
@Override
public Control getTableControl() {
return table;
}
@Override
public double getContentWidth() {
return table.contentWidth;
}
}
/* *************************************************************************
* *
* Support Classes *
* *
**************************************************************************/
/**
* A simple extension of the {@link SelectionModel} abstract class to
* allow for special support for TableView controls.
*
* @param the type of the item contained within the TableView
* @since JavaFX 2.0
*/
public static abstract class TableViewSelectionModel extends TableSelectionModel {
/* *********************************************************************
* *
* Private fields *
* *
**********************************************************************/
private final TableView tableView;
boolean blockFocusCall = false;
/* *********************************************************************
* *
* 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.
* @return a read-only ObservableList representing the currently
* selected cells in this TableView
*/
public abstract ObservableList getSelectedCells();
/* *********************************************************************
* *
* Generic (type erasure) bridging *
* *
**********************************************************************/
// --- isSelected
/** {@inheritDoc} */
@Override public boolean isSelected(int row, TableColumnBase column) {
return isSelected(row, (TableColumn)column);
}
/**
* Convenience function which tests whether the given row and column index
* is currently selected in this table instance.
* @param row the row
* @param column the column
* @return true if row and column index is currently selected
*/
public abstract boolean isSelected(int row, TableColumn column);
// --- select
/** {@inheritDoc} */
@Override public void select(int row, TableColumnBase column) {
select(row, (TableColumn)column);
}
/**
* Selects the cell at the given row/column intersection.
* @param row the row
* @param column the column
*/
public abstract void select(int row, TableColumn column);
// --- clearAndSelect
/** {@inheritDoc} */
@Override public void clearAndSelect(int row, TableColumnBase column) {
clearAndSelect(row, (TableColumn) column);
}
/**
* Clears all selection, and then selects the cell at the given row/column
* intersection.
* @param row the row
* @param column the column
*/
public abstract void clearAndSelect(int row, TableColumn column);
// --- clearSelection
/** {@inheritDoc} */
@Override public void clearSelection(int row, TableColumnBase column) {
clearSelection(row, (TableColumn) column);
}
/**
* Removes selection from the specified row/column position (in view indexes).
* If this particular cell (or row if the column value is -1) is not selected,
* nothing happens.
* @param row the row
* @param column the column
*/
public abstract void clearSelection(int row, TableColumn column);
/** {@inheritDoc} */
@Override public void selectRange(int minRow, TableColumnBase minColumn,
int maxRow, TableColumnBase maxColumn) {
final int minColumnIndex = tableView.getVisibleLeafIndex((TableColumn)minColumn);
final int maxColumnIndex = tableView.getVisibleLeafIndex((TableColumn)maxColumn);
for (int _row = minRow; _row <= maxRow; _row++) {
for (int _col = minColumnIndex; _col <= maxColumnIndex; _col++) {
select(_row, tableView.getVisibleLeafColumn(_col));
}
}
}
/* *********************************************************************
* *
* Public API *
* *
**********************************************************************/
/**
* Returns the TableView instance that this selection model is installed in.
* @return the TableView
*/
public TableView getTableView() {
return tableView;
}
/**
* Convenience method that returns getTableView().getItems().
* @return The items list of the current TableView.
*/
protected List 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));
getTableView().notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM);
}
void focus(TablePosition pos) {
if (blockFocusCall) return;
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.itemsPropertyListener = new InvalidationListener() {
private WeakReference> weakItemsRef = new WeakReference<>(tableView.getItems());
@Override public void invalidated(Observable observable) {
ObservableList oldItems = weakItemsRef.get();
weakItemsRef = new WeakReference<>(tableView.getItems());
updateItemsObserver(oldItems, tableView.getItems());
}
};
this.tableView.itemsProperty().addListener(itemsPropertyListener);
selectedCellsMap = new SelectedCellsMap<>(c -> fireCustomSelectedCellsListChangeEvent(c)) { // Note: use of method reference causes javac compilation error (see JDK-8297428)
@Override public boolean isCellSelectionEnabled() {
return TableViewArrayListSelectionModel.this.isCellSelectionEnabled();
}
};
selectedCellsSeq = new ReadOnlyUnbackedObservableList<>() {
@Override public TablePosition get(int i) {
return selectedCellsMap.get(i);
}
@Override public int size() {
return selectedCellsMap.size();
}
};
// selectedCellsSeq.addListener((ListChangeListener super TablePosition>) c -> {
// ControlUtils.updateSelectedIndices(this, c);
// });
/*
* The following listener is 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 content
ObservableList items = getTableView().getItems();
if (items != null) {
items.addListener(weakItemsContentListener);
}
updateItemCount();
updateDefaultSelection();
cellSelectionEnabledProperty().addListener(o -> {
updateDefaultSelection();
TableCellBehaviorBase.setAnchor(tableView, getFocusedCell(), true);
});
}
private void dispose() {
this.tableView.itemsProperty().removeListener(itemsPropertyListener);
ObservableList items = getTableView().getItems();
if (items != null) {
items.removeListener(weakItemsContentListener);
}
}
private final TableView tableView;
final InvalidationListener itemsPropertyListener;
final ListChangeListener itemsContentListener = c -> {
updateItemCount();
List items1 = getTableModel();
boolean doSelectionUpdate = true;
while (c.next()) {
if (c.wasReplaced() || c.getAddedSize() == getItemCount()) {
this.selectedItemChange = c;
updateDefaultSelection();
this.selectedItemChange = null;
return;
}
final S selectedItem = getSelectedItem();
final int selectedIndex = getSelectedIndex();
if (items1 == null || items1.isEmpty()) {
clearSelection();
} else if (getSelectedIndex() == -1 && getSelectedItem() != null) {
int newIndex = items1.indexOf(getSelectedItem());
if (newIndex != -1) {
setSelectedIndex(newIndex);
doSelectionUpdate = false;
}
} 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()) {
final int previousRow = selectedIndex == 0 ? 0 : selectedIndex - 1;
S newSelectedItem = getModelItem(previousRow);
if (! selectedItem.equals(newSelectedItem)) {
clearAndSelect(previousRow);
}
}
}
}
if (doSelectionUpdate) {
updateSelection(c);
}
};
final WeakListChangeListener weakItemsContentListener
= new WeakListChangeListener<>(itemsContentListener);
/* *********************************************************************
* *
* Observable properties (and getters/setters) *
* *
**********************************************************************/
// the only 'proper' internal data structure, selectedItems and selectedIndices
// are both 'read-only and unbacked'.
private final SelectedCellsMap> selectedCellsMap;
// we create a ReadOnlyUnbackedObservableList of selectedCells here so
// that we can fire custom list change events.
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 extends S> c) {
c.reset();
int shift = 0;
int startRow = -1;
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
startAtomic();
clearSelection(index);
stopAtomic();
select(index);
} else {
// Fix for RT-22079
clearSelection();
}
}
} else if (c.wasAdded() || c.wasRemoved()) {
startRow = c.getFrom();
shift += c.wasAdded() ? c.getAddedSize() : -c.getRemovedSize();
} 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)
// -- create a list containing the new indices (3)
// -- for each previously-selected index (4)
// -- if index is in the permutation lookup map
// -- add the new index to the new indices list
// -- Perform batch selection (5)
startAtomic();
final int oldSelectedIndex = getSelectedIndex();
// (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)
List> newIndices = new ArrayList<>(selectedIndices.size());
// (4)
boolean selectionIndicesChanged = false;
for (int i = 0; i < selectedIndices.size(); i++) {
final TablePosition oldIndex = selectedIndices.get(i);
final int oldRow = oldIndex.getRow();
if (pMap.containsKey(oldRow)) {
int newIndex = pMap.get(oldRow);
selectionIndicesChanged = selectionIndicesChanged || newIndex != oldRow;
newIndices.add(new TablePosition<>(oldIndex.getTableView(), newIndex, oldIndex.getTableColumn()));
}
}
if (selectionIndicesChanged) {
// (5)
quietClearSelection();
stopAtomic();
selectedCellsMap.setAll(newIndices);
if (oldSelectedIndex >= 0 && oldSelectedIndex < itemCount) {
int newIndex = c.getPermutation(oldSelectedIndex);
setSelectedIndex(newIndex);
focus(newIndex);
}
} else {
stopAtomic();
}
}
}
TablePosition anchor = TableCellBehavior.getAnchor(tableView, null);
if (shift != 0 && startRow >= 0 && anchor != null && anchor.getRow() >= startRow && (c.wasRemoved() || c.wasAdded())) {
if (isSelected(anchor.getRow(), anchor.getTableColumn())) {
TablePosition newAnchor = new TablePosition<>(tableView, anchor.getRow() + shift, anchor.getTableColumn());
TableCellBehavior.setAnchor(tableView, newAnchor, false);
}
}
shiftSelection(startRow, shift, new Callback<>() {
@Override public Void call(ShiftParams param) {
// we make the shifts atomic, as otherwise listeners to
// the items / indices lists get a lot of intermediate
// noise. They eventually get the summary event fired
// from within shiftSelection, so this is ok.
startAtomic();
final int clearIndex = param.getClearIndex();
final int setIndex = param.getSetIndex();
TablePosition oldTP = null;
if (clearIndex > -1) {
for (int i = 0; i < selectedCellsMap.size(); i++) {
TablePosition tp = selectedCellsMap.get(i);
if (tp.getRow() == clearIndex) {
oldTP = tp;
selectedCellsMap.remove(tp);
} else if (tp.getRow() == setIndex && !param.isSelected()) {
selectedCellsMap.remove(tp);
}
}
}
if (oldTP != null && param.isSelected()) {
TablePosition newTP = new TablePosition<>(
tableView, param.getSetIndex(), oldTP.getTableColumn());
selectedCellsMap.add(newTP);
}
stopAtomic();
return null;
}
});
previousModelSize = getItemCount();
}
/* *********************************************************************
* *
* Public selection API *
* *
**********************************************************************/
@Override public void clearAndSelect(int row) {
clearAndSelect(row, null);
}
@Override public void clearAndSelect(int row, TableColumn column) {
if (row < 0 || row >= getItemCount()) return;
final TablePosition newTablePosition = new TablePosition<>(getTableView(), row, column);
final boolean isCellSelectionEnabled = isCellSelectionEnabled();
// replace the anchor
TableCellBehavior.setAnchor(tableView, newTablePosition, false);
// firstly we make a copy of the selection, so that we can send out
// the correct details in the selection change event.
List> previousSelection = new ArrayList<>(selectedCellsMap.getSelectedCells());
// secondly we check if we can short-circuit out of here because the new selection
// equals the current selection
final boolean wasSelected = isSelected(row, column);
if (wasSelected && previousSelection.size() == 1) {
// before we return, we double-check that the selected item
// is equal to the item in the given index
TablePosition selectedCell = getSelectedCells().get(0);
if (getSelectedItem() == getModelItem(row)) {
if (selectedCell.getRow() == row && selectedCell.getTableColumn() == column) {
return;
}
}
}
// RT-32411 We used to call quietClearSelection() here, but this
// resulted in the selectedItems and selectedIndices lists never
// reporting that they were empty.
// makeAtomic toggle added to resolve RT-32618
startAtomic();
// then clear the current selection
clearSelection();
// and select the new cell
select(row, column);
stopAtomic();
// We remove the new selection from the list seeing as it is not removed.
if (isCellSelectionEnabled) {
previousSelection.remove(newTablePosition);
} else {
for (TablePosition tp : previousSelection) {
if (tp.getRow() == row) {
previousSelection.remove(tp);
break;
}
}
}
// fire off a single add/remove/replace notification (rather than
// individual remove and add notifications) - see RT-33324
ListChangeListener.Change> change;
/*
* getFrom() documentation:
* If wasAdded is true, the interval contains all the values that were added.
* If wasPermutated is true, the interval marks the values that were permutated.
* If wasRemoved is true and wasAdded is false, getFrom() and getTo() should
* return the same number - the place where the removed elements were positioned in the list.
*/
if (wasSelected) {
change = ControlUtils.buildClearAndSelectChange(
selectedCellsSeq, previousSelection, newTablePosition, Comparator.comparing(TablePosition::getRow));
} else {
final int changeIndex = isCellSelectionEnabled ? 0 : Math.max(0, selectedCellsSeq.indexOf(newTablePosition));
final int changeSize = isCellSelectionEnabled ? getSelectedCells().size() : 1;
change = new NonIterableChange.GenericAddRemoveChange<>(
changeIndex, changeIndex + changeSize, previousSelection, selectedCellsSeq);
// selectedCellsSeq._beginChange();
// selectedCellsSeq._nextAdd(changeIndex, changeIndex + changeSize);
// selectedCellsSeq._nextRemove(changeIndex, previousSelection);
// selectedCellsSeq._endChange();
}
fireCustomSelectedCellsListChangeEvent(change);
}
@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, select each
// of the contained cells individually
if (isCellSelectionEnabled() && column == null) {
List> columns = getTableView().getVisibleLeafColumns();
for (int i = 0; i < columns.size(); i++) {
select(row, columns.get(i));
}
return;
}
if (TableCellBehavior.hasDefaultAnchor(tableView)) {
TableCellBehavior.removeAnchor(tableView);
}
if (getSelectionMode() == SelectionMode.SINGLE) {
quietClearSelection();
}
selectedCellsMap.add(new TablePosition<>(getTableView(), row, column));
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.
setSelectedIndex(-1);
setSelectedItem(obj);
}
@Override public void selectIndices(int row, int... rows) {
if (rows == null || rows.length == 0) {
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 (selectedCellsMap.isEmpty()) {
if (row > 0 && row < rowCount) {
select(row);
}
}
} else {
int lastIndex = -1;
Set> positions = new LinkedHashSet<>();
// --- firstly, we special-case the non-varargs 'row' argument
if (row >= 0 && row < rowCount) {
// if I'm in cell selection mode, we want to select each
// of the contained cells individually
if (isCellSelectionEnabled()) {
List> columns = getTableView().getVisibleLeafColumns();
for (int column = 0; column < columns.size(); column++) {
if (! selectedCellsMap.isSelected(row, column)) {
positions.add(new TablePosition<>(getTableView(), row, columns.get(column)));
lastIndex = row;
}
}
} else {
boolean match = selectedCellsMap.isSelected(row, -1);
if (!match) {
positions.add(new TablePosition<>(getTableView(), row, null));
}
}
lastIndex = row;
}
// --- now we iterate through all varargs values
for (int i = 0; i < rows.length; i++) {
int index = rows[i];
if (index < 0 || index >= rowCount) continue;
lastIndex = index;
if (isCellSelectionEnabled()) {
List> columns = getTableView().getVisibleLeafColumns();
for (int column = 0; column < columns.size(); column++) {
if (! selectedCellsMap.isSelected(index, column)) {
positions.add(new TablePosition<>(getTableView(), index, columns.get(column)));
lastIndex = index;
}
}
} else {
if (! selectedCellsMap.isSelected(index, -1)) {
// if we are here then we have successfully gotten through the for-loop above
positions.add(new TablePosition<>(getTableView(), index, null));
}
}
}
selectedCellsMap.addAll(positions);
if (lastIndex != -1) {
select(lastIndex);
}
}
}
@Override public void selectAll() {
if (getSelectionMode() == SelectionMode.SINGLE) return;
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);
}
}
selectedCellsMap.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));
}
selectedCellsMap.setAll(indices);
int focusedIndex = getFocusedIndex();
if (focusedIndex == -1) {
final int itemCount = getItemCount();
if (itemCount > 0) {
select(itemCount - 1);
focus(indices.get(indices.size() - 1));
}
} else {
select(focusedIndex);
focus(focusedIndex);
}
}
}
@Override public void selectRange(int minRow, TableColumnBase minColumn,
int maxRow, TableColumnBase maxColumn) {
if (getSelectionMode() == SelectionMode.SINGLE) {
quietClearSelection();
select(maxRow, maxColumn);
return;
}
startAtomic();
final int itemCount = getItemCount();
final boolean isCellSelectionEnabled = isCellSelectionEnabled();
final int minColumnIndex = tableView.getVisibleLeafIndex((TableColumn)minColumn);
final int maxColumnIndex = tableView.getVisibleLeafIndex((TableColumn)maxColumn);
final int _minColumnIndex = Math.min(minColumnIndex, maxColumnIndex);
final int _maxColumnIndex = Math.max(minColumnIndex, maxColumnIndex);
final int _minRow = Math.min(minRow, maxRow);
final int _maxRow = Math.max(minRow, maxRow);
List> cellsToSelect = new ArrayList<>();
for (int _row = _minRow; _row <= _maxRow; _row++) {
// begin copy/paste of select(int, column) method (with some
// slight modifications)
if (_row < 0 || _row >= itemCount) continue;
if (! isCellSelectionEnabled) {
cellsToSelect.add(new TablePosition<>(tableView, _row, (TableColumn)minColumn));
} else {
for (int _col = _minColumnIndex; _col <= _maxColumnIndex; _col++) {
final TableColumn column = tableView.getVisibleLeafColumn(_col);
// if I'm in cell selection mode but the column is null, I don't want
// to select the whole row instead...
if (column == null && isCellSelectionEnabled) continue;
cellsToSelect.add(new TablePosition<>(tableView, _row, column));
// end copy/paste
}
}
}
// to prevent duplication we remove all currently selected cells from
// our list of cells to select.
cellsToSelect.removeAll(getSelectedCells());
selectedCellsMap.addAll(cellsToSelect);
stopAtomic();
// fire off events.
// Note that focus and selection always goes to maxRow, not _maxRow.
updateSelectedIndex(maxRow);
focus(maxRow, (TableColumn)maxColumn);
final TableColumn startColumn = (TableColumn)minColumn;
final TableColumn endColumn = isCellSelectionEnabled ? (TableColumn)maxColumn : startColumn;
final int startChangeIndex = selectedCellsMap.indexOf(new TablePosition<>(tableView, minRow, startColumn));
final int endChangeIndex = selectedCellsMap.indexOf(new TablePosition<>(tableView, maxRow, endColumn));
if (startChangeIndex > -1 && endChangeIndex > -1) {
final int startIndex = Math.min(startChangeIndex, endChangeIndex);
final int endIndex = Math.max(startChangeIndex, endChangeIndex);
ListChangeListener.Change c = new NonIterableChange.SimpleAddChange<>(startIndex, endIndex + 1, selectedCellsSeq);
fireCustomSelectedCellsListChangeEvent(c);
// selectedCellsSeq.fireChange(() -> selectedCellsSeq._nextAdd(startIndex, endIndex + 1));
}
}
@Override public void clearSelection(int index) {
clearSelection(index, null);
}
@Override
public void clearSelection(int row, TableColumn