• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.ide.common.layout;
18 
19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
20 import static com.android.ide.common.layout.LayoutConstants.ATTR_GRAVITY;
21 import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
22 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ABOVE;
23 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE;
24 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
25 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_LEFT;
26 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
27 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
28 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
29 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
30 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_RIGHT;
31 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_TOP;
32 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING;
33 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_BELOW;
34 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
35 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_IN_PARENT;
36 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL;
37 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX;
38 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF;
39 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF;
40 import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX;
41 import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
42 import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE;
43 
44 import com.android.ide.common.api.DropFeedback;
45 import com.android.ide.common.api.IDragElement;
46 import com.android.ide.common.api.IGraphics;
47 import com.android.ide.common.api.IMenuCallback;
48 import com.android.ide.common.api.INode;
49 import com.android.ide.common.api.INode.IAttribute;
50 import com.android.ide.common.api.INodeHandler;
51 import com.android.ide.common.api.IViewRule;
52 import com.android.ide.common.api.InsertType;
53 import com.android.ide.common.api.Point;
54 import com.android.ide.common.api.Rect;
55 import com.android.ide.common.api.RuleAction;
56 import com.android.ide.common.api.SegmentType;
57 import com.android.ide.common.layout.relative.ConstraintPainter;
58 import com.android.ide.common.layout.relative.GuidelinePainter;
59 import com.android.ide.common.layout.relative.MoveHandler;
60 import com.android.ide.common.layout.relative.ResizeHandler;
61 import com.android.util.Pair;
62 
63 import java.net.URL;
64 import java.util.ArrayList;
65 import java.util.Arrays;
66 import java.util.Collections;
67 import java.util.HashSet;
68 import java.util.List;
69 import java.util.Map;
70 import java.util.Set;
71 
72 /**
73  * An {@link IViewRule} for android.widget.RelativeLayout and all its derived
74  * classes.
75  */
76 public class RelativeLayoutRule extends BaseLayoutRule {
77     private static final String ACTION_SHOW_STRUCTURE = "_structure"; //$NON-NLS-1$
78     private static final String ACTION_SHOW_CONSTRAINTS = "_constraints"; //$NON-NLS-1$
79     private static final String ACTION_CENTER_VERTICAL = "_centerVert"; //$NON-NLS-1$
80     private static final String ACTION_CENTER_HORIZONTAL = "_centerHoriz"; //$NON-NLS-1$
81     private static final URL ICON_CENTER_VERTICALLY =
82         RelativeLayoutRule.class.getResource("centerVertically.png"); //$NON-NLS-1$
83     private static final URL ICON_CENTER_HORIZONTALLY =
84         RelativeLayoutRule.class.getResource("centerHorizontally.png"); //$NON-NLS-1$
85     private static final URL ICON_SHOW_STRUCTURE =
86         BaseLayoutRule.class.getResource("structure.png"); //$NON-NLS-1$
87     private static final URL ICON_SHOW_CONSTRAINTS =
88         BaseLayoutRule.class.getResource("constraints.png"); //$NON-NLS-1$
89 
90     public static boolean sShowStructure = false;
91     public static boolean sShowConstraints = true;
92 
93     // ==== Selection ====
94 
95     @Override
getSelectionHint(INode parentNode, INode childNode)96     public List<String> getSelectionHint(INode parentNode, INode childNode) {
97         List<String> infos = new ArrayList<String>(18);
98         addAttr(ATTR_LAYOUT_ABOVE, childNode, infos);
99         addAttr(ATTR_LAYOUT_BELOW, childNode, infos);
100         addAttr(ATTR_LAYOUT_TO_LEFT_OF, childNode, infos);
101         addAttr(ATTR_LAYOUT_TO_RIGHT_OF, childNode, infos);
102         addAttr(ATTR_LAYOUT_ALIGN_BASELINE, childNode, infos);
103         addAttr(ATTR_LAYOUT_ALIGN_TOP, childNode, infos);
104         addAttr(ATTR_LAYOUT_ALIGN_BOTTOM, childNode, infos);
105         addAttr(ATTR_LAYOUT_ALIGN_LEFT, childNode, infos);
106         addAttr(ATTR_LAYOUT_ALIGN_RIGHT, childNode, infos);
107         addAttr(ATTR_LAYOUT_ALIGN_PARENT_TOP, childNode, infos);
108         addAttr(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, childNode, infos);
109         addAttr(ATTR_LAYOUT_ALIGN_PARENT_LEFT, childNode, infos);
110         addAttr(ATTR_LAYOUT_ALIGN_PARENT_RIGHT, childNode, infos);
111         addAttr(ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING, childNode, infos);
112         addAttr(ATTR_LAYOUT_CENTER_HORIZONTAL, childNode, infos);
113         addAttr(ATTR_LAYOUT_CENTER_IN_PARENT, childNode, infos);
114         addAttr(ATTR_LAYOUT_CENTER_VERTICAL, childNode, infos);
115 
116         return infos;
117     }
118 
addAttr(String propertyName, INode childNode, List<String> infos)119     private void addAttr(String propertyName, INode childNode, List<String> infos) {
120         String a = childNode.getStringAttr(ANDROID_URI, propertyName);
121         if (a != null && a.length() > 0) {
122             // Display the layout parameters without the leading layout_ prefix
123             // and id references without the @+id/ prefix
124             if (propertyName.startsWith(ATTR_LAYOUT_PREFIX)) {
125                 propertyName = propertyName.substring(ATTR_LAYOUT_PREFIX.length());
126             }
127             a = stripIdPrefix(a);
128             String s = propertyName + ": " + a;
129             infos.add(s);
130         }
131     }
132 
133     @Override
paintSelectionFeedback(IGraphics graphics, INode parentNode, List<? extends INode> childNodes, Object view)134     public void paintSelectionFeedback(IGraphics graphics, INode parentNode,
135             List<? extends INode> childNodes, Object view) {
136         super.paintSelectionFeedback(graphics, parentNode, childNodes, view);
137 
138         boolean showDependents = true;
139         if (sShowStructure) {
140             childNodes = Arrays.asList(parentNode.getChildren());
141             // Avoid painting twice - both as incoming and outgoing
142             showDependents = false;
143         } else if (!sShowConstraints) {
144             return;
145         }
146 
147         ConstraintPainter.paintSelectionFeedback(graphics, parentNode, childNodes, showDependents);
148     }
149 
150     // ==== Drag'n'drop support ====
151 
152     @Override
onDropEnter(INode targetNode, Object targetView, IDragElement[] elements)153     public DropFeedback onDropEnter(INode targetNode, Object targetView, IDragElement[] elements) {
154         return new DropFeedback(new MoveHandler(targetNode, elements, mRulesEngine),
155                 new GuidelinePainter());
156     }
157 
158     @Override
onDropMove(INode targetNode, IDragElement[] elements, DropFeedback feedback, Point p)159     public DropFeedback onDropMove(INode targetNode, IDragElement[] elements,
160             DropFeedback feedback, Point p) {
161         if (elements == null || elements.length == 0) {
162             return null;
163         }
164 
165         MoveHandler state = (MoveHandler) feedback.userData;
166         int offsetX = p.x + (feedback.dragBounds != null ? feedback.dragBounds.x : 0);
167         int offsetY = p.y + (feedback.dragBounds != null ? feedback.dragBounds.y : 0);
168         state.updateMove(feedback, elements, offsetX, offsetY, feedback.modifierMask);
169 
170         // Or maybe only do this if the results changed...
171         feedback.requestPaint = true;
172 
173         return feedback;
174     }
175 
176     @Override
onDropLeave(INode targetNode, IDragElement[] elements, DropFeedback feedback)177     public void onDropLeave(INode targetNode, IDragElement[] elements, DropFeedback feedback) {
178     }
179 
180     @Override
onDropped(final INode targetNode, final IDragElement[] elements, final DropFeedback feedback, final Point p)181     public void onDropped(final INode targetNode, final IDragElement[] elements,
182             final DropFeedback feedback, final Point p) {
183         final MoveHandler state = (MoveHandler) feedback.userData;
184 
185         final Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements,
186                 feedback.isCopy || !feedback.sameCanvas);
187 
188         targetNode.editXml("Dropped", new INodeHandler() {
189             @Override
190             public void handle(INode n) {
191                 int index = -1;
192 
193                 // Remove cycles
194                 state.removeCycles();
195 
196                 // Now write the new elements.
197                 INode previous = null;
198                 for (IDragElement element : elements) {
199                     String fqcn = element.getFqcn();
200 
201                     // index==-1 means to insert at the end.
202                     // Otherwise increment the insertion position.
203                     if (index >= 0) {
204                         index++;
205                     }
206 
207                     INode newChild = targetNode.insertChildAt(fqcn, index);
208 
209                     // Copy all the attributes, modifying them as needed.
210                     addAttributes(newChild, element, idMap, BaseLayoutRule.DEFAULT_ATTR_FILTER);
211                     addInnerElements(newChild, element, idMap);
212 
213                     if (previous == null) {
214                         state.applyConstraints(newChild);
215                         previous = newChild;
216                     } else {
217                         // Arrange the nodes next to each other, depending on which
218                         // edge we are attaching to. For example, if attaching to the
219                         // top edge, arrange the subsequent nodes in a column below it.
220                         //
221                         // TODO: Try to do something smarter here where we detect
222                         // constraints between the dragged edges, and we preserve these.
223                         // We have to do this carefully though because if the
224                         // constraints go through some other nodes not part of the
225                         // selection, this doesn't work right, and you might be
226                         // dragging several connected components, which we'd then
227                         // need to stitch together such that they are all visible.
228 
229                         state.attachPrevious(previous, newChild);
230                         previous = newChild;
231                     }
232                 }
233             }
234         });
235     }
236 
237     @Override
onChildInserted(INode node, INode parent, InsertType insertType)238     public void onChildInserted(INode node, INode parent, InsertType insertType) {
239         // TODO: Handle more generically some way to ensure that widgets with no
240         // intrinsic size get some minimum size until they are attached on multiple
241         // opposing sides.
242         //String fqcn = node.getFqcn();
243         //if (fqcn.equals(FQCN_EDIT_TEXT)) {
244         //    node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, "100dp"); //$NON-NLS-1$
245         //}
246     }
247 
248     @Override
onRemovingChildren(List<INode> deleted, INode parent)249     public void onRemovingChildren(List<INode> deleted, INode parent) {
250         super.onRemovingChildren(deleted, parent);
251 
252         // Remove any attachments pointing to the deleted nodes.
253 
254         // Produce set of attribute values that we want to delete if
255         // present in a layout attribute
256         Set<String> removeValues = new HashSet<String>(deleted.size() * 2);
257         for (INode node : deleted) {
258             String id = node.getStringAttr(ANDROID_URI, ATTR_ID);
259             if (id != null) {
260                 removeValues.add(id);
261                 if (id.startsWith(NEW_ID_PREFIX)) {
262                     removeValues.add(ID_PREFIX + stripIdPrefix(id));
263                 } else {
264                     removeValues.add(NEW_ID_PREFIX + stripIdPrefix(id));
265                 }
266             }
267         }
268 
269         for (INode child : parent.getChildren()) {
270             if (deleted.contains(child)) {
271                 continue;
272             }
273             for (IAttribute attribute : child.getLiveAttributes()) {
274                 if (attribute.getName().startsWith(ATTR_LAYOUT_PREFIX) &&
275                         ANDROID_URI.equals(attribute.getUri())) {
276                     String value = attribute.getValue();
277                     if (removeValues.contains(value)) {
278                         // Unset this reference to a deleted widget.
279                         child.setAttribute(ANDROID_URI, attribute.getName(), null);
280                     }
281                 }
282             }
283         }
284     }
285 
286     // ==== Resize Support ====
287 
288     @Override
onResizeBegin(INode child, INode parent, SegmentType horizontalEdgeType, SegmentType verticalEdgeType, Object childView, Object parentView)289     public DropFeedback onResizeBegin(INode child, INode parent,
290             SegmentType horizontalEdgeType, SegmentType verticalEdgeType,
291             Object childView, Object parentView) {
292         ResizeHandler state = new ResizeHandler(parent, child, mRulesEngine,
293                 horizontalEdgeType, verticalEdgeType);
294         return new DropFeedback(state, new GuidelinePainter());
295     }
296 
297     @Override
onResizeUpdate(DropFeedback feedback, INode child, INode parent, Rect newBounds, int modifierMask)298     public void onResizeUpdate(DropFeedback feedback, INode child, INode parent, Rect newBounds,
299             int modifierMask) {
300         ResizeHandler state = (ResizeHandler) feedback.userData;
301         state.updateResize(feedback, child, newBounds, modifierMask);
302     }
303 
304     @Override
onResizeEnd(DropFeedback feedback, INode child, INode parent, final Rect newBounds)305     public void onResizeEnd(DropFeedback feedback, INode child, INode parent,
306             final Rect newBounds) {
307         final ResizeHandler state = (ResizeHandler) feedback.userData;
308 
309         child.editXml("Resize", new INodeHandler() {
310             @Override
311             public void handle(INode n) {
312                 state.removeCycles();
313                 state.applyConstraints(n);
314             }
315         });
316     }
317 
318     // ==== Layout Actions Bar ====
319 
320     @Override
addLayoutActions(List<RuleAction> actions, final INode parentNode, final List<? extends INode> children)321     public void addLayoutActions(List<RuleAction> actions, final INode parentNode,
322             final List<? extends INode> children) {
323         super.addLayoutActions(actions, parentNode, children);
324 
325         actions.add(createGravityAction(Collections.<INode>singletonList(parentNode),
326                 ATTR_GRAVITY));
327         actions.add(RuleAction.createSeparator(25));
328         actions.add(createMarginAction(parentNode, children));
329 
330         IMenuCallback callback = new IMenuCallback() {
331             @Override
332             public void action(RuleAction action, List<? extends INode> selectedNodes,
333                     final String valueId, final Boolean newValue) {
334                 final String id = action.getId();
335                 if (id.equals(ACTION_CENTER_VERTICAL)|| id.equals(ACTION_CENTER_HORIZONTAL)) {
336                     parentNode.editXml("Center", new INodeHandler() {
337                         @Override
338                         public void handle(INode n) {
339                             if (id.equals(ACTION_CENTER_VERTICAL)) {
340                                 for (INode child : children) {
341                                     centerVertically(child);
342                                 }
343                             } else if (id.equals(ACTION_CENTER_HORIZONTAL)) {
344                                 for (INode child : children) {
345                                     centerHorizontally(child);
346                                 }
347                             }
348                             mRulesEngine.redraw();
349                         }
350 
351                     });
352                 } else if (id.equals(ACTION_SHOW_CONSTRAINTS)) {
353                     sShowConstraints = !sShowConstraints;
354                     mRulesEngine.redraw();
355                 } else {
356                     assert id.equals(ACTION_SHOW_STRUCTURE);
357                     sShowStructure = !sShowStructure;
358                     mRulesEngine.redraw();
359                 }
360             }
361         };
362 
363         // Centering actions
364         if (children != null && children.size() > 0) {
365                         actions.add(RuleAction.createSeparator(150));
366             actions.add(RuleAction.createAction(ACTION_CENTER_VERTICAL, "Center Vertically",
367                     callback, ICON_CENTER_VERTICALLY, 160, false));
368             actions.add(RuleAction.createAction(ACTION_CENTER_HORIZONTAL, "Center Horizontally",
369                     callback, ICON_CENTER_HORIZONTALLY, 170, false));
370         }
371 
372         actions.add(RuleAction.createSeparator(80));
373         actions.add(RuleAction.createToggle(ACTION_SHOW_CONSTRAINTS, "Show Constraints",
374                 sShowConstraints, callback, ICON_SHOW_CONSTRAINTS, 180, false));
375         actions.add(RuleAction.createToggle(ACTION_SHOW_STRUCTURE, "Show All Relationships",
376                 sShowStructure, callback, ICON_SHOW_STRUCTURE, 190, false));
377     }
378 
centerHorizontally(INode node)379     private void centerHorizontally(INode node) {
380         // Clear horizontal-oriented attributes from the node
381         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_LEFT, null);
382         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_LEFT, null);
383         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF, null);
384         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
385         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_RIGHT, null);
386         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_RIGHT, null);
387         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_LEFT_OF, null);
388         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
389 
390         if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT))) {
391             // Already done
392         } else if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI,
393                 ATTR_LAYOUT_CENTER_VERTICAL))) {
394             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null);
395             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT, VALUE_TRUE);
396         } else {
397             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, VALUE_TRUE);
398         }
399     }
400 
centerVertically(INode node)401     private void centerVertically(INode node) {
402         // Clear vertical-oriented attributes from the node
403         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_TOP, null);
404         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_TOP, null);
405         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_BELOW, null);
406         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, null);
407         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BOTTOM, null);
408         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ABOVE, null);
409         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null);
410         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BASELINE, null);
411 
412         // Center vertically
413         if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT))) {
414             // ALready done
415         } else if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI,
416                 ATTR_LAYOUT_CENTER_HORIZONTAL))) {
417             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
418             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT, VALUE_TRUE);
419         } else {
420             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, VALUE_TRUE);
421         }
422     }
423 }
424