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     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