diff --git a/src/java.desktop/share/classes/java/awt/peer/WindowPeer.java b/src/java.desktop/share/classes/java/awt/peer/WindowPeer.java index 3b5da099ac52..d3072d6b68d9 100644 --- a/src/java.desktop/share/classes/java/awt/peer/WindowPeer.java +++ b/src/java.desktop/share/classes/java/awt/peer/WindowPeer.java @@ -114,6 +114,13 @@ public interface WindowPeer extends ContainerPeer { */ void updateWindow(); + /** + * Returns the system insets (in the scale of the Window device) when available. + * + * @return the system insets or null + */ + default Insets getSysInsets() { return null; } + /** * Requests a GC that best suits this Window. The returned GC may differ * from the requested GC passed as the argument to this method. This method diff --git a/src/java.desktop/share/classes/javax/swing/RepaintManager.java b/src/java.desktop/share/classes/javax/swing/RepaintManager.java index 8d9548d19e54..783518bce580 100644 --- a/src/java.desktop/share/classes/javax/swing/RepaintManager.java +++ b/src/java.desktop/share/classes/javax/swing/RepaintManager.java @@ -27,7 +27,10 @@ package javax.swing; import java.awt.*; import java.awt.event.*; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; import java.awt.image.VolatileImage; +import java.awt.peer.WindowPeer; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.applet.*; @@ -1567,11 +1570,65 @@ public class RepaintManager */ protected void paintDoubleBuffered(JComponent c, Image image, 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); + int clipW, int clipH) + { + SunGraphics2D sg = (SunGraphics2D)g.create(); + try { + // [tav] For the scaling graphics we need to compensate the toplevel insets rounding error + // to place [0, 0] of the client area in its correct device pixel. + if (sg.transformState == SunGraphics2D.TRANSFORM_TRANSLATESCALE) { + Point2D err = getInsetsRoundingError(sg); + double errX = err.getX(); + double errY = err.getY(); + if (errX != 0 || errY != 0) { + // save the current tx + AffineTransform tx = sg.transform; + + // translate the constrain + Region constrainClip = sg.constrainClip; + Shape usrClip = sg.usrClip; + if (constrainClip != null) { + // SunGraphics2D.constrain(..) rounds down x/y, so to compensate we need to round up + int _errX = (int)Math.ceil(errX); + int _errY = (int)Math.ceil(errY); + if ((_errX | _errY) != 0) { + // drop everything to default + sg.constrainClip = null; + sg.usrClip = null; + sg.clipState = SunGraphics2D.CLIP_DEVICE; + sg.transform = new AffineTransform(); + sg.setDevClip(sg.getSurfaceData().getBounds()); + + Region r = constrainClip.getTranslatedRegion(_errX, _errY); + sg.constrain(r.getLoX(), r.getLoY(), r.getWidth(), r.getHeight()); + } + } + + // translate usrClip + if (usrClip != null) { + if (usrClip instanceof Rectangle2D) { + Rectangle2D u = (Rectangle2D)usrClip; + u.setRect(u.getX() + errX, u.getY() + errY, u.getWidth(), u.getHeight()); + } else { + usrClip = AffineTransform.getTranslateInstance(errX, errY).createTransformedShape(usrClip); + } + sg.transform = new AffineTransform(); + sg.setClip(usrClip); // constrain clip is already valid + } + + // finally translate the tx + AffineTransform newTx = AffineTransform.getTranslateInstance(errX - sg.constrainX, errY - sg.constrainY); + newTx.concatenate(tx); + sg.setTransform(newTx); + } + } + if (image instanceof VolatileImage && isPixelsCopying(c, g)) { + paintDoubleBufferedFPScales(c, image, sg, clipX, clipY, clipW, clipH); + } else { + paintDoubleBufferedImpl(c, image, sg, clipX, clipY, clipW, clipH); + } + } finally { + sg.dispose(); } } @@ -1616,6 +1673,35 @@ public class RepaintManager } } + /** + * For the scaling graphics and a decorated toplevel as the destination, + * calculates the rounding error of the toplevel insets. + * + * @return the left/top insets rounding error, in device space + */ + private static Point2D getInsetsRoundingError(SunGraphics2D g) { + Point2D.Double err = new Point2D.Double(0, 0); + if (g.transformState >= SunGraphics2D.TRANSFORM_TRANSLATESCALE) { + Object dst = g.getSurfaceData().getDestination(); + if (dst instanceof Frame && !((Frame)dst).isUndecorated() || + dst instanceof Dialog && !((Dialog)dst).isUndecorated()) + { + Window wnd = (Window)dst; + WindowPeer peer = (WindowPeer)AWTAccessor.getComponentAccessor().getPeer(wnd); + Insets sysInsets = peer != null ? peer.getSysInsets() : null; + if (sysInsets != null) { + Insets insets = wnd.getInsets(); + // insets.left/top is a scaled down rounded value + // insets.left/top * tx.scale is a scaled up value (which contributes to graphics translate) + // sysInsets.left/top is the precise system value + err.x = sysInsets.left - insets.left * g.transform.getScaleX(); + err.y = sysInsets.top - insets.top * g.transform.getScaleY(); + } + } + } + return err; + } + private void paintDoubleBufferedFPScales(JComponent c, Image image, Graphics g, int clipX, int clipY, int clipW, int clipH) { diff --git a/src/java.desktop/share/classes/sun/java2d/SunGraphics2D.java b/src/java.desktop/share/classes/sun/java2d/SunGraphics2D.java index 1bebf3799977..eb283bfff14f 100644 --- a/src/java.desktop/share/classes/sun/java2d/SunGraphics2D.java +++ b/src/java.desktop/share/classes/sun/java2d/SunGraphics2D.java @@ -370,8 +370,9 @@ public final class SunGraphics2D // changes parameters according to the current scale and translate. final double scaleX = transform.getScaleX(); final double scaleY = transform.getScaleY(); - x = constrainX = (int) transform.getTranslateX(); - y = constrainY = (int) transform.getTranslateY(); + // [tav] rounding down affects aligning by insets in RepaintManager.paintDoubleBuffered + x = constrainX = (int)Math.floor(transform.getTranslateX()); + y = constrainY = (int)Math.floor(transform.getTranslateY()); w = Region.dimAdd(x, Region.clipScale(w, scaleX)); h = Region.dimAdd(y, Region.clipScale(h, scaleY)); diff --git a/src/java.desktop/windows/classes/sun/awt/windows/WWindowPeer.java b/src/java.desktop/windows/classes/sun/awt/windows/WWindowPeer.java index 0ba2217dde59..8e81e18ab0d8 100644 --- a/src/java.desktop/windows/classes/sun/awt/windows/WWindowPeer.java +++ b/src/java.desktop/windows/classes/sun/awt/windows/WWindowPeer.java @@ -110,6 +110,8 @@ public class WWindowPeer extends WPanelPeer implements WindowPeer, */ private WindowListener windowListener; + private Insets sysInsets; // set from native updateInsets + /** * Initialize JNI field IDs */ @@ -204,6 +206,7 @@ public class WWindowPeer extends WPanelPeer implements WindowPeer, void initialize() { super.initialize(); + sysInsets = (Insets)insets_.clone(); updateInsets(insets_); if (!((Window) target).isFontSet()) { @@ -317,6 +320,11 @@ public class WWindowPeer extends WPanelPeer implements WindowPeer, // state. native void updateInsets(Insets i); + @Override + public Insets getSysInsets() { + return (Insets)sysInsets.clone(); + } + static native int getSysMinWidth(); static native int getSysMinHeight(); static native int getSysIconWidth(); diff --git a/src/java.desktop/windows/native/libawt/windows/awt_Window.cpp b/src/java.desktop/windows/native/libawt/windows/awt_Window.cpp index e77a37c27053..448c2acbfcd3 100644 --- a/src/java.desktop/windows/native/libawt/windows/awt_Window.cpp +++ b/src/java.desktop/windows/native/libawt/windows/awt_Window.cpp @@ -157,6 +157,7 @@ jfieldID AwtWindow::autoRequestFocusID; jfieldID AwtWindow::windowTypeID; jmethodID AwtWindow::notifyWindowStateChangedMID; +jfieldID AwtWindow::sysInsetsID; jmethodID AwtWindow::windowTypeNameMID; @@ -919,12 +920,21 @@ BOOL AwtWindow::UpdateInsets(jobject insets) jobject peerInsets = (env)->GetObjectField(peer, AwtPanel::insets_ID); DASSERT(!safe_ExceptionOccurred(env)); + jobject peerSysInsets = (env)->GetObjectField(peer, AwtWindow::sysInsetsID); + DASSERT(!safe_ExceptionOccurred(env)); + if (peerInsets != NULL) { // may have been called during creation (env)->SetIntField(peerInsets, AwtInsets::topID, ScaleDownY(m_insets.top)); (env)->SetIntField(peerInsets, AwtInsets::bottomID, ScaleDownY(m_insets.bottom)); (env)->SetIntField(peerInsets, AwtInsets::leftID, ScaleDownX(m_insets.left)); (env)->SetIntField(peerInsets, AwtInsets::rightID, ScaleDownX(m_insets.right)); } + if (peerSysInsets != NULL) { + (env)->SetIntField(peerSysInsets, AwtInsets::topID, m_insets.top); + (env)->SetIntField(peerSysInsets, AwtInsets::bottomID, m_insets.bottom); + (env)->SetIntField(peerSysInsets, AwtInsets::leftID, m_insets.left); + (env)->SetIntField(peerSysInsets, AwtInsets::rightID, m_insets.right); + } /* Get insets into the Inset object (if any) that was passed */ if (insets != NULL) { (env)->SetIntField(insets, AwtInsets::topID, ScaleDownY(m_insets.top)); @@ -2917,6 +2927,8 @@ Java_sun_awt_windows_WWindowPeer_initIDs(JNIEnv *env, jclass cls) { TRY; + CHECK_NULL(AwtWindow::sysInsetsID = env->GetFieldID(cls, "sysInsets", "Ljava/awt/Insets;")); + AwtWindow::windowTypeID = env->GetFieldID(cls, "windowType", "Ljava/awt/Window$Type;"); diff --git a/src/java.desktop/windows/native/libawt/windows/awt_Window.h b/src/java.desktop/windows/native/libawt/windows/awt_Window.h index 322c2ade091a..368499eca6a7 100644 --- a/src/java.desktop/windows/native/libawt/windows/awt_Window.h +++ b/src/java.desktop/windows/native/libawt/windows/awt_Window.h @@ -61,6 +61,8 @@ public: /* java.awt.Window method IDs */ static jmethodID windowTypeNameMID; + static jfieldID sysInsetsID; + AwtWindow(); virtual ~AwtWindow(); diff --git a/test/jdk/java/awt/hidpi/ClientAreaOriginWindowsTest.java b/test/jdk/java/awt/hidpi/ClientAreaOriginWindowsTest.java new file mode 100644 index 000000000000..bf539dcda0be --- /dev/null +++ b/test/jdk/java/awt/hidpi/ClientAreaOriginWindowsTest.java @@ -0,0 +1,193 @@ +import javax.swing.*; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.util.concurrent.CountDownLatch; +import java.util.function.Function; + +/* @test + * bug JRE-604 + * @summary Tests that the frame's client area origin is correctly positioned in the frame. + * @author Anton Tarasov + * @requires (os.family == "windows") + * @run main/othervm -Dsun.java2d.uiScale.enabled=true + * -Dsun.java2d.uiScale=1.25 + * -Dsun.java2d.d3d=false + * ClientAreaOriginWindowsTest + * @run main/othervm -Dsun.java2d.uiScale.enabled=true + * -Dsun.java2d.uiScale=1.5 + * -Dsun.java2d.d3d=false + * ClientAreaOriginWindowsTest + * @run main/othervm -Dsun.java2d.uiScale.enabled=true + * -Dsun.java2d.uiScale=1.75 + * -Dsun.java2d.d3d=false + * ClientAreaOriginWindowsTest + * @run main/othervm -Dsun.java2d.uiScale.enabled=true + * -Dsun.java2d.uiScale=2.0 + * -Dsun.java2d.d3d=false + * ClientAreaOriginWindowsTest + * @run main/othervm -Dsun.java2d.uiScale.enabled=true + * -Dsun.java2d.uiScale=2.25 + * -Dsun.java2d.d3d=false + * ClientAreaOriginWindowsTest + * @run main/othervm -Dsun.java2d.uiScale.enabled=true + * -Dsun.java2d.uiScale=2.5 + * -Dsun.java2d.d3d=false + * ClientAreaOriginWindowsTest + * @run main/othervm -Dsun.java2d.uiScale.enabled=true + * -Dsun.java2d.uiScale=2.75 + * -Dsun.java2d.d3d=false + * ClientAreaOriginWindowsTest + */ +// +// Notes: +// 1) -Dsun.java2d.d3d=false is the current IDEA (ver. 181) mode. +// 2) The JDK build should contain the fix for JRE-573 for the test to pass. +// +public class ClientAreaOriginWindowsTest { + static final int F_WIDTH = 300; + static final int F_HEIGHT = 200; + + static final Color COLOR_BG = Color.green; + static final Color COLOR_OUTLINE = Color.red; + static final Color COLOR_FG = Color.blue; + + static volatile JFrame frame; + static volatile Timer timer; + + static volatile CountDownLatch latch = new CountDownLatch(1); + static volatile boolean framePainted = false; + + public static void main(String[] args) throws InterruptedException { + EventQueue.invokeLater(() -> show()); + + timer = new Timer(100, (event) -> { + Point loc; + try { + loc = frame.getContentPane().getLocationOnScreen(); + } catch (IllegalComponentStateException e) { + latch.countDown(); + return; + } + Rectangle rect = new Rectangle(loc.x - 1, loc.y - 1, 6, 6); + Robot robot; + try { + robot = new Robot(); + } catch (AWTException e) { + throw new RuntimeException(e); + } + BufferedImage capture = robot.createScreenCapture(rect); + int width = capture.getWidth(); + int height = capture.getHeight(); + + // First, check the frame's client area is painted, otherwise bounce. + Color fgPixel = new Color(capture.getRGB(width - 2, height - 2)); + if (!COLOR_FG.equals(fgPixel)) { + latch.countDown(); + return; + } + framePainted = true; + + Function check = (isXaxis) -> { + StringBuilder err = new StringBuilder(); + boolean hasOutline = false; + boolean hasBg = false; + boolean hasFg = false; + + for (int i = (isXaxis ? width - 1 : height - 1); i >= 0; i--) { + int x = isXaxis ? i : width - 1; + int y = isXaxis ? height - 1 : i; + Color c = new Color(capture.getRGB(x, y)); + hasOutline = c.equals(COLOR_OUTLINE) || hasOutline; + // assuming the frame's border system color is not COLOR_BG/COLOR_BG_FALLBACK. + hasBg = c.equals(COLOR_BG) || hasBg; + hasFg = c.equals(COLOR_FG) || hasFg; + } + String axis = isXaxis ? "X-axis" : "Y-axis"; + if (!hasOutline) err.append("no outline pixel by " + axis); + if (hasBg) err.append("; has background pixels by " + axis); + if (!hasFg) err.append("; no foreground pixels by " + axis); + return err.toString(); + }; + + String xAxis = check.apply(true); + String yAxis = check.apply(false); + + if (xAxis.length() > 0 || yAxis.length() > 0) { + StringBuilder err = new StringBuilder(). + append(xAxis). + append("; "). + append(yAxis); + throw new RuntimeException("Test FAILED: " + err); + } + latch.countDown(); + }); + timer.setRepeats(false); + + latch.await(); + latch = new CountDownLatch(1); + + while (!framePainted) { + timer.start(); + latch.await(); + if (!framePainted) latch = new CountDownLatch(1); + } + + System.out.println("Test PASSED"); + } + + static void show() { + frame = new JFrame("frame"); + frame.setLocationRelativeTo(null); + frame.setBackground(COLOR_BG); + frame.getContentPane().setBackground(COLOR_BG); + + JPanel panel = new JPanel() { + @Override + protected void paintComponent(Graphics g) { + Graphics2D g2d = (Graphics2D)g; + AffineTransform tx = g2d.getTransform(); + double pixelX = 1 / tx.getScaleX(); + double pixelY = 1 / tx.getScaleY(); + + g2d.setColor(COLOR_OUTLINE); + g2d.fill(new Rectangle2D.Double(0, 0, F_WIDTH, F_HEIGHT)); + g2d.setColor(COLOR_FG); + g2d.fill(new Rectangle2D.Double(pixelX, pixelY, F_WIDTH - pixelX * 2, F_HEIGHT - pixelY * 2)); + } + + @Override + public Dimension getPreferredSize() { + return new Dimension(F_WIDTH, F_HEIGHT); + } + }; + + // Backs the main frame with black color. + JFrame bgFrame = new JFrame("bg_frame"); + bgFrame.setUndecorated(true); + bgFrame.setSize(F_WIDTH * 2, F_HEIGHT * 2); + bgFrame.setLocationRelativeTo(null); + bgFrame.setAlwaysOnTop(true); + JPanel cp = new JPanel(); + cp.setOpaque(true); + cp.setBackground(Color.black); + bgFrame.setContentPane(cp); + bgFrame.setVisible(true); + + frame.add(panel); + frame.pack(); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setLocationRelativeTo(null); + frame.setAlwaysOnTop(true); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowActivated(WindowEvent e) { + latch.countDown(); + } + }); + frame.setVisible(true); + } +} \ No newline at end of file