javafx TableView with dynamic ContextMenu on rows - javafx

I'm trying to make a java media player with DLNA Control Point.
There is a table with media files.
With JavaFX TableView, what I have learned, within the setRowFactory callback, we can add listeners on events, generated by table elements properties. All event types of TableView are fired only on internal table data changes.
I can't find a way to get to the table rows in case of some external event or logic, and to modify, for example, the ContextMenu for each row.
Each row in a table represents a media file. The ContextMenu initially has only "Play" (locally) and "Delete" menu items.
For instance, a DLNA renderer device has appeared on the network. DLNA discovery thread has fired an event and I want to add a "Play to this device" menu item to the context menu of each table row. Respectively, I will need to remove this item, as soon as the corresponding device will go off.
How to hook to the ContextMenu of each row from outside of the rowFactory stuff?
Here's the code of the table and row factory
public FileManager(GuiController guiController) {
gCtrl = guiController;
gCtrl.fileName.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Name"));
gCtrl.fileType.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Type"));
gCtrl.fileSize.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Size"));
gCtrl.fileTime.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("modifiedTime"));
gCtrl.filesTable.setRowFactory(tv -> {
TableRow<FileTableItem> row = new TableRow<>();
row.emptyProperty().addListener((obs, wasEmpty, isEmpty) -> {
if (!isEmpty) {
FileTableItem file = row.getItem();
ContextMenu contextMenu = new ContextMenu();
if (file.isPlayable()) {
row.setOnMouseClicked(event -> {
if (event.getClickCount() == 2) {
gCtrl.playMedia(file.getAbsolutePath());
}
});
MenuItem playMenuItem = new MenuItem("Play");
playMenuItem.setOnAction(event -> {
gCtrl.playMedia(file.getAbsolutePath());
});
contextMenu.getItems().add(playMenuItem);
}
if (file.canWrite()) {
MenuItem deleteMenuItem = new MenuItem("Delete");
deleteMenuItem.setOnAction(event -> {
row.getItem().delete();
});
contextMenu.getItems().add(deleteMenuItem);
}
row.setContextMenu(contextMenu);
}
});
return row;
});
gCtrl.filesTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
}
...
public class FileTableItem extends File {
...
}
Thanks in advance

JavaFX generally follows MVC/MVP type patterns. In a table view, the TableRow is part of the view: therefore to change the appearance of the table row (including the content of the context menu associated with it in this case), you should let it observe some kind of model, and to change what is displayed in the context menu you change that model.
I'm not entirely sure if I've understood your use case correctly, but I think I understand that each item in the table may have a different set of devices associated with it. So you would have an entity class looking something like this:
public class FileTableItem extends File {
private final ObservableList<Device> devices = FXCollections.observableArrayList();
public ObservableList<Device> getDevices() {
return devices ;
}
}
When you create the table row, you need it to observe the list of devices associated with its current item; you can do this with a ListChangeListener. Of course, the item being displayed at any given time by a row can change at arbitrary times beyond your control (when the user scrolls the table, for example), so you need to observe the row's item property and make sure the ListChangeListener is observing the correct list of items. Here is some code that achieves this:
TableView<FileTableItem> filesTable = new TableView<>();
filesTable.setRowFactory(tv -> {
TableRow<FileTableItem> row = new TableRow<>();
ContextMenu menu = new ContextMenu();
ListChangeListener<FileTableItem> changeListener = (ListChangeListener.Change<? extends FileTableItem> c) ->
updateMenu(menu, row.getItem().getDevices());
row.itemProperty().addListener((obs, oldItem, newItem) -> {
if (oldItem != null) {
oldItem.getDevices().removeListener(changeListener);
}
if (newItem == null) {
contextMenu.getItems().clear();
} else {
newItem.getDevices().addListener(changeListener);
updateMenu(menu, newItem.getDevices());
}
});
row.emptyProperty().addListener((obs, wasEmpty, isNowEmpty) ->
row.setContextMenu(isNowEmpty ? null : menu));
return row ;
});
// ...
private void updateMenu(ContextMenu menu, List<Device> devices) {
menu.getItems().clear();
for (Device device : devices) {
MenuItem item = new MenuItem(device.toString());
item.setOnAction(e -> { /* ... */ });
menu.getItems().add(item);
}
}
This will now automatically update the context menu if the list of devices changes.
In the comments below your question you said you wanted there to be a getRows() method in the table. There isn't such a method, partly because the design is using a MVC approach as described. Even if there were, it wouldn't really help: suppose the list of devices for an item scrolled out of view changed - in that case there would not be a TableRow corresponding to that item, so you would not be able to get a reference to a row to change its context menu. Instead, with the setup described, you would simply update the model at the point in the code where you intend to update the table row.
You might need to modify this if you have menu items that are not dependent on the list, etc, but this should be enough to show the idea.
Here is a SSCCE. In this example, there are initially 20 items in the table, with no devices attached. The context menu for each shows just a "Delete" option which deletes the item. Instead of a background task which updates the items, I mimicked this with some controls. You can select an item in the table and add devices to it by pressing the "Add device" button: you will subsequently see "Play on device...." appearing in its context menu. Similarly "Remove device" will remove the last device in the list. The "Delay" check box will delay the addition or removal of a device by two seconds: this allows you to press the button and then (quickly) open the context menu; you will see the context menu update while it is being shown.
import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Duration;
public class DynamicContextMenuInTable extends Application {
private int deviceCount = 0 ;
private void addDeviceToItem(Item item) {
Device newDevice = new Device("Device "+(++deviceCount));
item.getDevices().add(newDevice);
}
private void removeDeviceFromItem(Item item) {
if (! item.getDevices().isEmpty()) {
item.getDevices().remove(item.getDevices().size() - 1);
}
}
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
TableColumn<Item, String> itemCol = new TableColumn<>("Item");
itemCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getName()));
table.getColumns().add(itemCol);
table.setRowFactory(tv -> {
TableRow<Item> row = new TableRow<>();
ContextMenu menu = new ContextMenu();
MenuItem delete = new MenuItem("Delete");
delete.setOnAction(e -> table.getItems().remove(row.getItem()));
menu.getItems().add(delete);
ListChangeListener<Device> deviceListListener = c ->
updateContextMenu(row.getItem(), menu);
row.itemProperty().addListener((obs, oldItem, newItem) -> {
if (oldItem != null) {
oldItem.getDevices().removeListener(deviceListListener);
}
if (newItem != null) {
newItem.getDevices().addListener(deviceListListener);
updateContextMenu(row.getItem(), menu);
}
});
row.emptyProperty().addListener((obs, wasEmpty, isNowEmpty) ->
row.setContextMenu(isNowEmpty ? null : menu));
return row ;
});
CheckBox delay = new CheckBox("Delay");
Button addDeviceButton = new Button("Add device");
addDeviceButton.disableProperty().bind(table.getSelectionModel().selectedItemProperty().isNull());
addDeviceButton.setOnAction(e -> {
Item selectedItem = table.getSelectionModel().getSelectedItem();
if (delay.isSelected()) {
PauseTransition pause = new PauseTransition(Duration.seconds(2));
pause.setOnFinished(evt -> {
addDeviceToItem(selectedItem);
});
pause.play();
} else {
addDeviceToItem(selectedItem);
}
});
Button removeDeviceButton = new Button("Remove device");
removeDeviceButton.disableProperty().bind(table.getSelectionModel().selectedItemProperty().isNull());
removeDeviceButton.setOnAction(e -> {
Item selectedItem = table.getSelectionModel().getSelectedItem() ;
if (delay.isSelected()) {
PauseTransition pause = new PauseTransition(Duration.seconds(2));
pause.setOnFinished(evt -> removeDeviceFromItem(selectedItem));
pause.play();
} else {
removeDeviceFromItem(selectedItem);
}
});
HBox buttons = new HBox(5, addDeviceButton, removeDeviceButton, delay);
BorderPane.setMargin(buttons, new Insets(5));
BorderPane root = new BorderPane(table, buttons, null, null, null);
for (int i = 1 ; i <= 20; i++) {
table.getItems().add(new Item("Item "+i));
}
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
private void updateContextMenu(Item item, ContextMenu menu) {
if (menu.getItems().size() > 1) {
menu.getItems().subList(1, menu.getItems().size()).clear();
}
for (Device device : item.getDevices()) {
MenuItem menuItem = new MenuItem("Play on "+device.getName());
menuItem.setOnAction(e -> System.out.println("Play "+item.getName()+" on "+device.getName()));
menu.getItems().add(menuItem);
}
}
public static class Device {
private final String name ;
public Device(String name) {
this.name = name ;
}
public String getName() {
return name ;
}
#Override
public String toString() {
return getName();
}
}
public static class Item {
private final ObservableList<Device> devices = FXCollections.observableArrayList() ;
private final String name ;
public Item(String name) {
this.name = name ;
}
public ObservableList<Device> getDevices() {
return devices ;
}
public String getName() {
return name ;
}
}
public static void main(String[] args) {
launch(args);
}
}

