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