/* * 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 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 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> mouseEventPromise = new AtomicReference<>(null); private final AtomicReference internalError = new AtomicReference<>(null); private Future 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 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 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 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; } }