diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java | 517 |
1 files changed, 517 insertions, 0 deletions
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 new file mode 100644 index 000000000..19d5e16b0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java @@ -0,0 +1,517 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.editors.layout.gre; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.api.IAttributeInfo; +import com.android.ide.common.api.INode; +import com.android.ide.common.api.INodeHandler; +import com.android.ide.common.api.Margins; +import com.android.ide.common.api.Rect; +import com.android.ide.common.resources.platform.AttributeInfo; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleAttribute; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.adt.internal.project.SupportLibraryHelper; + +import org.eclipse.core.resources.IProject; +import org.eclipse.swt.graphics.Rectangle; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * + */ +public class NodeProxy implements INode { + private static final Margins NO_MARGINS = new Margins(0, 0, 0, 0); + 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 + * actually valid in the current UI/XML model. The view may not be part of the canvas + * yet (e.g. if it has just been dynamically added and the canvas hasn't reloaded yet.) + * <p/> + * This method is package protected. To create a node, please use {@link NodeFactory} instead. + * + * @param uiNode The node to wrap. + * @param bounds The bounds of a the view in the canvas. Must be either: <br/> + * - a valid rect for a view that is actually in the canvas <br/> + * - <b>*or*</b> null (or an invalid rect) for a view that has just been added dynamically + * to the model. We never store a null bounds rectangle in the node, a null rectangle + * will be converted to an invalid rectangle. + * @param factory A {@link NodeFactory} to create unique children nodes. + */ + /*package*/ NodeProxy(UiViewElementNode uiNode, Rectangle bounds, NodeFactory factory) { + mNode = uiNode; + mFactory = factory; + if (bounds == null) { + mBounds = new Rect(); + } else { + mBounds = SwtUtils.toRect(bounds); + } + } + + @Override + public @NonNull Rect getBounds() { + return mBounds; + } + + @Override + public @NonNull Margins getMargins() { + ViewHierarchy viewHierarchy = mFactory.getCanvas().getViewHierarchy(); + CanvasViewInfo view = viewHierarchy.findViewInfoFor(this); + if (view != null) { + Margins margins = view.getMargins(); + if (margins != null) { + return margins; + } + } + + return NO_MARGINS; + } + + + @Override + public int getBaseline() { + ViewHierarchy viewHierarchy = mFactory.getCanvas().getViewHierarchy(); + CanvasViewInfo view = viewHierarchy.findViewInfoFor(this); + if (view != null) { + return view.getBaseline(); + } + + return -1; + } + + /** + * Updates the bounds of this node proxy. Bounds cannot be null, but it can be invalid. + * This is a package-protected method, only the {@link NodeFactory} uses this method. + */ + /*package*/ void setBounds(Rectangle bounds) { + SwtUtils.set(mBounds, bounds); + } + + /** + * Returns the {@link UiViewElementNode} corresponding to this + * {@link NodeProxy}. + * + * @return The {@link UiViewElementNode} corresponding to this + * {@link NodeProxy} + */ + public UiViewElementNode getNode() { + return mNode; + } + + @Override + public @NonNull String getFqcn() { + if (mNode != null) { + ElementDescriptor desc = mNode.getDescriptor(); + if (desc instanceof ViewElementDescriptor) { + return ((ViewElementDescriptor) desc).getFullClassName(); + } + } + + return ""; + } + + + // ---- Hierarchy handling ---- + + + @Override + public INode getRoot() { + if (mNode != null) { + UiElementNode p = mNode.getUiRoot(); + // The node root should be a document. Instead what we really mean to + // return is the top level view element. + if (p instanceof UiDocumentNode) { + List<UiElementNode> children = p.getUiChildren(); + if (children.size() > 0) { + p = children.get(0); + } + } + + // Cope with a badly structured XML layout + while (p != null && !(p instanceof UiViewElementNode)) { + p = p.getUiNextSibling(); + } + + if (p == mNode) { + return this; + } + if (p instanceof UiViewElementNode) { + return mFactory.create((UiViewElementNode) p); + } + } + + return null; + } + + @Override + public INode getParent() { + if (mNode != null) { + UiElementNode p = mNode.getUiParent(); + if (p instanceof UiViewElementNode) { + return mFactory.create((UiViewElementNode) p); + } + } + + return null; + } + + @Override + public @NonNull INode[] getChildren() { + if (mNode != null) { + List<UiElementNode> uiChildren = mNode.getUiChildren(); + List<INode> nodes = new ArrayList<INode>(uiChildren.size()); + for (UiElementNode uiChild : uiChildren) { + if (uiChild instanceof UiViewElementNode) { + nodes.add(mFactory.create((UiViewElementNode) uiChild)); + } + } + + return nodes.toArray(new INode[nodes.size()]); + } + + return new INode[0]; + } + + + // ---- XML Editing --- + + @Override + public void editXml(@NonNull String undoName, final @NonNull INodeHandler c) { + final AndroidXmlEditor editor = mNode.getEditor(); + + if (editor != null) { + // Create an undo edit XML wrapper, which takes a runnable + editor.wrapUndoEditXmlModel( + undoName, + new Runnable() { + @Override + public void run() { + // Here editor.isEditXmlModelPending returns true and it + // is safe to edit the model using any method from INode. + + // Finally execute the closure that will act on the XML + c.handle(NodeProxy.this); + applyPendingChanges(); + } + }); + } + } + + private void checkEditOK() { + final AndroidXmlEditor editor = mNode.getEditor(); + if (!editor.isEditXmlModelPending()) { + throw new RuntimeException("Error: XML edit call without using INode.editXml!"); + } + } + + @Override + public @NonNull INode appendChild(@NonNull String viewFqcn) { + return insertOrAppend(viewFqcn, -1); + } + + @Override + public @NonNull INode insertChildAt(@NonNull String viewFqcn, int index) { + return insertOrAppend(viewFqcn, index); + } + + @Override + public void removeChild(@NonNull INode node) { + checkEditOK(); + + ((NodeProxy) node).mNode.deleteXmlNode(); + } + + private INode insertOrAppend(String viewFqcn, int index) { + checkEditOK(); + + AndroidXmlEditor editor = mNode.getEditor(); + if (editor != null) { + // Possibly replace the tag with a compatibility version if the + // minimum SDK requires it + IProject project = editor.getProject(); + if (project != null) { + viewFqcn = SupportLibraryHelper.getTagFor(project, viewFqcn); + } + } + + // Find the descriptor for this FQCN + ViewElementDescriptor vd = getFqcnViewDescriptor(viewFqcn); + if (vd == null) { + warnPrintf("Can't create a new %s element", viewFqcn); + return null; + } + + final UiElementNode uiNew; + if (index == -1) { + // Append at the end. + uiNew = mNode.appendNewUiChild(vd); + } else { + // Insert at the requested position or at the end. + int n = mNode.getUiChildren().size(); + if (index < 0 || index >= n) { + uiNew = mNode.appendNewUiChild(vd); + } else { + uiNew = mNode.insertNewUiChild(index, vd); + } + } + + // Set default attributes -- but only for new widgets (not when moving or copying) + RulesEngine engine = null; + LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(editor); + if (delegate != null) { + engine = delegate.getRulesEngine(); + } + if (engine == null || engine.getInsertType().isCreate()) { + // TODO: This should probably use IViewRule#getDefaultAttributes() at some point + DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/); + } + + Node xmlNode = uiNew.createXmlNode(); + + if (!(uiNew instanceof UiViewElementNode) || xmlNode == null) { + // Both things are not supposed to happen. When they do, we're in big trouble. + // We don't really know how to revert the state at this point and the UI model is + // now out of sync with the XML model. + // Panic ensues. + // The best bet is to abort now. The edit wrapper will release the edit and the + // XML/UI should get reloaded properly (with a likely invalid XML.) + warnPrintf("Failed to create a new %s element", viewFqcn); + throw new RuntimeException("XML node creation failed."); //$NON-NLS-1$ + } + + UiViewElementNode uiNewView = (UiViewElementNode) uiNew; + NodeProxy newNode = mFactory.create(uiNewView); + + if (engine != null) { + engine.callCreateHooks(editor, this, newNode, null); + } + + return newNode; + } + + @Override + public boolean setAttribute( + @Nullable String uri, + @NonNull String name, + @Nullable String value) { + checkEditOK(); + UiAttributeNode attr = mNode.setAttributeValue(name, uri, value, true /* override */); + + 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; + } + + @Override + public String getStringAttr(@Nullable String uri, @NonNull String attrName) { + UiElementNode uiNode = mNode; + + if (attrName == null) { + 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) { + NamedNodeMap nodeAttributes = xmlNode.getAttributes(); + if (nodeAttributes != null) { + Node attr = nodeAttributes.getNamedItemNS(uri, attrName); + if (attr != null) { + return attr.getNodeValue(); + } + } + } + } + return null; + } + + @Override + public IAttributeInfo getAttributeInfo(@Nullable String uri, @NonNull String attrName) { + UiElementNode uiNode = mNode; + + if (attrName == null) { + return null; + } + + for (AttributeDescriptor desc : uiNode.getAttributeDescriptors()) { + String dUri = desc.getNamespaceUri(); + String dName = desc.getXmlLocalName(); + if ((uri == null && dUri == null) || (uri != null && uri.equals(dUri))) { + if (attrName.equals(dName)) { + return desc.getAttributeInfo(); + } + } + } + + return null; + } + + @Override + public @NonNull IAttributeInfo[] getDeclaredAttributes() { + + AttributeDescriptor[] descs = mNode.getAttributeDescriptors(); + int n = descs.length; + IAttributeInfo[] infos = new AttributeInfo[n]; + + for (int i = 0; i < n; i++) { + infos[i] = descs[i].getAttributeInfo(); + } + + return infos; + } + + @Override + public @NonNull List<String> getAttributeSources() { + ElementDescriptor descriptor = mNode.getDescriptor(); + if (descriptor instanceof ViewElementDescriptor) { + return ((ViewElementDescriptor) descriptor).getAttributeSources(); + } else { + return Collections.emptyList(); + } + } + + @Override + public @NonNull IAttribute[] getLiveAttributes() { + UiElementNode uiNode = mNode; + + if (uiNode.getXmlNode() != null) { + Node xmlNode = uiNode.getXmlNode(); + if (xmlNode != null) { + NamedNodeMap nodeAttributes = xmlNode.getAttributes(); + if (nodeAttributes != null) { + + int n = nodeAttributes.getLength(); + IAttribute[] result = new IAttribute[n]; + for (int i = 0; i < n; i++) { + Node attr = nodeAttributes.item(i); + String uri = attr.getNamespaceURI(); + String name = attr.getLocalName(); + String value = attr.getNodeValue(); + + result[i] = new SimpleAttribute(uri, name, value); + } + return result; + } + } + } + + return new IAttribute[0]; + + } + + @Override + public String toString() { + return "NodeProxy [node=" + mNode + ", bounds=" + mBounds + "]"; + } + + // --- internal helpers --- + + /** + * Helper methods that returns a {@link ViewElementDescriptor} for the requested FQCN. + * Will return null if we can't find that FQCN or we lack the editor/data/descriptors info + * (which shouldn't really happen since at this point the SDK should be fully loaded and + * isn't reloading, or we wouldn't be here editing XML for a layout rule.) + */ + private ViewElementDescriptor getFqcnViewDescriptor(String fqcn) { + LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(mNode.getEditor()); + if (delegate != null) { + return delegate.getFqcnViewDescriptor(fqcn); + } + + return null; + } + + private void warnPrintf(String msg, Object...params) { + AdtPlugin.printToConsole( + mNode == null ? "" : mNode.getDescriptor().getXmlLocalName(), + String.format(msg, params) + ); + } + + /** + * 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; + } +} |