You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ScrollbarBundle.java 31KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913
  1. /*
  2. * Copyright 2000-2018 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.client.widget.escalator;
  17. import com.google.gwt.animation.client.AnimationScheduler;
  18. import com.google.gwt.animation.client.AnimationScheduler.AnimationSupportDetector;
  19. import com.google.gwt.core.client.Scheduler;
  20. import com.google.gwt.core.client.Scheduler.ScheduledCommand;
  21. import com.google.gwt.dom.client.Element;
  22. import com.google.gwt.dom.client.Style.Display;
  23. import com.google.gwt.dom.client.Style.Overflow;
  24. import com.google.gwt.dom.client.Style.Unit;
  25. import com.google.gwt.dom.client.Style.Visibility;
  26. import com.google.gwt.event.shared.EventHandler;
  27. import com.google.gwt.event.shared.GwtEvent;
  28. import com.google.gwt.event.shared.HandlerManager;
  29. import com.google.gwt.event.shared.HandlerRegistration;
  30. import com.google.gwt.user.client.DOM;
  31. import com.google.gwt.user.client.Event;
  32. import com.google.gwt.user.client.Timer;
  33. import com.vaadin.client.BrowserInfo;
  34. import com.vaadin.client.DeferredWorker;
  35. import com.vaadin.client.WidgetUtil;
  36. import com.vaadin.client.widget.grid.events.ScrollEvent;
  37. import com.vaadin.client.widget.grid.events.ScrollHandler;
  38. /**
  39. * An element-like bundle representing a configurable and visual scrollbar in
  40. * one axis.
  41. *
  42. * @since 7.4
  43. * @author Vaadin Ltd
  44. * @see VerticalScrollbarBundle
  45. * @see HorizontalScrollbarBundle
  46. */
  47. public abstract class ScrollbarBundle implements DeferredWorker {
  48. private static final boolean SUPPORTS_REQUEST_ANIMATION_FRAME = new AnimationSupportDetector()
  49. .isNativelySupported();
  50. private class ScrollEventFirer {
  51. private final ScheduledCommand fireEventCommand = () -> {
  52. /*
  53. * Some kind of native-scroll-event related asynchronous problem
  54. * occurs here (at least on desktops) where the internal bookkeeping
  55. * isn't up to date with the real scroll position. The weird thing
  56. * is, that happens only once, and if you drag scrollbar fast
  57. * enough. After it has failed once, it never fails again.
  58. *
  59. * Theory: the user drags the scrollbar, and this command is
  60. * executed before the browser has a chance to fire a scroll event
  61. * (which normally would correct this situation). This would explain
  62. * why slow scrolling doesn't trigger the problem, while fast
  63. * scrolling does.
  64. *
  65. * To make absolutely sure that we have the latest scroll position,
  66. * let's update the internal value.
  67. *
  68. * This might lead to a slight performance hit (on my computer it
  69. * was never more than 3ms on either of Chrome 38 or Firefox 31). It
  70. * also _slightly_ counteracts the purpose of the internal
  71. * bookkeeping. But since getScrollPos is called 3 times (on one
  72. * direction) per scroll loop, it's still better to have take this
  73. * small penalty than removing it altogether.
  74. */
  75. updateScrollPosFromDom();
  76. getHandlerManager().fireEvent(new ScrollEvent());
  77. isBeingFired = false;
  78. };
  79. private boolean isBeingFired;
  80. public void scheduleEvent() {
  81. if (!isBeingFired) {
  82. /*
  83. * We'll gather all the scroll events, and only fire once, once
  84. * everything has calmed down.
  85. */
  86. if (SUPPORTS_REQUEST_ANIMATION_FRAME) {
  87. // Chrome MUST use this as deferred commands will sometimes
  88. // be run with a 300+ ms delay when scrolling.
  89. AnimationScheduler.get().requestAnimationFrame(
  90. timestamp -> fireEventCommand.execute());
  91. } else {
  92. // Does not support requestAnimationFrame and the fallback
  93. // uses a delay of 16ms, we stick to the old deferred
  94. // command which uses a delay of 0ms
  95. Scheduler.get().scheduleDeferred(fireEventCommand);
  96. }
  97. isBeingFired = true;
  98. }
  99. }
  100. }
  101. /**
  102. * The orientation of the scrollbar.
  103. */
  104. public enum Direction {
  105. VERTICAL, HORIZONTAL;
  106. }
  107. private class TemporaryResizer {
  108. private static final int TEMPORARY_RESIZE_DELAY = 1000;
  109. private final Timer timer = new Timer() {
  110. @Override
  111. public void run() {
  112. internalSetScrollbarThickness(1);
  113. root.getStyle().setVisibility(Visibility.HIDDEN);
  114. }
  115. };
  116. public void show() {
  117. internalSetScrollbarThickness(OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX);
  118. root.getStyle().setVisibility(Visibility.VISIBLE);
  119. timer.schedule(TEMPORARY_RESIZE_DELAY);
  120. }
  121. }
  122. /**
  123. * A means to listen to when the scrollbar handle in a
  124. * {@link ScrollbarBundle} either appears or is removed.
  125. */
  126. public interface VisibilityHandler extends EventHandler {
  127. /**
  128. * This method is called whenever the scrollbar handle's visibility is
  129. * changed in a {@link ScrollbarBundle}.
  130. *
  131. * @param event
  132. * the {@link VisibilityChangeEvent}
  133. */
  134. void visibilityChanged(VisibilityChangeEvent event);
  135. }
  136. public static class VisibilityChangeEvent
  137. extends GwtEvent<VisibilityHandler> {
  138. public static final Type<VisibilityHandler> TYPE = new Type<ScrollbarBundle.VisibilityHandler>() {
  139. @Override
  140. public String toString() {
  141. return "VisibilityChangeEvent";
  142. }
  143. };
  144. private final boolean isScrollerVisible;
  145. private VisibilityChangeEvent(boolean isScrollerVisible) {
  146. this.isScrollerVisible = isScrollerVisible;
  147. }
  148. /**
  149. * Checks whether the scroll handle is currently visible or not.
  150. *
  151. * @return <code>true</code> if the scroll handle is currently visible.
  152. * <code>false</code> if not.
  153. */
  154. public boolean isScrollerVisible() {
  155. return isScrollerVisible;
  156. }
  157. @Override
  158. public Type<VisibilityHandler> getAssociatedType() {
  159. return TYPE;
  160. }
  161. @Override
  162. protected void dispatch(VisibilityHandler handler) {
  163. handler.visibilityChanged(this);
  164. }
  165. }
  166. /**
  167. * The pixel size for OSX's invisible scrollbars.
  168. * <p>
  169. * Touch devices don't show a scrollbar at all, so the scrollbar size is
  170. * irrelevant in their case. There doesn't seem to be any other popular
  171. * platforms that has scrollbars similar to OSX. Thus, this behavior is
  172. * tailored for OSX only, until additional platforms start behaving this
  173. * way.
  174. */
  175. private static final int OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX = 13;
  176. /**
  177. * A representation of a single vertical scrollbar.
  178. *
  179. * @see VerticalScrollbarBundle#getElement()
  180. */
  181. public static final class VerticalScrollbarBundle extends ScrollbarBundle {
  182. @Override
  183. public void setStylePrimaryName(String primaryStyleName) {
  184. super.setStylePrimaryName(primaryStyleName);
  185. root.addClassName(primaryStyleName + "-scroller-vertical");
  186. }
  187. @Override
  188. protected void internalSetScrollPos(int px) {
  189. root.setScrollTop(px);
  190. }
  191. @Override
  192. protected int internalGetScrollPos() {
  193. return root.getScrollTop();
  194. }
  195. @Override
  196. protected void internalSetScrollSize(double px) {
  197. scrollSizeElement.getStyle().setHeight(px, Unit.PX);
  198. }
  199. @Override
  200. protected String internalGetScrollSize() {
  201. return scrollSizeElement.getStyle().getHeight();
  202. }
  203. @Override
  204. protected void internalSetOffsetSize(double px) {
  205. root.getStyle().setHeight(px, Unit.PX);
  206. }
  207. @Override
  208. public String internalGetOffsetSize() {
  209. return root.getStyle().getHeight();
  210. }
  211. @Override
  212. protected void internalSetScrollbarThickness(double px) {
  213. root.getStyle().setPaddingRight(px, Unit.PX);
  214. root.getStyle().setWidth(0, Unit.PX);
  215. scrollSizeElement.getStyle().setWidth(px, Unit.PX);
  216. }
  217. @Override
  218. protected String internalGetScrollbarThickness() {
  219. return scrollSizeElement.getStyle().getWidth();
  220. }
  221. @Override
  222. protected void internalForceScrollbar(boolean enable) {
  223. if (enable) {
  224. root.getStyle().setOverflowY(Overflow.SCROLL);
  225. } else {
  226. root.getStyle().clearOverflowY();
  227. }
  228. }
  229. @Override
  230. public Direction getDirection() {
  231. return Direction.VERTICAL;
  232. }
  233. }
  234. /**
  235. * A representation of a single horizontal scrollbar.
  236. *
  237. * @see HorizontalScrollbarBundle#getElement()
  238. */
  239. public static final class HorizontalScrollbarBundle
  240. extends ScrollbarBundle {
  241. @Override
  242. public void setStylePrimaryName(String primaryStyleName) {
  243. super.setStylePrimaryName(primaryStyleName);
  244. root.addClassName(primaryStyleName + "-scroller-horizontal");
  245. }
  246. @Override
  247. protected void internalSetScrollPos(int px) {
  248. root.setScrollLeft(px);
  249. }
  250. @Override
  251. protected int internalGetScrollPos() {
  252. return root.getScrollLeft();
  253. }
  254. @Override
  255. protected void internalSetScrollSize(double px) {
  256. scrollSizeElement.getStyle().setWidth(px, Unit.PX);
  257. }
  258. @Override
  259. protected String internalGetScrollSize() {
  260. return scrollSizeElement.getStyle().getWidth();
  261. }
  262. @Override
  263. protected void internalSetOffsetSize(double px) {
  264. root.getStyle().setWidth(px, Unit.PX);
  265. }
  266. @Override
  267. public String internalGetOffsetSize() {
  268. return root.getStyle().getWidth();
  269. }
  270. @Override
  271. protected void internalSetScrollbarThickness(double px) {
  272. root.getStyle().setPaddingBottom(px, Unit.PX);
  273. root.getStyle().setHeight(0, Unit.PX);
  274. scrollSizeElement.getStyle().setHeight(px, Unit.PX);
  275. }
  276. @Override
  277. protected String internalGetScrollbarThickness() {
  278. return scrollSizeElement.getStyle().getHeight();
  279. }
  280. @Override
  281. protected void internalForceScrollbar(boolean enable) {
  282. if (enable) {
  283. root.getStyle().setOverflowX(Overflow.SCROLL);
  284. } else {
  285. root.getStyle().clearOverflowX();
  286. }
  287. }
  288. @Override
  289. public Direction getDirection() {
  290. return Direction.HORIZONTAL;
  291. }
  292. }
  293. protected final Element root = DOM.createDiv();
  294. protected final Element scrollSizeElement = DOM.createDiv();
  295. protected boolean isInvisibleScrollbar = false;
  296. private double scrollPos = 0;
  297. private double maxScrollPos = 0;
  298. private boolean scrollHandleIsVisible = false;
  299. private boolean isLocked = false;
  300. /** @deprecated access via {@link #getHandlerManager()} instead. */
  301. @Deprecated
  302. private HandlerManager handlerManager;
  303. private TemporaryResizer invisibleScrollbarTemporaryResizer = new TemporaryResizer();
  304. private final ScrollEventFirer scrollEventFirer = new ScrollEventFirer();
  305. private HandlerRegistration scrollSizeTemporaryScrollHandler;
  306. private HandlerRegistration offsetSizeTemporaryScrollHandler;
  307. private HandlerRegistration scrollInProgress;
  308. private ScrollbarBundle() {
  309. root.appendChild(scrollSizeElement);
  310. root.getStyle().setDisplay(Display.NONE);
  311. root.setTabIndex(-1);
  312. }
  313. protected abstract String internalGetScrollSize();
  314. /**
  315. * Sets the primary style name.
  316. *
  317. * @param primaryStyleName
  318. * The primary style name to use
  319. */
  320. public void setStylePrimaryName(String primaryStyleName) {
  321. root.setClassName(primaryStyleName + "-scroller");
  322. }
  323. /**
  324. * Gets the root element of this scrollbar-composition.
  325. *
  326. * @return the root element
  327. */
  328. public final Element getElement() {
  329. return root;
  330. }
  331. /**
  332. * Modifies the scroll position of this scrollbar by a number of pixels.
  333. * <p>
  334. * <em>Note:</em> Even though {@code double} values are used, they are
  335. * currently only used as integers as large {@code int} (or small but fast
  336. * {@code long}). This means, all values are truncated to zero decimal
  337. * places.
  338. *
  339. * @param delta
  340. * the delta in pixels to change the scroll position by
  341. */
  342. public final void setScrollPosByDelta(double delta) {
  343. if (delta != 0) {
  344. setScrollPos(getScrollPos() + delta);
  345. }
  346. }
  347. /**
  348. * Modifies {@link #root root's} dimensions in the axis the scrollbar is
  349. * representing.
  350. *
  351. * @param px
  352. * the new size of {@link #root} in the dimension this scrollbar
  353. * is representing
  354. */
  355. protected abstract void internalSetOffsetSize(double px);
  356. /**
  357. * Sets the length of the scrollbar.
  358. *
  359. * @param px
  360. * the length of the scrollbar in pixels
  361. */
  362. public final void setOffsetSize(final double px) {
  363. /*
  364. * This needs to be made step-by-step because IE8 flat-out refuses to
  365. * fire a scroll event when the scroll size becomes smaller than the
  366. * offset size. All other browser need to suffer alongside.
  367. */
  368. boolean newOffsetSizeIsGreaterThanScrollSize = px > getScrollSize();
  369. boolean offsetSizeBecomesGreaterThanScrollSize = showsScrollHandle()
  370. && newOffsetSizeIsGreaterThanScrollSize;
  371. if (offsetSizeBecomesGreaterThanScrollSize && getScrollPos() != 0) {
  372. if (offsetSizeTemporaryScrollHandler != null) {
  373. offsetSizeTemporaryScrollHandler.removeHandler();
  374. }
  375. // must be a field because Java insists.
  376. offsetSizeTemporaryScrollHandler = addScrollHandler(
  377. event -> setOffsetSizeNow(px));
  378. setScrollPos(0);
  379. } else {
  380. setOffsetSizeNow(px);
  381. }
  382. }
  383. private void setOffsetSizeNow(double px) {
  384. internalSetOffsetSize(Math.max(0, px));
  385. recalculateMaxScrollPos();
  386. forceScrollbar(showsScrollHandle());
  387. fireVisibilityChangeIfNeeded();
  388. if (offsetSizeTemporaryScrollHandler != null) {
  389. offsetSizeTemporaryScrollHandler.removeHandler();
  390. offsetSizeTemporaryScrollHandler = null;
  391. }
  392. }
  393. /**
  394. * Force the scrollbar to be visible with CSS. In practice, this means to
  395. * set either <code>overflow-x</code> or <code>overflow-y</code> to "
  396. * <code>scroll</code>" in the scrollbar's direction.
  397. * <p>
  398. * This method is an IE8 workaround, since it doesn't always show scrollbars
  399. * with <code>overflow: auto</code> enabled.
  400. * <p>
  401. * Firefox on the other hand loses pending scroll events when the scrollbar
  402. * is hidden, so the event must be fired manually.
  403. * <p>
  404. * When IE8 support is dropped, this should really be simplified.
  405. */
  406. protected void forceScrollbar(boolean enable) {
  407. if (enable) {
  408. root.getStyle().clearDisplay();
  409. } else {
  410. if (BrowserInfo.get().isFirefox()) {
  411. /*
  412. * This is related to the Firefox workaround in setScrollSize
  413. * for setScrollPos(0)
  414. */
  415. scrollEventFirer.scheduleEvent();
  416. }
  417. root.getStyle().setDisplay(Display.NONE);
  418. }
  419. internalForceScrollbar(enable);
  420. }
  421. protected abstract void internalForceScrollbar(boolean enable);
  422. /**
  423. * Gets the length of the scrollbar.
  424. *
  425. * @return the length of the scrollbar in pixels
  426. */
  427. public double getOffsetSize() {
  428. return parseCssDimensionToPixels(internalGetOffsetSize());
  429. }
  430. public abstract String internalGetOffsetSize();
  431. /**
  432. * Sets the scroll position of the scrollbar in the axis the scrollbar is
  433. * representing.
  434. * <p>
  435. * <em>Note:</em> Even though {@code double} values are used, they are
  436. * currently only used as integers as large {@code int} (or small but fast
  437. * {@code long}). This means, all values are truncated to zero decimal
  438. * places.
  439. *
  440. * @param px
  441. * the new scroll position in pixels
  442. */
  443. public final void setScrollPos(double px) {
  444. if (isLocked()) {
  445. return;
  446. }
  447. double oldScrollPos = scrollPos;
  448. scrollPos = Math.max(0, Math.min(maxScrollPos, truncate(px)));
  449. if (!WidgetUtil.pixelValuesEqual(oldScrollPos, scrollPos)) {
  450. if (scrollInProgress == null) {
  451. // Only used for tracking that there is "workPending"
  452. scrollInProgress = addScrollHandler(event -> {
  453. scrollInProgress.removeHandler();
  454. scrollInProgress = null;
  455. });
  456. }
  457. if (isInvisibleScrollbar) {
  458. invisibleScrollbarTemporaryResizer.show();
  459. }
  460. /*
  461. * This is where the value needs to be converted into an integer no
  462. * matter how we flip it, since GWT expects an integer value.
  463. * There's no point making a JSNI method that accepts doubles as the
  464. * scroll position, since the browsers themselves don't support such
  465. * large numbers (as of today, 25.3.2014). This double-ranged is
  466. * only facilitating future virtual scrollbars.
  467. */
  468. internalSetScrollPos(toInt32(scrollPos));
  469. }
  470. }
  471. /**
  472. * Should be called whenever this bundle is attached to the DOM (typically,
  473. * from the onLoad of the containing widget). Used to ensure the DOM scroll
  474. * position is maintained when detaching and reattaching the bundle.
  475. *
  476. * @since 7.4.1
  477. */
  478. public void onLoad() {
  479. internalSetScrollPos(toInt32(scrollPos));
  480. }
  481. /**
  482. * Truncates a double such that no decimal places are retained.
  483. * <p>
  484. * E.g. {@code trunc(2.3d) == 2.0d} and {@code trunc(-2.3d) == -2.0d}.
  485. *
  486. * @param num
  487. * the double value to be truncated
  488. * @return the {@code num} value without any decimal digits
  489. */
  490. private static double truncate(double num) {
  491. if (num > 0) {
  492. return Math.floor(num);
  493. } else {
  494. return Math.ceil(num);
  495. }
  496. }
  497. /**
  498. * Modifies the element's scroll position (scrollTop or scrollLeft).
  499. * <p>
  500. * <em>Note:</em> The parameter here is a type of integer (instead of a
  501. * double) by design. The browsers internally convert all double values into
  502. * an integer value. To make this fact explicit, this API has chosen to
  503. * force integers already at this level.
  504. *
  505. * @param px
  506. * integer pixel value to scroll to
  507. */
  508. protected abstract void internalSetScrollPos(int px);
  509. /**
  510. * Gets the scroll position of the scrollbar in the axis the scrollbar is
  511. * representing.
  512. *
  513. * @return the new scroll position in pixels
  514. */
  515. public final double getScrollPos() {
  516. int internalScrollPos = internalGetScrollPos();
  517. assert Math.abs(internalScrollPos
  518. - toInt32(scrollPos)) <= 1 : "calculated scroll position ("
  519. + scrollPos
  520. + ") did not match the DOM element scroll position ("
  521. + internalScrollPos + ")";
  522. return scrollPos;
  523. }
  524. /**
  525. * Retrieves the element's scroll position (scrollTop or scrollLeft).
  526. * <p>
  527. * <em>Note:</em> The parameter here is a type of integer (instead of a
  528. * double) by design. The browsers internally convert all double values into
  529. * an integer value. To make this fact explicit, this API has chosen to
  530. * force integers already at this level.
  531. *
  532. * @return integer pixel value of the scroll position
  533. */
  534. protected abstract int internalGetScrollPos();
  535. /**
  536. * Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in
  537. * such a way that the scrollbar is able to scroll a certain number of
  538. * pixels in the axis it is representing.
  539. *
  540. * @param px
  541. * the new size of {@link #scrollSizeElement} in the dimension
  542. * this scrollbar is representing
  543. */
  544. protected abstract void internalSetScrollSize(double px);
  545. /**
  546. * Sets the amount of pixels the scrollbar needs to be able to scroll
  547. * through.
  548. *
  549. * @param px
  550. * the number of pixels the scrollbar should be able to scroll
  551. * through
  552. */
  553. public final void setScrollSize(final double px) {
  554. /*
  555. * This needs to be made step-by-step because IE8 flat-out refuses to
  556. * fire a scroll event when the scroll size becomes smaller than the
  557. * offset size. All other browser need to suffer alongside.
  558. *
  559. * This really should be changed to not use any temporary scroll
  560. * handlers at all once IE8 support is dropped, like now done only for
  561. * Firefox.
  562. */
  563. boolean newScrollSizeIsSmallerThanOffsetSize = px <= getOffsetSize();
  564. boolean scrollSizeBecomesSmallerThanOffsetSize = showsScrollHandle()
  565. && newScrollSizeIsSmallerThanOffsetSize;
  566. if (scrollSizeBecomesSmallerThanOffsetSize && getScrollPos() != 0) {
  567. /*
  568. * For whatever reason, Firefox loses the scroll event in this case
  569. * and the onscroll handler is never called (happens when reducing
  570. * size from 1000 items to 1 while being scrolled a bit down, see
  571. * #19802). Based on the comment above, only IE8 should really use
  572. * 'delayedSizeSet'
  573. */
  574. boolean delayedSizeSet = !BrowserInfo.get().isFirefox();
  575. if (delayedSizeSet) {
  576. if (scrollSizeTemporaryScrollHandler != null) {
  577. scrollSizeTemporaryScrollHandler.removeHandler();
  578. }
  579. scrollSizeTemporaryScrollHandler = addScrollHandler(
  580. event -> setScrollSizeNow(px));
  581. }
  582. setScrollPos(0);
  583. if (!delayedSizeSet) {
  584. setScrollSizeNow(px);
  585. }
  586. } else {
  587. setScrollSizeNow(px);
  588. }
  589. }
  590. private void setScrollSizeNow(double px) {
  591. internalSetScrollSize(Math.max(0, px));
  592. recalculateMaxScrollPos();
  593. forceScrollbar(showsScrollHandle());
  594. fireVisibilityChangeIfNeeded();
  595. if (scrollSizeTemporaryScrollHandler != null) {
  596. scrollSizeTemporaryScrollHandler.removeHandler();
  597. scrollSizeTemporaryScrollHandler = null;
  598. }
  599. }
  600. /**
  601. * Gets the amount of pixels the scrollbar needs to be able to scroll
  602. * through.
  603. *
  604. * @return the number of pixels the scrollbar should be able to scroll
  605. * through
  606. */
  607. public double getScrollSize() {
  608. return parseCssDimensionToPixels(internalGetScrollSize());
  609. }
  610. /**
  611. * Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in the
  612. * opposite axis to what the scrollbar is representing.
  613. *
  614. * @param px
  615. * the dimension that {@link #scrollSizeElement} should take in
  616. * the opposite axis to what the scrollbar is representing
  617. */
  618. protected abstract void internalSetScrollbarThickness(double px);
  619. /**
  620. * Sets the scrollbar's thickness.
  621. * <p>
  622. * If the thickness is set to 0, the scrollbar will be treated as an
  623. * "invisible" scrollbar. This means, the DOM structure will be given a
  624. * non-zero size, but {@link #getScrollbarThickness()} will still return the
  625. * value 0.
  626. *
  627. * @param px
  628. * the scrollbar's thickness in pixels
  629. */
  630. public final void setScrollbarThickness(double px) {
  631. isInvisibleScrollbar = (px == 0);
  632. if (isInvisibleScrollbar) {
  633. Event.sinkEvents(root, Event.ONSCROLL);
  634. Event.setEventListener(root,
  635. event -> invisibleScrollbarTemporaryResizer.show());
  636. root.getStyle().setVisibility(Visibility.HIDDEN);
  637. } else {
  638. Event.sinkEvents(root, 0);
  639. Event.setEventListener(root, null);
  640. root.getStyle().clearVisibility();
  641. }
  642. internalSetScrollbarThickness(Math.max(1d, px));
  643. }
  644. /**
  645. * Gets the scrollbar's thickness as defined in the DOM.
  646. *
  647. * @return the scrollbar's thickness as defined in the DOM, in pixels
  648. */
  649. protected abstract String internalGetScrollbarThickness();
  650. /**
  651. * Gets the scrollbar's thickness.
  652. * <p>
  653. * This value will differ from the value in the DOM, if the thickness was
  654. * set to 0 with {@link #setScrollbarThickness(double)}, as the scrollbar is
  655. * then treated as "invisible."
  656. *
  657. * @return the scrollbar's thickness in pixels
  658. */
  659. public final double getScrollbarThickness() {
  660. if (!isInvisibleScrollbar) {
  661. return parseCssDimensionToPixels(internalGetScrollbarThickness());
  662. } else {
  663. return 0;
  664. }
  665. }
  666. /**
  667. * Checks whether the scrollbar's handle is visible.
  668. * <p>
  669. * In other words, this method checks whether the contents is larger than
  670. * can visually fit in the element.
  671. *
  672. * @return <code>true</code> if the scrollbar's handle is visible
  673. */
  674. public boolean showsScrollHandle() {
  675. return getScrollSize() - getOffsetSize() > WidgetUtil.PIXEL_EPSILON;
  676. }
  677. public void recalculateMaxScrollPos() {
  678. double scrollSize = getScrollSize();
  679. double offsetSize = getOffsetSize();
  680. maxScrollPos = Math.max(0, scrollSize - offsetSize);
  681. // make sure that the correct max scroll position is maintained.
  682. setScrollPos(scrollPos);
  683. }
  684. /**
  685. * This is a method that JSNI can call to synchronize the object state from
  686. * the DOM.
  687. */
  688. private final void updateScrollPosFromDom() {
  689. /*
  690. * TODO: this method probably shouldn't be called from Escalator's JSNI,
  691. * but probably could be handled internally by this listening to its own
  692. * element. Would clean up the code quite a bit. Needs further
  693. * investigation.
  694. */
  695. int newScrollPos = internalGetScrollPos();
  696. if (!isLocked()) {
  697. scrollPos = newScrollPos;
  698. scrollEventFirer.scheduleEvent();
  699. } else if (scrollPos != newScrollPos) {
  700. // we need to actually undo the setting of the scroll.
  701. internalSetScrollPos(toInt32(scrollPos));
  702. }
  703. }
  704. protected HandlerManager getHandlerManager() {
  705. if (handlerManager == null) {
  706. handlerManager = new HandlerManager(this);
  707. }
  708. return handlerManager;
  709. }
  710. /**
  711. * Adds handler for the scrollbar handle visibility.
  712. *
  713. * @param handler
  714. * the {@link VisibilityHandler} to add
  715. * @return {@link HandlerRegistration} used to remove the handler
  716. */
  717. public HandlerRegistration addVisibilityHandler(
  718. final VisibilityHandler handler) {
  719. return getHandlerManager().addHandler(VisibilityChangeEvent.TYPE,
  720. handler);
  721. }
  722. private void fireVisibilityChangeIfNeeded() {
  723. final boolean oldHandleIsVisible = scrollHandleIsVisible;
  724. scrollHandleIsVisible = showsScrollHandle();
  725. if (oldHandleIsVisible != scrollHandleIsVisible) {
  726. final VisibilityChangeEvent event = new VisibilityChangeEvent(
  727. scrollHandleIsVisible);
  728. getHandlerManager().fireEvent(event);
  729. }
  730. }
  731. /**
  732. * Converts a double into an integer by JavaScript's terms.
  733. * <p>
  734. * Implementation copied from {@link Element#toInt32(double)}.
  735. *
  736. * @param val
  737. * the double value to convert into an integer
  738. * @return the double value converted to an integer
  739. */
  740. private static native int toInt32(double val)
  741. /*-{
  742. return Math.round(val) | 0;
  743. }-*/;
  744. /**
  745. * Locks or unlocks the scrollbar bundle.
  746. * <p>
  747. * A locked scrollbar bundle will refuse to scroll, both programmatically
  748. * and via user-triggered events.
  749. *
  750. * @param isLocked
  751. * <code>true</code> to lock, <code>false</code> to unlock
  752. */
  753. public void setLocked(boolean isLocked) {
  754. this.isLocked = isLocked;
  755. }
  756. /**
  757. * Checks whether the scrollbar bundle is locked or not.
  758. *
  759. * @return <code>true</code> if the scrollbar bundle is locked
  760. */
  761. public boolean isLocked() {
  762. return isLocked;
  763. }
  764. /**
  765. * Returns the scroll direction of this scrollbar bundle.
  766. *
  767. * @return the scroll direction of this scrollbar bundle
  768. */
  769. public abstract Direction getDirection();
  770. /**
  771. * Adds a scroll handler to the scrollbar bundle.
  772. *
  773. * @param handler
  774. * the handler to add
  775. * @return the registration object for the handler registration
  776. */
  777. public HandlerRegistration addScrollHandler(final ScrollHandler handler) {
  778. return getHandlerManager().addHandler(ScrollEvent.TYPE, handler);
  779. }
  780. private static double parseCssDimensionToPixels(String size) {
  781. /*
  782. * Sizes of elements are calculated from CSS rather than
  783. * element.getOffset*() because those values are 0 whenever display:
  784. * none. Because we know that all elements have populated
  785. * CSS-dimensions, it's better to do it that way.
  786. *
  787. * Another solution would be to make the elements visible while
  788. * measuring and then re-hide them, but that would cause unnecessary
  789. * reflows that would probably kill the performance dead.
  790. */
  791. if (size.isEmpty()) {
  792. return 0;
  793. } else {
  794. assert size.endsWith("px") : "Can't parse CSS dimension \"" + size
  795. + "\"";
  796. return Double.parseDouble(size.substring(0, size.length() - 2));
  797. }
  798. }
  799. @Override
  800. public boolean isWorkPending() {
  801. // Need to include scrollEventFirer.isBeingFired as it might use
  802. // requestAnimationFrame - which is not automatically checked
  803. return scrollSizeTemporaryScrollHandler != null
  804. || offsetSizeTemporaryScrollHandler != null
  805. || scrollInProgress != null || scrollEventFirer.isBeingFired;
  806. }
  807. }