Currently JavaFX provides a feature to dropdown the comboBox on hitting F4. We want to disable that feature and process other functions for F4. On first instance I thought this is pretty straight forward. My idea is that I will add a key event filter and consume it when F4 is pressed.
But unfortunately that didnt worked !! Upon investigation, I noticed that there is a part of code in ComboBoxPopupControl to handle the key event, which is set as KeyEvent.ANY filter. The strange part is that they consume the event after showing/hiding.
The part of code is as below:
private void handleKeyEvent(KeyEvent ke, boolean doConsume) {
// When the user hits the enter or F4 keys, we respond before
// ever giving the event to the TextField.
if (ke.getCode() == KeyCode.ENTER) {
setTextFromTextFieldIntoComboBoxValue();
if (doConsume && comboBoxBase.getOnAction() != null) {
ke.consume();
} else {
forwardToParent(ke);
}
} else if (ke.getCode() == KeyCode.F4) {
if (ke.getEventType() == KeyEvent.KEY_RELEASED) {
if (comboBoxBase.isShowing()) comboBoxBase.hide();
else comboBoxBase.show();
}
ke.consume(); // we always do a consume here (otherwise unit tests fail)
}
}
This makes me totally helpless, as now I can no more control this part of event chain by merely consuming the filters/handlers. None of the below filters helped me to stop showing the dropdown.
comboBox.addEventFilter(KeyEvent.ANY, e -> {
if (e.getCode() == KeyCode.F4) {
e.consume(); // Didn't stopped showing the drop down
}
});
comboBox.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.F4) {
e.consume(); // Didn't stopped showing the drop down
}
});
comboBox.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
if (e.getCode() == KeyCode.F4) {
e.consume(); // Didn't stopped showing the drop down
}
});
The only way I can stop it is to consume the event on its parent and not allowing to delegate to ComboBox. But this is definetly an overhead, with already tens of ComboBox(es) across the application and many more to come.
My question is:
Why did they implemented a feature which is tightly integrated not allowing the user to disable it?
Is there any alternate that I can implement on ComboBox level to stop showing/hiding the dropdown when F4 is pressed.
I tried the below approach to make it work. But I am not sure how much i can rely on Timeline based solution :(
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ComboBoxF4_Demo extends Application {
Timeline f4PressedTimeline = new Timeline(new KeyFrame(Duration.millis(100), e1 -> {
}));
#Override
public void start(Stage stage) throws Exception {
HBox root = new HBox();
root.setSpacing(15);
root.setPadding(new Insets(25));
root.setAlignment(Pos.CENTER);
Scene scene = new Scene(root, 600, 600);
stage.setScene(scene);
final ComboBox<String> comboBox = new ComboBox<String>() {
#Override
public void show() {
if (f4PressedTimeline.getStatus() != Animation.Status.RUNNING) {
super.show();
}
}
};
comboBox.setItems(FXCollections.observableArrayList("One", "Two", "Three"));
comboBox.addEventFilter(KeyEvent.ANY, e -> {
if (e.getCode() == KeyCode.F4) {
if (e.getEventType() == KeyEvent.KEY_RELEASED) {
f4PressedTimeline.playFromStart();
}
}
});
// NONE OF THE BELOW FILTERS WORKED :(
/*comboBox.addEventFilter(KeyEvent.ANY, e -> {
if (e.getCode() == KeyCode.F4) {
e.consume(); // Didn't stopped showing the drop down
}
});
comboBox.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.F4) {
e.consume(); // Didn't stopped showing the drop down
}
});
comboBox.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
if (e.getCode() == KeyCode.F4) {
e.consume(); // Didn't stopped showing the drop down
}
});
*/
root.getChildren().addAll(comboBox);
stage.show();
}
}
As mentioned by #kleopatra in the question comments, consuming an event does not stop its propagation within the same "level" of the same phase. In other words, all the event filters registered with the ComboBox (for the EventType and its supertypes) will still be notified even if one of them consumes the event. Then there's also the problem of changing the default behavior of controls which may be unexpected, and unappreciated, by your end users.
If you still want to change the control's behavior, and you don't find consuming the event on an ancestor satisfactory, you can intercept the event in a custom EventDispatcher instead of an event filter:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage primaryStage) {
var comboBox = new ComboBox<String>();
for (int i = 0; i < 20; i++) {
comboBox.getItems().add("Item #" + i);
}
comboBox.getSelectionModel().select(0);
var oldDispatcher = comboBox.getEventDispatcher();
comboBox.setEventDispatcher((event, tail) -> {
if (event.getEventType() == KeyEvent.KEY_RELEASED
&& ((KeyEvent) event).getCode() == KeyCode.F4) {
return null; // returning null indicates the event was consumed
}
return oldDispatcher.dispatchEvent(event, tail);
});
primaryStage.setScene(new Scene(new StackPane(comboBox), 500, 300));
primaryStage.show();
}
}
Related
I'm trying to come up with a solution to allow multiple Pane nodes handle mouse events independently when assembled into a StackPane
StackPane
Pane 1
Pane 2
Pane 3
I'd like to be able to handle mouse events in each child, and the first child calling consume() stops the event going to the next child.
I'm also aware of setPickOnBounds(false), but this does not solve all cases as some of the overlays will be pixel based with Canvas, i.e. not involving the scene graph.
I've tried various experiments with Node.fireEvent(). However these always lead to recursion ending in stack overflow. This is because the event is propagated from the root scene and triggers the same handler again.
What I'm looking for is some method to trigger the event handlers on the child panes individually without the event travelling through its normal path.
My best workaround so far is to capture the event with a filter and manually invoke the handler. I'd need to repeat this for MouseMoved etc
parent.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
for (Node each : parent.getChildren()) {
if (!event.isConsumed()) {
each.getOnMouseClicked().handle(event);
}
}
event.consume();
});
However this only triggers listeners added with setOnMouseClicked, not addEventHandler, and only on that node, not child nodes.
Another sort of solution is just to accept JavaFX doesn't work like this, and restructure the panes like this, this will allow normal event propagation to take place.
Pane 1
Pane 2
Pane 3
Example
import javafx.application.Application;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class EventsInStackPane extends Application {
public static void main(String[] args) {
launch(args);
}
private static class DebugPane extends Pane {
public DebugPane(Color color, String name) {
setBackground(new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY)));
setOnMouseClicked(event -> {
System.out.println("setOnMouseClicked " + name + " " + event);
});
addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
System.out.println("addEventHandler " + name + " " + event);
});
addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
System.out.println("addEventFilter " + name + " " + event);
});
}
}
#Override
public void start(Stage primaryStage) throws Exception {
DebugPane red = new DebugPane(Color.RED, "red");
DebugPane green = new DebugPane(Color.GREEN, "green");
DebugPane blue = new DebugPane(Color.BLUE, "blue");
setBounds(red, 0, 0, 400, 400);
setBounds(green, 25, 25, 350, 350);
setBounds(blue, 50, 50, 300, 300);
StackPane parent = new StackPane(red, green, blue);
eventHandling(parent);
primaryStage.setScene(new Scene(parent));
primaryStage.show();
}
private void eventHandling(StackPane parent) {
parent.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (!event.isConsumed()) {
for (Node each : parent.getChildren()) {
Event copy = event.copyFor(event.getSource(), each);
parent.fireEvent(copy);
if (copy.isConsumed()) {
break;
}
}
}
event.consume();
});
}
private void setBounds(DebugPane panel, int x, int y, int width, int height) {
panel.setLayoutX(x);
panel.setLayoutY(y);
panel.setPrefWidth(width);
panel.setPrefHeight(height);
}
}
Using the hint from #jewelsea I was able to use a custom chain. I've done this from a "catcher" Pane which is added to the front of the StackPane. This then builds a chain using all the children, in reverse order, excluding itself.
private void eventHandling(StackPane parent) {
Pane catcher = new Pane() {
#Override
public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
EventDispatchChain chain = super.buildEventDispatchChain(tail);
for (int i = parent.getChildren().size() - 1; i >= 0; i--) {
Node child = parent.getChildren().get(i);
if (child != this) {
chain = chain.prepend(child.getEventDispatcher());
}
}
return chain;
}
};
parent.getChildren().add(catcher);
}
I am encountering an issue with the "ENTER" key event propagation in ComboBox.
I have a form(VBox) which submits or perform validation when it receives "ENTER" key released event. The user will enter the details by tabbing between fields and hitting UP/DOWN keys for selection in combo box (Just to minimize the mouse interaction). So far everything works fine.
When the user tabs to ComboBox and press "DOWN" key, the ComboBox popup is shown. And user can select the options using UP/DOWN keys. After choosing an option, user will press the "ENTER" key to close the popup. Here I want the "ENTER" event to only close the popup but not to submit the form. If the popup not opened, then "ENTER" key on ComboBox should submit the form.
I tried to fix this by consuming the events on ComboBox. But that didn't work. So two questions::
Can anyone let me know "how to prevent the 'ENTER' key event propagation to VBox, only when the ComboBox is showing"?
And also can anyone let me know how come the event is propagated to "VBox" handler, in spite of consuming event on ComboBox filter/handler?
Below is the example show casing the issue:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ComboBoxEnterKeyIssue extends Application {
private DateFormat df = new SimpleDateFormat("hh:mm:ss");
#Override
public void start(Stage primaryStage) throws Exception {
TextField textField1 = new TextField();
textField1.setMaxWidth(150);
ComboBox<String> comboBox = new ComboBox<>();
comboBox.setPrefWidth(150);
comboBox.getItems().addAll("Item 1", "Item 2", "Item 3", "Item 4", "Item 5");
comboBox.addEventFilter(KeyEvent.KEY_RELEASED, ke -> {
if (ke.getCode() == KeyCode.DOWN && !comboBox.isShowing()) {
comboBox.show();
ke.consume();
} else if (ke.getCode() == KeyCode.ENTER) {
ke.consume(); // Has no effect !!
}
});
comboBox.addEventHandler(KeyEvent.KEY_RELEASED, ke -> {
if (ke.getCode() == KeyCode.ENTER) {
ke.consume(); // Has no effect !!
}
});
TextField textField2 = new TextField();
textField2.setMaxWidth(150);
ListView<String> output = new ListView<>();
VBox.setVgrow(output, Priority.ALWAYS);
VBox pane = new VBox(textField1, comboBox, textField2, output);
pane.setPadding(new Insets(10));
pane.setSpacing(10);
pane.setOnKeyReleased(ke -> {
if (ke.getCode() == KeyCode.ENTER) {
output.getItems().add(df.format(new Date()) + " :: Enter key released on pane...");
// Form submission/validation is performed here !!
}
});
Scene scene = new Scene(pane, 500, 400);
primaryStage.setScene(scene);
primaryStage.setTitle("ComboBox enter key issue");
primaryStage.show();
}
}
So I came with this nasty solution to differentiate the enter event.
The fix is as below:
Created a custom combo skin to indentify the listView and set a flag when an "enter" key is pressed on listview.
In the Custom combo event dispatcher,check the flag if the enter is pressed while the popup is opened and then consume the event in dispatcher.
The question is still open for any better ideas.
import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
import javafx.application.Application;
import javafx.event.EventDispatcher;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListView;
import javafx.scene.control.Skin;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ComboBoxEnterKeyIssue extends Application {
private DateFormat df = new SimpleDateFormat("hh:mm:ss");
#Override
public void start(Stage primaryStage) throws Exception {
TextField textField1 = new TextField();
textField1.setMaxWidth(150);
MyComboBox<String> comboBox = new MyComboBox<>();
comboBox.setPrefWidth(150);
comboBox.getItems().addAll("Item 1", "Item 2", "Item 3", "Item 4", "Item 5");
comboBox.addEventFilter(KeyEvent.KEY_RELEASED, ke -> {
if (ke.getCode() == KeyCode.DOWN && !comboBox.isShowing()) {
comboBox.show();
ke.consume();
} else if (ke.getCode() == KeyCode.ENTER) {
ke.consume(); // Has no effect !!
}
});
comboBox.addEventHandler(KeyEvent.KEY_RELEASED, ke -> {
if (ke.getCode() == KeyCode.ENTER) {
ke.consume(); // Has no effect !!
}
});
TextField textField2 = new TextField();
textField2.setMaxWidth(150);
ListView<String> output = new ListView<>();
VBox.setVgrow(output, Priority.ALWAYS);
VBox pane = new VBox(textField1, comboBox, textField2, output);
pane.setPadding(new Insets(10));
pane.setSpacing(10);
pane.setOnKeyReleased(ke -> {
if (ke.getCode() == KeyCode.ENTER) {
output.getItems().add(df.format(new Date()) + " :: Enter key released on pane...");
// Form submission/validation is performed here !!
}
});
Scene scene = new Scene(pane, 500, 400);
primaryStage.setScene(scene);
primaryStage.setTitle("ComboBox enter key issue");
primaryStage.show();
}
class MyComboBox<T> extends ComboBox<T> {
boolean hiddenByEnter = false;
public MyComboBox() {
init();
}
public void setHiddenByEnter(boolean hiddenByEnter) {
this.hiddenByEnter = hiddenByEnter;
}
private void init() {
final EventDispatcher initial = getEventDispatcher();
setEventDispatcher((event, tail) -> {
// Consuming the event only if the popup is hidden by 'enter' key event.
if (event instanceof KeyEvent && ((KeyEvent) event).getCode() == KeyCode.ENTER && hiddenByEnter) {
hiddenByEnter = false;
return null;
}
return initial.dispatchEvent(event, tail);
});
}
#Override
protected Skin<?> createDefaultSkin() {
return new MyComboBoxSkin<>(this);
}
}
class MyComboBoxSkin<T> extends ComboBoxListViewSkin<T> {
private ListView<T> listView;
public MyComboBoxSkin(MyComboBox<T> comboBox) {
super(comboBox);
/* Identifying the "ENTER" event on listView in popup, to differentiate the events. */
getPopup().showingProperty().addListener((obs, old, showing) -> {
if (showing) {
comboBox.setHiddenByEnter(false);
if (listView == null) {
listView = (ListView) getPopup().getScene().getRoot().lookup(".list-view");
listView.addEventFilter(KeyEvent.KEY_PRESSED, ke -> {
if (ke.getCode() == KeyCode.ENTER) {
comboBox.setHiddenByEnter(true);
}
});
}
}
});
}
}
}
I have a Button that I can move it from the screen, when clicking it has an action. The problem is, when I do Drag'n Drop the click event is called when I release the mouse on, I tried it:
setOnMouseClicked
setOnAction
setOnMousePressed
How can I do to just call the click function when it is a quick click, something like Android times that can differentiate because we have setOnLongClick, so differentiated when I have doing Drag'n Drop and when I really want to click?
Ex:
To move, do:
button.setOnMouseDragged(e -> {
//code move
});
To eventClick:
button.setOnMouseClicked/ Action / MousePressed (e -> {
//call method
});
But when I drop it, it calls setOnMouseClicked / Action / MousePressed, what I want is for it to just call in case I give a quick click, when I drop the drag'n drop do not call.
One option is to keep track of whether or not the Button was dragged; if not, only then execute the code in the onAction handler. Here's an example:
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
public class Main extends Application {
private Point2D origin;
private boolean wasDragged;
#Override
public void start(Stage primaryStage) {
Button button = new Button("Drag me!");
button.setOnAction(this::onAction);
button.setOnMousePressed(this::onMousePressed);
button.setOnMouseDragged(this::onMouseDragged);
button.setOnMouseReleased(this::onMouseReleased);
primaryStage.setScene(new Scene(new Group(button), 800, 600));
primaryStage.show();
}
private void onAction(ActionEvent event) {
event.consume();
if (!wasDragged) {
System.out.println("onAction");
}
}
private void onMousePressed(MouseEvent event) {
event.consume();
origin = new Point2D(event.getX(), event.getY());
System.out.println("onMousePressed");
}
private void onMouseDragged(MouseEvent event) {
event.consume();
wasDragged = true;
Button source = (Button) event.getSource();
source.setTranslateX(source.getTranslateX() + event.getX() - origin.getX());
source.setTranslateY(source.getTranslateY() + event.getY() - origin.getY());
}
private void onMouseReleased(MouseEvent event) {
event.consume();
origin = null;
wasDragged = false;
System.out.println("onMouseReleased");
System.out.println();
}
}
Unfortunately, I can't find documentation guaranteeing the onAction handler is always called before the onMouseReleased handler, but this worked on both Java 8u202 and JavaFX 11.0.2 when I tried it.
I'm making a combobox that is filtered by typed text, and shows the drop down whenever text is typed.
I found this example which works very well. I have modified it slightly so the dropdown appears when text is entered.
However, when I type a few letters and then press ctrl + A to select all text in the TextField, it does not select all of the text if the dropdown is visible. Something else is consuming that hotkey.
Here is the MCVE code:
import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class MCVE extends Application {
public void start(Stage stage) {
HBox root = new HBox();
ComboBox<String> cb = new ComboBox<String>();
cb.setEditable(true);
ObservableList<String> items = FXCollections.observableArrayList("One", "Two", "Three", "Four", "Five", "Six",
"Seven", "Eight", "Nine", "Ten");
FilteredList<String> filteredItems = new FilteredList<String>(items, p -> true);
cb.getEditor().textProperty().addListener((obs, oldValue, newValue) -> {
final TextField editor = cb.getEditor();
final String selected = cb.getSelectionModel().getSelectedItem();
Platform.runLater(() -> {
if ( !editor.getText().isEmpty() ) {
cb.show();
} else {
cb.hide();
}
if (selected == null || !selected.equals(editor.getText())) {
filteredItems.setPredicate(item -> {
if (item.toUpperCase().startsWith(newValue.toUpperCase())) {
return true;
} else {
return false;
}
});
}
});
});
cb.setItems(filteredItems);
root.getChildren().add(cb);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Here are a few solutions I've tried. None of them work. It seems that the issue is that JavaFX has reserved the hotkey Ctrl + A and will not let me grab onto it.
(this one works if the key is D, but not A
((ComboBoxListViewSkin)cb.getSkin()).getDisplayNode().addEventFilter( KeyEvent.KEY_PRESSED, keyEvent -> {
if ( keyEvent.isControlDown() && keyEvent.getCode() == KeyCode.A ) {
cb.getEditor().selectAll();
}
});
(this one also works if the key is D, but not A
cb.setOnKeyPressed( ( KeyEvent e ) -> {
if ( e.isControlDown() && e.getCode() == KeyCode.D ) {
cb.getEditor().selectAll();
}
});
The behaviour is the same for a plain combo (that is without any filtering) and looks like a bug: ctrl-A is eaten by the ListView in the dropDown. To work around, you can install the eventFilter on the list, f.i. in a onShown handler - at this time the skin is installed:
cb.setOnShown(e -> {
ComboBoxListViewSkin<?> skin = (ComboBoxListViewSkin<?>) cb.getSkin();
ListView<?> list = (ListView<?>) skin.getPopupContent();
list.addEventFilter( KeyEvent.KEY_PRESSED, keyEvent -> {
if (keyEvent.isControlDown() && keyEvent.getCode() == KeyCode.A ) {
cb.getEditor().selectAll();
}
});
cb.setOnShown(null);
});
This is working in all versions (8 and 9+). For 9+ the bug is worse in that all navigation inside the editor is disabled (aka: list eating left/right as well).
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);
}
}
}
});