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