With an advice from sillyfly I got the working solution, however it may potentially have performance drawbacks. So it would be interesting to find a better one.
class FileManager {
private GuiController gCtrl;
protected Menu playToSub = new Menu("Play to...");
Map<String, MenuItem> playToItems = new HashMap<String, MenuItem>();
public FileManager(GuiController guiController) {
gCtrl = guiController;
gCtrl.fileName.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Name"));
gCtrl.fileType.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Type"));
gCtrl.fileSize.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Size"));
gCtrl.fileTime.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("modifiedTime"));
gCtrl.filesTable.setRowFactory(tv -> {
TableRow<FileTableItem> row = new TableRow<>();
row.emptyProperty().addListener((obs, wasEmpty, isEmpty) -> {
if (!isEmpty) {
FileTableItem file = row.getItem();
ContextMenu contextMenu = new ContextMenu();
if (file.isPlayable()) {
row.setOnMouseClicked(event -> {
if (event.getClickCount() == 2) {
gCtrl.mainApp.playFile = file.getName();
gCtrl.playMedia(file.getAbsolutePath());
}
});
MenuItem playMenuItem = new MenuItem("Play");
playMenuItem.setOnAction(event -> {
gCtrl.mainApp.playFile = file.getName();
gCtrl.playMedia(file.getAbsolutePath());
});
contextMenu.getItems().add(playMenuItem);
}
if (file.canWrite()) {
MenuItem deleteMenuItem = new MenuItem("Delete");
deleteMenuItem.setOnAction(event -> {
row.getItem().delete();
});
contextMenu.getItems().add(deleteMenuItem);
}
row.setContextMenu(contextMenu);
}
});
row.setOnContextMenuRequested((event) -> {
/// Here, just before showing the context menu we can decide what to show in it
/// In this particular case it's OK, but it may be time expensive in general
if(! row.isEmpty()) {
if(gCtrl.mainApp.playDevices.size() > 0) {
if(! row.getContextMenu().getItems().contains(playToSub)) {
row.getContextMenu().getItems().add(1, playToSub);
}
}
else {
if(row.getContextMenu().getItems().contains(playToSub)) {
row.getContextMenu().getItems().remove(playToSub);
}
}
}
});
return row;
});
gCtrl.filesTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
}
/// addPlayToMenuItem and removePlayToMenuItem are run from Gui Controller
/// which in turn is notified by events in UPNP module
/// The playTo sub menu items are managed here
public void addPlayToMenuItem(String uuid, String name, URL iconUrl) {
MenuItem playToItem = new PlayToMenuItem(uuid, name, iconUrl);
playToItems.put(uuid, playToItem);
playToSub.getItems().add(playToItem);
}
public void removePlayToMenuItem(String uuid) {
if(playToItems.containsKey(uuid)) {
playToSub.getItems().remove(playToItems.get(uuid));
playToItems.remove(uuid);
}
}
public class PlayToMenuItem extends MenuItem {
PlayToMenuItem(String uuid, String name, URL iconUrl) {
super();
if (iconUrl != null) {
Image icon = new Image(iconUrl.toString());
ImageView imgView = new ImageView(icon);
imgView.setFitWidth(12);
imgView.setPreserveRatio(true);
imgView.setSmooth(true);
imgView.setCache(true);
setGraphic(imgView);
}
setText(name);
setOnAction(event -> {
gCtrl.mainApp.playFile = gCtrl.filesTable.getSelectionModel().getSelectedItem().getName();
gCtrl.mainApp.startRemotePlay(uuid);
});
}
}
/// Other class methods and members
}

