• 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             public void handle(INode n) {
190                 int index = -1;
191 
192                 // Remove cycles
193                 state.removeCycles();
194 
195                 // Now write the new elements.
196                 INode previous = null;
197                 for (IDragElement element : elements) {
198                     String fqcn = element.getFqcn();
199 
200                     // index==-1 means to insert at the end.
201                     // Otherwise increment the insertion position.
202                     if (index >= 0) {
203                         index++;
204                     }
205 
206                     INode newChild = targetNode.insertChildAt(fqcn, index);
207 
208                     // Copy all the attributes, modifying them as needed.
209                     addAttributes(newChild, element, idMap, BaseLayoutRule.DEFAULT_ATTR_FILTER);
210                     addInnerElements(newChild, element, idMap);
211 
212                     if (previous == null) {
213                         state.applyConstraints(newChild);
214                         previous = newChild;
215                     } else {
216                         // Arrange the nodes next to each other, depending on which
217                         // edge we are attaching to. For example, if attaching to the
218                         // top edge, arrange the subsequent nodes in a column below it.
219                         //
220                         // TODO: Try to do something smarter here where we detect
221                         // constraints between the dragged edges, and we preserve these.
222                         // We have to do this carefully though because if the
223                         // constraints go through some other nodes not part of the
224                         // selection, this doesn't work right, and you might be
225                         // dragging several connected components, which we'd then
226                         // need to stitch together such that they are all visible.
227 
228                         state.attachPrevious(previous, newChild);
229                         previous = newChild;
230                     }
231                 }
232             }
233         });
234     }
235 
236     @Override
onChildInserted(INode node, INode parent, InsertType insertType)237     public void onChildInserted(INode node, INode parent, InsertType insertType) {
238         // TODO: Handle more generically some way to ensure that widgets with no
239         // intrinsic size get some minimum size until they are attached on multiple
240         // opposing sides.
241         //String fqcn = node.getFqcn();
242         //if (fqcn.equals(FQCN_EDIT_TEXT)) {
243         //    node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, "100dp"); //$NON-NLS-1$
244         //}
245     }
246 
247     @Override
onRemovingChildren(List<INode> deleted, INode parent)248     public void onRemovingChildren(List<INode> deleted, INode parent) {
249         super.onRemovingChildren(deleted, parent);
250 
251         // Remove any attachments pointing to the deleted nodes.
252 
253         // Produce set of attribute values that we want to delete if
254         // present in a layout attribute
255         Set<String> removeValues = new HashSet<String>(deleted.size() * 2);
256         for (INode node : deleted) {
257             String id = node.getStringAttr(ANDROID_URI, ATTR_ID);
258             if (id != null) {
259                 removeValues.add(id);
260                 if (id.startsWith(NEW_ID_PREFIX)) {
261                     removeValues.add(ID_PREFIX + stripIdPrefix(id));
262                 } else {
263                     removeValues.add(NEW_ID_PREFIX + stripIdPrefix(id));
264                 }
265             }
266         }
267 
268         for (INode child : parent.getChildren()) {
269             if (deleted.contains(child)) {
270                 continue;
271             }
272             for (IAttribute attribute : child.getLiveAttributes()) {
273                 if (attribute.getName().startsWith(ATTR_LAYOUT_PREFIX) &&
274                         ANDROID_URI.equals(attribute.getUri())) {
275                     String value = attribute.getValue();
276                     if (removeValues.contains(value)) {
277                         // Unset this reference to a deleted widget.
278                         child.setAttribute(ANDROID_URI, attribute.getName(), null);
279                     }
280                 }
281             }
282         }
283     }
284 
285     // ==== Resize Support ====
286 
287     @Override
onResizeBegin(INode child, INode parent, SegmentType horizontalEdgeType, SegmentType verticalEdgeType, Object childView, Object parentView)288     public DropFeedback onResizeBegin(INode child, INode parent,
289             SegmentType horizontalEdgeType, SegmentType verticalEdgeType,
290             Object childView, Object parentView) {
291         ResizeHandler state = new ResizeHandler(parent, child, mRulesEngine,
292                 horizontalEdgeType, verticalEdgeType);
293         return new DropFeedback(state, new GuidelinePainter());
294     }
295 
296     @Override
onResizeUpdate(DropFeedback feedback, INode child, INode parent, Rect newBounds, int modifierMask)297     public void onResizeUpdate(DropFeedback feedback, INode child, INode parent, Rect newBounds,
298             int modifierMask) {
299         ResizeHandler state = (ResizeHandler) feedback.userData;
300         state.updateResize(feedback, child, newBounds, modifierMask);
301     }
302 
303     @Override
onResizeEnd(DropFeedback feedback, INode child, INode parent, final Rect newBounds)304     public void onResizeEnd(DropFeedback feedback, INode child, INode parent,
305             final Rect newBounds) {
306         final ResizeHandler state = (ResizeHandler) feedback.userData;
307 
308         child.editXml("Resize", new INodeHandler() {
309             public void handle(INode n) {
310                 state.removeCycles();
311                 state.applyConstraints(n);
312             }
313         });
314     }
315 
316     // ==== Layout Actions Bar ====
317 
318     @Override
addLayoutActions(List<RuleAction> actions, final INode parentNode, final List<? extends INode> children)319     public void addLayoutActions(List<RuleAction> actions, final INode parentNode,
320             final List<? extends INode> children) {
321         super.addLayoutActions(actions, parentNode, children);
322 
323         actions.add(createGravityAction(Collections.<INode>singletonList(parentNode),
324                 ATTR_GRAVITY));
325         actions.add(RuleAction.createSeparator(25));
326         actions.add(createMarginAction(parentNode, children));
327 
328         IMenuCallback callback = new IMenuCallback() {
329             public void action(RuleAction action, List<? extends INode> selectedNodes,
330                     final String valueId, final Boolean newValue) {
331                 final String id = action.getId();
332                 if (id.equals(ACTION_CENTER_VERTICAL)|| id.equals(ACTION_CENTER_HORIZONTAL)) {
333                     parentNode.editXml("Center", new INodeHandler() {
334                         public void handle(INode n) {
335                             if (id.equals(ACTION_CENTER_VERTICAL)) {
336                                 for (INode child : children) {
337                                     centerVertically(child);
338                                 }
339                             } else if (id.equals(ACTION_CENTER_HORIZONTAL)) {
340                                 for (INode child : children) {
341                                     centerHorizontally(child);
342                                 }
343                             }
344                             mRulesEngine.redraw();
345                         }
346 
347                     });
348                 } else if (id.equals(ACTION_SHOW_CONSTRAINTS)) {
349                     sShowConstraints = !sShowConstraints;
350                     mRulesEngine.redraw();
351                 } else {
352                     assert id.equals(ACTION_SHOW_STRUCTURE);
353                     sShowStructure = !sShowStructure;
354                     mRulesEngine.redraw();
355                 }
356             }
357         };
358 
359         // Centering actions
360         if (children != null && children.size() > 0) {
361                         actions.add(RuleAction.createSeparator(150));
362             actions.add(RuleAction.createAction(ACTION_CENTER_VERTICAL, "Center Vertically",
363                     callback, ICON_CENTER_VERTICALLY, 160, false));
364             actions.add(RuleAction.createAction(ACTION_CENTER_HORIZONTAL, "Center Horizontally",
365                     callback, ICON_CENTER_HORIZONTALLY, 170, false));
366         }
367 
368         actions.add(RuleAction.createSeparator(80));
369         actions.add(RuleAction.createToggle(ACTION_SHOW_CONSTRAINTS, "Show Constraints",
370                 sShowConstraints, callback, ICON_SHOW_CONSTRAINTS, 180, false));
371         actions.add(RuleAction.createToggle(ACTION_SHOW_STRUCTURE, "Show All Relationships",
372                 sShowStructure, callback, ICON_SHOW_STRUCTURE, 190, false));
373     }
374 
centerHorizontally(INode node)375     private void centerHorizontally(INode node) {
376         // Clear horizontal-oriented attributes from the node
377         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_LEFT, null);
378         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_LEFT, null);
379         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF, null);
380         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
381         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_RIGHT, null);
382         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_RIGHT, null);
383         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_LEFT_OF, null);
384         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
385 
386         if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT))) {
387             // Already done
388         } else if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI,
389                 ATTR_LAYOUT_CENTER_VERTICAL))) {
390             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null);
391             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT, VALUE_TRUE);
392         } else {
393             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, VALUE_TRUE);
394         }
395     }
396 
centerVertically(INode node)397     private void centerVertically(INode node) {
398         // Clear vertical-oriented attributes from the node
399         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_TOP, null);
400         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_TOP, null);
401         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_BELOW, null);
402         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, null);
403         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BOTTOM, null);
404         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ABOVE, null);
405         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null);
406         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BASELINE, null);
407 
408         // Center vertically
409         if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT))) {
410             // ALready done
411         } else if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI,
412                 ATTR_LAYOUT_CENTER_HORIZONTAL))) {
413             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
414             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT, VALUE_TRUE);
415         } else {
416             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, VALUE_TRUE);
417         }
418     }
419 }
420