javafxcomboboxeventfilter

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.


Solution

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