aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java
diff options
context:
space:
mode:
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.java517
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;
+ }
+}