diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel')
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> "<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); + } +} |