com.google.gwt.user.client.ui.HorizontalSplitPanel.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.user.client.ui.HorizontalSplitPanel.java

Source

/*
 * Copyright 2008 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.i18n.client.LocaleInfo;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.ImageResource;
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;
import com.google.gwt.user.client.Timer;

/**
 * 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>
 * This widget will <em>only</em> work in quirks mode. If your application is in
 * Standards Mode, use {@link SplitLayoutPanel} instead.
 * </p>
 * 
 * <p>
 * <img class='gallery' src='doc-files/HorizontalSplitPanel.png'/>
 * </p>
 * 
 * <h3>CSS Style Rules</h3>
 * <ul>
 * <li>.gwt-HorizontalSplitPanel { the panel itself }</li>
 * <li>.gwt-HorizontalSplitPanel hsplitter { the splitter }</li>
 * </ul>
 * 
 * @deprecated Use {@link SplitLayoutPanel} instead, but understand that it is
 *             not a drop in replacement for this class. It requires standards
 *             mode, and is most easily used under a {@link RootLayoutPanel} (as
 *             opposed to a {@link RootPanel}
 *
 * @see SplitLayoutPanel
 */
@Deprecated
public final class HorizontalSplitPanel extends SplitPanel {
    /**
     * The default resources used by this widget.
     */
    public interface Resources extends ClientBundle {
        /**
         * An image representing the drag thumb.
         */
        @Source("splitPanelThumb.png")
        ImageResource horizontalSplitPanelThumb();
    }

    /**
     * The standard implementation for horizontal split panels.
     */
    private static class Impl {
        private static void expandToFitParentHorizontally(Element elem) {
            addAbsolutePositoning(elem);
            final String zeroSize = "0px";
            setTop(elem, zeroSize);
            setBottom(elem, zeroSize);
        }

        protected HorizontalSplitPanel panel;

        public void init(HorizontalSplitPanel panel) {
            this.panel = panel;

            DOM.setStyleAttribute(panel.getElement(), "position", "relative");

            expandToFitParentHorizontally(panel.getElement(LEFT));
            expandToFitParentHorizontally(panel.getElement(RIGHT));
            expandToFitParentHorizontally(panel.getSplitElement());

            expandToFitParentUsingCssOffsets(panel.container);

            // Right now, both panes are stacked on top of each other
            // on either the left side or the right side of the containing
            // panel. This happens because both panes have position:absolute
            // and no left/top values. The panes will be on the left side
            // if the directionality is LTR, and on the right side if the
            // directionality is RTL. In the LTR case, we need to snap the
            // right pane to the right of the container, and in the RTL case,
            // we need to snap the left pane to the left of the container.
            if (LocaleInfo.getCurrentLocale().isRTL()) {
                setLeft(panel.getElement(LEFT), "0px");
            } else {
                setRight(panel.getElement(RIGHT), "0px");
            }
        }

        public void onAttach() {
        }

        public void onDetach() {
        }

        public void onSplitResize(int px) {
            setSplitPositionUsingPixels(px);
        }

        public void setSplitPosition(String pos) {
            final Element leftElem = panel.getElement(LEFT);
            setWidth(leftElem, pos);
            setSplitPositionUsingPixels(getOffsetWidth(leftElem));
        }

        /**
         * Set the splitter's position in units of pixels.
         * 
         * px represents the splitter's position as a distance of px pixels from the
         * left edge of the container. This is true even in a bidi environment.
         * Callers of this method must be aware of this constraint.
         */
        public void setSplitPositionUsingPixels(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);
        }

