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.gle2;
17
18import static com.android.SdkConstants.ANDROID_LAYOUT_RESOURCE_PREFIX;
19import static com.android.SdkConstants.ANDROID_URI;
20import static com.android.SdkConstants.ATTR_NUM_COLUMNS;
21import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
22import static com.android.SdkConstants.GRID_VIEW;
23import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
24import static com.android.SdkConstants.TOOLS_URI;
25import static com.android.SdkConstants.VALUE_AUTO_FIT;
26
27import com.android.annotations.NonNull;
28import com.android.annotations.Nullable;
29import com.android.ide.common.rendering.api.AdapterBinding;
30import com.android.ide.common.rendering.api.DataBindingItem;
31import com.android.ide.common.rendering.api.ResourceReference;
32import com.android.ide.eclipse.adt.AdtPlugin;
33import com.android.ide.eclipse.adt.AdtUtils;
34import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
35import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;
36import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
37import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
38
39import org.eclipse.core.resources.IFile;
40import org.eclipse.core.runtime.IProgressMonitor;
41import org.eclipse.core.runtime.IStatus;
42import org.eclipse.core.runtime.Status;
43import org.eclipse.swt.widgets.Display;
44import org.eclipse.ui.IEditorPart;
45import org.eclipse.ui.progress.WorkbenchJob;
46import org.w3c.dom.Document;
47import org.w3c.dom.Element;
48import org.w3c.dom.Node;
49import org.w3c.dom.NodeList;
50import org.xmlpull.v1.XmlPullParser;
51
52import java.util.Collection;
53import java.util.List;
54import java.util.Map;
55
56/**
57 * Design-time metadata lookup for layouts, such as fragment and AdapterView bindings.
58 */
59public 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 */
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
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
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
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     */
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
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
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 */
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
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
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
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