diff options
11 files changed, 277 insertions, 21 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java index f3582ec..c778271 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java @@ -16,8 +16,8 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import com.android.ide.common.api.IDragElement; -import com.android.ide.common.api.INode; import com.android.ide.common.api.IDragElement.IDragAttribute; +import com.android.ide.common.api.INode; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor; @@ -226,6 +226,7 @@ public class ClipboardSupport { List<INode> children = entry.getValue(); assert children != null && children.size() > 0; rulesEngine.callOnRemovingChildren(editor, parent, children); + parent.applyPendingChanges(); } for (SelectionItem cs : selection) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java index c8fab84..45e0234 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java @@ -361,6 +361,7 @@ public class MoveGesture extends DropGesture { mFeedback, new Point(canvasPoint.x, canvasPoint.y), insertType); + mTargetNode.applyPendingChanges(); // Clean up drag if applicable if (event.detail == DND.DROP_MOVE) { GlobalCanvasDragInfo.getInstance().removeSource(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDropListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDropListener.java index 5f516d5..cf09160 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDropListener.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDropListener.java @@ -130,6 +130,7 @@ import java.util.Set; Object sourceCanvas = GlobalCanvasDragInfo.getInstance().getSourceCanvas(); boolean createNew = event.detail == DND.DROP_COPY || sourceCanvas != canvas; BaseLayoutRule.insertAt(targetNode, elements, createNew, indexFinal); + targetNode.applyPendingChanges(); // Clean up drag if applicable if (event.detail == DND.DROP_MOVE) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java index cafcc13..7c0a365 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java @@ -838,6 +838,7 @@ public class OutlinePage extends ContentOutlinePage canvas.getRulesEngine().setInsertType(insertType); int index = target.getSecond(); BaseLayoutRule.insertAt(targetNode, elements, false, index); + targetNode.applyPendingChanges(); canvas.getClipboardSupport().deleteSelection("Remove", dragSelection); } }); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java index fef1a56..4233208 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java @@ -16,7 +16,6 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; -import com.android.ide.common.api.IDragElement.IDragAttribute; import com.android.ide.common.api.INode.IAttribute; import java.util.regex.Matcher; @@ -33,7 +32,7 @@ import java.util.regex.Pattern; * For a more detailed explanation of the purpose of this class, * please see {@link SimpleXmlTransfer}. */ -public class SimpleAttribute implements IDragAttribute, IAttribute { +public class SimpleAttribute implements IAttribute { private final String mName; private final String mValue; private final String mUri; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElement.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElement.java index 20a8433..cf42077 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElement.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElement.java @@ -20,6 +20,7 @@ import com.android.ide.common.api.IDragElement; import com.android.ide.common.api.Rect; import java.util.ArrayList; +import java.util.List; /** * Represents an XML element with a name, attributes and inner elements. @@ -39,8 +40,8 @@ public class SimpleElement implements IDragElement { private final String mParentFqcn; private final Rect mBounds; private final Rect mParentBounds; - private final ArrayList<IDragAttribute> mAttributes = new ArrayList<IDragAttribute>(); - private final ArrayList<IDragElement> mElements = new ArrayList<IDragElement>(); + private final List<IDragAttribute> mAttributes = new ArrayList<IDragAttribute>(); + private final List<IDragElement> mElements = new ArrayList<IDragElement>(); private IDragAttribute[] mCachedAttributes = null; private IDragElement[] mCachedElements = null; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java index c4c050a..b27954e 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java @@ -43,7 +43,9 @@ import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * @@ -53,6 +55,8 @@ public class NodeProxy implements INode { private final UiViewElementNode mNode; private final Rect mBounds; private final NodeFactory mFactory; + /** Map from URI to Map(key=>value) (where no namespace uses "" as a key) */ + private Map<String, Map<String, String>> mPendingAttributes; /** * Creates a new {@link INode} that wraps an {@link UiViewElementNode} that is @@ -208,6 +212,7 @@ public class NodeProxy implements INode { // Finally execute the closure that will act on the XML c.handle(NodeProxy.this); + applyPendingChanges(); } }); } @@ -295,9 +300,24 @@ public class NodeProxy implements INode { public boolean setAttribute(String uri, String name, String value) { checkEditOK(); - UiAttributeNode attr = mNode.setAttributeValue(name, uri, value, true /* override */); - mNode.commitDirtyAttributesToXml(); + + if (uri == null) { + uri = ""; //$NON-NLS-1$ + } + + Map<String, String> map = null; + if (mPendingAttributes == null) { + // Small initial size: we don't expect many different namespaces + mPendingAttributes = new HashMap<String, Map<String, String>>(3); + } else { + map = mPendingAttributes.get(uri); + } + if (map == null) { + map = new HashMap<String, String>(); + mPendingAttributes.put(uri, map); + } + map.put(name, value); return attr != null; } @@ -309,6 +329,16 @@ public class NodeProxy implements INode { return null; } + if (mPendingAttributes != null) { + Map<String, String> map = mPendingAttributes.get(uri == null ? "" : uri); //$NON-NLS-1$ + if (map != null) { + String value = map.get(attrName); + if (value != null) { + return value; + } + } + } + if (uiNode.getXmlNode() != null) { Node xmlNode = uiNode.getXmlNode(); if (xmlNode != null) { @@ -413,4 +443,25 @@ public class NodeProxy implements INode { ); } + /** + * If there are any pending changes in these nodes, apply them now + * + * @return true if any modifications were made + */ + public boolean applyPendingChanges() { + boolean modified = false; + + // Flush all pending attributes + if (mPendingAttributes != null) { + mNode.commitDirtyAttributesToXml(); + modified = true; + mPendingAttributes = null; + + } + for (INode child : getChildren()) { + modified |= ((NodeProxy) child).applyPendingChanges(); + } + + return modified; + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java index deb566e..5b2d430 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java @@ -555,6 +555,10 @@ public class RulesEngine { if (childRule != null) { childRule.onCreate(newNode, parentNode, insertType); } + + if (parentNode != null) { + ((NodeProxy) parentNode).applyPendingChanges(); + } } /** 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 index 9c822f6..94cea34 100644 --- 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 @@ -16,6 +16,12 @@ package com.android.ide.eclipse.adt.internal.editors.uimodel; +import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH; +import static com.android.ide.common.layout.LayoutConstants.ATTR_STYLE; + import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; @@ -23,6 +29,8 @@ import org.eclipse.swt.widgets.Composite; import org.eclipse.ui.forms.IManagedForm; import org.w3c.dom.Node; +import java.util.Comparator; + /** * Represents an XML attribute that can be modified by the XML editor's user interface. * <p/> @@ -32,7 +40,7 @@ import org.w3c.dom.Node; * 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 { +public abstract class UiAttributeNode implements Comparable<UiAttributeNode> { private AttributeDescriptor mDescriptor; private UiElementNode mUiParent; @@ -73,12 +81,14 @@ public abstract class UiAttributeNode { * <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 old_value = mIsDirty; + boolean wasDirty = mIsDirty; mIsDirty = isDirty; // TODO: for unknown attributes, getParent() != null && getParent().getEditor() != null - if (old_value != isDirty) { + if (wasDirty != isDirty) { AndroidXmlEditor editor = getUiParent().getEditor(); if (editor != null) { editor.editorDirtyStateChanged(); @@ -141,9 +151,9 @@ public abstract class UiAttributeNode { * so it will call this to refresh the attribute anyway. It's up to the * UI implementation to minimize refreshes. * - * @param xml_attribute_node + * @param node the node to read the value from */ - public abstract void updateValue(Node xml_attribute_node); + public abstract void updateValue(Node node); /** * Called by the user interface when the editor is saved or its state changed @@ -160,4 +170,63 @@ public abstract class UiAttributeNode { * </ul> */ public abstract void commit(); + + // ---- Implements Comparable ---- + public int compareTo(UiAttributeNode o) { + return compareAttributes(mDescriptor.getXmlLocalName(), o.mDescriptor.getXmlLocalName()); + } + + /** + * Returns {@link Comparator} values for ordering attributes in the following + * order: + * <ul> + * <li> id + * <li> style + * <li> layout_width + * <li> layout_height + * <li> other layout params, sorted alphabetically + * <li> other attributes, sorted alphabetically + * </ul> + * + * @param name1 the first attribute name to compare + * @param name2 the second attribute name to compare + * @return a negative number if name1 should be ordered before name2 + */ + public static int compareAttributes(String name1, String name2) { + int priority1 = getAttributePriority(name1); + int priority2 = getAttributePriority(name2); + if (priority1 != priority2) { + return priority1 - priority2; + } + + // Sort remaining attributes alphabetically + return name1.compareTo(name2); + } + + /** Returns a sorting priority for the given attribute name */ + private static int getAttributePriority(String name) { + if (ATTR_ID.equals(name)) { + return 10; + } + + if (ATTR_STYLE.equals(name)) { + return 20; + } + + if (name.startsWith(ATTR_LAYOUT_PREFIX)) { + // Width and height are special cased because we (a) want width and height + // before the other layout attributes, and (b) we want width to sort before height + // even though it comes after it alphabetically. + if (name.equals(ATTR_LAYOUT_WIDTH)) { + return 30; + } + if (name.equals(ATTR_LAYOUT_HEIGHT)) { + return 40; + } + + return 50; + } + + return 60; + } } 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 index 3f7fadd..37273eb 100644 --- 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 @@ -1045,6 +1045,8 @@ public class UiElementNode implements IPropertySource { // 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; @@ -1055,10 +1057,31 @@ public class UiElementNode implements IPropertySource { mXmlNode.getAttributes().setNamedItemNS(attr); } else { UiAttributeNode uiAttr = getInternalUiAttributes().get(attrDesc); - commitAttributeToXml(uiAttr, uiAttr.getCurrentValue()); + + // 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); + } + if (mUiParent != null) { mUiParent.formatOnInsert(this); } @@ -1518,12 +1541,18 @@ public class UiElementNode implements IPropertySource { if (doc != null) { Attr attr; if (attrNsUri != null && attrNsUri.length() > 0) { - attr = doc.createAttributeNS(attrNsUri, attrLocalName); - attr.setPrefix(lookupNamespacePrefix(element, attrNsUri)); - attrMap.setNamedItemNS(attr); + attr = (Attr) attrMap.getNamedItemNS(attrNsUri, attrLocalName); + if (attr == null) { + attr = doc.createAttributeNS(attrNsUri, attrLocalName); + attr.setPrefix(lookupNamespacePrefix(element, attrNsUri)); + attrMap.setNamedItemNS(attr); + } } else { - attr = doc.createAttribute(attrLocalName); - attrMap.setNamedItem(attr); + attr = (Attr) attrMap.getNamedItem(attrLocalName); + if (attr == null) { + attr = doc.createAttribute(attrLocalName); + attrMap.setNamedItem(attr); + } } attr.setValue(newValue); return true; @@ -1549,14 +1578,110 @@ public class UiElementNode implements IPropertySource { * @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()) { - result |= commitAttributeToXml(uiAttr, uiAttr.getCurrentValue()); - uiAttr.setDirty(false); + 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; + } + + String firstName = dirtyAttributes.get(0).getDescriptor().getXmlLocalName(); + NamedNodeMap attributes = ((Element) element).getAttributes(); + List<Attr> move = new ArrayList<Attr>(); + for (int i = 0, n = attributes.getLength(); i < n; i++) { + Attr attribute = (Attr) attributes.item(i); + if (UiAttributeNode.compareAttributes(attribute.getLocalName(), firstName) > 0) { + move.add(attribute); + } + } + + for (Attr attribute : move) { + if (attribute.getNamespaceURI() != null) { + attributes.removeNamedItemNS(attribute.getNamespaceURI(), + attribute.getLocalName()); + } else { + attributes.removeNamedItem(attribute.getName()); + } + } + + // Merge back the removed DOM attribute nodes and the new UI attribute nodes. + // In cases where the attribute DOM name and the UI attribute names equal, + // skip the DOM nodes and just apply the UI attributes. + int domAttributeIndex = 0; + int domAttributeIndexMax = move.size(); + int uiAttributeIndex = 0; + int uiAttributeIndexMax = dirtyAttributes.size(); + + while (true) { + Attr domAttribute; + UiAttributeNode uiAttribute; + + int compare; + if (uiAttributeIndex < uiAttributeIndexMax) { + if (domAttributeIndex < domAttributeIndexMax) { + domAttribute = move.get(domAttributeIndex); + uiAttribute = dirtyAttributes.get(uiAttributeIndex); + + String domAttributeName = domAttribute.getLocalName(); + String uiAttributeName = uiAttribute.getDescriptor().getXmlLocalName(); + compare = UiAttributeNode.compareAttributes(domAttributeName, + uiAttributeName); + } else { + compare = 1; + uiAttribute = dirtyAttributes.get(uiAttributeIndex); + domAttribute = null; + } + } else if (domAttributeIndex < domAttributeIndexMax) { + compare = -1; + domAttribute = move.get(domAttributeIndex); + uiAttribute = null; + } else { + break; + } + + if (compare < 0) { + if (domAttribute.getNamespaceURI() != null) { + attributes.setNamedItemNS(domAttribute); + } else { + attributes.setNamedItem(domAttribute); + } + domAttributeIndex++; + } else { + assert compare >= 0; + if (compare == 0) { + domAttributeIndex++; + } + commitAttributeToXml(uiAttribute, uiAttribute.getCurrentValue()); + uiAttribute.setDirty(false); + uiAttributeIndex++; + } + } + } + return result; } diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestAttribute.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestAttribute.java index fc59ba8..7ff425f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestAttribute.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestAttribute.java @@ -17,6 +17,7 @@ package com.android.ide.common.layout; import com.android.ide.common.api.IDragElement.IDragAttribute; import com.android.ide.common.api.INode.IAttribute; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; /** Test/mock implementation of {@link IAttribute} and {@link IDragAttribute} */ public class TestAttribute implements IAttribute, IDragAttribute { @@ -50,5 +51,7 @@ public class TestAttribute implements IAttribute, IDragAttribute { return "TestAttribute [name=" + mName + ", uri=" + mUri + ", value=" + mValue + "]"; } - + public int compareTo(IDragAttribute o) { + return UiAttributeNode.compareAttributes(mName, o.getName()); + } }
\ No newline at end of file |