8164032: JViewport backing store image is not scaled on HiDPI display
authoralexsch
Thu, 03 Nov 2016 18:43:55 +0300
changeset 42193 f7550ec880cf
parent 42192 35a00ea5c65d
child 42194 4c928df88d20
8164032: JViewport backing store image is not scaled on HiDPI display Reviewed-by: serb
jdk/src/java.desktop/share/classes/javax/swing/JViewport.java
jdk/test/javax/swing/JViewport/8164032/JViewPortBackingStoreImageTest.java
--- a/jdk/src/java.desktop/share/classes/javax/swing/JViewport.java	Thu Nov 03 12:13:59 2016 +0300
+++ b/jdk/src/java.desktop/share/classes/javax/swing/JViewport.java	Thu Nov 03 18:43:55 2016 +0300
@@ -27,6 +27,12 @@
 
 import java.awt.*;
 import java.awt.event.*;
+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.image.AbstractMultiResolutionImage;
+import java.awt.image.ImageObserver;
 import java.awt.peer.ComponentPeer;
 import java.beans.BeanProperty;
 import java.beans.Transient;
@@ -37,6 +43,8 @@
 import javax.accessibility.*;
 
 import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collections;
 
 import sun.awt.AWTAccessor;
 
@@ -739,7 +747,43 @@
             g.clipRect(0, 0, viewBounds.width, viewBounds.height);
         }
 
