• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
18 package com.android.ide.eclipse.adt.internal.wizards.newxmlfile;
19 
20 import com.android.ide.eclipse.adt.AdtPlugin;
21 import com.android.ide.eclipse.adt.AndroidConstants;
22 import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
23 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
24 import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
25 import com.android.ide.eclipse.adt.internal.editors.menu.descriptors.MenuDescriptors;
26 import com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors;
27 import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper;
28 import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration;
29 import com.android.ide.eclipse.adt.internal.resources.configurations.ResourceQualifier;
30 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType;
31 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
32 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
33 import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener;
34 import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector;
35 import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector.ConfigurationState;
36 import com.android.sdklib.IAndroidTarget;
37 import com.android.sdklib.SdkConstants;
38 
39 import org.eclipse.core.resources.IFile;
40 import org.eclipse.core.resources.IProject;
41 import org.eclipse.core.resources.IResource;
42 import org.eclipse.core.runtime.CoreException;
43 import org.eclipse.core.runtime.IAdaptable;
44 import org.eclipse.core.runtime.IPath;
45 import org.eclipse.core.runtime.IStatus;
46 import org.eclipse.core.runtime.Path;
47 import org.eclipse.jdt.core.IJavaProject;
48 import org.eclipse.jface.viewers.IStructuredSelection;
49 import org.eclipse.jface.wizard.WizardPage;
50 import org.eclipse.swt.SWT;
51 import org.eclipse.swt.events.ModifyEvent;
52 import org.eclipse.swt.events.ModifyListener;
53 import org.eclipse.swt.events.SelectionAdapter;
54 import org.eclipse.swt.events.SelectionEvent;
55 import org.eclipse.swt.events.SelectionListener;
56 import org.eclipse.swt.layout.GridData;
57 import org.eclipse.swt.layout.GridLayout;
58 import org.eclipse.swt.widgets.Button;
59 import org.eclipse.swt.widgets.Combo;
60 import org.eclipse.swt.widgets.Composite;
61 import org.eclipse.swt.widgets.Label;
62 import org.eclipse.swt.widgets.Text;
63 
64 import java.util.ArrayList;
65 import java.util.Collections;
66 import java.util.HashSet;
67 
68 /**
69  * This is the single page of the {@link NewXmlFileWizard} which provides the ability to create
70  * skeleton XML resources files for Android projects.
71  * <p/>
72  * This page is used to select the project, the resource folder, resource type and file name.
73  */
74 class NewXmlFileCreationPage extends WizardPage {
75 
76     /**
77      * Information on one type of resource that can be created (e.g. menu, pref, layout, etc.)
78      */
79     static class TypeInfo {
80         private final String mUiName;
81         private final ResourceFolderType mResFolderType;
82         private final String mTooltip;
83         private final Object mRootSeed;
84         private Button mWidget;
85         private ArrayList<String> mRoots = new ArrayList<String>();
86         private final String mXmlns;
87         private final String mDefaultAttrs;
88         private final String mDefaultRoot;
89         private final int mTargetApiLevel;
90 
TypeInfo(String uiName, String tooltip, ResourceFolderType resFolderType, Object rootSeed, String defaultRoot, String xmlns, String defaultAttrs, int targetApiLevel)91         public TypeInfo(String uiName,
92                         String tooltip,
93                         ResourceFolderType resFolderType,
94                         Object rootSeed,
95                         String defaultRoot,
96                         String xmlns,
97                         String defaultAttrs,
98                         int targetApiLevel) {
99             mUiName = uiName;
100             mResFolderType = resFolderType;
101             mTooltip = tooltip;
102             mRootSeed = rootSeed;
103             mDefaultRoot = defaultRoot;
104             mXmlns = xmlns;
105             mDefaultAttrs = defaultAttrs;
106             mTargetApiLevel = targetApiLevel;
107         }
108 
109         /** Returns the UI name for the resource type. Unique. Never null. */
getUiName()110         String getUiName() {
111             return mUiName;
112         }
113 
114         /** Returns the tooltip for the resource type. Can be null. */
getTooltip()115         String getTooltip() {
116             return mTooltip;
117         }
118 
119         /**
120          * Returns the name of the {@link ResourceFolderType}.
121          * Never null but not necessarily unique,
122          * e.g. two types use  {@link ResourceFolderType#XML}.
123          */
getResFolderName()124         String getResFolderName() {
125             return mResFolderType.getName();
126         }
127 
128         /**
129          * Returns the matching {@link ResourceFolderType}.
130          * Never null but not necessarily unique,
131          * e.g. two types use  {@link ResourceFolderType#XML}.
132          */
getResFolderType()133         ResourceFolderType getResFolderType() {
134             return mResFolderType;
135         }
136 
137         /** Sets the radio button associate with the resource type. Can be null. */
setWidget(Button widget)138         void setWidget(Button widget) {
139             mWidget = widget;
140         }
141 
142         /** Returns the radio button associate with the resource type. Can be null. */
getWidget()143         Button getWidget() {
144             return mWidget;
145         }
146 
147         /**
148          * Returns the seed used to fill the root element values.
149          * The seed might be either a String, a String array, an {@link ElementDescriptor},
150          * a {@link DocumentDescriptor} or null.
151          */
getRootSeed()152         Object getRootSeed() {
153             return mRootSeed;
154         }
155 
156         /** Returns the default root element that should be selected by default. Can be null. */
getDefaultRoot()157         String getDefaultRoot() {
158             return mDefaultRoot;
159         }
160 
161         /**
162          * Returns the list of all possible root elements for the resource type.
163          * This can be an empty ArrayList but not null.
164          * <p/>
165          * TODO: the root list SHOULD depend on the currently selected project, to include
166          * custom classes.
167          */
getRoots()168         ArrayList<String> getRoots() {
169             return mRoots;
170         }
171 
172         /**
173          * If the generated resource XML file requires an "android" XMLNS, this should be set
174          * to {@link SdkConstants#NS_RESOURCES}. When it is null, no XMLNS is generated.
175          */
getXmlns()176         String getXmlns() {
177             return mXmlns;
178         }
179 
180         /**
181          * When not null, this represent extra attributes that must be specified in the
182          * root element of the generated XML file. When null, no extra attributes are inserted.
183          */
getDefaultAttrs()184         String getDefaultAttrs() {
185             return mDefaultAttrs;
186         }
187 
188         /**
189          * The minimum API level required by the current SDK target to support this feature.
190          */
getTargetApiLevel()191         public int getTargetApiLevel() {
192             return mTargetApiLevel;
193         }
194     }
195 
196     /**
197      * TypeInfo, information for each "type" of file that can be created.
198      */
199     private static final TypeInfo[] sTypes = {
200         new TypeInfo(
201                 "Layout",                                           // UI name
202                 "An XML file that describes a screen layout.",      // tooltip
203                 ResourceFolderType.LAYOUT,                          // folder type
204                 AndroidTargetData.DESCRIPTOR_LAYOUT,                // root seed
205                 "LinearLayout",                                     // default root
206                 SdkConstants.NS_RESOURCES,                          // xmlns
207                 "android:layout_width=\"wrap_content\"\n" +         // default attributes
208                 "android:layout_height=\"wrap_content\"",
209                 1                                                   // target API level
210                 ),
211         new TypeInfo("Values",                                      // UI name
212                 "An XML file with simple values: colors, strings, dimensions, etc.", // tooltip
213                 ResourceFolderType.VALUES,                          // folder type
214                 ResourcesDescriptors.ROOT_ELEMENT,                  // root seed
215                 null,                                               // default root
216                 null,                                               // xmlns
217                 null,                                               // default attributes
218                 1                                                   // target API level
219                 ),
220         new TypeInfo("Menu",                                        // UI name
221                 "An XML file that describes an menu.",              // tooltip
222                 ResourceFolderType.MENU,                            // folder type
223                 MenuDescriptors.MENU_ROOT_ELEMENT,                  // root seed
224                 null,                                               // default root
225                 SdkConstants.NS_RESOURCES,                          // xmlns
226                 null,                                               // default attributes
227                 1                                                   // target API level
228                 ),
229         new TypeInfo("AppWidget Provider",                          // UI name
230                 "An XML file that describes a widget provider.",    // tooltip
231                 ResourceFolderType.XML,                             // folder type
232                 AndroidTargetData.DESCRIPTOR_APPWIDGET_PROVIDER,    // root seed
233                 null,                                               // default root
234                 SdkConstants.NS_RESOURCES,                          // xmlns
235                 null,                                               // default attributes
236                 3                                                   // target API level
237                 ),
238         new TypeInfo("Preference",                                  // UI name
239                 "An XML file that describes preferences.",          // tooltip
240                 ResourceFolderType.XML,                             // folder type
241                 AndroidTargetData.DESCRIPTOR_PREFERENCES,           // root seed
242                 SdkConstants.CLASS_NAME_PREFERENCE_SCREEN,          // default root
243                 SdkConstants.NS_RESOURCES,                          // xmlns
244                 null,                                               // default attributes
245                 1                                                   // target API level
246                 ),
247         new TypeInfo("Searchable",                                  // UI name
248                 "An XML file that describes a searchable.",         // tooltip
249                 ResourceFolderType.XML,                             // folder type
250                 AndroidTargetData.DESCRIPTOR_SEARCHABLE,            // root seed
251                 null,                                               // default root
252                 SdkConstants.NS_RESOURCES,                          // xmlns
253                 null,                                               // default attributes
254                 1                                                   // target API level
255                 ),
256         new TypeInfo("Animation",                                   // UI name
257                 "An XML file that describes an animation.",         // tooltip
258                 ResourceFolderType.ANIM,                            // folder type
259                 // TODO reuse constants if we ever make an editor with descriptors for animations
260                 new String[] {                                      // root seed
261                     "set",          //$NON-NLS-1$
262                     "alpha",        //$NON-NLS-1$
263                     "scale",        //$NON-NLS-1$
264                     "translate",    //$NON-NLS-1$
265                     "rotate"        //$NON-NLS-1$
266                     },
267                 "set",              //$NON-NLS-1$                   // default root
268                 null,                                               // xmlns
269                 null,                                               // default attributes
270                 1                                                   // target API level
271                 ),
272     };
273 
274     /** Number of columns in the grid layout */
275     final static int NUM_COL = 4;
276 
277     /** Absolute destination folder root, e.g. "/res/" */
278     private static final String RES_FOLDER_ABS = AndroidConstants.WS_RESOURCES + AndroidConstants.WS_SEP;
279     /** Relative destination folder root, e.g. "res/" */
280     private static final String RES_FOLDER_REL = SdkConstants.FD_RESOURCES + AndroidConstants.WS_SEP;
281 
282     private IProject mProject;
283     private Text mProjectTextField;
284     private Button mProjectBrowseButton;
285     private Text mFileNameTextField;
286     private Text mWsFolderPathTextField;
287     private Combo mRootElementCombo;
288     private IStructuredSelection mInitialSelection;
289     private ConfigurationSelector mConfigSelector;
290     private FolderConfiguration mTempConfig = new FolderConfiguration();
291     private boolean mInternalWsFolderPathUpdate;
292     private boolean mInternalTypeUpdate;
293     private boolean mInternalConfigSelectorUpdate;
294     private ProjectChooserHelper mProjectChooserHelper;
295     private TargetChangeListener mSdkTargetChangeListener;
296 
297     private TypeInfo mCurrentTypeInfo;
298 
299     // --- UI creation ---
300 
301     /**
302      * Constructs a new {@link NewXmlFileCreationPage}.
303      * <p/>
304      * Called by {@link NewXmlFileWizard#createMainPage()}.
305      */
NewXmlFileCreationPage(String pageName)306     protected NewXmlFileCreationPage(String pageName) {
307         super(pageName);
308         setPageComplete(false);
309     }
310 
setInitialSelection(IStructuredSelection initialSelection)311     public void setInitialSelection(IStructuredSelection initialSelection) {
312         mInitialSelection = initialSelection;
313     }
314 
315     /**
316      * Called by the parent Wizard to create the UI for this Wizard Page.
317      *
318      * {@inheritDoc}
319      *
320      * @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite)
321      */
createControl(Composite parent)322     public void createControl(Composite parent) {
323         Composite composite = new Composite(parent, SWT.NULL);
324         composite.setFont(parent.getFont());
325 
326         initializeDialogUnits(parent);
327 
328         composite.setLayout(new GridLayout(NUM_COL, false /*makeColumnsEqualWidth*/));
329         composite.setLayoutData(new GridData(GridData.FILL_BOTH));
330 
331         createProjectGroup(composite);
332         createTypeGroup(composite);
333         createRootGroup(composite);
334 
335         // Show description the first time
336         setErrorMessage(null);
337         setMessage(null);
338         setControl(composite);
339 
340         // Update state the first time
341         initializeFromSelection(mInitialSelection);
342         initializeRootValues();
343         enableTypesBasedOnApi();
344         if (mCurrentTypeInfo != null) {
345             updateRootCombo(mCurrentTypeInfo);
346         }
347         installTargetChangeListener();
348         validatePage();
349     }
350 
installTargetChangeListener()351     private void installTargetChangeListener() {
352         mSdkTargetChangeListener = new TargetChangeListener() {
353             @Override
354             public IProject getProject() {
355                 return mProject;
356             }
357 
358             @Override
359             public void reload() {
360                 if (mProject != null) {
361                     changeProject(mProject);
362                 }
363             }
364         };
365 
366         AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener);
367     }
368 
369     @Override
dispose()370     public void dispose() {
371 
372         if (mSdkTargetChangeListener != null) {
373             AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener);
374             mSdkTargetChangeListener = null;
375         }
376 
377         super.dispose();
378     }
379 
380     /**
381      * Returns the target project or null.
382      */
getProject()383     public IProject getProject() {
384         return mProject;
385     }
386 
387     /**
388      * Returns the destination filename or an empty string.
389      */
getFileName()390     public String getFileName() {
391         return mFileNameTextField == null ? "" : mFileNameTextField.getText();         //$NON-NLS-1$
392     }
393 
394     /**
395      * Returns the destination folder path relative to the project or an empty string.
396      */
getWsFolderPath()397     public String getWsFolderPath() {
398         return mWsFolderPathTextField == null ? "" : mWsFolderPathTextField.getText(); //$NON-NLS-1$
399     }
400 
401 
402     /**
403      * Returns an {@link IFile} on the destination file.
404      * <p/>
405      * Uses {@link #getProject()}, {@link #getWsFolderPath()} and {@link #getFileName()}.
406      * <p/>
407      * Returns null if the project, filename or folder are invalid and the destination file
408      * cannot be determined.
409      * <p/>
410      * The {@link IFile} is a resource. There might or might not be an actual real file.
411      */
getDestinationFile()412     public IFile getDestinationFile() {
413         IProject project = getProject();
414         String wsFolderPath = getWsFolderPath();
415         String fileName = getFileName();
416         if (project != null && wsFolderPath.length() > 0 && fileName.length() > 0) {
417             IPath dest = new Path(wsFolderPath).append(fileName);
418             IFile file = project.getFile(dest);
419             return file;
420         }
421         return null;
422     }
423 
424     /**
425      * Returns the {@link TypeInfo} for the currently selected type radio button.
426      * Returns null if no radio button is selected.
427      *
428      * @return A {@link TypeInfo} or null.
429      */
getSelectedType()430     public TypeInfo getSelectedType() {
431         TypeInfo type = null;
432         for (TypeInfo ti : sTypes) {
433             if (ti.getWidget().getSelection()) {
434                 type = ti;
435                 break;
436             }
437         }
438         return type;
439     }
440 
441     /**
442      * Returns the selected root element string, if any.
443      *
444      * @return The selected root element string or null.
445      */
getRootElement()446     public String getRootElement() {
447         int index = mRootElementCombo.getSelectionIndex();
448         if (index >= 0) {
449             return mRootElementCombo.getItem(index);
450         }
451         return null;
452     }
453 
454     // --- UI creation ---
455 
456     /**
457      * Helper method to create a new GridData with an horizontal span.
458      *
459      * @param horizSpan The number of cells for the horizontal span.
460      * @return A new GridData with the horizontal span.
461      */
newGridData(int horizSpan)462     private GridData newGridData(int horizSpan) {
463         GridData gd = new GridData();
464         gd.horizontalSpan = horizSpan;
465         return gd;
466     }
467 
468     /**
469      * Helper method to create a new GridData with an horizontal span and a style.
470      *
471      * @param horizSpan The number of cells for the horizontal span.
472      * @param style The style, e.g. {@link GridData#FILL_HORIZONTAL}
473      * @return A new GridData with the horizontal span and the style.
474      */
newGridData(int horizSpan, int style)475     private GridData newGridData(int horizSpan, int style) {
476         GridData gd = new GridData(style);
477         gd.horizontalSpan = horizSpan;
478         return gd;
479     }
480 
481     /**
482      * Helper method that creates an empty cell in the parent composite.
483      *
484      * @param parent The parent composite.
485      */
emptyCell(Composite parent)486     private void emptyCell(Composite parent) {
487         new Label(parent, SWT.NONE);
488     }
489 
490     /**
491      * Pads the parent with empty cells to match the number of columns of the parent grid.
492      *
493      * @param parent A grid layout with NUM_COL columns
494      * @param col The current number of columns used.
495      * @return 0, the new number of columns used, for convenience.
496      */
padWithEmptyCells(Composite parent, int col)497     private int padWithEmptyCells(Composite parent, int col) {
498         for (; col < NUM_COL; ++col) {
499             emptyCell(parent);
500         }
501         col = 0;
502         return col;
503     }
504 
505     /**
506      * Creates the project & filename fields.
507      * <p/>
508      * The parent must be a GridLayout with NUM_COL colums.
509      */
createProjectGroup(Composite parent)510     private void createProjectGroup(Composite parent) {
511         int col = 0;
512 
513         // project name
514         String tooltip = "The Android Project where the new resource file will be created.";
515         Label label = new Label(parent, SWT.NONE);
516         label.setText("Project");
517         label.setToolTipText(tooltip);
518         ++col;
519 
520         mProjectTextField = new Text(parent, SWT.BORDER);
521         mProjectTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
522         mProjectTextField.setToolTipText(tooltip);
523         mProjectTextField.addModifyListener(new ModifyListener() {
524             public void modifyText(ModifyEvent e) {
525                 onProjectFieldUpdated();
526             }
527         });
528         ++col;
529 
530         mProjectBrowseButton = new Button(parent, SWT.NONE);
531         mProjectBrowseButton.setText("Browse...");
532         mProjectBrowseButton.setToolTipText("Allows you to select the Android project to modify.");
533         mProjectBrowseButton.addSelectionListener(new SelectionAdapter() {
534            @Override
535             public void widgetSelected(SelectionEvent e) {
536                onProjectBrowse();
537             }
538         });
539         mProjectChooserHelper = new ProjectChooserHelper(parent.getShell(), null /*filter*/);
540         ++col;
541 
542         col = padWithEmptyCells(parent, col);
543 
544         // file name
545         tooltip = "The name of the resource file to create.";
546         label = new Label(parent, SWT.NONE);
547         label.setText("File");
548         label.setToolTipText(tooltip);
549         ++col;
550 
551         mFileNameTextField = new Text(parent, SWT.BORDER);
552         mFileNameTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
553         mFileNameTextField.setToolTipText(tooltip);
554         mFileNameTextField.addModifyListener(new ModifyListener() {
555             public void modifyText(ModifyEvent e) {
556                 validatePage();
557             }
558         });
559         ++col;
560 
561         padWithEmptyCells(parent, col);
562     }
563 
564     /**
565      * Creates the type field, {@link ConfigurationSelector} and the folder field.
566      * <p/>
567      * The parent must be a GridLayout with NUM_COL colums.
568      */
createTypeGroup(Composite parent)569     private void createTypeGroup(Composite parent) {
570         // separator
571         Label label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL);
572         label.setLayoutData(newGridData(NUM_COL, GridData.GRAB_HORIZONTAL));
573 
574         // label before type radios
575         label = new Label(parent, SWT.NONE);
576         label.setText("What type of resource would you like to create?");
577         label.setLayoutData(newGridData(NUM_COL));
578 
579         // display the types on three columns of radio buttons.
580         emptyCell(parent);
581         Composite grid = new Composite(parent, SWT.NONE);
582         padWithEmptyCells(parent, 2);
583 
584         grid.setLayout(new GridLayout(NUM_COL, true /*makeColumnsEqualWidth*/));
585 
586         SelectionListener radioListener = new SelectionAdapter() {
587             @Override
588             public void widgetSelected(SelectionEvent e) {
589                 // single-click. Only do something if activated.
590                 if (e.getSource() instanceof Button) {
591                     onRadioTypeUpdated((Button) e.getSource());
592                 }
593             }
594         };
595 
596         int n = sTypes.length;
597         int num_lines = (n + NUM_COL/2) / NUM_COL;
598         for (int line = 0, k = 0; line < num_lines; line++) {
599             for (int i = 0; i < NUM_COL; i++, k++) {
600                 if (k < n) {
601                     TypeInfo type = sTypes[k];
602                     Button radio = new Button(grid, SWT.RADIO);
603                     type.setWidget(radio);
604                     radio.setSelection(false);
605                     radio.setText(type.getUiName());
606                     radio.setToolTipText(type.getTooltip());
607                     radio.addSelectionListener(radioListener);
608                 } else {
609                     emptyCell(grid);
610                 }
611             }
612         }
613 
614         // label before configuration selector
615         label = new Label(parent, SWT.NONE);
616         label.setText("What type of resource configuration would you like?");
617         label.setLayoutData(newGridData(NUM_COL));
618 
619         // configuration selector
620         emptyCell(parent);
621         mConfigSelector = new ConfigurationSelector(parent, false /* deviceMode*/);
622         GridData gd = newGridData(2, GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL);
623         gd.widthHint = ConfigurationSelector.WIDTH_HINT;
624         gd.heightHint = ConfigurationSelector.HEIGHT_HINT;
625         mConfigSelector.setLayoutData(gd);
626         mConfigSelector.setOnChangeListener(new onConfigSelectorUpdated());
627         emptyCell(parent);
628 
629         // folder name
630         String tooltip = "The folder where the file will be generated, relative to the project.";
631         label = new Label(parent, SWT.NONE);
632         label.setText("Folder");
633         label.setToolTipText(tooltip);
634 
635         mWsFolderPathTextField = new Text(parent, SWT.BORDER);
636         mWsFolderPathTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
637         mWsFolderPathTextField.setToolTipText(tooltip);
638         mWsFolderPathTextField.addModifyListener(new ModifyListener() {
639             public void modifyText(ModifyEvent e) {
640                 onWsFolderPathUpdated();
641             }
642         });
643     }
644 
645     /**
646      * Creates the root element combo.
647      * <p/>
648      * The parent must be a GridLayout with NUM_COL colums.
649      */
createRootGroup(Composite parent)650     private void createRootGroup(Composite parent) {
651         // separator
652         Label label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL);
653         label.setLayoutData(newGridData(NUM_COL, GridData.GRAB_HORIZONTAL));
654 
655         // label before the root combo
656         String tooltip = "The root element to create in the XML file.";
657         label = new Label(parent, SWT.NONE);
658         label.setText("Select the root element for the XML file:");
659         label.setLayoutData(newGridData(NUM_COL));
660         label.setToolTipText(tooltip);
661 
662         // root combo
663         emptyCell(parent);
664 
665         mRootElementCombo = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY);
666         mRootElementCombo.setEnabled(false);
667         mRootElementCombo.select(0);
668         mRootElementCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
669         mRootElementCombo.setToolTipText(tooltip);
670 
671         padWithEmptyCells(parent, 2);
672     }
673 
674     /**
675      * Called by {@link NewXmlFileWizard} to initialize the page with the selection
676      * received by the wizard -- typically the current user workbench selection.
677      * <p/>
678      * Things we expect to find out from the selection:
679      * <ul>
680      * <li>The project name, valid if it's an android nature.</li>
681      * <li>The current folder, valid if it's a folder under /res</li>
682      * <li>An existing filename, in which case the user will be asked whether to override it.</li>
683      * <ul>
684      *
685      * @param selection The selection when the wizard was initiated.
686      */
initializeFromSelection(IStructuredSelection selection)687     private void initializeFromSelection(IStructuredSelection selection) {
688         if (selection == null) {
689             return;
690         }
691 
692         // Find the best match in the element list. In case there are multiple selected elements
693         // select the one that provides the most information and assign them a score,
694         // e.g. project=1 + folder=2 + file=4.
695         IProject targetProject = null;
696         String targetWsFolderPath = null;
697         String targetFileName = null;
698         int targetScore = 0;
699         for (Object element : selection.toList()) {
700             if (element instanceof IAdaptable) {
701                 IResource res = (IResource) ((IAdaptable) element).getAdapter(IResource.class);
702                 IProject project = res != null ? res.getProject() : null;
703 
704                 // Is this an Android project?
705                 try {
706                     if (project == null || !project.hasNature(AndroidConstants.NATURE_DEFAULT)) {
707                         continue;
708                     }
709                 } catch (CoreException e) {
710                     // checking the nature failed, ignore this resource
711                     continue;
712                 }
713 
714                 int score = 1; // we have a valid project at least
715 
716                 IPath wsFolderPath = null;
717                 String fileName = null;
718                 if (res.getType() == IResource.FOLDER) {
719                     wsFolderPath = res.getProjectRelativePath();
720                 } else if (res.getType() == IResource.FILE) {
721                     fileName = res.getName();
722                     wsFolderPath = res.getParent().getProjectRelativePath();
723                 }
724 
725                 // Disregard this folder selection if it doesn't point to /res/something
726                 if (wsFolderPath != null &&
727                         wsFolderPath.segmentCount() > 1 &&
728                         SdkConstants.FD_RESOURCES.equals(wsFolderPath.segment(0))) {
729                     score += 2;
730                 } else {
731                     wsFolderPath = null;
732                     fileName = null;
733                 }
734 
735                 score += fileName != null ? 4 : 0;
736 
737                 if (score > targetScore) {
738                     targetScore = score;
739                     targetProject = project;
740                     targetWsFolderPath = wsFolderPath != null ? wsFolderPath.toString() : null;
741                     targetFileName = fileName;
742                 }
743             }
744         }
745 
746         // Now set the UI accordingly
747         if (targetScore > 0) {
748             mProject = targetProject;
749             mProjectTextField.setText(targetProject != null ? targetProject.getName() : ""); //$NON-NLS-1$
750             mFileNameTextField.setText(targetFileName != null ? targetFileName : ""); //$NON-NLS-1$
751             mWsFolderPathTextField.setText(targetWsFolderPath != null ? targetWsFolderPath : ""); //$NON-NLS-1$
752         }
753     }
754 
755     /**
756      * Initialize the root values of the type infos based on the current framework values.
757      */
initializeRootValues()758     private void initializeRootValues() {
759         for (TypeInfo type : sTypes) {
760             // Clear all the roots for this type
761             ArrayList<String> roots = type.getRoots();
762             if (roots.size() > 0) {
763                 roots.clear();
764             }
765 
766             // depending of the type of the seed, initialize the root in different ways
767             Object rootSeed = type.getRootSeed();
768 
769             if (rootSeed instanceof String) {
770                 // The seed is a single string, Add it as-is.
771                 roots.add((String) rootSeed);
772             } else if (rootSeed instanceof String[]) {
773                 // The seed is an array of strings. Add them as-is.
774                 for (String value : (String[]) rootSeed) {
775                     roots.add(value);
776                 }
777             } else if (rootSeed instanceof Integer && mProject != null) {
778                 // The seed is a descriptor reference defined in AndroidTargetData.DESCRIPTOR_*
779                 // In this case add all the children element descriptors defined, recursively,
780                 // and avoid infinite recursion by keeping track of what has already been added.
781 
782                 // Note: if project is null, the root list will be empty since it has been
783                 // cleared above.
784 
785                 // get the AndroidTargetData from the project
786                 IAndroidTarget target = null;
787                 AndroidTargetData data = null;
788 
789                 target = Sdk.getCurrent().getTarget(mProject);
790                 if (target == null) {
791                     // A project should have a target. The target can be missing if the project
792                     // is an old project for which a target hasn't been affected or if the
793                     // target no longer exists in this SDK. Simply log the error and dismiss.
794 
795                     AdtPlugin.log(IStatus.INFO,
796                             "NewXmlFile wizard: no platform target for project %s",  //$NON-NLS-1$
797                             mProject.getName());
798                     continue;
799                 } else {
800                     data = Sdk.getCurrent().getTargetData(target);
801 
802                     if (data == null) {
803                         // We should have both a target and its data.
804                         // However if the wizard is invoked whilst the platform is still being
805                         // loaded we can end up in a weird case where we have a target but it
806                         // doesn't have any data yet.
807                         // Lets log a warning and silently ignore this root.
808 
809                         AdtPlugin.log(IStatus.INFO,
810                               "NewXmlFile wizard: no data for target %s, project %s",  //$NON-NLS-1$
811                               target.getName(), mProject.getName());
812                         continue;
813                     }
814                 }
815 
816                 IDescriptorProvider provider = data.getDescriptorProvider((Integer)rootSeed);
817                 ElementDescriptor descriptor = provider.getDescriptor();
818                 if (descriptor != null) {
819                     HashSet<ElementDescriptor> visited = new HashSet<ElementDescriptor>();
820                     initRootElementDescriptor(roots, descriptor, visited);
821                 }
822 
823                 // Sort alphabetically.
824                 Collections.sort(roots);
825             }
826         }
827     }
828 
829     /**
830      * Helper method to recursively insert all XML names for the given {@link ElementDescriptor}
831      * into the roots array list. Keeps track of visited nodes to avoid infinite recursion.
832      * Also avoids inserting the top {@link DocumentDescriptor} which is generally synthetic
833      * and not a valid root element.
834      */
initRootElementDescriptor(ArrayList<String> roots, ElementDescriptor desc, HashSet<ElementDescriptor> visited)835     private void initRootElementDescriptor(ArrayList<String> roots,
836             ElementDescriptor desc, HashSet<ElementDescriptor> visited) {
837         if (!(desc instanceof DocumentDescriptor)) {
838             String xmlName = desc.getXmlName();
839             if (xmlName != null && xmlName.length() > 0) {
840                 roots.add(xmlName);
841             }
842         }
843 
844         visited.add(desc);
845 
846         for (ElementDescriptor child : desc.getChildren()) {
847             if (!visited.contains(child)) {
848                 initRootElementDescriptor(roots, child, visited);
849             }
850         }
851     }
852 
853     /**
854      * Callback called when the user edits the project text field.
855      */
onProjectFieldUpdated()856     private void onProjectFieldUpdated() {
857         String project = mProjectTextField.getText();
858 
859         // Is this a valid project?
860         IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null /*javaModel*/);
861         IProject found = null;
862         for (IJavaProject p : projects) {
863             if (p.getProject().getName().equals(project)) {
864                 found = p.getProject();
865                 break;
866             }
867         }
868 
869         if (found != mProject) {
870             changeProject(found);
871         }
872     }
873 
874     /**
875      * Callback called when the user uses the "Browse Projects" button.
876      */
onProjectBrowse()877     private void onProjectBrowse() {
878         IJavaProject p = mProjectChooserHelper.chooseJavaProject(mProjectTextField.getText(),
879                 "Please select the target project");
880         if (p != null) {
881             changeProject(p.getProject());
882             mProjectTextField.setText(mProject.getName());
883         }
884     }
885 
886     /**
887      * Changes mProject to the given new project and update the UI accordingly.
888      * <p/>
889      * Note that this does not check if the new project is the same as the current one
890      * on purpose, which allows a project to be updated when its target has changed or
891      * when targets are loaded in the background.
892      */
changeProject(IProject newProject)893     private void changeProject(IProject newProject) {
894         mProject = newProject;
895 
896         // enable types based on new API level
897         enableTypesBasedOnApi();
898 
899         // update the folder name based on API level
900         resetFolderPath(false /*validate*/);
901 
902         // update the Type with the new descriptors.
903         initializeRootValues();
904 
905         // update the combo
906         updateRootCombo(getSelectedType());
907 
908         validatePage();
909     }
910 
911     /**
912      * Callback called when the Folder text field is changed, either programmatically
913      * or by the user.
914      */
onWsFolderPathUpdated()915     private void onWsFolderPathUpdated() {
916         if (mInternalWsFolderPathUpdate) {
917             return;
918         }
919 
920         String wsFolderPath = mWsFolderPathTextField.getText();
921 
922         // This is a custom path, we need to sanitize it.
923         // First it should start with "/res/". Then we need to make sure there are no
924         // relative paths, things like "../" or "./" or even "//".
925         wsFolderPath = wsFolderPath.replaceAll("/+\\.\\./+|/+\\./+|//+|\\\\+|^/+", "/");  //$NON-NLS-1$ //$NON-NLS-2$
926         wsFolderPath = wsFolderPath.replaceAll("^\\.\\./+|^\\./+", "");                   //$NON-NLS-1$ //$NON-NLS-2$
927         wsFolderPath = wsFolderPath.replaceAll("/+\\.\\.$|/+\\.$|/+$", "");               //$NON-NLS-1$ //$NON-NLS-2$
928 
929         ArrayList<TypeInfo> matches = new ArrayList<TypeInfo>();
930 
931         // We get "res/foo" from selections relative to the project when we want a "/res/foo" path.
932         if (wsFolderPath.startsWith(RES_FOLDER_REL)) {
933             wsFolderPath = RES_FOLDER_ABS + wsFolderPath.substring(RES_FOLDER_REL.length());
934 
935             mInternalWsFolderPathUpdate = true;
936             mWsFolderPathTextField.setText(wsFolderPath);
937             mInternalWsFolderPathUpdate = false;
938         }
939 
940         if (wsFolderPath.startsWith(RES_FOLDER_ABS)) {
941             wsFolderPath = wsFolderPath.substring(RES_FOLDER_ABS.length());
942 
943             int pos = wsFolderPath.indexOf(AndroidConstants.WS_SEP_CHAR);
944             if (pos >= 0) {
945                 wsFolderPath = wsFolderPath.substring(0, pos);
946             }
947 
948             String[] folderSegments = wsFolderPath.split(FolderConfiguration.QUALIFIER_SEP);
949 
950             if (folderSegments.length > 0) {
951                 String folderName = folderSegments[0];
952 
953                 // update config selector
954                 mInternalConfigSelectorUpdate = true;
955                 mConfigSelector.setConfiguration(folderSegments);
956                 mInternalConfigSelectorUpdate = false;
957 
958                 boolean selected = false;
959                 for (TypeInfo type : sTypes) {
960                     if (type.getResFolderName().equals(folderName)) {
961                         matches.add(type);
962                         selected |= type.getWidget().getSelection();
963                     }
964                 }
965 
966                 if (matches.size() == 1) {
967                     // If there's only one match, select it if it's not already selected
968                     if (!selected) {
969                         selectType(matches.get(0));
970                     }
971                 } else if (matches.size() > 1) {
972                     // There are multiple type candidates for this folder. This can happen
973                     // for /res/xml for example. Check to see if one of them is currently
974                     // selected. If yes, leave the selection unchanged. If not, deselect all type.
975                     if (!selected) {
976                         selectType(null);
977                     }
978                 } else {
979                     // Nothing valid was selected.
980                     selectType(null);
981                 }
982             }
983         }
984 
985         validatePage();
986     }
987 
988     /**
989      * Callback called when one of the type radio button is changed.
990      *
991      * @param typeWidget The type radio button that changed.
992      */
onRadioTypeUpdated(Button typeWidget)993     private void onRadioTypeUpdated(Button typeWidget) {
994         // Do nothing if this is an internal modification or if the widget has been
995         // de-selected.
996         if (mInternalTypeUpdate || !typeWidget.getSelection()) {
997             return;
998         }
999 
1000         // Find type info that has just been enabled.
1001         TypeInfo type = null;
1002         for (TypeInfo ti : sTypes) {
1003             if (ti.getWidget() == typeWidget) {
1004                 type = ti;
1005                 break;
1006             }
1007         }
1008 
1009         if (type == null) {
1010             return;
1011         }
1012 
1013         // update the combo
1014 
1015         updateRootCombo(type);
1016 
1017         // update the folder path
1018 
1019         String wsFolderPath = mWsFolderPathTextField.getText();
1020         String newPath = null;
1021 
1022         mConfigSelector.getConfiguration(mTempConfig);
1023         ResourceQualifier qual = mTempConfig.getInvalidQualifier();
1024         if (qual == null) {
1025             // The configuration is valid. Reformat the folder path using the canonical
1026             // value from the configuration.
1027 
1028             newPath = RES_FOLDER_ABS + mTempConfig.getFolderName(type.getResFolderType());
1029         } else {
1030             // The configuration is invalid. We still update the path but this time
1031             // do it manually on the string.
1032             if (wsFolderPath.startsWith(RES_FOLDER_ABS)) {
1033                 wsFolderPath.replaceFirst(
1034                         "^(" + RES_FOLDER_ABS +")[^-]*(.*)",         //$NON-NLS-1$ //$NON-NLS-2$
1035                         "\\1" + type.getResFolderName() + "\\2");   //$NON-NLS-1$ //$NON-NLS-2$
1036             } else {
1037                 newPath = RES_FOLDER_ABS + mTempConfig.getFolderName(type.getResFolderType());
1038             }
1039         }
1040 
1041         if (newPath != null && !newPath.equals(wsFolderPath)) {
1042             mInternalWsFolderPathUpdate = true;
1043             mWsFolderPathTextField.setText(newPath);
1044             mInternalWsFolderPathUpdate = false;
1045         }
1046 
1047         validatePage();
1048     }
1049 
1050     /**
1051      * Helper method that fills the values of the "root element" combo box based
1052      * on the currently selected type radio button. Also disables the combo is there's
1053      * only one choice. Always select the first root element for the given type.
1054      *
1055      * @param type The currently selected {@link TypeInfo}. Cannot be null.
1056      */
updateRootCombo(TypeInfo type)1057     private void updateRootCombo(TypeInfo type) {
1058         // reset all the values in the combo
1059         mRootElementCombo.removeAll();
1060 
1061         if (type != null) {
1062             // get the list of roots. The list can be empty but not null.
1063             ArrayList<String> roots = type.getRoots();
1064 
1065             // enable the combo if there's more than one choice
1066             mRootElementCombo.setEnabled(roots != null && roots.size() > 1);
1067 
1068             for (String root : roots) {
1069                 mRootElementCombo.add(root);
1070             }
1071 
1072             int index = 0; // default is to select the first one
1073             String defaultRoot = type.getDefaultRoot();
1074             if (defaultRoot != null) {
1075                 index = roots.indexOf(defaultRoot);
1076             }
1077             mRootElementCombo.select(index < 0 ? 0 : index);
1078         }
1079     }
1080 
1081     /**
1082      * Callback called when the configuration has changed in the {@link ConfigurationSelector}.
1083      */
1084     private class onConfigSelectorUpdated implements Runnable {
1085         public void run() {
1086             if (mInternalConfigSelectorUpdate) {
1087                 return;
1088             }
1089 
1090             resetFolderPath(true /*validate*/);
1091         }
1092     }
1093 
1094     /**
1095      * Helper method to select on of the type radio buttons.
1096      *
1097      * @param type The TypeInfo matching the radio button to selected or null to deselect them all.
1098      */
1099     private void selectType(TypeInfo type) {
1100         if (type == null || !type.getWidget().getSelection()) {
1101             mInternalTypeUpdate = true;
1102             mCurrentTypeInfo = type;
1103             for (TypeInfo type2 : sTypes) {
1104                 type2.getWidget().setSelection(type2 == type);
1105             }
1106             updateRootCombo(type);
1107             mInternalTypeUpdate = false;
1108         }
1109     }
1110 
1111     /**
1112      * Helper method to enable the type radio buttons depending on the current API level.
1113      * <p/>
1114      * A type radio button is enabled either if:
1115      * - if mProject is null, API level 1 is considered valid
1116      * - if mProject is !null, the project->target->API must be >= to the type's API level.
1117      */
1118     private void enableTypesBasedOnApi() {
1119 
1120         IAndroidTarget target = mProject != null ? Sdk.getCurrent().getTarget(mProject) : null;
1121         int currentApiLevel = 1;
1122         if (target != null) {
1123             currentApiLevel = target.getVersion().getApiLevel();
1124         }
1125 
1126         for (TypeInfo type : sTypes) {
1127             type.getWidget().setEnabled(type.getTargetApiLevel() <= currentApiLevel);
1128         }
1129     }
1130 
1131     /**
1132      * Reset the current Folder path based on the UI selection
1133      * @param validate if true, force a call to {@link #validatePage()}.
1134      */
1135     private void resetFolderPath(boolean validate) {
1136         TypeInfo type = getSelectedType();
1137 
1138         if (type != null) {
1139             mConfigSelector.getConfiguration(mTempConfig);
1140             StringBuffer sb = new StringBuffer(RES_FOLDER_ABS);
1141             sb.append(mTempConfig.getFolderName(type.getResFolderType()));
1142 
1143             mInternalWsFolderPathUpdate = true;
1144             mWsFolderPathTextField.setText(sb.toString());
1145             mInternalWsFolderPathUpdate = false;
1146 
1147             if (validate) {
1148                 validatePage();
1149             }
1150         }
1151     }
1152 
1153     /**
1154      * Validates the fields, displays errors and warnings.
1155      * Enables the finish button if there are no errors.
1156      */
1157     private void validatePage() {
1158         String error = null;
1159         String warning = null;
1160 
1161         // -- validate project
1162         if (getProject() == null) {
1163             error = "Please select an Android project.";
1164         }
1165 
1166         // -- validate filename
1167         if (error == null) {
1168             String fileName = getFileName();
1169             if (fileName == null || fileName.length() == 0) {
1170                 error = "A destination file name is required.";
1171             } else if (!fileName.endsWith(AndroidConstants.DOT_XML)) {
1172                 error = String.format("The filename must end with %1$s.", AndroidConstants.DOT_XML);
1173             }
1174         }
1175 
1176         // -- validate type
1177         if (error == null) {
1178             TypeInfo type = getSelectedType();
1179 
1180             if (type == null) {
1181                 error = "One of the types must be selected (e.g. layout, values, etc.)";
1182             }
1183         }
1184 
1185         // -- validate type API level
1186         if (error == null) {
1187             IAndroidTarget target = Sdk.getCurrent().getTarget(mProject);
1188             int currentApiLevel = 1;
1189             if (target != null) {
1190                 currentApiLevel = target.getVersion().getApiLevel();
1191             }
1192 
1193             TypeInfo type = getSelectedType();
1194 
1195             if (type.getTargetApiLevel() > currentApiLevel) {
1196                 error = "The API level of the selected type (e.g. AppWidget, etc.) is not " +
1197                         "compatible with the API level of the project.";
1198             }
1199         }
1200 
1201         // -- validate folder configuration
1202         if (error == null) {
1203             ConfigurationState state = mConfigSelector.getState();
1204             if (state == ConfigurationState.INVALID_CONFIG) {
1205                 ResourceQualifier qual = mConfigSelector.getInvalidQualifier();
1206                 if (qual != null) {
1207                     error = String.format("The qualifier '%1$s' is invalid in the folder configuration.",
1208                             qual.getName());
1209                 }
1210             } else if (state == ConfigurationState.REGION_WITHOUT_LANGUAGE) {
1211                 error = "The Region qualifier requires the Language qualifier.";
1212             }
1213         }
1214 
1215         // -- validate generated path
1216         if (error == null) {
1217             String wsFolderPath = getWsFolderPath();
1218             if (!wsFolderPath.startsWith(RES_FOLDER_ABS)) {
1219                 error = String.format("Target folder must start with %1$s.", RES_FOLDER_ABS);
1220             }
1221         }
1222 
1223         // -- validate destination file doesn't exist
1224         if (error == null) {
1225             IFile file = getDestinationFile();
1226             if (file != null && file.exists()) {
1227                 warning = "The destination file already exists";
1228             }
1229         }
1230 
1231         // -- update UI & enable finish if there's no error
1232         setPageComplete(error == null);
1233         if (error != null) {
1234             setMessage(error, WizardPage.ERROR);
1235         } else if (warning != null) {
1236             setMessage(warning, WizardPage.WARNING);
1237         } else {
1238             setErrorMessage(null);
1239             setMessage(null);
1240         }
1241     }
1242 
1243 }
1244