<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  </head>
  <body>
    <p>I think I figured out the reason why this fails.  The root cause
      lies in a misconception I've seen in a lot of FX code.<br>
    </p>
    <p>JavaFX uses a single event thread model, which ensures all
      structures are only ever accessed by a single thread.  This frees
      FX from having to do synchronization on almost every modification
      you make to properties or the scene graph.</p>
    <p>However, in many areas it makes the assumption that such code
      will always run sequentially to completion without interruption,
      and uses instance fields judiciously to communicate things to
      deeper nested code or to code further down the line.  But code
      using instance fields in this way is not safe to re-enter (it is
      not reentrant-safe) without precautions -- sharing instance fields
      in this way safely can easily get as complicated as writing
      multi-threaded code.</p>
    <p>A simple example that I saw in Parent's toFront code:</p>
    <div style="background-color:#ffffff;padding:0px 0px 0px 2px;">
      <div
style="color:#000000;background-color:#ffffff;font-family:"Consolas";font-size:11pt;white-space:pre;"><p
      style="margin:0;"><span style="color:#000000;">            </span><span
      style="color:#0000c0;">childrenTriggerPermutation</span><span
      style="color:#000000;"> = </span><span
      style="color:#0000a0;font-weight:bold;">true</span><span
      style="color:#000000;">;</span></p><p style="margin:0;"><span
      style="color:#000000;">            </span><span
      style="color:#0000a0;font-weight:bold;">try</span><span
      style="color:#000000;"> {</span></p><p style="margin:0;"><span
      style="color:#000000;">                </span><span
      style="color:#0000c0;">children</span><span style="color:#000000;">.remove(node);</span></p><p
      style="margin:0;"><span style="color:#000000;">                </span><span
      style="color:#0000c0;">children</span><span style="color:#000000;">.add(node);</span></p><p
      style="margin:0;"><span style="color:#000000;">            } </span><span
      style="color:#0000a0;font-weight:bold;">finally</span><span
      style="color:#000000;"> {</span></p><p style="margin:0;"><span
      style="color:#000000;">                </span><span
      style="color:#0000c0;">childrenTriggerPermutation</span><span
      style="color:#000000;"> = </span><span
      style="color:#0000a0;font-weight:bold;">false</span><span
      style="color:#000000;">;</span></p><p style="margin:0;"><span
      style="color:#000000;">            }</span></p></div>
    </div>
    <p></p>
    <p>The above code uses an instance field
      "childrenTriggerPermutation" to activate an optimization. The
      optimization will assume that the children are only re-arranged,
      and no new ones were added or removed.  However, "children" is an
      ObservableList, which means the user can register listeners on it,
      which do who knows what.  If such a listener modifies the children
      list in another way then the code is entered again, but the
      "childrenTriggerPermutation" optimization will still be enabled
      causing it to not notice the change the user did.</p>
    <p>This problem is similar to the ChangeListener old value bug. 
      When within a change listener you do another change (and so the
      same code is called **deeper** in the same stack), downstream
      change listeners will not receive the correct old values because
      the code is insufficiently reentrant-safe.  ExpressionHelper
      **tries** to mitigate some of these issues (for cases where
      listeners are added/removed reentrantly) by making copies of the
      listener list, but it does not handle this case.<br>
    </p>
    <p>Similarly, the bug I encountered in my original post is also such
      an issue.  While processing the children list changes, several
      **properties** are being manipulated.  Being properties, these can
      have listeners of their own that could trigger further
      modifications and, in complex enough programs, they may even
      re-enter the same class's code that is sharing instance fields in
      an unsafe way.  And that's exactly what is happening:</p>
    <p>1. The children list change processing is registering the offset
      of the first changed child in the children list (called
      "startIdx") as an instance field -- this field is used as an
      optimization for updatePeer (so it doesn't have to check/copy all
      children).  It assumes the processing always finishes completely
      and it will get to the point where it sets "startIdx" but...<br>
    </p>
    <p>2. Before it sets "startIdx" but after the children list is
      already modified, it modifies several properties.  Being
      properties, these can have listeners, and as such this can
      trigger a cascade of further calls in complicated applications.</p>
    <p>3. In this case, the cascade of calls included an
      "enterNestedEventLoop".  Pulses (and things like
      Platform#runLater) can be handled on such a nested loop, and FX
      decides that now is as good a time as any to handle a new pulse.</p>
    <p>4. The pulse triggers updatePeer calls, among which is the Parent
      that is still (higher in the stack) midway its children list
      processing code.</p>
    <p>5. The updatePeer code looks at "startIdx", the shared instance
      field that Parent uses for its optimizations.  This field is NOT
      modified yet.  The field indicates the first child that was
      modified, and the field is normally set to "children.size()" when
      there are no changes.  That's also the case in this case still,
      and so updatePeer updates nothing at all.  An assertion later in
      this code then checks if children.size() == peer.children.size()
      which fails... a stack trace is thrown, and
      synchronizeSceneNodes() blows up with infinite NPE's.</p>
    <p>I'm not entirely sure yet how to resolve this, and if it should
      be.</p>
    <p>Perhaps the safest way would be to undo some of the
      optimizations/assumptions, and perhaps reoptimize them if there's
      a pressing need.</p>
    <p>Another option would be to somehow delay listener callbacks until
      the code in Parent is in a safe state.</p>
    <p>The option I like the least is to introduce yet another instance
      flag ("processingListChange") and throwing an early exception if
      other code is entered that doesn't expect it...</p>
    <p>--John<br>
    </p>
    <p><br>
    </p>
    On 22/08/2024 05:24, John Hendrikx wrote:<br>
    <blockquote type="cite"
      cite="mid:efc29c6d-9b12-408b-9328-af181c42ba5d@gmail.com">
      <meta http-equiv="content-type" content="text/html; charset=UTF-8">
      <p>Hi List,</p>
      <p>This is a bit of a long post. I'm mainly wondering if I did
        something wrong that FX should detect early, or if I'm doing
        nothing unusual and FX should handle the case described below
        correctly.<br>
      </p>
      <p>I encountered the bug where an NPE occurs in
        Scene$ScenePulseListener#synchronizeNodes, and it is
        reproducable at the moment. I'm not sure it is the same one
        others sometimes see, but the version I encountered is prefaced
        with a failing assert (which may easily get lost as
        synchronizeNodes will spam NPE's in your log, as it doesn't
        recover).</p>
      <p>In Parent#validatePG it prints:</p>
      <p>    *** pgnodes.size validatePG() [1] != children.size() [2]</p>
      <p>And then throws an AssertionError with a long stacktrace.<br>
      </p>
      <p>    java.lang.AssertionError: validation of PGGroup children
        failed<br>
                (stack trace omitted)<br>
      </p>
      <p>Immediately after this, the NPE in synchronizeNodes starts to
        get spammed, and the application never recovers.<br>
      </p>
      <p>This seems to have something to do with nested event loops, as
        I introduced a new one in the code involved.  When I remove the
        nested event loop, there is no problem (it also initially works
        fine when the application is just starting, only in a different
        situation, when there is some user interaction via a keypress,
        does the bug trigger).</p>
      <p>The nested event loop is entered from a ChangeListener, which
        may be a bit unusual.  The documentation of
        Platform#enterNestedEventLoop says:</p>
      <div style="background-color:#ffffff;padding:0px 0px 0px 2px;">
        <div
style="color:#000000;background-color:#ffffff;font-family:"Consolas";font-size:11pt;white-space:pre;"><p
        style="margin:0;"><span style="color:#3f5fbf;">     * This method must either be called from an input event handler or</span></p><p
        style="margin:0;"><span style="color:#3f5fbf;">     * from the run method of a Runnable passed to</span></p><p
        style="margin:0;"><span style="color:#3f5fbf;">     * </span><span
        style="color:#3f3fbf;">{@link javafx.application.Platform#runLater Platform.runLater}</span><span
        style="color:#3f5fbf;">.</span></p><p style="margin:0;"><span
        style="color:#3f5fbf;">     * It must not be called during animation or layout processing.</span></p></div>
      </div>
      <p>It is also documented to throw an IllegalStateException if used
        incorrectly.  That is however not happening, so I guess I'm
        using it correctly...?  On the other hand, I'm not in an input
        event handler.... the whole process is triggered by a keypress
        though, and deep down in the AssertionError trace you can see:</p>
      <p>    at
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)<br>
            at
        com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)<br>
            at
        com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)<br>
            at javafx.event.Event.fireEvent(Event.java:198)<br>
            at javafx.scene.Scene$KeyHandler.process(Scene.java:4113)<br>
            at javafx.scene.Scene.processKeyEvent(Scene.java:2159)<br>
            at
        javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:2627)<br>
            at
