blob: 2434496d2b2cb7a3eed5c2035a246107d220ef3e [file] [log] [blame]
/*
* Copyright 2009 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.animation.client.Animation;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.HasCloseHandlers;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventPreview;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.ui.impl.PopupImpl;
import java.util.ArrayList;
import java.util.List;
/**
* A panel that can "pop up" over other widgets. It overlays the browser's
* client area (and any previously-created popups).
*
* <p>
* A PopupPanel should not generally be added to other panels; rather, it should
* be shown and hidden using the {@link #show()} and {@link #hide()} methods.
* </p>
* <p>
* The width and height of the PopupPanel cannot be explicitly set; they are
* determined by the PopupPanel's widget. Calls to {@link #setWidth(String)} and
* {@link #setHeight(String)} will call these methods on the PopupPanel's
* widget.
* </p>
* <p>
* <img class='gallery' src='doc-files/PopupPanel.png'/>
* </p>
*
* <p>
* The PopupPanel can be optionally displayed with a "glass" element behind it,
* which is commonly used to gray out the widgets behind it. It can be enabled
* using {@link #setGlassEnabled(boolean)}. It has a default style name of
* "gwt-PopupPanelGlass", which can be changed using
* {@link #setGlassStyleName(String)}.
* </p>
*
* <p>
* <h3>Example</h3>
* {@example com.google.gwt.examples.PopupPanelExample}
* </p>
* <h3>CSS Style Rules</h3>
* <dl>
* <dt>.gwt-PopupPanel</dt>
* <dd>the outside of the popup</dd>
* <dt>.gwt-PopupPanel .popupContent</dt>
* <dd>the wrapper around the content</dd>
* <dt>.gwt-PopupPanelGlass</dt>
* <dd>the glass background behind the popup</dd>
* </dl>
*/
@SuppressWarnings("deprecation")
public class PopupPanel extends SimplePanel implements SourcesPopupEvents,
EventPreview, HasAnimation, HasCloseHandlers<PopupPanel> {
/**
* A callback that is used to set the position of a {@link PopupPanel} right
* before it is shown.
*/
public interface PositionCallback {
/**
* Provides the opportunity to set the position of the PopupPanel right
* before the PopupPanel is shown. The offsetWidth and offsetHeight values
* of the PopupPanel are made available to allow for positioning based on
* its size.
*
* @param offsetWidth the offsetWidth of the PopupPanel
* @param offsetHeight the offsetHeight of the PopupPanel
* @see PopupPanel#setPopupPositionAndShow(PositionCallback)
*/
void setPosition(int offsetWidth, int offsetHeight);
}
/**
* The type of animation to use when opening the popup.
*
* <ul>
* <li>CENTER - Expand from the center of the popup</li>
* <li>ONE_WAY_CORNER - Expand from the top left corner, do not animate hiding
* </li>
* </ul>
*/
static enum AnimationType {
CENTER, ONE_WAY_CORNER, ROLL_DOWN
}
/**
* An {@link Animation} used to enlarge the popup into view.
*/
static class ResizeAnimation extends Animation {
/**
* The {@link PopupPanel} being affected.
*/
private PopupPanel curPanel = null;
/**
* Indicates whether or not the {@link PopupPanel} is in the process of
* unloading. If the popup is unloading, then the animation just does
* cleanup.
*/
private boolean isUnloading;
/**
* The offset height and width of the current {@link PopupPanel}.
*/
private int offsetHeight, offsetWidth = -1;
/**
* A boolean indicating whether we are showing or hiding the popup.
*/
private boolean showing;
/**
* The timer used to delay the show animation.
*/
private Timer showTimer;
/**
* A boolean indicating whether the glass element is currently attached.
*/
private boolean glassShowing;
private HandlerRegistration resizeRegistration;
/**
* Create a new {@link ResizeAnimation}.
*
* @param panel the panel to affect
*/
public ResizeAnimation(PopupPanel panel) {
this.curPanel = panel;
}
/**
* Open or close the content. This method always called immediately after
* the PopupPanel showing state has changed, so we base the animation on the
* current state.
*
* @param showing true if the popup is showing, false if not
*/
public void setState(boolean showing, boolean isUnloading) {
// Immediately complete previous open/close animation
this.isUnloading = isUnloading;
cancel();
// If there is a pending timer to start a show animation, then just cancel
// the timer and complete the show operation.
if (showTimer != null) {
showTimer.cancel();
showTimer = null;
onComplete();
}
// Update the logical state.
curPanel.showing = showing;
curPanel.updateHandlers();
// Determine if we need to animate
boolean animate = !isUnloading && curPanel.isAnimationEnabled;
if (curPanel.animType != AnimationType.CENTER && !showing) {
animate = false;
}
// Open the new item
this.showing = showing;
if (animate) {
// impl.onShow takes some time to complete, so we do it before starting
// the animation. If we move this to onStart, the animation will look
// choppy or not run at all.
if (showing) {
maybeShowGlass();
// Set the position attribute, and then attach to the DOM. Otherwise,
// the PopupPanel will appear to 'jump' from its static/relative
// position to its absolute position (issue #1231).
DOM.setStyleAttribute(curPanel.getElement(), "position", "absolute");
if (curPanel.topPosition != -1) {
curPanel.setPopupPosition(curPanel.leftPosition,
curPanel.topPosition);
}
impl.setClip(curPanel.getElement(), getRectString(0, 0, 0, 0));
RootPanel.get().add(curPanel);
impl.onShow(curPanel.getElement());
// Wait for the popup panel and iframe to be attached before running
// the animation. We use a Timer instead of a DeferredCommand so we
// can cancel it if the popup is hidden synchronously.
showTimer = new Timer() {
@Override
public void run() {
showTimer = null;
ResizeAnimation.this.run(ANIMATION_DURATION);
}
};
showTimer.schedule(1);
} else {
run(ANIMATION_DURATION);
}
} else {
onInstantaneousRun();
}
}
@Override
protected void onComplete() {
if (!showing) {
maybeShowGlass();
if (!isUnloading) {
RootPanel.get().remove(curPanel);
}
impl.onHide(curPanel.getElement());
}
impl.setClip(curPanel.getElement(), "rect(auto, auto, auto, auto)");
DOM.setStyleAttribute(curPanel.getElement(), "overflow", "visible");
}
@Override
protected void onStart() {
offsetHeight = curPanel.getOffsetHeight();
offsetWidth = curPanel.getOffsetWidth();
DOM.setStyleAttribute(curPanel.getElement(), "overflow", "hidden");
super.onStart();
}
@Override
protected void onUpdate(double progress) {
if (!showing) {
progress = 1.0 - progress;
}
// Determine the clipping size
int top = 0;
int left = 0;
int right = 0;
int bottom = 0;
int height = (int) (progress * offsetHeight);
int width = (int) (progress * offsetWidth);
switch (curPanel.animType) {
case ROLL_DOWN:
right = offsetWidth;
bottom = height;
break;
case CENTER:
top = (offsetHeight - height) >> 1;
left = (offsetWidth - width) >> 1;
right = left + width;
bottom = top + height;
break;
case ONE_WAY_CORNER:
if (LocaleInfo.getCurrentLocale().isRTL()) {
left = offsetWidth - width;
}
right = left + width;
bottom = top + height;
break;
}
// Set the rect clipping
impl.setClip(curPanel.getElement(), getRectString(top, right, bottom,
left));
}
/**
* Returns a rect string.
*/
private String getRectString(int top, int right, int bottom, int left) {
return "rect(" + top + "px, " + right + "px, " + bottom + "px, " + left
+ "px)";
}
/**
* Show or hide the glass.
*/
private void maybeShowGlass() {
if (showing) {
if (curPanel.isGlassEnabled) {
Document.get().getBody().appendChild(curPanel.glass);
impl.onShow(curPanel.glass);
resizeRegistration = Window.addResizeHandler(curPanel.glassResizer);
curPanel.glassResizer.onResize(null);
glassShowing = true;
}
} else if (glassShowing) {
Document.get().getBody().removeChild(curPanel.glass);
impl.onHide(curPanel.glass);
resizeRegistration.removeHandler();
resizeRegistration = null;
glassShowing = false;
}
}
private void onInstantaneousRun() {
maybeShowGlass();
if (showing) {
// Set the position attribute, and then attach to the DOM. Otherwise,
// the PopupPanel will appear to 'jump' from its static/relative
// position to its absolute position (issue #1231).
DOM.setStyleAttribute(curPanel.getElement(), "position", "absolute");
if (curPanel.topPosition != -1) {
curPanel.setPopupPosition(curPanel.leftPosition, curPanel.topPosition);
}
RootPanel.get().add(curPanel);
impl.onShow(curPanel.getElement());
} else {
if (!isUnloading) {
RootPanel.get().remove(curPanel);
}
impl.onHide(curPanel.getElement());
}
DOM.setStyleAttribute(curPanel.getElement(), "overflow", "visible");
}
}
/**
* The duration of the animation.
*/
private static final int ANIMATION_DURATION = 200;
/**
* The default style name.
*/
private static final String DEFAULT_STYLENAME = "gwt-PopupPanel";
private static final PopupImpl impl = GWT.create(PopupImpl.class);
/**
* Window resize handler used to keep the glass the proper size.
*/
private ResizeHandler glassResizer = new ResizeHandler() {
public void onResize(ResizeEvent event) {
Style style = glass.getStyle();
int winWidth = Window.getClientWidth();
int winHeight = Window.getClientHeight();
// Hide the glass while checking the document size. Otherwise it would
// interfere with the measurement.
style.setDisplay(Display.NONE);
style.setWidth(0, Unit.PX);
style.setHeight(0, Unit.PX);
int width = Document.get().getScrollWidth();
int height = Document.get().getScrollHeight();
// Set the glass size to the larger of the window's client size or the
// document's scroll size.
style.setWidth(Math.max(width, winWidth), Unit.PX);
style.setHeight(Math.max(height, winHeight), Unit.PX);
// The size is set. Show the glass again.
style.setDisplay(Display.BLOCK);
}
};
/**
* If true, animate the opening of this popup from the center. If false,
* animate it open from top to bottom, and do not animate closing. Use false
* to animate menus.
*/
private AnimationType animType = AnimationType.CENTER;
private boolean autoHide, previewAllNativeEvents, modal, showing;
private boolean autoHideOnHistoryEvents;
private List<Element> autoHidePartners;
// Used to track requested size across changing child widgets
private String desiredHeight;
private String desiredWidth;
/**
* The glass element.
*/
private Element glass;
private String glassStyleName = "gwt-PopupPanelGlass";
/**
* A boolean indicating that a glass element should be used.
*/
private boolean isGlassEnabled;
private boolean isAnimationEnabled = false;
// the left style attribute in pixels
private int leftPosition = -1;
private HandlerRegistration nativePreviewHandlerRegistration;
private HandlerRegistration historyHandlerRegistration;
/**
* The {@link ResizeAnimation} used to open and close the {@link PopupPanel}s.
*/
private ResizeAnimation resizeAnimation = new ResizeAnimation(this);
// The top style attribute in pixels
private int topPosition = -1;
/**
* Creates an empty popup panel. A child widget must be added to it before it
* is shown.
*/
public PopupPanel() {
super();
super.getContainerElement().appendChild(impl.createElement());
// Default position of popup should be in the upper-left corner of the
// window. By setting a default position, the popup will not appear in
// an undefined location if it is shown before its position is set.
setPopupPosition(0, 0);
setStyleName(DEFAULT_STYLENAME);
setStyleName(getContainerElement(), "popupContent");
}
/**
* Creates an empty popup panel, specifying its "auto-hide" property.
*
* @param autoHide <code>true</code> if the popup should be automatically
* hidden when the user clicks outside of it or the history token
* changes.
*/
public PopupPanel(boolean autoHide) {
this();
this.autoHide = autoHide;
this.autoHideOnHistoryEvents = autoHide;
}
/**
* Creates an empty popup panel, specifying its "auto-hide" and "modal"
* properties.
*
* @param autoHide <code>true</code> if the popup should be automatically
* hidden when the user clicks outside of it or the history token
* changes.
* @param modal <code>true</code> if keyboard or mouse events that do not
* target the PopupPanel or its children should be ignored
*/
public PopupPanel(boolean autoHide, boolean modal) {
this(autoHide);
this.modal = modal;
}
/**
* Mouse events that occur within an autoHide partner will not hide a panel
* set to autoHide.
*
* @param partner the auto hide partner to add
*/
public void addAutoHidePartner(Element partner) {
assert partner != null : "partner cannot be null";
if (autoHidePartners == null) {
autoHidePartners = new ArrayList<Element>();
}
autoHidePartners.add(partner);
}
public HandlerRegistration addCloseHandler(CloseHandler<PopupPanel> handler) {
return addHandler(handler, CloseEvent.getType());
}
/**
* @deprecated Use {@link #addCloseHandler} instead
*/
@Deprecated
public void addPopupListener(final PopupListener listener) {
ListenerWrapper.WrappedPopupListener.add(this, listener);
}
/**
* Centers the popup in the browser window and shows it. If the popup was
* already showing, then the popup is centered.
*/
public void center() {
boolean initiallyShowing = showing;
boolean initiallyAnimated = isAnimationEnabled;
if (!initiallyShowing) {
setVisible(false);
setAnimationEnabled(false);
show();
}
// If left/top are set from a previous center() call, and our content
// has changed, we may get a bogus getOffsetWidth because our new content
// is wrapping (giving a lower offset width) then it would without the
// previous left. Setting left/top back to 0 avoids this.
Element elem = getElement();
elem.getStyle().setPropertyPx("left", 0);
elem.getStyle().setPropertyPx("top", 0);
int left = (Window.getClientWidth() - getOffsetWidth()) >> 1;
int top = (Window.getClientHeight() - getOffsetHeight()) >> 1;
setPopupPosition(Math.max(Window.getScrollLeft() + left, 0), Math.max(
Window.getScrollTop() + top, 0));
if (!initiallyShowing) {
setAnimationEnabled(initiallyAnimated);
// Run the animation. The popup is already visible, so we can skip the
// call to setState.
if (initiallyAnimated) {
impl.setClip(getElement(), "rect(0px, 0px, 0px, 0px)");
setVisible(true);
resizeAnimation.run(ANIMATION_DURATION);
} else {
setVisible(true);
}
}
}
/**
* Gets the style name to be used on the glass element. By default, this is
* "gwt-PopupPanelGlass".
*
* @return the glass element's style name
*/
public String getGlassStyleName() {
return glassStyleName;
}
/**
* Gets the panel's offset height in pixels. Calls to
* {@link #setHeight(String)} before the panel's child widget is set will not
* influence the offset height.
*
* @return the object's offset height
*/
@Override
public int getOffsetHeight() {
return super.getOffsetHeight();
}
/**
* Gets the panel's offset width in pixels. Calls to {@link #setWidth(String)}
* before the panel's child widget is set will not influence the offset width.
*
* @return the object's offset width
*/
@Override
public int getOffsetWidth() {
return super.getOffsetWidth();
}
/**
* Gets the popup's left position relative to the browser's client area.
*
* @return the popup's left position
*/
public int getPopupLeft() {
return DOM.getAbsoluteLeft(getElement());
}
/**
* Gets the popup's top position relative to the browser's client area.
*
* @return the popup's top position
*/
public int getPopupTop() {
return DOM.getAbsoluteTop(getElement());
}
@Override
public String getTitle() {
return DOM.getElementProperty(getContainerElement(), "title");
}
/**
* Hides the popup and detaches it from the page. This has no effect if it is
* not currently showing.
*/
public void hide() {
hide(false);
}
/**
* Hides the popup and detaches it from the page. This has no effect if it is
* not currently showing.
*
* @param autoClosed the value that will be passed to
* {@link CloseHandler#onClose(CloseEvent)} when the popup is closed
*/
public void hide(boolean autoClosed) {
if (!isShowing()) {
return;
}
resizeAnimation.setState(false, false);
CloseEvent.fire(this, this, autoClosed);
}
public boolean isAnimationEnabled() {
return isAnimationEnabled;
}
/**
* Returns <code>true</code> if the popup should be automatically hidden when
* the user clicks outside of it.
*
* @return true if autoHide is enabled, false if disabled
*/
public boolean isAutoHideEnabled() {
return autoHide;
}
/**
* Returns <code>true</code> if the popup should be automatically hidden when
* the history token changes, such as when the user presses the browser's back
* button.
*
* @return true if enabled, false if disabled
*/
public boolean isAutoHideOnHistoryEventsEnabled() {
return autoHideOnHistoryEvents;
}
/**
* Returns <code>true</code> if a glass element will be displayed under the
* {@link PopupPanel}.
*
* @return true if enabled
*/
public boolean isGlassEnabled() {
return isGlassEnabled;
}
/**
* Returns <code>true</code> if keyboard or mouse events that do not target
* the PopupPanel or its children should be ignored.
*
* @return true if popup is modal, false if not
*/
public boolean isModal() {
return modal;
}
/**
* Returns <code>true</code> if the popup should preview all native events,
* even if the event has already been consumed by another popup.
*
* @return true if previewAllNativeEvents is enabled, false if disabled
*/
public boolean isPreviewingAllNativeEvents() {
return previewAllNativeEvents;
}
/**
* Determines whether or not this popup is showing.
*
* @return <code>true</code> if the popup is showing
* @see #show()
* @see #hide()
*/
public boolean isShowing() {
return showing;
}
/**
* Determines whether or not this popup is visible. Note that this just checks
* the <code>visibility</code> style attribute, which is set in the
* {@link #setVisible(boolean)} method. If you want to know if the popup is
* attached to the page, use {@link #isShowing()} instead.
*
* @return <code>true</code> if the object is visible
* @see #setVisible(boolean)
*/
@Override
public boolean isVisible() {
return !"hidden".equals(getElement().getStyle().getProperty("visibility"));
}
/**
* @deprecated Use {@link #onPreviewNativeEvent} instead
*/
@Deprecated
public boolean onEventPreview(Event event) {
return true;
}
/**
* Popups get an opportunity to preview keyboard events before they are passed
* to a widget contained by the Popup.
*
* @param key the key code of the depressed key
* @param modifiers keyboard modifiers, as specified in
* {@link com.google.gwt.event.dom.client.KeyCodes}.
* @return <code>false</code> to suppress the event
* @deprecated Use {@link #onPreviewNativeEvent} instead
*/
@Deprecated
public boolean onKeyDownPreview(char key, int modifiers) {
return true;
}
/**
* Popups get an opportunity to preview keyboard events before they are passed
* to a widget contained by the Popup.
*
* @param key the unicode character pressed
* @param modifiers keyboard modifiers, as specified in
* {@link com.google.gwt.event.dom.client.KeyCodes}.
* @return <code>false</code> to suppress the event
* @deprecated Use {@link #onPreviewNativeEvent} instead
*/
@Deprecated
public boolean onKeyPressPreview(char key, int modifiers) {
return true;
}
/**
* Popups get an opportunity to preview keyboard events before they are passed
* to a widget contained by the Popup.
*
* @param key the key code of the released key
* @param modifiers keyboard modifiers, as specified in
* {@link com.google.gwt.event.dom.client.KeyCodes}.
* @return <code>false</code> to suppress the event
* @deprecated Use {@link #onPreviewNativeEvent} instead
*/
@Deprecated
public boolean onKeyUpPreview(char key, int modifiers) {
return true;
}
/**
* Remove an autoHide partner.
*
* @param partner the auto hide partner to remove
*/
public void removeAutoHidePartner(Element partner) {
assert partner != null : "partner cannot be null";
if (autoHidePartners != null) {
autoHidePartners.remove(partner);
}
}
/**
* @deprecated Use the {@link HandlerRegistration#removeHandler} method on the
* object returned by {@link #addCloseHandler} instead
*/
@Deprecated
public void removePopupListener(PopupListener listener) {
ListenerWrapper.WrappedPopupListener.remove(this, listener);
}
public void setAnimationEnabled(boolean enable) {
isAnimationEnabled = enable;
}
/**
* Enable or disable the autoHide feature. When enabled, the popup will be
* automatically hidden when the user clicks outside of it.
*
* @param autoHide true to enable autoHide, false to disable
*/
public void setAutoHideEnabled(boolean autoHide) {
this.autoHide = autoHide;
}
/**
* Enable or disable autoHide on history change events. When enabled, the
* popup will be automatically hidden when the history token changes, such as
* when the user presses the browser's back button. Disabled by default.
*
* @param enabled true to enable, false to disable
*/
public void setAutoHideOnHistoryEventsEnabled(boolean enabled) {
this.autoHideOnHistoryEvents = enabled;
}
/**
* When enabled, the background will be blocked with a semi-transparent pane
* the next time it is shown. If the PopupPanel is already visible, the glass
* will not be displayed until it is hidden and shown again.
*
* @param enabled true to enable, false to disable
*/
public void setGlassEnabled(boolean enabled) {
this.isGlassEnabled = enabled;
if (enabled && glass == null) {
glass = Document.get().createDivElement();
glass.setClassName(glassStyleName);
glass.getStyle().setPosition(Position.ABSOLUTE);
glass.getStyle().setLeft(0, Unit.PX);
glass.getStyle().setTop(0, Unit.PX);
}
}
/**
* Sets the style name to be used on the glass element. By default, this is
* "gwt-PopupPanelGlass".
*
* @param glassStyleName the glass element's style name
*/
public void setGlassStyleName(String glassStyleName) {
this.glassStyleName = glassStyleName;
if (glass != null) {
glass.setClassName(glassStyleName);
}
}
/**
* Sets the height of the panel's child widget. If the panel's child widget
* has not been set, the height passed in will be cached and used to set the
* height immediately after the child widget is set.
*
* <p>
* Note that subclasses may have a different behavior. A subclass may decide
* not to change the height of the child widget. It may instead decide to
* change the height of an internal panel widget, which contains the child
* widget.
* </p>
*
* @param height the object's new height, in CSS units (e.g. "10px", "1em")
*/
@Override
public void setHeight(String height) {
desiredHeight = height;
maybeUpdateSize();
// If the user cleared the size, revert to not trying to control children.
if (height.length() == 0) {
desiredHeight = null;
}
}
/**
* When the popup is modal, keyboard or mouse events that do not target the
* PopupPanel or its children will be ignored.
*
* @param modal true to make the popup modal
*/
public void setModal(boolean modal) {
this.modal = modal;
}
/**
* Sets the popup's position relative to the browser's client area. The
* popup's position may be set before calling {@link #show()}.
*
* @param left the left position, in pixels
* @param top the top position, in pixels
*/
public void setPopupPosition(int left, int top) {
// Save the position of the popup
leftPosition = left;
topPosition = top;
// Account for the difference between absolute position and the
// body's positioning context.
left -= Document.get().getBodyOffsetLeft();
top -= Document.get().getBodyOffsetTop();
// Set the popup's position manually, allowing setPopupPosition() to be
// called before show() is called (so a popup can be positioned without it
// 'jumping' on the screen).
Element elem = getElement();
elem.getStyle().setPropertyPx("left", left);
elem.getStyle().setPropertyPx("top", top);
}
/**
* Sets the popup's position using a {@link PositionCallback}, and shows the
* popup. The callback allows positioning to be performed based on the
* offsetWidth and offsetHeight of the popup, which are normally not available
* until the popup is showing. By positioning the popup before it is shown,
* the the popup will not jump from its original position to the new position.
*
* @param callback the callback to set the position of the popup
* @see PositionCallback#setPosition(int offsetWidth, int offsetHeight)
*/
public void setPopupPositionAndShow(PositionCallback callback) {
setVisible(false);
show();
callback.setPosition(getOffsetWidth(), getOffsetHeight());
setVisible(true);
}
/**
* <p>
* When enabled, the popup will preview all native events, even if another
* popup was opened after this one.
* </p>
* <p>
* If autoHide is enabled, enabling this feature will cause the popup to
* autoHide even if another non-modal popup was shown after it. If this
* feature is disabled, the popup will only autoHide if it was the last popup
* opened.
* </p>
*
* @param previewAllNativeEvents true to enable, false to disable
*/
public void setPreviewingAllNativeEvents(boolean previewAllNativeEvents) {
this.previewAllNativeEvents = previewAllNativeEvents;
}
@Override
public void setTitle(String title) {
Element containerElement = getContainerElement();
if (title == null || title.length() == 0) {
containerElement.removeAttribute("title");
} else {
containerElement.setAttribute("title", title);
}
}
/**
* Sets whether this object is visible. This method just sets the
* <code>visibility</code> style attribute. You need to call {@link #show()}
* to actually attached/detach the {@link PopupPanel} to the page.
*
* @param visible <code>true</code> to show the object, <code>false</code> to
* hide it
* @see #show()
* @see #hide()
*/
@Override
public void setVisible(boolean visible) {
// We use visibility here instead of UIObject's default of display
// Because the panel is absolutely positioned, this will not create
// "holes" in displayed contents and it allows normal layout passes
// to occur so the size of the PopupPanel can be reliably determined.
DOM.setStyleAttribute(getElement(), "visibility", visible ? "visible"
: "hidden");
// If the PopupImpl creates an iframe shim, it's also necessary to hide it
// as well.
impl.setVisible(getElement(), visible);
if (glass != null) {
impl.setVisible(glass, visible);
glass.getStyle().setProperty("visibility", visible ? "visible" : "hidden");
}
}
@Override
public void setWidget(Widget w) {
super.setWidget(w);
maybeUpdateSize();
}
/**
* Sets the width of the panel's child widget. If the panel's child widget has
* not been set, the width passed in will be cached and used to set the width
* immediately after the child widget is set.
*
* <p>
* Note that subclasses may have a different behavior. A subclass may decide
* not to change the width of the child widget. It may instead decide to
* change the width of an internal panel widget, which contains the child
* widget.
* </p>
*
* @param width the object's new width, in CSS units (e.g. "10px", "1em")
*/
@Override
public void setWidth(String width) {
desiredWidth = width;
maybeUpdateSize();
// If the user cleared the size, revert to not trying to control children.
if (width.length() == 0) {
desiredWidth = null;
}
}
/**
* Shows the popup and attach it to the page. It must have a child widget
* before this method is called.
*/
public void show() {
if (showing) {
return;
} else if (isAttached()) {
// The popup is attached directly to another panel, so we need to remove
// it from its parent before showing it. This is a weird use case, but
// since PopupPanel is a Widget, its legal.
this.removeFromParent();
}
resizeAnimation.setState(true, false);
}
/**
* Normally, the popup is positioned directly below the relative target, with
* its left edge aligned with the left edge of the target. Depending on the
* width and height of the popup and the distance from the target to the
* bottom and right edges of the window, the popup may be displayed directly
* above the target, and/or its right edge may be aligned with the right edge
* of the target.
*
* @param target the target to show the popup below
*/
public final void showRelativeTo(final UIObject target) {
// Set the position of the popup right before it is shown.
setPopupPositionAndShow(new PositionCallback() {
public void setPosition(int offsetWidth, int offsetHeight) {
position(target, offsetWidth, offsetHeight);
}
});
}
@Override
protected com.google.gwt.user.client.Element getContainerElement() {
return impl.getContainerElement(getPopupImplElement()).cast();
}
/**
* Get the glass element used by this {@link PopupPanel}. The element is not
* created until it is enabled via {@link #setGlassEnabled(boolean)}.
*
* @return the glass element, or null if not created
*/
protected Element getGlassElement() {
return glass;
}
@Override
protected com.google.gwt.user.client.Element getStyleElement() {
return impl.getStyleElement(getPopupImplElement()).cast();
}
protected void onPreviewNativeEvent(NativePreviewEvent event) {
// Cancel the event based on the deprecated onEventPreview() method
if (event.isFirstHandler()
&& !onEventPreview(Event.as(event.getNativeEvent()))) {
event.cancel();
}
}
@Override
protected void onUnload() {
super.onUnload();
// Just to be sure, we perform cleanup when the popup is unloaded (i.e.
// removed from the DOM). This is normally taken care of in hide(), but it
// can be missed if someone removes the popup directly from the RootPanel.
if (isShowing()) {
resizeAnimation.setState(false, true);
}
}
/**
* We control size by setting our child widget's size. However, if we don't
* currently have a child, we record the size the user wanted so that when we
* do get a child, we can set it correctly. Until size is explicitly cleared,
* any child put into the popup will be given that size.
*/
void maybeUpdateSize() {
// For subclasses of PopupPanel, we want the default behavior of setWidth
// and setHeight to change the dimensions of PopupPanel's child widget.
// We do this because PopupPanel's child widget is the first widget in
// the hierarchy which provides structure to the panel. DialogBox is
// an example of this. We want to set the dimensions on DialogBox's
// FlexTable, which is PopupPanel's child widget. However, it is not
// DialogBox's child widget. To make sure that we are actually getting
// PopupPanel's child widget, we have to use super.getWidget().
Widget w = super.getWidget();
if (w != null) {
if (desiredHeight != null) {
w.setHeight(desiredHeight);
}
if (desiredWidth != null) {
w.setWidth(desiredWidth);
}
}
}
/**
* Sets the animation used to animate this popup. Used by gwt-incubator to
* allow DropDownPanel to override the default popup animation. Not protected
* because the exact API may change in gwt 1.6.
*
* @param animation the animation to use for this popup
*/
void setAnimation(ResizeAnimation animation) {
resizeAnimation = animation;
}
/**
* Enable or disable animation of the {@link PopupPanel}.
*
* @param type the type of animation to use
*/
void setAnimationType(AnimationType type) {
animType = type;
}
/**
* Remove focus from an Element.
*
* @param elt The Element on which <code>blur()</code> will be invoked
*/
private native void blur(Element elt) /*-{
// Issue 2390: blurring the body causes IE to disappear to the background
if (elt.blur && elt != $doc.body) {
elt.blur();
}
}-*/;
/**
* Does the event target one of the partner elements?
*
* @param event the native event
* @return true if the event targets a partner
*/
private boolean eventTargetsPartner(NativeEvent event) {
if (autoHidePartners == null) {
return false;
}
EventTarget target = event.getEventTarget();
if (Element.is(target)) {
for (Element elem : autoHidePartners) {
if (elem.isOrHasChild(Element.as(target))) {
return true;
}
}
}
return false;
}
/**
* Does the event target this popup?
*
* @param event the native event
* @return true if the event targets the popup
*/
private boolean eventTargetsPopup(NativeEvent event) {
EventTarget target = event.getEventTarget();
if (Element.is(target)) {
return getElement().isOrHasChild(Element.as(target));
}
return false;
}
/**
* Get the element that {@link PopupImpl} uses. PopupImpl creates an element
* that goes inside of the outer element, so all methods in PopupImpl are
* relative to the first child of the outer element, not the outer element
* itself.
*
* @return the Element that {@link PopupImpl} creates and expects
*/
private com.google.gwt.user.client.Element getPopupImplElement() {
return DOM.getFirstChild(super.getContainerElement());
}
/**
* Positions the popup, called after the offset width and height of the popup
* are known.
*
* @param relativeObject the ui object to position relative to
* @param offsetWidth the drop down's offset width
* @param offsetHeight the drop down's offset height
*/
private void position(final UIObject relativeObject, int offsetWidth,
int offsetHeight) {
// Calculate left position for the popup. The computation for
// the left position is bidi-sensitive.
int textBoxOffsetWidth = relativeObject.getOffsetWidth();
// Compute the difference between the popup's width and the
// textbox's width
int offsetWidthDiff = offsetWidth - textBoxOffsetWidth;
int left;
if (LocaleInfo.getCurrentLocale().isRTL()) { // RTL case
int textBoxAbsoluteLeft = relativeObject.getAbsoluteLeft();
// Right-align the popup. Note that this computation is
// valid in the case where offsetWidthDiff is negative.
left = textBoxAbsoluteLeft - offsetWidthDiff;
// If the suggestion popup is not as wide as the text box, always
// align to the right edge of the text box. Otherwise, figure out whether
// to right-align or left-align the popup.
if (offsetWidthDiff > 0) {
// Make sure scrolling is taken into account, since
// box.getAbsoluteLeft() takes scrolling into account.
int windowRight = Window.getClientWidth() + Window.getScrollLeft();
int windowLeft = Window.getScrollLeft();
// Compute the left value for the right edge of the textbox
int textBoxLeftValForRightEdge = textBoxAbsoluteLeft
+ textBoxOffsetWidth;
// Distance from the right edge of the text box to the right edge
// of the window
int distanceToWindowRight = windowRight - textBoxLeftValForRightEdge;
// Distance from the right edge of the text box to the left edge of the
// window
int distanceFromWindowLeft = textBoxLeftValForRightEdge - windowLeft;
// If there is not enough space for the overflow of the popup's
// width to the right of the text box and there IS enough space for the
// overflow to the right of the text box, then left-align the popup.
// However, if there is not enough space on either side, stick with
// right-alignment.
if (distanceFromWindowLeft < offsetWidth
&& distanceToWindowRight >= offsetWidthDiff) {
// Align with the left edge of the text box.
left = textBoxAbsoluteLeft;
}
}
} else { // LTR case
// Left-align the popup.
left = relativeObject.getAbsoluteLeft();
// If the suggestion popup is not as wide as the text box, always align to
// the left edge of the text box. Otherwise, figure out whether to
// left-align or right-align the popup.
if (offsetWidthDiff > 0) {
// Make sure scrolling is taken into account, since
// box.getAbsoluteLeft() takes scrolling into account.
int windowRight = Window.getClientWidth() + Window.getScrollLeft();
int windowLeft = Window.getScrollLeft();
// Distance from the left edge of the text box to the right edge
// of the window
int distanceToWindowRight = windowRight - left;
// Distance from the left edge of the text box to the left edge of the
// window
int distanceFromWindowLeft = left - windowLeft;
// If there is not enough space for the overflow of the popup's
// width to the right of hte text box, and there IS enough space for the
// overflow to the left of the text box, then right-align the popup.
// However, if there is not enough space on either side, then stick with
// left-alignment.
if (distanceToWindowRight < offsetWidth
&& distanceFromWindowLeft >= offsetWidthDiff) {
// Align with the right edge of the text box.
left -= offsetWidthDiff;
}
}
}
// Calculate top position for the popup
int top = relativeObject.getAbsoluteTop();
// Make sure scrolling is taken into account, since
// box.getAbsoluteTop() takes scrolling into account.
int windowTop = Window.getScrollTop();
int windowBottom = Window.getScrollTop() + Window.getClientHeight();
// Distance from the top edge of the window to the top edge of the
// text box
int distanceFromWindowTop = top - windowTop;
// Distance from the bottom edge of the window to the bottom edge of
// the text box
int distanceToWindowBottom = windowBottom
- (top + relativeObject.getOffsetHeight());
// If there is not enough space for the popup's height below the text
// box and there IS enough space for the popup's height above the text
// box, then then position the popup above the text box. However, if there
// is not enough space on either side, then stick with displaying the
// popup below the text box.
if (distanceToWindowBottom < offsetHeight
&& distanceFromWindowTop >= offsetHeight) {
top -= offsetHeight;
} else {
// Position above the text box
top += relativeObject.getOffsetHeight();
}
setPopupPosition(left, top);
}
/**
* Preview the {@link NativePreviewEvent}.
*
* @param event the {@link NativePreviewEvent}
*/
private void previewNativeEvent(NativePreviewEvent event) {
// If the event has been canceled or consumed, ignore it
if (event.isCanceled() || (!previewAllNativeEvents && event.isConsumed())) {
// We need to ensure that we cancel the event even if its been consumed so
// that popups lower on the stack do not auto hide
if (modal) {
event.cancel();
}
return;
}
// Fire the event hook and return if the event is canceled
onPreviewNativeEvent(event);
if (event.isCanceled()) {
return;
}
// If the event targets the popup or the partner, consume it
Event nativeEvent = Event.as(event.getNativeEvent());
boolean eventTargetsPopupOrPartner = eventTargetsPopup(nativeEvent)
|| eventTargetsPartner(nativeEvent);
if (eventTargetsPopupOrPartner) {
event.consume();
}
// Cancel the event if it doesn't target the modal popup. Note that the
// event can be both canceled and consumed.
if (modal) {
event.cancel();
}
// Switch on the event type
int type = nativeEvent.getTypeInt();
switch (type) {
case Event.ONKEYDOWN: {
if (!onKeyDownPreview((char) nativeEvent.getKeyCode(),
KeyboardListenerCollection.getKeyboardModifiers(nativeEvent))) {
event.cancel();
}
return;
}
case Event.ONKEYUP: {
if (!onKeyUpPreview((char) nativeEvent.getKeyCode(),
KeyboardListenerCollection.getKeyboardModifiers(nativeEvent))) {
event.cancel();
}
return;
}
case Event.ONKEYPRESS: {
if (!onKeyPressPreview((char) nativeEvent.getKeyCode(),
KeyboardListenerCollection.getKeyboardModifiers(nativeEvent))) {
event.cancel();
}
return;
}
case Event.ONMOUSEDOWN:
case Event.ONTOUCHSTART:
// Don't eat events if event capture is enabled, as this can
// interfere with dialog dragging, for example.
if (DOM.getCaptureElement() != null) {
event.consume();
return;
}
if (!eventTargetsPopupOrPartner && autoHide) {
hide(true);
return;
}
break;
case Event.ONMOUSEUP:
case Event.ONMOUSEMOVE:
case Event.ONCLICK:
case Event.ONDBLCLICK:
case Event.ONTOUCHEND: {
// Don't eat events if event capture is enabled, as this can
// interfere with dialog dragging, for example.
if (DOM.getCaptureElement() != null) {
event.consume();
return;
}
break;
}
case Event.ONFOCUS: {
Element target = nativeEvent.getTarget();
if (modal && !eventTargetsPopupOrPartner && (target != null)) {
blur(target);
event.cancel();
return;
}
break;
}
}
}
/**
* Register or unregister the handlers used by {@link PopupPanel}.
*/
private void updateHandlers() {
// Remove any existing handlers.
if (nativePreviewHandlerRegistration != null) {
nativePreviewHandlerRegistration.removeHandler();
nativePreviewHandlerRegistration = null;
}
if (historyHandlerRegistration != null) {
historyHandlerRegistration.removeHandler();
historyHandlerRegistration = null;
}
// Create handlers if showing.
if (showing) {
nativePreviewHandlerRegistration = Event.addNativePreviewHandler(new NativePreviewHandler() {
public void onPreviewNativeEvent(NativePreviewEvent event) {
previewNativeEvent(event);
}
});
historyHandlerRegistration = History.addValueChangeHandler(new ValueChangeHandler<String>() {
public void onValueChange(ValueChangeEvent<String> event) {
if (autoHideOnHistoryEvents) {
hide();
}
}
});
}
}
}