Home | History | Annotate | Download | only in tree
      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