com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:218)<br>
        <br>
        So IMHO technically I am in an input event handler...?</p>
      <p>Now the code does a LOT of stuff, which is going to make this
        tough to analyze.  In short:</p>
      <p>- I'm displaying a Scene<br>
        - The user indicates (with a keypress) they want to go to a
        different Pane<br>
        - The previous Pane is created, and added to the scene as a
        child to a TransitionPane that will cross-fade between the
        current Pane and the new Pane<br>
        - As soon as the new Pane becomes part of the Scene, lots of
        things get triggered because the Scene property goes from null
        to the active Scene:<br>
        <br>
            at javafx.scene.Node.invalidatedScenes(Node.java:1075)<br>
            at javafx.scene.Node.setScenes(Node.java:1142)             
        <-- the new Pane will get a Scene assigned to it<br>
            at javafx.scene.Parent$2.onChanged(Parent.java:372)     
        <-- the new Pane has its Parent changed to the TransitionPane<br>
        <br>
        - Several parts of the new Pane listen (directly or indirectly)
        to a change in Scene, as they only become "active" when the Node
        involved is displayed (this happens with a Scene listener)<br>
        - One of those things is some code that will create and populate
        a ListView; this code uses a relatively fast query, but I had
        marked it as something that should be done in the background as
        it sometimes does take a bit too much time on the FX thread</p>
      <p>Now, how I normally do things in the background is to display
        an automatically closing "Please wait" dialog potentially with a
        progress bar on it (this dialog is actually invisible, unless
        sufficient time passes, so in 99% of the cases when things
        respond fast, the user never sees it, even though it was created
        and is there).  This involves starting a nested event loop. 
        This works marvelously throughout this application (and has done
        so for years), and it is used for almost every transition from
        one part of the application to the next.  In all cases so far
        however I did this directly from an event handler.</p>
      <p>So the main difference is that I'm trying to enter a nested
        event loop from a ChangeListener (which deep down was triggered
        by an Event).  In the AssertionError stack trace (which I will
        include at the end), there is no layout or animation code
        **before** entering the nested loop, although there is some
        layout code **after** it was entered.</p>
      <p>I can live with the fact that I may be using
        enterNestedEventLoop incorrectly here, but in that case it
        should probably also detect this incorrect use and throw the
        IllegalStateException.</p>
      <p>Technically, all this code is triggered while "adding" a Child
        to the TransitionPane, so I suspect that is what the
        AssertionError is about (it indicates the child count 1 != 2,
        which is what TransitionPane has, one active pane, and just
        added to cross fade to).  Still, is this really incorrect usage?</p>
      <p>I've included an annotated stack trace below.</p>
      <p>As it is quite reproducable, I can debug this further by adding
        breakpoints/prints -- I'm just unsure where to start looking.<br>
      </p>
      <p>--John</p>
      <p>java.lang.AssertionError: validation of PGGroup children failed<br>
          at javafx.scene.Parent.validatePG(Parent.java:243)<br>
          at javafx.scene.Parent.doUpdatePeer(Parent.java:201)<br>
          at javafx.scene.Parent$1.doUpdatePeer(Parent.java:109)<br>
          at
        com.sun.javafx.scene.ParentHelper.updatePeerImpl(ParentHelper.java:78)<br>
          at