-        if (backingStoreImage == null) {
+        boolean recreateBackingStoreImage = (backingStoreImage == null);
+        int scaledWidth = width;
+        int scaledHeight = height;
+
+        if (g instanceof Graphics2D) {
+            double sw = width;
+            double sh = height;
+            Graphics2D g2d = (Graphics2D) g;
+            AffineTransform tx = g2d.getTransform();
+            int type = tx.getType();
+            if ((type & ~(TYPE_TRANSLATION | TYPE_FLIP)) == 0) {
+                // skip
+            } else if ((type & ~(TYPE_TRANSLATION | TYPE_FLIP | TYPE_MASK_SCALE)) == 0) {
+                sw = Math.abs(width * tx.getScaleX());
+                sh = Math.abs(height * tx.getScaleY());
+            } else {
+                sw = Math.abs(width * Math.hypot(tx.getScaleX(), tx.getShearY()));
+                sh = Math.abs(height * Math.hypot(tx.getShearX(), tx.getScaleY()));
+            }
+
+            scaledWidth = (int) Math.ceil(sw);
+            scaledHeight = (int) Math.ceil(sh);
+
+            if (!recreateBackingStoreImage) {
+                if (backingStoreImage instanceof BackingStoreMultiResolutionImage) {
+                    BackingStoreMultiResolutionImage mrImage
+                            = (BackingStoreMultiResolutionImage) backingStoreImage;
+                    recreateBackingStoreImage = (mrImage.scaledWidth != scaledWidth
+                            || mrImage.scaledHeight != scaledHeight);
+                } else {
+                    recreateBackingStoreImage = (width != scaledWidth
+                            || height != scaledHeight);
+                }
+            }
+        }
+
+        if (recreateBackingStoreImage) {
             // Backing store is enabled but this is the first call to paint.
             // Create the backing store, paint it and then copy to g.
             // The backing store image will be created with the size of
@@ -747,7 +791,8 @@
             // same size, otherwise when scrolling the backing image
             // the region outside of the clipped region will not be painted,
             // and result in empty areas.
-            backingStoreImage = createImage(width, height);
+            backingStoreImage = createScaledImage(width, height,
+                                                  scaledWidth, scaledHeight);
             Rectangle clip = g.getClipBounds();
             if (clip.width != width || clip.height != height) {
                 if (!isOpaque()) {
@@ -815,6 +860,74 @@
         scrollUnderway = false;
     }
 
+    private Image createScaledImage(final int width, final int height,
+                                    int scaledWidth, int scaledHeight)
+    {
+        if (scaledWidth == width && scaledHeight == height) {
+            return createImage(width, height);
+        }
+
+        Image rvImage = createImage(scaledWidth, scaledHeight);
+
+        return new BackingStoreMultiResolutionImage(width, height,
+                scaledWidth, scaledHeight, rvImage);
+    }
+
+    static class BackingStoreMultiResolutionImage
+            extends AbstractMultiResolutionImage {
+
+        private final int width;
+        private final int height;
+        private final int scaledWidth;
+        private final int scaledHeight;
+        private final Image rvImage;
+
+        public BackingStoreMultiResolutionImage(int width, int height,
+                int scaledWidth, int scaledHeight, Image rvImage) {
+            this.width = width;
+            this.height = height;
+            this.scaledWidth = scaledWidth;
+            this.scaledHeight = scaledHeight;
+            this.rvImage = rvImage;
+        }
+
+        @Override
+        public int getWidth(ImageObserver observer) {
+            return width;
+        }
+
+        @Override
+        public int getHeight(ImageObserver observer) {
+            return height;
+        }
+
+        @Override
+        protected Image getBaseImage() {
+            return rvImage;
+        }
+
+        @Override
+        public Graphics getGraphics() {
+            Graphics graphics = rvImage.getGraphics();
+            if (graphics instanceof Graphics2D) {
+                double sx = (double) scaledWidth / width;
+                double sy = (double) scaledHeight / height;
+                ((Graphics2D) graphics).scale(sx, sy);
+            }
+            return graphics;
+        }
+
+        @Override
+        public Image getResolutionVariant(double w, double h) {
+            return rvImage;
+        }
+
+        @Override
+        public java.util.List<Image> getResolutionVariants() {
+            return Collections.unmodifiableList(Arrays.asList(rvImage));
+        }
+    }
+
 
     /**
      * Sets the bounds of this viewport.  If the viewport's width
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/javax/swing/JViewport/8164032/JViewPortBackingStoreImageTest.java	Thu Nov 03 18:43:55 2016 +0300
@@ -0,0 +1,258 @@
+/*
+ * 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.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.HashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.JTextPane;
+import javax.swing.JViewport;
+import javax.swing.SwingUtilities;
+import javax.swing.UIManager;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.DefaultStyledDocument;
+import javax.swing.text.Style;
+import javax.swing.text.StyleConstants;
+import javax.swing.text.StyleContext;
+
+/*
+ * @test
+ * @bug 8164032 8156217
+ * @summary JViewport backing store image is not scaled on HiDPI display
+ * @run main/manual JViewPortBackingStoreImageTest
+ */
+public class JViewPortBackingStoreImageTest {
+
+    private static volatile boolean testResult = false;
+    private static volatile CountDownLatch countDownLatch;
+    private static final String INSTRUCTIONS = "INSTRUCTIONS:\n\n"
+            + "Verify text is drawn with high resolution and text selection "
+            + "is not shifted when JViewPort is used on HiDPI display.\n\n"
+            + "If the display does not support HiDPI mode press PASS.\n\n"
+            + "1. Check that the text does not have low resolution.\n"
+            + "If no, press FAIL.\n\n"
+            + "2. Select the current text from the end to the beginning.\n"
+            + "\n"
+            + "If the text is slightly shiftted from one side to another\n"
+            + "and back during selection press Fail.\n"
+            + "Otherwise, press Pass.";
+
+    private static DefaultStyledDocument doc;
+    private static StyleContext styles;
+    private static HashMap<String, Style> contentAttributes;
+
+    public static void main(String args[]) throws Exception {
+        countDownLatch = new CountDownLatch(1);
+
+        SwingUtilities.invokeLater(JViewPortBackingStoreImageTest::createUI);
+        countDownLatch.await(15, TimeUnit.MINUTES);
+
+        if (!testResult) {
+            throw new RuntimeException("Test fails!");
+        }
+    }
+
+    private static void createUI() {
+
+        try {
+            UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel");
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+        final JFrame mainFrame = new JFrame();
+        GridBagLayout layout = new GridBagLayout();
+        JPanel mainControlPanel = new JPanel(layout);
+        JPanel resultButtonPanel = new JPanel(layout);
+
+        GridBagConstraints gbc = new GridBagConstraints();
+
+        gbc.gridx = 0;
+        gbc.gridy = 0;
+        gbc.insets = new Insets(5, 15, 5, 15);
+        gbc.fill = GridBagConstraints.HORIZONTAL;
+        mainControlPanel.add(createComponent(), 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() {
+        createStyles();
+        for (int i = 0; i < data.length; i++) {
+            Paragraph p = data[i];
+            addParagraph(p);
+        }
+
+        JTextPane textPane = new JTextPane(doc);
+
+        JScrollPane scroller = new JScrollPane();
+        JViewport port = scroller.getViewport();
+        port.setScrollMode(JViewport.BACKINGSTORE_SCROLL_MODE);
+        port.add(textPane);
+
+        return scroller;
+    }
+
+    static void createStyles() {
+        styles = new StyleContext();
+        doc = new DefaultStyledDocument(styles);
+        contentAttributes = new HashMap<>();
+
+        // no attributes defined
+        Style s = styles.addStyle(null, null);
+        contentAttributes.put("none", s);
+
+        Style def = styles.getStyle(StyleContext.DEFAULT_STYLE);
+
+        Style heading = styles.addStyle("heading", def);
+        StyleConstants.setFontFamily(heading, "SansSerif");
+        StyleConstants.setBold(heading, true);
+        StyleConstants.setAlignment(heading, StyleConstants.ALIGN_CENTER);
+        StyleConstants.setSpaceAbove(heading, 10);
+        StyleConstants.setSpaceBelow(heading, 10);
+        StyleConstants.setFontSize(heading, 18);
+
+        // Title
+        Style sty = styles.addStyle("title", heading);
+        StyleConstants.setFontSize(sty, 32);
+
+        // author
+        sty = styles.addStyle("author", heading);
+        StyleConstants.setItalic(sty, true);
+        StyleConstants.setSpaceBelow(sty, 25);
+    }
+
+    static void addParagraph(Paragraph p) {
+        try {
+            Style s = null;
+            for (int i = 0; i < p.data.length; i++) {
+                AttributedContent run = p.data[i];
+                s = contentAttributes.get(run.attr);
+                doc.insertString(doc.getLength(), run.content, s);
+            }
+
+            Style ls = styles.getStyle(p.logical);
+            doc.setLogicalStyle(doc.getLength() - 1, ls);
+            doc.insertString(doc.getLength(), "\n", null);
+        } catch (BadLocationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static Paragraph[] data = new Paragraph[]{
+        new Paragraph("title", new AttributedContent[]{
+            new AttributedContent("none", "ALICE'S ADVENTURES IN WONDERLAND")
+        }),
+        new Paragraph("author", new AttributedContent[]{
+            new AttributedContent("none", "Lewis Carroll")
+        }),
+        new Paragraph("heading", new AttributedContent[]{
+            new AttributedContent("alice", " ")
+        })};
+
+    static class Paragraph {
+
+        Paragraph(String logical, AttributedContent[] data) {
+            this.logical = logical;
+            this.data = data;
+        }
+        String logical;
+        AttributedContent[] data;
+    }
+
+    static class AttributedContent {
+
+        AttributedContent(String attr, String content) {
+            this.attr = attr;
+            this.content = content;
+        }
+        String attr;
+        String content;
+    }
+}