diff --git a/tools/eclipse/changes.txt b/tools/eclipse/changes.txt index 5805681b7..0c653c340 100644 --- a/tools/eclipse/changes.txt +++ b/tools/eclipse/changes.txt @@ -1,3 +1,6 @@ +0.9.4: +- New "Create project from sample" choice in the New Project Wizard. + 0.9.3: - New wizard to create Android JUnit Test Projects. - New AVD wizard. diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreationPage.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreationPage.java index 5067111dc..8d5cf271a 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreationPage.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreationPage.java @@ -54,6 +54,7 @@ import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.DirectoryDialog; import org.eclipse.swt.widgets.Event; @@ -65,6 +66,7 @@ import org.eclipse.swt.widgets.Text; import java.io.File; import java.io.FileFilter; import java.net.URI; +import java.util.ArrayList; import java.util.regex.Pattern; /** @@ -90,9 +92,12 @@ public class NewProjectCreationPage extends WizardPage { /** Initial value for all name fields (project, activity, application, package). Used * whenever a value is requested before controls are created. */ private static final String INITIAL_NAME = ""; //$NON-NLS-1$ - /** Initial value for the Create New Project radio; False means Create From Existing would be - * the default.*/ + /** Initial value for the Create New Project radio. */ private static final boolean INITIAL_CREATE_NEW_PROJECT = true; + /** Initial value for the Create Project From Sample. */ + private static final boolean INITIAL_CREATE_FROM_SAMPLE = false; + /** Initial value for the Create Project From Existing Source. */ + private static final boolean INITIAL_CREATE_FROM_SOURCE = false; /** Initial value for the Use Default Location check box. */ private static final boolean INITIAL_USE_DEFAULT_LOCATION = true; /** Initial value for the Create Activity check box. */ @@ -127,6 +132,7 @@ public class NewProjectCreationPage extends WizardPage { private Text mActivityNameField; private Text mApplicationNameField; private Button mCreateNewProjectRadio; + private Button mCreateFromSampleRadio; private Button mUseDefaultLocation; private Label mLocationLabel; private Text mLocationPathField; @@ -145,6 +151,10 @@ public class NewProjectCreationPage extends WizardPage { private boolean mApplicationNameModifiedByUser; private boolean mInternalMinSdkVersionUpdate; + private final ArrayList mSamplesPaths = new ArrayList(); + private Combo mSamplesCombo; + + /** * Creates a new project creation wizard page. @@ -249,6 +259,12 @@ public class NewProjectCreationPage extends WizardPage { : mCreateNewProjectRadio.getSelection(); } + /** Returns the value of the "Create from Existing Sample" radio. */ + public boolean isCreateFromSample() { + return mCreateFromSampleRadio == null ? INITIAL_CREATE_FROM_SAMPLE + : mCreateFromSampleRadio.getSelection(); + } + /** Returns the value of the "Create Activity" checkbox. */ public boolean isCreateActivity() { return mCreateActivityCheck == null ? INITIAL_CREATE_ACTIVITY @@ -330,6 +346,7 @@ public class NewProjectCreationPage extends WizardPage { // Update state the first time enableLocationWidgets(); + loadSamplesForTarget(null /*target*/); // Show description the first time setErrorMessage(null); @@ -407,9 +424,10 @@ public class NewProjectCreationPage extends WizardPage { mCreateNewProjectRadio = new Button(group, SWT.RADIO); mCreateNewProjectRadio.setText("Create new project in workspace"); mCreateNewProjectRadio.setSelection(INITIAL_CREATE_NEW_PROJECT); + Button existing_project_radio = new Button(group, SWT.RADIO); existing_project_radio.setText("Create project from existing source"); - existing_project_radio.setSelection(!INITIAL_CREATE_NEW_PROJECT); + existing_project_radio.setSelection(INITIAL_CREATE_FROM_SOURCE); mUseDefaultLocation = new Button(group, SWT.CHECK); mUseDefaultLocation.setText("Use default location"); @@ -462,6 +480,33 @@ public class NewProjectCreationPage extends WizardPage { onOpenDirectoryBrowser(); } }); + + mCreateFromSampleRadio = new Button(group, SWT.RADIO); + mCreateFromSampleRadio.setText("Create project from existing sample"); + mCreateFromSampleRadio.setSelection(INITIAL_CREATE_FROM_SAMPLE); + mCreateFromSampleRadio.addSelectionListener(location_listener); + + Composite samples_group = new Composite(group, SWT.NONE); + samples_group.setLayout(new GridLayout(2, /* num columns */ + false /* columns of not equal size */)); + samples_group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + samples_group.setFont(parent.getFont()); + + new Label(samples_group, SWT.NONE).setText("Samples:"); + + mSamplesCombo = new Combo(samples_group, SWT.DROP_DOWN | SWT.READ_ONLY); + mSamplesCombo.setEnabled(false); + mSamplesCombo.select(0); + mSamplesCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mSamplesCombo.setToolTipText("Select a sample"); + + mSamplesCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onSampleSelected(); + } + }); + } /** @@ -625,9 +670,22 @@ public class NewProjectCreationPage extends WizardPage { return mLocationPathField == null ? "" : mLocationPathField.getText().trim(); //$NON-NLS-1$ } - /** Returns the current project location, depending on the Use Default Location check box. */ + /** Returns the current selected sample path, + * or an empty string if there's no valid selection. */ + private String getSelectedSamplePath() { + int selIndex = mSamplesCombo.getSelectionIndex(); + if (selIndex >= 0 && selIndex < mSamplesPaths.size()) { + return mSamplesPaths.get(selIndex); + } + return ""; + } + + /** Returns the current project location, depending on the Use Default Location check box + * or the Create From Sample check box. */ private String getProjectLocation() { - if (mInfo.isNewProject() && mInfo.useDefaultLocation()) { + if (mInfo.isCreateFromSample()) { + return getSelectedSamplePath(); + } else if (mInfo.isNewProject() && mInfo.useDefaultLocation()) { return Platform.getLocation().toString(); } else { return getLocationPathFieldValue(); @@ -680,6 +738,16 @@ public class NewProjectCreationPage extends WizardPage { } } + /** + * A sample was selected. Update the location field, manifest and validate. + */ + private void onSampleSelected() { + // Note that getProjectLocation() is automatically updated to use the currently + // selected sample. We just need to refresh the manifest data & validate. + extractNamesFromAndroidManifest(); + validatePageComplete(); + } + /** * Enables or disable the location widgets depending on the user selection: * the location path is enabled when using the "existing source" mode (i.e. not new project) @@ -687,8 +755,9 @@ public class NewProjectCreationPage extends WizardPage { */ private void enableLocationWidgets() { boolean is_new_project = mInfo.isNewProject(); - boolean use_default = mInfo.useDefaultLocation(); - boolean location_enabled = !is_new_project || !use_default; + boolean is_create_from_sample = mInfo.isCreateFromSample(); + boolean use_default = mInfo.useDefaultLocation() && !is_create_from_sample; + boolean location_enabled = (!is_new_project || !use_default) && !is_create_from_sample; boolean create_activity = mInfo.isCreateActivity(); mUseDefaultLocation.setEnabled(is_new_project); @@ -697,6 +766,8 @@ public class NewProjectCreationPage extends WizardPage { mLocationPathField.setEnabled(location_enabled); mBrowseButton.setEnabled(location_enabled); + mSamplesCombo.setEnabled(is_create_from_sample && mSamplesPaths.size() > 0); + mPackageNameField.setEnabled(is_new_project); mCreateActivityCheck.setEnabled(is_new_project); mActivityNameField.setEnabled(is_new_project & create_activity); @@ -718,6 +789,12 @@ public class NewProjectCreationPage extends WizardPage { * @param abs_dir A new absolute directory path or null to use the default. */ private void updateLocationPathField(String abs_dir) { + + // We don't touch the location path if using the "Create From Sample" mode + if (mInfo.isCreateFromSample()) { + return; + } + boolean is_new_project = mInfo.isNewProject(); boolean use_default = mInfo.useDefaultLocation(); boolean custom_location = !is_new_project || !use_default; @@ -872,6 +949,10 @@ public class NewProjectCreationPage extends WizardPage { mMinSdkVersionField.setText(target.getVersion().getApiString()); mInternalMinSdkVersionUpdate = false; } + + loadSamplesForTarget(target); + enableLocationWidgets(); + onSampleSelected(); } /** @@ -969,15 +1050,15 @@ public class NewProjectCreationPage extends WizardPage { String[] ids = activityName.split(AndroidConstants.RE_DOT); activityName = ids[ids.length - 1]; } - if (mProjectNameField.getText().length() == 0 || - !mProjectNameModifiedByUser) { + if (mProjectNameField.getText().length() == 0 || !mProjectNameModifiedByUser) { mInternalProjectNameUpdate = true; + mProjectNameModifiedByUser = false; mProjectNameField.setText(activityName); mInternalProjectNameUpdate = false; } - if (mApplicationNameField.getText().length() == 0 || - !mApplicationNameModifiedByUser) { + if (mApplicationNameField.getText().length() == 0 || !mApplicationNameModifiedByUser) { mInternalApplicationNameUpdate = true; + mApplicationNameModifiedByUser = false; mApplicationNameField.setText(activityName); mInternalApplicationNameUpdate = false; } @@ -1004,8 +1085,7 @@ public class NewProjectCreationPage extends WizardPage { // For the project name, remove any dots packageName = packageName.replace('.', '_'); - if (mProjectNameField.getText().length() == 0 || - !mProjectNameModifiedByUser) { + if (mProjectNameField.getText().length() == 0 || !mProjectNameModifiedByUser) { mInternalProjectNameUpdate = true; mProjectNameField.setText(packageName); mInternalProjectNameUpdate = false; @@ -1015,7 +1095,8 @@ public class NewProjectCreationPage extends WizardPage { } // Select the target matching the manifest's sdk or build properties, if any - boolean foundTarget = false; + IAndroidTarget foundTarget = null; + IAndroidTarget currentTarget = mInfo.getSdkTarget(); ProjectProperties p = ProjectProperties.create(projectLocation, null); if (p != null) { @@ -1023,33 +1104,43 @@ public class NewProjectCreationPage extends WizardPage { p.merge(PropertyType.BUILD).merge(PropertyType.DEFAULT); String v = p.getProperty(ProjectProperties.PROPERTY_TARGET); IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString(v); - if (target != null) { - mSdkTargetSelector.setSelection(target); - foundTarget = true; + // We can change the current target if: + // - we found a new target + // - there is no current target + // - there is a current target but the new target is not <= to the current one. + if (target != null && + (currentTarget == null || !target.isCompatibleBaseFor(currentTarget))) { + foundTarget = target; } } - if (!foundTarget && minSdkVersion != null) { + if (foundTarget == null && minSdkVersion != null) { + // Otherwise try to match the requested sdk version for (IAndroidTarget target : mSdkTargetSelector.getTargets()) { - if (target.getVersion().equals(minSdkVersion)) { - mSdkTargetSelector.setSelection(target); - foundTarget = true; + if (target != null && + target.getVersion().equals(minSdkVersion) && + (currentTarget == null || !target.isCompatibleBaseFor(currentTarget))) { + foundTarget = target; break; } } } - if (!foundTarget) { + if (foundTarget == null) { + // Or last attemp, try to match a sample project location for (IAndroidTarget target : mSdkTargetSelector.getTargets()) { - if (projectLocation.startsWith(target.getLocation())) { - mSdkTargetSelector.setSelection(target); - foundTarget = true; + if (target != null && + projectLocation.startsWith(target.getLocation()) && + (currentTarget == null || !target.isCompatibleBaseFor(currentTarget))) { + foundTarget = target; break; } } } - if (!foundTarget) { + if (foundTarget != null) { + mSdkTargetSelector.setSelection(foundTarget); + } else { mInternalMinSdkVersionUpdate = true; if (minSdkVersion != null) { mMinSdkVersionField.setText(minSdkVersion); @@ -1058,6 +1149,104 @@ public class NewProjectCreationPage extends WizardPage { } } + /** + * Updates the list of all samples for the given target SDK. + * The list is stored in mSamplesPaths as absolute directory paths. + * The combo is recreated to match this. + */ + private void loadSamplesForTarget(IAndroidTarget target) { + + // Keep the name of the old selection (if there were any samples) + String oldChoice = null; + if (mSamplesPaths.size() > 0) { + int selIndex = mSamplesCombo.getSelectionIndex(); + if (selIndex > -1) { + oldChoice = mSamplesCombo.getItem(selIndex); + } + } + + // Clear all current content + mSamplesCombo.removeAll(); + mSamplesPaths.clear(); + + if (target != null) { + // Get the sample root path and recompute the list of samples + String samplesRootPath = target.getPath(IAndroidTarget.SAMPLES); + + File samplesDir = new File(samplesRootPath); + findSamplesManifests(samplesDir, mSamplesPaths); + + if (mSamplesPaths.size() == 0) { + // Odd, this target has no samples. Could happen with an addon. + mSamplesCombo.add("This target has no samples. Please select another target."); + mSamplesCombo.select(0); + return; + } + + // Recompute the description of each sample (the relative path + // to the sample root). Also try to find the old selection. + int selIndex = 0; + int i = 0; + int n = samplesRootPath.length(); + for (String path : mSamplesPaths) { + if (path.length() > n) { + path = path.substring(n); + if (path.charAt(0) == File.separatorChar) { + path = path.substring(1); + } + if (path.endsWith(File.separator)) { + path = path.substring(0, path.length() - 1); + } + path = path.replaceAll(Pattern.quote(File.separator), " > "); + } + + if (oldChoice != null && oldChoice.equals(path)) { + selIndex = i; + } + + mSamplesCombo.add(path); + i++; + } + + mSamplesCombo.select(selIndex); + + } else { + mSamplesCombo.add("Please select a target."); + mSamplesCombo.select(0); + } + } + + /** + * Recursively find potential sample directories under the given directory. + * Actually lists any directory that contains an android manifest. + * Paths found are added the samplesPaths list. + */ + private void findSamplesManifests(File samplesDir, ArrayList samplesPaths) { + if (!samplesDir.isDirectory()) { + return; + } + + for (File f : samplesDir.listFiles()) { + if (f.isDirectory()) { + // Assume this is a sample if it contains an android manifest. + File manifestFile = new File(f, SdkConstants.FN_ANDROID_MANIFEST_XML); + if (manifestFile.isFile()) { + samplesPaths.add(f.getPath()); + } + + // Recurse in the project, to find embedded tests sub-projects + // We can however skip this recursion for known android sub-dirs that + // can't have projects, namely for sources, assets and resources. + String leaf = f.getName(); + if (!SdkConstants.FD_SOURCES.equals(leaf) && + !SdkConstants.FD_ASSETS.equals(leaf) && + !SdkConstants.FD_RES.equals(leaf)) { + findSamplesManifests(f, samplesPaths); + } + } + } + } + /** * Returns whether this page's controls currently all contain valid values. * @@ -1069,10 +1258,10 @@ public class NewProjectCreationPage extends WizardPage { int status = validateProjectField(workspace); if ((status & MSG_ERROR) == 0) { - status |= validateLocationPath(workspace); + status |= validateSdkTarget(); } if ((status & MSG_ERROR) == 0) { - status |= validateSdkTarget(); + status |= validateLocationPath(workspace); } if ((status & MSG_ERROR) == 0) { status |= validatePackageField();