JRE-604 [fps] frame's client area is one pixel beneath frame's borders

Adopted.

(cherry picked from commit ef2870ee38)
This commit is contained in:
Anton Tarasov
2021-03-16 20:41:23 +03:00
committed by alexey.ushakov@jetbrains.com
parent dbc9fe9eb6
commit f3b6d2a2c5
7 changed files with 316 additions and 7 deletions

View File

@@ -118,4 +118,11 @@ public interface WindowPeer extends ContainerPeer {
* Instructs the peer to update the position of the security warning.
*/
void repositionSecurityWarning();
/**
* Returns the system insets (in the scale of the Window device) when available.
*
* @return the system insets or null
*/
default Insets getSysInsets() { return null; }
}

View File

@@ -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.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
@@ -1612,11 +1615,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();
}
}
@@ -1661,6 +1718,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) {

View File

@@ -371,8 +371,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));

View File

@@ -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();

View File

@@ -170,6 +170,7 @@ jfieldID AwtWindow::securityWarningHeightID;
jfieldID AwtWindow::windowTypeID;
jmethodID AwtWindow::notifyWindowStateChangedMID;
jfieldID AwtWindow::sysInsetsID;
jmethodID AwtWindow::getWarningStringMID;
jmethodID AwtWindow::calculateSecurityWarningPositionMID;
@@ -1504,12 +1505,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));
@@ -3420,6 +3430,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;");

View File

@@ -66,6 +66,8 @@ public:
static jmethodID calculateSecurityWarningPositionMID;
static jmethodID windowTypeNameMID;
static jfieldID sysInsetsID;
AwtWindow();
virtual ~AwtWindow();

View File

@@ -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<Boolean, String> 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);
}
}