blob: b79e3b0a172719ba24b353a813be7f8e838977ab [file] [log] [blame]
/*
* 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.
*/
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$
// NOTE: If you add additional keys related to resources, make sure you update the
// ResourceRenameParticipant
/** 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();
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;
}
}