/** * Copyright 2010 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 org.waveprotocol.wave.client.widget.popup; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.Style.Visibility; import com.google.gwt.user.client.ui.RootPanel; import org.waveprotocol.wave.client.common.util.UserAgent; /** * Popup positioner that places the popup above the relative element, aligned to * its left side if there is space on the right, or it's right side if there * isn't. * <p> * Note that this positioner does not always align popups correctly on Firefox, * because of its sub-pixel rendering aliasing problems. */ public final class AlignedPopupPositioner implements RelativePopupPositioner { // TODO(user): Expose these constants on the resource bundle of the chrome // images. /** * Distance from the bottom edge of the north chrome frame to the top of the * blue border line in it. */ private final static int NORTH_CHROME_OFFSET_PX = 3; /** * Distance from the top edge of the south chrome frame to the bottom of the * blue border line in it. */ private final static int SOUTH_CHROME_OFFSET_PX = 3; /** * Distance from the left edge of the east chrome frame to the right of the * blue border line in it. Non-private only for tests. */ private final static int EAST_CHROME_OFFSET_PX = 1; /** * Distance from the right edge of the west chrome frame to the left of the * blue border line in it. Non-private only for tests. */ private final static int WEST_CHROME_OFFSET_PX = 1; /** * Desired vertical space between the visual top of the popup (i.e., the blue * border in the north chrome image) and the bottom of the element against * which it is positioned. If this value is 0, the popup should appear flush * against its relative element. */ private final static int VERTICAL_OFFSET_PX = 5; /** * Offset that shifts the alignment edge of the relative element's left * border-edge. This is so that the popup may be left-aligned to some point * within the relative element (e.g., if the relative element is an image, and * the alignment point is some visual part of that image). */ public static class Insets { private final int top; private final int right; private final int bottom; private final int left; /** No extra insets. Use an element's border box as the alignment box. */ // Firefox's sub-pixel rendering causes havoc with pixel-based alignment // strategies. The Firefox insets come from experimental verification of // what is required to get border-box alignent in most cases. public final static Insets NONE = UserAgent.isFirefox() ? Insets.of(0, -1, 0, 1) : Insets.of(0, 0, 0, 0); private Insets(int top, int right, int bottom, int left) { this.top = top; this.right = right; this.bottom = bottom; this.left = left; } public static Insets of(int top, int right, int bottom, int left) { return new Insets(top, right, bottom, left); } } // // The box from which the absolute positions of elements are measured is different on different // browsers, due to the use of margins on the body element (NOTE: the "absolute positions" of an // element, as reported by getAbsoluteX, are the positions of its border edge; i.e., adding // margins to an element changes its "absolute position", but padding and borders do not). // // IE measures absolute offsets from the top-left of the entire window: // __________________________________ // | | | | // | top | | // | __V_________|__________ | // |-left->| | top | | // | | ____V____ | | // |-----left----->|_________| | | // |-----------------right--> | | // | | | | // |----------------------right--->| | // | |_______________________| | // | | // |__________________________________| // // Safari measures the positions from the top-left of the body's border edge // (i.e., body margins do not affect top-left positions of anything, but body borders do). // ____________________________________ // | ______________________________ | // | | | top | | // | | ____V____ | | // | |----left----->|_________| | | // | |-----------------right-> | | // | |______________________________| | // |____________________________________| // // We treat the body's border edge as the constraining frame, so therefore we normalize the // reported positions by subtracting the top/left position of the body element. // Note that the position of the popup is set using absolute positioning, which positions the // entire popup box (i.e., its margin edge) within the context of the body element's padding // edge. We hope that nobody puts padding on the body, so we can use the border-edge as the // positioning context. // /** * Horizontal alignment strategies. */ enum Horizontal { /** Aligns the left of the popup to the left of the relative. */ LEFT { @Override public int getLeft(int relLeft, int relRight, int popupWidth) { return relLeft + WEST_CHROME_OFFSET_PX; } }, /** Aligns the right of the popup to the right of the relative. */ RIGHT { @Override public int getLeft(int relLeft, int relRight, int popupWidth) { return relRight - popupWidth - EAST_CHROME_OFFSET_PX; } }; abstract int getLeft(int relLeft, int relRight, int popupWidth); } /** * Vertical alignment strategies. */ enum Vertical { /** Aligns the top of the popup to the bottom of the relative. */ BOTTOM { @Override public int getTop(int relTop, int relBottom, int popupHeight) { return relBottom + NORTH_CHROME_OFFSET_PX + VERTICAL_OFFSET_PX; } }, /** Aligns the bottom of the popup to the top of the relative. */ TOP { @Override public int getTop(int relTop, int relBottom, int popupHeight) { return relTop - popupHeight - SOUTH_CHROME_OFFSET_PX - VERTICAL_OFFSET_PX; } }, ; abstract int getTop(int relTop, int relBottom, int popupHeight); } /** An above-left positioner. */ public static final RelativePopupPositioner ABOVE_LEFT = new AlignedPopupPositioner( Insets.NONE, Horizontal.LEFT, Horizontal.RIGHT, Vertical.TOP, Vertical.BOTTOM); /** A below-left positioner. */ public static final RelativePopupPositioner BELOW_LEFT = new AlignedPopupPositioner( Insets.NONE, Horizontal.LEFT, Horizontal.RIGHT, Vertical.BOTTOM, Vertical.TOP); /** A below-right positioner. */ public static final RelativePopupPositioner BELOW_RIGHT = new AlignedPopupPositioner( Insets.NONE, Horizontal.RIGHT, Horizontal.LEFT, Vertical.BOTTOM, Vertical.TOP); /** An above-right positioner. */ public static final RelativePopupPositioner ABOVE_RIGHT = new AlignedPopupPositioner( Insets.NONE, Horizontal.RIGHT, Horizontal.LEFT, Vertical.TOP, Vertical.BOTTOM); private final Insets insets; private final Horizontal primaryHorz; private final Horizontal secondaryHorz; private final Vertical primaryVert; private final Vertical secondaryVert; /** * Creates a positioner. * * @param insets insets from the border edge of the relative element, against * which the popup is aligned. * @param primaryHorz preferred horizontal alignment * @param secondaryHorz alternative horizontal alignment, in case the primary * would position the popup off screen * @param primaryVert preferred vertical alignment * @param secondaryVert alternative vertical alignment, in case the primary * would position the popup off screen */ public AlignedPopupPositioner(Insets insets, Horizontal primaryHorz, Horizontal secondaryHorz, Vertical primaryVert, Vertical secondaryVert) { this.insets = insets; this.primaryHorz = primaryHorz; this.secondaryHorz = secondaryHorz; this.primaryVert = primaryVert; this.secondaryVert = secondaryVert; } @Override public void setPopupPositionAndMakeVisible(Element relative, Element popup) { int bodyLeft = RootPanel.get().getElement().getAbsoluteLeft(); int bodyRight = RootPanel.get().getElement().getAbsoluteRight(); int bodyTop = RootPanel.get().getElement().getAbsoluteTop(); int bodyBottom = RootPanel.get().getElement().getAbsoluteBottom(); int relLeft = relative.getAbsoluteLeft() - bodyLeft + insets.left; int relRight = relative.getAbsoluteRight() - bodyLeft - insets.right; int relTop = relative.getAbsoluteTop() - bodyTop + insets.top; int relBottom = relative.getAbsoluteBottom() - bodyTop - insets.bottom; int popupWidth = popup.getOffsetWidth(); int popupHeight = popup.getOffsetHeight(); int left = primaryHorz.getLeft(relLeft, relRight, popupWidth); int right = left + popupWidth + EAST_CHROME_OFFSET_PX; if (left < bodyLeft || right > bodyRight) { // Primary alignment strategy failed. Try secondary. int secondaryLeft = secondaryHorz.getLeft(relLeft, relRight, popupWidth); int secondaryRight = secondaryLeft + popupWidth + EAST_CHROME_OFFSET_PX; if (secondaryLeft < bodyLeft || secondaryRight > bodyRight) { // Secondary alignment strategy also failed. Use clipped primary. left = PositionUtil.boundToScreenHorizontal(left, right - left); } else { left = secondaryLeft; } } int top = primaryVert.getTop(relTop, relBottom, popupHeight); int bottom = top + popupHeight + SOUTH_CHROME_OFFSET_PX; if (top < bodyTop || bottom > bodyBottom) { // Primary alignment strategy failed. Try secondary. int secondaryTop = secondaryVert.getTop(relTop, relBottom, popupHeight); int secondaryBottom = secondaryTop + popupHeight + SOUTH_CHROME_OFFSET_PX; if (secondaryTop < bodyTop || secondaryBottom > bodyBottom) { // Secondary alignment strategy also failed. Use clipped primary. top = PositionUtil.boundToScreenVertical(top, bottom - top); } else { top = secondaryTop; } } popup.getStyle().setLeft(left, Unit.PX); popup.getStyle().setTop(top, Unit.PX); popup.getStyle().setVisibility(Visibility.VISIBLE); } }