Question

I want to create a StackPane where, whatever is added, a node of my choice is always at the front (rather than having to be careful about the order I add things in, or remembering to call toFront() on that particular node whenever anything is added.)

In order to do this, I simply place a listener on the child list of the relevant StackPane object, so that whenever anything changes, it calls toFront() on the relevant node, for example:

public class Test extends Application {

    @Override
    public void start(Stage stage) {
        StackPane root = new StackPane();
        final Rectangle r1 = new Rectangle(50, 50);
        root.getChildren().add(r1);
        root.getChildren().addListener(new ListChangeListener<Node>() {
            @Override
            public void onChanged(ListChangeListener.Change<? extends Node> change) {
                try {
                    while(change.next()) {
                        if(change.wasAdded()) {
                            r1.toFront();
                        }
                    }
                }
                catch(Exception ex) {
                    ex.printStackTrace();
                }
            }
        });
        root.getChildren().add(new Rectangle(50, 50));

        stage.setScene(new Scene(root));
        stage.show();
    }
}

In Java 7, this works just fine. However, in JFX8 (latest build downloaded just now), it fails with the following:

java.lang.UnsupportedOperationException
        at java.util.Collections$UnmodifiableList.add(Collections.java:1374)
        at javafx.collections.ListChangeBuilder.nextRemove(ListChangeBuilder.java:208)
        at javafx.collections.ObservableListBase.nextRemove(ObservableListBase.java:150)
        at javafx.collections.ModifiableObservableListBase.remove(ModifiableObservableListBase.java:181)
        at com.sun.javafx.collections.VetoableListDecorator.remove(VetoableListDecorator.java:284)
        at com.sun.javafx.collections.VetoableListDecorator.remove(VetoableListDecorator.java:209)
        at javafx.scene.Parent.impl_toFront(Parent.java:624)
        at javafx.scene.Node.toFront(Node.java:1713)
        at test.Test$1.onChanged(Test.java:34)
        at com.sun.javafx.collections.ListListenerHelper$Generic.fireValueChangedEvent(ListListenerHelper.java:315)
        at com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(ListListenerHelper.java:72)
        at com.sun.javafx.collections.VetoableListDecorator$1.onChanged(VetoableListDecorator.java:77)
        at com.sun.javafx.collections.ListListenerHelper$Generic.fireValueChangedEvent(ListListenerHelper.java:315)
        at com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(ListListenerHelper.java:72)
        at javafx.collections.ObservableListBase.fireChange(ObservableListBase.java:233)
        at javafx.collections.ListChangeBuilder.commit(ListChangeBuilder.java:482)
        at javafx.collections.ListChangeBuilder.endChange(ListChangeBuilder.java:541)
        at javafx.collections.ObservableListBase.endChange(ObservableListBase.java:205)
        at javafx.collections.ModifiableObservableListBase.add(ModifiableObservableListBase.java:155)
        at java.util.AbstractList.add(AbstractList.java:108)
        at com.sun.javafx.collections.VetoableListDecorator.add(VetoableListDecorator.java:200)
        at test.Test.start(Test.java:41)
        at com.sun.javafx.application.LauncherImpl$8.run(LauncherImpl.java:837)
        at com.sun.javafx.application.PlatformImpl$7.run(PlatformImpl.java:331)
        at com.sun.javafx.application.PlatformImpl$6$1.run(PlatformImpl.java:297)
        at com.sun.javafx.application.PlatformImpl$6$1.run(PlatformImpl.java:294)
        at java.security.AccessController.doPrivileged(Native Method)
        at com.sun.javafx.application.PlatformImpl$6.run(PlatformImpl.java:294)
        at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
        at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
        at com.sun.glass.ui.win.WinApplication.access$300(WinApplication.java:39)
        at com.sun.glass.ui.win.WinApplication$4$1.run(WinApplication.java:112)
        at java.lang.Thread.run(Thread.java:744)

And yes, test.Test$1.onChanged(Test.java:34) does indeed refer to r1.toFront();.

Is this to be considered a bug, or am I breaking some rule I'm unaware of by trying to achieve things this way? I did wonder whether the list was still being changed while the onChanged() method was executing, and toFront() would also change the list contents, hence the exception - but the Javadoc to onChanged() clearly says:

Called after a change has been made to an ObservableList.

(Bolding is mine.)

EDIT: At this point I'm more certain that it's a bug, so the related bug report is here.

Was it helpful?

Solution

It seems that you are not allowed to modify a (JavaFX) list inside an event handler that is currently handling another (previous) modification event of the same list. Although this seems reasonable it is not self-evident, so there should be a more obvious exception in that case.

Unfortunately non-speeking exceptions are very common in JavaFX.

Fortunately the solution/workaround is pretty easy: Call your modifying code (here: r1.toFront) by Platform.runLater(), it will delay your modification to happen after the originating event:

root.getChildren().addListener(new ListChangeListener<Node>() {
    @Override
    public void onChanged(ListChangeListener.Change<? extends Node> change) {
        Platform.runLater(new Runnable() {
            public void run() { r1.toFront(); }                 
        });
    }
});

Sidenote: toFront does nothing, if the component is already at front. This prevents infinite loops. Nevertheless, as this is not explicitely mentioned in the documentation, you might not rely on that.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top