• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.eclipse.adt.AdtPlugin;
20 import com.android.ide.eclipse.adt.editors.layout.gscripts.IAttributeInfo;
21 import com.android.ide.eclipse.adt.editors.layout.gscripts.INode;
22 import com.android.ide.eclipse.adt.editors.layout.gscripts.Rect;
23 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
24 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
25 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
26 import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
27 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
28 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
29 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
30 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
31 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleAttribute;
32 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
33 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
34 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
35 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
36 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
37 
38 import org.eclipse.swt.graphics.Rectangle;
39 import org.w3c.dom.NamedNodeMap;
40 import org.w3c.dom.Node;
41 
42 import groovy.lang.Closure;
43 
44 import java.util.ArrayList;
45 import java.util.HashSet;
46 import java.util.List;
47 import java.util.Set;
48 
49 /**
50  *
51  */
52 public class NodeProxy implements INode {
53 
54     private final UiViewElementNode mNode;
55     private final Rect mBounds;
56     private final NodeFactory mFactory;
57 
58     /**
59      * Creates a new {@link INode} that wraps an {@link UiViewElementNode} that is
60      * actually valid in the current UI/XML model. The view may not be part of the canvas
61      * yet (e.g. if it has just been dynamically added and the canvas hasn't reloaded yet.)
62      * <p/>
63      * This method is package protected. To create a node, please use {@link NodeFactory} instead.
64      *
65      * @param uiNode The node to wrap.
66      * @param bounds The bounds of a the view in the canvas. Must be either: <br/>
67      *   - a valid rect for a view that is actually in the canvas <br/>
68      *   - <b>*or*</b> null (or an invalid rect) for a view that has just been added dynamically
69      *   to the model. We never store a null bounds rectangle in the node, a null rectangle
70      *   will be converted to an invalid rectangle.
71      * @param factory A {@link NodeFactory} to create unique children nodes.
72      */
NodeProxy(UiViewElementNode uiNode, Rectangle bounds, NodeFactory factory)73     /*package*/ NodeProxy(UiViewElementNode uiNode, Rectangle bounds, NodeFactory factory) {
74         mNode = uiNode;
75         mFactory = factory;
76         if (bounds == null) {
77             mBounds = new Rect();
78         } else {
79             mBounds = new Rect(bounds);
80         }
81     }
82 
debugPrintf(String msg, Object...params)83     public void debugPrintf(String msg, Object...params) {
84         AdtPlugin.printToConsole(
85                 mNode == null ? "Groovy" : mNode.getDescriptor().getXmlLocalName() + ".groovy",
86                 String.format(msg, params)
87                 );
88     }
89 
getBounds()90     public Rect getBounds() {
91         return mBounds;
92     }
93 
94 
95     /**
96      * Updates the bounds of this node proxy. Bounds cannot be null, but it can be invalid.
97      * This is a package-protected method, only the {@link NodeFactory} uses this method.
98      */
setBounds(Rectangle bounds)99     /*package*/ void setBounds(Rectangle bounds) {
100         mBounds.set(bounds);
101     }
102 
getNode()103     /* package */ UiViewElementNode getNode() {
104         return mNode;
105     }
106 
107 
108     // ---- Hierarchy handling ----
109 
110 
getRoot()111     public INode getRoot() {
112         if (mNode != null) {
113             UiElementNode p = mNode.getUiRoot();
114             // The node root should be a document. Instead what we really mean to
115             // return is the top level view element.
116             if (p instanceof UiDocumentNode) {
117                 List<UiElementNode> children = p.getUiChildren();
118                 if (children.size() > 0) {
119                     p = children.get(0);
120                 }
121             }
122 
123             // Cope with a badly structured XML layout
124             while (p != null && !(p instanceof UiViewElementNode)) {
125                 p = p.getUiNextSibling();
126             }
127 
128             if (p instanceof UiViewElementNode) {
129                 return mFactory.create((UiViewElementNode) p);
130             }
131         }
132 
133         return null;
134     }
135 
getParent()136     public INode getParent() {
137         if (mNode != null) {
138             UiElementNode p = mNode.getUiParent();
139             if (p instanceof UiViewElementNode) {
140                 return mFactory.create((UiViewElementNode) p);
141             }
142         }
143 
144         return null;
145     }
146 
getChildren()147     public INode[] getChildren() {
148         if (mNode != null) {
149             ArrayList<INode> nodes = new ArrayList<INode>();
150             for (UiElementNode uiChild : mNode.getUiChildren()) {
151                 if (uiChild instanceof UiViewElementNode) {
152                     nodes.add(mFactory.create((UiViewElementNode) uiChild));
153                 }
154             }
155 
156             return nodes.toArray(new INode[nodes.size()]);
157         }
158 
159         return new INode[0];
160     }
161 
162 
163     // ---- XML Editing ---
164 
editXml(String undoName, final Closure c)165     public void editXml(String undoName, final Closure c) {
166         final AndroidXmlEditor editor = mNode.getEditor();
167 
168         if (editor.isEditXmlModelPending()) {
169             throw new RuntimeException("Error: calls to INode.editXml cannot be nested!");
170         }
171 
172         if (editor instanceof LayoutEditor) {
173             // Create an undo wrapper, which takes a runnable
174             ((LayoutEditor) editor).wrapUndoRecording(
175                     undoName,
176                     new Runnable() {
177                         public void run() {
178                             // Create an edit-XML wrapper, which takes a runnable
179                             editor.editXmlModel(new Runnable() {
180                                 public void run() {
181                                     // Here editor.isEditXmlModelPending returns true and it
182                                     // is safe to edit the model using any method from INode.
183 
184                                     // Finally execute the closure that will act on the XML
185                                     c.call(NodeProxy.this);
186                                 }
187                             });
188                         }
189                     });
190         }
191     }
192 
checkEditOK()193     private void checkEditOK() {
194         final AndroidXmlEditor editor = mNode.getEditor();
195         if (!editor.isEditXmlModelPending()) {
196             throw new RuntimeException("Error: XML edit call without using INode.editXml!");
197         }
198     }
199 
appendChild(String viewFqcn)200     public INode appendChild(String viewFqcn) {
201         checkEditOK();
202 
203         // Find the descriptor for this FQCN
204         ViewElementDescriptor vd = getFqcnViewDescritor(viewFqcn);
205         if (vd == null) {
206             debugPrintf("Can't create a new %s element", viewFqcn);
207             return null;
208         }
209 
210         // Append at the end.
211         UiElementNode uiNew = mNode.appendNewUiChild(vd);
212 
213         // TODO we probably want to defer that to the GRE to use IViewRule#getDefaultAttributes()
214         DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);
215 
216         Node xmlNode = uiNew.createXmlNode();
217 
218         if (!(uiNew instanceof UiViewElementNode) || xmlNode == null) {
219             // Both things are not supposed to happen. When they do, we're in big trouble.
220             // We don't really know how to revert the state at this point and the UI model is
221             // now out of sync with the XML model.
222             // Panic ensues.
223             // The best bet is to abort now. The edit wrapper will release the edit and the
224             // XML/UI should get reloaded properly (with a likely invalid XML.)
225             debugPrintf("Failed to create a new %s element", viewFqcn);
226             throw new RuntimeException("XML node creation failed."); //$NON-NLS-1$
227         }
228 
229         return mFactory.create((UiViewElementNode) uiNew);
230     }
231 
insertChildAt(String viewFqcn, int index)232     public INode insertChildAt(String viewFqcn, int index) {
233         checkEditOK();
234 
235         // Find the descriptor for this FQCN
236         ViewElementDescriptor vd = getFqcnViewDescritor(viewFqcn);
237         if (vd == null) {
238             debugPrintf("Can't create a new %s element", viewFqcn);
239             return null;
240         }
241 
242         // Insert at the requested position or at the end.
243         int n = mNode.getUiChildren().size();
244         UiElementNode uiNew = null;
245         if (index < 0 || index >= n) {
246             uiNew = mNode.appendNewUiChild(vd);
247         } else {
248             uiNew = mNode.insertNewUiChild(index, vd);
249         }
250 
251         // TODO we probably want to defer that to the GRE to use IViewRule#getDefaultAttributes()
252         DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);
253 
254         Node xmlNode = uiNew.createXmlNode();
255 
256         if (!(uiNew instanceof UiViewElementNode) || xmlNode == null) {
257             // Both things are not supposed to happen. When they do, we're in big trouble.
258             // We don't really know how to revert the state at this point and the UI model is
259             // now out of sync with the XML model.
260             // Panic ensues.
261             // The best bet is to abort now. The edit wrapper will release the edit and the
262             // XML/UI should get reloaded properly (with a likely invalid XML.)
263             debugPrintf("Failed to create a new %s element", viewFqcn);
264             throw new RuntimeException("XML node creation failed."); //$NON-NLS-1$
265         }
266 
267         return mFactory.create((UiViewElementNode) uiNew);
268     }
269 
setAttribute(String uri, String name, String value)270     public boolean setAttribute(String uri, String name, String value) {
271         checkEditOK();
272 
273         UiAttributeNode attr = mNode.setAttributeValue(name, uri, value, true /* override */);
274         mNode.commitDirtyAttributesToXml();
275 
276         return attr != null;
277     }
278 
getStringAttr(String uri, String attrName)279     public String getStringAttr(String uri, String attrName) {
280         UiElementNode uiNode = mNode;
281 
282         if (attrName == null) {
283             return null;
284         }
285 
286         if (uiNode.getXmlNode() != null) {
287             Node xmlNode = uiNode.getXmlNode();
288             if (xmlNode != null) {
289                 NamedNodeMap nodeAttributes = xmlNode.getAttributes();
290                 if (nodeAttributes != null) {
291                     Node attr = nodeAttributes.getNamedItemNS(uri, attrName);
292                     if (attr != null) {
293                         return attr.getNodeValue();
294                     }
295                 }
296             }
297         }
298         return null;
299     }
300 
getAttributeInfo(String uri, String attrName)301     public IAttributeInfo getAttributeInfo(String uri, String attrName) {
302         UiElementNode uiNode = mNode;
303 
304         if (attrName == null) {
305             return null;
306         }
307 
308         for (AttributeDescriptor desc : uiNode.getAttributeDescriptors()) {
309             String dUri = desc.getNamespaceUri();
310             String dName = desc.getXmlLocalName();
311             if ((uri == null && dUri == null) || (uri != null && uri.equals(dUri))) {
312                 if (attrName.equals(dName)) {
313                     return desc.getAttributeInfo();
314                 }
315             }
316         }
317 
318         return null;
319     }
320 
getAttributes()321     public IAttribute[] getAttributes() {
322         UiElementNode uiNode = mNode;
323 
324         if (uiNode.getXmlNode() != null) {
325             Node xmlNode = uiNode.getXmlNode();
326             if (xmlNode != null) {
327                 NamedNodeMap nodeAttributes = xmlNode.getAttributes();
328                 if (nodeAttributes != null) {
329 
330                     int n = nodeAttributes.getLength();
331                     IAttribute[] result = new IAttribute[n];
332                     for (int i = 0; i < n; i++) {
333                         Node attr = nodeAttributes.item(i);
334                         String uri = attr.getNamespaceURI();
335                         String name = attr.getLocalName();
336                         String value = attr.getNodeValue();
337 
338                         result[i] = new SimpleAttribute(uri, name, value);
339                     }
340                     return result;
341                 }
342             }
343         }
344         return null;
345 
346     }
347 
348 
349     // --- internal helpers ---
350 
351     /**
352      * Helper methods that returns a {@link ViewElementDescriptor} for the requested FQCN.
353      * Will return null if we can't find that FQCN or we lack the editor/data/descriptors info
354      * (which shouldn't really happen since at this point the SDK should be fully loaded and
355      * isn't reloading, or we wouldn't be here editing XML for a groovy script.)
356      */
getFqcnViewDescritor(String fqcn)357     private ViewElementDescriptor getFqcnViewDescritor(String fqcn) {
358         AndroidXmlEditor editor = mNode.getEditor();
359         if (editor != null) {
360             AndroidTargetData data = editor.getTargetData();
361             if (data != null) {
362                 LayoutDescriptors layoutDesc = data.getLayoutDescriptors();
363                 if (layoutDesc != null) {
364                     DocumentDescriptor docDesc = layoutDesc.getDescriptor();
365                     if (docDesc != null) {
366                         return internalFindFqcnViewDescritor(fqcn, docDesc.getChildren(), null);
367                     }
368                 }
369             }
370         }
371 
372         return null;
373     }
374 
375     /**
376      * Internal helper to recursively search for a {@link ViewElementDescriptor} that matches
377      * the requested FQCN.
378      *
379      * @param fqcn The target View FQCN to find.
380      * @param descriptors A list of cildren descriptors to iterate through.
381      * @param visited A set we use to remember which descriptors have already been visited,
382      *  necessary since the view descriptor hierarchy is cyclic.
383      * @return Either a matching {@link ViewElementDescriptor} or null.
384      */
internalFindFqcnViewDescritor(String fqcn, ElementDescriptor[] descriptors, Set<ElementDescriptor> visited)385     private ViewElementDescriptor internalFindFqcnViewDescritor(String fqcn,
386             ElementDescriptor[] descriptors,
387             Set<ElementDescriptor> visited) {
388         if (visited == null) {
389             visited = new HashSet<ElementDescriptor>();
390         }
391 
392         if (descriptors != null) {
393             for (ElementDescriptor desc : descriptors) {
394                 if (visited.add(desc)) {
395                     // Set.add() returns true if this a new element that was added to the set.
396                     // That means we haven't visited this descriptor yet.
397                     // We want a ViewElementDescriptor with a matching FQCN.
398                     if (desc instanceof ViewElementDescriptor &&
399                             fqcn.equals(((ViewElementDescriptor) desc).getFullClassName())) {
400                         return (ViewElementDescriptor) desc;
401                     }
402 
403                     // Visit its children
404                     ViewElementDescriptor vd =
405                         internalFindFqcnViewDescritor(fqcn, desc.getChildren(), visited);
406                     if (vd != null) {
407                         return vd;
408                     }
409                 }
410             }
411         }
412 
413         return null;
414     }
415 
416 }
417