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