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