Remove the glass panel from DOM when Popup#removeFromParent is called.
http://gwt-code-reviews.appspot.com/220801

Review by: jgw@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7746 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/client/ui/PopupPanel.java b/user/src/com/google/gwt/user/client/ui/PopupPanel.java
index ed49e9f..8e9dcd2 100644
--- a/user/src/com/google/gwt/user/client/ui/PopupPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/PopupPanel.java
@@ -34,12 +34,11 @@
 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.Command;
 import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.DeferredCommand;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.EventPreview;
 import com.google.gwt.user.client.History;
+import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.Event.NativePreviewEvent;
 import com.google.gwt.user.client.Event.NativePreviewHandler;
@@ -134,6 +133,13 @@
     private PopupPanel curPanel = null;
 
     /**
+     * Indicates whether or not the {@link PopupPanel} is in the process of
+     * unloading. If the popup is unloading, then the animation just does
+     * cleanup.
+     */
+    private boolean isUnloading;
+
+    /**
      * The offset height and width of the current {@link PopupPanel}.
      */
     private int offsetHeight, offsetWidth = -1;
@@ -144,6 +150,11 @@
     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;
@@ -166,12 +177,25 @@
      * 
      * @param showing true if the popup is showing, false if not
      */
-    public void setState(boolean showing) {
+    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 = curPanel.isAnimationEnabled;
+      boolean animate = !isUnloading && curPanel.isAnimationEnabled;
       if (curPanel.animType != AnimationType.CENTER && !showing) {
         animate = false;
       }
@@ -196,14 +220,21 @@
           impl.setClip(curPanel.getElement(), getRectString(0, 0, 0, 0));
           RootPanel.get().add(curPanel);
           impl.onShow(curPanel.getElement());
-        }
 
-        // Wait for the popup panel to be attached before running the animation
-        DeferredCommand.addCommand(new Command() {
-          public void execute() {
-            run(ANIMATION_DURATION);
-          }
-        });
+          // 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();
       }
@@ -213,7 +244,9 @@
     protected void onComplete() {
       if (!showing) {
         maybeShowGlass();
-        RootPanel.get().remove(curPanel);
+        if (!isUnloading) {
+          RootPanel.get().remove(curPanel);
+        }
         impl.onHide(curPanel.getElement());
       }
       impl.setClip(curPanel.getElement(), "rect(auto, auto, auto, auto)");
@@ -311,7 +344,9 @@
         RootPanel.get().add(curPanel);
         impl.onShow(curPanel.getElement());
       } else {
-        RootPanel.get().remove(curPanel);
+        if (!isUnloading) {
+          RootPanel.get().remove(curPanel);
+        }
         impl.onHide(curPanel.getElement());
       }
       DOM.setStyleAttribute(curPanel.getElement(), "overflow", "visible");
@@ -582,7 +617,7 @@
     if (!isShowing()) {
       return;
     }
-    setState(false, true);
+    resizeAnimation.setState(false, false);
     CloseEvent.fire(this, this, autoClosed);
   }
 
@@ -967,7 +1002,7 @@
     if (showing) {
       return;
     }
-    setState(true, true);
+    resizeAnimation.setState(true, false);
   }
 
   /**
@@ -1019,11 +1054,13 @@
 
   @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()) {
-      setState(false, false);
+      resizeAnimation.setState(false, true);
     }
   }
 
@@ -1363,22 +1400,20 @@
   }
 
   /**
-   * 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
+   * Register or unregister the handlers used by {@link PopupPanel}.
    */
-  private void setState(boolean showing, boolean maybeAnimate) {
-    if (maybeAnimate) {
-      resizeAnimation.setState(showing);
-    } else {
-      resizeAnimation.cancel();
+  private void updateHandlers() {
+    // Remove any existing handlers.
+    if (nativePreviewHandlerRegistration != null) {
+      nativePreviewHandlerRegistration.removeHandler();
+      nativePreviewHandlerRegistration = null;
     }
-    this.showing = showing;
+    if (historyHandlerRegistration != null) {
+      historyHandlerRegistration.removeHandler();
+      historyHandlerRegistration = null;
+    }
 
-    // Create or remove the native preview handler
+    // Create handlers if showing.
     if (showing) {
       nativePreviewHandlerRegistration = Event.addNativePreviewHandler(new NativePreviewHandler() {
         public void onPreviewNativeEvent(NativePreviewEvent event) {
@@ -1392,15 +1427,6 @@
           }
         }
       });
-    } else {
-      if (nativePreviewHandlerRegistration != null) {
-        nativePreviewHandlerRegistration.removeHandler();
-        nativePreviewHandlerRegistration = null;
-      }
-      if (historyHandlerRegistration != null) {
-        historyHandlerRegistration.removeHandler();
-        historyHandlerRegistration = null;
-      }
     }
   }
 }
diff --git a/user/test/com/google/gwt/user/client/ui/PopupTest.java b/user/test/com/google/gwt/user/client/ui/PopupTest.java
index 8277412..d6de326 100644
--- a/user/test/com/google/gwt/user/client/ui/PopupTest.java
+++ b/user/test/com/google/gwt/user/client/ui/PopupTest.java
@@ -271,6 +271,23 @@
   }
 
   /**
+   * Test the hiding a popup while it is showing will not result in an illegal
+   * state.
+   */
+  public void testHideWhileShowing() {
+    PopupPanel popup = createPopupPanel();
+
+    // Start showing the popup.
+    popup.setAnimationEnabled(true);
+    popup.show();
+    assertTrue(popup.isShowing());
+
+    // Hide the popup while its showing.
+    popup.hide();
+    assertFalse(popup.isShowing());
+  }
+
+  /**
    * Test that the onLoad method is only called once when showing the popup.
    */
   public void testOnLoad() {
@@ -366,6 +383,21 @@
     popup.hide();
   }
 
+  /**
+   * Issue 4720: Glass is not removed when removeFromParent is called.
+   */
+  public void testRemoveFromParent() {
+    PopupPanel popup = createPopupPanel();
+    popup.setGlassEnabled(true);
+    assertNull(popup.getGlassElement().getParentElement());
+
+    popup.show();
+    assertNotNull(popup.getGlassElement().getParentElement());
+
+    popup.removeFromParent();
+    assertNull(popup.getGlassElement().getParentElement());
+  }
+
   public void testSeparateContainers() {
     TestablePopupPanel p1 = new TestablePopupPanel();
     TestablePopupPanel p2 = new TestablePopupPanel();