Java tutorial
/* * 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 cc.alcina.framework.gwt.client.widget.dialog; 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.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.BrowserEvents; 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.dom.client.KeyCodes; 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.History; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.ComplexPanel; import com.google.gwt.user.client.ui.HasAnimation; import com.google.gwt.user.client.ui.SimplePanel; 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; import cc.alcina.framework.common.client.collections.CollectionFilter; /** * A panel that can "pop up" over other widgets. It overlays the browser's * client area (and any previously-created popups). * * <p> * A RelativePopupPanel 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 RelativePopupPanel cannot be explicitly set; they * are determined by the RelativePopupPanel's widget. Calls to * {@link #setWidth(String)} and {@link #setHeight(String)} will call these * methods on the RelativePopupPanel's widget. * </p> * <p> * <img class='gallery' src='doc-files/RelativePopupPanel.png'/> * </p> * * <p> * The RelativePopupPanel 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-RelativePopupPanel</dt> * <dd>the outside of the popup</dd> * <dt>.gwt-RelativePopupPanel .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 RelativePopupPanel extends SimplePanel implements HasAnimation, HasCloseHandlers<RelativePopupPanel> { /** * 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 boolean hideOnEscape; 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 RelativePopupPanel}s. */ private ResizeAnimation resizeAnimation = new ResizeAnimation(this); // The top style attribute in pixels private int topPosition = -1; private ComplexPanel positioningContainer; /** * Creates an empty popup panel. A child widget must be added to it before * it is shown. */ public RelativePopupPanel() { 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 RelativePopupPanel(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 RelativePopupPanel or its children should be * ignored */ public RelativePopupPanel(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<RelativePopupPanel> handler) { return addHandler(handler, CloseEvent.getType()); } /** * 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()) >> 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()); } public ComplexPanel getPositioningContainer() { return positioningContainer; } @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 autoClosved * the value that will be passed to * {@link CloseHandler#onClose(CloseEvent)} when the popup is * closed */ public void hide(boolean autoClosed) { if (!isShowing()) { return; } setState(false, true); 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 RelativePopupPanel}. * * @return true if enabled */ public boolean isGlassEnabled() { return isGlassEnabled; } public boolean isHideOnEscape() { return this.hideOnEscape; } /** * Returns <code>true</code> if keyboard or mouse events that do not target * the RelativePopupPanel 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); } } 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 RelativePopupPanel 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; } } public void setHideOnEscape(boolean hideOnEscape) { this.hideOnEscape = hideOnEscape; } /** * When the popup is modal, keyboard or mouse events that do not target the * RelativePopupPanel 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); } public void setPositioningContainer(ComplexPanel positioningContainer) { this.positioningContainer = positioningContainer; } /** * <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 RelativePopupPanel} 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 RelativePopupPanel can be reliably // determined. DOM.setStyleAttribute(getElement(), "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; } setState(true, true); } /** * 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); } }); } /** * 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)) { Element eTarget = Element.as(target); return getElement().isOrHasChild(eTarget); } 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 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) { switch (nativeEvent.getType()) { case BrowserEvents.KEYDOWN: case BrowserEvents.KEYPRESS: case BrowserEvents.KEYUP: if (nativeEvent.getAltKey() || nativeEvent.getMetaKey() || nativeEvent.getCtrlKey()) { return; } } event.cancel(); } // Switch on the event type int type = nativeEvent.getTypeInt(); switch (type) { 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) { EventTarget target = nativeEvent.getEventTarget(); // if (Element.is(target)) { // ClientUtils.dumpElementTree(Element.as(target)); // } hide(true); return; } break; 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) { event.consume(); return; } break; } case Event.ONFOCUS: { if (nativeEvent.getEventTarget().is(Element.class)) { Element target = nativeEvent.getEventTarget().cast(); if (modal && !eventTargetsPopupOrPartner && (target != null)) { blur(target); event.cancel(); return; } } break; } case Event.ONKEYPRESS: { if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE && isHideOnEscape()) { hide(false); event.cancel(); return; } break; } } } /** * Set the showing state of the popup. If maybeAnimate is true, the * animation will be used to set the state. If it is false, the animation * will be cancelled. * * @param showing * the new state * @param maybeAnimate * true to possibly run the animation */ private void setState(boolean showing, boolean maybeAnimate) { if (maybeAnimate) { resizeAnimation.setState(showing); } else { resizeAnimation.cancel(); } this.showing = showing; // Create or remove the native preview handler 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(); } } }); } else { if (nativePreviewHandlerRegistration != null) { nativePreviewHandlerRegistration.removeHandler(); nativePreviewHandlerRegistration = null; } if (historyHandlerRegistration != null) { historyHandlerRegistration.removeHandler(); historyHandlerRegistration = null; } } } @Override protected Element getContainerElement() { return impl.getContainerElement(getPopupImplElement()).cast(); } /** * Get the glass element used by this {@link RelativePopupPanel}. 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 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() { // 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()) { setState(false, false); } } /** * 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 RelativePopupPanel, we want the default behavior of // setWidth // and setHeight to change the dimensions of RelativePopupPanel's child // widget. // We do this because RelativePopupPanel'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 RelativePopupPanel's child widget. However, it is // not // DialogBox's child widget. To make sure that we are actually getting // RelativePopupPanel'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 RelativePopupPanel}. * * @param type * the type of animation to use */ void setAnimationType(AnimationType type) { animType = type; } /** * A callback that is used to set the position of a * {@link RelativePopupPanel} right before it is shown. */ public interface PositionCallback { /** * Provides the opportunity to set the position of the * RelativePopupPanel right before the RelativePopupPanel is shown. The * offsetWidth and offsetHeight values of the RelativePopupPanel are * made available to allow for positioning based on its size. * * @param offsetWidth * the offsetWidth of the RelativePopupPanel * @param offsetHeight * the offsetHeight of the RelativePopupPanel * @see RelativePopupPanel#setPopupPositionAndShow(PositionCallback) */ void setPosition(int offsetWidth, int offsetHeight); } public static class RelativePopupPanelFilter implements CollectionFilter<Widget> { @Override public boolean allow(Widget o) { return o instanceof RelativePopupPanel; } } /** * 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 RelativePopupPanel} being affected. */ private RelativePopupPanel curPanel = null; /** * The offset height and width of the current {@link RelativePopupPanel} * . */ private int offsetHeight, offsetWidth = -1; /** * A boolean indicating whether we are showing or hiding the popup. */ private boolean showing; /** * 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(RelativePopupPanel panel) { this.curPanel = panel; } /** * Open or close the content. This method always called immediately * after the RelativePopupPanel 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) { // Immediately complete previous open/close animation cancel(); // Determine if we need to animate boolean animate = 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 RelativePopupPanel 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)); curPanel.getPositioningContainer().add(curPanel); } // Wait for the popup panel to be attached before running the // animation Scheduler.get().scheduleDeferred(new ScheduledCommand() { public void execute() { run(ANIMATION_DURATION); } }); } 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)"; } /** * Show or hide the glass. */ private void maybeShowGlass() { if (showing) { if (curPanel.isGlassEnabled) { Document.get().getBody().appendChild(curPanel.glass); resizeRegistration = Window.addResizeHandler(curPanel.glassResizer); curPanel.glassResizer.onResize(null); glassShowing = true; } } else if (glassShowing) { Document.get().getBody().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 RelativePopupPanel 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); } curPanel.getPositioningContainer().add(curPanel); } else { curPanel.getPositioningContainer().remove(curPanel); } DOM.setStyleAttribute(curPanel.getElement(), "overflow", "visible"); } @Override protected void onComplete() { if (!showing) { maybeShowGlass(); curPanel.getPositioningContainer().remove(curPanel); } 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)); } } }