/*
* Copyright (C) 2010 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.common.layout;
import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
import static com.android.ide.common.layout.LayoutConstants.ATTR_GRAVITY;
import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX;
import static com.android.ide.common.layout.LayoutConstants.VALUE_ABOVE;
import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_BASELINE;
import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_BOTTOM;
import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_LEFT;
import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_PARENT_BOTTOM;
import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_PARENT_LEFT;
import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_PARENT_RIGHT;
import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_PARENT_TOP;
import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_RIGHT;
import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_TOP;
import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_WITH_PARENT_MISSING;
import static com.android.ide.common.layout.LayoutConstants.VALUE_BELOW;
import static com.android.ide.common.layout.LayoutConstants.VALUE_CENTER_HORIZONTAL;
import static com.android.ide.common.layout.LayoutConstants.VALUE_CENTER_IN_PARENT;
import static com.android.ide.common.layout.LayoutConstants.VALUE_CENTER_VERTICAL;
import static com.android.ide.common.layout.LayoutConstants.VALUE_TO_LEFT_OF;
import static com.android.ide.common.layout.LayoutConstants.VAUE_TO_RIGHT_OF;
import com.android.ide.common.api.DrawingStyle;
import com.android.ide.common.api.DropFeedback;
import com.android.ide.common.api.IAttributeInfo;
import com.android.ide.common.api.IDragElement;
import com.android.ide.common.api.IFeedbackPainter;
import com.android.ide.common.api.IGraphics;
import com.android.ide.common.api.INode;
import com.android.ide.common.api.INodeHandler;
import com.android.ide.common.api.IViewRule;
import com.android.ide.common.api.MenuAction;
import com.android.ide.common.api.Point;
import com.android.ide.common.api.Rect;
import com.android.ide.common.api.IAttributeInfo.Format;
import com.android.ide.common.api.INode.IAttribute;
import com.android.util.Pair;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* An {@link IViewRule} for android.widget.RelativeLayout and all its derived
* classes.
*/
public class RelativeLayoutRule extends BaseLayoutRule {
// ==== Selection ====
@Override
public List<String> getSelectionHint(INode parentNode, INode childNode) {
List<String> infos = new ArrayList<String>(18);
addAttr(VALUE_ABOVE, childNode, infos);
addAttr(VALUE_BELOW, childNode, infos);
addAttr(VALUE_TO_LEFT_OF, childNode, infos);
addAttr(VAUE_TO_RIGHT_OF, childNode, infos);
addAttr(VALUE_ALIGN_BASELINE, childNode, infos);
addAttr(VALUE_ALIGN_TOP, childNode, infos);
addAttr(VALUE_ALIGN_BOTTOM, childNode, infos);
addAttr(VALUE_ALIGN_LEFT, childNode, infos);
addAttr(VALUE_ALIGN_RIGHT, childNode, infos);
addAttr(VALUE_ALIGN_PARENT_TOP, childNode, infos);
addAttr(VALUE_ALIGN_PARENT_BOTTOM, childNode, infos);
addAttr(VALUE_ALIGN_PARENT_LEFT, childNode, infos);
addAttr(VALUE_ALIGN_PARENT_RIGHT, childNode, infos);
addAttr(VALUE_ALIGN_WITH_PARENT_MISSING, childNode, infos);
addAttr(VALUE_CENTER_HORIZONTAL, childNode, infos);
addAttr(VALUE_CENTER_IN_PARENT, childNode, infos);
addAttr(VALUE_CENTER_VERTICAL, childNode, infos);
return infos;
}
private void addAttr(String propertyName, INode childNode, List<String> infos) {
String a = childNode.getStringAttr(ANDROID_URI, ATTR_LAYOUT_PREFIX + propertyName);
if (a != null && a.length() > 0) {
String s = propertyName + ": " + a;
infos.add(s);
}
}
// ==== Drag'n'drop support ====
@Override
public DropFeedback onDropEnter(INode targetNode, final IDragElement[] elements) {
if (elements.length == 0) {
return null;
}
Rect bn = targetNode.getBounds();
if (!bn.isValid()) {
return null;
}
// Collect the ids of the elements being dragged
List<String> movedIds = new ArrayList<String>(collectIds(
new HashMap<String, Pair<String, String>>(), elements).keySet());
// Prepare the drop feedback
return new DropFeedback(new RelativeDropData(movedIds), new IFeedbackPainter() {
public void paint(IGraphics gc, INode node, DropFeedback feedback) {
drawRelativeDropFeedback(gc, node, elements, feedback);
}
});
}
@Override
public DropFeedback onDropMove(INode targetNode, IDragElement[] elements,
DropFeedback feedback, Point p) {
RelativeDropData data = (RelativeDropData) feedback.userData;
Rect area = feedback.captureArea;
// Only look for a new child if cursor is no longer under the current
// rect
if (area == null || !area.contains(p)) {
// We're not capturing anymore since we got outside of the capture
// bounds
feedback.captureArea = null;
feedback.requestPaint = false;
data.setRejected(null);
// Find the current direct children under the cursor
INode childNode = null;
int childIndex = -1;
nextChild: for (INode child : targetNode.getChildren()) {
childIndex++;
Rect bc = child.getBounds();
if (bc.contains(p)) {
// If we're doing a move operation within the same canvas,
// we can't attach the moved object to one belonging to the
// selection since it will disappear after the move.
if (feedback.sameCanvas && !feedback.isCopy) {
for (IDragElement element : elements) {
if (bc.equals(element.getBounds())) {
data.setRejected(bc);
feedback.requestPaint = true;
continue nextChild;
}
}
}
// One more limitation: if we're moving one or more
// elements, we can't drop them on a child which relative
// position is expressed directly or indirectly based on the
// element being moved.
if (!feedback.isCopy) {
if (searchRelativeIds(child, data.getMovedIds(),
data.getCachedLinkIds())) {
data.setRejected(bc);
feedback.requestPaint = true;
continue nextChild;
}
}
childNode = child;
break;
}
}
// If there is a selected child and it changed, recompute child drop
// zones
if (childNode != null && childNode != data.getChild()) {
data.setChild(childNode);
data.setIndex(childIndex);
data.setCurr(null);
data.setZones(null);
Pair<Rect, List<DropZone>> result = computeChildDropZones(childNode);
data.setZones(result.getSecond());
// Capture this rect, to prevent the engine from switching the
// layout node.
feedback.captureArea = result.getFirst();
feedback.requestPaint = true;
} else if (childNode == null) {
// If there is no selected child, compute the border drop zone
data.setChild(null);
data.setIndex(-1);
data.setCurr(null);
DropZone zone = computeBorderDropZone(targetNode, p, feedback);
if (zone == null) {
data.setZones(null);
} else {
data.setZones(Collections.singletonList(zone));
feedback.captureArea = zone.getRect();
}
feedback.requestPaint |= (area == null || !area.equals(feedback.captureArea));
}
}
// Find the current zone
DropZone currZone = null;
if (data.getZones() != null) {
for (DropZone zone : data.getZones()) {
if (zone.getRect().contains(p)) {
currZone = zone;
break;
}
}
// Look to see if there's a border match if we didn't find anything better;
// a border match isn't required to have the mouse cursor within it since we
// do edge matching in the code which -adds- the border zones.
if (currZone == null && feedback.dragBounds != null) {
for (DropZone zone : data.getZones()) {
if (zone.isBorderZone()) {
currZone = zone;
break;
}
}
}
}
// Look for border match when there are no children: always offer one in this case
if (currZone == null && targetNode.getChildren().length == 0 && data.getZones() != null
&& data.getZones().size() > 0) {
currZone = data.getZones().get(0);
}
if (currZone != data.getCurr()) {
data.setCurr(currZone);
feedback.requestPaint = true;
}
feedback.invalidTarget = (currZone == null);
return feedback;
}
/**
* Returns true if the child has any attribute of Format.REFERENCE which
* value matches one of the ids in movedIds.
*/
private boolean searchRelativeIds(INode node, List<String> movedIds,
Map<INode, Set<String>> cachedLinkIds) {
Set<String> ids = getLinkedIds(node, cachedLinkIds);
for (String id : ids) {
if (movedIds.contains(id)) {
return true;
}
}
return false;
}
private Set<String> getLinkedIds(INode node, Map<INode, Set<String>> cachedLinkIds) {
Set<String> ids = cachedLinkIds.get(node);
if (ids != null) {
return ids;
}
// We don't have cached data on this child, so create a list of
// all the linked id it is referencing.
ids = new HashSet<String>();
cachedLinkIds.put(node, ids);
for (IAttribute attr : node.getLiveAttributes()) {
IAttributeInfo attrInfo = node.getAttributeInfo(attr.getUri(), attr.getName());
if (attrInfo == null) {
continue;
}
Format[] formats = attrInfo.getFormats();
if (!IAttributeInfo.Format.REFERENCE.in(formats)) {
continue;
}
String id = attr.getValue();
id = normalizeId(id);
if (ids.contains(id)) {
continue;
}
ids.add(id);
// Find the sibling with that id
INode p = node.getParent();
if (p == null) {
continue;
}
for (INode child : p.getChildren()) {
if (child == node) {
continue;
}
String childId = child.getStringAttr(ANDROID_URI, ATTR_ID);
if (childId == null) {
continue;
}
childId = normalizeId(childId);
if (id.equals(childId)) {
Set<String> linkedIds = getLinkedIds(child, cachedLinkIds);
ids.addAll(linkedIds);
break;
}
}
}
return ids;
}
private DropZone computeBorderDropZone(INode targetNode, Point p, DropFeedback feedback) {
Rect bounds = targetNode.getBounds();
int x = p.x;
int y = p.y;
int x1 = bounds.x;
int y1 = bounds.y;
int w = bounds.w;
int h = bounds.h;
int x2 = x1 + w;
int y2 = y1 + h;
// Default border zone size
int n = 10;
int n2 = 2*n;
// Size of -matched- border zone (not painted, but we detect edge overlaps here)
int hn = 0;
int vn = 0;
if (feedback.dragBounds != null) {
hn = feedback.dragBounds.w / 2;
vn = feedback.dragBounds.h / 2;
}
boolean vertical = false;
Rect r = null;
String attr = null;
if (x <= x1 + n + hn && y >= y1 && y <= y2) {
r = new Rect(x1 - n, y1, n2, h);
attr = VALUE_ALIGN_PARENT_LEFT;
vertical = true;
} else if (x >= x2 - hn - n && y >= y1 && y <= y2) {
r = new Rect(x2 - n, y1, n2, h);
attr = VALUE_ALIGN_PARENT_RIGHT;
vertical = true;
} else if (y <= y1 + n + vn && x >= x1 && x <= x2) {
r = new Rect(x1, y1 - n, w, n2);
attr = VALUE_ALIGN_PARENT_TOP;
} else if (y >= y2 - vn - n && x >= x1 && x <= x2) {
r = new Rect(x1, y2 - n, w, n2);
attr = VALUE_ALIGN_PARENT_BOTTOM;
} else {
// We're nowhere near a border.
// If there are no children, we will offer one anyway:
if (targetNode.getChildren().length == 0) {
r = new Rect(x1 - n, y1, n2, h);
attr = VALUE_ALIGN_PARENT_LEFT;
vertical = true;
} else {
return null;
}
}
return new DropZone(r, Collections.singletonList(attr), r.getCenter(), vertical);
}
private Pair<Rect, List<DropZone>> computeChildDropZones(INode childNode) {
Rect b = childNode.getBounds();
// Compute drop zone borders as follow:
//
// +---+-----+-----+-----+---+
// | 1 \ 2 \ 3 / 4 / 5 |
// +----+-----+---+-----+----+
//
// For the top and bottom borders, zones 1 and 5 have the same width,
// which is
// size1 = min(10, w/5)
// and zones 2, 3 and 4 have a width of
// size2 = (w - 2*size) / 3
//
// Same works for left and right borders vertically.
//
// Attributes generated:
// Horizontally:
// 1- toLeftOf / 2- alignLeft / 3- 2+4 / 4- alignRight / 5- toRightOf
// Vertically:
// 1- above / 2-alignTop / 3- 2+4 / 4- alignBottom / 5- below
int w1 = 20;
int w3 = b.w / 3;
int w2 = Math.max(20, w3);
int h1 = 20;
int h3 = b.h / 3;
int h2 = Math.max(20, h3);
int wt = w1 * 2 + w2 * 3;
int ht = h1 * 2 + h2 * 3;
int x1 = b.x + ((b.w - wt) / 2);
int y1 = b.y + ((b.h - ht) / 2);
Rect bounds = new Rect(x1, y1, wt, ht);
List<DropZone> zones = new ArrayList<DropZone>(16);
String a = VALUE_ABOVE;
int x = x1;
int y = y1;
x = addx(w1, a, x, y, h1, zones, VALUE_TO_LEFT_OF);
x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_LEFT);
x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_LEFT, VALUE_ALIGN_RIGHT);
x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_RIGHT);
x = addx(w1, a, x, y, h1, zones, VAUE_TO_RIGHT_OF);
a = VALUE_BELOW;
x = x1;
y = y1 + ht - h1;
x = addx(w1, a, x, y, h1, zones, VALUE_TO_LEFT_OF);
x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_LEFT);
x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_LEFT, VALUE_ALIGN_RIGHT);
x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_RIGHT);
x = addx(w1, a, x, y, h1, zones, VAUE_TO_RIGHT_OF);
a = VALUE_TO_LEFT_OF;
x = x1;
y = y1 + h1;
y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_TOP);
y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_TOP, VALUE_ALIGN_BOTTOM);
y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_BOTTOM);
a = VAUE_TO_RIGHT_OF;
x = x1 + wt - w1;
y = y1 + h1;
y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_TOP);
y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_TOP, VALUE_ALIGN_BOTTOM);
y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_BOTTOM);
return Pair.of(bounds, zones);
}
private int addx(int wn, String a, int x, int y, int h1, List<DropZone> zones, String... a2) {
Rect rect = new Rect(x, y, wn, h1);
List<String> attrs = new ArrayList<String>(a2.length + 1);
attrs.add(a);
for (String attribute : a2) {
attrs.add(attribute);
}
zones.add(new DropZone(rect, attrs));
return x + wn;
}
private int addy(int hn, String a, int x, int y, int w1, List<DropZone> zones, String... a2) {
Rect rect = new Rect(x, y, w1, hn);
List<String> attrs = new ArrayList<String>(a2.length + 1);
attrs.add(a);
for (String attribute : a2) {
attrs.add(attribute);
}
zones.add(new DropZone(rect, attrs));
return y + hn;
}
private void drawRelativeDropFeedback(IGraphics gc, INode targetNode, IDragElement[] elements,
DropFeedback feedback) {
Rect b = targetNode.getBounds();
if (!b.isValid()) {
return;
}
gc.useStyle(DrawingStyle.DROP_RECIPIENT);
gc.drawRect(b);
gc.useStyle(DrawingStyle.DROP_ZONE);
RelativeDropData data = (RelativeDropData) feedback.userData;
if (data.getZones() != null) {
for (DropZone it : data.getZones()) {
gc.drawRect(it.getRect());
}
}
if (data.getCurr() != null) {
gc.useStyle(DrawingStyle.DROP_ZONE_ACTIVE);
gc.fillRect(data.getCurr().getRect());
Rect r = feedback.captureArea;
int x = r.x + 5;
int y = r.y + r.h + 5;
String id = null;
if (data.getChild() != null) {
id = data.getChild().getStringAttr(ANDROID_URI, ATTR_ID);
}
// Print constraints (with id appended if applicable)
gc.useStyle(DrawingStyle.HELP);
List<String> strings = new ArrayList<String>();
for (String it : data.getCurr().getAttr()) {
strings.add(id != null && id.length() > 0 ? it + "=" + id : it);
}
gc.drawBoxedStrings(x, y, strings);
Point mark = data.getCurr().getMark();
if (mark != null) {
gc.useStyle(DrawingStyle.DROP_PREVIEW);
Rect nr = data.getCurr().getRect();
int nx = nr.x + nr.w / 2;
int ny = nr.y + nr.h / 2;
boolean vertical = data.getCurr().isVertical();
if (vertical) {
gc.drawLine(nx, nr.y, nx, nr.y + nr.h);
x = nx;
y = b.y;
} else {
gc.drawLine(nr.x, ny, nr.x + nr.w, ny);
x = b.x;
y = ny;
}
} else {
r = data.getCurr().getRect();
x = r.x + r.w / 2;
y = r.y + r.h / 2;
}
Rect be = elements[0].getBounds();
// Draw bound rectangles for all selected items
gc.useStyle(DrawingStyle.DROP_PREVIEW);
for (IDragElement element : elements) {
be = element.getBounds();
if (!be.isValid()) {
// We don't always have bounds - for example when dragging
// from the palette.
continue;
}
int offsetX = x - be.x;
int offsetY = y - be.y;
if (data.getCurr().getAttr().contains(VALUE_ALIGN_TOP)
&& data.getCurr().getAttr().contains(VALUE_ALIGN_BOTTOM)) {
offsetY -= be.h / 2;
} else if (data.getCurr().getAttr().contains(VALUE_ABOVE)
|| data.getCurr().getAttr().contains(VALUE_ALIGN_TOP)
|| data.getCurr().getAttr().contains(VALUE_ALIGN_PARENT_BOTTOM)) {
offsetY -= be.h;
}
if (data.getCurr().getAttr().contains(VALUE_ALIGN_RIGHT)
&& data.getCurr().getAttr().contains(VALUE_ALIGN_LEFT)) {
offsetX -= be.w / 2;
} else if (data.getCurr().getAttr().contains(VALUE_TO_LEFT_OF)
|| data.getCurr().getAttr().contains(VALUE_ALIGN_LEFT)
|| data.getCurr().getAttr().contains(VALUE_ALIGN_PARENT_RIGHT)) {
offsetX -= be.w;
}
drawElement(gc, element, offsetX, offsetY);
}
}
if (data.getRejected() != null) {
Rect br = data.getRejected();
gc.useStyle(DrawingStyle.INVALID);
gc.fillRect(br);
gc.drawLine(br.x, br.y, br.x + br.w, br.y + br.h);
gc.drawLine(br.x, br.y + br.h, br.x + br.w, br.y);
}
}
@Override
public void onDropLeave(INode targetNode, IDragElement[] elements, DropFeedback feedback) {
// Free the last captured rect, if any
feedback.captureArea = null;
}
@Override
public void onDropped(final INode targetNode, final IDragElement[] elements,
final DropFeedback feedback, final Point p) {
final RelativeDropData data = (RelativeDropData) feedback.userData;
if (data.getCurr() == null) {
return;
}
// Collect IDs from dropped elements and remap them to new IDs
// if this is a copy or from a different canvas.
final Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements,
feedback.isCopy || !feedback.sameCanvas);
targetNode.editXml("Add elements to RelativeLayout", new INodeHandler() {
public void handle(INode node) {
int index = data.getIndex();
// Now write the new elements.
for (IDragElement element : elements) {
String fqcn = element.getFqcn();
// index==-1 means to insert at the end.
// Otherwise increment the insertion position.
if (index >= 0) {
index++;
}
INode newChild = targetNode.insertChildAt(fqcn, index);
// Copy all the attributes, modifying them as needed.
addAttributes(newChild, element, idMap, DEFAULT_ATTR_FILTER);
// TODO... seems totally wrong. REVISIT or EXPLAIN
String id = null;
if (data.getChild() != null) {
id = data.getChild().getStringAttr(ANDROID_URI, ATTR_ID);
}
for (String it : data.getCurr().getAttr()) {
newChild.setAttribute(ANDROID_URI,
ATTR_LAYOUT_PREFIX + it, id != null ? id : "true"); //$NON-NLS-1$
}
addInnerElements(newChild, element, idMap);
}
}
});
}
@Override
public void addLayoutActions(List<MenuAction> actions, final INode parentNode,
final List<? extends INode> children) {
super.addLayoutActions(actions, parentNode, children);
actions.add(createGravityAction(Collections.<INode>singletonList(parentNode),
ATTR_GRAVITY));
actions.add(MenuAction.createSeparator(25));
actions.add(createMarginAction(parentNode, children));
}
/**
* Internal state used by the RelativeLayoutRule, stored as userData in the
* {@link DropFeedback}.
*/
private static class RelativeDropData {
/** Current child under cursor */
private INode mChild;
/** Index of child in the parent children list */
private int mIndex;
/**
* Valid "anchor" zones for the current child of type
*/
private List<DropZone> mZones;
/** Current zone */
private DropZone mCurr;
/** rejected target (Rect bounds) */
private Rect mRejected;
private List<String> mMovedIds;
private Map<INode, Set<String>> mCachedLinkIds = new HashMap<INode, Set<String>>();
public RelativeDropData(List<String> movedIds) {
this.mMovedIds = movedIds;
}
private void setChild(INode child) {
this.mChild = child;
}
private INode getChild() {
return mChild;
}
private void setIndex(int index) {
this.mIndex = index;
}
private int getIndex() {
return mIndex;
}
private void setZones(List<DropZone> zones) {
this.mZones = zones;
}
private List<DropZone> getZones() {
return mZones;
}
private void setCurr(DropZone curr) {
this.mCurr = curr;
}
private DropZone getCurr() {
return mCurr;
}
private void setRejected(Rect rejected) {
this.mRejected = rejected;
}
private Rect getRejected() {
return mRejected;
}
private List<String> getMovedIds() {
return mMovedIds;
}
private Map<INode, Set<String>> getCachedLinkIds() {
return mCachedLinkIds;
}
}
private static class DropZone {
/** The rectangular bounds of the drop zone */
private final Rect mRect;
/**
* Attributes that correspond to this drop zone, e.g. ["alignLeft",
* "alignBottom"]
*/
private final List<String> mAttr;
/** Non-null iff this is a border */
private final Point mMark;
/** Defined iff this is a border match */
private final boolean mVertical;
public DropZone(Rect rect, List<String> attr, Point mark, boolean vertical) {
super();
this.mRect = rect;
this.mAttr = attr;
this.mMark = mark;
this.mVertical = vertical;
}
public DropZone(Rect rect, List<String> attr) {
this(rect, attr, null, false);
}
private Rect getRect() {
return mRect;
}
private List<String> getAttr() {
return mAttr;
}
private Point getMark() {
return mMark;
}
private boolean isVertical() {
return mVertical;
}
private boolean isBorderZone() {
return mMark != null;
}
}
}