123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964 |
- /*
- * Copyright 2000-2021 Vaadin Ltd.
- *
- * 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.vaadin.client.widget.escalator;
-
- import com.google.gwt.animation.client.AnimationScheduler;
- import com.google.gwt.animation.client.AnimationScheduler.AnimationSupportDetector;
- import com.google.gwt.core.client.Scheduler;
- import com.google.gwt.core.client.Scheduler.ScheduledCommand;
- import com.google.gwt.dom.client.Element;
- import com.google.gwt.dom.client.Style.Display;
- import com.google.gwt.dom.client.Style.Overflow;
- import com.google.gwt.dom.client.Style.Unit;
- import com.google.gwt.dom.client.Style.Visibility;
- import com.google.gwt.event.shared.EventHandler;
- import com.google.gwt.event.shared.GwtEvent;
- import com.google.gwt.event.shared.HandlerManager;
- import com.google.gwt.event.shared.HandlerRegistration;
- import com.google.gwt.user.client.DOM;
- import com.google.gwt.user.client.Event;
- import com.google.gwt.user.client.Timer;
- import com.vaadin.client.BrowserInfo;
- import com.vaadin.client.DeferredWorker;
- import com.vaadin.client.WidgetUtil;
- import com.vaadin.client.widget.grid.events.ScrollEvent;
- import com.vaadin.client.widget.grid.events.ScrollHandler;
-
- /**
- * An element-like bundle representing a configurable and visual scrollbar in
- * one axis.
- *
- * @since 7.4
- * @author Vaadin Ltd
- * @see VerticalScrollbarBundle
- * @see HorizontalScrollbarBundle
- */
- public abstract class ScrollbarBundle implements DeferredWorker {
-
- private static final boolean SUPPORTS_REQUEST_ANIMATION_FRAME = new AnimationSupportDetector()
- .isNativelySupported();
-
- private class ScrollEventFirer {
- private final ScheduledCommand fireEventCommand = () -> {
- /*
- * Some kind of native-scroll-event related asynchronous problem
- * occurs here (at least on desktops) where the internal bookkeeping
- * isn't up to date with the real scroll position. The weird thing
- * is, that happens only once, and if you drag scrollbar fast
- * enough. After it has failed once, it never fails again.
- *
- * Theory: the user drags the scrollbar, and this command is
- * executed before the browser has a chance to fire a scroll event
- * (which normally would correct this situation). This would explain
- * why slow scrolling doesn't trigger the problem, while fast
- * scrolling does.
- *
- * To make absolutely sure that we have the latest scroll position,
- * let's update the internal value.
- *
- * This might lead to a slight performance hit (on my computer it
- * was never more than 3ms on either of Chrome 38 or Firefox 31). It
- * also _slightly_ counteracts the purpose of the internal
- * bookkeeping. But since getScrollPos is called 3 times (on one
- * direction) per scroll loop, it's still better to have take this
- * small penalty than removing it altogether.
- */
- updateScrollPosFromDom();
-
- getHandlerManager().fireEvent(new ScrollEvent());
- isBeingFired = false;
- };
-
- private boolean isBeingFired;
-
- public void scheduleEvent() {
- if (!isBeingFired) {
- /*
- * We'll gather all the scroll events, and only fire once, once
- * everything has calmed down.
- */
- if (SUPPORTS_REQUEST_ANIMATION_FRAME) {
- // Chrome MUST use this as deferred commands will sometimes
- // be run with a 300+ ms delay when scrolling.
- AnimationScheduler.get().requestAnimationFrame(
- timestamp -> fireEventCommand.execute());
- } else {
- // Does not support requestAnimationFrame and the fallback
- // uses a delay of 16ms, we stick to the old deferred
- // command which uses a delay of 0ms
- Scheduler.get().scheduleDeferred(fireEventCommand);
- }
- isBeingFired = true;
- }
- }
- }
-
- /**
- * The orientation of the scrollbar.
- */
- public enum Direction {
- /** Scrollbar orientation, indicated by the name. */
- VERTICAL, HORIZONTAL;
- }
-
- private class TemporaryResizer {
- private static final int TEMPORARY_RESIZE_DELAY = 1000;
-
- private final Timer timer = new Timer() {
- @Override
- public void run() {
- internalSetScrollbarThickness(1);
- root.getStyle().setVisibility(Visibility.HIDDEN);
- }
- };
-
- public void show() {
- internalSetScrollbarThickness(OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX);
- root.getStyle().setVisibility(Visibility.VISIBLE);
- timer.schedule(TEMPORARY_RESIZE_DELAY);
- }
- }
-
- /**
- * A means to listen to when the scrollbar handle in a
- * {@link ScrollbarBundle} either appears or is removed.
- */
- public interface VisibilityHandler extends EventHandler {
- /**
- * This method is called whenever the scrollbar handle's visibility is
- * changed in a {@link ScrollbarBundle}.
- *
- * @param event
- * the {@link VisibilityChangeEvent}
- */
- void visibilityChanged(VisibilityChangeEvent event);
- }
-
- /**
- * Event for scrollbar visibility changes.
- */
- public static class VisibilityChangeEvent
- extends GwtEvent<VisibilityHandler> {
- /** Event type. */
- public static final Type<VisibilityHandler> TYPE = new Type<ScrollbarBundle.VisibilityHandler>() {
- @Override
- public String toString() {
- return "VisibilityChangeEvent";
- }
- };
-
- private final boolean isScrollerVisible;
-
- private VisibilityChangeEvent(boolean isScrollerVisible) {
- this.isScrollerVisible = isScrollerVisible;
- }
-
- /**
- * Checks whether the scroll handle is currently visible or not.
- *
- * @return <code>true</code> if the scroll handle is currently visible.
- * <code>false</code> if not.
- */
- public boolean isScrollerVisible() {
- return isScrollerVisible;
- }
-
- @Override
- public Type<VisibilityHandler> getAssociatedType() {
- return TYPE;
- }
-
- @Override
- protected void dispatch(VisibilityHandler handler) {
- handler.visibilityChanged(this);
- }
- }
-
- /**
- * The pixel size for OSX's invisible scrollbars.
- * <p>
- * Touch devices don't show a scrollbar at all, so the scrollbar size is
- * irrelevant in their case. There doesn't seem to be any other popular
- * platforms that has scrollbars similar to OSX. Thus, this behavior is
- * tailored for OSX only, until additional platforms start behaving this
- * way.
- */
- private static final int OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX = 13;
-
- /**
- * A representation of a single vertical scrollbar.
- *
- * @see VerticalScrollbarBundle#getElement()
- */
- public static final class VerticalScrollbarBundle extends ScrollbarBundle {
-
- @Override
- public void setStylePrimaryName(String primaryStyleName) {
- super.setStylePrimaryName(primaryStyleName);
- root.addClassName(primaryStyleName + "-scroller-vertical");
- }
-
- @Override
- protected void internalSetScrollPos(int px) {
- root.setScrollTop(px);
- }
-
- @Override
- protected int internalGetScrollPos() {
- return root.getScrollTop();
- }
-
- @Override
- protected void internalSetScrollSize(double px) {
- scrollSizeElement.getStyle().setHeight(px, Unit.PX);
- }
-
- @Override
- protected String internalGetScrollSize() {
- return scrollSizeElement.getStyle().getHeight();
- }
-
- @Override
- protected void internalSetOffsetSize(double px) {
- root.getStyle().setHeight(px, Unit.PX);
- }
-
- @Override
- public String internalGetOffsetSize() {
- return root.getStyle().getHeight();
- }
-
- @Override
- protected void internalSetScrollbarThickness(double px) {
- root.getStyle().setPaddingRight(px, Unit.PX);
- root.getStyle().setWidth(0, Unit.PX);
- scrollSizeElement.getStyle().setWidth(px, Unit.PX);
- }
-
- @Override
- protected String internalGetScrollbarThickness() {
- return scrollSizeElement.getStyle().getWidth();
- }
-
- @Override
- protected void internalForceScrollbar(boolean enable) {
- if (enable) {
- root.getStyle().setOverflowY(Overflow.SCROLL);
- } else {
- root.getStyle().clearOverflowY();
- }
- }
-
- @Override
- public Direction getDirection() {
- return Direction.VERTICAL;
- }
- }
-
- /**
- * A representation of a single horizontal scrollbar.
- *
- * @see HorizontalScrollbarBundle#getElement()
- */
- public static final class HorizontalScrollbarBundle
- extends ScrollbarBundle {
-
- @Override
- public void setStylePrimaryName(String primaryStyleName) {
- super.setStylePrimaryName(primaryStyleName);
- root.addClassName(primaryStyleName + "-scroller-horizontal");
- }
-
- @Override
- protected void internalSetScrollPos(int px) {
- root.setScrollLeft(px);
- }
-
- @Override
- protected int internalGetScrollPos() {
- return root.getScrollLeft();
- }
-
- @Override
- protected void internalSetScrollSize(double px) {
- scrollSizeElement.getStyle().setWidth(px, Unit.PX);
- }
-
- @Override
- protected String internalGetScrollSize() {
- return scrollSizeElement.getStyle().getWidth();
- }
-
- @Override
- protected void internalSetOffsetSize(double px) {
- root.getStyle().setWidth(px, Unit.PX);
- }
-
- @Override
- public String internalGetOffsetSize() {
- return root.getStyle().getWidth();
- }
-
- @Override
- protected void internalSetScrollbarThickness(double px) {
- root.getStyle().setPaddingBottom(px, Unit.PX);
- root.getStyle().setHeight(0, Unit.PX);
- scrollSizeElement.getStyle().setHeight(px, Unit.PX);
- }
-
- @Override
- protected String internalGetScrollbarThickness() {
- return scrollSizeElement.getStyle().getHeight();
- }
-
- @Override
- protected void internalForceScrollbar(boolean enable) {
- if (enable) {
- root.getStyle().setOverflowX(Overflow.SCROLL);
- } else {
- root.getStyle().clearOverflowX();
- }
- }
-
- @Override
- public Direction getDirection() {
- return Direction.HORIZONTAL;
- }
- }
-
- /** Root element for the scrollbar-composition. */
- protected final Element root = DOM.createDiv();
- /**
- * Scroll size element. The size of this element determines the number of
- * pixels the scrollbar is able to scroll through
- */
- protected final Element scrollSizeElement = DOM.createDiv();
- /** Is the scrollbar "invisible" (thickness set to 0). */
- protected boolean isInvisibleScrollbar = false;
-
- private double scrollPos = 0;
- private double maxScrollPos = 0;
-
- private boolean scrollHandleIsVisible = false;
-
- private boolean isLocked = false;
-
- /** @deprecated access via {@link #getHandlerManager()} instead. */
- @Deprecated
- private HandlerManager handlerManager;
-
- private TemporaryResizer invisibleScrollbarTemporaryResizer = new TemporaryResizer();
-
- private final ScrollEventFirer scrollEventFirer = new ScrollEventFirer();
-
- private HandlerRegistration scrollInProgress;
-
- private ScrollbarBundle() {
- root.appendChild(scrollSizeElement);
- root.getStyle().setDisplay(Display.NONE);
- root.setTabIndex(-1);
- }
-
- /**
- * Returns the width (for horizontal) or height (for vertical) css property
- * for the scroll size element.
- *
- * @return the relevant size property based on orientation
- */
- protected abstract String internalGetScrollSize();
-
- /**
- * Sets the primary style name.
- *
- * @param primaryStyleName
- * The primary style name to use
- */
- public void setStylePrimaryName(String primaryStyleName) {
- root.setClassName(primaryStyleName + "-scroller");
- }
-
- /**
- * Gets the root element of this scrollbar-composition.
- *
- * @return the root element
- */
- public final Element getElement() {
- return root;
- }
-
- /**
- * Modifies the scroll position of this scrollbar by a number of pixels.
- * <p>
- * <em>Note:</em> Even though {@code double} values are used, they are
- * currently only used as integers as large {@code int} (or small but fast
- * {@code long}). This means, all values are truncated to zero decimal
- * places.
- *
- * @param delta
- * the delta in pixels to change the scroll position by
- */
- public final void setScrollPosByDelta(double delta) {
- if (delta != 0) {
- setScrollPos(getScrollPos() + delta);
- }
- }
-
- /**
- * Modifies {@link #root root's} dimensions in the axis the scrollbar is
- * representing.
- *
- * @param px
- * the new size of {@link #root} in the dimension this scrollbar
- * is representing
- */
- protected abstract void internalSetOffsetSize(double px);
-
- /**
- * Sets the length of the scrollbar.
- *
- * @param px
- * the length of the scrollbar in pixels
- * @see #setOffsetSizeAndScrollSize(double, double)
- */
- public final void setOffsetSize(final double px) {
-
- boolean newOffsetSizeIsGreaterThanScrollSize = px > getScrollSize();
- boolean offsetSizeBecomesGreaterThanScrollSize = showsScrollHandle()
- && newOffsetSizeIsGreaterThanScrollSize;
-
- if (offsetSizeBecomesGreaterThanScrollSize && getScrollPos() != 0) {
- setScrollPos(0);
- setOffsetSizeNow(px);
- } else if (px != getOffsetSize()) {
- setOffsetSizeNow(px);
- }
- }
-
- private void setOffsetSizeNow(double px) {
- internalSetOffsetSize(Math.max(0, px));
- recalculateMaxScrollPos();
- forceScrollbar(showsScrollHandle());
- fireVisibilityChangeIfNeeded();
- }
-
- /**
- * Sets the length of the scrollbar and the amount of pixels the scrollbar
- * needs to be able to scroll through.
- *
- * @param offsetPx
- * the length of the scrollbar in pixels
- * @param scrollPx
- * the number of pixels the scrollbar should be able to scroll
- * through
- */
- public final void setOffsetSizeAndScrollSize(final double offsetPx,
- final double scrollPx) {
-
- boolean newOffsetSizeIsGreaterThanScrollSize = offsetPx > scrollPx;
- boolean offsetSizeBecomesGreaterThanScrollSize = showsScrollHandle()
- && newOffsetSizeIsGreaterThanScrollSize;
-
- boolean needsMoreHandling = false;
- if (offsetSizeBecomesGreaterThanScrollSize && getScrollPos() != 0) {
- setScrollPos(0);
- if (offsetPx != getOffsetSize()) {
- internalSetOffsetSize(Math.max(0, offsetPx));
- }
- if (scrollPx != getScrollSize()) {
- internalSetScrollSize(Math.max(0, scrollPx));
- }
- needsMoreHandling = true;
- } else {
- if (offsetPx != getOffsetSize()) {
- internalSetOffsetSize(Math.max(0, offsetPx));
- needsMoreHandling = true;
- }
- if (scrollPx != getScrollSize()) {
- internalSetScrollSize(Math.max(0, scrollPx));
- needsMoreHandling = true;
- }
- }
-
- if (needsMoreHandling) {
- recalculateMaxScrollPos();
- forceScrollbar(showsScrollHandle());
- fireVisibilityChangeIfNeeded();
- }
- }
-
- /**
- * Force the scrollbar to be visible with CSS. In practice, this means to
- * set either <code>overflow-x</code> or <code>overflow-y</code> to "
- * <code>scroll</code>" in the scrollbar's direction.
- * <p>
- * This method is an IE8 workaround, since it doesn't always show scrollbars
- * with <code>overflow: auto</code> enabled.
- * <p>
- * Firefox on the other hand loses pending scroll events when the scrollbar
- * is hidden, so the event must be fired manually.
- * <p>
- * When IE8 support is dropped, this should really be simplified.
- *
- * @param enable
- * {@code true} if the scrollbar should be forced to be visible,
- * {@code false} otherwise.
- */
- protected void forceScrollbar(boolean enable) {
- if (enable) {
- root.getStyle().clearDisplay();
- } else {
- if (BrowserInfo.get().isFirefox()) {
- /*
- * This is related to the Firefox workaround in setScrollSize
- * for setScrollPos(0)
- */
- scrollEventFirer.scheduleEvent();
- }
- root.getStyle().setDisplay(Display.NONE);
- }
- internalForceScrollbar(enable);
- }
-
- /**
- * Sets the overflow-x (for horizontal) or overflow-y (for vertical)
- * property to {@code "scroll"} if enabled, or clears the property if
- * disabled.
- *
- * @param enable
- * {@code true} if the overflow property should be set,
- * {@code false} otherwise.
- */
- protected abstract void internalForceScrollbar(boolean enable);
-
- /**
- * Gets the length of the scrollbar.
- *
- * @return the length of the scrollbar in pixels
- */
- public double getOffsetSize() {
- return parseCssDimensionToPixels(internalGetOffsetSize());
- }
-
- /**
- * Returns the width (for horizontal) or height (for vertical) css property
- * for the root element.
- *
- * @return the relevant size property based on orientation
- */
- public abstract String internalGetOffsetSize();
-
- /**
- * Sets the scroll position of the scrollbar in the axis the scrollbar is
- * representing.
- * <p>
- * <em>Note:</em> Even though {@code double} values are used, they are
- * currently only used as integers as large {@code int} (or small but fast
- * {@code long}). This means, all values are truncated to zero decimal
- * places.
- *
- * @param px
- * the new scroll position in pixels
- */
- public final void setScrollPos(double px) {
- if (isLocked()) {
- return;
- }
-
- double oldScrollPos = scrollPos;
- scrollPos = Math.max(0, Math.min(maxScrollPos, truncate(px)));
-
- if (!WidgetUtil.pixelValuesEqual(oldScrollPos, scrollPos)) {
- if (scrollInProgress == null) {
- // Only used for tracking that there is "workPending"
- scrollInProgress = addScrollHandler(event -> {
- scrollInProgress.removeHandler();
- scrollInProgress = null;
- });
- }
- if (isInvisibleScrollbar) {
- invisibleScrollbarTemporaryResizer.show();
- }
-
- /*
- * This is where the value needs to be converted into an integer no
- * matter how we flip it, since GWT expects an integer value.
- * There's no point making a JSNI method that accepts doubles as the
- * scroll position, since the browsers themselves don't support such
- * large numbers (as of today, 25.3.2014). This double-ranged is
- * only facilitating future virtual scrollbars.
- */
- internalSetScrollPos(toInt32(scrollPos));
- }
- }
-
- /**
- * Should be called whenever this bundle is attached to the DOM (typically,
- * from the onLoad of the containing widget). Used to ensure the DOM scroll
- * position is maintained when detaching and reattaching the bundle.
- *
- * @since 7.4.1
- */
- public void onLoad() {
- internalSetScrollPos(toInt32(scrollPos));
- }
-
- /**
- * Truncates a double such that no decimal places are retained.
- * <p>
- * E.g. {@code trunc(2.3d) == 2.0d} and {@code trunc(-2.3d) == -2.0d}.
- *
- * @param num
- * the double value to be truncated
- * @return the {@code num} value without any decimal digits
- */
- private static double truncate(double num) {
- if (num > 0) {
- return Math.floor(num);
- } else {
- return Math.ceil(num);
- }
- }
-
- /**
- * Modifies the element's scroll position (scrollTop or scrollLeft).
- * <p>
- * <em>Note:</em> The parameter here is a type of integer (instead of a
- * double) by design. The browsers internally convert all double values into
- * an integer value. To make this fact explicit, this API has chosen to
- * force integers already at this level.
- *
- * @param px
- * integer pixel value to scroll to
- */
- protected abstract void internalSetScrollPos(int px);
-
- /**
- * Gets the scroll position of the scrollbar in the axis the scrollbar is
- * representing.
- *
- * @return the new scroll position in pixels
- */
- public final double getScrollPos() {
- int internalScrollPos = internalGetScrollPos();
- assert Math.abs(internalScrollPos
- - toInt32(scrollPos)) <= 1 : "calculated scroll position ("
- + scrollPos
- + ") did not match the DOM element scroll position ("
- + internalScrollPos + ")";
- return scrollPos;
- }
-
- /**
- * Retrieves the element's scroll position (scrollTop or scrollLeft).
- * <p>
- * <em>Note:</em> The parameter here is a type of integer (instead of a
- * double) by design. The browsers internally convert all double values into
- * an integer value. To make this fact explicit, this API has chosen to
- * force integers already at this level.
- *
- * @return integer pixel value of the scroll position
- */
- protected abstract int internalGetScrollPos();
-
- /**
- * Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in
- * such a way that the scrollbar is able to scroll a certain number of
- * pixels in the axis it is representing.
- *
- * @param px
- * the new size of {@link #scrollSizeElement} in the dimension
- * this scrollbar is representing
- */
- protected abstract void internalSetScrollSize(double px);
-
- /**
- * Sets the amount of pixels the scrollbar needs to be able to scroll
- * through.
- *
- * @param px
- * the number of pixels the scrollbar should be able to scroll
- * through
- * @see #setOffsetSizeAndScrollSize(double, double)
- */
- public final void setScrollSize(final double px) {
-
- boolean newScrollSizeIsSmallerThanOffsetSize = px <= getOffsetSize();
- boolean scrollSizeBecomesSmallerThanOffsetSize = showsScrollHandle()
- && newScrollSizeIsSmallerThanOffsetSize;
-
- if (scrollSizeBecomesSmallerThanOffsetSize && getScrollPos() != 0) {
- setScrollPos(0);
- setScrollSizeNow(px);
- } else if (px != getScrollSize()) {
- setScrollSizeNow(px);
- }
- }
-
- private void setScrollSizeNow(double px) {
- internalSetScrollSize(Math.max(0, px));
- recalculateMaxScrollPos();
- forceScrollbar(showsScrollHandle());
- fireVisibilityChangeIfNeeded();
- }
-
- /**
- * Gets the amount of pixels the scrollbar needs to be able to scroll
- * through.
- *
- * @return the number of pixels the scrollbar should be able to scroll
- * through
- */
- public double getScrollSize() {
- return parseCssDimensionToPixels(internalGetScrollSize());
- }
-
- /**
- * Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in the
- * opposite axis to what the scrollbar is representing.
- *
- * @param px
- * the dimension that {@link #scrollSizeElement} should take in
- * the opposite axis to what the scrollbar is representing
- */
- protected abstract void internalSetScrollbarThickness(double px);
-
- /**
- * Sets the scrollbar's thickness.
- * <p>
- * If the thickness is set to 0, the scrollbar will be treated as an
- * "invisible" scrollbar. This means, the DOM structure will be given a
- * non-zero size, but {@link #getScrollbarThickness()} will still return the
- * value 0.
- *
- * @param px
- * the scrollbar's thickness in pixels
- */
- public final void setScrollbarThickness(double px) {
- isInvisibleScrollbar = (px == 0);
-
- if (isInvisibleScrollbar) {
- Event.sinkEvents(root, Event.ONSCROLL);
- Event.setEventListener(root,
- event -> invisibleScrollbarTemporaryResizer.show());
-
- root.getStyle().setVisibility(Visibility.HIDDEN);
- } else {
- Event.sinkEvents(root, 0);
- Event.setEventListener(root, null);
- root.getStyle().clearVisibility();
- }
-
- internalSetScrollbarThickness(Math.max(1d, px));
- }
-
- /**
- * Gets the scrollbar's thickness as defined in the DOM.
- *
- * @return the scrollbar's thickness as defined in the DOM, in pixels
- */
- protected abstract String internalGetScrollbarThickness();
-
- /**
- * Gets the scrollbar's thickness.
- * <p>
- * This value will differ from the value in the DOM, if the thickness was
- * set to 0 with {@link #setScrollbarThickness(double)}, as the scrollbar is
- * then treated as "invisible."
- *
- * @return the scrollbar's thickness in pixels
- */
- public final double getScrollbarThickness() {
- if (!isInvisibleScrollbar) {
- return parseCssDimensionToPixels(internalGetScrollbarThickness());
- } else {
- return 0;
- }
- }
-
- /**
- * Checks whether the scrollbar's handle is visible.
- * <p>
- * In other words, this method checks whether the contents is larger than
- * can visually fit in the element.
- *
- * @return <code>true</code> if the scrollbar's handle is visible
- */
- public boolean showsScrollHandle() {
- return getScrollSize() - getOffsetSize() > WidgetUtil.PIXEL_EPSILON;
- }
-
- /**
- * Calculates and sets maximum scroll position based on the current scroll
- * size and the scrollbar's length.
- */
- public void recalculateMaxScrollPos() {
- double scrollSize = getScrollSize();
- double offsetSize = getOffsetSize();
- maxScrollPos = Math.max(0, scrollSize - offsetSize);
-
- // make sure that the correct max scroll position is maintained.
- setScrollPos(scrollPos);
- }
-
- /**
- * This is a method that JSNI can call to synchronize the object state from
- * the DOM.
- */
- private final void updateScrollPosFromDom() {
-
- /*
- * TODO: this method probably shouldn't be called from Escalator's JSNI,
- * but probably could be handled internally by this listening to its own
- * element. Would clean up the code quite a bit. Needs further
- * investigation.
- */
-
- int newScrollPos = internalGetScrollPos();
- if (!isLocked()) {
- scrollPos = newScrollPos;
- scrollEventFirer.scheduleEvent();
- } else {
- if (scrollPos != newScrollPos) {
- // we need to actually undo the setting of the scroll.
- internalSetScrollPos(toInt32(scrollPos));
- }
- if (scrollInProgress != null) {
- // cancel the in-progress indicator
- scrollInProgress.removeHandler();
- scrollInProgress = null;
- }
- }
- }
-
- /**
- * Returns the handler manager for this scrollbar bundle.
- *
- * @return the handler manager
- */
- protected HandlerManager getHandlerManager() {
- if (handlerManager == null) {
- handlerManager = new HandlerManager(this);
- }
- return handlerManager;
- }
-
- /**
- * Adds handler for the scrollbar handle visibility.
- *
- * @param handler
- * the {@link VisibilityHandler} to add
- * @return {@link HandlerRegistration} used to remove the handler
- */
- public HandlerRegistration addVisibilityHandler(
- final VisibilityHandler handler) {
- return getHandlerManager().addHandler(VisibilityChangeEvent.TYPE,
- handler);
- }
-
- private void fireVisibilityChangeIfNeeded() {
- final boolean oldHandleIsVisible = scrollHandleIsVisible;
- scrollHandleIsVisible = showsScrollHandle();
- if (oldHandleIsVisible != scrollHandleIsVisible) {
- final VisibilityChangeEvent event = new VisibilityChangeEvent(
- scrollHandleIsVisible);
- getHandlerManager().fireEvent(event);
- }
- }
-
- /**
- * Converts a double into an integer by JavaScript's terms.
- * <p>
- * Implementation copied from {@link Element#toInt32(double)}.
- *
- * @param val
- * the double value to convert into an integer
- * @return the double value converted to an integer
- */
- private static native int toInt32(double val)
- /*-{
- return Math.round(val) | 0;
- }-*/;
-
- /**
- * Locks or unlocks the scrollbar bundle.
- * <p>
- * A locked scrollbar bundle will refuse to scroll, both programmatically
- * and via user-triggered events.
- *
- * @param isLocked
- * <code>true</code> to lock, <code>false</code> to unlock
- */
- public void setLocked(boolean isLocked) {
- this.isLocked = isLocked;
- }
-
- /**
- * Checks whether the scrollbar bundle is locked or not.
- *
- * @return <code>true</code> if the scrollbar bundle is locked
- */
- public boolean isLocked() {
- return isLocked;
- }
-
- /**
- * Returns the scroll direction of this scrollbar bundle.
- *
- * @return the scroll direction of this scrollbar bundle
- */
- public abstract Direction getDirection();
-
- /**
- * Adds a scroll handler to the scrollbar bundle.
- *
- * @param handler
- * the handler to add
- * @return the registration object for the handler registration
- */
- public HandlerRegistration addScrollHandler(final ScrollHandler handler) {
- return getHandlerManager().addHandler(ScrollEvent.TYPE, handler);
- }
-
- private static double parseCssDimensionToPixels(String size) {
-
- /*
- * Sizes of elements are calculated from CSS rather than
- * element.getOffset*() because those values are 0 whenever display:
- * none. Because we know that all elements have populated
- * CSS-dimensions, it's better to do it that way.
- *
- * Another solution would be to make the elements visible while
- * measuring and then re-hide them, but that would cause unnecessary
- * reflows that would probably kill the performance dead.
- */
-
- if (size.isEmpty()) {
- return 0;
- } else {
- assert size.endsWith("px") : "Can't parse CSS dimension \"" + size
- + "\"";
- return Double.parseDouble(size.substring(0, size.length() - 2));
- }
- }
-
- @Override
- public boolean isWorkPending() {
- // Need to include scrollEventFirer.isBeingFired as it might use
- // requestAnimationFrame - which is not automatically checked
- return scrollInProgress != null || scrollEventFirer.isBeingFired;
- }
- }
|