/* * 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; } } }