1 /*
2  * Copyright (C) 2008 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.wizards.newxmlfile;
18 
19 import static com.android.SdkConstants.DOT_XML;
20 import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW;
21 import static com.android.SdkConstants.LINEAR_LAYOUT;
22 import static com.android.SdkConstants.RES_QUALIFIER_SEP;
23 import static com.android.SdkConstants.SCROLL_VIEW;
24 import static com.android.SdkConstants.VALUE_FILL_PARENT;
25 import static com.android.SdkConstants.VALUE_MATCH_PARENT;
26 import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP_CHAR;
27 import static com.android.ide.eclipse.adt.internal.wizards.newxmlfile.ChooseConfigurationPage.RES_FOLDER_ABS;
28 
29 import com.android.SdkConstants;
30 import com.android.ide.common.resources.configuration.FolderConfiguration;
31 import com.android.ide.common.resources.configuration.ResourceQualifier;
32 import com.android.ide.eclipse.adt.AdtConstants;
33 import com.android.ide.eclipse.adt.AdtPlugin;
34 import com.android.ide.eclipse.adt.AdtUtils;
35 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
36 import com.android.ide.eclipse.adt.internal.editors.IconFactory;
37 import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
38 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
39 import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
40 import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper;
41 import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper.ProjectCombo;
42 import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
43 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
44 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
45 import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener;
46 import com.android.resources.ResourceFolderType;
47 import com.android.sdklib.IAndroidTarget;
48 import com.android.utils.Pair;
49 import com.android.utils.SdkUtils;
50 
51 import org.eclipse.core.resources.IFile;
52 import org.eclipse.core.resources.IProject;
53 import org.eclipse.core.resources.IResource;
54 import org.eclipse.core.runtime.CoreException;
55 import org.eclipse.core.runtime.IAdaptable;
56 import org.eclipse.core.runtime.IPath;
57 import org.eclipse.core.runtime.IStatus;
58 import org.eclipse.jdt.core.IJavaProject;
59 import org.eclipse.jface.dialogs.IMessageProvider;
60 import org.eclipse.jface.viewers.ArrayContentProvider;
61 import org.eclipse.jface.viewers.ColumnLabelProvider;
62 import org.eclipse.jface.viewers.IBaseLabelProvider;
63 import org.eclipse.jface.viewers.IStructuredSelection;
64 import org.eclipse.jface.viewers.TableViewer;
65 import org.eclipse.jface.wizard.WizardPage;
66 import org.eclipse.swt.SWT;
67 import org.eclipse.swt.events.ModifyEvent;
68 import org.eclipse.swt.events.ModifyListener;
69 import org.eclipse.swt.events.SelectionAdapter;
70 import org.eclipse.swt.events.SelectionEvent;
71 import org.eclipse.swt.graphics.Image;
72 import org.eclipse.swt.layout.GridData;
73 import org.eclipse.swt.layout.GridLayout;
74 import org.eclipse.swt.widgets.Combo;
75 import org.eclipse.swt.widgets.Composite;
76 import org.eclipse.swt.widgets.Label;
77 import org.eclipse.swt.widgets.Table;
78 import org.eclipse.swt.widgets.Text;
79 import org.eclipse.ui.IEditorPart;
80 import org.eclipse.ui.IWorkbenchPage;
81 import org.eclipse.ui.IWorkbenchWindow;
82 import org.eclipse.ui.PlatformUI;
83 import org.eclipse.ui.part.FileEditorInput;
84 
85 import java.util.ArrayList;
86 import java.util.Collections;
87 import java.util.HashSet;
88 import java.util.List;
89 
90 /**
91  * This is the first page of the {@link NewXmlFileWizard} which provides the ability to create
92  * skeleton XML resources files for Android projects.
93  * <p/>
94  * This page is used to select the project, resource type and file name.
95  */
96 class NewXmlFileCreationPage extends WizardPage {
97 
98     @Override
setVisible(boolean visible)99     public void setVisible(boolean visible) {
100         super.setVisible(visible);
101         // Ensure the initial focus is in the Name field; you usually don't need
102         // to edit the default text field (the project name)
103         if (visible && mFileNameTextField != null) {
104             mFileNameTextField.setFocus();
105         }
106 
107         validatePage();
108     }
109 
110     /**
111      * Information on one type of resource that can be created (e.g. menu, pref, layout, etc.)
112      */
113     static class TypeInfo {
114         private final String mUiName;
115         private final ResourceFolderType mResFolderType;
116         private final String mTooltip;
117         private final Object mRootSeed;
118         private ArrayList<String> mRoots = new ArrayList<String>();
119         private final String mXmlns;
120         private final String mDefaultAttrs;
121         private final String mDefaultRoot;
122         private final int mTargetApiLevel;
123 
TypeInfo(String uiName, String tooltip, ResourceFolderType resFolderType, Object rootSeed, String defaultRoot, String xmlns, String defaultAttrs, int targetApiLevel)124         public TypeInfo(String uiName,
125                         String tooltip,
126                         ResourceFolderType resFolderType,
127                         Object rootSeed,
128                         String defaultRoot,
129                         String xmlns,
130                         String defaultAttrs,
131                         int targetApiLevel) {
132             mUiName = uiName;
133             mResFolderType = resFolderType;
134             mTooltip = tooltip;
135             mRootSeed = rootSeed;
136             mDefaultRoot = defaultRoot;
137             mXmlns = xmlns;
138             mDefaultAttrs = defaultAttrs;
139             mTargetApiLevel = targetApiLevel;
140         }
141 
142         /** Returns the UI name for the resource type. Unique. Never null. */
getUiName()143         String getUiName() {
144             return mUiName;
145         }
146 
147         /** Returns the tooltip for the resource type. Can be null. */
getTooltip()148         String getTooltip() {
149             return mTooltip;
150         }
151 
152         /**
153          * Returns the name of the {@link ResourceFolderType}.
154          * Never null but not necessarily unique,
155          * e.g. two types use  {@link ResourceFolderType#XML}.
156          */
getResFolderName()157         String getResFolderName() {
158             return mResFolderType.getName();
159         }
160 
161         /**
162          * Returns the matching {@link ResourceFolderType}.
163          * Never null but not necessarily unique,
164          * e.g. two types use  {@link ResourceFolderType#XML}.
165          */
getResFolderType()166         ResourceFolderType getResFolderType() {
167             return mResFolderType;
168         }
169 
170         /**
171          * Returns the seed used to fill the root element values.
172          * The seed might be either a String, a String array, an {@link ElementDescriptor},
173          * a {@link DocumentDescriptor} or null.
174          */
getRootSeed()175         Object getRootSeed() {
176             return mRootSeed;
177         }
178 
179         /**
180          * Returns the default root element that should be selected by default. Can be
181          * null.
182          *
183          * @param project the associated project, or null if not known
184          */
getDefaultRoot(IProject project)185         String getDefaultRoot(IProject project) {
186             return mDefaultRoot;
187         }
188 
189         /**
190          * Returns the list of all possible root elements for the resource type.
191          * This can be an empty ArrayList but not null.
192          * <p/>
193          * TODO: the root list SHOULD depend on the currently selected project, to include
194          * custom classes.
195          */
getRoots()196         ArrayList<String> getRoots() {
197             return mRoots;
198         }
199 
200         /**
201          * If the generated resource XML file requires an "android" XMLNS, this should be set
202          * to {@link SdkConstants#NS_RESOURCES}. When it is null, no XMLNS is generated.
203          */
getXmlns()204         String getXmlns() {
205             return mXmlns;
206         }
207 
208         /**
209          * When not null, this represent extra attributes that must be specified in the
210          * root element of the generated XML file. When null, no extra attributes are inserted.
211          *
212          * @param project the project to get the attributes for
213          * @param root the selected root element string, never null
214          */
getDefaultAttrs(IProject project, String root)215         String getDefaultAttrs(IProject project, String root) {
216             return mDefaultAttrs;
217         }
218 
219         /**
220          * When not null, represents an extra string that should be written inside
221          * the element when constructed
222          *
223          * @param project the project to get the child content for
224          * @param root the chosen root element
225          * @return a string to be written inside the root element, or null if nothing
226          */
getChild(IProject project, String root)227         String getChild(IProject project, String root) {
228             return null;
229         }
230 
231         /**
232          * The minimum API level required by the current SDK target to support this feature.
233          *
234          * @return the minimum API level
235          */
getTargetApiLevel()236         public int getTargetApiLevel() {
237             return mTargetApiLevel;
238         }
239     }
240 
241     /**
242      * TypeInfo, information for each "type" of file that can be created.
243      */
244     private static final TypeInfo[] sTypes = {
245         new TypeInfo(
246                 "Layout",                                                   // UI name
247                 "An XML file that describes a screen layout.",              // tooltip
248                 ResourceFolderType.LAYOUT,                                  // folder type
249                 AndroidTargetData.DESCRIPTOR_LAYOUT,                        // root seed
250                 LINEAR_LAYOUT,                                              // default root
251                 SdkConstants.NS_RESOURCES,                                  // xmlns
252                 "",                                                         // not used, see below
253                 1                                                           // target API level
254                 ) {
255 
256                 @Override
257                 String getDefaultRoot(IProject project) {
258                     // TODO: Use GridLayout by default for new SDKs
259                     // (when we've ironed out all the usability issues)
260                     //Sdk currentSdk = Sdk.getCurrent();
261                     //if (project != null && currentSdk != null) {
262                     //    IAndroidTarget target = currentSdk.getTarget(project);
263                     //    // fill_parent was renamed match_parent in API level 8
264                     //    if (target != null && target.getVersion().getApiLevel() >= 13) {
265                     //        return GRID_LAYOUT;
266                     //    }
267                     //}
268 
269                     return LINEAR_LAYOUT;
270                 };
271 
272                 // The default attributes must be determined dynamically since whether
273                 // we use match_parent or fill_parent depends on the API level of the
274                 // project
275                 @Override
276                 String getDefaultAttrs(IProject project, String root) {
277                     Sdk currentSdk = Sdk.getCurrent();
278                     String fill = VALUE_FILL_PARENT;
279                     if (currentSdk != null) {
280                         IAndroidTarget target = currentSdk.getTarget(project);
281                         // fill_parent was renamed match_parent in API level 8
282                         if (target != null && target.getVersion().getApiLevel() >= 8) {
283                             fill = VALUE_MATCH_PARENT;
284                         }
285                     }
286 
287                     // Only set "vertical" orientation of LinearLayouts by default;
288                     // for GridLayouts for example we want to rely on the real default
289                     // of the layout
290                     String size = String.format(
291                             "android:layout_width=\"%1$s\"\n"        //$NON-NLS-1$
292                             + "android:layout_height=\"%2$s\"",        //$NON-NLS-1$
293                             fill, fill);
294                     if (LINEAR_LAYOUT.equals(root)) {
295                         return "android:orientation=\"vertical\"\n" + size; //$NON-NLS-1$
296                     } else {
297                         return size;
298                     }
299                 }
300 
301                 @Override
302                 String getChild(IProject project, String root) {
303                     // Create vertical linear layouts inside new scroll views
304                     if (SCROLL_VIEW.equals(root) || HORIZONTAL_SCROLL_VIEW.equals(root)) {
305                         return "    <LinearLayout "         //$NON-NLS-1$
306                             + getDefaultAttrs(project, root).replace('\n', ' ')
307                             + " android:orientation=\"vertical\"" //$NON-NLS-1$
308                             + "></LinearLayout>\n";         //$NON-NLS-1$
309                     }
310                     return null;
311                 }
312         },
313         new TypeInfo("Values",                                              // UI name
314                 "An XML file with simple values: colors, strings, dimensions, etc.", // tooltip
315                 ResourceFolderType.VALUES,                                  // folder type
316                 SdkConstants.TAG_RESOURCES,                                 // root seed
317                 null,                                                       // default root
318                 null,                                                       // xmlns
319                 null,                                                       // default attributes
320                 1                                                           // target API level
321                 ),
322         new TypeInfo("Drawable",                                            // UI name
323                 "An XML file that describes a drawable.",                   // tooltip
324                 ResourceFolderType.DRAWABLE,                                // folder type
325                 AndroidTargetData.DESCRIPTOR_DRAWABLE,                      // root seed
326                 null,                                                       // default root
327                 SdkConstants.NS_RESOURCES,                                  // xmlns
328                 null,                                                       // default attributes
329                 1                                                           // target API level
330                 ),
331         new TypeInfo("Menu",                                                // UI name
332                 "An XML file that describes an menu.",                      // tooltip
333                 ResourceFolderType.MENU,                                    // folder type
334                 SdkConstants.TAG_MENU,                                      // root seed
335                 null,                                                       // default root
336                 SdkConstants.NS_RESOURCES,                                  // xmlns
337                 null,                                                       // default attributes
338                 1                                                           // target API level
339                 ),
340         new TypeInfo("Color List",                                          // UI name
341                 "An XML file that describes a color state list.",           // tooltip
342                 ResourceFolderType.COLOR,                                   // folder type
343                 AndroidTargetData.DESCRIPTOR_COLOR,                         // root seed
344                 "selector",  //$NON-NLS-1$                                  // default root
345                 SdkConstants.NS_RESOURCES,                                  // xmlns
346                 null,                                                       // default attributes
347                 1                                                           // target API level
348                 ),
349         new TypeInfo("Property Animation",                                  // UI name
350                 "An XML file that describes a property animation",          // tooltip
351                 ResourceFolderType.ANIMATOR,                                // folder type
352                 AndroidTargetData.DESCRIPTOR_ANIMATOR,                      // root seed
353                 "set", //$NON-NLS-1$                                        // default root
354                 SdkConstants.NS_RESOURCES,                                  // xmlns
355                 null,                                                       // default attributes
356                 11                                                          // target API level
357                 ),
358         new TypeInfo("Tween Animation",                                     // UI name
359                 "An XML file that describes a tween animation.",            // tooltip
360                 ResourceFolderType.ANIM,                                    // folder type
361                 AndroidTargetData.DESCRIPTOR_ANIM,                          // root seed
362                 "set", //$NON-NLS-1$                                        // default root
363                 null,                                                       // xmlns
364                 null,                                                       // default attributes
365                 1                                                           // target API level
366                 ),
367         new TypeInfo("AppWidget Provider",                                  // UI name
368                 "An XML file that describes a widget provider.",            // tooltip
369                 ResourceFolderType.XML,                                     // folder type
370                 AndroidTargetData.DESCRIPTOR_APPWIDGET_PROVIDER,            // root seed
371                 null,                                                       // default root
372                 SdkConstants.NS_RESOURCES,                                  // xmlns
373                 null,                                                       // default attributes
374                 3                                                           // target API level
375                 ),
376         new TypeInfo("Preference",                                          // UI name
377                 "An XML file that describes preferences.",                  // tooltip
378                 ResourceFolderType.XML,                                     // folder type
379                 AndroidTargetData.DESCRIPTOR_PREFERENCES,                   // root seed
380                 SdkConstants.CLASS_NAME_PREFERENCE_SCREEN,                  // default root
381                 SdkConstants.NS_RESOURCES,                                  // xmlns
382                 null,                                                       // default attributes
383                 1                                                           // target API level
384                 ),
385         new TypeInfo("Searchable",                                          // UI name
386                 "An XML file that describes a searchable.",                 // tooltip
387                 ResourceFolderType.XML,                                     // folder type
388                 AndroidTargetData.DESCRIPTOR_SEARCHABLE,                    // root seed
389                 null,                                                       // default root
390                 SdkConstants.NS_RESOURCES,                                  // xmlns
391                 null,                                                       // default attributes
392                 1                                                           // target API level
393                 ),
394         // Still missing: Interpolator, Raw and Mipmap. Raw should probably never be in
395         // this menu since it's not often used for creating XML files.
396     };
397 
398     private NewXmlFileWizard.Values mValues;
399     private ProjectCombo mProjectButton;
400     private Text mFileNameTextField;
401     private Combo mTypeCombo;
402     private IStructuredSelection mInitialSelection;
403     private ResourceFolderType mInitialFolderType;
404     private boolean mInternalTypeUpdate;
405     private TargetChangeListener mSdkTargetChangeListener;
406     private Table mRootTable;
407     private TableViewer mRootTableViewer;
408 
409     // --- UI creation ---
410 
411     /**
412      * Constructs a new {@link NewXmlFileCreationPage}.
413      * <p/>
414      * Called by {@link NewXmlFileWizard#createMainPage}.
415      */
NewXmlFileCreationPage(String pageName, NewXmlFileWizard.Values values)416     protected NewXmlFileCreationPage(String pageName, NewXmlFileWizard.Values values) {
417         super(pageName);
418         mValues = values;
419         setPageComplete(false);
420     }
421 
setInitialSelection(IStructuredSelection initialSelection)422     public void setInitialSelection(IStructuredSelection initialSelection) {
423         mInitialSelection = initialSelection;
424     }
425 
setInitialFolderType(ResourceFolderType initialType)426     public void setInitialFolderType(ResourceFolderType initialType) {
427         mInitialFolderType = initialType;
428     }
429 
430     /**
431      * Called by the parent Wizard to create the UI for this Wizard Page.
432      *
433      * {@inheritDoc}
434      *
435      * @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite)
436      */
437     @Override
438     @SuppressWarnings("unused") // SWT constructors have side effects, they aren't unused
createControl(Composite parent)439     public void createControl(Composite parent) {
440         // This UI is maintained with WindowBuilder.
441 
442         Composite composite = new Composite(parent, SWT.NULL);
443         composite.setLayout(new GridLayout(2, false /*makeColumnsEqualWidth*/));
444         composite.setLayoutData(new GridData(GridData.FILL_BOTH));
445 
446         // label before type radios
447         Label typeLabel = new Label(composite, SWT.NONE);
448         typeLabel.setText("Resource Type:");
449 
450         mTypeCombo = new Combo(composite, SWT.DROP_DOWN | SWT.READ_ONLY);
451         mTypeCombo.setToolTipText("What type of resource would you like to create?");
452         mTypeCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
453         if (mInitialFolderType != null) {
454             mTypeCombo.setEnabled(false);
455         }
456         mTypeCombo.addSelectionListener(new SelectionAdapter() {
457             @Override
458             public void widgetSelected(SelectionEvent e) {
459                 TypeInfo type = getSelectedType();
460                 if (type != null) {
461                     onSelectType(type);
462                 }
463             }
464         });
465 
466         // separator
467         Label separator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL);
468         GridData gd2 = new GridData(GridData.GRAB_HORIZONTAL);
469         gd2.horizontalAlignment = SWT.FILL;
470         gd2.horizontalSpan = 2;
471         separator.setLayoutData(gd2);
472 
473         // Project: [button]
474         String tooltip = "The Android Project where the new resource file will be created.";
475         Label projectLabel = new Label(composite, SWT.NONE);
476         projectLabel.setText("Project:");
477         projectLabel.setToolTipText(tooltip);
478 
479         ProjectChooserHelper helper =
480                 new ProjectChooserHelper(getShell(), null /* filter */);
481 
482         mProjectButton = new ProjectCombo(helper, composite, mValues.project);
483         mProjectButton.setToolTipText(tooltip);
484         mProjectButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
485         mProjectButton.addSelectionListener(new SelectionAdapter() {
486             @Override
487             public void widgetSelected(SelectionEvent e) {
488                 IProject project = mProjectButton.getSelectedProject();
489                 if (project != mValues.project) {
490                     changeProject(project);
491                 }
492             };
493         });
494 
495         // Filename: [text]
496         Label fileLabel = new Label(composite, SWT.NONE);
497         fileLabel.setText("File:");
498         fileLabel.setToolTipText("The name of the resource file to create.");
499 
500         mFileNameTextField = new Text(composite, SWT.BORDER);
501         mFileNameTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
502         mFileNameTextField.setToolTipText(tooltip);
503         mFileNameTextField.addModifyListener(new ModifyListener() {
504             @Override
505             public void modifyText(ModifyEvent e) {
506                 mValues.name = mFileNameTextField.getText();
507                 validatePage();
508             }
509         });
510 
511         // separator
512         Label rootSeparator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL);
513         GridData gd = new GridData(GridData.GRAB_HORIZONTAL);
514         gd.horizontalAlignment = SWT.FILL;
515         gd.horizontalSpan = 2;
516         rootSeparator.setLayoutData(gd);
517 
518         // Root Element:
519         // [TableViewer]
520         Label rootLabel = new Label(composite, SWT.NONE);
521         rootLabel.setText("Root Element:");
522         new Label(composite, SWT.NONE);
523 
524         mRootTableViewer = new TableViewer(composite, SWT.BORDER | SWT.FULL_SELECTION);
525         mRootTable = mRootTableViewer.getTable();
526         GridData tableGridData = new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1);
527         tableGridData.heightHint = 200;
528         mRootTable.setLayoutData(tableGridData);
529 
530         setControl(composite);
531 
532         // Update state the first time
533         setErrorMessage(null);
534         setMessage(null);
535 
536         initializeFromSelection(mInitialSelection);
537         updateAvailableTypes();
538         initializeFromFixedType();
539         initializeRootValues();
540         installTargetChangeListener();
541 
542         initialSelectType();
543         validatePage();
544     }
545 
initialSelectType()546     private void initialSelectType() {
547         TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData();
548         int typeIndex = getTypeComboIndex(mValues.type);
549         if (typeIndex == -1) {
550             typeIndex = 0;
551         } else {
552             assert mValues.type == types[typeIndex];
553         }
554         mTypeCombo.select(typeIndex);
555         onSelectType(types[typeIndex]);
556         updateRootCombo(types[typeIndex]);
557     }
558 
installTargetChangeListener()559     private void installTargetChangeListener() {
560         mSdkTargetChangeListener = new TargetChangeListener() {
561             @Override
562             public IProject getProject() {
563                 return mValues.project;
564             }
565 
566             @Override
567             public void reload() {
568                 if (mValues.project != null) {
569                     changeProject(mValues.project);
570                 }
571             }
572         };
573 
574         AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener);
575     }
576 
577     @Override
dispose()578     public void dispose() {
579 
580         if (mSdkTargetChangeListener != null) {
581             AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener);
582             mSdkTargetChangeListener = null;
583         }
584 
585         super.dispose();
586     }
587 
588     /**
589      * Returns the selected root element string, if any.
590      *
591      * @return The selected root element string or null.
592      */
getRootElement()593     public String getRootElement() {
594         int index = mRootTable.getSelectionIndex();
595         if (index >= 0) {
596             Object[] roots = (Object[]) mRootTableViewer.getInput();
597             return roots[index].toString();
598         }
599         return null;
600     }
601 
602     /**
603      * Called by {@link NewXmlFileWizard} to initialize the page with the selection
604      * received by the wizard -- typically the current user workbench selection.
605      * <p/>
606      * Things we expect to find out from the selection:
607      * <ul>
608      * <li>The project name, valid if it's an android nature.</li>
609      * <li>The current folder, valid if it's a folder under /res</li>
610      * <li>An existing filename, in which case the user will be asked whether to override it.</li>
611      * </ul>
612      * <p/>
613      * The selection can also be set to a {@link Pair} of {@link IProject} and a workspace
614      * resource path (where the resource path does not have to exist yet, such as res/anim/).
615      *
616      * @param selection The selection when the wizard was initiated.
617      */
initializeFromSelection(IStructuredSelection selection)618     private boolean initializeFromSelection(IStructuredSelection selection) {
619         if (selection == null) {
620             return false;
621         }
622 
623         // Find the best match in the element list. In case there are multiple selected elements
624         // select the one that provides the most information and assign them a score,
625         // e.g. project=1 + folder=2 + file=4.
626         IProject targetProject = null;
627         String targetWsFolderPath = null;
628         String targetFileName = null;
629         int targetScore = 0;
630         for (Object element : selection.toList()) {
631             if (element instanceof IAdaptable) {
632                 IResource res = (IResource) ((IAdaptable) element).getAdapter(IResource.class);
633                 IProject project = res != null ? res.getProject() : null;
634 
635                 // Is this an Android project?
636                 try {
637                     if (project == null || !project.hasNature(AdtConstants.NATURE_DEFAULT)) {
638                         continue;
639                     }
640                 } catch (CoreException e) {
641                     // checking the nature failed, ignore this resource
642                     continue;
643                 }
644 
645                 int score = 1; // we have a valid project at least
646 
647                 IPath wsFolderPath = null;
648                 String fileName = null;
649                 assert res != null; // Eclipse incorrectly thinks res could be null, so tell it no
650                 if (res.getType() == IResource.FOLDER) {
651                     wsFolderPath = res.getProjectRelativePath();
652                 } else if (res.getType() == IResource.FILE) {
653                     if (SdkUtils.endsWithIgnoreCase(res.getName(), DOT_XML)) {
654                         fileName = res.getName();
655                     }
656                     wsFolderPath = res.getParent().getProjectRelativePath();
657                 }
658 
659                 // Disregard this folder selection if it doesn't point to /res/something
660                 if (wsFolderPath != null &&
661                         wsFolderPath.segmentCount() > 1 &&
662                         SdkConstants.FD_RESOURCES.equals(wsFolderPath.segment(0))) {
663                     score += 2;
664                 } else {
665                     wsFolderPath = null;
666                     fileName = null;
667                 }
668 
669                 score += fileName != null ? 4 : 0;
670 
671                 if (score > targetScore) {
672                     targetScore = score;
673                     targetProject = project;
674                     targetWsFolderPath = wsFolderPath != null ? wsFolderPath.toString() : null;
675                     targetFileName = fileName;
676                 }
677             } else if (element instanceof Pair<?,?>) {
678                 // Pair of Project/String
679                 @SuppressWarnings("unchecked")
680                 Pair<IProject,String> pair = (Pair<IProject,String>)element;
681                 targetScore = 1;
682                 targetProject = pair.getFirst();
683                 targetWsFolderPath = pair.getSecond();
684                 targetFileName = "";
685             }
686         }
687 
688         if (targetProject == null) {
689             // Try to figure out the project from the active editor
690             IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
691             if (window != null) {
692                 IWorkbenchPage page = window.getActivePage();
693                 if (page != null) {
694                     IEditorPart activeEditor = page.getActiveEditor();
695                     if (activeEditor instanceof AndroidXmlEditor) {
696                         Object input = ((AndroidXmlEditor) activeEditor).getEditorInput();
697                         if (input instanceof FileEditorInput) {
698                             FileEditorInput fileInput = (FileEditorInput) input;
699                             targetScore = 1;
700                             IFile file = fileInput.getFile();
701                             targetProject = file.getProject();
702                             IPath path = file.getParent().getProjectRelativePath();
703                             targetWsFolderPath = path != null ? path.toString() : null;
704                         }
705                     }
706                 }
707             }
708         }
709 
710         if (targetProject == null) {
711             // If we didn't find a default project based on the selection, check how many
712             // open Android projects we can find in the current workspace. If there's only
713             // one, we'll just select it by default.
714             IJavaProject[] projects = AdtUtils.getOpenAndroidProjects();
715             if (projects != null && projects.length == 1) {
716                 targetScore = 1;
717                 targetProject = projects[0].getProject();
718             }
719         }
720 
721         // Now set the UI accordingly
722         if (targetScore > 0) {
723             mValues.project = targetProject;
724             mValues.folderPath = targetWsFolderPath;
725             mProjectButton.setSelectedProject(targetProject);
726             mFileNameTextField.setText(targetFileName != null ? targetFileName : ""); //$NON-NLS-1$
727 
728             // If the current selection context corresponds to a specific file type,
729             // select it.
730             if (targetWsFolderPath != null) {
731                 int pos = targetWsFolderPath.lastIndexOf(WS_SEP_CHAR);
732                 if (pos >= 0) {
733                     targetWsFolderPath = targetWsFolderPath.substring(pos + 1);
734                 }
735                 String[] folderSegments = targetWsFolderPath.split(RES_QUALIFIER_SEP);
736                 if (folderSegments.length > 0) {
737                     mValues.configuration = FolderConfiguration.getConfig(folderSegments);
738                     String folderName = folderSegments[0];
739                     selectTypeFromFolder(folderName);
740                 }
741             }
742         }
743 
744         return true;
745     }
746 
initializeFromFixedType()747     private void initializeFromFixedType() {
748         if (mInitialFolderType != null) {
749             for (TypeInfo type : sTypes) {
750                 if (type.getResFolderType() == mInitialFolderType) {
751                     mValues.type = type;
752                     updateFolderPath(type);
753                     break;
754                 }
755             }
756         }
757     }
758 
759     /**
760      * Given a folder name, such as "drawable", select the corresponding type in
761      * the dropdown.
762      */
selectTypeFromFolder(String folderName)763     void selectTypeFromFolder(String folderName) {
764         List<TypeInfo> matches = new ArrayList<TypeInfo>();
765         boolean selected = false;
766 
767         TypeInfo selectedType = getSelectedType();
768         for (TypeInfo type : sTypes) {
769             if (type.getResFolderName().equals(folderName)) {
770                 matches.add(type);
771                 selected |= type == selectedType;
772             }
773         }
774 
775         if (matches.size() == 1) {
776             // If there's only one match, select it if it's not already selected
777             if (!selected) {
778                 selectType(matches.get(0));
779             }
780         } else if (matches.size() > 1) {
781             // There are multiple type candidates for this folder. This can happen
782             // for /res/xml for example. Check to see if one of them is currently
783             // selected. If yes, leave the selection unchanged. If not, deselect all type.
784             if (!selected) {
785                 selectType(null);
786             }
787         } else {
788             // Nothing valid was selected.
789             selectType(null);
790         }
791     }
792 
793     /**
794      * Initialize the root values of the type infos based on the current framework values.
795      */
initializeRootValues()796     private void initializeRootValues() {
797         IProject project = mValues.project;
798         for (TypeInfo type : sTypes) {
799             // Clear all the roots for this type
800             ArrayList<String> roots = type.getRoots();
801             if (roots.size() > 0) {
802                 roots.clear();
803             }
804 
805             // depending of the type of the seed, initialize the root in different ways
806             Object rootSeed = type.getRootSeed();
807 
808             if (rootSeed instanceof String) {
809                 // The seed is a single string, Add it as-is.
810                 roots.add((String) rootSeed);
811             } else if (rootSeed instanceof String[]) {
812                 // The seed is an array of strings. Add them as-is.
813                 for (String value : (String[]) rootSeed) {
814                     roots.add(value);
815                 }
816             } else if (rootSeed instanceof Integer && project != null) {
817                 // The seed is a descriptor reference defined in AndroidTargetData.DESCRIPTOR_*
818                 // In this case add all the children element descriptors defined, recursively,
819                 // and avoid infinite recursion by keeping track of what has already been added.
820 
821                 // Note: if project is null, the root list will be empty since it has been
822                 // cleared above.
823 
824                 // get the AndroidTargetData from the project
825                 IAndroidTarget target = null;
826                 AndroidTargetData data = null;
827 
828                 target = Sdk.getCurrent().getTarget(project);
829                 if (target == null) {
830                     // A project should have a target. The target can be missing if the project
831                     // is an old project for which a target hasn't been affected or if the
832                     // target no longer exists in this SDK. Simply log the error and dismiss.
833 
834                     AdtPlugin.log(IStatus.INFO,
835                             "NewXmlFile wizard: no platform target for project %s",  //$NON-NLS-1$
836                             project.getName());
837                     continue;
838                 } else {
839                     data = Sdk.getCurrent().getTargetData(target);
840 
841                     if (data == null) {
842                         // We should have both a target and its data.
843                         // However if the wizard is invoked whilst the platform is still being
844                         // loaded we can end up in a weird case where we have a target but it
845                         // doesn't have any data yet.
846                         // Lets log a warning and silently ignore this root.
847 
848                         AdtPlugin.log(IStatus.INFO,
849                               "NewXmlFile wizard: no data for target %s, project %s",  //$NON-NLS-1$
850                               target.getName(), project.getName());
851                         continue;
852                     }
853                 }
854 
855                 IDescriptorProvider provider = data.getDescriptorProvider((Integer)rootSeed);
856                 ElementDescriptor descriptor = provider.getDescriptor();
857                 if (descriptor != null) {
858                     HashSet<ElementDescriptor> visited = new HashSet<ElementDescriptor>();
859                     initRootElementDescriptor(roots, descriptor, visited);
860                 }
861 
862                 // Sort alphabetically.
863                 Collections.sort(roots);
864             }
865         }
866     }
867 
868     /**
869      * Helper method to recursively insert all XML names for the given {@link ElementDescriptor}
870      * into the roots array list. Keeps track of visited nodes to avoid infinite recursion.
871      * Also avoids inserting the top {@link DocumentDescriptor} which is generally synthetic
872      * and not a valid root element.
873      */
initRootElementDescriptor(ArrayList<String> roots, ElementDescriptor desc, HashSet<ElementDescriptor> visited)874     private void initRootElementDescriptor(ArrayList<String> roots,
875             ElementDescriptor desc, HashSet<ElementDescriptor> visited) {
876         if (!(desc instanceof DocumentDescriptor)) {
877             String xmlName = desc.getXmlName();
878             if (xmlName != null && xmlName.length() > 0) {
879                 roots.add(xmlName);
880             }
881         }
882 
883         visited.add(desc);
884 
885         for (ElementDescriptor child : desc.getChildren()) {
886             if (!visited.contains(child)) {
887                 initRootElementDescriptor(roots, child, visited);
888             }
889         }
890     }
891 
892     /**
893      * Changes mProject to the given new project and update the UI accordingly.
894      * <p/>
895      * Note that this does not check if the new project is the same as the current one
896      * on purpose, which allows a project to be updated when its target has changed or
897      * when targets are loaded in the background.
898      */
changeProject(IProject newProject)899     private void changeProject(IProject newProject) {
900         mValues.project = newProject;
901 
902         // enable types based on new API level
903         updateAvailableTypes();
904         initialSelectType();
905 
906         // update the folder name based on API level
907         updateFolderPath(mValues.type);
908 
909         // update the Type with the new descriptors.
910         initializeRootValues();
911 
912         // update the combo
913         updateRootCombo(mValues.type);
914 
915         validatePage();
916     }
917 
onSelectType(TypeInfo type)918     private void onSelectType(TypeInfo type) {
919         // Do nothing if this is an internal modification or if the widget has been
920         // deselected.
921         if (mInternalTypeUpdate) {
922             return;
923         }
924 
925         mValues.type = type;
926 
927         if (type == null) {
928             return;
929         }
930 
931         // update the combo
932         updateRootCombo(type);
933 
934         // update the folder path
935         updateFolderPath(type);
936 
937         validatePage();
938     }
939 
940     /** Updates the selected type in the type dropdown control */
setSelectedType(TypeInfo type)941     private void setSelectedType(TypeInfo type) {
942         TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData();
943         if (types != null) {
944             for (int i = 0, n = types.length; i < n; i++) {
945                 if (types[i] == type) {
946                     mTypeCombo.select(i);
947                     break;
948                 }
949             }
950         }
951     }
952 
953     /** Returns the selected type in the type dropdown control */
getSelectedType()954     private TypeInfo getSelectedType() {
955         int index = mTypeCombo.getSelectionIndex();
956         if (index != -1) {
957             TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData();
958             return types[index];
959         }
960 
961         return null;
962     }
963 
964     /** Returns the selected index in the type dropdown control */
getTypeComboIndex(TypeInfo type)965     private int getTypeComboIndex(TypeInfo type) {
966         TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData();
967         for (int i = 0, n = types.length; i < n; i++) {
968             if (type == types[i]) {
969                 return i;
970             }
971         }
972 
973         return -1;
974     }
975 
976     /** Updates the folder path to reflect the given type */
updateFolderPath(TypeInfo type)977     private void updateFolderPath(TypeInfo type) {
978         String wsFolderPath = mValues.folderPath;
979         String newPath = null;
980         FolderConfiguration config = mValues.configuration;
981         ResourceQualifier qual = config.getInvalidQualifier();
982         if (qual == null) {
983             // The configuration is valid. Reformat the folder path using the canonical
984             // value from the configuration.
985             newPath = RES_FOLDER_ABS + config.getFolderName(type.getResFolderType());
986         } else {
987             // The configuration is invalid. We still update the path but this time
988             // do it manually on the string.
989             if (wsFolderPath.startsWith(RES_FOLDER_ABS)) {
990                 wsFolderPath = wsFolderPath.replaceFirst(
991                         "^(" + RES_FOLDER_ABS +")[^-]*(.*)",         //$NON-NLS-1$ //$NON-NLS-2$
992                         "\\1" + type.getResFolderName() + "\\2");    //$NON-NLS-1$ //$NON-NLS-2$
993             } else {
994                 newPath = RES_FOLDER_ABS + config.getFolderName(type.getResFolderType());
995             }
996         }
997 
998         if (newPath != null && !newPath.equals(wsFolderPath)) {
999             mValues.folderPath = newPath;
1000         }
1001     }
1002 
1003     /**
1004      * Helper method that fills the values of the "root element" combo box based
1005      * on the currently selected type radio button. Also disables the combo is there's
1006      * only one choice. Always select the first root element for the given type.
1007      *
1008      * @param type The currently selected {@link TypeInfo}, or null
1009      */
updateRootCombo(TypeInfo type)1010     private void updateRootCombo(TypeInfo type) {
1011         IBaseLabelProvider labelProvider = new ColumnLabelProvider() {
1012             @Override
1013             public Image getImage(Object element) {
1014                 return IconFactory.getInstance().getIcon(element.toString());
1015             }
1016         };
1017         mRootTableViewer.setContentProvider(new ArrayContentProvider());
1018         mRootTableViewer.setLabelProvider(labelProvider);
1019 
1020         if (type != null) {
1021             // get the list of roots. The list can be empty but not null.
1022             ArrayList<String> roots = type.getRoots();
1023             mRootTableViewer.setInput(roots.toArray());
1024 
1025             int index = 0; // default is to select the first one
1026             String defaultRoot = type.getDefaultRoot(mValues.project);
1027             if (defaultRoot != null) {
1028                 index = roots.indexOf(defaultRoot);
1029             }
1030             mRootTable.select(index < 0 ? 0 : index);
1031             mRootTable.showSelection();
1032         }
1033     }
1034 
1035     /**
1036      * Helper method to select the current type in the type dropdown
1037      *
1038      * @param type The TypeInfo matching the radio button to selected or null to deselect them all.
1039      */
1040     private void selectType(TypeInfo type) {
1041         mInternalTypeUpdate = true;
1042         mValues.type = type;
1043         if (type == null) {
1044             if (mTypeCombo.getSelectionIndex() != -1) {
1045                 mTypeCombo.deselect(mTypeCombo.getSelectionIndex());
1046             }
1047         } else {
1048             setSelectedType(type);
1049         }
1050         updateRootCombo(type);
1051         mInternalTypeUpdate = false;
1052     }
1053 
1054     /**
1055      * Add the available types in the type combobox, based on whether they are available
1056      * for the current SDK.
1057      * <p/>
1058      * A type is available either if:
1059      * - if mProject is null, API level 1 is considered valid
1060      * - if mProject is !null, the project->target->API must be >= to the type's API level.
1061      */
1062     private void updateAvailableTypes() {
1063         IProject project = mValues.project;
1064         IAndroidTarget target = project != null ? Sdk.getCurrent().getTarget(project) : null;
1065         int currentApiLevel = 1;
1066         if (target != null) {
1067             currentApiLevel = target.getVersion().getApiLevel();
1068         }
1069 
1070         List<String> items = new ArrayList<String>(sTypes.length);
1071         List<TypeInfo> types = new ArrayList<TypeInfo>(sTypes.length);
1072         for (int i = 0, n = sTypes.length; i < n; i++) {
1073             TypeInfo type = sTypes[i];
1074             if (type.getTargetApiLevel() <= currentApiLevel) {
1075                 items.add(type.getUiName());
1076                 types.add(type);
1077             }
1078         }
1079         mTypeCombo.setItems(items.toArray(new String[items.size()]));
1080         mTypeCombo.setData(types.toArray(new TypeInfo[types.size()]));
1081     }
1082 
1083     /**
1084      * Validates the fields, displays errors and warnings.
1085      * Enables the finish button if there are no errors.
1086      */
1087     private void validatePage() {
1088         String error = null;
1089         String warning = null;
1090 
1091         // -- validate type
1092         TypeInfo type = mValues.type;
1093         if (error == null) {
1094             if (type == null) {
1095                 error = "One of the types must be selected (e.g. layout, values, etc.)";
1096             }
1097         }
1098 
1099         // -- validate project
1100         if (mValues.project == null) {
1101             error = "Please select an Android project.";
1102         }
1103 
1104         // -- validate type API level
1105         if (error == null) {
1106             IAndroidTarget target = Sdk.getCurrent().getTarget(mValues.project);
1107             int currentApiLevel = 1;
1108             if (target != null) {
1109                 currentApiLevel = target.getVersion().getApiLevel();
1110             }
1111 
1112             assert type != null;
1113             if (type.getTargetApiLevel() > currentApiLevel) {
1114                 error = "The API level of the selected type (e.g. AppWidget, etc.) is not " +
1115                         "compatible with the API level of the project.";
1116             }
1117         }
1118 
1119         // -- validate filename
1120         if (error == null) {
1121             String fileName = mValues.getFileName();
1122             assert type != null;
1123             ResourceFolderType folderType = type.getResFolderType();
1124             error = ResourceNameValidator.create(true, folderType).isValid(fileName);
1125         }
1126 
1127         // -- validate destination file doesn't exist
1128         if (error == null) {
1129             IFile file = mValues.getDestinationFile();
1130             if (file != null && file.exists()) {
1131                 warning = "The destination file already exists";
1132             }
1133         }
1134 
1135         // -- update UI & enable finish if there's no error
1136         setPageComplete(error == null);
1137         if (error != null) {
1138             setMessage(error, IMessageProvider.ERROR);
1139         } else if (warning != null) {
1140             setMessage(warning, IMessageProvider.WARNING);
1141         } else {
1142             setErrorMessage(null);
1143             setMessage(null);
1144         }
1145     }
1146 
1147     /**
1148      * Returns the {@link TypeInfo} for the given {@link ResourceFolderType}, or null if
1149      * not found
1150      *
1151      * @param folderType the {@link ResourceFolderType} to look for
1152      * @return the corresponding {@link TypeInfo}
1153      */
1154     static TypeInfo getTypeInfo(ResourceFolderType folderType) {
1155         for (TypeInfo typeInfo : sTypes) {
1156             if (typeInfo.getResFolderType() == folderType) {
1157                 return typeInfo;
1158             }
1159         }
1160 
1161         return null;
1162     }
1163 }
1164