TableView row selection on Mouse Middle Button click - javafx

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";
}

Related

JavaFX TableView custom cell rendering split menu button

i've a problem with a custom cell render in a java fx table view component. I'm able to render the split menu button, but is only rendered from second row of table.
Below i put the code created to generate that image.
SplitMenuButtonApp.java
package com.example.splimenubtn;
import java.util.ArrayList;
import java.util.List;
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.SplitMenuButton;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
public class SplitMenuButtonApp extends Application {
private class Contact {
private StringProperty firstName;
private StringProperty lastName;
public Contact() {}
public Contact(String fName, String lName) {
firstName = new SimpleStringProperty(fName);
lastName = new SimpleStringProperty(lName);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public StringProperty firstName() {
return firstName;
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String lName) {
lastName.set(lName);
}
public StringProperty lastName() {
return lastName;
}
}
private ObservableList<Contact> data;
protected List<MenuItemFactory<Contact>> menuItemsList;
private TableView<Contact> table;
#Override
public void start(Stage primaryStage) throws Exception {
// Init data list
data = FXCollections.observableArrayList();
data.add(new Contact("Mickey", "Mouse"));
data.add(new Contact("Donald", "Duck"));
data.add(new Contact("Fantasy", "Name"));
initMenuButton();
SplitMenuButtonFactory<Contact> sMBtn = new SplitMenuButtonFactory<>();
sMBtn.setMenuItems(menuItemsList);
SplitMenuButton actions = sMBtn.buildButton();
// Build the list
table = new TableView<>();
TableColumn<Contact, String> col = new TableColumn<>("First Name");
col.setCellValueFactory(c -> c.getValue().firstName);
table.getColumns().add(col);
col = new TableColumn<>("Last Name");
col.setCellValueFactory(c -> c.getValue().lastName);
table.getColumns().add(col);
TableColumn<Contact, SplitMenuButton> aCol = new TableColumn<>("Action");
aCol.setCellValueFactory(new PropertyValueFactory<>(""));
aCol.setCellFactory(new ButtonCellFactory<>(actions));
table.getColumns().add(aCol);
table.setItems(data);
AnchorPane root = new AnchorPane();
AnchorPane.setTopAnchor(table, 5.0);
AnchorPane.setRightAnchor(table, 5.0);
AnchorPane.setBottomAnchor(table, 5.0);
AnchorPane.setLeftAnchor(table, 5.0);
root.getChildren().add(table);
Scene s = new Scene(root, 600d, 300d);
primaryStage.setScene(s);
primaryStage.setTitle("Split menu button on table row");
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
private void initMenuButton() {
if (menuItemsList == null) {
menuItemsList = new ArrayList<>();
menuItemsList.add(new MenuItemFactory<Contact>(MenuItemActions.EDIT, "Edit", true).setDataList(table));
menuItemsList.add(new MenuItemFactory<Contact>(MenuItemActions.DELETE, "Delete", false).setDataList(table));
}
}
}
MenuItemActions.java
package com.example.splimenubtn;
public enum MenuItemActions {
/**
* Detail item
*/
DETAILS,
/**
* Edit/Update item
*/
EDIT,
/**
* Delete item
*/
DELETE;
}
MenuItemFactory.java
package com.example.splimenubtn;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableView;
public class MenuItemFactory<S> {
private MenuItemActions itemType;
private String itemLbl;
private TableView<S> table;
private boolean defaultAction;
public MenuItemFactory() {}
public MenuItemFactory(MenuItemActions itemType, String itemLabel, boolean dA) {
this.itemType = itemType;
itemLbl = itemLabel;
defaultAction = dA;
}
public MenuItemFactory<S> setDataList(TableView<S> t) {
table = t;
return this;
}
public boolean isDefault() {
return defaultAction;
}
public MenuItem buildMenuItem() {
MenuItem mI = new MenuItem();
switch (itemType) {
case DETAILS:
mI.setText(itemLbl);
mI.setOnAction(handleDetails());
break;
case EDIT:
mI.setText(itemLbl);
mI.setOnAction(handleEdit());
break;
case DELETE:
mI.setText(itemLbl);
mI.setOnAction(handleDelete());
break;
default:
break;
}
return mI;
}
private EventHandler<ActionEvent> handleDetails() {
return new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent aE) {
System.out.println("*** DETAIL REQUESTED ***");
}
};
}
private EventHandler<ActionEvent> handleEdit() {
return new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent aE) {
System.out.println("*** EDIT REQUESTED ***");
}
};
}
private EventHandler<ActionEvent> handleDelete() {
return new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent aE) {
System.out.println("*** DELETE REQUESTED ***");
}
};
}
}
ButtonCellFactory.java
package com.example.splimenubtn;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.SplitMenuButton;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;
public class ButtonCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
private SplitMenuButton btn;
public ButtonCellFactory() {}
public ButtonCellFactory(SplitMenuButton b) {
btn = b;
}
#Override
public TableCell<S, T> call(TableColumn<S, T> param) {
return new TableCell<S, T>() {
#Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
setText(null);
} else {
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setGraphic(btn);
}
}
};
}
}
SpliMenuButtonFactory.java
package com.example.splimenubtn;
import java.util.List;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SplitMenuButton;
public class SplitMenuButtonFactory<T> {
private List<MenuItemFactory<T>> menuItems;
public SplitMenuButtonFactory() {}
public SplitMenuButtonFactory<T> setMenuItems(List<MenuItemFactory<T>> items) {
menuItems = items;
return this;
}
public SplitMenuButton buildButton() {
SplitMenuButton menuBtn = new SplitMenuButton();
for (MenuItemFactory<?> mIF : menuItems) {
MenuItem btn = mIF.buildMenuItem();
if (mIF.isDefault()) {
menuBtn.setText(btn.getText());
menuBtn.setOnAction(btn.getOnAction());
}
menuBtn.getItems().add(btn);
}
return menuBtn;
}
}
As you see in the image, with this code i'm able to create the spli menu button and add it to ta table, but is only rendered on last row.
I need suggestion to render the split menu button in the other row, any help is appreciated.
Cause you have use the same button in every cell, So it's set a button only last of the cell Value.
Remove this line in SplitMenuButtonApp class
SplitMenuButton actions = sMBtn.buildButton();
And replace this line
aCol.setCellFactory(new ButtonCellFactory<>(actions));
To below code
Callback<TableColumn<Contact, SplitMenuButton>, TableCell<Contact, SplitMenuButton>> actionsCol = new Callback<TableColumn<Contact, SplitMenuButton>, TableCell<Contact, SplitMenuButton>>() {
#Override
public TableCell call(final TableColumn<Contact, SplitMenuButton> param) {
final TableCell<Contact, SplitMenuButton> cell = new TableCell<Contact, SplitMenuButton>() {
SplitMenuButton actions = sMBtn.buildButton();
#Override
public void updateItem(SplitMenuButton item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
setText(null);
} else {
setGraphic(actions);
setText(null);
}
}
};
return cell;
}
};
aCol.setCellFactory(actionsCol);
I hope this code is working for you:)

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();
}
}

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/

