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