aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiSettableAttributeNode.java32
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiUpdateListener.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAbstractTextAttributeNode.java120
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java174
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiDocumentNode.java160
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java2160
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiFlagAttributeNode.java310
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiListAttributeNode.java220
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java523
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiSeparatorAttributeNode.java146
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextAttributeNode.java196
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextValueNode.java118
12 files changed, 4206 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiSettableAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiSettableAttributeNode.java
new file mode 100644
index 000000000..dd908ad7b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiSettableAttributeNode.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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;
+
+/**
+ * This interface decoration indicates that a given UiAttributeNode can both
+ * set and get its current value.
+ */
+public interface IUiSettableAttributeNode {
+
+ /** Returns the current value of the node. */
+ public String getCurrentValue();
+
+ /** Sets the current value of the node. Cannot be null (use an empty string). */
+ public void setCurrentValue(String value);
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiUpdateListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiUpdateListener.java
new file mode 100644
index 000000000..a4f1f74ea
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiUpdateListener.java
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+
+/**
+ * Listen to update notifications in UI nodes.
+ */
+public interface IUiUpdateListener {
+
+ /** Update state of the UI node */
+ public enum UiUpdateState {
+ /** The node's attributes have been updated. They may or may not actually have changed. */
+ ATTR_UPDATED,
+ /** The node sub-structure (i.e. child nodes) has changed */
+ CHILDREN_CHANGED,
+ /** The XML counterpart for the UI node has just been created. */
+ CREATED,
+ /** The XML counterpart for the UI node has just been deleted.
+ * Note that mandatory UI nodes are never actually deleted. */
+ DELETED
+ }
+
+ /**
+ * Indicates that an UiElementNode has been updated.
+ * <p/>
+ * This happens when an {@link UiElementNode} is refreshed to match the
+ * XML model. The actual UI element node may or may not have changed.
+ *
+ * @param ui_node The {@link UiElementNode} being updated.
+ */
+ public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state);
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAbstractTextAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAbstractTextAttributeNode.java
new file mode 100644
index 000000000..4f795904d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAbstractTextAttributeNode.java
@@ -0,0 +1,120 @@
+/*
+ * 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 com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+
+import org.w3c.dom.Node;
+
+/**
+ * Represents an XML attribute in that can be modified using a simple text field
+ * in the XML editor's user interface.
+ * <p/>
+ * The XML attribute has no default value. When unset, the text field is blank.
+ * When updating the XML, if the field is empty, the attribute will be removed
+ * from the XML element.
+ * <p/>
+ * See {@link UiAttributeNode} for more information.
+ */
+public abstract class UiAbstractTextAttributeNode extends UiAttributeNode
+ implements IUiSettableAttributeNode {
+
+ protected static final String DEFAULT_VALUE = ""; //$NON-NLS-1$
+
+ /** Prevent internal listener from firing when internally modifying the text */
+ private boolean mInternalTextModification;
+ /** Last value read from the XML model. Cannot be null. */
+ private String mCurrentValue = DEFAULT_VALUE;
+
+ public UiAbstractTextAttributeNode(AttributeDescriptor attributeDescriptor,
+ UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /** Returns the current value of the node. */
+ @Override
+ public final String getCurrentValue() {
+ return mCurrentValue;
+ }
+
+ /** Sets the current value of the node. Cannot be null (use an empty string). */
+ @Override
+ public final void setCurrentValue(String value) {
+ mCurrentValue = value;
+ }
+
+ /** Returns if the attribute node is valid, and its UI has been created. */
+ public abstract boolean isValid();
+
+ /** Returns the text value present in the UI. */
+ public abstract String getTextWidgetValue();
+
+ /** Sets the text value to be displayed in the UI. */
+ public abstract void setTextWidgetValue(String value);
+
+
+ /**
+ * Updates the current text field's value when the XML has changed.
+ * <p/>
+ * The caller doesn't really know if attributes have changed,
+ * so it will call this to refresh the attribute anyway. The value
+ * is only set if it has changed.
+ * <p/>
+ * This also resets the "dirty" flag.
+ */
+ @Override
+ public void updateValue(Node xml_attribute_node) {
+ mCurrentValue = DEFAULT_VALUE;
+ if (xml_attribute_node != null) {
+ mCurrentValue = xml_attribute_node.getNodeValue();
+ }
+
+ if (isValid() && !getTextWidgetValue().equals(mCurrentValue)) {
+ try {
+ mInternalTextModification = true;
+ setTextWidgetValue(mCurrentValue);
+ setDirty(false);
+ } finally {
+ mInternalTextModification = false;
+ }
+ }
+ }
+
+ /* (non-java doc)
+ * Called by the user interface when the editor is saved or its state changed
+ * and the modified attributes must be committed (i.e. written) to the XML model.
+ */
+ @Override
+ public void commit() {
+ UiElementNode parent = getUiParent();
+ if (parent != null && isValid() && isDirty()) {
+ String value = getTextWidgetValue();
+ if (parent.commitAttributeToXml(this, value)) {
+ mCurrentValue = value;
+ setDirty(false);
+ }
+ }
+ }
+
+ protected final boolean isInInternalTextModification() {
+ return mInternalTextModification;
+ }
+
+ protected final void setInInternalTextModification(boolean internalTextModification) {
+ mInternalTextModification = internalTextModification;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java
new file mode 100644
index 000000000..ffe637c5d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java
@@ -0,0 +1,174 @@
+/*
+ * 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 com.android.ide.common.xml.XmlAttributeSortOrder;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.forms.IManagedForm;
+import org.w3c.dom.Node;
+
+/**
+ * Represents an XML attribute that can be modified by the XML editor's user interface.
+ * <p/>
+ * The characteristics of an {@link UiAttributeNode} are declared by a
+ * corresponding {@link AttributeDescriptor}.
+ * <p/>
+ * This is an abstract class. Derived classes must implement the creation of the UI
+ * and manage its synchronization with the XML.
+ */
+public abstract class UiAttributeNode implements Comparable<UiAttributeNode> {
+
+ private AttributeDescriptor mDescriptor;
+ private UiElementNode mUiParent;
+ private boolean mIsDirty;
+ private boolean mHasError;
+
+ /** Creates a new {@link UiAttributeNode} linked to a specific {@link AttributeDescriptor}
+ * and the corresponding runtime {@link UiElementNode} parent. */
+ public UiAttributeNode(AttributeDescriptor attributeDescriptor, UiElementNode uiParent) {
+ mDescriptor = attributeDescriptor;
+ mUiParent = uiParent;
+ }
+
+ /** Returns the {@link AttributeDescriptor} specific to this UI attribute node */
+ public final AttributeDescriptor getDescriptor() {
+ return mDescriptor;
+ }
+
+ /** Returns the {@link UiElementNode} that owns this {@link UiAttributeNode} */
+ public final UiElementNode getUiParent() {
+ return mUiParent;
+ }
+
+ /** Returns the current value of the node. */
+ public abstract String getCurrentValue();
+
+ /**
+ * @return True if the attribute has been changed since it was last loaded
+ * from the XML model.
+ */
+ public final boolean isDirty() {
+ return mIsDirty;
+ }
+
+ /**
+ * Sets whether the attribute is dirty and also notifies the editor some part's dirty
+ * flag as changed.
+ * <p/>
+ * Subclasses should set the to true as a result of user interaction with the widgets in
+ * the section and then should set to false when the commit() method completed.
+ *
+ * @param isDirty the new value to set the dirty-flag to
+ */
+ public void setDirty(boolean isDirty) {
+ boolean wasDirty = mIsDirty;
+ mIsDirty = isDirty;
+ // TODO: for unknown attributes, getParent() != null && getParent().getEditor() != null
+ if (wasDirty != isDirty) {
+ AndroidXmlEditor editor = getUiParent().getEditor();
+ if (editor != null) {
+ editor.editorDirtyStateChanged();
+ }
+ }
+ }
+
+ /**
+ * Sets the error flag value.
+ * @param errorFlag the error flag
+ */
+ public final void setHasError(boolean errorFlag) {
+ mHasError = errorFlag;
+ }
+
+ /**
+ * Returns whether this node has errors.
+ */
+ public final boolean hasError() {
+ return mHasError;
+ }
+
+ /**
+ * Called once by the parent user interface to creates the necessary
+ * user interface to edit this attribute.
+ * <p/>
+ * This method can be called more than once in the life cycle of an UI node,
+ * typically when the UI is part of a master-detail tree, as pages are swapped.
+ *
+ * @param parent The composite where to create the user interface.
+ * @param managedForm The managed form owning this part.
+ */
+ public abstract void createUiControl(Composite parent, IManagedForm managedForm);
+
+ /**
+ * Used to get a list of all possible values for this UI attribute.
+ * <p/>
+ * This is used, among other things, by the XML Content Assists to complete values
+ * for an attribute.
+ * <p/>
+ * Implementations that do not have any known values should return null.
+ *
+ * @param prefix An optional prefix string, which is whatever the user has already started
+ * typing. Can be null or an empty string. The implementation can use this to filter choices
+ * and only return strings that match this prefix. A lazy or default implementation can
+ * simply ignore this and return everything.
+ * @return A list of possible completion values, and empty array or null.
+ */
+ public abstract String[] getPossibleValues(String prefix);
+
+ /**
+ * Called when the XML is being loaded or has changed to
+ * update the value held by this user interface attribute node.
+ * <p/>
+ * The XML Node <em>may</em> be null, which denotes that the attribute is not
+ * specified in the XML model. In general, this means the "default" value of the
+ * attribute should be used.
+ * <p/>
+ * The caller doesn't really know if attributes have changed,
+ * so it will call this to refresh the attribute anyway. It's up to the
+ * UI implementation to minimize refreshes.
+ *
+ * @param node the node to read the value from
+ */
+ public abstract void updateValue(Node node);
+
+ /**
+ * Called by the user interface when the editor is saved or its state changed
+ * and the modified attributes must be committed (i.e. written) to the XML model.
+ * <p/>
+ * Important behaviors:
+ * <ul>
+ * <li>The caller *must* have called IStructuredModel.aboutToChangeModel before.
+ * The implemented methods must assume it is safe to modify the XML model.
+ * <li>On success, the implementation *must* call setDirty(false).
+ * <li>On failure, the implementation can fail with an exception, which
+ * is trapped and logged by the caller, or do nothing, whichever is more
+ * appropriate.
+ * </ul>
+ */
+ public abstract void commit();
+
+ // ---- Implements Comparable ----
+
+ @Override
+ public int compareTo(UiAttributeNode o) {
+ return XmlAttributeSortOrder.compareAttributes(mDescriptor.getXmlLocalName(),
+ o.mDescriptor.getXmlLocalName());
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiDocumentNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiDocumentNode.java
new file mode 100644
index 000000000..1a85ea682
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiDocumentNode.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2008 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 com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener.UiUpdateState;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents an XML document node that can be modified by the user interface in the XML editor.
+ * <p/>
+ * The structure of a given {@link UiDocumentNode} is declared by a corresponding
+ * {@link DocumentDescriptor}.
+ */
+public class UiDocumentNode extends UiElementNode {
+
+ /**
+ * Creates a new {@link UiDocumentNode} described by a given {@link DocumentDescriptor}.
+ *
+ * @param documentDescriptor The {@link DocumentDescriptor} for the XML node. Cannot be null.
+ */
+ public UiDocumentNode(DocumentDescriptor documentDescriptor) {
+ super(documentDescriptor);
+ }
+
+ /**
+ * 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.
+ *
+ * @return A short string describing the UI node suitable for tree views.
+ */
+ @Override
+ public String getShortDescription() {
+ return "Document"; //$NON-NLS-1$
+ }
+
+ /**
+ * Computes a "breadcrumb trail" description for this node.
+ *
+ * @param include_root 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.
+ */
+ @Override
+ public String getBreadcrumbTrailDescription(boolean include_root) {
+ return "Document"; //$NON-NLS-1$
+ }
+
+ /**
+ * This method throws an exception when attempted to assign a parent, since XML documents
+ * cannot have a parent. It is OK to assign null.
+ */
+ @Override
+ protected void setUiParent(UiElementNode parent) {
+ if (parent != null) {
+ // DEBUG. Change to log warning.
+ throw new UnsupportedOperationException("Documents can't have UI parents"); //$NON-NLS-1$
+ }
+ super.setUiParent(null);
+ }
+
+ /**
+ * 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 xml_node The XML node to mirror
+ * @return Returns true if the XML structure has changed (nodes added, removed or replaced)
+ */
+ @Override
+ public boolean loadFromXmlNode(Node xml_node) {
+ boolean structure_changed = (getXmlDocument() != xml_node);
+ setXmlDocument((Document) xml_node);
+ structure_changed |= super.loadFromXmlNode(xml_node);
+ if (structure_changed) {
+ invokeUiUpdateListeners(UiUpdateState.CHILDREN_CHANGED);
+ }
+ return structure_changed;
+ }
+
+ /**
+ * This method throws an exception if there is no underlying XML document.
+ * <p/>
+ * XML documents cannot be created per se -- they are a by-product of the StructuredEditor
+ * XML parser.
+ *
+ * @return The current value of getXmlDocument().
+ */
+ @Override
+ public Node createXmlNode() {
+ if (getXmlDocument() == null) {
+ // By design, a document node cannot be created, it is owned by the XML parser.
+ // By "design" this should never happen since the XML parser always creates an XML
+ // document container, even for an empty file.
+ throw new UnsupportedOperationException("Documents cannot be created"); //$NON-NLS-1$
+ }
+ return getXmlDocument();
+ }
+
+ /**
+ * This method throws an exception and does not even try to delete the XML document.
+ * <p/>
+ * XML documents cannot be deleted per se -- they are a by-product of the StructuredEditor
+ * XML parser.
+ *
+ * @return The removed node or null if it didn't exist in the first place.
+ */
+ @Override
+ public Node deleteXmlNode() {
+ // DEBUG. Change to log warning.
+ throw new UnsupportedOperationException("Documents cannot be deleted"); //$NON-NLS-1$
+ }
+
+ /**
+ * Returns all elements in this document.
+ *
+ * @param document the document
+ * @return all elements in the document
+ */
+ public static List<UiElementNode> getAllElements(UiDocumentNode document) {
+ List<UiElementNode> elements = new ArrayList<UiElementNode>(64);
+ for (UiElementNode child : document.getUiChildren()) {
+ addElements(child, elements);
+ }
+ return elements;
+ }
+
+ private static void addElements(UiElementNode node, List<UiElementNode> elements) {
+ elements.add(node);
+
+ for (UiElementNode child : node.getUiChildren()) {
+ addElements(child, elements);
+ }
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java
new file mode 100644
index 000000000..ed447c634
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java
@@ -0,0 +1,2160 @@
+/*
+ * 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.common.xml.XmlAttributeSortOrder;
+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 (XmlAttributeSortOrder.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 = XmlAttributeSortOrder.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);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiFlagAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiFlagAttributeNode.java
new file mode 100644
index 000000000..13fcdb6b2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiFlagAttributeNode.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2008 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 com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.FlagAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.resource.FontDescriptor;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.dialogs.SelectionStatusDialog;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Represents an XML attribute that is defined by a set of flag values,
+ * i.e. enum names separated by pipe (|) characters.
+ *
+ * Note: in Android resources, a "flag" is a list of fixed values where one or
+ * more values can be selected using an "or", e.g. "align='left|top'".
+ * By contrast, an "enum" is a list of fixed values of which only one can be
+ * selected at a given time, e.g. "gravity='right'".
+ * <p/>
+ * This class handles the "flag" case.
+ * The "enum" case is done using {@link UiListAttributeNode}.
+ */
+public class UiFlagAttributeNode extends UiTextAttributeNode {
+
+ public UiFlagAttributeNode(FlagAttributeDescriptor attributeDescriptor,
+ UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /* (non-java doc)
+ * Creates a label widget and an associated text field.
+ * <p/>
+ * As most other parts of the android manifest editor, this assumes the
+ * parent uses a table layout with 2 columns.
+ */
+ @Override
+ public void createUiControl(Composite parent, IManagedForm managedForm) {
+ setManagedForm(managedForm);
+ FormToolkit toolkit = managedForm.getToolkit();
+ TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
+
+ Label label = toolkit.createLabel(parent, desc.getUiName());
+ label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
+ SectionHelper.addControlTooltip(label, DescriptorsUtils.formatTooltip(desc.getTooltip()));
+
+ Composite composite = toolkit.createComposite(parent);
+ composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
+ GridLayout gl = new GridLayout(2, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ composite.setLayout(gl);
+ // Fixes missing text borders under GTK... also requires adding a 1-pixel margin
+ // for the text field below
+ toolkit.paintBordersFor(composite);
+
+ final Text text = toolkit.createText(composite, getCurrentValue());
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK
+ text.setLayoutData(gd);
+ final Button selectButton = toolkit.createButton(composite, "Select...", SWT.PUSH);
+
+ setTextWidget(text);
+
+ selectButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+
+ String currentText = getTextWidgetValue();
+
+ String result = showDialog(selectButton.getShell(), currentText);
+
+ if (result != null) {
+ setTextWidgetValue(result);
+ }
+ }
+ });
+ }
+
+ /**
+ * Get the flag names, either from the initial names set in the attribute
+ * or by querying the framework resource parser.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public String[] getPossibleValues(String prefix) {
+ String attr_name = getDescriptor().getXmlLocalName();
+ String element_name = getUiParent().getDescriptor().getXmlName();
+
+ String[] values = null;
+
+ if (getDescriptor() instanceof FlagAttributeDescriptor &&
+ ((FlagAttributeDescriptor) getDescriptor()).getNames() != null) {
+ // Get enum values from the descriptor
+ values = ((FlagAttributeDescriptor) getDescriptor()).getNames();
+ }
+
+ if (values == null) {
+ // or from the AndroidTargetData
+ UiElementNode uiNode = getUiParent();
+ AndroidXmlEditor editor = uiNode.getEditor();
+ AndroidTargetData data = editor.getTargetData();
+ if (data != null) {
+ values = data.getAttributeValues(element_name, attr_name);
+ }
+ }
+
+ return values;
+ }
+
+ /**
+ * Shows a dialog letting the user choose a set of enum, and returns a string
+ * containing the result.
+ */
+ public String showDialog(Shell shell, String currentValue) {
+ FlagSelectionDialog dlg = new FlagSelectionDialog(
+ shell, currentValue.trim().split("\\s*\\|\\s*")); //$NON-NLS-1$
+ dlg.open();
+ Object[] result = dlg.getResult();
+ if (result != null) {
+ StringBuilder buf = new StringBuilder();
+ for (Object name : result) {
+ if (name instanceof String) {
+ if (buf.length() > 0) {
+ buf.append('|');
+ }
+ buf.append(name);
+ }
+ }
+
+ return buf.toString();
+ }
+
+ return null;
+
+ }
+
+ /**
+ * Displays a list of flag names with checkboxes.
+ */
+ private class FlagSelectionDialog extends SelectionStatusDialog {
+
+ private Set<String> mCurrentSet;
+ private Table mTable;
+
+ public FlagSelectionDialog(Shell parentShell, String[] currentNames) {
+ super(parentShell);
+
+ mCurrentSet = new HashSet<String>();
+ for (String name : currentNames) {
+ if (name.length() > 0) {
+ mCurrentSet.add(name);
+ }
+ }
+
+ int shellStyle = getShellStyle();
+ setShellStyle(shellStyle | SWT.MAX | SWT.RESIZE);
+ }
+
+ @Override
+ protected void computeResult() {
+ if (mTable != null) {
+ ArrayList<String> results = new ArrayList<String>();
+
+ for (TableItem item : mTable.getItems()) {
+ if (item.getChecked()) {
+ results.add((String)item.getData());
+ }
+ }
+
+ setResult(results);
+ }
+ }
+
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ Composite composite= new Composite(parent, SWT.NONE);
+ composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+ composite.setLayout(new GridLayout(1, true));
+ composite.setFont(parent.getFont());
+
+ Label label = new Label(composite, SWT.NONE);
+ label.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+ label.setText(String.format("Select the flag values for attribute %1$s:",
+ ((FlagAttributeDescriptor) getDescriptor()).getUiName()));
+
+ mTable = new Table(composite, SWT.CHECK | SWT.BORDER);
+ GridData data = new GridData();
+ // The 60,18 hints are the ones used by AbstractElementListSelectionDialog
+ data.widthHint = convertWidthInCharsToPixels(60);
+ data.heightHint = convertHeightInCharsToPixels(18);
+ data.grabExcessVerticalSpace = true;
+ data.grabExcessHorizontalSpace = true;
+ data.horizontalAlignment = GridData.FILL;
+ data.verticalAlignment = GridData.FILL;
+ mTable.setLayoutData(data);
+
+ mTable.setHeaderVisible(false);
+ final TableColumn column = new TableColumn(mTable, SWT.NONE);
+
+ // List all the expected flag names and check those which are currently used
+ String[] names = getPossibleValues(null);
+ if (names != null) {
+ for (String name : names) {
+ TableItem item = new TableItem(mTable, SWT.NONE);
+ item.setText(name);
+ item.setData(name);
+
+ boolean hasName = mCurrentSet.contains(name);
+ item.setChecked(hasName);
+ if (hasName) {
+ mCurrentSet.remove(name);
+ }
+ }
+ }
+
+ // If there are unknown flag names currently used, display them at the end if the
+ // table already checked.
+ if (!mCurrentSet.isEmpty()) {
+ FontDescriptor fontDesc = JFaceResources.getDialogFontDescriptor();
+ fontDesc = fontDesc.withStyle(SWT.ITALIC);
+ Font font = fontDesc.createFont(JFaceResources.getDialogFont().getDevice());
+
+ for (String name : mCurrentSet) {
+ TableItem item = new TableItem(mTable, SWT.NONE);
+ item.setText(String.format("%1$s (unknown flag)", name));
+ item.setData(name);
+ item.setChecked(true);
+ item.setFont(font);
+ }
+ }
+
+ // Add a listener that will resize the column to the full width of the table
+ // so that only one column appears in the table even if the dialog is resized.
+ ControlAdapter listener = new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Rectangle r = mTable.getClientArea();
+ column.setWidth(r.width);
+ }
+ };
+
+ mTable.addControlListener(listener);
+ listener.controlResized(null /* event not used */);
+
+ // Add a selection listener that will check/uncheck items when they are double-clicked
+ mTable.addSelectionListener(new SelectionAdapter() {
+ /** Default selection means double-click on "most" platforms */
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ if (e.item instanceof TableItem) {
+ TableItem i = (TableItem) e.item;
+ i.setChecked(!i.getChecked());
+ }
+ super.widgetDefaultSelected(e);
+ }
+ });
+
+ Dialog.applyDialogFont(composite);
+ setHelpAvailable(false);
+
+ return composite;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiListAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiListAttributeNode.java
new file mode 100644
index 000000000..0fd317c1c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiListAttributeNode.java
@@ -0,0 +1,220 @@
+/*
+ * 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 com.android.SdkConstants;
+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.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ListAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+
+/**
+ * Represents an XML attribute which has possible built-in values, and can be modified by
+ * an editable Combo box.
+ * <p/>
+ * See {@link UiTextAttributeNode} for more information.
+ */
+public class UiListAttributeNode extends UiAbstractTextAttributeNode {
+
+ protected Combo mCombo;
+
+ public UiListAttributeNode(ListAttributeDescriptor attributeDescriptor,
+ UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /* (non-java doc)
+ * Creates a label widget and an associated text field.
+ * <p/>
+ * As most other parts of the android manifest editor, this assumes the
+ * parent uses a table layout with 2 columns.
+ */
+ @Override
+ public final void createUiControl(final Composite parent, IManagedForm managedForm) {
+ FormToolkit toolkit = managedForm.getToolkit();
+ TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
+
+ Label label = toolkit.createLabel(parent, desc.getUiName());
+ label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
+ SectionHelper.addControlTooltip(label, DescriptorsUtils.formatTooltip(desc.getTooltip()));
+
+ int style = SWT.DROP_DOWN;
+ mCombo = new Combo(parent, style);
+ TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE);
+ twd.maxWidth = 100;
+ mCombo.setLayoutData(twd);
+
+ fillCombo();
+
+ setTextWidgetValue(getCurrentValue());
+
+ mCombo.addModifyListener(new ModifyListener() {
+ /**
+ * Sent when the text is modified, whether by the user via manual
+ * input or programmatic input via setText().
+ */
+ @Override
+ public void modifyText(ModifyEvent e) {
+ onComboChange();
+ }
+ });
+
+ mCombo.addSelectionListener(new SelectionAdapter() {
+ /** Sent when the text is changed from a list selection. */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ onComboChange();
+ }
+ });
+
+ // Remove self-reference when the widget is disposed
+ mCombo.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ mCombo = null;
+ }
+ });
+ }
+
+ protected void fillCombo() {
+ String[] values = getPossibleValues(null);
+
+ if (values == null) {
+ AdtPlugin.log(IStatus.ERROR,
+ "FrameworkResourceManager did not provide values yet for %1$s",
+ getDescriptor().getXmlLocalName());
+ } else {
+ for (String value : values) {
+ mCombo.add(value);
+ }
+ }
+ }
+
+ /**
+ * Get the list values, either from the initial values set in the attribute
+ * or by querying the framework resource parser.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public String[] getPossibleValues(String prefix) {
+ AttributeDescriptor descriptor = getDescriptor();
+ UiElementNode uiParent = getUiParent();
+
+ String attr_name = descriptor.getXmlLocalName();
+ String element_name = uiParent.getDescriptor().getXmlName();
+
+ // FrameworkResourceManager expects a specific prefix for the attribute.
+ String nsPrefix = "";
+ if (SdkConstants.NS_RESOURCES.equals(descriptor.getNamespaceUri())) {
+ nsPrefix = SdkConstants.ANDROID_NS_NAME + ':';
+ } else if (SdkConstants.XMLNS_URI.equals(descriptor.getNamespaceUri())) {
+ nsPrefix = SdkConstants.XMLNS_PREFIX;
+ }
+ attr_name = nsPrefix + attr_name;
+
+ String[] values = null;
+
+ if (descriptor instanceof ListAttributeDescriptor &&
+ ((ListAttributeDescriptor) descriptor).getValues() != null) {
+ // Get enum values from the descriptor
+ values = ((ListAttributeDescriptor) descriptor).getValues();
+ }
+
+ if (values == null) {
+ // or from the AndroidTargetData
+ UiElementNode uiNode = getUiParent();
+ AndroidXmlEditor editor = uiNode.getEditor();
+ AndroidTargetData data = editor.getTargetData();
+ if (data != null) {
+ // get the great-grand-parent descriptor.
+
+ // the parent should always exist.
+ UiElementNode grandParentNode = uiParent.getUiParent();
+
+ String greatGrandParentNodeName = null;
+ if (grandParentNode != null) {
+ UiElementNode greatGrandParentNode = grandParentNode.getUiParent();
+ if (greatGrandParentNode != null) {
+ greatGrandParentNodeName =
+ greatGrandParentNode.getDescriptor().getXmlName();
+ }
+ }
+
+ values = data.getAttributeValues(element_name, attr_name, greatGrandParentNodeName);
+ }
+ }
+
+ return values;
+ }
+
+ @Override
+ public String getTextWidgetValue() {
+ if (mCombo != null) {
+ return mCombo.getText();
+ }
+
+ return null;
+ }
+
+ @Override
+ public final boolean isValid() {
+ return mCombo != null;
+ }
+
+ @Override
+ public void setTextWidgetValue(String value) {
+ if (mCombo != null) {
+ mCombo.setText(value);
+ }
+ }
+
+ /**
+ * Handles Combo change, either from text edit or from selection change.
+ * <p/>
+ * Simply mark the attribute as dirty if it really changed.
+ * The container SectionPart will collect these flag and manage them.
+ */
+ private void onComboChange() {
+ if (!isInInternalTextModification() &&
+ !isDirty() &&
+ mCombo != null &&
+ getCurrentValue() != null &&
+ !mCombo.getText().equals(getCurrentValue())) {
+ setDirty(true);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java
new file mode 100644
index 000000000..eb51d3f86
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java
@@ -0,0 +1,523 @@
+/*
+ * 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;
+import static com.android.SdkConstants.ANDROID_PREFIX;
+import static com.android.SdkConstants.ANDROID_THEME_PREFIX;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT;
+import static com.android.SdkConstants.ATTR_STYLE;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.PREFIX_THEME_REF;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.common.resources.ResourceItem;
+import com.android.ide.common.resources.ResourceRepository;
+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.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.internal.ui.ReferenceChooserDialog;
+import com.android.ide.eclipse.adt.internal.ui.ResourceChooser;
+import com.android.resources.ResourceType;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Represents an XML attribute for a resource that can be modified using a simple text field or
+ * a dialog to choose an existing resource.
+ * <p/>
+ * It can be configured to represent any kind of resource, by providing the desired
+ * {@link ResourceType} in the constructor.
+ * <p/>
+ * See {@link UiTextAttributeNode} for more information.
+ */
+public class UiResourceAttributeNode extends UiTextAttributeNode {
+ private ResourceType mType;
+
+ /**
+ * Creates a new {@linkplain UiResourceAttributeNode}
+ *
+ * @param type the associated resource type
+ * @param attributeDescriptor the attribute descriptor for this attribute
+ * @param uiParent the parent ui node, if any
+ */
+ public UiResourceAttributeNode(ResourceType type,
+ AttributeDescriptor attributeDescriptor, UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+
+ mType = type;
+ }
+
+ /* (non-java doc)
+ * Creates a label widget and an associated text field.
+ * <p/>
+ * As most other parts of the android manifest editor, this assumes the
+ * parent uses a table layout with 2 columns.
+ */
+ @Override
+ public void createUiControl(final Composite parent, IManagedForm managedForm) {
+ setManagedForm(managedForm);
+ FormToolkit toolkit = managedForm.getToolkit();
+ TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
+
+ Label label = toolkit.createLabel(parent, desc.getUiName());
+ label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
+ SectionHelper.addControlTooltip(label, DescriptorsUtils.formatTooltip(desc.getTooltip()));
+
+ Composite composite = toolkit.createComposite(parent);
+ composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
+ GridLayout gl = new GridLayout(2, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ composite.setLayout(gl);
+ // Fixes missing text borders under GTK... also requires adding a 1-pixel margin
+ // for the text field below
+ toolkit.paintBordersFor(composite);
+
+ final Text text = toolkit.createText(composite, getCurrentValue());
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK
+ text.setLayoutData(gd);
+ Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH);
+
+ setTextWidget(text);
+
+ // TODO Add a validator using onAddModifyListener
+
+ browseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ String result = showDialog(parent.getShell(), text.getText().trim());
+ if (result != null) {
+ text.setText(result);
+ }
+ }
+ });
+ }
+
+ /**
+ * Shows a dialog letting the user choose a set of enum, and returns a
+ * string containing the result.
+ *
+ * @param shell the parent shell
+ * @param currentValue an initial value, if any
+ * @return the chosen string, or null
+ */
+ @Nullable
+ public String showDialog(@NonNull Shell shell, @Nullable String currentValue) {
+ // we need to get the project of the file being edited.
+ UiElementNode uiNode = getUiParent();
+ AndroidXmlEditor editor = uiNode.getEditor();
+ IProject project = editor.getProject();
+ if (project != null) {
+ // get the resource repository for this project and the system resources.
+ ResourceRepository projectRepository =
+ ResourceManager.getInstance().getProjectResources(project);
+
+ if (mType != null) {
+ // get the Target Data to get the system resources
+ AndroidTargetData data = editor.getTargetData();
+ ResourceChooser dlg = ResourceChooser.create(project, mType, data, shell)
+ .setCurrentResource(currentValue);
+ if (dlg.open() == Window.OK) {
+ return dlg.getCurrentResource();
+ }
+ } else {
+ ReferenceChooserDialog dlg = new ReferenceChooserDialog(
+ project,
+ projectRepository,
+ shell);
+
+ dlg.setCurrentResource(currentValue);
+
+ if (dlg.open() == Window.OK) {
+ return dlg.getCurrentResource();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets all the values one could use to auto-complete a "resource" value in an XML
+ * content assist.
+ * <p/>
+ * Typically the user is editing the value of an attribute in a resource XML, e.g.
+ * <pre> "&lt;Button android:test="@string/my_[caret]_string..." </pre>
+ * <p/>
+ *
+ * "prefix" is the value that the user has typed so far (or more exactly whatever is on the
+ * left side of the insertion point). In the example above it would be "@style/my_".
+ * <p/>
+ *
+ * To avoid a huge long list of values, the completion works on two levels:
+ * <ul>
+ * <li> If a resource type as been typed so far (e.g. "@style/"), then limit the values to
+ * the possible completions that match this type.
+ * <li> If no resource type as been typed so far, then return the various types that could be
+ * completed. So if the project has only strings and layouts resources, for example,
+ * the returned list will only include "@string/" and "@layout/".
+ * </ul>
+ *
+ * Finally if anywhere in the string we find the special token "android:", we use the
+ * current framework system resources rather than the project resources.
+ * This works for both "@android:style/foo" and "@style/android:foo" conventions even though
+ * the reconstructed name will always be of the former form.
+ *
+ * Note that "android:" here is a keyword specific to Android resources and should not be
+ * mixed with an XML namespace for an XML attribute name.
+ */
+ @Override
+ public String[] getPossibleValues(String prefix) {
+ return computeResourceStringMatches(getUiParent().getEditor(), getDescriptor(), prefix);
+ }
+
+ /**
+ * Computes the set of resource string matches for a given resource prefix in a given editor
+ *
+ * @param editor the editor context
+ * @param descriptor the attribute descriptor, if any
+ * @param prefix the prefix, if any
+ * @return an array of resource string matches
+ */
+ @Nullable
+ public static String[] computeResourceStringMatches(
+ @NonNull AndroidXmlEditor editor,
+ @Nullable AttributeDescriptor descriptor,
+ @Nullable String prefix) {
+
+ if (prefix == null || !prefix.regionMatches(1, ANDROID_PKG, 0, ANDROID_PKG.length())) {
+ IProject project = editor.getProject();
+ if (project != null) {
+ // get the resource repository for this project and the system resources.
+ ResourceManager resourceManager = ResourceManager.getInstance();
+ ResourceRepository repository = resourceManager.getProjectResources(project);
+
+ List<IProject> libraries = null;
+ ProjectState projectState = Sdk.getProjectState(project);
+ if (projectState != null) {
+ libraries = projectState.getFullLibraryProjects();
+ }
+
+ String[] projectMatches = computeResourceStringMatches(descriptor, prefix,
+ repository, false);
+
+ if (libraries == null || libraries.isEmpty()) {
+ return projectMatches;
+ }
+
+ // Also compute matches for each of the libraries, and combine them
+ Set<String> matches = new HashSet<String>(200);
+ for (String s : projectMatches) {
+ matches.add(s);
+ }
+
+ for (IProject library : libraries) {
+ repository = resourceManager.getProjectResources(library);
+ projectMatches = computeResourceStringMatches(descriptor, prefix,
+ repository, false);
+ for (String s : projectMatches) {
+ matches.add(s);
+ }
+ }
+
+ String[] sorted = matches.toArray(new String[matches.size()]);
+ Arrays.sort(sorted);
+ return sorted;
+ }
+ } else {
+ // If there's a prefix with "android:" in it, use the system resources
+ // Non-public framework resources are filtered out later.
+ AndroidTargetData data = editor.getTargetData();
+ if (data != null) {
+ ResourceRepository repository = data.getFrameworkResources();
+ return computeResourceStringMatches(descriptor, prefix, repository, true);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Computes the set of resource string matches for a given prefix and a
+ * given resource repository
+ *
+ * @param attributeDescriptor the attribute descriptor, if any
+ * @param prefix the prefix, if any
+ * @param repository the repository to seaerch in
+ * @param isSystem if true, the repository contains framework repository,
+ * otherwise it contains project repositories
+ * @return an array of resource string matches
+ */
+ @NonNull
+ public static String[] computeResourceStringMatches(
+ @Nullable AttributeDescriptor attributeDescriptor,
+ @Nullable String prefix,
+ @NonNull ResourceRepository repository,
+ boolean isSystem) {
+ // Get list of potential resource types, either specific to this project
+ // or the generic list.
+ Collection<ResourceType> resTypes = (repository != null) ?
+ repository.getAvailableResourceTypes() :
+ EnumSet.allOf(ResourceType.class);
+
+ // Get the type name from the prefix, if any. It's any word before the / if there's one
+ String typeName = null;
+ if (prefix != null) {
+ Matcher m = Pattern.compile(".*?([a-z]+)/.*").matcher(prefix); //$NON-NLS-1$
+ if (m.matches()) {
+ typeName = m.group(1);
+ }
+ }
+
+ // Now collect results
+ List<String> results = new ArrayList<String>();
+
+ if (typeName == null) {
+ // This prefix does not have a / in it, so the resource string is either empty
+ // or does not have the resource type in it. Simply offer the list of potential
+ // resource types.
+ if (prefix != null && prefix.startsWith(PREFIX_THEME_REF)) {
+ results.add(ANDROID_THEME_PREFIX + ResourceType.ATTR.getName() + '/');
+ if (resTypes.contains(ResourceType.ATTR)
+ || resTypes.contains(ResourceType.STYLE)) {
+ results.add(PREFIX_THEME_REF + ResourceType.ATTR.getName() + '/');
+ if (prefix != null && prefix.startsWith(ANDROID_THEME_PREFIX)) {
+ // including attr isn't required
+ for (ResourceItem item : repository.getResourceItemsOfType(
+ ResourceType.ATTR)) {
+ results.add(ANDROID_THEME_PREFIX + item.getName());
+ }
+ }
+ }
+ return results.toArray(new String[results.size()]);
+ }
+
+ for (ResourceType resType : resTypes) {
+ if (isSystem) {
+ results.add(ANDROID_PREFIX + resType.getName() + '/');
+ } else {
+ results.add('@' + resType.getName() + '/');
+ }
+ if (resType == ResourceType.ID) {
+ // Also offer the + version to create an id from scratch
+ results.add("@+" + resType.getName() + '/'); //$NON-NLS-1$
+ }
+ }
+
+ // Also add in @android: prefix to completion such that if user has typed
+ // "@an" we offer to complete it.
+ if (prefix == null ||
+ ANDROID_PKG.regionMatches(0, prefix, 1, prefix.length() - 1)) {
+ results.add(ANDROID_PREFIX);
+ }
+ } else if (repository != null) {
+ // We have a style name and a repository. Find all resources that match this
+ // type and recreate suggestions out of them.
+
+ String initial = prefix != null && prefix.startsWith(PREFIX_THEME_REF)
+ ? PREFIX_THEME_REF : PREFIX_RESOURCE_REF;
+ ResourceType resType = ResourceType.getEnum(typeName);
+ if (resType != null) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(initial);
+ if (prefix != null && prefix.indexOf('+') >= 0) {
+ sb.append('+');
+ }
+
+ if (isSystem) {
+ sb.append(ANDROID_PKG).append(':');
+ }
+
+ sb.append(typeName).append('/');
+ String base = sb.toString();
+
+ for (ResourceItem item : repository.getResourceItemsOfType(resType)) {
+ results.add(base + item.getName());
+ }
+
+ if (!isSystem && resType == ResourceType.ATTR) {
+ for (ResourceItem item : repository.getResourceItemsOfType(
+ ResourceType.STYLE)) {
+ results.add(base + item.getName());
+ }
+ }
+ }
+ }
+
+ if (attributeDescriptor != null) {
+ sortAttributeChoices(attributeDescriptor, results);
+ } else {
+ Collections.sort(results);
+ }
+
+ return results.toArray(new String[results.size()]);
+ }
+
+ /**
+ * Attempts to sort the attribute values to bubble up the most likely choices to
+ * the top.
+ * <p>
+ * For example, if you are editing a style attribute, it's likely that among the
+ * resource values you would rather see @style or @android than @string.
+ * @param descriptor the descriptor that the resource values are being completed for,
+ * used to prioritize some of the resource types
+ * @param choices the set of string resource values
+ */
+ public static void sortAttributeChoices(AttributeDescriptor descriptor,
+ List<String> choices) {
+ final IAttributeInfo attributeInfo = descriptor.getAttributeInfo();
+ Collections.sort(choices, new Comparator<String>() {
+ @Override
+ public int compare(String s1, String s2) {
+ int compare = score(attributeInfo, s1) - score(attributeInfo, s2);
+ if (compare == 0) {
+ // Sort alphabetically as a fallback
+ compare = s1.compareToIgnoreCase(s2);
+ }
+ return compare;
+ }
+ });
+ }
+
+ /** Compute a suitable sorting score for the given */
+ private static final int score(IAttributeInfo attributeInfo, String value) {
+ if (value.equals(ANDROID_PREFIX)) {
+ return -1;
+ }
+
+ for (Format format : attributeInfo.getFormats()) {
+ String type = null;
+ switch (format) {
+ case BOOLEAN:
+ type = "bool"; //$NON-NLS-1$
+ break;
+ case COLOR:
+ type = "color"; //$NON-NLS-1$
+ break;
+ case DIMENSION:
+ type = "dimen"; //$NON-NLS-1$
+ break;
+ case INTEGER:
+ type = "integer"; //$NON-NLS-1$
+ break;
+ case STRING:
+ type = "string"; //$NON-NLS-1$
+ break;
+ // default: REFERENCE, FLAG, ENUM, etc - don't have type info about individual
+ // elements to help make a decision
+ }
+
+ if (type != null) {
+ if (value.startsWith(PREFIX_RESOURCE_REF)) {
+ if (value.startsWith(PREFIX_RESOURCE_REF + type + '/')) {
+ return -2;
+ }
+
+ if (value.startsWith(ANDROID_PREFIX + type + '/')) {
+ return -2;
+ }
+ }
+ if (value.startsWith(PREFIX_THEME_REF)) {
+ if (value.startsWith(PREFIX_THEME_REF + type + '/')) {
+ return -2;
+ }
+
+ if (value.startsWith(ANDROID_THEME_PREFIX + type + '/')) {
+ return -2;
+ }
+ }
+ }
+ }
+
+ // Handle a few more cases not covered by the Format metadata check
+ String type = null;
+
+ String attribute = attributeInfo.getName();
+ if (attribute.equals(ATTR_ID)) {
+ type = "id"; //$NON-NLS-1$
+ } else if (attribute.equals(ATTR_STYLE)) {
+ type = "style"; //$NON-NLS-1$
+ } else if (attribute.equals(ATTR_LAYOUT)) {
+ type = "layout"; //$NON-NLS-1$
+ } else if (attribute.equals("drawable")) { //$NON-NLS-1$
+ type = "drawable"; //$NON-NLS-1$
+ } else if (attribute.equals("entries")) { //$NON-NLS-1$
+ // Spinner
+ type = "array"; //$NON-NLS-1$
+ }
+
+ if (type != null) {
+ if (value.startsWith(PREFIX_RESOURCE_REF)) {
+ if (value.startsWith(PREFIX_RESOURCE_REF + type + '/')) {
+ return -2;
+ }
+
+ if (value.startsWith(ANDROID_PREFIX + type + '/')) {
+ return -2;
+ }
+ }
+ if (value.startsWith(PREFIX_THEME_REF)) {
+ if (value.startsWith(PREFIX_THEME_REF + type + '/')) {
+ return -2;
+ }
+
+ if (value.startsWith(ANDROID_THEME_PREFIX + type + '/')) {
+ return -2;
+ }
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiSeparatorAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiSeparatorAttributeNode.java
new file mode 100644
index 000000000..3d2006299
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiSeparatorAttributeNode.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2008 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 com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+import org.eclipse.ui.forms.widgets.TableWrapLayout;
+import org.w3c.dom.Node;
+
+/**
+ * {@link UiSeparatorAttributeNode} does not represent any real attribute.
+ * <p/>
+ * It is used to separate groups of attributes visually.
+ */
+public class UiSeparatorAttributeNode extends UiAttributeNode {
+
+ /** Creates a new {@link UiAttributeNode} linked to a specific {@link AttributeDescriptor} */
+ public UiSeparatorAttributeNode(SeparatorAttributeDescriptor attrDesc,
+ UiElementNode uiParent) {
+ super(attrDesc, uiParent);
+ }
+
+ /** Returns the current value of the node. */
+ @Override
+ public String getCurrentValue() {
+ // There is no value here.
+ return null;
+ }
+
+ /**
+ * Sets whether the attribute is dirty and also notifies the editor some part's dirty
+ * flag as changed.
+ * <p/>
+ * Subclasses should set the to true as a result of user interaction with the widgets in
+ * the section and then should set to false when the commit() method completed.
+ */
+ @Override
+ public void setDirty(boolean isDirty) {
+ // This is never dirty.
+ }
+
+ /**
+ * Called once by the parent user interface to creates the necessary
+ * user interface to edit this attribute.
+ * <p/>
+ * This method can be called more than once in the life cycle of an UI node,
+ * typically when the UI is part of a master-detail tree, as pages are swapped.
+ *
+ * @param parent The composite where to create the user interface.
+ * @param managedForm The managed form owning this part.
+ */
+ @Override
+ public void createUiControl(Composite parent, IManagedForm managedForm) {
+ FormToolkit toolkit = managedForm.getToolkit();
+ Composite row = toolkit.createComposite(parent);
+
+ TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB);
+ if (parent.getLayout() instanceof TableWrapLayout) {
+ twd.colspan = ((TableWrapLayout) parent.getLayout()).numColumns;
+ }
+ row.setLayoutData(twd);
+ row.setLayout(new GridLayout(3, false /* equal width */));
+
+ Label sep = toolkit.createSeparator(row, SWT.HORIZONTAL);
+ GridData gd = new GridData(SWT.LEFT, SWT.CENTER, false, false);
+ gd.widthHint = 16;
+ sep.setLayoutData(gd);
+
+ Label label = toolkit.createLabel(row, getDescriptor().getXmlLocalName());
+ label.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false));
+
+ sep = toolkit.createSeparator(row, SWT.HORIZONTAL);
+ sep.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+ }
+
+ /**
+ * No completion values for this UI attribute.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public String[] getPossibleValues(String prefix) {
+ return null;
+ }
+
+ /**
+ * Called when the XML is being loaded or has changed to
+ * update the value held by this user interface attribute node.
+ * <p/>
+ * The XML Node <em>may</em> be null, which denotes that the attribute is not
+ * specified in the XML model. In general, this means the "default" value of the
+ * attribute should be used.
+ * <p/>
+ * The caller doesn't really know if attributes have changed,
+ * so it will call this to refresh the attribute anyway. It's up to the
+ * UI implementation to minimize refreshes.
+ *
+ * @param xml_attribute_node
+ */
+ @Override
+ public void updateValue(Node xml_attribute_node) {
+ // No value to update.
+ }
+
+ /**
+ * Called by the user interface when the editor is saved or its state changed
+ * and the modified attributes must be committed (i.e. written) to the XML model.
+ * <p/>
+ * Important behaviors:
+ * <ul>
+ * <li>The caller *must* have called IStructuredModel.aboutToChangeModel before.
+ * The implemented methods must assume it is safe to modify the XML model.
+ * <li>On success, the implementation *must* call setDirty(false).
+ * <li>On failure, the implementation can fail with an exception, which
+ * is trapped and logged by the caller, or do nothing, whichever is more
+ * appropriate.
+ * </ul>
+ */
+ @Override
+ public void commit() {
+ // No value to commit.
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextAttributeNode.java
new file mode 100644
index 000000000..504ac3122
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextAttributeNode.java
@@ -0,0 +1,196 @@
+/*
+ * 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 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.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
+
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+
+/**
+ * Represents an XML attribute in that can be modified using a simple text field
+ * in the XML editor's user interface.
+ * <p/>
+ * The XML attribute has no default value. When unset, the text field is blank.
+ * When updating the XML, if the field is empty, the attribute will be removed
+ * from the XML element.
+ * <p/>
+ * See {@link UiAttributeNode} for more information.
+ */
+public class UiTextAttributeNode extends UiAbstractTextAttributeNode {
+
+ /** Text field */
+ private Text mText;
+ /** The managed form, set only once createUiControl has been called. */
+ private IManagedForm mManagedForm;
+
+ public UiTextAttributeNode(AttributeDescriptor attributeDescriptor, UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /* (non-java doc)
+ * Creates a label widget and an associated text field.
+ * <p/>
+ * As most other parts of the android manifest editor, this assumes the
+ * parent uses a table layout with 2 columns.
+ */
+ @Override
+ public void createUiControl(Composite parent, IManagedForm managedForm) {
+ setManagedForm(managedForm);
+ TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
+ Text text = SectionHelper.createLabelAndText(parent, managedForm.getToolkit(),
+ desc.getUiName(), getCurrentValue(),
+ DescriptorsUtils.formatTooltip(desc.getTooltip()));
+
+ setTextWidget(text);
+ }
+
+ /**
+ * No completion values for this UI attribute.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public String[] getPossibleValues(String prefix) {
+ return null;
+ }
+
+ /**
+ * Sets the internal managed form.
+ * This is usually set by createUiControl.
+ */
+ protected void setManagedForm(IManagedForm managedForm) {
+ mManagedForm = managedForm;
+ }
+
+ /**
+ * @return The managed form, set only once createUiControl has been called.
+ */
+ protected IManagedForm getManagedForm() {
+ return mManagedForm;
+ }
+
+ /* (non-java doc)
+ * Returns if the attribute node is valid, and its UI has been created.
+ */
+ @Override
+ public boolean isValid() {
+ return mText != null;
+ }
+
+ @Override
+ public String getTextWidgetValue() {
+ if (mText != null) {
+ return mText.getText();
+ }
+
+ return null;
+ }
+
+ @Override
+ public void setTextWidgetValue(String value) {
+ if (mText != null) {
+ mText.setText(value);
+ }
+ }
+
+ /**
+ * Sets the Text widget object, and prepares it to handle modification and synchronization
+ * with the XML node.
+ * @param textWidget
+ */
+ protected final void setTextWidget(Text textWidget) {
+ mText = textWidget;
+
+ if (textWidget != null) {
+ // Sets the with hint for the text field. Derived classes can always override it.
+ // This helps the grid layout to resize correctly on smaller screen sizes.
+ Object data = textWidget.getLayoutData();
+ if (data == null) {
+ } else if (data instanceof GridData) {
+ ((GridData)data).widthHint = AndroidXmlEditor.TEXT_WIDTH_HINT;
+ } else if (data instanceof TableWrapData) {
+ ((TableWrapData)data).maxWidth = 100;
+ }
+
+ mText.addModifyListener(new ModifyListener() {
+ /**
+ * Sent when the text is modified, whether by the user via manual
+ * input or programmatic input via setText().
+ * <p/>
+ * Simply mark the attribute as dirty if it really changed.
+ * The container SectionPart will collect these flag and manage them.
+ */
+ @Override
+ public void modifyText(ModifyEvent e) {
+ if (!isInInternalTextModification() &&
+ !isDirty() &&
+ mText != null &&
+ getCurrentValue() != null &&
+ !mText.getText().equals(getCurrentValue())) {
+ setDirty(true);
+ }
+ }
+ });
+
+ // Remove self-reference when the widget is disposed
+ mText.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ mText = null;
+ }
+ });
+ }
+
+ onAddValidators(mText);
+ }
+
+ /**
+ * Called after the text widget as been created.
+ * <p/>
+ * Derived classes typically want to:
+ * <li> Create a new {@link ModifyListener} and attach it to the given {@link Text} widget.
+ * <li> In the modify listener, call getManagedForm().getMessageManager().addMessage()
+ * and getManagedForm().getMessageManager().removeMessage() as necessary.
+ * <li> Call removeMessage in a new text.addDisposeListener.
+ * <li> Call the validator once to setup the initial messages as needed.
+ * <p/>
+ * The base implementation does nothing.
+ *
+ * @param text The {@link Text} widget to validate.
+ */
+ protected void onAddValidators(Text text) {
+ }
+
+ /**
+ * Returns the text widget.
+ */
+ protected final Text getTextWidget() {
+ return mText;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextValueNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextValueNode.java
new file mode 100644
index 000000000..33fa9fc99
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextValueNode.java
@@ -0,0 +1,118 @@
+/*
+ * 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 com.android.ide.eclipse.adt.internal.editors.descriptors.TextValueDescriptor;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.Text;
+
+/**
+ * Represents an XML element value in that can be modified using a simple text field
+ * in the XML editor's user interface.
+ */
+public class UiTextValueNode extends UiTextAttributeNode {
+
+ public UiTextValueNode(TextValueDescriptor attributeDescriptor, UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /**
+ * Updates the current text field's value when the XML has changed.
+ * <p/>
+ * The caller doesn't really know if value of the element has changed,
+ * so it will call this to refresh the value anyway. The value
+ * is only set if it has changed.
+ * <p/>
+ * This also resets the "dirty" flag.
+ */
+ @Override
+ public void updateValue(Node xml_attribute_node) {
+ setCurrentValue(DEFAULT_VALUE);
+
+ // The argument xml_attribute_node is not used here. It should always be
+ // null since this is not an attribute. What we want is the "text value" of
+ // the parent element, which is actually the first text node of the element.
+
+ UiElementNode parent = getUiParent();
+ if (parent != null) {
+ Node xml_node = parent.getXmlNode();
+ if (xml_node != null) {
+ for (Node xml_child = xml_node.getFirstChild();
+ xml_child != null;
+ xml_child = xml_child.getNextSibling()) {
+ if (xml_child.getNodeType() == Node.TEXT_NODE) {
+ setCurrentValue(xml_child.getNodeValue());
+ break;
+ }
+ }
+ }
+ }
+
+ if (isValid() && !getTextWidgetValue().equals(getCurrentValue())) {
+ try {
+ setInInternalTextModification(true);
+ setTextWidgetValue(getCurrentValue());
+ setDirty(false);
+ } finally {
+ setInInternalTextModification(false);
+ }
+ }
+ }
+
+ /* (non-java doc)
+ * Called by the user interface when the editor is saved or its state changed
+ * and the modified "attributes" must be committed (i.e. written) to the XML model.
+ */
+ @Override
+ public void commit() {
+ UiElementNode parent = getUiParent();
+ if (parent != null && isValid() && isDirty()) {
+ // Get (or create) the underlying XML element node that contains the value.
+ Node element = parent.prepareCommit();
+ if (element != null) {
+ String value = getTextWidgetValue();
+
+ // Try to find an existing text child to update.
+ boolean updated = false;
+
+ for (Node xml_child = element.getFirstChild();
+ xml_child != null;
+ xml_child = xml_child.getNextSibling()) {
+ if (xml_child.getNodeType() == Node.TEXT_NODE) {
+ xml_child.setNodeValue(value);
+ updated = true;
+ break;
+ }
+ }
+
+ // If we didn't find a text child to update, we need to create one.
+ if (!updated) {
+ Document doc = element.getOwnerDocument();
+ if (doc != null) {
+ Text text = doc.createTextNode(value);
+ element.appendChild(text);
+ }
+ }
+
+ setCurrentValue(value);
+ }
+ }
+ setDirty(false);
+ }
+}