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.
i have a problem with my searching method.
With this method, I can enter a word in the textfield and display the word in the textarea. However, this only happens once if i let it run. I need to expand it so, that every time I click on "enter," the program should continue with searching in the textarea. How can i do this?
And please give me code examples. i have only 2 days left for my presentation.
Thanks a lot for the helps
textfield.setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
if (event.getCode() == KeyCode.ENTER) {
String text = textarea.getText();
Labeled errorText = null;
if (textfield.getText() != null && !textfield.getText().isEmpty()) {
index = textarea.getText().indexOf(textfield.getText());
textarea.getText();
if (index == -1) {
errorText.setText("Search key Not in the text");
} else {
// errorText.setText("Found");
textarea.selectRange(index, index + textfield.getLength());
}
}
}
}
});
There's an overloaded version of the indexOf method allowing you to search starting at a specific index. Keep track of the index of your last find and start searching from this position:
#Override
public void start(Stage primaryStage) throws Exception {
TextField textField = new TextField("foo");
TextArea textarea = new TextArea();
for (int i = 0; i < 10; i++) {
textarea.appendText("foo\nbarfoobarfoofoo\n");
}
textField.setOnAction(evt -> {
String searchText = textField.getText();
if (searchText.isEmpty()) {
return; // searching for empty text doesn't make sense
}
int index = textarea.getSelection().getEnd();
// in case of the first search, start at the beginning
// TODO: adjust condition/starting index according to needs
if (textarea.getSelection().getLength() == 0) {
index = 0;
}
// find next occurrence
int newStartIndex = textarea.getText().indexOf(searchText, index);
// mark occurrence
if (newStartIndex >= 0) {
textarea.selectRange(newStartIndex, newStartIndex + searchText.length());
}
});
Scene scene = new Scene(new VBox(textField, textarea));
primaryStage.setScene(scene);
primaryStage.show();
}
Edit
If you are not satisfied with searching the element after the selection ( or after the cursor, if there is no range selected), you could save the data of the end of the last match:
#Override
public void start(Stage primaryStage) throws Exception {
TextField textField = new TextField("foo");
TextArea textarea = new TextArea();
for (int i = 0; i < 10; i++) {
textarea.appendText("foo\nbarfoobarfoofoo\n");
}
class SearchHandler implements EventHandler<ActionEvent> {
int index = 0;
#Override
public void handle(ActionEvent event) {
String searchText = textField.getText();
String fullText = textarea.getText();
if (index + searchText.length() > fullText.length()) {
// no more matches possible
// TODO: notify user
return;
}
// find next occurrence
int newStartIndex = textarea.getText().indexOf(searchText, index);
// mark occurrence
if (newStartIndex >= 0) {
index = newStartIndex + searchText.length();
textarea.selectRange(newStartIndex, index);
} else {
index = fullText.length();
// TODO: notify user
}
}
}
SearchHandler handler = new SearchHandler();
textField.setOnAction(handler);
// reset index to search from start when changing the text of the TextField
textField.textProperty().addListener((o, oldValue, newValue) -> handler.index = 0);
Scene scene = new Scene(new VBox(textField, textarea));
primaryStage.setScene(scene);
primaryStage.show();
}
I am writing a TicTacToe game in JavaFX. I've decided to make a board as 9 (3x3) buttons with changing text: "" (if empty) or "X" or "O". Everything is going ok beside one thing... I got stuck here:
public void game() {
while(keepPlaying) {
if(computerTurn) {;
computerMove();
}else {
while(waitForUser) {
//wait until any of 9 buttons is pushed!
}
}
if (checkResult()) {
keepPlaying = false;
}
computerTurn = !computerTurn;
}
}
How to wait for user pushing any of those 9 buttons and then continue with computer turn??
I need something like waiting for scanner input in console application, but this input must be one of 9 buttons...
I know that there are few "possible duplicates", but in fact those problems were solved using methods I can't use here, for example timer. Correct me if I am wrong.
Blocking the application thread in JavaFX should not be done since it freezes the UI. For this reason a loop like this is not well suited for a JavaFX application. Instead you should react to user input:
public void game() {
if (keepPlaying && computerTurn) {
computerMove();
if (checkResult()) {
keepPlaying = false;
}
computerTurn = false;
}
}
// button event handler
private void button(ActionEvent event) {
if (keepPlaying) {
Button source = (Button) event.getSource();
// probably the following 2 coordinates are computed from GridPane indices
int x = getX(source);
int y = getY(source);
// TODO: update state according to button pressed
if (checkResult()) {
keepPlaying = false;
} else {
computerMove();
if (checkResult()) {
keepPlaying = false;
}
}
}
}
Starting with javafx 9 there is a public API for "pausing" on the application thread however:
private static class GridCoordinates {
int x,y;
GridCoordinates (int x, int y) {
this.x = x;
this.y = y;
}
}
private final Object loopKey = new Object();
public void game() {
while(keepPlaying) {
if(computerTurn) {
computerMove();
} else {
// wait for call of Platform.exitNestedEventLoop​(loopKey, *)
GridCoordinates coord = (GridCoordinates) Platform.enterNestedEventLoop​(loopKey);
// TODO: update state
}
if (checkResult()) {
keepPlaying = false;
}
computerTurn = !computerTurn;
}
}
private void button(ActionEvent event) {
if (keepPlaying) {
Button source = (Button) event.getSource();
// probably the following 2 coordinates are computed from GridPane indices
int x = getX(source);
int y = getY(source);
Platform.exitNestedEventLoop​(loopKey, new GridCoordinates(x, y));
}
}
The speed of Spinner update is slow when I click and hold the up/down arrow buttons. Is there a way to increase the change speed?
When I click, click, click with the mouse, the spinner values change as fast as I click. It also changes fast if I use the up/down arrows on the keyboard for each key press or if I hold down the up/down arrow keys. I want the values to change that fast when I click and hold on the arrow buttons.
Anyone know a way to do that?
The SpinnerBehavior of the SpinnerSkin triggers updates every 750 ms. Unfortunately there is no way to simply set/modify this behavour without using reflection to access private members. Therefore the only way to do this without reflection is using event filters to trigger the updates at a faster rate:
private static final PseudoClass PRESSED = PseudoClass.getPseudoClass("pressed");
#Override
public void start(Stage primaryStage) {
Spinner<Integer> spinner = new Spinner(Integer.MIN_VALUE, Integer.MAX_VALUE, 0);
class IncrementHandler implements EventHandler<MouseEvent> {
private Spinner spinner;
private boolean increment;
private long startTimestamp;
private static final long DELAY = 1000l * 1000L * 750L; // 0.75 sec
private Node button;
private final AnimationTimer timer = new AnimationTimer() {
#Override
public void handle(long now) {
if (now - startTimestamp >= DELAY) {
// trigger updates every frame once the initial delay is over
if (increment) {
spinner.increment();
} else {
spinner.decrement();
}
}
}
};
#Override
public void handle(MouseEvent event) {
if (event.getButton() == MouseButton.PRIMARY) {
Spinner source = (Spinner) event.getSource();
Node node = event.getPickResult().getIntersectedNode();
Boolean increment = null;
// find which kind of button was pressed and if one was pressed
while (increment == null && node != source) {
if (node.getStyleClass().contains("increment-arrow-button")) {
increment = Boolean.TRUE;
} else if (node.getStyleClass().contains("decrement-arrow-button")) {
increment = Boolean.FALSE;
} else {
node = node.getParent();
}
}
if (increment != null) {
event.consume();
source.requestFocus();
spinner = source;
this.increment = increment;
// timestamp to calculate the delay
startTimestamp = System.nanoTime();
button = node;
// update for css styling
node.pseudoClassStateChanged(PRESSED, true);
// first value update
timer.handle(startTimestamp + DELAY);
// trigger timer for more updates later
timer.start();
}
}
}
public void stop() {
timer.stop();
button.pseudoClassStateChanged(PRESSED, false);
button = null;
spinner = null;
}
}
IncrementHandler handler = new IncrementHandler();
spinner.addEventFilter(MouseEvent.MOUSE_PRESSED, handler);
spinner.addEventFilter(MouseEvent.MOUSE_RELEASED, evt -> {
if (evt.getButton() == MouseButton.PRIMARY) {
handler.stop();
}
});
Scene scene = new Scene(spinner);
primaryStage.setScene(scene);
primaryStage.show();
}
I modified the answer of fabian a little bit to decrease the speed of the spinner while holding mouse down:
private int currentFrame = 0;
private int previousFrame = 0;
#Override
public void handle(long now)
{
if (now - startTimestamp >= initialDelay)
{
// Single or holded mouse click
if (currentFrame == previousFrame || currentFrame % 10 == 0)
{
if (increment)
{
spinner.increment();
}
else
{
spinner.decrement();
}
}
}
++currentFrame;
}
And after stopping the timer we adjust previousFrame again:
public void stop()
{
previousFrame = currentFrame;
[...]
}
A small improvement to Fabian's answer. Making the following mod to the MOUSE_RELEASED addEventerFilter will stop a NullPointerException caused when clicking the textfield associated with the spinner. Cheers Fabian!
spinner.addEventFilter(MouseEvent.MOUSE_RELEASED, evt -> {
Node node = evt.getPickResult().getIntersectedNode();
if (node.getStyleClass().contains("increment-arrow-button") ||
node.getStyleClass().contains("decrement-arrow-button")) {
if (evt.getButton() == MouseButton.PRIMARY) {
handler.stop();
}
}
});
An alternative to changing the update speed might in some cases be adjusting the amount by which the value increments/decrements per update.
SpinnerValueFactory.IntegerSpinnerValueFactory intFactory =
(SpinnerValueFactory.IntegerSpinnerValueFactory) spinner.getValueFactory();
intFactory.setAmountToStepBy(100);
Reference: http://news.kynosarges.org/2016/10/28/javafx-spinner-for-numbers/
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.