JDK-8199441 : Wrong caret position in multiline text components on Windows with a screen resolution higher than 100%
  • Type: Bug
  • Component: client-libs
  • Sub-Component: javax.swing
  • Affected Version: 9,10,11
  • Priority: P2
  • Status: Resolved
  • Resolution: Fixed
  • OS: other
  • CPU: x86_64
  • Submitted: 2018-03-09
  • Updated: 2021-06-16
  • Resolved: 2018-06-15
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 11
11 b19Fixed
Related Reports
Relates :  
Relates :  
Description
FULL PRODUCT VERSION :
java version "9.0.4"
Java(TM) SE Runtime Environment (build 9.0.4+11)
Java HotSpot(TM) 64-Bit Server VM (build 9.0.4+11, mixed mode)

ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows [Version 10.0.16299.251]

A DESCRIPTION OF THE PROBLEM :
Happens on Windows only with Java 9. 

With a screen resolution higher than 100% and then clicking in a JTextArea having setLineWrap(true) set, the caret (insertion point) is not aligned with the cursor. 

REGRESSION.  Last worked in version 8u161

ADDITIONAL REGRESSION INFORMATION: 
java version "1.8.0_161"
Java(TM) SE Runtime Environment (build 1.8.0_161-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.161-b12, mixed mode)

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1) Run Windows
2) Set the screen resolution to 150%
3) Start the provided Java program
4) It shows a JTextArea pre-filled with text
5) Click at random places in the text and the caret is inserted find at the cursor position
6) Now check "Line Wrap" and repeat the previous step and you will see that the caret is not positioned at the cursor position

Further, the demo program shows a combobox with all available fonts. Almost all of them gives the same result, except for example the Monospaced font. It works fine with it even with Line Wrap checked.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The caret should always be inserted where the mouse is clicked.
ACTUAL -
The caret is not in the same position as where the cursor is clicked.

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import java.awt.*;
import java.awt.event.ItemListener;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import javax.swing.*;

/**
 * Test to show that the caret when left-click doesn't align with
 * the mouse pointer position on Windows with Java 9 and screen resolution > 100%.
 *
 * Using a plain JTextArea without any settings it works fine. Click "Line Wrap"
 * to stress the error as the caret (insertion point) is then a few characters
 * behind the cursor position.
 */
public class TestCaretJava9 {

   private TestCaretJava9() {
      JFrame f = new JFrame("Test Cursor/Caret with Java 9");
      f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

      JCheckBox wordWrap = new JCheckBox("Word Wrap");
      JCheckBox lineWrap = new JCheckBox("Line Wrap");

      JTextArea textArea = new JTextArea(35, 80);

      JComboBox<Font> fontCombo = new JComboBox<>();
      fontCombo.setMaximumRowCount(20);
      fontCombo.addItemListener(e -> {
         Font font = (Font) e.getItem();
         textArea.setFont(font);
      });
      fontCombo.addItem(textArea.getFont());
      fontCombo.addItem(new Font("Monospaced", Font.PLAIN, 12));
      List<Font> fonts = getFonts();
      for (Font font : fonts) {
         fontCombo.addItem(font);
      }

      ItemListener checkBoxListener = e -> {
         textArea.setWrapStyleWord(wordWrap.isSelected());
         textArea.setLineWrap(lineWrap.isSelected());
      };
      wordWrap.addItemListener(checkBoxListener);
      lineWrap.addItemListener(checkBoxListener);

      fillTextArea(textArea);

      JPanel toolbar = new JPanel();
      toolbar.add(wordWrap);
      toolbar.add(lineWrap);
      toolbar.add(fontCombo);

      f.add(toolbar, BorderLayout.NORTH);
      f.add(new JScrollPane(textArea), BorderLayout.CENTER);

      f.pack();
      f.setVisible(true);
   }

   private void fillTextArea(JTextArea area) {
      StringBuilder buf = new StringBuilder();
      addSystemProperties(buf);

      for (int i = 0; i < 30; i++) {
         StringBuilder row = new StringBuilder();
         for (int j = 0; j < 50; j++) {
            row.append(j);
            if (j % 5 == 0) {
               row.append(" ");
            }
         }
         buf.append(row).append(System.lineSeparator());
      }
      area.setText(buf.toString());
      area.setCaretPosition(0);
   }

