8162350: RepaintManager shifts repainted region when the floating point UI scale is used
authoralexsch
Mon, 12 Dec 2016 21:47:44 +0300
changeset 42746 cc51736a710c
parent 42745 7748a6083329
child 42747 807791cafb87
8162350: RepaintManager shifts repainted region when the floating point UI scale is used Reviewed-by: flar, serb
jdk/src/java.desktop/share/classes/javax/swing/JViewport.java
jdk/src/java.desktop/share/classes/javax/swing/RepaintManager.java
jdk/src/java.desktop/share/classes/sun/java2d/SunGraphics2D.java
jdk/src/java.desktop/share/classes/sun/swing/SwingUtilities2.java
jdk/test/javax/swing/RepaintManager/8162350/RepaintManagerFPUIScaleTest.java
--- a/jdk/src/java.desktop/share/classes/javax/swing/JViewport.java	Mon Dec 12 12:26:54 2016 +0300
+++ b/jdk/src/java.desktop/share/classes/javax/swing/JViewport.java	Mon Dec 12 21:47:44 2016 +0300
@@ -47,6 +47,7 @@
 import java.util.Collections;
 
 import sun.awt.AWTAccessor;
+import sun.swing.SwingUtilities2;
 
 /**
  * The "viewport" or "porthole" through which you see the underlying
@@ -1034,9 +1035,16 @@
     private boolean isBlitting() {
         Component view = getView();
         return (scrollMode == BLIT_SCROLL_MODE) &&
-               (view instanceof JComponent) && view.isOpaque();
+               (view instanceof JComponent) && view.isOpaque() && !isFPScale();
     }
 
+    private boolean isFPScale() {
+        GraphicsConfiguration gc = getGraphicsConfiguration();
+        if (gc != null) {
+            return SwingUtilities2.isFloatingPointScale(gc.getDefaultTransform());
+        }
+        return false;
+    }
 
     /**
      * Returns the <code>JViewport</code>'s one child or <code>null</code>.
--- a/jdk/src/java.desktop/share/classes/javax/swing/RepaintManager.java	Mon Dec 12 12:26:54 2016 +0300
+++ b/jdk/src/java.desktop/share/classes/javax/swing/RepaintManager.java	Mon Dec 12 21:47:44 2016 +0300
@@ -45,7 +45,11 @@
 import sun.security.action.GetPropertyAction;
 
 import com.sun.java.swing.SwingUtilities3;
+import java.awt.geom.AffineTransform;
+import sun.java2d.SunGraphics2D;
+import sun.java2d.pipe.Region;
 import sun.swing.SwingAccessor;
+import sun.swing.SwingUtilities2;
 import sun.swing.SwingUtilities2.RepaintListener;
 
 /**
@@ -1517,9 +1521,12 @@
             // standard Image buffer.
             boolean paintCompleted = false;
             Image offscreen;
+            int sw = w + 1;
+            int sh = h + 1;
+
             if (repaintManager.useVolatileDoubleBuffer() &&
                 (offscreen = getValidImage(repaintManager.
-                getVolatileOffscreenBuffer(bufferComponent, w, h))) != null) {
+                getVolatileOffscreenBuffer(bufferComponent, sw, sh))) != null) {
                 VolatileImage vImage = (java.awt.image.VolatileImage)offscreen;
                 GraphicsConfiguration gc = bufferComponent.
                                             getGraphicsConfiguration();
@@ -1529,7 +1536,7 @@
                                    VolatileImage.IMAGE_INCOMPATIBLE) {
                         repaintManager.resetVolatileDoubleBuffer(gc);
                         offscreen = repaintManager.getVolatileOffscreenBuffer(
-                            bufferComponent,w, h);
+                            bufferComponent, sw, sh);
                         vImage = (java.awt.image.VolatileImage)offscreen;
                     }
                     paintDoubleBuffered(paintingComponent, vImage, g, x, y,
@@ -1589,8 +1596,18 @@
          * Paints a portion of a component to an offscreen buffer.
          */
         protected void paintDoubleBuffered(JComponent c, Image image,
-                            Graphics g, int clipX, int clipY,
-                            int clipW, int clipH) {
+                Graphics g, int clipX, int clipY,
+                int clipW, int clipH) {
+            if (image instanceof VolatileImage && isPixelsCopying(c, g)) {
+                paintDoubleBufferedFPScales(c, image, g, clipX, clipY, clipW, clipH);
+            } else {
+                paintDoubleBufferedImpl(c, image, g, clipX, clipY, clipW, clipH);
+            }
+        }
+
+        private void paintDoubleBufferedImpl(JComponent c, Image image,
+                                             Graphics g, int clipX, int clipY,
+                                             int clipW, int clipH) {
             Graphics osg = image.getGraphics();
             int bw = Math.min(clipW, image.getWidth(null));
             int bh = Math.min(clipH, image.getHeight(null));
@@ -1629,6 +1646,76 @@
             }
         }
 
