Initial implementations of Stack and Split layout panels, along with a few
checkstyle tweaks.
Review: http://gwt-code-reviews.appspot.com/65804
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6110 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/javadoc/com/google/gwt/examples/SplitLayoutPanelExample.java b/user/javadoc/com/google/gwt/examples/SplitLayoutPanelExample.java
new file mode 100644
index 0000000..e12b1b3
--- /dev/null
+++ b/user/javadoc/com/google/gwt/examples/SplitLayoutPanelExample.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2009 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.examples;
+
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.RootLayoutPanel;
+import com.google.gwt.user.client.ui.SplitLayoutPanel;
+import com.google.gwt.user.client.ui.DockLayoutPanel.Direction;
+
+public class SplitLayoutPanelExample implements EntryPoint {
+
+ public void onModuleLoad() {
+ // Create a three-pane layout with splitters.
+ SplitLayoutPanel p = new SplitLayoutPanel();
+ p.add(new HTML("navigation"), Direction.WEST, 128);
+ p.add(new HTML("list"), Direction.NORTH, 384);
+ p.add(new HTML("details"), Direction.CENTER, 0);
+
+ // Note the explicit call to layout(). This is required for the layout to
+ // take effect.
+ p.layout();
+
+ // Attach the LayoutPanel to the RootLayoutPanel. The latter will listen for
+ // resize events on the window to ensure that its children are informed of
+ // possible size changes.
+ RootLayoutPanel rp = RootLayoutPanel.get();
+ rp.add(p);
+
+ // The RootLayoutPanel also requires that its layout() method be explicitly
+ // called for the initial layout to take effect.
+ rp.layout();
+ }
+}
diff --git a/user/javadoc/com/google/gwt/examples/StackLayoutPanelExample.java b/user/javadoc/com/google/gwt/examples/StackLayoutPanelExample.java
new file mode 100644
index 0000000..b493b01
--- /dev/null
+++ b/user/javadoc/com/google/gwt/examples/StackLayoutPanelExample.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2009 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.examples;
+
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.RootLayoutPanel;
+import com.google.gwt.user.client.ui.StackLayoutPanel;
+
+public class StackLayoutPanelExample implements EntryPoint {
+
+ public void onModuleLoad() {
+ // Create a three-item stack, with headers sized in EMs.
+ StackLayoutPanel p = new StackLayoutPanel(Unit.EM);
+ p.add(new HTML("this"), new HTML("[this]"), 128);
+ p.add(new HTML("that"), new HTML("[that]"), 384);
+ p.add(new HTML("the other"), new HTML("[the other]"), 0);
+
+ // Note the explicit call to layout(). This is required for the layout to
+ // take effect.
+ p.layout();
+
+ // Attach the LayoutPanel to the RootLayoutPanel. The latter will listen for
+ // resize events on the window to ensure that its children are informed of
+ // possible size changes.
+ RootLayoutPanel rp = RootLayoutPanel.get();
+ rp.add(p);
+
+ // The RootLayoutPanel also requires that its layout() method be explicitly
+ // called for the initial layout to take effect.
+ rp.layout();
+ }
+}
diff --git a/user/src/com/google/gwt/user/client/ui/DockLayoutPanel.java b/user/src/com/google/gwt/user/client/ui/DockLayoutPanel.java
index 4281dde..58e15d3 100644
--- a/user/src/com/google/gwt/user/client/ui/DockLayoutPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/DockLayoutPanel.java
@@ -101,6 +101,7 @@
*
* @param widget the widget to be added
* @param direction the widget's direction in the dock
+ * @param size the child widget's size
*
* @throws IllegalArgumentException when adding to the {@link #CENTER} and
* there is already a different widget there
@@ -114,7 +115,7 @@
*/
public Element getContainerElementFor(Widget widget) {
assertIsChild(widget);
- return ((LayoutData)widget.getLayoutData()).layer.getContainerElement();
+ return ((LayoutData) widget.getLayoutData()).layer.getContainerElement();
}
/**
diff --git a/user/src/com/google/gwt/user/client/ui/SplitLayoutPanel.java b/user/src/com/google/gwt/user/client/ui/SplitLayoutPanel.java
new file mode 100644
index 0000000..46a43e8
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/SplitLayoutPanel.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2009 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.dom.client.Document;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.Event;
+
+/**
+ * A panel that adds user-positioned splitters between each of its child
+ * widgets.
+ *
+ * <p>
+ * This panel is used in the same way as {@link DockLayoutPanel}, except that
+ * its children's sizes are always specified in {@link Unit#PX} units, and each
+ * pair of child widgets has a splitter between them that the user can drag.
+ * </p>
+ *
+ * <p>
+ * This widget will <em>only</em> work in standards mode, which requires
+ * that the HTML page in which it is run have an explicit <!DOCTYPE>
+ * declaration.
+ * </p>
+ *
+ * <p>
+ * NOTE: This class is still very new, and its interface may change without
+ * warning. Use at your own risk.
+ * </p>
+ *
+ * <p>
+ * <h3>Example</h3>
+ * {@example com.google.gwt.examples.SplitLayoutPanelExample}
+ * </p>
+ *
+ * TODO(jgw):
+ * - RTL Support.
+ * - implement insert().
+ * - Come up with a decent way to specify splitter style and size.
+ */
+public class SplitLayoutPanel extends DockLayoutPanel {
+
+ private class HSplitter extends Splitter {
+ public HSplitter(Widget target, boolean reverse) {
+ super(target, reverse);
+ getElement().getStyle().setPropertyPx("width", 4);
+ setStyleName("LayoutPanel-HDragger");
+ }
+
+ @Override
+ protected int getAbsolutePosition() {
+ return getAbsoluteLeft();
+ }
+
+ @Override
+ protected int getEventPosition(Event event) {
+ return event.getClientX();
+ }
+
+ @Override
+ protected int getTargetPosition() {
+ return target.getAbsoluteLeft();
+ }
+
+ @Override
+ protected int getTargetSize() {
+ return target.getOffsetWidth();
+ }
+ }
+
+ private abstract class Splitter extends Widget {
+ protected final Widget target;
+
+ private int offset;
+ private boolean mouseDown;
+ private Command layoutCommand;
+
+ private final boolean reverse;
+ private int minSize;
+
+ public Splitter(Widget target, boolean reverse) {
+ this.target = target;
+ this.reverse = reverse;
+
+ setElement(Document.get().createDivElement());
+ sinkEvents(Event.ONMOUSEDOWN | Event.ONMOUSEUP | Event.ONMOUSEMOVE
+ | Event.ONDBLCLICK);
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ switch (event.getTypeInt()) {
+ case Event.ONMOUSEDOWN:
+ mouseDown = true;
+ offset = getEventPosition(event) - getAbsolutePosition();
+ Event.setCapture(getElement());
+ event.preventDefault();
+ break;
+
+ case Event.ONMOUSEUP:
+ mouseDown = false;
+ Event.releaseCapture(getElement());
+ event.preventDefault();
+ break;
+
+ case Event.ONMOUSEMOVE:
+ if (mouseDown) {
+ int size;
+ if (reverse) {
+ size = getTargetPosition() + getTargetSize()
+ - getEventPosition(event) - offset;
+ } else {
+ size = getEventPosition(event) - getTargetPosition() - offset;
+ }
+
+ setAssociatedWidgetSize(size);
+ event.preventDefault();
+ }
+ break;
+ }
+ }
+
+ public void setMinSize(int minSize) {
+ this.minSize = minSize;
+ LayoutData layout = (LayoutData) target.getLayoutData();
+
+ // Try resetting the associated widget's size, which will enforce the new
+ // minSize value.
+ setAssociatedWidgetSize((int) layout.size);
+ }
+
+ protected abstract int getAbsolutePosition();
+
+ protected abstract int getEventPosition(Event event);
+
+ protected abstract int getTargetPosition();
+
+ protected abstract int getTargetSize();
+
+ private void setAssociatedWidgetSize(int size) {
+ if (size < minSize) {
+ size = minSize;
+ }
+
+ LayoutData layout = (LayoutData) target.getLayoutData();
+ if (size == layout.size) {
+ return;
+ }
+
+ layout.size = size;
+
+ // Defer actually updating the layout, so that if we receive many
+ // mouse events before layout/paint occurs, we'll only update once.
+ if (layoutCommand == null) {
+ layoutCommand = new Command() {
+ public void execute() {
+ layoutCommand = null;
+ layout();
+ }
+ };
+ DeferredCommand.addCommand(layoutCommand);
+ }
+ }
+ }
+
+ private class VSplitter extends Splitter {
+ public VSplitter(Widget target, boolean reverse) {
+ super(target, reverse);
+ getElement().getStyle().setPropertyPx("height", 4);
+ setStyleName("LayoutPanel-VDragger");
+ }
+
+ @Override
+ protected int getAbsolutePosition() {
+ return getAbsoluteTop();
+ }
+
+ @Override
+ protected int getEventPosition(Event event) {
+ return event.getClientY();
+ }
+
+ @Override
+ protected int getTargetPosition() {
+ return target.getAbsoluteTop();
+ }
+
+ @Override
+ protected int getTargetSize() {
+ return target.getOffsetHeight();
+ }
+ }
+
+ private static final int SPLITTER_SIZE = 4;
+
+ public SplitLayoutPanel() {
+ super(Unit.PX);
+ }
+
+ @Override
+ public void add(Widget child, Direction direction, double size) {
+ super.add(child, direction, size);
+ if (direction != Direction.CENTER) {
+ addSplitter();
+ }
+ }
+
+ @Override
+ public boolean remove(Widget child) {
+ assert !(child instanceof Splitter) : "Splitters may not be directly removed";
+
+ if (super.remove(child)) {
+ // Remove the associated splitter, if any.
+ int idx = getWidgetIndex(child);
+ if (idx < getWidgetCount() - 1) {
+ remove(idx + 1);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Sets the minimum allowable size for the given widget.
+ *
+ * <p>
+ * Its assocated splitter cannot be dragged to a position that would make it
+ * smaller than this size. This method has no effect for the
+ * {@link Direction#CENTER} widget.
+ * </p>
+ *
+ * @param child the child whose minimum size will be set
+ * @param minSize the minimum size for this widget
+ */
+ public void setWidgetMinSize(Widget child, int minSize) {
+ Splitter splitter = getAssociatedSplitter(child);
+ splitter.setMinSize(minSize);
+ }
+
+ private void addSplitter() {
+ assert getChildren().size() > 0 : "Can't add a splitter before any children";
+ assert getCenter() == null : "Can't add a splitter after the CENTER widget";
+
+ Widget lastChild = getChildren().get(getChildren().size() - 1);
+ LayoutData lastChildLayout = (LayoutData) lastChild.getLayoutData();
+ Splitter splitter;
+ switch (lastChildLayout.direction) {
+ case WEST:
+ splitter = new HSplitter(lastChild, false);
+ break;
+ case EAST:
+ splitter = new HSplitter(lastChild, true);
+ break;
+ case NORTH:
+ splitter = new VSplitter(lastChild, false);
+ break;
+ case SOUTH:
+ splitter = new VSplitter(lastChild, true);
+ break;
+ default:
+ assert false : "Unexpected direction";
+ return;
+ }
+
+ super.add(splitter, lastChildLayout.direction, SPLITTER_SIZE);
+ }
+
+ private Splitter getAssociatedSplitter(Widget child) {
+ // If a widget has a next sibling, it must be a splitter, because the only
+ // widget that *isn't* followed by a splitter must be the CENTER, which has
+ // no associated splitter.
+ int idx = getWidgetIndex(child);
+ if (idx < getWidgetCount() - 2) {
+ Widget splitter = getWidget(idx + 1);
+ assert splitter instanceof Splitter : "Expected child widget to be splitter";
+ return (Splitter) splitter;
+ }
+ return null;
+ }
+}
diff --git a/user/src/com/google/gwt/user/client/ui/StackLayoutPanel.java b/user/src/com/google/gwt/user/client/ui/StackLayoutPanel.java
new file mode 100644
index 0000000..a280d32
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/StackLayoutPanel.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2009 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.dom.client.Style.Unit;
+import com.google.gwt.layout.client.Layout.AnimationCallback;
+import com.google.gwt.layout.client.Layout.Layer;
+import com.google.gwt.user.client.Event;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * A panel that stacks its children vertically, displaying only one at a time,
+ * with a header for each child which the user can click to display.
+ *
+ * <p>
+ * This widget will <em>only</em> work in standards mode, which requires
+ * that the HTML page in which it is run have an explicit <!DOCTYPE>
+ * declaration.
+ * </p>
+ *
+ * <p>
+ * NOTE: This class is still very new, and its interface may change without
+ * warning. Use at your own risk.
+ * </p>
+ *
+ * <p>
+ * <h3>Example</h3>
+ * {@example com.google.gwt.examples.StackLayoutPanelExample}
+ * </p>
+ *
+ * TODO(jgw):
+ * - implement insert().
+ * - add() methods with default widgets for headers.
+ * - some way to get the header widget associated with a child.
+ * - make animation configurable (with {@link HasAnimation}).
+ * - default style.
+ */
+public class StackLayoutPanel extends Composite implements HasWidgets,
+ RequiresLayout, RequiresResize, ProvidesResize {
+
+ private class ClickWrapper extends Composite {
+ private Widget target;
+
+ public ClickWrapper(Widget target, Widget wrappee) {
+ this.target = target;
+ initWidget(wrappee);
+ sinkEvents(Event.ONCLICK);
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (event.getTypeInt() == Event.ONCLICK) {
+ showWidget(target);
+ }
+ }
+ }
+
+ private static class LayoutData {
+ public double headerSize;
+ public Widget header;
+ public Widget widget;
+ public Layer widgetLayer;
+ public Layer headerLayer;
+
+ public LayoutData(Widget widget, Widget header, double headerSize,
+ Layer widgetLayer, Layer headerLayer) {
+ this.widget = widget;
+ this.header = header;
+ this.headerSize = headerSize;
+ this.widgetLayer = widgetLayer;
+ this.headerLayer = headerLayer;
+ }
+ }
+
+ private static final int ANIMATION_TIME = 250;
+
+ private LayoutPanel layoutPanel;
+ private Unit unit;
+ private ArrayList<LayoutData> layoutData = new ArrayList<LayoutData>();
+ private Widget visibleWidget;
+
+ /**
+ * Creates an empty stack panel.
+ *
+ * @param unit the unit to be used for layout
+ */
+ public StackLayoutPanel(Unit unit) {
+ this.unit = unit;
+ initWidget(layoutPanel = new LayoutPanel());
+ }
+
+ public void add(Widget w) {
+ assert false : "Single-argument add() is not supported for this widget";
+ }
+
+ /**
+ * Adds a child widget to this stack, along with a widget representing the
+ * stack header.
+ *
+ * @param widget the child widget to be added
+ * @param header the header widget
+ * @param headerSize the size of the header widget
+ */
+ public void add(Widget widget, Widget header, double headerSize) {
+ ClickWrapper wrapper = new ClickWrapper(widget, header);
+ layoutPanel.add(wrapper);
+ layoutPanel.add(widget);
+
+ Layer headerLayer = layoutPanel.getLayer(wrapper);
+ headerLayer.setLeftRight(0, Unit.PX, 0, Unit.PX);
+
+ Layer widgetLayer = layoutPanel.getLayer(widget);
+ widgetLayer.setLeftRight(0, Unit.PX, 0, Unit.PX);
+
+ LayoutData data = new LayoutData(widget, wrapper, headerSize, widgetLayer,
+ headerLayer);
+ layoutData.add(data);
+
+ if (visibleWidget == null) {
+ visibleWidget = widget;
+ }
+ }
+
+ public void clear() {
+ layoutPanel.clear();
+ visibleWidget = null;
+ }
+
+ public Iterator<Widget> iterator() {
+ return new Iterator<Widget>() {
+ int i = 0, last = -1;
+
+ public boolean hasNext() {
+ return i < layoutData.size();
+ }
+
+ public Widget next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ return layoutData.get(last = i++).widget;
+ }
+
+ public void remove() {
+ if (last < 0) {
+ throw new IllegalStateException();
+ }
+
+ StackLayoutPanel.this.remove(layoutData.get(last).widget);
+ i = last;
+ last = -1;
+ }
+ };
+ }
+
+ public void layout() {
+ layout(0);
+ }
+
+ public void layout(int duration) {
+ layout(duration, null);
+ }
+
+ public void layout(int duration, AnimationCallback callback) {
+ int top = 0, bottom = 0;
+ int i = 0, visibleIndex = -1;
+ for (; i < layoutData.size(); ++i) {
+ LayoutData data = layoutData.get(i);
+ data.headerLayer.setTopHeight(top, unit, data.headerSize, unit);
+
+ top += data.headerSize;
+
+ data.widgetLayer.setTopHeight(top, unit, 0, unit);
+
+ if (data.widget == visibleWidget) {
+ visibleIndex = i;
+ break;
+ }
+ }
+
+ assert visibleIndex != -1;
+
+ for (int j = layoutData.size() - 1; j > i; --j) {
+ LayoutData data = layoutData.get(j);
+ data.headerLayer.setBottomHeight(bottom, unit, data.headerSize, unit);
+ data.widgetLayer.setBottomHeight(bottom, unit, 0, unit);
+ bottom += data.headerSize;
+ }
+
+ LayoutData data = layoutData.get(visibleIndex);
+ data.widgetLayer.setTopBottom(top, unit, bottom, unit);
+
+ layoutPanel.layout(duration, callback);
+ }
+
+ public void onResize() {
+ layoutPanel.onResize();
+ }
+
+ public boolean remove(Widget child) {
+ if (child.getParent() != this) {
+ return false;
+ }
+
+ LayoutData data = (LayoutData) child.getLayoutData();
+ layoutPanel.remove(data.header);
+ layoutPanel.remove(child);
+ return true;
+ }
+
+ /**
+ * Shows the specified widget.
+ *
+ * @param widget the child widget to be shown.
+ */
+ public void showWidget(Widget widget) {
+ visibleWidget = widget;
+ layout(ANIMATION_TIME);
+ }
+}