Revision: 9576
Author: gwt.mirror...@gmail.com
Date: Thu Jan 20 06:21:05 2011
Log: Adding a new widget ResizeLayoutPanel that can trigger a resize event when it changes size. The widget uses hidden scrollable divs to detect resize on non-IE browsers. This allow users to embed widgets that RequireResize in their app without having an unbroken chain of ProvidesResize up to the RootLayoutPanel.

Also adding a new widget HeaderPanel that uses the same resize implementation to layout content between naturally sized headers and footers. This is similar to a DockLayoutPanel (north/south/center only), but it doesn't require users to specify a specific height for the header and footer.

Review at http://gwt-code-reviews.appspot.com/1301801

Review by: p...@google.com
http://code.google.com/p/google-web-toolkit/source/detail?r=9576

Added:
 /trunk/user/src/com/google/gwt/user/ResizeLayoutPanel.gwt.xml
 /trunk/user/src/com/google/gwt/user/client/ui/HeaderPanel.java
 /trunk/user/src/com/google/gwt/user/client/ui/ResizeLayoutPanel.java
 /trunk/user/test/com/google/gwt/user/client/ui/HeaderPanelTest.java
 /trunk/user/test/com/google/gwt/user/client/ui/ResizeLayoutPanelTest.java
Modified:
 /trunk/user/src/com/google/gwt/user/User.gwt.xml
 /trunk/user/test/com/google/gwt/user/UISuite.java

