001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.user.client.ui;
017:
018: import com.google.gwt.core.client.GWT;
019: import com.google.gwt.user.client.DOM;
020: import com.google.gwt.user.client.Element;
021: import com.google.gwt.user.client.Event;
022: import com.google.gwt.user.client.EventPreview;
023: import com.google.gwt.user.client.Window;
024: import com.google.gwt.user.client.ui.impl.PopupImpl;
025:
026: /**
027: * A panel that can "pop up" over other widgets. It overlays the browser's
028: * client area (and any previously-created popups). <p/> The width and height of
029: * the PopupPanel cannot be explicitly set; they are determined by the
030: * PopupPanel's widget. Calls to {@link #setWidth(String)} and
031: * {@link #setHeight(String)} will call these methods on the PopupPanel's
032: * widget.
033: * <p>
034: * <img class='gallery' src='PopupPanel.png'/>
035: * </p>
036: *
037: * <p>
038: * <h3>Example</h3>
039: * {@example com.google.gwt.examples.PopupPanelExample}
040: * </p>
041: */
042: public class PopupPanel extends SimplePanel implements
043: SourcesPopupEvents, EventPreview {
044:
045: /**
046: * A callback that is used to set the position of a {@link PopupPanel} right
047: * before it is shown.
048: */
049: public interface PositionCallback {
050:
051: /**
052: * Provides the opportunity to set the position of the PopupPanel right
053: * before the PopupPanel is shown. The offsetWidth and offsetHeight values
054: * of the PopupPanel are made available to allow for positioning based on
055: * its size.
056: *
057: * @param offsetWidth the offsetWidth of the PopupPanel
058: * @param offsetHeight the offsetHeight of the PopupPanel
059: * @see PopupPanel#setPopupPositionAndShow(PositionCallback)
060: */
061: public void setPosition(int offsetWidth, int offsetHeight);
062: }
063:
064: private static final PopupImpl impl = GWT.create(PopupImpl.class);
065:
066: private boolean autoHide, modal, showing;
067:
068: // Used to track requested size across changing child widgets
069: private String desiredHeight;
070:
071: private String desiredWidth;
072:
073: // the left style attribute in pixels
074: private int leftPosition = -1;
075:
076: // The top style attribute in pixels
077: private int topPosition = -1;
078:
079: private PopupListenerCollection popupListeners;
080:
081: /**
082: * Creates an empty popup panel. A child widget must be added to it before it
083: * is shown.
084: */
085: public PopupPanel() {
086: super (impl.createElement());
087:
088: // Default position of popup should be in the upper-left corner of the
089: // window. By setting a default position, the popup will not appear in
090: // an undefined location if it is shown before its position is set.
091: setPopupPosition(0, 0);
092: }
093:
094: /**
095: * Creates an empty popup panel, specifying its "auto-hide" property.
096: *
097: * @param autoHide <code>true</code> if the popup should be automatically
098: * hidden when the user clicks outside of it
099: */
100: public PopupPanel(boolean autoHide) {
101: this ();
102: this .autoHide = autoHide;
103: }
104:
105: /**
106: * Creates an empty popup panel, specifying its "auto-hide" property.
107: *
108: * @param autoHide <code>true</code> if the popup should be automatically
109: * hidden when the user clicks outside of it
110: * @param modal <code>true</code> if keyboard or mouse events that do not
111: * target the PopupPanel or its children should be ignored
112: */
113: public PopupPanel(boolean autoHide, boolean modal) {
114: this (autoHide);
115: this .modal = modal;
116: }
117:
118: public void addPopupListener(PopupListener listener) {
119: if (popupListeners == null) {
120: popupListeners = new PopupListenerCollection();
121: }
122: popupListeners.add(listener);
123: }
124:
125: /**
126: * Centers the popup in the browser window and shows it. If the popup was
127: * already showing, then the popup is centered.
128: */
129: public void center() {
130: boolean initiallyShowing = showing;
131:
132: if (!initiallyShowing) {
133: setVisible(false);
134: show();
135: }
136:
137: int left = (Window.getClientWidth() - getOffsetWidth()) / 2;
138: int top = (Window.getClientHeight() - getOffsetHeight()) / 2;
139: setPopupPosition(Window.getScrollLeft() + left, Window
140: .getScrollTop()
141: + top);
142:
143: if (!initiallyShowing) {
144: setVisible(true);
145: }
146: }
147:
148: /**
149: * Gets the panel's offset height in pixels. Calls to
150: * {@link #setHeight(String)} before the panel's child widget is set will not
151: * influence the offset height.
152: *
153: * @return the object's offset height
154: */
155: @Override
156: public int getOffsetHeight() {
157: return super .getOffsetHeight();
158: }
159:
160: /**
161: * Gets the panel's offset width in pixels. Calls to {@link #setWidth(String)}
162: * before the panel's child widget is set will not influence the offset width.
163: *
164: * @return the object's offset width
165: */
166: @Override
167: public int getOffsetWidth() {
168: return super .getOffsetWidth();
169: }
170:
171: /**
172: * Gets the popup's left position relative to the browser's client area.
173: *
174: * @return the popup's left position
175: */
176: public int getPopupLeft() {
177: return DOM.getElementPropertyInt(getElement(), "offsetLeft");
178: }
179:
180: /**
181: * Gets the popup's top position relative to the browser's client area.
182: *
183: * @return the popup's top position
184: */
185: public int getPopupTop() {
186: return DOM.getElementPropertyInt(getElement(), "offsetTop");
187: }
188:
189: @Override
190: public String getTitle() {
191: return DOM.getElementProperty(getContainerElement(), "title");
192: }
193:
194: /**
195: * Hides the popup. This has no effect if it is not currently visible.
196: */
197: public void hide() {
198: hide(false);
199: }
200:
201: public boolean onEventPreview(Event event) {
202: Element target = DOM.eventGetTarget(event);
203: boolean eventTargetsPopup = DOM.isOrHasChild(getElement(),
204: target);
205:
206: int type = DOM.eventGetType(event);
207: switch (type) {
208: case Event.ONKEYDOWN: {
209: boolean allow = onKeyDownPreview((char) DOM
210: .eventGetKeyCode(event), KeyboardListenerCollection
211: .getKeyboardModifiers(event));
212: return allow && (eventTargetsPopup || !modal);
213: }
214: case Event.ONKEYUP: {
215: boolean allow = onKeyUpPreview((char) DOM
216: .eventGetKeyCode(event), KeyboardListenerCollection
217: .getKeyboardModifiers(event));
218: return allow && (eventTargetsPopup || !modal);
219: }
220: case Event.ONKEYPRESS: {
221: boolean allow = onKeyPressPreview((char) DOM
222: .eventGetKeyCode(event), KeyboardListenerCollection
223: .getKeyboardModifiers(event));
224: return allow && (eventTargetsPopup || !modal);
225: }
226:
227: case Event.ONMOUSEDOWN:
228: case Event.ONMOUSEUP:
229: case Event.ONMOUSEMOVE:
230: case Event.ONCLICK:
231: case Event.ONDBLCLICK: {
232: // Don't eat events if event capture is enabled, as this can interfere
233: // with dialog dragging, for example.
234: if (DOM.getCaptureElement() != null) {
235: return true;
236: }
237:
238: // If it's an outside click and auto-hide is enabled:
239: // hide the popup and _don't_ eat the event. ONMOUSEDOWN is used to
240: // prevent problems with showing a popup in response to a mousedown.
241: if (!eventTargetsPopup && autoHide
242: && (type == Event.ONMOUSEDOWN)) {
243: hide(true);
244: return true;
245: }
246:
247: break;
248: }
249:
250: case Event.ONFOCUS: {
251: if (modal && !eventTargetsPopup && (target != null)) {
252: blur(target);
253: return false;
254: }
255: }
256: }
257:
258: return !modal || eventTargetsPopup;
259: }
260:
261: /**
262: * Popups get an opportunity to preview keyboard events before they are passed
263: * to a widget contained by the Popup.
264: *
265: * @param key the key code of the depressed key
266: * @param modifiers keyboard modifiers, as specified in
267: * {@link KeyboardListener}.
268: * @return <code>false</code> to suppress the event
269: */
270: public boolean onKeyDownPreview(char key, int modifiers) {
271: return true;
272: }
273:
274: /**
275: * Popups get an opportunity to preview keyboard events before they are passed
276: * to a widget contained by the Popup.
277: *
278: * @param key the unicode character pressed
279: * @param modifiers keyboard modifiers, as specified in
280: * {@link KeyboardListener}.
281: * @return <code>false</code> to suppress the event
282: */
283: public boolean onKeyPressPreview(char key, int modifiers) {
284: return true;
285: }
286:
287: /**
288: * Popups get an opportunity to preview keyboard events before they are passed
289: * to a widget contained by the Popup.
290: *
291: * @param key the key code of the released key
292: * @param modifiers keyboard modifiers, as specified in
293: * {@link KeyboardListener}.
294: * @return <code>false</code> to suppress the event
295: */
296: public boolean onKeyUpPreview(char key, int modifiers) {
297: return true;
298: }
299:
300: public void removePopupListener(PopupListener listener) {
301: if (popupListeners != null) {
302: popupListeners.remove(listener);
303: }
304: }
305:
306: /**
307: * Sets the height of the panel's child widget. If the panel's child widget
308: * has not been set, the height passed in will be cached and used to set the
309: * height immediately after the child widget is set.
310: *
311: * <p>
312: * Note that subclasses may have a different behavior. A subclass may decide
313: * not to change the height of the child widget. It may instead decide to
314: * change the height of an internal panel widget, which contains the child
315: * widget.
316: * </p>
317: *
318: * @param height the object's new height, in CSS units (e.g. "10px", "1em")
319: */
320: @Override
321: public void setHeight(String height) {
322: desiredHeight = height;
323: maybeUpdateSize();
324: // If the user cleared the size, revert to not trying to control children.
325: if (height.length() == 0) {
326: desiredHeight = null;
327: }
328: }
329:
330: /**
331: * Sets the popup's position relative to the browser's client area. The
332: * popup's position may be set before calling {@link #show()}.
333: *
334: * @param left the left position, in pixels
335: * @param top the top position, in pixels
336: */
337: public void setPopupPosition(int left, int top) {
338: // Keep the popup within the browser's client area, so that they can't get
339: // 'lost' and become impossible to interact with. Note that we don't attempt
340: // to keep popups pegged to the bottom and right edges, as they will then
341: // cause scrollbars to appear, so the user can't lose them.
342: if (left < 0) {
343: left = 0;
344: }
345: if (top < 0) {
346: top = 0;
347: }
348:
349: // Save the position of the popup
350: leftPosition = left;
351: topPosition = top;
352:
353: // Set the popup's position manually, allowing setPopupPosition() to be
354: // called before show() is called (so a popup can be positioned without it
355: // 'jumping' on the screen).
356: Element elem = getElement();
357: DOM.setStyleAttribute(elem, "left", left + "px");
358: DOM.setStyleAttribute(elem, "top", top + "px");
359: }
360:
361: /**
362: * Sets the popup's position using a {@link PositionCallback}, and shows the
363: * popup. The callback allows positioning to be performed based on the
364: * offsetWidth and offsetHeight of the popup, which are normally not available
365: * until the popup is showing. By positioning the popup before it is shown,
366: * the the popup will not jump from its original position to the new position.
367: *
368: * @param callback the callback to set the position of the popup
369: * @see PositionCallback#setPosition(int offsetWidth, int offsetHeight)
370: */
371: public void setPopupPositionAndShow(PositionCallback callback) {
372: setVisible(false);
373: show();
374: callback.setPosition(getOffsetWidth(), getOffsetHeight());
375: setVisible(true);
376: }
377:
378: @Override
379: public void setTitle(String title) {
380: Element containerElement = getContainerElement();
381: if (title == null || title.length() == 0) {
382: DOM.removeElementAttribute(containerElement, "title");
383: } else {
384: DOM.setElementAttribute(containerElement, "title", title);
385: }
386: }
387:
388: /**
389: * Sets whether this object is visible.
390: *
391: * @param visible <code>true</code> to show the object, <code>false</code>
392: * to hide it
393: */
394: @Override
395: public void setVisible(boolean visible) {
396: // We use visibility here instead of UIObject's default of display
397: // Because the panel is absolutely positioned, this will not create
398: // "holes" in displayed contents and it allows normal layout passes
399: // to occur so the size of the PopupPanel can be reliably determined.
400: DOM.setStyleAttribute(getElement(), "visibility",
401: visible ? "visible" : "hidden");
402:
403: // If the PopupImpl creates an iframe shim, it's also necessary to hide it
404: // as well.
405: impl.setVisible(getElement(), visible);
406: }
407:
408: @Override
409: public void setWidget(Widget w) {
410: super .setWidget(w);
411: maybeUpdateSize();
412: }
413:
414: /**
415: * Sets the width of the panel's child widget. If the panel's child widget has
416: * not been set, the width passed in will be cached and used to set the width
417: * immediately after the child widget is set.
418: *
419: * <p>
420: * Note that subclasses may have a different behavior. A subclass may decide
421: * not to change the width of the child widget. It may instead decide to
422: * change the width of an internal panel widget, which contains the child
423: * widget.
424: * </p>
425: *
426: * @param width the object's new width, in CSS units (e.g. "10px", "1em")
427: */
428: @Override
429: public void setWidth(String width) {
430: desiredWidth = width;
431: maybeUpdateSize();
432: // If the user cleared the size, revert to not trying to control children.
433: if (width.length() == 0) {
434: desiredWidth = null;
435: }
436: }
437:
438: /**
439: * Shows the popup. It must have a child widget before this method is called.
440: */
441: public void show() {
442: if (showing) {
443: return;
444: }
445: showing = true;
446: DOM.addEventPreview(this );
447:
448: // Set the position attribute, and then attach to the DOM. Otherwise,
449: // the PopupPanel will appear to 'jump' from its static/relative position
450: // to its absolute position (issue #1231).
451: DOM.setStyleAttribute(getElement(), "position", "absolute");
452: if (topPosition != -1) {
453: setPopupPosition(leftPosition, topPosition);
454: }
455: RootPanel.get().add(this );
456:
457: impl.onShow(getElement());
458: }
459:
460: @Override
461: protected Element getContainerElement() {
462: return impl.getContainerElement(getElement());
463: }
464:
465: @Override
466: protected Element getStyleElement() {
467: return impl.getContainerElement(getElement());
468: }
469:
470: /**
471: * This method is called when a widget is detached from the browser's
472: * document. To receive notification before the PopupPanel is removed from the
473: * document, override the {@link Widget#onUnload()} method instead.
474: */
475: @Override
476: protected void onDetach() {
477: DOM.removeEventPreview(this );
478: super .onDetach();
479: }
480:
481: /**
482: * Remove focus from an Element.
483: *
484: * @param elt The Element on which <code>blur()</code> will be invoked
485: */
486: private native void blur(Element elt) /*-{
487: if (elt.blur) {
488: elt.blur();
489: }
490: }-*/;
491:
492: private void hide(boolean autoClosed) {
493: if (!showing) {
494: return;
495: }
496: showing = false;
497:
498: RootPanel.get().remove(this );
499: impl.onHide(getElement());
500: if (popupListeners != null) {
501: popupListeners.firePopupClosed(this , autoClosed);
502: }
503: }
504:
505: /**
506: * We control size by setting our child widget's size. However, if we don't
507: * currently have a child, we record the size the user wanted so that when we
508: * do get a child, we can set it correctly. Until size is explicitly cleared,
509: * any child put into the popup will be given that size.
510: */
511: private void maybeUpdateSize() {
512: // For subclasses of PopupPanel, we want the default behavior of setWidth
513: // and setHeight to change the dimensions of PopupPanel's child widget.
514: // We do this because PopupPanel's child widget is the first widget in
515: // the hierarchy which provides structure to the panel. DialogBox is
516: // an example of this. We want to set the dimensions on DialogBox's
517: // FlexTable, which is PopupPanel's child widget. However, it is not
518: // DialogBox's child widget. To make sure that we are actually getting
519: // PopupPanel's child widget, we have to use super.getWidget().
520: Widget w = super.getWidget();
521: if (w != null) {
522: if (desiredHeight != null) {
523: w.setHeight(desiredHeight);
524: }
525: if (desiredWidth != null) {
526: w.setWidth(desiredWidth);
527: }
528: }
529: }
530: }
|