tornadofx.control.TableRowExpanderColumn Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of tornadofx-controls Show documentation
Show all versions of tornadofx-controls Show documentation
CSS Stylable Controls for JavaFX
The newest version!
package tornadofx.control;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.util.Callback;
import tornadofx.control.skin.ExpandableTableRowSkin;
import java.util.HashMap;
import java.util.Map;
/**
* The TableRowExpanderColumn enables a TableView to provide an expandable editor below each table row.
* The column itself contains a toggle button that on click will show an editor for the current row right below the
* columns. Example:
*
* <pre>
* TableRowExpanderColumn<Customer> expander = new TableRowExpanderColumn<>(param -> {
* HBox editor = new HBox(10);
* TextField text = new TextField(param.getValue().getName());
* Button save = new Button("Save customer");
* save.setOnAction(event -> {
* save();
* param.toggleExpanded();
* });
* editor.getChildren().addAll(text, save);
* return editor;
* });
*
* tableView.getColumns().add(expander);
* </pre>
*
* You can provide a custom cellFactory to customize the toggle button. A typical custom toggle cell implementation
* would look like this:
*
* <pre>
* public class MyCustomToggleCell<S> extends TableCell<S, Boolean> {
* private Button button = new Button();
*
* public MyCustomToggleCell(TableRowExpanderColumn<S> column) {
* button.setOnAction(event -> column.toggleExpanded(getIndex()));
* }
*
* protected void updateItem(Boolean expanded, boolean empty) {
* super.updateItem(expanded, empty);
* if (expanded == null || empty) {
* setGraphic(null);
* } else {
* button.setText(expanded ? "Collapse" : "Expand");
* setGraphic(button);
* }
* }
* }
* </pre>
*
* The custom toggle cell utilizes the {@link TableRowExpanderColumn#toggleExpanded(int)} method to toggle
* the row expander.
*
* @param The item type of the TableView
*/
public final class TableRowExpanderColumn extends TableColumn {
private static final String STYLE_CLASS = "expander-column";
private static final String EXPANDER_BUTTON_STYLE_CLASS = "expander-button";
private final Map expandedNodeCache = new HashMap<>();
private final Map expansionState = new HashMap<>();
private Callback, Node> expandedNodeCallback;
/**
* Returns a Boolean property that can be used to manipulate the expanded state for a row
* corresponding to the given item value.
*
* @param item The item corresponding to a table row
* @return The boolean property
*/
public SimpleBooleanProperty getExpandedProperty(S item) {
SimpleBooleanProperty value = expansionState.get(item);
if (value == null) {
value = new SimpleBooleanProperty(item, "expanded", false) {
/**
* When the expanded state change we refresh the tableview.
* If the expanded state changes to false we remove the cached expanded node.
*/
@Override
protected void invalidated() {
getTableView().refresh();
if (!getValue()) expandedNodeCache.remove(getBean());
}
};
expansionState.put(item, value);
}
return value;
}
/**
* Get or create and cache the expanded node for a given item.
*
* @param tableRow The table row, used to find the item index
* @return The expanded node for the given item
*/
public Node getOrCreateExpandedNode(TableRow tableRow) {
int index = tableRow.getIndex();
if (index > -1 && index < getTableView().getItems().size()) {
S item = getTableView().getItems().get(index);
Node node = expandedNodeCache.get(item);
if (node == null) {
node = expandedNodeCallback.call(new TableRowDataFeatures<>(tableRow, this, item));
expandedNodeCache.put(item, node);
}
return node;
}
return null;
}
/**
* Return the expanded node for the given item, if it exists.
*
* @param item The item corresponding to a table row
* @return The expanded node, if it exists.
*/
public Node getExpandedNode(S item) {
return expandedNodeCache.get(item);
}
public TableRowExpanderColumn(TableView tableView, Callback, Node> expandedNodeCallback) {
this(expandedNodeCallback);
tableView.getColumns().add(this);
}
/**
* Create a row expander column that can be added to the TableView list of columns.
*
* The expandedNodeCallback is expected to return a Node representing the editor that should appear below the
* table row when the toggle button within the expander column is clicked.
*
* Once this column is assigned to a TableView, it will automatically install a custom row factory for the TableView
* so that it can configure a TableRow with the {@link ExpandableTableRowSkin}. It is within the skin that the actual
* rendering of the expanded node occurs.
*
* @see TableRowExpanderColumn
* @see TableRowDataFeatures
*
* @param expandedNodeCallback
*/
public TableRowExpanderColumn(Callback, Node> expandedNodeCallback) {
this.expandedNodeCallback = expandedNodeCallback;
getStyleClass().add(STYLE_CLASS);
setCellValueFactory(param -> getExpandedProperty(param.getValue()));
setCellFactory(param -> new ToggleCell());
installRowFactoryOnTableViewAssignment();
}
/**
* Install the row factory on the TableView when this column is assigned to a TableView.
*/
private void installRowFactoryOnTableViewAssignment() {
tableViewProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
getTableView().setRowFactory(param -> new TableRow() {
@Override
protected Skin> createDefaultSkin() {
return new ExpandableTableRowSkin<>(this, TableRowExpanderColumn.this);
}
});
}
});
}
/**
* The default toggle cell creates a button with a + or - sign as the text,
* depending on the expanded state of the row it represents.
*
* You can use this as a starting point to implement a custom toggle cell.
*/
private final class ToggleCell extends TableCell {
private Button button = new Button();
public ToggleCell() {
button.setFocusTraversable(false);
button.getStyleClass().add(EXPANDER_BUTTON_STYLE_CLASS);
button.setPrefSize(16, 16);
button.setPadding(new Insets(0));
button.setOnAction(event -> toggleExpanded(getIndex()));
}
@Override
protected void updateItem(Boolean expanded, boolean empty) {
super.updateItem(expanded, empty);
if (expanded == null || empty) {
setGraphic(null);
} else {
button.setText(expanded ? "-" : "+");
setGraphic(button);
}
}
}
/**
* Toggle the expanded state of the row at the given index.
*
* @param index The index of the row you want to toggle expansion for.
*/
public void toggleExpanded(int index) {
SimpleBooleanProperty expanded = (SimpleBooleanProperty) getCellObservableValue(index);
expanded.setValue(!expanded.getValue());
}
/**
* This object is passed to the expanded node callback when it is time to create a Node to represent the
* expanded editor of a certain row. The most important method is {@link #getValue()}} which returns the
* object represented by the current row.
*
* Further more, the {@link #expandedProperty()} returns a boolean property indicating the current expansion
* state of the current row. You can use this, or the {@link #toggleExpanded()} method to toggle and inspect
* the expanded state of the row, for example if you want an action inside the row editor to contract the editor.
*
* @param The type of items in the TableView
*/
public static class TableRowDataFeatures {
private TableRow tableRow;
private TableRowExpanderColumn tableColumn;
private SimpleBooleanProperty expandedProperty;
private S value;
public TableRowDataFeatures(TableRow tableRow, TableRowExpanderColumn tableColumn, S value) {
this.tableRow = tableRow;
this.tableColumn = tableColumn;
this.expandedProperty = (SimpleBooleanProperty) tableColumn.getCellObservableValue(tableRow.getIndex());
this.value = value;
}
/**
* Return the current TableRow. It is safe to assume that the index returned by {@link TableRow#getIndex()} is
* correct as long as you use it for the initial node creation. It is not safe to trust the result of this call
* at any later time, for example in a button action within the row editor.
*
* @return The current TableRow
*/
public TableRow getTableRow() {
return tableRow;
}
/**
* Return the TableColumn which contains the toggle button. Normally you would not need to use this directly,
* but rather consult the {@link #expandedProperty()} for inspection and mutation of the toggled state of this row.
*
* @return The TableColumn which contains the toggle button
*/
public TableRowExpanderColumn getTableColumn() {
return tableColumn;
}
/**
* The expanded property can be used to inspect or mutate the toggled state of this row editor. You can also
* listen for changes to it's state if needed.
*
* @return The expanded property
*/
public SimpleBooleanProperty expandedProperty() {
return expandedProperty;
}
/**
* Toggle the expanded state of this row editor.
*/
public void toggleExpanded() {
SimpleBooleanProperty expanded = expandedProperty();
expanded.setValue(!expanded.getValue());
}
/**
* Returns a boolean indicating if the current row is expanded or not
*
* @return A boolean indicating the expanded state of the current editor
*/
public Boolean isExpanded() {
return expandedProperty().getValue();
}
/**
* Set the expanded state. This will update the {@link #expandedProperty()} accordingly.
*
* @param expanded Wheter the row editor should be expanded or not
*/
public void setExpanded(Boolean expanded) {
expandedProperty().setValue(expanded);
}
/**
* The value represented by the current table row. It is important that the value has valid equals/hashCode
* methods, as the row value is used to keep track of the node editor for each row.
*
* @return The value represented by the current table row
*/
public S getValue() {
return value;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy