1

I do not want to disable the ComboBox because I want the user to be able to select the ComboBox button and look through the ComboBox items. And if the user tries selecting an item in the ComboBox, a window should pop up saying the user is in read only mode and the ComboBox should still have the original item in the ComboBox button cell.

Is there a way to do this?

By the way, I saw a previous post that asks this same question but using CheckComboBox from ControlsFX. But since I'm using a normal ComboBox from JavaFX 8, the solution from that post does not apply to a standard ComboBox.

Here's a minimal reproducible code example:

public class Main extends Application {
    Stage window;
    Scene scene;
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        window = primaryStage;
        window.setTitle("Read Only ComboBox");

        ObservableList<String> strings = FXCollections.observableArrayList();
        for (int i = 0; i <= 10; i++)
            strings.add("Item " + i);

        // Create the ComboBox with the data
        ComboBox<String> comboBox = new ComboBox<>(strings);
        comboBox.getSelectionModel().select(3);

        // Set comboBox to read only

        HBox layout = new HBox(10);
        layout.setPadding(new Insets(20, 20, 20,20));
        layout.getChildren().addAll(comboBox);

        scene = new Scene(layout, 300, 250);
        window.setScene(scene);
        window.show();
    }
}

And I'm trying to get the ComboBox to look like this when a user selects the ComboBox button cell. And then once they select any item, a window should pop up saying they are in Read Only mode and the ComboBox should still have "Item 3" selected.

Edit 1: Here is the full stacktrace using Abra's code. I did not modify any of the code.

Exception in thread "JavaFX Application Thread" java.lang.IndexOutOfBoundsException
at com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList.subList(ReadOnlyUnbackedObservableList.java:136)
at javafx.collections.ListChangeListener$Change.getAddedSubList(ListChangeListener.java:242)
at com.sun.javafx.scene.control.behavior.ListViewBehavior.lambda$new$59(ListViewBehavior.java:269)
at javafx.collections.WeakListChangeListener.onChanged(WeakListChangeListener.java:88)
at com.sun.javafx.collections.ListListenerHelper$Generic.fireValueChangedEvent(ListListenerHelper.java:329)
at com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(ListListenerHelper.java:73)
at com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList.callObservers(ReadOnlyUnbackedObservableList.java:75)
at javafx.scene.control.MultipleSelectionModelBase.clearAndSelect(MultipleSelectionModelBase.java:378)
at javafx.scene.control.ListView$ListViewBitSetSelectionModel.clearAndSelect(ListView.java:1403)
at com.sun.javafx.scene.control.behavior.CellBehaviorBase.simpleSelect(CellBehaviorBase.java:256)
at com.sun.javafx.scene.control.behavior.CellBehaviorBase.doSelect(CellBehaviorBase.java:220)
at com.sun.javafx.scene.control.behavior.CellBehaviorBase.mousePressed(CellBehaviorBase.java:150)
at com.sun.javafx.scene.control.skin.BehaviorSkinBase$1.handle(BehaviorSkinBase.java:95)
at com.sun.javafx.scene.control.skin.BehaviorSkinBase$1.handle(BehaviorSkinBase.java:89)
at com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
at com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
at javafx.event.Event.fireEvent(Event.java:198)
at javafx.scene.Scene$MouseHandler.process(Scene.java:3757)
at javafx.scene.Scene$MouseHandler.access$1500(Scene.java:3485)
at javafx.scene.Scene.impl_processMouseEvent(Scene.java:1762)
at javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2494)
at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:394)
at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:295)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$358(GlassViewEventHandler.java:432)
at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:431)
at com.sun.glass.ui.View.handleMouseEvent(View.java:555)
at com.sun.glass.ui.View.notifyMouse(View.java:937)
at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at com.sun.glass.ui.win.WinApplication.lambda$null$152(WinApplication.java:177)
at java.lang.Thread.run(Thread.java:748)

Edit 2: Both sorifiend's and Abra's code work as intended but not on JDK 8 that includes JavaFX. I used Zulu JDK 17 w/ JavaFX and both of their code worked using that JDK. I'm still looking for a solution since the project I'm working on is set on using Java 8 SE for a desktop application.

Abra
  • 19,142
  • 7
  • 29
  • 41