=======================================
--- /dev/null
+++ /trunk/user/src/com/google/gwt/user/ResizeLayoutPanel.gwt.xml Thu Jan 20 06:21:05 2011
@@ -0,0 +1,39 @@
+<!-- --> +<!-- Copyright 2011 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 --> +<!-- 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. License for the specific language governing permissions and --> +<!-- limitations under the License. -->
+
+<!-- Deferred binding rules for ResizeLayoutPanel. --> +<!-- --> +<!-- This module is typically inherited via com.google.gwt.user.User --> +<!-- -->
+<module>
+  <inherits name="com.google.gwt.core.Core"/>
+  <inherits name="com.google.gwt.user.UserAgent"/>
+
+  <!-- Most browsers do not support onresize. -->
+ <replace-with class="com.google.gwt.user.client.ui.ResizeLayoutPanel.ImplStandard"> + <when-type-is class="com.google.gwt.user.client.ui.ResizeLayoutPanel.Impl"/>
+  </replace-with>
+
+  <!-- IE supports onresize. -->
+ <replace-with class="com.google.gwt.user.client.ui.ResizeLayoutPanel.ImplTrident"> + <when-type-is class="com.google.gwt.user.client.ui.ResizeLayoutPanel.Impl"/>
+    <when-property-is name="user.agent" value="ie8"/>
+  </replace-with>
+
+  <!-- IE6 needs to be kicked to render correctly. -->
+ <replace-with class="com.google.gwt.user.client.ui.ResizeLayoutPanel.ImplIE6"> + <when-type-is class="com.google.gwt.user.client.ui.ResizeLayoutPanel.Impl"/>
+    <when-property-is name="user.agent" value="ie6"/>
+  </replace-with>
+</module>
=======================================
--- /dev/null
+++ /trunk/user/src/com/google/gwt/user/client/ui/HeaderPanel.java Thu Jan 20 06:21:05 2011
@@ -0,0 +1,376 @@
+/*
+ * Copyright 2011 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.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Overflow;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.Element;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * A panel that includes a header (top), footer (bottom), and content (middle)
+ * area. The header and footer areas resize naturally. The content area is
+ * allocated all of the remaining space between the header and footer area.
+ */
+public class HeaderPanel extends Panel implements RequiresResize {
+
+  private Widget content;
+  private final Element contentContainer;
+  private Widget footer;
+  private final Element footerContainer;
+  private final ResizeLayoutPanel.Impl footerImpl =
+    GWT.create(ResizeLayoutPanel.Impl.class);
+  private Widget header;
+  private final Element headerContainer;
+  private final ResizeLayoutPanel.Impl headerImpl =
+    GWT.create(ResizeLayoutPanel.Impl.class);
+  private final ScheduledCommand layoutCmd = new ScheduledCommand() {
+    public void execute() {
+      layoutScheduled = false;
+      forceLayout();
+    }
+  };
+  private boolean layoutScheduled = false;
+
+  public HeaderPanel() {
+    // Create the outer element
+    Element elem = Document.get().createDivElement().cast();
+    elem.getStyle().setPosition(Position.RELATIVE);
+    elem.getStyle().setOverflow(Overflow.HIDDEN);
+    setElement(elem);
+
+    // Create a delegate to handle resize from the header and footer.
+ ResizeLayoutPanel.Impl.Delegate resizeDelegate = new ResizeLayoutPanel.Impl.Delegate() {
+      public void onResize() {
+        scheduledLayout();
+      }
+    };
+
+    // Create the header container.
+    headerContainer = createContainer();
+    headerContainer.getStyle().setTop(0.0, Unit.PX);
+    headerImpl.init(headerContainer, resizeDelegate);
+    elem.appendChild(headerContainer);
+
+    // Create the footer container.
+    footerContainer = createContainer();
+    footerContainer.getStyle().setBottom(0.0, Unit.PX);
+    footerImpl.init(footerContainer, resizeDelegate);
+    elem.appendChild(footerContainer);
+
+    // Create the content container.
+    contentContainer = createContainer();
+    contentContainer.getStyle().setOverflow(Overflow.HIDDEN);
+    contentContainer.getStyle().setTop(0.0, Unit.PX);
+    contentContainer.getStyle().setHeight(0.0, Unit.PX);
+    elem.appendChild(contentContainer);
+  }
+
+  /**
+   * Adds a widget to this panel.
+   *
+   * @param w the child widget to be added
+   */
+  @Override
+  public void add(Widget w) {
+    // Add widgets in the order that they appear.
+    if (header == null) {
+      setHeaderWidget(w);
+    } else if (content == null) {
+      setContentWidget(w);
+    } else if (footer == null) {
+      setFooterWidget(w);
+    } else {
+      throw new IllegalStateException(
+ "HeaderPanel already contains header, content, and footer widgets.");
+    }
+  }
+
+  /**
+   * Get the content widget that appears between the header and footer.
+   *
+   * @return the content {@link Widget}
+   */
+  public Widget getContentWidget() {
+    return content;
+  }
+
+  /**
+   * Get the footer widget at the bottom of the panel.
+   *
+   * @return the footer {@link Widget}
+   */
+  public Widget getFooterWidget() {
+    return footer;
+  }
+
+  /**
+   * Get the header widget at the top of the panel.
+   *
+   * @return the header {@link Widget}
+   */
+  public Widget getHeaderWidget() {
+    return header;
+  }
+
+  public Iterator<Widget> iterator() {
+    // Return a simple iterator that iterates over the header, content, and
+    // footer in order.
+    return new Iterator<Widget>() {
+      private int index = -1;
+
+      public boolean hasNext() {
+        switch (index) {
+          case -1:
+            if (header != null) {
+              return true;
+            }
+          case 0: // Intentional fallthrough.
+            if (content != null) {
+              return true;
+            }
+          case 1: // Intentional fallthrough.
+            if (footer != null) {
+              return true;
+            }
+        }
+        return false;
+      }
+
+      public Widget next() {
+        switch (index) {
+          case -1:
+            index++;
+            if (header != null) {
+              return header;
+            }
+          case 0: // Intentional fallthrough.
+            index++;
+            if (content != null) {
+              return content;
+            }
+          case 1: // Intentional fallthrough.
+            index++;
+            if (footer != null) {
+              return footer;
+            }
+        }
+        throw new NoSuchElementException();
+      }
+
+      public void remove() {
+        switch (index) {
+          case 0:
+            doRemove(header, "Header");
+            break;
+          case 1:
+            doRemove(content, "Content");
+            break;
+          case 2:
+            doRemove(footer, "Footer");
+            break;
+          default:
+            throw new IllegalStateException();
+        }
+      }
+
+      private void doRemove(Widget widget, String position) {
+        if (widget == null) {
+ throw new IllegalStateException(position + " was already removed.");
+        }
+        HeaderPanel.this.remove(widget);
+      }
+    };
+  }
+
+  @Override
+  public void onAttach() {
+    super.onAttach();
+    headerImpl.onAttach();
+    footerImpl.onAttach();
+    scheduledLayout();
+  }
+
+  @Override
+  public void onDetach() {
+    super.onDetach();
+    headerImpl.onDetach();
+    footerImpl.onDetach();
+  }
+
+  public void onResize() {
+    // Handle the outer element resizing.
+    scheduledLayout();
+  }
+
+  @Override
+  public boolean remove(Widget w) {
+    // Validate.
+    if (w.getParent() != this) {
+      return false;
+    }
+    // Orphan.
+    try {
+      orphan(w);
+    } finally {
+      // Physical detach.
+      w.getElement().removeFromParent();
+
+      // Logical detach.
+      if (w == content) {
+        content = null;
+        contentContainer.getStyle().setDisplay(Display.NONE);
+      } else if (w == header) {
+        header = null;
+        headerContainer.getStyle().setDisplay(Display.NONE);
+      } else if (w == footer) {
+        footer = null;
+        footerContainer.getStyle().setDisplay(Display.NONE);
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Set the widget in the content portion between the header and footer.
+   *
+   * @param w the widget to use as the content
+   */
+  public void setContentWidget(Widget w) {
+    contentContainer.getStyle().clearDisplay();
+    add(w, content, contentContainer);
+
+    // Logical attach.
+    content = w;
+    scheduledLayout();
+  }
+
+  /**
+   * Set the widget in the footer portion at the bottom of the panel.
+   *
+   * @param w the widget to use as the footer
+   */
+  public void setFooterWidget(Widget w) {
+    footerContainer.getStyle().clearDisplay();
+    add(w, footer, footerContainer);
+
+    // Logical attach.
+    footer = w;
+    scheduledLayout();
+  }
+
+  /**
+   * Set the widget in the header portion at the top of the panel.
+   *
+   * @param w the widget to use as the header
+   */
+  public void setHeaderWidget(Widget w) {
+    headerContainer.getStyle().clearDisplay();
+    add(w, header, headerContainer);
+
+    // Logical attach.
+    header = w;
+    scheduledLayout();
+  }
+
+  /**
+ * Add a widget to the panel in the specified container. Note that this method
+   * does not do the logical attach.
+   *
+   * @param w the widget to add
+   * @param toReplace the widget to replace
+   * @param container the container in which to place the widget
+   */
+  private void add(Widget w, Widget toReplace, Element container) {
+    // Validate.
+    if (w == toReplace) {
+      return;
+    }
+
+    // Detach new child.
+    if (w != null) {
+      w.removeFromParent();
+    }
+
+    // Remove old child.
+    if (toReplace != null) {
+      remove(toReplace);
+    }
+
+    if (w != null) {
+      // Physical attach.
+      container.appendChild(w.getElement());
+
+      adopt(w);
+    }
+  }
+
+  private Element createContainer() {
+    Element container = Document.get().createDivElement().cast();
+    container.getStyle().setPosition(Position.ABSOLUTE);
+    container.getStyle().setDisplay(Display.NONE);
+    container.getStyle().setLeft(0.0, Unit.PX);
+    container.getStyle().setWidth(100.0, Unit.PCT);
+    return container;
+  }
+
+  /**
+   * Update the layout.
+   */
+  private void forceLayout() {
+    // No sense in doing layout if we aren't attached or have no content.
+    if (!isAttached() || content == null) {
+      return;
+    }
+
+    // Resize the content area to fit between the header and footer.
+    int remainingHeight = getElement().getClientHeight();
+    if (header != null) {
+      int height = Math.max(0, headerContainer.getOffsetHeight());
+      remainingHeight -= height;
+      contentContainer.getStyle().setTop(height, Unit.PX);
+    } else {
+      contentContainer.getStyle().setTop(0.0, Unit.PX);
+    }
+    if (footer != null) {
+      remainingHeight -= footerContainer.getOffsetHeight();
+    }
+ contentContainer.getStyle().setHeight(Math.max(0, remainingHeight), Unit.PX);
+
+    // Provide resize to child.
+    if (content instanceof RequiresResize) {
+      ((RequiresResize) content).onResize();
+    }
+  }
+
+  /**
+   * Schedule layout to adjust the height of the content area.
+   */
+  private void scheduledLayout() {
+    if (isAttached() && !layoutScheduled) {
+      layoutScheduled = true;
+      Scheduler.get().scheduleDeferred(layoutCmd);
+    }
+  }
+}
=======================================
--- /dev/null
+++ /trunk/user/src/com/google/gwt/user/client/ui/ResizeLayoutPanel.java Thu Jan 20 06:21:05 2011
@@ -0,0 +1,440 @@
+/*
+ * Copyright 2011 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.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.EventTarget;
+import com.google.gwt.dom.client.Style.Overflow;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.dom.client.Style.Visibility;
+import com.google.gwt.event.logical.shared.HasResizeHandlers;
+import com.google.gwt.event.logical.shared.ResizeEvent;
+import com.google.gwt.event.logical.shared.ResizeHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.layout.client.Layout;
+import com.google.gwt.layout.client.Layout.Layer;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.EventListener;
+import com.google.gwt.user.client.ui.ResizeLayoutPanel.Impl.Delegate;
+
+/**
+ * A simple panel that {@link ProvidesResize} to its one child, but does not
+ * {@link RequiresResize}. Use this to embed layout panels in any location
+ * within your application.
+ */
+public class ResizeLayoutPanel extends SimplePanel implements ProvidesResize,
+    HasResizeHandlers {
+
+  /**
+   * Implementation of resize event.
+   */
+  abstract static class Impl {
+    /**
+     * Delegate event handler.
+     */
+    abstract static interface Delegate {
+      /**
+       * Called when the element is resized.
+       */
+      void onResize();
+    }
+
+    boolean isAttached;
+    Element parent;
+    private Delegate delegate;
+
+    /**
+     * Initialize the implementation.
+     *
+     * @param elem the element to listen for resize
+     * @param delegate the {@link Delegate} to inform when resize occurs
+     */
+    public void init(Element elem, Delegate delegate) {
+      this.parent = elem;
+      this.delegate = delegate;
+    }
+
+    /**
+     * Called on attach.
+     */
+    public void onAttach() {
+      isAttached = true;
+    }
+
+    /**
+     * Called on detach.
+     *
+     * @param panel the panel
+     */
+    public void onDetach() {
+      isAttached = false;
+    }
+
+    /**
+     * Handle a resize event.
+     *
+     * @param panel the panel
+     */
+    protected void handleResize() {
+      if (isAttached && delegate != null) {
+        delegate.onResize();
+      }
+    }
+  }
+
+  /**
+   * Implementation of resize event.
+   */
+  static class ImplStandard extends Impl implements EventListener {
+    /**
+ * Chrome does not fire an onresize event if the dimensions are too small to
+     * render a scrollbar.
+     */
+    private static final String MIN_SIZE = "20px";
+
+    private Element collapsible;
+    private Element collapsibleInner;
+    private Element expandable;
+    private Element expandableInner;
+    private int lastOffsetHeight = -1;
+    private int lastOffsetWidth = -1;
+
+    @Override
+    public void init(Element elem, Delegate delegate) {
+      super.init(elem, delegate);
+
+      /*
+ * Set the minimum dimensions to ensure that scrollbars are rendered and
+       * fire onscroll events.
+       */
+      elem.getStyle().setProperty("minWidth", MIN_SIZE);
+      elem.getStyle().setProperty("minHeight", MIN_SIZE);
+
+      /*
+ * Detect expansion. In order to detect an increase in the size of the
+       * widget, we create an absolutely positioned, scrollable div with
+ * height=width=100%. We then add an inner div that has fixed height and + * width equal to 100% (converted to pixels) and set scrollLeft/scrollTop
+       * to their maximum. When the outer div expands, scrollLeft/scrollTop
+ * automatically becomes a smaller number and trigger an onscroll event.
+       */
+      expandable = Document.get().createDivElement().cast();
+      expandable.getStyle().setVisibility(Visibility.HIDDEN);
+      expandable.getStyle().setPosition(Position.ABSOLUTE);
+      expandable.getStyle().setHeight(100.0, Unit.PCT);
+      expandable.getStyle().setWidth(100.0, Unit.PCT);
+      expandable.getStyle().setOverflow(Overflow.SCROLL);
+      elem.appendChild(expandable);
+      expandableInner = Document.get().createDivElement().cast();
+      expandable.appendChild(expandableInner);
+      DOM.sinkEvents(expandable, Event.ONSCROLL);
+
+      /*
+       * Detect collapse. In order to detect a decrease in the size of the
+       * widget, we create an absolutely positioned, scrollable div with
+ * height=width=100%. We then add an inner div that has height=width=200%
+       * and max out the scrollTop/scrollLeft. When the height or width
+ * decreases, the inner div loses 2px for every 1px that the scrollable + * div loses, so the scrollTop/scrollLeft decrease and we get an onscroll
+       * event.
+       */
+      collapsible = Document.get().createDivElement().cast();
+      collapsible.getStyle().setVisibility(Visibility.HIDDEN);
+      collapsible.getStyle().setPosition(Position.ABSOLUTE);
+      collapsible.getStyle().setHeight(100.0, Unit.PCT);
+      collapsible.getStyle().setWidth(100.0, Unit.PCT);
+      collapsible.getStyle().setOverflow(Overflow.SCROLL);
+      elem.appendChild(collapsible);
+      collapsibleInner = Document.get().createDivElement().cast();
+      collapsibleInner.getStyle().setWidth(200, Unit.PCT);
+      collapsibleInner.getStyle().setHeight(200, Unit.PCT);
+      collapsible.appendChild(collapsibleInner);
+      DOM.sinkEvents(collapsible, Event.ONSCROLL);
+    }
+
+    @Override
+    public void onAttach() {
+      super.onAttach();
+      DOM.setEventListener(expandable, this);
+      DOM.setEventListener(collapsible, this);
+
+      /*
+ * Update the scrollables in a deferred command so the browser calculates
+       * the offsetHeight/Width correctly.
+       */
+      Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+        public void execute() {
+          resetScrollables();
+        }
+      });
+    }
+
+    public void onBrowserEvent(Event event) {
+      if (Event.ONSCROLL == event.getTypeInt()) {
+        EventTarget eventTarget = event.getEventTarget();
+        if (!Element.is(eventTarget)) {
+          return;
+        }
+        Element target = eventTarget.cast();
+        if (target == collapsible || target == expandable) {
+          handleResize();
+        }
+      }
+    }
+
+    @Override
+    public void onDetach() {
+      super.onDetach();
+      DOM.setEventListener(expandable, null);
+      DOM.setEventListener(collapsible, null);
+      lastOffsetHeight = -1;
+      lastOffsetWidth = -1;
+    }
+
+    @Override
+    protected void handleResize() {
+      if (resetScrollables()) {
+        super.handleResize();
+      }
+    }
+
+    /**
+     * Reset the positions of the scrollable elements.
+     *
+     * @return true if the size changed, false if not
+     */
+    private boolean resetScrollables() {
+      /*
+ * Reset expandable element. Scrollbars are not rendered if the div is too + * small, so we need to set the dimensions of the inner div to a value
+       * greater than the offsetWidth/Height.
+       */
+      int offsetHeight = parent.getOffsetHeight();
+      int offsetWidth = parent.getOffsetWidth();
+      int height = offsetHeight + 100;
+      int width = offsetWidth + 100;
+      expandableInner.getStyle().setHeight(height, Unit.PX);
+      expandableInner.getStyle().setWidth(width, Unit.PX);
+      expandable.setScrollTop(height);
+      expandable.setScrollLeft(width);
+
+      // Reset collapsible element.
+      collapsible.setScrollTop(collapsible.getScrollHeight() + 100);
+      collapsible.setScrollLeft(collapsible.getScrollWidth() + 100);
+
+ if (lastOffsetHeight != offsetHeight || lastOffsetWidth != offsetWidth) {
+        lastOffsetHeight = offsetHeight;
+        lastOffsetWidth = offsetWidth;
+        return true;
+      }
+      return false;
+    }
+  }
+
+  /**
+   * Implementation of resize event used by IE.
+   */
+  static class ImplTrident extends Impl {
+
+    @Override
+    public void init(Element elem, Delegate delegate) {
+      super.init(elem, delegate);
+      initResizeEventListener(elem);
+    }
+
+    @Override
+    public void onAttach() {
+      super.onAttach();
+      setResizeEventListener(parent, this);
+    }
+
+    @Override
+    public void onDetach() {
+      super.onDetach();
+      setResizeEventListener(parent, null);
+    }
+
+    /**
+ * Initalize the onresize listener. This method doesn't create a memory leak + * because we don't set a back reference to the Impl class until we attach
+     * to the DOM.
+     */
+    private native void initResizeEventListener(Element elem) /*-{
+      var theElem = elem;
+      var handleResize = $entry(function() {
+        if (theElem.__resizeImpl) {
+ theelem.__resizeim...@com.google.gwt.user.client.ui.resizelayoutpanel.impl::handleResize()();
+        }
+      });
+      elem.attachEvent('onresize', handleResize);
+    }-*/;
+
+    /**
+     * Set the event listener that handles resize events.
+     */
+ private native void setResizeEventListener(Element elem, Impl listener) /*-{
+      elem.__resizeImpl = listener;
+    }-*/;
+  }
+
+  /**
+   * Implementation of resize event used by IE6.
+   */
+  static class ImplIE6 extends ImplTrident {
+    @Override
+    public void onAttach() {
+      super.onAttach();
+
+      /*
+       * IE6 doesn't render this panel unless you kick it after its been
+       * attached.
+       */
+      Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+        public void execute() {
+          if (isAttached) {
+            parent.getStyle().setProperty("zoom", "1");
+          }
+        }
+      });
+    }
+  }
+
+  private final Impl impl = GWT.create(Impl.class);
+  private Layer layer;
+  private final Layout layout;
+  private final ScheduledCommand resizeCmd = new ScheduledCommand() {
+    public void execute() {
+      resizeCmdScheduled = false;
+      handleResize();
+    }
+  };
+  private boolean resizeCmdScheduled = false;
+
+  public ResizeLayoutPanel() {
+    layout = new Layout(getElement());
+    impl.init(getElement(), new Delegate() {
+      public void onResize() {
+        scheduleResize();
+      }
+    });
+  }
+
+  public HandlerRegistration addResizeHandler(ResizeHandler handler) {
+    return addHandler(handler, ResizeEvent.getType());
+  }
+
+  @Override
+  public boolean remove(Widget w) {
+    // Validate.
+    if (widget != w) {
+      return false;
+    }
+
+    // Orphan.
+    try {
+      orphan(w);
+    } finally {
+      // Physical detach.
+      layout.removeChild(layer);
+      layer = null;
+
+      // Logical detach.
+      widget = null;
+    }
+    return true;
+  }
+
+  @Override
+  public void setWidget(Widget w) {
+    // Validate
+    if (w == widget) {
+      return;
+    }
+
+    // Detach new child.
+    if (w != null) {
+      w.removeFromParent();
+    }
+
+    // Remove old child.
+    if (widget != null) {
+      remove(widget);
+    }
+
+    // Logical attach.
+    widget = w;
+
+    if (w != null) {
+      // Physical attach.
+      layer = layout.attachChild(widget.getElement(), widget);
+      layer.setTopHeight(0.0, Unit.PX, 100.0, Unit.PCT);
+      layer.setLeftWidth(0.0, Unit.PX, 100.0, Unit.PCT);
+
+      adopt(w);
+
+      // Update the layout.
+      layout.layout();
+      scheduleResize();
+    }
+  }
+
+  @Override
+  protected void onAttach() {
+    super.onAttach();
+    impl.onAttach();
+    layout.onAttach();
+    scheduleResize();
+  }
+
+  @Override
+  protected void onDetach() {
+    super.onDetach();
+    impl.onDetach();
+    layout.onDetach();
+  }
+
+  private void handleResize() {
+    if (!isAttached()) {
+      return;
+    }
+
+    // Provide resize to child.
+    if (widget instanceof RequiresResize) {
+      ((RequiresResize) widget).onResize();
+    }
+
+    // Fire resize event.
+    ResizeEvent.fire(this, getOffsetWidth(), getOffsetHeight());
+  }
+
+  /**
+ * Schedule a resize handler. We schedule the event so the DOM has time to + * update the offset sizes, and to avoid duplicate resize events from both a
+   * height and width resize.
+   */
+  private void scheduleResize() {
+    if (isAttached() && !resizeCmdScheduled) {
+      resizeCmdScheduled = true;
+      Scheduler.get().scheduleDeferred(resizeCmd);
+    }
+  }
+}
=======================================
--- /dev/null
+++ /trunk/user/test/com/google/gwt/user/client/ui/HeaderPanelTest.java Thu Jan 20 06:21:05 2011
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2011 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.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
+import com.google.gwt.user.client.Timer;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Tests for {@link HeaderPanel}.
+ */
+public class HeaderPanelTest extends PanelTestBase<HeaderPanel> {
+
+  public void testAdd() {
+    HeaderPanel panel = createPanel();
+
+    // Add the header first.
+    Label header = new Label("header");
+    panel.add(header);
+    assertEquals(header, panel.getHeaderWidget());
+    assertEquals(null, panel.getContentWidget());
+    assertEquals(null, panel.getFooterWidget());
+
+    // Add the content second.
+    Label content = new Label("content");
+    panel.add(content);
+    assertEquals(header, panel.getHeaderWidget());
+    assertEquals(content, panel.getContentWidget());
+    assertEquals(null, panel.getFooterWidget());
+
+    // Add the footer third.
+    Label footer = new Label("footer");
+    panel.add(footer);
+    assertEquals(header, panel.getHeaderWidget());
+    assertEquals(content, panel.getContentWidget());
+    assertEquals(footer, panel.getFooterWidget());
+
+    // Cannot add a fourth widget.
+    try {
+      panel.add(new Label());
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException e) {
+      // Expected.
+    }
+  }
+
+  public void testIterator() {
+    final HeaderPanel panel = createPanel();
+
+    // Empty iterator.
+    Iterator<Widget> iter = panel.iterator();
+    assertFalse(iter.hasNext());
+    try {
+      iter.next();
+      fail("Expected NoSuchElementException");
+    } catch (NoSuchElementException e) {
+      // Expected.
+    }
+    try {
+      iter.remove();
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException e) {
+      // Expected.
+    }
+
+    // Only a content.
+    Label content = new Label("content");
+    panel.setContentWidget(content);
+    iter = panel.iterator();
+    assertTrue(iter.hasNext());
+    assertEquals(content, iter.next());
+    assertFalse(iter.hasNext());
+    try {
+      iter.next();
+      fail("Expected NoSuchElementException");
+    } catch (NoSuchElementException e) {
+      // Expected.
+    }
+
+    // Header, Content, Footer.
+    Label header = new Label("header");
+    Label footer = new Label("footer");
+    panel.setHeaderWidget(header);
+    panel.setFooterWidget(footer);
+    iter = panel.iterator();
+    assertTrue(iter.hasNext());
+    assertEquals(header, iter.next());
+    assertTrue(iter.hasNext());
+    assertEquals(content, iter.next());
+    assertTrue(iter.hasNext());
+    assertEquals(footer, iter.next());
+    assertFalse(iter.hasNext());
+    try {
+      iter.next();
+      fail("Expected NoSuchElementException");
+    } catch (NoSuchElementException e) {
+      // Expected.
+    }
+
+    // Remove Content.
+    iter = panel.iterator();
+    assertEquals(header, iter.next());
+    assertEquals(content, iter.next());
+    iter.remove();
+    assertEquals(header, panel.getHeaderWidget());
+    assertNull(panel.getContentWidget());
+    assertEquals(footer, panel.getFooterWidget());
+    try {
+      iter.remove();
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException e) {
+      // Expected - cannot remove twice.
+    }
+    assertEquals(footer, iter.next());
+  }
+
+  @DoNotRunWith(Platform.HtmlUnitLayout)
+  public void testResizeFooter() {
+    final HeaderPanel panel = createPanel();
+    panel.setSize("200px", "400px");
+    RootPanel.get().add(panel);
+
+    final Label header = new Label();
+    header.setSize("100%", "50px");
+    panel.setHeaderWidget(header);
+
+    final Label content = new Label();
+    content.setHeight("100%");
+    panel.setContentWidget(content);
+
+    final Label footer = new Label();
+    footer.setSize("100%", "50px");
+    panel.setFooterWidget(footer);
+
+    delayTestFinish(5000);
+    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+      public void execute() {
+        assertEquals(300, content.getOffsetHeight());
+
+        // Resize the footer.
+        footer.setHeight("75px");
+        new Timer() {
+          @Override
+          public void run() {
+            assertEquals(275, content.getOffsetHeight());
+            RootPanel.get().remove(panel);
+            finishTest();
+          }
+        }.schedule(250);;
+      }
+    });
+  }
+
+  @DoNotRunWith(Platform.HtmlUnitLayout)
+  public void testResizeHeader() {
+    final HeaderPanel panel = createPanel();
+    panel.setSize("200px", "400px");
+    RootPanel.get().add(panel);
+
+    final Label header = new Label();
+    header.setSize("100%", "50px");
+    panel.setHeaderWidget(header);
+
+    final Label content = new Label();
+    content.setHeight("100%");
+    panel.setContentWidget(content);
+
+    final Label footer = new Label();
+    footer.setSize("100%", "50px");
+    panel.setFooterWidget(footer);
+
+    delayTestFinish(5000);
+    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+      public void execute() {
+        assertEquals(300, content.getOffsetHeight());
+
+        // Resize the header.
+        header.setHeight("75px");
+        new Timer() {
+          @Override
+          public void run() {
+            assertEquals(275, content.getOffsetHeight());
+            RootPanel.get().remove(panel);
+            finishTest();
+          }
+        }.schedule(250);;
+      }
+    });
+  }
+
+  public void testSetContentWidget() {
+    HeaderPanel panel = createPanel();
+    Label widget = new Label("hello world");
+    panel.setContentWidget(widget);
+    assertEquals(widget, panel.getContentWidget());
+
+    panel.remove(widget);
+    assertNull(panel.getContentWidget());
+  }
+
+  public void testSetFooterWidget() {
+    HeaderPanel panel = createPanel();
+    Label widget = new Label("hello world");
+    panel.setHeaderWidget(widget);
+    assertEquals(widget, panel.getHeaderWidget());
+
+    panel.remove(widget);
+    assertNull(panel.getHeaderWidget());
+  }
+
+  public void testSetHeaderWidget() {
+    HeaderPanel panel = createPanel();
+    Label widget = new Label("hello world");
+    panel.setFooterWidget(widget);
+    assertEquals(widget, panel.getFooterWidget());
+
+    panel.remove(widget);
+    assertNull(panel.getFooterWidget());
+  }
+
+  @Override
+  protected HeaderPanel createPanel() {
+    return new HeaderPanel();
+  }
+
+  @Override
+  protected boolean supportsMultipleWidgets() {
+    // HeaderPanel supports up to 3 widgets, but not an unbounded number.
+    return false;
+  }
+}
=======================================
--- /dev/null
+++ /trunk/user/test/com/google/gwt/user/client/ui/ResizeLayoutPanelTest.java Thu Jan 20 06:21:05 2011
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2011 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.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.logical.shared.ResizeEvent;
+import com.google.gwt.event.logical.shared.ResizeHandler;
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.Timer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for {@link ResizeLayoutPanel}.
+ */
+public class ResizeLayoutPanelTest extends
+    SimplePanelTestBase<ResizeLayoutPanel> {
+
+  /**
+   * A custom implementation of {@link ResizeHandler} used for testing.
+   */
+  private static class CustomResizeHandler implements ResizeHandler {
+
+    private boolean resizeFired;
+
+    public void assertResizeFired(boolean expected) {
+      assertEquals(expected, resizeFired);
+      resizeFired = false;
+    }
+
+    public void onResize(ResizeEvent event) {
+      assertFalse(resizeFired);
+      resizeFired = true;
+    }
+  }
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.user.User";
+  }
+
+  /**
+   * Test that a resize event is fired on attach.
+   */
+  public void testAttach() {
+    final ResizeLayoutPanel panel = createPanel();
+    panel.setWidget(new Label("hello world"));
+    final CustomResizeHandler handler = new CustomResizeHandler();
+    panel.addResizeHandler(handler);
+    handler.assertResizeFired(false);
+
+    delayTestFinish(10000);
+    RootPanel.get().add(panel);
+    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+      public void execute() {
+        handler.assertResizeFired(true);
+        panel.removeFromParent();
+        finishTest();
+      }
+    });
+  }
+
+  /**
+   * Test that changing the font size triggers a resize event.
+   */
+  @DoNotRunWith(Platform.HtmlUnitLayout)
+  public void testChangeFontSize() {
+    // Create a panel and add a handler.
+    ResizeLayoutPanel panel = createPanel();
+    panel.setWidget(new Label("hello world"));
+    panel.setWidth("10em");
+    panel.setHeight("10em");
+    final CustomResizeHandler handler = new CustomResizeHandler();
+    panel.addResizeHandler(handler);
+    handler.assertResizeFired(false);
+
+    // Create an outer container and attach it.
+    final SimplePanel container = new SimplePanel();
+    container.getElement().getStyle().setFontSize(10, Unit.PT);
+    container.setHeight("10em");
+    container.setWidth("10em");
+    container.setWidget(panel);
+    RootPanel.get().add(container);
+
+    // Wait for the resize event from attaching.
+    delayTestFinish(10000);
+    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+      public void execute() {
+        handler.assertResizeFired(true); // Triggered by attach.
+        handler.assertResizeFired(false);
+
+        // Change the font size.
+        container.getElement().getStyle().setFontSize(12, Unit.PT);
+        new Timer() {
+          @Override
+          public void run() {
+            handler.assertResizeFired(true);
+            container.removeFromParent();
+            finishTest();
+          }
+        }.schedule(250);
+      }
+    });
+  }
+
+  /**
+   * Test that resizing the outer container triggers a resize event.
+   */
+  @DoNotRunWith(Platform.HtmlUnitLayout)
+  public void testEnlargeContainerHeight() {
+    final SimplePanel container = new SimplePanel();
+    container.setPixelSize(100, 100);
+    testResizeContainer(container, new Command() {
+      public void execute() {
+        container.setHeight("101px");
+      }
+    });
+  }
+
+  /**
+   * Test that resizing the outer container triggers a resize event.
+   */
+  @DoNotRunWith(Platform.HtmlUnitLayout)
+  public void testEnlargeContainerWidth() {
+    final SimplePanel container = new SimplePanel();
+    container.setPixelSize(100, 100);
+    testResizeContainer(container, new Command() {
+      public void execute() {
+        container.setWidth("101px");
+      }
+    });
+  }
+
+  /**
+ * Test that resizing the outer container triggers a resize event even if the
+   * dimensions are too small to render a scrollbar.
+   */
+  @DoNotRunWith(Platform.HtmlUnitLayout)
+  public void testEnlargeSmallContainerHeight() {
+    final SimplePanel container = new SimplePanel();
+    container.setPixelSize(20, 20);
+    testResizeContainer(container, new Command() {
+      public void execute() {
+        container.setHeight("21px");
+      }
+    });
+  }
+
+  /**
+ * Test that resizing the outer container triggers a resize event even if the
+   * dimensions are too small to render a scrollbar.
+   */
+  @DoNotRunWith(Platform.HtmlUnitLayout)
+  public void testEnlargeSmallContainerWidth() {
+    final SimplePanel container = new SimplePanel();
+    container.setPixelSize(20, 20);
+    testResizeContainer(container, new Command() {
+      public void execute() {
+        container.setWidth("21px");
+      }
+    });
+  }
+
+  public void testProvidesResize() {
+    final List<String> resized = new ArrayList<String>();
+    ResizeLayoutPanel panel = createPanel();
+    panel.setWidget(new LayoutPanel() {
+      @Override
+      public void onResize() {
+        super.onResize();
+        resized.add("resized");
+      }
+    });
+
+    delayTestFinish(10000);
+    RootPanel.get().add(panel);
+    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+      public void execute() {
+        assertEquals(1, resized.size());
+        finishTest();
+      }
+    });
+  }
+
+  /**
+   * Test that resizing the outer container triggers a resize event.
+   */
+  @DoNotRunWith(Platform.HtmlUnitLayout)
+  public void testShrinkContainerHeight() {
+    final SimplePanel container = new SimplePanel();
+    container.setPixelSize(100, 100);
+    testResizeContainer(container, new Command() {
+      public void execute() {
+        container.setHeight("99px");
+      }
+    });
+  }
+
+  /**
+   * Test that resizing the outer container triggers a resize event.
+   */
+  @DoNotRunWith(Platform.HtmlUnitLayout)
+  public void testShrinkContainerWidth() {
+    final SimplePanel container = new SimplePanel();
+    container.setPixelSize(100, 100);
+    testResizeContainer(container, new Command() {
+      public void execute() {
+        container.setWidth("99px");
+      }
+    });
+  }
+
+  /**
+ * Test that resizing the outer container triggers a resize event even if the
+   * dimensions are too small to render a scrollbar.
+   */
+  @DoNotRunWith(Platform.HtmlUnitLayout)
+  public void testShrinkSmallContainerHeight() {
+    final SimplePanel container = new SimplePanel();
+    container.setPixelSize(21, 21);
+    testResizeContainer(container, new Command() {
+      public void execute() {
+        container.setHeight("20px");
+      }
+    });
+  }
+
+  /**
+ * Test that resizing the outer container triggers a resize event even if the
+   * dimensions are too small to render a scrollbar.
+   */
+  @DoNotRunWith(Platform.HtmlUnitLayout)
+  public void testShrinkSmallContainerWidth() {
+    final SimplePanel container = new SimplePanel();
+    container.setPixelSize(21, 21);
+    testResizeContainer(container, new Command() {
+      public void execute() {
+        container.setWidth("20px");
+      }
+    });
+  }
+
+  @Override
+  protected ResizeLayoutPanel createPanel() {
+    return new ResizeLayoutPanel();
+  }
+
+  /**
+   * Test that resizing the outer container triggers a resize event.
+   *
+   * @param container the container that will hold the panel
+   * @param resizeCommand the command that resizes the container
+   */
+  private void testResizeContainer(final SimplePanel container,
+      final Command resizeCommand) {
+    // Create a panel and add a handler.
+    ResizeLayoutPanel panel = createPanel();
+    panel.setWidget(new Label("hello world"));
+    panel.setWidth("100%");
+    panel.setHeight("100%");
+    final CustomResizeHandler handler = new CustomResizeHandler();
+    panel.addResizeHandler(handler);
+    handler.assertResizeFired(false);
+
+    // Create an outer container and attach it.
+    container.setWidget(panel);
+    RootPanel.get().add(container);
+
+    // Wait for the resize event from attaching.
+    delayTestFinish(10000);
+    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+      public void execute() {
+        handler.assertResizeFired(true); // Triggered by attach.
+
+        // Change the size of the container.
+        resizeCommand.execute();
+        new Timer() {
+          @Override
+          public void run() {
+            handler.assertResizeFired(true);
+            container.removeFromParent();
+            finishTest();
+          }
+        }.schedule(250);
+      }
+    });
+  }
+}
=======================================
--- /trunk/user/src/com/google/gwt/user/User.gwt.xml Tue Nov 30 06:11:17 2010 +++ /trunk/user/src/com/google/gwt/user/User.gwt.xml Thu Jan 20 06:21:05 2011
@@ -48,6 +48,7 @@
    <inherits name="com.google.gwt.user.Tree"/>
    <inherits name="com.google.gwt.user.Hyperlink"/>
    <inherits name="com.google.gwt.user.FileUpload"/>
