This change provides two new widgets HorizontalSplitPanel and
VerticalSplitPanel. These panels arrange two widgets in a single vertical
column or horizontal row and allow the user to interactively change the
proportion of the area dedicated to each of the two widgets.
Review by: jgw
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@834 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/doc/src/HorizontalSplitPanel.png b/doc/src/HorizontalSplitPanel.png
new file mode 100644
index 0000000..8e74d97
--- /dev/null
+++ b/doc/src/HorizontalSplitPanel.png
Binary files differ
diff --git a/doc/src/VerticalSplitPanel.png b/doc/src/VerticalSplitPanel.png
new file mode 100644
index 0000000..cf4697c
--- /dev/null
+++ b/doc/src/VerticalSplitPanel.png
Binary files differ
diff --git a/user/src/com/google/gwt/user/SplitPanel.gwt.xml b/user/src/com/google/gwt/user/SplitPanel.gwt.xml
new file mode 100644
index 0000000..d30648a
--- /dev/null
+++ b/user/src/com/google/gwt/user/SplitPanel.gwt.xml
@@ -0,0 +1,32 @@
+<!--
+ Copyright 2007 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.
+-->
+<module>
+ <inherits name="com.google.gwt.user.User"/>
+
+ <replace-with class="com.google.gwt.user.client.ui.HorizontalSplitPanel.Impl">
+ <when-type-is class="com.google.gwt.user.client.ui.HorizontalSplitPanel.Impl"/>
+ </replace-with>
+
+ <replace-with class="com.google.gwt.user.client.ui.HorizontalSplitPanel.ImplIE6">
+ <when-type-is class="com.google.gwt.user.client.ui.HorizontalSplitPanel.Impl"/>
+ <when-property-is name="user.agent" value="ie6"/>
+ </replace-with>
+
+ <replace-with class="com.google.gwt.user.client.ui.HorizontalSplitPanel.ImplSafari">
+ <when-type-is class="com.google.gwt.user.client.ui.HorizontalSplitPanel.Impl"/>
+ <when-property-is name="user.agent" value="safari"/>
+ </replace-with>
+</module>
\ No newline at end of file
diff --git a/user/src/com/google/gwt/user/User.gwt.xml b/user/src/com/google/gwt/user/User.gwt.xml
index af67fc6..3a84ffd 100644
--- a/user/src/com/google/gwt/user/User.gwt.xml
+++ b/user/src/com/google/gwt/user/User.gwt.xml
@@ -1,6 +1,19 @@
-<!-- -->
-<!-- Copyright 2007 Google Inc. All Rights Reserved. -->
-<!-- -->
+<!--
+ Copyright 2007 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.
+-->
+
<!-- Combines all user facilities into a single module for convenience. -->
<!-- Most new code should inherit this module. -->
<!-- -->
@@ -16,4 +29,5 @@
<inherits name="com.google.gwt.user.Focus"/>
<inherits name="com.google.gwt.user.ImageBundle"/>
<inherits name="com.google.gwt.user.ClippedImage"/>
+ <inherits name="com.google.gwt.user.SplitPanel"/>
</module>
diff --git a/user/src/com/google/gwt/user/client/ui/HorizontalSplitPanel.java b/user/src/com/google/gwt/user/client/ui/HorizontalSplitPanel.java
new file mode 100644
index 0000000..6409eea
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/HorizontalSplitPanel.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright 2007 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.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.Element;
+
+/**
+ * A panel that arranges two widgets in a single horizontal row and allows the
+ * user to interactively change the proportion of the width dedicated to each of
+ * the two widgets. Widgets contained within a <code>HorizontalSplitPanel</code>
+ * will be automatically decorated with scrollbars when necessary.
+ *
+ * <p>
+ * <img class='gallery' src='HorizontalSplitPanel.png'/>
+ * </p>
+ *
+ * <h3>CSS Style Rules</h3>
+ * <ul class='css'>
+ * <li>.gwt-HorizontalSplitPanel { the panel itself }</li>
+ * <li>.gwt-HorizontalSplitPanel left { the left container }</li>
+ * <li>.gwt-HorizontalSplitPanel right { the right container }</li>
+ * <li>.gwt-HorizontalSplitPanel splitter { the splitter }</li>
+ * </ul>
+ */
+public final class HorizontalSplitPanel extends SplitPanel {
+
+ /**
+ * The resizing implementation for standard browsers (Anything other than
+ * Safari and IE6/7)
+ */
+ private static class Impl {
+ // The x position of the mouse when drag resizing begins.
+ protected int initialThumbPos = 0;
+
+ // Widths of elements which are needed to do relative drag resizing.
+ protected int initialLeftWidth = 0;
+ protected int initialLeftContentWidth = 0;
+ protected int initialRightContentWidth = 0;
+
+ /**
+ * Initializes css properties on the panels DOM structure.
+ *
+ * @param panel the panel
+ */
+ protected void init(HorizontalSplitPanel panel) {
+ }
+
+ /**
+ * Called on each mouse move event during drag resizing.
+ *
+ * @param panel the panel
+ * @param pos the current horizontal mouse position relative to the panel
+ */
+ protected void onSplitResize(final HorizontalSplitPanel panel, int pos) {
+ // Compute the distance the splitter must be moved.
+ int offset = pos - initialThumbPos;
+
+ /*
+ * Compute the projected size of the content areas. This is to prevent
+ * out-of-bounds scrolling.
+ */
+ int newLeftContentWidth = initialLeftContentWidth + offset;
+ int newRightContentWidth = initialRightContentWidth - offset;
+
+ if (newLeftContentWidth < 0) {
+ offset -= newLeftContentWidth;
+ }
+
+ if (newRightContentWidth < 0) {
+ offset += newRightContentWidth;
+ }
+
+ // Move the split position by the offset.
+ setSplitPosition(panel, (initialLeftWidth + offset) + "px");
+ }
+
+ /**
+ * Called whenever drag resizing begins.
+ *
+ * @param panel the panel
+ * @param pos the current horizontal mouse position relative to the panel
+ */
+ protected void onSplitResizeStarted(final HorizontalSplitPanel panel,
+ int pos) {
+ initialThumbPos = pos;
+ initialLeftWidth = getOffsetWidth(panel.leftDiv);
+ initialLeftContentWidth = getClientWidth(panel.getElement(LEFT));
+ initialRightContentWidth = getClientWidth(panel.getElement(RIGHT));
+ }
+
+ /**
+ * Sets the horizontal position of the splitter.
+ *
+ * @param panel the panel
+ * @param pos the position as a css length
+ */
+ protected void setSplitPosition(final HorizontalSplitPanel panel,
+ final String pos) {
+ /*
+ * This default impl adjusts the width of the first level div and depends
+ * on the outer table to adjust its cell widths appropriately.
+ */
+ setWidth(panel.leftDiv, pos);
+ }
+ }
+
+ /**
+ * The resizing implementation for IE6/7.
+ */
+ private static class ImplIE6 extends Impl {
+ protected void init(final HorizontalSplitPanel panel) {
+ /*
+ * Without fixed table layout, IE will not respected the table width
+ * constraints.
+ */
+ DOM.setStyleAttribute(panel.table, "tableLayout", "fixed");
+
+ /*
+ * Since the DOM structure will not have finished full layout, we must
+ * defer until later to ensure that the inner divs update to the proper
+ * size.
+ */
+ DeferredCommand.addCommand(new Command() {
+ public void execute() {
+ updateDivWidth(panel);
+ }
+ });
+ }
+
+ protected void onSplitResizeStarted(final HorizontalSplitPanel panel,
+ final int x) {
+ initialThumbPos = x;
+ initialLeftWidth = getOffsetWidth(panel.leftTD);
+ initialLeftContentWidth = getClientWidth(panel.getElement(LEFT));
+ initialRightContentWidth = getClientWidth(panel.getElement(RIGHT));
+ }
+
+ protected void setSplitPosition(final HorizontalSplitPanel panel,
+ final String pos) {
+ final Element leftTD = panel.leftTD;
+ // adjust the width of the table cell instead of the inner div.
+ setWidth(leftTD, pos);
+ updateDivWidth(panel);
+ }
+
+ /*
+ * IE6 will not properly auto size the inner divs unless we explicitly set
+ * their width to something that forces a layout. NOTE: 100% works for
+ * quirks mode but will be problematic for standards mode when there are
+ * margins, border or padding.
+ */
+ private void updateDivWidth(final HorizontalSplitPanel panel) {
+ final String size = "100%";
+ setWidth(panel.leftDiv, size);
+ setWidth(panel.rightDiv, size);
+ setWidth(panel.getElement(LEFT), size);
+ setWidth(panel.getElement(RIGHT), size);
+ }
+ }
+
+ /**
+ * The resizing implemenation for Safari/WebKit.
+ */
+ private static class ImplSafari extends Impl {
+
+ protected void init(final HorizontalSplitPanel panel) {
+ /*
+ * Without fixed table layout, Safari will not respect the css width on
+ * the table.
+ */
+ DOM.setStyleAttribute(panel.table, "tableLayout", "fixed");
+
+ final String autoProp = "auto";
+ setWidth(panel.leftDiv, autoProp);
+ setWidth(panel.rightDiv, autoProp);
+ setWidth(panel.getElement(LEFT), autoProp);
+ setWidth(panel.getElement(RIGHT), autoProp);
+
+ /*
+ * Safari bug: a width must be set on the table when it is added to the
+ * DOM or else it cannot be set later.
+ */
+ panel.setWidth("100%");
+ }
+
+ protected void setSplitPosition(final HorizontalSplitPanel panel, String pos) {
+ // Adjust the width of the table cell instead of the inner div.
+ setWidth(panel.leftTD, pos);
+ }
+ }
+
+ /**
+ * Constants to provide more readable calls to {@link #getElement()} and
+ * {@link #getWidget(int)}.
+ */
+ private static final int LEFT = 0;
+ private static final int RIGHT = 1;
+
+ private static final int DEFAULT_SPLITTER_WIDTH = 10;
+
+ private static final int getClientWidth(final Element elem) {
+ return DOM.getIntAttribute(elem, "clientWidth");
+ }
+
+ private static final int getOffsetWidth(final Element elem) {
+ return DOM.getIntAttribute(elem, "offsetWidth");
+ }
+
+ private static final void setWidth(Element elem, String size) {
+ DOM.setStyleAttribute(elem, "width", size);
+ }
+
+ /**
+ * DOM elements needed to support splitter dragging. The underlying DOM
+ * structure is:
+ *
+ * <pre>
+ * table
+ * td (leftTD)
+ * div (leftDiv)
+ * div (getElement(LEFT))
+ * td (splitter)
+ * td (rightTD)
+ * div (rightDiv)
+ * div (getElement(RIGHT))
+ * </pre>
+ */
+ private final Element table;
+ private final Element leftTD, rightTD;
+ private final Element leftDiv, rightDiv;
+
+ private final Impl impl = (Impl) GWT.create(Impl.class);
+
+ /**
+ * Creates an empty horizontal split panel.
+ */
+ public HorizontalSplitPanel() {
+ super(DOM.createTable(), DOM.createTD(), DOM.createDiv(), DOM.createDiv());
+
+ table = getElement();
+ leftDiv = preventElementBoxStyles(DOM.createDiv());
+ rightDiv = preventElementBoxStyles(DOM.createDiv());
+ leftTD = preventElementBoxStyles(DOM.createTD());
+ rightTD = preventElementBoxStyles(DOM.createTD());
+
+ buildDOM();
+
+ setStyleName("gwt-HorizontalSplitPanel");
+
+ impl.init(this);
+ }
+
+ /**
+ * Gets the widget in the left side of the panel.
+ *
+ * @return the widget, <code>null</code> if there is not one.
+ */
+ public final Widget getLeftWidget() {
+ return getWidget(LEFT);
+ }
+
+ /**
+ * Gets the widget in the right side of the panel.
+ *
+ * @return the widget, <code>null</code> if there is not one.
+ */
+ public final Widget getRightWidget() {
+ return getWidget(RIGHT);
+ }
+
+ public final void setHeight(String height) {
+ DOM.setStyleAttribute(getElement(LEFT), "height", height);
+ DOM.setStyleAttribute(getElement(RIGHT), "height", height);
+ }
+
+ /**
+ * Sets the widget in the left side of the panel.
+ *
+ * @param w the widget
+ */
+ public final void setLeftWidget(Widget w) {
+ setWidget(LEFT, w);
+ }
+
+ /**
+ * Sets the widget in the right side of the panel.
+ *
+ * @param w the widget
+ */
+ public final void setRightWidget(Widget w) {
+ setWidget(RIGHT, w);
+ }
+
+ public final void setSplitPosition(String pos) {
+ impl.setSplitPosition(this, pos);
+ }
+
+ final void onSplitterResize(int x, int y) {
+ impl.onSplitResize(this, x);
+ }
+
+ final void onSplitterResizeStarted(int x, int y) {
+ impl.onSplitResizeStarted(this, x);
+ }
+
+ private void buildDOM() {
+ final Element leftContentDiv = getElement(LEFT);
+ final Element rightContentDiv = getElement(RIGHT);
+
+ final Element tbody = DOM.createTBody();
+ final Element tr = DOM.createTR();
+ final Element splitTD = getSplitElement();
+
+ DOM.appendChild(table, tbody);
+ DOM.appendChild(tbody, tr);
+ DOM.appendChild(tr, leftTD);
+ DOM.appendChild(tr, splitTD);
+ DOM.appendChild(tr, rightTD);
+ DOM.appendChild(leftTD, leftDiv);
+ DOM.appendChild(rightTD, rightDiv);
+ DOM.appendChild(leftDiv, leftContentDiv);
+ DOM.appendChild(rightDiv, rightContentDiv);
+
+ DOM.setInnerHTML(splitTD, " ");
+
+ DOM.setAttribute(table, "cellSpacing", "0");
+ DOM.setAttribute(table, "cellPadding", "0");
+
+ addElementScrolling(leftContentDiv);
+ addElementScrolling(rightContentDiv);
+
+ setElementClassname(leftContentDiv, "left");
+ setElementClassname(splitTD, "splitter");
+ setElementClassname(rightContentDiv, "right");
+
+ DOM.setStyleAttribute(leftTD, "verticalAlign", "top");
+ DOM.setStyleAttribute(rightTD, "verticalAlign", "top");
+
+ /*
+ * Ensures that the splitter is of reasonable width when no CSS is active on
+ * it, but this value is immediately overridden by CSS values.
+ */
+ DOM.setIntAttribute(splitTD, "width", DEFAULT_SPLITTER_WIDTH);
+ }
+}
diff --git a/user/src/com/google/gwt/user/client/ui/SplitPanel.java b/user/src/com/google/gwt/user/client/ui/SplitPanel.java
new file mode 100644
index 0000000..ce487e5
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/SplitPanel.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2007 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.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+
+import java.util.Iterator;
+
+/**
+ * Abstract base class for {@link HorizontalSplitPanel} and
+ * {@link VerticalSplitPanel}.
+ */
+abstract class SplitPanel extends Panel {
+
+ /**
+ * Adds clipping to an element.
+ *
+ * @param elem the element
+ */
+ static final void addElementClipping(final Element elem) {
+ DOM.setStyleAttribute(elem, "overflow", "hidden");
+ }
+
+ /**
+ * Adds as-needed scrolling to an element.
+ *
+ * @param elem the element
+ */
+ static final void addElementScrolling(final Element elem) {
+ DOM.setStyleAttribute(elem, "overflow", "auto");
+ }
+
+ /**
+ * Adds zero or none css values for padding, margin and border to prevent
+ * stylesheet overrides. Returns the element for convienence to support
+ * builder pattern.
+ *
+ * @param elem the element
+ * @return the element
+ */
+ static final Element preventElementBoxStyles(final Element elem) {
+ DOM.setIntStyleAttribute(elem, "padding", 0);
+ DOM.setIntStyleAttribute(elem, "margin", 0);
+ DOM.setStyleAttribute(elem, "border", "none");
+ return elem;
+ }
+
+ /**
+ * Adds zero size padding to an element.
+ *
+ * @param elem the element.
+ */
+ static final void preventElementPadding(final Element elem) {
+ DOM.setStyleAttribute(elem, "padding", "0");
+ }
+
+ /**
+ * Sets the elements css class name.
+ *
+ * @param elem the element
+ * @param className the class name
+ */
+ static final void setElementClassname(final Element elem,
+ final String className) {
+ DOM.setAttribute(elem, "className", className);
+ }
+
+ // The enclosed widgets.
+ private final Widget[] widgets = new Widget[2];
+
+ // The elements containing the widgets.
+ private final Element[] elements = new Element[2];
+
+ // The element that acts as the splitter.
+ private final Element splitElem;
+
+ // Indicates whether drag resizing is active.
+ private boolean isResizing = false;
+
+ /**
+ * Initializes the split panel.
+ *
+ * @param mainElem the root element for the split panel
+ * @param splitElem the element that acts as the splitter
+ * @param headElem the element to contain the top or left most widget
+ * @param tailElem the element to contain the bottom or right most widget
+ */
+ SplitPanel(Element mainElem, Element splitElem, Element headElem,
+ Element tailElem) {
+ setElement(mainElem);
+ this.splitElem = splitElem;
+ elements[0] = headElem;
+ elements[1] = tailElem;
+ sinkEvents(Event.MOUSEEVENTS);
+ }
+
+ public void add(Widget w) {
+ if (getWidget(0) == null) {
+ setWidget(0, w);
+ } else if (getWidget(1) == null) {
+ setWidget(1, w);
+ } else {
+ throw new IllegalStateException(
+ "A Splitter can only contain two Widgets.");
+ }
+ }
+
+ /**
+ * Indicates whether the split panel is being resized.
+ *
+ * @return <code>true</code> if the user is dragging the splitter,
+ * <code>false</code> otherwise
+ */
+ public boolean isResizing() {
+ return isResizing;
+ }
+
+ public Iterator iterator() {
+ return WidgetIterators.createWidgetIterator(this, widgets);
+ }
+
+ public void onBrowserEvent(Event event) {
+ switch (DOM.eventGetType(event)) {
+
+ case Event.ONMOUSEDOWN: {
+ Element target = DOM.eventGetTarget(event);
+ if (DOM.isOrHasChild(splitElem, target)) {
+ startResizingFrom(DOM.eventGetClientX(event) - getAbsoluteLeft(),
+ DOM.eventGetClientY(event) - getAbsoluteTop());
+ DOM.eventPreventDefault(event);
+ }
+ break;
+ }
+
+ case Event.ONMOUSEUP: {
+ stopResizing();
+ break;
+ }
+
+ case Event.ONMOUSEMOVE: {
+ if (isResizing()) {
+ onSplitterResize(DOM.eventGetClientX(event) - getAbsoluteLeft(),
+ DOM.eventGetClientY(event) - getAbsoluteTop());
+ DOM.eventPreventDefault(event);
+ }
+ break;
+ }
+ }
+ }
+
+ public boolean remove(Widget widget) {
+ if (widget == null) {
+ throw new IllegalArgumentException("Widget must not be null");
+ }
+
+ if (widgets[0] == widget) {
+ setWidget(0, null);
+ return true;
+ } else if (widgets[1] == widget) {
+ setWidget(1, null);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Moves the position of the splitter.
+ *
+ * @param size the new size of the left region in CSS units (e.g. "10px",
+ * "1em")
+ */
+ public abstract void setSplitPosition(String size);
+
+ /**
+ * Gets the content element for the given index.
+ *
+ * @param index the index of the element, only 0 and 1 are valid.
+ * @return the element
+ */
+ protected Element getElement(int index) {
+ return elements[index];
+ }
+
+ /**
+ * Gets the element that is acting as the splitter.
+ *
+ * @return the element
+ */
+ protected Element getSplitElement() {
+ return splitElem;
+ }
+
+ /**
+ * Gets one of the contained widgets.
+ *
+ * @param index the index of the widget, only 0 and 1 are valid.
+ * @return the widget
+ */
+ protected Widget getWidget(int index) {
+ return widgets[index];
+ }
+
+ /**
+ * Sets one of the contained widgets.
+ *
+ * @param index the index, only 0 and 1 are valid
+ * @param w the widget
+ */
+ protected final void setWidget(int index, Widget w) {
+ if (widgets[index] != null) {
+ disown(widgets[index]);
+ }
+
+ widgets[index] = w;
+
+ if (w != null) {
+ adopt(w, elements[index]);
+ }
+ }
+
+ /**
+ * Called on each mouse drag event as the user is dragging the splitter.
+ *
+ * @param x the x coordinate of the mouse relative to the panel's extent
+ * @param y the y coordinate of the mosue relative to the panel's extent
+ */
+ abstract void onSplitterResize(int x, int y);
+
+ /**
+ * Called when the user starts dragging the splitter.
+ *
+ * @param x the x coordinate of the mouse relative to the panel's extent
+ * @param y the y coordinate of the mouse relative to the panel's extent
+ */
+ abstract void onSplitterResizeStarted(int x, int y);
+
+ private void startResizingFrom(int x, int y) {
+ isResizing = true;
+ onSplitterResizeStarted(x, y);
+ }
+
+ private void stopResizing() {
+ isResizing = false;
+ }
+}
diff --git a/user/src/com/google/gwt/user/client/ui/VerticalSplitPanel.java b/user/src/com/google/gwt/user/client/ui/VerticalSplitPanel.java
new file mode 100644
index 0000000..b75e7d7
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/VerticalSplitPanel.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2007 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.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.Element;
+
+/**
+ * A panel that arranges two widgets in a single vertical column and allows the
+ * user to interactively change the proportion of the height dedicated to each
+ * of the two widgets. Widgets contained within a
+ * <code>VerticalSplitterPanel</code> will be automatically decorated with
+ * scrollbars when neccessary.
+ *
+ * <p>
+ * <img class='gallery' src='VerticalSplitPanel.png'/>
+ * </p>
+ *
+ * <h3>CSS Style Rules</h3>
+ * <ul class='css'>
+ * <li>.gwt-VerticalSplitPanel { the panel itself }</li>
+ * <li>.gwt-VerticalSplitPanel top { the top container }</li>
+ * <li>.gwt-VerticalSplitPanel bottom { the bottom container }</li>
+ * <li>.gwt-VerticalSplitPanel splitter { the splitter }</li>
+ * </ul>
+ */
+public final class VerticalSplitPanel extends SplitPanel {
+
+ private static final int TOP = 0;
+ private static final int BOTTOM = 1;
+
+ private static int getClientHeight(Element elem) {
+ return DOM.getIntAttribute(elem, "clientHeight");
+ }
+
+ private static int getOffsetTop(Element elem) {
+ return DOM.getIntAttribute(elem, "offsetTop");
+ }
+
+ private static Element lockStyles(final Element elem) {
+ DOM.setIntStyleAttribute(elem, "height", 0);
+ return preventElementBoxStyles(elem);
+ }
+
+ private static void setHeight(Element elem, int px) {
+ DOM.setStyleAttribute(elem, "height", Math.max(0, px) + "px");
+ }
+
+ // Element is added below bottom container element to make it possible to
+ // infer the bottom element's height.
+ private final Element probeElem;
+
+ // Captures the height of the top container when drag resizing starts.
+ private int initialTopHeight = 0;
+
+ // Captures the offset of a user's mouse pointer during drag resizing.
+ private int initialThumbPos = 0;
+
+ /**
+ * Creates an empty vertical split panel.
+ */
+ public VerticalSplitPanel() {
+ super(DOM.createDiv(), DOM.createDiv(), DOM.createDiv(), DOM.createDiv());
+
+ final Element thisElem = getElement();
+ final Element splitElem = getSplitElement();
+ final Element topElem = getElement(TOP);
+ final Element bottomElem = getElement(BOTTOM);
+ probeElem = lockStyles(DOM.createDiv());
+
+ DOM.appendChild(thisElem, topElem);
+ DOM.appendChild(thisElem, splitElem);
+ DOM.appendChild(thisElem, bottomElem);
+ DOM.appendChild(thisElem, probeElem);
+
+ addElementClipping(thisElem);
+ addElementScrolling(topElem);
+ addElementScrolling(bottomElem);
+
+ // Prevent padding on container elements.
+ preventElementPadding(thisElem);
+ preventElementPadding(topElem);
+ preventElementPadding(bottomElem);
+
+ setElementClassname(topElem, "top");
+ setElementClassname(splitElem, "splitter");
+ setElementClassname(bottomElem, "bottom");
+
+ setStyleName("gwt-VerticalSplitPanel");
+
+ // Must wait on layout to do the initial layout.
+ DeferredCommand.addCommand(new Command() {
+ public void execute() {
+ updateBottomHeight();
+ }
+ });
+ }
+
+ /**
+ * Gets the widget in the bottom of the panel.
+ *
+ * @return the widget, <code>null</code> if there is not one
+ */
+ public final Widget getBottomWidget() {
+ return getWidget(BOTTOM);
+ }
+
+ /**
+ * Gets the widget in the top of the panel.
+ *
+ * @return the widget, <code>null</code> if there is not one
+ */
+ public final Widget getTopWidget() {
+ return getWidget(TOP);
+ }
+
+ /**
+ * Sets the widget in the bottom of the panel.
+ *
+ * @param w the widget
+ */
+ public final void setBottomWidget(Widget w) {
+ setWidget(BOTTOM, w);
+ }
+
+ public final void setSplitPosition(String size) {
+ DOM.setStyleAttribute(getElement(TOP), "height", size);
+ updateBottomHeight();
+ }
+
+ /**
+ * Sets the widget in the top of the panel.
+ *
+ * @param w the widget
+ */
+ public final void setTopWidget(Widget w) {
+ setWidget(TOP, w);
+ }
+
+ final void onSplitterResize(int x, int y) {
+ /*
+ * When dragging starts we record the thumb position and the current height
+ * of the top div. On each subsequent resize event, we compute how far the
+ * thumb has moved and adjust the top and bottom div by that offset.
+ */
+ final Element topElem = getElement(TOP);
+ final Element botElem = getElement(BOTTOM);
+
+ // Compute what the new top height should be.
+ final int newTopHeight = initialTopHeight + (y - initialThumbPos);
+ final int newBotHeight = getClientHeight(botElem)
+ + getClientHeight(topElem) - newTopHeight;
+
+ /*
+ * NOTE: The bottom must be adjusted before the top due to FF bug which
+ * leaves scrollbar artifacts in the overflow region.
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=368190
+ */
+ if (newBotHeight < 0) {
+ setHeight(botElem, 0);
+ setHeight(topElem, newTopHeight + newBotHeight);
+ } else {
+ setHeight(botElem, newBotHeight);
+ setHeight(topElem, newTopHeight);
+ }
+
+ updateBottomHeight();
+ }
+
+ final void onSplitterResizeStarted(int x, int y) {
+ initialThumbPos = y;
+ initialTopHeight = getClientHeight(getElement(TOP));
+ }
+
+ /**
+ * Updates to the height on the bottom div so that it remains within the outer
+ * container.
+ */
+ private void updateBottomHeight() {
+ final Element thisElem = getElement();
+ final Element bottomElem = getElement(BOTTOM);
+
+ /*
+ * This is the definitive check that tells us how far (in pixels) the height
+ * of the bottom div must change. We do this by comparing the clientHeight
+ * of the root div with the offsetTop of a probe div under the bottom div.
+ */
+ final int adjust = getClientHeight(thisElem)
+ - (getOffsetTop(probeElem) - getOffsetTop(thisElem));
+
+ /*
+ * In the case where the user is dragging the splitter, resizeTopBy should
+ * generally guess the right adjustment based on how far the top div was
+ * adjusted. So for the most common case, we find we do not need adjustment
+ * and exit here.
+ */
+ if (adjust == 0) {
+ return;
+ }
+
+ /*
+ * We don't know what margins and borders are in play on the bottom div, so
+ * we naively guess they are all zero, which would mean that the CSS height
+ * property will be equal to the clientHeight attribute. After we set the
+ * height in css, we take the difference between what we set and the
+ * reported clientHeight. If that is non-zero, it tells us how much to
+ * accomodate for margin, border and what not.
+ */
+ final int curHeight = getClientHeight(bottomElem);
+ final int newHeight = curHeight + adjust;
+ setHeight(bottomElem, newHeight);
+ final int error = getClientHeight(bottomElem) - newHeight;
+
+ if (error == 0) {
+ return;
+ }
+
+ setHeight(bottomElem, newHeight - error);
+ }
+}
diff --git a/user/test/com/google/gwt/user/client/ui/SplitPanelTest.java b/user/test/com/google/gwt/user/client/ui/SplitPanelTest.java
new file mode 100644
index 0000000..bd082dd
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/ui/SplitPanelTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2007 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.DOM;
+
+/**
+ * Tests both {@link HorizontalSplitPanel} and {@link VerticalSplitPanel}.
+ *
+ */
+public class SplitPanelTest extends GWTTestCase {
+ public String getModuleName() {
+ return "com.google.gwt.user.User";
+ }
+
+ private Widget createMockWidget() {
+ final Label label = new Label();
+ label.setText("Testing 1, 2, 3");
+ DOM.setStyleAttribute(label.getElement(), "fontSize", "72pt");
+ return label;
+ }
+
+ /**
+ * Tests creation, widget assignment, null assignment for
+ * {@link HorizontalSplitPanel}.
+ */
+ public void testHorizontalSplitPanelCreate() {
+ final HorizontalSplitPanel panel = new HorizontalSplitPanel();
+ final Widget widgetA = createMockWidget();
+ final Widget widgetB = createMockWidget();
+
+ // Intentionally add before setting widgets.
+
+ RootPanel.get().add(panel);
+
+ panel.setHeight("100px");
+ panel.setWidth("100px");
+
+ // Ensure position can be set before widgets are added.
+ panel.setSplitPosition("20px");
+
+ panel.setRightWidget(widgetB);
+ panel.setLeftWidget(widgetA);
+
+ assertTrue(panel.getRightWidget() == widgetB);
+ assertTrue(panel.getLeftWidget() == widgetA);
+
+ panel.setLeftWidget(null);
+ panel.setRightWidget(null);
+
+ assertTrue(panel.getRightWidget() == null);
+ assertTrue(panel.getLeftWidget() == null);
+
+ panel.setLeftWidget(widgetB);
+ panel.setRightWidget(widgetA);
+
+ assertTrue(panel.getLeftWidget() == widgetB);
+ assertTrue(panel.getRightWidget() == widgetA);
+
+ // Ensure we ended up at the right size.
+ assertEquals(panel.getOffsetWidth(), 100);
+ assertEquals(panel.getOffsetHeight(), 100);
+ }
+
+ /**
+ * Tests creation, widget assignment, null assigment for
+ * {@link VerticalSplitPanel}.
+ */
+ public void testVerticalSplitPanelCreate() {
+
+ final VerticalSplitPanel panel = new VerticalSplitPanel();
+ final Widget widgetA = createMockWidget();
+ final Widget widgetB = createMockWidget();
+
+ // Intentionally add before setting widgets.
+ RootPanel.get().add(panel);
+
+ panel.setHeight("100px");
+ panel.setWidth("100px");
+ // Ensure position can be set before widgets are added.
+ panel.setSplitPosition("20px");
+
+ panel.setBottomWidget(widgetB);
+ panel.setTopWidget(widgetA);
+
+ assertTrue(panel.getBottomWidget() == widgetB);
+ assertTrue(panel.getTopWidget() == widgetA);
+
+ panel.setTopWidget(null);
+ panel.setBottomWidget(null);
+
+ assertTrue(panel.getTopWidget() == null);
+ assertTrue(panel.getBottomWidget() == null);
+
+ panel.setTopWidget(widgetB);
+ panel.setBottomWidget(widgetA);
+
+ assertTrue(panel.getTopWidget() == widgetB);
+ assertTrue(panel.getBottomWidget() == widgetA);
+
+ // Ensure we ended up at the right size.
+ assertEquals(panel.getOffsetWidth(), 100);
+ assertEquals(panel.getOffsetHeight(), 100);
+ }
+}