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