/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/org/documents/epl-v10.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import static com.android.SdkConstants.ANDROID_LAYOUT_RESOURCE_PREFIX; import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_NUM_COLUMNS; import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW; import static com.android.SdkConstants.GRID_VIEW; import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX; import static com.android.SdkConstants.TOOLS_URI; import static com.android.SdkConstants.VALUE_AUTO_FIT; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.rendering.api.AdapterBinding; import com.android.ide.common.rendering.api.DataBindingItem; import com.android.ide.common.rendering.api.ResourceReference; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.progress.WorkbenchJob; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xmlpull.v1.XmlPullParser; import java.util.Collection; import java.util.List; import java.util.Map; /** * Design-time metadata lookup for layouts, such as fragment and AdapterView bindings. */ @SuppressWarnings("restriction") // XML DOM model public class LayoutMetadata { /** The default layout to use for list items in expandable list views */ public static final String DEFAULT_EXPANDABLE_LIST_ITEM = "simple_expandable_list_item_2"; //$NON-NLS-1$ /** The default layout to use for list items in plain list views */ public static final String DEFAULT_LIST_ITEM = "simple_list_item_2"; //$NON-NLS-1$ /** The default layout to use for list items in spinners */ public static final String DEFAULT_SPINNER_ITEM = "simple_spinner_item"; //$NON-NLS-1$ /** The string to start metadata comments with */ private static final String COMMENT_PROLOGUE = " Preview: "; /** The property key, included in comments, which references a list item layout */ public static final String KEY_LV_ITEM = "listitem"; //$NON-NLS-1$ /** The property key, included in comments, which references a list header layout */ public static final String KEY_LV_HEADER = "listheader"; //$NON-NLS-1$ /** The property key, included in comments, which references a list footer layout */ public static final String KEY_LV_FOOTER = "listfooter"; //$NON-NLS-1$ /** The property key, included in comments, which references a fragment layout to show */ public static final String KEY_FRAGMENT_LAYOUT = "layout"; //$NON-NLS-1$ /** Utility class, do not create instances */ private LayoutMetadata() { } /** * Returns the given property specified in the <b>current</b> element being * processed by the given pull parser. * * @param parser the pull parser, which must be in the middle of processing * the target element * @param name the property name to look up * @return the property value, or null if not defined */ @Nullable public static String getProperty(@NonNull XmlPullParser parser, @NonNull String name) { String value = parser.getAttributeValue(TOOLS_URI, name); if (value != null && value.isEmpty()) { value = null; } return value; } /** * Clears the old metadata from the given node * * @param node the XML node to associate metadata with * @deprecated this method clears metadata using the old comment-based style; * should only be used for migration at this point */ @Deprecated public static void clearLegacyComment(Node node) { NodeList children = node.getChildNodes(); for (int i = 0, n = children.getLength(); i < n; i++) { Node child = children.item(i); if (child.getNodeType() == Node.COMMENT_NODE) { String text = child.getNodeValue(); if (text.startsWith(COMMENT_PROLOGUE)) { Node commentNode = child; // Remove the comment, along with surrounding whitespace if applicable Node previous = commentNode.getPreviousSibling(); if (previous != null && previous.getNodeType() == Node.TEXT_NODE) { if (previous.getNodeValue().trim().length() == 0) { node.removeChild(previous); } } node.removeChild(commentNode); Node first = node.getFirstChild(); if (first != null && first.getNextSibling() == null && first.getNodeType() == Node.TEXT_NODE) { if (first.getNodeValue().trim().length() == 0) { node.removeChild(first); } } } } } } /** * Returns the given property of the given DOM node, or null * * @param node the XML node to associate metadata with * @param name the name of the property to look up * @return the value stored with the given node and name, or null */ @Nullable public static String getProperty( @NonNull Node node, @NonNull String name) { if (node.getNodeType() == Node.ELEMENT_NODE) { Element element = (Element) node; String value = element.getAttributeNS(TOOLS_URI, name); if (value != null && value.isEmpty()) { value = null; } return value; } return null; } /** * Sets the given property of the given DOM node to a given value, or if null clears * the property. * * @param editor the editor associated with the property * @param node the XML node to associate metadata with * @param name the name of the property to set * @param value the value to store for the given node and name, or null to remove it */ public static void setProperty( @NonNull final AndroidXmlEditor editor, @NonNull final Node node, @NonNull final String name, @Nullable final String value) { // Clear out the old metadata clearLegacyComment(node); if (node.getNodeType() == Node.ELEMENT_NODE) { final Element element = (Element) node; final String undoLabel = "Bind View"; AdtUtils.setToolsAttribute(editor, element, undoLabel, name, value, false /*reveal*/, false /*append*/); // Also apply the same layout to any corresponding elements in other configurations // of this layout. final IFile file = editor.getInputFile(); if (file != null) { final List<IFile> variations = AdtUtils.getResourceVariations(file, false); if (variations.isEmpty()) { return; } Display display = AdtPlugin.getDisplay(); if (display == null) { return; } WorkbenchJob job = new WorkbenchJob(display, "Update alternate views") { @Override public IStatus runInUIThread(IProgressMonitor monitor) { for (IFile variation : variations) { if (variation.equals(file)) { continue; } try { // If the corresponding file is open in the IDE, use the // editor version instead if (!AdtPrefs.getPrefs().isSharedLayoutEditor()) { if (setPropertyInEditor(undoLabel, variation, element, name, value)) { return Status.OK_STATUS; } } boolean old = editor.getIgnoreXmlUpdate(); try { editor.setIgnoreXmlUpdate(true); setPropertyInFile(undoLabel, variation, element, name, value); } finally { editor.setIgnoreXmlUpdate(old); } } catch (Exception e) { AdtPlugin.log(e, variation.getFullPath().toOSString()); } } return Status.OK_STATUS; } }; job.setSystem(true); job.schedule(); } } } private static boolean setPropertyInEditor( @NonNull String undoLabel, @NonNull IFile variation, @NonNull final Element equivalentElement, @NonNull final String name, @Nullable final String value) { Collection<IEditorPart> editors = AdtUtils.findEditorsFor(variation, false /*restore*/); for (IEditorPart part : editors) { AndroidXmlEditor editor = AdtUtils.getXmlEditor(part); if (editor != null) { Document doc = DomUtilities.getDocument(editor); if (doc != null) { Element element = DomUtilities.findCorresponding(equivalentElement, doc); if (element != null) { AdtUtils.setToolsAttribute(editor, element, undoLabel, name, value, false /*reveal*/, false /*append*/); if (part instanceof GraphicalEditorPart) { GraphicalEditorPart g = (GraphicalEditorPart) part; g.recomputeLayout(); g.getCanvasControl().redraw(); } return true; } } } } return false; } private static boolean setPropertyInFile( @NonNull String undoLabel, @NonNull IFile variation, @NonNull final Element element, @NonNull final String name, @Nullable final String value) { Document doc = DomUtilities.getDocument(variation); if (doc != null && element.getOwnerDocument() != doc) { Element other = DomUtilities.findCorresponding(element, doc); if (other != null) { AdtUtils.setToolsAttribute(variation, other, undoLabel, name, value, false); return true; } } return false; } /** Strips out @layout/ or @android:layout/ from the given layout reference */ private static String stripLayoutPrefix(String layout) { if (layout.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX)) { layout = layout.substring(ANDROID_LAYOUT_RESOURCE_PREFIX.length()); } else if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) { layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length()); } return layout; } /** * Creates an {@link AdapterBinding} for the given view object, or null if the user * has not yet chosen a target layout to use for the given AdapterView. * * @param viewObject the view object to create an adapter binding for * @param map a map containing tools attribute metadata * @return a binding, or null */ @Nullable public static AdapterBinding getNodeBinding( @Nullable Object viewObject, @NonNull Map<String, String> map) { String header = map.get(KEY_LV_HEADER); String footer = map.get(KEY_LV_FOOTER); String layout = map.get(KEY_LV_ITEM); if (layout != null || header != null || footer != null) { int count = 12; return getNodeBinding(viewObject, header, footer, layout, count); } return null; } /** * Creates an {@link AdapterBinding} for the given view object, or null if the user * has not yet chosen a target layout to use for the given AdapterView. * * @param viewObject the view object to create an adapter binding for * @param uiNode the ui node corresponding to the view object * @return a binding, or null */ @Nullable public static AdapterBinding getNodeBinding( @Nullable Object viewObject, @NonNull UiViewElementNode uiNode) { Node xmlNode = uiNode.getXmlNode(); String header = getProperty(xmlNode, KEY_LV_HEADER); String footer = getProperty(xmlNode, KEY_LV_FOOTER); String layout = getProperty(xmlNode, KEY_LV_ITEM); if (layout != null || header != null || footer != null) { int count = 12; // If we're dealing with a grid view, multiply the list item count // by the number of columns to ensure we have enough items if (xmlNode instanceof Element && xmlNode.getNodeName().endsWith(GRID_VIEW)) { Element element = (Element) xmlNode; String columns = element.getAttributeNS(ANDROID_URI, ATTR_NUM_COLUMNS); int multiplier = 2; if (columns != null && columns.length() > 0 && !columns.equals(VALUE_AUTO_FIT)) { try { int c = Integer.parseInt(columns); if (c >= 1 && c <= 10) { multiplier = c; } } catch (NumberFormatException nufe) { // some unexpected numColumns value: just stick with 2 columns for // preview purposes } } count *= multiplier; } return getNodeBinding(viewObject, header, footer, layout, count); } return null; } private static AdapterBinding getNodeBinding(Object viewObject, String header, String footer, String layout, int count) { if (layout != null || header != null || footer != null) { AdapterBinding binding = new AdapterBinding(count); if (header != null) { boolean isFramework = header.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX); binding.addHeader(new ResourceReference(stripLayoutPrefix(header), isFramework)); } if (footer != null) { boolean isFramework = footer.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX); binding.addFooter(new ResourceReference(stripLayoutPrefix(footer), isFramework)); } if (layout != null) { boolean isFramework = layout.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX); if (isFramework) { layout = layout.substring(ANDROID_LAYOUT_RESOURCE_PREFIX.length()); } else if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) { layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length()); } binding.addItem(new DataBindingItem(layout, isFramework, 1)); } else if (viewObject != null) { String listFqcn = ProjectCallback.getListAdapterViewFqcn(viewObject.getClass()); if (listFqcn != null) { if (listFqcn.endsWith(EXPANDABLE_LIST_VIEW)) { binding.addItem( new DataBindingItem(DEFAULT_EXPANDABLE_LIST_ITEM, true /* isFramework */, 1)); } else { binding.addItem( new DataBindingItem(DEFAULT_LIST_ITEM, true /* isFramework */, 1)); } } } else { binding.addItem( new DataBindingItem(DEFAULT_LIST_ITEM, true /* isFramework */, 1)); } return binding; } return null; } }