true smooth scrolling: fine-grained settings

Registry settings to fine-tune the following:
* high-precision scrolling,
* pixel-perfect scrolling,
* interpolation (general, scrollbars, mouse wheel, touchpad, etc),
* interpolation delays,
* dynamic scrollbars.
This commit is contained in:
Pavel Fatin
2017-02-02 19:45:15 +01:00
parent aba4d44762
commit 71f67e4a50
8 changed files with 196 additions and 39 deletions
+18 -1
View File
@@ -130,13 +130,30 @@ sun.java2d.uiScale.enabled=false
# Although Java 7 introduced MouseWheelEven.getPreciseWheelRotation() method, JScrollPane doesn't use it so far.
# Depends on the Editor / General / Smooth Scrolling setting, remote desktop detection and power save mode state.
# Ideally, we need to patch the runtime (on Windows, Linux and Mac OS) to improve handling of the fine-grained input data.
# This feature can be toggled via "idea.true.smooth.scrolling.high.precision" option.
#
# * Enables interpolation of scrolling input (scrollbar thumb, mouse wheel, etc).
# * Enables handling of pixel-perfect scrolling events.
# Currently this mode is available only under Mac OS with JetBrains Runtime.
# This feature can be toggled via "idea.true.smooth.scrolling.pixel.perfect" option.
#
# * Enables interpolation of scrolling input (scrollbar, mouse wheel, touchpad, keys, etc).
# Smooths input which lacks both spatial and temporal resolution, performs the rendering asynchronously.
# Depends on the Editor / General / Smooth Scrolling setting, remote desktop detection and power save mode state.
# The feature can be tweaked using the following options:
# "idea.true.smooth.scrolling.interpolation" - the main switch
# "idea.true.smooth.scrolling.interpolation.scrollbar" - scrollbar interpolation
# "idea.true.smooth.scrolling.interpolation.scrollbar.delay" - initial delay for scrollbar interpolation (ms)
# "idea.true.smooth.scrolling.interpolation.mouse.wheel" - mouse wheel / touchpad interpolation
# "idea.true.smooth.scrolling.interpolation.mouse.wheel.delay.min" - minimum initial delay for mouse wheel interpolation (ms)
# "idea.true.smooth.scrolling.interpolation.mouse.wheel.delay.max" - maximum initial delay for mouse wheel interpolation (ms)
# "idea.true.smooth.scrolling.interpolation.precision.touchpad" - precision touchpad interpolation
# "idea.true.smooth.scrolling.interpolation.precision.touchpad.delay" - initial delay for precision touchpad interpolation (ms)
# "idea.true.smooth.scrolling.interpolation.other" - interpolation of other input sources
# "idea.true.smooth.scrolling.interpolation.other.delay" - initial delay for other input source interpolation (ms)
#
# * Adds on-demand horizontal scrollbar in editor.
# The horizontal scrollbar is shown only when it's actually needed for currently visible content.
# This helps to save editor space and to prevent occasional horizontal "jitter" on vertical touchpad scrolling.
# This feature can be toggled via "idea.true.smooth.scrolling.dynamic.scrollbars" option.
#-----------------------------------------------------------------------
#idea.true.smooth.scrolling=true
@@ -16,8 +16,9 @@
package com.intellij.ui;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.registry.RegistryValue;
import com.intellij.util.SystemProperties;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
@@ -26,6 +27,18 @@ import java.awt.*;
* IDE-agnostic component settings.
*/
public class ComponentSettings {
private static final RegistryValue HIGH_PRECISION_SCROLLING = Registry.get("idea.true.smooth.scrolling.high.precision");
private static final RegistryValue PIXEL_PERFECT_SCROLLING = Registry.get("idea.true.smooth.scrolling.pixel.perfect");
private static final RegistryValue SCROLLING_INTERPOLATION = Registry.get("idea.true.smooth.scrolling.interpolation");
private static final RegistryValue SCROLLBAR_INTERPOLATION = Registry.get("idea.true.smooth.scrolling.interpolation.scrollbar");
private static final RegistryValue MOUSE_WHEEL_INTERPOLATION = Registry.get("idea.true.smooth.scrolling.interpolation.mouse.wheel");
private static final RegistryValue PRECISION_TOUCHPAD_INTERPOLATION = Registry.get("idea.true.smooth.scrolling.interpolation.precision.touchpad");
private static final RegistryValue OTHER_SOURCES_INTERPOLATION = Registry.get("idea.true.smooth.scrolling.interpolation.other");
private static final RegistryValue DYNAMIC_SCROLLBARS = Registry.get("idea.true.smooth.scrolling.dynamic.scrollbars");
private boolean mySmoothScrollingEnabled = true;
private boolean myRemoteDesktopConnected;
private boolean myPowerSaveModeEnabled;
@@ -36,7 +49,8 @@ public class ComponentSettings {
return ourInstance;
}
public boolean isSmoothScrollingEligibleFor(Component component) {
// Returns whether "true smooth scrolling" is applicable to the particular component
public boolean isTrueSmoothScrollingEligibleFor(Component component) {
return SystemProperties.isTrueSmoothScrollingEnabled() &&
!ApplicationManager.getApplication().isUnitTestMode() &&
mySmoothScrollingEnabled &&
@@ -46,6 +60,39 @@ public class ComponentSettings {
component.isShowing();
}
// Returns whether high-precision scrolling events are enabled
public boolean isHighPrecisionScrollingEnabled() {
return HIGH_PRECISION_SCROLLING.asBoolean();
}
// Returns whether pixel-perfect scrolling events are enabled (requires high-precision events to be effective)
public boolean isPixelPerfectScrollingEnabled() {
return PIXEL_PERFECT_SCROLLING.asBoolean();
}
// Returns whether scrolling interpolation is enabled for particular input source
public boolean isInterpolationEnabledFor(InputSource source) {
if (!SCROLLING_INTERPOLATION.asBoolean()) {
return false;
}
switch (source) {
case SCROLLBAR:
return SCROLLBAR_INTERPOLATION.asBoolean();
case MOUSE_WHEEL:
return MOUSE_WHEEL_INTERPOLATION.asBoolean();
case PRECISION_TOUCHPAD:
return PRECISION_TOUCHPAD_INTERPOLATION.asBoolean();
default:
return OTHER_SOURCES_INTERPOLATION.asBoolean();
}
}
// Returns whether dymaics scrollbars are enabled (currently applies only to editor's horizontal scrollbar)
public boolean areDynamicScrollbarsEnabled() {
return DYNAMIC_SCROLLBARS.asBoolean();
}
/* A heuristics that disables scrolling interpolation in diff / merge windows.
We need to to make scrolling synchronization compatible with the interpolation first.
@@ -0,0 +1,26 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.ui;
/**
* Source of input event.
*/
public enum InputSource {
SCROLLBAR,
PRECISION_TOUCHPAD,
MOUSE_WHEEL,
UNKNOWN
}
@@ -20,6 +20,7 @@ import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.wm.IdeGlassPane;
import com.intellij.ui.ComponentSettings;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ReflectionUtil;
@@ -744,9 +745,10 @@ public class JBScrollPane extends SmoothScrollPane {
if (event.isConsumed()) return false;
// any rotation expected (forward or backward)
boolean ignore = event.getWheelRotation() == 0;
if (ignore && (isPreciseRotationSupported() || isTrueSmoothScrollingEnabled())) {
if (ignore && (isPreciseRotationSupported() ||
(isTrueSmoothScrollingEnabled() && ComponentSettings.getInstance().isHighPrecisionScrollingEnabled()))) {
double rotation = event.getPreciseWheelRotation();
double delta = MouseWheelEventEx.getScrollingDelta(event);
double delta = MouseWheelEventEx.getAbsoluteDelta(event);
ignore = (rotation == 0.0D || !Double.isFinite(rotation)) && (delta == 0.0D || !Double.isFinite(delta));
}
return !ignore && 0 == (SCROLL_MODIFIERS & event.getModifiers());
@@ -15,6 +15,9 @@
*/
package com.intellij.ui.components;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.ui.ComponentSettings;
import java.awt.event.MouseWheelEvent;
/**
@@ -44,8 +47,15 @@ class MouseWheelEventEx {
* @return negative values for scrolling up, positive values for scrolling down
* 0.0 when absolute deltas are not supported or not available
*/
static double getScrollingDelta(MouseWheelEvent e) {
// This API is temporary disabled as there's no way to detect relative deltas.
return 0.0D;//SystemInfo.isJetbrainsJvm && SystemInfo.isMac ? 10.0D * e.getPreciseWheelRotation() : 0.0D;
static double getAbsoluteDelta(MouseWheelEvent e) {
ComponentSettings settings = ComponentSettings.getInstance();
return settings.isPixelPerfectScrollingEnabled()
? areAbsoluteDeltasSupported() ? 10.0D * e.getPreciseWheelRotation() : 0.0D
: 0.0D;
}
private static boolean areAbsoluteDeltasSupported() {
return SystemInfo.isJetbrainsJvm && SystemInfo.isMac;
}
}
@@ -16,7 +16,10 @@
package com.intellij.ui.components;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.registry.RegistryValue;
import com.intellij.ui.ComponentSettings;
import com.intellij.ui.InputSource;
import com.intellij.util.SystemProperties;
import org.jetbrains.annotations.Nullable;
@@ -31,29 +34,20 @@ import java.lang.reflect.Field;
import static java.lang.Math.*;
/**
* Scroll pane that supports high-precision mouse wheel events and input interpolation.
* Scroll pane that supports high-precision mouse wheel events (including pixel-perfect ones) and input interpolation.
*/
public class SmoothScrollPane extends JScrollPane {
private static final Logger LOG = Logger.getInstance(SmoothScrollPane.class);
private static final double EPSILON = 1E-5D;
// We may enhance the value derivation (make it distance-dependent, etc),
// additionally, we may add these values to the Registry.
private static final int THUMB_DELAY = 50; // ms
private static final int PRECISION_TOUCHPAD_DELAY = 40; // ms
private static final int WHEEL_MIN_DELAY = 60; // ms
private static final int WHEEL_MAX_DELAY = 140; // ms
private static final int DEFAULT_DELAY = 120; // ms
private enum InputSource {
SCROLLBAR_THUMB,
MOUSE_WHEEL,
UNKNOWN
}
private static final RegistryValue SCROLLBAR_DELAY = Registry.get("idea.true.smooth.scrolling.interpolation.scrollbar.delay");
private static final RegistryValue PRECISION_TOUCHPAD_DELAY = Registry.get("idea.true.smooth.scrolling.interpolation.precision.touchpad.delay");
private static final RegistryValue MOUSE_WHEEL_MIN_DELAY = Registry.get("idea.true.smooth.scrolling.interpolation.mouse.wheel.delay.min");
private static final RegistryValue MOUSE_WHEEL_MAX_DELAY = Registry.get("idea.true.smooth.scrolling.interpolation.mouse.wheel.delay.max");
private static final RegistryValue DEFAULT_DELAY = Registry.get("idea.true.smooth.scrolling.interpolation.other.delay");
private InputSource myInputSource = InputSource.UNKNOWN;
private double myWheelRotation;
private double myScrollingDelta;
public SmoothScrollPane() {
}
@@ -72,9 +66,10 @@ public class SmoothScrollPane extends JScrollPane {
@Override
protected void processMouseWheelEvent(MouseWheelEvent e) {
myInputSource = InputSource.MOUSE_WHEEL;
boolean hasAbsoluteDelta = ComponentSettings.getInstance().isPixelPerfectScrollingEnabled() &&
MouseWheelEventEx.getAbsoluteDelta(e) != 0.0D;
myInputSource = hasAbsoluteDelta ? InputSource.PRECISION_TOUCHPAD : InputSource.MOUSE_WHEEL;
myWheelRotation = e.getPreciseWheelRotation();
myScrollingDelta = MouseWheelEventEx.getScrollingDelta(e);
super.processMouseWheelEvent(e);
myInputSource = InputSource.UNKNOWN;
}
@@ -115,7 +110,10 @@ public class SmoothScrollPane extends JScrollPane {
}
private void handleMouseWheelEvent(MouseWheelEvent e, MouseWheelListener delegate) {
if (ComponentSettings.getInstance().isSmoothScrollingEligibleFor(this) &&
ComponentSettings settings = ComponentSettings.getInstance();
if (settings.isTrueSmoothScrollingEligibleFor(this) &&
settings.isHighPrecisionScrollingEnabled() &&
isWheelScrollingEnabled() && e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {
mouseWheelMoved(e);
@@ -141,7 +139,7 @@ public class SmoothScrollPane extends JScrollPane {
private void mouseWheelMoved(MouseWheelEvent e) {
JScrollBar scrollbar = e.isShiftDown() ? getHorizontalScrollBar() : getVerticalScrollBar();
double delta = MouseWheelEventEx.getScrollingDelta(e);
double delta = MouseWheelEventEx.getAbsoluteDelta(e);
if (delta == 0.0D) {
delta = getRelativeDelta(e, scrollbar);
}
@@ -197,17 +195,22 @@ public class SmoothScrollPane extends JScrollPane {
return viewport != null && (viewport.getView() instanceof Scrollable) ? (Scrollable)(viewport.getView()) : null;
}
public int getInitialDelay(boolean valueIsAdjusting) {
InputSource source = valueIsAdjusting ? InputSource.SCROLLBAR_THUMB : myInputSource;
public InputSource getInputSource(boolean valueIsAdjusting) {
return valueIsAdjusting ? InputSource.SCROLLBAR : myInputSource;
}
public int getInitialDelay(InputSource source) {
switch (source) {
case SCROLLBAR_THUMB:
return THUMB_DELAY;
case SCROLLBAR:
return SCROLLBAR_DELAY.asInteger();
case PRECISION_TOUCHPAD:
return PRECISION_TOUCHPAD_DELAY.asInteger();
case MOUSE_WHEEL:
if (myScrollingDelta != 0.0D) return PRECISION_TOUCHPAD_DELAY;
return max(WHEEL_MIN_DELAY, min((int)round(abs(myWheelRotation) * WHEEL_MAX_DELAY), WHEEL_MAX_DELAY));
return max(MOUSE_WHEEL_MIN_DELAY.asInteger(),
min((int)round(abs(myWheelRotation) * MOUSE_WHEEL_MAX_DELAY.asInteger()),
MOUSE_WHEEL_MAX_DELAY.asInteger()));
default:
return DEFAULT_DELAY;
return DEFAULT_DELAY.asInteger();
}
}
@@ -225,8 +228,14 @@ public class SmoothScrollPane extends JScrollPane {
@Override
public void setValue(int value) {
ComponentSettings settings = ComponentSettings.getInstance();
if (settings.isSmoothScrollingEligibleFor(getViewport().getView()) && settings.isInterpolationEligibleFor(this)) {
myInterpolator.setTarget(value, getInitialDelay(getValueIsAdjusting()));
InputSource source = getInputSource(getValueIsAdjusting());
if (settings.isTrueSmoothScrollingEligibleFor(getViewport().getView()) &&
settings.isInterpolationEligibleFor(this) &&
settings.isInterpolationEnabledFor(source)) {
myInterpolator.setTarget(value, getInitialDelay(source));
}
else {
super.setValue(value);
@@ -1864,7 +1864,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
public Dimension getPreferredSize() {
return isReleased ? new Dimension()
: SystemProperties.isTrueSmoothScrollingEnabled()
: SystemProperties.isTrueSmoothScrollingEnabled() && ComponentSettings.getInstance().areDynamicScrollbarsEnabled()
? new Dimension(getPreferredWidthOfVisibleLines(), myView.getPreferredHeight())
: myView.getPreferredSize();
}
@@ -2794,10 +2794,17 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
@Override
public void setValue(int value) {
ComponentSettings settings = ComponentSettings.getInstance();
if (settings.isSmoothScrollingEligibleFor(myEditorComponent) &&
MyScrollPane scrollPane = (MyScrollPane)myScrollPane;
InputSource source = scrollPane.getInputSource(getValueIsAdjusting());
if (settings.isTrueSmoothScrollingEligibleFor(myEditorComponent) &&
settings.isInterpolationEligibleFor(this) &&
settings.isInterpolationEnabledFor(source) &&
myScrollingModel.isAnimationEnabled()) {
myInterpolator.setTarget(value, ((MyScrollPane)myScrollPane).getInitialDelay(getValueIsAdjusting()));
myInterpolator.setTarget(value, scrollPane.getInitialDelay(source));
}
else {
super.setValue(value);
@@ -700,6 +700,45 @@ editor.zero.latency.rendering.debug.description=Make a pause after immediate ren
idea.true.smooth.scrolling.debug=false
idea.true.smooth.scrolling.debug.description=Check window blitter and true double buffering utilization
idea.true.smooth.scrolling.high.precision=true
idea.true.smooth.scrolling.high.precision.description=Enables high-precision scrolling
idea.true.smooth.scrolling.pixel.perfect=true
idea.true.smooth.scrolling.pixel.perfect.description=Enables pixel-perfect scrolling
idea.true.smooth.scrolling.interpolation=true
idea.true.smooth.scrolling.interpolation.description=Enables scrolling interpolation (smooth scrolling) in general
idea.true.smooth.scrolling.interpolation.scrollbar=true
idea.true.smooth.scrolling.interpolation.scrollbar.description=Enables scrolling interpolation for scrollbars
idea.true.smooth.scrolling.interpolation.scrollbar.delay=30
idea.true.smooth.scrolling.interpolation.scrollbar.delay.description=Initial delay for scrollbar interpolation (ms)
idea.true.smooth.scrolling.interpolation.mouse.wheel=true
idea.true.smooth.scrolling.interpolation.mouse.wheel.description=Enables scrolling interpolation for mouse wheel / touchpad
idea.true.smooth.scrolling.interpolation.mouse.wheel.delay.min=60
idea.true.smooth.scrolling.interpolation.mouse.wheel.delay.min.description=Minimum initial delay for mouse wheel interpolation (ms)
idea.true.smooth.scrolling.interpolation.mouse.wheel.delay.max=140
idea.true.smooth.scrolling.interpolation.mouse.wheel.delay.max.description=Maximum initial delay for mouse wheel interpolation (ms)
idea.true.smooth.scrolling.interpolation.precision.touchpad=true
idea.true.smooth.scrolling.interpolation.precision.touchpad.description=Enables scrolling interpolation of pixel-perfect scrolling
idea.true.smooth.scrolling.interpolation.precision.touchpad.delay=20
idea.true.smooth.scrolling.interpolation.precision.touchpad.delay.description=Initial delay for pixel-perfect scrolling interpolation (ms)
idea.true.smooth.scrolling.interpolation.other=true
idea.true.smooth.scrolling.interpolation.other.description=Enables scrolling interpolation for other input sources (keyboard, etc.)
idea.true.smooth.scrolling.interpolation.other.delay=120
idea.true.smooth.scrolling.interpolation.other.delay.description=Initial delay for other input source interpolation (ms)
idea.true.smooth.scrolling.dynamic.scrollbars=true
idea.true.smooth.scrolling.dynamic.scrollbars.description=Show scrollbars only when they are needed for currently visible content
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