--- old/modules/graphics/src/main/java/javafx/scene/Node.java 2013-11-07 14:46:28.647331501 +0100 +++ new/modules/graphics/src/main/java/javafx/scene/Node.java 2013-11-07 14:46:28.547331498 +0100 @@ -729,7 +729,7 @@ // notifyParentsOfInvalidatedCSS() will be skipped thus leaving the node un-styled. cssFlag = CssFlags.CLEAN; } - updateTreeVisible(); + updateTreeVisible(true); oldParent = newParent; invalidateLocalToSceneTransform(); parentResolvedOrientationInvalidated(); @@ -757,7 +757,7 @@ private final InvalidationListener parentTreeVisibleChangedListener = new InvalidationListener() { @Override public void invalidated(Observable valueModel) { - updateTreeVisible(); + updateTreeVisible(true); } }; @@ -1097,7 +1097,7 @@ if (oldValue != get()) { impl_markDirty(DirtyBits.NODE_VISIBLE); impl_geomChanged(); - updateTreeVisible(); + updateTreeVisible(false); if (getParent() != null) { // notify the parent of the potential change in visibility // of this node, since visibility affects bounds of the @@ -2294,7 +2294,7 @@ // PerformanceTracker.logEvent("Node.init for [{this}, id=\"{id}\"]"); //} setDirty(); - updateTreeVisible(); + updateTreeVisible(false); //if (PerformanceTracker.isLoggingEnabled()) { // PerformanceTracker.logEvent("Node.postinit " + // "for [{this}, id=\"{id}\"] finished"); @@ -6367,13 +6367,13 @@ if (oldClip != null) { oldClip.clipParent = null; oldClip.setScenes(null, null); - oldClip.updateTreeVisible(); + oldClip.updateTreeVisible(false); } if (newClip != null) { newClip.clipParent = Node.this; newClip.setScenes(getScene(), getSubScene()); - newClip.updateTreeVisible(); + newClip.updateTreeVisible(true); } impl_markDirty(DirtyBits.NODE_CLIP); @@ -7738,13 +7738,20 @@ } - private void updateTreeVisible() { + private void updateTreeVisible(boolean parentChanged) { boolean isTreeVisible = isVisible(); + final Node parentNode = getParent() != null ? getParent() : + clipParent != null ? clipParent : + getSubScene() != null ? getSubScene() : null; if (isTreeVisible) { - final Parent p = getParent(); - isTreeVisible = p != null ? p.impl_isTreeVisible() : - clipParent != null ? clipParent.impl_isTreeVisible() : - getSubScene() == null || getSubScene().impl_isTreeVisible(); + isTreeVisible = parentNode == null || parentNode.impl_isTreeVisible(); + } + // When the parent has changed to visible and we have unsynchornized visibility, + // we have to synchronize, because the rendering will now pass throught the newly-visible parent + // Otherwise an invisible Node might get rendered + if (parentChanged && parentNode != null && parentNode.impl_isTreeVisible() + && impl_isDirty(DirtyBits.NODE_VISIBLE)) { + addToSceneDirtyList(); } setTreeVisible(isTreeVisible); } @@ -7758,11 +7765,9 @@ updateCanReceiveFocus(); focusSetDirty(getScene()); if (getClip() != null) { - getClip().updateTreeVisible(); + getClip().updateTreeVisible(true); } if (treeVisible && !impl_isDirtyEmpty()) { - // The node hasn't been synchronized while invisible, so - // synchronize now addToSceneDirtyList(); } ((TreeVisiblePropertyReadOnly)impl_treeVisibleProperty()).invalidate(); --- old/modules/graphics/src/test/java/javafx/scene/NodeTest.java 2013-11-07 14:46:29.027331513 +0100 +++ new/modules/graphics/src/test/java/javafx/scene/NodeTest.java 2013-11-07 14:46:28.931331510 +0100 @@ -1044,6 +1044,96 @@ assertEquals(100.0, sc.getRadius(), 0.01); } + + @Test + public void testSynchronizationOfInvisibleNodes_2() { + final Group g = new Group(); + final Circle c = new CircleTest.StubCircle(50); + + Scene s = new Scene(g); + Stage st = new Stage(); + st.show(); + st.setScene(s); + + final NGGroup sg = g.impl_getPeer(); + final CircleTest.StubNGCircle sc = c.impl_getPeer(); + + g.getChildren().add(c); + + s.scenePulseListener.pulse(); + + g.setVisible(false); + + s.scenePulseListener.pulse(); + + assertFalse(sg.isVisible()); + assertTrue(sc.isVisible()); + + c.setCenterX(10); // Make the circle dirty. It won't be synchronized as it is practically invisible (through the parent) + + s.scenePulseListener.pulse(); + + c.setVisible(false); // As circle is invisible and dirty, this won't trigger a synchronization + + s.scenePulseListener.pulse(); + + assertFalse(sg.isVisible()); + assertTrue(sc.isVisible()); // This has not been synchronized, as it's not necessary + // The rendering will stop at the Group, which is invisible + + g.setVisible(true); + + s.scenePulseListener.pulse(); + + assertTrue(sg.isVisible()); + assertFalse(sc.isVisible()); // Now the group is visible again, we need to synchronize also + // the Circle + } + + @Test + public void testSynchronizationOfInvisibleNodes_2_withClip() { + final Group g = new Group(); + final Circle c = new CircleTest.StubCircle(50); + + Scene s = new Scene(g); + Stage st = new Stage(); + st.show(); + st.setScene(s); + + final NGGroup sg = g.impl_getPeer(); + final CircleTest.StubNGCircle sc = c.impl_getPeer(); + + g.setClip(c); + + s.scenePulseListener.pulse(); + + g.setVisible(false); + + s.scenePulseListener.pulse(); + + assertFalse(sg.isVisible()); + assertTrue(sc.isVisible()); + + c.setCenterX(10); // Make the circle dirty. It won't be synchronized as it is practically invisible (through the parent) + + s.scenePulseListener.pulse(); + + c.setVisible(false); // As circle is invisible and dirty, this won't trigger a synchronization + + s.scenePulseListener.pulse(); + + assertFalse(sg.isVisible()); + assertTrue(sc.isVisible()); // This has not been synchronized, as it's not necessary + // The rendering will stop at the Group, which is invisible + + g.setVisible(true); + + s.scenePulseListener.pulse(); + + assertTrue(sg.isVisible()); + assertFalse(sc.isVisible()); // Now the group is visible again, we need to synchronize also + // the Circle + } @Test public void testLocalToScreen() {