buoto
buoto

Reputation: 415

ListView with custom Cell click handling

In my JavaFX app I use ListView with custom cells. I want to handle list item clicks differently than clicks on the empty space below the items. I've set an event listener on whole ListView, but I can't determine which of the items was clicked (getSelectedItem() is null, probably because of bug in my custom cell code). How to handle following situation properly?

My component looks like this:

component with custom cells

<fx:root type="javafx.scene.layout.VBox">
    <Label fx:id="dayname" text="${controller.day.name}" />
    <ListView fx:id="appointmentsListView" items="${controller.day.events}" 
        onMouseClicked="#handleAppointmentsClick" />
</fx:root>

ListView has custom cell factory which is set in component constructor:

public class DayComponent extends VBox {
    @FXML
    private ListView<Appointment> appointmentsListView;

    public DayComponent() throws IOException {
        // ...
        appointmentsListView.setCellFactory(l -> new AppointmentCell());
    }

    @FXML
    public void handleAppointmentsClick(MouseEvent event) {
        System.out.println(appointmentsListView.getSelectionModel()
            .getSelectedItem()); // null in every case
    }
}

Custom cell code:

public class AppointmentCell extends ListCell<Appointment> {

    @Override
    protected void updateItem(Appointment item, boolean empty) {
        super.updateItem(item, empty);
        if (!empty) {
            setGraphic(new AppointmentLabel(item));
        } else {
            setGraphic(null);
        }
    }
}

Upvotes: 1

Views: 4656

Answers (2)

James_D
James_D

Reputation: 209418

One reasonably clean approach is to register a mouse listener with the cells in the list view, to handle clicks on non-empty cells. Consume the mouse event if the cell is non-empty. Register a second mouse listener on the list view itself, to handle clicks on empty cells (or on the list view's placeholder if the list view is empty). This needs two handlers, but at least separates the "empty" and "non-empty" functionality in a sensible way.

Here is a quick example:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class ListViewMouseHandlerExample extends Application {

    private int count = 0 ;

    @Override
    public void start(Stage primaryStage) {
        ListView<String> listView = new ListView<>();

        Button addButton = new Button("Add");
        addButton.setOnAction(e -> listView.getItems().add("Item " + (++count)));

        Button deleteButton = new Button("Delete");
        deleteButton.setOnAction(e -> listView.getItems().remove(listView.getSelectionModel().getSelectedIndex()));
        deleteButton.disableProperty().bind(listView.getSelectionModel().selectedItemProperty().isNull());

        listView.setCellFactory(lv -> {
            ListCell<String> cell = new ListCell<String>() {
                @Override
                protected void updateItem(String item, boolean empty) {
                    super.updateItem(item, empty);
                    setText(item);
                }
            };
            cell.setOnMouseClicked(e -> {
                if (!cell.isEmpty()) {
                    System.out.println("You clicked on " + cell.getItem());
                    e.consume();
                }
            });
            return cell;
        });

        listView.setOnMouseClicked(e -> {
            System.out.println("You clicked on an empty cell");
        });

        BorderPane root = new BorderPane(listView);
        ButtonBar buttons = new ButtonBar();
        buttons.getButtons().addAll(addButton, deleteButton);
        root.setBottom(buttons);


        Scene scene = new Scene(root, 600, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Upvotes: 6

Baptiste Beauvais
Baptiste Beauvais

Reputation: 2086

You could try something like that:

public class AppointmentCellMouseClickHandler implements EventHandler<MouseEvent> {

    private final Appointment appointment;

    public AppointmentCellMouseClickHandler(Appointment appointment) {
        this.appointment = appointment;
    }

    @Override
    public void handle(MouseEvent arg0) {
        //Do stuff with appointment
    }
}

public class EmptyAppointmentCellMouseClickHandler implements EventHandler<MouseEvent> {

    @Override
    public void handle(MouseEvent arg0) {
        //Do stuff without appointment
    }
}

public class AppointmentCell extends ListCell<Appointment> {

    @Override
    protected void updateItem(Appointment item, boolean empty) {
        super.updateItem(item, empty);
        if (!empty) {
            setGraphic(new AppointmentLabel(item));
            this.setOnMouseClicked(new AppointmentCellMouseClickHandler(item));
        } else {
            setGraphic(null);
            this.setOnMouseClicked(new EmptyAppointmentCellMouseClickHandler());
        }
    }
}

Edit related to comment:

To handle when the ListView is empty you can do:

//To handle the initialisation where the listener won't be call
appointmentsListView.setOnMouseClicked(new EmptyAppointmentCellMouseClickHandler());
appointmentsListView.getItems().addListener(new ListChangeListener<Appointment>() {
            @Override
            public void onChanged(Change<? extends Appointment> change) {
                while(change.next()){
                    //Toogle the listener depending on the content of the list
                    listView.setOnMouseClicked(change.getList().isEmpty() ? new EmptyAppointmentCellMouseClickHandler() : null);
                }
            }
        });

Upvotes: 0

Related Questions