# HG changeset patch # User alexsch # Date 1481568464 -10800 # Node ID cc51736a710c11d3bf2d608e64d5ba2b533d0fee # Parent 7748a6083329b064cfe54bad5eee8a362600f55f 8162350: RepaintManager shifts repainted region when the floating point UI scale is used Reviewed-by: flar, serb diff -r 7748a6083329 -r cc51736a710c jdk/src/java.desktop/share/classes/javax/swing/JViewport.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 JViewport's one child or null. diff -r 7748a6083329 -r cc51736a710c jdk/src/java.desktop/share/classes/javax/swing/RepaintManager.java --- 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 image 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; diff -r 7748a6083329 -r cc51736a710c jdk/src/java.desktop/share/classes/sun/java2d/SunGraphics2D.java --- 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) { diff -r 7748a6083329 -r cc51736a710c jdk/src/java.desktop/share/classes/sun/swing/SwingUtilities2.java --- 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. diff -r 7748a6083329 -r cc51736a710c jdk/test/javax/swing/RepaintManager/8162350/RepaintManagerFPUIScaleTest.java --- /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; + } + } +}