   private void addSystemProperties(StringBuilder buf) {
      buf.append("os.name:        ").append(System.getProperty("os.name")).append(System.lineSeparator());
      buf.append("os.version:     ").append(System.getProperty("os.version")).append(System.lineSeparator());
      buf.append("os.arch:        ").append(System.getProperty("os.arch")).append(System.lineSeparator());
      buf.append("java.version:   ").append(System.getProperty("java.version")).append(System.lineSeparator());
      buf.append("java.vm.name:   ").append(System.getProperty("java.vm.name")).append(System.lineSeparator());
      buf.append("java.vm.vendor: ").append(System.getProperty("java.vm.vendor")).append(System.lineSeparator());
      buf.append("java.home:      ").append(System.getProperty("java.home")).append(System.lineSeparator());
      buf.append("Monitors:       ").append(System.lineSeparator()).append(getScreenInfo()).append(System.lineSeparator());
      buf.append(System.lineSeparator());
   }

   private static String getScreenInfo() {
      GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
      GraphicsDevice[] devices = env.getScreenDevices();

      StringBuilder returnString = new StringBuilder();
      int screenNo = 1;
      for (GraphicsDevice device : devices) {
         DisplayMode displayMode = device.getDisplayMode();
         if (returnString.length() > 0) {
            returnString.append(System.lineSeparator());
         }
         returnString.append("Screen ").append(screenNo++);
         returnString.append(": size: ").append(displayMode.getWidth()).append(" x ").append(displayMode.getHeight());
         returnString.append(", refresh rate: ").append(displayMode.getRefreshRate() != DisplayMode.REFRESH_RATE_UNKNOWN ? displayMode.getRefreshRate() : "unknown");
         returnString.append(", bit depth: ").append(displayMode.getBitDepth());
         if (isHiDPI(device)) {
            returnString.append(", HiDPI display: true");
         }
      }
      return returnString.toString();
   }

   private static boolean isHiDPI(GraphicsDevice device) {
      try {
         Field field = device.getClass().getDeclaredField("scale");

         if (field != null) {
            field.setAccessible(true);
            Object scale = field.get(device);

            if (scale instanceof Integer && (Integer) scale == 2) {
               return true;
            }
         }
      }
      catch (Throwable ignore) {
      }

      return device.getDefaultConfiguration().getDefaultTransform().getScaleX() == 2;
   }

   private List<Font> getFonts() {
      List<Font> fonts = new ArrayList<>();

      String[] fontFamilyNames = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();

      for (String fontFamilyName : fontFamilyNames) {
         Font font = new Font(fontFamilyName, Font.PLAIN, 12);
         fonts.add(font);
      }
      return fonts;
   }

   public static void main(String[] args) {
      SwingUtilities.invokeLater(TestCaretJava9::new);
   }
}

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

CUSTOMER SUBMITTED WORKAROUND :
Either set resolution in Windows to 100% or use the Monospaced font in Java


Comments
The issue seems to stem from the fact that caret position calculation in DefaultCaret class utilises API that uses integer calculation than floating point calculations. The code flow as seen is DefaultCaret#positionCaret=>BasicTextUI#viewToModel=>BoxView.viewToModel=>CompositeView#viewToModel=>WrappedPlainView#viewToModel=>Utilities.getTabbedOffset Now, getTabbedOffset utilises FontMetrics.charsWidth() which uses integer arithmetic to get the caret position. The same getTabbedOffset uses Font.getStringBounds() which uses floating point arithmetic via Rectangle2D.Float. Proposed fix is to make sure getTabbedOffset uses floating point calculations by using getStringBounds() instead of charsWidth() so that it calculates the character width(s) of text present in JTextArea in floating point to align the caret. webrev: http://cr.openjdk.java.net/~psadhukhan/8199441/webrev.00/
27-04-2018

Aftereffect of hidpi support introduced in jdk9 via JDK-8073320. Looks like rounding issue when caret position is in float.
16-03-2018

Regression reported with JDK 9 and above where clicking a JTextArea with setLineWrap(true) set, the caret (insertion point) is not aligned with the cursor when screen resolution is higher then 100%. Reported with: JDK 9.0.4 Windows 10 Checked this with reported JDK version 9.0.4 and could confirm the issue as reported. Results: ======== 8u161: OK 8u172 ea b03: OK 9: Fail 9.0.4: Fail 10 ea b46: Fail To verify, 1. Set the display scaling to 150% (does not occur when scaling is at 100%) 2. Run the attached test case with respective JDK versions. 3. Cick at random places in the test area. Observation: Caret and cursor are found inserted at the exact position. 4. Now, check the line wrap and repeat the previous step. Observation: With JDK 9 and above the cusror position varies from caret position. This seems a regression in JDK 9.
12-03-2018