JavaFX TableView: Show sorted items + Background Task - javafx

I'm working with Javafx and I'm struggling to get a sorted table in place when using background Tasks to update. In the code here, which can be run standalone, I update a table in background.
What I'd like to do is that this table gets updated, and stays sorted in chronological order, so older train times appear at the top, and later ones at the bottom. The example produces times that are the opposite order on purpose, to see if sorting works.
I run some tests before I added concurrent update to the table, and the way I would do it is by calling:
private final ObservableList<StationBoardLine> data = FXCollections.observableArrayList(
new StationBoardLine("RE", "17:14", "Basel Bad Bf", "Basel SBB", "+3", "RE 5343"));
SortedList<StationBoardLine> sorted = new SortedList<>(data, new DelayComparator());
table.setItems(sorted);
However, now I'm not setting the items, but using the background task together with ReadOnlyObjectProperty and ReadOnlyObjectWrapper to append to it.
So, my question is, how can I make sure that as items are added, the list remains ordered? I've tried to see if I could reorder the list inside the call to Platform.runLater but didn't seem to work.
The link between the task updating the table and the table is set is here:
table.itemsProperty().bind(task.partialResultsProperty());
Thanks for help,
Galder

The way I would do it is by updating an ObservableList by the background task and use it as a "source" to create a SortedList. This SortedList would then act as the source of "items" to the TableView.
A general structure would be :
public class MyClass {
private TableView<T> tableView = new TableView;
private ObservableList<T> sourceList = FXCollections.observableArrayList();
public MyClass() {
...
SortedList<T> sortedList = new SortedList<>(sourceList, new MyComparator());
tableView.setItems(sortedList);
...
new Task<Void> {
protected Void call() {
... // Some background data fetch
Platform.runLater(() -> sourceList.add(data));
return null;
}
};
}
}
For your scenario, I would go with something that you already have. Therefore, instead of creating a new ObservableList to be used as a source for your SortedList, I would use the list returned by Task#getPartialResults().
The DelayComparator uses the value of the delay to compare and show the data in the TableView.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import java.util.Comparator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class App extends Application {
private TableView<StationBoardLine> table = new TableView<>();
private final ExecutorService exec = Executors.newSingleThreadExecutor();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
BorderPane root = new BorderPane();
Scene scene = new Scene(root, 800, 600);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
table.setEditable(true);
TableColumn typeCol = getTableCol("Type", 10, "type");
TableColumn departureCol = getTableCol("Departure", 30, "departure");
TableColumn stationCol = getTableCol("Station", 200, "station");
TableColumn destinationCol = getTableCol("Destination", 200, "destination");
TableColumn delayCol = getTableCol("Delay", 20, "delay");
TableColumn trainName = getTableCol("Train Name", 50, "trainName");
table.getColumns().addAll(
typeCol, departureCol, stationCol, destinationCol, delayCol, trainName);
root.setCenter(table);
PartialResultsTask task = new PartialResultsTask();
SortedList<StationBoardLine> sorted = new SortedList<>(task.getPartialResults(), new DelayComparator());
table.setItems(sorted);
exec.submit(task);
stage.setTitle("Swiss Transport Delays Board");
stage.setScene(scene);
stage.show();
}
private TableColumn getTableCol(String colName, int minWidth, String fieldName) {
TableColumn<StationBoardLine, String> typeCol = new TableColumn<>(colName);
typeCol.setMinWidth(minWidth);
typeCol.setCellValueFactory(new PropertyValueFactory<>(fieldName));
return typeCol;
}
static final class DelayComparator implements Comparator<StationBoardLine> {
#Override
public int compare(StationBoardLine o1, StationBoardLine o2) {
return o1.getDelay().compareTo(o2.getDelay());
}
}
public class PartialResultsTask extends Task<Void> {
private ObservableList<StationBoardLine>partialResults = FXCollections.observableArrayList();
public final ObservableList<StationBoardLine> getPartialResults() {
return partialResults;
}
#Override protected Void call() throws Exception {
System.out.println("Creating station board entries...");
for (int i=5; i >= 1; i--) {
Thread.sleep(1000);
if (isCancelled()) break;
StationBoardLine l = new StationBoardLine(
"ICE", "16:" + i, "Basel Bad Bf", "Chur", String.valueOf(i), "ICE 75");
Platform.runLater(() -> partialResults.add(l));
}
return null;
}
}
public static final class StationBoardLine {
private final SimpleStringProperty type;
private final SimpleStringProperty departure;
private final SimpleStringProperty station;
private final SimpleStringProperty destination;
private final SimpleStringProperty delay;
private final SimpleStringProperty trainName;
StationBoardLine(String type,
String departure,
String station,
String destination,
String delay,
String trainName) {
this.type = new SimpleStringProperty(type);
this.departure = new SimpleStringProperty(departure);
this.station = new SimpleStringProperty(station);
this.destination = new SimpleStringProperty(destination);
this.delay = new SimpleStringProperty(delay);
this.trainName = new SimpleStringProperty(trainName);
}
public String getType() {
return type.get();
}
public SimpleStringProperty typeProperty() {
return type;
}
public void setType(String type) {
this.type.set(type);
}
public String getDeparture() {
return departure.get();
}
public SimpleStringProperty departureProperty() {
return departure;
}
public void setDeparture(String departure) {
this.departure.set(departure);
}
public String getStation() {
return station.get();
}
public SimpleStringProperty stationProperty() {
return station;
}
public void setStation(String station) {
this.station.set(station);
}
public String getDestination() {
return destination.get();
}
public SimpleStringProperty destinationProperty() {
return destination;
}
public void setDestination(String destination) {
this.destination.set(destination);
}
public String getDelay() {
return delay.get();
}
public SimpleStringProperty delayProperty() {
return delay;
}
public void setDelay(String delay) {
this.delay.set(delay);
}
public String getTrainName() {
return trainName.get();
}
public SimpleStringProperty trainNameProperty() {
return trainName;
}
public void setTrainName(String trainName) {
this.trainName.set(trainName);
}
}
}