+        private void paintDoubleBufferedFPScales(JComponent c, Image image,
+                                                 Graphics g, int clipX, int clipY,
+                                                 int clipW, int clipH) {
+            Graphics osg = image.getGraphics();
+            Graphics2D g2d = (Graphics2D) g;
+            Graphics2D osg2d = (Graphics2D) osg;
+
+            AffineTransform identity = new AffineTransform();
+            int bw = Math.min(clipW, image.getWidth(null));
+            int bh = Math.min(clipH, image.getHeight(null));
+            int x, y, maxx, maxy;
+
+            AffineTransform tx = g2d.getTransform();
+            double scaleX = tx.getScaleX();
+            double scaleY = tx.getScaleY();
+            double trX = tx.getTranslateX();
+            double trY = tx.getTranslateY();
+
+            boolean translucent = volatileBufferType != Transparency.OPAQUE;
+            Composite oldComposite = g2d.getComposite();
+
+            try {
+                for (x = clipX, maxx = clipX + clipW; x < maxx; x += bw) {
+                    for (y = clipY, maxy = clipY + clipH; y < maxy; y += bh) {
+
+                        // draw x, y, bw, bh
+                        int pixelx1 = Region.clipRound(x * scaleX + trX);
+                        int pixely1 = Region.clipRound(y * scaleY + trY);
+                        int pixelx2 = Region.clipRound((x + bw) * scaleX + trX);
+                        int pixely2 = Region.clipRound((y + bh) * scaleY + trY);
+                        int pixelw = pixelx2 - pixelx1;
+                        int pixelh = pixely2 - pixely1;
+
+                        osg2d.setTransform(identity);
+                        if (translucent) {
+                            final Color oldBg = g2d.getBackground();
+                            g2d.setBackground(c.getBackground());
+                            g2d.clearRect(pixelx1, pixely1, pixelw, pixelh);
+                            g2d.setBackground(oldBg);
+                        }
+
+                        osg2d.setClip(0, 0, pixelw, pixelh);
+                        osg2d.translate(trX - pixelx1, trY - pixely1);
+                        osg2d.scale(scaleX, scaleY);
+                        c.paintToOffscreen(osg, x, y, bw, bh, maxx, maxy);
+
+                        g2d.setTransform(identity);
+                        g2d.setClip(pixelx1, pixely1, pixelw, pixelh);
+                        AffineTransform stx = new AffineTransform();
+                        stx.translate(pixelx1, pixely1);
+                        stx.scale(scaleX, scaleY);
+                        g2d.setTransform(stx);
+
+                        if (translucent) {
+                            g2d.setComposite(AlphaComposite.Src);
+                        }
+
+                        g2d.drawImage(image, 0, 0, c);
+
+                        if (translucent) {
+                            g2d.setComposite(oldComposite);
+                        }
+                        g2d.setTransform(tx);
+                    }
+                }
+            } finally {
+                osg.dispose();
+            }
+        }
+
         /**
          * If <code>image</code> is non-null with a positive size it
          * is returned, otherwise null is returned.
@@ -1671,9 +1758,33 @@
          */
         protected void dispose() {
         }
+
+        private boolean isPixelsCopying(JComponent c, Graphics g) {
+
+            AffineTransform tx = getTransform(g);
+            GraphicsConfiguration gc = c.getGraphicsConfiguration();
+
+            if (tx == null || gc == null
+                    || !SwingUtilities2.isFloatingPointScale(tx)) {
+                return false;
+            }
+
+            AffineTransform gcTx = gc.getDefaultTransform();
+
+            return gcTx.getScaleX() == tx.getScaleX()
+                    && gcTx.getScaleY() == tx.getScaleY();
+        }
+
+        private static AffineTransform getTransform(Graphics g) {
+            if (g instanceof SunGraphics2D) {
+                return ((SunGraphics2D) g).transform;
+            } else if (g instanceof Graphics2D) {
+                return ((Graphics2D) g).getTransform();
+            }
+            return null;
+        }
     }
 
