/* * 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; } }