Related

CellEditEvent edit neighbouring cell

I defined a TableView with 4 columns, the first 3 of them should be editable. The fourth column is the mathematical result of the difference of the second (expenses) and third (earnings) one. I managed this so far but when I edit the second or third column the fourth column dont get updatet. I tried different approaches but it didnt work. The problem is that I dont know how to get access to the neighbouring cell.
package application;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
public class Main extends Application {
private TableView<Member> table = new TableView<>();
//private final ObservableList<Member> data = FXCollections.observableArrayList();
#SuppressWarnings("unchecked")
#Override
public void start(Stage primaryStage) {
try {
//Definition Layout-Container
GridPane primarygridpane = new GridPane();
primarygridpane.setHgap(10);
primarygridpane.setVgap(10);
primarygridpane.setPadding(new Insets(5, 5, 5, 5));
HBox hboxTable = new HBox(10);
hboxTable.setPadding(new Insets(5, 5, 5, 5));
Text titel = new Text("Application");
titel.setFont(Font.font("Arial", FontWeight.BOLD, 28));
// Spalte Name mit Edit-Funktion
TableColumn<Member, String> memberColumn = new TableColumn<>("Name");
memberColumn.setMinWidth(150);
memberColumn.setCellValueFactory(new PropertyValueFactory<Member, String>("member"));
memberColumn.setCellFactory(TextFieldTableCell.forTableColumn());
memberColumn.setOnEditCommit(
new EventHandler<CellEditEvent<Member, String>>() {
#Override
public void handle(CellEditEvent<Member, String> t) {
((Member) t.getTableView().getItems().get(t.getTablePosition().getRow())).setMember(t.getNewValue());
}
}
);
// Spalte Ausgaben mit Edit-Funktion
TableColumn<Member, String> expensesColumn = new TableColumn<>("Ausgaben");
expensesColumn.setMinWidth(50);
expensesColumn.setCellValueFactory(new PropertyValueFactory<>("expenses"));
expensesColumn.setCellFactory(TextFieldTableCell.forTableColumn());
expensesColumn.setOnEditCommit(
new EventHandler<CellEditEvent<Member, String>>() {
#Override
public void handle(CellEditEvent<Member, String> t) {
((Member) t.getTableView().getItems().get(t.getTablePosition().getRow())).setExpenses(t.getNewValue());
//((Member) t.getTableView().getItems().get(t.getTablePosition().getRow())).setDifference(Double.parseDouble(t.getNewValue()));
}
}
);
// Spalte Pfand mit Edit-Funktion
TableColumn<Member, String> earningsColumn = new TableColumn<>("Pfand");
earningsColumn.setMinWidth(50);
earningsColumn.setCellValueFactory(new PropertyValueFactory<Member, String>("earnings"));
earningsColumn.setCellFactory(TextFieldTableCell.forTableColumn());
earningsColumn.setOnEditCommit(
new EventHandler<CellEditEvent<Member, String>>() {
#Override
public void handle(CellEditEvent<Member, String> t) {
((Member) t.getTableView().getItems().get(t.getTablePosition().getRow())).setEarnings(t.getNewValue());
}
}
);
//Spalte Differenz ohne Edit-Funktion
TableColumn<Member, Double> differenceColumn = new TableColumn<>("Differenz");
differenceColumn.setMinWidth(50);
differenceColumn.setCellValueFactory(new PropertyValueFactory<>("difference"));
//Editier-Leiste
TextField tfMember = new TextField();
tfMember.setMinWidth(150);
tfMember.setPromptText("Name");
TextField tfExpenses = new TextField();
tfExpenses.setMinWidth(50);
tfExpenses.setPromptText("Ausgaben");
TextField tfEarnings = new TextField();
tfEarnings.setMinWidth(50);
tfEarnings.setPromptText("Pfand");
Button btnAdd = new Button("Hinzufügen");
Button btnDelete = new Button("Löschen");
hboxTable.getChildren().addAll(tfMember, tfExpenses, tfEarnings, btnAdd, btnDelete);
// Spalten der Tabelle hinzufügen und Tabelle editierbar machen
table.getColumns().addAll(memberColumn, expensesColumn, earningsColumn, differenceColumn);
table.setEditable(true);
// table.setItems(data);
btnAdd.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent e) {
try {
Member member = new Member();
member.setMember(tfMember.getText());
member.setExpenses(tfExpenses.getText());
member.setEarnings(tfEarnings.getText());
member.setDifference(Double.parseDouble(tfExpenses.getText()) - Double.parseDouble(tfEarnings.getText()));
table.getItems().add(member);
//data.add(member);
tfMember.clear();
tfExpenses.clear();
tfEarnings.clear();
} catch (NumberFormatException Exception) {}
}
});
//Elemente dem Gridpane hinzufügen und Rest
primarygridpane.add(titel, 0, 0, 2, 1);
primarygridpane.add(table, 0, 2);
primarygridpane.add(hboxTable, 0, 3);
Scene scene = new Scene(primarygridpane,450,550);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {e.printStackTrace();}
}
public static void main(String[] args) {
launch(args);
}
}
package application;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.NumberBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Member {
private SimpleStringProperty member = new SimpleStringProperty();
private DoubleProperty expenses = new SimpleDoubleProperty();
private DoubleProperty earnings = new SimpleDoubleProperty();
private DoubleProperty difference = new SimpleDoubleProperty();
public Member(String member, Double expenses, Double earnings) {
this.member = new SimpleStringProperty(member);
this.expenses = new SimpleDoubleProperty(expenses);
this.earnings = new SimpleDoubleProperty(earnings);
// NumberBinding nb = earningsProperty().subtract(expensesProperty());
// this.difference = new SimpleDoubleProperty(nb);
// NumberBinding nb2 = expenses.subtract(earnings);
}
public String getMember() {
return member.get();
}
public void setMember(String name) {
member.set(name);
}
public final StringProperty MemberProperty(){
return member;
}
public Double getExpenses() {
return expenses.get();
}
public void setExpenses(Double value) {
expenses.set(value);
}
public final DoubleProperty expensesProperty(){
return expenses;
}
public Double getEarnings() {
return earnings.get();
}
public void setEarnings(Double value) {
earnings.set(value);
}
public final DoubleProperty earningsProperty(){
return earnings;
}
public Double getDifference() {
return difference.get();
}
public void setDifference(Double value) {
difference.set(value);
}
public final DoubleProperty differenceProperty(){
return difference;
}
}
I would be very thankfull if someone could help me :)
You can establish the binding simply by calling bind(...) on the property, in the constructor.
Since the differenceProperty() is bound, calling setDifference() would throw an exception, so you should omit that method1. You can do:
public class Member {
private StringProperty member = new SimpleStringProperty();
private DoubleProperty expenses = new SimpleDoubleProperty();
private DoubleProperty earnings = new SimpleDoubleProperty();
private DoubleProperty difference = new SimpleDoubleProperty();
public Member(String member, double expenses, double earnings) {
this.member = new SimpleStringProperty(member);
this.expenses = new SimpleDoubleProperty(expenses);
this.earnings = new SimpleDoubleProperty(earnings);
this.difference = new SimpleDoubleProperty();
this.difference.bind(this.earnings.subtract(this.expenses));
}
public String getMember() {
return member.get();
}
public void setMember(String name) {
member.set(name);
}
public final StringProperty memberProperty(){
return member;
}
public Double getExpenses() {
return expenses.get();
}
public void setExpenses(Double value) {
expenses.set(value);
}
public final DoubleProperty expensesProperty(){
return expenses;
}
public Double getEarnings() {
return earnings.get();
}
public void setEarnings(Double value) {
earnings.set(value);
}
public final DoubleProperty earningsProperty(){
return earnings;
}
public Double getDifference() {
return difference.get();
}
// public void setDifference(Double value) {
// difference.set(value);
// }
public final DoubleProperty differenceProperty(){
return difference;
}
}
In your table, the earnings and expenses columns represent numeric values, so they should be typed appropriately. For reasons explained at JavaFX Properties in TableView, the type here should be Number, not Double. Also note that if you tell your table cell implementation how to convert from a String to the value (Number) you want to display:
expensesColumn.setCellFactory(TextFieldTableCell.forTableColumn(new NumberStringConverter()));
then you no longer need to provide the onEditCommit handler (the table cell will take care of the update).
So your table configuration simplifies to
// Spalte Name mit Edit-Funktion
TableColumn<Member, String> memberColumn = new TableColumn<>("Name");
memberColumn.setMinWidth(150);
memberColumn.setCellValueFactory(new PropertyValueFactory<Member, String>("member"));
memberColumn.setCellFactory(TextFieldTableCell.forTableColumn());
// Spalte Ausgaben mit Edit-Funktion
TableColumn<Member, Number> expensesColumn = new TableColumn<>("Ausgaben");
expensesColumn.setMinWidth(50);
expensesColumn.setCellValueFactory(new PropertyValueFactory<>("expenses"));
expensesColumn.setCellFactory(TextFieldTableCell.forTableColumn(new NumberStringConverter()));
// Spalte Pfand mit Edit-Funktion
TableColumn<Member, Number> earningsColumn = new TableColumn<>("Pfand");
earningsColumn.setMinWidth(50);
earningsColumn.setCellValueFactory(new PropertyValueFactory<>("earnings"));
earningsColumn.setCellFactory(TextFieldTableCell.forTableColumn(new NumberStringConverter()));
//Spalte Differenz ohne Edit-Funktion
TableColumn<Member, Number> differenceColumn = new TableColumn<>("Differenz");
differenceColumn.setMinWidth(50);
differenceColumn.setCellValueFactory(new PropertyValueFactory<>("difference"));
Note that the NumberStringConverter has a fairly simplistic implementation; you might want to implement your own StringConverter<Number> to support, e.g., locale-based string parsing.
Also note that the model class Member now ensures that difference is always the difference between earnings and expenses, so you do not need to (and can not) set the difference property:
btnAdd.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent e) {
try {
Member member = new Member(tfMember.getText(),
Double.parseDouble(tfExpenses.getText()),
Double.parseDouble(tfEarnings.getText()));
table.getItems().add(member);
tfMember.clear();
tfExpenses.clear();
tfEarnings.clear();
} catch (NumberFormatException Exception) {}
}
});
Just as a side note, use of PropertyValueFactory is not really necessary as of Java 8, and because it relies on reflection is prone to failing silently if, e.g., the property name is mistyped (it's also slightly inefficient). You can prefer implementing the callback directly with a lambda expression:
// memberColumn.setCellValueFactory(new PropertyValueFactory<Member, String>("member"));
memberColumn.setCellValueFactory(cellData -> cellData.getValue().memberProperty());
// expensesColumn.setCellValueFactory(new PropertyValueFactory<>("expenses"));
expensesColumn.setCellValueFactory(cellData -> cellData.getValue().expensesProperty());
// earningsColumn.setCellValueFactory(new PropertyValueFactory<>("earnings"));
earningsColumn.setCellValueFactory(cellData -> cellData.getValue().earningsProperty());
This approach also allows you to omit the difference property entirely from the Member class and just use a binding directly in the table column:
differenceColumn.setCellValueFactory(cd ->
cd.getValue().earningsProperty().subtract(cd.getValue().expensesProperty()));
The choice here depends basically on whether you consider the difference an integral part of the data (so it should be included in the model), or whether the data just consist of the earnings and expenses, and the difference is just something you want to visualize in the table.
(1) Really, you should use a ReadOnlyProperty here to represent the difference, since calling differenceProperty().set(...) would also throw an exception with the code the way it's written. Basically:
private final ReadOnlyDoubleWrapper difference = new ReadOnlyDoubleWrapper() ;
// Constructor and other properties as before ...
public final ReadOnlyDoubleProperty differenceProperty() {
return difference.getReadOnlyProperty();
}
public final double getDifference() {
return differenceProperty().get();
}

TableView row selection on Mouse Middle Button click

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);
}
}
}
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";
}
Accepted #kleopatra's approach as an answer. However the solution to my question is a bit different to #kleopatra's answer. But the core idea is still the same.
I used the approach to override the doSelect method of TableCellBehavior
#Override
protected void doSelect(double x, double y, MouseButton button, int clickCount, boolean shiftDown, boolean shortcutDown) {
MouseButton btn = button;
if (button == MouseButton.MIDDLE) {
btn = MouseButton.PRIMARY;
}
super.doSelect(x, y, btn, clickCount, shiftDown, shortcutDown);
}
Below is the working demo which solved my issue:
DemoClass:
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);
sc.getStylesheets().add(this.getClass().getResource("tableRowSelectionOnMiddleButton.css").toExternalForm());
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);
}
}
}
CustomTableCellSkin class:
import com.sun.javafx.scene.control.behavior.TableCellBehavior;
import com.sun.javafx.scene.control.skin.TableCellSkinBase;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.input.MouseButton;
public class CustomTableCellSkin<S, T> extends TableCellSkinBase<TableCell<S, T>, TableCellBehavior<S, T>> {
private final TableColumn<S, T> tableColumn;
public CustomTableCellSkin(TableCell<S, T> tableCell) {
super(tableCell, new CustomTableCellBehavior<S, T>(tableCell));
this.tableColumn = tableCell.getTableColumn();
super.init(tableCell);
}
#Override
protected BooleanProperty columnVisibleProperty() {
return tableColumn.visibleProperty();
}
#Override
protected ReadOnlyDoubleProperty columnWidthProperty() {
return tableColumn.widthProperty();
}
}
class CustomTableCellBehavior<S, T> extends TableCellBehavior<S, T> {
public CustomTableCellBehavior(TableCell<S, T> control) {
super(control);
}
#Override
protected void doSelect(double x, double y, MouseButton button, int clickCount, boolean shiftDown, boolean shortcutDown) {
MouseButton btn = button;
if (button == MouseButton.MIDDLE) {
btn = MouseButton.PRIMARY;
}
super.doSelect(x, y, btn, clickCount, shiftDown, shortcutDown);
}
}
tableRowSelectionOnMiddleButton.css
.table-cell {
-fx-skin: "<package>.CustomTableCellSkin";
}

