Search code examples
javajavafxjavafx-8

Set ComboBox to read only without disabling the ComboBox using JavaFX


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.


Solution

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