diff --git a/platform/platform-api/src/com/intellij/ui/components/JBScrollBar.java b/platform/platform-api/src/com/intellij/ui/components/JBScrollBar.java index eb4fcba89ae7..f2449bf5bcc2 100644 --- a/platform/platform-api/src/com/intellij/ui/components/JBScrollBar.java +++ b/platform/platform-api/src/com/intellij/ui/components/JBScrollBar.java @@ -6,6 +6,7 @@ import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.wm.IdeGlassPane.TopComponent; import com.intellij.ui.scale.JBUIScale; import com.intellij.util.ui.RegionPainter; +import com.intellij.util.ui.TouchScrollUtil; import com.intellij.util.ui.UIUtil; import org.intellij.lang.annotations.JdkConstants; import org.jetbrains.annotations.NotNull; @@ -202,7 +203,7 @@ public class JBScrollBar extends JScrollBar implements TopComponent, Interpolabl * @return {@code true} if the specified event is handled and consumed, {@code false} otherwise */ public boolean handleMouseWheelEvent(MouseWheelEvent event) { - if (MouseWheelEvent.WHEEL_UNIT_SCROLL != event.getScrollType()) return false; + if (!isSupportedScrollType(event)) return false; if (event.isShiftDown() == (orientation == VERTICAL)) return false; if (!ScrollSettings.isEligibleFor(this)) return false; @@ -235,6 +236,10 @@ public class JBScrollBar extends JScrollBar implements TopComponent, Interpolabl return true; } + private static boolean isSupportedScrollType(MouseWheelEvent e) { + return e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL || TouchScrollUtil.isUpdate(e); + } + private JViewport getViewport() { Component parent = getParent(); if (parent instanceof JScrollPane) { @@ -284,6 +289,9 @@ public class JBScrollBar extends JScrollBar implements TopComponent, Interpolabl * @return a scrolling delta for this scrollbar */ private double getPreciseDelta(MouseWheelEvent event) { + if (TouchScrollUtil.isTouchScroll(event)) { + return TouchScrollUtil.getDelta(event); + } double rotation = event.getPreciseWheelRotation(); if (ScrollSettings.isPixelPerfectEnabled()) { // calculate an absolute delta if possible diff --git a/platform/platform-api/src/com/intellij/util/ui/MouseWheelSmoothScroll.java b/platform/platform-api/src/com/intellij/util/ui/MouseWheelSmoothScroll.java index c0fe611ab426..60b666416a9d 100644 --- a/platform/platform-api/src/com/intellij/util/ui/MouseWheelSmoothScroll.java +++ b/platform/platform-api/src/com/intellij/util/ui/MouseWheelSmoothScroll.java @@ -25,6 +25,8 @@ import static java.lang.Math.*; public class MouseWheelSmoothScroll { private final InertialAnimator horizontal = new InertialAnimator(), vertical = new InertialAnimator(); + private final FlingDetector horizontalFling = new FlingDetector(), verticalFling = new FlingDetector(); + private final EventCounter touchpadRecognizer = new EventCounter(100); private final @NotNull Supplier myScrollEnabled; public static MouseWheelSmoothScroll create() { @@ -51,23 +53,63 @@ public class MouseWheelSmoothScroll { return; } - InertialAnimator animator = isHorizontalScroll(e) ? horizontal : vertical; - int delta = (int)round(getDelta(bar, e)); int value = bar.getValue(); - int minimum = bar.getMinimum(); - int maximum = bar.getMaximum(); - if (abs(delta) > 0.01) { // ignore small delta event - animator.start(value, value + delta, bar::setValue, (v) -> { - return v - bar.getValue() != 0 || minimum != bar.getMinimum() || maximum != bar.getMaximum() || !bar.isShowing(); - }); - e.consume(); + int delta = (int)round(getDelta(bar, e)); + int targetValue = value + delta; + + if (TouchScrollUtil.isUpdate(e)) { + bar.setValue(targetValue); + FlingDetector fling = isHorizontalScroll(e) ? horizontalFling : verticalFling; + fling.updateDelta(delta); + } else if (TouchScrollUtil.isEnd(e)) { + startFling(getEventVerticalScrollBar(e), verticalFling, vertical); + startFling(getEventHorizontalScrollBar(e), horizontalFling, horizontal); + } else if (abs(delta) != 0) { // ignore small delta event + InertialAnimator animator = isHorizontalScroll(e) ? horizontal : vertical; + touchpadRecognizer.addTime(System.currentTimeMillis()); + if (touchpadRecognizer.getSize() >= getTouchpadThreshold()) { + bar.setValue(targetValue); + animator.stop(); + } else { + animator.start(value, targetValue, new ScrollAnimationSettings(), bar::setValue, shouldStop(bar)); + } + } + + e.consume(); + } + + public void startFling(JScrollBar bar, FlingDetector fling, InertialAnimator animator) { + if (bar != null && fling.shouldStart()) { + animator.stop(); + int value = bar.getValue(); + int targetValue = fling.getTargetValue(value); + animator.start(value, targetValue, new FlingAnimationSettings(), bar::setValue, shouldStop(bar)); } } - public static @Nullable JScrollBar getEventScrollBar(@NotNull MouseWheelEvent e) { - JScrollPane scroller = (JScrollPane) e.getComponent(); - if (scroller == null) return null; - return isHorizontalScroll(e) ? scroller.getHorizontalScrollBar() : scroller.getVerticalScrollBar(); + public static @Nullable + JScrollBar getEventScrollBar(@NotNull MouseWheelEvent e) { + return isHorizontalScroll(e) ? getEventHorizontalScrollBar(e) : getEventVerticalScrollBar(e); + } + + public static @Nullable + JScrollBar getEventHorizontalScrollBar(@NotNull MouseWheelEvent e) { + JScrollPane scroller = (JScrollPane)e.getComponent(); + return scroller == null ? null : scroller.getHorizontalScrollBar(); + } + + public static @Nullable + JScrollBar getEventVerticalScrollBar(@NotNull MouseWheelEvent e) { + JScrollPane scroller = (JScrollPane)e.getComponent(); + return scroller == null ? null : scroller.getVerticalScrollBar(); + } + + private static Predicate shouldStop(JScrollBar bar) { + int minimum = bar.getMinimum(); + int maximum = bar.getMaximum(); + return (v) -> { + return v - bar.getValue() != 0 || minimum != bar.getMinimum() || maximum != bar.getMaximum() || !bar.isShowing(); + }; } public static boolean isHorizontalScroll(@NotNull MouseWheelEvent e) { @@ -78,7 +120,14 @@ public class MouseWheelSmoothScroll { return Registry.intValue("idea.inertial.smooth.scrolling.unit.increment"); } + public int getTouchpadThreshold() { + return Registry.intValue("idea.inertial.smooth.scrolling.touchpad.threshold"); + } + public static double getDelta(@NotNull JScrollBar bar, @NotNull MouseWheelEvent event) { + if (TouchScrollUtil.isTouchScroll(event)) { + return TouchScrollUtil.getDelta(event); + } double rotation = event.getPreciseWheelRotation(); int direction = rotation < 0 ? -1 : 1; // bar.getUnitIncrement can return -1 for top bound value. Fix it @@ -87,7 +136,7 @@ public class MouseWheelSmoothScroll { return unitIncrement * rotation * event.getScrollAmount(); } - private final static class InertialAnimator implements ActionListener { + private static class InertialAnimator implements ActionListener { private final static int REFRESH_TIME = 1000 / 60; // 60 Hz private final static double VELOCITY_THRESHOLD = 0.001; @@ -100,31 +149,20 @@ public class MouseWheelSmoothScroll { private @NotNull Consumer myConsumer = BLACK_HOLE; private final Predicate FALSE_PREDICATE = (value) -> false; private @NotNull Predicate myShouldStop = FALSE_PREDICATE; + private AnimationSettings myAnimationSettings = null; private final Timer myTimer = TimerUtil.createNamedTimer("Inertial Animation Timer", REFRESH_TIME, this); - private final EventCounter myTouchpadRecognizer = new EventCounter(100); private InertialAnimator() { myTimer.setInitialDelay(0); } - public double getDuration() { - return max(abs(Registry.doubleValue("idea.inertial.smooth.scrolling.duration")), 0); - } - - public double getDecayDuration() { - return max(abs(Registry.doubleValue("idea.inertial.smooth.scrolling.decay.duration")), 0); - } - - public int getTouchpadThreshold() { - return Registry.intValue("idea.inertial.smooth.scrolling.touchpad.threshold"); - } - - public final void start(int initValue, int targetValue, @NotNull Consumer consumer, @Nullable Predicate shouldStop) { - long currentEventTime = System.currentTimeMillis(); - myTouchpadRecognizer.addTime(currentEventTime); - double duration = getDuration(); - if (duration == 0 || myTouchpadRecognizer.getSize() >= getTouchpadThreshold()) { + public final void start(int initValue, int targetValue, + @NotNull AnimationSettings animationSettings, + @NotNull Consumer consumer, + @Nullable Predicate shouldStop) { + double duration = animationSettings.getDuration(); + if (duration == 0) { consumer.accept(targetValue); stop(); return; @@ -137,11 +175,12 @@ public class MouseWheelSmoothScroll { myTargetValue = targetValue; } - myLastEventTime = currentEventTime - myTimer.getDelay(); + myLastEventTime = System.currentTimeMillis() - myTimer.getDelay(); myVelocity = (myTargetValue - initValue) / duration; myLambda = 1.0; myConsumer = Objects.requireNonNull(consumer); myShouldStop = shouldStop == null ? FALSE_PREDICATE : shouldStop; + myAnimationSettings = animationSettings; myCurrentValue = initValue; myTimer.start(); } @@ -163,7 +202,7 @@ public class MouseWheelSmoothScroll { // slowdown the animation double animationTimeLeft = (myTargetValue - myCurrentValue) / myVelocity; - if (myLambda == 1.0 && animationTimeLeft > REFRESH_TIME && animationTimeLeft < getDecayDuration()) { + if (myLambda == 1.0 && animationTimeLeft > REFRESH_TIME && animationTimeLeft < myAnimationSettings.getDecayDuration()) { // find q of geometric progression using n-th member formulae myLambda = pow(abs(VELOCITY_THRESHOLD / myVelocity), 1.0 / (animationTimeLeft / REFRESH_TIME)); } @@ -189,11 +228,63 @@ public class MouseWheelSmoothScroll { } } + private static class FlingDetector { + private int lastDelta = 0; + + public void updateDelta(int delta) { + lastDelta = delta; + } + + private static int getPixelThreshold() { + return Registry.intValue("idea.inertial.smooth.scrolling.touch.pixel.threshold"); + } + + private static int getFlingMultiplier() { + return Registry.intValue("idea.inertial.smooth.scrolling.touch.multiplier"); + } + + public boolean shouldStart() { + return abs(lastDelta) >= getPixelThreshold(); + } + + public int getTargetValue(int initValue) { + return initValue + lastDelta * getFlingMultiplier(); + } + } + + private interface AnimationSettings { + double getDuration(); + double getDecayDuration(); + } + + private static class ScrollAnimationSettings implements AnimationSettings { + @Override + public double getDuration() { + return max(abs(Registry.doubleValue("idea.inertial.smooth.scrolling.duration")), 0); + } + + @Override + public double getDecayDuration() { + return max(abs(Registry.doubleValue("idea.inertial.smooth.scrolling.decay.duration")), 0); + } + } + + private static class FlingAnimationSettings implements AnimationSettings { + @Override + public double getDuration() { + return max(abs(Registry.doubleValue("idea.inertial.smooth.scrolling.touch.duration")), 0); + } + + @Override + public double getDecayDuration() { + return max(abs(Registry.doubleValue("idea.inertial.smooth.scrolling.touch.decay")), 0); + } + } + private final static class EventCounter { private final List myValues = new LinkedList<>(); private final long myDuration; - private EventCounter(long duration) { myDuration = max(duration, 1); } diff --git a/platform/platform-api/src/com/intellij/util/ui/TouchScrollUtil.java b/platform/platform-api/src/com/intellij/util/ui/TouchScrollUtil.java new file mode 100644 index 000000000000..24a95971762e --- /dev/null +++ b/platform/platform-api/src/com/intellij/util/ui/TouchScrollUtil.java @@ -0,0 +1,34 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.util.ui; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.awt.event.MouseWheelEvent; + +@ApiStatus.Experimental +public class TouchScrollUtil { + private static final int TOUCH_BEGIN = 2; + private static final int TOUCH_UPDATE = 3; + private static final int TOUCH_END = 4; + + public static boolean isTouchScroll(@NotNull MouseWheelEvent e) { + return e.getScrollType() >= TOUCH_BEGIN && e.getScrollType() <= TOUCH_END; + } + + public static double getDelta(@NotNull MouseWheelEvent e) { + return e.getPreciseWheelRotation() * e.getScrollAmount(); + } + + public static boolean isBegin(@NotNull MouseWheelEvent e) { + return e.getScrollType() == TOUCH_BEGIN; + } + + public static boolean isUpdate(@NotNull MouseWheelEvent e) { + return e.getScrollType() == TOUCH_UPDATE; + } + + public static boolean isEnd(@NotNull MouseWheelEvent e) { + return e.getScrollType() == TOUCH_END; + } +} diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index 43c808f53df2..4c54b2654147 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -1066,6 +1066,21 @@ idea.inertial.smooth.scrolling.touchpad.threshold=10 idea.inertial.smooth.scrolling.touchpad.threshold.description=If number of events during last 100 ms is greater than this count, \ then no animation is used +idea.inertial.smooth.scrolling.touch.duration=1500 +idea.inertial.smooth.scrolling.touch.duration.description=Base duration (ms) of touch inertial scroll animation. \ + See full description in idea.inertial.smooth.scrolling.duration + +idea.inertial.smooth.scrolling.touch.decay=1200 +idea.inertial.smooth.scrolling.touch.decay.description=Works as decay in smooth scrolling, \ + See full description in idea.inertial.smooth.scrolling.decay.duration + +idea.inertial.smooth.scrolling.touch.pixel.threshold=3 +idea.inertial.smooth.scrolling.touch.pixel.threshold.description=Threshold to decide, should we start \ + inertial scroll animation + +idea.inertial.smooth.scrolling.touch.multiplier=100 +idea.inertial.smooth.scrolling.touch.multiplier.description=Multiplier to increase fling scroll amount + decompiler.use.line.mapping=true decompiler.use.line.mapping.description=Maps original to decompiled line numbers when stepping in debugger. decompiler.dump.original.lines=false