8166591: [macos 10.12] Trackpad scrolling of text on OS X 10.12 Sierra is very fast (Trackpad, Retina only)
Reviewed-by: malenkov, serb
--- a/jdk/src/java.desktop/macosx/classes/sun/lwawt/macosx/CEmbeddedFrame.java Tue Oct 04 12:18:05 2016 +0100
+++ b/jdk/src/java.desktop/macosx/classes/sun/lwawt/macosx/CEmbeddedFrame.java Wed Oct 05 18:29:18 2016 +0400
@@ -97,7 +97,7 @@
int absY = locationOnScreen.y + y;
responder.handleScrollEvent(x, y, absX, absY, modifierFlags, deltaX,
- deltaY);
+ deltaY, NSEvent.SCROLL_PHASE_UNSUPPORTED);
}
public void handleKeyEvent(int eventType, int modifierFlags, String characters,
--- a/jdk/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPlatformResponder.java Tue Oct 04 12:18:05 2016 +0100
+++ b/jdk/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPlatformResponder.java Wed Oct 05 18:29:18 2016 +0400
@@ -44,6 +44,8 @@
private final PlatformEventNotifier eventNotifier;
private final boolean isNpapiCallback;
private int lastKeyPressCode = KeyEvent.VK_UNDEFINED;
+ private final DeltaAccumulator deltaAccumulatorX = new DeltaAccumulator();
+ private final DeltaAccumulator deltaAccumulatorY = new DeltaAccumulator();
CPlatformResponder(final PlatformEventNotifier eventNotifier,
final boolean isNpapiCallback) {
@@ -89,37 +91,37 @@
*/
void handleScrollEvent(final int x, final int y, final int absX,
final int absY, final int modifierFlags,
- final double deltaX, final double deltaY) {
+ final double deltaX, final double deltaY,
+ final int scrollPhase) {
int jmodifiers = NSEvent.nsToJavaModifiers(modifierFlags);
final boolean isShift = (jmodifiers & InputEvent.SHIFT_DOWN_MASK) != 0;
+ int roundDeltaX = deltaAccumulatorX.getRoundedDelta(deltaX, scrollPhase);
+ int roundDeltaY = deltaAccumulatorY.getRoundedDelta(deltaY, scrollPhase);
+
// Vertical scroll.
- if (!isShift && deltaY != 0.0) {
- dispatchScrollEvent(x, y, absX, absY, jmodifiers, deltaY);
+ if (!isShift && (deltaY != 0.0 || roundDeltaY != 0)) {
+ dispatchScrollEvent(x, y, absX, absY, jmodifiers, roundDeltaY, deltaY);
}
// Horizontal scroll or shirt+vertical scroll.
final double delta = isShift && deltaY != 0.0 ? deltaY : deltaX;
- if (delta != 0.0) {
+ final int roundDelta = isShift && roundDeltaY != 0 ? roundDeltaY : roundDeltaX;
+ if (delta != 0.0 || roundDelta != 0) {
jmodifiers |= InputEvent.SHIFT_DOWN_MASK;
- dispatchScrollEvent(x, y, absX, absY, jmodifiers, delta);
+ dispatchScrollEvent(x, y, absX, absY, jmodifiers, roundDelta, delta);
}
}
private void dispatchScrollEvent(final int x, final int y, final int absX,
final int absY, final int modifiers,
- final double delta) {
+ final int roundDelta, final double delta) {
final long when = System.currentTimeMillis();
final int scrollType = MouseWheelEvent.WHEEL_UNIT_SCROLL;
final int scrollAmount = 1;
- int wheelRotation = (int) delta;
- int signum = (int) Math.signum(delta);
- if (signum * delta < 1) {
- wheelRotation = signum;
- }
// invert the wheelRotation for the peer
eventNotifier.notifyMouseWheelEvent(when, x, y, absX, absY, modifiers,
scrollType, scrollAmount,
- -wheelRotation, -delta, null);
+ -roundDelta, -delta, null);
}
/**
@@ -260,4 +262,46 @@
void handleWindowFocusEvent(boolean gained, LWWindowPeer opposite) {
eventNotifier.notifyActivation(gained, opposite);
}
+
+ static class DeltaAccumulator {
+
+ static final double MIN_THRESHOLD = 0.1;
+ static final double MAX_THRESHOLD = 0.5;
+ double accumulatedDelta;
+
+ int getRoundedDelta(double delta, int scrollPhase) {
+
+ int roundDelta = (int) Math.round(delta);
+
+ if (scrollPhase == NSEvent.SCROLL_PHASE_UNSUPPORTED) { // mouse wheel
+ if (roundDelta == 0 && delta != 0) {
+ roundDelta = delta > 0 ? 1 : -1;
+ }
+ } else { // trackpad
+ boolean begin = scrollPhase == NSEvent.SCROLL_PHASE_BEGAN;
+ boolean end = scrollPhase == NSEvent.SCROLL_MASK_PHASE_ENDED
+ || scrollPhase == NSEvent.SCROLL_MASK_PHASE_CANCELLED;
+
+ if (begin) {
+ accumulatedDelta = 0;
+ }
+
+ accumulatedDelta += delta;
+
+ double absAccumulatedDelta = Math.abs(accumulatedDelta);
+ if (absAccumulatedDelta > MAX_THRESHOLD) {
+ roundDelta = (int) Math.round(accumulatedDelta);
+ accumulatedDelta -= roundDelta;
+ }
+
+ if (end) {
+ if (roundDelta == 0 && absAccumulatedDelta > MIN_THRESHOLD) {
+ roundDelta = accumulatedDelta > 0 ? 1 : -1;
+ }
+ }
+ }
+
+ return roundDelta;
+ }
+ }
}
--- a/jdk/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPlatformView.java Tue Oct 04 12:18:05 2016 +0100
+++ b/jdk/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPlatformView.java Wed Oct 05 18:29:18 2016 +0400
@@ -194,7 +194,8 @@
if (event.getType() == CocoaConstants.NSScrollWheel) {
responder.handleScrollEvent(x, y, absX, absY, event.getModifierFlags(),
- event.getScrollDeltaX(), event.getScrollDeltaY());
+ event.getScrollDeltaX(), event.getScrollDeltaY(),
+ event.getScrollPhase());
} else {
responder.handleMouseEvent(event.getType(), event.getModifierFlags(), event.getButtonNumber(),
event.getClickCount(), x, y,
--- a/jdk/src/java.desktop/macosx/classes/sun/lwawt/macosx/NSEvent.java Tue Oct 04 12:18:05 2016 +0100
+++ b/jdk/src/java.desktop/macosx/classes/sun/lwawt/macosx/NSEvent.java Wed Oct 05 18:29:18 2016 +0400
@@ -32,6 +32,13 @@
* JDK functionality.
*/
final class NSEvent {
+
+ static final int SCROLL_PHASE_UNSUPPORTED = 1;
+ static final int SCROLL_PHASE_BEGAN = 2;
+ static final int SCROLL_PHASE_CONTINUED = 3;
+ static final int SCROLL_MASK_PHASE_CANCELLED = 4;
+ static final int SCROLL_MASK_PHASE_ENDED = 5;
+
private int type;
private int modifierFlags;
@@ -42,6 +49,7 @@
private int y;
private double scrollDeltaY;
private double scrollDeltaX;
+ private int scrollPhase;
private int absX;
private int absY;
@@ -62,7 +70,7 @@
// Called from native
NSEvent(int type, int modifierFlags, int clickCount, int buttonNumber,
int x, int y, int absX, int absY,
- double scrollDeltaY, double scrollDeltaX) {
+ double scrollDeltaY, double scrollDeltaX, int scrollPhase) {
this.type = type;
this.modifierFlags = modifierFlags;
this.clickCount = clickCount;
@@ -73,6 +81,7 @@
this.absY = absY;
this.scrollDeltaY = scrollDeltaY;
this.scrollDeltaX = scrollDeltaX;
+ this.scrollPhase = scrollPhase;
}
int getType() {
@@ -107,6 +116,10 @@
return scrollDeltaX;
}
+ int getScrollPhase() {
+ return scrollPhase;
+ }
+
int getAbsX() {
return absX;
}
--- a/jdk/src/java.desktop/macosx/native/libawt_lwawt/awt/AWTView.m Tue Oct 04 12:18:05 2016 +0100
+++ b/jdk/src/java.desktop/macosx/native/libawt_lwawt/awt/AWTView.m Wed Oct 05 18:29:18 2016 +0400
@@ -383,7 +383,7 @@
}
static JNF_CLASS_CACHE(jc_NSEvent, "sun/lwawt/macosx/NSEvent");
- static JNF_CTOR_CACHE(jctor_NSEvent, jc_NSEvent, "(IIIIIIIIDD)V");
+ static JNF_CTOR_CACHE(jctor_NSEvent, jc_NSEvent, "(IIIIIIIIDDI)V");
jobject jEvent = JNFNewObject(env, jctor_NSEvent,
[event type],
[event modifierFlags],
@@ -392,7 +392,8 @@
(jint)localPoint.x, (jint)localPoint.y,
(jint)absP.x, (jint)absP.y,
[event deltaY],
- [event deltaX]);
+ [event deltaX],
+ [AWTToolkit scrollStateWithEvent: event]);
CHECK_NULL(jEvent);
static JNF_CLASS_CACHE(jc_PlatformView, "sun/lwawt/macosx/CPlatformView");
--- a/jdk/src/java.desktop/macosx/native/libawt_lwawt/awt/CTrayIcon.m Tue Oct 04 12:18:05 2016 +0100
+++ b/jdk/src/java.desktop/macosx/native/libawt_lwawt/awt/CTrayIcon.m Wed Oct 05 18:29:18 2016 +0400
@@ -139,9 +139,9 @@
jint clickCount;
clickCount = [event clickCount];
-
+
static JNF_CLASS_CACHE(jc_NSEvent, "sun/lwawt/macosx/NSEvent");
- static JNF_CTOR_CACHE(jctor_NSEvent, jc_NSEvent, "(IIIIIIIIDD)V");
+ static JNF_CTOR_CACHE(jctor_NSEvent, jc_NSEvent, "(IIIIIIIIDDI)V");
jobject jEvent = JNFNewObject(env, jctor_NSEvent,
[event type],
[event modifierFlags],
@@ -150,7 +150,8 @@
(jint)localPoint.x, (jint)localPoint.y,
(jint)absP.x, (jint)absP.y,
[event deltaY],
- [event deltaX]);
+ [event deltaX],
+ [AWTToolkit scrollStateWithEvent: event]);
CHECK_NULL(jEvent);
static JNF_CLASS_CACHE(jc_TrayIcon, "sun/lwawt/macosx/CTrayIcon");
--- a/jdk/src/java.desktop/macosx/native/libawt_lwawt/awt/LWCToolkit.h Tue Oct 04 12:18:05 2016 +0100
+++ b/jdk/src/java.desktop/macosx/native/libawt_lwawt/awt/LWCToolkit.h Wed Oct 05 18:29:18 2016 +0400
@@ -41,6 +41,7 @@
@interface AWTToolkit : NSObject { }
+ (long) getEventCount;
+ (void) eventCountPlusPlus;
++ (jint) scrollStateWithEvent: (NSEvent*) event;
@end
/*
--- a/jdk/src/java.desktop/macosx/native/libawt_lwawt/awt/LWCToolkit.m Tue Oct 04 12:18:05 2016 +0100
+++ b/jdk/src/java.desktop/macosx/native/libawt_lwawt/awt/LWCToolkit.m Wed Oct 05 18:29:18 2016 +0400
@@ -43,6 +43,13 @@
#import <JavaRuntimeSupport/JavaRuntimeSupport.h>
+// SCROLL PHASE STATE
+#define SCROLL_PHASE_UNSUPPORTED 1
+#define SCROLL_PHASE_BEGAN 2
+#define SCROLL_PHASE_CONTINUED 3
+#define SCROLL_PHASE_CANCELLED 4
+#define SCROLL_PHASE_ENDED 5
+
int gNumberOfButtons;
jint* gButtonDownMasks;
@@ -72,6 +79,23 @@
eventCount++;
}
++ (jint) scrollStateWithEvent: (NSEvent*) event {
+
+ if ([event type] != NSScrollWheel) {
+ return 0;
+ }
+
+ NSEventPhase phase = [event phase];
+ NSEventPhase momentumPhase = [event momentumPhase];
+
+ if (!phase && !momentumPhase) return SCROLL_PHASE_UNSUPPORTED;
+ switch (phase) {
+ case NSEventPhaseBegan: return SCROLL_PHASE_BEGAN;
+ case NSEventPhaseCancelled: return SCROLL_PHASE_CANCELLED;
+ case NSEventPhaseEnded: return SCROLL_PHASE_ENDED;
+ }
+ return SCROLL_PHASE_CONTINUED;
+}
@end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/javax/swing/plaf/basic/BasicScrollPaneUI/8166591/TooMuchWheelRotationEventsTest.java Wed Oct 05 18:29:18 2016 +0400
@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2016, Oracle and/or its affiliates. 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.
+ */
+
+import java.awt.Color;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.SwingUtilities;
+
+/*
+ * @test
+ * @bug 8166591
+ * @key headful
+ * @summary [macos 10.12] Trackpad scrolling of text on OS X 10.12 Sierra
+ * is very fast (Trackpad, Retina only)
+ * @run main/manual/othervm TooMuchWheelRotationEventsTest
+ */
+public class TooMuchWheelRotationEventsTest {
+
+ private static volatile boolean testResult = false;
+ private static volatile CountDownLatch countDownLatch;
+ private static final String INSTRUCTIONS = "INSTRUCTIONS:\n"
+ + "Try to check the issue on Mac OS X 10.12 Sierra with trackpad"
+ + " on Retina display.\n"
+ + "\n"
+ + "If the trackpad is not supported, press PASS\n"
+ + "\n"
+ + "Use the trackpad to slightly scroll the JTextArea horizontally and vertically.\n"
+ + "If the text area is scrolled too fast press FAIL, else press PASS.";
+
+ public static void main(String args[]) throws Exception {
+ countDownLatch = new CountDownLatch(1);
+
+ SwingUtilities.invokeLater(TooMuchWheelRotationEventsTest::createUI);
+ countDownLatch.await(15, TimeUnit.MINUTES);
+
+ if (!testResult) {
+ throw new RuntimeException("Test fails!");
+ }
+ }
+
+ private static void createUI() {
+
+ final JFrame mainFrame = new JFrame("Trackpad scrolling test");
+ GridBagLayout layout = new GridBagLayout();
+ JPanel mainControlPanel = new JPanel(layout);
+ JPanel resultButtonPanel = new JPanel(layout);
+
+ GridBagConstraints gbc = new GridBagConstraints();
+
+ JPanel testPanel = createTestPanel();
+
+ gbc.gridx = 0;
+ gbc.gridy = 0;
+ gbc.fill = GridBagConstraints.HORIZONTAL;
+ mainControlPanel.add(testPanel, gbc);
+
+ JTextArea instructionTextArea = new JTextArea();
+ instructionTextArea.setText(INSTRUCTIONS);
+ instructionTextArea.setEditable(false);
+ instructionTextArea.setBackground(Color.white);
+
+ gbc.gridx = 0;
+ gbc.gridy = 1;
+ gbc.fill = GridBagConstraints.HORIZONTAL;
+ mainControlPanel.add(instructionTextArea, gbc);
+
+ JButton passButton = new JButton("Pass");
+ passButton.setActionCommand("Pass");
+ passButton.addActionListener((ActionEvent e) -> {
+ testResult = true;
+ mainFrame.dispose();
+ countDownLatch.countDown();
+
+ });
+
+ JButton failButton = new JButton("Fail");
+ failButton.setActionCommand("Fail");
+ failButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ mainFrame.dispose();
+ countDownLatch.countDown();
+ }
+ });
+
+ gbc.gridx = 0;
+ gbc.gridy = 0;
+ resultButtonPanel.add(passButton, gbc);
+
+ gbc.gridx = 1;
+ gbc.gridy = 0;
+ resultButtonPanel.add(failButton, gbc);
+
+ gbc.gridx = 0;
+ gbc.gridy = 2;
+ mainControlPanel.add(resultButtonPanel, gbc);
+
+ mainFrame.add(mainControlPanel);
+ mainFrame.pack();
+
+ mainFrame.addWindowListener(new WindowAdapter() {
+
+ @Override
+ public void windowClosing(WindowEvent e) {
+ mainFrame.dispose();
+ countDownLatch.countDown();
+ }
+ });
+ mainFrame.setVisible(true);
+ }
+
+ private static JPanel createTestPanel() {
+ JPanel panel = new JPanel();
+ panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
+ JTextArea textArea = new JTextArea(20, 20);
+ textArea.setText(getLongString());
+ JScrollPane scrollPane = new JScrollPane(textArea);
+ panel.add(scrollPane);
+ return panel;
+ }
+
+ private static String getLongString() {
+
+ String lowCaseString = getLongString('a', 'z');
+ String upperCaseString = getLongString('A', 'Z');
+ String digitsString = getLongString('0', '9');
+
+ int repeat = 30;
+ StringBuilder lowCaseBuilder = new StringBuilder();
+ StringBuilder upperCaseBuilder = new StringBuilder();
+ StringBuilder digitsBuilder = new StringBuilder();
+
+ for (int i = 0; i < repeat; i++) {
+ lowCaseBuilder.append(lowCaseString).append(' ');
+ upperCaseBuilder.append(upperCaseString).append(' ');
+ digitsBuilder.append(digitsString).append(' ');
+ }
+
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < 200; i++) {
+ builder.append(upperCaseBuilder).append('\n')
+ .append(lowCaseBuilder).append('\n')
+ .append(digitsBuilder).append("\n\n\n");
+ }
+
+ return builder.toString();
+ }
+
+ private static String getLongString(char c1, char c2) {
+
+ char[] chars = new char[c2 - c1 + 1];
+ for (char i = c1; i <= c2; i++) {
+ chars[i - c1] = i;
+ }
+ return new String(chars);
+ }
+}