        /**
         * Implemented by subclasses.
         * 
         * @param rightElem
         * @param newRightWidth
         */
        public void updateRightWidth(Element rightElem, int newRightWidth) {
            // No need to update the width of the right side; this will be
            // recomputed automatically by CSS. This is helpful, as we do not
            // have to worry about watching for resize events and adjusting the
            // right-side width.
        }
    }

    /**
     * The IE6 implementation for horizontal split panels.
     */
    @SuppressWarnings("unused")
    // will be used by IE6 permutation
    private static class ImplIE6 extends Impl {

        private boolean isResizeInProgress = false;

        private int splitPosition = 0;

        @Override
        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. The setting we choose must be bidi-sensitive,
            // as left-alignment is the default with LTR directionality, and
            // right-alignment is the default with RTL directionality.
            if (LocaleInfo.getCurrentLocale().isRTL()) {
                DOM.setStyleAttribute(elem, "textAlign", "right");
            } else {
                DOM.setStyleAttribute(elem, "textAlign", "left");
            }

            DOM.setStyleAttribute(elem, "position", "relative");

            /*
             * 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());

            expandToFitParentUsingPercentages(panel.container);

            if (LocaleInfo.getCurrentLocale().isRTL()) {
                // Snap the left pane to the left edge of the container. We
                // only need to do this when layout is RTL; if we don't, the
                // left pane will overlap the right pane.
                setLeft(panel.getElement(LEFT), "0px");
            }
        }

        @Override
        public void onAttach() {
            addResizeListener(panel.container);
            onResize();
        }

        @Override
        public void onDetach() {
            DOM.setElementAttribute(panel.container, "onresize", null);
        }

        @Override
        public void onSplitResize(int px) {
            final int resizeUpdatePeriod = 20; // ms
            if (!isResizeInProgress) {
                isResizeInProgress = true;
                new Timer() {
                    @Override
                    public void run() {
                        setSplitPositionUsingPixels(splitPosition);
                        isResizeInProgress = false;
                    }
                }.schedule(resizeUpdatePeriod);
            }
            splitPosition = px;
        }

        @Override
        public void setSplitPositionUsingPixels(int px) {
            if (LocaleInfo.getCurrentLocale().isRTL()) {
                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;
                }

                // Set the width of the right side.
                setWidth(panel.getElement(RIGHT), newRightWidth + "px");

                // Move the splitter to the right edge of the left element.
                setLeft(splitElem, px + "px");

                // Update the width of the left side
                if (px == 0) {

                    // This takes care of a qurky RTL layout bug with IE6.
                    // During DOM construction and layout, onResize events
                    // are fired, and this method is called with px == 0.
                    // If one tries to set the width of the LEFT element to
                    // before layout completes, the RIGHT element will
                    // appear to be blanked out.

                    DeferredCommand.addCommand(new Command() {
                        public void execute() {
                            setWidth(panel.getElement(LEFT), "0px");
                        }
                    });
                } else {
                    setWidth(panel.getElement(LEFT), px + "px");
                }

            } else {
                super.setSplitPositionUsingPixels(px);
            }
        }

        @Override
        public void updateRightWidth(Element rightElem, int newRightWidth) {
            setWidth(rightElem, newRightWidth + "px");
        }

        private native void addResizeListener(Element container) /*-{
                                                                 var self = this;
                                                                 container.onresize = $entry(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);
            setSplitPositionUsingPixels(getOffsetWidth(leftElem));
        }
    }

    /**
     * The Safari implementation which owes its existence entirely to a single
     * WebKit bug: http://bugs.webkit.org/show_bug.cgi?id=9137.
     */
    @SuppressWarnings("unused")
    // will be used by Safari permutation
    private static class ImplSafari extends Impl {
        @Override
        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;

    // A style-free element to serve as the root container.
    private final Element container;

    private final Impl impl = GWT.create(Impl.class);

    /**
     * If the split position is set while the split panel is not attached, save it
     * here to be applied when the panel is attached to the document.
     */
    private String lastSplitPosition = "50%";

    private int initialThumbPos;

    private int initialLeftWidth;

    public HorizontalSplitPanel() {
        this(GWT.<Resources>create(Resources.class));
    }

    /**
     * Creates an empty horizontal split panel.
     * 
     * @param images ImageBundle containing an image for the splitter's drag thumb
     * @deprecated replaced by {@link #HorizontalSplitPanel(Resources)}
     */
    @Deprecated
    public HorizontalSplitPanel(HorizontalSplitPanelImages images) {
        this(images.horizontalSplitPanelThumb());
    }

    /**
     * Creates an empty horizontal split panel.
     * 
     * @param resources ClientBundle containing an image for the splitter's drag
     *          thumb
     */
    public HorizontalSplitPanel(Resources resources) {
        this(AbstractImagePrototype.create(resources.horizontalSplitPanelThumb()));
    }

    private HorizontalSplitPanel(AbstractImagePrototype thumbImage) {
        super(DOM.createDiv(), DOM.createDiv(), preventBoxStyles(DOM.createDiv()),
                preventBoxStyles(DOM.createDiv()));

        container = preventBoxStyles(DOM.createDiv());

        buildDOM(thumbImage);

        setStyleName("gwt-HorizontalSplitPanel");

        impl.init(this);

        // By default, the panel will fill its parent vertically and horizontally.
        // The horizontal case is covered by the fact that the top level div is
        // block display.
        setHeight("100%");
    }

    /**
     * Adds a widget to a pane in the HorizontalSplitPanel. The method will first
     * attempt to add the widget to the left pane. If a widget is already in that
     * position, it will attempt to add the widget to the right pane. If a widget
     * is already in that position, an exception will be thrown, as a
     * HorizontalSplitPanel can contain at most two widgets.
     * 
     * Note that this method is bidi-sensitive. In an RTL environment, this method
     * will first attempt to add the widget to the right pane, and if a widget is
     * already in that position, it will attempt to add the widget to the left
     * pane.
     * 
     * @param w the widget to be added
     * @throws IllegalStateException
     */
    @Override
    public void add(Widget w) {
        if (getStartOfLineWidget() == null) {
            setStartOfLineWidget(w);
        } else if (getEndOfLineWidget() == null) {
            setEndOfLineWidget(w);
        } else {
            throw new IllegalStateException("A Splitter can only contain two Widgets.");
        }
    }

    /**
     * Gets the widget in the pane that is at the end of the line direction for
     * the layout. That is, in an RTL layout, gets the widget in the left pane,
     * and in an LTR layout, gets the widget in the right pane.
     * 
     * @return the widget, <code>null</code> if there is not one.
     */
    public Widget getEndOfLineWidget() {
        return getWidget(getEndOfLinePos());
    }

    /**
     * Gets the widget in the left side of the panel.
     * 
     * @return the widget, <code>null</code> if there is not one.
     */
    public 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 Widget getRightWidget() {
        return getWidget(RIGHT);
    }

    /**
     * Gets the widget in the pane that is at the start of the line direction for
     * the layout. That is, in an RTL environment, gets the widget in the right
     * pane, and in an LTR environment, gets the widget in the left pane.
     * 
     * @return the widget, <code>null</code> if there is not one.
     */
    public Widget getStartOfLineWidget() {
        return getWidget(getStartOfLinePos());
    }

    /**
     * Sets the widget in the pane that is at the end of the line direction for
     * the layout. That is, in an RTL layout, sets the widget in the left pane,
     * and in and RTL layout, sets the widget in the right pane.
     * 
     * @param w the widget
     */
    public void setEndOfLineWidget(Widget w) {
        setWidget(getEndOfLinePos(), w);
    }

    /**
     * Sets the widget in the left side of the panel.
     * 
     * @param w the widget
     */
    public void setLeftWidget(Widget w) {
        setWidget(LEFT, w);
    }

    /**
     * Sets the widget in the right side of the panel.
     * 
     * @param w the widget
     */
    public void setRightWidget(Widget w) {
        setWidget(RIGHT, w);
    }

    /**
     * Moves the position of the splitter.
     * 
     * This method is not bidi-sensitive. The size specified is always the size of
     * the left region, regardless of directionality.
     * 
     * @param pos the new size of the left region in CSS units (e.g. "10px",
     *          "1em")
     */
    @Override
    public void setSplitPosition(String pos) {
        lastSplitPosition = pos;
        impl.setSplitPosition(pos);
    }

    /**
     * Sets the widget in the pane that is at the start of the line direction for
     * the layout. That is, in an RTL layout, sets the widget in the right pane,
     * and in and RTL layout, sets the widget in the left pane.
     * 
     * @param w the widget
     */
    public void setStartOfLineWidget(Widget w) {
        setWidget(getStartOfLinePos(), w);
    }

    /**
     * <b>Affected Elements:</b>
     * <ul>
     * <li>-splitter = the container containing the splitter element.</li>
     * <li>-right = the container on the right side of the splitter.</li>
     * <li>-left = the container on the left side of the splitter.</li>
     * </ul>
     * 
     * @see UIObject#onEnsureDebugId(String)
     */
    @Override
    protected void onEnsureDebugId(String baseID) {
        super.onEnsureDebugId(baseID);
        ensureDebugId(getElement(LEFT), baseID, "left");
        ensureDebugId(getElement(RIGHT), baseID, "right");
    }

    @Override
    protected void onLoad() {
        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.
         */
        setSplitPosition(lastSplitPosition);
        DeferredCommand.addCommand(new Command() {
            public void execute() {
                setSplitPosition(lastSplitPosition);
            }
        });
    }

    @Override
    protected void onUnload() {
        impl.onDetach();
    }

    @Override
    void onSplitterResize(int x, int y) {
        impl.onSplitResize(initialLeftWidth + x - initialThumbPos);
    }

    @Override
    void onSplitterResizeStarted(int x, int y) {
        initialThumbPos = x;
        initialLeftWidth = getOffsetWidth(getElement(LEFT));
    }

    private void buildDOM(AbstractImagePrototype thumbImage) {
        final Element leftDiv = getElement(LEFT);
        final Element rightDiv = getElement(RIGHT);
        final Element splitDiv = getSplitElement();

        DOM.appendChild(getElement(), container);

        DOM.appendChild(container, leftDiv);
        DOM.appendChild(container, splitDiv);
        DOM.appendChild(container, 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());

        addScrolling(leftDiv);
        addScrolling(rightDiv);
    }

    private int getEndOfLinePos() {
        return (LocaleInfo.getCurrentLocale().isRTL() ? LEFT : RIGHT);
    }

    private int getStartOfLinePos() {
        return (LocaleInfo.getCurrentLocale().isRTL() ? RIGHT : LEFT);
    }
}