JAVAFX 8 ComboBox and ObservableList

I need a combobox populated through observablelist which contains specific data retrieved from DB. This is my source.
Model
public ObservableList<Bank> listBank = FXCollections.observableArrayList();
public static class Bank {
private final StringProperty id;
private final StringProperty name;
private Bank(
String id,
String name
) {
this.id = new SimpleStringProperty(id);
this.name = new SimpleStringProperty(name);
}
public StringProperty idProperty() { return id; }
public StringProperty nameProperty() { return name; }
}
View
#FXML
private ComboBox comboBank<Bank>;
public final void getBankDataFields() {
comboBank.setItems(model.listBank);
}
comboBank.setButtonCell(new ListCell<Bank>() {
#Override
protected void updateItem(Bank t, boolean bln) {
super.updateItem(t, bln);
if (t != null) {
setText(t.nameProperty().getValue().toUpperCase());
} else {
setText(null);
}
}
});
comboBank.setCellFactory(new Callback<ListView<Bank>, ListCell<Bank>>() {
#Override
public ListCell<Bank> call(ListView<Bank> p) {
return new ListCell<Bank>() {
#Override
protected void updateItem(Bank t, boolean bln) {
super.updateItem(t, bln);
if(t != null){
setText(t.nomeProperty().getValue().toUpperCase());
} else {
setText(null);
}
}
};
}
});
comboBank.valueProperty().addListener((ObservableValue<? extends Bank> observable, Bank oldValue, Bank newValue) -> {
setIdBank(newValue.idProperty().getValue());
});
ComboBox is populated with NAME field and listener is used to get relative ID and pass it to a query for storing data on DB.
Ok, everything seems to work but i have two questions:
When user need to modify this record, i need to get the ID from DB and select the relative NAME in ComboBox. How can i do that?
comboBank.setValue(????);
Is there a better way to achieve this goal? An ObservableMap may substitute the ObservableList?
Thanks in advance.
There is an easier way to what you are trying to achieve. You should use a StringConverter on the ComboBox to display the names for the Bank instances.
comboBox.setConverter(new StringConverter<Bank>() {
#Override
public String toString(Bank object) {
return object.nameProperty().get();
}
#Override
public Bank fromString(String string) {
// Somehow pass id and return bank instance
// If not important, just return null
return null;
}
});
To get selected value i.e. instance of the selected bank, just use :
comboBox.getValue();
MCVE
import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
import javafx.util.StringConverter;
import java.util.stream.Collectors;
public class Main extends Application {
#Override
public void start(Stage stage) {
ComboBox<Bank> comboBox = new ComboBox<>();
ObservableList<Bank> items = FXCollections.observableArrayList(
new Bank("1", "A"), new Bank("2", "B"),
new Bank("3", "C"), new Bank("4", "D"));
comboBox.setItems(items);
StringConverter<Bank> converter = new StringConverter<Bank>() {
#Override
public String toString(Bank bank) {
return bank.nameProperty().get();
}
#Override
public Bank fromString(String id) {
return items.stream()
.filter(item -> item.idProperty().get().equals(id))
.collect(Collectors.toList()).get(0);
}
};
comboBox.setConverter(converter);
// Print the name of the Bank that is selected
comboBox.getSelectionModel().selectedItemProperty().addListener((o, ol, nw) -> {
System.out.println(comboBox.getValue().nameProperty().get());
});
// Wait for 3 seconds and select the item with id = 2
PauseTransition pauseTransition = new PauseTransition(Duration.seconds(3));
pauseTransition.setOnFinished(event -> comboBox.getSelectionModel().select(converter.fromString("2")));
pauseTransition.play();
VBox root = new VBox(comboBox);
root.setAlignment(Pos.CENTER);
Scene scene = new Scene(root, 200, 200);
stage.setScene(scene);
stage.show();
}
}

