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