blob: b4bc86978b001daa6c9a7750528a9905bce9a67f [file] [log] [blame]
/*
* 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.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_GRAVITY;
import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING;
import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_IN_PARENT;
import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_VERTICAL;
import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
import static com.android.SdkConstants.VALUE_TRUE;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.api.DropFeedback;
import com.android.ide.common.api.IDragElement;
import com.android.ide.common.api.IGraphics;
import com.android.ide.common.api.IMenuCallback;
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.InsertType;
import com.android.ide.common.api.Point;
import com.android.ide.common.api.Rect;
import com.android.ide.common.api.RuleAction;
import com.android.ide.common.api.SegmentType;
import com.android.ide.common.layout.relative.ConstraintPainter;
import com.android.ide.common.layout.relative.DeletionHandler;
import com.android.ide.common.layout.relative.GuidelinePainter;
import com.android.ide.common.layout.relative.MoveHandler;
import com.android.ide.common.layout.relative.ResizeHandler;
import com.android.utils.Pair;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* An {@link IViewRule} for android.widget.RelativeLayout and all its derived
* classes.
*/
public class RelativeLayoutRule extends BaseLayoutRule {
private static final String ACTION_SHOW_STRUCTURE = "_structure"; //$NON-NLS-1$
private static final String ACTION_SHOW_CONSTRAINTS = "_constraints"; //$NON-NLS-1$
private static final String ACTION_CENTER_VERTICAL = "_centerVert"; //$NON-NLS-1$
private static final String ACTION_CENTER_HORIZONTAL = "_centerHoriz"; //$NON-NLS-1$
private static final URL ICON_CENTER_VERTICALLY =
RelativeLayoutRule.class.getResource("centerVertically.png"); //$NON-NLS-1$
private static final URL ICON_CENTER_HORIZONTALLY =
RelativeLayoutRule.class.getResource("centerHorizontally.png"); //$NON-NLS-1$
private static final URL ICON_SHOW_STRUCTURE =
BaseLayoutRule.class.getResource("structure.png"); //$NON-NLS-1$
private static final URL ICON_SHOW_CONSTRAINTS =
BaseLayoutRule.class.getResource("constraints.png"); //$NON-NLS-1$
public static boolean sShowStructure = false;
public static boolean sShowConstraints = true;
// ==== Selection ====
@Override
public List<String> getSelectionHint(@NonNull INode parentNode, @NonNull INode childNode) {
List<String> infos = new ArrayList<String>(18);
addAttr(ATTR_LAYOUT_ABOVE, childNode, infos);
addAttr(ATTR_LAYOUT_BELOW, childNode, infos);
addAttr(ATTR_LAYOUT_TO_LEFT_OF, childNode, infos);
addAttr(ATTR_LAYOUT_TO_RIGHT_OF, childNode, infos);
addAttr(ATTR_LAYOUT_ALIGN_BASELINE, childNode, infos);
addAttr(ATTR_LAYOUT_ALIGN_TOP, childNode, infos);
addAttr(ATTR_LAYOUT_ALIGN_BOTTOM, childNode, infos);
addAttr(ATTR_LAYOUT_ALIGN_LEFT, childNode, infos);
addAttr(ATTR_LAYOUT_ALIGN_RIGHT, childNode, infos);
addAttr(ATTR_LAYOUT_ALIGN_PARENT_TOP, childNode, infos);
addAttr(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, childNode, infos);
addAttr(ATTR_LAYOUT_ALIGN_PARENT_LEFT, childNode, infos);
addAttr(ATTR_LAYOUT_ALIGN_PARENT_RIGHT, childNode, infos);
addAttr(ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING, childNode, infos);
addAttr(ATTR_LAYOUT_CENTER_HORIZONTAL, childNode, infos);
addAttr(ATTR_LAYOUT_CENTER_IN_PARENT, childNode, infos);
addAttr(ATTR_LAYOUT_CENTER_VERTICAL, childNode, infos);
return infos;
}
private void addAttr(String propertyName, INode childNode, List<String> infos) {
String a = childNode.getStringAttr(ANDROID_URI, propertyName);
if (a != null && a.length() > 0) {
// Display the layout parameters without the leading layout_ prefix
// and id references without the @+id/ prefix
if (propertyName.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
propertyName = propertyName.substring(ATTR_LAYOUT_RESOURCE_PREFIX.length());
}
a = stripIdPrefix(a);
String s = propertyName + ": " + a;
infos.add(s);
}
}
@Override
public void paintSelectionFeedback(@NonNull IGraphics graphics, @NonNull INode parentNode,
@NonNull List<? extends INode> childNodes, @Nullable Object view) {
super.paintSelectionFeedback(graphics, parentNode, childNodes, view);
boolean showDependents = true;
if (sShowStructure) {
childNodes = Arrays.asList(parentNode.getChildren());
// Avoid painting twice - both as incoming and outgoing
showDependents = false;
} else if (!sShowConstraints) {
return;
}
ConstraintPainter.paintSelectionFeedback(graphics, parentNode, childNodes, showDependents);
}
// ==== Drag'n'drop support ====
@Override
public DropFeedback onDropEnter(@NonNull INode targetNode, @Nullable Object targetView,
@Nullable IDragElement[] elements) {
return new DropFeedback(new MoveHandler(targetNode, elements, mRulesEngine),
new GuidelinePainter());
}
@Override
public DropFeedback onDropMove(@NonNull INode targetNode, @NonNull IDragElement[] elements,
@Nullable DropFeedback feedback, @NonNull Point p) {
if (elements == null || elements.length == 0 || feedback == null) {
return null;
}
MoveHandler state = (MoveHandler) feedback.userData;
int offsetX = p.x + (feedback.dragBounds != null ? feedback.dragBounds.x : 0);
int offsetY = p.y + (feedback.dragBounds != null ? feedback.dragBounds.y : 0);
state.updateMove(feedback, elements, offsetX, offsetY, feedback.modifierMask);
// Or maybe only do this if the results changed...
feedback.requestPaint = true;
return feedback;
}
@Override
public void onDropLeave(@NonNull INode targetNode, @NonNull IDragElement[] elements,
@Nullable DropFeedback feedback) {
}
@Override
public void onDropped(final @NonNull INode targetNode, final @NonNull IDragElement[] elements,
final @Nullable DropFeedback feedback, final @NonNull Point p) {
if (feedback == null) {
return;
}
final MoveHandler state = (MoveHandler) feedback.userData;
final Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements,
feedback.isCopy || !feedback.sameCanvas);
targetNode.editXml("Dropped", new INodeHandler() {
@Override
public void handle(@NonNull INode n) {
int index = -1;
// Remove cycles
state.removeCycles();
// Now write the new elements.
INode previous = null;
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, BaseLayoutRule.DEFAULT_ATTR_FILTER);
addInnerElements(newChild, element, idMap);
if (previous == null) {
state.applyConstraints(newChild);
previous = newChild;
} else {
// Arrange the nodes next to each other, depending on which
// edge we are attaching to. For example, if attaching to the
// top edge, arrange the subsequent nodes in a column below it.
//
// TODO: Try to do something smarter here where we detect
// constraints between the dragged edges, and we preserve these.
// We have to do this carefully though because if the
// constraints go through some other nodes not part of the
// selection, this doesn't work right, and you might be
// dragging several connected components, which we'd then
// need to stitch together such that they are all visible.
state.attachPrevious(previous, newChild);
previous = newChild;
}
}
}
});
}
@Override
public void onChildInserted(@NonNull INode node, @NonNull INode parent,
@NonNull InsertType insertType) {
// TODO: Handle more generically some way to ensure that widgets with no
// intrinsic size get some minimum size until they are attached on multiple
// opposing sides.
//String fqcn = node.getFqcn();
//if (fqcn.equals(FQCN_EDIT_TEXT)) {
// node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, "100dp"); //$NON-NLS-1$
//}
}
@Override
public void onRemovingChildren(@NonNull List<INode> deleted, @NonNull INode parent,
boolean moved) {
super.onRemovingChildren(deleted, parent, moved);
if (!moved) {
DeletionHandler handler = new DeletionHandler(deleted, Collections.<INode>emptyList(),
parent);
handler.updateConstraints();
}
}
// ==== Resize Support ====
@Override
public DropFeedback onResizeBegin(@NonNull INode child, @NonNull INode parent,
@Nullable SegmentType horizontalEdgeType, @Nullable SegmentType verticalEdgeType,
@Nullable Object childView, @Nullable Object parentView) {
ResizeHandler state = new ResizeHandler(parent, child, mRulesEngine,
horizontalEdgeType, verticalEdgeType);
return new DropFeedback(state, new GuidelinePainter());
}
@Override
public void onResizeUpdate(@Nullable DropFeedback feedback, @NonNull INode child,
@NonNull INode parent, @NonNull Rect newBounds,
int modifierMask) {
if (feedback == null) {
return;
}
ResizeHandler state = (ResizeHandler) feedback.userData;
state.updateResize(feedback, child, newBounds, modifierMask);
}
@Override
public void onResizeEnd(@Nullable DropFeedback feedback, @NonNull INode child,
@NonNull INode parent, final @NonNull Rect newBounds) {
if (feedback == null) {
return;
}
final ResizeHandler state = (ResizeHandler) feedback.userData;
child.editXml("Resize", new INodeHandler() {
@Override
public void handle(@NonNull INode n) {
state.removeCycles();
state.applyConstraints(n);
}
});
}
// ==== Layout Actions Bar ====
@Override
public void addLayoutActions(
@NonNull List<RuleAction> actions,
final @NonNull INode parentNode,
final @NonNull List<? extends INode> children) {
super.addLayoutActions(actions, parentNode, children);
actions.add(createGravityAction(Collections.<INode>singletonList(parentNode),
ATTR_GRAVITY));
actions.add(RuleAction.createSeparator(25));
actions.add(createMarginAction(parentNode, children));
IMenuCallback callback = new IMenuCallback() {
@Override
public void action(@NonNull RuleAction action,
@NonNull List<? extends INode> selectedNodes,
final @Nullable String valueId,
final @Nullable Boolean newValue) {
final String id = action.getId();
if (id.equals(ACTION_CENTER_VERTICAL)|| id.equals(ACTION_CENTER_HORIZONTAL)) {
parentNode.editXml("Center", new INodeHandler() {
@Override
public void handle(@NonNull INode n) {
if (id.equals(ACTION_CENTER_VERTICAL)) {
for (INode child : children) {
centerVertically(child);
}
} else if (id.equals(ACTION_CENTER_HORIZONTAL)) {
for (INode child : children) {
centerHorizontally(child);
}
}
mRulesEngine.redraw();
}
});
} else if (id.equals(ACTION_SHOW_CONSTRAINTS)) {
sShowConstraints = !sShowConstraints;
mRulesEngine.redraw();
} else {
assert id.equals(ACTION_SHOW_STRUCTURE);
sShowStructure = !sShowStructure;
mRulesEngine.redraw();
}
}
};
// Centering actions
if (children != null && children.size() > 0) {
actions.add(RuleAction.createSeparator(150));
actions.add(RuleAction.createAction(ACTION_CENTER_VERTICAL, "Center Vertically",
callback, ICON_CENTER_VERTICALLY, 160, false));
actions.add(RuleAction.createAction(ACTION_CENTER_HORIZONTAL, "Center Horizontally",
callback, ICON_CENTER_HORIZONTALLY, 170, false));
}
actions.add(RuleAction.createSeparator(80));
actions.add(RuleAction.createToggle(ACTION_SHOW_CONSTRAINTS, "Show Constraints",
sShowConstraints, callback, ICON_SHOW_CONSTRAINTS, 180, false));
actions.add(RuleAction.createToggle(ACTION_SHOW_STRUCTURE, "Show All Relationships",
sShowStructure, callback, ICON_SHOW_STRUCTURE, 190, false));
}
private void centerHorizontally(INode node) {
// Clear horizontal-oriented attributes from the node
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_LEFT, null);
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_LEFT, null);
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF, null);
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_RIGHT, null);
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_RIGHT, null);
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_LEFT_OF, null);
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT))) {
// Already done
} else if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI,
ATTR_LAYOUT_CENTER_VERTICAL))) {
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null);
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT, VALUE_TRUE);
} else {
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, VALUE_TRUE);
}
}
private void centerVertically(INode node) {
// Clear vertical-oriented attributes from the node
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_TOP, null);
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_TOP, null);
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_BELOW, null);
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, null);
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BOTTOM, null);
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ABOVE, null);
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null);
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BASELINE, null);
// Center vertically
if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT))) {
// ALready done
} else if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI,
ATTR_LAYOUT_CENTER_HORIZONTAL))) {
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT, VALUE_TRUE);
} else {
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, VALUE_TRUE);
}
}
}