JavaFX TableColumn resize to fit cell content - javafx

I'm looking for a way to resize a TableColumn in a TableView so that all of the content is visible in each cell (i.e. no truncation).
I noticed that double clicking on the column divider's does auto fit the column to the contents of its cells. Is there a way to trigger this programmatically?

Digging through the javafx source, I found that the actual method called when you click TableView columns divider is
/*
* FIXME: Naive implementation ahead
* Attempts to resize column based on the pref width of all items contained
* in this column. This can be potentially very expensive if the number of
* rows is large.
*/
#Override protected void resizeColumnToFitContent(TableColumn<T, ?> tc, int maxRows) {
if (!tc.isResizable()) return;
// final TableColumn<T, ?> col = tc;
List<?> items = itemsProperty().get();
if (items == null || items.isEmpty()) return;
Callback/*<TableColumn<T, ?>, TableCell<T,?>>*/ cellFactory = tc.getCellFactory();
if (cellFactory == null) return;
TableCell<T,?> cell = (TableCell<T, ?>) cellFactory.call(tc);
if (cell == null) return;
// set this property to tell the TableCell we want to know its actual
// preferred width, not the width of the associated TableColumnBase
cell.getProperties().put(TableCellSkin.DEFER_TO_PARENT_PREF_WIDTH, Boolean.TRUE);
// determine cell padding
double padding = 10;
Node n = cell.getSkin() == null ? null : cell.getSkin().getNode();
if (n instanceof Region) {
Region r = (Region) n;
padding = r.snappedLeftInset() + r.snappedRightInset();
}
int rows = maxRows == -1 ? items.size() : Math.min(items.size(), maxRows);
double maxWidth = 0;
for (int row = 0; row < rows; row++) {
cell.updateTableColumn(tc);
cell.updateTableView(tableView);
cell.updateIndex(row);
if ((cell.getText() != null && !cell.getText().isEmpty()) || cell.getGraphic() != null) {
getChildren().add(cell);
cell.applyCss();
maxWidth = Math.max(maxWidth, cell.prefWidth(-1));
getChildren().remove(cell);
}
}
// dispose of the cell to prevent it retaining listeners (see RT-31015)
cell.updateIndex(-1);
// RT-36855 - take into account the column header text / graphic widths.
// Magic 10 is to allow for sort arrow to appear without text truncation.
TableColumnHeader header = getTableHeaderRow().getColumnHeaderFor(tc);
double headerTextWidth = Utils.computeTextWidth(header.label.getFont(), tc.getText(), -1);
Node graphic = header.label.getGraphic();
double headerGraphicWidth = graphic == null ? 0 : graphic.prefWidth(-1) + header.label.getGraphicTextGap();
double headerWidth = headerTextWidth + headerGraphicWidth + 10 + header.snappedLeftInset() + header.snappedRightInset();
maxWidth = Math.max(maxWidth, headerWidth);
// RT-23486
maxWidth += padding;
if(tableView.getColumnResizePolicy() == TableView.CONSTRAINED_RESIZE_POLICY) {
maxWidth = Math.max(maxWidth, tc.getWidth());
}
tc.impl_setWidth(maxWidth);
}
It's declared in
com.sun.javafx.scene.control.skin.TableViewSkinBase
method signature
protected abstract void resizeColumnToFitContent(TC tc, int maxRows)
Since it's protected, You cannot call it from e.g. tableView.getSkin(), but You can always extend the TableViewSkin overriding only resizeColumnToFitContent method and make it public.

