blob: d75e016849168a85fbf2b9ede2f3fe9c51622178 [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
*
* 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.android.ide.eclipse.adt.internal.editors.layout.parts;
import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.LayoutConstants;
import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor.UiEditorActions;
import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiLayoutEditPart.HighlightInfo;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import java.util.HashMap;
import java.util.Map.Entry;
/**
* Utility methods used when dealing with dropping EditPart on the GLE.
* <p/>
* This class uses some temporary static storage to avoid excessive allocations during
* drop operations. It is expected to only be invoked from the main UI thread with no
* concurrent access.
*
* @since GLE1
*/
class DropFeedback {
private static final int TOP = 0;
private static final int LEFT = 1;
private static final int BOTTOM = 2;
private static final int RIGHT = 3;
private static final int MAX_DIR = RIGHT;
private static final int sOppositeDirection[] = { BOTTOM, RIGHT, TOP, LEFT };
private static final UiElementEditPart sTempClosests[] = new UiElementEditPart[4];
private static final int sTempMinDists[] = new int[4];
/**
* Target information computed from a drop on a RelativeLayout.
* We need only one instance of this and it is sRelativeInfo.
*/
private static class RelativeInfo {
/** The two target parts 0 and 1. They can be null, meaning a border is used.
* The direction from part 0 to 1 is always to-the-right or to-the-bottom. */
final UiElementEditPart targetParts[] = new UiElementEditPart[2];
/** Direction from the anchor part to the drop point. */
int direction;
/** The index of the "anchor" part, i.e. the closest one selected by the drop.
* This can be either 0 or 1. The corresponding part can be null. */
int anchorIndex;
}
/** The single RelativeInfo used to compute results from a drop on a RelativeLayout */
private static final RelativeInfo sRelativeInfo = new RelativeInfo();
/** A temporary array of 2 {@link UiElementEditPart} to avoid allocations. */
private static final UiElementEditPart sTempTwoParts[] = new UiElementEditPart[2];
private DropFeedback() {
}
//----- Package methods called by users of this helper class -----
/**
* This method is used by {@link ElementCreateCommand#execute()} when a new item
* needs to be "dropped" in the current XML document. It creates the new item using
* the given descriptor as a child of the given parent part.
*
* @param parentPart The parent part.
* @param descriptor The descriptor for the new XML element.
* @param where The drop location (in parent coordinates)
* @param actions The helper that actually modifies the XML model.
*/
static void addElementToXml(UiElementEditPart parentPart,
ElementDescriptor descriptor, Point where,
UiEditorActions actions) {
String layoutXmlName = getXmlLocalName(parentPart);
RelativeInfo info = null;
UiElementEditPart sibling = null;
// TODO consider merge like a vertical layout
// TODO consider TableLayout like a linear
if (LayoutConstants.LINEAR_LAYOUT.equals(layoutXmlName)) {
sibling = findLinearTarget(parentPart, where)[1];
} else if (LayoutConstants.RELATIVE_LAYOUT.equals(layoutXmlName)) {
info = findRelativeTarget(parentPart, where, sRelativeInfo);
if (info != null) {
sibling = info.targetParts[info.anchorIndex];
sibling = getNextUiSibling(sibling);
}
}
if (actions != null) {
UiElementNode uiSibling = sibling != null ? sibling.getUiNode() : null;
UiElementNode uiParent = parentPart.getUiNode();
UiElementNode uiNode = actions.addElement(uiParent, uiSibling, descriptor,
false /*updateLayout*/);
if (LayoutConstants.ABSOLUTE_LAYOUT.equals(layoutXmlName)) {
adjustAbsoluteAttributes(uiNode, where);
} else if (LayoutConstants.RELATIVE_LAYOUT.equals(layoutXmlName)) {
adustRelativeAttributes(uiNode, info);
}
}
}
/**
* This method is used by {@link UiLayoutEditPart#showDropTarget(Point)} to compute
* highlight information when a drop target is moved over a valid drop area.
* <p/>
* Since there are no "out" parameters in Java, all the information is returned
* via the {@link HighlightInfo} structure passed as parameter.
*
* @param parentPart The parent part, always a layout.
* @param highlightInfo A structure where result is stored to perform highlight.
* @param where The target drop point, in parent's coordinates
* @return The {@link HighlightInfo} structured passed as a parameter, for convenience.
*/
static HighlightInfo computeDropFeedback(UiLayoutEditPart parentPart,
HighlightInfo highlightInfo,
Point where) {
String layoutType = getXmlLocalName(parentPart);
if (LayoutConstants.ABSOLUTE_LAYOUT.equals(layoutType)) {
highlightInfo.anchorPoint = where;
} else if (LayoutConstants.LINEAR_LAYOUT.equals(layoutType)) {
boolean isVertical = isVertical(parentPart);
highlightInfo.childParts = findLinearTarget(parentPart, where);
computeLinearLine(parentPart, isVertical, highlightInfo);
} else if (LayoutConstants.RELATIVE_LAYOUT.equals(layoutType)) {
RelativeInfo info = findRelativeTarget(parentPart, where, sRelativeInfo);
if (info != null) {
highlightInfo.childParts = sRelativeInfo.targetParts;
computeRelativeLine(parentPart, info, highlightInfo);
}
}
return highlightInfo;
}
//----- Misc utilities -----
/**
* Returns the next UI sibling of this part, i.e. the element which is just after in
* the UI/XML order in the same parent. Returns null if there's no such part.
* <p/>
* Note: by "UI sibling" here we mean the sibling in the UiNode hierarchy. By design the
* UiNode model has the <em>exact</em> same order as the XML model. This has nothing to do
* with the "user interface" order that you see on the rendered Android layouts (e.g. for
* LinearLayout they are the same but for AbsoluteLayout or RelativeLayout the UI/XML model
* order can be vastly different from the user interface order.)
*/
private static UiElementEditPart getNextUiSibling(UiElementEditPart part) {
if (part != null) {
UiElementNode uiNode = part.getUiNode();
if (uiNode != null) {
uiNode = uiNode.getUiNextSibling();
}
if (uiNode != null) {
for (Object childPart : part.getParent().getChildren()) {
if (childPart instanceof UiElementEditPart &&
((UiElementEditPart) childPart).getUiNode() == uiNode) {
return (UiElementEditPart) childPart;
}
}
}
}
return null;
}
/**
* Returns the XML local name of the ui node associated with this edit part or null.
*/
private static String getXmlLocalName(UiElementEditPart editPart) {
UiElementNode uiNode = editPart.getUiNode();
if (uiNode != null) {
ElementDescriptor desc = uiNode.getDescriptor();
if (desc != null) {
return desc.getXmlLocalName();
}
}
return null;
}
/**
* Adjusts the attributes of a new node dropped in an AbsoluteLayout.
*
* @param uiNode The new node being dropped.
* @param where The drop location (in parent coordinates)
*/
private static void adjustAbsoluteAttributes(final UiElementNode uiNode, final Point where) {
if (where == null) {
return;
}
uiNode.getEditor().editXmlModel(new Runnable() {
public void run() {
uiNode.setAttributeValue(LayoutConstants.ATTR_LAYOUT_X,
String.format(LayoutConstants.VALUE_N_DIP, where.x),
false /* override */);
uiNode.setAttributeValue(LayoutConstants.ATTR_LAYOUT_Y,
String.format(LayoutConstants.VALUE_N_DIP, where.y),
false /* override */);
uiNode.commitDirtyAttributesToXml();
}
});
}
/**
* Adjusts the attributes of a new node dropped in a RelativeLayout:
* <ul>
* <li> anchor part: the one the user selected (or the closest) and to which the new one
* will "attach". The anchor part can be null, either because the layout is currently
* empty or the user is attaching to an existing empty border.
* <li> direction: the direction from the anchor part to the drop point. That's also the
* direction from the anchor part to the new part.
* <li> the new node; it is created either after the anchor for right or top directions
* or before the anchor for left or bottom directions. This means the new part can
* reference the id of the anchor part.
* </ul>
*
* Several cases:
* <ul>
* <li> set: layout_above/below/toLeftOf/toRightOf to point to the anchor.
* <li> copy: layout_centerHorizontal for top/bottom directions
* <li> copy: layout_centerVertical for left/right directions.
* <li> copy: layout_above/below/toLeftOf/toRightOf for the orthogonal direction
* (i.e. top/bottom or left/right.)
* </ul>
*
* @param uiNode The new node being dropped.
* @param info The context computed by {@link #findRelativeTarget(UiElementEditPart, Point, RelativeInfo)}.
*/
private static void adustRelativeAttributes(final UiElementNode uiNode, RelativeInfo info) {
if (uiNode == null || info == null) {
return;
}
final UiElementEditPart anchorPart = info.targetParts[info.anchorIndex]; // can be null
final int direction = info.direction;
uiNode.getEditor().editXmlModel(new Runnable() {
public void run() {
HashMap<String, String> map = new HashMap<String, String>();
UiElementNode anchorUiNode = anchorPart != null ? anchorPart.getUiNode() : null;
String anchorId = anchorUiNode != null
? anchorUiNode.getAttributeValue("id") //$NON-NLS-1$
: null;
if (anchorId == null) {
anchorId = DescriptorsUtils.getFreeWidgetId(anchorUiNode);
anchorUiNode.setAttributeValue("id", anchorId, true /*override*/); //$NON-NLS-1$
}
if (anchorId != null) {
switch(direction) {
case TOP:
map.put(LayoutConstants.ATTR_LAYOUT_ABOVE, anchorId);
break;
case BOTTOM:
map.put(LayoutConstants.ATTR_LAYOUT_BELOW, anchorId);
break;
case LEFT:
map.put(LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF, anchorId);
break;
case RIGHT:
map.put(LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF, anchorId);
break;
}
switch(direction) {
case TOP:
case BOTTOM:
map.put(LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL,
anchorUiNode.getAttributeValue(
LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL));
map.put(LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF,
anchorUiNode.getAttributeValue(
LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF));
map.put(LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF,
anchorUiNode.getAttributeValue(
LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF));
break;
case LEFT:
case RIGHT:
map.put(LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL,
anchorUiNode.getAttributeValue(
LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL));
map.put(LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE,
anchorUiNode.getAttributeValue(
LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE));
map.put(LayoutConstants.ATTR_LAYOUT_ABOVE,
anchorUiNode.getAttributeValue(LayoutConstants.ATTR_LAYOUT_ABOVE));
map.put(LayoutConstants.ATTR_LAYOUT_BELOW,
anchorUiNode.getAttributeValue(LayoutConstants.ATTR_LAYOUT_BELOW));
break;
}
} else {
// We don't have an anchor node. Assume we're targeting a border and align
// to the parent.
switch(direction) {
case TOP:
map.put(LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP,
LayoutConstants.VALUE_TRUE);
break;
case BOTTOM:
map.put(LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
LayoutConstants.VALUE_TRUE);
break;
case LEFT:
map.put(LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT,
LayoutConstants.VALUE_TRUE);
break;
case RIGHT:
map.put(LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
LayoutConstants.VALUE_TRUE);
break;
}
}
for (Entry<String, String> entry : map.entrySet()) {
uiNode.setAttributeValue(entry.getKey(), entry.getValue(), true /* override */);
}
uiNode.commitDirtyAttributesToXml();
}
});
}
//----- LinearLayout --------
/**
* For a given parent edit part that MUST represent a LinearLayout, finds the
* element before which the location points.
* <p/>
* This computes the edit part that corresponds to what will be the "next sibling" of the new
* element.
* <p/>
* It returns null if it can't be determined, in which case the element will be added at the
* end of the parent child list.
*
* @return The edit parts that correspond to what will be the "prev" and "next sibling" of the
* new element. The previous sibling can be null if adding before the first element.
* The next sibling can be null if adding after the last element.
*/
private static UiElementEditPart[] findLinearTarget(UiElementEditPart parent, Point point) {
// default orientation is horizontal
boolean isVertical = isVertical(parent);
int target = isVertical ? point.y : point.x;
UiElementEditPart prev = null;
UiElementEditPart next = null;
for (Object child : parent.getChildren()) {
if (child instanceof UiElementEditPart) {
UiElementEditPart childPart = (UiElementEditPart) child;
Point p = childPart.getBounds().getCenter();
int middle = isVertical ? p.y : p.x;
if (target < middle) {
next = childPart;
break;
}
prev = childPart;
}
}
sTempTwoParts[0] = prev;
sTempTwoParts[1] = next;
return sTempTwoParts;
}
/**
* Computes the highlight line between two parts.
* <p/>
* The two parts are listed in HighlightInfo.childParts[2]. Any of the parts
* can be null.
* The result is stored in HighlightInfo.
* <p/>
* Caller must clear the HighlightInfo as appropriate before this call.
*
* @param parentPart The parent part, always a layout.
* @param isVertical True for vertical parts, thus computing an horizontal line.
* @param highlightInfo The in-out highlight info.
*/
private static void computeLinearLine(UiLayoutEditPart parentPart,
boolean isVertical, HighlightInfo highlightInfo) {
Rectangle r = parentPart.getBounds();
if (isVertical) {
Point p = null;
UiElementEditPart part = highlightInfo.childParts[0];
if (part != null) {
p = part.getBounds().getBottom();
} else {
part = highlightInfo.childParts[1];
if (part != null) {
p = part.getBounds().getTop();
}
}
if (p != null) {
// horizontal line with middle anchor point
highlightInfo.tempPoints[0].setLocation(0, p.y);
highlightInfo.tempPoints[1].setLocation(r.width, p.y);
highlightInfo.linePoints = highlightInfo.tempPoints;
highlightInfo.anchorPoint = p.setLocation(r.width / 2, p.y);
}
} else {
Point p = null;
UiElementEditPart part = highlightInfo.childParts[0];
if (part != null) {
p = part.getBounds().getRight();
} else {
part = highlightInfo.childParts[1];
if (part != null) {
p = part.getBounds().getLeft();
}
}
if (p != null) {
// vertical line with middle anchor point
highlightInfo.tempPoints[0].setLocation(p.x, 0);
highlightInfo.tempPoints[1].setLocation(p.x, r.height);
highlightInfo.linePoints = highlightInfo.tempPoints;
highlightInfo.anchorPoint = p.setLocation(p.x, r.height / 2);
}
}
}
/**
* Returns true if the linear layout is marked as vertical.
*
* @param parent The a layout part that must be a LinearLayout
* @return True if the linear layout has a vertical orientation attribute.
*/
private static boolean isVertical(UiElementEditPart parent) {
String orientation = parent.getStringAttr("orientation"); //$NON-NLS-1$
boolean isVertical = "vertical".equals(orientation) || //$NON-NLS-1$
"1".equals(orientation); //$NON-NLS-1$
return isVertical;
}
//----- RelativeLayout --------
/**
* Finds the "target" relative layout item for the drop operation & feedback.
* <p/>
* If the drop point is exactly on a current item, simply returns the side the drop will occur
* compared to the center of that element. For the actual XML, we'll need to insert *after*
* that element to make sure that referenced are defined in the right order.
* In that case the result contains two elements, the second one always being on the right or
* bottom side of the first one. When insert in XML, we want to insert right before that
* second element or at the end of the child list if the second element is null.
* <p/>
* If the drop point is not exactly on a current element, find the closest in each
* direction and align with the two closest of these.
*
* @return null if we fail to find anything (such as there are currently no items to compare
* with); otherwise fills the {@link RelativeInfo} and return it.
*/
private static RelativeInfo findRelativeTarget(UiElementEditPart parent,
Point point,
RelativeInfo outInfo) {
for (int i = 0; i < 4; i++) {
sTempMinDists[i] = Integer.MAX_VALUE;
sTempClosests[i] = null;
}
for (Object child : parent.getChildren()) {
if (child instanceof UiElementEditPart) {
UiElementEditPart childPart = (UiElementEditPart) child;
Rectangle r = childPart.getBounds();
if (r.contains(point)) {
float rx = ((float)(point.x - r.x) / (float)r.width ) - 0.5f;
float ry = ((float)(point.y - r.y) / (float)r.height) - 0.5f;
/* TOP
* \ /
* \ /
* L X R
* / \
* / \
* BOT
*/
int index = 0;
if (Math.abs(rx) >= Math.abs(ry)) {
if (rx < 0) {
outInfo.direction = LEFT;
index = 1;
} else {
outInfo.direction = RIGHT;
}
} else {
if (ry < 0) {
outInfo.direction = TOP;
index = 1;
} else {
outInfo.direction = BOTTOM;
}
}
outInfo.anchorIndex = index;
outInfo.targetParts[index] = childPart;
outInfo.targetParts[1 - index] = findClosestPart(childPart,
outInfo.direction);
return outInfo;
}
computeClosest(point, childPart, sTempClosests, sTempMinDists, TOP);
computeClosest(point, childPart, sTempClosests, sTempMinDists, LEFT);
computeClosest(point, childPart, sTempClosests, sTempMinDists, BOTTOM);
computeClosest(point, childPart, sTempClosests, sTempMinDists, RIGHT);
}
}
UiElementEditPart closest = null;
int minDist = Integer.MAX_VALUE;
int minDir = -1;
for (int i = 0; i <= MAX_DIR; i++) {
if (sTempClosests[i] != null && sTempMinDists[i] < minDist) {
closest = sTempClosests[i];
minDist = sTempMinDists[i];
minDir = i;
}
}
if (closest != null) {
int index = 0;
switch(minDir) {
case TOP:
case LEFT:
index = 0;
break;
case BOTTOM:
case RIGHT:
index = 1;
break;
}
outInfo.anchorIndex = index;
outInfo.targetParts[index] = closest;
outInfo.targetParts[1 - index] = findClosestPart(closest, sOppositeDirection[minDir]);
outInfo.direction = sOppositeDirection[minDir];
return outInfo;
}
return null;
}
/**
* Computes the highlight line for a drop on a RelativeLayout.
* <p/>
* The line is always placed on the side of the anchor part indicated by the
* direction. The direction always point from the anchor part to the drop point.
* <p/>
* If there's no anchor part, use the other one with a reversed direction.
* <p/>
* On output, this updates the {@link HighlightInfo}.
*/
private static void computeRelativeLine(UiLayoutEditPart parentPart,
RelativeInfo relInfo,
HighlightInfo highlightInfo) {
UiElementEditPart[] parts = relInfo.targetParts;
int dir = relInfo.direction;
int index = relInfo.anchorIndex;
UiElementEditPart part = parts[index];
if (part == null) {
dir = sOppositeDirection[dir];
part = parts[1 - index];
}
if (part == null) {
// give up if both parts are null
return;
}
Rectangle r = part.getBounds();
Point p = null;
switch(dir) {
case TOP:
p = r.getTop();
break;
case BOTTOM:
p = r.getBottom();
break;
case LEFT:
p = r.getLeft();
break;
case RIGHT:
p = r.getRight();
break;
}
highlightInfo.anchorPoint = p;
r = parentPart.getBounds();
switch(dir) {
case TOP:
case BOTTOM:
// horizontal line with middle anchor point
highlightInfo.tempPoints[0].setLocation(0, p.y);
highlightInfo.tempPoints[1].setLocation(r.width, p.y);
highlightInfo.linePoints = highlightInfo.tempPoints;
highlightInfo.anchorPoint = p;
break;
case LEFT:
case RIGHT:
// vertical line with middle anchor point
highlightInfo.tempPoints[0].setLocation(p.x, 0);
highlightInfo.tempPoints[1].setLocation(p.x, r.height);
highlightInfo.linePoints = highlightInfo.tempPoints;
highlightInfo.anchorPoint = p;
break;
}
}
/**
* Given a certain reference point (drop point), computes the distance to the given
* part in the given direction. For example if direction is top, only accepts parts which
* bottom is above the reference point, computes their distance and then updates the
* current minimal distances and current closest parts arrays accordingly.
*/
private static void computeClosest(Point refPoint,
UiElementEditPart compareToPart,
UiElementEditPart[] currClosests,
int[] currMinDists,
int direction) {
Rectangle r = compareToPart.getBounds();
Point p = null;
boolean usable = false;
switch(direction) {
case TOP:
p = r.getBottom();
usable = p.y <= refPoint.y;
break;
case BOTTOM:
p = r.getTop();
usable = p.y >= refPoint.y;
break;
case LEFT:
p = r.getRight();
usable = p.x <= refPoint.x;
break;
case RIGHT:
p = r.getLeft();
usable = p.x >= refPoint.x;
break;
}
if (usable) {
int d = p.getDistance2(refPoint);
if (d < currMinDists[direction]) {
currMinDists[direction] = d;
currClosests[direction] = compareToPart;
}
}
}
/**
* Given a reference parts, finds the closest part in the parent in the given direction.
* For example if direction is top, finds the closest sibling part which is above the
* reference part and non-overlapping (they can touch.)
*/
private static UiElementEditPart findClosestPart(UiElementEditPart referencePart,
int direction) {
if (referencePart == null || referencePart.getParent() == null) {
return null;
}
Rectangle r = referencePart.getBounds();
Point ref = null;
switch(direction) {
case TOP:
ref = r.getTop();
break;
case BOTTOM:
ref = r.getBottom();
break;
case LEFT:
ref = r.getLeft();
break;
case RIGHT:
ref = r.getRight();
break;
}
int minDist = Integer.MAX_VALUE;
UiElementEditPart closestPart = null;
for (Object childPart : referencePart.getParent().getChildren()) {
if (childPart != referencePart && childPart instanceof UiElementEditPart) {
r = ((UiElementEditPart) childPart).getBounds();
Point p = null;
boolean usable = false;
switch(direction) {
case TOP:
p = r.getBottom();
usable = p.y <= ref.y;
break;
case BOTTOM:
p = r.getTop();
usable = p.y >= ref.y;
break;
case LEFT:
p = r.getRight();
usable = p.x <= ref.x;
break;
case RIGHT:
p = r.getLeft();
usable = p.x >= ref.x;
break;
}
if (usable) {
int d = p.getDistance2(ref);
if (d < minDist) {
minDist = d;
closestPart = (UiElementEditPart) childPart;
}
}
}
}
return closestPart;
}
}