001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.user.client.ui;
017:
018: import com.google.gwt.core.client.GWT;
019: import com.google.gwt.user.client.Command;
020: import com.google.gwt.user.client.DOM;
021: import com.google.gwt.user.client.DeferredCommand;
022: import com.google.gwt.user.client.Element;
023: import com.google.gwt.user.client.Timer;
024:
025: /**
026: * A panel that arranges two widgets in a single horizontal row and allows the
027: * user to interactively change the proportion of the width dedicated to each of
028: * the two widgets. Widgets contained within a <code>HorizontalSplitPanel</code>
029: * will be automatically decorated with scrollbars when necessary.
030: *
031: * <p>
032: * <img class='gallery' src='HorizontalSplitPanel.png'/>
033: * </p>
034: *
035: * <h3>CSS Style Rules</h3>
036: * <ul class='css'>
037: * <li>.gwt-HorizontalSplitPanel { the panel itself }</li>
038: * <li>.gwt-HorizontalSplitPanel hsplitter { the splitter }</li>
039: * </ul>
040: */
041: public final class HorizontalSplitPanel extends SplitPanel {
042:
043: /**
044: * The standard implementation for horizontal split panels.
045: */
046: private static class Impl {
047: private static void expandToFitParentHorizontally(Element elem) {
048: addAbsolutePositoning(elem);
049: final String zeroSize = "0px";
050: setTop(elem, zeroSize);
051: setBottom(elem, zeroSize);
052: }
053:
054: protected HorizontalSplitPanel panel;
055:
056: public void init(HorizontalSplitPanel panel) {
057: this .panel = panel;
058:
059: DOM.setStyleAttribute(panel.getElement(), "position",
060: "relative");
061:
062: final Element rightElem = panel.getElement(RIGHT);
063:
064: expandToFitParentHorizontally(panel.getElement(LEFT));
065: expandToFitParentHorizontally(rightElem);
066: expandToFitParentHorizontally(panel.getSplitElement());
067:
068: expandToFitParentUsingCssOffsets(panel.container);
069:
070: // Snap the right wrapper to the right side.
071: setRight(rightElem, "0px");
072: }
073:
074: public void onAttach() {
075: }
076:
077: public void onDetach() {
078: }
079:
080: public void onSplitResize(int px) {
081: setSplitPosition(px);
082: }
083:
084: public void setSplitPosition(int px) {
085: final Element splitElem = panel.getSplitElement();
086:
087: final int rootElemWidth = getOffsetWidth(panel.container);
088: final int splitElemWidth = getOffsetWidth(splitElem);
089:
090: // This represents an invalid state where layout is incomplete. This
091: // typically happens before DOM attachment, but I leave it here as a
092: // precaution because negative width/height style attributes produce
093: // errors on IE.
094: if (rootElemWidth < splitElemWidth) {
095: return;
096: }
097:
098: // Compute the new right side width.
099: int newRightWidth = rootElemWidth - px - splitElemWidth;
100:
101: // Constrain the dragging to the physical size of the panel.
102: if (px < 0) {
103: px = 0;
104: newRightWidth = rootElemWidth - splitElemWidth;
105: } else if (newRightWidth < 0) {
106: px = rootElemWidth - splitElemWidth;
107: newRightWidth = 0;
108: }
109:
110: final Element rightElem = panel.getElement(RIGHT);
111:
112: // Set the width of the left side.
113: setWidth(panel.getElement(LEFT), px + "px");
114:
115: // Move the splitter to the right edge of the left element.
116: setLeft(splitElem, px + "px");
117:
118: // Move the right element to the right of the splitter.
119: setLeft(rightElem, (px + splitElemWidth) + "px");
120:
121: updateRightWidth(rightElem, newRightWidth);
122: }
123:
124: public void updateRightWidth(Element rightElem,
125: int newRightWidth) {
126: // Update is handled by CSS.
127: }
128: }
129:
130: /**
131: * The IE6 implementation for horizontal split panels.
132: */
133: private static class ImplIE6 extends Impl {
134:
135: private boolean isResizeInProgress = false;
136:
137: private int splitPosition = 0;
138:
139: @Override
140: public void init(HorizontalSplitPanel panel) {
141: this .panel = panel;
142:
143: final Element elem = panel.getElement();
144: // Prevents inherited text-align settings from interfering with the
145: // panel's layout.
146: DOM.setStyleAttribute(elem, "textAlign", "left");
147: DOM.setStyleAttribute(elem, "position", "relative");
148:
149: /*
150: * Technically, these are snapped to the top and bottom, but IE doesn't
151: * provide a reliable way to make that happen, so a resize listener is
152: * wired up to control the height of these elements.
153: */
154: addAbsolutePositoning(panel.getElement(LEFT));
155: addAbsolutePositoning(panel.getElement(RIGHT));
156: addAbsolutePositoning(panel.getSplitElement());
157:
158: expandToFitParentUsingPercentages(panel.container);
159: }
160:
161: @Override
162: public void onAttach() {
163: addResizeListener(panel.container);
164: onResize();
165: }
166:
167: @Override
168: public void onDetach() {
169: DOM.setElementAttribute(panel.container, "onresize", null);
170: }
171:
172: @Override
173: public void onSplitResize(int px) {
174: final int resizeUpdatePeriod = 20; // ms
175: if (!isResizeInProgress) {
176: isResizeInProgress = true;
177: new Timer() {
178: @Override
179: public void run() {
180: setSplitPosition(splitPosition);
181: isResizeInProgress = false;
182: }
183: }.schedule(resizeUpdatePeriod);
184: }
185: splitPosition = px;
186: }
187:
188: @Override
189: public void updateRightWidth(Element rightElem,
190: int newRightWidth) {
191: setWidth(rightElem, newRightWidth + "px");
192: }
193:
194: private native void addResizeListener(Element container) /*-{
195: var self = this;
196: container.onresize = function() {
197: self.@com.google.gwt.user.client.ui.HorizontalSplitPanel$ImplIE6::onResize()();
198: };
199: }-*/;
200:
201: private void onResize() {
202: final Element leftElem = panel.getElement(LEFT);
203: final Element rightElem = panel.getElement(RIGHT);
204:
205: final String height = getOffsetHeight(panel.container)
206: + "px";
207: setHeight(rightElem, height);
208: setHeight(panel.getSplitElement(), height);
209: setHeight(leftElem, height);
210: setSplitPosition(getOffsetWidth(leftElem));
211: }
212: }
213:
214: /**
215: * The Safari implementation which owes its existence entirely to a single
216: * WebKit bug: http://bugs.webkit.org/show_bug.cgi?id=9137.
217: */
218: private static class ImplSafari extends Impl {
219: @Override
220: public void init(HorizontalSplitPanel panel) {
221: this .panel = panel;
222: final String fullSize = "100%";
223: super .init(panel);
224: setHeight(panel.container, fullSize);
225: setHeight(panel.getElement(LEFT), fullSize);
226: setHeight(panel.getElement(RIGHT), fullSize);
227: setHeight(panel.getSplitElement(), fullSize);
228: }
229: }
230:
231: /**
232: * Constant makes for readable calls to {@link #getElement(int)} and
233: * {@link #getWidget(int)}.
234: */
235: private static final int LEFT = 0;
236:
237: /**
238: * Constant makes for readable calls to {@link #getElement(int)} and
239: * {@link #getWidget(int)}.
240: */
241: private static final int RIGHT = 1;
242:
243: // A style-free element to serve as the root container.
244: private final Element container;
245:
246: private final Impl impl = GWT.create(Impl.class);
247:
248: /**
249: * If the split position is set while the split panel is not attached, save it
250: * here to be applied when the panel is attached to the document.
251: */
252: private String lastSplitPosition = "50%";
253:
254: private int initialThumbPos;
255:
256: private int initialLeftWidth;
257:
258: public HorizontalSplitPanel() {
259: this (
260: GWT
261: .<HorizontalSplitPanelImages> create(HorizontalSplitPanelImages.class));
262: }
263:
264: /**
265: * Creates an empty horizontal split panel.
266: */
267: public HorizontalSplitPanel(HorizontalSplitPanelImages images) {
268: super (DOM.createDiv(), DOM.createDiv(), preventBoxStyles(DOM
269: .createDiv()), preventBoxStyles(DOM.createDiv()));
270:
271: container = preventBoxStyles(DOM.createDiv());
272:
273: buildDOM(images.horizontalSplitPanelThumb());
274:
275: setStyleName("gwt-HorizontalSplitPanel");
276:
277: impl.init(this );
278:
279: // By default, the panel will fill its parent vertically and horizontally.
280: // The horizontal case is covered by the fact that the top level div is
281: // block display.
282: setHeight("100%");
283: }
284:
285: /**
286: * Gets the widget in the left side of the panel.
287: *
288: * @return the widget, <code>null</code> if there is not one.
289: */
290: public final Widget getLeftWidget() {
291: return getWidget(LEFT);
292: }
293:
294: /**
295: * Gets the widget in the right side of the panel.
296: *
297: * @return the widget, <code>null</code> if there is not one.
298: */
299: public final Widget getRightWidget() {
300: return getWidget(RIGHT);
301: }
302:
303: /**
304: * Sets the widget in the left side of the panel.
305: *
306: * @param w the widget
307: */
308: public final void setLeftWidget(Widget w) {
309: setWidget(LEFT, w);
310: }
311:
312: /**
313: * Sets the widget in the right side of the panel.
314: *
315: * @param w the widget
316: */
317: public final void setRightWidget(Widget w) {
318: setWidget(RIGHT, w);
319: }
320:
321: @Override
322: public final void setSplitPosition(String pos) {
323: lastSplitPosition = pos;
324: final Element leftElem = getElement(LEFT);
325: setWidth(leftElem, pos);
326: impl.setSplitPosition(getOffsetWidth(leftElem));
327: }
328:
329: @Override
330: protected void onLoad() {
331: impl.onAttach();
332:
333: /*
334: * If the split position has been changed while detached, apply the change.
335: * Set the position realizing that it might not work until after layout
336: * runs. This first call is simply to try to avoid a jitter effect if
337: * possible.
338: */
339: setSplitPosition(lastSplitPosition);
340: DeferredCommand.addCommand(new Command() {
341: public void execute() {
342: setSplitPosition(lastSplitPosition);
343: }
344: });
345: }
346:
347: @Override
348: protected void onUnload() {
349: impl.onDetach();
350: }
351:
352: @Override
353: final void onSplitterResize(int x, int y) {
354: impl.onSplitResize(initialLeftWidth + x - initialThumbPos);
355: }
356:
357: @Override
358: final void onSplitterResizeStarted(int x, int y) {
359: initialThumbPos = x;
360: initialLeftWidth = getOffsetWidth(getElement(LEFT));
361: }
362:
363: private void buildDOM(AbstractImagePrototype thumbImage) {
364: final Element leftDiv = getElement(LEFT);
365: final Element rightDiv = getElement(RIGHT);
366: final Element splitDiv = getSplitElement();
367:
368: DOM.appendChild(getElement(), container);
369:
370: DOM.appendChild(container, leftDiv);
371: DOM.appendChild(container, splitDiv);
372: DOM.appendChild(container, rightDiv);
373:
374: /*
375: * Sadly, this is the only way I've found to get vertical centering in this
376: * case. The usually CSS hacks (display: table-cell, vertical-align: middle)
377: * don't work in an absolute positioned DIV.
378: */
379: DOM
380: .setInnerHTML(
381: splitDiv,
382: "<table class='hsplitter' height='100%' cellpadding='0' "
383: + "cellspacing='0'><tr><td align='center' valign='middle'>"
384: + thumbImage.getHTML());
385:
386: addScrolling(leftDiv);
387: addScrolling(rightDiv);
388: }
389: }
|