As #Tomasz suggestion, I resolve by reflection:
import com.sun.javafx.scene.control.skin.TableViewSkin;
import javafx.scene.control.Skin;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class GUIUtils {
private static Method columnToFitMethod;
static {
try {
columnToFitMethod = TableViewSkin.class.getDeclaredMethod("resizeColumnToFitContent", TableColumn.class, int.class);
columnToFitMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
public static void autoFitTable(TableView tableView) {
tableView.getItems().addListener(new ListChangeListener<Object>() {
#Override
public void onChanged(Change<?> c) {
for (Object column : tableView.getColumns()) {
try {
columnToFitMethod.invoke(tableView.getSkin(), column, -1);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
});
}
}

The current versions of JavaFX (e.g. 15-ea+1) resize table columns, if the prefWidth was never set (or is set to 80.0F, see TableColumnHeader enter link description here).

In Java 16 we can extend TableView to use a custom TableViewSkin which in turn uses a custom TableColumnHeader
class FitWidthTableView<T> extends TableView<T> {
public FitWidthTableView() {
setSkin(new FitWidthTableViewSkin<>(this));
}
public void resizeColumnsToFitContent() {
Skin<?> skin = getSkin();
if (skin instanceof FitWidthTableViewSkin<?> tvs) tvs.resizeColumnsToFitContent();
}
}
class FitWidthTableViewSkin<T> extends TableViewSkin<T> {
public FitWidthTableViewSkin(TableView<T> tableView) {
super(tableView);
}
#Override
protected TableHeaderRow createTableHeaderRow() {
return new TableHeaderRow(this) {
#Override
protected NestedTableColumnHeader createRootHeader() {
return new NestedTableColumnHeader(null) {
#Override
protected TableColumnHeader createTableColumnHeader(TableColumnBase col) {
return new FitWidthTableColumnHeader(col);
}
};
}
};
}
public void resizeColumnsToFitContent() {
for (TableColumnHeader columnHeader : getTableHeaderRow().getRootHeader().getColumnHeaders()) {
if (columnHeader instanceof FitWidthTableColumnHeader colHead) colHead.resizeColumnToFitContent(-1);
}
}
}
class FitWidthTableColumnHeader extends TableColumnHeader {
public FitWidthTableColumnHeader(TableColumnBase col) {
super(col);
}
#Override
public void resizeColumnToFitContent(int rows) {
super.resizeColumnToFitContent(-1);
}
}

You can use tableView.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
You can also try switching between the two policies TableView.CONSTRAINED_RESIZE_POLICY
and TableView.UNCONSTRAINED_RESIZE_POLICY in case TableView.UNCONSTRAINED_RESIZE_POLICY alone doesn't fit your need.
Here's a useful link.

Related

JavaFX TreeTableView - Prevent selection of certain TreeItems

I have looked at a couple of questions here, but I can't seem to find anything related to disabling selection of rows for TreeTableViews in particular in JavaFX.
The closest related questions I have come across are all related to TreeViews:
How to make certain JavaFX TreeView nodes non-selectable?
TreeView - Certain TreeItems are not allowed to be selected
The answer given in question 2 where a custom selection-model is used that extends from MultipleSelectionModel seems to be the most promising. However, the problem with TreeTableViews are that they use a custom TreeTableViewSelectionModel that itself extends from TableSelectionModel.
If you do a naive implementation where you just forward the calls to a wrapped TreeTableViewSelectionModel and provide filtering in the select() and selectAndClear() methods as follows:
import javafx.collections.ObservableList;
import javafx.scene.control.TableColumnBase;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTablePosition;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.TreeTableView.TreeTableViewSelectionModel;
public class FilteredTreeTableViewSelectionModel<S> extends TreeTableViewSelectionModel<S> {
private final TreeTableViewSelectionModel<S> selectionModel;
private final TreeItemSelectionFilter<S> filter;
public FilteredTreeTableViewSelectionModel(
TreeTableView<S> treeTableView,
TreeTableViewSelectionModel<S> selectionModel,
TreeItemSelectionFilter<S> filter) {
super(treeTableView);
this.selectionModel = selectionModel;
this.filter = filter;
}
#Override
public ObservableList<TreeTablePosition<S, ?>> getSelectedCells() {
return this.selectionModel.getSelectedCells();
}
#Override
public boolean isSelected(int row, TableColumnBase<TreeItem<S>, ?> column) {
return this.selectionModel.isSelected(row, column);
}
#Override
public void select(int row, TableColumnBase<TreeItem<S>, ?> column) {
TreeTableView<S> treeTableView = getTreeTableView();
TreeItem<S> treeItem = treeTableView.getTreeItem(row);
if (this.filter.isSelectable(treeItem)) {
this.selectionModel.select(row);
}
}
#Override
public void clearAndSelect(int row, TableColumnBase<TreeItem<S>, ?> column) {
TreeTableView<S> treeTableView = getTreeTableView();
TreeItem<S> treeItem = treeTableView.getTreeItem(row);
// If the specified row is selectable, we forward clear-and-select
// call to the delegate selection-model.
if (this.filter.isSelectable(treeItem)) {
this.selectionModel.clearAndSelect(row);
}
// Else, we just do a normal clear-selection call.
else {
this.selectionModel.clearSelection();
}
}
#Override
public void clearSelection(int row, TableColumnBase<TreeItem<S>, ?> column) {
this.selectionModel.clearSelection(row, column);
}
#Override
public void selectLeftCell() {
this.selectionModel.selectLeftCell();
}
#Override
public void selectRightCell() {
this.selectionModel.selectRightCell();
}
#Override
public void selectAboveCell() {
this.selectionModel.selectAboveCell();
}
#Override
public void selectBelowCell() {
this.selectionModel.selectBelowCell();
}
}
where you filter with this interface:
public interface TreeItemSelectionFilter<S> {
public boolean isSelectable(TreeItem<S> treeItem);
}
Your TreeTableView's selection will not work correctly as you'll see that the selected rows are not properly highlighted as in this example where I selected the first row:
Am I missing something in the way it needs to be implemented, or is there another way to provide selection filtering for TreeTableViews?
I pieced together a class by looking at the internals of TreeTableViewArrayListSelectionModel. As the TreeTableViewSelectionModel class itself has state, a lot of the superclass methods needed to be overridden to provide implementations that forwarded either to the internal selection-model or to one of the other methods that itself forwards to the internal selection-model. The implementation of this class is as follows:
import java.util.Arrays;
import java.util.OptionalInt;
import java.util.stream.IntStream;
import javafx.collections.ObservableList;
import javafx.scene.control.*;
import javafx.scene.control.TreeTableView.*;
public class FilteredTreeTableViewSelectionModel<S> extends TreeTableViewSelectionModel<S> {
private final TreeTableViewSelectionModel<S> selectionModel;
private final TreeItemSelectionFilter<S> selectionFilter;
public FilteredTreeTableViewSelectionModel(
TreeTableView<S> treeTableView,
TreeTableViewSelectionModel<S> selectionModel,
TreeItemSelectionFilter<S> selectionFilter) {
super(treeTableView);
this.selectionModel = selectionModel;
this.selectionFilter = selectionFilter;
cellSelectionEnabledProperty().bindBidirectional(selectionModel.cellSelectionEnabledProperty());
selectionModeProperty().bindBidirectional(selectionModel.selectionModeProperty());
}
#Override
public ObservableList<Integer> getSelectedIndices() {
return this.selectionModel.getSelectedIndices();
}
#Override
public ObservableList<TreeItem<S>> getSelectedItems() {
return this.selectionModel.getSelectedItems();
}
#Override
public ObservableList<TreeTablePosition<S, ?>> getSelectedCells() {
return this.selectionModel.getSelectedCells();
}
#Override
public boolean isSelected(int index) {
return this.selectionModel.isSelected(index);
}
#Override
public boolean isSelected(int row, TableColumnBase<TreeItem<S>, ?> column) {
return this.selectionModel.isSelected(row, column);
}
#Override
public boolean isEmpty() {
return this.selectionModel.isEmpty();
}
#Override
public TreeItem<S> getModelItem(int index) {
return this.selectionModel.getModelItem(index);
}
#Override
public void focus(int row) {
this.selectionModel.focus(row);
}
#Override
public int getFocusedIndex() {
return this.selectionModel.getFocusedIndex();
}
private TreeTablePosition<S,?> getFocusedCell() {
TreeTableView<S> treeTableView = getTreeTableView();
TreeTableViewFocusModel<S> focusModel = treeTableView.getFocusModel();
return (focusModel == null) ?
new TreeTablePosition<>(treeTableView, -1, null) :
focusModel.getFocusedCell();
}
private TreeTableColumn<S,?> getTableColumn(int pos) {
return getTreeTableView().getVisibleLeafColumn(pos);
}
// Gets a table column to the left or right of the current one, given an offset.
private TreeTableColumn<S,?> getTableColumn(TreeTableColumn<S,?> column, int offset) {
int columnIndex = getTreeTableView().getVisibleLeafIndex(column);
int newColumnIndex = columnIndex + offset;
TreeTableView<S> treeTableView = getTreeTableView();
return treeTableView.getVisibleLeafColumn(newColumnIndex);
}
private int getRowCount() {
TreeTableView<S> treeTableView = getTreeTableView();
return treeTableView.getExpandedItemCount();
}
#Override
public void select(int row) {
select(row, null);
}
#Override
public void select(int row, TableColumnBase<TreeItem<S>, ?> column) {
// If the row is -1, we need to clear the selection.
if (row == -1) {
this.selectionModel.clearSelection();
} else if (row >= 0 && row < getRowCount()) {
// If the tree-item at the specified row-index is selectable, we
// forward select call to the internal selection-model.
TreeTableView<S> treeTableView = getTreeTableView();
TreeItem<S> treeItem = treeTableView.getTreeItem(row);
if (this.selectionFilter.isSelectable(treeItem)) {
this.selectionModel.select(row, column);
}
}
}
#Override
public void select(TreeItem<S> treeItem) {
if (treeItem == null) {
// If the provided tree-item is null, and we are in single-selection
// mode we need to clear the selection.
if (getSelectionMode() == SelectionMode.SINGLE) {
this.selectionModel.clearSelection();
}
// Else, we just forward to the internal selection-model so that
// the selected-index can be set to -1, and the selected-item
// can be set to null.
else {
this.selectionModel.select(null);
}
} else if (this.selectionFilter.isSelectable(treeItem)) {
this.selectionModel.select(treeItem);
}
}
#Override
public void selectIndices(int row, int ... rows) {
// If we have no trailing rows, we forward to normal row-selection.
if (rows == null || rows.length == 0) {
select(row);
return;
}
// Filter rows so that we only end up with those rows whose corresponding
// tree-items are selectable.
TreeTableView<S> treeTableView = getTreeTableView();
int[] filteredRows = IntStream.concat(IntStream.of(row), Arrays.stream(rows)).filter(rowToCheck -> {
TreeItem<S> treeItem = treeTableView.getTreeItem(rowToCheck);
return (treeItem != null) && selectionFilter.isSelectable(treeItem);
}).toArray();
// If we have rows left, we proceed to forward to internal selection-model.
if (filteredRows.length > 0) {
int newRow = filteredRows[0];
int[] newRows = Arrays.copyOfRange(filteredRows, 1, filteredRows.length);
this.selectionModel.selectIndices(newRow, newRows);
}
}
#Override
public void selectRange(int start, int end) {
super.selectRange(start, end);
}
#Override
public void selectRange(int minRow, TableColumnBase<TreeItem<S>, ?> minColumn, int maxRow, TableColumnBase<TreeItem<S>, ?> maxColumn) {
super.selectRange(minRow, minColumn, maxRow, maxColumn);
}
#Override
public void clearAndSelect(int row) {
clearAndSelect(row, null);
}
#Override
public void clearAndSelect(int row, TableColumnBase<TreeItem<S>, ?> column) {
// If the row is out-of-bounds we just clear and return.
if (row < 0 || row >= getRowCount()) {
clearSelection();
return;
}
TreeTableView<S> treeTableView = getTreeTableView();
TreeItem<S> treeItem = treeTableView.getTreeItem(row);
// If the tree-item at the specified row-index is selectable, we forward
// clear-and-select call to the internal selection-model.
if (this.selectionFilter.isSelectable(treeItem)) {
this.selectionModel.clearAndSelect(row, column);
}
// Else, we just do a normal clear-selection call.
else {
this.selectionModel.clearSelection();
}
}
#Override
public void selectAll() {
int rowCount = getRowCount();
// If we are in single-selection mode, we exit prematurely as
// we cannot select all rows.
if (getSelectionMode() == SelectionMode.SINGLE) {
return;
}
// If we only have a single row to select, we forward to the
// row-index select-method.
if (rowCount == 1) {
select(0);
}
// Else, if we have more than one row available, we construct an array
// of all the indices and forward to the selectIndices-method.
else if (rowCount > 1) {
int row = 0;
int[] rows = IntStream.range(1, rowCount).toArray();
selectIndices(row, rows);
}
}
#Override
public void clearSelection(int index) {
this.selectionModel.clearSelection(index);
}
#Override
public void clearSelection(int row, TableColumnBase<TreeItem<S>, ?> column) {
this.selectionModel.clearSelection(row, column);
}
#Override
public void clearSelection() {
this.selectionModel.clearSelection();
}
#Override
public void selectFirst() {
// Find first selectable row in the tree-table by testing each tree-item
// against our selection-filter.
TreeTableView<S> treeTableView = getTreeTableView();
OptionalInt firstRow = IntStream.range(0, getRowCount()).
filter(row -> this.selectionFilter.isSelectable(treeTableView.getTreeItem(row))).
findFirst();
TreeTablePosition<S,?> focusedCell = getFocusedCell();
// If we managed to find a row, we forward to the appropriate internal
// selection-model's select-method based on whether cell-seleciton is
// enabled or not.
firstRow.ifPresent(row -> {
if (isCellSelectionEnabled()) {
this.selectionModel.select(row, focusedCell.getTableColumn());
} else {
this.selectionModel.select(row);
}
});
}
#Override
public void selectLast() {
// Find first selectable row (by iterating in reverse) in the tree-table
// by testing each tree-item against our selection-filter.
int rowCount = getRowCount();
TreeTableView<S> treeTableView = getTreeTableView();
OptionalInt lastRow = IntStream.iterate(rowCount - 1, i -> i - 1).
limit(rowCount).
filter(row -> this.selectionFilter.isSelectable(treeTableView.getTreeItem(row))).
findFirst();
TreeTablePosition<S,?> focusedCell = getFocusedCell();
// If we managed to find a row, we forward to the appropriate internal
// selection-model's select-method based on whether cell-seleciton is
// enabled or not.
lastRow.ifPresent(row -> {
if (isCellSelectionEnabled()) {
this.selectionModel.select(row, focusedCell.getTableColumn());
} else {
this.selectionModel.select(row);
}
});
}
#Override
public void selectPrevious() {
TreeTableView<S> treeTableView = getTreeTableView();
if (isCellSelectionEnabled()) {
// In cell selection mode, we have to wrap around, going from
// right-to-left, and then wrapping to the end of the previous line.
TreeTablePosition<S,?> pos = getFocusedCell();
// If we are not at the first column, we go to the previous column.
if (pos.getColumn() - 1 >= 0) {
this.selectionModel.select(pos.getRow(), getTableColumn(pos.getTableColumn(), -1));
}
// Else, we wrap to end of previous selectable row.
else {
// If we have nothing selected, wrap around to the last index.
int startIndex = (pos.getRow() == -1) ? getRowCount() : pos.getRow();
// Find previous selectable row.
OptionalInt previousRow = IntStream.iterate(startIndex - 1, i -> i - 1).
limit(startIndex).
filter(row -> this.selectionFilter.isSelectable(treeTableView.getTreeItem(row))).
findFirst();
// Last column index.
int lastColumnIndex = getTreeTableView().getVisibleLeafColumns().size() - 1;
// If we have a previous row, forward selection to internal selection-model.
previousRow.ifPresent(row -> this.selectionModel.select(row, getTableColumn(lastColumnIndex)));
}
} else {
// If we have nothing selected, wrap around to the last index.
int startIndex = (getFocusedIndex() == -1) ? getRowCount() : getFocusedIndex();
if (startIndex > 0) {
OptionalInt previousRow = IntStream.iterate(startIndex - 1, i -> i - 1).
limit(startIndex).
filter(row -> this.selectionFilter.isSelectable(treeTableView.getTreeItem(row))).
findFirst();
previousRow.ifPresent(this.selectionModel::select);
}
}
}
#Override
public void selectNext() {
TreeTableView<S> treeTableView = getTreeTableView();
if (isCellSelectionEnabled()) {
// In cell selection mode, we have to wrap around, going from
// left-to-right, and then wrapping to the start of the next line.
TreeTablePosition<S,?> pos = getFocusedCell();
// If we are not at the last column, then go to the next column.
if (pos.getRow() != -1 && pos.getColumn() + 1 < getTreeTableView().getVisibleLeafColumns().size()) {
this.selectionModel.select(pos.getRow(), getTableColumn(pos.getTableColumn(), 1));
}
// Else, wrap to start of next selectable row.
else if (pos.getRow() < getRowCount() - 1) {
// If we have nothing selected, starting at -1 will work out correctly
// because we'll search from 0 onwards.
int startIndex = pos.getRow();
// Find next selectable row.
OptionalInt nextItem = IntStream.range(startIndex + 1, getRowCount()).
filter(row -> this.selectionFilter.isSelectable(treeTableView.getTreeItem(row))).
findFirst();
// If we have a next row, forward selection to internal selection-model.
nextItem.ifPresent(row -> this.selectionModel.select(row, getTableColumn(0)));
}
} else {
// If we have nothing selected, starting at -1 will work out correctly
// because we'll search from 0 onwards.
int startIndex = getFocusedIndex();
if (startIndex < getRowCount() - 1) {
OptionalInt nextRow = IntStream.range(startIndex + 1, getRowCount()).
filter(row -> this.selectionFilter.isSelectable(treeTableView.getTreeItem(row))).
findFirst();
nextRow.ifPresent(this.selectionModel::select);
}
}
}
#Override
public void selectLeftCell() {
if (!isCellSelectionEnabled()) {
return;
}
TreeTablePosition<S,?> pos = getFocusedCell();
if (pos.getColumn() - 1 >= 0) {
select(pos.getRow(), getTableColumn(pos.getTableColumn(), -1));
}
}
#Override
public void selectRightCell() {
if (!isCellSelectionEnabled()) {
return;
}
TreeTablePosition<S,?> pos = getFocusedCell();
if (pos.getColumn() + 1 < getTreeTableView().getVisibleLeafColumns().size()) {
select(pos.getRow(), getTableColumn(pos.getTableColumn(), 1));
}
}
#Override
public void selectAboveCell() {
TreeTablePosition<S,?> pos = getFocusedCell();
// If we have nothing selected, wrap around to the last row.
if (pos.getRow() == -1) {
selectLast();
} else if (pos.getRow() > 0) {
TreeTableView<S> treeTableView = getTreeTableView();
// Find previous selectable row.
OptionalInt previousRow = IntStream.iterate(pos.getRow() - 1, i -> i - 1).
limit(pos.getRow()).
filter(row -> this.selectionFilter.isSelectable(treeTableView.getTreeItem(row))).
findFirst();
// If we have a previous row, forward selection to internal selection-model.
previousRow.ifPresent(row -> this.selectionModel.select(row, pos.getTableColumn()));
}
}
#Override
public void selectBelowCell() {
TreeTablePosition<S,?> pos = getFocusedCell();
// If we have nothing selected, start at the first row.
if (pos.getRow() == -1) {
selectFirst();
} else if (pos.getRow() < getRowCount() -1) {
TreeTableView<S> treeTableView = getTreeTableView();
// Find next selectable row.
OptionalInt nextItem = IntStream.range(pos.getRow() + 1, getRowCount()).
filter(row -> this.selectionFilter.isSelectable(treeTableView.getTreeItem(row))).
findFirst();
// If we have a next row, forward selection to internal selection-model.
nextItem.ifPresent(row -> this.selectionModel.select(row, pos.getTableColumn()));
}
}
}
The selectFirst(), selectLast(), selectNext() and selectPrevious() methods were implemented by searching for the first TreeItem that satisfied the filter from an index in a specific direction. The corresponding row is then forwarded to the internal selection-model.
The selectRange() methods were just implemented by forwarding to their superclass counterparts that is actually implemented in MultipleSelectionModel (not sure if this is correct) as the implementation in TreeTableViewArrayListSelectionModel requires access to its internal selectedCellsMap data-structure that opens up a whole can of worms if we try to copy all of that functionality.
I also bind the cellSelectionEnabledProperty and the selectionModeProperty to the internal selection-model as this needs to reflect the state that the internal model was in when first created. This also allows us to change these properties for the internal selection-model and the updates will be reflected in the FilteredTreeTableViewSelectionModel.
To use the FilteredTreeTableViewSelectionModel you need to implement the TreeItemSelectionFilter interface (as given in the question) and pass it as one of the constructor arguments for the FilteredTreeTableViewSelectionModel together with the TreeTableView and existing selection-model:
...
TreeTableViewSelectionModel<S> selectionModel = treeTableView.getSelectionModel();
TreeItemSelectionFilter<S> treeItemFilter = MyCustomSelectionFilter<>();
FilteredTreeTableViewSelectionModel<S> filteredSelectionModel = new FilteredTreeTableViewSelectionModel(treeTableView, selectionModel, treeItemFilter);
treeTableView.setSelectionModel(filteredSelectionModel);
...
I've uploaded the source-code of an example application here so that you can easily test the behavior of the FilteredTreeTableViewSelectionModel for yourself. Compare it with the default selection-model and see if you are satisfied with the behavior.

JavaFX 11 TableView Cell navigation by TAB key pressed without custom editableCell class

The problem:
I want to navigate through a TableView from one cell to the next right neighbor cell in JavaFX by using the TAB key.
Notice: The TableView is set to editable. And CellSelection is enabled too.
tableReceipt.getSelectionModel().setCellSelectionEnabled(true);
The handling of the KeyPressedEvent seemingly is not my problem, but to request the focus of the single cell on the right of the current cell.
I can focus one cell but when i press the TAB key the focus goes out of the table on other form elements.
The TableView contains some editable TextFieldTableCells and one editable ComboBoxTableCell.
I don't use a custom class for the editable Cells but Code like this:
Callback<TableColumn<Receipt, int>, TableCell<Receipt, int>> tfCallBack = TextFieldTableCell.forTableColumn();
columnAmount.setCellFactory(tfCallBack);
for a TableCell with editable TextField nature.
My question:
How can I implement a solution to solve my problem? A theoretical solution would help too. I allready searched for this topic but only found an example that's using a custom EditableCell class. I think there must be a solution using the callback method like I do.
Solution approach:
tableReceipt.setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent t) {
if (t.getCode() == KeyCode.TAB) {
tableReceipt.getFocusModel().focusRightCell();
Robot r = new Robot();
r.keyPress(KeyCode.ENTER);
}
}
});
With this code I can get focus of the right cell next to the current one. And I need the ENTER KeyPress to enable the editable mode of the Cell. But when I press TAB on keyboard the new value is not committed. For example I press '2' the default value is '0' and after pressing TAB the value is again '0'.
Question No.2:
How can I combine the code above with a changeListener/onEditCommitListener, that the new value is stored in the cell after pressing TAB?
Thank you.
This may be helpful. You have to manually call it after you complete a cell edit.
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Brian Boutin on 5/14/2019
*/
class CellNav<T> {
private final TableView<T> tableView;
private KeyEvent lastKeyEvent;
private TablePosition editingPosition;
CellNav(TableView<T> tableView) {
this.tableView = tableView;
this.editingPosition = null;
tableView.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.TAB || event.getCode() == KeyCode.ENTER) {
editingPosition = tableView.getEditingCell();
lastKeyEvent = event;
}
});
tableView.setOnMouseReleased(e -> editingPosition = null);
for (TableColumn col : tableView.getColumns()) {
col.setOnEditCancel(event -> editingPosition = null);
}
}
void doCellNav() {
if (editingPosition == null) {
return;
}
if (lastKeyEvent.getCode() == KeyCode.ENTER) {
int selectIdx;
if (lastKeyEvent.isShiftDown()) {
selectIdx = editingPosition.getRow() - 1;
} else {
selectIdx = editingPosition.getRow() + 1;
}
if (selectIdx < 0 ) {
selectIdx = tableView.getItems().size() - 1;
} else if (selectIdx > tableView.getItems().size() - 1) {
selectIdx = 0;
}
tableView.layout();
tableView.scrollTo(selectIdx == 0 ? selectIdx : selectIdx - 1);
tableView.getSelectionModel().clearAndSelect(selectIdx);
tableView.edit(selectIdx, editingPosition.getTableColumn());
} else if (lastKeyEvent.getCode() == KeyCode.TAB) {
TableColumn colToEdit = getNextColumn(!lastKeyEvent.isShiftDown(), editingPosition.getTableColumn());
if (colToEdit != null) {
tableView.layout();
tableView.scrollToColumn(colToEdit);
tableView.edit(editingPosition.getRow(), colToEdit);
}
}
editingPosition = null;
}
boolean isNavigating() {
return editingPosition != null;
}
private TableColumn getNextColumn(boolean forward, TableColumn currentCol) {
List<TableColumn> columns = new ArrayList<>();
for (TableColumn col : tableView.getColumns()) {
if (col.isEditable() && col.isVisible() && (col.getStyleClass().contains("editable-col") || col.getStyleClass().contains("result-col"))) {
columns.add(col);
}
}
if (columns.size() < 2) {
return null;
}
int currentIndex = -1;
for (int i = 0; i < columns.size(); i++) {
if (columns.get(i) == currentCol) {
currentIndex = i;
break;
}
}
int nextIndex = currentIndex;
if (forward) {
nextIndex++;
if (nextIndex > columns.size() - 1) {
nextIndex = 0;
}
} else {
nextIndex--;
if (nextIndex < 0) {
nextIndex = columns.size() - 1;
}
}
return columns.get(nextIndex);
}
}
public class ExampleClass{
CellNav cellNav;
public ExampleClass() {
TableView yourTableView = new TableView();
cellNav = new CellNav(yourTableView);
TableColumn someCol = new TableColumn();
someCol.setOnEditCommit(e -> {
//perform a save of your table data here
//pick up cell nav again
if (cellNav.isNavigating()) {
cellNav.doCellNav();
}
});
}
}

