OSDN Git Service

Form editor up/down move accross siblings.
[android-x86/sdk.git] / eclipse / plugins / com.android.ide.eclipse.adt / src / com / android / ide / eclipse / adt / internal / editors / ui / tree / UiActions.java
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 }