Issue with removing\adding items from JavaFX TableColumn's combo box CellFactory

I have a JavaFX TableColumn. The column has a ComboBoxTableCell populated by ObservableList I pass to it.
I have an "active" list and a "deactive" list for the combobox. After a selection is made, I wish to remove the selected item from the active, "real", list (and add it to the deactivated items list).
After a selection has been made, a CellEditEvent is being fired and sets up the row object by the selected one (from the combobox).
The thing is, when I remove the select item from the list, in the event handler, my CellEditEvent event handler got fired again - this time with a wrong "new value"!
Of course this behavior breaks my flow logic completely.
Any ideas about how to solve this situation? Thank you
An SSCCE of the situation:
package tableviewexample;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.collections.*;
import javafx.event.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.*;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.StringConverter;
public class TableViewExample extends Application {
#Override
public void start(Stage primaryStage) {
TableView<MappingItem> table = new TableView<>();
// FIRST COLUMN
TableColumn<MappingItem, String> colA = new TableColumn<>("Excel Column");
colA.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<MappingItem, String>, ObservableValue<String>> () {
#Override
public ObservableValue<String> call(TableColumn.CellDataFeatures<MappingItem, String> param) {
return new ReadOnlyObjectWrapper(param.getValue().getExcelColumnName());
}
});
//SECOND COLUMN
TableColumn<MappingItem, GoldplusField> colB = new TableColumn<>("Database Field Column");
colB.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<MappingItem, GoldplusField>, ObservableValue<GoldplusField>> () {
#Override
public ObservableValue<GoldplusField> call(TableColumn.CellDataFeatures<MappingItem, GoldplusField> param) {
return new ReadOnlyObjectWrapper(param.getValue().getGpField());
}
});
GoldplusField gp1 = new GoldplusField("T1", "fName", "First Name");
GoldplusField gp2 = new GoldplusField("T1", "phn", "Phone");
GoldplusField gp3 = new GoldplusField("T2", "lName", "Last Name");
GoldplusField gp4 = new GoldplusField("T2", "adrs", "Address");
ObservableList<GoldplusField> deactiveFieldsList = FXCollections.observableArrayList();
ObservableList<GoldplusField> activeFieldsList = FXCollections.observableArrayList(gp1, gp2, gp3, gp4);
colB.setCellFactory(ComboBoxTableCell.forTableColumn(new FieldToStringConvertor(), activeFieldsList));
colB.setOnEditCommit(
new EventHandler<TableColumn.CellEditEvent<MappingItem, GoldplusField>>() {
#Override
public void handle(TableColumn.CellEditEvent<MappingItem, GoldplusField> t) {
if (t.getNewValue() != null) {
deactiveFieldsList.add(t.getNewValue());
((MappingItem) t.getTableView().getItems().get(
t.getTablePosition().getRow())
).setGpField(t.getNewValue());
// ******************************************************************************************** //
// This creates a new instance of the EventHandler in which I get the "next" item on the List.
// ******************************************************************************************** //
activeFieldsList.remove(t.getNewValue());
}
}
}
);
//THIRD COLUMN
TableColumn<MappingItem, String> colC = new TableColumn<>("Test Column");
PropertyValueFactory<MappingItem, String> nameFac = new PropertyValueFactory<>("name");
colC.setCellValueFactory(nameFac);
colC.setCellFactory(TextFieldTableCell.forTableColumn());
table.setEditable(true);
table.getColumns().addAll(colA, colB, colC);
GoldplusField gp5 = new GoldplusField("T1", "other", "Other");
MappingItem mi1 = new MappingItem("name", gp5);
mi1.excelColumnName.set("name1");
MappingItem mi2 = new MappingItem("phone", gp5);
mi2.excelColumnName.set("nam2");
ObservableList<MappingItem> miList = FXCollections.observableArrayList(mi1, mi2);
table.setItems(miList);
StackPane root = new StackPane();
root.getChildren().add(table);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
class FieldToStringConvertor extends StringConverter<GoldplusField> {
#Override
public String toString(GoldplusField object) {
if (object != null)
return object.getGpName();
else
return "";
}
#Override
public GoldplusField fromString(String string) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
}
public class MappingItem {
private StringProperty excelColumnName = new SimpleStringProperty(this, "excelColumnName");
private ObjectProperty<GoldplusField> gpField = new SimpleObjectProperty<GoldplusField>(this, "gpField");
public String getExcelColumnName() { return excelColumnName.get(); }
public void setExcelColumnName(String excelColumnName) { this.excelColumnName.set(excelColumnName); }
public StringProperty excelColumnNameProperty() { return excelColumnName; }
public GoldplusField getGpField() { return gpField.get(); }
public void setGpField(GoldplusField gpField) { this.gpField.set(gpField); }
public ObjectProperty gpFieldProperty() { return this.gpField; }
public MappingItem(String columnName) { this.excelColumnName.set(columnName); }
public MappingItem(GoldplusField gpField) { this.gpField.set(gpField); }
public MappingItem(String columnName, GoldplusField gpField) {
this.excelColumnName.set(columnName);
this.gpField.set(gpField);
}
}
public class GoldplusField {
private StringProperty table = new SimpleStringProperty(this, "table");
private StringProperty dbName = new SimpleStringProperty(this, "dbName");
private StringProperty gpName = new SimpleStringProperty(this, "gpName");
public String getDbName() { return dbName.get(); }
public String getGpName() { return gpName.get(); }
public String getTable() { return table.get(); }
public void setDbName(String dbName) { this.dbName.set(dbName); }
public void setGpName(String gpName) { this.gpName.set(gpName); }
public void setTable(String table) { this.table.set(table); }
public StringProperty tableProperty() { return this.table; }
public StringProperty gpNameProperty() { return this.gpName; }
public StringProperty dbNameProperty() { return this.dbName; }
public GoldplusField(String table, String dbName, String gpName) {
this.dbName.set(dbName);
this.gpName.set(gpName);
this.table.set(table);
}
}
}

