Issue
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);
}
}
Answered By - kleopatra
Answer Checked By - Pedro (JavaFixing Volunteer)