jdk-24/test/jdk/java/awt/Mouse/MacTouchPressEventModifiers/MouseMacTouchPressEventModifiers.java
Harshitha Onkar d7dc474a5a 8305712: [MacOS] Deprecated Cocoa-NSEvent names
Reviewed-by: dmarkov, prr
2023-04-13 15:26:47 +00:00

360 lines
14 KiB
Java

/*
* Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2022, JetBrains s.r.o.. 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.
*/
/*
* @test
* @bug 8294426
* @summary The test verifies that a press {@link java.awt.event.MouseEvent}
* contains correct modifiers although the according native mouse event is
* accompanied by no mouse modifiers.
* @author Nikita.Provotorov@jetbrains.com
*
* @key headful
* @requires (os.family == "mac")
*
* @modules java.desktop/java.awt:open java.desktop/sun.lwawt:open java.desktop/sun.lwawt.macosx:+open
* @run main/othervm MouseMacTouchPressEventModifiers
*/
import sun.lwawt.macosx.CocoaConstants;
import sun.lwawt.macosx.LWCToolkit;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* Sometimes native mouse events aren't accompanied by the correct mouse modifiers, i.e.
* {@link sun.lwawt.macosx.NSEvent#nsToJavaModifiers} returns 0 inside
* {@link sun.lwawt.macosx.CPlatformResponder#handleMouseEvent(int, int, int, int, int, int, int, int)}.
* E.g. the situation above happens when a user taps (NOT clicks) on a trackpad on a M2 MacBooks while
* System Preferences -> Trackpad -> Tap to click is turned on.
* The test emulates the situation via a direct invocation of
* {@link sun.lwawt.macosx.CPlatformResponder#handleMouseEvent(int, int, int, int, int, int, int, int)};
* unfortunately it's impossible to use {@link java.awt.Robot} because its mouse press events ARE accompanied
* by the correct modifiers ({@link sun.lwawt.macosx.NSEvent#nsToJavaModifiers} returns correct values).
*/
public class MouseMacTouchPressEventModifiers
{
/**
* How it works:
* 1. Send a native mouse press to {@code frame} via
* {@link sun.lwawt.macosx.CPlatformResponder#handleMouseEvent(int, int, int, int, int, int, int, int)}
* (using reflection).
* 2. Wait (via {@link Future#get()}) until it generates a usual java MouseEvent
* and dispatches it to the MouseListener of the {@code frame}.
* 3. Verify the dispatched MouseEvent contains correct modifiers, modifiersEx and button number.
* 4. Do all the steps above but for a corresponding mouse release.
*/
public static void main(String[] args) throws Throwable {
// TreeMap to preserve the testing order
final var testCases = new TreeMap<>(Map.of(
CocoaConstants.kCGMouseButtonLeft, new MouseEventFieldsToTest(MouseEvent.BUTTON1_MASK, MouseEvent.BUTTON1_DOWN_MASK, MouseEvent.BUTTON1),
CocoaConstants.kCGMouseButtonRight, new MouseEventFieldsToTest(MouseEvent.BUTTON3_MASK, MouseEvent.BUTTON3_DOWN_MASK, MouseEvent.BUTTON3),
CocoaConstants.kCGMouseButtonCenter, new MouseEventFieldsToTest(MouseEvent.BUTTON2_MASK, MouseEvent.BUTTON2_DOWN_MASK, MouseEvent.BUTTON2)
));
SwingUtilities.invokeAndWait(MouseMacTouchPressEventModifiers::createAndShowGUI);
try {
for (var testCase : testCases.entrySet()) {
final var fieldsToTest = testCase.getValue();
final int mouseX = (frame.getWidth() - 1) / 2;
final int mouseY = (frame.getHeight() - 1) / 2;
// press
MouseEvent event = frame.sendNativeMousePress(
0,
testCase.getKey(),
1,
mouseX,
mouseY
).get(500, TimeUnit.MILLISECONDS);
System.out.println("A mouse press turned into: " + event);
frame.checkInternalErrors();
checkMouseEvent(event,
MouseEvent.MOUSE_PRESSED, fieldsToTest.modifiers, fieldsToTest.pressModifiersEx, fieldsToTest.button);
// release
event = frame.sendNativeMouseRelease(
0,
testCase.getKey(),
1,
mouseX,
mouseY
).get(500, TimeUnit.MILLISECONDS);
System.out.println("A mouse release turned into: " + event);
frame.checkInternalErrors();
checkMouseEvent(event,
MouseEvent.MOUSE_RELEASED, fieldsToTest.modifiers, 0, fieldsToTest.button);
System.out.println();
}
} finally {
SwingUtilities.invokeAndWait(MouseMacTouchPressEventModifiers::disposeGUI);
System.out.flush();
}
}
private record MouseEventFieldsToTest(int modifiers, int pressModifiersEx, int button) {}
private static MyFrame frame;
private static void createAndShowGUI() {
frame = new MyFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.pack();
frame.setSize(800, 500);
frame.setLocationRelativeTo(null);
frame.setAlwaysOnTop(true);
frame.setVisible(true);
}
private static void disposeGUI() {
if (frame != null) {
frame.dispose();
}
}
private static void checkMouseEvent(MouseEvent me,
int expectedId, int expectedModifiers, int expectedModifiersEx, int expectedButton
) {
boolean wrong = false;
final var errMsg = new StringBuilder(1024);
errMsg.append("Wrong MouseEvent ").append(me).append(':');
if (me.getID() != expectedId) {
errMsg.append("\n eventId: expected <").append(expectedId).append(">, actual <").append(me.getID()).append('>');
wrong = true;
}
if (me.getModifiers() != expectedModifiers) {
errMsg.append("\n modifiers: expected <").append(expectedModifiers).append(">, actual <").append(me.getModifiers()).append('>');
wrong = true;
}
if (me.getModifiersEx() != expectedModifiersEx) {
errMsg.append("\n modifiersEx: expected <").append(expectedModifiersEx).append(">, actual <").append(me.getModifiersEx()).append('>');
wrong = true;
}
if (me.getButton() != expectedButton) {
errMsg.append("\n button: expected <").append(expectedButton).append(">, actual <").append(me.getButton()).append('>');
wrong = true;
}
if (wrong) {
throw new IllegalArgumentException(errMsg.append('\n').toString());
}
}
}
class MyFrame extends JFrame {
public MyFrame() {
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
System.out.println("MyFrame::mousePressed: " + e);
keepPromiseVia(e);
}
@Override
public void mouseReleased(MouseEvent e) {
System.out.println("MyFrame::mouseReleased: " + e);
keepPromiseVia(e);
}
});
}
public Future<MouseEvent> sendNativeMousePress(int modifierFlags, int buttonNumber, int clickCount, int x, int y) {
final int eventType = (buttonNumber == CocoaConstants.kCGMouseButtonLeft) ? CocoaConstants.NSEventTypeLeftMouseDown
: (buttonNumber == CocoaConstants.kCGMouseButtonRight) ? CocoaConstants.NSEventTypeRightMouseDown
: CocoaConstants.NSEventTypeOtherMouseDown;
return sendNativeMouseEvent(eventType, modifierFlags, buttonNumber, clickCount, x, y, getX() + x, getY() + y);
}
public Future<MouseEvent> sendNativeMouseRelease(int modifierFlags, int buttonNumber, int clickCount, int x, int y) {
final int eventType = (buttonNumber == CocoaConstants.kCGMouseButtonLeft) ? CocoaConstants.NSEventTypeLeftMouseUp
: (buttonNumber == CocoaConstants.kCGMouseButtonRight) ? CocoaConstants.NSEventTypeRightMouseUp
: CocoaConstants.NSEventTypeOtherMouseUp;
return sendNativeMouseEvent(eventType, modifierFlags, buttonNumber, clickCount, x, y, getX() + x, getY() + y);
}
public void checkInternalErrors() throws Throwable {
final Throwable result = internalError.getAndSet(null);
if (result != null) {
throw result;
}
}
private final AtomicReference<CompletableFuture<MouseEvent>> mouseEventPromise = new AtomicReference<>(null);
private final AtomicReference<Throwable> internalError = new AtomicReference<>(null);
private Future<MouseEvent> sendNativeMouseEvent(
final int eventType,
final int modifierFlags,
final int buttonNumber,
final int clickCount,
final int x,
final int y,
final int absX,
final int absY
) {
assert !SwingUtilities.isEventDispatchThread();
assert mouseEventPromise.get() == null : "Trying to send a mouse event while there is already a processing one";
final CompletableFuture<MouseEvent> result = new CompletableFuture<>();
SwingUtilities.invokeLater(() -> {
try {
LWCToolkit.invokeLater(() -> {
try {
final Object thisPlatformResponder = obtainFramePlatformResponder(this);
final Method thisPlatformResponderHandleMouseEventMethod = obtainHandleMouseEventMethod(thisPlatformResponder);
if (mouseEventPromise.compareAndExchange(null, result) != null) {
throw new IllegalStateException("Trying to send a mouse event while there is already a processing one");
}
thisPlatformResponderHandleMouseEventMethod.invoke(thisPlatformResponder,
eventType, modifierFlags, buttonNumber, clickCount, x, y, absX, absY);
} catch (Throwable err) {
// Remove the promise if thisPlatformResponderHandleMouseEventMethod.invoke(...) failed
mouseEventPromise.compareAndExchange(result, null);
failPromiseDueTo(result, err);
}
}, this);
} catch (Throwable err) {
failPromiseDueTo(result, err);
}
});
return result;
}
/** Wraps {@link CompletableFuture#complete(Object)} */
private void keepPromiseVia(MouseEvent mouseEvent) {
try {
final CompletableFuture<MouseEvent> promise = mouseEventPromise.getAndSet(null);
if (promise == null) {
throw new IllegalStateException("The following unexpected MouseEvent has arrived: " + mouseEvent);
}
if (!promise.complete(mouseEvent)) {
throw new IllegalStateException("The promise had already been completed when the following MouseEvent arrived: " + mouseEvent);
}
} catch (Throwable err) {
setInternalError(err);
}
}
/** Wraps {@link CompletableFuture#completeExceptionally(Throwable)} */
private void failPromiseDueTo(CompletableFuture<MouseEvent> promise, Throwable cause) {
try {
if (!promise.completeExceptionally(cause)) {
throw new IllegalStateException("The promise had already been completed when the following error arrived: " + cause);
}
} catch (Throwable err) {
setInternalError(err);
}
}
private void setInternalError(Throwable err) {
if (internalError.compareAndExchange(null, err) != null) {
System.err.println("Failed to set the following internal error because there is another one: " + err);
}
}
/** Obtains {@code component.peer.platformWindow.responder} */
private static Object obtainFramePlatformResponder(Component component) throws NoSuchFieldException, IllegalAccessException {
final Object framePeer;
{
final var frameClass = Component.class;
final var peerField = frameClass.getDeclaredField("peer");
peerField.setAccessible(true);
framePeer = peerField.get(component);
}
final Object peerPlatformWindow;
{
final var peerClass = framePeer.getClass();
final var platformWindowField = peerClass.getDeclaredField("platformWindow");
platformWindowField.setAccessible(true);
peerPlatformWindow = platformWindowField.get(framePeer);
}
final Object platformWindowResponder;
{
final var peerPlatformWindowClass = peerPlatformWindow.getClass();
final var platformWindowResponderField = peerPlatformWindowClass.getDeclaredField("responder");
platformWindowResponderField.setAccessible(true);
platformWindowResponder = platformWindowResponderField.get(peerPlatformWindow);
}
return platformWindowResponder;
}
/** Obtains {@link sun.lwawt.macosx.CPlatformResponder#handleMouseEvent(int, int, int, int, int, int, int, int)} */
private static Method obtainHandleMouseEventMethod(final Object platformResponder) throws NoSuchMethodException {
final var responderClass = platformResponder.getClass();
final var handleMouseEventMethod = responderClass.getDeclaredMethod(
"handleMouseEvent",
int.class, int.class, int.class, int.class, int.class, int.class, int.class, int.class
);
handleMouseEventMethod.setAccessible(true);
return handleMouseEventMethod;
}
}