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

Window content is shrunk on rescaling after moving to 125% DPI screen

    Details

    • Type: Bug
    • Status: Open
    • Priority: P4
    • Resolution: Unresolved
    • Affects Version/s: openjfx11
    • Fix Version/s: tbd
    • Component/s: javafx
    • Labels:
    • Subcomponent:
    • CPU:
      x86_64
    • OS:
      windows_10

      Description

      ADDITIONAL SYSTEM INFORMATION :
      Microsoft Windows [Version 10.0.17134.765]
      openjdk version "11.0.3" 2019-04-16
      OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.3+7)
      OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.3+7, mixed mode)

      A DESCRIPTION OF THE PROBLEM :
      Window content is shrunk on rescaling after moving to 125% DPI screen. This happens due to window's border change by the system on moving it to screen with higher DPI setting - border becomes bigger, so there's less space for content inside window, and it becomes shrunk. Due to that, ScrollPane's scroll bar is being shown, which should not happen.

      I have observed that window can be in intermediate state when it's not rescaled by JavaFX, but rescaled by Window: border is bigger, but content stays the same as on 100% DPI screen - due to WinWindow#notifyMoving. In that state it can be easily observed that content is shrunk by few pixels, when border (including title bar) becomes bigger.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Preconditions:
      Two screens environment:
      - left side monitor 100% DPI,
      - right side monitor 125% DPI.
      Application starts on left monitor by default.

      Test case:
      1. Startup provided test case application.
      2. Move window to the center of second monitor with 125% DPI.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      After 2. content should look the same as on 1st monitor - no scroll bar visible.
      ACTUAL -
      After 2. Scroll bar is visible, because content is shrunk by bigger window's border.

      ---------- BEGIN SOURCE ----------
      package com.example;

      import javafx.application.Application;
      import javafx.geometry.Insets;
      import javafx.scene.Scene;
      import javafx.scene.control.Label;
      import javafx.scene.control.ScrollPane;
      import javafx.scene.layout.Background;
      import javafx.scene.layout.BackgroundFill;
      import javafx.scene.layout.CornerRadii;
      import javafx.scene.layout.Priority;
      import javafx.scene.layout.VBox;
      import javafx.scene.paint.Color;
      import javafx.stage.Stage;

      public class HiDpiIssueSample
      {

          public static void main( String[] args )
          {
              System.err.println( Runtime.version().toString() );
              Application.launch( MainFx.class, args );
          }

          public static class MainFx extends Application
          {

              private static final int LABEL_SIZE = 250;

              @Override
              public void start( final Stage primaryStage ) throws Exception
              {
                  final var labelSp = createLabel();
                  labelSp.setBackground(
                      new Background( new BackgroundFill( Color.RED, CornerRadii.EMPTY, Insets.EMPTY ) ) );
                  final var sp = new ScrollPane( labelSp );
                  sp.setFitToHeight( true );
                  sp.setFitToWidth( true );
                  final var labelTop = createLabel();
                  labelTop.setBackground(
                      new Background( new BackgroundFill( Color.GREEN, CornerRadii.EMPTY, Insets.EMPTY ) ) );
                  final VBox vBox = new VBox( labelTop, sp );
                  VBox.setVgrow( labelTop, Priority.ALWAYS );
                  VBox.setVgrow( sp, Priority.ALWAYS );
                  final Scene aScene = new Scene( vBox );
                  primaryStage.setScene( aScene );
                  primaryStage.sizeToScene();
                  primaryStage.show();
              }

              private Label createLabel()
              {
                  return new Label( LABEL_SIZE + "px square min/pref" )
                  {

                      @Override
                      protected double computeMinWidth( final double height )
                      {
                          return snapSizeX( LABEL_SIZE );
                      }

                      @Override
                      protected double computeMinHeight( final double width )
                      {
                          return snapSizeY( LABEL_SIZE );
                      }

                      @Override
                      protected double computePrefHeight( final double width )
                      {
                          return snapSizeY( LABEL_SIZE );
                      }

                      @Override
                      protected double computePrefWidth( final double height )
                      {
                          return snapSizeX( LABEL_SIZE );
                      }
                  };
              }

          }
      }
      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :

      package de.psi.pjf.wsm.app.util;

      import java.util.Objects;
      import java.util.Optional;

      import javafx.beans.value.ChangeListener;
      import javafx.collections.ListChangeListener;
      import javafx.stage.Stage;
      import javafx.stage.Window;

      public final class FxWindowHiDpiWorkaround
      {
          private static final String FX_HIDPI_WORKAROUND_KEY = "FxWindowHiDpiWorkaround";
          private static final int WINDOW_INSETS_TOP = 31;
          private static final int WINDOW_INSETS_LEFT = 8;
          private static final int WINDOW_INSETS_BOTTOM = 8;
          private static final int WINDOW_INSETS_RIGHT = 8;
          private Window window;
          private final ChangeListener< Number > outputScaleXListener =
              ( observable, oldValue, newValue ) -> updateRendererScaleX( oldValue, newValue );
          private final ChangeListener< Number > outputScaleYListener =
              ( observable, oldValue, newValue ) -> updateRendererScaleY( oldValue, newValue );

          private FxWindowHiDpiWorkaround( final Window aWindow )
          {
              window = Objects.requireNonNull( aWindow );
          }

          private void uninstall()
          {
              window.outputScaleXProperty().removeListener( outputScaleXListener );
              window.outputScaleYProperty().removeListener( outputScaleYListener );
              window = null;
          }

          private void installListeners()
          {
              window.outputScaleXProperty().addListener( outputScaleXListener );
              window.outputScaleYProperty().addListener( outputScaleYListener );
          }

          private void updateRendererScaleY( final Number aOldValue, final Number aNewValue )
          {
              if( aOldValue == null || aNewValue == null )
              {
                  return;
              }
              if( window instanceof Stage && ( (Stage)window ).isMaximized() )
              {
                  return;
              }
              final double height = window.getHeight();
              if( isValidSize( height ) )
              {
                  updateRendererScaleYImpl( aOldValue, aNewValue, height );
              }
          }

          private void updateRendererScaleYImpl( final Number aOldValue, final Number aNewValue,
              final double aHeight )
          {
              final int topBotInsets = WINDOW_INSETS_TOP + WINDOW_INSETS_BOTTOM;
              final double previous = Math.ceil( topBotInsets * aOldValue.doubleValue() );
              final double current = Math.ceil( topBotInsets * aNewValue.doubleValue() );
              final double deltaY = current - previous;
              window.setHeight( Math.ceil( aHeight ) + deltaY );
          }

          private void updateRendererScaleX( final Number aOldValue, final Number aNewValue )
          {
              if( aOldValue == null || aNewValue == null )
              {
                  return;
              }
              if( window instanceof Stage && ( (Stage)window ).isMaximized() )
              {
                  return;
              }
              final double width = window.getWidth();
              if( isValidSize( width ) )
              {
                  updateRendererScaleXImpl( aOldValue, aNewValue, width );
              }
          }

          /**
           * If window is showing directly on HiDPI panel, it <code>aSize</code> is {@link Double#NaN}.
           */
          private boolean isValidSize( final double aSize )
          {
              return aSize > 0 && !Double.valueOf( aSize ).isNaN();
          }

          private void updateRendererScaleXImpl( final Number aOldValue, final Number aNewValue,
              final double aWidth )
          {
              final int leftRightInsets = WINDOW_INSETS_LEFT + WINDOW_INSETS_RIGHT;
              final double previous = Math.ceil( leftRightInsets * aOldValue.doubleValue() );
              final double current = Math.ceil( leftRightInsets * aNewValue.doubleValue() );
              final double deltaX = current - previous;
              window.setWidth( Math.ceil( aWidth ) + deltaX );
          }

          public static void install()
          {
              Window.getWindows().addListener( (ListChangeListener< ? super Window >)change -> {
                  while( change.next() )
                  {
                      change.getAddedSubList().forEach( FxWindowHiDpiWorkaround::installWindow );
                      change.getRemoved().forEach( FxWindowHiDpiWorkaround::uninstallWindow );
                  }
              } );
          }

          private static void uninstallWindow( final Window aWindow )
          {
              final FxWindowHiDpiWorkaround fxWindowHiDpiWorkaround =
                  (FxWindowHiDpiWorkaround)aWindow.getProperties().remove( FX_HIDPI_WORKAROUND_KEY );
              if( fxWindowHiDpiWorkaround == null )
              {
                  return;
              }
              fxWindowHiDpiWorkaround.uninstall();
          }

          private static void installWindow( final Window aWindow )
          {
              if( aWindow == null )
              {
                  return;
              }
              if( getFxHiDpiWorkaround( aWindow ).isPresent() )
              {
                  return;
              }
              final FxWindowHiDpiWorkaround fxWindowHiDpiWorkaround = new FxWindowHiDpiWorkaround( aWindow );
              fxWindowHiDpiWorkaround.installListeners();
              aWindow.getProperties().put( FX_HIDPI_WORKAROUND_KEY, fxWindowHiDpiWorkaround );
          }

          private static Optional< FxWindowHiDpiWorkaround > getFxHiDpiWorkaround( final Window aWindow )
          {
              if( aWindow == null )
              {
                  return Optional.empty();
              }
              return Optional
                  .ofNullable( (FxWindowHiDpiWorkaround)aWindow.getProperties().get( FX_HIDPI_WORKAROUND_KEY ) );
          }
      }

      FREQUENCY : always


        Attachments

          Issue Links

            Activity

              People

              • Assignee:
                kcr Kevin Rushforth
                Reporter:
                webbuggrp Webbug Group
              • Votes:
                0 Vote for this issue
                Watchers:
                2 Start watching this issue

                Dates

                • Created:
                  Updated: