8

I want to include checks for my combobox to restrict "access" to some of the values. I could just remove those unaccessible items from the list, yes, but I'd like the user to see the other options, even if he can't select them (yet).

Problem: Selecting another value inside a changelistener causes an IndexOutOfBoundsException, and I have no idea why.

Here is a SSCCE. It creates a ComboBox with Integer values, and the first one is selected on default. Then I tried to keep it very easy: Every change of the value is considered as "wrong" and I change the selection back to the first element. But still, IndexOutOfBounds:

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.stage.Stage;

public class Tester extends Application{
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        ComboBox<Integer> box = new ComboBox<Integer>();
        ObservableList<Integer> vals= FXCollections.observableArrayList(0,1,2,3);

        box.setItems(vals);
        box.getSelectionModel().select(0);
        /*box.valueProperty().addListener((observable, oldValue, newValue) -> {
            box.getSelectionModel().select(0);
        });*/
        /*box.getSelectionModel().selectedItemProperty().addListener((observable,oldValue,newValue)->{
            System.out.println(oldValue+","+newValue);
            box.getSelectionModel().select(0);
        });*/

        box.getSelectionModel().selectedIndexProperty().addListener((observable,oldValue,newValue)->{
            System.out.println(oldValue+","+newValue);
            box.getSelectionModel().select(0);
        });
        Scene scene = new Scene(new Group(box),500,500);
        stage.setScene(scene);
        stage.show();
    }
}

I tested it with valueProperty, selectedItemProperty and selectedIndexProperty, as well as all of these:

box.getSelectionModel().select(0);

box.getSelectionModel().selectFirst();

box.getSelectionModel().selectPrevious();

box.setValue(0);

if (oldValue.intValue() < newValue.intValue())
            box.getSelectionModel().select(oldValue.intValue());

The only think that works is setting the value itself:

box.getSelectionModel().select(box.getSelectionModel().getSelectedIndex());
box.setValue(box.getValue));

Here is the exception:

Exception in thread "JavaFX Application Thread" java.lang.IndexOutOfBoundsException
    at com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList.subList(Unknown Source)
    at javafx.collections.ListChangeListener$Change.getAddedSubList(Unknown Source)
    at com.sun.javafx.scene.control.behavior.ListViewBehavior.lambda$new$178(Unknown Source)
    at com.sun.javafx.scene.control.behavior.ListViewBehavior$$Lambda$126/644961012.onChanged(Unknown Source)
    at javafx.collections.WeakListChangeListener.onChanged(Unknown Source)
    at com.sun.javafx.collections.ListListenerHelper$Generic.fireValueChangedEvent(Unknown Source)
    at com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(Unknown Source)
    at com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList.callObservers(Unknown Source)
    at javafx.scene.control.MultipleSelectionModelBase.clearAndSelect(Unknown Source)
    at javafx.scene.control.ListView$ListViewBitSetSelectionModel.clearAndSelect(Unknown Source)
    at com.sun.javafx.scene.control.behavior.CellBehaviorBase.simpleSelect(Unknown Source)
    at com.sun.javafx.scene.control.behavior.CellBehaviorBase.doSelect(Unknown Source)
    at com.sun.javafx.scene.control.behavior.CellBehaviorBase.mousePressed(Unknown Source)
    at com.sun.javafx.scene.control.skin.BehaviorSkinBase$1.handle(Unknown Source)
    at com.sun.javafx.scene.control.skin.BehaviorSkinBase$1.handle(Unknown Source)
    at com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(Unknown Source)
    at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(Unknown Source)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(Unknown Source)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(Unknown Source)
    at com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(Unknown Source)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.EventUtil.fireEventImpl(Unknown Source)
    at com.sun.javafx.event.EventUtil.fireEvent(Unknown Source)
    at javafx.event.Event.fireEvent(Unknown Source)
    at javafx.scene.Scene$MouseHandler.process(Unknown Source)
    at javafx.scene.Scene$MouseHandler.access$1500(Unknown Source)
    at javafx.scene.Scene.impl_processMouseEvent(Unknown Source)
    at javafx.scene.Scene$ScenePeerListener.mouseEvent(Unknown Source)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(Unknown Source)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(Unknown Source)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$350(Unknown Source)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$$Lambda$172/2037973250.get(Unknown Source)
    at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(Unknown Source)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(Unknown Source)
    at com.sun.glass.ui.View.handleMouseEvent(Unknown Source)
    at com.sun.glass.ui.View.notifyMouse(Unknown Source)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.lambda$null$145(Unknown Source)
    at com.sun.glass.ui.win.WinApplication$$Lambda$36/2117255219.run(Unknown Source)
    at java.lang.Thread.run(Unknown Source)

