/*
* 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 org.geogebra.web.html5.gui;
import java.util.ArrayList;
import java.util.List;
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.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
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.ui.HasAnimation;
import com.google.gwt.user.client.ui.KeyboardListenerCollection;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.PopupListener;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.SourcesPopupEvents;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.Widget;
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>
* 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 GPopupPanel extends SimplePanel implements SourcesPopupEvents,
EventPreview, HasAnimation, HasCloseHandlers<GPopupPanel> {
/**
* A callback that is used to set the position of a {@link GPopupPanel}
* 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 GPopupPanel#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>
* <li>ROLL_DOWN - Expand from the top to the bottom, do not animate hiding</li>
* </ul>
*/
public 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 GPopupPanel 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;
private final Panel root;
/**
* Create a new {@link ResizeAnimation}.
*
* @param panel
* the panel to affect
*/
public ResizeAnimation(GPopupPanel panel, Panel root) {
this.curPanel = panel;
this.root = root;
}
/**
* 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).
curPanel.getElement().getStyle()
.setProperty("position", "absolute");
if (curPanel.topPosition != -1) {
curPanel.setPopupPosition(curPanel.leftPosition,
curPanel.topPosition);
}
impl.setClip(curPanel.getElement(),
getRectString(0, 0, 0, 0));
getRootPanel().add(curPanel);
// 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) {
getRootPanel().remove(curPanel);
}
}
impl.setClip(curPanel.getElement(), "rect(auto, auto, auto, auto)");
curPanel.getElement().getStyle().setProperty("overflow", "visible");
}
@Override
protected void onStart() {
offsetHeight = curPanel.getOffsetHeight();
offsetWidth = curPanel.getOffsetWidth();
curPanel.getElement().getStyle().setProperty("overflow", "hidden");
super.onStart();
}
@Override
protected void onUpdate(double progress0) {
double progress = progress0;
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 static 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) {
getRootPanel().getElement().appendChild(curPanel.glass);
resizeRegistration = Window
.addResizeHandler(curPanel.glassResizer);
curPanel.glassResizer.onResize(null);
glassShowing = true;
}
} else if (glassShowing) {
getRootPanel().getElement().removeChild(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).
curPanel.getElement().getStyle()
.setProperty("position", "absolute");
if (curPanel.topPosition != -1) {
curPanel.setPopupPosition(curPanel.leftPosition,
curPanel.topPosition);
}
getRootPanel().add(curPanel);
} else {
if (!isUnloading) {
getRootPanel().remove(curPanel);
}
}
curPanel.getElement().getStyle().setProperty("overflow", "visible");
}
private Panel getRootPanel() {
return root;
}
}
/**
* 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() {
@Override
public void onResize(ResizeEvent event) {
Style style = glass.getStyle();
int winWidth = getRootPanel().getOffsetWidth();
int winHeight = getRootPanel().getOffsetHeight();
// 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);
// Set the glass size to the larger of the window's client size or
// the
// document's scroll size.
style.setWidth(winWidth, Unit.PX);
style.setHeight(winHeight, Unit.PX);
// The size is set. Show the glass again.
style.setDisplay(Display.BLOCK);
}
};
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 final ResizeAnimation resizeAnimation;
// The top style attribute in pixels
private int topPosition = -1;
private final Panel root;
//temporary variable for checking feature flag
public boolean hasOverlapFeature = false;
/**
* Creates an empty popup panel. A child widget must be added to it before
* it is shown.
*/
public GPopupPanel(Panel root) {
super();
this.root = root;
resizeAnimation = new ResizeAnimation(this, root);
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");
// if (this instanceof HasKeyboardPopup) {
// super.getContainerElement().getFirstChildElement()
// .addClassName("mainChild");
// }
}
protected Panel getRootPanel() {
return root;
}
/**
* 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 GPopupPanel(boolean autoHide, Panel root) {
this(root);
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 GPopupPanel(boolean autoHide, boolean modal, Panel root) {
this(autoHide, root);
this.modal = modal;
}
/**
* Temporary function to set feature flag. After releasing
* DIALOGS_OVERLAP_KEYBOARD it can be removed and the class name setting
* which in it can be moved to the constructor.
*/
public void setOverlapFeature(boolean b){
hasOverlapFeature = b;
if (b) {
if (this instanceof HasKeyboardPopup) {
super.getContainerElement().getFirstChildElement()
.addClassName("mainChild");
}
}
}
/**
* 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);
}
@Override
public HandlerRegistration addCloseHandler(CloseHandler<GPopupPanel> handler) {
return addHandler(handler, CloseEvent.getType());
}
/**
* @deprecated Use {@link #addCloseHandler} instead
*/
@Override
@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() {
center(0);
}
public void centerAndResize(double keyboardHeight){
Element childElement = super.getContainerElement()
.getFirstChildElement();
if (hasOverlapFeature){
childElement.getStyle().clearHeight();
}
center(keyboardHeight);
if (!hasOverlapFeature){
return;
}
int paddings = 30; //TODO: get sum of top and bottom paddings
int maxHeight = (int) (getRootPanel().getOffsetHeight() - keyboardHeight
- paddings);
if (childElement.getOffsetHeight() > maxHeight) {
childElement.getStyle().setHeight(maxHeight, Unit.PX);
}
}
public int center(double keyboardHeight) {
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 = (getRootPanel().getOffsetWidth() - getOffsetWidth()) >> 1;
int top = (getRootPanel().getOffsetHeight() - getOffsetHeight()
- (int) keyboardHeight) >> 1;
setPopupPosition(Math.max(left, 0), Math.max(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);
}
}
return top;
}
/**
* 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 getElement().getAbsoluteLeft();
}
/**
* Gets the popup's top position relative to the browser's client area.
*
* @return the popup's top position
*/
public int getPopupTop() {
return getElement().getAbsoluteTop();
}
@Override
public String getTitle() {
return getContainerElement().getPropertyString("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);
}
@Override
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
*/
@Override
@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
*/
@Override
@Deprecated
public void removePopupListener(PopupListener listener) {
// ListenerWrapper.WrappedPopupListener.remove(this, listener);
}
@Override
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.
int left1 = left - Document.get().getBodyOffsetLeft();
int top1 = 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", left1);
elem.getStyle().setPropertyPx("top", top1);
}
/**
* 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 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.
getElement().getStyle().setProperty("visibility",
visible ? "visible" : "hidden");
// If the PopupImpl creates an iframe shim, it's also necessary to hide
// it
// as well.
if (glass != null) {
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() {
@Override
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
*/
/**
* Set the type of animation to use when opening and closing the popup.
*
* @see AnimationType
* @param type
* the type of animation to use
*/
public void setAnimationType(AnimationType type) {
animType = type != null ? type : AnimationType.CENTER;
}
/**
* Get the type of animation to use when opening and closing the popup.
*
* @see AnimationType
* @return the type of animation used
*/
public AnimationType getAnimationType() {
return animType;
}
/**
* 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() - root
.getAbsoluteLeft()) / getScale(root.getElement(), "x");
// 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 windowLeft = root.getAbsoluteLeft();
int windowRight = root.getOffsetWidth() + windowLeft;
// 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() - root.getAbsoluteLeft())
/ getScale(root.getElement(), "x");
// 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.
// Distance from the left edge of the text box to the right edge
// of the window
int distanceToWindowRight = root.getOffsetWidth() - left;
// Distance from the left edge of the text box to the left edge
// of the
// window
int distanceFromWindowLeft = (relativeObject.getAbsoluteLeft()
- root.getAbsoluteLeft());
// 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 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() - root.getAbsoluteTop())
/ getScale(root.getElement(), "y");
// 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);
}
private static native int getScale(Element start, String dir) /*-{
while (start) {
if (start.getAttribute("data-scale" + dir)) {
return start.getAttribute("data-scale" + dir);
}
start = start.parentElement;
}
return 1;
}-*/;
/**
* 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() {
@Override
public void onPreviewNativeEvent(
NativePreviewEvent event) {
previewNativeEvent(event);
}
});
historyHandlerRegistration = History
.addValueChangeHandler(new ValueChangeHandler<String>() {
@Override
public void onValueChange(ValueChangeEvent<String> event) {
if (autoHideOnHistoryEvents) {
hide();
}
}
});
}
}
}