Thursday, October 16, 2014

Per-row mouse cursor in JavaFX TableViews

Today I want to show how to set the mouse cursor in a TableView on a per-row basis.

In a real-world scenario this can be used to indicate what will happen when you click on the row (will it be selected? will it work like a link? is it unselectable?, etc.). To that end it will often be necessary to access the data displayed in the row.

In our example we will set a crosshair cursor for Persons with the letter 'U' in their first name.

First we setup a row factory for the table:

    private void setupPerRowCursor(TableView<Person> table) {
        table.setRowFactory(this::createTableRow);
    }


That row factory will create the row and listen for changes to its indexProperty:

    private TableRow<Person> createTableRow(TableView<Person> table) {
        TableRow<Person> row =  new TableRow<Person>();
        row.indexProperty().addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {
            rowChanged(row, newValue.intValue());
        });
        return row;
    }


Remember that rows (just like cells) will be reused in the table view so even if you have millions of data rows, there will only be a couple of TableRow instances. Therefore we must change react to a row being reused:

    private void rowChanged(TableRow<Person> row, int idx) {
        items = row.getTableView().getItems();
        if (idx >= 0 && idx < items.size()) {
            Person p = items.get(idx);
            if (p.getFirstName().contains("U")) {
                row.setCursor(Cursor.CROSSHAIR);
            }
            else {
                row.setCursor(Cursor.DEFAULT);
            }
        }
    }


We get the (newly associated) data by examining the items of the table. The new index the row points to can be out of bounds, e.g. for TableRows that are displayed but do not have a data row associated, therefore we must check idx to be within limits.

Here's a screenshot with the mouse cursor over a person with the (strange) first name HBZZU:


If you go down with the mouse a bit, the cursor will turn into a normal arrow again.

Here's full source code of the example:
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

import org.apache.commons.lang.RandomStringUtils;

public class TableViewMouseCursorPerRow extends Application {

    ObservableList<Person> items = FXCollections.observableArrayList();
    TableView<Person> table = new TableView<Person>();

    public TableViewMouseCursorPerRow() {
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        GridPane grid = new GridPane();
        table.setItems(items);
        TableColumn<Person, String> firstNameCol = new TableColumn<Person, String>("First Name");
        firstNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
        TableColumn<Person, String> lastNameCol = new TableColumn<Person, String>("Last Name");
        lastNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("lastName"));
        table.getColumns().setAll(firstNameCol, lastNameCol);
        setupPerRowCursor(table);
        addPersons();
        grid.add(table, 0, 1, 3, 1);
       
        Scene scene = new Scene(grid, 400, 400);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void setupPerRowCursor(TableView<Person> table) {
        table.setRowFactory(this::createTableRow);
    }
   
    private TableRow<Person> createTableRow(TableView<Person> table) {
        TableRow<Person> row =  new TableRow<Person>();
        row.indexProperty().addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {
            rowChanged(row, newValue.intValue());
        });
        return row;
    }

    private void rowChanged(TableRow<Person> row, int idx) {
        items = row.getTableView().getItems();
        if (idx >= 0 && idx < items.size()) {
            Person p = items.get(idx);
            if (p.getFirstName().contains("U")) {
                row.setCursor(Cursor.CROSSHAIR);
            }
            else {
                row.setCursor(Cursor.DEFAULT);
            }
        }
    }

    private void addPersons() {
        for (int i = 0; i < 50; i++) {
            Person p = new Person();
            p.setFirstName(items.size() + " " + RandomStringUtils.random(5, "ABEGHILOUZ"));
            p.setLastName(RandomStringUtils.random(10, "QWRTOUPL"));
            items.add(p);
        }
    }

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

    public class Person {
        private StringProperty firstName;

        public void setFirstName(String value) {
            firstNameProperty().set(value);
        }

        public String getFirstName() {
            return firstNameProperty().get();
        }

        public StringProperty firstNameProperty() {
            if (firstName == null)
                firstName = new SimpleStringProperty(this, "firstName");
            return firstName;
        }

        private StringProperty lastName;

        public void setLastName(String value) {
            lastNameProperty().set(value);
        }

        public String getLastName() {
            return lastNameProperty().get();
        }

        public StringProperty lastNameProperty() {
            if (lastName == null)
                lastName = new SimpleStringProperty(this, "lastName");
            return lastName;
        }
    }
}

No comments:

Post a Comment