What am I doing wrong?

leyren
  • 524
  • 6
  • 20

2 Answers2

13

In JavaFX, you cannot change the contents of an ObservableList while a change is already in progress. What is happening here is that your listeners (any of the ones you try) are being fired as part of the box.getSelctionModel().getSelectedItems() ObservableList changing. So basically, you cannot change the selection while a selection change is being processed.

Your solution is a bit unwieldy anyway. If you had another listener on the selected item (or combo box value), even if your method worked it would temporarily see the combo box with an "illegal" selection. E.g in the example above, if the user tries to select "1", another listener would see the selection change to the disallowed value "1", then back to "0". Dealing with values that are not supposed to be allowed in this listener would likely make your program logic pretty complex.

A better approach, imho, is to prevent the user from selecting the disallowed values. You can do this with a cell factory that sets the disable property of the cell:

    box.setCellFactory(lv -> new ListCell<Integer>() {
        @Override
        public void updateItem(Integer item, boolean empty) {
            super.updateItem(item, empty);
            if (empty) {
                setText(null);
            } else {
                setText(item.toString());
                setDisable(item.intValue() != 0);
            }
        }
    });

Including the following in an external style sheet will give the user the usual visual hint that the items are not selectable:

.combo-box-popup .list-cell:disabled  {
    -fx-opacity: 0.4 ;
}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • I agree with this answer for the particular problem of disallowing the user to select certain items. But I haven't seen a good argument why changing the selection from within changing the selection should not be allowed. I have filed a [bug report](https://bugs.openjdk.java.net/browse/JDK-8133228) for it recently. I have little hope that it will be fixed, but again, I don't see why it shouldn't be allowed. For the record, ReactFX's [LiveList](http://www.reactfx.org/javadoc/2.0-M4u1/org/reactfx/collection/LiveList.html) supports recursive changes (changes made from within change listeners). – Tomas Mikula Sep 03 '15 at 19:33
  • Agreed. (I never said it was a good thing: just that it is a thing :).) I (sort of) see, given the `ListChangeListener.Change` API, that it might not be a good idea to allow changes to the list while iterating an existing `Change`, but it seems that changes to the `selectedItem` (and `value` in a `ComboBox`) could be made outside of this anyway. But I haven't really delved into the source code for this... – James_D Sep 03 '15 at 19:51
  • 2
    `ListChangeListener.Change` is a terrible API. Disallowing changes while handling the current change is a leaking implementation detail, not a fundamental constraint. – Tomas Mikula Sep 04 '15 at 03:07
  • 1
    This approach wouldn't work in the case where you need to perform some action upon clicking on the disabled item. – DarkMental Sep 21 '18 at 05:13
13

I know the thread is quite old but I had a similar problem and I worked it out in a different way. I tried to change ComboBox' selected item in its onAction method when the item wasn't available at that moment (for example because of the given condition). As @James_D said in his answer, the problem is setting the object that is being currently modified.

Just add your code inside the Platform.runLater() method:

Platform.runLater(() -> box.getSelectionModel().select(0));

In my case it worked, hope it will work in the others too.