com.sun.javafx.scene.layout.RegionHelper.updatePeerImpl(RegionHelper.java:72)<br>
          at
        com.sun.javafx.scene.NodeHelper.updatePeer(NodeHelper.java:104)<br>
          at javafx.scene.Node.syncPeer(Node.java:721)<br>
          at
javafx.scene.Scene$ScenePulseListener.synchronizeSceneNodes(Scene.java:2396)<br>
          at
        javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2542)<br>
          at
        com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:407)<br>
          at
java.base/java.security.AccessController.doPrivileged(AccessController.java:400)<br>
          at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:406)<br>
          at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:436)<br>
          at
        com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:575)<br>
          at
        com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:555)<br>
          at
com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:548)<br>
          at
com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$11(QuantumToolkit.java:352)<br>
          at
com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)<br>
          at
        com.sun.glass.ui.win.WinApplication._enterNestedEventLoopImpl(Native
        Method)<br>
          at
com.sun.glass.ui.win.WinApplication._enterNestedEventLoop(WinApplication.java:211)<br>
          at
        com.sun.glass.ui.Application.enterNestedEventLoop(Application.java:515)<br>
          at com.sun.glass.ui.EventLoop.enter(EventLoop.java:107)<br>
          at
com.sun.javafx.tk.quantum.QuantumToolkit.enterNestedEventLoop(QuantumToolkit.java:631)<br>
          at
        javafx.application.Platform.enterNestedEventLoop(Platform.java:301)<br>
          at