How to fetch value of selected item in choice box from a table view coloumn in javafx

How can i fetch the value of the selected choice from the choce box in the following table.
column3 has 13 choice box nodes populated using following code.I want to fetch the selected item.
final ObservableList LogLevelList=FXCollections.observableArrayList("FATAL", "ERROR", "WARN", "INFO", "INOUT", "DEBUG");
column3.setCellFactory(new Callback<TableColumn<Feature,String>,TableCell<Feature,String>>(){
#Override
public TableCell<Feature,String> call(TableColumn<Feature,String> param) {
TableCell<Feature,String> cell = new TableCell<Feature,String>(){
#Override
public void updateItem(String item, boolean empty) {
System.out.println("Inside UpdateItem");
ChoiceBox choice = new ChoiceBox(LogLevelList);
choice.getSelectionModel().select(LogLevelList.indexOf(item));
//SETTING ALL THE GRAPHICS COMPONENT FOR CELL
setGraphic(choice);
}
};
return cell;
}
});
Does the predefined ChoiceBoxTableCell do what you need?
column3.setCellFactory(ChoiceBoxTableCell.forTableColumn(logLevelList));
See if this helps:
import java.util.stream.Collectors;
import java.util.stream.IntStream;
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.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.ChoiceBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TableChoiceBoxTest extends Application {
#Override
public void start(Stage primaryStage) {
final TableView<Feature> table = new TableView<>();
table.setEditable(true);
final TableColumn<Feature, String> nameCol = new TableColumn<>("Name");
nameCol.setCellValueFactory(new PropertyValueFactory<>("name"));
final TableColumn<Feature, String> logLevelCol = new TableColumn<>("Log level");
logLevelCol.setCellValueFactory(new PropertyValueFactory<>("logLevel"));
logLevelCol.setPrefWidth(150);
final ObservableList<String> logLevelList = FXCollections.observableArrayList("FATAL", "ERROR", "WARN", "INFO", "INOUT", "DEBUG");
logLevelCol.setCellFactory(ChoiceBoxTableCell.forTableColumn(logLevelList));
table.getColumns().addAll(nameCol, logLevelCol);
table.getItems().setAll(
IntStream.rangeClosed(1, 20)
.mapToObj(i -> new Feature("Item "+i, "FATAL"))
.collect(Collectors.toList())
);
Button showDataButton = new Button("Dump data");
showDataButton.setOnAction(event -> table.getItems().forEach(System.out::println));
BorderPane root = new BorderPane();
root.setCenter(table);
root.setBottom(showDataButton);
Scene scene = new Scene(root, 400, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class Feature {
private final StringProperty name ;
private final StringProperty logLevel ;
public Feature(String name, String logLevel) {
this.name = new SimpleStringProperty(this, "name", name);
this.logLevel = new SimpleStringProperty(this, "logLevel", logLevel);
}
public StringProperty nameProperty() {
return name ;
}
public final String getName() {
return name.get();
}
public final void setName(String name) {
this.name.set(name);
}
public StringProperty logLevelProperty() {
return logLevel ;
}
public final String getLogLevel() {
return logLevel.get();
}
public final void setLogLevel(String logLevel) {
this.logLevel.set(logLevel);
}
#Override
public String toString() {
return getName() + ": " + getLogLevel();
}
}
public static void main(String[] args) {
launch(args);
}
}
The provided ChoiceBoxTableCell updates the property of the associated item for you, so there's never any need to get the value from the ChoiceBox; you can just get the value from your model object.
I think there are mistakes in your code. You do not want to display your Choice box in each and every cell of that column (i.e Emptied Row's Cell) and Also you should call super class function.
Now for getting the selected value of ChoiceBox , instead of just displaying your choicebox with the values you will have to save them in some ArrayList or Map or best options is to save inside your Feature class. So that you can finally use
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item,empty);
if(item != null){
ChoiceBox choice = new ChoiceBox(LogLevelList);
choice.getSelectionModel().select(LogLevelList.indexOf(item));
choice.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<String>() {
#Override
public void changed(ObservableValue<? extends String> ov, String t, String t1) {
//either use : myMap.put(getIndex(),t1);
//or : item.setChoice(t1);
}
});
//SETTING ALL THE GRAPHICS COMPONENT FOR CELL
setGraphic(choice);
}
}
Also for demo of ChoiceBox in TableView there is one blog post for you :http://blog.ngopal.com.np/2011/10/01/tableview-cell-modifiy-in-javafx/

Resources