d7dc474a5a
Reviewed-by: dmarkov, prr
360 lines
14 KiB
Java
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;
|
|
}
|
|
}
|