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