hs.mediasystem.util.javafx.SceneUtil.enterNestedEventLoop(SceneUtil.java:75)  
        <-- just my wrapper to trace slow calls, it delegates to
        Platform<br>
          at
        hs.mediasystem.runner.dialog.DialogPane.showDialog(DialogPane.java:78)<br>
          at
hs.mediasystem.runner.dialog.Dialogs.showProgressDialog(Dialogs.java:163)<br>
          at
        hs.mediasystem.runner.dialog.Dialogs.runNested(Dialogs.java:103)<br>
          at
hs.mediasystem.plugin.home.HomeScreenNodeFactory.lambda$1(HomeScreenNodeFactory.java:123) <br>
      </p>
      <p>The above line is where the nested event loop is entered.  It
        starts to display a "busy" dialog. A background task (on a new
        thread) will create a ListView (this never actually happens, the
        AssertionError is thrown immediately even with an empty task
        that just sleeps 10 seconds).<br>
      </p>
      <p>  at
com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:360)<br>
          at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
          at
javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:80)<br>
          at
javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)<br>
          at
javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:118)<br>
          at
javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:172)<br>
          at
javafx.scene.control.SelectionModel.setSelectedItem(SelectionModel.java:105)<br>
          at
javafx.scene.control.MultipleSelectionModelBase.lambda$new$0(MultipleSelectionModelBase.java:67)<br>
          at
com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:348)<br>
          at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
          at
javafx.beans.property.ReadOnlyIntegerPropertyBase.fireValueChangedEvent(ReadOnlyIntegerPropertyBase.java:78)<br>
          at
javafx.beans.property.ReadOnlyIntegerWrapper.fireValueChangedEvent(ReadOnlyIntegerWrapper.java:102)<br>
          at
javafx.beans.property.IntegerPropertyBase.markInvalid(IntegerPropertyBase.java:114)<br>
          at
javafx.beans.property.IntegerPropertyBase.set(IntegerPropertyBase.java:148)<br>
          at
javafx.scene.control.SelectionModel.setSelectedIndex(SelectionModel.java:69)<br>
          at
javafx.scene.control.MultipleSelectionModelBase.select(MultipleSelectionModelBase.java:424)<br>
          at
javafx.scene.control.MultipleSelectionModelBase.select(MultipleSelectionModelBase.java:456)<br>
          at
hs.mediasystem.plugin.home.HomeScreenNodeFactory.lambda$3(HomeScreenNodeFactory.java:176)   
        <-- triggers the creation of a ListView<br>
          at
        javafx.beans.value.ObservableValue.lambda$0(ObservableValue.java:364)<br>
          at
com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)<br>
          at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
          at
        javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)<br>
          at
com.sun.javafx.binding.ConditionalBinding.conditionChanged(ConditionalBinding.java:53)<br>
          at
        com.sun.javafx.binding.Subscription.lambda$2(Subscription.java:63)<br>
          at
