Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 37 additions & 14 deletions modules/javafx.graphics/src/main/java/javafx/stage/PopupWindow.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2010, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -458,31 +458,34 @@ public void show(Window ownerWindow, double anchorX, double anchorY) {
}

private void showImpl(final Window owner) {
Window rootWindow = getRootWindow(owner);
if (rootWindow == null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not equivalent change: the old code set this.ownerWindow (to null in this case, see L467)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a point but I don't see how this could be a problem. I can't imagine a usecase where you want to call show just to reset the owner

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was pointing out that the change is not equivalent - in the old code, a null owner causes this.ownerWindow be set to null, in the new code it does not.

return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am just a bit confused here. Referring to old version' line numbers, it looks like we should not have a null value returned from getRootWindow(owner): it's being dereferenced in L477 and also L485.

}

// Update the owner field
this.ownerWindow.set(owner);
if (owner instanceof PopupWindow) {
((PopupWindow)owner).children.add(this);
}

// PopupWindow should disappear when owner node is not visible
if (owner != null) {
owner.showingProperty().addListener(weakOwnerNodeListener);
}
owner.showingProperty().addListener(weakOwnerNodeListener);

final Scene sceneValue = getScene();
SceneHelper.parentEffectiveOrientationInvalidated(sceneValue);

// JDK-8116444
applyStylesheetFromOwner(owner);

final Scene ownerScene = getRootWindow(owner).getScene();
final Scene ownerScene = rootWindow.getScene();
if (ownerScene != null) {
copyStylesheetFromOwnerScene(ownerScene);

if (sceneValue.getCursor() == null) {
sceneValue.setCursor(ownerScene.getCursor());
}
}

// It is required that the root window exist and be visible to show the popup.
if (getRootWindow(owner).isShowing()) {
if (rootWindow.isShowing()) {
// We do show() first so that the width and height of the
// popup window are initialized. This way the x,y location of the
// popup calculated below uses the right width and height values for
Expand All @@ -498,14 +501,34 @@ private void showImpl(final Window owner) {
* @param owner the owner {@link Window}
*/
void applyStylesheetFromOwner(Window owner) {
// JDK-8116444
Window rootWindow = getRootWindow(owner);
if (rootWindow == null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing - is it even possible for the rootWindow to be null?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, as you can see in the tests I wrote.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a question about the tests you wrote.
Sorry, I need to spell my question out. Looking at (old) L502:

final Scene ownerScene = getRootWindow(owner).getScene();

you see that getScene() dereferences the root window, so it can't be null there, I guess, otherwise we had seen an NPE.

return;
}

final Scene ownerScene = rootWindow.getScene();
if (ownerScene == null) {
return;
}

copyStylesheetFromOwnerScene(ownerScene);
}

private void copyStylesheetFromOwnerScene(Scene ownerScene) {
Scene scene = getScene();
final Scene ownerScene = getRootWindow(owner).getScene();
if (ownerScene != null) {
if (ownerScene.getUserAgentStylesheet() != null) {
scene.setUserAgentStylesheet(ownerScene.getUserAgentStylesheet());
if (scene.getUserAgentStylesheet() == null && ownerScene.getUserAgentStylesheet() != null) {
scene.setUserAgentStylesheet(ownerScene.getUserAgentStylesheet());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there might be another issue:

  • owner A has the user agent stylesheet A.css
  • a popup with no stylesheet is shown with the owner A. A.css is shown
  • the popup gets hidden
  • either owner A changes the stylesheet to B.css, or the popup is reusing with a different owner
  • the popup gets shown

since scene.getUserAgentStylesheet() is already A.css, this code does not set the new stylesheet, leaving the popup with a wrong style.

I know reusing the popup (context menu, etc.) is a bad idea, but surprisingly, I saw this happen many times in the past.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK, there is no way that you can actually change the user agent stylesheet. It is read once and then cached.
See e.g. #525, where a user proposed to change that.

So I think we are actually good here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the case for the PopupTest that works in master and fails with this fix:

    @Test
    public void updateStylesheetFromOwnerStage() {
        String cssA = toBase64(".root { -fx-fill: green; }");
        String cssB = toBase64(".root { -fx-fill: red; }");
        scene.setUserAgentStylesheet(cssA);

        Popup p = new Popup();
        p.show(stage);
        assertEquals(cssA, p.getScene().getUserAgentStylesheet());

        p.hide();
        scene.setUserAgentStylesheet(cssB);
        p.show(stage);
        assertEquals(cssB, p.getScene().getUserAgentStylesheet());
    }

}

List<String> newStylesheets = new ArrayList<>();
for (String stylesheet : ownerScene.getStylesheets()) {
if (!scene.getStylesheets().contains(stylesheet)) {
newStylesheets.add(stylesheet);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can a PopupWindow be reused? if so, the stylesheets might accumulate from the earlier cycles. is this ok? should we keep track of the stylesheets added here and remove them on hide()?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does not happen as we will check with contains. There is also a test for this scenario.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose I need to spell this out. L527 adds new stylesheets, but there is not removing operation. So if you keep adding stylesheets and not removing the old stylesheets, they just get accumulated.

Think of a scenario when a stylesheet is not sourced from a file/resource, but generated programmatically via a data url.

}
scene.getStylesheets().setAll(ownerScene.getStylesheets());
}

scene.getStylesheets().addAll(newStylesheets);
}

/**
Expand Down
112 changes: 106 additions & 6 deletions modules/javafx.graphics/src/test/java/test/javafx/stage/PopupTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2010, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -48,6 +48,9 @@
import javafx.scene.input.MouseEvent;
import javafx.scene.shape.Rectangle;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import javafx.scene.Node;
import javafx.scene.ParentShim;
Expand All @@ -63,6 +66,7 @@
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertThrows;
import test.com.sun.javafx.stage.PopupRootHelper;
Expand Down Expand Up @@ -108,10 +112,8 @@ public void testShow() {
assertFalse(p2.isShowing());

// test showing popup without parent
// TODO should result in an exception
// Popup p3 = new Popup();
// p3.show(null);
// assertFalse(p3.isVisible());
Popup p3 = new Popup();
assertThrows(NullPointerException.class, () -> p3.show(null));
}

@Test
Expand Down Expand Up @@ -498,7 +500,7 @@ public void testPeerListener() {
}

@Test
public void testDefautValueOfAutofix() {
public void testDefaultValueOfAutofix() {
Popup p = new Popup();
assertTrue(p.isAutoFix());
assertTrue(p.autoFixProperty().get());
Expand Down Expand Up @@ -802,6 +804,104 @@ public void testCursorInheritance() {

}

@Test
public void testShowWithOwnerWithoutRootWindow() {
final Popup ownerPopup = new Popup();
final Popup popup = new Popup();

popup.show(ownerPopup);

assertFalse(popup.isShowing());
}

@Test
public void testShowWithOwnerWithoutScene() {
final Stage ownerWithoutScene = new Stage();

final Popup popup = new Popup();

popup.show(ownerWithoutScene);

assertFalse(popup.isShowing());
}

@Test
public void testShowKeepsCursorOfPopup() {
stage.getScene().setCursor(Cursor.CLOSED_HAND);

final Popup popup = new Popup();
popup.getScene().setCursor(Cursor.TEXT);

popup.show(stage);

assertEquals(Cursor.TEXT, popup.getScene().getCursor());
}

@Test
public void testShowAppliesUserAgentStylesheetOfOwnerWhenPopupHasNone() {
final String ownerUserAgentStylesheet = toBase64(".owner-ua { -fx-fill: red; }");
scene.setUserAgentStylesheet(ownerUserAgentStylesheet);

final Popup popup = new Popup();
assertNull(popup.getScene().getUserAgentStylesheet());

popup.show(stage);

assertEquals(ownerUserAgentStylesheet, popup.getScene().getUserAgentStylesheet());
}

@Test
public void testShowKeepsStylesheetsOfPopupAndAddsStylesheetsOfOwner() {
final String ownerUserAgentStylesheet = toBase64(".owner-ua { -fx-fill: red; }");
final String ownerStylesheet = toBase64(".owner { -fx-fill: green; }");
final String popupUserAgentStylesheet = toBase64(".popup-ua { -fx-fill: blue; }");
final String popupStylesheet = toBase64(".popup { -fx-fill: yellow; }");

scene.setUserAgentStylesheet(ownerUserAgentStylesheet);
scene.getStylesheets().add(ownerStylesheet);

final Popup popup = new Popup();
popup.getScene().setUserAgentStylesheet(popupUserAgentStylesheet);
popup.getScene().getStylesheets().add(popupStylesheet);

popup.show(stage);

assertEquals(popupUserAgentStylesheet, popup.getScene().getUserAgentStylesheet());
assertEquals(List.of(popupStylesheet, ownerStylesheet), popup.getScene().getStylesheets());
}

@Test
public void testShowTwiceAddsStylesheetOfOwnerOnlyOnce() {
final String ownerStylesheet = toBase64(".owner { -fx-fill: green; }");
scene.getStylesheets().add(ownerStylesheet);

final Popup popup = new Popup();

popup.show(stage);
popup.hide();
popup.show(stage);

assertEquals(List.of(ownerStylesheet), popup.getScene().getStylesheets());
}

@Test
public void testShowDoesNotAddStylesheetTwiceWhenPopupAndOwnerShareIt() {
final String sharedStylesheet = toBase64(".owner { -fx-fill: green; }");

scene.getStylesheets().addAll(sharedStylesheet);

final Popup popup = new Popup();
popup.getScene().getStylesheets().add(sharedStylesheet);

popup.show(stage);

assertEquals(List.of(sharedStylesheet), popup.getScene().getStylesheets());
}

private String toBase64(String stylesheet) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably be static...
and since we are using it more than once, maybe we can move it to a new graphics-specific Utils class?

return "data:base64," + Base64.getEncoder().encodeToString(stylesheet.getBytes(StandardCharsets.UTF_8));
}

private static final class EventCounter implements EventHandler<Event> {
private int counter;

Expand Down