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