-
     private class DoubleBufferInfo {
         public Image image;
         public Dimension size;
--- a/jdk/src/java.desktop/share/classes/sun/java2d/SunGraphics2D.java	Mon Dec 12 12:26:54 2016 +0300
+++ b/jdk/src/java.desktop/share/classes/sun/java2d/SunGraphics2D.java	Mon Dec 12 21:47:44 2016 +0300
@@ -3101,10 +3101,10 @@
             if (scaleX == 1 && scaleY == 1) {
                 return null;
             }
-            sx1 = Region.clipScale(sx1, scaleX);
-            sx2 = Region.clipScale(sx2, scaleX);
-            sy1 = Region.clipScale(sy1, scaleY);
-            sy2 = Region.clipScale(sy2, scaleY);
+            sx1 = Region.clipRound(sx1 * scaleX);
+            sx2 = Region.clipRound(sx2 * scaleX);
+            sy1 = Region.clipRound(sy1 * scaleY);
+            sy2 = Region.clipRound(sy2 * scaleY);
 
             AffineTransform tx = null;
             if (xform != null) {
--- a/jdk/src/java.desktop/share/classes/sun/swing/SwingUtilities2.java	Mon Dec 12 12:26:54 2016 +0300
+++ b/jdk/src/java.desktop/share/classes/sun/swing/SwingUtilities2.java	Mon Dec 12 21:47:44 2016 +0300
@@ -33,6 +33,7 @@
 import java.awt.geom.Rectangle2D;
 import java.awt.geom.AffineTransform;
 import static java.awt.geom.AffineTransform.TYPE_FLIP;
+import static java.awt.geom.AffineTransform.TYPE_MASK_SCALE;
 import static java.awt.geom.AffineTransform.TYPE_TRANSLATION;
 import java.awt.print.PrinterGraphics;
 import java.text.BreakIterator;
@@ -2162,6 +2163,19 @@
         return false;
     }
 
+    public static boolean isFloatingPointScale(AffineTransform tx) {
+        int type = tx.getType() & ~(TYPE_FLIP | TYPE_TRANSLATION);
+        if (type == 0) {
+            return false;
+        } else if ((type & ~TYPE_MASK_SCALE) == 0) {
+            double scaleX = tx.getScaleX();
+            double scaleY = tx.getScaleY();
+            return (scaleX != (int) scaleX) || (scaleY != (int) scaleY);
+        } else {
+            return false;
+        }
+    }
+
     /**
      * Returns the client property for the given key if it is set; otherwise
      * returns the {@L&F} property.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/javax/swing/RepaintManager/8162350/RepaintManagerFPUIScaleTest.java	Mon Dec 12 21:47:44 2016 +0300
@@ -0,0 +1,223 @@
+/*
+ * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Graphics2D;
+import java.awt.GraphicsEnvironment;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Image;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.image.BaseMultiResolutionImage;
+import java.awt.image.BufferedImage;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JFrame;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.SwingUtilities;
+
+/*
+ * @test
+ * @bug 8162350
+ * @summary RepaintManager shifts repainted region when the floating point UI scale is used
+ * @run main/manual/othervm -Dsun.java2d.uiScale=1.5 RepaintManagerFPUIScaleTest
+ */
+public class RepaintManagerFPUIScaleTest {
+
+    private static volatile boolean testResult = false;
+    private static volatile CountDownLatch countDownLatch;
+    private static final String INSTRUCTIONS = "INSTRUCTIONS:\n"
+            + "Check JScrollPane correctly repaints the view"
+            + " when UI scale has floating point value:\n"
+            + "\n"
+            + "1. Scroll down the JScrollPane\n"
+            + "2. Select some values\n"
+            + "If the scrolled selected value is painted without artifacts,"
+            + "press PASS, else press FAIL.";
+
+    public static void main(String args[]) throws Exception {
+
+        countDownLatch = new CountDownLatch(1);
+
+        SwingUtilities.invokeLater(RepaintManagerFPUIScaleTest::createUI);
+        countDownLatch.await(15, TimeUnit.MINUTES);
+
+        if (!testResult) {
+            throw new RuntimeException("Test fails!");
+        }
+    }
+
+    private static void createUI() {
+
+        final JFrame mainFrame = new JFrame("Motif L&F icons test");
+        GridBagLayout layout = new GridBagLayout();
+        JPanel mainControlPanel = new JPanel(layout);
+        JPanel resultButtonPanel = new JPanel(layout);
+
+        GridBagConstraints gbc = new GridBagConstraints();
+
+        JComponent testPanel = createComponent();
+        gbc.gridx = 0;
+        gbc.gridy = 0;
+        gbc.fill = GridBagConstraints.HORIZONTAL;
+        mainControlPanel.add(testPanel, gbc);
+
+        JTextArea instructionTextArea = new JTextArea();
+        instructionTextArea.setText(INSTRUCTIONS);
+        instructionTextArea.setEditable(false);
+        instructionTextArea.setBackground(Color.white);
+
+        gbc.gridx = 0;
+        gbc.gridy = 1;
+        gbc.fill = GridBagConstraints.HORIZONTAL;
+        mainControlPanel.add(instructionTextArea, gbc);
+
+        JButton passButton = new JButton("Pass");
+        passButton.setActionCommand("Pass");
+        passButton.addActionListener((ActionEvent e) -> {
+            testResult = true;
+            mainFrame.dispose();
+            countDownLatch.countDown();
+
+        });
+
+        JButton failButton = new JButton("Fail");
+        failButton.setActionCommand("Fail");
+        failButton.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                mainFrame.dispose();
+                countDownLatch.countDown();
+            }
+        });
+
+        gbc.gridx = 0;
+        gbc.gridy = 0;
+        resultButtonPanel.add(passButton, gbc);
+
+        gbc.gridx = 1;
+        gbc.gridy = 0;
+        resultButtonPanel.add(failButton, gbc);
+
+        gbc.gridx = 0;
+        gbc.gridy = 2;
+        mainControlPanel.add(resultButtonPanel, gbc);
+
+        mainFrame.add(mainControlPanel);
+        mainFrame.pack();
+
+        mainFrame.addWindowListener(new WindowAdapter() {
+
+            @Override
+            public void windowClosing(WindowEvent e) {
+                mainFrame.dispose();
+                countDownLatch.countDown();
+            }
+        });
+        mainFrame.setVisible(true);
+    }
+
+    private static JComponent createComponent() {
+
+        int N = 100;
+        String[] data = new String[N];
+        for (int i = 0; i < N; i++) {
+            data[i] = "Floating point test List Item: " + i;
+        }
+        JList list = new JList(data);
+        list.setCellRenderer(new TestListCellRenderer());
+
+        JScrollPane scrollPane = new JScrollPane(list);
+        return scrollPane;
+    }
+
+    private static Color[] COLORS = {
+        Color.RED, Color.ORANGE, Color.GREEN, Color.BLUE, Color.GRAY
+    };
+
+    private static Image createTestImage(int width, int height, int colorindex) {
+
+        Color color = COLORS[colorindex % COLORS.length];
+
+        AffineTransform tx = GraphicsEnvironment
+                .getLocalGraphicsEnvironment()
+                .getDefaultScreenDevice()
+                .getDefaultConfiguration()
+                .getDefaultTransform();
+
+        Image baseImage = createTestImage(width, height, 1, 1, color);
+        Image rvImage = createTestImage(width, height, tx.getScaleX(), tx.getScaleY(), color);
+
+        return new BaseMultiResolutionImage(baseImage, rvImage);
+    }
+
+    private static Image createTestImage(int w, int h,
+            double scaleX, double scaleY, Color color) {
+
+        int width = (int) Math.ceil(scaleX * w);
+        int height = (int) Math.ceil(scaleY * h);
+        BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+
+        Graphics2D g = img.createGraphics();
+        g.setColor(Color.WHITE);
+        g.fillRect(0, 0, width, height);
+        g.scale(scaleX, scaleY);
+        g.setColor(color);
+        int d = 1;
+        int d2 = 2 * d;
+        g.drawLine(d, h / 2, w - d2, h / 2);
+        g.drawLine(w / 2, d, w / 2, h - d2);
+        g.drawRect(d, d, w - d2, h - d2);
+        g.dispose();
+
+        return img;
+    }
+
+    static class TestListCellRenderer extends DefaultListCellRenderer {
+
+        public Component getListCellRendererComponent(
+                JList list,
+                Object value,
+                int index,
+                boolean isSelected,
+                boolean cellHasFocus) {
+            Component retValue = super.getListCellRendererComponent(
+                    list, value, index, isSelected, cellHasFocus
+            );
+            setIcon(new ImageIcon(createTestImage(20, 10, index)));
+            return retValue;
+        }
+    }
+}