1 /*
2  * Copyright (C) 2012 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 package com.android.ide.eclipse.adt.internal.wizards.templates;
17 
18 
19 import static com.android.SdkConstants.ATTR_ID;
20 import static com.android.ide.eclipse.adt.AdtUtils.extractClassName;
21 import static com.android.ide.eclipse.adt.internal.wizards.templates.NewTemplatePage.WIZARD_PAGE_WIDTH;
22 
23 import com.android.annotations.Nullable;
24 import com.android.sdklib.SdkVersionInfo;
25 import com.android.ide.eclipse.adt.AdtPlugin;
26 import com.android.ide.eclipse.adt.AdtUtils;
27 import com.android.ide.eclipse.adt.internal.editors.IconFactory;
28 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
29 import com.android.ide.eclipse.adt.internal.wizards.newproject.ApplicationInfoPage;
30 import com.android.ide.eclipse.adt.internal.wizards.newproject.ProjectNamePage;
31 import com.android.sdklib.AndroidVersion;
32 import com.android.sdklib.IAndroidTarget;
33 import com.google.common.collect.Lists;
34 import com.google.common.collect.Maps;
35 
36 import org.eclipse.core.resources.IResource;
37 import org.eclipse.core.resources.IWorkspace;
38 import org.eclipse.core.resources.ResourcesPlugin;
39 import org.eclipse.core.runtime.IPath;
40 import org.eclipse.core.runtime.IStatus;
41 import org.eclipse.core.runtime.Platform;
42 import org.eclipse.core.runtime.Status;
43 import org.eclipse.jface.dialogs.IMessageProvider;
44 import org.eclipse.jface.fieldassist.ControlDecoration;
45 import org.eclipse.jface.fieldassist.FieldDecoration;
46 import org.eclipse.jface.fieldassist.FieldDecorationRegistry;
47 import org.eclipse.jface.wizard.WizardPage;
48 import org.eclipse.swt.SWT;
49 import org.eclipse.swt.events.FocusEvent;
50 import org.eclipse.swt.events.FocusListener;
51 import org.eclipse.swt.events.ModifyEvent;
52 import org.eclipse.swt.events.ModifyListener;
53 import org.eclipse.swt.events.SelectionEvent;
54 import org.eclipse.swt.events.SelectionListener;
55 import org.eclipse.swt.graphics.Image;
56 import org.eclipse.swt.layout.GridData;
57 import org.eclipse.swt.layout.GridLayout;
58 import org.eclipse.swt.widgets.Combo;
59 import org.eclipse.swt.widgets.Composite;
60 import org.eclipse.swt.widgets.Control;
61 import org.eclipse.swt.widgets.Label;
62 import org.eclipse.swt.widgets.Text;
63 
64 import java.util.ArrayList;
65 import java.util.List;
66 import java.util.Map;
67 
68 /**
69  * First wizard page in the "New Project From Template" wizard
70  */
71 public class NewProjectPage extends WizardPage
72         implements ModifyListener, SelectionListener, FocusListener {
73     private static final int FIELD_WIDTH = 300;
74     private static final String SAMPLE_PACKAGE_PREFIX = "com.example."; //$NON-NLS-1$
75     /** Suffix added by default to activity names */
76     static final String ACTIVITY_NAME_SUFFIX = "Activity";              //$NON-NLS-1$
77     /** Prefix added to default layout names */
78     static final String LAYOUT_NAME_PREFIX = "activity_";               //$NON-NLS-1$
79     private static final int INITIAL_MIN_SDK = 8;
80 
81     private final NewProjectWizardState mValues;
82     private Map<String, Integer> mMinNameToApi;
83     private Parameter mThemeParameter;
84     private Combo mThemeCombo;
85 
86     private Text mProjectText;
87     private Text mPackageText;
88     private Text mApplicationText;
89     private Combo mMinSdkCombo;
90     private Combo mTargetSdkCombo;
91     private Combo mBuildSdkCombo;
92     private Label mHelpIcon;
93     private Label mTipLabel;
94 
95     private boolean mIgnore;
96     private ControlDecoration mApplicationDec;
97     private ControlDecoration mProjectDec;
98     private ControlDecoration mPackageDec;
99     private ControlDecoration mBuildTargetDec;
100     private ControlDecoration mMinSdkDec;
101     private ControlDecoration mTargetSdkDec;
102     private ControlDecoration mThemeDec;
103 
NewProjectPage(NewProjectWizardState values)104     NewProjectPage(NewProjectWizardState values) {
105         super("newAndroidApp"); //$NON-NLS-1$
106         mValues = values;
107         setTitle("New Android Application");
108         setDescription("Creates a new Android Application");
109     }
110 
111     @SuppressWarnings("unused") // SWT constructors have side effects and aren't unused
112     @Override
createControl(Composite parent)113     public void createControl(Composite parent) {
114         Composite container = new Composite(parent, SWT.NULL);
115         setControl(container);
116         GridLayout gl_container = new GridLayout(4, false);
117         gl_container.horizontalSpacing = 10;
118         container.setLayout(gl_container);
119 
120         Label applicationLabel = new Label(container, SWT.NONE);
121         applicationLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1));
122         applicationLabel.setText("Application Name:");
123 
124         mApplicationText = new Text(container, SWT.BORDER);
125         GridData gdApplicationText = new GridData(SWT.LEFT, SWT.CENTER, true, false, 2, 1);
126         gdApplicationText.widthHint = FIELD_WIDTH;
127         mApplicationText.setLayoutData(gdApplicationText);
128         mApplicationText.addModifyListener(this);
129         mApplicationText.addFocusListener(this);
130         mApplicationDec = createFieldDecoration(mApplicationText,
131                 "The application name is shown in the Play Store, as well as in the " +
132                 "Manage Application list in Settings.");
133 
134         Label projectLabel = new Label(container, SWT.NONE);
135         projectLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1));
136         projectLabel.setText("Project Name:");
137         mProjectText = new Text(container, SWT.BORDER);
138         GridData gdProjectText = new GridData(SWT.LEFT, SWT.CENTER, true, false, 2, 1);
139         gdProjectText.widthHint = FIELD_WIDTH;
140         mProjectText.setLayoutData(gdProjectText);
141         mProjectText.addModifyListener(this);
142         mProjectText.addFocusListener(this);
143         mProjectDec = createFieldDecoration(mProjectText,
144                 "The project name is only used by Eclipse, but must be unique within the " +
145                 "workspace. This can typically be the same as the application name.");
146 
147         Label packageLabel = new Label(container, SWT.NONE);
148         packageLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1));
149         packageLabel.setText("Package Name:");
150 
151         mPackageText = new Text(container, SWT.BORDER);
152         GridData gdPackageText = new GridData(SWT.LEFT, SWT.CENTER, true, false, 2, 1);
153         gdPackageText.widthHint = FIELD_WIDTH;
154         mPackageText.setLayoutData(gdPackageText);
155         mPackageText.addModifyListener(this);
156         mPackageText.addFocusListener(this);
157         mPackageDec = createFieldDecoration(mPackageText,
158                 "The package name must be a unique identifier for your application.\n" +
159                 "It is typically not shown to users, but it *must* stay the same " +
160                 "for the lifetime of your application; it is how multiple versions " +
161                 "of the same application are considered the \"same app\".\nThis is " +
162                 "typically the reverse domain name of your organization plus one or " +
163                 "more application identifiers, and it must be a valid Java package " +
164                 "name.");
165         new Label(container, SWT.NONE);
166 
167         new Label(container, SWT.NONE);
168         new Label(container, SWT.NONE);
169         new Label(container, SWT.NONE);
170 
171         // Min SDK
172 
173         Label minSdkLabel = new Label(container, SWT.NONE);
174         minSdkLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1));
175         minSdkLabel.setText("Minimum Required SDK:");
176 
177         mMinSdkCombo = new Combo(container, SWT.READ_ONLY);
178         GridData gdMinSdkCombo = new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1);
179         gdMinSdkCombo.widthHint = FIELD_WIDTH;
180         mMinSdkCombo.setLayoutData(gdMinSdkCombo);
181 
182         // Pick most recent platform
183         IAndroidTarget[] targets = getCompilationTargets();
184         mMinNameToApi = Maps.newHashMap();
185         List<String> targetLabels = new ArrayList<String>(targets.length);
186         for (IAndroidTarget target : targets) {
187             String targetLabel;
188             if (target.isPlatform()
189                     && target.getVersion().getApiLevel() <= AdtUtils.getHighestKnownApiLevel()) {
190                 targetLabel = AdtUtils.getAndroidName(target.getVersion().getApiLevel());
191             } else {
192                 targetLabel = AdtUtils.getTargetLabel(target);
193             }
194             targetLabels.add(targetLabel);
195             mMinNameToApi.put(targetLabel, target.getVersion().getApiLevel());
196         }
197 
198         List<String> codeNames = Lists.newArrayList();
199         int buildTargetIndex = -1;
200         for (int i = 0, n = targets.length; i < n; i++) {
201             IAndroidTarget target = targets[i];
202             AndroidVersion version = target.getVersion();
203             int apiLevel = version.getApiLevel();
204             if (version.isPreview()) {
205                 String codeName = version.getCodename();
206                 String targetLabel = codeName + " Preview";
207                 codeNames.add(targetLabel);
208                 mMinNameToApi.put(targetLabel, apiLevel);
209             } else if (target.isPlatform()
210                     && (mValues.target == null ||
211                         apiLevel > mValues.target.getVersion().getApiLevel())) {
212                 mValues.target = target;
213                 buildTargetIndex = i;
214             }
215         }
216         List<String> labels = new ArrayList<String>(24);
217         for (String label : AdtUtils.getKnownVersions()) {
218             labels.add(label);
219         }
220         assert labels.size() >= 15; // *Known* versions to ADT, not installed/available versions
221         for (String codeName : codeNames) {
222             labels.add(codeName);
223         }
224         String[] versions = labels.toArray(new String[labels.size()]);
225         mMinSdkCombo.setItems(versions);
226         if (mValues.target != null && mValues.target.getVersion().isPreview()) {
227             mValues.minSdk = mValues.target.getVersion().getCodename();
228             mMinSdkCombo.setText(mValues.minSdk);
229             mValues.iconState.minSdk = mValues.target.getVersion().getApiLevel();
230             mValues.minSdkLevel = mValues.iconState.minSdk;
231         } else {
232             mMinSdkCombo.select(INITIAL_MIN_SDK - 1);
233             mValues.minSdk = Integer.toString(INITIAL_MIN_SDK);
234             mValues.minSdkLevel = INITIAL_MIN_SDK;
235             mValues.iconState.minSdk = INITIAL_MIN_SDK;
236         }
237         mMinSdkCombo.addSelectionListener(this);
238         mMinSdkCombo.addFocusListener(this);
239         mMinSdkDec = createFieldDecoration(mMinSdkCombo,
240                 "Choose the lowest version of Android that your application will support. Lower " +
241                 "API levels target more devices, but means fewer features are available. By " +
242                 "targeting API 8 and later, you reach approximately 95% of the market.");
243         new Label(container, SWT.NONE);
244 
245         // Target SDK
246         Label targetSdkLabel = new Label(container, SWT.NONE);
247         targetSdkLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1));
248         targetSdkLabel.setText("Target SDK:");
249 
250         mTargetSdkCombo = new Combo(container, SWT.READ_ONLY);
251         GridData gdTargetSdkCombo = new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1);
252         gdTargetSdkCombo.widthHint = FIELD_WIDTH;
253         mTargetSdkCombo.setLayoutData(gdTargetSdkCombo);
254 
255         mTargetSdkCombo.setItems(versions);
256         mTargetSdkCombo.select(mValues.targetSdkLevel - 1);
257 
258         mTargetSdkCombo.addSelectionListener(this);
259         mTargetSdkCombo.addFocusListener(this);
260         mTargetSdkDec = createFieldDecoration(mTargetSdkCombo,
261                 "Choose the highest API level that the application is known to work with. " +
262                 "This attribute informs the system that you have tested against the target " +
263                 "version and the system should not enable any compatibility behaviors to " +
264                 "maintain your app's forward-compatibility with the target version. " +
265                 "The application is still able to run on older versions " +
266                 "(down to minSdkVersion). Your application may look dated if you are not " +
267                 "targeting the current version.");
268         new Label(container, SWT.NONE);
269 
270         // Build Version
271 
272         Label buildSdkLabel = new Label(container, SWT.NONE);
273         buildSdkLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1));
274         buildSdkLabel.setText("Compile With:");
275 
276         mBuildSdkCombo = new Combo(container, SWT.READ_ONLY);
277         GridData gdBuildSdkCombo = new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1);
278         gdBuildSdkCombo.widthHint = FIELD_WIDTH;
279         mBuildSdkCombo.setLayoutData(gdBuildSdkCombo);
280         mBuildSdkCombo.setData(targets);
281         mBuildSdkCombo.setItems(targetLabels.toArray(new String[targetLabels.size()]));
282         if (buildTargetIndex != -1) {
283             mBuildSdkCombo.select(buildTargetIndex);
284         }
285 
286         mBuildSdkCombo.addSelectionListener(this);
287         mBuildSdkCombo.addFocusListener(this);
288         mBuildTargetDec = createFieldDecoration(mBuildSdkCombo,
289                 "Choose a target API to compile your code against, from your installed SDKs. " +
290                 "This is typically the most recent version, or the first version that supports " +
291                 "all the APIs you want to directly access without reflection.");
292         new Label(container, SWT.NONE);
293 
294         TemplateMetadata metadata = mValues.template.getTemplate();
295         if (metadata != null) {
296             mThemeParameter = metadata.getParameter("baseTheme"); //$NON-NLS-1$
297             if (mThemeParameter != null && mThemeParameter.element != null) {
298                 Label themeLabel = new Label(container, SWT.NONE);
299                 themeLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1));
300                 themeLabel.setText("Theme:");
301 
302                 mThemeCombo = NewTemplatePage.createOptionCombo(mThemeParameter, container,
303                         mValues.parameters, this, this);
304                 GridData gdThemeCombo = new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1);
305                 gdThemeCombo.widthHint = FIELD_WIDTH;
306                 mThemeCombo.setLayoutData(gdThemeCombo);
307                 new Label(container, SWT.NONE);
308 
309                 mThemeDec = createFieldDecoration(mThemeCombo,
310                         "Choose the base theme to use for the application");
311             }
312         }
313 
314         new Label(container, SWT.NONE);
315         new Label(container, SWT.NONE);
316         new Label(container, SWT.NONE);
317         new Label(container, SWT.NONE);
318 
319         Label label = new Label(container, SWT.SEPARATOR | SWT.HORIZONTAL);
320         label.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 4, 1));
321 
322         mHelpIcon = new Label(container, SWT.NONE);
323         mHelpIcon.setLayoutData(new GridData(SWT.RIGHT, SWT.TOP, false, false, 1, 1));
324         Image icon = IconFactory.getInstance().getIcon("quickfix");
325         mHelpIcon.setImage(icon);
326         mHelpIcon.setVisible(false);
327 
328         mTipLabel = new Label(container, SWT.WRAP);
329         mTipLabel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 3, 1));
330 
331         // Reserve space for 4 lines
332         mTipLabel.setText("\n\n\n\n"); //$NON-NLS-1$
333 
334         // Reserve enough width to accommodate the various wizard pages up front
335         // (since they are created lazily, and we don't want the wizard to dynamically
336         // resize itself for small size adjustments as each successive page is slightly
337         // larger)
338         Label dummy = new Label(container, SWT.NONE);
339         GridData data = new GridData();
340         data.horizontalSpan = 4;
341         data.widthHint = WIZARD_PAGE_WIDTH;
342         dummy.setLayoutData(data);
343     }
344 
345     /**
346      * Updates the theme selection such that it's valid for the current build
347      * and min sdk targets. Also runs {@link #validatePage} in case no valid entry was found.
348      * Does nothing if called on a template that does not supply a theme.
349      */
updateTheme()350     void updateTheme() {
351         if (mThemeParameter != null) {
352             // Pick the highest theme version that works for the current SDK level
353             Parameter parameter = NewTemplatePage.getParameter(mThemeCombo);
354             assert parameter == mThemeParameter;
355             if (parameter != null) {
356                 String[] optionIds = (String[]) mThemeCombo.getData(ATTR_ID);
357                 for (int index = optionIds.length - 1; index >= 0; index--) {
358                     IStatus status = NewTemplatePage.validateCombo(null, mThemeParameter,
359                             index, mValues.minSdkLevel, mValues.getBuildApi());
360                     if (status == null || status.isOK()) {
361                         String optionId = optionIds[index];
362                         parameter.value = optionId;
363                         parameter.edited = optionId != null && !optionId.toString().isEmpty();
364                         mValues.parameters.put(parameter.id, optionId);
365                         try {
366                             mIgnore = true;
367                             mThemeCombo.select(index);
368                         } finally {
369                             mIgnore = false;
370                         }
371                         break;
372                     }
373                 }
374             }
375 
376             validatePage();
377         }
378     }
379 
getCompilationTargets()380     private IAndroidTarget[] getCompilationTargets() {
381         Sdk current = Sdk.getCurrent();
382         if (current == null) {
383             return new IAndroidTarget[0];
384         }
385         IAndroidTarget[] targets = current.getTargets();
386         List<IAndroidTarget> list = new ArrayList<IAndroidTarget>();
387 
388         for (IAndroidTarget target : targets) {
389             if (target.isPlatform() == false &&
390                     (target.getOptionalLibraries() == null ||
391                             target.getOptionalLibraries().length == 0)) {
392                 continue;
393             }
394             list.add(target);
395         }
396 
397         return list.toArray(new IAndroidTarget[list.size()]);
398     }
399 
createFieldDecoration(Control control, String description)400     private ControlDecoration createFieldDecoration(Control control, String description) {
401         ControlDecoration dec = new ControlDecoration(control, SWT.LEFT);
402         dec.setMarginWidth(2);
403         FieldDecoration errorFieldIndicator = FieldDecorationRegistry.getDefault().
404            getFieldDecoration(FieldDecorationRegistry.DEC_INFORMATION);
405         dec.setImage(errorFieldIndicator.getImage());
406         dec.setDescriptionText(description);
407         control.setToolTipText(description);
408 
409         return dec;
410     }
411 
412     @Override
setVisible(boolean visible)413     public void setVisible(boolean visible) {
414         super.setVisible(visible);
415 
416         // DURING DEVELOPMENT ONLY
417         //if (assertionsEnabled()) {
418         //    String uniqueProjectName = AdtUtils.getUniqueProjectName("Test", "");
419         //    mProjectText.setText(uniqueProjectName);
420         //    mPackageText.setText("test.pkg");
421         //}
422 
423         validatePage();
424     }
425 
426     // ---- Implements ModifyListener ----
427 
428     @Override
modifyText(ModifyEvent e)429     public void modifyText(ModifyEvent e) {
430         if (mIgnore) {
431             return;
432         }
433 
434         Object source = e.getSource();
435         if (source == mProjectText) {
436             mValues.projectName = mProjectText.getText();
437             updateProjectLocation(mValues.projectName);
438             mValues.projectModified = true;
439 
440             try {
441                 mIgnore = true;
442                 if (!mValues.applicationModified) {
443                     mValues.applicationName = mValues.projectName;
444                     mApplicationText.setText(mValues.projectName);
445                 }
446                 updateActivityNames(mValues.projectName);
447             } finally {
448                 mIgnore = false;
449             }
450             suggestPackage(mValues.projectName);
451         } else if (source == mPackageText) {
452             mValues.packageName = mPackageText.getText();
453             mValues.packageModified = true;
454         } else if (source == mApplicationText) {
455             mValues.applicationName = mApplicationText.getText();
456             mValues.applicationModified = true;
457 
458             try {
459                 mIgnore = true;
460                 if (!mValues.projectModified) {
461                     mValues.projectName = appNameToProjectName(mValues.applicationName);
462                     mProjectText.setText(mValues.projectName);
463                     updateProjectLocation(mValues.projectName);
464                 }
465                 updateActivityNames(mValues.applicationName);
466             } finally {
467                 mIgnore = false;
468             }
469             suggestPackage(mValues.applicationName);
470         }
471 
472         validatePage();
473     }
474 
appNameToProjectName(String appName)475     private String appNameToProjectName(String appName) {
476         // Strip out whitespace (and capitalize subsequent words where spaces were removed
477         boolean upcaseNext = false;
478         StringBuilder sb = new StringBuilder(appName.length());
479         for (int i = 0, n = appName.length(); i < n; i++) {
480             char c = appName.charAt(i);
481             if (c == ' ') {
482                 upcaseNext = true;
483             } else if (upcaseNext) {
484                 sb.append(Character.toUpperCase(c));
485                 upcaseNext = false;
486             } else {
487                 sb.append(c);
488             }
489         }
490 
491         appName = sb.toString().trim();
492 
493         IWorkspace workspace = ResourcesPlugin.getWorkspace();
494         IStatus nameStatus = workspace.validateName(appName, IResource.PROJECT);
495         if (nameStatus.isOK()) {
496             return appName;
497         }
498 
499         sb = new StringBuilder(appName.length());
500         for (int i = 0, n = appName.length(); i < n; i++) {
501             char c = appName.charAt(i);
502             if (Character.isLetterOrDigit(c) || c == '.' || c == '-') {
503                 sb.append(c);
504             }
505         }
506 
507         return sb.toString().trim();
508     }
509 
510     /** If the project should be created in the workspace, then update the project location
511      * based on the project name. */
updateProjectLocation(String projectName)512     private void updateProjectLocation(String projectName) {
513         if (projectName == null) {
514             projectName = "";
515         }
516 
517         if (mValues.useDefaultLocation) {
518             IPath workspace = Platform.getLocation();
519             String projectLocation = workspace.append(projectName).toOSString();
520             mValues.projectLocation = projectLocation;
521         }
522     }
523 
updateActivityNames(String name)524     private void updateActivityNames(String name) {
525         try {
526             mIgnore = true;
527             if (!mValues.activityNameModified) {
528                 mValues.activityName = extractClassName(name) + ACTIVITY_NAME_SUFFIX;
529             }
530             if (!mValues.activityTitleModified) {
531                 mValues.activityTitle = name;
532             }
533         } finally {
534             mIgnore = false;
535         }
536     }
537 
538     // ---- Implements SelectionListener ----
539 
540     @Override
widgetSelected(SelectionEvent e)541     public void widgetSelected(SelectionEvent e) {
542         if (mIgnore) {
543             return;
544         }
545 
546         Object source = e.getSource();
547         if (source == mMinSdkCombo) {
548             mValues.minSdk = getSelectedMinSdk();
549             Integer minSdk = mMinNameToApi.get(mValues.minSdk);
550             if (minSdk == null) {
551                 try {
552                     minSdk = Integer.parseInt(mValues.minSdk);
553                 } catch (NumberFormatException nufe) {
554                     // If not a number, then the string is a codename, so treat it
555                     // as a preview version.
556                     minSdk = SdkVersionInfo.HIGHEST_KNOWN_API + 1;
557                 }
558             }
559             mValues.iconState.minSdk = minSdk.intValue();
560             mValues.minSdkLevel = minSdk.intValue();
561 
562             // If higher than build target, adjust build target
563             if (mValues.minSdkLevel > mValues.getBuildApi()) {
564                 // Try to find a build target with an adequate build API
565                 IAndroidTarget[] targets = (IAndroidTarget[]) mBuildSdkCombo.getData();
566                 IAndroidTarget best = null;
567                 int bestApi = Integer.MAX_VALUE;
568                 int bestTargetIndex = -1;
569                 for (int i = 0; i < targets.length; i++) {
570                     IAndroidTarget target = targets[i];
571                     if (!target.isPlatform()) {
572                         continue;
573                     }
574                     int api = target.getVersion().getApiLevel();
575                     if (api >= mValues.minSdkLevel && api < bestApi) {
576                         best = target;
577                         bestApi = api;
578                         bestTargetIndex = i;
579                     }
580                 }
581 
582                 if (best != null) {
583                     assert bestTargetIndex != -1;
584                     mValues.target = best;
585                     try {
586                         mIgnore = true;
587                         mBuildSdkCombo.select(bestTargetIndex);
588                     } finally {
589                         mIgnore = false;
590                     }
591                 }
592             }
593 
594             // If higher than targetSdkVersion, adjust targetSdkVersion
595             if (mValues.minSdkLevel > mValues.targetSdkLevel) {
596                 mValues.targetSdkLevel = mValues.minSdkLevel;
597                 try {
598                     mIgnore = true;
599                     setSelectedTargetSdk(mValues.targetSdkLevel);
600                 } finally {
601                     mIgnore = false;
602                 }
603             }
604         } else if (source == mBuildSdkCombo) {
605             mValues.target = getSelectedBuildTarget();
606 
607             // If lower than min sdk target, adjust min sdk target
608             if (mValues.target.getVersion().isPreview()) {
609                 mValues.minSdk = mValues.target.getVersion().getCodename();
610                 try {
611                     mIgnore = true;
612                     mMinSdkCombo.setText(mValues.minSdk);
613                 } finally {
614                     mIgnore = false;
615                 }
616             } else {
617                 String minSdk = mValues.minSdk;
618                 int buildApiLevel = mValues.target.getVersion().getApiLevel();
619                 if (minSdk != null && !minSdk.isEmpty()
620                         && Character.isDigit(minSdk.charAt(0))
621                         && buildApiLevel < Integer.parseInt(minSdk)) {
622                     mValues.minSdk = Integer.toString(buildApiLevel);
623                     try {
624                         mIgnore = true;
625                         setSelectedMinSdk(buildApiLevel);
626                     } finally {
627                         mIgnore = false;
628                     }
629                 }
630             }
631         } else if (source == mTargetSdkCombo) {
632             mValues.targetSdkLevel = getSelectedTargetSdk();
633         }
634 
635         validatePage();
636     }
637 
getSelectedMinSdk()638     private String getSelectedMinSdk() {
639         // If you're using a preview build, such as android-JellyBean, you have
640         // to use the codename, e.g. JellyBean, as the minimum SDK as well.
641         IAndroidTarget buildTarget = getSelectedBuildTarget();
642         if (buildTarget != null && buildTarget.getVersion().isPreview()) {
643             return buildTarget.getVersion().getCodename();
644         }
645 
646         // +1: First API level (at index 0) is 1
647         return Integer.toString(mMinSdkCombo.getSelectionIndex() + 1);
648     }
649 
getSelectedTargetSdk()650     private int getSelectedTargetSdk() {
651         // +1: First API level (at index 0) is 1
652         return mTargetSdkCombo.getSelectionIndex() + 1;
653     }
654 
setSelectedMinSdk(int api)655     private void setSelectedMinSdk(int api) {
656         mMinSdkCombo.select(api - 1); // -1: First API level (at index 0) is 1
657     }
658 
setSelectedTargetSdk(int api)659     private void setSelectedTargetSdk(int api) {
660         mTargetSdkCombo.select(api - 1); // -1: First API level (at index 0) is 1
661     }
662 
663     @Nullable
getSelectedBuildTarget()664     private IAndroidTarget getSelectedBuildTarget() {
665         IAndroidTarget[] targets = (IAndroidTarget[]) mBuildSdkCombo.getData();
666         int index = mBuildSdkCombo.getSelectionIndex();
667         if (index >= 0 && index < targets.length) {
668             return targets[index];
669         } else {
670             return null;
671         }
672     }
673 
suggestPackage(String original)674     private void suggestPackage(String original) {
675         if (!mValues.packageModified) {
676             // Create default package name
677             StringBuilder sb = new StringBuilder();
678             sb.append(SAMPLE_PACKAGE_PREFIX);
679             appendPackage(sb, original);
680 
681             String pkg = sb.toString();
682             if (pkg.endsWith(".")) { //$NON-NLS-1$
683                 pkg = pkg.substring(0, pkg.length() - 1);
684             }
685             mValues.packageName = pkg;
686             try {
687                 mIgnore = true;
688                 mPackageText.setText(mValues.packageName);
689             } finally {
690                 mIgnore = false;
691             }
692         }
693     }
694 
appendPackage(StringBuilder sb, String string)695     private static void appendPackage(StringBuilder sb, String string) {
696         for (int i = 0, n = string.length(); i < n; i++) {
697             char c = string.charAt(i);
698             if (i == 0 && Character.isJavaIdentifierStart(c)
699                     || i != 0 && Character.isJavaIdentifierPart(c)) {
700                 sb.append(Character.toLowerCase(c));
701             } else if ((c == '.')
702                     && (sb.length() > 0 && sb.charAt(sb.length() - 1) != '.')) {
703                 sb.append('.');
704             } else if (c == '-') {
705                 sb.append('_');
706             }
707         }
708     }
709 
710     @Override
widgetDefaultSelected(SelectionEvent e)711     public void widgetDefaultSelected(SelectionEvent e) {
712     }
713 
714     // ---- Implements FocusListener ----
715 
716     @Override
focusGained(FocusEvent e)717     public void focusGained(FocusEvent e) {
718         Object source = e.getSource();
719         String tip = "";
720         if (source == mApplicationText) {
721             tip = mApplicationDec.getDescriptionText();
722         } else if (source == mProjectText) {
723             tip = mProjectDec.getDescriptionText();
724         } else if (source == mBuildSdkCombo) {
725             tip = mBuildTargetDec.getDescriptionText();
726         } else if (source == mMinSdkCombo) {
727             tip = mMinSdkDec.getDescriptionText();
728         } else if (source == mPackageText) {
729             tip = mPackageDec.getDescriptionText();
730             if (mPackageText.getText().startsWith(SAMPLE_PACKAGE_PREFIX)) {
731                 int length = SAMPLE_PACKAGE_PREFIX.length();
732                 if (mPackageText.getText().length() > length
733                         && SAMPLE_PACKAGE_PREFIX.endsWith(".")) { //$NON-NLS-1$
734                     length--;
735                 }
736                 mPackageText.setSelection(0, length);
737             }
738         } else if (source == mTargetSdkCombo) {
739             tip = mTargetSdkDec.getDescriptionText();
740         } else if (source == mThemeCombo) {
741             tip = mThemeDec.getDescriptionText();
742         }
743         mTipLabel.setText(tip);
744         mHelpIcon.setVisible(tip.length() > 0);
745     }
746 
747     @Override
focusLost(FocusEvent e)748     public void focusLost(FocusEvent e) {
749         mTipLabel.setText("");
750         mHelpIcon.setVisible(false);
751     }
752 
753     // Validation
754 
validatePage()755     private void validatePage() {
756         IStatus status = mValues.template.validateTemplate(mValues.minSdkLevel,
757                 mValues.getBuildApi());
758         if (status != null && !status.isOK()) {
759             updateDecorator(mApplicationDec, null, true);
760             updateDecorator(mPackageDec, null, true);
761             updateDecorator(mProjectDec, null, true);
762             updateDecorator(mThemeDec, null, true);
763             /* These never get marked with errors:
764             updateDecorator(mBuildTargetDec, null, true);
765             updateDecorator(mMinSdkDec, null, true);
766             updateDecorator(mTargetSdkDec, null, true);
767             */
768         } else {
769             IStatus appStatus = validateAppName();
770             if (appStatus != null && (status == null
771                     || appStatus.getSeverity() > status.getSeverity())) {
772                 status = appStatus;
773             }
774 
775             IStatus projectStatus = validateProjectName();
776             if (projectStatus != null && (status == null
777                     || projectStatus.getSeverity() > status.getSeverity())) {
778                 status = projectStatus;
779             }
780 
781             IStatus packageStatus = validatePackageName();
782             if (packageStatus != null && (status == null
783                     || packageStatus.getSeverity() > status.getSeverity())) {
784                 status = packageStatus;
785             }
786 
787             IStatus locationStatus = ProjectContentsPage.validateLocationInWorkspace(mValues);
788             if (locationStatus != null && (status == null
789                     || locationStatus.getSeverity() > status.getSeverity())) {
790                 status = locationStatus;
791             }
792 
793             if (status == null || status.getSeverity() != IStatus.ERROR) {
794                 if (mValues.target == null) {
795                     status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID,
796                             "Select an Android build target version");
797                 }
798             }
799 
800             if (status == null || status.getSeverity() != IStatus.ERROR) {
801                 if (mValues.minSdk == null || mValues.minSdk.isEmpty()) {
802                     status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID,
803                             "Select a minimum SDK version");
804                 } else {
805                     AndroidVersion version = mValues.target.getVersion();
806                     if (version.isPreview()) {
807                         if (version.getCodename().equals(mValues.minSdk) == false) {
808                             status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
809                             "Preview platforms require the min SDK version to match their codenames.");
810                        }
811                     } else if (mValues.target.getVersion().compareTo(
812                             mValues.minSdkLevel,
813                             version.isPreview() ? mValues.minSdk : null) < 0) {
814                         status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID,
815                             "The minimum SDK version is higher than the build target version");
816                     }
817                     if (status == null || status.getSeverity() != IStatus.ERROR) {
818                         if (mValues.targetSdkLevel < mValues.minSdkLevel) {
819                             status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
820                                 "The target SDK version should be at least as high as the minimum SDK version");
821                         }
822                     }
823                 }
824             }
825 
826             IStatus themeStatus = validateTheme();
827             if (themeStatus != null && (status == null
828                     || themeStatus.getSeverity() > status.getSeverity())) {
829                 status = themeStatus;
830             }
831         }
832 
833         setPageComplete(status == null || status.getSeverity() != IStatus.ERROR);
834         if (status != null) {
835             setMessage(status.getMessage(),
836                     status.getSeverity() == IStatus.ERROR
837                         ? IMessageProvider.ERROR : IMessageProvider.WARNING);
838         } else {
839             setErrorMessage(null);
840             setMessage(null);
841         }
842     }
843 
validateAppName()844     private IStatus validateAppName() {
845         String appName = mValues.applicationName;
846         IStatus status = null;
847         if (appName == null || appName.isEmpty()) {
848             status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
849                     "Enter an application name (shown in launcher)");
850         } else if (Character.isLowerCase(mValues.applicationName.charAt(0))) {
851             status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID,
852                     "The application name for most apps begins with an uppercase letter");
853         }
854 
855         updateDecorator(mApplicationDec, status, true);
856 
857         return status;
858     }
859 
validateProjectName()860     private IStatus validateProjectName() {
861         IStatus status = ProjectNamePage.validateProjectName(mValues.projectName);
862         updateDecorator(mProjectDec, status, true);
863 
864         return status;
865     }
866 
validatePackageName()867     private IStatus validatePackageName() {
868         IStatus status;
869         if (mValues.packageName == null || mValues.packageName.startsWith(SAMPLE_PACKAGE_PREFIX)) {
870             if (mValues.packageName != null
871                     && !mValues.packageName.equals(SAMPLE_PACKAGE_PREFIX)) {
872                 status = ApplicationInfoPage.validatePackage(mValues.packageName);
873                 if (status == null || status.isOK()) {
874                     status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID,
875                         String.format("The prefix '%1$s' is meant as a placeholder and should " +
876                                       "not be used", SAMPLE_PACKAGE_PREFIX));
877                 }
878             } else {
879                 status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
880                         "Package name must be specified.");
881             }
882         } else {
883             status = ApplicationInfoPage.validatePackage(mValues.packageName);
884         }
885 
886         updateDecorator(mPackageDec, status, true);
887 
888         return status;
889     }
890 
validateTheme()891     private IStatus validateTheme() {
892         IStatus status = null;
893 
894         if (mThemeParameter != null) {
895             status = NewTemplatePage.validateCombo(null, mThemeParameter,
896                     mThemeCombo.getSelectionIndex(),  mValues.minSdkLevel,
897                     mValues.getBuildApi());
898 
899             updateDecorator(mThemeDec, status, true);
900         }
901 
902         return status;
903     }
904 
updateDecorator(ControlDecoration decorator, IStatus status, boolean hasInfo)905     private void updateDecorator(ControlDecoration decorator, IStatus status, boolean hasInfo) {
906         if (hasInfo) {
907             int severity = status != null ? status.getSeverity() : IStatus.OK;
908             setDecoratorType(decorator, severity);
909         } else {
910             if (status == null || status.isOK()) {
911                 decorator.hide();
912             } else {
913                 decorator.show();
914             }
915         }
916     }
917 
setDecoratorType(ControlDecoration decorator, int severity)918     private void setDecoratorType(ControlDecoration decorator, int severity) {
919         String id;
920         if (severity == IStatus.ERROR) {
921             id = FieldDecorationRegistry.DEC_ERROR;
922         } else if (severity == IStatus.WARNING) {
923             id = FieldDecorationRegistry.DEC_WARNING;
924         } else {
925             id = FieldDecorationRegistry.DEC_INFORMATION;
926         }
927         FieldDecoration errorFieldIndicator = FieldDecorationRegistry.getDefault().
928                 getFieldDecoration(id);
929         decorator.setImage(errorFieldIndicator.getImage());
930     }
931 }
932