»
December 09, 2009
»

LayoutPanel Fill Parent in GWT-2.0

When I started constructing UI with GWT 2.0 LayoutPanels it had Layout instance publicly available but somewhere between 2.0.0-ms2 and 2.0.0-rc2 the API has changed and now Layout is available only via LayoutPanel#setWidget* methods. This left me with non functional UI because I had used getLayout().fillParent() calls what forces Widget to fill its parent element. Why? Because most of UI is constructed with floating panels what are resizable and has container element for it’s content what may be LayoutPanel instance.

I’m not arguing that fillParent() should be publicly available (child widget should not be aware of it’s parent (in)capabilities but parent should be able to force child LayoutPanel to fill it’s content, ideally without knowing it’s LayoutPanel).

So here is kind-of hack to get back fillParent() in 2.0 release. This is wrapper widget what can be added in for example FloatingPanel:

package com.google.gwt.user.client.ui;

import com.google.gwt.dom.client.Style;

import static com.google.gwt.dom.client.Style.Overflow.HIDDEN;
import static com.google.gwt.dom.client.Style.Position.RELATIVE;
import static com.google.gwt.dom.client.Style.Unit.PCT;

/**
 * Widget what extends {@link com.google.gwt.user.client.ui.SimplePanel} and can be used as 
 * "host" for {@link com.google.gwt.user.client.ui.LayoutPanel} inside 
 * non-{@link com.google.gwt.layout.client.Layout} based UI.
 * <p/>
 * This widget forces {@link com.google.gwt.user.client.ui.LayoutPanel} to fill host 
 * dimensions by calling {@link com.google.gwt.layout.client.Layout#fillParent()} in
 * {@link com.google.gwt.user.client.ui.LayoutPanelHost#setWidget(Widget)}.
 *
 * @author ampatspell
 */
public class LayoutPanelHost extends SimplePanel implements RequiresResize {

  public LayoutPanelHost() {
    this(null);
  }

  /**
   * Child may be null
   *
   * @param child widget to set
   */
  public LayoutPanelHost(Widget child) {
    Style style = getElement().getStyle();
    style.setPosition(RELATIVE);
    style.setWidth(100, PCT);
    style.setHeight(100, PCT);
    // overflow: hidden is to hide ruler element what is added by Layout
    style.setOverflow(HIDDEN);

    if (child != null)
      setWidget(child);
  }

  /**
   * If widget is instanceof {@link com.google.gwt.user.client.ui.LayoutPanel},
   * {@link com.google.gwt.layout.client.Layout#fillParent()} is called for it.
   *
   * @param w any widget or null
   */
  @Override
  public void setWidget(Widget w) {
    LayoutPanel layoutPanel = cast(w);
    if (layoutPanel != null)
      layoutPanel.getLayout().fillParent();
    super.setWidget(w);
  }

  /**
   * Delegates call to {@link com.google.gwt.user.client.ui.LayoutPanel}.
   * Should be called whenever this Widget or some of its parents are resized. 
   * Normally {@link LayoutPanel#onResize()} is called automatically by GWT Layout API 
   * but here it is impossible as there is no way (except from polling) to get informed 
   * about Element dimension changes.
   *
   * Update: Is it really needed? LayoutImplIE6 adds js hook to onresize.
   */
  public void onResize() {
    LayoutPanel layoutPanel = cast(getWidget());
    if (layoutPanel != null)
      layoutPanel.onResize();
  }

  /**
   * Casts {@link com.google.gwt.user.client.ui.Widget} or 
   * {@link com.google.gwt.user.client.ui.Composite} child widget
   * to {@link com.google.gwt.user.client.ui.LayoutPanel} if widget is not null and
   * is instanceof {@link com.google.gwt.user.client.ui.LayoutPanel}.
   *
   * @param widget Widget
   * @return widget casted to LayoutPanel or null
   */
  private LayoutPanel cast(Widget widget) {
    Widget w = getActualWidget(widget);
    if (w != null && w instanceof LayoutPanel)
      return (LayoutPanel) w;
    return null;
  }

  private Widget getActualWidget(Widget widget) {
    if (widget != null && widget instanceof Composite) {
      return ((Composite) widget).getWidget();
    }
    return widget;
  }

}

Usage:

FlowPanel base = new FlowPanel();
base.add(new LayoutPanelHost(content = new SomeLayoutPanel()));

Here SomeLayoutPanel now fills entire base element.

Update: Forgot to mention that on modern browsers instead of this simple Widget#setSize should work:

FlowPanel base = new FlowPanel();
base.add(content = new SomeLayoutPanel());
content.setSize("100%", "100%");

But looking at LayoutImplIE6 it seems that won’t work reliably in IE6 (if anyone cares). Also see note in LayoutPanelHost#onResize method docs. Sorry, currently I can’t test it in IE6 so I’m not sure.

Update #2: LayoutPanelHost#cast now supports Composite and ResizeComposite.

With UIBinder

It turns out this helper class is also needed to compose Layout-based UIs using UIBinder while following MVP.

Let’s consider simple case. I have Workspace MVP stack what can contain either Index, People or Projects “content” presenter’s view:

<!-- Workspace.ui.xml -->
<g:LayoutPanel>
  <g:layer top="0px" height="30px">
    <!-- Some header or whatever -->
  </g:layer>
  <g:layer top="30px" bottom="0px">
    <g:LayoutPanelHost ui:field="content"/>
  </g:layer>
</g:LayoutPanel>
// WorkspacePresenterImpl
view.setContentView(presenter.getView());

Where given presenter instance has view based on LayoutPanel:

<g:LayoutPanel>
  <g:layer top="0px" height="50px">
  </g:layer>
  <g:layer top="50px" bottom="0px">
  </g:layer>
</g:LayoutPanel>

If Layout#fillParent() is not called for workspace content widget the widget must set it’s height explicitly (i.e. for example bottom="0px" doesn’t work).

 
Internet Explorer 6
Are you serious?