javaman
  • 13
  • 5
  • 1
    what a terrible UX :( As a general rule, __never-ever__ let the users assume they can interact with a control and tell them _after_ they did that they were not allowed to! Instead think of a way to relate the reduced interaction capabibilties up front. – kleopatra Sep 07 '22 at 09:20
  • 1
    Subclass `SingleSelectionModel`, create a `ReadOnlySingleSelectionModel`. Upon switching to read-only mode, replace the default selection model with the read-only model. Restore the default selection model, when switching back to read-write mode. – jewelsea Sep 07 '22 at 11:40
  • 1
    @kleopatra I agree this read-only ComboBox UX is awful. I'm all for changing it to something else but I'm not sure if JavaFX 8 has other Controls that offer the intended behavior. – javaman Sep 07 '22 at 15:29
  • I tried my suggestion of switching the selection model. It did not work well, at least in my implementation (the updated selection model prevented the value from changing but the UI would still update to show the new value the user attempted to select). Perhaps it could be made to work, but could be tricky to get right and may perhaps break as the internal combo box implementation evolves, so perhaps not to adopt the selection model switch approach. – jewelsea Sep 07 '22 at 21:39
  • As others have said, this seems like a weird user experience. Maybe a `MenuButton` with a collection of disabled `MenuItem`s would be close enough to what you want? – James_D Sep 08 '22 at 10:42

3 Answers3

3

Beware: the UX is terrible - we must not fool our users into believing they can change anything and tell them they couldn't after they tried!

Also, we must not (I know it's in the java doc somewhere but never find it when I need it ;) change the state of a property in a listener to that property: most of the time we get away with doing it, but it might have nasty, hard to debug side-effects.

All that said (and the boss insists on implementing the wrongish UX :) - here's an alternative to the doing the wrong thingy in a listener. The basic idea is to bind the combo's value to a fixed value. Doing so will effectively disconnect it from the selection state - users can use keys to change the selection (if the popup is closed) or navigate in the drop-down list (if the popup is showing) without changing the value.

Below is an example that

  • has a property that holds the fixed value
  • has a property that toggles the readOnly state
  • un/binds the combo's value from/to the fixed value based on the toggle state
  • sync's the selection on un/bind and on showing the popup
  • note: while bound, listeners to the selection state will still receive notifications from user interaction (which at that time are not in-sync with combo's value) - application code must be aware of that fact

The code:

public class ReadonlyComboSelection extends Application {
    StringProperty fixedValue;
    BooleanProperty readonly;
    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Read Only ComboBox");

        ObservableList<String> strings = FXCollections.observableArrayList();
        for (int i = 0; i <= 10; i++)
            strings.add("Item " + i);

        // Create the ComboBox with the data
        ComboBox<String> comboBox = new ComboBox<>(strings);
        // initialize the fixed selection
        fixedValue = new SimpleStringProperty(strings.get(3));
        readonly = new SimpleBooleanProperty() {

            @Override
            protected void invalidated() {
                if (get()) {
                    comboBox.valueProperty().bind(fixedValue);
                } else {
                    comboBox.valueProperty().unbind();
                }
                comboBox.getSelectionModel().select(comboBox.getValue());
            }

        };
        readonly.set(true);

        // make sure the selection in the popup is showing the value
        comboBox.setOnShowing(e -> {
            if (comboBox.valueProperty().isBound()) {
                comboBox.getSelectionModel().select(comboBox.getValue());
                if (comboBox.getSkin() instanceof ComboBoxListViewSkin skin) {
                    ListView<String> list = (ListView<String>) skin.getPopupContent();
                    list.getSelectionModel().select(comboBox.getValue());
                }
            }
        });

        // just for fun: dynamically change the readonly state
        CheckBox check = new CheckBox("selection is readonly");
        check.selectedProperty().bindBidirectional(readonly);

        HBox layout = new HBox(10, comboBox, check);

        Scene scene = new Scene(layout, 300, 250);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
kleopatra
  • 51,061
  • 28
  • 99
  • 211
  • Thank you so much! It worked in JDK 8 with a minor change to the skin variable. I feel bad for even trying to get this functionality to work with a ComboBox.. but I'm not sure other JFX Controls have the behavior I want. Do you have any suggestions @kleopatra ? – javaman Sep 07 '22 at 17:59
  • Also I tried calling comboBox.setOnAction(e -> { Alert readOnlyAlert = new Alert(...); readOnlyAlert.showAndWait(); }); But the event never gets fired since the value property never gets changed to something else. Do you know if there's a method I could override to trigger an Alert object? – javaman Sep 07 '22 at 19:22
2

Here is a rough solution that will capture the event and revert it to the default selection and we can do whatever else we want like showing a message. If the application is not in read only mode, then we can process the action as normal:

//Value to track read only mode
Boolean readOnlyMode = true;

//Values to help revert the selection
int defaultSelection = 3;
//We use a flag so that the message is not displayed twice when we reset the selection
//You can remove then need for this by checking the item that was selected instead and only show the message if it was not te default item
boolean flagToggle = false;

//Add an event to revert the selection and show a message
comboBox.setOnAction((ActionEvent t) ->
{
    if(readOnlyMode){
        //Code here to show a message and revert the selection
        if(!flagToggle){
            //Replace this line with your pop up dialogue, etc
            System.out.println("The application is in Read Only mode");
            //flip the flag for displaying the message
            flagToggle = true;
        }
        //restore the flag
        else{
            flagToggle = false;
        }

        //reset the selection to default, if the selection is already back to the default then this will not trigger another selection event
        comboBox.getSelectionModel().select(defaultSelection);
    }
    else{
        System.out.println("The application is in Edit mode. Item "+ comboBox.getValue() + " selected.");
        //perform normal actions
        //call some method here?
    }
});
sorifiend
  • 5,927
  • 1
  • 28
  • 45
  • Thanks for your help! I'm getting an Index out of Bounds exception though – javaman Sep 07 '22 at 06:04
  • It sounds like you have an issue with referencing a different `comboBox` than the one that is being selected. The code itself works just fine, the issue will be in how/where you have placed the code, and if you have changed things from the code in your question. Or the default selection that is being reverted to is out of bounds. – sorifiend Sep 07 '22 at 06:06
  • I created only one comboBox just like in the minimal reproducible code example I posted. I think it has something to do with the setOnAction EventHandler calling comboBox.getSelectionModel().select(defaultSelection) more than once but I'm not sure – javaman Sep 07 '22 at 06:11
  • The code works in my IDE when used with the code you provided. Did you change the value of `defaultSelection`? Remember that the first item is at `0` not `1` What happens if you change `comboBox.getSelectionModel().select(defaultSelection)` to `comboBox.getSelectionModel().select(0)`? Did you change anything else? – sorifiend Sep 07 '22 at 06:12
  • I didn't change the defaultSelection variable. But I did have to change the flagToggle variable from boolean to final boolean[] since I got the error "Local variables referenced from a lambda expression must be final or effectively final" Here's a screenshot of what it looks like anytime I select any item besides the already selected item 3. https://imgur.com/a/lMSo73c – javaman Sep 07 '22 at 06:35
  • @javaman The variable change is not the cause of the out of bounds issue. There is something else going on here that is not caused by the code in my answer or the answer by Abra. Please post a new question and include the full stack trace error in the question as text, and indicate your java version and your javafx version. The versions that worked for me was Java Hotspot 15.0.1.9 and JavaFX 11.0.2 on Windows 10. – sorifiend Sep 07 '22 at 06:40
  • I was wondering about that since Abra is using JDK 17 and they said it worked for them. I'll post another question then. Thanks! – javaman Sep 07 '22 at 06:45
  • I download Zulu JDK 17 with JavaFX and everything worked! I guess something is up with JDK 8 with JavaFX and the ComboBox api maybe – javaman Sep 07 '22 at 07:09
1

Add a ChangeListener to the selection model of the ComboBox. Whenever the selection changes, the below code restores the original selection and displays a message. Note that I arbitrarily set the initial ComboBox selection to the first item in its list of values.

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ComboBox;
import javafx.scene.control.SingleSelectionModel;
import javafx.stage.Stage;

public class ApplicationMain extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        ComboBox<String> combo = new ComboBox<>(FXCollections.observableArrayList("First",
                                                                                  "Second",
                                                                                  "Third",
                                                                                  "Fourth",
                                                                                  "Last"));
        SingleSelectionModel<String> selectionModel = combo.getSelectionModel();
        selectionModel.select(0);
        selectionModel.selectedItemProperty().addListener(new ChangeListener<String>() {
            boolean flag = true;
            @Override
            public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
                if (flag) {
                    flag = false;
                    selectionModel.select(oldValue);
                    flag = true;
                    Alert alert = new Alert(AlertType.WARNING, "Read only mode.", ButtonType.CLOSE);
                    alert.setHeaderText(null);
                    alert.showAndWait();
                }
            }
        });
        Group root = new Group(combo);
        Scene scene = new Scene(root, 250.0D, 60.0D);
        stage.setTitle("Example");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
Abra
  • 19,142
  • 7
  • 29
  • 41
  • I appreciate your help. I'm getting an Index out of Bounds exception though – javaman Sep 07 '22 at 06:05
  • @javaman are you running the code as it appears in my answer, or have you changed it? – Abra Sep 07 '22 at 06:16
  • I'm running it as it appears. Nothing was changed. Here's a screenshot of what it looks like anytime I select any item besides the already selected item. https://imgur.com/a/DUh3Zl9 – javaman Sep 07 '22 at 06:28
  • 1
    @javaman I can't read the stack trace in the screen capture because it's too small. By the way I am using Zulu JDK 17 (which includes JavaFX), Windows 10 and Eclipse 2022-06. – Abra Sep 07 '22 at 06:35
  • Here's screenshot of the full stacktrace. https://imgur.com/a/B6DG8MP Maybe it's because I'm using JDK 8 / JavaFX 8 instead of JDK 17? I'm also using Windows 10 and IntelliJ IDEA 2022.2.1 – javaman Sep 07 '22 at 06:41
  • @javaman is that the entire stack trace? If not, then please provide the entire stack trace **as text** (and not as a screen capture). You can [edit] your question and post it there. – Abra Sep 07 '22 at 06:51
  • I added the full stackstrace to my Post – javaman Sep 07 '22 at 06:58
  • I download Zulu JDK 17 with JavaFX and it worked! I guess something is up with JDK 8 with JavaFX and the ComboBox api maybe – javaman Sep 07 '22 at 07:07
  • 1
    beware: don't change the state of the sender in a listener to that state! – kleopatra Sep 07 '22 at 09:21