Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8222210

JFXPanel popups open at wrong coordinates when using multiple hidpi monitors

    Details

    • Subcomponent:
    • CPU:
      x86_64
    • OS:
      windows_10

      Description

      ADDITIONAL SYSTEM INFORMATION :
      Multiple monitors run at different Windows scalings.

      A DESCRIPTION OF THE PROBLEM :
      JFXPanel communicates Swing coordinates to JavaFX, e.g. in updateScreenLocation() or sendMouseEventToFX(). Unfortunately, the coordinate systems seems to differ when using multiple monitors with different scalings. This is usually not a big deal unless JavaFX needs to paint something on its own, like popup windows. Those can appear at completely wrong locations, potentially on different monitor.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Configure the main monitor (located at (0,0)) to run at 100% (without scaling).
      Configure the second monitor to be located at the right of the main monitor and run at 125%, 150% or more.

      Start the JfxPanelCoordinates program.
      Click the combo box at the top.
      Right-click the button at the bottom.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      Both popups appear at the correct location.
      ACTUAL -
      The popups appear misplaced, i.e. the popup is too much to the left. The wider the main monitor, the bigger the misplacement.

      ---------- BEGIN SOURCE ----------
      import java.util.stream.Collectors;
      import java.util.stream.IntStream;

      import javax.swing.JFrame;
      import javax.swing.SwingUtilities;
      import javax.swing.WindowConstants;

      import javafx.application.Platform;
      import javafx.embed.swing.JFXPanel;
      import javafx.geometry.Insets;
      import javafx.scene.Scene;
      import javafx.scene.control.Button;
      import javafx.scene.control.ComboBox;
      import javafx.scene.control.ContextMenu;
      import javafx.scene.control.MenuItem;
      import javafx.scene.layout.VBox;
      import javafx.scene.paint.Color;

      public class JfxPanelCoordinates
      {
         public static void main(String[] args)
         {
            SwingUtilities.invokeLater(() -> initAndShowGUI());
         }

         private static void initAndShowGUI()
         {
            JFrame frame = new JFrame("Swing and JavaFX");
            final JFXPanel fxPanel = new JFXPanel();
            frame.add(fxPanel);
            frame.setSize(500, 500);
            frame.setVisible(true);
            frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

            Platform.runLater(() -> initFX(fxPanel));
         }

         private static void initFX(JFXPanel fxPanel)
         {
            Scene scene = createScene();
            fxPanel.setScene(scene);
         }

         private static Scene createScene()
         {
            VBox root = new VBox(20);
            root.setPadding(new Insets(10));
            root.setFillWidth(true);

            ComboBox<String> comboBox = new ComboBox<>();
            comboBox.setMaxWidth(Integer.MAX_VALUE);
            comboBox.getItems().addAll(IntStream.rangeClosed(1, 10).mapToObj(x -> "Option " + x).collect(Collectors.toList()));
            comboBox.getSelectionModel().selectFirst();

            Button button = new Button("Right-click me");
            button.setMaxWidth(Integer.MAX_VALUE);
            ContextMenu contextMenu = new ContextMenu();
            button.setContextMenu(contextMenu);
            contextMenu.getItems().addAll(IntStream.rangeClosed(1, 10).mapToObj(x -> new MenuItem("Menu Item " + x)).collect(Collectors.toList()));

            root.getChildren().addAll(comboBox, button);

            return new Scene(root, Color.ALICEBLUE);
         }
      }

      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      Copy and patched JFXPanel.

      private Map<GraphicsConfiguration, Dimension2D> swingToFxPixelOffsets = new WeakHashMap<>();

         /**
          * Determines an offset that needs to be applied to swing pixel coordinates
          * when passed to JavaFX. On scaled monitors with multiple resolutions, Swing
          * and JavaFX differ in how they organise their logical pixels:
          *
          * Swing: All coordinates and widths can be multiplied by the monitor scales,
          * i.e. monitors can look as if they overlap
          * JavaFX: Logical views are laid out next to each other, so they never overlap.
          */
         private Dimension2D getSwingToFxPixelOffset(GraphicsConfiguration graphicsConfiguration)
         {
            Dimension2D offset = swingToFxPixelOffsets.get(graphicsConfiguration);
            if (offset == null)
            {
               Screen screen = findScreen(graphicsConfiguration);
               if (screen != null)
               {
                  offset = new Dimension2D(screen.getX() - graphicsConfiguration.getBounds().getX(),
                                           screen.getY() - graphicsConfiguration.getBounds().getY());
               }
               else
               {
                  offset = new Dimension2D(0.0d, 0.0d);
               }
               swingToFxPixelOffsets.put(graphicsConfiguration, offset);
            }
            return offset;
         }

         private Screen findScreen(GraphicsConfiguration graphicsConfiguration)
         {
            Rectangle awtBounds = graphicsConfiguration.getBounds();
            AffineTransform awtScales = graphicsConfiguration.getDefaultTransform();
            for (Screen screen : Screen.getScreens())
            {
               if ((Math.abs(screen.getPlatformX() - awtBounds.getX() * awtScales.getScaleX()) < 0.001) &&
                   (Math.abs(screen.getPlatformY() - awtBounds.getY() * awtScales.getScaleY()) < 0.001) &&
                   (Math.abs(screen.getWidth() - awtBounds.getWidth()) < 0.001) &&
                   (Math.abs(screen.getHeight() - awtBounds.getHeight()) < 0.001))
               {
                  return screen;
               }
            }
            return null;
         }

         private boolean updateScreenLocation()
         {
            synchronized (getTreeLock())
            {
               if (isShowing())
               {
                  Point p = getLocationOnScreen();
                  Dimension2D offset = getSwingToFxPixelOffset(GUIHelper.getGraphicsConfiguration(this, false));
                  screenX = (int) (p.x + offset.getWidth());
                  screenY = (int) (p.y + offset.getHeight());
                  return true;
               }
            }
            return false;
         }

      In sendMouseEventToFX():
            Dimension2D screenOffset = getSwingToFxPixelOffset(getGraphicsConfiguration());
            int xOnScreen = (int) (e.getXOnScreen() + screenOffset.getWidth());
            int yOnScreen = (int) (e.getYOnScreen() + screenOffset.getHeight());
      Use these instead of e.getXOnScreen() and e.getYOnScreen().

      FREQUENCY : always


        Attachments

          Activity

            People

            • Assignee:
              psadhukhan Prasanta Sadhukhan
              Reporter:
              webbuggrp Webbug Group
            • Votes:
              0 Vote for this issue
              Watchers:
              2 Start watching this issue

              Dates

              • Created:
                Updated: