Skip to content

Commit 2da079c

Browse files
OnePatchGuyAlexey Ushakov
authored andcommitted
8294426: Two fingers tap generates wrong mouse modifiers on M2 MacBooks
Reviewed-by: prr
1 parent 449b52f commit 2da079c

File tree

2 files changed

+365
-0
lines changed

2 files changed

+365
-0
lines changed

src/java.desktop/macosx/classes/sun/lwawt/macosx/CPlatformResponder.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ void handleMouseEvent(int eventType, int modifierFlags, int buttonNumber,
7979
}
8080

8181
int jmodifiers = NSEvent.nsToJavaModifiers(modifierFlags);
82+
if ((jeventType == MouseEvent.MOUSE_PRESSED) && (jbuttonNumber > MouseEvent.NOBUTTON)) {
83+
// 8294426: NSEvent.nsToJavaModifiers returns 0 on M2 MacBooks if the event is generated
84+
// via tapping (not pressing) on a trackpad
85+
// (System Preferences -> Trackpad -> Tap to click must be turned on).
86+
// So let's set the modifiers manually.
87+
jmodifiers |= MouseEvent.getMaskForButton(jbuttonNumber);
88+
}
89+
8290
boolean jpopupTrigger = NSEvent.isPopupTrigger(jmodifiers);
8391