com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)<br>
          at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
          at
        javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)<br>
          at
        com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)<br>
          at
com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)<br>
          at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
          at
        javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)<br>
          at
        com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)<br>
          at
com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)<br>
          at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
          at
        javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)<br>
          at
com.sun.javafx.binding.FlatMappedBinding.invalidateAll(FlatMappedBinding.java:102)<br>
          at
        com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)<br>
          at
com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)<br>
          at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
          at
        javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)<br>
          at
com.sun.javafx.binding.FlatMappedBinding.invalidateAll(FlatMappedBinding.java:102)<br>
          at
        com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)<br>
          at
com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:348)<br>
          at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
          at
javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:80)<br>
          at
javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)<br>
          at
javafx.scene.Node$ReadOnlyObjectWrapperManualFire.fireSuperValueChangedEvent(Node.java:1053)  
        <-- there is a listener on a Scene property<br>
          at javafx.scene.Node.invalidatedScenes(Node.java:1104)<br>
          at javafx.scene.Node.setScenes(Node.java:1142)<br>
          at javafx.scene.Parent.scenesChanged(Parent.java:772)   
        <-- the new Pane has a few nested Panes, so you see multiple
        Parent assignments<br>
          at javafx.scene.Node.invalidatedScenes(Node.java:1075)<br>
          at javafx.scene.Node.setScenes(Node.java:1142)<br>
          at javafx.scene.Parent.scenesChanged(Parent.java:772)<br>
          at javafx.scene.Node.invalidatedScenes(Node.java:1075)<br>
          at javafx.scene.Node.setScenes(Node.java:1142)           
        <-- the Parent is part of a Scene, so this new Pane also gets
        the same Scene assigned<br>
          at javafx.scene.Parent$2.onChanged(Parent.java:372)    <--
        the new Pane gets its parent assigned (TransitionPane)<br>
          at
com.sun.javafx.collections.TrackableObservableList.lambda$new$0(TrackableObservableList.java:45)<br>
          at
com.sun.javafx.collections.ListListenerHelper$Generic.fireValueChangedEvent(ListListenerHelper.java:329)<br>
          at
com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(ListListenerHelper.java:73)<br>
          at
javafx.collections.ObservableListBase.fireChange(ObservableListBase.java:239)<br>
          at
        javafx.collections.ListChangeBuilder.commit(ListChangeBuilder.java:482)<br>
          at
javafx.collections.ListChangeBuilder.endChange(ListChangeBuilder.java:541)<br>
          at
javafx.collections.ObservableListBase.endChange(ObservableListBase.java:211)<br>
          at
javafx.collections.ModifiableObservableListBase.add(ModifiableObservableListBase.java:162)<br>
          at
com.sun.javafx.collections.VetoableListDecorator.add(VetoableListDecorator.java:319)<br>
          at
hs.mediasystem.util.javafx.ui.transition.TransitionPane.add(TransitionPane.java:94)     
        <-- new Pane is added to a TransitionPane that handles cross
        fade between old and new Pane<br>
          at
hs.mediasystem.util.javafx.ui.transition.TransitionPane.addAtStart(TransitionPane.java:105)<br>
          at
hs.mediasystem.util.javafx.ui.transition.TransitionPane.add(TransitionPane.java:113)<br>
          at
hs.mediasystem.runner.presentation.ViewPort.updateChildNode(ViewPort.java:68)      
        <-- here it installs the new Pane to display<br>
          at
        hs.mediasystem.runner.presentation.ViewPort.lambda$0(ViewPort.java:39)<br>
          at
com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)<br>
          at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
          at
        javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)<br>
          at
        com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)<br>
          at
com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)<br>
          at
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)<br>
          at
javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:111)<br>
          at
javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:118)<br>
          at
javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:172)<br>
          at
hs.mediasystem.presentation.ParentPresentation.navigateBack(ParentPresentation.java:39)   
        <-- this handles the keyPress (it was a "back" press)<br>
          at hs.mediasystem.util.expose.Expose.lambda$3(Expose.java:55)<br>
          at hs.mediasystem.util.expose.Trigger$1.run(Trigger.java:58)<br>
          at
