• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
UiActions()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. */
getRootNode()46     abstract protected UiElementNode getRootNode();
47 
48     /** Commits pending data before the XML model is modified. */
49     @Override
commitPendingXmlChanges()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      */
selectUiNode(UiElementNode uiNode)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      */
doAdd(UiElementNode uiNode, Shell shell)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      */
doAdd(UiElementNode uiNode, ElementDescriptor[] descriptorFilters, Shell shell, ILabelProvider labelProvider)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      */
addElement(UiElementNode uiParent, UiElementNode uiSibling, ElementDescriptor descriptor, boolean updateLayout)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      */
doRemove(final List<UiElementNode> nodes, Shell shell)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      */
doUp( final List<UiElementNode> uiNodes, final ElementDescriptor[] descriptorFilters)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      */
canDoUp( List<UiElementNode> uiNodes, ElementDescriptor[] descriptorFilters)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 
doUpInternal( UiElementNode uiNode, ElementDescriptor[] descriptorFilters, Node[] outSelectXmlNode, UiElementNode[] outUiSearchRoot, boolean testOnly)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 
matchDescFilter( ElementDescriptor[] descriptorFilters, UiElementNode uiNode)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      */
doDown( final List<UiElementNode> nodes, final ElementDescriptor[] descriptorFilters)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      */
canDoDown( List<UiElementNode> uiNodes, ElementDescriptor[] descriptorFilters)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 
doDownInternal( UiElementNode uiNode, ElementDescriptor[] descriptorFilters, Node[] outSelectXmlNode, UiElementNode[] outUiSearchRoot, boolean testOnly)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      */
addNewTreeElement(UiElementNode uiParent, UiElementNode uiSibling, ElementDescriptor descriptor, final boolean updateLayout)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