Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to prevent edit mode on double-click in JavaFX TableView while keeping single-click edit?

Tags:

java

javafx

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?

like image 651
SilverCube Avatar asked Oct 26 '25 05:10

SilverCube


1 Answers

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,

  • 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.

enter image description here

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());
        }
    }
}
like image 134
Sai Dandem Avatar answered Oct 27 '25 19:10

Sai Dandem



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!