Skip to content

Conversation

@mstr2
Copy link
Collaborator

@mstr2 mstr2 commented Jun 15, 2025

The window button states (disabled/hidden) of extended stages with a HeaderButtonOverlay or custom header buttons are inconsistent with what we would expect from the OS (Windows and Linux). To figure out what we would expect, I started with gathering some data. The following table shows the button states of system-decorated windows on various platforms:

Windows 11

Window attributes Iconify Maximize Close
resizable, not modal visible visible visible
not resizable, not modal visible visible, disabled visible
resizable, modal visible, disabled visible visible
not resizable, modal hidden hidden visible, utility-style

Ubuntu 24 / Fedora 41 (GNOME)

Window attributes Iconify Maximize Close
resizable, not modal visible visible visible
not resizable, not modal visible hidden visible
resizable, modal visible, not working visible, not working visible
not resizable, modal visible, not working hidden visible

Kubuntu 24 (KDE)

Window attributes Iconify Maximize Close
resizable, not modal visible visible visible
not resizable, not modal visible hidden visible
resizable, modal visible, not working visible visible
not resizable, modal visible, not working hidden visible

Observations

  1. On Windows, buttons are generally disabled when their operation is not possible with the given window attributes.
    • Exception: modal/non-resizable windows look like utility windows (iconify and maximize are hidden)
  2. On GNOME and KDE, buttons are generally hidden when their operation is not possible.
    • Exception: iconify and maximize on modal windows is not hidden, but seems to simply not do anything (bug?)

Permitted window button operations

Given the gathered observations and some simple logic, this is the table of operations that are permitted for all combinations of modal and resizable window attributes:

Window attributes Iconify Maximize Close
resizable, not modal yes yes yes
not resizable, not modal yes no yes
resizable, modal no yes yes
not resizable, modal no no yes

Fixes

This PR includes the following changes:

  1. Unused code relating to window modality is removed.
  2. The disabled states of HeaderButtonOverlay as well as HeaderButtonBehavior are changed to match the table above.
  3. The stylesheets for GNOME and KDE are changed such that disabled buttons are hidden.
  4. The stylesheet for Windows is changed such that a modal/non-resizable window looks like a utility window.

Progress

  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue
  • Change must be properly reviewed (2 reviews required, with at least 1 Reviewer, 1 Author)

Issues

  • JDK-8359601: Fix window button states of an extended stage (Bug - P4)
  • JDK-8359763: Close request handler is not called for an extended stage (Bug - P4)

Reviewers

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jfx.git pull/1831/head:pull/1831
$ git checkout pull/1831

Update a local copy of the PR:
$ git checkout pull/1831
$ git pull https://git.openjdk.org/jfx.git pull/1831/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 1831

View PR using the GUI difftool:
$ git pr show -t 1831

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jfx/pull/1831.diff

Using Webrev

Link to Webrev Comment

@mstr2
Copy link
Collaborator Author

mstr2 commented Jun 15, 2025

/reviewers 2

@bridgekeeper
Copy link

bridgekeeper bot commented Jun 15, 2025

👋 Welcome back mstrauss! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link

openjdk bot commented Jun 15, 2025

@mstr2 This change now passes all automated pre-integration checks.

ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details.

After integration, the commit message for the final commit will be:

8359601: Fix window button states of an extended stage
8359763: Close request handler is not called for an extended stage

Reviewed-by: mmack, kcr

You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed.

At the time when this comment was updated there had been 27 new commits pushed to the master branch:

As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details.

➡️ To integrate this PR with the above commit message to the master branch, type /integrate in a new comment.

@openjdk openjdk bot added the rfr Ready for review label Jun 15, 2025
@openjdk
Copy link

openjdk bot commented Jun 15, 2025

@mstr2
The total number of required reviews for this PR (including the jcheck configuration and the last /reviewers command) is now set to 2 (with at least 1 Reviewer, 1 Author).

@mlbridge
Copy link

mlbridge bot commented Jun 15, 2025

Webrevs

@credmond
Copy link

Most of this seems to be working well!

But I have found a likely bug. Hopefully fairly self-explanatory from the code.

Minimise icon is normally disabled, but still enabled when EXTENDED is used & when dialog is resizable.