Related

javafx combobox checkbox multiselect filtered

I have looked days on any ready solution for the subject of having TOGETHER in javafx (pure) :
Combobox
Multiselect of items through Checkboxes
Filter items by the "editable" part of the Combobox
I have had no luck finding what I was looking for so I have now a working solution taken from different separate solution... Thank you to all for this !
Now I would like to know if what I have done follows the best practices or not... It's working... but is it "ugly" solution ? Or would that be a sort of base anyone could use ?
I tied to comment as much as I could, and also kept the basic comment of the sources :
user:2436221 (Jonatan Stenbacka) --> https://stackoverflow.com/a/34609439/14021197
user:5844477 (Sai Dandem) --> https://stackoverflow.com/a/52471561/14021197
Thank you for your opinions, and suggestions...
Here is the working example :
package application;
import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Callback;
#SuppressWarnings ("restriction") // Only applies for PROTECTD library : com.sun.javafx.scene.control.skin.ComboBoxListViewSkin
public class MultiSelectFiltered2 extends Application {
// These 2 next fields are used in order to keep the FILTERED TEXT entered by user.
private String aFilterText = "";
private boolean isUserChangeText = true;
public void start(Stage stage) {
Text txt = new Text(); // A place where to expose the result of checked items.
HBox vbxRoot = new HBox(); // A basic root to order the GUI
ComboBox<ChbxItems> cb = new ComboBox<ChbxItems>() {
// This part is needed in order to NOT have the list hided when an item is selected...
// TODO --> Seems a little ugly to me since this part is the PROTECTED part !
protected javafx.scene.control.Skin<?> createDefaultSkin() {
return new ComboBoxListViewSkin<ChbxItems>(this) {
#Override
protected boolean isHideOnClickEnabled() {
return false;
}
};
}
};
cb.setEditable(true);
// Create a list with some dummy values.
ObservableList<ChbxItems> items = FXCollections.observableArrayList();
items.add(new ChbxItems("One"));
items.add(new ChbxItems("Two"));
items.add(new ChbxItems("Three"));
items.add(new ChbxItems("Four"));
items.add(new ChbxItems("Five"));
items.add(new ChbxItems("Six"));
items.add(new ChbxItems("Seven"));
items.add(new ChbxItems("Eight"));
items.add(new ChbxItems("Nine"));
items.add(new ChbxItems("Ten"));
// Create a FilteredList wrapping the ObservableList.
FilteredList<ChbxItems> filteredItems = new FilteredList<ChbxItems>(items, p -> true);
// Add a listener to the textProperty of the combo box editor. The
// listener will simply filter the list every time the input is changed
// as long as the user hasn't selected an item in the list.
cb.getEditor().textProperty().addListener((obs, oldValue, newValue) -> {
// This needs to run on the GUI thread to avoid the error described here:
// https://bugs.openjdk.java.net/browse/JDK-8081700.
Platform.runLater(() -> {
if (isUserChangeText) {
aFilterText = cb.getEditor().getText();
}
// If the no item in the list is selected or the selected item
// isn't equal to the current input, we re-filter the list.
filteredItems.setPredicate(item -> {
boolean isPartOfFilter = true;
// We return true for any items that starts with the
// same letters as the input. We use toUpperCase to
// avoid case sensitivity.
if (!item.getText().toUpperCase().startsWith(newValue.toUpperCase())) {
isPartOfFilter = false;
}
return isPartOfFilter;
});
isUserChangeText = true;
});
});
cb.setCellFactory(new Callback<ListView<ChbxItems>, ListCell<ChbxItems>>() {
#Override
public ListCell<ChbxItems> call(ListView<ChbxItems> param) {
return new ListCell<ChbxItems>() {
private CheckBox chbx = new CheckBox();
// This 'just open bracket' opens the newly CheckBox Class specifics
{
chbx.setOnAction(new EventHandler<ActionEvent>() {
// This VERY IMPORTANT part will effectively set the ChbxItems item
// The argument is never used, thus left as 'arg0'
#Override
public void handle(ActionEvent arg0) {
// This is where the usual update of the check box refreshes the editor' text of the parent combo box... we want to avoid this ;-)
isUserChangeText = false;
// The one line without which your check boxes are going to be checked depending on the position in the list... which changes when the list gets filtered.
getListView().getSelectionModel().select(getItem());
// Updating the exposed text from the list of checked items... This is added here to have a 'live' update.
txt.setText(updateListOfValuesChosen(items));
}
});
}
private BooleanProperty booleanProperty; //Will be used for binding... explained bellow.
#Override
protected void updateItem(ChbxItems item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
// Binding is used in order to link the checking (selecting) of the item, with the actual 'isSelected' field of the ChbxItems object.
if (booleanProperty != null) {
chbx.selectedProperty().unbindBidirectional(booleanProperty);
}
booleanProperty = item.isSelectedProperty();
chbx.selectedProperty().bindBidirectional(booleanProperty);
// This is the usual part for the look of the cell
setGraphic(chbx);
setText(item.getText() + "");
} else {
// Look of the cell, which has to be "reseted" if no item is attached (empty is true).
setGraphic(null);
setText("");
}
// Setting the 'editable' part of the combo box to what the USER wanted
// --> When 'onAction' of the check box, the 'behind the scene' update will refresh the combo box editor with the selected object reference otherwise.
cb.getEditor().setText(aFilterText);
cb.getEditor().positionCaret(aFilterText.length());
}
};
}
});
// Yes, it's the filtered items we want to show in the combo box...
// ...but we want to run through the original items to find out if they are checked or not.
cb.setItems(filteredItems);
// Some basic cosmetics
vbxRoot.setSpacing(15);
vbxRoot.setPadding(new Insets(25));
vbxRoot.setAlignment(Pos.TOP_LEFT);
// Adding the visual children to root VBOX
vbxRoot.getChildren().addAll(txt, cb);
// Ordinary Scene & Stage settings and initialization
Scene scene = new Scene(vbxRoot);
stage.setScene(scene);
stage.show();
}
// Just a method to expose the list of items checked...
// This is the result that will be probably the input for following code.
// -->
// If the class ChbxItems had a custom object rather than 'text' field,
// the resulting checked items from here could be a list of these custom objects --> VERY USEFUL
private String updateListOfValuesChosen(ObservableList<ChbxItems> items) {
StringBuilder sb = new StringBuilder();
items.stream().filter(ChbxItems::getIsSelected).forEach(cbitem -> {
sb.append(cbitem.getText()).append("\n");
});
return sb.toString();
}
// The CHECKBOX object, with 2 fields :
// - The boolean part (checked ot not)
// - The text part which is shown --> Could be a custom object with 'toString()' overridden ;-)
class ChbxItems {
private SimpleStringProperty text = new SimpleStringProperty();
private BooleanProperty isSelected = new SimpleBooleanProperty();
public ChbxItems(String sText) {
setText(sText);
}
public void setText(String text) {
this.text.set(text);
}
public String getText() {
return text.get();
}
public SimpleStringProperty textProperty() {
return text;
}
public void setIsSelected(boolean isSelected) {
this.isSelected.set(isSelected);
}
public boolean getIsSelected() {
return isSelected.get();
}
public BooleanProperty isSelectedProperty() {
return isSelected;
}
}
public static void main(String[] args) {
launch();
}
}

