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