JavaFX: Fit Columns of TableView to content to avoid cut the content

I have following problem: When i create a TableView and put data in the columns, the columns fit to content automatically. But if there many rows (more than 30) JavaFX optimize the columnwidth to the average length of all content.
In the first example i put the long strings first in the table and everything is fine.
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Example extends Application {
#Override
public void start(Stage stage) {
// Init data
ObservableList<Person> persons = FXCollections.observableArrayList();
persons.add(new Person("Maximus-Superman", "Power", "Maximus-Superman.Power#test.com"));
persons.add(new Person("Max", "Powerstrongsupercool", "Max.Powerstrongsupercool#test.com"));
persons.add(new Person("Maximus-Superman", "Powerstrongsupercool", "Maximus-Superman.Powerstrongsupercool#test.com"));
for (int i = 0; i < 100; i++) {
persons.add(new Person("Max", "Power", "Max.Power#test.com"));
}
// Init table
TableView<Person> table = new TableView<Person>();
table.setMaxWidth(Double.MAX_VALUE);
table.setMaxHeight(Double.MAX_VALUE);
table.setItems(persons);
// Init columns
TableColumn<Person, String> firstname = new TableColumn<Person, String>("Firstname");
firstname.setCellValueFactory(new PropertyValueFactory<Person, String>("firstname"));
table.getColumns().add(firstname);
TableColumn<Person, String> lastname = new TableColumn<Person, String>("Lastname");
lastname.setCellValueFactory(new PropertyValueFactory<Person, String>("lastname"));
table.getColumns().add(lastname);
TableColumn<Person, String> email = new TableColumn<Person, String>("E-Mail");
email.setCellValueFactory(new PropertyValueFactory<Person, String>("email"));
table.getColumns().add(email);
// Init Stage
Scene scene = new Scene(table, 400, 150);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
public class Person {
private String firstname;
private String lastname;
private String email;
public Person(String firstname, String lastname, String email) {
this.firstname = firstname;
this.lastname = lastname;
this.email = email;
}
// Getters and Setters
}
}
Looks good...
In the second example i put short strings first in the table and at least the long strings. In this example JavaFX choose columnwidth to small and the content gets cut.
public class Example extends Application {
#Override
public void start(Stage stage) {
// Init data
ObservableList<Person> persons = FXCollections.observableArrayList();
for (int i = 0; i < 100; i++) {
persons.add(new Person("Max", "Power", "Max.Power#test.com"));
}
persons.add(new Person("Maximus-Superman", "Power", "Maximus-Superman.Power#test.com"));
persons.add(new Person("Max", "Powerstrongsupercool", "Max.Powerstrongsupercool#test.com"));
persons.add(new Person("Maximus-Superman", "Powerstrongsupercool", "Maximus-Superman.Powerstrongsupercool#test.com"));
[...]
Looks bad...
How can i avoid this?​
EDIT 19.05.2018
The links in the comments didn`t work.
So, i have found the problem in the source of JavaFx:
In the updateScene() method the maxRows are set to 30. There is no way to change the value because every methods that are involved are protected or private.
The comment is right. It can be take much time to create a table if there many rows in the table. But sometimes a developer knows the much possible rows of a table or it´s okay to risk higher loadingtime.
One solution is to contact Oracle to create a setter() for the value of max. rows. So the developer can choose the max. rows for each column individually.
public class TableColumnHeader extends Region {
[...]
private void updateScene() {
// RT-17684: If the TableColumn widths are all currently the default,
// we attempt to 'auto-size' based on the preferred width of the first
// n rows (we can't do all rows, as that could conceivably be an unlimited
// number of rows retrieved from a very slow (e.g. remote) data source.
// Obviously, the bigger the value of n, the more likely the default
// width will be suitable for most values in the column
final int n = 30; // ------------------------------------------> This is the problem!
if (! autoSizeComplete) {
if (getTableColumn() == null || getTableColumn().getWidth() != DEFAULT_COLUMN_WIDTH || getScene() == null) {
return;
}
doColumnAutoSize(getTableColumn(), n);
autoSizeComplete = true;
}
}
[...]
private void doColumnAutoSize(TableColumnBase<?,?> column, int cellsToMeasure) {
double prefWidth = column.getPrefWidth();
// if the prefWidth has been set, we do _not_ autosize columns
if (prefWidth == DEFAULT_COLUMN_WIDTH) {
getTableViewSkin().resizeColumnToFitContent(column, cellsToMeasure);
}
}
[...]
}
public class TableViewSkin<T> extends TableViewSkinBase<T, T, TableView<T>, TableViewBehavior<T>, TableRow<T>, TableColumn<T, ?>> {
[...]
#Override protected void resizeColumnToFitContent(TableColumn<T, ?> tc, int maxRows) {
if (!tc.isResizable()) return;
// final TableColumn<T, ?> col = tc;
List<?> items = itemsProperty().get();
if (items == null || items.isEmpty()) return;
Callback/*<TableColumn<T, ?>, TableCell<T,?>>*/ cellFactory = tc.getCellFactory();
if (cellFactory == null) return;
TableCell<T,?> cell = (TableCell<T, ?>) cellFactory.call(tc);
if (cell == null) return;
// set this property to tell the TableCell we want to know its actual
// preferred width, not the width of the associated TableColumnBase
cell.getProperties().put(TableCellSkin.DEFER_TO_PARENT_PREF_WIDTH, Boolean.TRUE);
// determine cell padding
double padding = 10;
Node n = cell.getSkin() == null ? null : cell.getSkin().getNode();
if (n instanceof Region) {
Region r = (Region) n;
padding = r.snappedLeftInset() + r.snappedRightInset();
}
int rows = maxRows == -1 ? items.size() : Math.min(items.size(), maxRows); // ------------------> if maxRows equals -1 every item will be checked
double maxWidth = 0;
for (int row = 0; row < rows; row++) {
cell.updateTableColumn(tc);
cell.updateTableView(tableView);
cell.updateIndex(row);
if ((cell.getText() != null && !cell.getText().isEmpty()) || cell.getGraphic() != null) {
getChildren().add(cell);
cell.applyCss();
maxWidth = Math.max(maxWidth, cell.prefWidth(-1));
getChildren().remove(cell);
}
}
// dispose of the cell to prevent it retaining listeners (see RT-31015)
cell.updateIndex(-1);
// RT-36855 - take into account the column header text / graphic widths.
// Magic 10 is to allow for sort arrow to appear without text truncation.
TableColumnHeader header = getTableHeaderRow().getColumnHeaderFor(tc);
double headerTextWidth = Utils.computeTextWidth(header.label.getFont(), tc.getText(), -1);
Node graphic = header.label.getGraphic();
double headerGraphicWidth = graphic == null ? 0 : graphic.prefWidth(-1) + header.label.getGraphicTextGap();
double headerWidth = headerTextWidth + headerGraphicWidth + 10 + header.snappedLeftInset() + header.snappedRightInset();
maxWidth = Math.max(maxWidth, headerWidth);
// RT-23486
maxWidth += padding;
if(tableView.getColumnResizePolicy() == TableView.CONSTRAINED_RESIZE_POLICY) {
maxWidth = Math.max(maxWidth, tc.getWidth());
}
tc.impl_setWidth(maxWidth);
}
[...]
}
Another solution is to fire a MouseEvent on the header of the rect of the TableColumn.
If there a MouseEvent with a ClickCount equals 2 and the PrimaryButton is down the resizeColumnToFitContent() method is called with a value for maxRows of -1.
int rows = maxRows == -1 ? items.size() : Math.min(items.size(), maxRows);
-1 means all rows that are in the TableView.
public class NestedTableColumnHeader extends TableColumnHeader {
[...]
private static final EventHandler<MouseEvent> rectMousePressed = new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent me) {
Rectangle rect = (Rectangle) me.getSource();
TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
if (! header.isColumnResizingEnabled()) return;
if (me.getClickCount() == 2 && me.isPrimaryButtonDown()) {
// the user wants to resize the column such that its
// width is equal to the widest element in the column
header.getTableViewSkin().resizeColumnToFitContent(column, -1); // -----------------------> this method should be call and everything is fine
} else {
// rather than refer to the rect variable, we just grab
// it from the source to prevent a small memory leak.
Rectangle innerRect = (Rectangle) me.getSource();
double startX = header.getTableHeaderRow().sceneToLocal(innerRect.localToScene(innerRect.getBoundsInLocal())).getMinX() + 2;
header.dragAnchorX = me.getSceneX();
header.columnResizingStarted(startX);
}
me.consume();
}
};
[...]
}
So, is it possible to create a new MouseEvent with a ClickCount of 2 and the PrimaryButtonDown-boolean is true and fire this to the TableColumn?
And: How can i contact Oracle to please them to create a setter() for the maxRows in the next release?

Gluon Mobile Cardpane UI Enhancements: Cardcell Generation/Deletion & Cardpane Styling

I'm trying to create a cardpane with custom HBox CardCells.
Issue #1
How do I set the background of this CardPane? I want it to be transparent, but it won't change from this grey color. I have tried adding styling to the node directly as well as add a custom stylesheet. I have also tried the setBackground method:
Issue #2
Taken from this SO post, I was able to add an animation for cell generation in which it fades in upwards. However, in random card inserts, different cells lose the node that I have embedded in that cell. I don't know if this is because of the recycling concept of these cards (based on Gluon docs) or what:
Issue #3
I created functionality such that the user can delete the cards by swiping left. However, the same issue from Issue #2 arises, but to an even greater extent in which the entire cell is missing but still taking space. If I have only one cell and swipe left, it works all the time. However when I have more than one cell (for example I have 3 cells and I delete the 2nd cell), things get broken, event handlers for cells get removed, swiping left on one cell starts the animation on a cell below it, etc. Is there a way I can perform this functionality or is my best bet to just get rid of the CardPane and use a combination of VBox and HBox elements?
private void addToCardPane(CustomCard newCard) {
ObservableList<Node> items = cardpane.getItems();
boolean override = false;
for (int i = 0; i < cardpane.getItems().size(); i++) {
CustomCard box = (CustomCard) items.get(i);
if (box.checkEquality(newCard)) {
box.increaseNumber(newCard);
override = true;
break;
}
}
if (override == false) {
cardpane.getItems().add(newCard);
cardpane.layout();
VirtualFlow vf = (VirtualFlow) cardpane.lookup(".virtual-flow");
Node cell = vf.getCell(cardpane.getItems().size() - 1);
cell.setTranslateX(0);
cell.setOpacity(1.0);
if (!cardpane.lookup(".scroll-bar").isVisible()) {
FadeInUpTransition f = new FadeInUpTransition(cell);
f.setRate(2);
f.play();
} else {
PauseTransition p = new PauseTransition(Duration.millis(20));
p.setOnFinished(e -> {
vf.getCell(cardpane.getItems().size() - 1).setOpacity(0);
vf.show(cardpane.getItems().size() - 1);
FadeTransition f = new FadeTransition();
f.setDuration(Duration.seconds(1));
f.setFromValue(0);
f.setToValue(1);
f.setNode(vf.getCell(cardpane.getItems().size() - 1));
f.setOnFinished(t -> {
});
f.play();
});
p.play();
}
}
initializeDeletionLogic();
}
private void initializeDeletionLogic() {
VirtualFlow vf = (VirtualFlow) cardpane.lookup(".virtual-flow");
for (int i = 0; i < cardpane.getItems().size(); i++) {
CustomCard card = (CustomCard ) cardpane.getItems().get(i);
Node cell2 = vf.getCell(i);
addRemovalLogicForCell(card, cell2);
}
}
private static double initX = 0;
private void addRemovalLogicForCell(OpioidCard card, Node cell) {
card.setOnMousePressed(e -> {
initX = e.getX();
});
card.setOnMouseDragged(e -> {
double current = e.getX();
if (current < initX) {
if ((current - initX) < 0 && (current - initX) > -50) {
cell.setTranslateX(current - initX);
}
}
});
card.setOnMouseReleased(e -> {
double current = e.getX();
double delta = current - initX;
System.out.println(delta);
if (delta > -50) {
int originalMillis = 500;
double ratio = (50 - delta) / 50;
int newMillis = (int) (500 * ratio);
TranslateTransition translate = new TranslateTransition(Duration.millis(newMillis));
translate.setToX(0);
translate.setNode(cell);
translate.play();
} else {
FadeTransition ft = new FadeTransition(Duration.millis(300), cell);
ft.setFromValue(1.0);
ft.setToValue(0);
TranslateTransition translateTransition
= new TranslateTransition(Duration.millis(300), cell);
translateTransition.setFromX(cell.getTranslateX());
translateTransition.setToX(-400);
ParallelTransition parallel = new ParallelTransition();
parallel.getChildren().addAll(ft, translateTransition);
parallel.setOnFinished(evt -> {
removeCard(card);
ObservableList<CustomCard > cells = FXCollections.observableArrayList();
for(int i = 0; i < this.cardpane.getItems().size(); i++){
cells.add((CustomCard )this.cardpane.getItems().get(i));
}
this.cardpane.getItems().clear();
for(int i = 0; i < cells.size(); i++){
this.cardpane.getItems().add(cells.get(i));
}
initializeDeletionLogic();
initX = 0;
});
parallel.play();
}
});
}
private void removeCard(OpioidCard card) {
for (int i = 0; i < cardpane.getItems().size(); i++) {
if (cardpane.getItems().get(i) == card) {
cardpane.getItems().remove(i);
updateNumber(this.totalNumber);
break;
}
}
for (int i = 0; i < dataList.size(); i++) {
if (dataList.get(i).getName().equalsIgnoreCase(card.getName())) {
dataList.remove(i);
}
}
this.cardpane.layout();
initializeDeletionLogic();
}
WORKING DEMO OF ISSUE:
package com.mobiletestapp;
import com.gluonhq.charm.glisten.animation.FadeInUpTransition;
import com.gluonhq.charm.glisten.control.AppBar;
import com.gluonhq.charm.glisten.control.CardCell;
import com.gluonhq.charm.glisten.control.CardPane;
import com.gluonhq.charm.glisten.mvc.View;
import com.gluonhq.charm.glisten.visual.MaterialDesignIcon;
import com.sun.javafx.scene.control.skin.VirtualFlow;
import javafx.animation.FadeTransition;
import javafx.animation.ParallelTransition;
import javafx.animation.PauseTransition;
import javafx.animation.TranslateTransition;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
public class BasicView extends View {
class CustomCard extends StackPane{
public CustomCard(String text){
this.getChildren().add(new Label(text));
}
}
private static double initX = 0;
private static void addRemovalLogicForCell(CustomCard card, Node cell) {
card.setOnMousePressed(e -> {
initX = e.getX();
});
card.setOnMouseDragged(e -> {
double current = e.getX();
if (current < initX) {
if ((current - initX) < 0 && (current - initX) > -50) {
cell.setTranslateX(current - initX);
}
}
});
card.setOnMouseReleased(e -> {
double current = e.getX();
double delta = current - initX;
System.out.println(delta);
if (delta > -50) {
int originalMillis = 500;
double ratio = (50 - delta) / 50;
int newMillis = (int) (500 * ratio);
TranslateTransition translate = new TranslateTransition(Duration.millis(newMillis));
translate.setToX(0);
translate.setNode(cell);
translate.play();
} else {
FadeTransition ft = new FadeTransition(Duration.millis(300), cell);
ft.setFromValue(1.0);
ft.setToValue(0);
TranslateTransition translateTransition
= new TranslateTransition(Duration.millis(300), cell);
translateTransition.setFromX(cell.getTranslateX());
translateTransition.setToX(-400);
ParallelTransition parallel = new ParallelTransition();
parallel.getChildren().addAll(ft, translateTransition);
parallel.setOnFinished(evt -> {
for(int i = 0; i < cardPane.getItems().size(); i++){
if(cardPane.getItems().get(i) == card){
cardPane.getItems().remove(i);
}
}
initX = 0;
});
parallel.play();
}
});
}
private static CardPane cardPane = null;
public BasicView(String name) {
super(name);
cardPane = new CardPane();
cardPane.setCellFactory(p -> new CardCell<CustomCard>() {
#Override
public void updateItem(CustomCard item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
setText(null);
setGraphic(item);
} else {
setText(null);
setGraphic(null);
}
}
});
setCenter(cardPane);
}
private static void addCard(CustomCard newCard){
cardPane.getItems().add(newCard);
cardPane.layout();
VirtualFlow vf = (VirtualFlow) cardPane.lookup(".virtual-flow");
Node cell = vf.getCell(cardPane.getItems().size() - 1);
cell.setTranslateX(0);
cell.setOpacity(1.0);
if (!cardPane.lookup(".scroll-bar").isVisible()) {
FadeInUpTransition f = new FadeInUpTransition(cell);
f.setRate(2);
f.play();
} else {
PauseTransition p = new PauseTransition(Duration.millis(20));
p.setOnFinished(e -> {
vf.getCell(cardPane.getItems().size() - 1).setOpacity(0);
vf.show(cardPane.getItems().size() - 1);
FadeTransition f = new FadeTransition();
f.setDuration(Duration.seconds(1));
f.setFromValue(0);
f.setToValue(1);
f.setNode(vf.getCell(cardPane.getItems().size() - 1));
f.setOnFinished(t -> {
});
f.play();
});
p.play();
}
addRemovalLogicForCell(newCard, cell);
}
#Override
protected void updateAppBar(AppBar appBar) {
appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> System.out.println("Menu")));
appBar.setTitleText("Basic View");
appBar.getActionItems().add(MaterialDesignIcon.ADD.button(e -> addCard(new CustomCard("Hello"))));
}
}
This leads to the following output when adding and swiping left for deletion:
If you check with ScenicView, you will notice that the CardPane holds a CharmListView control, which in terms uses an inner ListView that takes the size of its parent.
So this should work:
.card-pane > .charm-list-view > .list-view {
-fx-background-color: transparent;
}
As I mentioned, the control is based on a ListView, so the way to provide cells is using the cell factory. As you can read in the control's JavaDoc:
The CardPane is prepared for a big number of items by reusing its cards.
A developer may personalize cell creation by specifying a cell factory through cellFactoryProperty(). The default cell factory is prepared to accept objects from classes that extend Node or other classes that don't extend from Node, in the latter case the card text will be given by the Object.toString() implementation of the object.
If you are not using it yet, consider using something like this:
cardPane.setCellFactory(p -> new CardCell<T>() {
#Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
setText(null);
setGraphic(createContent(item));
} else {
setText(null);
setGraphic(null);
}
}
});
This should manage for you the cards layout, avoiding blank cells or wrong reuse of them.
As for the animation, there shouldn't be a problem in using it.
For swipe animations, the Comments2.0 sample provides a similar use case: A ListView where each cell uses a SlidingListTile. Have a look at its implementation.
You should be able to reuse it with the CardPane.
Try it out, and if you still have issues, post a working sample here (or provide a link), so we can reproduce them.
EDIT
Based on the posted code, a comment related to how the factory cell should be set:
All the JavaFX controls using cells (like ListView or TableView), and also the Gluon CardPane, follow the MVC pattern:
Model. The control is bound to a model, using an observable list of items of that model. In the case of the sample, a String, or any regular POJO, or, as the preferred choice, a JavaFX bean (with observable properties).
So in this case, you should have:
CardPane<String> cardPane = new CardPane<>();
View. The control has a method to set how the cell renders the model, the cellFactory. This factory can define just text, or any graphic node, like your CustomCard.
In this case, you should have:
cardPane.setCellFactory(p -> new CardCell<String>() {
private final CustomCard card;
{
card = new CustomCard();
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
card.setText(item);
setGraphic(card);
setText(null);
} else {
setGraphic(null);
setText(null);
}
}
});
where:
class CustomCard extends StackPane {
private final Label label;
public CustomCard(){
label = new Label();
getChildren().add(label);
}
public void setText(String text) {
label.setText(text);
}
}
Internally, the control uses a VirtualFlow that manages to reuse cells, and only modify the content (the model) when scrolling.
As you can see in the cell factory, now you'll iterate over the model (String), while the CustomCard remains the same, and only the content its updated.
Using this approach doesn't present any of the issues you have described, at least when adding cells.
EDIT 2
I've come up with a solution that works fine for me and should solve all the issues mentioned. Besides what was mentioned before, it is also required restoring the transformations applied to the CustomCard in the updateItem callbacks.
public class BasicView extends View {
private final CardPane<String> cardPane;
public BasicView(String name) {
super(name);
cardPane = new CardPane<>();
cardPane.setCellFactory(p -> new CardCell<String>() {
private final CustomCard card;
private final HBox box;
{
card = new CustomCard();
card.setMaxWidth(Double.MAX_VALUE);
card.prefWidthProperty().bind(widthProperty());
box = new HBox(card);
box.setAlignment(Pos.CENTER);
box.setStyle("-fx-background-color: grey");
addRemovalLogicForCell(card);
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
card.setText(item);
card.setTranslateX(0);
card.setOpacity(1.0);
setGraphic(box);
setText(null);
} else {
setGraphic(null);
setText(null);
}
}
});
setCenter(cardPane);
}
class CustomCard extends StackPane {
private final Label label;
public CustomCard(){
label = new Label();
label.setStyle("-fx-font-size: 20;");
getChildren().add(label);
setStyle("-fx-padding: 20; -fx-background-color: white");
setPrefHeight(100);
}
public void setText(String text) {
label.setText(text);
}
public String getText() {
return label.getText();
}
}
private double initX = 0;
private void addRemovalLogicForCell(CustomCard card) {
card.setOnMousePressed(e -> {
initX = e.getX();
});
card.setOnMouseDragged(e -> {
double current = e.getX();
if ((current - initX) < 0 && (current - initX) > -50) {
card.setTranslateX(current - initX);
}
});
card.setOnMouseReleased(e -> {
double current = e.getX();
double delta = current - initX;
if (delta < 50) {
if (delta > -50) {
int originalMillis = 500;
double ratio = (50 - delta) / 50;
int newMillis = (int) (500 * ratio);
TranslateTransition translate = new TranslateTransition(Duration.millis(newMillis));
translate.setToX(0);
translate.setNode(card);
translate.play();
} else {
FadeTransition ft = new FadeTransition(Duration.millis(300), card);
ft.setFromValue(1.0);
ft.setToValue(0);
TranslateTransition translateTransition
= new TranslateTransition(Duration.millis(300), card);
translateTransition.setFromX(card.getTranslateX());
translateTransition.setToX(-400);
ParallelTransition parallel = new ParallelTransition();
parallel.getChildren().addAll(ft, translateTransition);
parallel.setOnFinished(evt -> {
cardPane.getItems().remove(card.getText());
initX = 0;
});
parallel.play();
}
}
});
}
private void addCard(String newCard){
cardPane.getItems().add(newCard);
cardPane.layout();
VirtualFlow vf = (VirtualFlow) cardPane.lookup(".virtual-flow");
IndexedCell cell = vf.getCell(cardPane.getItems().size() - 1);
cell.setTranslateX(0);
cell.setOpacity(0);
if (! cardPane.lookup(".scroll-bar").isVisible()) {
FadeInUpTransition f = new FadeInUpTransition(cell, true);
f.setRate(2);
f.play();
} else {
PauseTransition p = new PauseTransition(Duration.millis(20));
p.setOnFinished(e -> {
vf.show(cardPane.getItems().size() - 1);
FadeInTransition f = new FadeInTransition(cell);
f.setRate(2);
f.play();
});
p.play();
}
}
#Override
protected void updateAppBar(AppBar appBar) {
appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> System.out.println("Menu")));
appBar.setTitleText("Basic View");
appBar.getActionItems().add(MaterialDesignIcon.ADD.button(e -> addCard("Hello #" + new Random().nextInt(100))));
}
}

JavaFX Auto Scroll Table Up or Down When Dragging Rows Outside Of Viewport

I've got a table view which you can drag rows to re-position the data. The issue is getting the table view to auto scroll up or down when dragging the row above or below the records within the view port.
Any ideas how this can be achieved within JavaFX?
categoryProductsTable.setRowFactory(tv -> {
TableRow<EasyCatalogueRow> row = new TableRow<EasyCatalogueRow>();
row.setOnDragDetected(event -> {
if (!row.isEmpty()) {
Dragboard db = row.startDragAndDrop(TransferMode.MOVE);
db.setDragView(row.snapshot(null, null));
ClipboardContent cc = new ClipboardContent();
cc.put(SERIALIZED_MIME_TYPE, new ArrayList<Integer>(categoryProductsTable.getSelectionModel().getSelectedIndices()));
db.setContent(cc);
event.consume();
}
});
row.setOnDragOver(event -> {
Dragboard db = event.getDragboard();
if (db.hasContent(SERIALIZED_MIME_TYPE)) {
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
event.consume();
}
});
row.setOnDragDropped(event -> {
Dragboard db = event.getDragboard();
if (db.hasContent(SERIALIZED_MIME_TYPE)) {
int dropIndex;
if (row.isEmpty()) {
dropIndex = categoryProductsTable.getItems().size();
} else {
dropIndex = row.getIndex();
}
ArrayList<Integer> indexes = (ArrayList<Integer>) db.getContent(SERIALIZED_MIME_TYPE);
for (int index : indexes) {
EasyCatalogueRow draggedProduct = categoryProductsTable.getItems().remove(index);
categoryProductsTable.getItems().add(dropIndex, draggedProduct);
dropIndex++;
}
event.setDropCompleted(true);
categoryProductsTable.getSelectionModel().select(null);
event.consume();
updateSortIndicies();
}
});
return row;
});
Ok, so I figured it out. Not sure it's the best way to do it but it works. Basically I added an event listener to the table view which handles the DragOver event. This event is fired whilst dragging the rows within the table view.
Essentially, whilst the drag is being performed, I work out if we need to scroll up or down or not scroll at all. This is done by working out if the items being dragged are within either the upper or lower proximity areas of the table view.
A separate thread controlled by the DragOver event listener then handles the scrolling.
public class CategoryProductsReportController extends ReportController implements Initializable {
#FXML
private TableView<EasyCatalogueRow> categoryProductsTable;
private ObservableList<EasyCatalogueRow> categoryProducts = FXCollections.observableArrayList();
public enum ScrollMode {
UP, DOWN, NONE
}
private AutoScrollableTableThread autoScrollThread = null;
/**
* Initializes the controller class.
*/
#Override
public void initialize(URL url, ResourceBundle rb) {
initProductTable();
}
private void initProductTable() {
categoryProductsTable.setItems(categoryProducts);
...
...
// Multi Row Drag And Drop To Allow Items To Be Re-Positioned Within
// Table
categoryProductsTable.setRowFactory(tv -> {
TableRow<EasyCatalogueRow> row = new TableRow<EasyCatalogueRow>();
row.setOnDragDetected(event -> {
if (!row.isEmpty()) {
Dragboard db = row.startDragAndDrop(TransferMode.MOVE);
db.setDragView(row.snapshot(null, null));
ClipboardContent cc = new ClipboardContent();
cc.put(SERIALIZED_MIME_TYPE, new ArrayList<Integer>(categoryProductsTable.getSelectionModel().getSelectedIndices()));
db.setContent(cc);
event.consume();
}
});
row.setOnDragOver(event -> {
Dragboard db = event.getDragboard();
if (db.hasContent(SERIALIZED_MIME_TYPE)) {
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
event.consume();
}
});
row.setOnDragDropped(event -> {
Dragboard db = event.getDragboard();
if (db.hasContent(SERIALIZED_MIME_TYPE)) {
int dropIndex;
if (row.isEmpty()) {
dropIndex = categoryProductsTable.getItems().size();
} else {
dropIndex = row.getIndex();
}
ArrayList<Integer> indexes = (ArrayList<Integer>) db.getContent(SERIALIZED_MIME_TYPE);
for (int index : indexes) {
EasyCatalogueRow draggedProduct = categoryProductsTable.getItems().remove(index);
categoryProductsTable.getItems().add(dropIndex, draggedProduct);
dropIndex++;
}
event.setDropCompleted(true);
categoryProductsTable.getSelectionModel().select(null);
event.consume();
updateSortIndicies();
}
});
return row;
});
categoryProductsTable.addEventFilter(DragEvent.DRAG_DROPPED, event -> {
if (autoScrollThread != null) {
autoScrollThread.stopScrolling();
autoScrollThread = null;
}
});
categoryProductsTable.addEventFilter(DragEvent.DRAG_OVER, event -> {
double proximity = 100;
Bounds tableBounds = categoryProductsTable.getLayoutBounds();
double dragY = event.getY();
//System.out.println(tableBounds.getMinY() + " --> " + tableBounds.getMaxY() + " --> " + dragY);
// Area At Top Of Table View. i.e Initiate Upwards Auto Scroll If
// We Detect Anything Being Dragged Above This Line.
double topYProximity = tableBounds.getMinY() + proximity;
// Area At Bottom Of Table View. i.e Initiate Downwards Auto Scroll If
// We Detect Anything Being Dragged Below This Line.
double bottomYProximity = tableBounds.getMaxY() - proximity;
// We Now Make Use Of A Thread To Scroll The Table Up Or Down If
// The Objects Being Dragged Are Within The Upper Or Lower
// Proximity Areas
if (dragY < topYProximity) {
// We Need To Scroll Up
if (autoScrollThread == null) {
autoScrollThread = new AutoScrollableTableThread(categoryProductsTable);
autoScrollThread.scrollUp();
autoScrollThread.start();
}
} else if (dragY > bottomYProximity) {
// We Need To Scroll Down
if (autoScrollThread == null) {
autoScrollThread = new AutoScrollableTableThread(categoryProductsTable);
autoScrollThread.scrollDown();
autoScrollThread.start();
}
} else {
// No Auto Scroll Required We Are Within Bounds
if (autoScrollThread != null) {
autoScrollThread.stopScrolling();
autoScrollThread = null;
}
}
});
}
}
class AutoScrollableTableThread extends Thread {
private boolean running = true;
private ScrollMode scrollMode = ScrollMode.NONE;
private ScrollBar verticalScrollBar = null;
public AutoScrollableTableThread(TableView tableView) {
super();
setDaemon(true);
verticalScrollBar = (ScrollBar) tableView.lookup(".scroll-bar:vertical");
}
#Override
public void run() {
try {
Thread.sleep(300);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
while (running) {
Platform.runLater(() -> {
if (verticalScrollBar != null && scrollMode == ScrollMode.UP) {
verticalScrollBar.setValue(verticalScrollBar.getValue() - 0.01);
} else if (verticalScrollBar != null && scrollMode == ScrollMode.DOWN) {
verticalScrollBar.setValue(verticalScrollBar.getValue() + 0.01);
}
});
try {
sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void scrollUp() {
System.out.println("Start To Scroll Up");
scrollMode = ScrollMode.UP;
running = true;
}
public void scrollDown() {
System.out.println("Start To Scroll Down");
scrollMode = ScrollMode.DOWN;
running = true;
}
public void stopScrolling() {
System.out.println("Stop Scrolling");
running = false;
scrollMode = ScrollMode.NONE;
}
}

Resources