Context menu on TableRow<Object> does not show up on first right click

So I followed this example on using context menu with TableViews from here. I noticed that using this code
row.contextMenuProperty().bind(Bindings.when(Bindings.isNotNull(row.itemProperty()))
.then(rowMenu)
.otherwise((ContextMenu)null));
does not show up on first right click on a row with values. I need to right click on that row again for the context menu to show up. I also tried this code(which is my first approach, but not using it anymore because I've read somewhere that that guide is the best/good practice for anything related about context menu and tableview), and it displays the context menu immediately
if (row.getItem() != null) {
rowMenu.show(row, event.getScreenX(), event.getScreenY());
}
else {
// do nothing
}
but my problem with this code is it throws a NullPointerException whenever i try to right click on a row that has no data.
What could I possibly do to prevent NullPointerException while having the context menu show up immediately after a right click? In my code, I also have a code that a certain menu item in the context menu will be disabled based on the property of the myObject binded to row, that's why i need the context menu to pop up right away.
I noticed this too with the first block of code. Even if the property of myObject has already changed, it still has a menu item enabled/disabled unless I right click on that row again. I hope that you could help me. Thank you!
Here is a MCVE:
public class MCVE_TableView extends Application{
#Override
public void start(Stage primaryStage) throws Exception {
BorderPane myBorderPane = new BorderPane();
TableView<People> myTable = new TableView<>();
TableColumn<People, String> nameColumn = new TableColumn<>();
TableColumn<People, Integer> ageColumn = new TableColumn<>();
ContextMenu rowMenu = new ContextMenu();
ObservableList<People> peopleList = FXCollections.observableArrayList();
peopleList.add(new People("John Doe", 23));
nameColumn.setMinWidth(100);
nameColumn.setCellValueFactory(
new PropertyValueFactory<>("Name"));
ageColumn.setMinWidth(100);
ageColumn.setCellValueFactory(
new PropertyValueFactory<>("Age"));
myTable.setItems(peopleList);
myTable.getColumns().addAll(nameColumn, ageColumn);
myTable.setRowFactory(tv -> {
TableRow<People> row = new TableRow<>();
row.setOnContextMenuRequested((event) -> {
People selectedRow = row.getItem();
rowMenu.getItems().clear();
MenuItem sampleMenuItem = new MenuItem("Sample Button");
if (selectedRow != null) {
if (selectedRow.getAge() > 100) {
sampleMenuItem.setDisable(true);
}
rowMenu.getItems().add(sampleMenuItem);
}
else {
event.consume();
}
/*if (row.getItem() != null) { // this block comment displays the context menu instantly
rowMenu.show(row, event.getScreenX(), event.getScreenY());
}
else {
// do nothing
}*/
// this requires the row to be right clicked 2 times before displaying the context menu
row.contextMenuProperty().bind(Bindings.when(Bindings.isNotNull(row.itemProperty()))
.then(rowMenu)
.otherwise((ContextMenu)null));
});
return row;
});
myBorderPane.setCenter(myTable);
Scene scene = new Scene(myBorderPane, 500, 500);
primaryStage.setTitle("MCVE");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main (String[] args) {
launch(args);
}
}
Here is the People Class
public class People {
SimpleStringProperty name;
SimpleIntegerProperty age;
public People(String name, int age) {
this.name = new SimpleStringProperty(name);
this.age = new SimpleIntegerProperty(age);
}
public SimpleStringProperty NameProperty() {
return this.name;
}
public SimpleIntegerProperty AgeProperty() {
return this.age;
}
public String getName() {
return this.name.get();
}
public int getAge() {
return this.age.get();
}
}
Edit: MCVE added
Edit2: Updated the MCVE. Still requires to be right-clicked twice before the contextMenu pops up
Below's a code snippet as a quick demonstration of how-to/where-to instantiate and configure a per-row ContextMenu. It
creates a ContextMenu/MenuItem for each TableRow at the row's instantiation time
creates a conditional binding that binds the menu to the row's contextMenuProperty if not empty (just the same as you did)
configures the contextMenu in an onShowing handler, depending on the current item (note: no need for a guard against null, because the conditional binding will implicitly guarantee to not show the the menu in that case)
The snippet:
myTable.setRowFactory(tv -> {
TableRow<People> row = new TableRow<>() {
ContextMenu rowMenu = new ContextMenu();
MenuItem sampleMenuItem = new MenuItem("Sample Button");
{
rowMenu.getItems().addAll(sampleMenuItem);
contextMenuProperty()
.bind(Bindings
.when(Bindings.isNotNull(itemProperty()))
.then(rowMenu).otherwise((ContextMenu) null));
rowMenu.setOnShowing(e -> {
People selectedRow = getItem();
sampleMenuItem.setDisable(selectedRow.getAge() > 100);
});
}
};
return row;
});

EventFilter for ComboBox selected Item

How can I write an EventFilter for the SelectedItem property of a ComboBox? This Article only describes it for user Events like a MouseEvent, and I cant seem to find out what EventType the selectedItem property changing is.
I ask because I have a 3D Application in a Dialog that displays materials on a slot. That slot can be switched with my Combobox, but I want to be able to filter BEFORE the actual change in the selection happens, see if I have any unsaved changes and show a dialog wheter the user wants to save the changes or abort. And since I have a variety of listeners on the combobox that switch out the materials in the 3D when the selection in the ComboBox changes, the abort functionality on that dialog is not easily achieved.
I am also open to other approaches of a "Do you want to save Changes?" implementation which may be better suited.
Consider creating another property to represent the value in the combo box, and only updating it if the user confirms. Then the rest of your application can just observe that property.
So, e.g.
private ComboBox<MyData> combo = ... ;
private boolean needsConfirmation = true ;
private final ReadOnlyObjectWrapper<MyData> selectedValue = new ReadOnlyObjectWrapper<>();
public ReadOnlyObjectProperty<MyData> selectedValueProperty() {
return selectedValue.getReadOnlyProperty() ;
}
public final MyData getSelectedValue() {
return selectedValueProperty().get();
}
// ...
combo.valueProperty().addListener((obs, oldValue, newValue) -> {
if (needsConfirmation) {
// save changes dialog:
Dialog<ButtonType> dialog = ... ;
Optional<ButtonType> response = dialog.showAndWait();
if (response.isPresent()) {
if (response.get() == ButtonType.YES) {
// save changes, then:
selectedValue.set(newValue);
} else if (response.get() == ButtonType.NO) {
// make change without saving:
selectedValue.set(newValue);
} else if (response.get() == ButtonType.CANCEL) {
// revert to old value, make sure we don't display dialog again:
// Platform.runLater() is annoying workaround required to avoid
// changing contents of list (combo's selected items) while list is processing change:
Platform.runLater(() -> {
needsConfirmation = false ;
combo.setValue(oldValue);
needsConfirmation = true ;
});
}
} else {
needsConfirmation = false ;
combo.setValue(oldValue);
needsConfirmation = true ;
}
}
});
Now your application can just observe the selectedValueProperty() and respond if it changes:
selectionController.selectedValueProperty().addListener((obs, oldValue, newValue) -> {
// respond to change...
});
Here's a (very simple) SSCCE:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class InterceptComboBox extends Application {
private ComboBox<String> combo ;
private boolean needsConfirmation = true ;
private Label view ;
private final ReadOnlyObjectWrapper<String> selectedValue = new ReadOnlyObjectWrapper<String>();
public ReadOnlyObjectProperty<String> selectedValueProperty() {
return selectedValue.getReadOnlyProperty();
}
public final String getSelectedValue() {
return selectedValueProperty().get();
}
#Override
public void start(Stage primaryStage) {
combo = new ComboBox<>();
combo.getItems().addAll("One", "Two", "Three");
combo.setValue("One");
selectedValue.set("One");
view = new Label();
view.textProperty().bind(Bindings.concat("This is view ", selectedValue));
combo.valueProperty().addListener((obs, oldValue, newValue) -> {
if (needsConfirmation) {
SaveChangesResult saveChanges = showSaveChangesDialog();
if (saveChanges.save) {
saveChanges();
}
if (saveChanges.proceed) {
selectedValue.set(newValue);
} else {
Platform.runLater(() -> {
needsConfirmation = false ;
combo.setValue(oldValue);
needsConfirmation = true ;
});
}
}
});
BorderPane root = new BorderPane(view);
BorderPane.setAlignment(combo, Pos.CENTER);
BorderPane.setMargin(combo, new Insets(5));
root.setTop(combo);
primaryStage.setScene(new Scene(root, 400, 400));
primaryStage.show();
}
private void saveChanges() {
System.out.println("Save changes");
}
private SaveChangesResult showSaveChangesDialog() {
DialogPane dialogPane = new DialogPane();
dialogPane.setContentText("Save changes?");
dialogPane.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
Dialog<SaveChangesResult> dialog = new Dialog<>();
dialog.setDialogPane(dialogPane);
dialog.setResultConverter(button -> {
if (button == ButtonType.YES) return SaveChangesResult.SAVE_CHANGES ;
else if (button == ButtonType.NO) return SaveChangesResult.PROCEED_WITHOUT_SAVING ;
else return SaveChangesResult.CANCEL ;
});
return dialog.showAndWait().orElse(SaveChangesResult.CANCEL);
}
enum SaveChangesResult {
SAVE_CHANGES(true, true), PROCEED_WITHOUT_SAVING(true, false), CANCEL(false, false) ;
private boolean proceed ;
private boolean save ;
SaveChangesResult(boolean proceed, boolean save) {
this.proceed = proceed ;
this.save = save ;
}
}
public static void main(String[] args) {
launch(args);
}
}
To do this you want to add a ChangeListener to the valueProperty() of the ComboBox
Here is an example:
comboBox.valueProperty().addListener(new ChangeListener<Object>()
{
#Override
public void changed(ObservableValue observable, Object oldValue, Object newValue)
{
Optional<ButtonType> result = saveAlert.showAndWait();
if(result.isPresent())
{
if(result.get() == ButtonType.YES)
{
//Your Save Functionality
comboBox.valueProperty().setValue(newValue);
}
else
{
//Whatever
comboBox.valueProperty().setValue(oldValue);
}
}
}
});

Implementing tab functionality for CheckBox cells in TableView

I've created a TableView where each cell contains a TextField or a CheckBox. In the TableView you're supposed to be able to navigate left and right between cells using TAB and SHIFT+TAB, and up and down between cells using the UP and DOWN keys.
This works perfectly when a text field cell is focused. But when a check box cell is focused, the tab funcationality behaves strange. You can tab in the opposite direction of the cell you tabbed from, but you can't switch tab direction.
So for instance if you tabbed to the check box cell using only the TAB key, then SHIFT+TAB wont work. But if you tab to the next cell using the TAB key, and then TAB back using SHIFT+TAB (assuming the next cell is a text field cell), then TAB wont work.
I've tried running any code handling focus on the UI thread using Platform.runLater(), without any noteable difference. All I know is that the TAB KeyEvent is properly catched, but the check box cell and the check box never loses focus in the first place anyway. I've tried for instance removing its focus manually by doing e.g. getParent().requestFocus() but that just results in the parent being focused instead of the next cell. What makes it strange is that the same code is executed and working properly when you tab in the opposite direction of the cell you came from.
Here's a MCVE on the issue. Sadly it does not really live up to the "M" of the abbreviation:
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.CheckBox;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.stage.Stage;
public class AlwaysEditableTable extends Application {
public void start(Stage stage) {
TableView<ObservableList<StringProperty>> table = new TableView<>();
table.setEditable(true);
table.getSelectionModel().setCellSelectionEnabled(true);
table.setPrefWidth(510);
// Dummy columns
ObservableList<String> columns = FXCollections.observableArrayList("Column1", "Column2", "Column3", "Column4",
"Column5");
// Dummy data
ObservableList<StringProperty> row1 = FXCollections.observableArrayList(new SimpleStringProperty("Cell1"),
new SimpleStringProperty("Cell2"), new SimpleStringProperty("0"), new SimpleStringProperty("Cell4"),
new SimpleStringProperty("0"));
ObservableList<StringProperty> row2 = FXCollections.observableArrayList(new SimpleStringProperty("Cell1"),
new SimpleStringProperty("Cell2"), new SimpleStringProperty("1"), new SimpleStringProperty("Cell4"),
new SimpleStringProperty("0"));
ObservableList<StringProperty> row3 = FXCollections.observableArrayList(new SimpleStringProperty("Cell1"),
new SimpleStringProperty("Cell2"), new SimpleStringProperty("1"), new SimpleStringProperty("Cell4"),
new SimpleStringProperty("0"));
ObservableList<ObservableList<StringProperty>> data = FXCollections.observableArrayList(row1, row2, row3);
for (int i = 0; i < columns.size(); i++) {
final int j = i;
TableColumn<ObservableList<StringProperty>, String> col = new TableColumn<>(columns.get(i));
col.setCellValueFactory(param -> param.getValue().get(j));
col.setPrefWidth(100);
if (i == 2 || i == 4) {
col.setCellFactory(e -> new CheckBoxCell(j));
} else {
col.setCellFactory(e -> new AlwaysEditingCell(j));
}
table.getColumns().add(col);
}
table.setItems(data);
Scene scene = new Scene(table);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
/**
* A cell that contains a text field that is always shown.
*/
public static class AlwaysEditingCell extends TableCell<ObservableList<StringProperty>, String> {
private final TextField textField;
public AlwaysEditingCell(int columnIndex) {
textField = new TextField();
this.emptyProperty().addListener((obs, wasEmpty, isNowEmpty) -> {
if (isNowEmpty) {
setGraphic(null);
} else {
setGraphic(textField);
}
});
// The index is not changed until tableData is instantiated, so this
// ensure the we wont get a NullPointerException when we do the
// binding.
this.indexProperty().addListener((obs, oldValue, newValue) -> {
ObservableList<ObservableList<StringProperty>> tableData = getTableView().getItems();
int oldIndex = oldValue.intValue();
if (oldIndex >= 0 && oldIndex < tableData.size()) {
textField.textProperty().unbindBidirectional(tableData.get(oldIndex).get(columnIndex));
}
int newIndex = newValue.intValue();
if (newIndex >= 0 && newIndex < tableData.size()) {
textField.textProperty().bindBidirectional(tableData.get(newIndex).get(columnIndex));
setGraphic(textField);
} else {
setGraphic(null);
}
});
// Every time the cell is focused, the focused is passed down to the
// text field and all of the text in the textfield is selected.
this.focusedProperty().addListener((obs, oldValue, newValue) -> {
if (newValue) {
textField.requestFocus();
textField.selectAll();
System.out.println("Cell focused!");
}
});
// Switches focus to the cell below if ENTER or the DOWN arrow key
// is pressed, and to the cell above if the UP arrow key is pressed.
// Works like a charm. We don't have to add any functionality to the
// TAB key in these cells because the default tab behavior in
// JavaFX works here.
this.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
if (e.getCode().equals(KeyCode.UP)) {
getTableView().getFocusModel().focus(getIndex() - 1, this.getTableColumn());
e.consume();
} else if (e.getCode().equals(KeyCode.DOWN)) {
getTableView().getFocusModel().focus(getIndex() + 1, this.getTableColumn());
e.consume();
} else if (e.getCode().equals(KeyCode.ENTER)) {
getTableView().getFocusModel().focus(getIndex() + 1, this.getTableColumn());
e.consume();
}
});
}
}
/**
* A cell containing a checkbox. The checkbox represent the underlying value
* in the cell. If the cell value is 0, the checkbox is unchecked. Checking
* or unchecking the checkbox will change the underlying value.
*/
public static class CheckBoxCell extends TableCell<ObservableList<StringProperty>, String> {
private final CheckBox box;
public CheckBoxCell(int columnIndex) {
this.box = new CheckBox();
this.emptyProperty().addListener((obs, wasEmpty, isNowEmpty) -> {
if (isNowEmpty) {
setGraphic(null);
} else {
setGraphic(box);
}
});
this.indexProperty().addListener((obs, oldValue, newValue) -> {
// System.out.println("Row: " + getIndex() + ", Column: " +
// columnIndex + ". Old index: " + oldValue
// + ". New Index: " + newValue);
ObservableList<ObservableList<StringProperty>> tableData = getTableView().getItems();
int newIndex = newValue.intValue();
if (newIndex >= 0 && newIndex < tableData.size()) {
// If active value is "1", the check box will be set to
// selected.
box.setSelected(tableData.get(getIndex()).get(columnIndex).equals("1"));
// We add a listener to the selected property. This will
// allow us to execute code every time the check box is
// selected or deselected.
box.selectedProperty().addListener((observable, oldVal, newVal) -> {
if (newVal) {
// If newValue is true the checkBox is selected, and
// we set the corresponding cell value to "1".
tableData.get(getIndex()).get(columnIndex).set("1");
} else {
// Otherwise we set it to "0".
tableData.get(getIndex()).get(columnIndex).set("0");
}
});
setGraphic(box);
} else {
setGraphic(null);
}
});
// If I listen to KEY_RELEASED instead, pressing tab next to a
// checkbox will make the focus jump past the checkbox cell. This is
// probably because the default TAB functionality is invoked on key
// pressed, which switches the focus to the check box cell, and then
// upon release this EventFilter catches it and switches focus
// again.
this.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode().equals(KeyCode.UP)) {
System.out.println("UP key pressed in checkbox");
getTableView().getFocusModel().focus(getIndex() - 1, this.getTableColumn());
e.consume();
} else if (e.getCode().equals(KeyCode.DOWN)) {
System.out.println("DOWN key pressed in checkbox");
getTableView().getFocusModel().focus(getIndex() + 1, this.getTableColumn());
e.consume();
} else if (e.getCode().equals(KeyCode.TAB)) {
System.out.println("Checkbox TAB pressed!");
TableColumn<ObservableList<StringProperty>, ?> nextColumn = getNextColumn(!e.isShiftDown());
if (nextColumn != null) {
getTableView().getFocusModel().focus(getIndex(), nextColumn);
}
e.consume();
// ENTER key will set the check box to selected if it is
// unselected and vice versa.
} else if (e.getCode().equals(KeyCode.ENTER)) {
box.setSelected(!box.isSelected());
e.consume();
}
});
// Tracking the focused property of the check box for debug
// purposes.
box.focusedProperty().addListener((obs, oldValue, newValue) ->
{
if (newValue) {
System.out.println("Box focused on index " + getIndex());
} else {
System.out.println("Box unfocused on index " + getIndex());
}
});
// Tracking the focused property of the check box for debug
// purposes.
this.focusedProperty().addListener((obs, oldValue, newValue) ->
{
if (newValue) {
System.out.println("Box cell focused on index " + getIndex());
box.requestFocus();
} else {
System.out.println("Box cell unfocused on index " + getIndex());
}
});
}
/**
* Gets the column to the right or to the left of the current column
* depending no the value of forward.
*
* #param forward
* If true, the column to the right of the current column
* will be returned. If false, the column to the left of the
* current column will be returned.
*/
private TableColumn<ObservableList<StringProperty>, ?> getNextColumn(boolean forward) {
List<TableColumn<ObservableList<StringProperty>, ?>> columns = getTableView().getColumns();
// If there's less than two columns in the table view we return null
// since there can be no column to the right or left of this
// column.
if (columns.size() < 2) {
return null;
}
// We get the index of the current column and then we get the next
// or previous index depending on forward.
int currentIndex = columns.indexOf(getTableColumn());
int nextIndex = currentIndex;
if (forward) {
nextIndex++;
if (nextIndex > columns.size() - 1) {
nextIndex = 0;
}
} else {
nextIndex--;
if (nextIndex < 0) {
nextIndex = columns.size() - 1;
}
}
// We return the column on the next index.
return columns.get(nextIndex);
}
}
}
After some digging in the TableView source code I found the issue. Here's the source code for the focus(int row, TableColumn<S, ?> column) method:
#Override public void focus(int row, TableColumn<S,?> column) {
if (row < 0 || row >= getItemCount()) {
setFocusedCell(EMPTY_CELL);
} else {
TablePosition<S,?> oldFocusCell = getFocusedCell();
TablePosition<S,?> newFocusCell = new TablePosition<>(tableView, row, column);
setFocusedCell(newFocusCell);
if (newFocusCell.equals(oldFocusCell)) {
// manually update the focus properties to ensure consistency
setFocusedIndex(row);
setFocusedItem(getModelItem(row));
}
}
}
The issue arises when newFocusCell is compared to oldFocusCell. When tabbing to a checkbox cell the cell would for some reason not get set as the focused cell. Hence the focusedCell property returned by getFocusedCell() will be the cell we focused before the check box cell. So when we then try to focus that previous cell again, newFocusCell.equals(oldFocusCell) will return true, and the focus will be set to the currently focused cell again by doing:
setFocusedIndex(row);
setFocusedItem(getModelItem(row));`
So what I had to do was make sure that the cell isn't be the value of the focusedCell property when we want to focus it. I solved this by setting the focus manually to the whole table before trying to switch the focus from the check box cell:
table.requestFocus();

javafx previous tabs affect newest tab

Everything works fine until I create a new tab. Then when I go to the previous and try to use any of the buttons they affect the latest tab not the one I have selected. But if I go to the latest tab it works like normal. Here is the class that I use to make my tabs. So, why is the previous tabs affecting the lastest? And how do I fix it?
public class JTab {
private javafx.scene.control.Tab tab;
private ImageView imgView;
private Image logo;
private BorderPane root;
private Button reloadButton, backButton, forwardButton;
private TextField field;
private WebView view;
private WebEngine engine;
private static JTab instance;
private JBrowser jBrowser;
private JTab(JBrowser jBrowser) {
this.jBrowser = jBrowser;
}
public static JTab getInstance(JBrowser browser) {
if(instance == null)
instance = new JTab(browser);
return instance;
}
public javafx.scene.control.Tab addTab() {
tab = new Tab();
tab.setText("New Tab");
tab.setOnClosed(event2 -> {
if(jBrowser.getTabPane().getTabs().size() == 1) {
jBrowser.getTabPane().setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
}
});
logo = new Image("unknown-document.png");
imgView = new ImageView(logo);
tab.setGraphic(imgView);
HBox hBox = new HBox(5);
hBox.setAlignment(Pos.CENTER);
reloadButton = new Button("Reload");
backButton = new Button("<");
forwardButton = new Button(">");
reloadButton.setOnAction(event1 -> engine.reload());
backButton.setOnAction(event1 -> loadData(goBack()));
forwardButton.setOnAction(event1 -> loadData(goForward()));
//The TextField for entering web addresses.
field = new TextField("Enter URL");
field.setPrefColumnCount(50); //make the field at least 50 columns wide.
field.focusedProperty().addListener((ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) -> { //When click on field entire thing selected
Platform.runLater(() -> {
if (field.isFocused() && !field.getText().isEmpty()) {
field.selectAll();
}
});
});
field.setOnKeyPressed(event -> { //When ENTER is pressed it will load page
if (event.getCode() == KeyCode.ENTER) {
if (!field.getText().isEmpty()) {
loadData(field.getText());
}
}
});
//Add all out navigation nodes to the vbox.
hBox.getChildren().addAll(backButton, forwardButton, reloadButton, field);
view = new WebView();
engine = view.getEngine();
engine.setJavaScriptEnabled(true);
engine.getLoadWorker().stateProperty().addListener(
(ov, oldState, newState) -> {
if (newState == Worker.State.SUCCEEDED) {
tab.setText(getTitle());
//TODO setGraphic
}
});
loadData("google.com");
root = new BorderPane();
root.setPrefSize(1024, 768);
root.setTop(hBox);
root.setCenter(view);
tab.setContent(root);
return tab;
}
public void loadData(String URL) {
if(!URL.startsWith("http://")) {
URL = "http://" + URL;
}
field.setText(URL);
tab.setText(URL);
engine.load(URL);
}
private String getTitle() {
Document doc = engine.getDocument();
NodeList heads = doc.getElementsByTagName("head");
String titleText = engine.getLocation() ; // use location if page does not define a title
if (heads.getLength() > 0) {
Element head = (Element)heads.item(0);
NodeList titles = head.getElementsByTagName("title");
if (titles.getLength() > 0) {
Node title = titles.item(0);
titleText = title.getTextContent();
}
}
return titleText;
}
private String goBack() {
final WebHistory history = engine.getHistory();
ObservableList<WebHistory.Entry> entryList = history.getEntries();
int currentIndex=history.getCurrentIndex();
Platform.runLater(() -> history.go(-1));
return entryList.get(currentIndex>0?currentIndex-1:currentIndex).getUrl();
}
private String goForward() {
final WebHistory history = engine.getHistory();
ObservableList<WebHistory.Entry> entryList=history.getEntries();
int currentIndex=history.getCurrentIndex();
Platform.runLater(() -> history.go(1));
return entryList.get(currentIndex<entryList.size()-1?currentIndex+1:currentIndex).getUrl();
}
}
Remove getInstance(jBrowser) method
Make the constructor public.
Then to add a tab to a tabPane do
tabPane.getTabs().add(new JTab(jBrowser).addTab());

Resources