+   <inherits name="com.google.gwt.user.ResizeLayoutPanel"/>
    <inherits name="com.google.gwt.user.datepicker.DatePicker"/>
    <inherits name="com.google.gwt.user.cellview.CellView"/>
    <inherits name="com.google.gwt.safehtml.SafeHtml" />
=======================================
--- /trunk/user/test/com/google/gwt/user/UISuite.java Fri Jan 14 05:54:41 2011 +++ /trunk/user/test/com/google/gwt/user/UISuite.java Thu Jan 20 06:21:05 2011
@@ -62,6 +62,7 @@
 import com.google.gwt.user.client.ui.GridTest;
 import com.google.gwt.user.client.ui.HTMLPanelTest;
 import com.google.gwt.user.client.ui.HTMLTest;
+import com.google.gwt.user.client.ui.HeaderPanelTest;
 import com.google.gwt.user.client.ui.HiddenTest;
 import com.google.gwt.user.client.ui.HistoryTest;
 import com.google.gwt.user.client.ui.HorizontalPanelTest;
@@ -82,6 +83,7 @@
 import com.google.gwt.user.client.ui.PrefixTreeTest;
 import com.google.gwt.user.client.ui.RadioButtonTest;
 import com.google.gwt.user.client.ui.ResetButtonTest;
+import com.google.gwt.user.client.ui.ResizeLayoutPanelTest;
 import com.google.gwt.user.client.ui.RichTextAreaTest;
 import com.google.gwt.user.client.ui.RootPanelTest;
 import com.google.gwt.user.client.ui.ScrollPanelTest;
@@ -169,6 +171,7 @@
     suite.addTestSuite(FormPanelTest.class);
     suite.addTestSuite(GestureEventSinkTest.class);
     suite.addTestSuite(GridTest.class);
+    suite.addTestSuite(HeaderPanelTest.class);
     suite.addTestSuite(HiddenTest.class);
     suite.addTestSuite(HistoryTest.class);
     suite.addTestSuite(HistoryDisabledTest.class);
@@ -194,6 +197,7 @@
     suite.addTestSuite(PrefixTreeTest.class);
     suite.addTestSuite(RadioButtonTest.class);
     suite.addTestSuite(ResetButtonTest.class);
+    suite.addTestSuite(ResizeLayoutPanelTest.class);
     suite.addTestSuite(RichTextAreaTest.class);
     suite.addTestSuite(RootPanelTest.class);
     suite.addTestSuite(ScrollPanelTest.class);

--
http://groups.google.com/group/Google-Web-Toolkit-Contributors

Reply via email to