Fixes for three issues:
1652: Use getElement() consistently in UIObject and Widget
2195: Change UIObject.setElement() so that it's callable only once
2196: Remove Composite.getElement()
2197: Composite.onBrowserEvent() is never called

I have also confirmed that the changes for issue 1652 (along with removing Composite.getElement()) generate a compiled KitchenSink with no references to getElement() (it's inlined in every case).

Patch by: jgw
Review by: bruce (desk review)


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@2133 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/client/ui/AbsolutePanel.java b/user/src/com/google/gwt/user/client/ui/AbsolutePanel.java
index 8118e29..1cd716b 100644
--- a/user/src/com/google/gwt/user/client/ui/AbsolutePanel.java
+++ b/user/src/com/google/gwt/user/client/ui/AbsolutePanel.java
@@ -51,7 +51,17 @@
    * Creates an empty absolute panel.
    */
   public AbsolutePanel() {
-    setElement(DOM.createDiv());
+    this(DOM.createDiv());
+  }
+
+  /**
+   * Creates an AbsolutePanel with the given element. This is package-protected
+   * so that it can be used by {@link RootPanel}.
+   * 
+   * @param elem the element to be used for this panel
+   */
+  AbsolutePanel(Element elem) {
+    setElement(elem);
 
     // Setting the panel's position style to 'relative' causes it to be treated
     // as a new positioning context for its children.
diff --git a/user/src/com/google/gwt/user/client/ui/Composite.java b/user/src/com/google/gwt/user/client/ui/Composite.java
index 1a7b893..7368260 100644
--- a/user/src/com/google/gwt/user/client/ui/Composite.java
+++ b/user/src/com/google/gwt/user/client/ui/Composite.java
@@ -15,7 +15,8 @@
  */
 package com.google.gwt.user.client.ui;
 
-import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
 
 /**
  * A type of widget that can wrap another widget, hiding the wrapped widget's
@@ -36,18 +37,6 @@
 
   private Widget widget;
 
-  /**
-   * This override checks to ensure {@link #initWidget(Widget)} has been called.
-   */
-  @Override
-  public Element getElement() {
-    if (widget == null) {
-      throw new IllegalStateException("initWidget() was never called in "
-          + this.getClass().getName());
-    }
-    return super.getElement();
-  }
-
   @Override
   public boolean isAttached() {
     if (widget != null) {
@@ -95,8 +84,23 @@
   }
 
   @Override
+  public void onBrowserEvent(Event event) {
+    // Delegate events to the widget.
+    widget.onBrowserEvent(event);
+  }
+
+  @Override
   protected void onAttach() {
+    // Call the widget's onAttach() first.
     widget.onAttach();
+
+    // Clobber the widget's call to setEventListener(), causing all events to
+    // be routed to this composite, which will delegate back to the widget by
+    // default (note: it's not necessary to clear this in onDetach(), because
+    // the widget's onDetach will do so).
+    DOM.setEventListener(getElement(), this);
+
+    // Call onLoad() directly, because we're not calling super.onAttach().
     onLoad();
   }
 
diff --git a/user/src/com/google/gwt/user/client/ui/Image.java b/user/src/com/google/gwt/user/client/ui/Image.java
index 979c9a3..e5942f1 100644
--- a/user/src/com/google/gwt/user/client/ui/Image.java
+++ b/user/src/com/google/gwt/user/client/ui/Image.java
@@ -99,7 +99,7 @@
   private static class UnclippedState extends State {
 
     UnclippedState(Image image) {
-      image.setElement(DOM.createImg());
+      image.replaceElement(DOM.createImg());
       image.sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.ONLOAD
           | Event.ONERROR | Event.ONMOUSEWHEEL);
     }
@@ -179,7 +179,7 @@
       this.width = width;
       this.height = height;
       this.url = url;
-      image.setElement(impl.createStructure(url, left, top, width, height));
+      image.replaceElement(impl.createStructure(url, left, top, width, height));
       image.sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.ONMOUSEWHEEL);
       fireSyntheticLoadEvent(image);
     }
diff --git a/user/src/com/google/gwt/user/client/ui/NamedFrame.java b/user/src/com/google/gwt/user/client/ui/NamedFrame.java
index 08ce31d..66e0cc9 100644
--- a/user/src/com/google/gwt/user/client/ui/NamedFrame.java
+++ b/user/src/com/google/gwt/user/client/ui/NamedFrame.java
@@ -69,7 +69,7 @@
     DOM.setInnerHTML(div, "<iframe name='" + name + "'>");
 
     Element iframe = DOM.getFirstChild(div);
-    setElement(iframe);
+    replaceElement(iframe);
   }
 
   /**
diff --git a/user/src/com/google/gwt/user/client/ui/RootPanel.java b/user/src/com/google/gwt/user/client/ui/RootPanel.java
index d10b6d6..c1f0738 100644
--- a/user/src/com/google/gwt/user/client/ui/RootPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/RootPanel.java
@@ -57,10 +57,11 @@
    */
   public static RootPanel get(String id) {
     // See if this RootPanel is already created.
-    RootPanel gwt = rootPanels.get(id);
-    if (gwt != null) {
-      return gwt;
+    RootPanel rp = rootPanels.get(id);
+    if (rp != null) {
+      return rp;
     }
+
     // Find the element that this RootPanel will wrap.
     Element elem = null;
     if (id != null) {
@@ -74,8 +75,12 @@
     }
 
     // Create the panel and put it in the map.
-    rootPanels.put(id, gwt = new RootPanel(elem));
-    return gwt;
+    if (elem == null) {
+      // 'null' means use document's body element.
+      elem = getBodyElement();
+    }
+    rootPanels.put(id, rp = new RootPanel(elem));
+    return rp;
   }
 
   /**
@@ -108,12 +113,7 @@
   }
 
   private RootPanel(Element elem) {
-    if (elem == null) {
-      // 'null' means use document's body element.
-      elem = getBodyElement();
-    }
-
-    setElement(elem);
+    super(elem);
     onAttach();
   }
 }
diff --git a/user/src/com/google/gwt/user/client/ui/UIObject.java b/user/src/com/google/gwt/user/client/ui/UIObject.java
index 65c7fad..ea27f10 100644
--- a/user/src/com/google/gwt/user/client/ui/UIObject.java
+++ b/user/src/com/google/gwt/user/client/ui/UIObject.java
@@ -82,6 +82,13 @@
  * </p>
  */
 public abstract class UIObject {
+
+  static final String SETELEMENT_TWICE_ERROR = "Element may only be set once";
+
+  static final String MISSING_ELEMENT_ERROR = "This UIObject's element is not set; "
+    + "you may be missing a call to either Composite.initWidget() or "
+    + "UIObject.setElement()";
+
   /**
    * The implementation of the set debug id method, which does nothing by
    * default.
@@ -440,9 +447,15 @@
   /**
    * Gets a handle to the object's underlying DOM element.
    * 
+   * This method should not be overridden. It is non-final solely to support
+   * legacy code that depends upon overriding it. If it is overridden, the
+   * subclass implementation must not return a different element than was
+   * previously set using {@link #setElement(Element)}.
+   * 
    * @return the object's browser element
    */
   public Element getElement() {
+    assert (element != null) : MISSING_ELEMENT_ERROR;
     return element;
   }
 
@@ -453,7 +466,7 @@
    * @return the object's offset height
    */
   public int getOffsetHeight() {
-    return DOM.getElementPropertyInt(element, "offsetHeight");
+    return DOM.getElementPropertyInt(getElement(), "offsetHeight");
   }
 
   /**
@@ -463,7 +476,7 @@
    * @return the object's offset width
    */
   public int getOffsetWidth() {
-    return DOM.getElementPropertyInt(element, "offsetWidth");
+    return DOM.getElementPropertyInt(getElement(), "offsetWidth");
   }
 
   /**
@@ -497,7 +510,7 @@
    * @return the object's title
    */
   public String getTitle() {
-    return DOM.getElementProperty(element, "title");
+    return DOM.getElementProperty(getElement(), "title");
   }
 
   /**
@@ -506,7 +519,7 @@
    * @return <code>true</code> if the object is visible
    */
   public boolean isVisible() {
-    return isVisible(element);
+    return isVisible(getElement());
   }
 
   /**
@@ -543,7 +556,7 @@
     // This exists to deal with an inconsistency in IE's implementation where
     // it won't accept negative numbers in length measurements
     assert extractLengthValue(height.trim().toLowerCase()) >= 0 : "CSS heights should not be negative";
-    DOM.setStyleAttribute(element, "height", height);
+    DOM.setStyleAttribute(getElement(), "height", height);
   }
 
   /**
@@ -605,9 +618,9 @@
    */
   public void setTitle(String title) {
     if (title == null || title.length() == 0) {
-      DOM.removeElementAttribute(element, "title");
+      DOM.removeElementAttribute(getElement(), "title");
     } else {
-      DOM.setElementAttribute(element, "title", title);
+      DOM.setElementAttribute(getElement(), "title", title);
     }
   }
 
@@ -618,7 +631,7 @@
    *          to hide it
    */
   public void setVisible(boolean visible) {
-    setVisible(element, visible);
+    setVisible(getElement(), visible);
   }
 
   /**
@@ -631,7 +644,7 @@
     // This exists to deal with an inconsistency in IE's implementation where
     // it won't accept negative numbers in length measurements
     assert extractLengthValue(width.trim().toLowerCase()) >= 0 : "CSS widths should not be negative";
-    DOM.setStyleAttribute(element, "width", width);
+    DOM.setStyleAttribute(getElement(), "width", width);
   }
 
   /**
@@ -656,10 +669,10 @@
    */
   @Override
   public String toString() {
-    if (element == null) {
+    if (getElement() == null) {
       return "(null handle)";
     }
-    return DOM.toString(element);
+    return DOM.toString(getElement());
   }
 
   /**
@@ -683,7 +696,7 @@
    * @return the element to which style names will be applied
    */
   protected Element getStyleElement() {
-    return element;
+    return getElement();
   }
 
   /**
@@ -713,18 +726,28 @@
 
   /**
    * Sets this object's browser element. UIObject subclasses must call this
-   * method before attempting to call any other methods.
+   * method before attempting to call any other methods, and it may only be
+   * called once.
    * 
-   * If the browser element has already been set, then the current element's
-   * position is located in the DOM and removed. The new element is added into
-   * the previous element's position.
+   * @param elem the object's element
+   */
+  protected void setElement(Element elem) {
+    assert (element == null) : SETELEMENT_TWICE_ERROR;
+    this.element = elem;
+  }
+
+  /**
+   * Replaces this object's browser element.
+   * 
+   * This method exists only to support a specific use-case in Image, and
+   * should not be used by other classes.
    * 
    * @param elem the object's new element
    */
-  protected void setElement(Element elem) {
-    if (this.element != null) {
+  void replaceElement(Element elem) {
+    if (element != null) {
       // replace this.element in its parent with elem.
-      replaceNode(this.element, elem);
+      replaceNode(element, elem);
     }
 
     this.element = elem;
diff --git a/user/src/com/google/gwt/user/client/ui/Widget.java b/user/src/com/google/gwt/user/client/ui/Widget.java
index f8809c7..e4590cd 100644
--- a/user/src/com/google/gwt/user/client/ui/Widget.java
+++ b/user/src/com/google/gwt/user/client/ui/Widget.java
@@ -164,18 +164,8 @@
   protected void onUnload() {
   }
 
-  /**
-   * Sets this object's browser element. Widget subclasses must call this method
-   * before attempting to call any other methods.
-   * 
-   * If a browser element has already been attached, then it is replaced with
-   * the new element. The old event listeners are removed from the old browser
-   * element, and the event listeners are set up on the new browser element.
-   * 
-   * @param elem the object's new element
-   */
   @Override
-  protected void setElement(Element elem) {
+  void replaceElement(Element elem) {
     if (isAttached()) {
       // Remove old event listener to avoid leaking. onDetach will not do this
       // for us, because it is only called when the widget itself is detached
@@ -183,7 +173,7 @@
       DOM.setEventListener(getElement(), null);
     }
 
-    super.setElement(elem);
+    super.replaceElement(elem);
     if (isAttached()) {
       // Hook the event listener back up on the new element. onAttach will not
       // do this for us, because it is only called when the widget itself is
diff --git a/user/test/com/google/gwt/user/UISuite.java b/user/test/com/google/gwt/user/UISuite.java
index 3fc844d..b2798e5 100644
--- a/user/test/com/google/gwt/user/UISuite.java
+++ b/user/test/com/google/gwt/user/UISuite.java
@@ -21,6 +21,7 @@
 import com.google.gwt.user.client.WindowTest;
 import com.google.gwt.user.client.ui.AbsolutePanelTest;
 import com.google.gwt.user.client.ui.CheckBoxTest;
+import com.google.gwt.user.client.ui.CompositeTest;
 import com.google.gwt.user.client.ui.CustomButtonTest;
 import com.google.gwt.user.client.ui.DOMTest;
 import com.google.gwt.user.client.ui.DecoratorPanelTest;
@@ -78,6 +79,7 @@
     suite.addTestSuite(CheckBoxTest.class);
     suite.addTestSuite(ClippedImagePrototypeTest.class);
     suite.addTestSuite(CommandExecutorTest.class);
+    suite.addTestSuite(CompositeTest.class);
     suite.addTestSuite(CookieTest.class);
     suite.addTestSuite(CustomButtonTest.class);
     suite.addTestSuite(DecoratorPanelTest.class);
diff --git a/user/test/com/google/gwt/user/client/ui/CompositeTest.java b/user/test/com/google/gwt/user/client/ui/CompositeTest.java
new file mode 100644
index 0000000..0343bc9
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/ui/CompositeTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.client.ui;
+
+import com.google.gwt.junit.client.GWTTestCase;
+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;
+
+/**
+ * Tests for {@link Composite}.
+ */
+public class CompositeTest extends GWTTestCase {
+
+  public String getModuleName() {
+    return "com.google.gwt.user.User";
+  }
+
+  private static class EventTestComposite extends Composite {
+    TextBox tb = new TextBox();
+    boolean widgetFocusFired;
+    boolean widgetLostFocusFired;
+    boolean domFocusFired;
+    boolean domBlurFired;
+
+    public EventTestComposite() {
+      initWidget(tb);
+      sinkEvents(Event.FOCUSEVENTS);
+
+      tb.addFocusListener(new FocusListener() {
+        public void onLostFocus(Widget sender) {
+          widgetLostFocusFired = true;
+        }
+
+        public void onFocus(Widget sender) {
+          widgetFocusFired = true;
+        }
+      });
+    }
+
+    @Override
+    public void onBrowserEvent(Event event) {
+      switch (DOM.eventGetType(event)) {
+        case Event.ONFOCUS:
+          domFocusFired = true;
+          // Eat the focus event.
+          return;
+
+        case Event.ONBLUR:
+          domBlurFired = true;
+          // *Don't* eat the blur event.
+          break;
+      }
+
+      super.onBrowserEvent(event);
+    }
+  }
+
+  public void testBrowserEvents() {
+    final EventTestComposite c = new EventTestComposite();
+    RootPanel.get().add(c);
+
+    this.delayTestFinish(1000);
+
+    // Focus, then blur, the composite's text box. This has to be done in
+    // deferred commands, because focus events usually require the event loop
+    // to be pumped in order to fire.
+    DeferredCommand.addCommand(new Command() {
+      public void execute() {
+        DeferredCommand.addCommand(new Command() {
+          public void execute() {
+            // Ensure all events fired as expected.
+            assertTrue(c.domFocusFired);
+            assertTrue(c.domBlurFired);
+            assertTrue(c.widgetLostFocusFired);
+
+            // Ensure that the widget's focus event was eaten by the
+            // composite's implementation of onBrowserEvent().
+            assertFalse(c.widgetFocusFired);
+            finishTest();
+          }
+        });
+
+        c.tb.setFocus(false);
+      }
+    });
+
+    c.tb.setFocus(true);
+  }
+}
diff --git a/user/test/com/google/gwt/user/client/ui/UIObjectTest.java b/user/test/com/google/gwt/user/client/ui/UIObjectTest.java
index e5dec24..7f9bc0c 100644
--- a/user/test/com/google/gwt/user/client/ui/UIObjectTest.java
+++ b/user/test/com/google/gwt/user/client/ui/UIObjectTest.java
@@ -199,6 +199,43 @@
     assertFalse(containsClass(o, "i-awt-heart"));
   }
 
+  public void testMissingElementAssertion() {
+    try {
+      Widget w = new Widget() {
+      };
+
+      w.getElement();
+      fail("Expected assertion failure");
+    } catch (AssertionError e) {
+      assertEquals(UIObject.MISSING_ELEMENT_ERROR, e.getMessage());
+    }
+
+    try {
+      Composite c = new Composite() {
+      };
+
+      c.getElement();
+      fail("Expected assertion failure");
+    } catch (AssertionError e) {
+      assertEquals(UIObject.MISSING_ELEMENT_ERROR, e.getMessage());
+    }
+  }
+
+  public void testSetElementTwiceFails() {
+    UIObject o = new UIObject() {
+      {
+        setElement(DOM.createDiv());
+      }
+    };
+
+    try {
+      o.setElement(DOM.createSpan());
+      fail("Expected assertion failure");
+    } catch (AssertionError e) {
+      assertEquals(UIObject.SETELEMENT_TWICE_ERROR, e.getMessage());
+    }
+  }
+
   private void assertPrimaryStyleNameEquals(UIObject o, String className) {
     String attr = DOM.getElementProperty(o.getElement(), "className");
     assertTrue(attr.indexOf(className) == 0);