1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 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 18 package com.android.ide.eclipse.adt.internal.editors.ui.tree; 19 20 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; 21 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; 22 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor.Mandatory; 23 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; 24 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 25 26 import org.eclipse.jface.dialogs.MessageDialog; 27 import org.eclipse.jface.viewers.ILabelProvider; 28 import org.eclipse.swt.widgets.Shell; 29 import org.w3c.dom.Document; 30 import org.w3c.dom.Node; 31 32 import java.util.List; 33 34 /** 35 * Performs basic actions on an XML tree: add node, remove node, move up/down. 36 */ 37 public abstract class UiActions implements ICommitXml { 38 39 public UiActions() { 40 } 41 42 //--------------------- 43 // Actual implementations must override these to provide specific hooks 44 45 /** Returns the UiDocumentNode for the current model. */ 46 abstract protected UiElementNode getRootNode(); 47 48 /** Commits pending data before the XML model is modified. */ 49 abstract public void commitPendingXmlChanges(); 50 51 /** 52 * Utility method to select an outline item based on its model node 53 * 54 * @param uiNode The node to select. Can be null (in which case nothing should happen) 55 */ 56 abstract protected void selectUiNode(UiElementNode uiNode); 57 58 //--------------------- 59 60 /** 61 * Called when the "Add..." button next to the tree view is selected. 62 * <p/> 63 * This simplified version of doAdd does not support descriptor filters and creates 64 * a new {@link UiModelTreeLabelProvider} for each call. 65 */ 66 public void doAdd(UiElementNode uiNode, Shell shell) { 67 doAdd(uiNode, null /* descriptorFilters */, shell, new UiModelTreeLabelProvider()); 68 } 69 70 /** 71 * Called when the "Add..." button next to the tree view is selected. 72 * 73 * Displays a selection dialog that lets the user select which kind of node 74 * to create, depending on the current selection. 75 */ 76 public void doAdd(UiElementNode uiNode, 77 ElementDescriptor[] descriptorFilters, 78 Shell shell, ILabelProvider labelProvider) { 79 // If the root node is a document with already a root, use it as the root node 80 UiElementNode rootNode = getRootNode(); 81 if (rootNode instanceof UiDocumentNode && rootNode.getUiChildren().size() > 0) { 82 rootNode = rootNode.getUiChildren().get(0); 83 } 84 85 NewItemSelectionDialog dlg = new NewItemSelectionDialog( 86 shell, 87 labelProvider, 88 descriptorFilters, 89 uiNode, rootNode); 90 dlg.open(); 91 Object[] results = dlg.getResult(); 92 if (results != null && results.length > 0) { 93 addElement(dlg.getChosenRootNode(), null, (ElementDescriptor) results[0], 94 true /*updateLayout*/); 95 } 96 } 97 98 /** 99 * Adds a new XML element based on the {@link ElementDescriptor} to the given parent 100 * {@link UiElementNode}, and then select it. 101 * <p/> 102 * If the parent is a document root which already contains a root element, the inner 103 * root element is used as the actual parent. This ensure you can't create a broken 104 * XML file with more than one root element. 105 * <p/> 106 * If a sibling is given and that sibling has the same parent, the new node is added 107 * right after that sibling. Otherwise the new node is added at the end of the parent 108 * child list. 109 * 110 * @param uiParent An existing UI node or null to add to the tree root 111 * @param uiSibling An existing UI node before which to insert the new node. Can be null. 112 * @param descriptor The descriptor of the element to add 113 * @param updateLayout True if layout attributes should be set 114 * @return The new {@link UiElementNode} or null. 115 */ 116 public UiElementNode addElement(UiElementNode uiParent, 117 UiElementNode uiSibling, 118 ElementDescriptor descriptor, 119 boolean updateLayout) { 120 if (uiParent instanceof UiDocumentNode && uiParent.getUiChildren().size() > 0) { 121 uiParent = uiParent.getUiChildren().get(0); 122 } 123 if (uiSibling != null && uiSibling.getUiParent() != uiParent) { 124 uiSibling = null; 125 } 126 127 UiElementNode uiNew = addNewTreeElement(uiParent, uiSibling, descriptor, updateLayout); 128 selectUiNode(uiNew); 129 130 return uiNew; 131 } 132 133 /** 134 * Called when the "Remove" button is selected. 135 * 136 * If the tree has a selection, remove it. 137 * This simply deletes the XML node attached to the UI node: when the XML model fires the 138 * update event, the tree will get refreshed. 139 */ 140 public void doRemove(final List<UiElementNode> nodes, Shell shell) { 141 142 if (nodes == null || nodes.size() == 0) { 143 return; 144 } 145 146 final int len = nodes.size(); 147 148 StringBuilder sb = new StringBuilder(); 149 for (UiElementNode node : nodes) { 150 sb.append("\n- "); //$NON-NLS-1$ 151 sb.append(node.getBreadcrumbTrailDescription(false /* include_root */)); 152 } 153 154 if (MessageDialog.openQuestion(shell, 155 len > 1 ? "Remove elements from Android XML" // title 156 : "Remove element from Android XML", 157 String.format("Do you really want to remove %1$s?", sb.toString()))) { 158 commitPendingXmlChanges(); 159 getRootNode().getEditor().wrapEditXmlModel(new Runnable() { 160 public void run() { 161 UiElementNode previous = null; 162 UiElementNode parent = null; 163 164 for (int i = len - 1; i >= 0; i--) { 165 UiElementNode node = nodes.get(i); 166 previous = node.getUiPreviousSibling(); 167 parent = node.getUiParent(); 168 169 // delete node 170 node.deleteXmlNode(); 171 } 172 173 // try to select the last previous sibling or the last parent 174 if (previous != null) { 175 selectUiNode(previous); 176 } else if (parent != null) { 177 selectUiNode(parent); 178 } 179 } 180 }); 181 } 182 } 183 184 /** 185 * Called when the "Up" button is selected. 186 * <p/> 187 * If the tree has a selection, move it up, either in the child list or as the last child 188 * of the previous parent. 189 */ 190 public void doUp( 191 final List<UiElementNode> uiNodes, 192 final ElementDescriptor[] descriptorFilters) { 193 if (uiNodes == null || uiNodes.size() < 1) { 194 return; 195 } 196 197 final Node[] selectXmlNode = { null }; 198 final UiElementNode[] uiLastNode = { null }; 199 final UiElementNode[] uiSearchRoot = { null }; 200 201 commitPendingXmlChanges(); 202 getRootNode().getEditor().wrapEditXmlModel(new Runnable() { 203 public void run() { 204 for (int i = 0; i < uiNodes.size(); i++) { 205 UiElementNode uiNode = uiLastNode[0] = uiNodes.get(i); 206 doUpInternal( 207 uiNode, 208 descriptorFilters, 209 selectXmlNode, 210 uiSearchRoot, 211 false /*testOnly*/); 212 } 213 } 214 }); 215 216 assert uiLastNode[0] != null; // tell Eclipse this can't be null below 217 218 if (selectXmlNode[0] == null) { 219 // The XML node has not been moved, we can just select the same UI node 220 selectUiNode(uiLastNode[0]); 221 } else { 222 // The XML node has moved. At this point the UI model has been reloaded 223 // and the XML node has been affected to a new UI node. Find that new UI 224 // node and select it. 225 if (uiSearchRoot[0] == null) { 226 uiSearchRoot[0] = uiLastNode[0].getUiRoot(); 227 } 228 if (uiSearchRoot[0] != null) { 229 selectUiNode(uiSearchRoot[0].findXmlNode(selectXmlNode[0])); 230 } 231 } 232 } 233 234 /** 235 * Checks whether the "up" action can be performed on all items. 236 * 237 * @return True if the up action can be carried on *all* items. 238 */ 239 public boolean canDoUp( 240 List<UiElementNode> uiNodes, 241 ElementDescriptor[] descriptorFilters) { 242 if (uiNodes == null || uiNodes.size() < 1) { 243 return false; 244 } 245 246 final Node[] selectXmlNode = { null }; 247 final UiElementNode[] uiSearchRoot = { null }; 248 249 commitPendingXmlChanges(); 250 251 for (int i = 0; i < uiNodes.size(); i++) { 252 if (!doUpInternal( 253 uiNodes.get(i), 254 descriptorFilters, 255 selectXmlNode, 256 uiSearchRoot, 257 true /*testOnly*/)) { 258 return false; 259 } 260 } 261 262 return true; 263 } 264 265 private boolean doUpInternal( 266 UiElementNode uiNode, 267 ElementDescriptor[] descriptorFilters, 268 Node[] outSelectXmlNode, 269 UiElementNode[] outUiSearchRoot, 270 boolean testOnly) { 271 // the node will move either up to its parent or grand-parent 272 outUiSearchRoot[0] = uiNode.getUiParent(); 273 if (outUiSearchRoot[0] != null && outUiSearchRoot[0].getUiParent() != null) { 274 outUiSearchRoot[0] = outUiSearchRoot[0].getUiParent(); 275 } 276 Node xmlNode = uiNode.getXmlNode(); 277 ElementDescriptor nodeDesc = uiNode.getDescriptor(); 278 if (xmlNode == null || nodeDesc == null) { 279 return false; 280 } 281 UiElementNode uiParentNode = uiNode.getUiParent(); 282 Node xmlParent = uiParentNode == null ? null : uiParentNode.getXmlNode(); 283 if (xmlParent == null) { 284 return false; 285 } 286 287 UiElementNode uiPrev = uiNode.getUiPreviousSibling(); 288 289 // Only accept a sibling that has an XML attached and 290 // is part of the allowed descriptor filters. 291 while (uiPrev != null && 292 (uiPrev.getXmlNode() == null || !matchDescFilter(descriptorFilters, uiPrev))) { 293 uiPrev = uiPrev.getUiPreviousSibling(); 294 } 295 296 if (uiPrev != null && uiPrev.getXmlNode() != null) { 297 // This node is not the first one of the parent. 298 Node xmlPrev = uiPrev.getXmlNode(); 299 if (uiPrev.getDescriptor().acceptChild(nodeDesc)) { 300 // If the previous sibling can accept this child, then it 301 // is inserted at the end of the children list. 302 if (testOnly) { 303 return true; 304 } 305 xmlPrev.appendChild(xmlParent.removeChild(xmlNode)); 306 outSelectXmlNode[0] = xmlNode; 307 } else { 308 // This node is not the first one of the parent, so it can be 309 // removed and then inserted before its previous sibling. 310 if (testOnly) { 311 return true; 312 } 313 xmlParent.insertBefore( 314 xmlParent.removeChild(xmlNode), 315 xmlPrev); 316 outSelectXmlNode[0] = xmlNode; 317 } 318 } else if (uiParentNode != null && !(xmlParent instanceof Document)) { 319 UiElementNode uiGrandParent = uiParentNode.getUiParent(); 320 Node xmlGrandParent = uiGrandParent == null ? null : uiGrandParent.getXmlNode(); 321 ElementDescriptor grandDesc = 322 uiGrandParent == null ? null : uiGrandParent.getDescriptor(); 323 324 if (xmlGrandParent != null && 325 !(xmlGrandParent instanceof Document) && 326 grandDesc != null && 327 grandDesc.acceptChild(nodeDesc)) { 328 // If the node is the first one of the child list of its 329 // parent, move it up in the hierarchy as previous sibling 330 // to the parent. This is only possible if the parent of the 331 // parent is not a document. 332 // The parent node must actually accept this kind of child. 333 334 if (testOnly) { 335 return true; 336 } 337 xmlGrandParent.insertBefore( 338 xmlParent.removeChild(xmlNode), 339 xmlParent); 340 outSelectXmlNode[0] = xmlNode; 341 } 342 } 343 344 return false; 345 } 346 347 private boolean matchDescFilter( 348 ElementDescriptor[] descriptorFilters, 349 UiElementNode uiNode) { 350 if (descriptorFilters == null || descriptorFilters.length < 1) { 351 return true; 352 } 353 354 ElementDescriptor desc = uiNode.getDescriptor(); 355 356 for (ElementDescriptor filter : descriptorFilters) { 357 if (filter.equals(desc)) { 358 return true; 359 } 360 } 361 return false; 362 } 363 364 /** 365 * Called when the "Down" button is selected. 366 * 367 * If the tree has a selection, move it down, either in the same child list or as the 368 * first child of the next parent. 369 */ 370 public void doDown( 371 final List<UiElementNode> nodes, 372 final ElementDescriptor[] descriptorFilters) { 373 if (nodes == null || nodes.size() < 1) { 374 return; 375 } 376 377 final Node[] selectXmlNode = { null }; 378 final UiElementNode[] uiLastNode = { null }; 379 final UiElementNode[] uiSearchRoot = { null }; 380 381 commitPendingXmlChanges(); 382 getRootNode().getEditor().wrapEditXmlModel(new Runnable() { 383 public void run() { 384 for (int i = nodes.size() - 1; i >= 0; i--) { 385 final UiElementNode node = uiLastNode[0] = nodes.get(i); 386 doDownInternal( 387 node, 388 descriptorFilters, 389 selectXmlNode, 390 uiSearchRoot, 391 false /*testOnly*/); 392 } 393 } 394 }); 395 396 assert uiLastNode[0] != null; // tell Eclipse this can't be null below 397 398 if (selectXmlNode[0] == null) { 399 // The XML node has not been moved, we can just select the same UI node 400 selectUiNode(uiLastNode[0]); 401 } else { 402 // The XML node has moved. At this point the UI model has been reloaded 403 // and the XML node has been affected to a new UI node. Find that new UI 404 // node and select it. 405 if (uiSearchRoot[0] == null) { 406 uiSearchRoot[0] = uiLastNode[0].getUiRoot(); 407 } 408 if (uiSearchRoot[0] != null) { 409 selectUiNode(uiSearchRoot[0].findXmlNode(selectXmlNode[0])); 410 } 411 } 412 } 413 414 /** 415 * Checks whether the "down" action can be performed on all items. 416 * 417 * @return True if the down action can be carried on *all* items. 418 */ 419 public boolean canDoDown( 420 List<UiElementNode> uiNodes, 421 ElementDescriptor[] descriptorFilters) { 422 if (uiNodes == null || uiNodes.size() < 1) { 423 return false; 424 } 425 426 final Node[] selectXmlNode = { null }; 427 final UiElementNode[] uiSearchRoot = { null }; 428 429 commitPendingXmlChanges(); 430 431 for (int i = 0; i < uiNodes.size(); i++) { 432 if (!doDownInternal( 433 uiNodes.get(i), 434 descriptorFilters, 435 selectXmlNode, 436 uiSearchRoot, 437 true /*testOnly*/)) { 438 return false; 439 } 440 } 441 442 return true; 443 } 444 445 private boolean doDownInternal( 446 UiElementNode uiNode, 447 ElementDescriptor[] descriptorFilters, 448 Node[] outSelectXmlNode, 449 UiElementNode[] outUiSearchRoot, 450 boolean testOnly) { 451 // the node will move either down to its parent or grand-parent 452 outUiSearchRoot[0] = uiNode.getUiParent(); 453 if (outUiSearchRoot[0] != null && outUiSearchRoot[0].getUiParent() != null) { 454 outUiSearchRoot[0] = outUiSearchRoot[0].getUiParent(); 455 } 456 457 Node xmlNode = uiNode.getXmlNode(); 458 ElementDescriptor nodeDesc = uiNode.getDescriptor(); 459 if (xmlNode == null || nodeDesc == null) { 460 return false; 461 } 462 UiElementNode uiParentNode = uiNode.getUiParent(); 463 Node xmlParent = uiParentNode == null ? null : uiParentNode.getXmlNode(); 464 if (xmlParent == null) { 465 return false; 466 } 467 468 UiElementNode uiNext = uiNode.getUiNextSibling(); 469 470 // Only accept a sibling that has an XML attached and 471 // is part of the allowed descriptor filters. 472 while (uiNext != null && 473 (uiNext.getXmlNode() == null || !matchDescFilter(descriptorFilters, uiNext))) { 474 uiNext = uiNext.getUiNextSibling(); 475 } 476 477 if (uiNext != null && uiNext.getXmlNode() != null) { 478 // This node is not the last one of the parent. 479 Node xmlNext = uiNext.getXmlNode(); 480 // If the next sibling is a node that can have children, though, 481 // then the node is inserted as the first child. 482 if (uiNext.getDescriptor().acceptChild(nodeDesc)) { 483 if (testOnly) { 484 return true; 485 } 486 // Note: insertBefore works as append if the ref node is 487 // null, i.e. when the node doesn't have children yet. 488 xmlNext.insertBefore( 489 xmlParent.removeChild(xmlNode), 490 xmlNext.getFirstChild()); 491 outSelectXmlNode[0] = xmlNode; 492 } else { 493 // This node is not the last one of the parent, so it can be 494 // removed and then inserted after its next sibling. 495 496 if (testOnly) { 497 return true; 498 } 499 // Insert "before after next" ;-) 500 xmlParent.insertBefore( 501 xmlParent.removeChild(xmlNode), 502 xmlNext.getNextSibling()); 503 outSelectXmlNode[0] = xmlNode; 504 } 505 } else if (uiParentNode != null && !(xmlParent instanceof Document)) { 506 UiElementNode uiGrandParent = uiParentNode.getUiParent(); 507 Node xmlGrandParent = uiGrandParent == null ? null : uiGrandParent.getXmlNode(); 508 ElementDescriptor grandDesc = 509 uiGrandParent == null ? null : uiGrandParent.getDescriptor(); 510 511 if (xmlGrandParent != null && 512 !(xmlGrandParent instanceof Document) && 513 grandDesc != null && 514 grandDesc.acceptChild(nodeDesc)) { 515 // This node is the last node of its parent. 516 // If neither the parent nor the grandparent is a document, 517 // then the node can be inserted right after the parent. 518 // The parent node must actually accept this kind of child. 519 if (testOnly) { 520 return true; 521 } 522 xmlGrandParent.insertBefore( 523 xmlParent.removeChild(xmlNode), 524 xmlParent.getNextSibling()); 525 outSelectXmlNode[0] = xmlNode; 526 } 527 } 528 529 return false; 530 } 531 532 //--------------------- 533 534 /** 535 * Adds a new element of the given descriptor's type to the given UI parent node. 536 * 537 * This actually creates the corresponding XML node in the XML model, which in turn 538 * will refresh the current tree view. 539 * 540 * @param uiParent An existing UI node or null to add to the tree root 541 * @param uiSibling An existing UI node to insert right before. Can be null. 542 * @param descriptor The descriptor of the element to add 543 * @param updateLayout True if layout attributes should be set 544 * @return The {@link UiElementNode} that has been added to the UI tree. 545 */ 546 private UiElementNode addNewTreeElement(UiElementNode uiParent, 547 UiElementNode uiSibling, 548 ElementDescriptor descriptor, 549 final boolean updateLayout) { 550 commitPendingXmlChanges(); 551 552 List<UiElementNode> uiChildren = uiParent.getUiChildren(); 553 int n = uiChildren.size(); 554 555 // The default is to append at the end of the list. 556 int index = n; 557 558 if (uiSibling != null) { 559 // Try to find the requested sibling. 560 index = uiChildren.indexOf(uiSibling); 561 if (index < 0) { 562 // This sibling didn't exist. Should not happen but compensate 563 // by simply adding to the end of the list. 564 uiSibling = null; 565 index = n; 566 } 567 } 568 569 if (uiSibling == null) { 570 // If we don't require any specific position, make sure to insert before the 571 // first mandatory_last descriptor's position, if any. 572 573 for (int i = 0; i < n; i++) { 574 UiElementNode uiChild = uiChildren.get(i); 575 if (uiChild.getDescriptor().getMandatory() == Mandatory.MANDATORY_LAST) { 576 index = i; 577 break; 578 } 579 } 580 } 581 582 final UiElementNode uiNew = uiParent.insertNewUiChild(index, descriptor); 583 UiElementNode rootNode = getRootNode(); 584 585 rootNode.getEditor().wrapEditXmlModel(new Runnable() { 586 public void run() { 587 DescriptorsUtils.setDefaultLayoutAttributes(uiNew, updateLayout); 588 uiNew.createXmlNode(); 589 } 590 }); 591 return uiNew; 592 } 593 } 594