Issue
Currently in TableView, the row selection is not happening when we click mouse middle button. The row is selected if we do right or left click. I am trying to have the feature of selecting the row on middle button click.
I am already aware that including an event handler in table row factory can fix this. But I have a custom table view with lot of custom features for my application. And this custom TableView is widely used across my application. My main problem is, I cannot ask each and every table row factory to include this fix.
I am looking for a way to do this on higher level (may be on TableView level) so that the row factory does not need to care of that.
Any ideas/help is highly appreciated.
Below is a quick example of what I am trying to achieve.
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;
public class TableRowSelectionOnMiddleButtonDemo extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
ObservableList<Person> persons = FXCollections.observableArrayList();
for (int i = 0; i < 4; i++) {
persons.add(new Person("First name" + i, "Last Name" + i));
}
CustomTableView<Person> tableView = new CustomTableView<>();
TableColumn<Person, String> fnCol = new TableColumn<>("First Name");
fnCol.setCellValueFactory(param -> param.getValue().firstNameProperty());
TableColumn<Person, String> lnCol = new TableColumn<>("Last Name");
lnCol.setCellValueFactory(param -> param.getValue().lastNameProperty());
tableView.getColumns().addAll(fnCol, lnCol);
tableView.getItems().addAll(persons);
tableView.setRowFactory(new Callback<TableView<Person>, TableRow<Person>>() {
@Override
public TableRow<Person> call(TableView<Person> param) {
return new TableRow<Person>(){
{
/* This will fix my issue, but I dont want each tableView rowfactory to set this behavior.*/
// addEventHandler(MouseEvent.MOUSE_PRESSED,e->{
// getTableView().getSelectionModel().select(getItem());
// });
}
@Override
protected void updateItem(Person item, boolean empty) {
super.updateItem(item, empty);
}
};
}
});
VBox sp = new VBox();
sp.setAlignment(Pos.TOP_LEFT);
sp.getChildren().addAll(tableView);
Scene sc = new Scene(sp);
primaryStage.setScene(sc);
primaryStage.show();
}
public static void main(String... a) {
Application.launch(a);
}
/**
* My custom tableView.
* @param <S>
*/
class CustomTableView<S> extends TableView<S> {
public CustomTableView() {
// A lot of custom behavior is included to this TableView.
}
}
class Person {
private StringProperty firstName = new SimpleStringProperty();
private StringProperty lastName = new SimpleStringProperty();
public Person(String fn, String ln) {
setFirstName(fn);
setLastName(ln);
}
public String getFirstName() {
return firstName.get();
}
public StringProperty firstNameProperty() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName.set(firstName);
}
public String getLastName() {
return lastName.get();
}
public StringProperty lastNameProperty() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName.set(lastName);
}
}
}
Solution
The entry point for any global custom (per-control) functionality/behavior is the control's skin. More specifically: user interaction is controlled by the skin's behavior - which unfortunately didn't make it into public scope, such that its access/modification requires to go dirty (as in accessing internal classes/methods, partly via reflection).
Assuming that such access is allowed, the steps to tweak the mouse interaction into reacting the same way for the middle as for the primary button for a TableCell are
- implement a custom TableCellSkin
- reflectively access its behavior
- find the mousePressed handler in the behavior's inputMap
- replace the original handler with a custom handler that replaces the mouseEvent coming from the middle button by a mouseEvent coming from the primary button
- make the custom TableCellSkin the default by css
Note: the TableRowSkin which is responsible for handling mouseEvents in the free space at the right of the table doesn't separate out the middle button, so currently nothing to do. If that changes in future, simple apply the same trick as for the table cells.
Example:
public class TableRowCustomMouse extends Application {
public static class CustomMouseTableCellSkin<T, S> extends TableCellSkin<T, S> {
EventHandler<MouseEvent> original;
public CustomMouseTableCellSkin(TableCell<T, S> control) {
super(control);
adjustMouseBehavior();
}
private void adjustMouseBehavior() {
// dirty: reflective access to behavior, use your custom reflective accessor
TableCellBehavior<T, S> behavior =
(TableCellBehavior<T, S>) FXUtils.invokeGetFieldValue(TableCellSkin.class, this, "behavior");
InputMap<TableCell<T, S>> inputMap = behavior.getInputMap();
ObservableList<Mapping<?>> mappings = inputMap.getMappings();
List<Mapping<?>> pressedMapping = mappings.stream()
.filter(mapping -> mapping.getEventType() == MouseEvent.MOUSE_PRESSED)
.collect(Collectors.toList());
if (pressedMapping.size() == 1) {
Mapping<?> originalMapping = pressedMapping.get(0);
original = (EventHandler<MouseEvent>) pressedMapping.get(0).getEventHandler();
if (original != null) {
EventHandler<MouseEvent> replaced = this::replaceMouseEvent;
mappings.remove(originalMapping);
mappings.add(new MouseMapping(MouseEvent.MOUSE_PRESSED, replaced));
}
}
}
private void replaceMouseEvent(MouseEvent e) {
MouseEvent replaced = e;
if (e.isMiddleButtonDown()) {
replaced = new MouseEvent(e.getSource(), e.getTarget(), e.getEventType(),
e.getX(), e.getY(),
e.getScreenX(), e.getScreenY(),
MouseButton.PRIMARY,
e.getClickCount(),
e.isShiftDown(), e.isControlDown(), e.isAltDown(), e.isMetaDown(),
true, false, false,
e.isSynthesized(), e.isPopupTrigger(), e.isStillSincePress(),
null
);
}
original.handle(replaced);
}
}
private Parent createContent() {
TableView<Person> table = new TableView<>(Person.persons());
TableColumn<Person, String> first = new TableColumn("First Name");
first.setCellValueFactory(cc -> cc.getValue().firstNameProperty());
TableColumn<Person, String> last = new TableColumn<>("Last Name");
last.setCellValueFactory(cc -> cc.getValue().lastNameProperty());
table.getColumns().addAll(first, last);
BorderPane content = new BorderPane(table);
return content;
}
@Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
// load the default css
stage.getScene().getStylesheets()
.add(getClass().getResource("customtablecellskin.css").toExternalForm());
stage.setTitle(FXUtils.version());
stage.show();
}
public static void main(String[] args) {
launch(args);
}
@SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(TableRowCustomMouse.class.getName());
}
The css with custom skin for TableCell:
.table-cell {
-fx-skin: "<yourpackage>.TableRowCustomMouse$CustomMouseTableCellSkin";
}
Answered By - kleopatra