| /* |
| * Copyright 2011 Google Inc. |
| * |
| * 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.gwt.user.client.ui; |
| |
| import com.google.gwt.core.client.Duration; |
| import com.google.gwt.core.client.GWT; |
| import com.google.gwt.core.client.Scheduler; |
| import com.google.gwt.core.client.Scheduler.ScheduledCommand; |
| import com.google.gwt.dom.client.Document; |
| import com.google.gwt.dom.client.Element; |
| import com.google.gwt.dom.client.Style.Overflow; |
| import com.google.gwt.dom.client.Style.Unit; |
| import com.google.gwt.event.dom.client.ScrollEvent; |
| import com.google.gwt.event.dom.client.ScrollHandler; |
| import com.google.gwt.event.shared.HandlerRegistration; |
| import com.google.gwt.layout.client.Layout; |
| import com.google.gwt.layout.client.Layout.Layer; |
| import com.google.gwt.resources.client.ClientBundle; |
| import com.google.gwt.resources.client.CommonResources; |
| import com.google.gwt.resources.client.CssResource; |
| import com.google.gwt.resources.client.CssResource.ImportedWithPrefix; |
| import com.google.gwt.user.client.DOM; |
| import com.google.gwt.user.client.Event; |
| |
| /** |
| * A custom version of the {@link ScrollPanel} that allows user provided |
| * scrollbars. |
| * |
| * <p> |
| * The postion of scrollbars in a {@link CustomScrollPanel} differs from that of |
| * a native scrollable element. In a native element, scrollbars appear adjacent |
| * to the content, shrinking the content client height and width when they |
| * appear. {@link CustomScrollPanel} instead overlays scrollbars on top of the |
| * content, so the content does not change size when scrollbars appear. If the |
| * scrollbars obscures the content, you can set the <code>padding-top</code> and |
| * <code>padding-bottom</code> of the content to shift the content out from |
| * under the scrollbars. |
| * </p> |
| * |
| * <p> |
| * NOTE: Unlike {@link ScrollPanel}, which implements {@link RequiresResize} but |
| * doesn't really require it, {@link CustomScrollPanel} actually does require |
| * resize and should only be added to a panel that implements |
| * {@link ProvidesResize}, such as most layout panels and |
| * {@link ResizeLayoutPanel}. |
| * </p> |
| */ |
| public class CustomScrollPanel extends ScrollPanel { |
| |
| /** |
| * A ClientBundle of resources used by this widget. |
| */ |
| public interface Resources extends ClientBundle { |
| /** |
| * The styles used in this widget. |
| */ |
| @Source(Style.DEFAULT_CSS) |
| Style customScrollPanelStyle(); |
| } |
| |
| /** |
| * Styles used by this widget. |
| */ |
| @ImportedWithPrefix("gwt-CustomScrollPanel") |
| public interface Style extends CssResource { |
| /** |
| * The path to the default CSS styles used by this resource. |
| */ |
| String DEFAULT_CSS = "com/google/gwt/user/client/ui/CustomScrollPanel.css"; |
| |
| /** |
| * Applied to the widget. |
| */ |
| String customScrollPanel(); |
| |
| /** |
| * Applied to the square that appears in the bottom corner where the |
| * vertical and horizontal scrollbars meet, when both are visible. |
| */ |
| String customScrollPanelCorner(); |
| } |
| |
| private static Resources DEFAULT_RESOURCES; |
| |
| /** |
| * The timeout to ignore scroll events after updating the scroll position. |
| * Some browsers queue up scroll events and fire them after a delay. So, if |
| * the user quickly scrolls from position 0 to 100 to 200, the scroll event |
| * will fire for position 100 after the scroller has already moved to position |
| * 200. If we do not ignore the scroll events, we can end up with a loop where |
| * the scrollbars update the scroll position, and vice versa. |
| */ |
| private static int IGNORE_SCROLL_TIMEOUT = 500; |
| |
| /** |
| * Get the default {@link Resources} for this widget. |
| */ |
| private static Resources getDefaultResources() { |
| if (DEFAULT_RESOURCES == null) { |
| DEFAULT_RESOURCES = GWT.create(Resources.class); |
| } |
| return DEFAULT_RESOURCES; |
| } |
| |
| private boolean alwaysShowScrollbars; |
| private final ResizeLayoutPanel.Impl containerResizeImpl = GWT |
| .create(ResizeLayoutPanel.Impl.class); |
| private final Element cornerElem; |
| private final Layer cornerLayer; |
| private double ignoreContentUntil = 0; |
| private double ignoreScrollbarsUntil = 0; |
| private final Layout layout; |
| private final Layer scrollableLayer; |
| |
| // Information about the horizontal scrollbar. |
| private HorizontalScrollbar hScrollbar; |
| private int hScrollbarHeight; |
| private HandlerRegistration hScrollbarHandler; |
| private Layer hScrollbarLayer; |
| |
| // Information about the vertical scrollbar. |
| private VerticalScrollbar vScrollbar; |
| private int vScrollbarWidth; |
| private HandlerRegistration vScrollbarHandler; |
| private Layer vScrollbarLayer; |
| |
| /** |
| * Creates an empty {@link CustomScrollPanel}. |
| */ |
| public CustomScrollPanel() { |
| this(getDefaultResources()); |
| } |
| |
| public CustomScrollPanel(Resources resources) { |
| super(DOM.createDiv(), DOM.createDiv(), DOM.createDiv()); |
| |
| // Inject the styles used by this widget. |
| Style style = resources.customScrollPanelStyle(); |
| style.ensureInjected(); |
| setStyleName(style.customScrollPanel()); |
| |
| // Initialize the layout implementation. |
| layout = new Layout(getElement()); |
| |
| /* |
| * Apply the inline block style to the container element so it resizes with |
| * the content. |
| */ |
| Element containerElem = getContainerElement(); |
| containerElem.setClassName(CommonResources.getInlineBlockStyle()); |
| |
| /* |
| * Attach the scrollable element with the container. The scrollable element |
| * always shows its scrollbars, but they are hidden beneath the root |
| * element. |
| */ |
| Element scrollable = getScrollableElement(); |
| scrollable.getStyle().setOverflow(Overflow.SCROLL); |
| scrollable.appendChild(containerElem); |
| scrollableLayer = layout.attachChild(scrollable); |
| |
| /* |
| * Hide the native scrollbars beneath the root element. The scrollable |
| * element's dimensions are large enough that the scrollbars are outside of |
| * the root element, and the root element is set to hide overflow, making |
| * the scrollbars invisible. The scrollable elements dimensions are set |
| * after the widget is attached to the document in hideNativeScrollbars(). |
| */ |
| getElement().getStyle().setOverflow(Overflow.HIDDEN); |
| |
| /* |
| * Create a corner element that appears at the gap where the vertical and |
| * horizontal scrollbars meet (when both are visible). This prevents the |
| * content from peeking out from this gap. |
| */ |
| cornerElem = Document.get().createDivElement(); |
| cornerElem.addClassName(style.customScrollPanelCorner()); |
| cornerLayer = layout.attachChild(cornerElem); |
| |
| // Initialize the default scrollbars using the transparent styles. |
| NativeHorizontalScrollbar.Resources hResources = |
| GWT.create(NativeHorizontalScrollbar.ResourcesTransparant.class); |
| setHorizontalScrollbar(new NativeHorizontalScrollbar(hResources), AbstractNativeScrollbar |
| .getNativeScrollbarHeight()); |
| NativeVerticalScrollbar.Resources vResources = |
| GWT.create(NativeVerticalScrollbar.ResourcesTransparant.class); |
| setVerticalScrollbar(new NativeVerticalScrollbar(vResources), AbstractNativeScrollbar |
| .getNativeScrollbarWidth()); |
| |
| /* |
| * Add a handler to catch changes in the content size and update the |
| * scrollbars accordingly. |
| */ |
| ResizeLayoutPanel.Impl.Delegate containerResizeDelegate = |
| new ResizeLayoutPanel.Impl.Delegate() { |
| @Override |
| public void onResize() { |
| maybeUpdateScrollbars(); |
| } |
| }; |
| containerResizeImpl.init(getContainerElement(), containerResizeDelegate); |
| |
| /* |
| * Listen for scroll events from the root element and the scrollable element |
| * so we can align the scrollbars with the content. Scroll events usually |
| * come from the scrollable element, but they can also come from the root |
| * element if the user clicks and drags the content, which reveals the |
| * hidden scrollbars. |
| */ |
| Event.sinkEvents(getElement(), Event.ONSCROLL); |
| Event.sinkEvents(getScrollableElement(), Event.ONSCROLL); |
| } |
| |
| /** |
| * Creates a {@link CustomScrollPanel} with the specified child widget. |
| * |
| * @param child the widget to be wrapped by the scroll panel |
| */ |
| public CustomScrollPanel(Widget child) { |
| this(getDefaultResources()); |
| setWidget(child); |
| } |
| |
| /** |
| * Get the scrollbar used for horizontal scrolling. |
| * |
| * @return the horizontal scrollbar, or null if none specified |
| */ |
| public HorizontalScrollbar getHorizontalScrollbar() { |
| return hScrollbar; |
| } |
| |
| /** |
| * Get the scrollbar used for vertical scrolling. |
| * |
| * @return the vertical scrollbar, or null if none specified |
| */ |
| public VerticalScrollbar getVerticalScrollbar() { |
| return vScrollbar; |
| } |
| |
| @Override |
| public void onBrowserEvent(Event event) { |
| // Align the scrollbars with the content. |
| if (Event.ONSCROLL == event.getTypeInt()) { |
| double curTime = Duration.currentTimeMillis(); |
| if (curTime > ignoreContentUntil) { |
| ignoreScrollbarsUntil = curTime + IGNORE_SCROLL_TIMEOUT; |
| maybeUpdateScrollbarPositions(); |
| } |
| } |
| |
| super.onBrowserEvent(event); |
| } |
| |
| @Override |
| public void onResize() { |
| maybeUpdateScrollbars(); |
| super.onResize(); |
| } |
| |
| @Override |
| public boolean remove(Widget w) { |
| // Validate. |
| if (w.getParent() != this) { |
| return false; |
| } |
| |
| if (w == getWidget()) { |
| // Remove the content widget. |
| boolean toRet = super.remove(w); |
| maybeUpdateScrollbars(); |
| return toRet; |
| } |
| |
| // Remove a scrollbar. |
| try { |
| // Orphan. |
| orphan(w); |
| } finally { |
| // Physical detach. |
| w.getElement().removeFromParent(); |
| |
| // Logical detach. |
| Widget hScrollbarWidget = (hScrollbar == null) ? null : hScrollbar.asWidget(); |
| Widget vScrollbarWidget = (vScrollbar == null) ? null : vScrollbar.asWidget(); |
| if (w == hScrollbarWidget) { |
| hScrollbar = null; |
| hScrollbarHandler.removeHandler(); |
| hScrollbarHandler = null; |
| layout.removeChild(hScrollbarLayer); |
| hScrollbarLayer = null; |
| } else if (w == vScrollbarWidget) { |
| vScrollbar = null; |
| vScrollbarHandler.removeHandler(); |
| vScrollbarHandler = null; |
| layout.removeChild(vScrollbarLayer); |
| vScrollbarLayer = null; |
| } |
| } |
| maybeUpdateScrollbars(); |
| return true; |
| } |
| |
| /** |
| * Remove the {@link HorizontalScrollbar}, if one exists. |
| */ |
| public void removeHorizontalScrollbar() { |
| if (hScrollbar != null) { |
| remove(hScrollbar); |
| } |
| } |
| |
| /** |
| * Remove the {@link VerticalScrollbar}, if one exists. |
| */ |
| public void removeVerticalScrollbar() { |
| if (vScrollbar != null) { |
| remove(vScrollbar); |
| } |
| } |
| |
| @Override |
| public void setAlwaysShowScrollBars(boolean alwaysShow) { |
| if (this.alwaysShowScrollbars != alwaysShow) { |
| this.alwaysShowScrollbars = alwaysShow; |
| maybeUpdateScrollbars(); |
| } |
| } |
| |
| /** |
| * Set the scrollbar used for horizontal scrolling. |
| * |
| * @param scrollbar the scrollbar, or null to clear it |
| * @param height the height of the scrollbar in pixels |
| */ |
| public void setHorizontalScrollbar(final HorizontalScrollbar scrollbar, int height) { |
| // Physical attach. |
| hScrollbarLayer = add(scrollbar, hScrollbar, hScrollbarLayer); |
| |
| // Logical attach. |
| hScrollbar = scrollbar; |
| hScrollbarHeight = height; |
| |
| // Initialize the new scrollbar. |
| if (scrollbar != null) { |
| hScrollbarHandler = scrollbar.addScrollHandler(new ScrollHandler() { |
| @Override |
| public void onScroll(ScrollEvent event) { |
| double curTime = Duration.currentTimeMillis(); |
| if (curTime > ignoreScrollbarsUntil) { |
| ignoreContentUntil = curTime + IGNORE_SCROLL_TIMEOUT; |
| int hPos = scrollbar.getHorizontalScrollPosition(); |
| if (getHorizontalScrollPosition() != hPos) { |
| setHorizontalScrollPosition(hPos); |
| } |
| } |
| } |
| }); |
| } |
| maybeUpdateScrollbars(); |
| } |
| |
| /** |
| * Set the scrollbar used for vertical scrolling. |
| * |
| * @param scrollbar the scrollbar, or null to clear it |
| * @param width the width of the scrollbar in pixels |
| */ |
| public void setVerticalScrollbar(final VerticalScrollbar scrollbar, int width) { |
| // Physical attach. |
| vScrollbarLayer = add(scrollbar, vScrollbar, vScrollbarLayer); |
| |
| // Logical attach. |
| vScrollbar = scrollbar; |
| vScrollbarWidth = width; |
| |
| // Initialize the new scrollbar. |
| if (scrollbar != null) { |
| vScrollbarHandler = scrollbar.addScrollHandler(new ScrollHandler() { |
| @Override |
| public void onScroll(ScrollEvent event) { |
| double curTime = Duration.currentTimeMillis(); |
| if (curTime > ignoreScrollbarsUntil) { |
| ignoreContentUntil = curTime + IGNORE_SCROLL_TIMEOUT; |
| int vPos = scrollbar.getVerticalScrollPosition(); |
| int v = getVerticalScrollPosition(); |
| if (getVerticalScrollPosition() != vPos) { |
| setVerticalScrollPosition(vPos); |
| } |
| } |
| } |
| }); |
| } |
| maybeUpdateScrollbars(); |
| } |
| |
| @Override |
| public void setWidget(Widget w) { |
| // Early exit if the widget is unchanged. Avoids updating the scrollbars. |
| if (w == getWidget()) { |
| return; |
| } |
| |
| super.setWidget(w); |
| maybeUpdateScrollbars(); |
| } |
| |
| @Override |
| protected void doAttachChildren() { |
| AttachDetachException.tryCommand(AttachDetachException.attachCommand, getWidget(), hScrollbar, |
| vScrollbar); |
| } |
| |
| @Override |
| protected void doDetachChildren() { |
| AttachDetachException.tryCommand(AttachDetachException.detachCommand, getWidget(), hScrollbar, |
| vScrollbar); |
| } |
| |
| @Override |
| protected void onAttach() { |
| super.onAttach(); |
| containerResizeImpl.onAttach(); |
| layout.onAttach(); |
| } |
| |
| @Override |
| protected void onDetach() { |
| super.onDetach(); |
| containerResizeImpl.onDetach(); |
| layout.onDetach(); |
| } |
| |
| @Override |
| protected void onLoad() { |
| hideNativeScrollbars(); |
| Scheduler.get().scheduleDeferred(new ScheduledCommand() { |
| @Override |
| public void execute() { |
| maybeUpdateScrollbars(); |
| } |
| }); |
| } |
| |
| /** |
| * Add a widget to the panel in the specified layer. Note that this method |
| * does not do the logical attach. |
| * |
| * @param w the widget to add, or null to clear the widget |
| * @param toReplace the widget to replace |
| * @param layer the layer in which the existing widget is placed |
| * @return the layer in which the new widget is placed, or null if no widget |
| */ |
| private Layer add(IsWidget w, IsWidget toReplace, Layer layer) { |
| // Validate. |
| if (w == toReplace) { |
| return layer; |
| } |
| |
| // Detach new child. |
| if (w != null) { |
| w.asWidget().removeFromParent(); |
| } |
| |
| // Remove old child. |
| if (toReplace != null) { |
| remove(toReplace); |
| } |
| |
| Layer toRet = null; |
| if (w != null) { |
| // Physical attach. |
| toRet = layout.attachChild(w.asWidget().getElement()); |
| |
| adopt(w.asWidget()); |
| } |
| |
| return toRet; |
| } |
| |
| /** |
| * Hide the native scrollbars. We call this after attaching to ensure that we |
| * inherit the direction (rtl or ltr). |
| */ |
| private void hideNativeScrollbars() { |
| int barWidth = AbstractNativeScrollbar.getNativeScrollbarWidth(); |
| int barHeight = AbstractNativeScrollbar.getNativeScrollbarHeight(); |
| scrollableLayer.setTopBottom(0.0, Unit.PX, -barHeight, Unit.PX); |
| if (AbstractNativeScrollbar.isScrollbarLeftAlignedInRtl() |
| && ScrollImpl.get().isRtl(getScrollableElement())) { |
| scrollableLayer.setLeftRight(-barWidth, Unit.PX, 0.0, Unit.PX); |
| } else { |
| scrollableLayer.setLeftRight(0.0, Unit.PX, -barWidth, Unit.PX); |
| } |
| layout.layout(); |
| } |
| |
| /** |
| * Synchronize the scroll positions of the scrollbars with the actual scroll |
| * position of the content. |
| */ |
| private void maybeUpdateScrollbarPositions() { |
| if (!isAttached()) { |
| return; |
| } |
| |
| if (hScrollbar != null) { |
| int hPos = getHorizontalScrollPosition(); |
| if (hScrollbar.getHorizontalScrollPosition() != hPos) { |
| hScrollbar.setHorizontalScrollPosition(hPos); |
| } |
| } |
| if (vScrollbar != null) { |
| int vPos = getVerticalScrollPosition(); |
| if (vScrollbar.getVerticalScrollPosition() != vPos) { |
| vScrollbar.setVerticalScrollPosition(vPos); |
| } |
| } |
| |
| /* |
| * Ensure that the viewport is anchored to the corner. If the user clicks |
| * and drags the content, its possible to shift the viewport and reveal the |
| * hidden scrollbars. |
| */ |
| if (getElement().getScrollLeft() != 0) { |
| getElement().setScrollLeft(0); |
| } |
| if (getElement().getScrollTop() != 0) { |
| getElement().setScrollTop(0); |
| } |
| } |
| |
| /** |
| * Update the position of the scrollbars. |
| * |
| * <p> |
| * If only the vertical scrollbar is present, it takes up the entire height of |
| * the right side. If only the horizontal scrollbar is present, it takes up |
| * the entire width of the bottom. If both scrollbars are present, the |
| * vertical scrollbar extends from the top to just above the horizontal |
| * scrollbar, and the horizontal scrollbar extends from the left to just right |
| * of the vertical scrollbar, leaving a small square in the bottom right |
| * corner. |
| * |
| * <p> |
| * In RTL, the vertical scrollbar appears on the left. |
| */ |
| private void maybeUpdateScrollbars() { |
| if (!isAttached()) { |
| return; |
| } |
| |
| /* |
| * Measure the height and width of the content directly. Note that measuring |
| * the height and width of the container element (which should be the same) |
| * doesn't work correctly in IE. |
| */ |
| Widget w = getWidget(); |
| int contentHeight = (w == null) ? 0 : w.getOffsetHeight(); |
| int contentWidth = (w == null) ? 0 : w.getOffsetWidth(); |
| |
| // Determine which scrollbars to show. |
| int realScrollbarHeight = 0; |
| int realScrollbarWidth = 0; |
| if (hScrollbar != null |
| && (alwaysShowScrollbars || getElement().getClientWidth() < contentWidth)) { |
| // Horizontal scrollbar is defined and required. |
| realScrollbarHeight = hScrollbarHeight; |
| } |
| if (vScrollbar != null |
| && (alwaysShowScrollbars || getElement().getClientHeight() < contentHeight)) { |
| // Vertical scrollbar is defined and required. |
| realScrollbarWidth = vScrollbarWidth; |
| } |
| |
| /* |
| * Add some padding to the so bottom we can scroll to the bottom without the |
| * content being hidden beneath the horizontal scrollbar. |
| */ |
| if (w != null) { |
| if (realScrollbarHeight > 0) { |
| w.getElement().getStyle().setMarginBottom(realScrollbarHeight, Unit.PX); |
| contentHeight += realScrollbarHeight; |
| } else { |
| w.getElement().getStyle().clearMarginBottom(); |
| } |
| } |
| |
| // Adjust the scrollbar layers to display the visible scrollbars. |
| boolean isRtl = ScrollImpl.get().isRtl(getScrollableElement()); |
| if (realScrollbarHeight > 0) { |
| hScrollbarLayer.setVisible(true); |
| if (isRtl) { |
| hScrollbarLayer.setLeftRight(realScrollbarWidth, Unit.PX, 0.0, Unit.PX); |
| } else { |
| hScrollbarLayer.setLeftRight(0.0, Unit.PX, realScrollbarWidth, Unit.PX); |
| } |
| hScrollbarLayer.setBottomHeight(0.0, Unit.PX, realScrollbarHeight, Unit.PX); |
| hScrollbar.setScrollWidth(Math.max(0, contentWidth - realScrollbarWidth)); |
| } else if (hScrollbarLayer != null) { |
| hScrollbarLayer.setVisible(false); |
| } |
| if (realScrollbarWidth > 0) { |
| vScrollbarLayer.setVisible(true); |
| vScrollbarLayer.setTopBottom(0.0, Unit.PX, realScrollbarHeight, Unit.PX); |
| if (isRtl) { |
| vScrollbarLayer.setLeftWidth(0.0, Unit.PX, realScrollbarWidth, Unit.PX); |
| } else { |
| vScrollbarLayer.setRightWidth(0.0, Unit.PX, realScrollbarWidth, Unit.PX); |
| } |
| vScrollbar.setScrollHeight(Math.max(0, contentHeight - realScrollbarHeight)); |
| } else if (vScrollbarLayer != null) { |
| vScrollbarLayer.setVisible(false); |
| } |
| |
| /* |
| * Show the corner in the gap between the vertical and horizontal |
| * scrollbars. |
| */ |
| cornerLayer.setBottomHeight(0.0, Unit.PX, realScrollbarHeight, Unit.PX); |
| if (isRtl) { |
| cornerLayer.setLeftWidth(0.0, Unit.PX, realScrollbarWidth, Unit.PX); |
| } else { |
| cornerLayer.setRightWidth(0.0, Unit.PX, realScrollbarWidth, Unit.PX); |
| } |
| cornerLayer.setVisible(hScrollbarHeight > 0 && vScrollbarWidth > 0); |
| |
| // Apply the layout. |
| layout.layout(); |
| maybeUpdateScrollbarPositions(); |
| } |
| } |