8492
eventNotifier.notifyMouseEvent(jeventType, System.currentTimeMillis(), jbuttonNumber,
Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
/*
2+
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
3+
* Copyright (c) 2022, JetBrains s.r.o.. All rights reserved.
4+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
5+
*
6+
* This code is free software; you can redistribute it and/or modify it
7+
* under the terms of the GNU General Public License version 2 only, as
8+
* published by the Free Software Foundation.
9+
*
10+
* This code is distributed in the hope that it will be useful, but WITHOUT
11+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13+
* version 2 for more details (a copy is included in the LICENSE file that
14+
* accompanied this code).
15+
*
16+
* You should have received a copy of the GNU General Public License version
17+
* 2 along with this work; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
19+
*
20+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
21+
* or visit www.oracle.com if you need additional information or have any
22+
* questions.
23+
*/
24+
25+
/**
26+
* @test
27+
* @bug 8294426
28+
* @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.
29+
* @author Nikita.Provotorov@jetbrains.com
30+
*
31+
* @key headful
32+
* @requires (os.family == "mac")
33+
*
34+
* @modules java.desktop/java.awt:open java.desktop/sun.lwawt:open java.desktop/sun.lwawt.macosx:+open
35+
* @run main/othervm MouseMacTouchPressEventModifiers
36+
*/
37+
38+
import sun.lwawt.macosx.CocoaConstants;
39+
import sun.lwawt.macosx.LWCToolkit;
40+
41+
import javax.swing.*;
42+
import java.awt.*;
43+
import java.awt.event.MouseAdapter;
44+
import java.awt.event.MouseEvent;
45+
import java.lang.reflect.Method;
46+
import java.util.Map;
47+
import java.util.TreeMap;
48+
import java.util.concurrent.CompletableFuture;
49+
import java.util.concurrent.Future;
50+
import java.util.concurrent.TimeUnit;
51+
import java.util.concurrent.atomic.AtomicReference;
52+
53+
54+
/**
55+
* Sometimes native mouse events aren't accompanied by the correct mouse modifiers, i.e.
56+
* {@link sun.lwawt.macosx.NSEvent#nsToJavaModifiers} returns 0 inside
57+
* {@link sun.lwawt.macosx.CPlatformResponder#handleMouseEvent(int, int, int, int, int, int, int, int)}.
58+
* E.g. the situation above happens when a user taps (NOT clicks) on a trackpad on a M2 MacBooks while
59+
* System Preferences -> Trackpad -> Tap to click is turned on.
60+
* The test emulates the situation via a direct invocation of
61+
* {@link sun.lwawt.macosx.CPlatformResponder#handleMouseEvent(int, int, int, int, int, int, int, int)};
62+
* unfortunately it's impossible to use {@link java.awt.Robot} because its mouse press events ARE accompanied
63+
* by the correct modifiers ({@link sun.lwawt.macosx.NSEvent#nsToJavaModifiers} returns correct values).
64+
*/
65+
public class MouseMacTouchPressEventModifiers
66+
{
67+
/**
68+
* How it works:
69+
* 1. Send a native mouse press to {@code frame} via
70+
* {@link sun.lwawt.macosx.CPlatformResponder#handleMouseEvent(int, int, int, int, int, int, int, int)}
71+
* (using reflection).
72+
* 2. Wait (via {@link Future#get()}) until it generates a usual java MouseEvent
73+
* and dispatches it to the MouseListener of the {@code frame}.
74+
* 3. Verify the dispatched MouseEvent contains correct modifiers, modifiersEx and button number.
75+
* 4. Do all the steps above but for a corresponding mouse release.
76+
*/
77+
public static void main(String[] args) throws Throwable {
78+
// TreeMap to preserve the testing order
79+
final var testCases = new TreeMap<>(Map.of(
80+
CocoaConstants.kCGMouseButtonLeft, new MouseEventFieldsToTest(MouseEvent.BUTTON1_MASK, MouseEvent.BUTTON1_DOWN_MASK, MouseEvent.BUTTON1),
81+
CocoaConstants.kCGMouseButtonRight, new MouseEventFieldsToTest(MouseEvent.BUTTON3_MASK, MouseEvent.BUTTON3_DOWN_MASK, MouseEvent.BUTTON3),
82+
CocoaConstants.kCGMouseButtonCenter, new MouseEventFieldsToTest(MouseEvent.BUTTON2_MASK, MouseEvent.BUTTON2_DOWN_MASK, MouseEvent.BUTTON2)
83+
));
84+
85+
SwingUtilities.invokeAndWait(MouseMacTouchPressEventModifiers::createAndShowGUI);
86+
87+
try {
88+
for (var testCase : testCases.entrySet()) {
89+
final var fieldsToTest = testCase.getValue();
90+
91+
final int mouseX = (frame.getWidth() - 1) / 2;
92+
final int mouseY = (frame.getHeight() - 1) / 2;
93+
94+
// press
95+
96+
MouseEvent event = frame.sendNativeMousePress(
97+
0,
98+
testCase.getKey(),
99+
1,
100+
mouseX,
101+
mouseY
102+
).get(500, TimeUnit.MILLISECONDS);
103+
System.out.println("A mouse press turned into: " + event);
104+
105+
frame.checkInternalErrors();
106+
107+
checkMouseEvent(event,
108+
MouseEvent.MOUSE_PRESSED, fieldsToTest.modifiers, fieldsToTest.pressModifiersEx, fieldsToTest.button);
109+
110+
// release
111+
112+
event = frame.sendNativeMouseRelease(
113+
0,
114+
testCase.getKey(),
115+
1,
116+
mouseX,
117+
mouseY
118+
).get(500, TimeUnit.MILLISECONDS);
119+
System.out.println("A mouse release turned into: " + event);
120+
121+
frame.checkInternalErrors();
122+
123+
checkMouseEvent(event,
124+
MouseEvent.MOUSE_RELEASED, fieldsToTest.modifiers, 0, fieldsToTest.button);
125+
126+
System.out.println();
127+
}
128+
} finally {
129+
SwingUtilities.invokeAndWait(MouseMacTouchPressEventModifiers::disposeGUI);
130+
System.out.flush();
131+
}
132+
}
133+
134+
135+
private record MouseEventFieldsToTest(int modifiers, int pressModifiersEx, int button) {}
136+
137+
private static MyFrame frame;
138+
139+
private static void createAndShowGUI() {
140+
frame = new MyFrame();
141+
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
142+
143+
frame.pack();
144+
frame.setSize(800, 500);
145+
146+
frame.setLocationRelativeTo(null);
147+
frame.setAlwaysOnTop(true);
148+
149+
frame.setVisible(true);
150+
}
151+
152+
private static void disposeGUI() {
153+
if (frame != null) {
154+
frame.dispose();
155+
}
156+
}
157+
158+
private static void checkMouseEvent(MouseEvent me,
159+
int expectedId, int expectedModifiers, int expectedModifiersEx, int expectedButton
160+
) {
161+
boolean wrong = false;
162+
163+
final var errMsg = new StringBuilder(1024);
164+
errMsg.append("Wrong MouseEvent ").append(me).append(':');
165+
166+
if (me.getID() != expectedId) {
167+
errMsg.append("\n eventId: expected <").append(expectedId).append(">, actual <").append(me.getID()).append('>');
168+
wrong = true;
169+
}
170+
if (me.getModifiers() != expectedModifiers) {
171+
errMsg.append("\n modifiers: expected <").append(expectedModifiers).append(">, actual <").append(me.getModifiers()).append('>');
172+
wrong = true;
173+
}
174+
if (me.getModifiersEx() != expectedModifiersEx) {
175+
errMsg.append("\n modifiersEx: expected <").append(expectedModifiersEx).append(">, actual <").append(me.getModifiersEx()).append('>');
176+
wrong = true;
177+
}
178+
if (me.getButton() != expectedButton) {
179+
errMsg.append("\n button: expected <").append(expectedButton).append(">, actual <").append(me.getButton()).append('>');
180+
wrong = true;
181+
}
182+
183+
if (wrong) {
184+
throw new IllegalArgumentException(errMsg.append('\n').toString());
185+
}
186+
}
187+
}
188+
189+
190+
class MyFrame extends JFrame {
191+
public MyFrame() {
192+
addMouseListener(new MouseAdapter() {
193+
@Override
194+
public void mousePressed(MouseEvent e) {
195+
System.out.println("MyFrame::mousePressed: " + e);
196+
keepPromiseVia(e);
197+
}
198+
199+
@Override
200+
public void mouseReleased(MouseEvent e) {
201+
System.out.println("MyFrame::mouseReleased: " + e);
202+
keepPromiseVia(e);
203+
}
204+
});
205+
}
206+
207+
public Future<MouseEvent> sendNativeMousePress(int modifierFlags, int buttonNumber, int clickCount, int x, int y) {
208+
final int eventType = (buttonNumber == CocoaConstants.kCGMouseButtonLeft) ? CocoaConstants.NSLeftMouseDown
209+
: (buttonNumber == CocoaConstants.kCGMouseButtonRight) ? CocoaConstants.NSRightMouseDown
210+
: CocoaConstants.NSOtherMouseDown;
211+
212+
return sendNativeMouseEvent(eventType, modifierFlags, buttonNumber, clickCount, x, y, getX() + x, getY() + y);
213+
}
214+
215+
public Future<MouseEvent> sendNativeMouseRelease(int modifierFlags, int buttonNumber, int clickCount, int x, int y) {
216+
final int eventType = (buttonNumber == CocoaConstants.kCGMouseButtonLeft) ? CocoaConstants.NSLeftMouseUp
217+
: (buttonNumber == CocoaConstants.kCGMouseButtonRight) ? CocoaConstants.NSRightMouseUp
218+
: CocoaConstants.NSOtherMouseUp;
219+
220+
return sendNativeMouseEvent(eventType, modifierFlags, buttonNumber, clickCount, x, y, getX() + x, getY() + y);
221+
}
222+
223+
public void checkInternalErrors() throws Throwable {
224+
final Throwable result = internalError.getAndSet(null);
225+
if (result != null) {
226+
throw result;
227+
}
228+
}
229+
230+
231+
private final AtomicReference<CompletableFuture<MouseEvent>> mouseEventPromise = new AtomicReference<>(null);
232+
233+
private final AtomicReference<Throwable> internalError = new AtomicReference<>(null);
234+
235+
private Future<MouseEvent> sendNativeMouseEvent(
236+
final int eventType,
237+
final int modifierFlags,
238+
final int buttonNumber,
239+
final int clickCount,
240+
final int x,
241+
final int y,
242+
final int absX,
243+
final int absY
244+
) {
245+
assert !SwingUtilities.isEventDispatchThread();
246+
assert mouseEventPromise.get() == null : "Trying to send a mouse event while there is already a processing one";
247+
248+
final CompletableFuture<MouseEvent> result = new CompletableFuture<>();
249+
250+
SwingUtilities.invokeLater(() -> {
251+
try {
252+
LWCToolkit.invokeLater(() -> {
253+
try {
254+
final Object thisPlatformResponder = obtainFramePlatformResponder(this);
255+
final Method thisPlatformResponderHandleMouseEventMethod = obtainHandleMouseEventMethod(thisPlatformResponder);
256+
257+
if (mouseEventPromise.compareAndExchange(null, result) != null) {
258+
throw new IllegalStateException("Trying to send a mouse event while there is already a processing one");
259+
}
260+
261+
thisPlatformResponderHandleMouseEventMethod.invoke(thisPlatformResponder,
262+
eventType, modifierFlags, buttonNumber, clickCount, x, y, absX, absY);
263+
} catch (Throwable err) {
264+
// Remove the promise if thisPlatformResponderHandleMouseEventMethod.invoke(...) failed
265+
mouseEventPromise.compareAndExchange(result, null);
266+
failPromiseDueTo(result, err);
267+
}
268+
}, this);
269+
} catch (Throwable err) {
270+
failPromiseDueTo(result, err);
271+
}
272+
});
273+
274+
return result;
275+
}
276+
277+
/** Wraps {@link CompletableFuture#complete(Object)} */
278+
private void keepPromiseVia(MouseEvent mouseEvent) {
279+
try {
280+
final CompletableFuture<MouseEvent> promise = mouseEventPromise.getAndSet(null);
281+
if (promise == null) {
282+
throw new IllegalStateException("The following unexpected MouseEvent has arrived: " + mouseEvent);
283+
}
284+
285+
if (!promise.complete(mouseEvent)) {
286+
throw new IllegalStateException("The promise had already been completed when the following MouseEvent arrived: " + mouseEvent);
287+
}
288+
} catch (Throwable err) {
289+
setInternalError(err);
290+
}
291+
}
292+
293+
/** Wraps {@link CompletableFuture#completeExceptionally(Throwable)} */
294+
private void failPromiseDueTo(CompletableFuture<MouseEvent> promise, Throwable cause) {
295+
try {
296+
if (!promise.completeExceptionally(cause)) {
297+
throw new IllegalStateException("The promise had already been completed when the following error arrived: " + cause);
298+
}
299+
} catch (Throwable err) {
300+
setInternalError(err);
301+
}
302+
}
303+
304+
private void setInternalError(Throwable err) {
305+
if (internalError.compareAndExchange(null, err) != null) {
306+
System.err.println("Failed to set the following internal error because there is another one: " + err);
307+
}
308+
}
309+
310+
/** Obtains {@code component.peer.platformWindow.responder} */
311+
private static Object obtainFramePlatformResponder(Component component) throws NoSuchFieldException, IllegalAccessException {
312+
final Object framePeer;
313+
{
314+
final var frameClass = Component.class;
315+
final var peerField = frameClass.getDeclaredField("peer");
316+
317+
peerField.setAccessible(true);
318+
319+
framePeer = peerField.get(component);
320+
}
321+
322+
final Object peerPlatformWindow;
323+
{
324+
final var peerClass = framePeer.getClass();
325+
final var platformWindowField = peerClass.getDeclaredField("platformWindow");
326+
327+
platformWindowField.setAccessible(true);
328+
329+
peerPlatformWindow = platformWindowField.get(framePeer);
330+
}
331+
332+
final Object platformWindowResponder;
333+
{
334+
final var peerPlatformWindowClass = peerPlatformWindow.getClass();
335+
final var platformWindowResponderField = peerPlatformWindowClass.getDeclaredField("responder");
336+
337+
platformWindowResponderField.setAccessible(true);
338+
339+
platformWindowResponder = platformWindowResponderField.get(peerPlatformWindow);
340+
}
341+
342+
return platformWindowResponder;
343+
}
344+
345+
/** Obtains {@link sun.lwawt.macosx.CPlatformResponder#handleMouseEvent(int, int, int, int, int, int, int, int)} */
346+
private static Method obtainHandleMouseEventMethod(final Object platformResponder) throws NoSuchMethodException {
347+
final var responderClass = platformResponder.getClass();
348+
final var handleMouseEventMethod = responderClass.getDeclaredMethod(
349+
"handleMouseEvent",
350+
int.class, int.class, int.class, int.class, int.class, int.class, int.class, int.class
351+
);
352+
353+
handleMouseEventMethod.setAccessible(true);
354+
355+
return handleMouseEventMethod;
356+
}
357+
}

0 commit comments

Comments
 (0)