Home | History | Annotate | Download | only in gre
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.editors.layout.gre;
     18 
     19 import com.android.annotations.NonNull;
     20 import com.android.annotations.Nullable;
     21 import com.android.ide.common.api.IAttributeInfo;
     22 import com.android.ide.common.api.INode;
     23 import com.android.ide.common.api.INodeHandler;
     24 import com.android.ide.common.api.Margins;
     25 import com.android.ide.common.api.Rect;
     26 import com.android.ide.common.resources.platform.AttributeInfo;
     27 import com.android.ide.eclipse.adt.AdtPlugin;
     28 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
     29 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
     30 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
     31 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
     32 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
     33 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
     34 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
     35 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleAttribute;
     36 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils;
     37 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy;
     38 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
     39 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
     40 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
     41 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
     42 import com.android.ide.eclipse.adt.internal.project.SupportLibraryHelper;
     43 
     44 import org.eclipse.core.resources.IProject;
     45 import org.eclipse.swt.graphics.Rectangle;
     46 import org.w3c.dom.NamedNodeMap;
     47 import org.w3c.dom.Node;
     48 
     49 import java.util.ArrayList;
     50 import java.util.Collections;
     51 import java.util.HashMap;
     52 import java.util.List;
     53 import java.util.Map;
     54 
     55 /**
     56  *
     57  */
     58 public class NodeProxy implements INode {
     59     private static final Margins NO_MARGINS = new Margins(0, 0, 0, 0);
     60     private final UiViewElementNode mNode;
     61     private final Rect mBounds;
     62     private final NodeFactory mFactory;
     63     /** Map from URI to Map(key=>value) (where no namespace uses "" as a key) */
     64     private Map<String, Map<String, String>> mPendingAttributes;
     65 
     66     /**
     67      * Creates a new {@link INode} that wraps an {@link UiViewElementNode} that is
     68      * actually valid in the current UI/XML model. The view may not be part of the canvas
     69      * yet (e.g. if it has just been dynamically added and the canvas hasn't reloaded yet.)
     70      * <p/>
     71      * This method is package protected. To create a node, please use {@link NodeFactory} instead.
     72      *
     73      * @param uiNode The node to wrap.
     74      * @param bounds The bounds of a the view in the canvas. Must be either: <br/>
     75      *   - a valid rect for a view that is actually in the canvas <br/>
     76      *   - <b>*or*</b> null (or an invalid rect) for a view that has just been added dynamically
     77      *   to the model. We never store a null bounds rectangle in the node, a null rectangle
     78      *   will be converted to an invalid rectangle.
     79      * @param factory A {@link NodeFactory} to create unique children nodes.
     80      */
     81     /*package*/ NodeProxy(UiViewElementNode uiNode, Rectangle bounds, NodeFactory factory) {
     82         mNode = uiNode;
     83         mFactory = factory;
     84         if (bounds == null) {
     85             mBounds = new Rect();
     86         } else {
     87             mBounds = SwtUtils.toRect(bounds);
     88         }
     89     }
     90 
     91     @Override
     92     public @NonNull Rect getBounds() {
     93         return mBounds;
     94     }
     95 
     96     @Override
     97     public @NonNull Margins getMargins() {
     98         ViewHierarchy viewHierarchy = mFactory.getCanvas().getViewHierarchy();
     99         CanvasViewInfo view = viewHierarchy.findViewInfoFor(this);
    100         if (view != null) {
    101             Margins margins = view.getMargins();
    102             if (margins != null) {
    103                 return margins;
    104             }
    105         }
    106 
    107         return NO_MARGINS;
    108     }
    109 
    110 
    111     @Override
    112     public int getBaseline() {
    113         ViewHierarchy viewHierarchy = mFactory.getCanvas().getViewHierarchy();
    114         CanvasViewInfo view = viewHierarchy.findViewInfoFor(this);
    115         if (view != null) {
    116             return view.getBaseline();
    117         }
    118 
    119         return -1;
    120     }
    121 
    122     /**
    123      * Updates the bounds of this node proxy. Bounds cannot be null, but it can be invalid.
    124      * This is a package-protected method, only the {@link NodeFactory} uses this method.
    125      */
    126     /*package*/ void setBounds(Rectangle bounds) {
    127         SwtUtils.set(mBounds, bounds);
    128     }
    129 
    130     /**
    131      * Returns the {@link UiViewElementNode} corresponding to this
    132      * {@link NodeProxy}.
    133      *
    134      * @return The {@link UiViewElementNode} corresponding to this
    135      *         {@link NodeProxy}
    136      */
    137     public UiViewElementNode getNode() {
    138         return mNode;
    139     }
    140 
    141     @Override
    142     public @NonNull String getFqcn() {
    143         if (mNode != null) {
    144             ElementDescriptor desc = mNode.getDescriptor();
    145             if (desc instanceof ViewElementDescriptor) {
    146                 return ((ViewElementDescriptor) desc).getFullClassName();
    147             }
    148         }
    149 
    150         return "";
    151     }
    152 
    153 
    154     // ---- Hierarchy handling ----
    155 
    156 
    157     @Override
    158     public INode getRoot() {
    159         if (mNode != null) {
    160             UiElementNode p = mNode.getUiRoot();
    161             // The node root should be a document. Instead what we really mean to
    162             // return is the top level view element.
    163             if (p instanceof UiDocumentNode) {
    164                 List<UiElementNode> children = p.getUiChildren();
    165                 if (children.size() > 0) {
    166                     p = children.get(0);
    167                 }
    168             }
    169 
    170             // Cope with a badly structured XML layout
    171             while (p != null && !(p instanceof UiViewElementNode)) {
    172                 p = p.getUiNextSibling();
    173             }
    174 
    175             if (p == mNode) {
    176                 return this;
    177             }
    178             if (p instanceof UiViewElementNode) {
    179                 return mFactory.create((UiViewElementNode) p);
    180             }
    181         }
    182 
    183         return null;
    184     }
    185 
    186     @Override
    187     public INode getParent() {
    188         if (mNode != null) {
    189             UiElementNode p = mNode.getUiParent();
    190             if (p instanceof UiViewElementNode) {
    191                 return mFactory.create((UiViewElementNode) p);
    192             }
    193         }
    194 
    195         return null;
    196     }
    197 
    198     @Override
    199     public @NonNull INode[] getChildren() {
    200         if (mNode != null) {
    201             List<UiElementNode> uiChildren = mNode.getUiChildren();
    202             List<INode> nodes = new ArrayList<INode>(uiChildren.size());
    203             for (UiElementNode uiChild : uiChildren) {
    204                 if (uiChild instanceof UiViewElementNode) {
    205                     nodes.add(mFactory.create((UiViewElementNode) uiChild));
    206                 }
    207             }
    208 
    209             return nodes.toArray(new INode[nodes.size()]);
    210         }
    211 
    212         return new INode[0];
    213     }
    214 
    215 
    216     // ---- XML Editing ---
    217 
    218     @Override
    219     public void editXml(@NonNull String undoName, final @NonNull INodeHandler c) {
    220         final AndroidXmlEditor editor = mNode.getEditor();
    221 
    222         if (editor != null) {
    223             // Create an undo edit XML wrapper, which takes a runnable
    224             editor.wrapUndoEditXmlModel(
    225                     undoName,
    226                     new Runnable() {
    227                         @Override
    228                         public void run() {
    229                             // Here editor.isEditXmlModelPending returns true and it
    230                             // is safe to edit the model using any method from INode.
    231 
    232                             // Finally execute the closure that will act on the XML
    233                             c.handle(NodeProxy.this);
    234                             applyPendingChanges();
    235                         }
    236                     });
    237         }
    238     }
    239 
    240     private void checkEditOK() {
    241         final AndroidXmlEditor editor = mNode.getEditor();
    242         if (!editor.isEditXmlModelPending()) {
    243             throw new RuntimeException("Error: XML edit call without using INode.editXml!");
    244         }
    245     }
    246 
    247     @Override
    248     public @NonNull INode appendChild(@NonNull String viewFqcn) {
    249         return insertOrAppend(viewFqcn, -1);
    250     }
    251 
    252     @Override
    253     public @NonNull INode insertChildAt(@NonNull String viewFqcn, int index) {
    254         return insertOrAppend(viewFqcn, index);
    255     }
    256 
    257     @Override
    258     public void removeChild(@NonNull INode node) {
    259         checkEditOK();
    260 
    261         ((NodeProxy) node).mNode.deleteXmlNode();
    262     }
    263 
    264     private INode insertOrAppend(String viewFqcn, int index) {
    265         checkEditOK();
    266 
    267         AndroidXmlEditor editor = mNode.getEditor();
    268         if (editor != null) {
    269             // Possibly replace the tag with a compatibility version if the
    270             // minimum SDK requires it
    271             IProject project = editor.getProject();
    272             if (project != null) {
    273                 viewFqcn = SupportLibraryHelper.getTagFor(project, viewFqcn);
    274             }
    275         }
    276 
    277         // Find the descriptor for this FQCN
    278         ViewElementDescriptor vd = getFqcnViewDescriptor(viewFqcn);
    279         if (vd == null) {
    280             warnPrintf("Can't create a new %s element", viewFqcn);
    281             return null;
    282         }
    283 
    284         final UiElementNode uiNew;
    285         if (index == -1) {
    286             // Append at the end.
    287             uiNew = mNode.appendNewUiChild(vd);
    288         } else {
    289             // Insert at the requested position or at the end.
    290             int n = mNode.getUiChildren().size();
    291             if (index < 0 || index >= n) {
    292                 uiNew = mNode.appendNewUiChild(vd);
    293             } else {
    294                 uiNew = mNode.insertNewUiChild(index, vd);
    295             }
    296         }
    297 
    298         // Set default attributes -- but only for new widgets (not when moving or copying)
    299         RulesEngine engine = null;
    300         LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(editor);
    301         if (delegate != null) {
    302             engine = delegate.getRulesEngine();
    303         }
    304         if (engine == null || engine.getInsertType().isCreate()) {
    305             // TODO: This should probably use IViewRule#getDefaultAttributes() at some point
    306             DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);
    307         }
    308 
    309         Node xmlNode = uiNew.createXmlNode();
    310 
    311         if (!(uiNew instanceof UiViewElementNode) || xmlNode == null) {
    312             // Both things are not supposed to happen. When they do, we're in big trouble.
    313             // We don't really know how to revert the state at this point and the UI model is
    314             // now out of sync with the XML model.
    315             // Panic ensues.
    316             // The best bet is to abort now. The edit wrapper will release the edit and the
    317             // XML/UI should get reloaded properly (with a likely invalid XML.)
    318             warnPrintf("Failed to create a new %s element", viewFqcn);
    319             throw new RuntimeException("XML node creation failed."); //$NON-NLS-1$
    320         }
    321 
    322         UiViewElementNode uiNewView = (UiViewElementNode) uiNew;
    323         NodeProxy newNode = mFactory.create(uiNewView);
    324 
    325         if (engine != null) {
    326             engine.callCreateHooks(editor, this, newNode, null);
    327         }
    328 
    329         return newNode;
    330     }
    331 
    332     @Override
    333     public boolean setAttribute(
    334             @Nullable String uri,
    335             @NonNull String name,
    336             @Nullable String value) {
    337         checkEditOK();
    338         UiAttributeNode attr = mNode.setAttributeValue(name, uri, value, true /* override */);
    339 
    340         if (uri == null) {
    341             uri = ""; //$NON-NLS-1$
    342         }
    343 
    344         Map<String, String> map = null;
    345         if (mPendingAttributes == null) {
    346             // Small initial size: we don't expect many different namespaces
    347             mPendingAttributes = new HashMap<String, Map<String, String>>(3);
    348         } else {
    349             map = mPendingAttributes.get(uri);
    350         }
    351         if (map == null) {
    352             map = new HashMap<String, String>();
    353             mPendingAttributes.put(uri, map);
    354         }
    355         map.put(name, value);
    356 
    357         return attr != null;
    358     }
    359 
    360     @Override
    361     public String getStringAttr(@Nullable String uri, @NonNull String attrName) {
    362         UiElementNode uiNode = mNode;
    363 
    364         if (attrName == null) {
    365             return null;
    366         }
    367 
    368         if (mPendingAttributes != null) {
    369             Map<String, String> map = mPendingAttributes.get(uri == null ? "" : uri); //$NON-NLS-1$
    370             if (map != null) {
    371                 String value = map.get(attrName);
    372                 if (value != null) {
    373                     return value;
    374                 }
    375             }
    376         }
    377 
    378         if (uiNode.getXmlNode() != null) {
    379             Node xmlNode = uiNode.getXmlNode();
    380             if (xmlNode != null) {
    381                 NamedNodeMap nodeAttributes = xmlNode.getAttributes();
    382                 if (nodeAttributes != null) {
    383                     Node attr = nodeAttributes.getNamedItemNS(uri, attrName);
    384                     if (attr != null) {
    385                         return attr.getNodeValue();
    386                     }
    387                 }
    388             }
    389         }
    390         return null;
    391     }
    392 
    393     @Override
    394     public IAttributeInfo getAttributeInfo(@Nullable String uri, @NonNull String attrName) {
    395         UiElementNode uiNode = mNode;
    396 
    397         if (attrName == null) {
    398             return null;
    399         }
    400 
    401         for (AttributeDescriptor desc : uiNode.getAttributeDescriptors()) {
    402             String dUri = desc.getNamespaceUri();
    403             String dName = desc.getXmlLocalName();
    404             if ((uri == null && dUri == null) || (uri != null && uri.equals(dUri))) {
    405                 if (attrName.equals(dName)) {
    406                     return desc.getAttributeInfo();
    407                 }
    408             }
    409         }
    410 
    411         return null;
    412     }
    413 
    414     @Override
    415     public @NonNull IAttributeInfo[] getDeclaredAttributes() {
    416 
    417         AttributeDescriptor[] descs = mNode.getAttributeDescriptors();
    418         int n = descs.length;
    419         IAttributeInfo[] infos = new AttributeInfo[n];
    420 
    421         for (int i = 0; i < n; i++) {
    422             infos[i] = descs[i].getAttributeInfo();
    423         }
    424 
    425         return infos;
    426     }
    427 
    428     @Override
    429     public @NonNull List<String> getAttributeSources() {
    430         ElementDescriptor descriptor = mNode.getDescriptor();
    431         if (descriptor instanceof ViewElementDescriptor) {
    432             return ((ViewElementDescriptor) descriptor).getAttributeSources();
    433         } else {
    434             return Collections.emptyList();
    435         }
    436     }
    437 
    438     @Override
    439     public @NonNull IAttribute[] getLiveAttributes() {
    440         UiElementNode uiNode = mNode;
    441 
    442         if (uiNode.getXmlNode() != null) {
    443             Node xmlNode = uiNode.getXmlNode();
    444             if (xmlNode != null) {
    445                 NamedNodeMap nodeAttributes = xmlNode.getAttributes();
    446                 if (nodeAttributes != null) {
    447 
    448                     int n = nodeAttributes.getLength();
    449                     IAttribute[] result = new IAttribute[n];
    450                     for (int i = 0; i < n; i++) {
    451                         Node attr = nodeAttributes.item(i);
    452                         String uri = attr.getNamespaceURI();
    453                         String name = attr.getLocalName();
    454                         String value = attr.getNodeValue();
    455 
    456                         result[i] = new SimpleAttribute(uri, name, value);
    457                     }
    458                     return result;
    459                 }
    460             }
    461         }
    462 
    463         return new IAttribute[0];
    464 
    465     }
    466 
    467     @Override
    468     public String toString() {
    469         return "NodeProxy [node=" + mNode + ", bounds=" + mBounds + "]";
    470     }
    471 
    472     // --- internal helpers ---
    473 
    474     /**
    475      * Helper methods that returns a {@link ViewElementDescriptor} for the requested FQCN.
    476      * Will return null if we can't find that FQCN or we lack the editor/data/descriptors info
    477      * (which shouldn't really happen since at this point the SDK should be fully loaded and
    478      * isn't reloading, or we wouldn't be here editing XML for a layout rule.)
    479      */
    480     private ViewElementDescriptor getFqcnViewDescriptor(String fqcn) {
    481         LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(mNode.getEditor());
    482         if (delegate != null) {
    483             return delegate.getFqcnViewDescriptor(fqcn);
    484         }
    485 
    486         return null;
    487     }
    488 
    489     private void warnPrintf(String msg, Object...params) {
    490         AdtPlugin.printToConsole(
    491                 mNode == null ? "" : mNode.getDescriptor().getXmlLocalName(),
    492                 String.format(msg, params)
    493                 );
    494     }
    495 
    496     /**
    497      * If there are any pending changes in these nodes, apply them now
    498      *
    499      * @return true if any modifications were made
    500      */
    501     public boolean applyPendingChanges() {
    502         boolean modified = false;
    503 
    504         // Flush all pending attributes
    505         if (mPendingAttributes != null) {
    506             mNode.commitDirtyAttributesToXml();
    507             modified = true;
    508             mPendingAttributes = null;
    509 
    510         }
    511         for (INode child : getChildren()) {
    512             modified |= ((NodeProxy) child).applyPendingChanges();
    513         }
    514 
    515         return modified;
    516     }
    517 }
    518