I have a JavaFX TableView with editable cells where I need:
This is the table that shows current and desired behaviors:
| Action | Current Behavior | Desired Behavior |
|---|---|---|
| Single-click | Selects cell | (keep) Selects cell |
| Single-click selected | Enters edit mode | (keep) Enters edit mode |
| Double-click | Custom action + edit mode | Selects cell + custom action ONLY |
This is my code:
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class NewMain1 extends Application {
public static class Person {
private final StringProperty firstName;
private final StringProperty lastName;
public Person(String firstName, String lastName) {
this.firstName = new SimpleStringProperty(firstName);
this.lastName = new SimpleStringProperty(lastName);
}
public StringProperty firstNameProperty() { return firstName; }
public StringProperty lastNameProperty() { return lastName; }
}
@Override
public void start(Stage primaryStage) {
TableView<Person> tableView = new TableView<>();
tableView.setEditable(true);
TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
firstNameCol.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
firstNameCol.setCellFactory(TextFieldTableCell.forTableColumn());
firstNameCol.setOnEditStart(e -> System.out.println("First Name Edit Start"));
TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
lastNameCol.setCellValueFactory(cellData -> cellData.getValue().lastNameProperty());
lastNameCol.setCellFactory(TextFieldTableCell.forTableColumn());
lastNameCol.setOnEditStart(e -> System.out.println("Last Name Edit Start"));
tableView.getColumns().addAll(firstNameCol, lastNameCol);
ObservableList<Person> data = FXCollections.observableArrayList(
new Person("John", "Smith"),
new Person("Emily", "Johnson"),
new Person("Michael", "Williams"),
new Person("Sarah", "Brown")
);
tableView.setItems(data);
tableView.setRowFactory(tv -> {
TableRow<Person> row = new TableRow<>();
row.setOnMouseClicked(event -> {
if (event.getClickCount() == 2 && !row.isEmpty()) {
Person person = row.getItem();
System.out.println("DoubleClick on "
+ person.firstNameProperty().get() + " " + person.lastNameProperty().get());
event.consume();
}
});
return row;
});
VBox root = new VBox(tableView);
Scene scene = new Scene(root, 400, 300);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Could anyone say how to do it?
I agree with the comments mentioned by @James_D regarding predicting for next click. By default the single click mouse pressed will do the selection or get to edit mode. If you are desperate to fix this problem, you need to separate the event processing of double click from single click.
You can achieve this by creating some custom events and a custom event dispatcher that can process the events separately.
The below solution involved four steps:
Step#1: Create a custom event and event dispatcher
You can create a custom double pressed event and event dispatcher to separate the events. The general idea is,
The code will be as below. In
/**
* Custom double pressed mouse event.
*/
interface CustomMouseEvent {
EventType<MouseEvent> MOUSE_DOUBLE_PRESSED = new EventType<>(MouseEvent.ANY, "MOUSE_DOUBLE_PRESSED");
}
/**
* Custom EventDispatcher to differentiate from double click and single click.
*/
class DoubleClickEventDispatcher implements EventDispatcher {
/** Default delay to fire a double click event in milliseconds. */
private static final long DEFAULT_DOUBLE_CLICK_DELAY = 215;
/** Default event dispatcher of a node. */
private final EventDispatcher defaultEventDispatcher;
/** Timeline for dispatching mouse clicked event. */
private Timeline singleClickTimeline;
/**
* Constructor.
*
* @param initial Default event dispatcher of a node
*/
public DoubleClickEventDispatcher(final EventDispatcher initial) {
defaultEventDispatcher = initial;
}
@Override
public Event dispatchEvent(final Event event, final EventDispatchChain tail) {
final EventType<? extends Event> type = event.getEventType();
if (type == MouseEvent.MOUSE_PRESSED && ((MouseEvent)event).getButton()== MouseButton.PRIMARY) {
final MouseEvent mouseEvent = (MouseEvent) event;
final EventTarget eventTarget = event.getTarget();
// If it is a double click , stop the single click timeline and fire the double pressed event manually
if (mouseEvent.getClickCount() > 1) {
if (singleClickTimeline != null) {
singleClickTimeline.stop();
singleClickTimeline = null;
final MouseEvent dblClickedEvent = copy(mouseEvent, CustomMouseEvent.MOUSE_DOUBLE_PRESSED);
Event.fireEvent(eventTarget, dblClickedEvent);
}
return mouseEvent;
}
// If it is single click, start a timeline to fire the single click after a certain duration.
if (singleClickTimeline == null) {
final MouseEvent singleClickEvent = copy(mouseEvent, mouseEvent.getEventType());
singleClickTimeline = new Timeline(new KeyFrame(Duration.millis(DEFAULT_DOUBLE_CLICK_DELAY), e -> {
Event.fireEvent(eventTarget, singleClickEvent);
// Because we are firing the pressed event, we have to fire the release event, to clear the cached values in Table classes.
final MouseEvent releaseEvent = copy(singleClickEvent, MouseEvent.MOUSE_RELEASED);
Event.fireEvent(eventTarget, releaseEvent);
singleClickTimeline = null;
}));
// Start a timeline to see if we get a double click in future.
singleClickTimeline.play();
return mouseEvent;
}
}
return defaultEventDispatcher.dispatchEvent(event, tail);
}
/**
* Creates a copy of the provided mouse event type with the mouse event.
*
* @param e MouseEvent
* @param eventType Event type that need to be created
* @return New mouse event instance
*/
private MouseEvent copy(final MouseEvent e, final EventType<? extends MouseEvent> eventType) {
return new MouseEvent(eventType, e.getSceneX(), e.getSceneY(), e.getScreenX(), e.getScreenY(),
e.getButton(), e.getClickCount(), e.isShiftDown(), e.isControlDown(), e.isAltDown(),
e.isMetaDown(), e.isPrimaryButtonDown(), e.isMiddleButtonDown(),
e.isSecondaryButtonDown(), e.isSynthesized(), e.isPopupTrigger(),
e.isStillSincePress(), e.getPickResult());
}
}
Step#2: Set the custom event dispatcher on tableView
Now we have created the dispatcher that separates the two types of events. Set this event dispatcher on the node we are interested in (TableView).
tableView.setEventDispatcher(new DoubleClickEventDispatcher(tableView.getEventDispatcher()));
Step#3: Add the double pressed event on the table row.
row.addEventHandler(CustomMouseEvent.MOUSE_DOUBLE_PRESSED, event -> {
// your code
});
Step#4: Handle the missing functionality that we loose for not firing the single pressed.
Because we are diverting the single pressed event, we want to handle all the unwanted cases in the double pressed event handler. Particularly here, we need to handle the row selection and cancel the edit (if any)
tableView.getSelectionModel().select(person);
if (tableView.getEditingCell() != null) {
tableView.edit(-1, null);
}
Check the below demo and code when combining all the above metioned steps. I hope this can give you some direction on how to approach this requirement.

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.*;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
public class CustomEventClickOnTableCellDemo extends Application {
public static class Person {
private final StringProperty firstName;
private final StringProperty lastName;
public Person(String firstName, String lastName) {
this.firstName = new SimpleStringProperty(firstName);
this.lastName = new SimpleStringProperty(lastName);
}
public StringProperty firstNameProperty() {
return firstName;
}
public StringProperty lastNameProperty() {
return lastName;
}
}
@Override
public void start(Stage primaryStage) {
TableView<Person> tableView = new TableView<>();
tableView.setEditable(true);
TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
firstNameCol.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
firstNameCol.setCellFactory(TextFieldTableCell.forTableColumn());
firstNameCol.setOnEditStart(e -> System.out.println("First Name Edit Start"));
TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
lastNameCol.setCellValueFactory(cellData -> cellData.getValue().lastNameProperty());
lastNameCol.setCellFactory(TextFieldTableCell.forTableColumn());
lastNameCol.setOnEditStart(e -> System.out.println("Last Name Edit Start"));
tableView.getColumns().addAll(firstNameCol, lastNameCol);
ObservableList<Person> data = FXCollections.observableArrayList(
new Person("John", "Smith"),
new Person("Emily", "Johnson"),
new Person("Michael", "Williams"),
new Person("Sarah", "Brown")
);
tableView.setItems(data);
/* STEP#2 : Set the custom event dispatcher to tableView */
tableView.setEventDispatcher(new DoubleClickEventDispatcher(tableView.getEventDispatcher()));
tableView.setRowFactory(tv -> {
TableRow<Person> row = new TableRow<>();
/* STEP#3 : Add custom mouse double pressed event. */
row.addEventHandler(CustomMouseEvent.MOUSE_DOUBLE_PRESSED, event -> {
if (!row.isEmpty()) {
Person person = row.getItem();
System.out.println("DoubleClick on "
+ person.firstNameProperty().get() + " " + person.lastNameProperty().get());
/* STEP#4 : On double-click, select the row and cancel the current editing(if any), as we are not processing the single click. */
tableView.getSelectionModel().select(person);
if (tableView.getEditingCell() != null) {
tableView.edit(-1, null);
}
event.consume();
}
});
return row;
});
VBox root = new VBox(tableView);
Scene scene = new Scene(root, 400, 300);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
/* STEP#1 : Create custom events and event dispatcher to separate single and double click events. */
/**
* Custom double pressed mouse event.
*/
interface CustomMouseEvent {
EventType<MouseEvent> MOUSE_DOUBLE_PRESSED = new EventType<>(MouseEvent.ANY, "MOUSE_DOUBLE_PRESSED");
}
/**
* Custom EventDispatcher to differentiate from double click and single click.
*/
class DoubleClickEventDispatcher implements EventDispatcher {
/**
* Default delay to fire a double click event in milliseconds.
*/
private static final long DEFAULT_DOUBLE_CLICK_DELAY = 215;
/**
* Default event dispatcher of a node.
*/
private final EventDispatcher defaultEventDispatcher;
/**
* Timeline for dispatching mouse clicked event.
*/
private Timeline singleClickTimeline;
/**
* Constructor.
*
* @param initial Default event dispatcher of a node
*/
public DoubleClickEventDispatcher(final EventDispatcher initial) {
defaultEventDispatcher = initial;
}
@Override
public Event dispatchEvent(final Event event, final EventDispatchChain tail) {
final EventType<? extends Event> type = event.getEventType();
if (type == MouseEvent.MOUSE_PRESSED && ((MouseEvent)event).getButton()== MouseButton.PRIMARY) {
final MouseEvent mouseEvent = (MouseEvent) event;
final EventTarget eventTarget = event.getTarget();
// If it is a double click , stop the single click timeline and fire the double pressed event manually
if (mouseEvent.getClickCount() > 1) {
if (singleClickTimeline != null) {
singleClickTimeline.stop();
singleClickTimeline = null;
final MouseEvent dblClickedEvent = copy(mouseEvent, CustomMouseEvent.MOUSE_DOUBLE_PRESSED);
Event.fireEvent(eventTarget, dblClickedEvent);
}
return mouseEvent;
}
// If it is single click, start a timeline to fire the single click after a certain duration.
if (singleClickTimeline == null) {
final MouseEvent singleClickEvent = copy(mouseEvent, mouseEvent.getEventType());
singleClickTimeline = new Timeline(new KeyFrame(Duration.millis(DEFAULT_DOUBLE_CLICK_DELAY), e -> {
Event.fireEvent(eventTarget, singleClickEvent);
// Because we are firing the pressed event, we have to fire the release event, to clear the cached values in Table classes.
final MouseEvent releaseEvent = copy(singleClickEvent, MouseEvent.MOUSE_RELEASED);
Event.fireEvent(eventTarget, releaseEvent);
singleClickTimeline = null;
}));
// Start a timeline to see if we get a double click in future.
singleClickTimeline.play();
return mouseEvent;
}
}
return defaultEventDispatcher.dispatchEvent(event, tail);
}
/**
* Creates a copy of the provided mouse event type with the mouse event.
*
* @param e MouseEvent
* @param eventType Event type that need to be created
* @return New mouse event instance
*/
private MouseEvent copy(final MouseEvent e, final EventType<? extends MouseEvent> eventType) {
return new MouseEvent(eventType, e.getSceneX(), e.getSceneY(), e.getScreenX(), e.getScreenY(),
e.getButton(), e.getClickCount(), e.isShiftDown(), e.isControlDown(), e.isAltDown(),
e.isMetaDown(), e.isPrimaryButtonDown(), e.isMiddleButtonDown(),
e.isSecondaryButtonDown(), e.isSynthesized(), e.isPopupTrigger(),
e.isStillSincePress(), e.getPickResult());
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With