javafx.scene.control.TableColumn Maven / Gradle / Ivy
/*
* Copyright (c) 2010, 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 com.sun.javafx.scene.control.Properties;
import javafx.scene.control.skin.NestedTableColumnHeader;
import javafx.scene.control.skin.TableColumnHeader;
import javafx.scene.control.skin.TableHeaderRow;
import javafx.scene.control.skin.TableViewSkin;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventTarget;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.util.Callback;
import javafx.collections.WeakListChangeListener;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WritableValue;
/**
* A {@link TableView} is made up of a number of TableColumn instances. Each
* TableColumn in a table is responsible for displaying (and editing) the contents
* of that column. As well as being responsible for displaying and editing data
* for a single column, a TableColumn also contains the necessary properties to:
*
* - Be resized (using {@link #minWidthProperty() minWidth}/{@link #prefWidthProperty() prefWidth}/{@link #maxWidthProperty() maxWidth}
* and {@link #widthProperty() width} properties)
*
- Have its {@link #visibleProperty() visibility} toggled
*
- Display {@link #textProperty() header text}
*
- Display any {@link #getColumns() nested columns} it may contain
*
- Have a {@link #contextMenuProperty() context menu} when the user
* right-clicks the column header area
*
- Have the contents of the table be sorted (using
* {@link #comparatorProperty() comparator}, {@link #sortableProperty() sortable} and
* {@link #sortTypeProperty() sortType})
*
*
* When creating a TableColumn instance, perhaps the two most important properties
* to set are the column {@link #textProperty() text} (what to show in the column
* header area), and the column {@link #cellValueFactoryProperty() cell value factory}
* (which is used to populate individual cells in the column). This can be
* achieved using some variation on the following code:
*
*
* {@code
* ObservableList data = ...
* TableView tableView = new TableView(data);
*
* TableColumn firstNameCol = new TableColumn("First Name");
* firstNameCol.setCellValueFactory(new Callback, ObservableValue>() {
* public ObservableValue call(CellDataFeatures p) {
* // p.getValue() returns the Person instance for a particular TableView row
* return p.getValue().firstNameProperty();
* }
* });
* }
* tableView.getColumns().add(firstNameCol);}
*
* This approach assumes that the object returned from p.getValue()
* has a JavaFX {@link ObservableValue} that can simply be returned. The benefit of this
* is that the TableView will internally create bindings to ensure that,
* should the returned {@link ObservableValue} change, the cell contents will be
* automatically refreshed.
*
* In situations where a TableColumn must interact with classes created before
* JavaFX, or that generally do not wish to use JavaFX apis for properties, it is
* possible to wrap the returned value in a {@link ReadOnlyObjectWrapper} instance. For
* example:
*
*
* {@code
* firstNameCol.setCellValueFactory(new Callback, ObservableValue>() {
* public ObservableValue call(CellDataFeatures p) {
* return new ReadOnlyObjectWrapper(p.getValue().getFirstName());
* }
* });}
*
* It is hoped that over time there will be convenience cell value factories
* developed and made available to developers. As of the JavaFX 2.0 release,
* there is one such convenience class: {@link PropertyValueFactory}. This class
* removes the need to write the code above, instead relying on reflection to
* look up a given property from a String. Refer to the
* PropertyValueFactory
class documentation for more information
* on how to use this with a TableColumn.
*
* Finally, for more detail on how to use TableColumn, there is further documentation in
* the {@link TableView} class documentation.
*
* @param The type of the TableView generic type (i.e. S == TableView<S>)
* @param The type of the content in all cells in this TableColumn.
* @see TableView
* @see TableCell
* @see TablePosition
* @since JavaFX 2.0
*/
public class TableColumn extends TableColumnBase implements EventTarget {
/* *************************************************************************
* *
* Static properties and methods *
* *
**************************************************************************/
/**
* Parent event for any TableColumn edit event.
* @param The type of the TableView generic type
* @param The type of the content in all cells in this TableColumn
* @return The any TableColumn edit event
*/
@SuppressWarnings("unchecked")
public static EventType> editAnyEvent() {
return (EventType>) EDIT_ANY_EVENT;
}
private static final EventType> EDIT_ANY_EVENT =
new EventType<>(Event.ANY, "TABLE_COLUMN_EDIT");
/**
* Indicates that the user has performed some interaction to start an edit
* event, or alternatively the {@link TableView#edit(int, javafx.scene.control.TableColumn)}
* method has been called.
* @param The type of the TableView generic type
* @param The type of the content in all cells in this TableColumn
* @return The start an edit event
*/
@SuppressWarnings("unchecked")
public static EventType> editStartEvent() {
return (EventType>) EDIT_START_EVENT;
}
private static final EventType> EDIT_START_EVENT =
new EventType<>(editAnyEvent(), "EDIT_START");
/**
* Indicates that the editing has been canceled, meaning that no change should
* be made to the backing data source.
* @param The type of the TableView generic type
* @param The type of the content in all cells in this TableColumn
* @return The cancel an edit event
*/
@SuppressWarnings("unchecked")
public static EventType> editCancelEvent() {
return (EventType>) EDIT_CANCEL_EVENT;
}
private static final EventType> EDIT_CANCEL_EVENT =
new EventType<>(editAnyEvent(), "EDIT_CANCEL");
/**
* Indicates that the editing has been committed by the user, meaning that
* a change should be made to the backing data source to reflect the new
* data.
* @param The type of the TableView generic type
* @param The type of the content in all cells in this TableColumn
* @return The commit an edit event
*/
@SuppressWarnings("unchecked")
public static EventType> editCommitEvent() {
return (EventType>) EDIT_COMMIT_EVENT;
}
private static final EventType> EDIT_COMMIT_EVENT =
new EventType<>(editAnyEvent(), "EDIT_COMMIT");
/**
* If no cellFactory is specified on a TableColumn instance, then this one
* will be used by default. At present it simply renders the TableCell item
* property within the {@link TableCell#graphicProperty() graphic} property
* if the {@link Cell#item item} is a Node, or it simply calls
* toString()
if it is not null, setting the resulting string
* inside the {@link Cell#textProperty() text} property.
*/
public static final Callback, TableCell,?>> DEFAULT_CELL_FACTORY =
new Callback<>() {
@Override public TableCell,?> call(TableColumn,?> param) {
return new TableCell<>() {
@Override protected void updateItem(Object item, boolean empty) {
if (item == getItem()) return;
super.updateItem(item, empty);
if (item == null) {
super.setText(null);
super.setGraphic(null);
} else if (item instanceof Node) {
super.setText(null);
super.setGraphic((Node)item);
} else {
super.setText(item.toString());
super.setGraphic(null);
}
}
};
}
};
/* *************************************************************************
* *
* Constructors *
* *
**************************************************************************/
/**
* Creates a default TableColumn with default cell factory, comparator, and
* onEditCommit implementation.
*/
public TableColumn() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
setOnEditCommit(DEFAULT_EDIT_COMMIT_HANDLER);
// we listen to the columns list here to ensure that widths are
// maintained properly, and to also set the column hierarchy such that
// all children columns know that this TableColumn is their parent.
getColumns().addListener(weakColumnsListener);
tableViewProperty().addListener(observable -> {
// set all children of this tableView to have the same TableView
// as this column
for (TableColumn tc : getColumns()) {
tc.setTableView(getTableView());
}
// This code was commented out due to RT-22391, with this enabled
// the parent column will be null, which is not desired
// // set the parent of this column to also have this tableView
// if (getParentColumn() != null) {
// getParentColumn().setTableView(getTableView());
// }
});
}
/**
* Creates a TableColumn with the text set to the provided string, with
* default cell factory, comparator, and onEditCommit implementation.
* @param text The string to show when the TableColumn is placed within the TableView.
*/
public TableColumn(String text) {
this();
setText(text);
}
/* *************************************************************************
* *
* Listeners *
* *
**************************************************************************/
private EventHandler> DEFAULT_EDIT_COMMIT_HANDLER = t -> {
int index = t.getTablePosition() != null ? t.getTablePosition().getRow() : -1;
List list = t.getTableView() != null ? t.getTableView().getItems() : null;
if (list == null || index < 0 || index >= list.size()) return;
S rowData = list.get(index);
ObservableValue ov = getCellObservableValue(rowData);
if (ov instanceof WritableValue) {
((WritableValue)ov).setValue(t.getNewValue());
}
};
private ListChangeListener> columnsListener = c -> {
while (c.next()) {
// update the TableColumn.tableView property
for (TableColumn tc : c.getRemoved()) {
// Fix for RT-16978. In TableColumnHeader we add before we
// remove when moving a TableColumn. This means that for
// a very brief moment the tc is duplicated, and we can prevent
// nulling out the tableview and parent column. Without this
// here, in a very special circumstance it is possible to null
// out the entire content of a column by reordering and then
// sorting another column.
if (getColumns().contains(tc)) continue;
tc.setTableView(null);
tc.setParentColumn(null);
}
for (TableColumn tc : c.getAddedSubList()) {
tc.setTableView(getTableView());
}
updateColumnWidths();
}
};
private WeakListChangeListener> weakColumnsListener =
new WeakListChangeListener<>(columnsListener);
/* *************************************************************************
* *
* Instance Variables *
* *
**************************************************************************/
// Contains any children columns that should be nested within this column
private final ObservableList> columns = FXCollections.>observableArrayList();
/* *************************************************************************
* *
* Properties *
* *
**************************************************************************/
// --- TableView
/**
* The TableView that this TableColumn belongs to.
*/
private ReadOnlyObjectWrapper> tableView = new ReadOnlyObjectWrapper<>(this, "tableView");
public final ReadOnlyObjectProperty> tableViewProperty() {
return tableView.getReadOnlyProperty();
}
final void setTableView(TableView value) { tableView.set(value); }
public final TableView getTableView() { return tableView.get(); }
// --- Cell value factory
/**
* The cell value factory needs to be set to specify how to populate all
* cells within a single TableColumn. A cell value factory is a {@link Callback}
* that provides a {@link CellDataFeatures} instance, and expects an
* {@link ObservableValue} to be returned. The returned ObservableValue instance
* will be observed internally to allow for immediate updates to the value
* to be reflected on screen.
*
* An example of how to set a cell value factory is:
*
*
* lastNameCol.setCellValueFactory(new Callback<CellDataFeatures<Person, String>, ObservableValue<String>>() {
* public ObservableValue<String> call(CellDataFeatures<Person, String> p) {
* // p.getValue() returns the Person instance for a particular TableView row
* return p.getValue().lastNameProperty();
* }
* });
* }
*
*
* A common approach is to want to populate cells in a TableColumn using
* a single value from a Java bean. To support this common scenario, there
* is the {@link PropertyValueFactory} class. Refer to this class for more
* information on how to use it, but briefly here is how the above use case
* could be simplified using the PropertyValueFactory class:
*
*
* lastNameCol.setCellValueFactory(new PropertyValueFactory<Person,String>("lastName"));
*
*
* @see PropertyValueFactory
*/
private ObjectProperty, ObservableValue>> cellValueFactory;
public final void setCellValueFactory(Callback, ObservableValue> value) {
cellValueFactoryProperty().set(value);
}
public final Callback, ObservableValue> getCellValueFactory() {
return cellValueFactory == null ? null : cellValueFactory.get();
}
public final ObjectProperty, ObservableValue>> cellValueFactoryProperty() {
if (cellValueFactory == null) {
cellValueFactory = new SimpleObjectProperty<>(this, "cellValueFactory");
}
return cellValueFactory;
}
// --- Cell Factory
/**
* The cell factory for all cells in this column. The cell factory
* is responsible for rendering the data contained within each {@code TableCell} for
* a single table column.
*
* By default, {@code TableColumn} uses the {@link #DEFAULT_CELL_FACTORY default cell
* factory}, but this can be replaced with a custom implementation, for
* example, to show data in a different way or to support editing. There is a
* lot of documentation on creating custom cell factories
* elsewhere (see {@link Cell} and {@link TableView} for example).
*
* Finally, there are a number of pre-built cell factories available in the
* {@link javafx.scene.control.cell} package.
*/
private final ObjectProperty, TableCell>> cellFactory =
new SimpleObjectProperty<>(
this, "cellFactory", ((Callback) DEFAULT_CELL_FACTORY)) {
@Override protected void invalidated() {
TableView table = getTableView();
if (table == null) return;
Map