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  */
16 package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
17 
18 import static com.android.SdkConstants.ANDROID_URI;
19 import static com.android.SdkConstants.ATTR_BACKGROUND;
20 import static com.android.SdkConstants.ATTR_COLUMN_COUNT;
21 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
22 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
23 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
24 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
25 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
26 import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
27 import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN;
28 import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
29 import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
30 import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
31 import static com.android.SdkConstants.ATTR_LAYOUT_ROW;
32 import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN;
33 import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
34 import static com.android.SdkConstants.ATTR_ORIENTATION;
35 import static com.android.SdkConstants.FQCN_GRID_LAYOUT;
36 import static com.android.SdkConstants.FQCN_SPACE;
37 import static com.android.SdkConstants.GRAVITY_VALUE_FILL;
38 import static com.android.SdkConstants.GRAVITY_VALUE_FILL_HORIZONTAL;
39 import static com.android.SdkConstants.GRAVITY_VALUE_FILL_VERTICAL;
40 import static com.android.SdkConstants.ID_PREFIX;
41 import static com.android.SdkConstants.LINEAR_LAYOUT;
42 import static com.android.SdkConstants.NEW_ID_PREFIX;
43 import static com.android.SdkConstants.RADIO_GROUP;
44 import static com.android.SdkConstants.RELATIVE_LAYOUT;
45 import static com.android.SdkConstants.SPACE;
46 import static com.android.SdkConstants.TABLE_LAYOUT;
47 import static com.android.SdkConstants.TABLE_ROW;
48 import static com.android.SdkConstants.VALUE_FILL_PARENT;
49 import static com.android.SdkConstants.VALUE_HORIZONTAL;
50 import static com.android.SdkConstants.VALUE_MATCH_PARENT;
51 import static com.android.SdkConstants.VALUE_VERTICAL;
52 import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
53 import static com.android.ide.common.layout.GravityHelper.GRAVITY_HORIZ_MASK;
54 import static com.android.ide.common.layout.GravityHelper.GRAVITY_VERT_MASK;
55 
56 import com.android.ide.common.api.IViewMetadata.FillPreference;
57 import com.android.ide.common.layout.BaseLayoutRule;
58 import com.android.ide.common.layout.GravityHelper;
59 import com.android.ide.common.layout.GridLayoutRule;
60 import com.android.ide.eclipse.adt.AdtPlugin;
61 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
62 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
63 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
64 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
65 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
66 import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
67 import com.android.ide.eclipse.adt.internal.project.SupportLibraryHelper;
68 
69 import org.eclipse.core.resources.IFile;
70 import org.eclipse.core.runtime.IStatus;
71 import org.eclipse.swt.graphics.Rectangle;
72 import org.eclipse.text.edits.InsertEdit;
73 import org.eclipse.text.edits.MalformedTreeException;
74 import org.eclipse.text.edits.MultiTextEdit;
75 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
76 import org.w3c.dom.Attr;
77 import org.w3c.dom.Element;
78 import org.w3c.dom.NamedNodeMap;
79 import org.w3c.dom.Node;
80 
81 import java.util.ArrayList;
82 import java.util.Collection;
83 import java.util.Collections;
84 import java.util.HashMap;
85 import java.util.HashSet;
86 import java.util.Iterator;
87 import java.util.List;
88 import java.util.Map;
89 import java.util.Set;
90 
91 /**
92  * Helper class which performs the bulk of the layout conversion to grid layout
93  * <p>
94  * Future enhancements:
95  * <ul>
96  * <li>Render the layout at multiple screen sizes and analyze how the widget bounds
97  *  change and use this to infer gravity
98  *  <li> Use the layout_width and layout_height attributes on views to infer column and
99  *  row flexibility (and as mentioned above, possibly layout_weight).
100  * move and stretch and use that to add in additional constraints
101  *  <li> Take into account existing margins and add/subtract those from the
102  *  bounds computations and either clear or update them.
103  * <li>Try to reorder elements into their natural order
104  * <li> Try to preserve spacing? Right now everything gets converted into a compact
105  *   grid with no spacing between the views; consider inserting {@code <Space>} views
106  *   with dimensions based on existing distances.
107  * </ul>
108  */
109 @SuppressWarnings("restriction") // DOM model access
110 class GridLayoutConverter {
111     private final MultiTextEdit mRootEdit;
112     private final boolean mFlatten;
113     private final Element mLayout;
114     private final ChangeLayoutRefactoring mRefactoring;
115     private final CanvasViewInfo mRootView;
116 
117     private List<View> mViews;
118     private String mNamespace;
119     private int mColumnCount;
120 
121     /** Creates a new {@link GridLayoutConverter} */
GridLayoutConverter(ChangeLayoutRefactoring refactoring, Element layout, boolean flatten, MultiTextEdit rootEdit, CanvasViewInfo rootView)122     GridLayoutConverter(ChangeLayoutRefactoring refactoring,
123             Element layout, boolean flatten, MultiTextEdit rootEdit, CanvasViewInfo rootView) {
124         mRefactoring = refactoring;
125         mLayout = layout;
126         mFlatten = flatten;
127         mRootEdit = rootEdit;
128         mRootView = rootView;
129     }
130 
131     /** Performs conversion from any layout to a RelativeLayout */
convertToGridLayout()132     public void convertToGridLayout() {
133         if (mRootView == null) {
134             return;
135         }
136 
137         // Locate the view for the layout
138         CanvasViewInfo layoutView = findViewForElement(mRootView, mLayout);
139         if (layoutView == null || layoutView.getChildren().size() == 0) {
140             // No children. THAT was an easy conversion!
141             return;
142         }
143 
144         // Study the layout and get information about how to place individual elements
145         GridModel gridModel = new GridModel(layoutView, mLayout, mFlatten);
146         mViews = gridModel.getViews();
147         mColumnCount = gridModel.computeColumnCount();
148 
149         deleteRemovedElements(gridModel.getDeletedElements());
150         mNamespace = mRefactoring.getAndroidNamespacePrefix();
151 
152         processGravities();
153 
154         // Insert space views if necessary
155         insertStretchableSpans();
156 
157         // Create/update relative layout constraints
158         assignGridAttributes();
159 
160         removeUndefinedAttrs();
161 
162         if (mColumnCount > 0) {
163             mRefactoring.setAttribute(mRootEdit, mLayout, ANDROID_URI,
164                 mNamespace, ATTR_COLUMN_COUNT, Integer.toString(mColumnCount));
165         }
166     }
167 
insertStretchableSpans()168     private void insertStretchableSpans() {
169         // Look at the rows and columns and determine if we need to have a stretchable
170         // row and/or a stretchable column in the layout.
171         // In a GridLayout, a row or column is stretchable if it defines a gravity (regardless
172         // of what the gravity is -- in other words, a column is not just stretchable if it
173         // has gravity=fill but also if it has gravity=left). Furthermore, ALL the elements
174         // in the row/column have to be stretchable for the overall row/column to be
175         // considered stretchable.
176 
177         // Map from row index to boolean for "is the row fixed/inflexible?"
178         Map<Integer, Boolean> rowFixed = new HashMap<Integer, Boolean>();
179         Map<Integer, Boolean> columnFixed = new HashMap<Integer, Boolean>();
180         for (View view : mViews) {
181             if (view.mElement == mLayout) {
182                 continue;
183             }
184 
185             int gravity = GravityHelper.getGravity(view.mGravity, 0);
186             if ((gravity & GRAVITY_HORIZ_MASK) == 0) {
187                 columnFixed.put(view.mCol, true);
188             } else if (!columnFixed.containsKey(view.mCol)) {
189                 columnFixed.put(view.mCol, false);
190             }
191             if ((gravity & GRAVITY_VERT_MASK) == 0) {
192                 rowFixed.put(view.mRow, true);
193             } else if (!rowFixed.containsKey(view.mRow)) {
194                 rowFixed.put(view.mRow, false);
195             }
196         }
197 
198         boolean hasStretchableRow = false;
199         boolean hasStretchableColumn = false;
200         for (boolean fixed : rowFixed.values()) {
201             if (!fixed) {
202                 hasStretchableRow = true;
203             }
204         }
205         for (boolean fixed : columnFixed.values()) {
206             if (!fixed) {
207                 hasStretchableColumn = true;
208             }
209         }
210 
211         if (!hasStretchableRow || !hasStretchableColumn) {
212             // Insert <Space> to hold stretchable space
213             // TODO: May also have to increment column count!
214             int offset = 0; // WHERE?
215 
216             String gridLayout = mLayout.getTagName();
217             if (mLayout instanceof IndexedRegion) {
218                 IndexedRegion region = (IndexedRegion) mLayout;
219                 int end = region.getEndOffset();
220                 // TODO: Look backwards for the "</"
221                 // (and can it ever be <foo/>) ?
222                 end -= (gridLayout.length() + 3); // 3: <, /, >
223                 offset = end;
224             }
225 
226             int row = rowFixed.size();
227             int column = columnFixed.size();
228             StringBuilder sb = new StringBuilder(64);
229             String spaceTag = SPACE;
230             IFile file = mRefactoring.getFile();
231             if (file != null) {
232                 spaceTag = SupportLibraryHelper.getTagFor(file.getProject(), FQCN_SPACE);
233                 if (spaceTag.equals(FQCN_SPACE)) {
234                     spaceTag = SPACE;
235                 }
236             }
237 
238             sb.append('<').append(spaceTag).append(' ');
239             String gravity;
240             if (!hasStretchableRow && !hasStretchableColumn) {
241                 gravity = GRAVITY_VALUE_FILL;
242             } else if (!hasStretchableRow) {
243                 gravity = GRAVITY_VALUE_FILL_VERTICAL;
244             } else {
245                 assert !hasStretchableColumn;
246                 gravity = GRAVITY_VALUE_FILL_HORIZONTAL;
247             }
248 
249             sb.append(mNamespace).append(':');
250             sb.append(ATTR_LAYOUT_GRAVITY).append('=').append('"').append(gravity);
251             sb.append('"').append(' ');
252 
253             sb.append(mNamespace).append(':');
254             sb.append(ATTR_LAYOUT_ROW).append('=').append('"').append(Integer.toString(row));
255             sb.append('"').append(' ');
256 
257             sb.append(mNamespace).append(':');
258             sb.append(ATTR_LAYOUT_COLUMN).append('=').append('"').append(Integer.toString(column));
259             sb.append('"').append('/').append('>');
260 
261             String space = sb.toString();
262             InsertEdit replace = new InsertEdit(offset, space);
263             mRootEdit.addChild(replace);
264 
265             mColumnCount++;
266         }
267     }
268 
removeUndefinedAttrs()269     private void removeUndefinedAttrs() {
270         ViewElementDescriptor descriptor = mRefactoring.getElementDescriptor(FQCN_GRID_LAYOUT);
271         if (descriptor == null) {
272             return;
273         }
274 
275         Set<String> defined = new HashSet<String>();
276         AttributeDescriptor[] layoutAttributes = descriptor.getLayoutAttributes();
277         for (AttributeDescriptor attribute : layoutAttributes) {
278             defined.add(attribute.getXmlLocalName());
279         }
280 
281         for (View view : mViews) {
282             Element child = view.mElement;
283 
284             List<Attr> attributes = mRefactoring.findLayoutAttributes(child);
285             for (Attr attribute : attributes) {
286                 String name = attribute.getLocalName();
287                 if (!defined.contains(name)) {
288                     // Remove it
289                     try {
290                         mRefactoring.removeAttribute(mRootEdit, child, attribute.getNamespaceURI(),
291                                 name);
292                     } catch (MalformedTreeException mte) {
293                         // Sometimes refactoring has modified attribute; not
294                         // removing
295                         // it is non-fatal so just warn instead of letting
296                         // refactoring
297                         // operation abort
298                         AdtPlugin.log(IStatus.WARNING,
299                                 "Could not remove unsupported attribute %1$s; " + //$NON-NLS-1$
300                                         "already modified during refactoring?", //$NON-NLS-1$
301                                 attribute.getLocalName());
302                     }
303                 }
304             }
305         }
306     }
307 
308     /** Removes any elements targeted for deletion */
deleteRemovedElements(List<Element> delete)309     private void deleteRemovedElements(List<Element> delete) {
310         if (mFlatten && delete.size() > 0) {
311             for (Element element : delete) {
312                 mRefactoring.removeElementTags(mRootEdit, element, delete,
313                         false /*changeIndentation*/);
314             }
315         }
316     }
317 
318     /**
319      * Creates refactoring edits which adds or updates the grid attributes
320      */
assignGridAttributes()321     private void assignGridAttributes() {
322         // We always convert to horizontal grid layouts for now
323         mRefactoring.setAttribute(mRootEdit, mLayout, ANDROID_URI,
324                 mNamespace, ATTR_ORIENTATION, VALUE_HORIZONTAL);
325 
326         assignCellAttributes();
327     }
328 
329     /**
330      * Assign cell attributes to the table, skipping those that will be implied
331      * by the grid model
332      */
assignCellAttributes()333     private void assignCellAttributes() {
334         int implicitRow = 0;
335         int implicitColumn = 0;
336         int nextRow = 0;
337         for (View view : mViews) {
338             Element element = view.getElement();
339             if (element == mLayout) {
340                 continue;
341             }
342 
343             int row = view.getRow();
344             int column = view.getColumn();
345 
346             if (column != implicitColumn && (implicitColumn > 0 || implicitRow > 0)) {
347                 mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI,
348                         mNamespace, ATTR_LAYOUT_COLUMN, Integer.toString(column));
349                 if (column < implicitColumn) {
350                     implicitRow++;
351                 }
352                 implicitColumn = column;
353             }
354             if (row != implicitRow) {
355                 mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI,
356                         mNamespace, ATTR_LAYOUT_ROW, Integer.toString(row));
357                 implicitRow = row;
358             }
359 
360             int rowSpan = view.getRowSpan();
361             int columnSpan = view.getColumnSpan();
362             assert columnSpan >= 1;
363 
364             if (rowSpan > 1) {
365                 mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI,
366                         mNamespace, ATTR_LAYOUT_ROW_SPAN, Integer.toString(rowSpan));
367             }
368             if (columnSpan > 1) {
369                 mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI,
370                         mNamespace, ATTR_LAYOUT_COLUMN_SPAN,
371                         Integer.toString(columnSpan));
372             }
373             nextRow = Math.max(nextRow, row + rowSpan);
374 
375             // wrap_content is redundant in GridLayouts
376             Attr width = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
377             if (width != null && VALUE_WRAP_CONTENT.equals(width.getValue())) {
378                 mRefactoring.removeAttribute(mRootEdit, width);
379             }
380             Attr height = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
381             if (height != null && VALUE_WRAP_CONTENT.equals(height.getValue())) {
382                 mRefactoring.removeAttribute(mRootEdit, height);
383             }
384 
385             // Fix up children moved from LinearLayouts that have "invalid" sizes that
386             // was intended for layout weight handling in their old parent
387             if (LINEAR_LAYOUT.equals(element.getParentNode().getNodeName())) {
388                 convert0dipToWrapContent(element);
389             }
390 
391             implicitColumn += columnSpan;
392             if (implicitColumn >= mColumnCount) {
393                 implicitColumn = 0;
394                 assert nextRow > implicitRow;
395                 implicitRow = nextRow;
396             }
397         }
398     }
399 
processGravities()400     private void processGravities() {
401         for (View view : mViews) {
402             Element element = view.getElement();
403             if (element == mLayout) {
404                 continue;
405             }
406 
407             Attr width = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
408             Attr height = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
409             String gravity = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY);
410             String newGravity = null;
411             if (width != null && (VALUE_MATCH_PARENT.equals(width.getValue()) ||
412                     VALUE_FILL_PARENT.equals(width.getValue()))) {
413                 mRefactoring.removeAttribute(mRootEdit, width);
414                 newGravity = gravity = GRAVITY_VALUE_FILL_HORIZONTAL;
415             }
416             if (height != null && (VALUE_MATCH_PARENT.equals(height.getValue()) ||
417                     VALUE_FILL_PARENT.equals(height.getValue()))) {
418                 mRefactoring.removeAttribute(mRootEdit, height);
419                 if (newGravity == GRAVITY_VALUE_FILL_HORIZONTAL) {
420                     newGravity = GRAVITY_VALUE_FILL;
421                 } else {
422                     newGravity = GRAVITY_VALUE_FILL_VERTICAL;
423                 }
424                 gravity = newGravity;
425             }
426 
427             if (gravity == null || gravity.length() == 0) {
428                 ElementDescriptor descriptor = view.mInfo.getUiViewNode().getDescriptor();
429                 if (descriptor instanceof ViewElementDescriptor) {
430                     ViewElementDescriptor viewDescriptor = (ViewElementDescriptor) descriptor;
431                     String fqcn = viewDescriptor.getFullClassName();
432                     FillPreference fill = ViewMetadataRepository.get().getFillPreference(fqcn);
433                     gravity = GridLayoutRule.computeDefaultGravity(fill);
434                     if (gravity != null) {
435                         newGravity = gravity;
436                     }
437                 }
438             }
439 
440             if (newGravity != null) {
441                 mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI,
442                         mNamespace, ATTR_LAYOUT_GRAVITY, newGravity);
443             }
444 
445             view.mGravity = newGravity != null ? newGravity : gravity;
446         }
447     }
448 
449 
450     /** Converts 0dip values in layout_width and layout_height to wrap_content instead */
convert0dipToWrapContent(Element child)451     private void convert0dipToWrapContent(Element child) {
452         // Must convert layout_height="0dip" to layout_height="wrap_content".
453         // (And since wrap_content is the default, what we really do is remove
454         // the attribute completely.)
455         // 0dip is a special trick used in linear layouts in the presence of
456         // weights where 0dip ensures that the height of the view is not taken
457         // into account when distributing the weights. However, when converted
458         // to RelativeLayout this will instead cause the view to actually be assigned
459         // 0 height.
460         Attr height = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
461         // 0dip, 0dp, 0px, etc
462         if (height != null && height.getValue().startsWith("0")) { //$NON-NLS-1$
463             mRefactoring.removeAttribute(mRootEdit, height);
464         }
465         Attr width = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
466         if (width != null && width.getValue().startsWith("0")) { //$NON-NLS-1$
467             mRefactoring.removeAttribute(mRootEdit, width);
468         }
469     }
470 
471     /**
472      * Searches a view hierarchy and locates the {@link CanvasViewInfo} for the given
473      * {@link Element}
474      *
475      * @param info the root {@link CanvasViewInfo} to search below
476      * @param element the target element
477      * @return the {@link CanvasViewInfo} which corresponds to the given element
478      */
findViewForElement(CanvasViewInfo info, Element element)479     private CanvasViewInfo findViewForElement(CanvasViewInfo info, Element element) {
480         if (getElement(info) == element) {
481             return info;
482         }
483 
484         for (CanvasViewInfo child : info.getChildren()) {
485             CanvasViewInfo result = findViewForElement(child, element);
486             if (result != null) {
487                 return result;
488             }
489         }
490 
491         return null;
492     }
493 
494     /** Returns the {@link Element} for the given {@link CanvasViewInfo} */
getElement(CanvasViewInfo info)495     private static Element getElement(CanvasViewInfo info) {
496         Node node = info.getUiViewNode().getXmlNode();
497         if (node instanceof Element) {
498             return (Element) node;
499         }
500 
501         return null;
502     }
503 
504 
505     /** Holds layout information about an individual view */
506     private static class View {
507         private final Element mElement;
508         private int mRow = -1;
509         private int mCol = -1;
510         private int mRowSpan = -1;
511         private int mColSpan = -1;
512         private int mX1;
513         private int mY1;
514         private int mX2;
515         private int mY2;
516         private CanvasViewInfo mInfo;
517         private String mGravity;
518 
View(CanvasViewInfo view, Element element)519         public View(CanvasViewInfo view, Element element) {
520             mInfo = view;
521             mElement = element;
522 
523             Rectangle b = mInfo.getAbsRect();
524             mX1 = b.x;
525             mX2 = b.x + b.width;
526             mY1 = b.y;
527             mY2 = b.y + b.height;
528         }
529 
530         /**
531          * Returns the element for this view
532          *
533          * @return the element for the view
534          */
getElement()535         public Element getElement() {
536             return mElement;
537         }
538 
539         /**
540          * The assigned row for this view
541          *
542          * @return the assigned row
543          */
getRow()544         public int getRow() {
545             return mRow;
546         }
547 
548         /**
549          * The assigned column for this view
550          *
551          * @return the assigned column
552          */
getColumn()553         public int getColumn() {
554             return mCol;
555         }
556 
557         /**
558          * The assigned row span for this view
559          *
560          * @return the assigned row span
561          */
getRowSpan()562         public int getRowSpan() {
563             return mRowSpan;
564         }
565 
566         /**
567          * The assigned column span for this view
568          *
569          * @return the assigned column span
570          */
getColumnSpan()571         public int getColumnSpan() {
572             return mColSpan;
573         }
574 
575         /**
576          * The left edge of the view to be used for placement
577          *
578          * @return the left edge x coordinate
579          */
getLeftEdge()580         public int getLeftEdge() {
581             return mX1;
582         }
583 
584         /**
585          * The top edge of the view to be used for placement
586          *
587          * @return the top edge y coordinate
588          */
getTopEdge()589         public int getTopEdge() {
590             return mY1;
591         }
592 
593         /**
594          * The right edge of the view to be used for placement
595          *
596          * @return the right edge x coordinate
597          */
getRightEdge()598         public int getRightEdge() {
599             return mX2;
600         }
601 
602         /**
603          * The bottom edge of the view to be used for placement
604          *
605          * @return the bottom edge y coordinate
606          */
getBottomEdge()607         public int getBottomEdge() {
608             return mY2;
609         }
610 
611         @Override
toString()612         public String toString() {
613             return "View(" + VisualRefactoring.getId(mElement) + ": " + mX1 + "," + mY1 + ")";
614         }
615     }
616 
617     /** Grid model for the views found in the view hierarchy, partitioned into rows and columns */
618     private static class GridModel {
619         private final List<View> mViews = new ArrayList<View>();
620         private final List<Element> mDelete = new ArrayList<Element>();
621         private final Map<Element, View> mElementToView = new HashMap<Element, View>();
622         private Element mLayout;
623         private boolean mFlatten;
624 
GridModel(CanvasViewInfo view, Element layout, boolean flatten)625         GridModel(CanvasViewInfo view, Element layout, boolean flatten) {
626             mLayout = layout;
627             mFlatten = flatten;
628 
629             scan(view, true);
630             analyzeKnownLayouts();
631             initializeColumns();
632             initializeRows();
633             mDelete.remove(getElement(view));
634         }
635 
636         /**
637          * Returns the {@link View} objects to be placed in the grid
638          *
639          * @return list of {@link View} objects, never null but possibly empty
640          */
getViews()641         public List<View> getViews() {
642             return mViews;
643         }
644 
645         /**
646          * Returns the list of elements that are scheduled for deletion in the
647          * flattening operation
648          *
649          * @return elements to be deleted, never null but possibly empty
650          */
getDeletedElements()651         public List<Element> getDeletedElements() {
652             return mDelete;
653         }
654 
655         /**
656          * Compute and return column count
657          *
658          * @return the column count
659          */
computeColumnCount()660         public int computeColumnCount() {
661             int columnCount = 0;
662             for (View view : mViews) {
663                 if (view.getElement() == mLayout) {
664                     continue;
665                 }
666 
667                 int column = view.getColumn();
668                 int columnSpan = view.getColumnSpan();
669                 if (column + columnSpan > columnCount) {
670                     columnCount = column + columnSpan;
671                 }
672             }
673             return columnCount;
674         }
675 
676         /**
677          * Initializes the column and columnSpan attributes of the views
678          */
initializeColumns()679         private void initializeColumns() {
680             // Now initialize table view row, column and spans
681             Map<Integer, List<View>> mColumnViews = new HashMap<Integer, List<View>>();
682             for (View view : mViews) {
683                 if (view.mElement == mLayout) {
684                     continue;
685                 }
686                 int x = view.getLeftEdge();
687                 List<View> list = mColumnViews.get(x);
688                 if (list == null) {
689                     list = new ArrayList<View>();
690                     mColumnViews.put(x, list);
691                 }
692                 list.add(view);
693             }
694 
695             List<Integer> columnOffsets = new ArrayList<Integer>(mColumnViews.keySet());
696             Collections.sort(columnOffsets);
697 
698             int columnIndex = 0;
699             for (Integer column : columnOffsets) {
700                 List<View> views = mColumnViews.get(column);
701                 if (views != null) {
702                     for (View view : views) {
703                         view.mCol = columnIndex;
704                     }
705                 }
706                 columnIndex++;
707             }
708             // Initialize column spans
709             for (View view : mViews) {
710                 if (view.mElement == mLayout) {
711                     continue;
712                 }
713                 int index = Collections.binarySearch(columnOffsets, view.getRightEdge());
714                 int column;
715                 if (index == -1) {
716                     // Smaller than the first element; just use the first column
717                     column = 0;
718                 } else if (index < 0) {
719                     column = -(index + 2);
720                 } else {
721                     column = index;
722                 }
723 
724                 if (column < view.mCol) {
725                     column = view.mCol;
726                 }
727 
728                 view.mColSpan = column - view.mCol + 1;
729             }
730         }
731 
732         /**
733          * Initializes the row and rowSpan attributes of the views
734          */
initializeRows()735         private void initializeRows() {
736             Map<Integer, List<View>> mRowViews = new HashMap<Integer, List<View>>();
737             for (View view : mViews) {
738                 if (view.mElement == mLayout) {
739                     continue;
740                 }
741                 int y = view.getTopEdge();
742                 List<View> list = mRowViews.get(y);
743                 if (list == null) {
744                     list = new ArrayList<View>();
745                     mRowViews.put(y, list);
746                 }
747                 list.add(view);
748             }
749 
750             List<Integer> rowOffsets = new ArrayList<Integer>(mRowViews.keySet());
751             Collections.sort(rowOffsets);
752 
753             int rowIndex = 0;
754             for (Integer row : rowOffsets) {
755                 List<View> views = mRowViews.get(row);
756                 if (views != null) {
757                     for (View view : views) {
758                         view.mRow = rowIndex;
759                     }
760                 }
761                 rowIndex++;
762             }
763 
764             // Initialize row spans
765             for (View view : mViews) {
766                 if (view.mElement == mLayout) {
767                     continue;
768                 }
769                 int index = Collections.binarySearch(rowOffsets, view.getBottomEdge());
770                 int row;
771                 if (index == -1) {
772                     // Smaller than the first element; just use the first row
773                     row = 0;
774                 } else if (index < 0) {
775                     row = -(index + 2);
776                 } else {
777                     row = index;
778                 }
779 
780                 if (row < view.mRow) {
781                     row = view.mRow;
782                 }
783 
784                 view.mRowSpan = row - view.mRow + 1;
785             }
786         }
787 
788         /**
789          * Walks over a given view hierarchy and locates views to be placed in
790          * the grid layout (or deleted if we are flattening the hierarchy)
791          *
792          * @param view the view to analyze
793          * @param isRoot whether this view is the root (which cannot be removed)
794          * @return the {@link View} object for the {@link CanvasViewInfo}
795          *         hierarchy we just analyzed, or null
796          */
scan(CanvasViewInfo view, boolean isRoot)797         private View scan(CanvasViewInfo view, boolean isRoot) {
798             View added = null;
799             if (!mFlatten || !isRemovableLayout(view)) {
800                 added = add(view);
801                 if (!isRoot) {
802                     return added;
803                 }
804             } else {
805                 mDelete.add(getElement(view));
806             }
807 
808             // Build up a table model of the view
809             for (CanvasViewInfo child : view.getChildren()) {
810                 Element childElement = getElement(child);
811 
812                 // See if this view shares the edge with the removed
813                 // parent layout, and if so, record that such that we can
814                 // later handle attachments to the removed parent edges
815 
816                 if (mFlatten && isRemovableLayout(child)) {
817                     // When flattening, we want to disregard all layouts and instead
818                     // add their children!
819                     for (CanvasViewInfo childView : child.getChildren()) {
820                         scan(childView, false);
821                     }
822                     mDelete.add(childElement);
823                 } else {
824                     scan(child, false);
825                 }
826             }
827 
828             return added;
829         }
830 
831         /** Adds the given {@link CanvasViewInfo} into our internal view list */
add(CanvasViewInfo info)832         private View add(CanvasViewInfo info) {
833             Element element = getElement(info);
834             View view = new View(info, element);
835             mViews.add(view);
836             mElementToView.put(element, view);
837             return view;
838         }
839 
analyzeKnownLayouts()840         private void analyzeKnownLayouts() {
841             Set<Element> parents = new HashSet<Element>();
842             for (View view : mViews) {
843                 Node parent = view.getElement().getParentNode();
844                 if (parent instanceof Element) {
845                     parents.add((Element) parent);
846                 }
847             }
848 
849             List<Collection<View>> rowGroups = new ArrayList<Collection<View>>();
850             List<Collection<View>> columnGroups = new ArrayList<Collection<View>>();
851             for (Element parent : parents) {
852                 String tagName = parent.getTagName();
853                 if (tagName.equals(LINEAR_LAYOUT) || tagName.equals(TABLE_LAYOUT) ||
854                         tagName.equals(TABLE_ROW) || tagName.equals(RADIO_GROUP)) {
855                     Set<View> group = new HashSet<View>();
856                     for (Element child : DomUtilities.getChildren(parent)) {
857                         View view = mElementToView.get(child);
858                         if (view != null) {
859                             group.add(view);
860                         }
861                     }
862                     if (group.size() > 1) {
863                         boolean isVertical = VALUE_VERTICAL.equals(parent.getAttributeNS(
864                                 ANDROID_URI, ATTR_ORIENTATION));
865                         if (tagName.equals(TABLE_LAYOUT)) {
866                             isVertical = true;
867                         } else if (tagName.equals(TABLE_ROW)) {
868                             isVertical = false;
869                         }
870                         if (isVertical) {
871                             columnGroups.add(group);
872                         } else {
873                             rowGroups.add(group);
874                         }
875                     }
876                 } else if (tagName.equals(RELATIVE_LAYOUT)) {
877                     List<Element> children = DomUtilities.getChildren(parent);
878                     for (Element child : children) {
879                         View view = mElementToView.get(child);
880                         if (view == null) {
881                             continue;
882                         }
883                         NamedNodeMap attributes = child.getAttributes();
884                         for (int i = 0, n = attributes.getLength(); i < n; i++) {
885                             Attr attr = (Attr) attributes.item(i);
886                             String name = attr.getLocalName();
887                             if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
888                                 boolean alignVertical =
889                                         name.equals(ATTR_LAYOUT_ALIGN_TOP) ||
890                                         name.equals(ATTR_LAYOUT_ALIGN_BOTTOM) ||
891                                         name.equals(ATTR_LAYOUT_ALIGN_BASELINE);
892                                 boolean alignHorizontal =
893                                         name.equals(ATTR_LAYOUT_ALIGN_LEFT) ||
894                                         name.equals(ATTR_LAYOUT_ALIGN_RIGHT);
895                                 if (!alignVertical && !alignHorizontal) {
896                                     continue;
897                                 }
898                                 String value = attr.getValue();
899                                 if (value.startsWith(ID_PREFIX)
900                                         || value.startsWith(NEW_ID_PREFIX)) {
901                                     String targetName = BaseLayoutRule.stripIdPrefix(value);
902                                     Element target = null;
903                                     for (Element c : children) {
904                                         String id = VisualRefactoring.getId(c);
905                                         if (targetName.equals(BaseLayoutRule.stripIdPrefix(id))) {
906                                             target = c;
907                                             break;
908                                         }
909                                     }
910                                     View targetView = mElementToView.get(target);
911                                     if (targetView != null) {
912                                         List<View> group = new ArrayList<View>(2);
913                                         group.add(view);
914                                         group.add(targetView);
915                                         if (alignHorizontal) {
916                                             columnGroups.add(group);
917                                         } else {
918                                             assert alignVertical;
919                                             rowGroups.add(group);
920                                         }
921                                     }
922                                 }
923                             }
924                         }
925                     }
926                 } else {
927                     // TODO: Consider looking for interesting metadata from other layouts
928                 }
929             }
930 
931             // Assign the same top or left coordinates to the groups to ensure that they
932             // all get positioned in the same row or column
933             for (Collection<View> rowGroup : rowGroups) {
934                 // Find the smallest one
935                 Iterator<View> iterator = rowGroup.iterator();
936                 int smallest = iterator.next().mY1;
937                 while (iterator.hasNext()) {
938                     smallest = Math.min(smallest, iterator.next().mY1);
939                 }
940                 for (View view : rowGroup) {
941                    view.mY2 -= (view.mY1 - smallest);
942                    view.mY1 = smallest;
943                 }
944             }
945             for (Collection<View> columnGroup : columnGroups) {
946                 Iterator<View> iterator = columnGroup.iterator();
947                 int smallest = iterator.next().mX1;
948                 while (iterator.hasNext()) {
949                     smallest = Math.min(smallest, iterator.next().mX1);
950                 }
951                 for (View view : columnGroup) {
952                    view.mX2 -= (view.mX1 - smallest);
953                    view.mX1 = smallest;
954                 }
955             }
956         }
957 
958         /**
959          * Returns true if the given {@link CanvasViewInfo} represents an element we
960          * should remove in a flattening conversion. We don't want to remove non-layout
961          * views, or layout views that for example contain drawables on their own.
962          */
isRemovableLayout(CanvasViewInfo child)963         private boolean isRemovableLayout(CanvasViewInfo child) {
964             // The element being converted is NOT removable!
965             Element element = getElement(child);
966             if (element == mLayout) {
967                 return false;
968             }
969 
970             ElementDescriptor descriptor = child.getUiViewNode().getDescriptor();
971             String name = descriptor.getXmlLocalName();
972             if (name.equals(LINEAR_LAYOUT) || name.equals(RELATIVE_LAYOUT)
973                     || name.equals(TABLE_LAYOUT) || name.equals(TABLE_ROW)) {
974                 // Don't delete layouts that provide a background image or gradient
975                 if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) {
976                     AdtPlugin.log(IStatus.WARNING,
977                             "Did not flatten layout %1$s because it defines a '%2$s' attribute",
978                             VisualRefactoring.getId(element), ATTR_BACKGROUND);
979                     return false;
980                 }
981 
982                 return true;
983             }
984 
985             return false;
986         }
987     }
988 }
989