Custom ListCell implements InvalidationListener, repaint components

I have a custom ListCell implemented that contains a BorderPane layout with some components.
The cell registers itself to the item. So when the duration of the item changes the invalidated method is called.
In this method I set the text of the duration label. My problem is now the method is called but the label is not repainted.
I think if setText is called the cell should repaint. It is possible to manually repaint the cell or the Label.?
public static class ListItemCell extends ListCell<MusicListItem> implements InvalidationListener{
private AnchorPane listItem;
private Label artist;
private Label title;
private Label duration;
private BorderPane borderPane;
private FlowPane flowPane;
public ListItemCell() {
initCellLayout();
}
public ListItemCell(final LogicInterfaceFX logic) {
...
}
public void initCellLayout() {
try {
this.listItem = (AnchorPane) FXMLLoader.load(getClass().getResource("/de/roth/jsona/view/themes/" + Config.getInstance().THEME + "/" + "layout_list_item.fxml"));
} catch (Exception e) {
e.printStackTrace();
}
this.borderPane = (BorderPane) listItem.getChildren().get(0);
this.flowPane = (FlowPane) borderPane.getLeft();
this.artist = (Label) flowPane.getChildren().get(0);
this.artist.getStyleClass().add(defaultTextClass);
this.title = (Label) flowPane.getChildren().get(1);
this.title.getStyleClass().add(defaultTextClass);
this.duration = (Label) borderPane.getRight();
this.duration.getStyleClass().add(defaultTextClass);
this.setGraphic(listItem);
}
#Override
public void updateItem(MusicListItem item, boolean empty) {
super.updateItem(item, empty);
if (!empty && item != null) {
item.addListener(this);
item.durationProperty().addListener(this);
// Duration
this.duration.setText(item.getDuration());
// Artist / Title
if (item.getArtist() != null) {
this.artist.setText(item.getArtist());
this.title.setText(" - " + item.getTitle());
} else {
this.artist.setText("");
this.title.setText(item.getFile().getName());
}
} else {
this.artist.setText("");
this.title.setText("");
this.duration.setText("");
}
}
#Override
public void invalidated(Observable observable) {
System.out.println("INVALIDATE!!!" + getItem().getFile().getAbsolutePath());
this.duration.setText(getItem().getDuration());
}
}
You have a bug in there: you need to make sure you remove listeners from old items when the item is updated. Remember that ListCells are reused, so updateItem(...) is called multiple times during the lifespan of your ListView.
I don't know if that's what is causing it to fail to update. This works for me:
import java.util.Random;
import javafx.application.Application;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Callback;
public class ListViewUpdatableProperties extends Application {
#Override
public void start(Stage primaryStage) {
final ListView<Item> listView = new ListView<>();
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
final Random rng = new Random();
for (int i=1; i<=20; i++) {
listView.getItems().add(new Item("Item "+i, rng.nextInt(100)));
}
BorderPane root = new BorderPane();
root.setCenter(listView);
listView.setCellFactory(new Callback<ListView<Item>, ListCell<Item>>() {
#Override
public ListCell<Item> call(ListView<Item> param) {
return new ItemListCell();
}
});
HBox controls = new HBox();
controls.setPadding(new Insets(5));
Button incButton = new Button("Increment selected");
incButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
for (Item item : listView.getSelectionModel().getSelectedItems()) {
item.increment();
}
}
});
controls.getChildren().add(incButton);
root.setBottom(controls);
Scene scene = new Scene(root, 250, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class ItemListCell extends ListCell<Item> implements InvalidationListener {
private final HBox hbox ;
private final Label nameLabel ;
private final Label valueLabel ;
public ItemListCell() {
hbox = new HBox(5);
nameLabel = new Label();
valueLabel = new Label();
hbox.getChildren().addAll(nameLabel, valueLabel);
setGraphic(hbox);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
public void updateItem(Item item, boolean empty) {
Item oldItem = getItem();
if (oldItem != null) {
oldItem.valueProperty().removeListener(this);
}
super.updateItem(item, empty);
if (item != null) {
nameLabel.setText(item.getName());
valueLabel.setText(String.valueOf(item.getValue()));
item.valueProperty().addListener(this);
} else {
nameLabel.setText("");
valueLabel.setText("");
}
}
#Override
public void invalidated(Observable observable) {
final int value = getItem().getValue();
System.out.println("Invalidated: item is "+getItem().getName() + " with value "+value);
valueLabel.setText(String.valueOf(value));
}
}
public static class Item {
public Item(String name, int value) {
setName(name);
setValue(value);
}
private final StringProperty name = new SimpleStringProperty(this, "name");
public StringProperty nameProperty() {
return name ;
}
public String getName() {
return name.get();
}
public void setName(String name) {
this.name.set(name);
}
private final IntegerProperty value = new SimpleIntegerProperty(this, "value");
public IntegerProperty valueProperty() {
return value ;
}
public int getValue() {
return value.get();
}
public void setValue(int value) {
this.value.set(value);
}
public void increment() {
value.set(value.get()+1);
}
}
public static void main(String[] args) {
launch(args);
}
}
As stated in the other answer, there is no repaint() method in JavaFX. If you wire things up correctly, when the properties are invalidated, it will know to repaint.
JavaFX uses a "retained mode" rendering model whereas Swing uses an "immediate mode".
See: Retained Mode Versus Immediate Mode (Windows)
So, you don't have a direct repaint() method.

Get ListCell via ListView

I have a ListView with my own ListCell<MyObject> implementation. Via a network signal, I receive an index of my ListCell that should be changed.
Over listView.getItems().get(index); there is no problem to access the model, but I want to make a layout change to the listCell with the received index and a layout change to the ListCell with the index+1;
How can I access the ListCell via the ListView?
I search for a method like this:
listView.getListCell(index);
Unfortunately right now there is no API to get List Cell by index or to get All children's(listcells) for ListView. One solution can be, define a new StringProperty specialIndicator in your MyObject class.
class MyObject {
....//u r properties
private StringProperty specialIndicator;
When ever you get index from network signal set this specialIndicator property of object and do forcerefresh of ListView
public void onReceivedNetWorkSignalIndex() {
listView.getItems().get(indexFromService).setSpecialIndicator("selected");
listView.getItems().get(indexFromService+1).setSpecialIndicator("selectedplusone");
//force refresh listview (it will trigger cellFactory again so that you can manipulate layout)
listView.setItems(null);
listView.setItems(allObjects);
}
As you already have custom Object ListView , i am assuming you already have custom cellFactory (if not you have to create one ) ,Modify your custom cell factory to handle this special Indicators
listView.setCellFactory(new Callback<ListView<MyObject>, ListCell<MyObject>>() {
#Override
public ListCell<MyObject> call(ListView<MyObject> myObjectListView) {
ListCell<MyObject> cell = new ListCell<MyObject>(){
#Override
protected void updateItem(MyObject myObject, boolean b) {
super.updateItem(myObject, b);
if(myObject != null) {
setText(myObject.getName());
if("selected".equalsIgnoreCase(myObject.getSpecialIndicator())) {
System.out.println("Setting new CSS/graphics for index retun from service." + myObject.getName());
} else if("selectedplusone".equalsIgnoreCase(myObject.getSpecialIndicator())) {
System.out.println("Setting new CSS/Graphics for index+1 returned from service" + myObject.getName());
}
myObject.setSpecialIndicator(""); // reset it back to empty
}
}
};
return cell;
}
});
Here is the whole sample Application ,you can look into it (in case the above explanation is not clear).
public class ListViewTest extends Application {
#Override
public void start(Stage stage) throws Exception {
VBox root = new VBox();
final ObservableList<MyObject> allObjects = FXCollections.observableArrayList(new MyObject("object0"), new MyObject("object1"),new MyObject("object2"),new MyObject("object3"),new MyObject("object4"));
final ListView<MyObject> listView = new ListView<>(allObjects);
listView.setCellFactory(new Callback<ListView<MyObject>, ListCell<MyObject>>() {
#Override
public ListCell<MyObject> call(ListView<MyObject> myObjectListView) {
ListCell<MyObject> cell = new ListCell<MyObject>(){
#Override
protected void updateItem(MyObject myObject, boolean b) {
super.updateItem(myObject, b);
if(myObject != null) {
setText(myObject.getName());
if("selected".equalsIgnoreCase(myObject.getSpecialIndicator())) {
System.out.println("Setting new CSS/graphics for index retun from service." + myObject.getName());
setText("I am selected Index from Service");
} else if("selectedplusone".equalsIgnoreCase(myObject.getSpecialIndicator())) {
System.out.println("Setting new CSS/Graphics for index+1 returned from service" + myObject.getName());
setText("I am selected Index +1 from Service");
}
myObject.setSpecialIndicator(""); // reset it back to empty
}
}
};
return cell;
}
});
Button serviceIndex2 = new Button("ServiceIndex2");
serviceIndex2.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent actionEvent) {
int indexFromService =2;
listView.getItems().get(indexFromService).setSpecialIndicator("selected");
listView.getItems().get(indexFromService+1).setSpecialIndicator("selectedplusone");
listView.setItems(null);
listView.setItems(allObjects);
}
});
root.getChildren().addAll(listView,serviceIndex2);
Scene scene = new Scene(root,500,500);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
class MyObject {
private StringProperty name;
private StringProperty specialIndicator;
MyObject(String name) {
this.name = new SimpleStringProperty(name);
this.specialIndicator = new SimpleStringProperty();
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
public String getSpecialIndicator() {
return specialIndicator.get();
}
public StringProperty specialIndicatorProperty() {
return specialIndicator;
}
public void setSpecialIndicator(String specialIndicator) {
this.specialIndicator.set(specialIndicator);
}
}
}
Here's a relatively simple approach, where there is just one "selected" index. Here I create a property to hold the index that is selected, and the cell factory just observes it, along with the cell's item property and index property, and sets the text via a binding. You could do something similar to set the graphic, if needed.
import java.util.concurrent.Callable;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Callback;
public class ListViewStyleAroundSelection extends Application {
#Override
public void start(Stage primaryStage) {
final ListView<String> listView = new ListView<>();
for (int i=1; i<=20; i++) {
listView.getItems().add("Item "+i);
}
final HBox controls = new HBox(5);
final Button button = new Button("Set selection");
final TextField indexField = new TextField();
final IntegerProperty selectionIndex = new SimpleIntegerProperty();
button.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
try {
selectionIndex.set(Integer.parseInt(indexField.getText()));
} catch (NumberFormatException nfe) {
indexField.setText("");
}
}
});
controls.getChildren().addAll(new Label("Enter selection index:"), indexField, button);
final BorderPane root = new BorderPane();
root.setCenter(listView);
root.setBottom(controls);
listView.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
#Override
public ListCell<String> call(ListView<String> lv) {
final ListCell<String> cell = new ListCell<>();
cell.textProperty().bind(Bindings.createStringBinding(new Callable<String>() {
#Override
public String call() throws Exception {
if (cell.getItem() == null) {
return null ;
} else {
switch (cell.getIndex() - selectionIndex.get()) {
case -1: return cell.getItem() + " (selected item below)";
case 0: return cell.getItem() + " (selected)";
case 1: return cell.getItem() + " (selected item above)";
default: return cell.getItem();
}
}
}
}, cell.itemProperty(), cell.indexProperty(), selectionIndex));
return cell;
}
});
final Scene scene = new Scene(root, 600, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
And here's a slightly more complex version. Here I have a custom data type which includes a boolean property. The update sets the boolean property of the specified item to true. The cell factory creates a cell, and observes the selected property both of the current item and of the previous item. Then, as before, it uses a binding to update the text of the cell.
import java.util.concurrent.Callable;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.IntegerBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Callback;
public class ListViewStyleAroundSelection extends Application {
#Override
public void start(Stage primaryStage) {
final ListView<MyDataType> listView = new ListView<>();
for (int i=0; i<=20; i++) {
listView.getItems().add(new MyDataType("Item "+i, false));
}
final HBox controls = new HBox(5);
controls.setPadding(new Insets(5));
final Button button = new Button("Set selection");
final TextField indexField = new TextField();
button.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
try {
int index = Integer.parseInt(indexField.getText());
if (index >= 0 && index < listView.getItems().size()) {
final MyDataType item = listView.getItems().get(index);
item.setSelected( ! item.isSelected() );
}
} catch (NumberFormatException nfe) {
indexField.setText("");
}
}
});
controls.getChildren().addAll(new Label("Enter selection index:"), indexField, button);
final BorderPane root = new BorderPane();
root.setCenter(listView);
root.setBottom(controls);
listView.setCellFactory(new Callback<ListView<MyDataType>, ListCell<MyDataType>>() {
#Override
public ListCell<MyDataType> call(ListView<MyDataType> lv) {
final ListCell<MyDataType> cell = new ListCell<>();
final IntegerBinding previousIndex = cell.indexProperty().subtract(1);
final ObjectBinding<MyDataType> previousItem = Bindings.valueAt(listView.getItems(), previousIndex);
final BooleanBinding previousItemSelected = Bindings.selectBoolean(previousItem, "selected");
final StringBinding thisItemName = Bindings.selectString(cell.itemProperty(), "name");
final BooleanBinding thisItemSelected = Bindings.selectBoolean(cell.itemProperty(), "selected");
cell.textProperty().bind(Bindings.createStringBinding(new Callable<String>() {
#Override
public String call() throws Exception {
if (cell.getItem() == null) {
return null ;
} else {
String value = cell.getItem().getName();
if (thisItemSelected.get()) {
value = value + " (selected) " ;
} else if (previousItemSelected.get()) {
value = value + " (selected item is above)";
}
return value ;
}
}
}, thisItemName, thisItemSelected, previousItemSelected));
return cell;
}
});
final Scene scene = new Scene(root, 600, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class MyDataType {
private final BooleanProperty selected ;
private final StringProperty name ;
public MyDataType(String name, boolean selected) {
this.name = new SimpleStringProperty(this, "name", name);
this.selected = new SimpleBooleanProperty(this, "selected", selected);
}
public final String getName() {
return name.get();
}
public final void setName(String name) {
this.name.set(name);
}
public final StringProperty nameProperty() {
return name ;
}
public final boolean isSelected() {
return selected.get();
}
public final void setSelected(boolean selected) {
this.selected.set(selected);
}
public final BooleanProperty selectedProperty() {
return selected;
}
}
public static void main(String[] args) {
launch(args);
}
}
Cell has a style class called ".cell"
public Cell getListCell(ListView list, int index){
Object[]cells = list.lookupAll(".cell").toArray();
return (Cell)cells[index];
}
This is the method I used to solve the same problem. Please note that getting the cell view is considered bad practice, and shouldn't be done in a normal context, updating cells should only be done through the model, my special case was that I wanted to fire an event manually as part of a workaround.
private ListCell<?> getListCell(ListView<?> listView, int cellIndex) {
if (cellIndex == -1) {
return null;
}
//Virtual Flow is the container of all list cells
//Each ListView has exactly one VirtualFlow which we are searching for
Optional<VirtualFlow> virtualFlowOptional = listView.getChildrenUnmodifiable()
.stream()
.filter(node -> node instanceof VirtualFlow)
.map(n -> (VirtualFlow) n)
.findFirst();
if (virtualFlowOptional.isEmpty()) {
return null;
}
VirtualFlow<ListCell<?>> virtualFlow = virtualFlowOptional.get();
return virtualFlow.getCell(cellIndex);
}

Resources