• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
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 package com.android.ide.eclipse.adt.internal.editors.ui.tree;
18 
19 import com.android.ide.eclipse.adt.AdtPlugin;
20 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
21 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
22 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
23 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
24 
25 import org.eclipse.core.resources.IFile;
26 import org.eclipse.core.runtime.IStatus;
27 import org.eclipse.core.runtime.Status;
28 import org.eclipse.jface.viewers.ILabelProvider;
29 import org.eclipse.swt.SWT;
30 import org.eclipse.swt.events.SelectionAdapter;
31 import org.eclipse.swt.events.SelectionEvent;
32 import org.eclipse.swt.layout.GridData;
33 import org.eclipse.swt.widgets.Button;
34 import org.eclipse.swt.widgets.Composite;
35 import org.eclipse.swt.widgets.Control;
36 import org.eclipse.swt.widgets.Label;
37 import org.eclipse.swt.widgets.Shell;
38 import org.eclipse.ui.IEditorInput;
39 import org.eclipse.ui.dialogs.AbstractElementListSelectionDialog;
40 import org.eclipse.ui.dialogs.ISelectionStatusValidator;
41 import org.eclipse.ui.part.FileEditorInput;
42 
43 import java.util.Arrays;
44 import java.util.HashMap;
45 import java.util.Locale;
46 import java.util.Map;
47 import java.util.Map.Entry;
48 import java.util.TreeMap;
49 
50 /**
51  * A selection dialog to select the type of the new element node to
52  * create, either in the application node or the selected sub node.
53  */
54 public class NewItemSelectionDialog extends AbstractElementListSelectionDialog {
55 
56     /** The UI node selected in the tree view before creating the new item selection dialog.
57      *  Can be null -- which means new items must be created in the root_node. */
58     private UiElementNode mSelectedUiNode;
59     /** The root node chosen by the user, either root_node or the one passed
60      *  to the constructor if not null */
61     private UiElementNode mChosenRootNode;
62     private UiElementNode mLocalRootNode;
63     /** The descriptor of the elements to be displayed as root in this tree view. All elements
64      *  of the same type in the root will be displayed. Can be null. */
65     private ElementDescriptor[] mDescriptorFilters;
66     /** The key for the {@link #setLastUsedXmlName(Object[])}. It corresponds to the full
67      * workspace path of the currently edited file, if this can be computed. This is computed
68      * by {@link #getLastUsedXmlName(UiElementNode)}, called from the constructor. */
69     private String mLastUsedKey;
70     /** A static map of known XML Names used for a given file. The map has full workspace
71      * paths as key and XML names as values. */
72     private static final Map<String, String> sLastUsedXmlName = new HashMap<String, String>();
73     /** The potential XML Name to initially select in the selection dialog. This is computed
74      * in the constructor and set by {@link #setInitialSelection(UiElementNode)}. */
75     private String mInitialXmlName;
76 
77     /**
78      * Creates the new item selection dialog.
79      *
80      * @param shell The parent shell for the list.
81      * @param labelProvider ILabelProvider for the list.
82      * @param descriptorFilters The element allows at the root of the tree. Can be null.
83      * @param ui_node The selected node, or null if none is selected.
84      * @param root_node The root of the Ui Tree, either the UiDocumentNode or a sub-node.
85      */
NewItemSelectionDialog(Shell shell, ILabelProvider labelProvider, ElementDescriptor[] descriptorFilters, UiElementNode ui_node, UiElementNode root_node)86     public NewItemSelectionDialog(Shell shell, ILabelProvider labelProvider,
87             ElementDescriptor[] descriptorFilters,
88             UiElementNode ui_node,
89             UiElementNode root_node) {
90         super(shell, labelProvider);
91         mDescriptorFilters = descriptorFilters;
92         mLocalRootNode = root_node;
93 
94         // Only accept the UI node if it is not the UI root node and it can have children.
95         // If the node cannot have children, select its parent as a potential target.
96         if (ui_node != null && ui_node != mLocalRootNode) {
97             if (ui_node.getDescriptor().hasChildren()) {
98                 mSelectedUiNode = ui_node;
99             } else {
100                 UiElementNode parent = ui_node.getUiParent();
101                 if (parent != null && parent != mLocalRootNode) {
102                     mSelectedUiNode = parent;
103                 }
104             }
105         }
106 
107         setHelpAvailable(false);
108         setMultipleSelection(false);
109 
110         setValidator(new ISelectionStatusValidator() {
111             @Override
112             public IStatus validate(Object[] selection) {
113                 if (selection.length == 1 && selection[0] instanceof ViewElementDescriptor) {
114                     return new Status(IStatus.OK, // severity
115                             AdtPlugin.PLUGIN_ID, //plugin id
116                             IStatus.OK, // code
117                             ((ViewElementDescriptor) selection[0]).getFullClassName(), //msg
118                             null); // exception
119                 } else if (selection.length == 1 && selection[0] instanceof ElementDescriptor) {
120                     return new Status(IStatus.OK, // severity
121                             AdtPlugin.PLUGIN_ID, //plugin id
122                             IStatus.OK, // code
123                             "", //$NON-NLS-1$ // msg
124                             null); // exception
125                 } else {
126                     return new Status(IStatus.ERROR, // severity
127                             AdtPlugin.PLUGIN_ID, //plugin id
128                             IStatus.ERROR, // code
129                             "Invalid selection", // msg, translatable
130                             null); // exception
131                 }
132             }
133         });
134 
135         // Determine the initial selection using a couple heuristics.
136 
137         // First check if we can get the last used node type for this file.
138         // The heuristic is that generally one keeps adding the same kind of items to the
139         // same file, so reusing the last used item type makes most sense.
140         String xmlName = getLastUsedXmlName(root_node);
141         if (xmlName == null) {
142             // Another heuristic is to find the most used item and default to that.
143             xmlName = getMostUsedXmlName(root_node);
144         }
145         if (xmlName == null) {
146             // Finally the last heuristic is to see if there's an item with a name
147             // similar to the edited file name.
148             xmlName = getLeafFileName(root_node);
149         }
150         // Set the potential name. Selecting the right item is done later by setInitialSelection().
151         mInitialXmlName = xmlName;
152     }
153 
154     /**
155      * Returns a potential XML name based on the file name.
156      * The item name is marked with an asterisk to identify it as a partial match.
157      */
getLeafFileName(UiElementNode ui_node)158     private String getLeafFileName(UiElementNode ui_node) {
159         if (ui_node != null) {
160             AndroidXmlEditor editor = ui_node.getEditor();
161             if (editor != null) {
162                 IEditorInput editorInput = editor.getEditorInput();
163                 if (editorInput instanceof FileEditorInput) {
164                     IFile f = ((FileEditorInput) editorInput).getFile();
165                     if (f != null) {
166                         String leafName = f.getFullPath().removeFileExtension().lastSegment();
167                         return "*" + leafName; //$NON-NLS-1$
168                     }
169                 }
170             }
171         }
172 
173         return null;
174     }
175 
176     /**
177      * Given a potential non-null root node, this method looks for the currently edited
178      * file path and uses it as a key to retrieve the last used item for this file by this
179      * selection dialog. Returns null if nothing can be found, otherwise returns the string
180      * name of the item.
181      */
getLastUsedXmlName(UiElementNode ui_node)182     private String getLastUsedXmlName(UiElementNode ui_node) {
183         if (ui_node != null) {
184             AndroidXmlEditor editor = ui_node.getEditor();
185             if (editor != null) {
186                 IEditorInput editorInput = editor.getEditorInput();
187                 if (editorInput instanceof FileEditorInput) {
188                     IFile f = ((FileEditorInput) editorInput).getFile();
189                     if (f != null) {
190                         mLastUsedKey = f.getFullPath().toPortableString();
191 
192                         return sLastUsedXmlName.get(mLastUsedKey);
193                     }
194                 }
195             }
196         }
197 
198         return null;
199     }
200 
201     /**
202      * Sets the last used item for this selection dialog for this file.
203      * @param objects The currently selected items. Only the first one is used if it is an
204      *                {@link ElementDescriptor}.
205      */
setLastUsedXmlName(Object[] objects)206     private void setLastUsedXmlName(Object[] objects) {
207         if (mLastUsedKey != null &&
208                 objects != null &&
209                 objects.length > 0 &&
210                 objects[0] instanceof ElementDescriptor) {
211             ElementDescriptor desc = (ElementDescriptor) objects[0];
212             sLastUsedXmlName.put(mLastUsedKey, desc.getXmlName());
213         }
214     }
215 
216     /**
217      * Returns the most used sub-element name, if any, or null.
218      */
getMostUsedXmlName(UiElementNode ui_node)219     private String getMostUsedXmlName(UiElementNode ui_node) {
220         if (ui_node != null) {
221             TreeMap<String, Integer> counts = new TreeMap<String, Integer>();
222             int max = -1;
223 
224             for (UiElementNode child : ui_node.getUiChildren()) {
225                 String name = child.getDescriptor().getXmlName();
226                 Integer i = counts.get(name);
227                 int count = i == null ? 1 : i.intValue() + 1;
228                 counts.put(name, count);
229                 max = Math.max(max, count);
230             }
231 
232             if (max > 0) {
233                 // Find first key with this max and return it
234                 for (Entry<String, Integer> entry : counts.entrySet()) {
235                     if (entry.getValue().intValue() == max) {
236                         return entry.getKey();
237                     }
238                 }
239             }
240         }
241         return null;
242     }
243 
244     /**
245      * @return The root node selected by the user, either root node or the
246      *         one passed to the constructor if not null.
247      */
getChosenRootNode()248     public UiElementNode getChosenRootNode() {
249         return mChosenRootNode;
250     }
251 
252     /**
253      * Internal helper to compute the result. Returns the selection from
254      * the list view, if any.
255      */
256     @Override
computeResult()257     protected void computeResult() {
258         setResult(Arrays.asList(getSelectedElements()));
259         setLastUsedXmlName(getSelectedElements());
260     }
261 
262     /**
263      * Creates the dialog area.
264      *
265      * First add a radio area, which may be either 2 radio controls or
266      * just a message area if there's only one choice (the app root node).
267      *
268      * Then uses the default from the AbstractElementListSelectionDialog
269      * which is to add both a filter text and a filtered list. Adding both
270      * is necessary (since the base class accesses both internal directly
271      * fields without checking for null pointers.)
272      *
273      * Finally sets the initial selection list.
274      */
275     @Override
createDialogArea(Composite parent)276     protected Control createDialogArea(Composite parent) {
277         Composite contents = (Composite) super.createDialogArea(parent);
278 
279         createRadioControl(contents);
280         createFilterText(contents);
281         createFilteredList(contents);
282 
283         // We don't want the builtin message area label (we use a radio control
284         // instead), but if we don't create it, Bad Stuff happens on
285         // Eclipse 3.8 and later (see issue 32527).
286         Label label = createMessageArea(contents);
287         if (label != null) {
288             GridData data = (GridData) label.getLayoutData();
289             data.exclude = true;
290         }
291 
292         // Initialize the list state.
293         // This must be done after the filtered list as been created.
294         chooseNode(mChosenRootNode);
295 
296         // Set the initial selection
297         setInitialSelection(mChosenRootNode);
298         return contents;
299     }
300 
301     /**
302      * Tries to set the initial selection based on the {@link #mInitialXmlName} computed
303      * in the constructor. The selection is only set if there's an element descriptor
304      * that matches the same exact XML name. When {@link #mInitialXmlName} starts with an
305      * asterisk, it means to do a partial case-insensitive match on the start of the
306      * strings.
307      */
setInitialSelection(UiElementNode rootNode)308     private void setInitialSelection(UiElementNode rootNode) {
309         ElementDescriptor initialElement = null;
310 
311         if (mInitialXmlName != null && mInitialXmlName.length() > 0) {
312             String name = mInitialXmlName;
313             boolean partial = name.startsWith("*");   //$NON-NLS-1$
314             if (partial) {
315                 name = name.substring(1).toLowerCase(Locale.US);
316             }
317 
318             for (ElementDescriptor desc : getAllowedDescriptors(rootNode)) {
319                 if (!partial && desc.getXmlName().equals(name)) {
320                     initialElement = desc;
321                     break;
322                 } else if (partial) {
323                     String name2 = desc.getXmlLocalName().toLowerCase(Locale.US);
324                     if (name.startsWith(name2) || name2.startsWith(name)) {
325                         initialElement = desc;
326                         break;
327                     }
328                 }
329             }
330         }
331 
332         setSelection(initialElement == null ? null : new ElementDescriptor[] { initialElement });
333     }
334 
335     /**
336      * Creates the message text widget and sets layout data.
337      * @param content the parent composite of the message area.
338      */
createRadioControl(Composite content)339     private Composite createRadioControl(Composite content) {
340 
341         if (mSelectedUiNode != null) {
342             Button radio1 = new Button(content, SWT.RADIO);
343             radio1.setText(String.format("Create a new element at the top level, in %1$s.",
344                     mLocalRootNode.getShortDescription()));
345 
346             Button radio2 = new Button(content, SWT.RADIO);
347             radio2.setText(String.format("Create a new element in the selected element, %1$s.",
348                     mSelectedUiNode.getBreadcrumbTrailDescription(false /* include_root */)));
349 
350             // Set the initial selection before adding the listeners
351             // (they can't be run till the filtered list has been created)
352             radio1.setSelection(false);
353             radio2.setSelection(true);
354             mChosenRootNode = mSelectedUiNode;
355 
356             radio1.addSelectionListener(new SelectionAdapter() {
357                 @Override
358                 public void widgetSelected(SelectionEvent e) {
359                     super.widgetSelected(e);
360                     chooseNode(mLocalRootNode);
361                 }
362             });
363 
364             radio2.addSelectionListener(new SelectionAdapter() {
365                 @Override
366                 public void widgetSelected(SelectionEvent e) {
367                     super.widgetSelected(e);
368                     chooseNode(mSelectedUiNode);
369                 }
370             });
371         } else {
372             setMessage(String.format("Create a new element at the top level, in %1$s.",
373                     mLocalRootNode.getShortDescription()));
374             createMessageArea(content);
375 
376             mChosenRootNode = mLocalRootNode;
377         }
378 
379         return content;
380     }
381 
382     /**
383      * Internal helper to remember the root node choosen by the user.
384      * It also sets the list view to the adequate list of children that can
385      * be added to the chosen root node.
386      *
387      * If the chosen root node is mLocalRootNode and a descriptor filter was specified
388      * when creating the master-detail part, we use this as the set of nodes that
389      * can be created on the root node.
390      *
391      * @param ui_node The chosen root node, either mLocalRootNode or
392      *                mSelectedUiNode.
393      */
chooseNode(UiElementNode ui_node)394     private void chooseNode(UiElementNode ui_node) {
395         mChosenRootNode = ui_node;
396         setListElements(getAllowedDescriptors(ui_node));
397     }
398 
399     /**
400      * Returns the list of {@link ElementDescriptor}s that can be added to the given
401      * UI node.
402      *
403      * @param ui_node The UI node to which element should be added. Cannot be null.
404      * @return A non-null array of {@link ElementDescriptor}. The array might be empty.
405      */
getAllowedDescriptors(UiElementNode ui_node)406     private ElementDescriptor[] getAllowedDescriptors(UiElementNode ui_node) {
407         if (ui_node == mLocalRootNode &&
408                 mDescriptorFilters != null &&
409                 mDescriptorFilters.length != 0) {
410             return mDescriptorFilters;
411         } else {
412             return ui_node.getDescriptor().getChildren();
413         }
414     }
415 }
416