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); + } +}