// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.google.collide.client.editor;
import com.google.collide.client.editor.Buffer.ScrollListener;
import com.google.collide.client.util.BrowserUtils;
import com.google.collide.json.client.Jso;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.events.MouseWheelEvent;
import elemental.html.Element;
import org.waveprotocol.wave.client.common.util.UserAgent;
/*
* We want to behave as close to native scrolling as possible, but still prevent
* flickering (without expanding the viewport/prerendering lines). The simplest
* approach is to capture the amount of scroll per mousewheel, and manually
* scroll the buffer.
*
* There is a known issue with ChromeOS where it sends a deltaWheel of +/-120
* regardless of how much it will actually scroll. See
* http://code.google.com/p/chromium-os/issues/detail?id=23607 . Because of
* this, we cannot properly redirect mousewheels on ChromeOS. We disable this
* behavior which allows ChromeOS to have native-feeling scrolling (albeit
* with flicker.)
*/
/**
* An object that intercepts mousewheel events that would have otherwise gone to
* the scrollable layer in the buffer. Instead, we manually scroll the buffer
*
* <p>The purpose is to prevent flickering of the newly scrolled region. If the
* scrollable element takes the scroll, that element will be scrolled before we
* can fill it with contents. Therefore, there will be white displayed for a
* split-second, and then text.
*/
class MouseWheelRedirector {
public static void redirect(Buffer buffer, Element scrollableElement) {
// ChromeOS early exit (see class implementation comment)
if (BrowserUtils.isChromeOs()) {
return;
}
new MouseWheelRedirector(buffer).attachEventHandlers(scrollableElement);
}
/**
* Default value for {@link #mouseWheelToScrollDelta} indicating it has not
* been defined yet.
*/
private static final int UNDEFINED = 0;
private final Buffer buffer;
/**
* The magnitude to scroll per wheelDelta unit. Even though mousewheel events
* have wheelDelta in multiples of 120, this is the magnitude of a scroll
* corresponding to 1.
*/
private double mouseWheelToScrollDelta = UNDEFINED;
private MouseWheelRedirector(Buffer buffer) {
this.buffer = buffer;
}
private static native int getWheelDeltaX(Event event) /*-{
// if using webkit (such as in Chrome) we can detect horizontal scroll
if (event.wheelDeltaX) {
return event.wheelDeltaX;
} else {
return 0;
}
}-*/;
private void attachEventHandlers(Element scrollableElement) {
/*
* The MOUSEWHEEL does not exist on FF (it has DOMMouseScroll which we don't
* bother to support). This means FF mousewheel scrolling will flicker.
*/
scrollableElement.addEventListener(Event.MOUSEWHEEL, new EventListener() {
@Override
public void handleEvent(Event evt) {
MouseWheelEvent mouseWheelEvent = (MouseWheelEvent) evt;
/*
* The negative is so the deltaX,Y are positive when the scroll delta
* is. That is, a positive "deltaY" will scroll down.
*/
int deltaY = -((Jso) mouseWheelEvent).getIntField("wheelDeltaY");
int deltaX = -((Jso) mouseWheelEvent).getIntField("wheelDeltaX");
/*
* If the deltaY is 0, this is probably a horizontal-only scroll, in
* which case we let it proceed as normal (no preventDefault, no manual
* scrolling, etc.)
*/
if (deltaY != 0) {
if (mouseWheelToScrollDelta == UNDEFINED) {
captureFirstMouseWheelToScrollDelta(deltaY);
} else {
/*
* There is a chance that we have both a horizontal and vertical
* scroll here. For vertical scroll, we must manually scroll to
* prevent flickering. Since we'll need to preventDefault, we
* must scroll the event's horizontal component too (otherwise
* the intended horizontal scroll would be lost.)
*/
buffer.setScrollTop(buffer.getScrollTop() + (int) (mouseWheelToScrollDelta * deltaY));
buffer.setScrollLeft(buffer.getScrollLeft() + (int) (mouseWheelToScrollDelta * deltaX));
evt.preventDefault();
}
}
}
}, false);
}
private void captureFirstMouseWheelToScrollDelta(final int wheelDelta) {
if (UserAgent.debugUserAgentString().contains("Chrome/17") && UserAgent.isMac()
&& (wheelDelta < 120 || (wheelDelta % 120) != 0)) {
/*
* This is a workaround for Mac trackpads that typically send the actual pixel scroll amount
* instead of a factor of 120. It seems like without this special check, we would still get a
* sane mapping for mouseWheelToScrollDelta, but if the initial touchpad scroll is really
* fast, we get a mouseWheelToScrollDelta that is way too big.
*
* Chrome 18 and above fix this with a fixed constant of 1 to 3, so we don't need special
* casing. (17 is stable at the time of this writing.)
*/
mouseWheelToScrollDelta = 1;
} else {
final int initialScrollTop = buffer.getScrollTop();
buffer.getScrollListenerRegistrar().add(new ScrollListener() {
@Override
public void onScroll(Buffer buffer, int scrollTop) {
mouseWheelToScrollDelta = Math.abs(((float) scrollTop - initialScrollTop) / wheelDelta);
buffer.getScrollListenerRegistrar().remove(this);
}
});
}
}
}