diff --git a/src/macosx/classes/sun/lwawt/macosx/CEmbeddedFrame.java b/src/macosx/classes/sun/lwawt/macosx/CEmbeddedFrame.java index f218c90866b9a120e0cc1f3de70181969b1eced0..11f05b5bc05a753f2ad3df2bc0eadde11afb67b7 100644 --- a/src/macosx/classes/sun/lwawt/macosx/CEmbeddedFrame.java +++ b/src/macosx/classes/sun/lwawt/macosx/CEmbeddedFrame.java @@ -91,7 +91,7 @@ public class CEmbeddedFrame extends EmbeddedFrame { int x = (int)pluginX; int y = (int)pluginY; - responder.handleScrollEvent(x, y, modifierFlags, deltaX, deltaY); + responder.handleScrollEvent(x, y, modifierFlags, deltaX, deltaY, NSEvent.SCROLL_PHASE_UNSUPPORTED); } public void handleKeyEvent(int eventType, int modifierFlags, String characters, diff --git a/src/macosx/classes/sun/lwawt/macosx/CPlatformResponder.java b/src/macosx/classes/sun/lwawt/macosx/CPlatformResponder.java index 804d2d0a969a9b6572bf4d9b3d0ff08469787c36..15c377f9947a862b91b1fd03c26823e529e5c5b2 100644 --- a/src/macosx/classes/sun/lwawt/macosx/CPlatformResponder.java +++ b/src/macosx/classes/sun/lwawt/macosx/CPlatformResponder.java @@ -44,6 +44,8 @@ final class CPlatformResponder { 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) { @@ -90,37 +92,38 @@ final class CPlatformResponder { * Handles scroll events. */ void handleScrollEvent(final int x, final int y, final int modifierFlags, - final double deltaX, final double deltaY) { + final double deltaX, final double deltaY, + final int scrollPhase) { final int buttonNumber = CocoaConstants.kCGMouseButtonCenter; int jmodifiers = NSEvent.nsToJavaMouseModifiers(buttonNumber, 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, jmodifiers, deltaY); + if (!isShift && (deltaY != 0.0 || roundDeltaY != 0)) { + dispatchScrollEvent(x, y, 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, jmodifiers, delta); + dispatchScrollEvent(x, y, jmodifiers, roundDelta, delta); } } private void dispatchScrollEvent(final int x, final int y, - final int modifiers, final double delta) { + final int modifiers, + 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, modifiers, scrollType, - scrollAmount, -wheelRotation, -delta, null); + scrollAmount, -roundDelta, -delta, null); } /** @@ -256,4 +259,46 @@ final class CPlatformResponder { 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; + } + } } diff --git a/src/macosx/classes/sun/lwawt/macosx/CPlatformView.java b/src/macosx/classes/sun/lwawt/macosx/CPlatformView.java index 3c2cd2325e44e8a38f3c42183252de3179adf785..9d1acafacc2d29d70d08dfc3d3fc621a1fc1fb6e 100644 --- a/src/macosx/classes/sun/lwawt/macosx/CPlatformView.java +++ b/src/macosx/classes/sun/lwawt/macosx/CPlatformView.java @@ -192,7 +192,8 @@ public class CPlatformView extends CFRetainedResource { if (event.getType() == CocoaConstants.NSScrollWheel) { responder.handleScrollEvent(x, y, 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, event.getAbsX(), event.getAbsY()); diff --git a/src/macosx/classes/sun/lwawt/macosx/NSEvent.java b/src/macosx/classes/sun/lwawt/macosx/NSEvent.java index d30a39cdfb96a6183d61699ba034ecac141156c4..eccc25743c3a8be4584b765ccde11c4a308b0037 100644 --- a/src/macosx/classes/sun/lwawt/macosx/NSEvent.java +++ b/src/macosx/classes/sun/lwawt/macosx/NSEvent.java @@ -32,6 +32,13 @@ import java.awt.event.*; * 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 @@ final class NSEvent { private int y; private double scrollDeltaY; private double scrollDeltaX; + private int scrollPhase; private int absX; private int absY; @@ -62,7 +70,7 @@ final class NSEvent { // 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 @@ final class NSEvent { this.absY = absY; this.scrollDeltaY = scrollDeltaY; this.scrollDeltaX = scrollDeltaX; + this.scrollPhase = scrollPhase; } int getType() { @@ -107,6 +116,10 @@ final class NSEvent { return scrollDeltaX; } + int getScrollPhase() { + return scrollPhase; + } + int getAbsX() { return absX; } diff --git a/src/macosx/native/sun/awt/AWTView.m b/src/macosx/native/sun/awt/AWTView.m index 457193a0ad46a9d8b0857937bfcfdda911bf2c98..35c9440bad54ef89503cc4ac157e1946ec68baf5 100644 --- a/src/macosx/native/sun/awt/AWTView.m +++ b/src/macosx/native/sun/awt/AWTView.m @@ -386,7 +386,7 @@ AWT_ASSERT_APPKIT_THREAD; } 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], @@ -395,7 +395,8 @@ AWT_ASSERT_APPKIT_THREAD; (jint)localPoint.x, (jint)localPoint.y, (jint)absP.x, (jint)absP.y, [event deltaY], - [event deltaX]); + [event deltaX], + [AWTToolkit scrollStateWithEvent: event]); if (jEvent == nil) { // Unable to create event by some reason. return; diff --git a/src/macosx/native/sun/awt/CTrayIcon.m b/src/macosx/native/sun/awt/CTrayIcon.m index 79ae024f76fe3ad74970816c15ac617404ebfb23..b3967df97d1619b3634b0ad32872646543b456d9 100644 --- a/src/macosx/native/sun/awt/CTrayIcon.m +++ b/src/macosx/native/sun/awt/CTrayIcon.m @@ -136,7 +136,7 @@ static NSSize ScaledImageSizeForStatusBar(NSSize imageSize) { 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], @@ -145,7 +145,8 @@ static NSSize ScaledImageSizeForStatusBar(NSSize imageSize) { (jint)localPoint.x, (jint)localPoint.y, (jint)absP.x, (jint)absP.y, [event deltaY], - [event deltaX]); + [event deltaX], + [AWTToolkit scrollStateWithEvent: event]); if (jEvent == nil) { // Unable to create event by some reason. return; diff --git a/src/macosx/native/sun/awt/LWCToolkit.h b/src/macosx/native/sun/awt/LWCToolkit.h index c3e1b538ac995de5fc619893388fbc26d1e98148..0a5750151151668bd771f005809d09824940e60f 100644 --- a/src/macosx/native/sun/awt/LWCToolkit.h +++ b/src/macosx/native/sun/awt/LWCToolkit.h @@ -40,6 +40,7 @@ extern jint* gButtonDownMasks; @interface AWTToolkit : NSObject { } + (long) getEventCount; + (void) eventCountPlusPlus; ++ (jint) scrollStateWithEvent: (NSEvent*) event; @end /* diff --git a/src/macosx/native/sun/awt/LWCToolkit.m b/src/macosx/native/sun/awt/LWCToolkit.m index 170ea49ad5d742fc5ffc2d61e4b4b3d820b26e4f..871f319d4bfc1471917e49002b08c1065f2171bd 100644 --- a/src/macosx/native/sun/awt/LWCToolkit.m +++ b/src/macosx/native/sun/awt/LWCToolkit.m @@ -39,6 +39,13 @@ #import "sizecalc.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; @@ -54,6 +61,23 @@ static long eventCount; 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 diff --git a/test/javax/swing/plaf/basic/BasicScrollPaneUI/8166591/TooMuchWheelRotationEventsTest.java b/test/javax/swing/plaf/basic/BasicScrollPaneUI/8166591/TooMuchWheelRotationEventsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a58bc701601a284f868ecdaaca6afde665172a40 --- /dev/null +++ b/test/javax/swing/plaf/basic/BasicScrollPaneUI/8166591/TooMuchWheelRotationEventsTest.java @@ -0,0 +1,188 @@ +/* + * 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 + * @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); + } +}