I have a JavaFX TableView with editable cells where I need:
- Single click → selects cell (standard behavior)
- Single click on selected cell → enters edit mode (standard behavior)
- Double click → selects cell and custom action ONLY (without entering edit mode)
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 |
I have a JavaFX TableView with editable cells where I need:
- Single click → selects cell (standard behavior)
- Single click on selected cell → enters edit mode (standard behavior)
- Double click → selects cell and custom action ONLY (without entering edit mode)
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?
Share Improve this question edited Mar 30 at 23:42 Sai Dandem 10.2k14 silver badges27 bronze badges asked Mar 30 at 14:53 SilverCubeSilverCube 6403 silver badges12 bronze badges 4 |3 Answers
Reset to default 3I 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,
- when we receive a single click event, we start a timeline to execute the event after certain duration.
- If we receive another event within this duration, we consider this as a double pressed event, and will fire the custom double pressed event and cancel the previous single pressed event. That way you can separate the two events and handle them separately.
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());
}
}
}
This answer comes with a big caveat: modifying the behavior of JavaFX controls is really not well supported. Doing so typically relies on undocumented implementation details of the control, which are subject to change in later releases. Hence implementing any such functionality is really not very robust.
That said, the current table cell implementation appears to manage selection and editing via listeners for mouse pressed and released events. So consuming a mouse pressed event (note: mouse pressed, not mouse clicked) in an event filter (note: filter not handler, so it is invoked before the cell's internal handlers)
will prevent the default selection and editing behavior. The following (as a replacement for your existing row.setOnMouseClicked(...)
) seems to result in the functionality you specify:
row.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
if (event.getClickCount() == 2 && !row.isEmpty()) {
Person person = row.getItem();
System.out.println("DoubleClick on "
+ person.firstNameProperty().get() + " " + person.lastNameProperty().get());
event.consume();
}
});
I would record the time of the first click in a variable. Then, compare it to the time of the next click before deciding what to do. If the second click is within X milliseconds, then consider it a "double-click" and execute the special action. Else, consider the second click the trigger for entering edit mode.
OK, what happens if someone clicks a field, which selects it, and then double-clicks? With this scenario, it would go into Edit mode and then the second click of the double would do whatever a click does when editing that field.
If that is a problem, one possibility is to set the "edit" click to wait X millis before going to edit mode, giving time for a possible "double click" to assert itself.
I've done this sort of thing with Java, using Timer
, and JavaScript, using setTimeout
. I can't recall if JavaFX has a preferred timer.
For similar logic, check the topic of "debouncing". A quick search gave me a JS example here What is debouncing? but I'm only seeing a couple articles in Medium that show debouncing using Java or JavaFX. I haven't clicked on them because I am conserving my Medium freebies.
Platform.runLater(row::cancelEdit);
orif (tableView.getEditingCell() != null) Platform.runLater(() -> tableView.getEditingCell().cancelEdit());
right after event.consume() will suffice? – VGR Commented Mar 30 at 15:37setOnMouseClicked
? – James_D Commented Mar 30 at 16:52row.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {... event.consume(); })
. It didn't help - same result. – SilverCube Commented Mar 30 at 17:17ContextMenu
to select things I want to happen, like opening a dialog that allows any data on the row to be updated. For me, this approach led to less headaches. Good luck coding! – SedJ601 Commented Mar 30 at 17:55