hs.mediasystem.runner.RootPresentationHandler.tryRunAction(RootPresentationHandler.java:106)<br>
          at
hs.mediasystem.runner.RootPresentationHandler.handleActionEvent(RootPresentationHandler.java:81)<br>
          at
com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:247)<br>
          at
com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)<br>
          at
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:234)<br>
          at
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)<br>
          at
com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)<br>
          at
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)<br>
          at
        hs.mediasystem.util.javafx.SceneUtil.lambda$1(SceneUtil.java:101)     
        <-- this is just my Slow Event detection, and just delegates
        the event<br>
          at
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)<br>
          at
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)<br>
          at
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)<br>
          at
        com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)<br>
          at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49)<br>
          at
        hs.mediasystem.util.javafx.base.Events.dispatchEvent(Events.java:35)<br>
          at
hs.mediasystem.runner.action.InputActionHandler.handleKeyEvent(InputActionHandler.java:153)    
        <-- just code that handles a keypress that bubbled all the
        way to the top<br>
          at
hs.mediasystem.runner.action.InputActionHandler.onKeyPressed(InputActionHandler.java:138)<br>
          at
com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)<br>
          at
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:234)<br>
          at
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)<br>
          at
com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)<br>
          at
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)<br>
          at
        hs.mediasystem.util.javafx.SceneUtil.lambda$1(SceneUtil.java:101)     
        <-- this is just my Slow Event detection, and just delegates
        the event<br>
          at
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)<br>
          at
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)<br>
          at
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)<br>
          at
        com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)<br>
          at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)<br>
          at javafx.event.Event.fireEvent(Event.java:198)<br>
          at javafx.scene.Scene$KeyHandler.process(Scene.java:4113)<br>
          at javafx.scene.Scene.processKeyEvent(Scene.java:2159)<br>
          at
        javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:2627)<br>
          at
com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:218)<br>
          at
com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:150)<br>
          at
java.base/java.security.AccessController.doPrivileged(AccessController.java:400)<br>
          at
com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleKeyEvent$1(GlassViewEventHandler.java:250)<br>
          at
com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:424)<br>
          at
com.sun.javafx.tk.quantum.GlassViewEventHandler.handleKeyEvent(GlassViewEventHandler.java:249)<br>
          at com.sun.glass.ui.View.handleKeyEvent(View.java:542)<br>
          at com.sun.glass.ui.View.notifyKey(View.java:966)<br>
          at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)<br>
          at
com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:184)<br>
          at java.base/java.lang.Thread.run(Thread.java:1583)<br>
      </p>
      <p>And the NPE exception:</p>
      <p>java.lang.NullPointerException: Cannot invoke
        "javafx.scene.Node.getScene()" because "<local2>" is null<br>
            at
javafx.scene.Scene$ScenePulseListener.synchronizeSceneNodes(Scene.java:2395)<br>
            at
        javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2542)<br>
            at
        com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:407)<br>
            at
java.base/java.security.AccessController.doPrivileged(AccessController.java:400)<br>
            at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:406)<br>
            (rest of trace identical to the AssertionError one)</p>
      <p>And another variant of the NPE exception:<br>
        <br>
        java.lang.NullPointerException: Cannot invoke
        "javafx.scene.Node.getScene()" because "<local2>" is null<br>
          at
javafx.scene.Scene$ScenePulseListener.synchronizeSceneNodes(Scene.java:2395)<br>
          at
        javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2542)<br>
          at
        com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:407)<br>
          at
java.base/java.security.AccessController.doPrivileged(AccessController.java:400)<br>
          at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:406)<br>
          at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:436)<br>
          at
        com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:575)<br>
          at
        com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:555)<br>
          at
com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:548)<br>
          at
com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$11(QuantumToolkit.java:352)<br>
          at
com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)<br>
          at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)<br>
          at
com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:184)<br>
          at java.base/java.lang.Thread.run(Thread.java:1583)<br>
      </p>
      <p><br>
      </p>
      <p><br>
      </p>
      <p><br>
      </p>
      <p><br>
      </p>
    </blockquote>
  </body>
</html>