1/*
2 * Copyright (C) 2011 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 */
16package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
17
18import static com.android.ide.common.layout.GravityHelper.GRAVITY_BOTTOM;
19import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_HORIZ;
20import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_VERT;
21import static com.android.ide.common.layout.GravityHelper.GRAVITY_FILL_HORIZ;
22import static com.android.ide.common.layout.GravityHelper.GRAVITY_FILL_VERT;
23import static com.android.ide.common.layout.GravityHelper.GRAVITY_LEFT;
24import static com.android.ide.common.layout.GravityHelper.GRAVITY_RIGHT;
25import static com.android.ide.common.layout.GravityHelper.GRAVITY_TOP;
26import static com.android.ide.common.layout.GravityHelper.GRAVITY_VERT_MASK;
27import static com.android.SdkConstants.ATTR_BACKGROUND;
28import static com.android.SdkConstants.ATTR_BASELINE_ALIGNED;
29import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE;
30import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
31import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
32import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
33import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
34import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
35import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
36import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
37import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
38import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
39import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING;
40import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
41import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
42import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_VERTICAL;
43import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
44import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT;
45import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP;
46import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
47import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
48import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
49import static com.android.SdkConstants.ATTR_LAYOUT_WEIGHT;
50import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
51import static com.android.SdkConstants.ATTR_ORIENTATION;
52import static com.android.SdkConstants.ID_PREFIX;
53import static com.android.SdkConstants.LINEAR_LAYOUT;
54import static com.android.SdkConstants.NEW_ID_PREFIX;
55import static com.android.SdkConstants.RELATIVE_LAYOUT;
56import static com.android.SdkConstants.VALUE_FALSE;
57import static com.android.SdkConstants.VALUE_N_DP;
58import static com.android.SdkConstants.VALUE_TRUE;
59import static com.android.SdkConstants.VALUE_VERTICAL;
60import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
61
62
63import com.android.SdkConstants;
64import static com.android.SdkConstants.ANDROID_URI;
65import com.android.ide.common.layout.GravityHelper;
66import com.android.ide.eclipse.adt.AdtPlugin;
67import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
68import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
69import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
70import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
71import com.android.utils.Pair;
72
73import org.eclipse.core.runtime.IStatus;
74import org.eclipse.swt.graphics.Rectangle;
75import org.eclipse.text.edits.MultiTextEdit;
76import org.w3c.dom.Attr;
77import org.w3c.dom.Element;
78import org.w3c.dom.NamedNodeMap;
79import org.w3c.dom.Node;
80import org.w3c.dom.NodeList;
81
82import java.io.PrintWriter;
83import java.io.StringWriter;
84import java.util.ArrayList;
85import java.util.Collections;
86import java.util.Comparator;
87import java.util.HashMap;
88import java.util.HashSet;
89import java.util.List;
90import java.util.Map;
91import java.util.Set;
92
93/**
94 * Helper class which performs the bulk of the layout conversion to relative layout
95 * <p>
96 * Future enhancements:
97 * <ul>
98 * <li>Render the layout at multiple screen sizes and analyze how the widgets move and
99 * stretch and use that to add in additional constraints
100 * <li> Adapt the LinearLayout analysis code to work with TableLayouts and TableRows as well
101 * (just need to tweak the "isVertical" interpretation to account for the different defaults,
102 * and perhaps do something about column size properties.
103 * <li> We need to take into account existing margins and clear/update them
104 * </ul>
105 */
106class RelativeLayoutConversionHelper {
107    private final MultiTextEdit mRootEdit;
108    private final boolean mFlatten;
109    private final Element mLayout;
110    private final ChangeLayoutRefactoring mRefactoring;
111    private final CanvasViewInfo mRootView;
112    private List<Element> mDeletedElements;
113
114    RelativeLayoutConversionHelper(ChangeLayoutRefactoring refactoring,
115            Element layout, boolean flatten, MultiTextEdit rootEdit, CanvasViewInfo rootView) {
116        mRefactoring = refactoring;
117        mLayout = layout;
118        mFlatten = flatten;
119        mRootEdit = rootEdit;
120        mRootView = rootView;
121    }
122
123    /** Performs conversion from any layout to a RelativeLayout */
124    public void convertToRelative() {
125        if (mRootView == null) {
126            return;
127        }
128
129        // Locate the view for the layout
130        CanvasViewInfo layoutView = findViewForElement(mRootView, mLayout);
131        if (layoutView == null || layoutView.getChildren().size() == 0) {
132            // No children. THAT was an easy conversion!
133            return;
134        }
135
136        // Study the layout and get information about how to place individual elements
137        List<View> views = analyzeLayout(layoutView);
138
139        // Create/update relative layout constraints
140        createAttachments(views);
141    }
142
143    /** Returns the elements that were deleted, or null */
144    List<Element> getDeletedElements() {
145        return mDeletedElements;
146    }
147
148    /**
149     * Analyzes the given view hierarchy and produces a list of {@link View} objects which
150     * contain placement information for each element
151     */
152    private List<View> analyzeLayout(CanvasViewInfo layoutView) {
153        EdgeList edgeList = new EdgeList(layoutView);
154        mDeletedElements = edgeList.getDeletedElements();
155        deleteRemovedElements(mDeletedElements);
156
157        List<Integer> columnOffsets = edgeList.getColumnOffsets();
158        List<Integer> rowOffsets = edgeList.getRowOffsets();
159
160        // Compute x/y offsets for each row/column index
161        int[] left = new int[columnOffsets.size()];
162        int[] top = new int[rowOffsets.size()];
163
164        Map<Integer, Integer> xToCol = new HashMap<Integer, Integer>();
165        int columnIndex = 0;
166        for (Integer offset : columnOffsets) {
167            left[columnIndex] = offset;
168            xToCol.put(offset, columnIndex++);
169        }
170        Map<Integer, Integer> yToRow = new HashMap<Integer, Integer>();
171        int rowIndex = 0;
172        for (Integer offset : rowOffsets) {
173            top[rowIndex] = offset;
174            yToRow.put(offset, rowIndex++);
175        }
176
177        // Create a complete list of view objects
178        List<View> views = createViews(edgeList, columnOffsets);
179        initializeSpans(edgeList, columnOffsets, rowOffsets, xToCol, yToRow);
180
181        // Sanity check
182        for (View view : views) {
183            assert view.getLeftEdge() == left[view.mCol];
184            assert view.getTopEdge() == top[view.mRow];
185            assert view.getRightEdge() == left[view.mCol+view.mColSpan];
186            assert view.getBottomEdge() == top[view.mRow+view.mRowSpan];
187        }
188
189        // Ensure that every view has a proper id such that it can be referred to
190        // with a constraint
191        initializeIds(edgeList, views);
192
193        // Attempt to lay the views out in a grid with constraints (though not that widgets
194        // can overlap as well)
195        Grid grid = new Grid(views, left, top);
196        computeKnownConstraints(views, edgeList);
197        computeHorizontalConstraints(grid);
198        computeVerticalConstraints(grid);
199
200        return views;
201    }
202
203    /** Produces a list of {@link View} objects from an {@link EdgeList} */
204    private List<View> createViews(EdgeList edgeList, List<Integer> columnOffsets) {
205        List<View> views = new ArrayList<View>();
206        for (Integer offset : columnOffsets) {
207            List<View> leftEdgeViews = edgeList.getLeftEdgeViews(offset);
208            if (leftEdgeViews == null) {
209                // must have been a right edge
210                continue;
211            }
212            for (View view : leftEdgeViews) {
213                views.add(view);
214            }
215        }
216        return views;
217    }
218
219    /** Removes any elements targeted for deletion */
220    private void deleteRemovedElements(List<Element> delete) {
221        if (mFlatten && delete.size() > 0) {
222            for (Element element : delete) {
223                mRefactoring.removeElementTags(mRootEdit, element, delete,
224                        !AdtPrefs.getPrefs().getFormatGuiXml() /*changeIndentation*/);
225            }
226        }
227    }
228
229    /** Ensures that every element has an id such that it can be referenced from a constraint */
230    private void initializeIds(EdgeList edgeList, List<View> views) {
231        // Ensure that all views have a valid id
232        for (View view : views) {
233            String id = mRefactoring.ensureHasId(mRootEdit, view.mElement, null);
234            edgeList.setIdAttributeValue(view, id);
235        }
236    }
237
238    /**
239     * Initializes the column and row indices, as well as any column span and row span
240     * values
241     */
242    private void initializeSpans(EdgeList edgeList, List<Integer> columnOffsets,
243            List<Integer> rowOffsets, Map<Integer, Integer> xToCol, Map<Integer, Integer> yToRow) {
244        // Now initialize table view row, column and spans
245        for (Integer offset : columnOffsets) {
246            List<View> leftEdgeViews = edgeList.getLeftEdgeViews(offset);
247            if (leftEdgeViews == null) {
248                // must have been a right edge
249                continue;
250            }
251            for (View view : leftEdgeViews) {
252                Integer col = xToCol.get(view.getLeftEdge());
253                assert col != null;
254                Integer end = xToCol.get(view.getRightEdge());
255                assert end != null;
256
257                view.mCol = col;
258                view.mColSpan = end - col;
259            }
260        }
261
262        for (Integer offset : rowOffsets) {
263            List<View> topEdgeViews = edgeList.getTopEdgeViews(offset);
264            if (topEdgeViews == null) {
265                // must have been a bottom edge
266                continue;
267            }
268            for (View view : topEdgeViews) {
269                Integer row = yToRow.get(view.getTopEdge());
270                assert row != null;
271                Integer end = yToRow.get(view.getBottomEdge());
272                assert end != null;
273
274                view.mRow = row;
275                view.mRowSpan = end - row;
276            }
277        }
278    }
279
280    /**
281     * Creates refactoring edits which adds or updates constraints for the given list of
282     * views
283     */
284    private void createAttachments(List<View> views) {
285        // Make the attachments
286        String namespace = mRefactoring.getAndroidNamespacePrefix();
287        for (View view : views) {
288            for (Pair<String, String> constraint : view.getHorizConstraints()) {
289                mRefactoring.setAttribute(mRootEdit, view.mElement, ANDROID_URI,
290                        namespace, constraint.getFirst(), constraint.getSecond());
291            }
292            for (Pair<String, String> constraint : view.getVerticalConstraints()) {
293                mRefactoring.setAttribute(mRootEdit, view.mElement, ANDROID_URI,
294                        namespace, constraint.getFirst(), constraint.getSecond());
295            }
296        }
297    }
298
299    /**
300     * Analyzes the existing layouts and layout parameter objects in the document to infer
301     * constraints for layout types that we know about - such as LinearLayout baseline
302     * alignment, weights, gravity, etc.
303     */
304    private void computeKnownConstraints(List<View> views, EdgeList edgeList) {
305        // List of parent layout elements we've already processed. We iterate through all
306        // the -children-, and we ask each for its element parent (which won't have a view)
307        // and we look at the parent's layout attributes and its children layout constraints,
308        // and then we stash away constraints that we can infer. This means that we will
309        // encounter the same parent for every sibling, so that's why there's a map to
310        // prevent duplicate work.
311        Set<Node> seen = new HashSet<Node>();
312
313        for (View view : views) {
314            Element element = view.getElement();
315            Node parent = element.getParentNode();
316            if (seen.contains(parent)) {
317                continue;
318            }
319            seen.add(parent);
320
321            if (parent.getNodeType() != Node.ELEMENT_NODE) {
322                continue;
323            }
324            Element layout = (Element) parent;
325            String layoutName = layout.getTagName();
326
327            if (LINEAR_LAYOUT.equals(layoutName)) {
328                analyzeLinearLayout(edgeList, layout);
329            } else if (RELATIVE_LAYOUT.equals(layoutName)) {
330                analyzeRelativeLayout(edgeList, layout);
331            } else {
332                // Some other layout -- add more conditional handling here
333                // for framelayout, tables, etc.
334            }
335        }
336    }
337
338    /**
339     * Returns the layout weight of of the given child of a LinearLayout, or 0.0 if it
340     * does not define a weight
341     */
342    private float getWeight(Element linearLayoutChild) {
343        String weight = linearLayoutChild.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT);
344        if (weight != null && weight.length() > 0) {
345            try {
346                return Float.parseFloat(weight);
347            } catch (NumberFormatException nfe) {
348                AdtPlugin.log(nfe, "Invalid weight %1$s", weight);
349            }
350        }
351
352        return 0.0f;
353    }
354
355    /**
356     * Returns the sum of all the layout weights of the children in the given LinearLayout
357     *
358     * @param linearLayout the layout to compute the total sum for
359     * @return the total sum of all the layout weights in the given layout
360     */
361    private float getWeightSum(Element linearLayout) {
362        float sum = 0;
363        for (Element child : DomUtilities.getChildren(linearLayout)) {
364            sum += getWeight(child);
365        }
366
367        return sum;
368    }
369
370    /**
371     * Analyzes the given LinearLayout and updates the constraints to reflect
372     * relationships it can infer - based on baseline alignment, gravity, order and
373     * weights. This method also removes "0dip" as a special width/height used in
374     * LinearLayouts with weight distribution.
375     */
376    private void analyzeLinearLayout(EdgeList edgeList, Element layout) {
377        boolean isVertical = VALUE_VERTICAL.equals(layout.getAttributeNS(ANDROID_URI,
378                ATTR_ORIENTATION));
379        View baselineRef = null;
380        if (!isVertical &&
381            !VALUE_FALSE.equals(layout.getAttributeNS(ANDROID_URI, ATTR_BASELINE_ALIGNED))) {
382            // Baseline alignment. Find the tallest child and set it as the baseline reference.
383            int tallestHeight = 0;
384            View tallest = null;
385            for (Element child : DomUtilities.getChildren(layout)) {
386                View view = edgeList.getView(child);
387                if (view != null && view.getHeight() > tallestHeight) {
388                    tallestHeight = view.getHeight();
389                    tallest = view;
390                }
391            }
392            if (tallest != null) {
393                baselineRef = tallest;
394            }
395        }
396
397        float weightSum = getWeightSum(layout);
398        float cumulativeWeight = 0;
399
400        List<Element> children = DomUtilities.getChildren(layout);
401        String prevId = null;
402        boolean isFirstChild = true;
403        boolean linkBackwards = true;
404        boolean linkForwards = false;
405
406        for (int index = 0, childCount = children.size(); index < childCount; index++) {
407            Element child = children.get(index);
408
409            View childView = edgeList.getView(child);
410            if (childView == null) {
411                // Could be a nested layout that is being removed etc
412                prevId = null;
413                isFirstChild = false;
414                continue;
415            }
416
417            // Look at the layout_weight attributes and determine whether we should be
418            // attached on the bottom/right or on the top/left
419            if (weightSum > 0.0f) {
420                float weight = getWeight(child);
421
422                // We can't emulate a LinearLayout where multiple children have positive
423                // weights. However, we CAN support the common scenario where a single
424                // child has a non-zero weight, and all children after it are pushed
425                // to the end and the weighted child fills the remaining space.
426                if (cumulativeWeight == 0 && weight > 0) {
427                    // See if we have a bottom/right edge to attach the forwards link to
428                    // (at the end of the forwards chains). Only if so can we link forwards.
429                    View referenced;
430                    if (isVertical) {
431                        referenced = edgeList.getSharedBottomEdge(layout);
432                    } else {
433                        referenced = edgeList.getSharedRightEdge(layout);
434                    }
435                    if (referenced != null) {
436                        linkForwards = true;
437                    }
438                } else if (cumulativeWeight > 0) {
439                    linkBackwards = false;
440                }
441
442                cumulativeWeight += weight;
443            }
444
445            analyzeGravity(edgeList, layout, isVertical, child, childView);
446            convert0dipToWrapContent(child);
447
448            // Chain elements together in the flow direction of the linear layout
449            if (prevId != null) { // No constraint for first child
450                if (linkBackwards) {
451                    if (isVertical) {
452                        childView.addVerticalConstraint(ATTR_LAYOUT_BELOW, prevId);
453                    } else {
454                        childView.addHorizConstraint(ATTR_LAYOUT_TO_RIGHT_OF, prevId);
455                    }
456                }
457            } else if (isFirstChild) {
458                assert linkBackwards;
459
460                // First element; attach it to the parent if we can
461                if (isVertical) {
462                    View referenced = edgeList.getSharedTopEdge(layout);
463                    if (referenced != null) {
464                        if (isAncestor(referenced.getElement(), child)) {
465                            childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP,
466                                VALUE_TRUE);
467                        } else {
468                            childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
469                                    referenced.getId());
470                        }
471                    }
472                } else {
473                    View referenced = edgeList.getSharedLeftEdge(layout);
474                    if (referenced != null) {
475                        if (isAncestor(referenced.getElement(), child)) {
476                            childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT,
477                                    VALUE_TRUE);
478                        } else {
479                            childView.addHorizConstraint(
480                                    ATTR_LAYOUT_ALIGN_LEFT, referenced.getId());
481                        }
482                    }
483                }
484            }
485
486            if (linkForwards) {
487                if (index < (childCount - 1)) {
488                    Element nextChild = children.get(index + 1);
489                    String nextId = mRefactoring.ensureHasId(mRootEdit, nextChild, null);
490                    if (nextId != null) {
491                        if (isVertical) {
492                            childView.addVerticalConstraint(ATTR_LAYOUT_ABOVE, nextId);
493                        } else {
494                            childView.addHorizConstraint(ATTR_LAYOUT_TO_LEFT_OF, nextId);
495                        }
496                    }
497                } else {
498                    // Attach to right/bottom edge of the layout
499                    if (isVertical) {
500                        View referenced = edgeList.getSharedBottomEdge(layout);
501                        if (referenced != null) {
502                            if (isAncestor(referenced.getElement(), child)) {
503                                childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
504                                    VALUE_TRUE);
505                            } else {
506                                childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
507                                        referenced.getId());
508                            }
509                        }
510                    } else {
511                        View referenced = edgeList.getSharedRightEdge(layout);
512                        if (referenced != null) {
513                            if (isAncestor(referenced.getElement(), child)) {
514                                childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
515                                        VALUE_TRUE);
516                            } else {
517                                childView.addHorizConstraint(
518                                        ATTR_LAYOUT_ALIGN_RIGHT, referenced.getId());
519                            }
520                        }
521                    }
522                }
523            }
524
525            if (baselineRef != null && baselineRef.getId() != null
526                    && !baselineRef.getId().equals(childView.getId())) {
527                assert !isVertical;
528                // Only align if they share the same gravity
529                if ((childView.getGravity() & GRAVITY_VERT_MASK) ==
530                        (baselineRef.getGravity() & GRAVITY_VERT_MASK)) {
531                    childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_BASELINE, baselineRef.getId());
532                }
533            }
534
535            prevId = mRefactoring.ensureHasId(mRootEdit, child, null);
536            isFirstChild = false;
537        }
538    }
539
540    /**
541     * Checks the layout "gravity" value for the given child and updates the constraints
542     * to account for the gravity
543     */
544    private int analyzeGravity(EdgeList edgeList, Element layout, boolean isVertical,
545            Element child, View childView) {
546        // Use gravity to constrain elements in the axis orthogonal to the
547        // direction of the layout
548        int gravity = childView.getGravity();
549        if (isVertical) {
550            if ((gravity & GRAVITY_RIGHT) != 0) {
551                View referenced = edgeList.getSharedRightEdge(layout);
552                if (referenced != null) {
553                    if (isAncestor(referenced.getElement(), child)) {
554                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
555                                VALUE_TRUE);
556                    } else {
557                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_RIGHT,
558                                referenced.getId());
559                    }
560                }
561            } else if ((gravity & GRAVITY_CENTER_HORIZ) != 0) {
562                View referenced1 = edgeList.getSharedLeftEdge(layout);
563                View referenced2 = edgeList.getSharedRightEdge(layout);
564                if (referenced1 != null && referenced2 == referenced1) {
565                    if (isAncestor(referenced1.getElement(), child)) {
566                        childView.addHorizConstraint(ATTR_LAYOUT_CENTER_HORIZONTAL,
567                                VALUE_TRUE);
568                    }
569                }
570            } else if ((gravity & GRAVITY_FILL_HORIZ) != 0) {
571                View referenced1 = edgeList.getSharedLeftEdge(layout);
572                View referenced2 = edgeList.getSharedRightEdge(layout);
573                if (referenced1 != null && referenced2 == referenced1) {
574                    if (isAncestor(referenced1.getElement(), child)) {
575                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT,
576                                VALUE_TRUE);
577                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
578                                VALUE_TRUE);
579                    } else {
580                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT,
581                                referenced1.getId());
582                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_RIGHT,
583                                referenced2.getId());
584                    }
585                }
586            } else if ((gravity & GRAVITY_LEFT) != 0) {
587                View referenced = edgeList.getSharedLeftEdge(layout);
588                if (referenced != null) {
589                    if (isAncestor(referenced.getElement(), child)) {
590                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT,
591                                VALUE_TRUE);
592                    } else {
593                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT,
594                                referenced.getId());
595                    }
596                }
597            }
598        } else {
599            // Handle horizontal layout: perform vertical gravity attachments
600            if ((gravity & GRAVITY_BOTTOM) != 0) {
601                View referenced = edgeList.getSharedBottomEdge(layout);
602                if (referenced != null) {
603                    if (isAncestor(referenced.getElement(), child)) {
604                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
605                                VALUE_TRUE);
606                    } else {
607                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
608                                referenced.getId());
609                    }
610                }
611            } else if ((gravity & GRAVITY_CENTER_VERT) != 0) {
612                View referenced1 = edgeList.getSharedTopEdge(layout);
613                View referenced2 = edgeList.getSharedBottomEdge(layout);
614                if (referenced1 != null && referenced2 == referenced1) {
615                    if (isAncestor(referenced1.getElement(), child)) {
616                        childView.addVerticalConstraint(ATTR_LAYOUT_CENTER_VERTICAL,
617                                VALUE_TRUE);
618                    }
619                }
620            } else if ((gravity & GRAVITY_FILL_VERT) != 0) {
621                View referenced1 = edgeList.getSharedTopEdge(layout);
622                View referenced2 = edgeList.getSharedBottomEdge(layout);
623                if (referenced1 != null && referenced2 == referenced1) {
624                    if (isAncestor(referenced1.getElement(), child)) {
625                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP,
626                                VALUE_TRUE);
627                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
628                                VALUE_TRUE);
629                    } else {
630                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
631                                referenced1.getId());
632                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
633                                referenced2.getId());
634                    }
635                }
636            } else if ((gravity & GRAVITY_TOP) != 0) {
637                View referenced = edgeList.getSharedTopEdge(layout);
638                if (referenced != null) {
639                    if (isAncestor(referenced.getElement(), child)) {
640                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP,
641                                VALUE_TRUE);
642                    } else {
643                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
644                                referenced.getId());
645                    }
646                }
647            }
648        }
649        return gravity;
650    }
651
652    /** Converts 0dip values in layout_width and layout_height to wrap_content instead */
653    private void convert0dipToWrapContent(Element child) {
654        // Must convert layout_height="0dip" to layout_height="wrap_content".
655        // 0dip is a special trick used in linear layouts in the presence of
656        // weights where 0dip ensures that the height of the view is not taken
657        // into account when distributing the weights. However, when converted
658        // to RelativeLayout this will instead cause the view to actually be assigned
659        // 0 height.
660        String height = child.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
661        // 0dip, 0dp, 0px, etc
662        if (height != null && height.startsWith("0")) { //$NON-NLS-1$
663            mRefactoring.setAttribute(mRootEdit, child, ANDROID_URI,
664                    mRefactoring.getAndroidNamespacePrefix(), ATTR_LAYOUT_HEIGHT,
665                    VALUE_WRAP_CONTENT);
666        }
667        String width = child.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
668        if (width != null && width.startsWith("0")) { //$NON-NLS-1$
669            mRefactoring.setAttribute(mRootEdit, child, ANDROID_URI,
670                    mRefactoring.getAndroidNamespacePrefix(), ATTR_LAYOUT_WIDTH,
671                    VALUE_WRAP_CONTENT);
672        }
673    }
674
675    /**
676     * Analyzes an embedded RelativeLayout within a layout hierarchy and updates the
677     * constraints in the EdgeList with those relationships which can continue in the
678     * outer single RelativeLayout.
679     */
680    private void analyzeRelativeLayout(EdgeList edgeList, Element layout) {
681        NodeList children = layout.getChildNodes();
682        for (int i = 0, n = children.getLength(); i < n; i++) {
683            Node node = children.item(i);
684            if (node.getNodeType() == Node.ELEMENT_NODE) {
685                Element child = (Element) node;
686                View childView = edgeList.getView(child);
687                if (childView == null) {
688                    // Could be a nested layout that is being removed etc
689                    continue;
690                }
691
692                NamedNodeMap attributes = child.getAttributes();
693                for (int j = 0, m = attributes.getLength(); j < m; j++) {
694                    Attr attribute = (Attr) attributes.item(j);
695                    String name = attribute.getLocalName();
696                    String value = attribute.getValue();
697                    if (name.equals(ATTR_LAYOUT_WIDTH)
698                            || name.equals(ATTR_LAYOUT_HEIGHT)) {
699                        // Ignore these for now
700                    } else if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
701                            && ANDROID_URI.equals(attribute.getNamespaceURI())) {
702                        // Determine if the reference is to a known edge
703                        String id = getIdBasename(value);
704                        if (id != null) {
705                            View referenced = edgeList.getView(id);
706                            if (referenced != null) {
707                                // This is a valid reference, so preserve
708                                // the attribute
709                                if (name.equals(ATTR_LAYOUT_BELOW) ||
710                                        name.equals(ATTR_LAYOUT_ABOVE) ||
711                                        name.equals(ATTR_LAYOUT_ALIGN_TOP) ||
712                                        name.equals(ATTR_LAYOUT_ALIGN_BOTTOM) ||
713                                        name.equals(ATTR_LAYOUT_ALIGN_BASELINE)) {
714                                    // Vertical constraint
715                                    childView.addVerticalConstraint(name, value);
716                                } else if (name.equals(ATTR_LAYOUT_ALIGN_LEFT) ||
717                                        name.equals(ATTR_LAYOUT_TO_LEFT_OF) ||
718                                        name.equals(ATTR_LAYOUT_TO_RIGHT_OF) ||
719                                        name.equals(ATTR_LAYOUT_ALIGN_RIGHT)) {
720                                    // Horizontal constraint
721                                    childView.addHorizConstraint(name, value);
722                                } else {
723                                    // We don't expect this
724                                    assert false : name;
725                                }
726                            } else {
727                                // Reference to some layout that is not included here.
728                                // TODO: See if the given layout has an edge
729                                // that corresponds to one of our known views
730                                // so we can adjust the constraints and keep it after all.
731                            }
732                        } else {
733                            // It's a parent-relative constraint (such
734                            // as aligning with a parent edge, or centering
735                            // in the parent view)
736                            boolean remove = true;
737                            if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_LEFT)) {
738                                View referenced = edgeList.getSharedLeftEdge(layout);
739                                if (referenced != null) {
740                                    if (isAncestor(referenced.getElement(), child)) {
741                                        childView.addHorizConstraint(name, VALUE_TRUE);
742                                    } else {
743                                        childView.addHorizConstraint(
744                                                ATTR_LAYOUT_ALIGN_LEFT, referenced.getId());
745                                    }
746                                    remove = false;
747                                }
748                            } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_RIGHT)) {
749                                View referenced = edgeList.getSharedRightEdge(layout);
750                                if (referenced != null) {
751                                    if (isAncestor(referenced.getElement(), child)) {
752                                        childView.addHorizConstraint(name, VALUE_TRUE);
753                                    } else {
754                                        childView.addHorizConstraint(
755                                            ATTR_LAYOUT_ALIGN_RIGHT, referenced.getId());
756                                    }
757                                    remove = false;
758                                }
759                            } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_TOP)) {
760                                View referenced = edgeList.getSharedTopEdge(layout);
761                                if (referenced != null) {
762                                    if (isAncestor(referenced.getElement(), child)) {
763                                        childView.addVerticalConstraint(name, VALUE_TRUE);
764                                    } else {
765                                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
766                                                referenced.getId());
767                                    }
768                                    remove = false;
769                                }
770                            } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM)) {
771                                View referenced = edgeList.getSharedBottomEdge(layout);
772                                if (referenced != null) {
773                                    if (isAncestor(referenced.getElement(), child)) {
774                                        childView.addVerticalConstraint(name, VALUE_TRUE);
775                                    } else {
776                                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
777                                                referenced.getId());
778                                    }
779                                    remove = false;
780                                }
781                            }
782
783                            boolean alignWithParent =
784                                    name.equals(ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING);
785                            if (remove && alignWithParent) {
786                                // TODO - look for this one AFTER we have processed
787                                // everything else, and then set constraints as necessary
788                                // IF there are no other conflicting constraints!
789                            }
790
791                            // Otherwise it's some kind of centering which we don't support
792                            // yet.
793
794                            // TODO: Find a way to determine whether we have
795                            // a corresponding edge for the parent (e.g. if
796                            // the ViewInfo bounds match our outer parent or
797                            // some other edge) and if so, substitute for that
798                            // id.
799                            // For example, if this element was centered
800                            // horizontally in a RelativeLayout that actually
801                            // occupies the entire width of our outer layout,
802                            // then it can be preserved after all!
803
804                            if (remove) {
805                                if (name.startsWith("layout_margin")) { //$NON-NLS-1$
806                                    continue;
807                                }
808
809                                // Remove unknown attributes?
810                                // It's too early to do this, because we may later want
811                                // to *set* this value and it would result in an overlapping edits
812                                // exception. Therefore, we need to RECORD which attributes should
813                                // be removed, which lines should have its indentation adjusted
814                                // etc and finally process it all at the end!
815                                //mRefactoring.removeAttribute(mRootEdit, child,
816                                //        attribute.getNamespaceURI(), name);
817                            }
818                        }
819                    }
820                }
821            }
822        }
823    }
824
825    /**
826     * Given {@code @id/foo} or {@code @+id/foo}, returns foo. Note that given foo it will
827     * return null.
828     */
829    private static String getIdBasename(String id) {
830        if (id.startsWith(NEW_ID_PREFIX)) {
831            return id.substring(NEW_ID_PREFIX.length());
832        } else if (id.startsWith(ID_PREFIX)) {
833            return id.substring(ID_PREFIX.length());
834        }
835
836        return null;
837    }
838
839    /** Returns true if the given second argument is a descendant of the first argument */
840    private static boolean isAncestor(Node ancestor, Node node) {
841        while (node != null) {
842            if (node == ancestor) {
843                return true;
844            }
845            node = node.getParentNode();
846        }
847        return false;
848    }
849
850    /**
851     * Computes horizontal constraints for the views in the grid for any remaining views
852     * that do not have constraints (as the result of the analysis of known layouts). This
853     * will look at the rendered layout coordinates and attempt to connect elements based
854     * on a spatial layout in the grid.
855     */
856    private void computeHorizontalConstraints(Grid grid) {
857        int columns = grid.getColumns();
858
859        String attachLeftProperty = ATTR_LAYOUT_ALIGN_PARENT_LEFT;
860        String attachLeftValue = VALUE_TRUE;
861        int marginLeft = 0;
862        for (int col = 0; col < columns; col++) {
863            if (!grid.colContainsTopLeftCorner(col)) {
864                // Just accumulate margins for the next column
865                marginLeft += grid.getColumnWidth(col);
866            } else {
867                // Add horizontal attachments
868                String firstId = null;
869                for (View view : grid.viewsStartingInCol(col, true)) {
870                    assert view.getId() != null;
871                    if (firstId == null) {
872                        firstId = view.getId();
873                        if (view.isConstrainedHorizontally()) {
874                            // Nothing to do -- we already have an accurate position for
875                            // this view
876                        } else if (attachLeftProperty != null) {
877                            view.addHorizConstraint(attachLeftProperty, attachLeftValue);
878                            if (marginLeft > 0) {
879                                view.addHorizConstraint(ATTR_LAYOUT_MARGIN_LEFT,
880                                        String.format(VALUE_N_DP, marginLeft));
881                                marginLeft = 0;
882                            }
883                        } else {
884                            assert false;
885                        }
886                    } else if (!view.isConstrainedHorizontally()) {
887                        view.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT, firstId);
888                    }
889                }
890            }
891
892            // Figure out edge for the next column
893            View view = grid.findRightEdgeView(col);
894            if (view != null) {
895                assert view.getId() != null;
896                attachLeftProperty = ATTR_LAYOUT_TO_RIGHT_OF;
897                attachLeftValue = view.getId();
898
899                marginLeft = 0;
900            } else if (marginLeft == 0) {
901                marginLeft = grid.getColumnWidth(col);
902            }
903        }
904    }
905
906    /**
907     * Performs vertical layout just like the {@link #computeHorizontalConstraints} method
908     * did horizontally
909     */
910    private void computeVerticalConstraints(Grid grid) {
911        int rows = grid.getRows();
912
913        String attachTopProperty = ATTR_LAYOUT_ALIGN_PARENT_TOP;
914        String attachTopValue = VALUE_TRUE;
915        int marginTop = 0;
916        for (int row = 0; row < rows; row++) {
917            if (!grid.rowContainsTopLeftCorner(row)) {
918                // Just accumulate margins for the next column
919                marginTop += grid.getRowHeight(row);
920            } else {
921                // Add horizontal attachments
922                String firstId = null;
923                for (View view : grid.viewsStartingInRow(row, true)) {
924                    assert view.getId() != null;
925                    if (firstId == null) {
926                        firstId = view.getId();
927                        if (view.isConstrainedVertically()) {
928                            // Nothing to do -- we already have an accurate position for
929                            // this view
930                        } else if (attachTopProperty != null) {
931                            view.addVerticalConstraint(attachTopProperty, attachTopValue);
932                            if (marginTop > 0) {
933                                view.addVerticalConstraint(ATTR_LAYOUT_MARGIN_TOP,
934                                        String.format(VALUE_N_DP, marginTop));
935                                marginTop = 0;
936                            }
937                        } else {
938                            assert false;
939                        }
940                    } else if (!view.isConstrainedVertically()) {
941                        view.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP, firstId);
942                    }
943                }
944            }
945
946            // Figure out edge for the next row
947            View view = grid.findBottomEdgeView(row);
948            if (view != null) {
949                assert view.getId() != null;
950                attachTopProperty = ATTR_LAYOUT_BELOW;
951                attachTopValue = view.getId();
952                marginTop = 0;
953            } else if (marginTop == 0) {
954                marginTop = grid.getRowHeight(row);
955            }
956        }
957    }
958
959    /**
960     * Searches a view hierarchy and locates the {@link CanvasViewInfo} for the given
961     * {@link Element}
962     *
963     * @param info the root {@link CanvasViewInfo} to search below
964     * @param element the target element
965     * @return the {@link CanvasViewInfo} which corresponds to the given element
966     */
967    private CanvasViewInfo findViewForElement(CanvasViewInfo info, Element element) {
968        if (getElement(info) == element) {
969            return info;
970        }
971
972        for (CanvasViewInfo child : info.getChildren()) {
973            CanvasViewInfo result = findViewForElement(child, element);
974            if (result != null) {
975                return result;
976            }
977        }
978
979        return null;
980    }
981
982    /** Returns the {@link Element} for the given {@link CanvasViewInfo} */
983    private static Element getElement(CanvasViewInfo info) {
984        Node node = info.getUiViewNode().getXmlNode();
985        if (node instanceof Element) {
986            return (Element) node;
987        }
988
989        return null;
990    }
991
992    /**
993     * A grid of cells which can contain views, used to infer spatial relationships when
994     * computing constraints. Note that a view can appear in than one cell; they will
995     * appear in all cells that their bounds overlap with!
996     */
997    private class Grid {
998        private final int[] mLeft;
999        private final int[] mTop;
1000        // A list from row to column to cell, where a cell is a list of views
1001        private final List<List<List<View>>> mRowList;
1002        private int mRowCount;
1003        private int mColCount;
1004
1005        Grid(List<View> views, int[] left, int[] top) {
1006            mLeft = left;
1007            mTop = top;
1008
1009            // The left/top arrays should include the ending point too
1010            mColCount = left.length - 1;
1011            mRowCount = top.length - 1;
1012
1013            // Using nested lists rather than arrays to avoid lack of typed arrays
1014            // (can't create List<View>[row][column] arrays)
1015            mRowList = new ArrayList<List<List<View>>>(top.length);
1016            for (int row = 0; row < top.length; row++) {
1017                List<List<View>> columnList = new ArrayList<List<View>>(left.length);
1018                for (int col = 0; col < left.length; col++) {
1019                    columnList.add(new ArrayList<View>(4));
1020                }
1021                mRowList.add(columnList);
1022            }
1023
1024            for (View view : views) {
1025                // Get rid of the root view; we don't want that in the attachments logic;
1026                // it was there originally such that it would contribute the outermost
1027                // edges.
1028                if (view.mElement == mLayout) {
1029                    continue;
1030                }
1031
1032                for (int i = 0; i < view.mRowSpan; i++) {
1033                    for (int j = 0; j < view.mColSpan; j++) {
1034                        mRowList.get(view.mRow + i).get(view.mCol + j).add(view);
1035                    }
1036                }
1037            }
1038        }
1039
1040        /**
1041         * Returns the number of rows in the grid
1042         *
1043         * @return the row count
1044         */
1045        public int getRows() {
1046            return mRowCount;
1047        }
1048
1049        /**
1050         * Returns the number of columns in the grid
1051         *
1052         * @return the column count
1053         */
1054        public int getColumns() {
1055            return mColCount;
1056        }
1057
1058        /**
1059         * Returns the list of views overlapping the given cell
1060         *
1061         * @param row the row of the target cell
1062         * @param col the column of the target cell
1063         * @return a list of views overlapping the given column
1064         */
1065        public List<View> get(int row, int col) {
1066            return mRowList.get(row).get(col);
1067        }
1068
1069        /**
1070         * Returns true if the given column contains a top left corner of a view
1071         *
1072         * @param column the column to check
1073         * @return true if one or more views have their top left corner in this column
1074         */
1075        public boolean colContainsTopLeftCorner(int column) {
1076            for (int row = 0; row < mRowCount; row++) {
1077                View view = getTopLeftCorner(row, column);
1078                if (view != null) {
1079                    return true;
1080                }
1081            }
1082
1083            return false;
1084        }
1085
1086        /**
1087         * Returns true if the given row contains a top left corner of a view
1088         *
1089         * @param row the row to check
1090         * @return true if one or more views have their top left corner in this row
1091         */
1092        public boolean rowContainsTopLeftCorner(int row) {
1093            for (int col = 0; col < mColCount; col++) {
1094                View view = getTopLeftCorner(row, col);
1095                if (view != null) {
1096                    return true;
1097                }
1098            }
1099
1100            return false;
1101        }
1102
1103        /**
1104         * Returns a list of views (optionally sorted by increasing row index) that have
1105         * their left edge starting in the given column
1106         *
1107         * @param col the column to look up views for
1108         * @param sort whether to sort the result in increasing row order
1109         * @return a list of views starting in the given column
1110         */
1111        public List<View> viewsStartingInCol(int col, boolean sort) {
1112            List<View> views = new ArrayList<View>();
1113            for (int row = 0; row < mRowCount; row++) {
1114                View view = getTopLeftCorner(row, col);
1115                if (view != null) {
1116                    views.add(view);
1117                }
1118            }
1119
1120            if (sort) {
1121                View.sortByRow(views);
1122            }
1123
1124            return views;
1125        }
1126
1127        /**
1128         * Returns a list of views (optionally sorted by increasing column index) that have
1129         * their top edge starting in the given row
1130         *
1131         * @param row the row to look up views for
1132         * @param sort whether to sort the result in increasing column order
1133         * @return a list of views starting in the given row
1134         */
1135        public List<View> viewsStartingInRow(int row, boolean sort) {
1136            List<View> views = new ArrayList<View>();
1137            for (int col = 0; col < mColCount; col++) {
1138                View view = getTopLeftCorner(row, col);
1139                if (view != null) {
1140                    views.add(view);
1141                }
1142            }
1143
1144            if (sort) {
1145                View.sortByColumn(views);
1146            }
1147
1148            return views;
1149        }
1150
1151        /**
1152         * Returns the pixel width of the given column
1153         *
1154         * @param col the column to look up the width of
1155         * @return the width of the column
1156         */
1157        public int getColumnWidth(int col) {
1158            return mLeft[col + 1] - mLeft[col];
1159        }
1160
1161        /**
1162         * Returns the pixel height of the given row
1163         *
1164         * @param row the row to look up the height of
1165         * @return the height of the row
1166         */
1167        public int getRowHeight(int row) {
1168            return mTop[row + 1] - mTop[row];
1169        }
1170
1171        /**
1172         * Returns the first view found that has its top left corner in the cell given by
1173         * the row and column indexes, or null if not found.
1174         *
1175         * @param row the row of the target cell
1176         * @param col the column of the target cell
1177         * @return a view with its top left corner in the given cell, or null if not found
1178         */
1179        View getTopLeftCorner(int row, int col) {
1180            List<View> views = get(row, col);
1181            if (views.size() > 0) {
1182                for (View view : views) {
1183                    if (view.mRow == row && view.mCol == col) {
1184                        return view;
1185                    }
1186                }
1187            }
1188
1189            return null;
1190        }
1191
1192        public View findRightEdgeView(int col) {
1193            for (int row = 0; row < mRowCount; row++) {
1194                List<View> views = get(row, col);
1195                if (views.size() > 0) {
1196                    List<View> result = new ArrayList<View>();
1197                    for (View view : views) {
1198                        // Ends on the right edge of this column?
1199                        if (view.mCol + view.mColSpan == col + 1) {
1200                            result.add(view);
1201                        }
1202                    }
1203                    if (result.size() > 1) {
1204                        View.sortByColumn(result);
1205                    }
1206                    if (result.size() > 0) {
1207                        return result.get(0);
1208                    }
1209                }
1210            }
1211
1212            return null;
1213        }
1214
1215        public View findBottomEdgeView(int row) {
1216            for (int col = 0; col < mColCount; col++) {
1217                List<View> views = get(row, col);
1218                if (views.size() > 0) {
1219                    List<View> result = new ArrayList<View>();
1220                    for (View view : views) {
1221                        // Ends on the bottom edge of this column?
1222                        if (view.mRow + view.mRowSpan == row + 1) {
1223                            result.add(view);
1224                        }
1225                    }
1226                    if (result.size() > 1) {
1227                        View.sortByRow(result);
1228                    }
1229                    if (result.size() > 0) {
1230                        return result.get(0);
1231                    }
1232
1233                }
1234            }
1235
1236            return null;
1237        }
1238
1239        /**
1240         * Produces a display of view contents along with the pixel positions of each row/column,
1241         * like the following (used for diagnostics only)
1242         * <pre>
1243         *          |0                  |49                 |143                |192           |240
1244         *        36|                   |                   |button2            |
1245         *        72|                   |radioButton1       |button2            |
1246         *        74|button1            |radioButton1       |button2            |
1247         *       108|button1            |                   |button2            |
1248         *       110|                   |                   |button2            |
1249         *       149|                   |                   |                   |
1250         *       320
1251         * </pre>
1252         */
1253        @Override
1254        public String toString() {
1255            // Dump out the view table
1256            int cellWidth = 20;
1257
1258            StringWriter stringWriter = new StringWriter();
1259            PrintWriter out = new PrintWriter(stringWriter);
1260            out.printf("%" + cellWidth + "s", ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
1261            for (int col = 0; col < mColCount + 1; col++) {
1262                out.printf("|%-" + (cellWidth - 1) + "d", mLeft[col]); //$NON-NLS-1$ //$NON-NLS-2$
1263            }
1264            out.printf("\n"); //$NON-NLS-1$
1265            for (int row = 0; row < mRowCount + 1; row++) {
1266                out.printf("%" + cellWidth + "d", mTop[row]); //$NON-NLS-1$ //$NON-NLS-2$
1267                if (row == mRowCount) {
1268                    break;
1269                }
1270                for (int col = 0; col < mColCount; col++) {
1271                    List<View> views = get(row, col);
1272                    StringBuilder sb = new StringBuilder();
1273                    for (View view : views) {
1274                        String id = view != null ? view.getId() : ""; //$NON-NLS-1$
1275                        if (id.startsWith(NEW_ID_PREFIX)) {
1276                            id = id.substring(NEW_ID_PREFIX.length());
1277                        }
1278                        if (id.length() > cellWidth - 2) {
1279                            id = id.substring(0, cellWidth - 2);
1280                        }
1281                        if (sb.length() > 0) {
1282                            sb.append(',');
1283                        }
1284                        sb.append(id);
1285                    }
1286                    String cellString = sb.toString();
1287                    if (cellString.contains(",") && cellString.length() > cellWidth - 2) { //$NON-NLS-1$
1288                        cellString = cellString.substring(0, cellWidth - 6) + "...,"; //$NON-NLS-1$
1289                    }
1290                    out.printf("|%-" + (cellWidth - 2) + "s ", cellString); //$NON-NLS-1$ //$NON-NLS-2$
1291                }
1292                out.printf("\n"); //$NON-NLS-1$
1293            }
1294
1295            out.flush();
1296            return stringWriter.toString();
1297        }
1298    }
1299
1300    /** Holds layout information about an individual view. */
1301    private static class View {
1302        private final Element mElement;
1303        private int mRow = -1;
1304        private int mCol = -1;
1305        private int mRowSpan = -1;
1306        private int mColSpan = -1;
1307        private CanvasViewInfo mInfo;
1308        private String mId;
1309        private List<Pair<String, String>> mHorizConstraints =
1310            new ArrayList<Pair<String, String>>(4);
1311        private List<Pair<String, String>> mVerticalConstraints =
1312            new ArrayList<Pair<String, String>>(4);
1313        private int mGravity;
1314
1315        public View(CanvasViewInfo view, Element element) {
1316            mInfo = view;
1317            mElement = element;
1318            mGravity = GravityHelper.getGravity(element);
1319        }
1320
1321        public int getHeight() {
1322            return mInfo.getAbsRect().height;
1323        }
1324
1325        public int getGravity() {
1326            return mGravity;
1327        }
1328
1329        public String getId() {
1330            return mId;
1331        }
1332
1333        public Element getElement() {
1334            return mElement;
1335        }
1336
1337        public List<Pair<String, String>> getHorizConstraints() {
1338            return mHorizConstraints;
1339        }
1340
1341        public List<Pair<String, String>> getVerticalConstraints() {
1342            return mVerticalConstraints;
1343        }
1344
1345        public boolean isConstrainedHorizontally() {
1346            return mHorizConstraints.size() > 0;
1347        }
1348
1349        public boolean isConstrainedVertically() {
1350            return mVerticalConstraints.size() > 0;
1351        }
1352
1353        public void addHorizConstraint(String property, String value) {
1354            assert property != null && value != null;
1355            // TODO - look for duplicates?
1356            mHorizConstraints.add(Pair.of(property, value));
1357        }
1358
1359        public void addVerticalConstraint(String property, String value) {
1360            assert property != null && value != null;
1361            mVerticalConstraints.add(Pair.of(property, value));
1362        }
1363
1364        public int getLeftEdge() {
1365            return mInfo.getAbsRect().x;
1366        }
1367
1368        public int getTopEdge() {
1369            return mInfo.getAbsRect().y;
1370        }
1371
1372        public int getRightEdge() {
1373            Rectangle bounds = mInfo.getAbsRect();
1374            // +1: make the bounds overlap, so the right edge is the same as the
1375            // left edge of the neighbor etc. Otherwise we end up with lots of 1-pixel wide
1376            // columns between adjacent items.
1377            return bounds.x + bounds.width + 1;
1378        }
1379
1380        public int getBottomEdge() {
1381            Rectangle bounds = mInfo.getAbsRect();
1382            return bounds.y + bounds.height + 1;
1383        }
1384
1385        @Override
1386        public String toString() {
1387            return "View [mId=" + mId + "]"; //$NON-NLS-1$ //$NON-NLS-2$
1388        }
1389
1390        public static void sortByRow(List<View> views) {
1391            Collections.sort(views, new ViewComparator(true/*rowSort*/));
1392        }
1393
1394        public static void sortByColumn(List<View> views) {
1395            Collections.sort(views, new ViewComparator(false/*rowSort*/));
1396        }
1397
1398        /** Comparator to help sort views by row or column index */
1399        private static class ViewComparator implements Comparator<View> {
1400            boolean mRowSort;
1401
1402            public ViewComparator(boolean rowSort) {
1403                mRowSort = rowSort;
1404            }
1405
1406            @Override
1407            public int compare(View view1, View view2) {
1408                if (mRowSort) {
1409                    return view1.mRow - view2.mRow;
1410                } else {
1411                    return view1.mCol - view2.mCol;
1412                }
1413            }
1414        }
1415    }
1416
1417    /**
1418     * An edge list takes a hierarchy of elements and records the bounds of each element
1419     * into various lists such that it can answer queries about shared edges, about which
1420     * particular pixels occur as a boundary edge, etc.
1421     */
1422    private class EdgeList {
1423        private final Map<Element, View> mElementToViewMap = new HashMap<Element, View>(100);
1424        private final Map<String, View> mIdToViewMap = new HashMap<String, View>(100);
1425        private final Map<Integer, List<View>> mLeft = new HashMap<Integer, List<View>>();
1426        private final Map<Integer, List<View>> mTop = new HashMap<Integer, List<View>>();
1427        private final Map<Integer, List<View>> mRight = new HashMap<Integer, List<View>>();
1428        private final Map<Integer, List<View>> mBottom = new HashMap<Integer, List<View>>();
1429        private final Map<Element, Element> mSharedLeftEdge = new HashMap<Element, Element>();
1430        private final Map<Element, Element> mSharedTopEdge = new HashMap<Element, Element>();
1431        private final Map<Element, Element> mSharedRightEdge = new HashMap<Element, Element>();
1432        private final Map<Element, Element> mSharedBottomEdge = new HashMap<Element, Element>();
1433        private final List<Element> mDelete = new ArrayList<Element>();
1434
1435        EdgeList(CanvasViewInfo view) {
1436            analyze(view, true);
1437            mDelete.remove(getElement(view));
1438        }
1439
1440        public void setIdAttributeValue(View view, String id) {
1441            assert id.startsWith(NEW_ID_PREFIX) || id.startsWith(ID_PREFIX);
1442            view.mId = id;
1443            mIdToViewMap.put(getIdBasename(id), view);
1444        }
1445
1446        public View getView(Element element) {
1447            return mElementToViewMap.get(element);
1448        }
1449
1450        public View getView(String id) {
1451            return mIdToViewMap.get(id);
1452        }
1453
1454        public List<View> getTopEdgeViews(Integer topOffset) {
1455            return mTop.get(topOffset);
1456        }
1457
1458        public List<View> getLeftEdgeViews(Integer leftOffset) {
1459            return mLeft.get(leftOffset);
1460        }
1461
1462        void record(Map<Integer, List<View>> map, Integer edge, View info) {
1463            List<View> list = map.get(edge);
1464            if (list == null) {
1465                list = new ArrayList<View>();
1466                map.put(edge, list);
1467            }
1468            list.add(info);
1469        }
1470
1471        private List<Integer> getOffsets(Set<Integer> first, Set<Integer> second) {
1472            Set<Integer> joined = new HashSet<Integer>(first.size() + second.size());
1473            joined.addAll(first);
1474            joined.addAll(second);
1475            List<Integer> unique = new ArrayList<Integer>(joined);
1476            Collections.sort(unique);
1477
1478            return unique;
1479        }
1480
1481        public List<Element> getDeletedElements() {
1482            return mDelete;
1483        }
1484
1485        public List<Integer> getColumnOffsets() {
1486            return getOffsets(mLeft.keySet(), mRight.keySet());
1487        }
1488        public List<Integer> getRowOffsets() {
1489            return getOffsets(mTop.keySet(), mBottom.keySet());
1490        }
1491
1492        private View analyze(CanvasViewInfo view, boolean isRoot) {
1493            View added = null;
1494            if (!mFlatten || !isRemovableLayout(view)) {
1495                added = add(view);
1496                if (!isRoot) {
1497                    return added;
1498                }
1499            } else {
1500                mDelete.add(getElement(view));
1501            }
1502
1503            Element parentElement = getElement(view);
1504            Rectangle parentBounds = view.getAbsRect();
1505
1506            // Build up a table model of the view
1507            for (CanvasViewInfo child : view.getChildren()) {
1508                Rectangle childBounds = child.getAbsRect();
1509                Element childElement = getElement(child);
1510
1511                // See if this view shares the edge with the removed
1512                // parent layout, and if so, record that such that we can
1513                // later handle attachments to the removed parent edges
1514                if (parentBounds.x == childBounds.x) {
1515                    mSharedLeftEdge.put(childElement, parentElement);
1516                }
1517                if (parentBounds.y == childBounds.y) {
1518                    mSharedTopEdge.put(childElement, parentElement);
1519                }
1520                if (parentBounds.x + parentBounds.width == childBounds.x + childBounds.width) {
1521                    mSharedRightEdge.put(childElement, parentElement);
1522                }
1523                if (parentBounds.y + parentBounds.height == childBounds.y + childBounds.height) {
1524                    mSharedBottomEdge.put(childElement, parentElement);
1525                }
1526
1527                if (mFlatten && isRemovableLayout(child)) {
1528                    // When flattening, we want to disregard all layouts and instead
1529                    // add their children!
1530                    for (CanvasViewInfo childView : child.getChildren()) {
1531                        analyze(childView, false);
1532
1533                        Element childViewElement = getElement(childView);
1534                        Rectangle childViewBounds = childView.getAbsRect();
1535
1536                        // See if this view shares the edge with the removed
1537                        // parent layout, and if so, record that such that we can
1538                        // later handle attachments to the removed parent edges
1539                        if (parentBounds.x == childViewBounds.x) {
1540                            mSharedLeftEdge.put(childViewElement, parentElement);
1541                        }
1542                        if (parentBounds.y == childViewBounds.y) {
1543                            mSharedTopEdge.put(childViewElement, parentElement);
1544                        }
1545                        if (parentBounds.x + parentBounds.width == childViewBounds.x
1546                                + childViewBounds.width) {
1547                            mSharedRightEdge.put(childViewElement, parentElement);
1548                        }
1549                        if (parentBounds.y + parentBounds.height == childViewBounds.y
1550                                + childViewBounds.height) {
1551                            mSharedBottomEdge.put(childViewElement, parentElement);
1552                        }
1553                    }
1554                    mDelete.add(childElement);
1555                } else {
1556                    analyze(child, false);
1557                }
1558            }
1559
1560            return added;
1561        }
1562
1563        public View getSharedLeftEdge(Element element) {
1564            return getSharedEdge(element, mSharedLeftEdge);
1565        }
1566
1567        public View getSharedRightEdge(Element element) {
1568            return getSharedEdge(element, mSharedRightEdge);
1569        }
1570
1571        public View getSharedTopEdge(Element element) {
1572            return getSharedEdge(element, mSharedTopEdge);
1573        }
1574
1575        public View getSharedBottomEdge(Element element) {
1576            return getSharedEdge(element, mSharedBottomEdge);
1577        }
1578
1579        private View getSharedEdge(Element element, Map<Element, Element> sharedEdgeMap) {
1580            Element original = element;
1581
1582            while (element != null) {
1583                View view = getView(element);
1584                if (view != null) {
1585                    assert isAncestor(element, original);
1586                    return view;
1587                }
1588                element = sharedEdgeMap.get(element);
1589            }
1590
1591            return null;
1592        }
1593
1594        private View add(CanvasViewInfo info) {
1595            Rectangle bounds = info.getAbsRect();
1596            Element element = getElement(info);
1597            View view = new View(info, element);
1598            mElementToViewMap.put(element, view);
1599            record(mLeft, Integer.valueOf(bounds.x), view);
1600            record(mTop, Integer.valueOf(bounds.y), view);
1601            record(mRight, Integer.valueOf(view.getRightEdge()), view);
1602            record(mBottom, Integer.valueOf(view.getBottomEdge()), view);
1603            return view;
1604        }
1605
1606        /**
1607         * Returns true if the given {@link CanvasViewInfo} represents an element we
1608         * should remove in a flattening conversion. We don't want to remove non-layout
1609         * views, or layout views that for example contain drawables on their own.
1610         */
1611        private boolean isRemovableLayout(CanvasViewInfo child) {
1612            // The element being converted is NOT removable!
1613            Element element = getElement(child);
1614            if (element == mLayout) {
1615                return false;
1616            }
1617
1618            ElementDescriptor descriptor = child.getUiViewNode().getDescriptor();
1619            String name = descriptor.getXmlLocalName();
1620            if (name.equals(LINEAR_LAYOUT) || name.equals(RELATIVE_LAYOUT)) {
1621                // Don't delete layouts that provide a background image or gradient
1622                if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) {
1623                    AdtPlugin.log(IStatus.WARNING,
1624                            "Did not flatten layout %1$s because it defines a '%2$s' attribute",
1625                            VisualRefactoring.getId(element), ATTR_BACKGROUND);
1626                    return false;
1627                }
1628
1629                return true;
1630            }
1631
1632            return false;
1633        }
1634    }
1635}
1636