• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.ide.common.layout.LayoutConstants.ANDROID_LAYOUT_PREFIX;
19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
20 import static com.android.ide.common.layout.LayoutConstants.ATTR_NUM_COLUMNS;
21 import static com.android.ide.common.layout.LayoutConstants.EXPANDABLE_LIST_VIEW;
22 import static com.android.ide.common.layout.LayoutConstants.GRID_VIEW;
23 import static com.android.ide.common.layout.LayoutConstants.LAYOUT_PREFIX;
24 
25 import com.android.ide.common.rendering.api.AdapterBinding;
26 import com.android.ide.common.rendering.api.DataBindingItem;
27 import com.android.ide.common.rendering.api.ResourceReference;
28 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
29 import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;
30 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
31 
32 import org.eclipse.jface.text.IDocument;
33 import org.eclipse.wst.sse.core.StructuredModelManager;
34 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
35 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
36 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
37 import org.w3c.dom.Document;
38 import org.w3c.dom.Element;
39 import org.w3c.dom.Node;
40 import org.w3c.dom.NodeList;
41 
42 import java.util.ArrayList;
43 import java.util.Collections;
44 import java.util.List;
45 
46 @SuppressWarnings("restriction") // XML DOM model
47 public class LayoutMetadata {
48     /** The default layout to use for list items in expandable list views */
49     public static final String DEFAULT_EXPANDABLE_LIST_ITEM = "simple_expandable_list_item_2"; //$NON-NLS-1$
50     /** The default layout to use for list items in plain list views */
51     public static final String DEFAULT_LIST_ITEM = "simple_list_item_2"; //$NON-NLS-1$
52     /** The default layout to use for list items in spinners */
53     public static final String DEFAULT_SPINNER_ITEM = "simple_spinner_item"; //$NON-NLS-1$
54 
55     /** The string to start metadata comments with */
56     private static final String COMMENT_PROLOGUE = " Preview: ";
57     /** The string to end metadata comments with */
58     private static final String COMMENT_EPILOGUE = " ";
59     /** The property key, included in comments, which references a list item layout */
60     public static final String KEY_LV_ITEM = "listitem";        //$NON-NLS-1$
61     /** The property key, included in comments, which references a list header layout */
62     public static final String KEY_LV_HEADER = "listheader";    //$NON-NLS-1$
63     /** The property key, included in comments, which references a list footer layout */
64     public static final String KEY_LV_FOOTER = "listfooter";    //$NON-NLS-1$
65     /** The property key, included in comments, which references a fragment layout to show */
66     public static final String KEY_FRAGMENT_LAYOUT = "layout";        //$NON-NLS-1$
67 
68     /** The metadata class is a singleton for now since it has no state of its own */
69     private static final LayoutMetadata sInstance = new LayoutMetadata();
70 
71     /** Do not use -- use factory instead */
LayoutMetadata()72     private LayoutMetadata() {
73     }
74 
75     /**
76      * Return the {@link LayoutMetadata} instance
77      *
78      * @return the {@link LayoutMetadata} instance
79      */
get()80     public static LayoutMetadata get() {
81         return sInstance;
82     }
83 
84     /**
85      * Returns the given property of the given DOM node, or null
86      *
87      * @param document the document to look up and read lock the model for
88      * @param node the XML node to associate metadata with
89      * @param name the name of the property to look up
90      * @return the value stored with the given node and name, or null
91      */
getProperty(IDocument document, Node node, String name)92     public String getProperty(IDocument document, Node node, String name) {
93         IStructuredModel model = null;
94         try {
95             if (document != null) {
96                 IModelManager modelManager = StructuredModelManager.getModelManager();
97                 model = modelManager.getExistingModelForRead(document);
98             }
99 
100             Node comment = findComment(node);
101             if (comment != null) {
102                 String text = comment.getNodeValue();
103                 return getProperty(name, text);
104             }
105 
106             return null;
107         } finally {
108             if (model != null) {
109                 model.releaseFromRead();
110             }
111         }
112     }
113 
114     /**
115      * Returns the given property specified in the given XML comment
116      *
117      * @param name the name of the property to look up
118      * @param text the comment text for an XML node
119      * @return the value stored with the given node and name, or null
120      */
getProperty(String name, String text)121     public static String getProperty(String name, String text) {
122         assert text.startsWith(COMMENT_PROLOGUE);
123         String valuesString = text.substring(COMMENT_PROLOGUE.length());
124         String[] values = valuesString.split(","); //$NON-NLS-1$
125         if (values.length == 1) {
126             valuesString = values[0].trim();
127             if (valuesString.indexOf('\n') != -1) {
128                 values = valuesString.split("\n"); //$NON-NLS-1$
129             }
130         }
131         String target = name + '=';
132         for (int j = 0; j < values.length; j++) {
133             String value = values[j].trim();
134             if (value.startsWith(target)) {
135                 return value.substring(target.length()).trim();
136             }
137         }
138         return null;
139     }
140 
141     /**
142      * Sets the given property of the given DOM node to a given value, or if null clears
143      * the property.
144      *
145      * @param document the document to look up and write lock the model for
146      * @param node the XML node to associate metadata with
147      * @param name the name of the property to set
148      * @param value the value to store for the given node and name, or null to remove it
149      */
setProperty(IDocument document, Node node, String name, String value)150     public void setProperty(IDocument document, Node node, String name, String value) {
151         // Reserved characters: [,-=]
152         assert name.indexOf('-') == -1;
153         assert value == null || value.indexOf('-') == -1;
154         assert name.indexOf(',') == -1;
155         assert value == null || value.indexOf(',') == -1;
156         assert name.indexOf('=') == -1;
157         assert value == null || value.indexOf('=') == -1;
158 
159         IStructuredModel model = null;
160         try {
161             IModelManager modelManager = StructuredModelManager.getModelManager();
162             model = modelManager.getExistingModelForEdit(document);
163             if (model instanceof IDOMModel) {
164                 IDOMModel domModel = (IDOMModel) model;
165                 Document domDocument = domModel.getDocument();
166                 assert node.getOwnerDocument() == domDocument;
167             }
168 
169             Document doc = node.getOwnerDocument();
170             Node commentNode = findComment(node);
171 
172             String commentText = null;
173             if (commentNode != null) {
174                 String text = commentNode.getNodeValue();
175                 assert text.startsWith(COMMENT_PROLOGUE);
176                 String valuesString = text.substring(COMMENT_PROLOGUE.length());
177                 String[] values = valuesString.split(","); //$NON-NLS-1$
178                 if (values.length == 1) {
179                     valuesString = values[0].trim();
180                     if (valuesString.indexOf('\n') != -1) {
181                         values = valuesString.split("\n"); //$NON-NLS-1$
182                     }
183                 }
184                 String target = name + '=';
185                 List<String> preserve = new ArrayList<String>();
186                 for (int j = 0; j < values.length; j++) {
187                     String v = values[j].trim();
188                     if (v.length() == 0) {
189                         continue;
190                     }
191                     if (!v.startsWith(target)) {
192                         preserve.add(v.trim());
193                     }
194                 }
195                 if (value != null) {
196                     preserve.add(name + '=' + value.trim());
197                 }
198                 if (preserve.size() > 0) {
199                     if (preserve.size() > 1) {
200                         Collections.sort(preserve);
201                         String firstLineIndent = AndroidXmlEditor.getIndent(document, commentNode);
202                         String oneIndentLevel = "    "; //$NON-NLS-1$
203                         StringBuilder sb = new StringBuilder();
204                         sb.append(COMMENT_PROLOGUE);
205                         sb.append('\n');
206                         for (String s : preserve) {
207                             sb.append(firstLineIndent);
208                             sb.append(oneIndentLevel);
209                             sb.append(s);
210                             sb.append('\n');
211                         }
212                         sb.append(firstLineIndent);
213                         sb.append(COMMENT_EPILOGUE);
214                         commentText = sb.toString();
215                     } else {
216                         commentText = COMMENT_PROLOGUE + preserve.get(0) + COMMENT_EPILOGUE;
217                     }
218                 }
219             } else if (value != null) {
220                 commentText = COMMENT_PROLOGUE + name + '=' + value + COMMENT_EPILOGUE;
221             }
222 
223             if (commentText == null) {
224                 if (commentNode != null) {
225                     // Remove the comment, along with surrounding whitespace if applicable
226                     Node previous = commentNode.getPreviousSibling();
227                     if (previous != null && previous.getNodeType() == Node.TEXT_NODE) {
228                         String text = previous.getNodeValue();
229                         if (text.trim().length() == 0) {
230                             node.removeChild(previous);
231                         }
232                     }
233                     node.removeChild(commentNode);
234                     Node first = node.getFirstChild();
235                     if (first != null && first.getNextSibling() == null
236                             && first.getNodeType() == Node.TEXT_NODE) {
237                         String text = first.getNodeValue();
238                         if (text.trim().length() == 0) {
239                             node.removeChild(first);
240                         }
241                     }
242                 }
243                 return;
244             }
245 
246             if (commentNode != null) {
247                 commentNode.setNodeValue(commentText);
248             } else {
249                 commentNode = doc.createComment(commentText);
250                 String firstLineIndent = AndroidXmlEditor.getIndent(document, node);
251                 Node firstChild = node.getFirstChild();
252                 boolean indentAfter = firstChild == null
253                         || firstChild.getNodeType() != Node.TEXT_NODE
254                         || firstChild.getNodeValue().indexOf('\n') == -1;
255                 String oneIndentLevel = "    "; //$NON-NLS-1$
256                 node.insertBefore(doc.createTextNode('\n' + firstLineIndent + oneIndentLevel),
257                         firstChild);
258                 node.insertBefore(commentNode, firstChild);
259                 if (indentAfter) {
260                     node.insertBefore(doc.createTextNode('\n' + firstLineIndent), firstChild);
261                 }
262             }
263         } finally {
264             if (model != null) {
265                 model.releaseFromEdit();
266             }
267         }
268     }
269 
270     /** Finds the comment node associated with the given node, or null if not found */
findComment(Node node)271     private Node findComment(Node node) {
272         NodeList children = node.getChildNodes();
273         for (int i = 0, n = children.getLength(); i < n; i++) {
274             Node child = children.item(i);
275             if (child.getNodeType() == Node.COMMENT_NODE) {
276                 String text = child.getNodeValue();
277                 if (text.startsWith(COMMENT_PROLOGUE)) {
278                     return child;
279                 }
280             }
281         }
282 
283         return null;
284     }
285 
286     /**
287      * Returns the given property of the given DOM node, or null
288      *
289      * @param editor the editor associated with the property
290      * @param node the XML node to associate metadata with
291      * @param name the name of the property to look up
292      * @return the value stored with the given node and name, or null
293      */
getProperty(AndroidXmlEditor editor, Node node, String name)294     public String getProperty(AndroidXmlEditor editor, Node node, String name) {
295         IDocument document = editor.getStructuredSourceViewer().getDocument();
296         return getProperty(document, node, name);
297     }
298 
299     /**
300      * Sets the given property of the given DOM node to a given value, or if null clears
301      * the property.
302      *
303      * @param editor the editor associated with the property
304      * @param node the XML node to associate metadata with
305      * @param name the name of the property to set
306      * @param value the value to store for the given node and name, or null to remove it
307      */
setProperty(AndroidXmlEditor editor, Node node, String name, String value)308     public void setProperty(AndroidXmlEditor editor, Node node, String name, String value) {
309         IDocument document = editor.getStructuredSourceViewer().getDocument();
310         setProperty(document, node, name, value);
311     }
312 
313     /** Strips out @layout/ or @android:layout/ from the given layout reference */
stripLayoutPrefix(String layout)314     private static String stripLayoutPrefix(String layout) {
315         if (layout.startsWith(ANDROID_LAYOUT_PREFIX)) {
316             layout = layout.substring(ANDROID_LAYOUT_PREFIX.length());
317         } else if (layout.startsWith(LAYOUT_PREFIX)) {
318             layout = layout.substring(LAYOUT_PREFIX.length());
319         }
320 
321         return layout;
322     }
323 
324     /**
325      * Creates an {@link AdapterBinding} for the given view object, or null if the user
326      * has not yet chosen a target layout to use for the given AdapterView.
327      *
328      * @param viewObject the view object to create an adapter binding for
329      * @param uiNode the ui node corresponding to the view object
330      * @return a binding, or null
331      */
getNodeBinding(Object viewObject, UiViewElementNode uiNode)332     public AdapterBinding getNodeBinding(Object viewObject, UiViewElementNode uiNode) {
333         AndroidXmlEditor editor = uiNode.getEditor();
334         if (editor != null) {
335             Node xmlNode = uiNode.getXmlNode();
336 
337             String header = getProperty(editor, xmlNode, KEY_LV_HEADER);
338             String footer = getProperty(editor, xmlNode, KEY_LV_FOOTER);
339             String layout = getProperty(editor, xmlNode, KEY_LV_ITEM);
340             if (layout != null || header != null || footer != null) {
341                 int count = 12;
342                 // If we're dealing with a grid view, multiply the list item count
343                 // by the number of columns to ensure we have enough items
344                 if (xmlNode instanceof Element && xmlNode.getNodeName().endsWith(GRID_VIEW)) {
345                     Element element = (Element) xmlNode;
346                     String columns = element.getAttributeNS(ANDROID_URI, ATTR_NUM_COLUMNS);
347                     int multiplier = 2;
348                     if (columns != null && columns.length() > 0) {
349                         int c = Integer.parseInt(columns);
350                         if (c >= 1 && c <= 10) {
351                             multiplier = c;
352                         }
353                     }
354                     count *= multiplier;
355                 }
356                 AdapterBinding binding = new AdapterBinding(count);
357 
358                 if (header != null) {
359                     boolean isFramework = header.startsWith(ANDROID_LAYOUT_PREFIX);
360                     binding.addHeader(new ResourceReference(stripLayoutPrefix(header),
361                             isFramework));
362                 }
363 
364                 if (footer != null) {
365                     boolean isFramework = footer.startsWith(ANDROID_LAYOUT_PREFIX);
366                     binding.addFooter(new ResourceReference(stripLayoutPrefix(footer),
367                             isFramework));
368                 }
369 
370                 if (layout != null) {
371                     boolean isFramework = layout.startsWith(ANDROID_LAYOUT_PREFIX);
372                     if (isFramework) {
373                         layout = layout.substring(ANDROID_LAYOUT_PREFIX.length());
374                     } else if (layout.startsWith(LAYOUT_PREFIX)) {
375                         layout = layout.substring(LAYOUT_PREFIX.length());
376                     }
377 
378                     binding.addItem(new DataBindingItem(layout, isFramework, 1));
379                 } else if (viewObject != null) {
380                     String listFqcn = ProjectCallback.getListAdapterViewFqcn(viewObject.getClass());
381                     if (listFqcn != null) {
382                         if (listFqcn.endsWith(EXPANDABLE_LIST_VIEW)) {
383                             binding.addItem(
384                                     new DataBindingItem(DEFAULT_EXPANDABLE_LIST_ITEM,
385                                     true /* isFramework */, 1));
386                         } else {
387                             binding.addItem(
388                                     new DataBindingItem(DEFAULT_LIST_ITEM,
389                                     true /* isFramework */, 1));
390                         }
391                     }
392                 } else {
393                     binding.addItem(
394                             new DataBindingItem(DEFAULT_LIST_ITEM,
395                             true /* isFramework */, 1));
396                 }
397                 return binding;
398             }
399         }
400 
401         return null;
402     }
403 }
404