Just flip comment on dialog.initStyle(StageStyle.EXTENDED) to see difference.

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.layout.StackPane;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class MinimiseIconEnabledBug extends Application {
    @Override
    public void start(Stage primaryStage) {
        Button button = new Button("Click");
        button.setOnAction(e -> {
            final Dialog<Object> dialog = new Dialog<>();
            dialog.initOwner(primaryStage);
            // Existing behaviour: minimise icon disabled when commented
            // Possible "bug": Minimise icon enabled when uncommented
            // dialog.initStyle(StageStyle.EXTENDED);
            dialog.initModality(Modality.NONE);
            dialog.setResizable(true); // This is important
            dialog.setTitle("My Dialog");
            dialog.setContentText("Lorem ipsum dolor sit amet, consectetur adipiscing...");
            dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK);
            dialog.show();
        });

        StackPane root = new StackPane(button);
        Scene scene = new Scene(root, 300, 200);

        primaryStage.setTitle("My First JavaFX App");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

@credmond
Copy link

Another suspected issue (not really related to this PR), close request handlers are not called when using EXTENDED. E.g.:

        stage.setOnCloseRequest(event -> {
            System.out.println("Never called...");;
        });

Same for dialogs...

@mstr2
Copy link
Collaborator Author

mstr2 commented Jun 17, 2025

Good catch, a non-modal owned window also can't be iconified. I've added code and tests for this scenario.

@credmond
Copy link

Another suspected issue (not really related to this PR), close request handlers are not called when using EXTENDED. E.g.:

        stage.setOnCloseRequest(event -> {
            System.out.println("Never called...");;
        });

Same for dialogs...

Sample:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.layout.StackPane;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class MinimiseIconEnabledBug extends Application {
    @Override
    public void start(Stage primaryStage) {
        Button button = new Button("Click");
        button.setOnAction(e -> {
            final Dialog<Object> dialog = new Dialog<>();
            dialog.initOwner(primaryStage);
            // dialog.initStyle(StageStyle.EXTENDED); // uncomment for bug
            dialog.initModality(Modality.NONE);
            dialog.setResizable(true); // This is important
            dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK);
            dialog.setOnCloseRequest(dialogEvent -> {
                System.out.println("Close the dialog pane. This is not called when EXTENDED is used.");
            });
            dialog.show();
        });

        StackPane root = new StackPane(button);
        Scene scene = new Scene(root, 300, 200);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

@mstr2
Copy link
Collaborator Author

mstr2 commented Jun 17, 2025

I've filed another ticket for that: JDK-8359763.

@credmond
Copy link

Good catch, a non-modal owned window also can't be iconified. I've added code and tests for this scenario.

Looks good, can't find anymore issues...

@tsayao
Copy link
Collaborator

tsayao commented Jun 19, 2025

JavaFX modal windows are not native modals, as evidenced by the code removal on this PR (it's not used) . On GNOME, native modal windows automatically remove the minimize and maximize buttons.

image

I did an experiment and found that it's possible to get native modal windows (at least with GTK Glass) by passing the modality flag from WindowStage to createWindow. I haven’t done extensive testing, but it seems to work - likely because the modality can’t be changed afterward.

@mstr2
Copy link
Collaborator Author

mstr2 commented Jun 19, 2025

JavaFX modal windows are not native modals, as evidenced by the code removal on this PR (it's not used) . On GNOME, native modal windows automatically remove the minimize and maximize buttons.

I did an experiment and found that it's possible to get native modal windows (at least with GTK Glass) by passing the modality flag from WindowStage to createWindow. I haven’t done extensive testing, but it seems to work - likely because the modality can’t be changed afterward.

That's a good idea for an enhancement.

Copy link
Contributor

@drmarmac drmarmac left a comment

Choose a reason for hiding this comment

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

The updated behavior looks good on windows 11, I tested various combinations of modal/owned/resizable/StageStyle settings, including modifying the resizeable bit when a stage is already showing.

I also checked the behavior in macOS Sequoia 15.5. There, a window where setResizable(false) is called before showing the stage, unexpectedly has a working maximize button even without StageStyle.EXTENDED.
Looks like this was not the case prior to the integration of #1605, so this behavior was probably introduced there and may need a fix in this PR or elsewhere.

I don't see any code using the java and native enterModal/exitModal methods which were removed, so this looks like a safe change.
Finally, I left some comments regarding potentially confusing definitions of modal, otherwise the code looks good.

private Node buttonAtMouseDown;

public HeaderButtonOverlay(ObservableValue<String> stylesheet, boolean utility, boolean rightToLeft) {
public HeaderButtonOverlay(ObservableValue<String> stylesheet, boolean modal,
Copy link
Contributor

Choose a reason for hiding this comment

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

it's a bit confusing to have isModal in Window and getModality in Stage on one hand, and a modal field / parameter here, which is set to isModal() || getOwner() != null by callers. Maybe clarify by renaming to something like effecitvelyModal or modalOrOwned?

if (!node.disableProperty().isBound()) {
node.setDisable(resizable == Boolean.FALSE);
boolean utility = stage.getStyle() == StageStyle.UTILITY;
boolean modal = stage.getOwner() != null || stage.getModality() != Modality.NONE;
Copy link
Contributor

Choose a reason for hiding this comment

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

in case HeaderButtonOverlay.modal is renamed, this variable could be renamed in the same way.

}

final boolean resizable;
final boolean modal;
Copy link
Contributor

Choose a reason for hiding this comment

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

same here

@credmond
Copy link

I've filed another ticket for that: JDK-8359763.

Just curious, why a separate bug for this instead of fixing in this PR and avoiding the headaches/conversations of more PRs -- is it more complicated that it sounds?

@mstr2
Copy link
Collaborator Author

mstr2 commented Jun 21, 2025

I've filed another ticket for that: JDK-8359763.

Just curious, why a separate bug for this instead of fixing in this PR and avoiding the headaches/conversations of more PRs -- is it more complicated that it sounds?

Shorter PRs tend to get more timely reviews. That being said, since it is closely related, I've added the fix to this PR.

/issue add JDK-8359763

@openjdk
Copy link

openjdk bot commented Jun 21, 2025

@mstr2
Adding additional issue to issue list: 8359763: Close request handler is not called for an extended stage.

@mstr2
Copy link
Collaborator Author

mstr2 commented Jun 21, 2025

I also checked the behavior in macOS Sequoia 15.5. There, a window where setResizable(false) is called before showing the stage, unexpectedly has a working maximize button even without StageStyle.EXTENDED. Looks like this was not the case prior to the integration of #1605, so this behavior was probably introduced there and may need a fix in this PR or elsewhere.

The implementation relied on the _setResizable JNI method being called in a very particular way that has since changed (internally, it would call another method with the same name, but the implementation would actually toggle the resizable state, not set it to a given value). I've fixed this problem.

Copy link
Contributor

@drmarmac drmarmac left a comment

Choose a reason for hiding this comment

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

Changes look good, all combinations that I've tested manually seem to work fine now. I also tested with gnome & KDE this time.

@credmond
Copy link

credmond commented Jun 24, 2025

I have found another bug/issue, which is again related to wrapping a root node in a StackPane (something that is common in, for example, ControlsFx). Although the observed behaviour is probably strictly logically correct, I think this should be prevented (e.g, HeaderBar has/gets special meaning/treatment).

public class DisappearingHeaderBarNodesBug extends Application {

    @Override
    public void start(Stage primaryStage) {
        BorderPane root = new BorderPane();
        Button button = new Button("Click for \"bug()\"!");
        button.setOnAction(_ -> bug(root));
        root.setCenter(new VBox(new Label("Something above"), button, new Label("Something below")));
        root.setTop(getHeaderBar());

        primaryStage.initStyle(StageStyle.EXTENDED);
        primaryStage.setScene(new Scene(root, 300, 200));
        primaryStage.show();
    }

    // Wrapping the root node in a stackpane is common
    private void bug(Pane root) {
        StackPane stackPane = new StackPane();
        root.getScene().setRoot(stackPane);
        stackPane.getChildren().addFirst(root);
    }

    private HeaderBar getHeaderBar() {
        HeaderBar headerBar = new HeaderBar();
        headerBar.setCenter(new Label("!!! HeaderBar !!!"));
        headerBar.setLeading(new Label("L"));
        return headerBar;
    }

    public static void main(String[] args) {
        launch(args);
    }
}

As you can see, after you click the button, and resize, the HeaderBar can disappear completely:

bug.mp4

@mstr2
Copy link
Collaborator Author

mstr2 commented Jun 24, 2025

@credmond We should keep this PR focused, let's use the mailing list or JBS to discuss other issues.

@credmond
Copy link

credmond commented Jul 7, 2025

FYI to any reviewer: I've been using this branch's EXTENDED/HeaderBar extensively now without any new issues...

@mstr2
Copy link
Collaborator Author

mstr2 commented Jul 22, 2025

I think this fix should make the 25 release if possible, as it would be unfortunate to preview the new feature with distracting bugs included. The fix is also quite well-tested at this point.

@kevinrushforth @andy-goryachev-oracle Do you have any review capacity to spare for this within the current RDP?

@credmond
Copy link

I think this fix should make the 25 release if possible, as it would be unfortunate to preview the new feature with distracting bugs included. The fix is also quite well-tested at this point.

@kevinrushforth @andy-goryachev-oracle Do you have any review capacity to spare for this within the current RDP?

Agreed. It wouldn't take long until others come across this and raise the same issue.

@kevinrushforth
Copy link
Member

I think this fix should make the 25 release if possible, as it would be unfortunate to preview the new feature with distracting bugs included. The fix is also quite well-tested at this point.

@kevinrushforth @andy-goryachev-oracle Do you have any review capacity to spare for this within the current RDP?

Yes, I'll review it this week. It seems a good candidate for backporting to jfx25.

}

if (type == HeaderButtonType.MAXIMIZE) {
subscription = Subscription.combine(subscription,
Copy link
Contributor

@andy-goryachev-oracle andy-goryachev-oracle Jul 23, 2025

Choose a reason for hiding this comment

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

here and on L66 the subscription pointer (set in L61) gets overwritten. does it mean some subscription(s) won't be cancelled in dispose()?

edit: please disregard, I see it's combining subscriptions.

@kevinrushforth kevinrushforth self-requested a review July 24, 2025 19:21
Copy link
Member

@kevinrushforth kevinrushforth left a comment

Choose a reason for hiding this comment

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

The code looks good. I submitted a set of headful test jobs and will report when done. I want to do a little light manual testing, especially on macOS where there are a few changes in the non-extended case (although the code looks equivalent).

}

- (void)_setResizable
- (void)_setResizable:(bool)resizable
Copy link
Member

Choose a reason for hiding this comment

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

Good. It definitely seems cleaner to pass in resizable as a parameter rather than having this method be a "toggle" resizable as it was previously.

{
[window performSelectorOnMainThread:@selector(_setResizable) withObject:nil waitUntilDone:YES];
}
[window _setResizable:jResizable];
Copy link
Member

Choose a reason for hiding this comment

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

I presume you removed the performSelectorOnMainThread because the calling method always calls this on the main thread?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, this is a call that always happens on the FX thread as a result of calling javafx.stage.Window.show(). This is verified just a few lines earlier with GLASS_ASSERT_MAIN_JAVA_THREAD (that's always been there, I didn't add it).

Copy link
Member

Choose a reason for hiding this comment

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

Also, the calling Java code does a thread check. So we're good.

@kevinrushforth
Copy link
Member

The headful CI tests passed on Linux and macOS (I will need to manually run it on Windows).

@kevinrushforth
Copy link
Member

Headful tests passed on Windows.

@openjdk openjdk bot added the ready Ready to be integrated label Jul 25, 2025
@mstr2
Copy link
Collaborator Author

mstr2 commented Jul 25, 2025

/integrate

@openjdk
Copy link

openjdk bot commented Jul 25, 2025

Going to push as commit bc433da.
Since your change was applied there have been 27 commits pushed to the master branch:

Your commit was automatically rebased without conflicts.

@openjdk openjdk bot added the integrated Pull request has been integrated label Jul 25, 2025
@openjdk openjdk bot closed this Jul 25, 2025
@openjdk openjdk bot removed ready Ready to be integrated rfr Ready for review labels Jul 25, 2025
@openjdk
Copy link

openjdk bot commented Jul 25, 2025

@mstr2 Pushed as commit bc433da.

💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored.

@mstr2
Copy link
Collaborator Author

mstr2 commented Jul 25, 2025

/backport jfx jfx25

@openjdk
Copy link

openjdk bot commented Jul 25, 2025

@mstr2 the backport was successfully created on the branch backport-mstr2-bc433da8-jfx25 in my personal fork of openjdk/jfx. To create a pull request with this backport targeting openjdk/jfx:jfx25, just click the following link:

➡️ Create pull request

The title of the pull request is automatically filled in correctly and below you find a suggestion for the pull request body:

Hi all,

This pull request contains a backport of commit bc433da8 from the openjdk/jfx repository.

The commit being backported was authored by Michael Strauß on 25 Jul 2025 and was reviewed by Markus Mack and Kevin Rushforth.

Thanks!

If you need to update the source branch of the pull then run the following commands in a local clone of your personal fork of openjdk/jfx:

$ git fetch https://github.com/openjdk-bots/jfx.git backport-mstr2-bc433da8-jfx25:backport-mstr2-bc433da8-jfx25
$ git checkout backport-mstr2-bc433da8-jfx25
# make changes
$ git add paths/to/changed/files
$ git commit --message 'Describe additional changes made'
$ git push https://github.com/openjdk-bots/jfx.git backport-mstr2-bc433da8-jfx25

@mstr2 mstr2 deleted the fixes/modal-window branch July 26, 2025 01:53
@kapilkumar9976
Copy link

kapilkumar9976 commented Sep 20, 2025

Hi @kapilkumar9976, thanks for making a comment in an OpenJDK project!

All comments and discussions in the OpenJDK Community must be made available under the OpenJDK Terms of Use. If you already are an OpenJDK Author, Committer or Reviewer, please click here to open a new issue so that we can record that fact. Please Use "Add GitHub user kapilkumar9976" for the summary.

If you are not an OpenJDK Author, Committer or Reviewer, simply check the box below to accept the OpenJDK Terms of Use for your comments.

Your comment will be automatically restored once you have accepted the OpenJDK Terms of Use.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integrated Pull request has been integrated

Development

Successfully merging this pull request may close these issues.

7 participants