javafx combobox checkbox multiselect filtered - javafx

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

Related

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

TableCell.setText(String) doesn't set the data value associated with the cell

In my particular case I have a custom implementation of a TableCell that contains a Button. This button invokes a method that returns a String to be displayed instead of the button. The visual change is done by setting the graphic in the cell to null and setting the text to the String, using TableCell.setText(String).
What I've realized - and worked around so far, is that TableCell.setText(String) doesn't change the data value associated with the cell in the TableView. It just changes the visual representation of the cell. The underlying data structure is in my case a ObservableList<String> that represents a row, and each element in the list is, of course, cell data.
My current solution is to set the underlying value doing this:
getTableView().getItems().get(getIndex()).set(getTableView().getColumns().indexOf(getTableColumn()), "Value");
And this works fine. But I mean, the code is barely readable.
It seems like the data in the TableView and the TableCell are entirely separated, since you need to access the TableView to set the underlying data for a cell. There is a TableCell.getItem() to get the data value, but there's no setItem(String) method to set it.
I hope I explained my issue good enough.
Is there a better and prettier way to do this? Why doesn't just `TableCell.setText(String) change the data value as well?
Edit: I'll explain what I am trying to implement:
I basically have a table where one column contains a button that will load some arbitrary data to the column when pressed. Once the data has been loaded, the button is removed from the column and the data is displayed instead. That is basically it. This works fine unless the table is sorted/filtered. Here's a MCVE of my implementation:
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
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.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.Duration;
public class MCVE extends Application {
private final BooleanProperty countLoading = new SimpleBooleanProperty(this, "countLoading", false);
#Override
public void start(Stage stage) {
int numOfCols = 3;
ObservableList<ObservableList<String>> tableData = FXCollections.observableArrayList();
// Generate dummy data.
for (int i = 0; i < 100; i++) {
ObservableList<String> row = FXCollections.observableArrayList();
for (int j = 0; j < numOfCols; j++)
row.add("Row" + i + "Col" + j);
tableData.add(row);
}
TableView<ObservableList<String>> table = new TableView<ObservableList<String>>();
// Add columns to the table.
for (int i = 0; i < numOfCols; i++) {
if (i == 2) {
final int j = i;
table.getColumns().add(addColumn(i, "Column " + i, e -> new QueueCountCell(j, countLoading)));
} else {
table.getColumns().add(addColumn(i, "Column " + i, null));
}
}
table.getItems().addAll(tableData);
Scene scene = new Scene(table);
stage.setScene(scene);
stage.show();
}
/**
* Returns a simple column.
*/
private TableColumn<ObservableList<String>, String> addColumn(int index, String name,
Callback<TableColumn<ObservableList<String>, String>, TableCell<ObservableList<String>, String>> callback) {
TableColumn<ObservableList<String>, String> col = new TableColumn<ObservableList<String>, String>(name);
col.setCellValueFactory(e -> new SimpleStringProperty(e.getValue().get(index)));
if (callback != null) {
col.setCellFactory(callback);
}
return col;
}
public static void main(String[] args) {
launch();
}
class QueueCountCell extends TableCell<ObservableList<String>, String> {
private final Button loadButton = new Button("Load");
public QueueCountCell(int colIndex, BooleanProperty countLoading) {
countLoading.addListener((obs, oldValue, newValue) -> {
if (newValue) {
loadButton.setDisable(true);
} else {
if (getIndex() >= 0 && getIndex() < this.getTableView().getItems().size()) {
loadButton.setDisable(false);
}
}
});
final Timeline timeline = new Timeline(new KeyFrame(Duration.ZERO, e -> setText("Loading .")),
new KeyFrame(Duration.millis(500), e -> setText("Loading . .")),
new KeyFrame(Duration.millis(1000), e -> setText("Loading . . .")),
new KeyFrame(Duration.millis(1500)));
timeline.setCycleCount(Animation.INDEFINITE);
loadButton.setOnAction(e -> {
new Thread(new Task<String>() {
#Override
public String call() throws InterruptedException {
// Simlute task working.
Thread.sleep(3000);
return "5";
}
#Override
public void running() {
setGraphic(null);
timeline.play();
countLoading.set(true);
}
#Override
public void succeeded() {
timeline.stop();
countLoading.set(false);
setText(getValue());
}
#Override
public void failed() {
timeline.stop();
countLoading.set(false);
setGraphic(loadButton);
setText(null);
this.getException().printStackTrace();
}
}).start();
});
}
#Override
public final void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setGraphic(null);
} else {
setGraphic(loadButton);
}
}
}
}
Background: MVC
Much of JavaFX is designed around a Model-View-Controller (MVC) pattern. This is a loosely-defined pattern with many variants, but the basic idea is that there are three components:
Model: an object (or objects) that represent the data. The Model knows nothing about how the data is presented to the user.
View: an object that presents the data to the user. The view does not do any logical processing or store the data; it just knows how to convert the data to some kind of presentation for the user.
Controller: an object that modifies the data in the model, often (though not exclusively) in response to user input.
There are several variants of this pattern, including MVP, MVVM, supervising controller, passive view, and others, but the unifying theme in all of them is that there is a separation between the view, which simply presents data but does not otherwise "know" what the data is, and the model, which stores the state (data) but knows nothing about how it might be presented. The usually-cited motivation for this is the ability to have multiple views of the same data which have no need to refer to each other.
In the "classical" implementation of this, the view "observes" the model via some kind of subscriber-notification pattern (e.g. an observer pattern). So the view will register with the model to be notified of changes to the data, and will repaint accordingly. Often, since the controller relies on event listeners on the components in the view, the controller and view are tightly coupled; however there is always clear separation between the view and the model.
The best reference I know for learning more about this is Martin Fowler.
Background: JavaFX Virtualized Controls
JavaFX has a set of "virtualized controls", which includes ListView, TableView, TreeView, and TreeTableView. These controls are designed to be able to present large quantities of data to the user in an efficient manner. The key observation behind the design is that data is relatively inexpensive to store in memory, whereas the UI components (which typically have hundreds of properties) consume a relatively large amount of memory and are computationally expensive (e.g. to perform layout, apply style, etc). Moreover, in a table (for example) with a large amount of backing data, only a small proportion of those data are visible at any time, and there is no real need for UI controls for the remaining data.
Virtualized controls in JavaFX employ a cell rendering mechanism, in which "cells" are created only for the visible data. As the user scrolls around the table, the cells are reused to display data that was previously not visible. This allows the creation of a relatively small number of cells even for extremely large data sets: the number of (expensive) cells created is basically constant with respect to the size of the data. The Cell class defines an updateItem(...) method that is invoked when the cell is reused to present different data. All this is possible because the design is built on MVC principles: the cell is the view, and the data is stored in the model. The documentation for Cell has details on this.
Note that this means that you must not use the cell for any kind of data storage, because when the user scrolls in the control, that state will be lost. General MVC principles dictate that this is what you should do anyway.
The code you posted doesn't work correctly, as it violates these rules. In particular, if you click one of the "Load" buttons, and then scroll before the loading is complete, the cell that is performing the loading will now be referring to the wrong item in the model, and you end up with a corrupted view. The following series of screenshots occurred from pressing "Load", taking a screenshot, scrolling, waiting for the load to complete, and taking another screenshot. Note the value appears to have changed for an item that is different to the item for which "Load" was pressed.
To fix this, you have to have a model that stores all of the state of the application: you cannot store any state in the cells. It is a general truth in JavaFX that in order to make the UI code elegant, you should start with a well-defined data model. In particular, since your view (cell) changes when the data is in the process of loading, the "loading state" needs to be part of the model. So each item in each row in your table is represented by two pieces of data: the actual data value (strings in your case), and the "loading state" of the data.
So I would start with a class that represents that. You could just use a String for the data, or you could make it more general by making it a generic class. I'll do the latter. A good implementation will also keep the two states consistent: if the data is null and we have not explicitly stated it is loading, we consider it not loaded; if the data is non-null, we consider it loaded. So we have:
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
public class LazyLoadingData<T> {
public enum LoadingState { NOT_LOADED, LOADING, LOADED }
private final ObjectProperty<T> data = new SimpleObjectProperty<>(null);
private final ReadOnlyObjectWrapper<LoadingState> loadingState
= new ReadOnlyObjectWrapper<>(LoadingState.NOT_LOADED);
public LazyLoadingData(T data) {
// listeners to keep properties consistent with each other:
this.data.addListener((obs, oldData, newData) -> {
if (newData == null) {
loadingState.set(LoadingState.NOT_LOADED);
} else {
loadingState.set(LoadingState.LOADED);
}
});
this.loadingState.addListener((obs, oldState, newState) -> {
if (newState != LoadingState.LOADED) {
this.data.set(null);
}
});
this.data.set(data);
}
public LazyLoadingData() {
this(null);
}
public void startLoading() {
loadingState.set(LoadingState.LOADING);
}
public final ObjectProperty<T> dataProperty() {
return this.data;
}
public final T getData() {
return this.dataProperty().get();
}
public final void setData(final T data) {
this.dataProperty().set(data);
}
public final ReadOnlyObjectProperty<LoadingState> loadingStateProperty() {
return this.loadingState.getReadOnlyProperty();
}
public final LazyLoadingData.LoadingState getLoadingState() {
return this.loadingStateProperty().get();
}
}
The model here will just be an ObservableList<List<LazyLoadingData<String>>>, so each cell is a LazyLoadingData<String> and each row is a list of them.
To make this properly MVC, let's have a separate controller class which has a way of updating data in the model:
import java.util.List;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import javafx.concurrent.Task;
public class LazyLoadingDataController {
// data model:
private final List<List<LazyLoadingData<String>>> data ;
private final Random rng = new Random();
private final Executor exec = Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r);
t.setDaemon(true);
return t ;
});
public LazyLoadingDataController(List<List<LazyLoadingData<String>>> data) {
this.data = data ;
}
public void loadData(int column, int row) {
Task<String> loader = new Task<String>() {
#Override
protected String call() throws InterruptedException {
int value = rng.nextInt(1000);
Thread.sleep(3000);
return "Data: "+value;
}
};
data.get(row).get(column).startLoading();
loader.setOnSucceeded(e -> data.get(row).get(column).setData(loader.getValue()));
exec.execute(loader);
}
}
Now our cell implementation is pretty straightforward. The only tricky part is that each item has two properties, and we actually need to observe both of those properties and update the cell if either of them changes. We need to be careful to remove listener from items the cell is no longer displaying. So the cell looks like:
import java.util.List;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.value.ChangeListener;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.util.Duration;
public class LazyLoadingDataCell<T>
extends TableCell<List<LazyLoadingData<T>>, LazyLoadingData<T>>{
private final Button loadButton = new Button("Load");
private final Timeline loadingAnimation = new Timeline(
new KeyFrame(Duration.ZERO, e -> setText("Loading")),
new KeyFrame(Duration.millis(500), e -> setText("Loading.")),
new KeyFrame(Duration.millis(1000), e -> setText("Loading..")),
new KeyFrame(Duration.millis(1500), e -> setText("Loading..."))
);
public LazyLoadingDataCell(LazyLoadingDataController controller, int columnIndex) {
loadingAnimation.setCycleCount(Animation.INDEFINITE);
loadButton.setOnAction(e -> controller.loadData(columnIndex, getIndex()));
// listener for observing either the dataProperty()
// or the loadingStateProperty() of the current item:
ChangeListener<Object> listener = (obs, oldState, newState) -> doUpdate();
// when the item changes, remove and add the listener:
itemProperty().addListener((obs, oldItem, newItem) -> {
if (oldItem != null) {
oldItem.dataProperty().removeListener(listener);
oldItem.loadingStateProperty().removeListener(listener);
}
if (newItem != null) {
newItem.dataProperty().addListener(listener);
newItem.loadingStateProperty().addListener(listener);
}
doUpdate();
});
}
#Override
protected void updateItem(LazyLoadingData<T> item, boolean empty) {
super.updateItem(item, empty);
doUpdate();
}
private void doUpdate() {
if (isEmpty() || getItem() == null) {
setText(null);
setGraphic(null);
} else {
LazyLoadingData.LoadingState state = getItem().getLoadingState();
if (state == LazyLoadingData.LoadingState.NOT_LOADED) {
loadingAnimation.stop();
setText(null);
setGraphic(loadButton);
} else if (state == LazyLoadingData.LoadingState.LOADING) {
setGraphic(null);
loadingAnimation.play();
} else if (state == LazyLoadingData.LoadingState.LOADED) {
loadingAnimation.stop();
setGraphic(null);
setText(getItem().getData().toString());
}
}
}
}
Note how
The cell contains no state. The fields in the cell are entirely related to the display of data (a button and an animation).
The action of the button doesn't (directly) change anything in the view. It simply tells the controller to update the data in the model. Because the cell (view) is observing the model, when the model changes, the view updates.
The model also changes independently of user action, when the task in the controller completes. Because the view is observing the model for changes, it updates automatically.
Finally an example using this. There is not much unexpected here, we just create a model (ObservableList of List<LazyLoadingData<String>>), create a controller, and then a table with some columns.
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class LazyLoadingTableExample extends Application {
private final int numCols = 3 ;
private final int numRows = 100 ;
#Override
public void start(Stage primaryStage) {
TableView<List<LazyLoadingData<String>>> table = new TableView<>();
// data model:
ObservableList<List<LazyLoadingData<String>>> data
= FXCollections.observableArrayList();
table.setItems(data);
LazyLoadingDataController controller = new LazyLoadingDataController(data);
// build data:
for (int i = 0; i < numRows; i++) {
ObservableList<LazyLoadingData<String>> row
= FXCollections.observableArrayList();
for (int j = 0 ; j < numCols - 1 ; j++) {
row.add(new LazyLoadingData<>("Cell ["+j+", "+i+"]"));
}
row.add(new LazyLoadingData<>());
data.add(row);
}
for (int i = 0 ; i < numCols ; i++) {
table.getColumns().add(createColumn(controller, i));
}
Scene scene = new Scene(table, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private TableColumn<List<LazyLoadingData<String>>,LazyLoadingData<String>>
createColumn(LazyLoadingDataController controller, int columnIndex) {
TableColumn<List<LazyLoadingData<String>>,LazyLoadingData<String>> col
= new TableColumn<>("Column "+columnIndex);
col.setCellValueFactory(cellData ->
new SimpleObjectProperty<>(cellData.getValue().get(columnIndex)));
col.setCellFactory(tc ->
new LazyLoadingDataCell<>(controller, columnIndex));
return col ;
}
public static void main(String[] args) {
launch(args);
}
}

javafx TableView with dynamic ContextMenu on rows

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
}

JavaFX: How to highlight certain Items in a TreeView

I am trying to implement a search function for a TreeView in JavaFX. I want to highlight all the matches when the user hits the enter key. So I added a boolean isHighlighted to my TreeItem and in my TreeCells updateItem, I check whether the item isHighlighted and if so I apply a certain CSS. Everything works fine with the items/cells not visible at the moment of the search -- when I scroll to them, they are properly highlighted. The problem is: How can I "repaint" the TreeCells visible at search so that they reflect whether their item isHighlighted? My Controller does currently not have any reference to the TreeCells the TreeView creates.
This answer is based on this one, but adapted for TreeView instead of TableView, and updated to use JavaFX 8 functionality (greatly reducing the amount of code required).
One strategy for this is to maintain an ObservableSet of TreeItems that match the search (this is sometimes useful for other functionality you may want anyway). Use a CSS PseudoClass and an external CSS file to highlight the required cells. You can create a BooleanBinding in the cell factory that binds to the cell's treeItemProperty and the ObservableSet, evaluating to true if the set contains the cell's current tree item. Then just register a listener with the binding and update the pseudoclass state of the cell when it changes.
Here's a SSCCE. It contains a tree whose items are Integer-valued. It will update the search when you type in the search box, matching those whose value is a multiple of the value entered.
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TreeWithSearchAndHighlight extends Application {
#Override
public void start(Stage primaryStage) {
TreeView<Integer> tree = new TreeView<>(createRandomTree(100));
// keep track of items that match our search:
ObservableSet<TreeItem<Integer>> searchMatches = FXCollections.observableSet(new HashSet<>());
// cell factory returns an instance of TreeCell implementation defined below.
// pass the cell implementation a reference to the set of search matches
tree.setCellFactory(tv -> new SearchHighlightingTreeCell(searchMatches));
// search text field:
TextField textField = new TextField();
// allow only numeric input:
textField.setTextFormatter(new TextFormatter<Integer>(change ->
change.getControlNewText().matches("\\d*")
? change
: null));
// when the text changes, update the search matches:
textField.textProperty().addListener((obs, oldText, newText) -> {
// clear search:
searchMatches.clear();
// if no text, or 0, just exit:
if (newText.isEmpty()) {
return ;
}
int searchValue = Integer.parseInt(newText);
if (searchValue == 0) {
return ;
}
// search for matching nodes and put them in searchMatches:
Set<TreeItem<Integer>> matches = new HashSet<>();
searchMatchingItems(tree.getRoot(), matches, searchValue);
searchMatches.addAll(matches);
});
BorderPane root = new BorderPane(tree, textField, null, null, null);
BorderPane.setMargin(textField, new Insets(5));
BorderPane.setMargin(tree, new Insets(5));
Scene scene = new Scene(root, 600, 600);
// stylesheet sets style for cells matching search by using the selector
// .tree-cell:search-match
// (specified in the initalization of the Pseudoclass at the top of the code)
scene.getStylesheets().add("tree-highlight-search.css");
primaryStage.setScene(scene);
primaryStage.show();
}
// find all tree items whose value is a multiple of the search value:
private void searchMatchingItems(TreeItem<Integer> searchNode, Set<TreeItem<Integer>> matches, int searchValue) {
if (searchNode.getValue() % searchValue == 0) {
matches.add(searchNode);
}
for (TreeItem<Integer> child : searchNode.getChildren()) {
searchMatchingItems(child, matches, searchValue);
}
}
// build a random tree with numNodes nodes (all nodes expanded):
private TreeItem<Integer> createRandomTree(int numNodes) {
List<TreeItem<Integer>> items = new ArrayList<>();
TreeItem<Integer> root = new TreeItem<>(1);
root.setExpanded(true);
items.add(root);
Random rng = new Random();
for (int i = 2 ; i <= numNodes ; i++) {
TreeItem<Integer> item = new TreeItem<>(i);
item.setExpanded(true);
TreeItem<Integer> parent = items.get(rng.nextInt(items.size()));
parent.getChildren().add(item);
items.add(item);
}
return root ;
}
public static class SearchHighlightingTreeCell extends TreeCell<Integer> {
// must keep reference to binding to prevent premature garbage collection:
private BooleanBinding matchesSearch ;
public SearchHighlightingTreeCell(ObservableSet<TreeItem<Integer>> searchMatches) {
// pseudoclass for highlighting state
// css can set style with selector
// .tree-cell:search-match { ... }
PseudoClass searchMatch = PseudoClass.getPseudoClass("search-match");
// initialize binding. Evaluates to true if searchMatches
// contains the current treeItem
// note the binding observes both the treeItemProperty and searchMatches,
// so it updates if either one changes:
matchesSearch = Bindings.createBooleanBinding(() ->
searchMatches.contains(getTreeItem()),
treeItemProperty(), searchMatches);
// update the pseudoclass state if the binding value changes:
matchesSearch.addListener((obs, didMatchSearch, nowMatchesSearch) ->
pseudoClassStateChanged(searchMatch, nowMatchesSearch));
}
// update the text when the item displayed changes:
#Override
protected void updateItem(Integer item, boolean empty) {
super.updateItem(item, empty);
setText(empty ? null : "Item "+item);
}
}
public static void main(String[] args) {
launch(args);
}
}
The CSS file tree-highlight-search.css just has to contain a style for the highlighted cells:
.tree-cell:search-match {
-fx-background: yellow ;
}

JavaFX ChoiceBox add separator with type safety

I'm looking to add a separator into a choice box and still retain the type safety.
On all of the examples I've seen, they just do the following:
ChoiceBox<Object> cb = new ChoiceBox<>();
cb.getItems().addAll("one", "two", new Separator(), "fadfadfasd", "afdafdsfas");
Has anyone come up with a solution to be able to add separators and still retain type safety?
I would expect that if I wanted to add separators, I should be able do something along the following:
ChoiceBox<T> cb = new ChoiceBox<T>();
cb.getSeparators().add(1, new Separator()); // 1 is the index of where the separator should be
I shouldn't have to sacrifice type safety just to add separators.
As already noted, are Separators only supported if added to the items (dirty, dirty). To support them along the lines expected in the question, we need to:
add the notion of list of separator to choiceBox
make its skin aware of that list
While the former is not a big deal, the latter requires a complete re-write (mostly c&p) of its skin, as everything is tightly hidden in privacy. If the re-write has happened anyway, then it's just a couple of lines more :-)
Just for fun, I'm experimenting with ChoiceBoxX that solves some nasty bugs in its selection handling, so couldn't resist to try.
First, add support to the ChoiceBoxx itself:
/**
* Adds a separator index to the list. The separator is inserted
* after the item with the same index. Client code
* must keep this list in sync with the data.
*
* #param separator
*/
public final void addSeparator(int separator) {
if (separatorsList.getValue() == null) {
separatorsList.setValue(FXCollections.observableArrayList());
}
separatorsList.getValue().add(separator);
};
Then some changes in ChoiceBoxXSkin
must listen to the separatorsList
must expect index-of-menuItem != index-of-choiceItem
menuItem must keep its index-of-choiceItem
At its simplest, the listener re-builds the popup, the menuItem stores the dataIndex in its properties and all code that needs to access a popup by its dataIndex is delegated to a method that loops through the menuItems until it finds one that fits:
protected RadioMenuItem getMenuItemFor(int dataIndex) {
if (dataIndex < 0) return null;
int loopIndex = dataIndex;
while (loopIndex < popup.getItems().size()) {
MenuItem item = popup.getItems().get(loopIndex);
ObservableMap<Object, Object> properties = item.getProperties();
Object object = properties.get("data-index");
if ((object instanceof Integer) && dataIndex == (Integer) object) {
return item instanceof RadioMenuItem ? (RadioMenuItem)item : null;
}
loopIndex++;
}
return null;
}
Well you can work around it by creating an interface and then subclassing Separator to implement this interface:
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Separator;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class ChoiceBoxIsSafe extends Application {
interface FruitInterface { }
static public class Fruit implements FruitInterface {
private StringProperty name = new SimpleStringProperty();
Fruit(String name) {
this.name.set(name);
}
public StringProperty nameProperty() {
return name;
}
#Override
public String toString() {
return name.get();
}
}
static public class FruitySeparator extends Separator implements FruitInterface { }
#Override
public void start(Stage primaryStage) throws Exception {
GridPane grid = new GridPane();
grid.setHgap(10); grid.setVgap(10); grid.setPadding(new Insets(10));
ChoiceBox<FruitInterface> cb = new ChoiceBox<>();
cb.getItems().addAll(new Fruit("Apple"), new Fruit("Orange"), new FruitySeparator(), new Fruit("Peach"));
Text text = new Text("");
ReadOnlyObjectProperty<FruitInterface> selected = cb.getSelectionModel().selectedItemProperty();
text.textProperty().bind(Bindings.select(selected, "name"));
grid.add(cb, 0, 0);
grid.add(text, 1, 0);
Scene scene = new Scene(grid);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
but that is hardly an "elegant" solution and cannot be done in all cases (e.g. ChoiceBox<String>).
From the implementation of ChoiceBox it certainly looks like it wasn't a good idea to treat Separators like items in the ChoiceBox :-(.
FOR THE REST OF US:
There is a MUCH easier way to do this using code (there are easy ways to do it using FXML too, doing it in code offers more flexibility).
You simply create an ObservableList, then populate it with your items, including the separator then assign that list to the ChoiceBox like this:
private void fillChoiceBox(ChoiceBox choiceBox) {
ObservableList items = FXCollections.observableArrayList();
items.add("one");
items.add("two");
items.add("three");
items.add(new Separator());
items.add("Apples");
items.add("Oranges");
items.add("Pears");
choiceBox.getItems().clear();
choiceBox.getItems().addAll(items);
}

Resources