blob: 8b62d94bb18019e29a57c9500f049ebedbc8ca6f [file] [log] [blame]
/*
* 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();
}
}