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.gle2;
17 
18 import static com.android.SdkConstants.ANDROID_LAYOUT_RESOURCE_PREFIX;
19 import static com.android.SdkConstants.ANDROID_URI;
20 import static com.android.SdkConstants.ATTR_NUM_COLUMNS;
21 import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
22 import static com.android.SdkConstants.GRID_VIEW;
23 import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
24 import static com.android.SdkConstants.TOOLS_URI;
25 import static com.android.SdkConstants.VALUE_AUTO_FIT;
26 
27 import com.android.annotations.NonNull;
28 import com.android.annotations.Nullable;
29 import com.android.ide.common.rendering.api.AdapterBinding;
30 import com.android.ide.common.rendering.api.DataBindingItem;
31 import com.android.ide.common.rendering.api.ResourceReference;
32 import com.android.ide.eclipse.adt.AdtPlugin;
33 import com.android.ide.eclipse.adt.AdtUtils;
34 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
35 import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;
36 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
37 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
38 
39 import org.eclipse.core.resources.IFile;
40 import org.eclipse.core.runtime.IProgressMonitor;
41 import org.eclipse.core.runtime.IStatus;
42 import org.eclipse.core.runtime.Status;
43 import org.eclipse.swt.widgets.Display;
44 import org.eclipse.ui.IEditorPart;
45 import org.eclipse.ui.progress.WorkbenchJob;
46 import org.w3c.dom.Document;
47 import org.w3c.dom.Element;
48 import org.w3c.dom.Node;
49 import org.w3c.dom.NodeList;
50 import org.xmlpull.v1.XmlPullParser;
51 
52 import java.util.Collection;
53 import java.util.List;
54 import java.util.Map;
55 
56 /**
57  * Design-time metadata lookup for layouts, such as fragment and AdapterView bindings.
58  */
59 public class LayoutMetadata {
60     /** The default layout to use for list items in expandable list views */
61     public static final String DEFAULT_EXPANDABLE_LIST_ITEM = "simple_expandable_list_item_2"; //$NON-NLS-1$
62     /** The default layout to use for list items in plain list views */
63     public static final String DEFAULT_LIST_ITEM = "simple_list_item_2"; //$NON-NLS-1$
64     /** The default layout to use for list items in spinners */
65     public static final String DEFAULT_SPINNER_ITEM = "simple_spinner_item"; //$NON-NLS-1$
66 
67     /** The string to start metadata comments with */
68     private static final String COMMENT_PROLOGUE = " Preview: ";
69     /** The property key, included in comments, which references a list item layout */
70     public static final String KEY_LV_ITEM = "listitem";        //$NON-NLS-1$
71     /** The property key, included in comments, which references a list header layout */
72     public static final String KEY_LV_HEADER = "listheader";    //$NON-NLS-1$
73     /** The property key, included in comments, which references a list footer layout */
74     public static final String KEY_LV_FOOTER = "listfooter";    //$NON-NLS-1$
75     /** The property key, included in comments, which references a fragment layout to show */
76     public static final String KEY_FRAGMENT_LAYOUT = "layout";        //$NON-NLS-1$
77     // NOTE: If you add additional keys related to resources, make sure you update the
78     // ResourceRenameParticipant
79 
80     /** Utility class, do not create instances */
LayoutMetadata()81     private LayoutMetadata() {
82     }
83 
84     /**
85      * Returns the given property specified in the <b>current</b> element being
86      * processed by the given pull parser.
87      *
88      * @param parser the pull parser, which must be in the middle of processing
89      *            the target element
90      * @param name the property name to look up
91      * @return the property value, or null if not defined
92      */
93     @Nullable
getProperty(@onNull XmlPullParser parser, @NonNull String name)94     public static String getProperty(@NonNull XmlPullParser parser, @NonNull String name) {
95         String value = parser.getAttributeValue(TOOLS_URI, name);
96         if (value != null && value.isEmpty()) {
97             value = null;
98         }
99 
100         return value;
101     }
102 
103     /**
104      * Clears the old metadata from the given node
105      *
106      * @param node the XML node to associate metadata with
107      * @deprecated this method clears metadata using the old comment-based style;
108      *             should only be used for migration at this point
109      */
110     @Deprecated
clearLegacyComment(Node node)111     public static void clearLegacyComment(Node node) {
112         NodeList children = node.getChildNodes();
113         for (int i = 0, n = children.getLength(); i < n; i++) {
114             Node child = children.item(i);
115             if (child.getNodeType() == Node.COMMENT_NODE) {
116                 String text = child.getNodeValue();
117                 if (text.startsWith(COMMENT_PROLOGUE)) {
118                     Node commentNode = child;
119                     // Remove the comment, along with surrounding whitespace if applicable
120                     Node previous = commentNode.getPreviousSibling();
121                     if (previous != null && previous.getNodeType() == Node.TEXT_NODE) {
122                         if (previous.getNodeValue().trim().length() == 0) {
123                             node.removeChild(previous);
124                         }
125                     }
126                     node.removeChild(commentNode);
127                     Node first = node.getFirstChild();
128                     if (first != null && first.getNextSibling() == null
129                             && first.getNodeType() == Node.TEXT_NODE) {
130                         if (first.getNodeValue().trim().length() == 0) {
131                             node.removeChild(first);
132                         }
133                     }
134                 }
135             }
136         }
137     }
138 
139     /**
140      * Returns the given property of the given DOM node, or null
141      *
142      * @param node the XML node to associate metadata with
143      * @param name the name of the property to look up
144      * @return the value stored with the given node and name, or null
145      */
146     @Nullable
getProperty( @onNull Node node, @NonNull String name)147     public static String getProperty(
148             @NonNull Node node,
149             @NonNull String name) {
150         if (node.getNodeType() == Node.ELEMENT_NODE) {
151             Element element = (Element) node;
152             String value = element.getAttributeNS(TOOLS_URI, name);
153             if (value != null && value.isEmpty()) {
154                 value = null;
155             }
156 
157             return value;
158         }
159 
160         return null;
161     }
162 
163     /**
164      * Sets the given property of the given DOM node to a given value, or if null clears
165      * the property.
166      *
167      * @param editor the editor associated with the property
168      * @param node the XML node to associate metadata with
169      * @param name the name of the property to set
170      * @param value the value to store for the given node and name, or null to remove it
171      */
setProperty( @onNull final AndroidXmlEditor editor, @NonNull final Node node, @NonNull final String name, @Nullable final String value)172     public static void setProperty(
173             @NonNull final AndroidXmlEditor editor,
174             @NonNull final Node node,
175             @NonNull final String name,
176             @Nullable final String value) {
177         // Clear out the old metadata
178         clearLegacyComment(node);
179 
180         if (node.getNodeType() == Node.ELEMENT_NODE) {
181             final Element element = (Element) node;
182             final String undoLabel = "Bind View";
183             AdtUtils.setToolsAttribute(editor, element, undoLabel, name, value,
184                     false /*reveal*/, false /*append*/);
185 
186             // Also apply the same layout to any corresponding elements in other configurations
187             // of this layout.
188             final IFile file = editor.getInputFile();
189             if (file != null) {
190                 final List<IFile> variations = AdtUtils.getResourceVariations(file, false);
191                 if (variations.isEmpty()) {
192                     return;
193                 }
194                 Display display = AdtPlugin.getDisplay();
195                 WorkbenchJob job = new WorkbenchJob(display, "Update alternate views") {
196                     @Override
197                     public IStatus runInUIThread(IProgressMonitor monitor) {
198                         for (IFile variation : variations) {
199                             if (variation.equals(file)) {
200                                 continue;
201                             }
202                             try {
203                                 // If the corresponding file is open in the IDE, use the
204                                 // editor version instead
205                                 if (!AdtPrefs.getPrefs().isSharedLayoutEditor()) {
206                                     if (setPropertyInEditor(undoLabel, variation, element, name,
207                                             value)) {
208                                         return Status.OK_STATUS;
209                                     }
210                                 }
211 
212                                 boolean old = editor.getIgnoreXmlUpdate();
213                                 try {
214                                     editor.setIgnoreXmlUpdate(true);
215                                     setPropertyInFile(undoLabel, variation, element, name, value);
216                                 } finally {
217                                     editor.setIgnoreXmlUpdate(old);
218                                 }
219                             } catch (Exception e) {
220                                 AdtPlugin.log(e, variation.getFullPath().toOSString());
221                             }
222                         }
223                         return Status.OK_STATUS;
224                     }
225 
226                 };
227                 job.setSystem(true);
228                 job.schedule();
229             }
230         }
231     }
232 
setPropertyInEditor( @onNull String undoLabel, @NonNull IFile variation, @NonNull final Element equivalentElement, @NonNull final String name, @Nullable final String value)233     private static boolean setPropertyInEditor(
234             @NonNull String undoLabel,
235             @NonNull IFile variation,
236             @NonNull final Element equivalentElement,
237             @NonNull final String name,
238             @Nullable final String value) {
239         Collection<IEditorPart> editors =
240                 AdtUtils.findEditorsFor(variation, false /*restore*/);
241         for (IEditorPart part : editors) {
242             AndroidXmlEditor editor = AdtUtils.getXmlEditor(part);
243             if (editor != null) {
244                 Document doc = DomUtilities.getDocument(editor);
245                 if (doc != null) {
246                     Element element = DomUtilities.findCorresponding(equivalentElement, doc);
247                     if (element != null) {
248                         AdtUtils.setToolsAttribute(editor, element, undoLabel, name,
249                                 value, false /*reveal*/, false /*append*/);
250                         if (part instanceof GraphicalEditorPart) {
251                             GraphicalEditorPart g = (GraphicalEditorPart) part;
252                             g.recomputeLayout();
253                             g.getCanvasControl().redraw();
254                         }
255                         return true;
256                     }
257                 }
258             }
259         }
260 
261         return false;
262     }
263 
setPropertyInFile( @onNull String undoLabel, @NonNull IFile variation, @NonNull final Element element, @NonNull final String name, @Nullable final String value)264     private static boolean setPropertyInFile(
265             @NonNull String undoLabel,
266             @NonNull IFile variation,
267             @NonNull final Element element,
268             @NonNull final String name,
269             @Nullable final String value) {
270         Document doc = DomUtilities.getDocument(variation);
271         if (doc != null && element.getOwnerDocument() != doc) {
272             Element other = DomUtilities.findCorresponding(element, doc);
273             if (other != null) {
274                 AdtUtils.setToolsAttribute(variation, other, undoLabel,
275                         name, value, false);
276 
277                 return true;
278             }
279         }
280 
281         return false;
282     }
283 
284     /** Strips out @layout/ or @android:layout/ from the given layout reference */
stripLayoutPrefix(String layout)285     private static String stripLayoutPrefix(String layout) {
286         if (layout.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX)) {
287             layout = layout.substring(ANDROID_LAYOUT_RESOURCE_PREFIX.length());
288         } else if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) {
289             layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length());
290         }
291 
292         return layout;
293     }
294 
295     /**
296      * Creates an {@link AdapterBinding} for the given view object, or null if the user
297      * has not yet chosen a target layout to use for the given AdapterView.
298      *
299      * @param viewObject the view object to create an adapter binding for
300      * @param map a map containing tools attribute metadata
301      * @return a binding, or null
302      */
303     @Nullable
getNodeBinding( @ullable Object viewObject, @NonNull Map<String, String> map)304     public static AdapterBinding getNodeBinding(
305             @Nullable Object viewObject,
306             @NonNull Map<String, String> map) {
307         String header = map.get(KEY_LV_HEADER);
308         String footer = map.get(KEY_LV_FOOTER);
309         String layout = map.get(KEY_LV_ITEM);
310         if (layout != null || header != null || footer != null) {
311             int count = 12;
312             return getNodeBinding(viewObject, header, footer, layout, count);
313         }
314 
315         return null;
316     }
317 
318     /**
319      * Creates an {@link AdapterBinding} for the given view object, or null if the user
320      * has not yet chosen a target layout to use for the given AdapterView.
321      *
322      * @param viewObject the view object to create an adapter binding for
323      * @param uiNode the ui node corresponding to the view object
324      * @return a binding, or null
325      */
326     @Nullable
getNodeBinding( @ullable Object viewObject, @NonNull UiViewElementNode uiNode)327     public static AdapterBinding getNodeBinding(
328             @Nullable Object viewObject,
329             @NonNull UiViewElementNode uiNode) {
330         Node xmlNode = uiNode.getXmlNode();
331 
332         String header = getProperty(xmlNode, KEY_LV_HEADER);
333         String footer = getProperty(xmlNode, KEY_LV_FOOTER);
334         String layout = getProperty(xmlNode, KEY_LV_ITEM);
335         if (layout != null || header != null || footer != null) {
336             int count = 12;
337             // If we're dealing with a grid view, multiply the list item count
338             // by the number of columns to ensure we have enough items
339             if (xmlNode instanceof Element && xmlNode.getNodeName().endsWith(GRID_VIEW)) {
340                 Element element = (Element) xmlNode;
341                 String columns = element.getAttributeNS(ANDROID_URI, ATTR_NUM_COLUMNS);
342                 int multiplier = 2;
343                 if (columns != null && columns.length() > 0 &&
344                         !columns.equals(VALUE_AUTO_FIT)) {
345                     try {
346                         int c = Integer.parseInt(columns);
347                         if (c >= 1 && c <= 10) {
348                             multiplier = c;
349                         }
350                     } catch (NumberFormatException nufe) {
351                         // some unexpected numColumns value: just stick with 2 columns for
352                         // preview purposes
353                     }
354                 }
355                 count *= multiplier;
356             }
357 
358             return getNodeBinding(viewObject, header, footer, layout, count);
359         }
360 
361         return null;
362     }
363 
getNodeBinding(Object viewObject, String header, String footer, String layout, int count)364     private static AdapterBinding getNodeBinding(Object viewObject,
365             String header, String footer, String layout, int count) {
366         if (layout != null || header != null || footer != null) {
367             AdapterBinding binding = new AdapterBinding(count);
368 
369             if (header != null) {
370                 boolean isFramework = header.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX);
371                 binding.addHeader(new ResourceReference(stripLayoutPrefix(header),
372                         isFramework));
373             }
374 
375             if (footer != null) {
376                 boolean isFramework = footer.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX);
377                 binding.addFooter(new ResourceReference(stripLayoutPrefix(footer),
378                         isFramework));
379             }
380 
381             if (layout != null) {
382                 boolean isFramework = layout.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX);
383                 if (isFramework) {
384                     layout = layout.substring(ANDROID_LAYOUT_RESOURCE_PREFIX.length());
385                 } else if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) {
386                     layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length());
387                 }
388 
389                 binding.addItem(new DataBindingItem(layout, isFramework, 1));
390             } else if (viewObject != null) {
391                 String listFqcn = ProjectCallback.getListAdapterViewFqcn(viewObject.getClass());
392                 if (listFqcn != null) {
393                     if (listFqcn.endsWith(EXPANDABLE_LIST_VIEW)) {
394                         binding.addItem(
395                                 new DataBindingItem(DEFAULT_EXPANDABLE_LIST_ITEM,
396                                 true /* isFramework */, 1));
397                     } else {
398                         binding.addItem(
399                                 new DataBindingItem(DEFAULT_LIST_ITEM,
400                                 true /* isFramework */, 1));
401                     }
402                 }
403             } else {
404                 binding.addItem(
405                         new DataBindingItem(DEFAULT_LIST_ITEM,
406                         true /* isFramework */, 1));
407             }
408             return binding;
409         }
410 
411         return null;
412     }
413 }
414