Adds addtional improvements to HorizontalSplitPanel and VerticalSplitPanel.
The changes include:
1 - Limited styling on container elements to work around Safari2 bug with
percentage heights.
2 - Added thumb images to each of the split panels (via ImageBundle).
3 - Reworked VerticalSplitPanel to be more like HorizontalSplitPanel.
4 - Refactored most of the utility methods into static methods inside of
SplitPanel.
5 - Added mouse move event coalescing to the IE6/7 impl to make repaints less
choppy.
Review by: jgw
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@1319 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/samples/kitchensink/src/com/google/gwt/sample/kitchensink/public/KitchenSink.css b/samples/kitchensink/src/com/google/gwt/sample/kitchensink/public/KitchenSink.css
index 79f8266..b8812c9 100644
--- a/samples/kitchensink/src/com/google/gwt/sample/kitchensink/public/KitchenSink.css
+++ b/samples/kitchensink/src/com/google/gwt/sample/kitchensink/public/KitchenSink.css
@@ -2,16 +2,16 @@
background-color: white;
color: black;
font-family: Helvetica, Arial, sans-serif;
- font-size: 10pt;
+ font-size: 10pt;
margin: 0px 20px 20px 20px;
}
h2 {
- font-weight: normal;
+ font-weight: normal;
}
table {
- font-size: 100%;
+ font-size: 100%;
}
code {
@@ -114,7 +114,7 @@
}
.gwt-TabPanel {
- margin-top: 4px;
+ margin-top: 4px;
}
.gwt-TabPanelBottom {
@@ -160,11 +160,11 @@
}
.gwt-TextBox-readonly {
- color: #888;
+ color: #888;
}
.gwt-Tree {
- background: white;
+ background: white;
}
.gwt-Tree .gwt-TreeItem {
@@ -319,12 +319,12 @@
}
.gwt-HorizontalSplitPanel {
- border: 8px solid #C3D9FF;
+ border: 8px solid #C3D9FF;
}
-.gwt-HorizontalSplitPanel .splitter {
- background-color: #C3D9FF;
- cursor: move;
+.gwt-HorizontalSplitPanel .hsplitter {
+ background-color: #C3D9FF;
+ cursor: move;
}
.gwt-HorizontalSplitPanel .left {
@@ -335,22 +335,22 @@
}
.gwt-VerticalSplitPanel .splitter {
- background-color: #C3D9FF;
- height: 8px;
- cursor: move;
+ background-color: #C3D9FF;
+ height: 8px;
+ cursor: move;
}
.gwt-SuggestBoxPopup {
- border: 2px solid #C3D9FF;
+ border: 2px solid #C3D9FF;
}
.gwt-SuggestBoxPopup .item {
- padding: 2px;
+ padding: 2px;
}
.gwt-SuggestBoxPopup .item-selected {
- background-color: #C3D9FF;
- padding: 2px;
+ background-color: #C3D9FF;
+ padding: 2px;
}
/* -------------------------------------------------------------------------- */
@@ -360,7 +360,7 @@
}
.ks-Info {
- color: white;
+ color: white;
padding: 20px 10px 20px 40px;
margin-bottom: 10px;
}
@@ -369,32 +369,32 @@
}
.ks-List .gwt-Image {
- position: relative;
- top: 8px;
+ position: relative;
+ top: 8px;
}
.ks-List .ks-SinkItem a {
- text-decoration: none;
- color: white;
+ text-decoration: none;
+ color: white;
}
.ks-List .ks-SinkItem-selected a {
- text-decoration: none;
- color: white;
+ text-decoration: none;
+ color: white;
}
.ks-List .ks-FirstSinkItem a {
- text-decoration: none;
- color: white;
+ text-decoration: none;
+ color: white;
}
.ks-List .ks-FirstSinkItem-selected a {
- text-decoration: none;
- color: white;
+ text-decoration: none;
+ color: white;
}
.ks-List .ks-SinkItem {
- background: #c6cab7;
+ background: #c6cab7;
padding: 4px;
padding-left: 16px;
padding-right: 16px;
@@ -405,7 +405,7 @@
}
.ks-List .ks-SinkItem-selected {
- background: #c6cab7;
+ background: #c6cab7;
padding: 4px;
padding-left: 16px;
padding-right: 16px;
@@ -414,7 +414,7 @@
}
.ks-List .ks-FirstSinkItem {
- background: #c6cab7 url(images/corner.gif) no-repeat top left;
+ background: #c6cab7 url(images/corner.gif) no-repeat top left;
padding: 4px;
padding-left: 16px;
padding-right: 16px;
@@ -425,7 +425,7 @@
}
.ks-List .ks-FirstSinkItem-selected {
- background: #c3d9ff url(images/corner.gif) no-repeat top left;
+ background: #c3d9ff url(images/corner.gif) no-repeat top left;
padding: 4px;
padding-left: 16px;
padding-right: 16px;
diff --git a/user/src/com/google/gwt/user/SplitPanel.gwt.xml b/user/src/com/google/gwt/user/SplitPanel.gwt.xml
index 4ea6171..a0bb8db 100644
--- a/user/src/com/google/gwt/user/SplitPanel.gwt.xml
+++ b/user/src/com/google/gwt/user/SplitPanel.gwt.xml
@@ -14,14 +14,29 @@
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>
+ <inherits name="com.google.gwt.user.User"/>
- <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.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>
+
+ <replace-with class="com.google.gwt.user.client.ui.VerticalSplitPanel.Impl">
+ <when-type-is class="com.google.gwt.user.client.ui.VerticalSplitPanel.Impl"/>
+ </replace-with>
+
+ <replace-with class="com.google.gwt.user.client.ui.VerticalSplitPanel.ImplIE6">
+ <when-type-is class="com.google.gwt.user.client.ui.VerticalSplitPanel.Impl"/>
+ <when-property-is name="user.agent" value="ie6"/>
+ </replace-with>
+
</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
index 9c3f2a7..cd8a2b9 100644
--- a/user/src/com/google/gwt/user/client/ui/HorizontalSplitPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/HorizontalSplitPanel.java
@@ -20,6 +20,7 @@
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Timer;
/**
* A panel that arranges two widgets in a single horizontal row and allows the
@@ -34,8 +35,6 @@
* <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 hsplitter { the splitter }</li>
* </ul>
*/
@@ -45,40 +44,84 @@
* The standard implementation for horizontal split panels.
*/
private static class Impl {
- private static void snapToAllEdges(Element elem) {
- enableAbsolutePositon(elem);
- DOM.setStyleAttribute(elem, "left", "0");
- DOM.setStyleAttribute(elem, "right", "0");
- DOM.setStyleAttribute(elem, "top", "0");
- DOM.setStyleAttribute(elem, "bottom", "0");
+ private static void expandToFitParentHorizontally(Element elem) {
+ addAbsolutePositoning(elem);
+ final String zeroSize = "0px";
+ setTop(elem, zeroSize);
+ setBottom(elem, zeroSize);
}
- private static void snapToTopAndBottomEdges(Element elem) {
- enableAbsolutePositon(elem);
- DOM.setStyleAttribute(elem, "top", "0");
- DOM.setStyleAttribute(elem, "bottom", "0");
- }
+ protected HorizontalSplitPanel panel;
public void init(HorizontalSplitPanel panel) {
+ this.panel = panel;
+
DOM.setStyleAttribute(panel.getElement(), "position", "relative");
- snapToAllEdges(panel.rootWrapper);
- snapToAllEdges(panel.getElement(LEFT));
- snapToAllEdges(panel.getElement(RIGHT));
- snapToTopAndBottomEdges(panel.leftWrapper);
- snapToTopAndBottomEdges(panel.rightWrapper);
- snapToTopAndBottomEdges(panel.getSplitElement());
+ final Element rightElem = panel.getElement(RIGHT);
- // This ensures that any overflow is hidden on the right side of the
- // panel. This can happen when the right side panel is smaller than
- // the borders on its child.
- addElementClipping(panel.rightWrapper);
+ expandToFitParentHorizontally(panel.getElement(LEFT));
+ expandToFitParentHorizontally(rightElem);
+ expandToFitParentHorizontally(panel.getSplitElement());
+
+ expandToFitParentUsingCssOffsets(panel.container);
+
+ // Snap the right wrapper to the right side.
+ setRight(rightElem, "0px");
}
- public void onAttach(HorizontalSplitPanel panel) {
+ public void onAttach() {
}
- public void onDetach(HorizontalSplitPanel panel) {
+ public void onDetach() {
+ }
+
+ public void onSplitResize(int px) {
+ setSplitPosition(px);
+ }
+
+ public void setSplitPosition(int px) {
+ final Element splitElem = panel.getSplitElement();
+
+ final int rootElemWidth = getOffsetWidth(panel.container);
+ final int splitElemWidth = getOffsetWidth(splitElem);
+
+ // This represents an invalid state where layout is incomplete. This
+ // typically happens before DOM attachment, but I leave it here as a
+ // precaution because negative width/height style attributes produce
+ // errors on IE.
+ if (rootElemWidth < splitElemWidth) {
+ return;
+ }
+
+ // Compute the new right side width.
+ int newRightWidth = rootElemWidth - px - splitElemWidth;
+
+ // Constrain the dragging to the physical size of the panel.
+ if (px < 0) {
+ px = 0;
+ newRightWidth = rootElemWidth - splitElemWidth;
+ } else if (newRightWidth < 0) {
+ px = rootElemWidth - splitElemWidth;
+ newRightWidth = 0;
+ }
+
+ final Element rightElem = panel.getElement(RIGHT);
+
+ // Set the width of the left side.
+ setWidth(panel.getElement(LEFT), px + "px");
+
+ // Move the splitter to the right edge of the left element.
+ setLeft(splitElem, px + "px");
+
+ // Move the right element to the right of the splitter.
+ setLeft(rightElem, (px + splitElemWidth) + "px");
+
+ updateRightWidth(rightElem, newRightWidth);
+ }
+
+ public void updateRightWidth(Element rightElem, int newRightWidth) {
+ // Update is handled by CSS.
}
}
@@ -86,112 +129,110 @@
* The IE6 implementation for horizontal split panels.
*/
private static class ImplIE6 extends Impl {
- private static final String FULLSIZE = "100%";
- private static native void addResizeListener(HorizontalSplitPanel panel) /*-{
- var root = panel.@com.google.gwt.user.client.ui.HorizontalSplitPanel::rootWrapper;
- root.onresize = function() {
- @com.google.gwt.user.client.ui.HorizontalSplitPanel$ImplIE6::onResize(Lcom/google/gwt/user/client/ui/HorizontalSplitPanel;)(panel);
- };
- }-*/;
+ private boolean isResizeInProgress = false;
- private static void onResize(HorizontalSplitPanel panel) {
- final String height = getOffsetHeight(panel.rootWrapper) + "px";
- setHeight(panel.rightWrapper, height);
- setHeight(panel.getSplitElement(), height);
- setHeight(panel.leftWrapper, height);
- }
-
- private static void snapToAllEdges(Element elem) {
- enableAbsolutePositon(elem);
- setWidth(elem, FULLSIZE);
- setHeight(elem, FULLSIZE);
- }
-
- private static void snapToTopAndBottomEdges(Element elem) {
- enableAbsolutePositon(elem);
- setHeight(elem, FULLSIZE);
- }
+ private int splitPosition = 0;
public void init(HorizontalSplitPanel panel) {
+ this.panel = panel;
+
final Element elem = panel.getElement();
// Prevents inherited text-align settings from interfering with the
// panel's layout.
DOM.setStyleAttribute(elem, "textAlign", "left");
DOM.setStyleAttribute(elem, "position", "relative");
- enableAbsolutePositon(panel.rightWrapper);
- enableAbsolutePositon(panel.leftWrapper);
+ /*
+ * Technically, these are snapped to the top and bottom, but IE doesn't
+ * provide a reliable way to make that happen, so a resize listener is
+ * wired up to control the height of these elements.
+ */
+ addAbsolutePositoning(panel.getElement(LEFT));
+ addAbsolutePositoning(panel.getElement(RIGHT));
+ addAbsolutePositoning(panel.getSplitElement());
- snapToAllEdges(panel.rootWrapper);
- snapToAllEdges(panel.getElement(LEFT));
- snapToAllEdges(panel.getElement(RIGHT));
- snapToTopAndBottomEdges(panel.getSplitElement());
+ expandToFitParentUsingPercentages(panel.container);
}
- public void onAttach(HorizontalSplitPanel panel) {
- addResizeListener(panel);
- onResize(panel);
+ public void onAttach() {
+ addResizeListener(panel.container);
+ onResize();
}
- public void onDetach(HorizontalSplitPanel panel) {
- DOM.setElementAttribute(panel.rootWrapper, "onresize", null);
+ public void onDetach() {
+ DOM.setElementAttribute(panel.container, "onresize", null);
+ }
+
+ public void onSplitResize(int px) {
+ final int resizeUpdatePeriod = 20; // ms
+ if (!isResizeInProgress) {
+ isResizeInProgress = true;
+ new Timer() {
+ public void run() {
+ setSplitPosition(splitPosition);
+ isResizeInProgress = false;
+ }
+ }.schedule(resizeUpdatePeriod);
+ }
+ splitPosition = px;
+ }
+
+ public void updateRightWidth(Element rightElem, int newRightWidth) {
+ setWidth(rightElem, newRightWidth + "px");
+ }
+
+ private native void addResizeListener(Element container) /*-{
+ var self = this;
+ container.onresize = function() {
+ self.@com.google.gwt.user.client.ui.HorizontalSplitPanel$ImplIE6::onResize()();
+ };
+ }-*/;
+
+ private void onResize() {
+ final Element leftElem = panel.getElement(LEFT);
+ final Element rightElem = panel.getElement(RIGHT);
+
+ final String height = getOffsetHeight(panel.container) + "px";
+ setHeight(rightElem, height);
+ setHeight(panel.getSplitElement(), height);
+ setHeight(leftElem, height);
+ setSplitPosition(getOffsetWidth(leftElem));
}
}
/**
- * Constants to provide more readable calls to {@link #getElement()} and
+ * The Safari implementation which owes its existence entirely to a single
+ * WebKit bug: http://bugs.webkit.org/show_bug.cgi?id=9137.
+ */
+ private static class ImplSafari extends Impl {
+ public void init(HorizontalSplitPanel panel) {
+ this.panel = panel;
+ final String fullSize = "100%";
+ super.init(panel);
+ setHeight(panel.container, fullSize);
+ setHeight(panel.getElement(LEFT), fullSize);
+ setHeight(panel.getElement(RIGHT), fullSize);
+ setHeight(panel.getSplitElement(), fullSize);
+ }
+ }
+
+ /**
+ * Constant makes for readable calls to {@link #getElement(int)} and
* {@link #getWidget(int)}.
*/
private static final int LEFT = 0;
+ /**
+ * Constant makes for readable calls to {@link #getElement(int)} and
+ * {@link #getWidget(int)}.
+ */
private static final int RIGHT = 1;
- private static final Impl impl = (Impl) GWT.create(Impl.class);
+ // A style-free element to serve as the root container.
+ private final Element container;
- private static void enableAbsolutePositon(Element elem) {
- DOM.setStyleAttribute(elem, "position", "absolute");
- }
-
- private static final int getClientWidth(final Element elem) {
- return DOM.getElementPropertyInt(elem, "clientWidth");
- }
-
- private static final int getOffsetHeight(Element elem) {
- return DOM.getElementPropertyInt(elem, "offsetHeight");
- }
-
- private static final int getOffsetWidth(final Element elem) {
- return DOM.getElementPropertyInt(elem, "offsetWidth");
- }
-
- private static final void setHeight(Element elem, String size) {
- DOM.setStyleAttribute(elem, "height", size);
- }
-
- private static final void setLeft(Element elem, String size) {
- DOM.setStyleAttribute(elem, "left", size);
- }
-
- 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>
- * div
- * div (rootWrapper)
- * div (leftWrapper)
- * div (getElement(LEFT))
- * div (getSplitElement())
- * div (rightWrapper)
- * div (getElement(RIGHT))
- * </pre>
- */
- private final Element rootWrapper, leftWrapper, rightWrapper;
+ private final Impl impl = (Impl) GWT.create(Impl.class);
/**
* If the split position is set while the split panel is not attached, save it
@@ -203,17 +244,21 @@
private int initialLeftWidth;
+ public HorizontalSplitPanel() {
+ this(
+ (HorizontalSplitPanelImages) GWT.create(HorizontalSplitPanelImages.class));
+ }
+
/**
* Creates an empty horizontal split panel.
*/
- public HorizontalSplitPanel() {
- super(DOM.createDiv(), DOM.createDiv(), DOM.createDiv(), DOM.createDiv());
+ public HorizontalSplitPanel(HorizontalSplitPanelImages images) {
+ super(DOM.createDiv(), DOM.createDiv(), preventBoxStyles(DOM.createDiv()),
+ preventBoxStyles(DOM.createDiv()));
- rootWrapper = preventElementBoxStyles(DOM.createDiv());
- leftWrapper = preventElementBoxStyles(DOM.createDiv());
- rightWrapper = preventElementBoxStyles(DOM.createDiv());
+ container = preventBoxStyles(DOM.createDiv());
- buildDOM();
+ buildDOM(images.horizontalSplitPanelThumb());
setStyleName("gwt-HorizontalSplitPanel");
@@ -263,21 +308,16 @@
public final void setSplitPosition(String pos) {
lastSplitPosition = pos;
- final Element leftElem = leftWrapper;
+ final Element leftElem = getElement(LEFT);
setWidth(leftElem, pos);
- setSplitPosition(getOffsetWidth(leftElem));
- }
-
- public void setWidth(String width) {
- super.setWidth(width);
- setSplitPosition(getOffsetWidth(leftWrapper));
+ impl.setSplitPosition(getOffsetWidth(leftElem));
}
protected void onLoad() {
- // If the split position has been changed while detached, apply the change
- impl.onAttach(this);
+ impl.onAttach();
/*
+ * If the split position has been changed while detached, apply the change.
* Set the position realizing that it might not work until after layout
* runs. This first call is simply to try to avoid a jitter effect if
* possible.
@@ -291,81 +331,40 @@
}
protected void onUnload() {
- impl.onDetach(this);
+ impl.onDetach();
}
final void onSplitterResize(int x, int y) {
- // Move the split position by the appropriate offset.
- setSplitPosition(initialLeftWidth + x - initialThumbPos);
+ impl.onSplitResize(initialLeftWidth + x - initialThumbPos);
}
final void onSplitterResizeStarted(int x, int y) {
initialThumbPos = x;
- initialLeftWidth = getClientWidth(leftWrapper);
+ initialLeftWidth = getOffsetWidth(getElement(LEFT));
}
- private void buildDOM() {
+ private void buildDOM(AbstractImagePrototype thumbImage) {
final Element leftDiv = getElement(LEFT);
final Element rightDiv = getElement(RIGHT);
final Element splitDiv = getSplitElement();
- DOM.appendChild(getElement(), rootWrapper);
+ DOM.appendChild(getElement(), container);
- DOM.appendChild(rootWrapper, leftWrapper);
- DOM.appendChild(rootWrapper, splitDiv);
- DOM.appendChild(rootWrapper, rightWrapper);
+ DOM.appendChild(container, leftDiv);
+ DOM.appendChild(container, splitDiv);
+ DOM.appendChild(container, rightDiv);
- DOM.appendChild(leftWrapper, leftDiv);
- DOM.appendChild(rightWrapper, rightDiv);
+ /*
+ * Sadly, this is the only way I've found to get vertical centering in this
+ * case. The usually CSS hacks (display: table-cell, vertical-align: middle)
+ * don't work in an absolute positioned DIV.
+ */
+ DOM.setInnerHTML(splitDiv,
+ "<table class='hsplitter' height='100%' cellpadding='0' "
+ + "cellspacing='0'><tr><td align='center' valign='middle'>"
+ + thumbImage.getHTML());
- DOM.setInnerHTML(splitDiv, " ");
-
- addElementScrolling(leftDiv);
- addElementScrolling(rightDiv);
-
- setElementClassname(leftDiv, "left");
- setElementClassname(splitDiv, "hsplitter");
- setElementClassname(rightDiv, "right");
- }
-
- private final void setSplitPosition(int px) {
- final Element splitElem = getSplitElement();
-
- final int rootElemWidth = getOffsetWidth(rootWrapper);
- final int splitElemWidth = getOffsetWidth(splitElem);
-
- // This represents an invalid state where layout is incomplete. This
- // typically happens before DOM attachment, but I leave it here as a
- // precaution because negative width/height style attributes produce
- // errors on IE.
- if (rootElemWidth < splitElemWidth) {
- return;
- }
-
- // Compute the new right side width.
- int newRightWidth = rootElemWidth - px - splitElemWidth;
-
- // Constrain the dragging to the physical size of the panel.
- if (px < 0) {
- px = 0;
- newRightWidth = rootElemWidth - splitElemWidth;
- } else if (newRightWidth < 0) {
- px = rootElemWidth - splitElemWidth;
- newRightWidth = 0;
- }
-
- final Element rightElem = rightWrapper;
-
- // Set the width of the left side.
- setWidth(leftWrapper, px + "px");
-
- // Move the splitter to the right edge of the left element.
- setLeft(splitElem, px + "px");
-
- // Move the right element to the right of the splitter.
- setLeft(rightElem, (px + splitElemWidth) + "px");
-
- // Update the right element's width.
- setWidth(rightElem, newRightWidth + "px");
+ addScrolling(leftDiv);
+ addScrolling(rightDiv);
}
}
diff --git a/user/src/com/google/gwt/user/client/ui/HorizontalSplitPanelImages.java b/user/src/com/google/gwt/user/client/ui/HorizontalSplitPanelImages.java
new file mode 100644
index 0000000..dd2b959
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/HorizontalSplitPanelImages.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+/**
+ * An {@link ImageBundle} that provides images for
+ * {@link com.google.gwt.user.client.ui.HorizontalSplitPanel}.
+ */
+public interface HorizontalSplitPanelImages extends ImageBundle {
+
+ /**
+ * An image representing the drag thumb.
+ *
+ * @gwt.resource splitPanelThumb.png
+ */
+ AbstractImagePrototype horizontalSplitPanelThumb();
+}
diff --git a/user/src/com/google/gwt/user/client/ui/SplitPanel.java b/user/src/com/google/gwt/user/client/ui/SplitPanel.java
index 99f954f..bc30e92 100644
--- a/user/src/com/google/gwt/user/client/ui/SplitPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/SplitPanel.java
@@ -28,11 +28,20 @@
abstract class SplitPanel extends Panel {
/**
+ * Sets an elements positioning to absolute.
+ *
+ * @param elem the element
+ */
+ static void addAbsolutePositoning(Element elem) {
+ DOM.setStyleAttribute(elem, "position", "absolute");
+ }
+
+ /**
* Adds clipping to an element.
*
* @param elem the element
*/
- static final void addElementClipping(final Element elem) {
+ static final void addClipping(final Element elem) {
DOM.setStyleAttribute(elem, "overflow", "hidden");
}
@@ -41,19 +50,74 @@
*
* @param elem the element
*/
- static final void addElementScrolling(final Element elem) {
+ static final void addScrolling(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
+ * Sizes and element to consume the full area of its parent using the CSS
+ * properties left, right, top, and bottom. This method is used for all
+ * browsers except IE6/7.
+ *
+ * @param elem the element
+ */
+ static final void expandToFitParentUsingCssOffsets(Element elem) {
+ final String zeroSize = "0px";
+
+ addAbsolutePositoning(elem);
+ setLeft(elem, zeroSize);
+ setRight(elem, zeroSize);
+ setTop(elem, zeroSize);
+ setBottom(elem, zeroSize);
+ }
+
+ /**
+ * Sizes an element to consume the full areas of its parent using 100% width
+ * and height. This method is used on IE6/7 where CSS offsets don't work
+ * reliably.
+ *
+ * @param elem the element
+ */
+ static final void expandToFitParentUsingPercentages(Element elem) {
+ final String zeroSize = "0px";
+ final String fullSize = "100%";
+
+ addAbsolutePositoning(elem);
+ setTop(elem, zeroSize);
+ setLeft(elem, zeroSize);
+ setWidth(elem, fullSize);
+ setHeight(elem, fullSize);
+ }
+
+ /**
+ * Returns the offsetHeight element property.
+ *
+ * @param elem the element
+ * @return the offsetHeight property
+ */
+ static final int getOffsetHeight(Element elem) {
+ return DOM.getElementPropertyInt(elem, "offsetHeight");
+ }
+
+ /**
+ * Returns the offsetWidth element property.
+ *
+ * @param elem the element
+ * @return the offsetWidth property
+ */
+ static final int getOffsetWidth(Element elem) {
+ return DOM.getElementPropertyInt(elem, "offsetWidth");
+ }
+
+ /**
+ * Adds zero or none CSS values for padding, margin and border to prevent
+ * stylesheet overrides. Returns the element for convenience to support
* builder pattern.
*
* @param elem the element
* @return the element
*/
- static final Element preventElementBoxStyles(final Element elem) {
+ static final Element preventBoxStyles(final Element elem) {
DOM.setIntStyleAttribute(elem, "padding", 0);
DOM.setIntStyleAttribute(elem, "margin", 0);
DOM.setStyleAttribute(elem, "border", "none");
@@ -61,12 +125,13 @@
}
/**
- * Adds zero size padding to an element.
+ * Convenience method to set bottom offset of an element.
*
- * @param elem the element.
+ * @param elem the element
+ * @param size a CSS length value for bottom
*/
- static final void preventElementPadding(final Element elem) {
- DOM.setStyleAttribute(elem, "padding", "0");
+ static void setBottom(Element elem, String size) {
+ DOM.setStyleAttribute(elem, "bottom", size);
}
/**
@@ -75,11 +140,60 @@
* @param elem the element
* @param className the class name
*/
- static final void setElementClassname(final Element elem,
- final String className) {
+ static final void setClassname(final Element elem, final String className) {
DOM.setElementProperty(elem, "className", className);
}
+ /**
+ * Convenience method to set the height of an element.
+ *
+ * @param elem the element
+ * @param height a CSS length value for the height
+ */
+ static final void setHeight(Element elem, String height) {
+ DOM.setStyleAttribute(elem, "height", height);
+ }
+
+ /**
+ * Convenience method to set the left offset of an element.
+ *
+ * @param elem the element
+ * @param height a CSS length value for left
+ */
+ static final void setLeft(Element elem, String left) {
+ DOM.setStyleAttribute(elem, "left", left);
+ }
+
+ /**
+ * Convenience method to set the right offset of an element.
+ *
+ * @param elem the element
+ * @param height a CSS length value for right
+ */
+ static final void setRight(Element elem, String right) {
+ DOM.setStyleAttribute(elem, "right", right);
+ }
+
+ /**
+ * Convenience method to set the top offset of an element.
+ *
+ * @param elem the element
+ * @param height a CSS length value for top
+ */
+ static final void setTop(Element elem, String top) {
+ DOM.setStyleAttribute(elem, "top", top);
+ }
+
+ /**
+ * Convenience method to set the width of an element.
+ *
+ * @param elem the element
+ * @param height a CSS length value for the width
+ */
+ static final void setWidth(Element elem, String width) {
+ DOM.setStyleAttribute(elem, "width", width);
+ }
+
// The enclosed widgets.
private final Widget[] widgets = new Widget[2];
@@ -181,7 +295,7 @@
* Moves the position of the splitter.
*
* @param size the new size of the left region in CSS units (e.g. "10px",
- * "1em")
+ * "1em")
*/
public abstract void setSplitPosition(String size);
diff --git a/user/src/com/google/gwt/user/client/ui/VerticalSplitPanel.java b/user/src/com/google/gwt/user/client/ui/VerticalSplitPanel.java
index c8679e6..0b037aa 100644
--- a/user/src/com/google/gwt/user/client/ui/VerticalSplitPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/VerticalSplitPanel.java
@@ -1,12 +1,12 @@
/*
* 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
@@ -20,128 +20,260 @@
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Timer;
/**
* 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.
- *
+ * scrollbars when necessary.
+ *
* <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 vsplitter { the splitter }</li>
* </ul>
*/
public final class VerticalSplitPanel extends SplitPanel {
/**
- * Provides different implementations for retrieving an element's height. The
- * default binding is based on DOM1 clientHeight.
+ * Provides a base implementation for splitter layout that relies on CSS
+ * positioned layout.
*/
private static class Impl {
- /**
- * Gets an element's height.
- *
- * @param elem an element
- * @return the height of the element
- */
- protected int getElementHeight(Element elem) {
- return DOM.getElementPropertyInt(elem, "clientHeight");
+ private static void expandToFitParentHorizontally(Element elem) {
+ addAbsolutePositoning(elem);
+ DOM.setStyleAttribute(elem, "left", "0");
+ DOM.setStyleAttribute(elem, "right", "0");
+ }
+
+ protected VerticalSplitPanel panel;
+
+ public void init(VerticalSplitPanel panel) {
+ this.panel = panel;
+
+ DOM.setStyleAttribute(panel.getElement(), "position", "relative");
+
+ final Element topElem = panel.getElement(TOP);
+ final Element bottomElem = panel.getElement(BOTTOM);
+
+ expandToFitParentHorizontally(topElem);
+ expandToFitParentHorizontally(bottomElem);
+ expandToFitParentHorizontally(panel.getSplitElement());
+
+ expandToFitParentUsingCssOffsets(panel.container);
+
+ // Snap the bottom wrapper to the bottom side.
+ DOM.setStyleAttribute(bottomElem, "bottom", "0");
+ }
+
+ public void onAttach() {
+ }
+
+ public void onDetach() {
+ }
+
+ public void onSplitterResize(int px) {
+ setSplitPosition(px);
+ }
+
+ public void setSplitPosition(int px) {
+ final Element splitElem = panel.getSplitElement();
+
+ final int rootElemHeight = getOffsetHeight(panel.container);
+ final int splitElemHeight = getOffsetHeight(splitElem);
+
+ if (rootElemHeight < splitElemHeight) {
+ return;
+ }
+
+ int newBottomHeight = rootElemHeight - px - splitElemHeight;
+ if (px < 0) {
+ px = 0;
+ newBottomHeight = rootElemHeight - splitElemHeight;
+ } else if (newBottomHeight < 0) {
+ px = rootElemHeight - splitElemHeight;
+ newBottomHeight = 0;
+ }
+
+ updateElements(panel.getElement(TOP), splitElem,
+ panel.getElement(BOTTOM), px, px + splitElemHeight, newBottomHeight);
+ }
+
+ protected void updateElements(Element topElem, Element splitElem,
+ Element bottomElem, int topHeight, int bottomTop, int bottomHeight) {
+ setHeight(topElem, topHeight + "px");
+
+ setTop(splitElem, topHeight + "px");
+
+ setTop(bottomElem, bottomTop + "px");
+
+ // bottom's height is handled by CSS.
}
}
/**
- * Provides an implementation for IE6 based on Element.getBoundingClientRect.
+ * Provides an implementation for IE6/7 that relies on 100% length in CSS.
*/
private static class ImplIE6 extends Impl {
- protected native int getElementHeight(Element elem) /*-{
- var box = elem.getBoundingClientRect();
- return box.bottom - box.top;
- }-*/;
+
+ private static void expandToFitParentHorizontally(Element elem) {
+ addAbsolutePositoning(elem);
+ setLeft(elem, "0");
+ setWidth(elem, "100%");
+ }
+
+ private boolean isResizeInProgress = false;
+
+ private int splitPosition;
+
+ private boolean isTopHidden = false, isBottomHidden = false;
+
+ public void init(VerticalSplitPanel panel) {
+ this.panel = panel;
+
+ final Element elem = panel.getElement();
+
+ // Prevents inherited text-align settings from interfering with the
+ // panel's layout.
+ DOM.setStyleAttribute(elem, "textAlign", "left");
+ DOM.setStyleAttribute(elem, "position", "relative");
+
+ final Element topElem = panel.getElement(TOP);
+ final Element bottomElem = panel.getElement(BOTTOM);
+
+ expandToFitParentHorizontally(topElem);
+ expandToFitParentHorizontally(bottomElem);
+ expandToFitParentHorizontally(panel.getSplitElement());
+
+ expandToFitParentUsingPercentages(panel.container);
+ }
+
+ public void onAttach() {
+ addResizeListener(panel.container);
+ onResize();
+ }
+
+ public void onDetach() {
+ DOM.setElementProperty(panel.container, "onresize", null);
+ }
+
+ public void onSplitterResize(int px) {
+ /*
+ * IE6/7 has event priority issues that will prevent the repaints from
+ * happening quickly enough causing the interaction to seem unresponsive.
+ * The following is simply a poor man's mouse event coalescing.
+ */
+ final int resizeUpdatePeriod = 20; // ms
+ if (!isResizeInProgress) {
+ isResizeInProgress = true;
+ new Timer() {
+ public void run() {
+ setSplitPosition(splitPosition);
+ isResizeInProgress = false;
+ }
+ }.schedule(resizeUpdatePeriod);
+ }
+ splitPosition = px;
+ }
+
+ protected void updateElements(Element topElem, Element splitElem,
+ Element bottomElem, int topHeight, int bottomTop, int bottomHeight) {
+ /*
+ * IE6/7 has a quirk where a zero height element with non-zero height
+ * children will expand larger than 100%. To prevent this, the width is
+ * explicitly set to zero when height is zero.
+ */
+ if (topHeight == 0) {
+ setWidth(topElem, "0px");
+ isTopHidden = true;
+ } else if (isTopHidden) {
+ setWidth(topElem, "100%");
+ isTopHidden = false;
+ }
+
+ if (bottomHeight == 0) {
+ setWidth(bottomElem, "0px");
+ isBottomHidden = true;
+ } else if (isBottomHidden) {
+ setWidth(bottomElem, "100%");
+ isBottomHidden = false;
+ }
+
+ super.updateElements(topElem, splitElem, bottomElem, topHeight,
+ bottomTop, bottomHeight);
+
+ // IE6/7 cannot update properly with CSS alone.
+ setHeight(bottomElem, bottomHeight + "px");
+ }
+
+ private native void addResizeListener(Element container) /*-{
+ var self = this;
+ container.onresize = function() {
+ self.@com.google.gwt.user.client.ui.VerticalSplitPanel$ImplIE6::onResize()();
+ };
+ }-*/;
+
+ private void onResize() {
+ setSplitPosition(getOffsetHeight(panel.getElement(TOP)));
+ }
}
+ /**
+ * Constant makes for readable calls to {@link #getElement(int)} and
+ * {@link #getWidget(int)}.
+ */
private static final int TOP = 0;
+ /**
+ * Constant makes for readable calls to {@link #getElement(int)} and
+ * {@link #getWidget(int)}.
+ */
private static final int BOTTOM = 1;
- private static final Impl impl = (Impl) GWT.create(Impl.class);
-
- private static int getOffsetTop(Element elem) {
- return DOM.getElementPropertyInt(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;
+ // A style-free element to serve as the root container.
+ private final Element container;
+
+ private final Impl impl = (Impl) GWT.create(Impl.class);
+
+ private String lastSplitPosition;
+
+ public VerticalSplitPanel() {
+ this((VerticalSplitPanelImages) GWT.create(VerticalSplitPanelImages.class));
+ }
+
/**
* Creates an empty vertical split panel.
*/
- public VerticalSplitPanel() {
- super(DOM.createDiv(), DOM.createDiv(), DOM.createDiv(), DOM.createDiv());
+ public VerticalSplitPanel(VerticalSplitPanelImages images) {
+ super(DOM.createDiv(), DOM.createDiv(), preventBoxStyles(DOM.createDiv()),
+ preventBoxStyles(DOM.createDiv()));
- final Element thisElem = getElement();
- final Element splitElem = getSplitElement();
- final Element topElem = getElement(TOP);
- final Element bottomElem = getElement(BOTTOM);
- probeElem = lockStyles(DOM.createDiv());
+ container = preventBoxStyles(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, "vsplitter");
- setElementClassname(bottomElem, "bottom");
+ buildDOM(images.verticalSplitPanelThumb());
setStyleName("gwt-VerticalSplitPanel");
- // Must wait on layout to do the initial layout.
- DeferredCommand.addCommand(new Command() {
- public void execute() {
- updateBottomHeight();
- }
- });
+ impl.init(this);
setSplitPosition("50%");
}
/**
* Gets the widget in the bottom of the panel.
- *
+ *
* @return the widget, <code>null</code> if there is not one
*/
public final Widget getBottomWidget() {
@@ -150,7 +282,7 @@
/**
* Gets the widget in the top of the panel.
- *
+ *
* @return the widget, <code>null</code> if there is not one
*/
public final Widget getTopWidget() {
@@ -159,7 +291,7 @@
/**
* Sets the widget in the bottom of the panel.
- *
+ *
* @param w the widget
*/
public final void setBottomWidget(Widget w) {
@@ -168,101 +300,71 @@
public void setHeight(String height) {
super.setHeight(height);
- updateBottomHeight();
}
- public final void setSplitPosition(String size) {
- DOM.setStyleAttribute(getElement(TOP), "height", size);
- updateBottomHeight();
+ public final void setSplitPosition(String pos) {
+ lastSplitPosition = pos;
+ final Element topElem = getElement(TOP);
+ setHeight(topElem, pos);
+ impl.setSplitPosition(getOffsetHeight(topElem));
}
/**
* Sets the widget in the top of the panel.
- *
+ *
* @param w the widget
*/
public final void setTopWidget(Widget w) {
setWidget(TOP, w);
}
+ protected void onLoad() {
+ impl.onAttach();
+
+ /*
+ * Set the position realizing it might not work until after layout runs.
+ * This first call is simply to try to avoid a jitter effect if possible.
+ */
+ setSplitPosition(lastSplitPosition);
+ DeferredCommand.addCommand(new Command() {
+ public void execute() {
+ setSplitPosition(lastSplitPosition);
+ }
+ });
+ }
+
+ protected void onUnload() {
+ impl.onDetach();
+ }
+
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 = impl.getElementHeight(botElem)
- + impl.getElementHeight(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();
+ impl.onSplitterResize(initialTopHeight + y - initialThumbPos);
}
final void onSplitterResizeStarted(int x, int y) {
initialThumbPos = y;
- initialTopHeight = impl.getElementHeight(getElement(TOP));
+ initialTopHeight = getOffsetHeight(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);
+ private void buildDOM(AbstractImagePrototype thumb) {
+ final Element topDiv = getElement(TOP);
+ final Element bottomDiv = getElement(BOTTOM);
+ final Element splitDiv = getSplitElement();
+
+ DOM.appendChild(getElement(), container);
+
+ DOM.appendChild(container, topDiv);
+ DOM.appendChild(container, splitDiv);
+ DOM.appendChild(container, bottomDiv);
/*
- * 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.
+ * The style name is placed on the table rather than splitElem to allow the
+ * splitter to be styled without interfering with layout.
*/
- final int adjust = impl.getElementHeight(thisElem)
- - (getOffsetTop(probeElem) - getOffsetTop(thisElem));
+ DOM.setInnerHTML(splitDiv, "<div class='vsplitter' "
+ + "style='text-align:center;'>" + thumb.getHTML() + "</div>");
- /*
- * 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 = impl.getElementHeight(bottomElem);
- final int newHeight = curHeight + adjust;
- setHeight(bottomElem, newHeight);
- final int error = impl.getElementHeight(bottomElem) - newHeight;
-
- if (error == 0) {
- return;
- }
-
- setHeight(bottomElem, newHeight - error);
+ addScrolling(topDiv);
+ addScrolling(bottomDiv);
}
}
diff --git a/user/src/com/google/gwt/user/client/ui/VerticalSplitPanelImages.java b/user/src/com/google/gwt/user/client/ui/VerticalSplitPanelImages.java
new file mode 100644
index 0000000..409b6d3
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/VerticalSplitPanelImages.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+/**
+ * An {@link ImageBundle} that provides images for
+ * {@link com.google.gwt.user.client.ui.HorizontalSplitPanel}.
+ */
+public interface VerticalSplitPanelImages extends ImageBundle {
+
+ /**
+ * An image representing the drag thumb.
+ *
+ * @gwt.resource splitPanelThumb.png
+ */
+ AbstractImagePrototype verticalSplitPanelThumb();
+}
diff --git a/user/src/com/google/gwt/user/client/ui/splitPanelThumb.png b/user/src/com/google/gwt/user/client/ui/splitPanelThumb.png
new file mode 100644
index 0000000..d2ef025
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/splitPanelThumb.png
Binary files differ