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