/* * Copyright (C) 2007 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.uimodel; import static com.android.SdkConstants.ANDROID_PKG_PREFIX; import static com.android.SdkConstants.ANDROID_SUPPORT_PKG_PREFIX; import static com.android.SdkConstants.ATTR_CLASS; import static com.android.SdkConstants.ID_PREFIX; import static com.android.SdkConstants.NEW_ID_PREFIX; import com.android.SdkConstants; import com.android.annotations.VisibleForTesting; import com.android.ide.common.api.IAttributeInfo.Format; import com.android.ide.common.resources.platform.AttributeInfo; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor.Mandatory; import com.android.ide.eclipse.adt.internal.editors.descriptors.IUnknownDescriptorProvider; import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService; import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors; import com.android.ide.eclipse.adt.internal.editors.otherxml.descriptors.OtherXmlDescriptors; import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener.UiUpdateState; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.utils.SdkUtils; import com.android.utils.XmlUtils; import org.eclipse.jface.text.TextUtilities; import org.eclipse.jface.viewers.StyledString; import org.eclipse.ui.views.properties.IPropertyDescriptor; import org.eclipse.ui.views.properties.IPropertySource; import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; import org.eclipse.wst.xml.core.internal.document.ElementImpl; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.Text; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; /** * Represents an XML node that can be modified by the user interface in the XML editor. * <p/> * Each tree viewer used in the application page's parts needs to keep a model representing * each underlying node in the tree. This interface represents the base type for such a node. * <p/> * Each node acts as an intermediary model between the actual XML model (the real data support) * and the tree viewers or the corresponding page parts. * <p/> * Element nodes don't contain data per se. Their data is contained in their attributes * as well as their children's attributes, see {@link UiAttributeNode}. * <p/> * The structure of a given {@link UiElementNode} is declared by a corresponding * {@link ElementDescriptor}. * <p/> * The class implements {@link IPropertySource}, in order to fill the Eclipse property tab when * an element is selected. The {@link AttributeDescriptor} are used property descriptors. */ @SuppressWarnings("restriction") // XML model public class UiElementNode implements IPropertySource { /** List of prefixes removed from android:id strings when creating short descriptions. */ private static String[] ID_PREFIXES = { "@android:id/", //$NON-NLS-1$ NEW_ID_PREFIX, ID_PREFIX, "@+", "@" }; //$NON-NLS-1$ //$NON-NLS-2$ /** The element descriptor for the node. Always present, never null. */ private ElementDescriptor mDescriptor; /** The parent element node in the UI model. It is null for a root element or until * the node is attached to its parent. */ private UiElementNode mUiParent; /** The {@link AndroidXmlEditor} handling the UI hierarchy. This is defined only for the * root node. All children have the value set to null and query their parent. */ private AndroidXmlEditor mEditor; /** The XML {@link Document} model that is being mirror by the UI model. This is defined * only for the root node. All children have the value set to null and query their parent. */ private Document mXmlDocument; /** The XML {@link Node} mirror by this UI node. This can be null for mandatory UI node which * have no corresponding XML node or for new UI nodes before their XML node is set. */ private Node mXmlNode; /** The list of all UI children nodes. Can be empty but never null. There's one UI children * node per existing XML children node. */ private ArrayList<UiElementNode> mUiChildren; /** The list of <em>all</em> UI attributes, as declared in the {@link ElementDescriptor}. * The list is always defined and never null. Unlike the UiElementNode children list, this * is always defined, even for attributes that do not exist in the XML model - that's because * "missing" attributes in the XML model simply mean a default value is used. Also note that * the underlying collection is a map, so order is not respected. To get the desired attribute * order, iterate through the {@link ElementDescriptor}'s attribute list. */ private HashMap<AttributeDescriptor, UiAttributeNode> mUiAttributes; private HashSet<UiAttributeNode> mUnknownUiAttributes; /** A read-only view of the UI children node collection. */ private List<UiElementNode> mReadOnlyUiChildren; /** A read-only view of the UI attributes collection. */ private Collection<UiAttributeNode> mCachedAllUiAttributes; /** A map of hidden attribute descriptors. Key is the XML name. */ private Map<String, AttributeDescriptor> mCachedHiddenAttributes; /** An optional list of {@link IUiUpdateListener}. Most element nodes will not have any * listeners attached, so the list is only created on demand and can be null. */ private List<IUiUpdateListener> mUiUpdateListeners; /** A provider that knows how to create {@link ElementDescriptor} from unmapped XML names. * The default is to have one that creates new {@link ElementDescriptor}. */ private IUnknownDescriptorProvider mUnknownDescProvider; /** Error Flag */ private boolean mHasError; /** * Creates a new {@link UiElementNode} described by a given {@link ElementDescriptor}. * * @param elementDescriptor The {@link ElementDescriptor} for the XML node. Cannot be null. */ public UiElementNode(ElementDescriptor elementDescriptor) { mDescriptor = elementDescriptor; clearContent(); } @Override public String toString() { return String.format("%s [desc: %s, parent: %s, children: %d]", //$NON-NLS-1$ this.getClass().getSimpleName(), mDescriptor, mUiParent != null ? mUiParent.toString() : "none", //$NON-NLS-1$ mUiChildren != null ? mUiChildren.size() : 0 ); } /** * Clears the {@link UiElementNode} by resetting the children list and * the {@link UiAttributeNode}s list. * Also resets the attached XML node, document, editor if any. * <p/> * The parent {@link UiElementNode} node is not reset so that it's position * in the hierarchy be left intact, if any. */ /* package */ void clearContent() { mXmlNode = null; mXmlDocument = null; mEditor = null; clearAttributes(); mReadOnlyUiChildren = null; if (mUiChildren == null) { mUiChildren = new ArrayList<UiElementNode>(); } else { // We can't remove mandatory nodes, we just clear them. for (int i = mUiChildren.size() - 1; i >= 0; --i) { removeUiChildAtIndex(i); } } } /** * Clears the internal list of attributes, the read-only cached version of it * and the read-only cached hidden attribute list. */ private void clearAttributes() { mUiAttributes = null; mCachedAllUiAttributes = null; mCachedHiddenAttributes = null; mUnknownUiAttributes = new HashSet<UiAttributeNode>(); } /** * Gets or creates the internal UiAttributes list. * <p/> * When the descriptor derives from ViewElementDescriptor, this list depends on the * current UiParent node. * * @return A new set of {@link UiAttributeNode} that matches the expected * attributes for this node. */ private HashMap<AttributeDescriptor, UiAttributeNode> getInternalUiAttributes() { if (mUiAttributes == null) { AttributeDescriptor[] attrList = getAttributeDescriptors(); mUiAttributes = new HashMap<AttributeDescriptor, UiAttributeNode>(attrList.length); for (AttributeDescriptor desc : attrList) { UiAttributeNode uiNode = desc.createUiNode(this); if (uiNode != null) { // Some AttributeDescriptors do not have UI associated mUiAttributes.put(desc, uiNode); } } } return mUiAttributes; } /** * Computes a short string describing the UI node suitable for tree views. * Uses the element's attribute "android:name" if present, or the "android:label" one * followed by the element's name if not repeated. * * @return A short string describing the UI node suitable for tree views. */ public String getShortDescription() { String name = mDescriptor.getUiName(); String attr = getDescAttribute(); if (attr != null) { // If the ui name is repeated in the attribute value, don't use it. // Typical case is to avoid ".pkg.MyActivity (Activity)". if (attr.contains(name)) { return attr; } else { return String.format("%1$s (%2$s)", attr, name); } } return name; } /** Returns the key attribute that can be used to describe this node, or null */ private String getDescAttribute() { if (mXmlNode != null && mXmlNode instanceof Element && mXmlNode.hasAttributes()) { // Application and Manifest nodes have a special treatment: they are unique nodes // so we don't bother trying to differentiate their strings and we fall back to // just using the UI name below. Element elem = (Element) mXmlNode; String attr = _Element_getAttributeNS(elem, SdkConstants.NS_RESOURCES, AndroidManifestDescriptors.ANDROID_NAME_ATTR); if (attr == null || attr.length() == 0) { attr = _Element_getAttributeNS(elem, SdkConstants.NS_RESOURCES, AndroidManifestDescriptors.ANDROID_LABEL_ATTR); } else if (mXmlNode.getNodeName().equals(SdkConstants.VIEW_FRAGMENT)) { attr = attr.substring(attr.lastIndexOf('.') + 1); } if (attr == null || attr.length() == 0) { attr = _Element_getAttributeNS(elem, SdkConstants.NS_RESOURCES, OtherXmlDescriptors.PREF_KEY_ATTR); } if (attr == null || attr.length() == 0) { attr = _Element_getAttributeNS(elem, null, // no namespace SdkConstants.ATTR_NAME); } if (attr == null || attr.length() == 0) { attr = _Element_getAttributeNS(elem, SdkConstants.NS_RESOURCES, SdkConstants.ATTR_ID); if (attr != null && attr.length() > 0) { for (String prefix : ID_PREFIXES) { if (attr.startsWith(prefix)) { attr = attr.substring(prefix.length()); break; } } } } if (attr != null && attr.length() > 0) { return attr; } } return null; } /** * Computes a styled string describing the UI node suitable for tree views. * Similar to {@link #getShortDescription()} but styles the Strings. * * @return A styled string describing the UI node suitable for tree views. */ public StyledString getStyledDescription() { String uiName = mDescriptor.getUiName(); // Special case: for <view>, show the class attribute value instead. // This is done here rather than in the descriptor since this depends on // node instance data. if (SdkConstants.VIEW_TAG.equals(uiName) && mXmlNode instanceof Element) { Element element = (Element) mXmlNode; String cls = element.getAttribute(ATTR_CLASS); if (cls != null) { uiName = cls.substring(cls.lastIndexOf('.') + 1); } } StyledString styledString = new StyledString(); String attr = getDescAttribute(); if (attr != null) { // Don't append the two when it's a repeat, e.g. Button01 (Button), // only when the ui name is not part of the attribute if (attr.toLowerCase(Locale.US).indexOf(uiName.toLowerCase(Locale.US)) == -1) { styledString.append(attr); styledString.append(String.format(" (%1$s)", uiName), StyledString.DECORATIONS_STYLER); } else { styledString.append(attr); } } if (styledString.length() == 0) { styledString.append(uiName); } return styledString; } /** * Retrieves an attribute value by local name and namespace URI. * <br>Per [<a href='http://www.w3.org/TR/1999/REC-xml-names-19990114/'>XML Namespaces</a>] * , applications must use the value <code>null</code> as the * <code>namespaceURI</code> parameter for methods if they wish to have * no namespace. * <p/> * Note: This is a wrapper around {@link Element#getAttributeNS(String, String)}. * In some versions of webtools, the getAttributeNS implementation crashes with an NPE. * This wrapper will return an empty string instead. * * @see Element#getAttributeNS(String, String) * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108">https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108</a> * @return The result from {@link Element#getAttributeNS(String, String)} or an empty string. */ private String _Element_getAttributeNS(Element element, String namespaceURI, String localName) { try { return element.getAttributeNS(namespaceURI, localName); } catch (Exception ignore) { return ""; } } /** * Computes a "breadcrumb trail" description for this node. * It will look something like "Manifest > Application > .myactivity (Activity) > Intent-Filter" * * @param includeRoot Whether to include the root (e.g. "Manifest") or not. Has no effect * when called on the root node itself. * @return The "breadcrumb trail" description for this node. */ public String getBreadcrumbTrailDescription(boolean includeRoot) { StringBuilder sb = new StringBuilder(getShortDescription()); for (UiElementNode uiNode = getUiParent(); uiNode != null; uiNode = uiNode.getUiParent()) { if (!includeRoot && uiNode.getUiParent() == null) { break; } sb.insert(0, String.format("%1$s > ", uiNode.getShortDescription())); //$NON-NLS-1$ } return sb.toString(); } /** * Sets the XML {@link Document}. * <p/> * The XML {@link Document} is initially null. The XML {@link Document} must be set only on the * UI root element node (this method takes care of that.) * @param xmlDoc The new XML document to associate this node with. */ public void setXmlDocument(Document xmlDoc) { if (mUiParent == null) { mXmlDocument = xmlDoc; } else { mUiParent.setXmlDocument(xmlDoc); } } /** * Returns the XML {@link Document}. * <p/> * The value is initially null until the UI node is attached to its UI parent -- the value * of the document is then propagated. * * @return the XML {@link Document} or the parent's XML {@link Document} or null. */ public Document getXmlDocument() { if (mXmlDocument != null) { return mXmlDocument; } else if (mUiParent != null) { return mUiParent.getXmlDocument(); } return null; } /** * Returns the XML node associated with this UI node. * <p/> * Some {@link ElementDescriptor} are declared as being "mandatory". This means the * corresponding UI node will exist even if there is no corresponding XML node. Such structure * is created and enforced by the parent of the tree, not the element themselves. However * such nodes will likely not have an XML node associated, so getXmlNode() can return null. * * @return The associated XML node. Can be null for mandatory nodes. */ public Node getXmlNode() { return mXmlNode; } /** * Returns the {@link ElementDescriptor} for this node. This is never null. * <p/> * Do not use this to call getDescriptor().getAttributes(), instead call * getAttributeDescriptors() which can be overridden by derived classes. * @return The {@link ElementDescriptor} for this node. This is never null. */ public ElementDescriptor getDescriptor() { return mDescriptor; } /** * Returns the {@link AttributeDescriptor} array for the descriptor of this node. * <p/> * Use this instead of getDescriptor().getAttributes() -- derived classes can override * this to manipulate the attribute descriptor list depending on the current UI node. * @return The {@link AttributeDescriptor} array for the descriptor of this node. */ public AttributeDescriptor[] getAttributeDescriptors() { return mDescriptor.getAttributes(); } /** * Returns the hidden {@link AttributeDescriptor} array for the descriptor of this node. * This is a subset of the getAttributeDescriptors() list. * <p/> * Use this instead of getDescriptor().getHiddenAttributes() -- potentially derived classes * could override this to manipulate the attribute descriptor list depending on the current * UI node. There's no need for it right now so keep it private. */ private Map<String, AttributeDescriptor> getHiddenAttributeDescriptors() { if (mCachedHiddenAttributes == null) { mCachedHiddenAttributes = new HashMap<String, AttributeDescriptor>(); for (AttributeDescriptor attrDesc : getAttributeDescriptors()) { if (attrDesc instanceof XmlnsAttributeDescriptor) { mCachedHiddenAttributes.put( ((XmlnsAttributeDescriptor) attrDesc).getXmlNsName(), attrDesc); } } } return mCachedHiddenAttributes; } /** * Sets the parent of this UiElementNode. * <p/> * The root node has no parent. */ protected void setUiParent(UiElementNode parent) { mUiParent = parent; // Invalidate the internal UiAttributes list, as it may depend on the actual UiParent. clearAttributes(); } /** * @return The parent {@link UiElementNode} or null if this is the root node. */ public UiElementNode getUiParent() { return mUiParent; } /** * Returns the root {@link UiElementNode}. * * @return The root {@link UiElementNode}. */ public UiElementNode getUiRoot() { UiElementNode root = this; while (root.mUiParent != null) { root = root.mUiParent; } return root; } /** * Returns the index of this sibling (where the first child has index 0, the second child * has index 1, and so on.) * * @return The sibling index of this node */ public int getUiSiblingIndex() { if (mUiParent != null) { int index = 0; for (UiElementNode node : mUiParent.getUiChildren()) { if (node == this) { break; } index++; } return index; } return 0; } /** * Returns the previous UI sibling of this UI node. If the node does not have a previous * sibling, returns null. * * @return The previous UI sibling of this UI node, or null if not applicable. */ public UiElementNode getUiPreviousSibling() { if (mUiParent != null) { List<UiElementNode> childlist = mUiParent.getUiChildren(); if (childlist != null && childlist.size() > 1 && childlist.get(0) != this) { int index = childlist.indexOf(this); return index > 0 ? childlist.get(index - 1) : null; } } return null; } /** * Returns the next UI sibling of this UI node. * If the node does not have a next sibling, returns null. * * @return The next UI sibling of this UI node, or null. */ public UiElementNode getUiNextSibling() { if (mUiParent != null) { List<UiElementNode> childlist = mUiParent.getUiChildren(); if (childlist != null) { int size = childlist.size(); if (size > 1 && childlist.get(size - 1) != this) { int index = childlist.indexOf(this); return index >= 0 && index < size - 1 ? childlist.get(index + 1) : null; } } } return null; } /** * Sets the {@link AndroidXmlEditor} handling this {@link UiElementNode} hierarchy. * <p/> * The editor must always be set on the root node. This method takes care of that. * * @param editor The editor to associate this node with. */ public void setEditor(AndroidXmlEditor editor) { if (mUiParent == null) { mEditor = editor; } else { mUiParent.setEditor(editor); } } /** * Returns the {@link AndroidXmlEditor} that embeds this {@link UiElementNode}. * <p/> * The value is initially null until the node is attached to its parent -- the value * of the root node is then propagated. * * @return The embedding {@link AndroidXmlEditor} or null. */ public AndroidXmlEditor getEditor() { return mUiParent == null ? mEditor : mUiParent.getEditor(); } /** * Returns the Android target data for the file being edited. * * @return The Android target data for the file being edited. */ public AndroidTargetData getAndroidTarget() { return getEditor().getTargetData(); } /** * @return A read-only version of the children collection. */ public List<UiElementNode> getUiChildren() { if (mReadOnlyUiChildren == null) { mReadOnlyUiChildren = Collections.unmodifiableList(mUiChildren); } return mReadOnlyUiChildren; } /** * Returns a collection containing all the known attributes as well as * all the unknown ui attributes. * * @return A read-only version of the attributes collection. */ public Collection<UiAttributeNode> getAllUiAttributes() { if (mCachedAllUiAttributes == null) { List<UiAttributeNode> allValues = new ArrayList<UiAttributeNode>(getInternalUiAttributes().values()); allValues.addAll(mUnknownUiAttributes); mCachedAllUiAttributes = Collections.unmodifiableCollection(allValues); } return mCachedAllUiAttributes; } /** * Returns all the unknown ui attributes, that is those we found defined in the * actual XML but that we don't have descriptors for. * * @return A read-only version of the unknown attributes collection. */ public Collection<UiAttributeNode> getUnknownUiAttributes() { return Collections.unmodifiableCollection(mUnknownUiAttributes); } /** * Sets the error flag value. * * @param errorFlag the error flag */ public final void setHasError(boolean errorFlag) { mHasError = errorFlag; } /** * Returns whether this node, its attributes, or one of the children nodes (and attributes) * has errors. * * @return True if this node, its attributes, or one of the children nodes (and attributes) * has errors. */ public final boolean hasError() { if (mHasError) { return true; } // get the error value from the attributes. for (UiAttributeNode attribute : getAllUiAttributes()) { if (attribute.hasError()) { return true; } } // and now from the children. for (UiElementNode child : mUiChildren) { if (child.hasError()) { return true; } } return false; } /** * Returns the provider that knows how to create {@link ElementDescriptor} from unmapped * XML names. * <p/> * The default is to have one that creates new {@link ElementDescriptor}. * <p/> * There is only one such provider in any UI model tree, attached to the root node. * * @return An instance of {@link IUnknownDescriptorProvider}. Can never be null. */ public IUnknownDescriptorProvider getUnknownDescriptorProvider() { if (mUiParent != null) { return mUiParent.getUnknownDescriptorProvider(); } if (mUnknownDescProvider == null) { // Create the default one on demand. mUnknownDescProvider = new IUnknownDescriptorProvider() { private final HashMap<String, ElementDescriptor> mMap = new HashMap<String, ElementDescriptor>(); /** * The default is to create a new ElementDescriptor wrapping * the unknown XML local name and reuse previously created descriptors. */ @Override public ElementDescriptor getDescriptor(String xmlLocalName) { ElementDescriptor desc = mMap.get(xmlLocalName); if (desc == null) { desc = new ElementDescriptor(xmlLocalName); mMap.put(xmlLocalName, desc); } return desc; } }; } return mUnknownDescProvider; } /** * Sets the provider that knows how to create {@link ElementDescriptor} from unmapped * XML names. * <p/> * The default is to have one that creates new {@link ElementDescriptor}. * <p/> * There is only one such provider in any UI model tree, attached to the root node. * * @param unknownDescProvider The new provider to use. Must not be null. */ public void setUnknownDescriptorProvider(IUnknownDescriptorProvider unknownDescProvider) { if (mUiParent == null) { mUnknownDescProvider = unknownDescProvider; } else { mUiParent.setUnknownDescriptorProvider(unknownDescProvider); } } /** * Adds a new {@link IUiUpdateListener} to the internal update listener list. * * @param listener The listener to add. */ public void addUpdateListener(IUiUpdateListener listener) { if (mUiUpdateListeners == null) { mUiUpdateListeners = new ArrayList<IUiUpdateListener>(); } if (!mUiUpdateListeners.contains(listener)) { mUiUpdateListeners.add(listener); } } /** * Removes an existing {@link IUiUpdateListener} from the internal update listener list. * Does nothing if the list is empty or the listener is not registered. * * @param listener The listener to remove. */ public void removeUpdateListener(IUiUpdateListener listener) { if (mUiUpdateListeners != null) { mUiUpdateListeners.remove(listener); } } /** * Finds a child node relative to this node using a path-like expression. * F.ex. "node1/node2" would find a child "node1" that contains a child "node2" and * returns the latter. If there are multiple nodes with the same name at the same * level, always uses the first one found. * * @param path The path like expression to select a child node. * @return The ui node found or null. */ public UiElementNode findUiChildNode(String path) { String[] items = path.split("/"); //$NON-NLS-1$ UiElementNode uiNode = this; for (String item : items) { boolean nextSegment = false; for (UiElementNode c : uiNode.mUiChildren) { if (c.getDescriptor().getXmlName().equals(item)) { uiNode = c; nextSegment = true; break; } } if (!nextSegment) { return null; } } return uiNode; } /** * Finds an {@link UiElementNode} which contains the give XML {@link Node}. * Looks recursively in all children UI nodes. * * @param xmlNode The XML node to look for. * @return The {@link UiElementNode} that contains xmlNode or null if not found, */ public UiElementNode findXmlNode(Node xmlNode) { if (xmlNode == null) { return null; } if (getXmlNode() == xmlNode) { return this; } for (UiElementNode uiChild : mUiChildren) { UiElementNode found = uiChild.findXmlNode(xmlNode); if (found != null) { return found; } } return null; } /** * Returns the {@link UiAttributeNode} matching this attribute descriptor or * null if not found. * * @param attrDesc The {@link AttributeDescriptor} to match. * @return the {@link UiAttributeNode} matching this attribute descriptor or null * if not found. */ public UiAttributeNode findUiAttribute(AttributeDescriptor attrDesc) { return getInternalUiAttributes().get(attrDesc); } /** * Populate this element node with all values from the given XML node. * * This fails if the given XML node has a different element name -- it won't change the * type of this ui node. * * This method can be both used for populating values the first time and updating values * after the XML model changed. * * @param xmlNode The XML node to mirror * @return Returns true if the XML structure has changed (nodes added, removed or replaced) */ public boolean loadFromXmlNode(Node xmlNode) { boolean structureChanged = (mXmlNode != xmlNode); mXmlNode = xmlNode; if (xmlNode != null) { updateAttributeList(xmlNode); structureChanged |= updateElementList(xmlNode); invokeUiUpdateListeners(structureChanged ? UiUpdateState.CHILDREN_CHANGED : UiUpdateState.ATTR_UPDATED); } return structureChanged; } /** * Clears the UI node and reload it from the given XML node. * <p/> * This works by clearing all references to any previous XML or UI nodes and * then reloads the XML document from scratch. The editor reference is kept. * <p/> * This is used in the special case where the ElementDescriptor structure has changed. * Rather than try to diff inflated UI nodes (as loadFromXmlNode does), we don't bother * and reload everything. This is not subtle and should be used very rarely. * * @param xmlNode The XML node or document to reload. Can be null. */ public void reloadFromXmlNode(Node xmlNode) { // The editor needs to be preserved, it is not affected by an XML change. AndroidXmlEditor editor = getEditor(); clearContent(); setEditor(editor); if (xmlNode != null) { setXmlDocument(xmlNode.getOwnerDocument()); } // This will reload all the XML and recreate the UI structure from scratch. loadFromXmlNode(xmlNode); } /** * Called by attributes when they want to commit their value * to an XML node. * <p/> * For mandatory nodes, this makes sure the underlying XML element node * exists in the model. If not, it is created and assigned as the underlying * XML node. * </br> * For non-mandatory nodes, simply return the underlying XML node, which * must always exists. * * @return The XML node matching this {@link UiElementNode} or null. */ public Node prepareCommit() { if (getDescriptor().getMandatory() != Mandatory.NOT_MANDATORY) { createXmlNode(); // The new XML node has been created. // We don't need to refresh using loadFromXmlNode() since there are // no attributes or elements that need to be loading into this node. } return getXmlNode(); } /** * Commits the attributes (all internal, inherited from UI parent & unknown attributes). * This is called by the UI when the embedding part needs to be committed. */ public void commit() { for (UiAttributeNode uiAttr : getAllUiAttributes()) { uiAttr.commit(); } } /** * Returns true if the part has been modified with respect to the data * loaded from the model. * @return True if the part has been modified with respect to the data * loaded from the model. */ public boolean isDirty() { for (UiAttributeNode uiAttr : getAllUiAttributes()) { if (uiAttr.isDirty()) { return true; } } return false; } /** * Creates the underlying XML element node for this UI node if it doesn't already * exists. * * @return The new value of getXmlNode() (can be null if creation failed) */ public Node createXmlNode() { if (mXmlNode != null) { return null; } Node parentXmlNode = null; if (mUiParent != null) { parentXmlNode = mUiParent.prepareCommit(); if (parentXmlNode == null) { // The parent failed to create its own backing XML node. Abort. // No need to throw an exception, the parent will most likely // have done so itself. return null; } } String elementName = getDescriptor().getXmlName(); Document doc = getXmlDocument(); // We *must* have a root node. If not, we need to abort. if (doc == null) { throw new RuntimeException( String.format("Missing XML document for %1$s XML node.", elementName)); } // If we get here and parentXmlNode is null, the node is to be created // as the root node of the document (which can't be null, cf. check above). if (parentXmlNode == null) { parentXmlNode = doc; } mXmlNode = doc.createElement(elementName); // If this element does not have children, mark it as an empty tag // such that the XML looks like <tag/> instead of <tag></tag> if (!mDescriptor.hasChildren()) { if (mXmlNode instanceof ElementImpl) { ElementImpl element = (ElementImpl) mXmlNode; element.setEmptyTag(true); } } Node xmlNextSibling = null; UiElementNode uiNextSibling = getUiNextSibling(); if (uiNextSibling != null) { xmlNextSibling = uiNextSibling.getXmlNode(); } Node previousTextNode = null; if (xmlNextSibling != null) { Node previousNode = xmlNextSibling.getPreviousSibling(); if (previousNode != null && previousNode.getNodeType() == Node.TEXT_NODE) { previousTextNode = previousNode; } } else { Node lastChild = parentXmlNode.getLastChild(); if (lastChild != null && lastChild.getNodeType() == Node.TEXT_NODE) { previousTextNode = lastChild; } } String insertAfter = null; // Try to figure out the indentation node to insert. Even in auto-formatting // we need to do this, because it turns out the XML editor's formatter does // not do a very good job with completely botched up XML; it does a much better // job if the new XML is already mostly well formatted. Thus, the main purpose // of applying the real XML formatter after our own indentation attempts here is // to make it apply its own tab-versus-spaces indentation properties, have it // insert line breaks before attributes (if the user has configured that), etc. // First figure out the indentation level of the newly inserted element; // this is either the same as the previous sibling, or if there is no sibling, // it's the indentation of the parent plus one indentation level. boolean isFirstChild = getUiPreviousSibling() == null || parentXmlNode.getFirstChild() == null; AndroidXmlEditor editor = getEditor(); String indent; String parentIndent = ""; //$NON-NLS-1$ if (isFirstChild) { indent = parentIndent = editor.getIndent(parentXmlNode); // We need to add one level of indentation. Are we using tabs? // Can't get to formatting settings so let's just look at the // parent indentation and see if we can guess if (indent.length() > 0 && indent.charAt(indent.length()-1) == '\t') { indent = indent + '\t'; } else { // Not using tabs, or we can't figure it out (because parent had no // indentation). In that case, indent with 4 spaces, as seems to // be the Android default. indent = indent + " "; //$NON-NLS-1$ } } else { // Find out the indent of the previous sibling indent = editor.getIndent(getUiPreviousSibling().getXmlNode()); } // We want to insert the new element BEFORE the text node which precedes // the next element, since that text node is the next element's indentation! if (previousTextNode != null) { xmlNextSibling = previousTextNode; } else { // If there's no previous text node, we are probably inside an // empty element (<LinearLayout>|</LinearLayout>) and in that case we need // to not only insert a newline and indentation before the new element, but // after it as well. insertAfter = parentIndent; } // Insert indent text node before the new element IStructuredDocument document = editor.getStructuredDocument(); String newLine; if (document != null) { newLine = TextUtilities.getDefaultLineDelimiter(document); } else { newLine = SdkUtils.getLineSeparator(); } Text indentNode = doc.createTextNode(newLine + indent); parentXmlNode.insertBefore(indentNode, xmlNextSibling); // Insert the element itself parentXmlNode.insertBefore(mXmlNode, xmlNextSibling); // Insert a separator after the tag. We only do this when we've inserted // a tag into an area where there was no whitespace before // (e.g. a new child of <LinearLayout></LinearLayout>). if (insertAfter != null) { Text sep = doc.createTextNode(newLine + insertAfter); parentXmlNode.insertBefore(sep, xmlNextSibling); } // Set all initial attributes in the XML node if they are not empty. // Iterate on the descriptor list to get the desired order and then use the // internal values, if any. List<UiAttributeNode> addAttributes = new ArrayList<UiAttributeNode>(); for (AttributeDescriptor attrDesc : getAttributeDescriptors()) { if (attrDesc instanceof XmlnsAttributeDescriptor) { XmlnsAttributeDescriptor desc = (XmlnsAttributeDescriptor) attrDesc; Attr attr = doc.createAttributeNS(SdkConstants.XMLNS_URI, desc.getXmlNsName()); attr.setValue(desc.getValue()); attr.setPrefix(desc.getXmlNsPrefix()); mXmlNode.getAttributes().setNamedItemNS(attr); } else { UiAttributeNode uiAttr = getInternalUiAttributes().get(attrDesc); // Don't apply the attribute immediately, instead record this attribute // such that we can gather all attributes and sort them first. // This is necessary because the XML model will *append* all attributes // so we want to add them in a particular order. // (Note that we only have to worry about UiAttributeNodes with non null // values, since this is a new node and we therefore don't need to attempt // to remove existing attributes) String value = uiAttr.getCurrentValue(); if (value != null && value.length() > 0) { addAttributes.add(uiAttr); } } } // Sort and apply the attributes in order, because the Eclipse XML model will always // append the XML attributes, so by inserting them in our desired order they will // appear that way in the XML Collections.sort(addAttributes); for (UiAttributeNode node : addAttributes) { commitAttributeToXml(node, node.getCurrentValue()); node.setDirty(false); } getEditor().scheduleNodeReformat(this, false); // Notify per-node listeners invokeUiUpdateListeners(UiUpdateState.CREATED); // Notify global listeners fireNodeCreated(this, getUiSiblingIndex()); return mXmlNode; } /** * Removes the XML node corresponding to this UI node if it exists * and also removes all mirrored information in this UI node (i.e. children, attributes) * * @return The removed node or null if it didn't exist in the first place. */ public Node deleteXmlNode() { if (mXmlNode == null) { return null; } int previousIndex = getUiSiblingIndex(); // First clear the internals of the node and *then* actually deletes the XML // node (because doing so will generate an update even and this node may be // revisited via loadFromXmlNode). Node oldXmlNode = mXmlNode; clearContent(); Node xmlParent = oldXmlNode.getParentNode(); if (xmlParent == null) { xmlParent = getXmlDocument(); } Node previousSibling = oldXmlNode.getPreviousSibling(); oldXmlNode = xmlParent.removeChild(oldXmlNode); // We need to remove the text node BEFORE the removed element, since THAT's the // indentation node for the removed element. if (previousSibling != null && previousSibling.getNodeType() == Node.TEXT_NODE && previousSibling.getNodeValue().trim().length() == 0) { xmlParent.removeChild(previousSibling); } invokeUiUpdateListeners(UiUpdateState.DELETED); fireNodeDeleted(this, previousIndex); return oldXmlNode; } /** * Updates the element list for this UiElementNode. * At the end, the list of children UiElementNode here will match the one from the * provided XML {@link Node}: * <ul> * <li> Walk both the current ui children list and the xml children list at the same time. * <li> If we have a new xml child but already reached the end of the ui child list, add the * new xml node. * <li> Otherwise, check if the xml node is referenced later in the ui child list and if so, * move it here. It means the XML child list has been reordered. * <li> Otherwise, this is a new XML node that we add in the middle of the ui child list. * <li> At the end, we may have finished walking the xml child list but still have remaining * ui children, simply delete them as they matching trailing xml nodes that have been * removed unless they are mandatory ui nodes. * </ul> * Note that only the first case is used when populating the ui list the first time. * * @param xmlNode The XML node to mirror * @return True when the XML structure has changed. */ protected boolean updateElementList(Node xmlNode) { boolean structureChanged = false; boolean hasMandatoryLast = false; int uiIndex = 0; Node xmlChild = xmlNode.getFirstChild(); while (xmlChild != null) { if (xmlChild.getNodeType() == Node.ELEMENT_NODE) { String elementName = xmlChild.getNodeName(); UiElementNode uiNode = null; CustomViewDescriptorService service = CustomViewDescriptorService.getInstance(); if (mUiChildren.size() <= uiIndex) { // A new node is being added at the end of the list ElementDescriptor desc = mDescriptor.findChildrenDescriptor(elementName, false /* recursive */); if (desc == null && elementName.indexOf('.') != -1 && (!elementName.startsWith(ANDROID_PKG_PREFIX) || elementName.startsWith(ANDROID_SUPPORT_PKG_PREFIX))) { AndroidXmlEditor editor = getEditor(); if (editor != null && editor.getProject() != null) { desc = service.getDescriptor(editor.getProject(), elementName); } } if (desc == null) { // Unknown node. Create a temporary descriptor for it. // We'll add unknown attributes to it later. IUnknownDescriptorProvider p = getUnknownDescriptorProvider(); desc = p.getDescriptor(elementName); } structureChanged = true; uiNode = appendNewUiChild(desc); uiIndex++; } else { // A new node is being inserted or moved. // Note: mandatory nodes can be created without an XML node in which case // getXmlNode() is null. UiElementNode uiChild; int n = mUiChildren.size(); for (int j = uiIndex; j < n; j++) { uiChild = mUiChildren.get(j); if (uiChild.getXmlNode() != null && uiChild.getXmlNode() == xmlChild) { if (j > uiIndex) { // Found the same XML node at some later index, now move it here. mUiChildren.remove(j); mUiChildren.add(uiIndex, uiChild); structureChanged = true; } uiNode = uiChild; uiIndex++; break; } } if (uiNode == null) { // Look for an unused mandatory node with no XML node attached // referencing the same XML element name for (int j = uiIndex; j < n; j++) { uiChild = mUiChildren.get(j); if (uiChild.getXmlNode() == null && uiChild.getDescriptor().getMandatory() != Mandatory.NOT_MANDATORY && uiChild.getDescriptor().getXmlName().equals(elementName)) { if (j > uiIndex) { // Found it, now move it here mUiChildren.remove(j); mUiChildren.add(uiIndex, uiChild); } // Assign the XML node to this empty mandatory element. uiChild.mXmlNode = xmlChild; structureChanged = true; uiNode = uiChild; uiIndex++; } } } if (uiNode == null) { // Inserting new node ElementDescriptor desc = mDescriptor.findChildrenDescriptor(elementName, false /* recursive */); if (desc == null && elementName.indexOf('.') != -1 && (!elementName.startsWith(ANDROID_PKG_PREFIX) || elementName.startsWith(ANDROID_SUPPORT_PKG_PREFIX))) { AndroidXmlEditor editor = getEditor(); if (editor != null && editor.getProject() != null) { desc = service.getDescriptor(editor.getProject(), elementName); } } if (desc == null) { // Unknown node. Create a temporary descriptor for it. // We'll add unknown attributes to it later. IUnknownDescriptorProvider p = getUnknownDescriptorProvider(); desc = p.getDescriptor(elementName); } else { structureChanged = true; uiNode = insertNewUiChild(uiIndex, desc); uiIndex++; } } } if (uiNode != null) { // If we touched an UI Node, even an existing one, refresh its content. // For new nodes, this will populate them recursively. structureChanged |= uiNode.loadFromXmlNode(xmlChild); // Remember if there are any mandatory-last nodes to reorder. hasMandatoryLast |= uiNode.getDescriptor().getMandatory() == Mandatory.MANDATORY_LAST; } } xmlChild = xmlChild.getNextSibling(); } // There might be extra UI nodes at the end if the XML node list got shorter. for (int index = mUiChildren.size() - 1; index >= uiIndex; --index) { structureChanged |= removeUiChildAtIndex(index); } if (hasMandatoryLast) { // At least one mandatory-last uiNode was moved. Let's see if we can // move them back to the last position. That's possible if the only // thing between these and the end are other mandatory empty uiNodes // (mandatory uiNodes with no XML attached are pure "virtual" reserved // slots and it's ok to reorganize them but other can't.) int n = mUiChildren.size() - 1; for (int index = n; index >= 0; index--) { UiElementNode uiChild = mUiChildren.get(index); Mandatory mand = uiChild.getDescriptor().getMandatory(); if (mand == Mandatory.MANDATORY_LAST && index < n) { // Remove it from index and move it back at the end of the list. mUiChildren.remove(index); mUiChildren.add(uiChild); } else if (mand == Mandatory.NOT_MANDATORY || uiChild.getXmlNode() != null) { // We found at least one non-mandatory or a mandatory node with an actual // XML attached, so there's nothing we can reorganize past this point. break; } } } return structureChanged; } /** * Internal helper to remove an UI child node given by its index in the * internal child list. * * Also invokes the update listener on the node to be deleted *after* the node has * been removed. * * @param uiIndex The index of the UI child to remove, range 0 .. mUiChildren.size()-1 * @return True if the structure has changed * @throws IndexOutOfBoundsException if index is out of mUiChildren's bounds. Of course you * know that could never happen unless the computer is on fire or something. */ private boolean removeUiChildAtIndex(int uiIndex) { UiElementNode uiNode = mUiChildren.get(uiIndex); ElementDescriptor desc = uiNode.getDescriptor(); try { if (uiNode.getDescriptor().getMandatory() != Mandatory.NOT_MANDATORY) { // This is a mandatory node. Such a node must exist in the UiNode hierarchy // even if there's no XML counterpart. However we only need to keep one. // Check if the parent (e.g. this node) has another similar ui child node. boolean keepNode = true; for (UiElementNode child : mUiChildren) { if (child != uiNode && child.getDescriptor() == desc) { // We found another child with the same descriptor that is not // the node we want to remove. This means we have one mandatory // node so we can safely remove uiNode. keepNode = false; break; } } if (keepNode) { // We can't remove a mandatory node as we need to keep at least one // mandatory node in the parent. Instead we just clear its content // (including its XML Node reference). // A mandatory node with no XML means it doesn't really exist, so it can't be // deleted. So the structure will change only if the ui node is actually // associated to an XML node. boolean xmlExists = (uiNode.getXmlNode() != null); uiNode.clearContent(); return xmlExists; } } mUiChildren.remove(uiIndex); return true; } finally { // Tell listeners that a node has been removed. // The model has already been modified. invokeUiUpdateListeners(UiUpdateState.DELETED); } } /** * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor} * and appends it to the end of the element children list. * * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node. * @return The new UI node that has been appended */ public UiElementNode appendNewUiChild(ElementDescriptor descriptor) { UiElementNode uiNode; uiNode = descriptor.createUiNode(); mUiChildren.add(uiNode); uiNode.setUiParent(this); uiNode.invokeUiUpdateListeners(UiUpdateState.CREATED); return uiNode; } /** * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor} * and inserts it in the element children list at the specified position. * * @param index The position where to insert in the element children list. * Shifts the element currently at that position (if any) and any * subsequent elements to the right (adds one to their indices). * Index must >= 0 and <= getUiChildren.size(). * Using size() means to append to the end of the list. * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node. * @return The new UI node. */ public UiElementNode insertNewUiChild(int index, ElementDescriptor descriptor) { UiElementNode uiNode; uiNode = descriptor.createUiNode(); mUiChildren.add(index, uiNode); uiNode.setUiParent(this); uiNode.invokeUiUpdateListeners(UiUpdateState.CREATED); return uiNode; } /** * Updates the {@link UiAttributeNode} list for this {@link UiElementNode} * using the values from the XML element. * <p/> * For a given {@link UiElementNode}, the attribute list always exists in * full and is totally independent of whether the XML model actually * has the corresponding attributes. * <p/> * For each attribute declared in this {@link UiElementNode}, get * the corresponding XML attribute. It may not exist, in which case the * value will be null. We don't really know if a value has changed, so * the updateValue() is called on the UI attribute in all cases. * * @param xmlNode The XML node to mirror */ protected void updateAttributeList(Node xmlNode) { NamedNodeMap xmlAttrMap = xmlNode.getAttributes(); HashSet<Node> visited = new HashSet<Node>(); // For all known (i.e. expected) UI attributes, find an existing XML attribute of // same (uri, local name) and update the internal Ui attribute value. for (UiAttributeNode uiAttr : getInternalUiAttributes().values()) { AttributeDescriptor desc = uiAttr.getDescriptor(); if (!(desc instanceof SeparatorAttributeDescriptor)) { Node xmlAttr = xmlAttrMap == null ? null : xmlAttrMap.getNamedItemNS(desc.getNamespaceUri(), desc.getXmlLocalName()); uiAttr.updateValue(xmlAttr); visited.add(xmlAttr); } } // Clone the current list of unknown attributes. We'll then remove from this list when // we find attributes which are still unknown. What will be left are the old unknown // attributes that have been deleted in the current XML attribute list. @SuppressWarnings("unchecked") HashSet<UiAttributeNode> deleted = (HashSet<UiAttributeNode>) mUnknownUiAttributes.clone(); // We need to ignore hidden attributes. Map<String, AttributeDescriptor> hiddenAttrDesc = getHiddenAttributeDescriptors(); // Traverse the actual XML attribute list to find unknown attributes if (xmlAttrMap != null) { for (int i = 0; i < xmlAttrMap.getLength(); i++) { Node xmlAttr = xmlAttrMap.item(i); // Ignore attributes which have actual descriptors if (visited.contains(xmlAttr)) { continue; } String xmlFullName = xmlAttr.getNodeName(); // Ignore attributes which are hidden (based on the prefix:localName key) if (hiddenAttrDesc.containsKey(xmlFullName)) { continue; } String xmlAttrLocalName = xmlAttr.getLocalName(); String xmlNsUri = xmlAttr.getNamespaceURI(); UiAttributeNode uiAttr = null; for (UiAttributeNode a : mUnknownUiAttributes) { String aLocalName = a.getDescriptor().getXmlLocalName(); String aNsUri = a.getDescriptor().getNamespaceUri(); if (aLocalName.equals(xmlAttrLocalName) && (aNsUri == xmlNsUri || (aNsUri != null && aNsUri.equals(xmlNsUri)))) { // This attribute is still present in the unknown list uiAttr = a; // It has not been deleted deleted.remove(a); break; } } if (uiAttr == null) { uiAttr = addUnknownAttribute(xmlFullName, xmlAttrLocalName, xmlNsUri); } uiAttr.updateValue(xmlAttr); } // Remove from the internal list unknown attributes that have been deleted from the xml for (UiAttributeNode a : deleted) { mUnknownUiAttributes.remove(a); mCachedAllUiAttributes = null; } } } /** * Create a new temporary text attribute descriptor for the unknown attribute * and returns a new {@link UiAttributeNode} associated to this descriptor. * <p/> * The attribute is not marked as dirty, doing so is up to the caller. */ private UiAttributeNode addUnknownAttribute(String xmlFullName, String xmlAttrLocalName, String xmlNsUri) { // Create a new unknown attribute of format string TextAttributeDescriptor desc = new TextAttributeDescriptor( xmlAttrLocalName, // xml name xmlNsUri, // ui name new AttributeInfo(xmlAttrLocalName, Format.STRING_SET) ); UiAttributeNode uiAttr = desc.createUiNode(this); mUnknownUiAttributes.add(uiAttr); mCachedAllUiAttributes = null; return uiAttr; } /** * Invoke all registered {@link IUiUpdateListener} listening on this UI update for this node. */ protected void invokeUiUpdateListeners(UiUpdateState state) { if (mUiUpdateListeners != null) { for (IUiUpdateListener listener : mUiUpdateListeners) { try { listener.uiElementNodeUpdated(this, state); } catch (Exception e) { // prevent a crashing listener from crashing the whole invocation chain AdtPlugin.log(e, "UIElement Listener failed: %s, state=%s", //$NON-NLS-1$ getBreadcrumbTrailDescription(true), state.toString()); } } } } // --- for derived implementations only --- @VisibleForTesting public void setXmlNode(Node xmlNode) { mXmlNode = xmlNode; } public void refreshUi() { invokeUiUpdateListeners(UiUpdateState.ATTR_UPDATED); } // ------------- Helpers /** * Helper method to commit a single attribute value to XML. * <p/> * This method updates the XML regardless of the current XML value. * Callers should check first if an update is needed. * If the new value is empty, the XML attribute will be actually removed. * <p/> * Note that the caller MUST ensure that modifying the underlying XML model is * safe and must take care of marking the model as dirty if necessary. * * @see AndroidXmlEditor#wrapEditXmlModel(Runnable) * * @param uiAttr The attribute node to commit. Must be a child of this UiElementNode. * @param newValue The new value to set. * @return True if the XML attribute was modified or removed, false if nothing changed. */ public boolean commitAttributeToXml(UiAttributeNode uiAttr, String newValue) { // Get (or create) the underlying XML element node that contains the attributes. Node element = prepareCommit(); if (element != null && uiAttr != null) { String attrLocalName = uiAttr.getDescriptor().getXmlLocalName(); String attrNsUri = uiAttr.getDescriptor().getNamespaceUri(); NamedNodeMap attrMap = element.getAttributes(); if (newValue == null || newValue.length() == 0) { // Remove attribute if it's empty if (attrMap.getNamedItemNS(attrNsUri, attrLocalName) != null) { attrMap.removeNamedItemNS(attrNsUri, attrLocalName); return true; } } else { // Add or replace an attribute Document doc = element.getOwnerDocument(); if (doc != null) { Attr attr; if (attrNsUri != null && attrNsUri.length() > 0) { attr = (Attr) attrMap.getNamedItemNS(attrNsUri, attrLocalName); if (attr == null) { attr = doc.createAttributeNS(attrNsUri, attrLocalName); attr.setPrefix(XmlUtils.lookupNamespacePrefix(element, attrNsUri)); attrMap.setNamedItemNS(attr); } } else { attr = (Attr) attrMap.getNamedItem(attrLocalName); if (attr == null) { attr = doc.createAttribute(attrLocalName); attrMap.setNamedItem(attr); } } attr.setValue(newValue); return true; } } } return false; } /** * Helper method to commit all dirty attributes values to XML. * <p/> * This method is useful if {@link #setAttributeValue(String, String, String, boolean)} has * been called more than once and all the attributes marked as dirty must be committed to * the XML. It calls {@link #commitAttributeToXml(UiAttributeNode, String)} on each dirty * attribute. * <p/> * Note that the caller MUST ensure that modifying the underlying XML model is * safe and must take care of marking the model as dirty if necessary. * * @see AndroidXmlEditor#wrapEditXmlModel(Runnable) * * @return True if one or more values were actually modified or removed, * false if nothing changed. */ @SuppressWarnings("null") // Eclipse is confused by the logic and gets it wrong public boolean commitDirtyAttributesToXml() { boolean result = false; List<UiAttributeNode> dirtyAttributes = new ArrayList<UiAttributeNode>(); for (UiAttributeNode uiAttr : getAllUiAttributes()) { if (uiAttr.isDirty()) { String value = uiAttr.getCurrentValue(); if (value != null && value.length() > 0) { // Defer the new attributes: set these last and in order dirtyAttributes.add(uiAttr); } else { result |= commitAttributeToXml(uiAttr, value); uiAttr.setDirty(false); } } } if (dirtyAttributes.size() > 0) { result = true; Collections.sort(dirtyAttributes); // The Eclipse XML model will *always* append new attributes. // Therefore, if any of the dirty attributes are new, they will appear // after any existing, clean attributes on the element. To fix this, // we need to first remove any of these attributes, then insert them // back in the right order. Node element = prepareCommit(); if (element == null) { return result; } if (AdtPrefs.getPrefs().getFormatGuiXml() && getEditor().supportsFormatOnGuiEdit()) { // If auto formatting, don't bother with attribute sorting here since the // order will be corrected as soon as the edit is committed anyway for (UiAttributeNode uiAttribute : dirtyAttributes) { commitAttributeToXml(uiAttribute, uiAttribute.getCurrentValue()); uiAttribute.setDirty(false); } return result; } AttributeDescriptor descriptor = dirtyAttributes.get(0).getDescriptor(); String firstName = descriptor.getXmlLocalName(); String firstNamePrefix = null; String namespaceUri = descriptor.getNamespaceUri(); if (namespaceUri != null) { firstNamePrefix = XmlUtils.lookupNamespacePrefix(element, namespaceUri); } NamedNodeMap attributes = ((Element) element).getAttributes(); List<Attr> move = new ArrayList<Attr>(); for (int i = 0, n = attributes.getLength(); i < n; i++) { Attr attribute = (Attr) attributes.item(i); if (UiAttributeNode.compareAttributes( attribute.getPrefix(), attribute.getLocalName(), firstNamePrefix, firstName) > 0) { move.add(attribute); } } for (Attr attribute : move) { if (attribute.getNamespaceURI() != null) { attributes.removeNamedItemNS(attribute.getNamespaceURI(), attribute.getLocalName()); } else { attributes.removeNamedItem(attribute.getName()); } } // Merge back the removed DOM attribute nodes and the new UI attribute nodes. // In cases where the attribute DOM name and the UI attribute names equal, // skip the DOM nodes and just apply the UI attributes. int domAttributeIndex = 0; int domAttributeIndexMax = move.size(); int uiAttributeIndex = 0; int uiAttributeIndexMax = dirtyAttributes.size(); while (true) { Attr domAttribute; UiAttributeNode uiAttribute; int compare; if (uiAttributeIndex < uiAttributeIndexMax) { if (domAttributeIndex < domAttributeIndexMax) { domAttribute = move.get(domAttributeIndex); uiAttribute = dirtyAttributes.get(uiAttributeIndex); String domAttributeName = domAttribute.getLocalName(); String uiAttributeName = uiAttribute.getDescriptor().getXmlLocalName(); compare = UiAttributeNode.compareAttributes(domAttributeName, uiAttributeName); } else { compare = 1; uiAttribute = dirtyAttributes.get(uiAttributeIndex); domAttribute = null; } } else if (domAttributeIndex < domAttributeIndexMax) { compare = -1; domAttribute = move.get(domAttributeIndex); uiAttribute = null; } else { break; } if (compare < 0) { if (domAttribute.getNamespaceURI() != null) { attributes.setNamedItemNS(domAttribute); } else { attributes.setNamedItem(domAttribute); } domAttributeIndex++; } else { assert compare >= 0; if (compare == 0) { domAttributeIndex++; } commitAttributeToXml(uiAttribute, uiAttribute.getCurrentValue()); uiAttribute.setDirty(false); uiAttributeIndex++; } } } return result; } /** * Utility method to internally set the value of a text attribute for the current * UiElementNode. * <p/> * This method is a helper. It silently ignores the errors such as the requested * attribute not being present in the element or attribute not being settable. * It accepts inherited attributes (such as layout). * <p/> * This does not commit to the XML model. It does mark the attribute node as dirty. * This is up to the caller. * * @see #commitAttributeToXml(UiAttributeNode, String) * @see #commitDirtyAttributesToXml() * * @param attrXmlName The XML <em>local</em> name of the attribute to modify * @param attrNsUri The namespace URI of the attribute. * Can be null if the attribute uses the global namespace. * @param value The new value for the attribute. If set to null, the attribute is removed. * @param override True if the value must be set even if one already exists. * @return The {@link UiAttributeNode} that has been modified or null. */ public UiAttributeNode setAttributeValue( String attrXmlName, String attrNsUri, String value, boolean override) { if (value == null) { value = ""; //$NON-NLS-1$ -- this removes an attribute } getEditor().scheduleNodeReformat(this, true); // Try with all internal attributes UiAttributeNode uiAttr = setInternalAttrValue( getAllUiAttributes(), attrXmlName, attrNsUri, value, override); if (uiAttr != null) { return uiAttr; } if (uiAttr == null) { // Failed to find the attribute. For non-android attributes that is mostly expected, // in which case we just create a new custom one. As a side effect, we'll find the // attribute descriptor via getAllUiAttributes(). addUnknownAttribute(attrXmlName, attrXmlName, attrNsUri); // We've created the attribute, but not actually set the value on it, so let's do it. // Try with the updated internal attributes. // Implementation detail: we could just do a setCurrentValue + setDirty on the // uiAttr returned by addUnknownAttribute(); however going through setInternalAttrValue // means we won't duplicate the logic, at the expense of doing one more lookup. uiAttr = setInternalAttrValue( getAllUiAttributes(), attrXmlName, attrNsUri, value, override); } return uiAttr; } private UiAttributeNode setInternalAttrValue( Collection<UiAttributeNode> attributes, String attrXmlName, String attrNsUri, String value, boolean override) { // For namespace less attributes (like the "layout" attribute of an <include> tag // we may be passed "" as the namespace (during an attribute copy), and it // should really be null instead. if (attrNsUri != null && attrNsUri.length() == 0) { attrNsUri = null; } for (UiAttributeNode uiAttr : attributes) { AttributeDescriptor uiDesc = uiAttr.getDescriptor(); if (uiDesc.getXmlLocalName().equals(attrXmlName)) { // Both NS URI must be either null or equal. if ((attrNsUri == null && uiDesc.getNamespaceUri() == null) || (attrNsUri != null && attrNsUri.equals(uiDesc.getNamespaceUri()))) { // Not all attributes are editable, ignore those which are not. if (uiAttr instanceof IUiSettableAttributeNode) { String current = uiAttr.getCurrentValue(); // Only update (and mark as dirty) if the attribute did not have any // value or if the value was different. if (override || current == null || !current.equals(value)) { ((IUiSettableAttributeNode) uiAttr).setCurrentValue(value); // mark the attribute as dirty since their internal content // as been modified, but not the underlying XML model uiAttr.setDirty(true); return uiAttr; } } // We found the attribute but it's not settable. Since attributes are // not duplicated, just abandon here. break; } } } return null; } /** * Utility method to retrieve the internal value of an attribute. * <p/> * Note that this retrieves the *field* value if the attribute has some UI, and * not the actual XML value. They may differ if the attribute is dirty. * * @param attrXmlName The XML name of the attribute to modify * @return The current internal value for the attribute or null in case of error. */ public String getAttributeValue(String attrXmlName) { HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); for (Entry<AttributeDescriptor, UiAttributeNode> entry : attributeMap.entrySet()) { AttributeDescriptor uiDesc = entry.getKey(); if (uiDesc.getXmlLocalName().equals(attrXmlName)) { UiAttributeNode uiAttr = entry.getValue(); return uiAttr.getCurrentValue(); } } return null; } // ------ IPropertySource methods @Override public Object getEditableValue() { return null; } /* * (non-Javadoc) * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyDescriptors() * * Returns the property descriptor for this node. Since the descriptors are not linked to the * data, the AttributeDescriptor are used directly. */ @Override public IPropertyDescriptor[] getPropertyDescriptors() { List<IPropertyDescriptor> propDescs = new ArrayList<IPropertyDescriptor>(); // get the standard descriptors HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); Set<AttributeDescriptor> keys = attributeMap.keySet(); // we only want the descriptor that do implement the IPropertyDescriptor interface. for (AttributeDescriptor key : keys) { if (key instanceof IPropertyDescriptor) { propDescs.add((IPropertyDescriptor)key); } } // now get the descriptor from the unknown attributes for (UiAttributeNode unknownNode : mUnknownUiAttributes) { if (unknownNode.getDescriptor() instanceof IPropertyDescriptor) { propDescs.add((IPropertyDescriptor)unknownNode.getDescriptor()); } } // TODO cache this maybe, as it's not going to change (except for unknown descriptors) return propDescs.toArray(new IPropertyDescriptor[propDescs.size()]); } /* * (non-Javadoc) * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyValue(java.lang.Object) * * Returns the value of a given property. The id is the result of IPropertyDescriptor.getId(), * which return the AttributeDescriptor itself. */ @Override public Object getPropertyValue(Object id) { HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); UiAttributeNode attribute = attributeMap.get(id); if (attribute == null) { // look for the id in the unknown attributes. for (UiAttributeNode unknownAttr : mUnknownUiAttributes) { if (id == unknownAttr.getDescriptor()) { return unknownAttr; } } } return attribute; } /* * (non-Javadoc) * @see org.eclipse.ui.views.properties.IPropertySource#isPropertySet(java.lang.Object) * * Returns whether the property is set. In our case this is if the string is non empty. */ @Override public boolean isPropertySet(Object id) { HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); UiAttributeNode attribute = attributeMap.get(id); if (attribute != null) { return attribute.getCurrentValue().length() > 0; } // look for the id in the unknown attributes. for (UiAttributeNode unknownAttr : mUnknownUiAttributes) { if (id == unknownAttr.getDescriptor()) { return unknownAttr.getCurrentValue().length() > 0; } } return false; } /* * (non-Javadoc) * @see org.eclipse.ui.views.properties.IPropertySource#resetPropertyValue(java.lang.Object) * * Reset the property to its default value. For now we simply empty it. */ @Override public void resetPropertyValue(Object id) { HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); UiAttributeNode attribute = attributeMap.get(id); if (attribute != null) { // TODO: reset the value of the attribute return; } // look for the id in the unknown attributes. for (UiAttributeNode unknownAttr : mUnknownUiAttributes) { if (id == unknownAttr.getDescriptor()) { // TODO: reset the value of the attribute return; } } } /* * (non-Javadoc) * @see org.eclipse.ui.views.properties.IPropertySource#setPropertyValue(java.lang.Object, java.lang.Object) * * Set the property value. id is the result of IPropertyDescriptor.getId(), which is the * AttributeDescriptor itself. Value should be a String. */ @Override public void setPropertyValue(Object id, Object value) { HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); UiAttributeNode attribute = attributeMap.get(id); if (attribute == null) { // look for the id in the unknown attributes. for (UiAttributeNode unknownAttr : mUnknownUiAttributes) { if (id == unknownAttr.getDescriptor()) { attribute = unknownAttr; break; } } } if (attribute != null) { // get the current value and compare it to the new value String oldValue = attribute.getCurrentValue(); final String newValue = (String)value; if (oldValue.equals(newValue)) { return; } final UiAttributeNode fAttribute = attribute; AndroidXmlEditor editor = getEditor(); editor.wrapEditXmlModel(new Runnable() { @Override public void run() { commitAttributeToXml(fAttribute, newValue); } }); } } /** * Returns true if this node is an ancestor (parent, grandparent, and so on) * of the given node. Note that a node is not considered an ancestor of * itself. * * @param node the node to test * @return true if this node is an ancestor of the given node */ public boolean isAncestorOf(UiElementNode node) { node = node.getUiParent(); while (node != null) { if (node == this) { return true; } node = node.getUiParent(); } return false; } /** * Finds the nearest common parent of the two given nodes (which could be one of the * two nodes as well) * * @param node1 the first node to test * @param node2 the second node to test * @return the nearest common parent of the two given nodes */ public static UiElementNode getCommonAncestor(UiElementNode node1, UiElementNode node2) { while (node2 != null) { UiElementNode current = node1; while (current != null && current != node2) { current = current.getUiParent(); } if (current == node2) { return current; } node2 = node2.getUiParent(); } return null; } // ---- Global node create/delete Listeners ---- /** List of listeners to be notified of newly created nodes, or null */ private static List<NodeCreationListener> sListeners; /** Notify listeners that a new node has been created */ private void fireNodeCreated(UiElementNode newChild, int index) { // Nothing to do if there aren't any listeners. We don't need to worry about // the case where one thread is firing node changes while another is adding a listener // (in that case it's still okay for this node firing not to be heard) so perform // the check outside of synchronization. if (sListeners == null) { return; } synchronized (UiElementNode.class) { if (sListeners != null) { UiElementNode parent = newChild.getUiParent(); for (NodeCreationListener listener : sListeners) { listener.nodeCreated(parent, newChild, index); } } } } /** Notify listeners that a new node has been deleted */ private void fireNodeDeleted(UiElementNode oldChild, int index) { if (sListeners == null) { return; } synchronized (UiElementNode.class) { if (sListeners != null) { UiElementNode parent = oldChild.getUiParent(); for (NodeCreationListener listener : sListeners) { listener.nodeDeleted(parent, oldChild, index); } } } } /** * Adds a {@link NodeCreationListener} to be notified when new nodes are created * * @param listener the listener to be notified */ public static void addNodeCreationListener(NodeCreationListener listener) { synchronized (UiElementNode.class) { if (sListeners == null) { sListeners = new ArrayList<NodeCreationListener>(1); } sListeners.add(listener); } } /** * Removes a {@link NodeCreationListener} from the set of listeners such that it is * no longer notified when nodes are created. * * @param listener the listener to be removed from the notification list */ public static void removeNodeCreationListener(NodeCreationListener listener) { synchronized (UiElementNode.class) { sListeners.remove(listener); if (sListeners.size() == 0) { sListeners = null; } } } /** Interface implemented by listeners to be notified of newly created nodes */ public interface NodeCreationListener { /** * Called when a new child node is created and added to the given parent * * @param parent the parent of the created node * @param child the newly node * @param index the index among the siblings of the child <b>after</b> * insertion */ void nodeCreated(UiElementNode parent, UiElementNode child, int index); /** * Called when a child node is removed from the given parent * * @param parent the parent of the removed node * @param child the removed node * @param previousIndex the index among the siblings of the child * <b>before</b> removal */ void nodeDeleted(UiElementNode parent, UiElementNode child, int previousIndex); } }