| /* | |
| * Copyright 2009 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.layout.client; | |
| import static com.google.gwt.dom.client.Style.Unit.PX; | |
| import com.google.gwt.animation.client.Animation; | |
| import com.google.gwt.core.client.GWT; | |
| import com.google.gwt.dom.client.Element; | |
| import com.google.gwt.dom.client.Style.Unit; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| /** | |
| * Helper class for laying out a container element and its children. | |
| * | |
| * <p> | |
| * This class is typically used by higher-level widgets to implement layout on | |
| * their behalf. It is intended to wrap an element (usually a <div>), and | |
| * lay its children out in a predictable fashion, automatically accounting for | |
| * changes to the parent's size, and for all elements' margins, borders, and | |
| * padding. | |
| * </p> | |
| * | |
| * <p> | |
| * To use this class, create a container element (again, usually a <div>) | |
| * and pass it to {@link #Layout(Element)}. Rather than attaching child elements | |
| * directly to the element managed by this {@link Layout}, use the | |
| * {@link Layout#attachChild(Element)} method. This will attach the child | |
| * element and return a {@link Layout.Layer} object which is used to manage the | |
| * child. | |
| * </p> | |
| * | |
| * <p> | |
| * A separate {@link Layout.Layer} instance is associated with each child | |
| * element. There is a set of methods available on this class to manipulate the | |
| * child element's position and size. In order for changes to a layer to take | |
| * effect, you must finally call one of {@link #layout()} or | |
| * {@link #layout(int)}. This allows many changes to different layers to be | |
| * applied efficiently, and to be animated. | |
| * </p> | |
| * | |
| * <p> | |
| * On most browsers, this is implemented using absolute positioning. It also | |
| * contains extra logic to make IE6 work properly. | |
| * </p> | |
| * | |
| * <p> | |
| * <h3>Example</h3> | |
| * {@example com.google.gwt.examples.LayoutExample} | |
| * </p> | |
| * | |
| * <p> | |
| * NOTE: This class will <em>only</em> work in standards mode, which requires | |
| * that the HTML page in which it is run have an explicit <!DOCTYPE> | |
| * declaration. | |
| * </p> | |
| * | |
| * <p> | |
| * NOTE: This class is still very new, and its interface may change without | |
| * warning. Use at your own risk. | |
| * </p> | |
| */ | |
| public class Layout { | |
| /** | |
| * Used to specify the alignment of child elements within a layer. | |
| */ | |
| public enum Alignment { | |
| /** | |
| * Positions an element at the beginning of a given axis. | |
| */ | |
| BEGIN, | |
| /** | |
| * Positions an element at the beginning of a given axis. | |
| */ | |
| END, | |
| /** | |
| * Stretches an element to fill the layer on a given axis. | |
| */ | |
| STRETCH; | |
| } | |
| /** | |
| * Callback interface used by {@link Layout#layout(int, AnimationCallback)} | |
| * to provide updates on animation progress. | |
| */ | |
| public interface AnimationCallback { | |
| /** | |
| * Called immediately after the animation is complete, and the entire layout | |
| * is in its final state. | |
| */ | |
| void onAnimationComplete(); | |
| /** | |
| * Called at each step of the animation, for each layer being laid out. | |
| * | |
| * @param layer the layer being laid out | |
| */ | |
| void onLayout(Layer layer, double progress); | |
| } | |
| /** | |
| * This class is used to set the position and size of child elements. | |
| * | |
| * <p> | |
| * Each child element has three values associated with each axis: {left, | |
| * right, width} on the horizontal axis, and {top, bottom, height} on the | |
| * vertical axis. Precisely two of three values may be set at a time, or the | |
| * system will be over- or under-contrained. For this reason, the following | |
| * methods are provided for setting these values: | |
| * <ul> | |
| * <li>{@link Layout.Layer#setLeftRight}</li> | |
| * <li>{@link Layout.Layer#setLeftWidth}</li> | |
| * <li>{@link Layout.Layer#setRightWidth}</li> | |
| * <li>{@link Layout.Layer#setTopBottom}</li> | |
| * <li>{@link Layout.Layer#setTopHeight}</li> | |
| * <li>{@link Layout.Layer#setBottomHeight}</li> | |
| * </ul> | |
| * </p> | |
| * | |
| * <p> | |
| * By default, each layer is set to fill the entire parent (i.e., {left, top, | |
| * right, bottom} = {0, 0, 0, 0}). | |
| * </p> | |
| */ | |
| public class Layer { | |
| final Element container, child; | |
| Object userObject; | |
| boolean setLeft, setRight, setTop, setBottom, setWidth, setHeight; | |
| boolean setTargetLeft = true, setTargetRight = true, setTargetTop = true, | |
| setTargetBottom = true, setTargetWidth, setTargetHeight; | |
| Unit leftUnit, topUnit, rightUnit, bottomUnit, widthUnit, heightUnit; | |
| Unit targetLeftUnit = PX, targetTopUnit = PX, targetRightUnit = PX, | |
| targetBottomUnit = PX, targetWidthUnit, targetHeightUnit; | |
| double left, top, right, bottom, width, height; | |
| double sourceLeft, sourceTop, sourceRight, sourceBottom, sourceWidth, | |
| sourceHeight; | |
| double targetLeft, targetTop, targetRight, targetBottom, targetWidth, | |
| targetHeight; | |
| Alignment hPos = Alignment.STRETCH, vPos = Alignment.STRETCH; | |
| boolean visible = true; | |
| Layer(Element container, Element child, Object userObject) { | |
| this.container = container; | |
| this.child = child; | |
| this.userObject = userObject; | |
| } | |
| /** | |
| * Gets the container element associated with this layer. | |
| * | |
| * <p> | |
| * This is the element that sits between the parent and child elements. It | |
| * is normally necessary to operate on this element only when you need to | |
| * modify CSS properties that are not directly modeled by the Layer class. | |
| * </p> | |
| * | |
| * @return the container element | |
| */ | |
| public Element getContainerElement() { | |
| return container; | |
| } | |
| /** | |
| * Gets the user-data associated with this layer. | |
| * | |
| * @return the layer's user-data object | |
| */ | |
| public Object getUserObject() { | |
| return this.userObject; | |
| } | |
| /** | |
| * Sets the layer's bottom and height values. | |
| * | |
| * @param bottom | |
| * @param bottomUnit | |
| * @param height | |
| * @param heightUnit | |
| */ | |
| public void setBottomHeight(double bottom, Unit bottomUnit, double height, | |
| Unit heightUnit) { | |
| this.setTargetBottom = this.setTargetHeight = true; | |
| this.setTargetTop = false; | |
| this.targetBottom = bottom; | |
| this.targetHeight = height; | |
| this.targetBottomUnit = bottomUnit; | |
| this.targetHeightUnit = heightUnit; | |
| } | |
| /** | |
| * Sets the child element's horizontal position within the layer. | |
| * | |
| * @param position | |
| */ | |
| public void setChildHorizontalPosition(Alignment position) { | |
| this.hPos = position; | |
| } | |
| /** | |
| * Sets the child element's vertical position within the layer. | |
| * | |
| * @param position | |
| */ | |
| public void setChildVerticalPosition(Alignment position) { | |
| this.vPos = position; | |
| } | |
| /** | |
| * Sets the layer's left and right values. | |
| * | |
| * @param left | |
| * @param leftUnit | |
| * @param right | |
| * @param rightUnit | |
| */ | |
| public void setLeftRight(double left, Unit leftUnit, double right, | |
| Unit rightUnit) { | |
| this.setTargetLeft = this.setTargetRight = true; | |
| this.setTargetWidth = false; | |
| this.targetLeft = left; | |
| this.targetRight = right; | |
| this.targetLeftUnit = leftUnit; | |
| this.targetRightUnit = rightUnit; | |
| } | |
| /** | |
| * Sets the layer's left and width values. | |
| * | |
| * @param left | |
| * @param leftUnit | |
| * @param width | |
| * @param widthUnit | |
| */ | |
| public void setLeftWidth(double left, Unit leftUnit, double width, | |
| Unit widthUnit) { | |
| this.setTargetLeft = this.setTargetWidth = true; | |
| this.setTargetRight = false; | |
| this.targetLeft = left; | |
| this.targetWidth = width; | |
| this.targetLeftUnit = leftUnit; | |
| this.targetWidthUnit = widthUnit; | |
| } | |
| /** | |
| * Sets the layer's right and width values. | |
| * | |
| * @param right | |
| * @param rightUnit | |
| * @param width | |
| * @param widthUnit | |
| */ | |
| public void setRightWidth(double right, Unit rightUnit, double width, | |
| Unit widthUnit) { | |
| this.setTargetRight = this.setTargetWidth = true; | |
| this.setTargetLeft = false; | |
| this.targetRight = right; | |
| this.targetWidth = width; | |
| this.targetRightUnit = rightUnit; | |
| this.targetWidthUnit = widthUnit; | |
| } | |
| /** | |
| * Sets the layer's top and bottom values. | |
| * | |
| * @param top | |
| * @param topUnit | |
| * @param bottom | |
| * @param bottomUnit | |
| */ | |
| public void setTopBottom(double top, Unit topUnit, double bottom, | |
| Unit bottomUnit) { | |
| this.setTargetTop = this.setTargetBottom = true; | |
| this.setTargetHeight = false; | |
| this.targetTop = top; | |
| this.targetBottom = bottom; | |
| this.targetTopUnit = topUnit; | |
| this.targetBottomUnit = bottomUnit; | |
| } | |
| /** | |
| * Sets the layer's top and height values. | |
| * | |
| * @param top | |
| * @param topUnit | |
| * @param height | |
| * @param heightUnit | |
| */ | |
| public void setTopHeight(double top, Unit topUnit, double height, | |
| Unit heightUnit) { | |
| this.setTargetTop = this.setTargetHeight = true; | |
| this.setTargetBottom = false; | |
| this.targetTop = top; | |
| this.targetHeight = height; | |
| this.targetTopUnit = topUnit; | |
| this.targetHeightUnit = heightUnit; | |
| } | |
| /** | |
| * Sets the layer's visibility. | |
| * | |
| * @param visible | |
| */ | |
| public void setVisible(boolean visible) { | |
| this.visible = visible; | |
| } | |
| } | |
| private LayoutImpl impl = GWT.create(LayoutImpl.class); | |
| private List<Layer> layers = new ArrayList<Layer>(); | |
| private final Element parentElem; | |
| private Animation animation; | |
| /** | |
| * Constructs a new layout associated with the given parent element. | |
| * | |
| * @param parent the element to serve as the layout parent | |
| */ | |
| public Layout(Element parent) { | |
| this.parentElem = parent; | |
| impl.initParent(parent); | |
| } | |
| /** | |
| * Asserts that the given child element is managed by this layout. | |
| * | |
| * @param elem the element to be tested | |
| */ | |
| public void assertIsChild(Element elem) { | |
| assert elem.getParentElement().getParentElement() == this.parentElem : "Element is not a child of this layout"; | |
| } | |
| /** | |
| * Attaches a child element to this layout. | |
| * | |
| * <p> | |
| * This method will attach the child to the layout, removing it from its | |
| * current parent element. Use the {@link Layer} it returns to manipulate the | |
| * child. | |
| * </p> | |
| * | |
| * @param child the child to be attached | |
| * @return the {@link Layer} associated with the element | |
| */ | |
| public Layer attachChild(Element child) { | |
| return attachChild(child, null); | |
| } | |
| /** | |
| * Attaches a child element to this layout. | |
| * | |
| * <p> | |
| * This method will attach the child to the layout, removing it from its | |
| * current parent element. Use the {@link Layer} it returns to manipulate the | |
| * child. | |
| * </p> | |
| * | |
| * @param child the child to be attached | |
| * @param before the child element before which to insert | |
| * @return the {@link Layer} associated with the element | |
| */ | |
| public Layer attachChild(Element child, Element before) { | |
| return attachChild(child, before, null); | |
| } | |
| /** | |
| * Attaches a child element to this layout. | |
| * | |
| * <p> | |
| * This method will attach the child to the layout, removing it from its | |
| * current parent element. Use the {@link Layer} it returns to manipulate the | |
| * child. | |
| * </p> | |
| * | |
| * @param child the child to be attached | |
| * @param userObject an arbitrary object to be associated with this layer | |
| * @return the {@link Layer} associated with the element | |
| */ | |
| public Layer attachChild(Element child, Object userObject) { | |
| return attachChild(child, null, userObject); | |
| } | |
| /** | |
| * Attaches a child element to this layout. | |
| * | |
| * <p> | |
| * This method will attach the child to the layout, removing it from its | |
| * current parent element. Use the {@link Layer} it returns to manipulate the | |
| * child. | |
| * </p> | |
| * | |
| * @param child the child to be attached | |
| * @param before the child element before which to insert | |
| * @param userObject an arbitrary object to be associated with this layer | |
| * @return the {@link Layer} associated with the element | |
| */ | |
| public Layer attachChild(Element child, Element before, Object userObject) { | |
| Element container = impl.attachChild(parentElem, child, before); | |
| Layer layer = new Layer(container, child, userObject); | |
| layers.add(layer); | |
| return layer; | |
| } | |
| /** | |
| * Causes the parent element to fill its own parent. | |
| * | |
| * <p> | |
| * This is most useful for top-level layouts that need to follow the size of | |
| * another element, such as the <body>. | |
| * </p> | |
| */ | |
| public void fillParent() { | |
| impl.fillParent(parentElem); | |
| } | |
| /** | |
| * Returns the size of one unit, in pixels, in the context of this layout. | |
| * | |
| * <p> | |
| * This will work for any unit type, but be aware that certain unit types, | |
| * such as {@link Unit#EM}, and {@link Unit#EX}, will return different values | |
| * based upon the parent's associated font size. {@link Unit#PCT} is dependent | |
| * upon the parent's actual size, and the axis to be measured. | |
| * </p> | |
| * | |
| * @param unit the unit type to be measured | |
| * @param vertical whether the unit to be measured is on the vertical or | |
| * horizontal axis (this matters only for {@link Unit#PCT}) | |
| * @return the unit size, in pixels | |
| */ | |
| public double getUnitSize(Unit unit, boolean vertical) { | |
| return impl.getUnitSizeInPixels(parentElem, unit, vertical); | |
| } | |
| /** | |
| * Updates this layout's children immediately. This method <em>must</em> be | |
| * called after updating any of its children's {@link Layer layers}. | |
| */ | |
| public void layout() { | |
| layout(0); | |
| } | |
| /** | |
| * Updates the layout by animating it over time. | |
| * | |
| * @param duration the duration of the animation | |
| * @see #layout(int, AnimationCallback) | |
| */ | |
| public void layout(int duration) { | |
| layout(duration, null); | |
| } | |
| /** | |
| * Updates the layout by animating it over time, with a callback on each frame | |
| * of the animation, and upon completion. | |
| * | |
| * @param duration the duration of the animation | |
| * @param callback the animation callback | |
| */ | |
| public void layout(int duration, final AnimationCallback callback) { | |
| // Cancel the old animation, if there is one. | |
| if (animation != null) { | |
| animation.cancel(); | |
| } | |
| // If there's no actual animation going on, don't do any of the expensive | |
| // constraint calculations or anything like that. | |
| if (duration == 0) { | |
| for (Layer l : layers) { | |
| l.left = l.sourceLeft = l.targetLeft; | |
| l.top = l.sourceTop = l.targetTop; | |
| l.right = l.sourceRight = l.targetRight; | |
| l.bottom = l.sourceBottom = l.targetBottom; | |
| l.width = l.sourceWidth = l.targetWidth; | |
| l.height = l.sourceHeight = l.targetHeight; | |
| l.setLeft = l.setTargetLeft; | |
| l.setTop = l.setTargetTop; | |
| l.setRight = l.setTargetRight; | |
| l.setBottom = l.setTargetBottom; | |
| l.setWidth = l.setTargetWidth; | |
| l.setHeight = l.setTargetHeight; | |
| l.leftUnit = l.targetLeftUnit; | |
| l.topUnit = l.targetTopUnit; | |
| l.rightUnit = l.targetRightUnit; | |
| l.bottomUnit = l.targetBottomUnit; | |
| l.widthUnit = l.targetWidthUnit; | |
| l.heightUnit = l.targetHeightUnit; | |
| impl.layout(l); | |
| if (callback != null) { | |
| callback.onLayout(l, 1.0); | |
| } | |
| } | |
| impl.finalizeLayout(parentElem); | |
| if (callback != null) { | |
| callback.onAnimationComplete(); | |
| } | |
| return; | |
| } | |
| // Deal with constraint changes (e.g. left-width => right-width, etc) | |
| int parentWidth = parentElem.getClientWidth(); | |
| int parentHeight = parentElem.getClientHeight(); | |
| for (Layer l : layers) { | |
| adjustHorizontalConstraints(parentWidth, l); | |
| adjustVerticalConstraints(parentHeight, l); | |
| } | |
| animation = new Animation() { | |
| @Override | |
| protected void onCancel() { | |
| onComplete(); | |
| } | |
| @Override | |
| protected void onComplete() { | |
| animation = null; | |
| layout(); | |
| if (callback != null) { | |
| callback.onAnimationComplete(); | |
| } | |
| } | |
| @Override | |
| protected void onUpdate(double progress) { | |
| for (Layer l : layers) { | |
| if (l.setTargetLeft) { | |
| l.left = l.sourceLeft + (l.targetLeft - l.sourceLeft) * progress; | |
| } | |
| if (l.setTargetRight) { | |
| l.right = l.sourceRight + (l.targetRight - l.sourceRight) | |
| * progress; | |
| } | |
| if (l.setTargetTop) { | |
| l.top = l.sourceTop + (l.targetTop - l.sourceTop) * progress; | |
| } | |
| if (l.setTargetBottom) { | |
| l.bottom = l.sourceBottom + (l.targetBottom - l.sourceBottom) | |
| * progress; | |
| } | |
| if (l.setTargetWidth) { | |
| l.width = l.sourceWidth + (l.targetWidth - l.sourceWidth) | |
| * progress; | |
| } | |
| if (l.setTargetHeight) { | |
| l.height = l.sourceHeight + (l.targetHeight - l.sourceHeight) | |
| * progress; | |
| } | |
| impl.layout(l); | |
| if (callback != null) { | |
| callback.onLayout(l, progress); | |
| } | |
| } | |
| impl.finalizeLayout(parentElem); | |
| } | |
| }; | |
| animation.run(duration, parentElem); | |
| } | |
| /** | |
| * This method must be called when the parent element becomes attached to the | |
| * document. | |
| * | |
| * @see #onDetach() | |
| */ | |
| public void onAttach() { | |
| impl.onAttach(parentElem); | |
| } | |
| /** | |
| * This method must be called when the parent element becomes detached from | |
| * the document. | |
| * | |
| * @see #onAttach() | |
| */ | |
| public void onDetach() { | |
| impl.onDetach(parentElem); | |
| } | |
| /** | |
| * Removes a child element from this layout. | |
| * | |
| * @param layer the layer associated with the child to be removed | |
| */ | |
| public void removeChild(Layer layer) { | |
| impl.removeChild(layer.container, layer.child); | |
| layers.remove(layer); | |
| } | |
| private void adjustHorizontalConstraints(int parentWidth, Layer l) { | |
| double leftPx = l.left * getUnitSize(l.leftUnit, false); | |
| double rightPx = l.right * getUnitSize(l.rightUnit, false); | |
| double widthPx = l.width * getUnitSize(l.widthUnit, false); | |
| if (l.setLeft && !l.setTargetLeft) { | |
| // -left | |
| l.setLeft = false; | |
| if (!l.setWidth) { | |
| // +width | |
| l.setTargetWidth = true; | |
| l.sourceWidth = (parentWidth - (leftPx + rightPx)) | |
| / getUnitSize(l.targetWidthUnit, false); | |
| } else { | |
| // +right | |
| l.setTargetRight = true; | |
| l.sourceRight = (parentWidth - (leftPx + widthPx)) | |
| / getUnitSize(l.targetRightUnit, false); | |
| } | |
| } else if (l.setWidth && !l.setTargetWidth) { | |
| // -width | |
| l.setWidth = false; | |
| if (!l.setLeft) { | |
| // +left | |
| l.setTargetLeft = true; | |
| l.sourceLeft = (parentWidth - (rightPx + widthPx)) | |
| / getUnitSize(l.targetLeftUnit, false); | |
| } else { | |
| // +right | |
| l.setTargetRight = true; | |
| l.sourceRight = (parentWidth - (leftPx + widthPx)) | |
| / getUnitSize(l.targetRightUnit, false); | |
| } | |
| } else if (l.setRight && !l.setTargetRight) { | |
| // -right | |
| l.setRight = false; | |
| if (!l.setWidth) { | |
| // +width | |
| l.setTargetWidth = true; | |
| l.sourceWidth = (parentWidth - (leftPx + rightPx)) | |
| / getUnitSize(l.targetWidthUnit, false); | |
| } else { | |
| // +left | |
| l.setTargetLeft = true; | |
| l.sourceLeft = (parentWidth - (rightPx + widthPx)) | |
| / getUnitSize(l.targetLeftUnit, false); | |
| } | |
| } | |
| l.setLeft = l.setTargetLeft; | |
| l.setRight = l.setTargetRight; | |
| l.setWidth = l.setTargetWidth; | |
| l.leftUnit = l.targetLeftUnit; | |
| l.rightUnit = l.targetRightUnit; | |
| l.widthUnit = l.targetWidthUnit; | |
| } | |
| private void adjustVerticalConstraints(int parentHeight, Layer l) { | |
| double topPx = l.top * getUnitSize(l.topUnit, true); | |
| double bottomPx = l.bottom * getUnitSize(l.bottomUnit, true); | |
| double heightPx = l.height * getUnitSize(l.heightUnit, true); | |
| if (l.setTop && !l.setTargetTop) { | |
| // -top | |
| l.setTop = false; | |
| if (!l.setHeight) { | |
| // +height | |
| l.setTargetHeight = true; | |
| l.sourceHeight = (parentHeight - (topPx + bottomPx)) | |
| / getUnitSize(l.targetHeightUnit, true); | |
| } else { | |
| // +bottom | |
| l.setTargetBottom = true; | |
| l.sourceBottom = (parentHeight - (topPx + heightPx)) | |
| / getUnitSize(l.targetBottomUnit, true); | |
| } | |
| } else if (l.setHeight && !l.setTargetHeight) { | |
| // -height | |
| l.setHeight = false; | |
| if (!l.setTop) { | |
| // +top | |
| l.setTargetTop = true; | |
| l.sourceTop = (parentHeight - (bottomPx + heightPx)) | |
| / getUnitSize(l.targetTopUnit, true); | |
| } else { | |
| // +bottom | |
| l.setTargetBottom = true; | |
| l.sourceBottom = (parentHeight - (topPx + heightPx)) | |
| / getUnitSize(l.targetBottomUnit, true); | |
| } | |
| } else if (l.setBottom && !l.setTargetBottom) { | |
| // -bottom | |
| l.setBottom = false; | |
| if (!l.setHeight) { | |
| // +height | |
| l.setTargetHeight = true; | |
| l.sourceHeight = (parentHeight - (topPx + bottomPx)) | |
| / getUnitSize(l.targetHeightUnit, true); | |
| } else { | |
| // +top | |
| l.setTargetTop = true; | |
| l.sourceTop = (parentHeight - (bottomPx + heightPx)) | |
| / getUnitSize(l.targetTopUnit, true); | |
| } | |
| } | |
| l.setTop = l.setTargetTop; | |
| l.setBottom = l.setTargetBottom; | |
| l.setHeight = l.setTargetHeight; | |
| l.topUnit = l.targetTopUnit; | |
| l.bottomUnit = l.targetBottomUnit; | |
| l.heightUnit = l.targetHeightUnit; | |
| } | |
| } |