javalinuxswingjavafx

Displaying a JavaFX Popup on a Swing app in cross-platform manner


Before all of the commenters start saying "Just don't mix Swing and JavaFX", yeah I know, I know. It has caused us a lot of problems over the years but my team has a ton of swing code and we really don't have budget to re-write it all in JavaFX. We're trying to gradually transition portions of our code to JavaFX over time.

Years ago I wrote a nice JavaFX utility that I use in several different applications that shows a little popup warnings in the bottom right corner of an application. It has a lot of cool features, which are pretty JavaFX dependent.

I also wrote some code that allowed our team to use these warnings in Swing applications. It does this by making an invisible JavaFX stage to own the popups (since popups can't display without an owner window).

I've been happy with the code for years, but only recently tried running it on Linux for the first time. It turns out that on Linux, my invisible stage is not invisible and I can't find any way to make it go away. I guess it's not the end of the world if there's a little empty stage, but it's pretty annoying and I'd like to get rid of it. I've tested on Ubuntu with both LXDE and Gnome.

The full code for my warning popups is more than I'd like to post here, but here's a minimum reproducible example of displaying a JavaFX popup in a Swing app. It works fine in Windows, but in Linux, the dummy stage is visible and has its own icon in the task bar.

import java.awt.Dimension;
import java.lang.reflect.InvocationTargetException;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.stage.Popup;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class FxOnSwingProblemDemo extends JFrame {

    private Stage dummyStage;
    
    public FxOnSwingProblemDemo() {
        
        initializeFxToolkit();
        
        JButton button = new JButton("Show popup");
        
        button.addActionListener(e -> 
            Platform.runLater(() -> {
                Popup popup = new Popup();
                popup.getContent().add(new Label("Hey look, a popup!"));
                popup.show(getDummyStage());
            })
        );
        
        JPanel root = new JPanel();
        root.add(button);
        
        this.add(root);
        this.setPreferredSize(new Dimension(500, 500));
        this.pack();
        //Note exit on close is important here to kill the FX Platform thread
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);
        this.setVisible(true);
    }
    
    private void initializeFxToolkit() {
        Platform.setImplicitExit(false);
        try {
            SwingUtilities.invokeAndWait(JFXPanel::new);
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    
    private Stage getDummyStage() {
        if(dummyStage == null) {

          //Utility prevents icon from showing up on windows task bar
          dummyStage = new Stage(StageStyle.UTILITY);
          dummyStage.setScene(new Scene(new Pane()));
          //Note, setting the width and height to very small numbers (so far tested with 0.1, 1, and 2)
          //will result in GTK warnings that can't be suppressed printing to stderr on at least
          //some linux environments. So we'll set it to a larger size. It really doesn't matter
          //what size we pick as long as it is large enough to avoid the GTK warnings.
          dummyStage.setWidth(100);
          dummyStage.setHeight(100);
          //Make it invisible
          dummyStage.setOpacity(0.0);
          //Don't allow closing
          dummyStage.setOnCloseRequest(e -> e.consume());
          dummyStage.show();
        }
        
        return dummyStage;
    }

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

I tried everything I can think of to make the window invisible, but haven't found a way yet. I also tried seeing if there's a way I can display a Popup without a real window by making my own subclass of Window that doesn't really display anything, but I haven't gotten very far since a lot Window's methods are final. Open to any ideas. If I have to just write a swing version of the warnings, then I guess I will, but I'd like to avoid it if I can.


Solution

  • The comments from VGR and Slaw were both very helpful so I'm going to expand on them here for anyone else who comes looking at this question.

    Adding a JFXPanel to the Swing GlassPane

    This was based on VGR's suggestion and it is what I ended up doing. This could be considered a workaround more than an answer to the actual question in the title since it does not actually display a popup, but in my particular case, this was actually the better solution.

    Basically, my implementation boiled down to:

    Using a Node instead of a Stage

    This isn't the approach I used but I did test it out just to see. Slaw suggested that I try using Popup.show(Node, double, double) instead of Popup.show(Window). The documentation says that this method will throw an IllegalArgumentException if the specified Node is not associated with a Window but I suppose the interpretation of "Window" there is up to some interpretation. I found that if I created a Node and didn't add it to anything, then I would get an IllegalArgumentException. However, if I added that node to a JFXPanel and added that panel somewhere in my Swing app, then the Popup.show(Node, double, double) method works just fine. I don't know if that's guaranteed to work in every version of JavaFX with every JRE, but in my case it seemed to work fine.