blob: c3f1eb968fecc1b6f340250935ebbe123ac65698 [file] [log] [blame]
/*
* Copyright 2008 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.GWT;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventPreview;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.animation.WidgetAnimation;
import com.google.gwt.user.client.ui.impl.PopupImpl;
/**
* A panel that can "pop up" over other widgets. It overlays the browser's
* client area (and any previously-created popups). <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>
* <img class='gallery' src='PopupPanel.png'/>
* </p>
*
* <p>
* <h3>Example</h3>
* {@example com.google.gwt.examples.PopupPanelExample}
* </p>
* <h3>CSS Style Rules</h3>
* <ul class='css'>
* <li>.gwt-PopupPanel { the outside of the popup }</li>
* <li>.gwt-PopupPanel .content { the wrapper around the content }</li>
* </ul>
* <p>
* The styles that apply to {@link DecoratorPanel} also apply to PopupPanel.
* </p>
*/
public class PopupPanel extends DecoratorPanel implements SourcesPopupEvents,
EventPreview, HasAnimation {
/**
* 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>
*/
public static enum AnimationType {
CENTER, ONE_WAY_CORNER
}
/**
* An {@link WidgetAnimation} used to enlarge the popup into view.
*/
private static class ResizeAnimation extends WidgetAnimation {
/**
* The offset height and width of the current {@link PopupPanel}.
*/
private int offsetHeight, offsetWidth = -1;
/**
* The {@link PopupPanel} being affected.
*/
private PopupPanel curPanel = null;
@Override
public void onCancel() {
onComplete();
}
@Override
public void onComplete() {
if (!curPanel.showing) {
RootPanel.get().remove(curPanel);
impl.onHide(curPanel.getElement());
}
impl.setClip(curPanel.getElement(), "rect(auto, auto, auto, auto)");
curPanel = null;
}
@Override
public void onInstantaneousRun() {
if (curPanel.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 {
RootPanel.get().remove(curPanel);
impl.onHide(curPanel.getElement());
}
curPanel = null;
}
@Override
public void onStart() {
// Attach to the page
if (curPanel.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);
}
impl.setClip(curPanel.getElement(), getRectString(0, 0, 0, 0));
RootPanel.get().add(curPanel);
impl.onShow(curPanel.getElement());
}
offsetHeight = curPanel.getOffsetHeight();
offsetWidth = curPanel.getOffsetWidth();
onUpdate(0.0);
}
@Override
public void onUpdate(double progress) {
if (!curPanel.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);
if (curPanel.animType == AnimationType.CENTER) {
top = (offsetHeight - height) / 2;
left = (offsetWidth - width) / 2;
}
right = left + width;
bottom = top + height;
// Set the rect clipping
impl.setClip(curPanel.getElement(), getRectString(top, right, bottom,
left));
}
/**
* 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 panel the panel to open or close
*/
public void setOpen(final PopupPanel panel) {
// Immediately complete previous open/close animation
cancel();
// Determine if we need to animate
boolean animate = panel.isAnimationEnabled;
if (panel.animType == AnimationType.ONE_WAY_CORNER && !panel.showing) {
animate = false;
}
// Open the new item
curPanel = panel;
if (animate) {
run(200);
} else {
onInstantaneousRun();
}
}
/**
* @return a rect string
*/
private String getRectString(int top, int right, int bottom, int left) {
return "rect(" + top + "px, " + right + "px, " + bottom + "px, " + left
+ "px)";
}
}
/**
* 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 default style name.
*/
private static final String DEFAULT_STYLENAME = "gwt-PopupPanel";
private static final PopupImpl impl = GWT.create(PopupImpl.class);
/**
* The {@link ResizeAnimation} used to open and close the {@link PopupPanel}s.
*/
private static ResizeAnimation resizeAnimation;
/**
* 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, modal, showing;
// Used to track requested size across changing child widgets
private String desiredHeight;
private String desiredWidth;
private boolean isAnimationEnabled = true;
// the left style attribute in pixels
private int leftPosition = -1;
// The top style attribute in pixels
private int topPosition = -1;
private PopupListenerCollection popupListeners;
/**
* Creates an empty popup panel. A child widget must be added to it before it
* is shown.
*/
public PopupPanel() {
super();
DOM.appendChild(super.getContainerElement(), 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(), "content");
DOM.setStyleAttribute(getElement(), "overflow", "hidden");
}
/**
* 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
*/
public PopupPanel(boolean autoHide) {
this();
this.autoHide = autoHide;
}
/**
* 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
* @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;
}
public void addPopupListener(PopupListener listener) {
if (popupListeners == null) {
popupListeners = new PopupListenerCollection();
}
popupListeners.add(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();
}
int left = (Window.getClientWidth() - getOffsetWidth()) / 2;
int top = (Window.getClientHeight() - getOffsetHeight()) / 2;
setPopupPosition(Window.getScrollLeft() + left, Window.getScrollTop() + top);
if (!initiallyShowing) {
hide();
setVisible(true);
setAnimationEnabled(initiallyAnimated);
show();
}
}
/**
* 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.getElementPropertyInt(getElement(), "offsetLeft");
}
/**
* Gets the popup's top position relative to the browser's client area.
*
* @return the popup's top position
*/
public int getPopupTop() {
return DOM.getElementPropertyInt(getElement(), "offsetTop");
}
@Override
public String getTitle() {
return DOM.getElementProperty(getContainerElement(), "title");
}
/**
* Hides the popup. This has no effect if it is not currently visible.
*/
public void hide() {
hide(false);
}
/**
* @see HasAnimation#isAnimationEnabled()
*/
public boolean isAnimationEnabled() {
return isAnimationEnabled;
}
public boolean onEventPreview(Event event) {
Element target = DOM.eventGetTarget(event);
boolean eventTargetsPopup = (target != null)
&& DOM.isOrHasChild(getElement(), target);
int type = DOM.eventGetType(event);
switch (type) {
case Event.ONKEYDOWN: {
boolean allow = onKeyDownPreview((char) DOM.eventGetKeyCode(event),
KeyboardListenerCollection.getKeyboardModifiers(event));
return allow && (eventTargetsPopup || !modal);
}
case Event.ONKEYUP: {
boolean allow = onKeyUpPreview((char) DOM.eventGetKeyCode(event),
KeyboardListenerCollection.getKeyboardModifiers(event));
return allow && (eventTargetsPopup || !modal);
}
case Event.ONKEYPRESS: {
boolean allow = onKeyPressPreview((char) DOM.eventGetKeyCode(event),
KeyboardListenerCollection.getKeyboardModifiers(event));
return allow && (eventTargetsPopup || !modal);
}
case Event.ONMOUSEDOWN:
case Event.ONMOUSEUP:
case Event.ONMOUSEMOVE:
case Event.ONCLICK:
case Event.ONDBLCLICK: {
// Don't eat events if event capture is enabled, as this can interfere
// with dialog dragging, for example.
if (DOM.getCaptureElement() != null) {
return true;
}
// If it's an outside click and auto-hide is enabled:
// hide the popup and _don't_ eat the event. ONMOUSEDOWN is used to
// prevent problems with showing a popup in response to a mousedown.
if (!eventTargetsPopup && autoHide && (type == Event.ONMOUSEDOWN)) {
hide(true);
return true;
}
break;
}
case Event.ONFOCUS: {
if (modal && !eventTargetsPopup && (target != null)) {
blur(target);
return false;
}
}
}
return !modal || eventTargetsPopup;
}
/**
* 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 KeyboardListener}.
* @return <code>false</code> to suppress the event
*/
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 KeyboardListener}.
* @return <code>false</code> to suppress the event
*/
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 KeyboardListener}.
* @return <code>false</code> to suppress the event
*/
public boolean onKeyUpPreview(char key, int modifiers) {
return true;
}
public void removePopupListener(PopupListener listener) {
if (popupListeners != null) {
popupListeners.remove(listener);
}
}
/**
* @see HasAnimation#setAnimationEnabled(boolean)
*/
public void setAnimationEnabled(boolean enable) {
isAnimationEnabled = enable;
}
/**
* 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;
}
}
/**
* 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) {
// Keep the popup within the browser's client area, so that they can't get
// 'lost' and become impossible to interact with. Note that we don't attempt
// to keep popups pegged to the bottom and right edges, as they will then
// cause scrollbars to appear, so the user can't lose them.
if (left < 0) {
left = 0;
}
if (top < 0) {
top = 0;
}
// Save the position of the popup
leftPosition = left;
topPosition = top;
// 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();
DOM.setStyleAttribute(elem, "left", left + "px");
DOM.setStyleAttribute(elem, "top", top + "px");
}
/**
* 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);
}
@Override
public void setTitle(String title) {
Element containerElement = getContainerElement();
if (title == null || title.length() == 0) {
DOM.removeElementAttribute(containerElement, "title");
} else {
DOM.setElementAttribute(containerElement, "title", title);
}
}
/**
* Sets whether this object is visible.
*
* @param visible <code>true</code> to show the object, <code>false</code>
* to hide it
*/
@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);
}
@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. It must have a child widget before this method is called.
*/
public void show() {
if (showing) {
return;
}
showing = true;
DOM.addEventPreview(this);
if (resizeAnimation == null) {
resizeAnimation = new ResizeAnimation();
}
resizeAnimation.setOpen(this);
}
@Override
protected Element getContainerElement() {
return impl.getContainerElement(DOM.getFirstChild(super.getContainerElement()));
}
/**
* This method is called when a widget is detached from the browser's
* document. To receive notification before the PopupPanel is removed from the
* document, override the {@link Widget#onUnload()} method instead.
*/
@Override
protected void onDetach() {
DOM.removeEventPreview(this);
super.onDetach();
}
/**
* Enable or disable animation of the {@link PopupPanel}.
*
* @param type the type of animation to use
*/
protected 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) /*-{
if (elt.blur) {
elt.blur();
}
}-*/;
private void hide(boolean autoClosed) {
if (!showing) {
return;
}
showing = false;
// Hide the popup
if (resizeAnimation == null) {
resizeAnimation = new ResizeAnimation();
}
resizeAnimation.setOpen(this);
// Fire the event listeners
if (popupListeners != null) {
popupListeners.firePopupClosed(this, autoClosed);
}
}
/**
* 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.
*/
private 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);
}
}
}
}