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