diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject')
16 files changed, 5904 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ApplicationInfoPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ApplicationInfoPage.java new file mode 100644 index 000000000..c8325345a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ApplicationInfoPage.java @@ -0,0 +1,809 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import com.android.SdkConstants; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.filesystem.URIUtil; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Status; +import org.eclipse.jdt.core.JavaConventions; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionEvent; +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.Label; +import org.eclipse.swt.widgets.Text; + +import java.io.File; +import java.io.FileFilter; +import java.net.URI; + +/** Page where you choose the application name, activity name, and optional test project info */ +public class ApplicationInfoPage extends WizardPage implements SelectionListener, ModifyListener, + ITargetChangeListener { + private static final String JDK_15 = "1.5"; //$NON-NLS-1$ + private final static String DUMMY_PACKAGE = "your.package.namespace"; + + /** Suffix added by default to activity names */ + static final String ACTIVITY_NAME_SUFFIX = "Activity"; //$NON-NLS-1$ + + private final NewProjectWizardState mValues; + + private Text mApplicationText; + private Text mPackageText; + private Text mActivityText; + private Button mCreateActivityCheckbox; + private Combo mSdkCombo; + + private boolean mIgnore; + private Button mCreateTestCheckbox; + private Text mTestProjectNameText; + private Text mTestApplicationText; + private Text mTestPackageText; + private Label mTestProjectNameLabel; + private Label mTestApplicationLabel; + private Label mTestPackageLabel; + + /** + * Create the wizard. + */ + ApplicationInfoPage(NewProjectWizardState values) { + super("appInfo"); //$NON-NLS-1$ + mValues = values; + + setTitle("Application Info"); + setDescription("Configure the new Android Project"); + AdtPlugin.getDefault().addTargetListener(this); + } + + /** + * Create contents of the wizard. + */ + @Override + @SuppressWarnings("unused") // Eclipse marks SWT constructors with side effects as unused + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NULL); + container.setLayout(new GridLayout(2, false)); + + Label applicationLabel = new Label(container, SWT.NONE); + applicationLabel.setText("Application Name:"); + + mApplicationText = new Text(container, SWT.BORDER); + mApplicationText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mApplicationText.addModifyListener(this); + + Label packageLabel = new Label(container, SWT.NONE); + packageLabel.setText("Package Name:"); + + mPackageText = new Text(container, SWT.BORDER); + mPackageText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mPackageText.addModifyListener(this); + + if (mValues.mode != Mode.TEST) { + mCreateActivityCheckbox = new Button(container, SWT.CHECK); + mCreateActivityCheckbox.setText("Create Activity:"); + mCreateActivityCheckbox.addSelectionListener(this); + + mActivityText = new Text(container, SWT.BORDER); + mActivityText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mActivityText.addModifyListener(this); + } + + Label minSdkLabel = new Label(container, SWT.NONE); + minSdkLabel.setText("Minimum SDK:"); + + mSdkCombo = new Combo(container, SWT.NONE); + GridData gdSdkCombo = new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1); + gdSdkCombo.widthHint = 200; + mSdkCombo.setLayoutData(gdSdkCombo); + mSdkCombo.addSelectionListener(this); + mSdkCombo.addModifyListener(this); + + onSdkLoaded(); + + setControl(container); + new Label(container, SWT.NONE); + new Label(container, SWT.NONE); + + mCreateTestCheckbox = new Button(container, SWT.CHECK); + mCreateTestCheckbox.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1)); + mCreateTestCheckbox.setText("Create a Test Project"); + mCreateTestCheckbox.addSelectionListener(this); + + mTestProjectNameLabel = new Label(container, SWT.NONE); + mTestProjectNameLabel.setText("Test Project Name:"); + + mTestProjectNameText = new Text(container, SWT.BORDER); + mTestProjectNameText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mTestProjectNameText.addModifyListener(this); + + mTestApplicationLabel = new Label(container, SWT.NONE); + mTestApplicationLabel.setText("Test Application:"); + + mTestApplicationText = new Text(container, SWT.BORDER); + mTestApplicationText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mTestApplicationText.addModifyListener(this); + + mTestPackageLabel = new Label(container, SWT.NONE); + mTestPackageLabel.setText("Test Package:"); + + mTestPackageText = new Text(container, SWT.BORDER); + mTestPackageText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mTestPackageText.addModifyListener(this); + } + + /** Controls whether the options for creating a paired test project should be shown */ + private void showTestOptions(boolean visible) { + if (mValues.mode == Mode.SAMPLE) { + visible = false; + } + + mCreateTestCheckbox.setVisible(visible); + mTestProjectNameLabel.setVisible(visible); + mTestProjectNameText.setVisible(visible); + mTestApplicationLabel.setVisible(visible); + mTestApplicationText.setVisible(visible); + mTestPackageLabel.setVisible(visible); + mTestPackageText.setVisible(visible); + } + + /** Controls whether the options for creating a paired test project should be enabled */ + private void enableTestOptions(boolean enabled) { + mTestProjectNameLabel.setEnabled(enabled); + mTestProjectNameText.setEnabled(enabled); + mTestApplicationLabel.setEnabled(enabled); + mTestApplicationText.setEnabled(enabled); + mTestPackageLabel.setEnabled(enabled); + mTestPackageText.setEnabled(enabled); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + + if (visible) { + try { + mIgnore = true; + if (mValues.applicationName != null) { + mApplicationText.setText(mValues.applicationName); + } + if (mValues.packageName != null) { + mPackageText.setText(mValues.packageName); + } else { + mPackageText.setText(DUMMY_PACKAGE); + } + + if (mValues.mode != Mode.TEST) { + mCreateActivityCheckbox.setSelection(mValues.createActivity); + mActivityText.setEnabled(mValues.createActivity); + if (mValues.activityName != null) { + mActivityText.setText(mValues.activityName); + } + } + if (mValues.minSdk != null && mValues.minSdk.length() > 0) { + mSdkCombo.setText(mValues.minSdk); + } + + showTestOptions(mValues.mode == Mode.ANY); + enableTestOptions(mCreateTestCheckbox.getSelection()); + + if (mValues.testProjectName != null) { + mTestProjectNameText.setText(mValues.testProjectName); + } + if (mValues.testApplicationName != null) { + mTestApplicationText.setText(mValues.testApplicationName); + } + if (mValues.testProjectName != null) { + mTestPackageText.setText(mValues.testProjectName); + } + } finally { + mIgnore = false; + } + } + + // Start focus with the package name, since the other fields are typically assigned + // reasonable defaults + mPackageText.setFocus(); + mPackageText.selectAll(); + + validatePage(); + } + + protected void setSdkTargets(IAndroidTarget[] targets, IAndroidTarget target) { + if (targets == null) { + targets = new IAndroidTarget[0]; + } + int selectionIndex = -1; + String[] items = new String[targets.length]; + for (int i = 0, n = targets.length; i < n; i++) { + items[i] = targetLabel(targets[i]); + if (targets[i] == target) { + selectionIndex = i; + } + } + try { + mIgnore = true; + mSdkCombo.setItems(items); + mSdkCombo.setData(targets); + if (selectionIndex != -1) { + mSdkCombo.select(selectionIndex); + } + } finally { + mIgnore = false; + } + } + + private String targetLabel(IAndroidTarget target) { + // In the minimum SDK chooser, show the targets with api number and description, + // such as "11 (Android 3.0)" + return String.format("%1$s (%2$s)", target.getVersion().getApiString(), + target.getFullName()); + } + + @Override + public void dispose() { + AdtPlugin.getDefault().removeTargetListener(this); + super.dispose(); + } + + @Override + public boolean isPageComplete() { + // This page is only needed when creating new projects + if (mValues.useExisting || mValues.mode != Mode.ANY) { + return true; + } + + // Ensure that we reach this page + if (mValues.packageName == null) { + return false; + } + + return super.isPageComplete(); + } + + @Override + public void modifyText(ModifyEvent e) { + if (mIgnore) { + return; + } + + Object source = e.getSource(); + if (source == mSdkCombo) { + mValues.minSdk = mSdkCombo.getText().trim(); + IAndroidTarget[] targets = (IAndroidTarget[]) mSdkCombo.getData(); + // An editable combo will treat item selection the same way as a user edit, + // so we need to see if the string looks like a labeled version + int index = mSdkCombo.getSelectionIndex(); + if (index != -1) { + if (index >= 0 && index < targets.length) { + IAndroidTarget target = targets[index]; + if (targetLabel(target).equals(mValues.minSdk)) { + mValues.minSdk = target.getVersion().getApiString(); + } + } + } + + // Ensure that we never pick up the (Android x.y) suffix shown in combobox + // for readability + int separator = mValues.minSdk.indexOf(' '); + if (separator != -1) { + mValues.minSdk = mValues.minSdk.substring(0, separator); + } + mValues.minSdkModifiedByUser = true; + mValues.updateSdkTargetToMatchMinSdkVersion(); + } else if (source == mApplicationText) { + mValues.applicationName = mApplicationText.getText().trim(); + mValues.applicationNameModifiedByUser = true; + + if (!mValues.testApplicationNameModified) { + mValues.testApplicationName = suggestTestApplicationName(mValues.applicationName); + try { + mIgnore = true; + mTestApplicationText.setText(mValues.testApplicationName); + } finally { + mIgnore = false; + } + } + + } else if (source == mPackageText) { + mValues.packageName = mPackageText.getText().trim(); + mValues.packageNameModifiedByUser = true; + + if (!mValues.testPackageModified) { + mValues.testPackageName = suggestTestPackage(mValues.packageName); + try { + mIgnore = true; + mTestPackageText.setText(mValues.testPackageName); + } finally { + mIgnore = false; + } + } + } else if (source == mActivityText) { + mValues.activityName = mActivityText.getText().trim(); + mValues.activityNameModifiedByUser = true; + } else if (source == mTestApplicationText) { + mValues.testApplicationName = mTestApplicationText.getText().trim(); + mValues.testApplicationNameModified = true; + } else if (source == mTestPackageText) { + mValues.testPackageName = mTestPackageText.getText().trim(); + mValues.testPackageModified = true; + } else if (source == mTestProjectNameText) { + mValues.testProjectName = mTestProjectNameText.getText().trim(); + mValues.testProjectModified = true; + } + + validatePage(); + } + + @Override + public void widgetSelected(SelectionEvent e) { + if (mIgnore) { + return; + } + + Object source = e.getSource(); + + if (source == mCreateActivityCheckbox) { + mValues.createActivity = mCreateActivityCheckbox.getSelection(); + mActivityText.setEnabled(mValues.createActivity); + } else if (source == mSdkCombo) { + int index = mSdkCombo.getSelectionIndex(); + IAndroidTarget[] targets = (IAndroidTarget[]) mSdkCombo.getData(); + if (index != -1) { + if (index >= 0 && index < targets.length) { + IAndroidTarget target = targets[index]; + // Even though we are showing the logical version name, we place the + // actual api number as the minimum SDK + mValues.minSdk = target.getVersion().getApiString(); + } + } else { + String text = mSdkCombo.getText(); + boolean found = false; + for (IAndroidTarget target : targets) { + if (targetLabel(target).equals(text)) { + mValues.minSdk = target.getVersion().getApiString(); + found = true; + break; + } + } + if (!found) { + mValues.minSdk = text; + } + } + } else if (source == mCreateTestCheckbox) { + mValues.createPairProject = mCreateTestCheckbox.getSelection(); + enableTestOptions(mValues.createPairProject); + if (mValues.createPairProject) { + if (mValues.testProjectName == null || mValues.testProjectName.length() == 0) { + mValues.testProjectName = suggestTestProjectName(mValues.projectName); + } + if (mValues.testApplicationName == null || + mValues.testApplicationName.length() == 0) { + mValues.testApplicationName = + suggestTestApplicationName(mValues.applicationName); + } + if (mValues.testPackageName == null || mValues.testPackageName.length() == 0) { + mValues.testPackageName = suggestTestPackage(mValues.packageName); + } + + try { + mIgnore = true; + mTestProjectNameText.setText(mValues.testProjectName); + mTestApplicationText.setText(mValues.testApplicationName); + mTestPackageText.setText(mValues.testPackageName); + } finally { + mIgnore = false; + } + } + } + + validatePage(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + + private void validatePage() { + IStatus status = validatePackage(mValues.packageName); + if (status == null || status.getSeverity() != IStatus.ERROR) { + IStatus validActivity = validateActivity(); + if (validActivity != null) { + status = validActivity; + } + } + if (status == null || status.getSeverity() != IStatus.ERROR) { + IStatus validMinSdk = validateMinSdk(); + if (validMinSdk != null) { + status = validMinSdk; + } + } + + if (status == null || status.getSeverity() != IStatus.ERROR) { + IStatus validSourceFolder = validateSourceFolder(); + if (validSourceFolder != null) { + status = validSourceFolder; + } + } + + // If creating a test project to go along with the main project, also validate + // the additional test project parameters + if (status == null || status.getSeverity() != IStatus.ERROR) { + if (mValues.createPairProject) { + IStatus validTestProject = ProjectNamePage.validateProjectName( + mValues.testProjectName); + if (validTestProject != null) { + status = validTestProject; + } + + if (status == null || status.getSeverity() != IStatus.ERROR) { + IStatus validTestLocation = validateTestProjectLocation(); + if (validTestLocation != null) { + status = validTestLocation; + } + } + + if (status == null || status.getSeverity() != IStatus.ERROR) { + IStatus validTestPackage = validatePackage(mValues.testPackageName); + if (validTestPackage != null) { + status = new Status(validTestPackage.getSeverity(), + AdtPlugin.PLUGIN_ID, + validTestPackage.getMessage() + " (in test package)"); + } + } + + if (status == null || status.getSeverity() != IStatus.ERROR) { + if (mValues.projectName.equals(mValues.testProjectName)) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "The main project name and the test project name must be different."); + } + } + } + } + + // -- update UI & enable finish if there's no error + setPageComplete(status == null || status.getSeverity() != IStatus.ERROR); + if (status != null) { + setMessage(status.getMessage(), + status.getSeverity() == IStatus.ERROR + ? IMessageProvider.ERROR : IMessageProvider.WARNING); + } else { + setErrorMessage(null); + setMessage(null); + } + } + + private IStatus validateTestProjectLocation() { + assert mValues.createPairProject; + + // Validate location + Path path = new Path(mValues.projectLocation.getPath()); + if (!mValues.useExisting) { + if (!mValues.useDefaultLocation) { + // If not using the default value validate the location. + URI uri = URIUtil.toURI(path.toOSString()); + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IProject handle = workspace.getRoot().getProject(mValues.testProjectName); + IStatus locationStatus = workspace.validateProjectLocationURI(handle, uri); + if (!locationStatus.isOK()) { + return locationStatus; + } + // The location is valid as far as Eclipse is concerned (i.e. mostly not + // an existing workspace project.) Check it either doesn't exist or is + // a directory that is empty. + File f = path.toFile(); + if (f.exists() && !f.isDirectory()) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "A directory name must be specified."); + } else if (f.isDirectory()) { + // However if the directory exists, we should put a + // warning if it is not empty. We don't put an error + // (we'll ask the user again for confirmation before + // using the directory.) + String[] l = f.list(); + if (l != null && l.length != 0) { + return new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID, + "The selected output directory is not empty."); + } + } + } else { + IPath destPath = path.removeLastSegments(1).append(mValues.testProjectName); + File dest = destPath.toFile(); + if (dest.exists()) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format( + "There is already a file or directory named \"%1$s\" in the selected location.", + mValues.testProjectName)); + } + } + } + + return null; + } + + private IStatus validateSourceFolder() { + // This check does nothing when creating a new project. + // This check is also useless when no activity is present or created. + mValues.sourceFolder = SdkConstants.FD_SOURCES; + if (!mValues.useExisting || !mValues.createActivity) { + return null; + } + + String osTarget = mValues.activityName; + if (osTarget.indexOf('.') == -1) { + osTarget = mValues.packageName + File.separator + osTarget; + } else if (osTarget.indexOf('.') == 0) { + osTarget = mValues.packageName + osTarget; + } + osTarget = osTarget.replace('.', File.separatorChar) + SdkConstants.DOT_JAVA; + + File projectDir = mValues.projectLocation; + File[] allDirs = projectDir.listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.isDirectory(); + } + }); + if (allDirs != null) { + boolean found = false; + for (File f : allDirs) { + Path path = new Path(f.getAbsolutePath()); + File java_activity = path.append(osTarget).toFile(); + if (java_activity.isFile()) { + mValues.sourceFolder = f.getName(); + found = true; + break; + } + } + + if (!found) { + String projectPath = projectDir.getPath(); + if (allDirs.length > 0) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("%1$s can not be found under %2$s.", osTarget, + projectPath)); + } else { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("No source folders can be found in %1$s.", + projectPath)); + } + } + } + + return null; + } + + private IStatus validateMinSdk() { + // Validate min SDK field + // If the min sdk version is empty, it is always accepted. + if (mValues.minSdk == null || mValues.minSdk.length() == 0) { + return null; + } + + IAndroidTarget target = mValues.target; + if (target == null) { + return null; + } + + // If the current target is a preview, explicitly indicate minSdkVersion + // must be set to this target name. + if (target.getVersion().isPreview() && !target.getVersion().equals(mValues.minSdk)) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format( + "The SDK target is a preview. Min SDK Version must be set to '%s'.", + target.getVersion().getCodename())); + } + + if (!target.getVersion().equals(mValues.minSdk)) { + return new Status(target.getVersion().isPreview() ? IStatus.ERROR : IStatus.WARNING, + AdtPlugin.PLUGIN_ID, + "The API level for the selected SDK target does not match the Min SDK Version." + ); + } + + return null; + } + + public static IStatus validatePackage(String packageFieldContents) { + // Validate package + if (packageFieldContents == null || packageFieldContents.length() == 0) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Package name must be specified."); + } else if (packageFieldContents.equals(DUMMY_PACKAGE)) { + // The dummy package name is just a placeholder package (which isn't even valid + // because it contains the reserved Java keyword "package") but we want to + // make the error message say that a proper package should be entered rather than + // what's wrong with this specific package. (And the reason we provide a dummy + // package rather than a blank line is to make it more clear to beginners what + // we're looking for. + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Package name must be specified."); + } + // Check it's a valid package string + IStatus status = JavaConventions.validatePackageName(packageFieldContents, JDK_15, + JDK_15); + if (!status.isOK()) { + return status; + } + + // The Android Activity Manager does not accept packages names with only one + // identifier. Check the package name has at least one dot in them (the previous rule + // validated that if such a dot exist, it's not the first nor last characters of the + // string.) + if (packageFieldContents.indexOf('.') == -1) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Package name must have at least two identifiers."); + } + + return null; + } + + public static IStatus validateClass(String className) { + if (className == null || className.length() == 0) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Class name must be specified."); + } + if (className.indexOf('.') != -1) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Enter just a class name, not a full package name"); + } + return JavaConventions.validateJavaTypeName(className, JDK_15, JDK_15); + } + + private IStatus validateActivity() { + // Validate activity (if creating an activity) + if (!mValues.createActivity) { + return null; + } + + return validateActivity(mValues.activityName); + } + + /** + * Validates the given activity name + * + * @param activityFieldContents the activity name to validate + * @return a status for whether the activity name is valid + */ + public static IStatus validateActivity(String activityFieldContents) { + // Validate activity field + if (activityFieldContents == null || activityFieldContents.length() == 0) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Activity name must be specified."); + } else if (ACTIVITY_NAME_SUFFIX.equals(activityFieldContents)) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, "Enter a valid activity name"); + } else if (activityFieldContents.contains("..")) { //$NON-NLS-1$ + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Package segments in activity name cannot be empty (..)"); + } + // The activity field can actually contain part of a sub-package name + // or it can start with a dot "." to indicates it comes from the parent package + // name. + String packageName = ""; //$NON-NLS-1$ + int pos = activityFieldContents.lastIndexOf('.'); + if (pos >= 0) { + packageName = activityFieldContents.substring(0, pos); + if (packageName.startsWith(".")) { //$NON-NLS-1$ + packageName = packageName.substring(1); + } + + activityFieldContents = activityFieldContents.substring(pos + 1); + } + + // the activity field can contain a simple java identifier, or a + // package name or one that starts with a dot. So if it starts with a dot, + // ignore this dot -- the rest must look like a package name. + if (activityFieldContents.length() > 0 && activityFieldContents.charAt(0) == '.') { + activityFieldContents = activityFieldContents.substring(1); + } + + // Check it's a valid activity string + IStatus status = JavaConventions.validateTypeVariableName(activityFieldContents, JDK_15, + JDK_15); + if (!status.isOK()) { + return status; + } + + // Check it's a valid package string + if (packageName.length() > 0) { + status = JavaConventions.validatePackageName(packageName, JDK_15, JDK_15); + if (!status.isOK()) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + status.getMessage() + " (in the activity name)"); + } + } + + return null; + } + + // ---- Implement ITargetChangeListener ---- + + @Override + public void onSdkLoaded() { + if (mSdkCombo == null) { + return; + } + + // Update the sdk target selector with the new targets + + // get the targets from the sdk + IAndroidTarget[] targets = null; + if (Sdk.getCurrent() != null) { + targets = Sdk.getCurrent().getTargets(); + } + setSdkTargets(targets, mValues.target); + } + + @Override + public void onProjectTargetChange(IProject changedProject) { + // Ignore + } + + @Override + public void onTargetLoaded(IAndroidTarget target) { + // Ignore + } + + public static String suggestTestApplicationName(String applicationName) { + if (applicationName == null) { + applicationName = ""; //$NON-NLS-1$ + } + if (applicationName.indexOf(' ') != -1) { + return applicationName + " Test"; //$NON-NLS-1$ + } else { + return applicationName + "Test"; //$NON-NLS-1$ + } + } + + public static String suggestTestProjectName(String projectName) { + if (projectName == null) { + projectName = ""; //$NON-NLS-1$ + } + if (projectName.length() > 0 && Character.isUpperCase(projectName.charAt(0))) { + return projectName + "Test"; //$NON-NLS-1$ + } else { + return projectName + "-test"; //$NON-NLS-1$ + } + } + + + public static String suggestTestPackage(String packagePath) { + if (packagePath == null) { + packagePath = ""; //$NON-NLS-1$ + } + return packagePath + ".test"; //$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/FileStoreAdapter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/FileStoreAdapter.java new file mode 100755 index 000000000..0f4e87e9f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/FileStoreAdapter.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import org.eclipse.core.filesystem.IFileInfo; +import org.eclipse.core.filesystem.IFileStore; +import org.eclipse.core.filesystem.IFileSystem; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; + +/** + * IFileStore implementation that delegates to the give {@link IFileStore}. + * This makes it easier to just override a single method from a store. + */ +class FileStoreAdapter implements IFileStore { + + private final IFileStore mStore; + + public FileStoreAdapter(IFileStore store) { + mStore = store; + } + + @SuppressWarnings("rawtypes") + @Override + public Object getAdapter(Class adapter) { + return mStore.getAdapter(adapter); + } + + @Override + public IFileInfo[] childInfos(int options, IProgressMonitor monitor) throws CoreException { + return mStore.childInfos(options, monitor); + } + + @Override + public String[] childNames(int options, IProgressMonitor monitor) + throws CoreException { + return mStore.childNames(options, monitor); + } + + @Override + public IFileStore[] childStores(int options, IProgressMonitor monitor) throws CoreException { + return mStore.childStores(options, monitor); + } + + @Override + public void copy(IFileStore destination, int options, IProgressMonitor monitor) + throws CoreException { + mStore.copy(destination, options, monitor); + } + + @Override + public void delete(int options, IProgressMonitor monitor) throws CoreException { + mStore.delete(options, monitor); + } + + @Override + public IFileInfo fetchInfo() { + return mStore.fetchInfo(); + } + + @Override + public IFileInfo fetchInfo(int options, IProgressMonitor monitor) throws CoreException { + return mStore.fetchInfo(options, monitor); + } + + @Deprecated + @Override + public IFileStore getChild(IPath path) { + return mStore.getChild(path); + } + + @Override + public IFileStore getFileStore(IPath path) { + return mStore.getFileStore(path); + } + + @Override + public IFileStore getChild(String name) { + return mStore.getChild(name); + } + + @Override + public IFileSystem getFileSystem() { + return mStore.getFileSystem(); + } + + @Override + public String getName() { + return mStore.getName(); + } + + @Override + public IFileStore getParent() { + return mStore.getParent(); + } + + @Override + public boolean isParentOf(IFileStore other) { + return mStore.isParentOf(other); + } + + @Override + public IFileStore mkdir(int options, IProgressMonitor monitor) throws CoreException { + return mStore.mkdir(options, monitor); + } + + @Override + public void move(IFileStore destination, int options, IProgressMonitor monitor) + throws CoreException { + mStore.move(destination, options, monitor); + } + + @Override + public InputStream openInputStream(int options, IProgressMonitor monitor) + throws CoreException { + return mStore.openInputStream(options, monitor); + } + + @Override + public OutputStream openOutputStream(int options, IProgressMonitor monitor) + throws CoreException { + return mStore.openOutputStream(options, monitor); + } + + @Override + public void putInfo(IFileInfo info, int options, IProgressMonitor monitor) + throws CoreException { + mStore.putInfo(info, options, monitor); + } + + @Override + public File toLocalFile(int options, IProgressMonitor monitor) throws CoreException { + return mStore.toLocalFile(options, monitor); + } + + @Override + public URI toURI() { + return mStore.toURI(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportPage.java new file mode 100644 index 000000000..1e02fedae --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportPage.java @@ -0,0 +1,512 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.tools.lint.detector.api.LintUtils; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.jface.viewers.CellLabelProvider; +import org.eclipse.jface.viewers.CheckStateChangedEvent; +import org.eclipse.jface.viewers.CheckboxTableViewer; +import org.eclipse.jface.viewers.ColumnViewer; +import org.eclipse.jface.viewers.EditingSupport; +import org.eclipse.jface.viewers.ICheckStateListener; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.viewers.TextCellEditor; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.jface.wizard.IWizardPage; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.events.TraverseEvent; +import org.eclipse.swt.events.TraverseListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.DirectoryDialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkingSet; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** WizardPage for importing Android projects */ +class ImportPage extends WizardPage implements SelectionListener, IStructuredContentProvider, + ICheckStateListener, KeyListener, TraverseListener, ControlListener { + private static final int DIR_COLUMN = 0; + private static final int NAME_COLUMN = 1; + + private final NewProjectWizardState mValues; + private List<ImportedProject> mProjectPaths; + private final IProject[] mExistingProjects; + + private Text mDir; + private Button mBrowseButton; + private Button mCopyCheckBox; + private Button mRefreshButton; + private Button mDeselectAllButton; + private Button mSelectAllButton; + private Table mTable; + private CheckboxTableViewer mCheckboxTableViewer; + private WorkingSetGroup mWorkingSetGroup; + + ImportPage(NewProjectWizardState values) { + super("importPage"); //$NON-NLS-1$ + mValues = values; + setTitle("Import Projects"); + setDescription("Select a directory to search for existing Android projects"); + mWorkingSetGroup = new WorkingSetGroup(); + setWorkingSets(new IWorkingSet[0]); + + // Record all projects such that we can ensure that the project names are unique + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + mExistingProjects = workspaceRoot.getProjects(); + } + + public void init(IStructuredSelection selection, IWorkbenchPart activePart) { + setWorkingSets(WorkingSetHelper.getSelectedWorkingSet(selection, activePart)); + } + + @SuppressWarnings("unused") // SWT constructors have side effects and aren't unused + @Override + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NULL); + setControl(container); + container.setLayout(new GridLayout(3, false)); + + Label directoryLabel = new Label(container, SWT.NONE); + directoryLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + directoryLabel.setText("Root Directory:"); + + mDir = new Text(container, SWT.BORDER); + mDir.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mDir.addKeyListener(this); + mDir.addTraverseListener(this); + + mBrowseButton = new Button(container, SWT.NONE); + mBrowseButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + mBrowseButton.setText("Browse..."); + mBrowseButton.addSelectionListener(this); + + Label projectsLabel = new Label(container, SWT.NONE); + projectsLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1)); + projectsLabel.setText("Projects:"); + + mTable = new Table(container, SWT.CHECK); + mTable.setHeaderVisible(true); + mCheckboxTableViewer = new CheckboxTableViewer(mTable); + + TableViewerColumn dirViewerColumn = new TableViewerColumn(mCheckboxTableViewer, SWT.NONE); + TableColumn dirColumn = dirViewerColumn.getColumn(); + dirColumn.setWidth(200); + dirColumn.setText("Project to Import"); + TableViewerColumn nameViewerColumn = new TableViewerColumn(mCheckboxTableViewer, SWT.NONE); + TableColumn nameColumn = nameViewerColumn.getColumn(); + nameColumn.setWidth(200); + nameColumn.setText("New Project Name"); + nameViewerColumn.setEditingSupport(new ProjectNameEditingSupport(mCheckboxTableViewer)); + + mTable.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 4)); + mTable.setLinesVisible(true); + mTable.setHeaderVisible(true); + mTable.addSelectionListener(this); + mTable.addControlListener(this); + mCheckboxTableViewer.setContentProvider(this); + mCheckboxTableViewer.setInput(this); + mCheckboxTableViewer.addCheckStateListener(this); + mCheckboxTableViewer.setLabelProvider(new ProjectCellLabelProvider()); + + mSelectAllButton = new Button(container, SWT.NONE); + mSelectAllButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + mSelectAllButton.setText("Select All"); + mSelectAllButton.addSelectionListener(this); + + mDeselectAllButton = new Button(container, SWT.NONE); + mDeselectAllButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + mDeselectAllButton.setText("Deselect All"); + mDeselectAllButton.addSelectionListener(this); + + mRefreshButton = new Button(container, SWT.NONE); + mRefreshButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + mRefreshButton.setText("Refresh"); + mRefreshButton.addSelectionListener(this); + new Label(container, SWT.NONE); + + mCopyCheckBox = new Button(container, SWT.CHECK); + mCopyCheckBox.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1)); + mCopyCheckBox.setText("Copy projects into workspace"); + mCopyCheckBox.addSelectionListener(this); + + Composite group = mWorkingSetGroup.createControl(container); + group.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 3, 1)); + + updateColumnWidths(); + } + + private void updateColumnWidths() { + Rectangle r = mTable.getClientArea(); + int availableWidth = r.width; + // Add all available size to the first column + for (int i = 1; i < mTable.getColumnCount(); i++) { + TableColumn column = mTable.getColumn(i); + availableWidth -= column.getWidth(); + } + if (availableWidth > 100) { + mTable.getColumn(0).setWidth(availableWidth); + } + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + validatePage(); + } + + private void refresh() { + File root = new File(mDir.getText().trim()); + mProjectPaths = searchForProjects(root); + mCheckboxTableViewer.refresh(); + mCheckboxTableViewer.setAllChecked(true); + + updateValidity(); + validatePage(); + } + + private void updateValidity(){ + List<ImportedProject> selected = new ArrayList<ImportedProject>(); + List<ImportedProject> disabled = new ArrayList<ImportedProject>(); + for (ImportedProject project : mProjectPaths) { + String projectName = project.getProjectName(); + boolean invalid = false; + for (IProject existingProject : mExistingProjects) { + if (projectName.equals(existingProject.getName())) { + invalid = true; + break; + } + } + if (invalid) { + disabled.add(project); + } else { + selected.add(project); + } + } + + mValues.importProjects = selected; + + mCheckboxTableViewer.setGrayedElements(disabled.toArray()); + mCheckboxTableViewer.setCheckedElements(selected.toArray()); + mCheckboxTableViewer.refresh(); + mCheckboxTableViewer.getTable().setFocus(); + } + + private List<ImportedProject> searchForProjects(File dir) { + List<ImportedProject> projects = new ArrayList<ImportedProject>(); + addProjects(dir, projects, dir.getPath().length() + 1); + return projects; + } + + /** Finds all project directories under the given directory */ + private void addProjects(File dir, List<ImportedProject> projects, int prefixLength) { + if (dir.isDirectory()) { + if (LintUtils.isManifestFolder(dir)) { + String relative = dir.getPath(); + if (relative.length() > prefixLength) { + relative = relative.substring(prefixLength); + } + projects.add(new ImportedProject(dir, relative)); + } + + File[] children = dir.listFiles(); + if (children != null) { + for (File child : children) { + addProjects(child, projects, prefixLength); + } + } + } + } + + private void validatePage() { + IStatus status = null; + + // Validate project name -- unless we're creating a sample, in which case + // the user will get a chance to pick the name on the Sample page + if (mProjectPaths == null || mProjectPaths.isEmpty()) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Select a directory to search for existing Android projects"); + } else if (mValues.importProjects == null || mValues.importProjects.isEmpty()) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Select at least one project"); + } else { + for (ImportedProject project : mValues.importProjects) { + if (mCheckboxTableViewer.getGrayed(project)) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("Cannot import %1$s because the project name is in use", + project.getProjectName())); + break; + } else { + status = ProjectNamePage.validateProjectName(project.getProjectName()); + if (status != null && !status.isOK()) { + // Need to insert project name to make it clear which project name + // is in violation + if (mValues.importProjects.size() > 1) { + String message = String.format("%1$s: %2$s", + project.getProjectName(), status.getMessage()); + status = new Status(status.getSeverity(), AdtPlugin.PLUGIN_ID, + message); + } + break; + } else { + status = null; // Don't leave non null status with isOK() == true + } + } + } + } + + // -- update UI & enable finish if there's no error + setPageComplete(status == null || status.getSeverity() != IStatus.ERROR); + if (status != null) { + setMessage(status.getMessage(), + status.getSeverity() == IStatus.ERROR + ? IMessageProvider.ERROR : IMessageProvider.WARNING); + } else { + setErrorMessage(null); + setMessage(null); + } + } + + /** + * Returns the working sets to which the new project should be added. + * + * @return the selected working sets to which the new project should be added + */ + private IWorkingSet[] getWorkingSets() { + return mWorkingSetGroup.getSelectedWorkingSets(); + } + + /** + * Sets the working sets to which the new project should be added. + * + * @param workingSets the initial selected working sets + */ + private void setWorkingSets(IWorkingSet[] workingSets) { + assert workingSets != null; + mWorkingSetGroup.setWorkingSets(workingSets); + } + + @Override + public IWizardPage getNextPage() { + // Sync working set data to the value object, since the WorkingSetGroup + // doesn't let us add listeners to do this lazily + mValues.workingSets = getWorkingSets(); + + return super.getNextPage(); + } + + // ---- Implements SelectionListener ---- + + @Override + public void widgetSelected(SelectionEvent e) { + Object source = e.getSource(); + if (source == mBrowseButton) { + // Choose directory + DirectoryDialog dialog = new DirectoryDialog(getShell(), SWT.OPEN); + String path = mDir.getText().trim(); + if (path.length() > 0) { + dialog.setFilterPath(path); + } + String file = dialog.open(); + if (file != null) { + mDir.setText(file); + refresh(); + } + } else if (source == mSelectAllButton) { + mCheckboxTableViewer.setAllChecked(true); + mValues.importProjects = mProjectPaths; + } else if (source == mDeselectAllButton) { + mCheckboxTableViewer.setAllChecked(false); + mValues.importProjects = Collections.emptyList(); + } else if (source == mRefreshButton || source == mDir) { + refresh(); + } else if (source == mCopyCheckBox) { + mValues.copyIntoWorkspace = mCopyCheckBox.getSelection(); + } + + validatePage(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + + // ---- KeyListener ---- + + @Override + public void keyPressed(KeyEvent e) { + if (e.getSource() == mDir) { + if (e.keyCode == SWT.CR) { + refresh(); + } + } + } + + @Override + public void keyReleased(KeyEvent e) { + } + + // ---- TraverseListener ---- + + @Override + public void keyTraversed(TraverseEvent e) { + // Prevent Return from running through the wizard; return is handled by + // key listener to refresh project list instead + if (SWT.TRAVERSE_RETURN == e.detail) { + e.doit = false; + } + } + + // ---- Implements IStructuredContentProvider ---- + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + @Override + public Object[] getElements(Object inputElement) { + return mProjectPaths != null ? mProjectPaths.toArray() : new Object[0]; + } + + // ---- Implements ICheckStateListener ---- + + @Override + public void checkStateChanged(CheckStateChangedEvent event) { + // Try to disable other elements that conflict with this + Object[] checked = mCheckboxTableViewer.getCheckedElements(); + List<ImportedProject> selected = new ArrayList<ImportedProject>(checked.length); + for (Object o : checked) { + if (!mCheckboxTableViewer.getGrayed(o)) { + selected.add((ImportedProject) o); + } + } + mValues.importProjects = selected; + validatePage(); + + mCheckboxTableViewer.update(event.getElement(), null); + } + + // ---- Implements ControlListener ---- + + @Override + public void controlMoved(ControlEvent e) { + } + + @Override + public void controlResized(ControlEvent e) { + updateColumnWidths(); + } + + private final class ProjectCellLabelProvider extends CellLabelProvider { + @Override + public void update(ViewerCell cell) { + Object element = cell.getElement(); + int index = cell.getColumnIndex(); + ImportedProject project = (ImportedProject) element; + + Display display = mTable.getDisplay(); + Color fg; + if (mCheckboxTableViewer.getGrayed(element)) { + fg = display.getSystemColor(SWT.COLOR_DARK_GRAY); + } else { + fg = display.getSystemColor(SWT.COLOR_LIST_FOREGROUND); + } + cell.setForeground(fg); + cell.setBackground(display.getSystemColor(SWT.COLOR_LIST_BACKGROUND)); + + switch (index) { + case DIR_COLUMN: { + // Directory name + cell.setText(project.getRelativePath()); + return; + } + + case NAME_COLUMN: { + // New name + cell.setText(project.getProjectName()); + return; + } + default: + assert false : index; + } + cell.setText(""); + } + } + + /** Editing support for the project name column */ + private class ProjectNameEditingSupport extends EditingSupport { + private ProjectNameEditingSupport(ColumnViewer viewer) { + super(viewer); + } + + @Override + protected void setValue(Object element, Object value) { + ImportedProject project = (ImportedProject) element; + project.setProjectName(value.toString()); + mCheckboxTableViewer.update(element, null); + updateValidity(); + validatePage(); + } + + @Override + protected Object getValue(Object element) { + ImportedProject project = (ImportedProject) element; + return project.getProjectName(); + } + + @Override + protected CellEditor getCellEditor(Object element) { + return new TextCellEditor(mTable); + } + + @Override + protected boolean canEdit(Object element) { + return true; + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportProjectWizard.java new file mode 100644 index 000000000..1004fd692 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportProjectWizard.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE; +import static com.android.SdkConstants.OS_SDK_TOOLS_LIB_FOLDER; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; + +import org.eclipse.jdt.ui.actions.OpenJavaPerspectiveAction; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.ui.INewWizard; +import org.eclipse.ui.IWorkbench; + +import java.io.File; + + +/** + * An "Import Android Project" wizard. + */ +public class ImportProjectWizard extends Wizard implements INewWizard { + private static final String PROJECT_LOGO_LARGE = "icons/android-64.png"; //$NON-NLS-1$ + + private NewProjectWizardState mValues; + private ImportPage mImportPage; + private IStructuredSelection mSelection; + + /** Constructs a new wizard default project wizard */ + public ImportProjectWizard() { + } + + @Override + public void addPages() { + mValues = new NewProjectWizardState(Mode.ANY); + mImportPage = new ImportPage(mValues); + if (mSelection != null) { + mImportPage.init(mSelection, AdtUtils.getActivePart()); + } + addPage(mImportPage); + } + + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) { + mSelection = selection; + + setHelpAvailable(false); // TODO have help + ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE); + setDefaultPageImageDescriptor(desc); + + // Trigger a check to see if the SDK needs to be reloaded (which will + // invoke onSdkLoaded asynchronously as needed). + AdtPlugin.getDefault().refreshSdk(); + } + + @Override + public boolean performFinish() { + File file = new File(AdtPlugin.getOsSdkFolder(), OS_SDK_TOOLS_LIB_FOLDER + File.separator + + FN_PROJECT_PROGUARD_FILE); + if (!file.exists()) { + AdtPlugin.displayError("Tools Out of Date?", + String.format("It looks like you do not have the latest version of the " + + "SDK Tools installed. Make sure you update via the SDK Manager " + + "first. (Could not find %1$s)", file.getPath())); + return false; + } + + NewProjectCreator creator = new NewProjectCreator(mValues, getContainer()); + if (!(creator.createAndroidProjects())) { + return false; + } + + // Open the default Java Perspective + OpenJavaPerspectiveAction action = new OpenJavaPerspectiveAction(); + action.run(); + return true; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportedProject.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportedProject.java new file mode 100644 index 000000000..74af651ca --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportedProject.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import static com.android.SdkConstants.ATTR_NAME; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.xml.AndroidManifestParser; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.common.xml.ManifestData.Activity; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.io.FolderWrapper; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.internal.project.ProjectProperties.PropertyType; +import com.google.common.base.Charsets; +import com.google.common.io.Files; + +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IStatus; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** An Android project to be imported */ +class ImportedProject { + private final File mLocation; + private String mActivityName; + private ManifestData mManifest; + private String mProjectName; + private String mRelativePath; + + ImportedProject(File location, String relativePath) { + super(); + mLocation = location; + mRelativePath = relativePath; + } + + File getLocation() { + return mLocation; + } + + String getRelativePath() { + return mRelativePath; + } + + @Nullable + ManifestData getManifest() { + if (mManifest == null) { + try { + mManifest = AndroidManifestParser.parse(new FolderWrapper(mLocation)); + } catch (SAXException e) { + // Some sort of error in the manifest file: report to the user in a better way? + AdtPlugin.log(e, null); + return null; + } catch (Exception e) { + AdtPlugin.log(e, null); + return null; + } + } + + return mManifest; + } + + @Nullable + public String getActivityName() { + if (mActivityName == null) { + // Compute the project name and the package name from the manifest + ManifestData manifest = getManifest(); + if (manifest != null) { + if (manifest.getLauncherActivity() != null) { + mActivityName = manifest.getLauncherActivity().getName(); + } + if (mActivityName == null || mActivityName.isEmpty()) { + Activity[] activities = manifest.getActivities(); + for (Activity activity : activities) { + mActivityName = activity.getName(); + if (mActivityName != null && !mActivityName.isEmpty()) { + break; + } + } + } + if (mActivityName != null) { + int index = mActivityName.lastIndexOf('.'); + mActivityName = mActivityName.substring(index + 1); + } + } + } + + return mActivityName; + } + + @NonNull + public String getProjectName() { + if (mProjectName == null) { + // Are we importing an Eclipse project? If so just use the existing project name + mProjectName = findEclipseProjectName(); + if (mProjectName != null) { + return mProjectName; + } + + String activityName = getActivityName(); + if (activityName == null || activityName.isEmpty()) { + // I could also look at the build files, say build.xml from ant, and + // try to glean the project name from there + mProjectName = mLocation.getName(); + } else { + // Try to derive it from the activity name: + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IStatus nameStatus = workspace.validateName(activityName, IResource.PROJECT); + if (nameStatus.isOK()) { + mProjectName = activityName; + } else { + // Try to derive it by escaping characters + StringBuilder sb = new StringBuilder(); + for (int i = 0, n = activityName.length(); i < n; i++) { + char c = activityName.charAt(i); + if (c != IPath.DEVICE_SEPARATOR && c != IPath.SEPARATOR && c != '\\') { + sb.append(c); + } + } + if (sb.length() == 0) { + mProjectName = mLocation.getName(); + } else { + mProjectName = sb.toString(); + } + } + } + } + + return mProjectName; + } + + @Nullable + private String findEclipseProjectName() { + File projectFile = new File(mLocation, ".project"); //$NON-NLS-1$ + if (projectFile.exists()) { + String xml; + try { + xml = Files.toString(projectFile, Charsets.UTF_8); + Document doc = DomUtilities.parseDocument(xml, false); + if (doc != null) { + NodeList names = doc.getElementsByTagName(ATTR_NAME); + if (names.getLength() >= 1) { + Node nameElement = names.item(0); + String name = nameElement.getTextContent().trim(); + if (!name.isEmpty()) { + return name; + } + } + } + } catch (IOException e) { + // pass: don't attempt to read project name; must be some sort of unrelated + // file with the same name, perhaps from a different editor or IDE + } + } + + return null; + } + + public void setProjectName(@NonNull String newName) { + mProjectName = newName; + } + + public IAndroidTarget getTarget() { + // Pick a target: + // First try to find the one requested by project.properties + IAndroidTarget[] targets = Sdk.getCurrent().getTargets(); + ProjectProperties properties = ProjectProperties.load(mLocation.getPath(), + PropertyType.PROJECT); + if (properties != null) { + String targetProperty = properties.getProperty(ProjectProperties.PROPERTY_TARGET); + if (targetProperty != null) { + Matcher m = Pattern.compile("android-(.+)").matcher( //$NON-NLS-1$ + targetProperty.trim()); + if (m.matches()) { + String targetName = m.group(1); + int targetLevel; + try { + targetLevel = Integer.parseInt(targetName); + } catch (NumberFormatException nufe) { + // pass + targetLevel = -1; + } + for (IAndroidTarget t : targets) { + AndroidVersion version = t.getVersion(); + if (version.isPreview() && targetName.equals(version.getCodename())) { + return t; + } else if (targetLevel == version.getApiLevel()) { + return t; + } + } + if (targetLevel > 0) { + // If not found, pick the closest one that is higher than the + // api level + IAndroidTarget target = targets[targets.length - 1]; + int targetDelta = target.getVersion().getApiLevel() - targetLevel; + for (IAndroidTarget t : targets) { + int newDelta = t.getVersion().getApiLevel() - targetLevel; + if (newDelta >= 0 && newDelta < targetDelta) { + targetDelta = newDelta; + target = t; + } + } + + return target; + } + } + } + } + + // If not found, pick the closest one to the one requested by the + // project (in project.properties) that is still >= the minSdk version + IAndroidTarget target = targets[targets.length - 1]; + ManifestData manifest = getManifest(); + if (manifest != null) { + int minSdkLevel = manifest.getMinSdkVersion(); + int targetDelta = target.getVersion().getApiLevel() - minSdkLevel; + for (IAndroidTarget t : targets) { + int newDelta = t.getVersion().getApiLevel() - minSdkLevel; + if (newDelta >= 0 && newDelta < targetDelta) { + targetDelta = newDelta; + target = t; + } + } + } + + return target; + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java new file mode 100644 index 000000000..d168c7503 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java @@ -0,0 +1,1520 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import static com.android.SdkConstants.FN_PROJECT_PROPERTIES; +import static com.android.sdklib.internal.project.ProjectProperties.PROPERTY_LIBRARY; + +import static org.eclipse.core.resources.IResource.DEPTH_ZERO; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.ide.common.res2.ValueXmlHelper; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.common.xml.XmlFormatStyle; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.project.AndroidNature; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; +import com.android.io.StreamException; +import com.android.resources.Density; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy; + +import org.eclipse.core.filesystem.EFS; +import org.eclipse.core.filesystem.IFileInfo; +import org.eclipse.core.filesystem.IFileStore; +import org.eclipse.core.filesystem.IFileSystem; +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceStatus; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.IWorkspaceRunnable; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.SubProgressMonitor; +import org.eclipse.jdt.core.IAccessRule; +import org.eclipse.jdt.core.IClasspathAttribute; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.operation.IRunnableContext; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IWorkingSet; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.actions.WorkspaceModifyOperation; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * The actual project creator invoked from the New Project Wizard + * <p/> + * Note: this class is public so that it can be accessed from unit tests. + * It is however an internal class. Its API may change without notice. + * It should semantically be considered as a private final class. + */ +public class NewProjectCreator { + + private static final String PARAM_SDK_TOOLS_DIR = "ANDROID_SDK_TOOLS"; //$NON-NLS-1$ + private static final String PARAM_ACTIVITY = "ACTIVITY_NAME"; //$NON-NLS-1$ + private static final String PARAM_APPLICATION = "APPLICATION_NAME"; //$NON-NLS-1$ + private static final String PARAM_PACKAGE = "PACKAGE"; //$NON-NLS-1$ + private static final String PARAM_IMPORT_RESOURCE_CLASS = "IMPORT_RESOURCE_CLASS"; //$NON-NLS-1$ + private static final String PARAM_PROJECT = "PROJECT_NAME"; //$NON-NLS-1$ + private static final String PARAM_STRING_NAME = "STRING_NAME"; //$NON-NLS-1$ + private static final String PARAM_STRING_CONTENT = "STRING_CONTENT"; //$NON-NLS-1$ + private static final String PARAM_IS_NEW_PROJECT = "IS_NEW_PROJECT"; //$NON-NLS-1$ + private static final String PARAM_SAMPLE_LOCATION = "SAMPLE_LOCATION"; //$NON-NLS-1$ + private static final String PARAM_SOURCE = "SOURCE"; //$NON-NLS-1$ + private static final String PARAM_SRC_FOLDER = "SRC_FOLDER"; //$NON-NLS-1$ + private static final String PARAM_SDK_TARGET = "SDK_TARGET"; //$NON-NLS-1$ + private static final String PARAM_IS_LIBRARY = "IS_LIBRARY"; //$NON-NLS-1$ + private static final String PARAM_MIN_SDK_VERSION = "MIN_SDK_VERSION"; //$NON-NLS-1$ + // Warning: The expanded string PARAM_TEST_TARGET_PACKAGE must not contain the + // string "PACKAGE" since it collides with the replacement of PARAM_PACKAGE. + private static final String PARAM_TEST_TARGET_PACKAGE = "TEST_TARGET_PCKG"; //$NON-NLS-1$ + private static final String PARAM_TARGET_SELF = "TARGET_SELF"; //$NON-NLS-1$ + private static final String PARAM_TARGET_MAIN = "TARGET_MAIN"; //$NON-NLS-1$ + private static final String PARAM_TARGET_EXISTING = "TARGET_EXISTING"; //$NON-NLS-1$ + private static final String PARAM_REFERENCE_PROJECT = "REFERENCE_PROJECT"; //$NON-NLS-1$ + + private static final String PH_ACTIVITIES = "ACTIVITIES"; //$NON-NLS-1$ + private static final String PH_USES_SDK = "USES-SDK"; //$NON-NLS-1$ + private static final String PH_INTENT_FILTERS = "INTENT_FILTERS"; //$NON-NLS-1$ + private static final String PH_STRINGS = "STRINGS"; //$NON-NLS-1$ + private static final String PH_TEST_USES_LIBRARY = "TEST-USES-LIBRARY"; //$NON-NLS-1$ + private static final String PH_TEST_INSTRUMENTATION = "TEST-INSTRUMENTATION"; //$NON-NLS-1$ + + private static final String BIN_DIRECTORY = + SdkConstants.FD_OUTPUT + AdtConstants.WS_SEP; + private static final String BIN_CLASSES_DIRECTORY = + SdkConstants.FD_OUTPUT + AdtConstants.WS_SEP + + SdkConstants.FD_CLASSES_OUTPUT + AdtConstants.WS_SEP; + private static final String RES_DIRECTORY = + SdkConstants.FD_RESOURCES + AdtConstants.WS_SEP; + private static final String ASSETS_DIRECTORY = + SdkConstants.FD_ASSETS + AdtConstants.WS_SEP; + private static final String DRAWABLE_DIRECTORY = + SdkConstants.FD_RES_DRAWABLE + AdtConstants.WS_SEP; + private static final String DRAWABLE_XHDPI_DIRECTORY = + SdkConstants.FD_RES_DRAWABLE + '-' + Density.XHIGH.getResourceValue() + + AdtConstants.WS_SEP; + private static final String DRAWABLE_HDPI_DIRECTORY = + SdkConstants.FD_RES_DRAWABLE + '-' + Density.HIGH.getResourceValue() + + AdtConstants.WS_SEP; + private static final String DRAWABLE_MDPI_DIRECTORY = + SdkConstants.FD_RES_DRAWABLE + '-' + Density.MEDIUM.getResourceValue() + + AdtConstants.WS_SEP; + private static final String DRAWABLE_LDPI_DIRECTORY = + SdkConstants.FD_RES_DRAWABLE + '-' + Density.LOW.getResourceValue() + + AdtConstants.WS_SEP; + private static final String LAYOUT_DIRECTORY = + SdkConstants.FD_RES_LAYOUT + AdtConstants.WS_SEP; + private static final String VALUES_DIRECTORY = + SdkConstants.FD_RES_VALUES + AdtConstants.WS_SEP; + private static final String GEN_SRC_DIRECTORY = + SdkConstants.FD_GEN_SOURCES + AdtConstants.WS_SEP; + + private static final String TEMPLATES_DIRECTORY = "templates/"; //$NON-NLS-1$ + private static final String TEMPLATE_MANIFEST = TEMPLATES_DIRECTORY + + "AndroidManifest.template"; //$NON-NLS-1$ + private static final String TEMPLATE_ACTIVITIES = TEMPLATES_DIRECTORY + + "activity.template"; //$NON-NLS-1$ + private static final String TEMPLATE_USES_SDK = TEMPLATES_DIRECTORY + + "uses-sdk.template"; //$NON-NLS-1$ + private static final String TEMPLATE_INTENT_LAUNCHER = TEMPLATES_DIRECTORY + + "launcher_intent_filter.template"; //$NON-NLS-1$ + private static final String TEMPLATE_TEST_USES_LIBRARY = TEMPLATES_DIRECTORY + + "test_uses-library.template"; //$NON-NLS-1$ + private static final String TEMPLATE_TEST_INSTRUMENTATION = TEMPLATES_DIRECTORY + + "test_instrumentation.template"; //$NON-NLS-1$ + + + + private static final String TEMPLATE_STRINGS = TEMPLATES_DIRECTORY + + "strings.template"; //$NON-NLS-1$ + private static final String TEMPLATE_STRING = TEMPLATES_DIRECTORY + + "string.template"; //$NON-NLS-1$ + private static final String PROJECT_ICON = "ic_launcher.png"; //$NON-NLS-1$ + private static final String ICON_XHDPI = "ic_launcher_xhdpi.png"; //$NON-NLS-1$ + private static final String ICON_HDPI = "ic_launcher_hdpi.png"; //$NON-NLS-1$ + private static final String ICON_MDPI = "ic_launcher_mdpi.png"; //$NON-NLS-1$ + private static final String ICON_LDPI = "ic_launcher_ldpi.png"; //$NON-NLS-1$ + + private static final String STRINGS_FILE = "strings.xml"; //$NON-NLS-1$ + + private static final String STRING_RSRC_PREFIX = SdkConstants.STRING_PREFIX; + private static final String STRING_APP_NAME = "app_name"; //$NON-NLS-1$ + private static final String STRING_HELLO_WORLD = "hello"; //$NON-NLS-1$ + + private static final String[] DEFAULT_DIRECTORIES = new String[] { + BIN_DIRECTORY, BIN_CLASSES_DIRECTORY, RES_DIRECTORY, ASSETS_DIRECTORY }; + private static final String[] RES_DIRECTORIES = new String[] { + DRAWABLE_DIRECTORY, LAYOUT_DIRECTORY, VALUES_DIRECTORY }; + private static final String[] RES_DENSITY_ENABLED_DIRECTORIES = new String[] { + DRAWABLE_XHDPI_DIRECTORY, + DRAWABLE_HDPI_DIRECTORY, DRAWABLE_MDPI_DIRECTORY, DRAWABLE_LDPI_DIRECTORY, + LAYOUT_DIRECTORY, VALUES_DIRECTORY }; + + private static final String JAVA_ACTIVITY_TEMPLATE = "java_file.template"; //$NON-NLS-1$ + private static final String LAYOUT_TEMPLATE = "layout.template"; //$NON-NLS-1$ + private static final String MAIN_LAYOUT_XML = "main.xml"; //$NON-NLS-1$ + + private final NewProjectWizardState mValues; + private final IRunnableContext mRunnableContext; + + /** + * Creates a new {@linkplain NewProjectCreator} + * @param values the wizard state with initial project parameters + * @param runnableContext the context to run project creation in + */ + public NewProjectCreator(NewProjectWizardState values, IRunnableContext runnableContext) { + mValues = values; + mRunnableContext = runnableContext; + } + + /** + * Before actually creating the project for a new project (as opposed to using an + * existing project), we check if the target location is a directory that either does + * not exist or is empty. + * + * If it's not empty, ask the user for confirmation. + * + * @param destination The destination folder where the new project is to be created. + * @return True if the destination doesn't exist yet or is an empty directory or is + * accepted by the user. + */ + private boolean validateNewProjectLocationIsEmpty(IPath destination) { + File f = new File(destination.toOSString()); + if (f.isDirectory() && f.list().length > 0) { + return AdtPlugin.displayPrompt("New Android Project", + "You are going to create a new Android Project in an existing, non-empty, directory. Are you sure you want to proceed?"); + } + return true; + } + + /** + * Structure that describes all the information needed to create a project. + * This is collected from the pages by {@link NewProjectCreator#createAndroidProjects()} + * and then used by + * {@link NewProjectCreator#createProjectAsync(IProgressMonitor, ProjectInfo, ProjectInfo)}. + */ + private static class ProjectInfo { + private final IProject mProject; + private final IProjectDescription mDescription; + private final Map<String, Object> mParameters; + private final HashMap<String, String> mDictionary; + + public ProjectInfo(IProject project, + IProjectDescription description, + Map<String, Object> parameters, + HashMap<String, String> dictionary) { + mProject = project; + mDescription = description; + mParameters = parameters; + mDictionary = dictionary; + } + + public IProject getProject() { + return mProject; + } + + public IProjectDescription getDescription() { + return mDescription; + } + + public Map<String, Object> getParameters() { + return mParameters; + } + + public HashMap<String, String> getDictionary() { + return mDictionary; + } + } + + /** + * Creates the android project. + * @return True if the project could be created. + */ + public boolean createAndroidProjects() { + if (mValues.importProjects != null && !mValues.importProjects.isEmpty()) { + return importProjects(); + } + + final ProjectInfo mainData = collectMainPageInfo(); + final ProjectInfo testData = collectTestPageInfo(); + + // Create a monitored operation to create the actual project + WorkspaceModifyOperation op = new WorkspaceModifyOperation() { + @Override + protected void execute(IProgressMonitor monitor) throws InvocationTargetException { + createProjectAsync(monitor, mainData, testData, null, true); + } + }; + + // Run the operation in a different thread + runAsyncOperation(op); + return true; + } + + /** + * Creates the a plain Java project without typical android directories or an Android Nature. + * This is intended for use by unit tests and not as a general-purpose Java project creator. + * @return True if the project could be created. + */ + @VisibleForTesting + public boolean createJavaProjects() { + if (mValues.importProjects != null && !mValues.importProjects.isEmpty()) { + return importProjects(); + } + + final ProjectInfo mainData = collectMainPageInfo(); + final ProjectInfo testData = collectTestPageInfo(); + + // Create a monitored operation to create the actual project + WorkspaceModifyOperation op = new WorkspaceModifyOperation() { + @Override + protected void execute(IProgressMonitor monitor) throws InvocationTargetException { + createProjectAsync(monitor, mainData, testData, null, false); + } + }; + + // Run the operation in a different thread + runAsyncOperation(op); + return true; + } + + /** + * Imports a list of projects + */ + private boolean importProjects() { + assert mValues.importProjects != null && !mValues.importProjects.isEmpty(); + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + + final List<ProjectInfo> projectData = new ArrayList<ProjectInfo>(); + for (ImportedProject p : mValues.importProjects) { + + // Compute the project name and the package name from the manifest + ManifestData manifest = p.getManifest(); + if (manifest == null) { + continue; + } + String packageName = manifest.getPackage(); + String projectName = p.getProjectName(); + String minSdk = manifest.getMinSdkVersionString(); + + final IProject project = workspace.getRoot().getProject(projectName); + final IProjectDescription description = + workspace.newProjectDescription(project.getName()); + + final Map<String, Object> parameters = new HashMap<String, Object>(); + parameters.put(PARAM_PROJECT, projectName); + parameters.put(PARAM_PACKAGE, packageName); + parameters.put(PARAM_SDK_TOOLS_DIR, AdtPlugin.getOsSdkToolsFolder()); + parameters.put(PARAM_IS_NEW_PROJECT, Boolean.FALSE); + parameters.put(PARAM_SRC_FOLDER, SdkConstants.FD_SOURCES); + + parameters.put(PARAM_SDK_TARGET, p.getTarget()); + + // TODO: Find out if these end up getting used in the import-path through the code! + parameters.put(PARAM_MIN_SDK_VERSION, minSdk); + parameters.put(PARAM_APPLICATION, STRING_RSRC_PREFIX + STRING_APP_NAME); + final HashMap<String, String> dictionary = new HashMap<String, String>(); + dictionary.put(STRING_APP_NAME, mValues.applicationName); + + if (mValues.copyIntoWorkspace) { + parameters.put(PARAM_SOURCE, p.getLocation()); + + // TODO: Make sure it isn't *already* in the workspace! + //IPath defaultLocation = Platform.getLocation(); + //if ((!mValues.useDefaultLocation || mValues.useExisting) + // && !defaultLocation.isPrefixOf(path)) { + //IPath workspaceLocation = Platform.getLocation().append(projectName); + //description.setLocation(workspaceLocation); + // DON'T SET THE LOCATION: It's IMPLIED and in fact it will generate + // an error if you set it! + } else { + // Create in place + description.setLocation(new Path(p.getLocation().getPath())); + } + + projectData.add(new ProjectInfo(project, description, parameters, dictionary)); + } + + // Create a monitored operation to create the actual project + WorkspaceModifyOperation op = new WorkspaceModifyOperation() { + @Override + protected void execute(IProgressMonitor monitor) throws InvocationTargetException { + createProjectAsync(monitor, null, null, projectData, true); + } + }; + + // Run the operation in a different thread + runAsyncOperation(op); + return true; + } + + /** + * Collects all the parameters needed to create the main project. + * @return A new {@link ProjectInfo} on success. Returns null if the project cannot be + * created because parameters are incorrect or should not be created because there + * is no main page. + */ + private ProjectInfo collectMainPageInfo() { + if (mValues.mode == Mode.TEST) { + return null; + } + + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + final IProject project = workspace.getRoot().getProject(mValues.projectName); + final IProjectDescription description = workspace.newProjectDescription(project.getName()); + + final Map<String, Object> parameters = new HashMap<String, Object>(); + parameters.put(PARAM_PROJECT, mValues.projectName); + parameters.put(PARAM_PACKAGE, mValues.packageName); + parameters.put(PARAM_APPLICATION, STRING_RSRC_PREFIX + STRING_APP_NAME); + parameters.put(PARAM_SDK_TOOLS_DIR, AdtPlugin.getOsSdkToolsFolder()); + parameters.put(PARAM_IS_NEW_PROJECT, mValues.mode == Mode.ANY && !mValues.useExisting); + parameters.put(PARAM_SAMPLE_LOCATION, mValues.chosenSample); + parameters.put(PARAM_SRC_FOLDER, mValues.sourceFolder); + parameters.put(PARAM_SDK_TARGET, mValues.target); + parameters.put(PARAM_MIN_SDK_VERSION, mValues.minSdk); + + if (mValues.createActivity) { + parameters.put(PARAM_ACTIVITY, mValues.activityName); + } + + // create a dictionary of string that will contain name+content. + // we'll put all the strings into values/strings.xml + final HashMap<String, String> dictionary = new HashMap<String, String>(); + dictionary.put(STRING_APP_NAME, mValues.applicationName); + + IPath path = new Path(mValues.projectLocation.getPath()); + IPath defaultLocation = Platform.getLocation(); + if ((!mValues.useDefaultLocation || mValues.useExisting) + && !defaultLocation.isPrefixOf(path)) { + description.setLocation(path); + } + + if (mValues.mode == Mode.ANY && !mValues.useExisting && !mValues.useDefaultLocation && + !validateNewProjectLocationIsEmpty(path)) { + return null; + } + + return new ProjectInfo(project, description, parameters, dictionary); + } + + /** + * Collects all the parameters needed to create the test project. + * + * @return A new {@link ProjectInfo} on success. Returns null if the project cannot be + * created because parameters are incorrect or should not be created because there + * is no test page. + */ + private ProjectInfo collectTestPageInfo() { + if (mValues.mode != Mode.TEST && !mValues.createPairProject) { + return null; + } + + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + String projectName = + mValues.mode == Mode.TEST ? mValues.projectName : mValues.testProjectName; + final IProject project = workspace.getRoot().getProject(projectName); + final IProjectDescription description = workspace.newProjectDescription(project.getName()); + + final Map<String, Object> parameters = new HashMap<String, Object>(); + + String pkg = + mValues.mode == Mode.TEST ? mValues.packageName : mValues.testPackageName; + + parameters.put(PARAM_PACKAGE, pkg); + parameters.put(PARAM_APPLICATION, STRING_RSRC_PREFIX + STRING_APP_NAME); + parameters.put(PARAM_SDK_TOOLS_DIR, AdtPlugin.getOsSdkToolsFolder()); + parameters.put(PARAM_IS_NEW_PROJECT, !mValues.useExisting); + parameters.put(PARAM_SRC_FOLDER, mValues.sourceFolder); + parameters.put(PARAM_SDK_TARGET, mValues.target); + parameters.put(PARAM_MIN_SDK_VERSION, mValues.minSdk); + + // Test-specific parameters + String testedPkg = mValues.createPairProject + ? mValues.packageName : mValues.testTargetPackageName; + if (testedPkg == null) { + assert mValues.testingSelf; + testedPkg = pkg; + } + + parameters.put(PARAM_TEST_TARGET_PACKAGE, testedPkg); + + if (mValues.testingSelf) { + parameters.put(PARAM_TARGET_SELF, true); + } else { + parameters.put(PARAM_TARGET_EXISTING, true); + parameters.put(PARAM_REFERENCE_PROJECT, mValues.testedProject); + } + + if (mValues.createPairProject) { + parameters.put(PARAM_TARGET_MAIN, true); + } + + // create a dictionary of string that will contain name+content. + // we'll put all the strings into values/strings.xml + final HashMap<String, String> dictionary = new HashMap<String, String>(); + dictionary.put(STRING_APP_NAME, mValues.testApplicationName); + + // Use the same logic to determine test project location as in + // ApplicationInfoPage#validateTestProjectLocation + IPath path = new Path(mValues.projectLocation.getPath()); + path = path.removeLastSegments(1).append(mValues.testProjectName); + IPath defaultLocation = Platform.getLocation(); + if ((!mValues.useDefaultLocation || mValues.useExisting) + && !path.equals(defaultLocation)) { + description.setLocation(path); + } + + if (!mValues.useExisting && !mValues.useDefaultLocation && + !validateNewProjectLocationIsEmpty(path)) { + return null; + } + + return new ProjectInfo(project, description, parameters, dictionary); + } + + /** + * Runs the operation in a different thread and display generated + * exceptions. + * + * @param op The asynchronous operation to run. + */ + private void runAsyncOperation(WorkspaceModifyOperation op) { + try { + mRunnableContext.run(true /* fork */, true /* cancelable */, op); + } catch (InvocationTargetException e) { + + AdtPlugin.log(e, "New Project Wizard failed"); + + // The runnable threw an exception + Throwable t = e.getTargetException(); + if (t instanceof CoreException) { + CoreException core = (CoreException) t; + if (core.getStatus().getCode() == IResourceStatus.CASE_VARIANT_EXISTS) { + // The error indicates the file system is not case sensitive + // and there's a resource with a similar name. + MessageDialog.openError(AdtPlugin.getShell(), + "Error", "Error: Case Variant Exists"); + } else { + ErrorDialog.openError(AdtPlugin.getShell(), + "Error", core.getMessage(), core.getStatus()); + } + } else { + // Some other kind of exception + String msg = t.getMessage(); + Throwable t1 = t; + while (msg == null && t1.getCause() != null) { + msg = t1.getMessage(); + t1 = t1.getCause(); + } + if (msg == null) { + msg = t.toString(); + } + MessageDialog.openError(AdtPlugin.getShell(), "Error", msg); + } + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + /** + * Creates the actual project(s). This is run asynchronously in a different thread. + * + * @param monitor An existing monitor. + * @param mainData Data for main project. Can be null. + * @param isAndroidProject true if the project is to be set up as a full Android project; false + * for a plain Java project. + * @throws InvocationTargetException to wrap any unmanaged exception and + * return it to the calling thread. The method can fail if it fails + * to create or modify the project or if it is canceled by the user. + */ + private void createProjectAsync(IProgressMonitor monitor, + ProjectInfo mainData, + ProjectInfo testData, + List<ProjectInfo> importData, + boolean isAndroidProject) + throws InvocationTargetException { + monitor.beginTask("Create Android Project", 100); + try { + IProject mainProject = null; + + if (mainData != null) { + mainProject = createEclipseProject( + new SubProgressMonitor(monitor, 50), + mainData.getProject(), + mainData.getDescription(), + mainData.getParameters(), + mainData.getDictionary(), + null, + isAndroidProject); + + if (mainProject != null) { + final IJavaProject javaProject = JavaCore.create(mainProject); + Display.getDefault().syncExec(new WorksetAdder(javaProject, + mValues.workingSets)); + } + } + + if (testData != null) { + Map<String, Object> parameters = testData.getParameters(); + if (parameters.containsKey(PARAM_TARGET_MAIN) && mainProject != null) { + parameters.put(PARAM_REFERENCE_PROJECT, mainProject); + } + + IProject testProject = createEclipseProject( + new SubProgressMonitor(monitor, 50), + testData.getProject(), + testData.getDescription(), + parameters, + testData.getDictionary(), + null, + isAndroidProject); + if (testProject != null) { + final IJavaProject javaProject = JavaCore.create(testProject); + Display.getDefault().syncExec(new WorksetAdder(javaProject, + mValues.workingSets)); + } + } + + if (importData != null) { + for (final ProjectInfo data : importData) { + ProjectPopulator projectPopulator = null; + if (mValues.copyIntoWorkspace) { + projectPopulator = new ProjectPopulator() { + @Override + public void populate(IProject project) { + // Copy + IFileSystem fileSystem = EFS.getLocalFileSystem(); + File source = (File) data.getParameters().get(PARAM_SOURCE); + IFileStore sourceDir = new ReadWriteFileStore( + fileSystem.getStore(source.toURI())); + IFileStore destDir = new ReadWriteFileStore( + fileSystem.getStore(AdtUtils.getAbsolutePath(project))); + try { + sourceDir.copy(destDir, EFS.OVERWRITE, null); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + }; + } + IProject project = createEclipseProject( + new SubProgressMonitor(monitor, 50), + data.getProject(), + data.getDescription(), + data.getParameters(), + data.getDictionary(), + projectPopulator, + isAndroidProject); + if (project != null) { + final IJavaProject javaProject = JavaCore.create(project); + Display.getDefault().syncExec(new WorksetAdder(javaProject, + mValues.workingSets)); + ProjectHelper.enforcePreferredCompilerCompliance(javaProject); + } + } + } + } catch (CoreException e) { + throw new InvocationTargetException(e); + } catch (IOException e) { + throw new InvocationTargetException(e); + } catch (StreamException e) { + throw new InvocationTargetException(e); + } finally { + monitor.done(); + } + } + + /** Handler which can write contents into a project */ + public interface ProjectPopulator { + /** + * Add contents into the given project + * + * @param project the project to write into + * @throws InvocationTargetException if anything goes wrong + */ + public void populate(IProject project) throws InvocationTargetException; + } + + /** + * Creates the actual project, sets its nature and adds the required folders + * and files to it. This is run asynchronously in a different thread. + * + * @param monitor An existing monitor. + * @param project The project to create. + * @param description A description of the project. + * @param parameters Template parameters. + * @param dictionary String definition. + * @param isAndroidProject true if the project is to be set up as a full Android project; false + * for a plain Java project. + * @return The project newly created + * @throws StreamException + */ + private IProject createEclipseProject( + @NonNull IProgressMonitor monitor, + @NonNull IProject project, + @NonNull IProjectDescription description, + @NonNull Map<String, Object> parameters, + @Nullable Map<String, String> dictionary, + @Nullable ProjectPopulator projectPopulator, + boolean isAndroidProject) + throws CoreException, IOException, StreamException { + + // get the project target + IAndroidTarget target = (IAndroidTarget) parameters.get(PARAM_SDK_TARGET); + boolean legacy = isAndroidProject && target.getVersion().getApiLevel() < 4; + + // Create project and open it + project.create(description, new SubProgressMonitor(monitor, 10)); + if (monitor.isCanceled()) throw new OperationCanceledException(); + + project.open(IResource.BACKGROUND_REFRESH, new SubProgressMonitor(monitor, 10)); + + // Add the Java and android nature to the project + AndroidNature.setupProjectNatures(project, monitor, isAndroidProject); + + // Create folders in the project if they don't already exist + addDefaultDirectories(project, AdtConstants.WS_ROOT, DEFAULT_DIRECTORIES, monitor); + String[] sourceFolders; + if (isAndroidProject) { + sourceFolders = new String[] { + (String) parameters.get(PARAM_SRC_FOLDER), + GEN_SRC_DIRECTORY + }; + } else { + sourceFolders = new String[] { + (String) parameters.get(PARAM_SRC_FOLDER) + }; + } + addDefaultDirectories(project, AdtConstants.WS_ROOT, sourceFolders, monitor); + + // Create the resource folders in the project if they don't already exist. + if (legacy) { + addDefaultDirectories(project, RES_DIRECTORY, RES_DIRECTORIES, monitor); + } else { + addDefaultDirectories(project, RES_DIRECTORY, RES_DENSITY_ENABLED_DIRECTORIES, monitor); + } + + if (projectPopulator != null) { + try { + projectPopulator.populate(project); + } catch (InvocationTargetException ite) { + AdtPlugin.log(ite, null); + } + } + + // Setup class path: mark folders as source folders + IJavaProject javaProject = JavaCore.create(project); + setupSourceFolders(javaProject, sourceFolders, monitor); + + if (((Boolean) parameters.get(PARAM_IS_NEW_PROJECT)).booleanValue()) { + // Create files in the project if they don't already exist + addManifest(project, parameters, dictionary, monitor); + + // add the default app icon + addIcon(project, legacy, monitor); + + // Create the default package components + addSampleCode(project, sourceFolders[0], parameters, dictionary, monitor); + + // add the string definition file if needed + if (dictionary != null && dictionary.size() > 0) { + addStringDictionaryFile(project, dictionary, monitor); + } + + // add the default proguard config + File libFolder = new File((String) parameters.get(PARAM_SDK_TOOLS_DIR), + SdkConstants.FD_LIB); + addLocalFile(project, + new File(libFolder, SdkConstants.FN_PROJECT_PROGUARD_FILE), + // Write ProGuard config files with the extension .pro which + // is what is used in the ProGuard documentation and samples + SdkConstants.FN_PROJECT_PROGUARD_FILE, + monitor); + + // Set output location + javaProject.setOutputLocation(project.getFolder(BIN_CLASSES_DIRECTORY).getFullPath(), + monitor); + } + + File sampleDir = (File) parameters.get(PARAM_SAMPLE_LOCATION); + if (sampleDir != null) { + // Copy project + copySampleCode(project, sampleDir, parameters, dictionary, monitor); + } + + // Create the reference to the target project + if (parameters.containsKey(PARAM_REFERENCE_PROJECT)) { + IProject refProject = (IProject) parameters.get(PARAM_REFERENCE_PROJECT); + if (refProject != null) { + IProjectDescription desc = project.getDescription(); + + // Add out reference to the existing project reference. + // We just created a project with no references so we don't need to expand + // the currently-empty current list. + desc.setReferencedProjects(new IProject[] { refProject }); + + project.setDescription(desc, IResource.KEEP_HISTORY, + new SubProgressMonitor(monitor, 10)); + + IClasspathEntry entry = JavaCore.newProjectEntry( + refProject.getFullPath(), //path + new IAccessRule[0], //accessRules + false, //combineAccessRules + new IClasspathAttribute[0], //extraAttributes + false //isExported + + ); + ProjectHelper.addEntryToClasspath(javaProject, entry); + } + } + + if (isAndroidProject) { + Sdk.getCurrent().initProject(project, target); + } + + // Fix the project to make sure all properties are as expected. + // Necessary for existing projects and good for new ones to. + ProjectHelper.fixProject(project); + + Boolean isLibraryProject = (Boolean) parameters.get(PARAM_IS_LIBRARY); + if (isLibraryProject != null && isLibraryProject.booleanValue() + && Sdk.getCurrent() != null && project.isOpen()) { + ProjectState state = Sdk.getProjectState(project); + if (state != null) { + // make a working copy of the properties + ProjectPropertiesWorkingCopy properties = + state.getProperties().makeWorkingCopy(); + + properties.setProperty(PROPERTY_LIBRARY, Boolean.TRUE.toString()); + try { + properties.save(); + IResource projectProp = project.findMember(FN_PROJECT_PROPERTIES); + if (projectProp != null) { + projectProp.refreshLocal(DEPTH_ZERO, new NullProgressMonitor()); + } + } catch (Exception e) { + String msg = String.format( + "Failed to save %1$s for project %2$s", + SdkConstants.FN_PROJECT_PROPERTIES, project.getName()); + AdtPlugin.log(e, msg); + } + } + } + + return project; + } + + /** + * Creates a new project + * + * @param monitor An existing monitor. + * @param project The project to create. + * @param target the build target to associate with the project + * @param projectPopulator a handler for writing the template contents + * @param isLibrary whether this project should be marked as a library project + * @param projectLocation the location to write the project into + * @param workingSets Eclipse working sets, if any, to add the project to + * @throws CoreException if anything goes wrong + */ + public static void create( + @NonNull IProgressMonitor monitor, + @NonNull final IProject project, + @NonNull IAndroidTarget target, + @Nullable final ProjectPopulator projectPopulator, + boolean isLibrary, + @NonNull String projectLocation, + @NonNull final IWorkingSet[] workingSets) + throws CoreException { + final NewProjectCreator creator = new NewProjectCreator(null, null); + + final Map<String, String> dictionary = null; + final Map<String, Object> parameters = new HashMap<String, Object>(); + parameters.put(PARAM_SDK_TARGET, target); + parameters.put(PARAM_SRC_FOLDER, SdkConstants.FD_SOURCES); + parameters.put(PARAM_IS_NEW_PROJECT, false); + parameters.put(PARAM_SAMPLE_LOCATION, null); + parameters.put(PARAM_IS_LIBRARY, isLibrary); + + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + final IProjectDescription description = workspace.newProjectDescription(project.getName()); + + if (projectLocation != null) { + IPath path = new Path(projectLocation); + IPath parent = new Path(path.toFile().getParent()); + IPath workspaceLocation = Platform.getLocation(); + if (!workspaceLocation.equals(parent)) { + description.setLocation(path); + } + } + + IWorkspaceRunnable workspaceRunnable = new IWorkspaceRunnable() { + @Override + public void run(IProgressMonitor submonitor) throws CoreException { + try { + creator.createEclipseProject(submonitor, project, description, parameters, + dictionary, projectPopulator, true); + } catch (IOException e) { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Unexpected error while creating project", e)); + } catch (StreamException e) { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Unexpected error while creating project", e)); + } + if (workingSets != null && workingSets.length > 0) { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + if (javaProject != null) { + Display.getDefault().syncExec(new WorksetAdder(javaProject, + workingSets)); + } + } + } + }; + + ResourcesPlugin.getWorkspace().run(workspaceRunnable, monitor); + } + + /** + * Adds default directories to the project. + * + * @param project The Java Project to update. + * @param parentFolder The path of the parent folder. Must end with a + * separator. + * @param folders Folders to be added. + * @param monitor An existing monitor. + * @throws CoreException if the method fails to create the directories in + * the project. + */ + private void addDefaultDirectories(IProject project, String parentFolder, + String[] folders, IProgressMonitor monitor) throws CoreException { + for (String name : folders) { + if (name.length() > 0) { + IFolder folder = project.getFolder(parentFolder + name); + if (!folder.exists()) { + folder.create(true /* force */, true /* local */, + new SubProgressMonitor(monitor, 10)); + } + } + } + } + + /** + * Adds the manifest to the project. + * + * @param project The Java Project to update. + * @param parameters Template Parameters. + * @param dictionary String List to be added to a string definition + * file. This map will be filled by this method. + * @param monitor An existing monitor. + * @throws CoreException if the method fails to update the project. + * @throws IOException if the method fails to create the files in the + * project. + */ + private void addManifest(IProject project, Map<String, Object> parameters, + Map<String, String> dictionary, IProgressMonitor monitor) + throws CoreException, IOException { + + // get IFile to the manifest and check if it's not already there. + IFile file = project.getFile(SdkConstants.FN_ANDROID_MANIFEST_XML); + if (!file.exists()) { + + // Read manifest template + String manifestTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_MANIFEST); + + // Replace all keyword parameters + manifestTemplate = replaceParameters(manifestTemplate, parameters); + + if (manifestTemplate == null) { + // Inform the user there will be not manifest. + AdtPlugin.logAndPrintError(null, "Create Project" /*TAG*/, + "Failed to generate the Android manifest. Missing template %s", + TEMPLATE_MANIFEST); + // Abort now, there's no need to continue + return; + } + + if (parameters.containsKey(PARAM_ACTIVITY)) { + // now get the activity template + String activityTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_ACTIVITIES); + + // If the activity name doesn't contain any dot, it's in the form + // "ClassName" and we need to expand it to ".ClassName" in the XML. + String name = (String) parameters.get(PARAM_ACTIVITY); + if (name.indexOf('.') == -1) { + // Duplicate the parameters map to avoid changing the caller + parameters = new HashMap<String, Object>(parameters); + parameters.put(PARAM_ACTIVITY, "." + name); //$NON-NLS-1$ + } + + // Replace all keyword parameters to make main activity. + String activities = replaceParameters(activityTemplate, parameters); + + // set the intent. + String intent = AdtPlugin.readEmbeddedTextFile(TEMPLATE_INTENT_LAUNCHER); + + if (activities != null) { + if (intent != null) { + // set the intent to the main activity + activities = activities.replaceAll(PH_INTENT_FILTERS, intent); + } + + // set the activity(ies) in the manifest + manifestTemplate = manifestTemplate.replaceAll(PH_ACTIVITIES, activities); + } + } else { + // remove the activity(ies) from the manifest + manifestTemplate = manifestTemplate.replaceAll(PH_ACTIVITIES, ""); //$NON-NLS-1$ + } + + // Handle the case of the test projects + if (parameters.containsKey(PARAM_TEST_TARGET_PACKAGE)) { + // Set the uses-library needed by the test project + String usesLibrary = AdtPlugin.readEmbeddedTextFile(TEMPLATE_TEST_USES_LIBRARY); + if (usesLibrary != null) { + manifestTemplate = manifestTemplate.replaceAll( + PH_TEST_USES_LIBRARY, usesLibrary); + } + + // Set the instrumentation element needed by the test project + String instru = AdtPlugin.readEmbeddedTextFile(TEMPLATE_TEST_INSTRUMENTATION); + if (instru != null) { + manifestTemplate = manifestTemplate.replaceAll( + PH_TEST_INSTRUMENTATION, instru); + } + + // Replace PARAM_TEST_TARGET_PACKAGE itself now + manifestTemplate = replaceParameters(manifestTemplate, parameters); + + } else { + // remove the unused entries + manifestTemplate = manifestTemplate.replaceAll(PH_TEST_USES_LIBRARY, ""); //$NON-NLS-1$ + manifestTemplate = manifestTemplate.replaceAll(PH_TEST_INSTRUMENTATION, ""); //$NON-NLS-1$ + } + + String minSdkVersion = (String) parameters.get(PARAM_MIN_SDK_VERSION); + if (minSdkVersion != null && minSdkVersion.length() > 0) { + String usesSdkTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_USES_SDK); + if (usesSdkTemplate != null) { + String usesSdk = replaceParameters(usesSdkTemplate, parameters); + manifestTemplate = manifestTemplate.replaceAll(PH_USES_SDK, usesSdk); + } + } else { + manifestTemplate = manifestTemplate.replaceAll(PH_USES_SDK, ""); + } + + // Reformat the file according to the user's formatting settings + manifestTemplate = reformat(XmlFormatStyle.MANIFEST, manifestTemplate); + + // Save in the project as UTF-8 + InputStream stream = new ByteArrayInputStream( + manifestTemplate.getBytes("UTF-8")); //$NON-NLS-1$ + file.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); + } + } + + /** + * Adds the string resource file. + * + * @param project The Java Project to update. + * @param strings The list of strings to be added to the string file. + * @param monitor An existing monitor. + * @throws CoreException if the method fails to update the project. + * @throws IOException if the method fails to create the files in the + * project. + */ + private void addStringDictionaryFile(IProject project, + Map<String, String> strings, IProgressMonitor monitor) + throws CoreException, IOException { + + // create the IFile object and check if the file doesn't already exist. + IFile file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP + + VALUES_DIRECTORY + AdtConstants.WS_SEP + STRINGS_FILE); + if (!file.exists()) { + // get the Strings.xml template + String stringDefinitionTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_STRINGS); + + // get the template for one string + String stringTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_STRING); + + // get all the string names + Set<String> stringNames = strings.keySet(); + + // loop on it and create the string definitions + StringBuilder stringNodes = new StringBuilder(); + for (String key : stringNames) { + // get the value from the key + String value = strings.get(key); + + // Escape values if necessary + value = ValueXmlHelper.escapeResourceString(value); + + // place them in the template + String stringDef = stringTemplate.replace(PARAM_STRING_NAME, key); + stringDef = stringDef.replace(PARAM_STRING_CONTENT, value); + + // append to the other string + if (stringNodes.length() > 0) { + stringNodes.append('\n'); + } + stringNodes.append(stringDef); + } + + // put the string nodes in the Strings.xml template + stringDefinitionTemplate = stringDefinitionTemplate.replace(PH_STRINGS, + stringNodes.toString()); + + // reformat the file according to the user's formatting settings + stringDefinitionTemplate = reformat(XmlFormatStyle.RESOURCE, stringDefinitionTemplate); + + // write the file as UTF-8 + InputStream stream = new ByteArrayInputStream( + stringDefinitionTemplate.getBytes("UTF-8")); //$NON-NLS-1$ + file.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); + } + } + + /** Reformats the given contents with the current formatting settings */ + private String reformat(XmlFormatStyle style, String contents) { + if (AdtPrefs.getPrefs().getUseCustomXmlFormatter()) { + EclipseXmlFormatPreferences formatPrefs = EclipseXmlFormatPreferences.create(); + return EclipseXmlPrettyPrinter.prettyPrint(contents, formatPrefs, style, + null /*lineSeparator*/); + } else { + return contents; + } + } + + /** + * Adds default application icon to the project. + * + * @param project The Java Project to update. + * @param legacy whether we're running in legacy mode (no density support) + * @param monitor An existing monitor. + * @throws CoreException if the method fails to update the project. + */ + private void addIcon(IProject project, boolean legacy, IProgressMonitor monitor) + throws CoreException { + if (legacy) { // density support + // do medium density icon only, in the default drawable folder. + IFile file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP + + DRAWABLE_DIRECTORY + AdtConstants.WS_SEP + PROJECT_ICON); + if (!file.exists()) { + addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_MDPI), monitor); + } + } else { + // do all 4 icons. + IFile file; + + // extra high density + file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP + + DRAWABLE_XHDPI_DIRECTORY + AdtConstants.WS_SEP + PROJECT_ICON); + if (!file.exists()) { + addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_XHDPI), monitor); + } + + // high density + file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP + + DRAWABLE_HDPI_DIRECTORY + AdtConstants.WS_SEP + PROJECT_ICON); + if (!file.exists()) { + addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_HDPI), monitor); + } + + // medium density + file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP + + DRAWABLE_MDPI_DIRECTORY + AdtConstants.WS_SEP + PROJECT_ICON); + if (!file.exists()) { + addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_MDPI), monitor); + } + + // low density + file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP + + DRAWABLE_LDPI_DIRECTORY + AdtConstants.WS_SEP + PROJECT_ICON); + if (!file.exists()) { + addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_LDPI), monitor); + } + } + } + + /** + * Creates a file from a data source. + * @param dest the file to write + * @param source the content of the file. + * @param monitor the progress monitor + * @throws CoreException + */ + private void addFile(IFile dest, byte[] source, IProgressMonitor monitor) throws CoreException { + if (source != null) { + // Save in the project + InputStream stream = new ByteArrayInputStream(source); + dest.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); + } + } + + /** + * Creates the package folder and copies the sample code in the project. + * + * @param project The Java Project to update. + * @param parameters Template Parameters. + * @param dictionary String List to be added to a string definition + * file. This map will be filled by this method. + * @param monitor An existing monitor. + * @throws CoreException if the method fails to update the project. + * @throws IOException if the method fails to create the files in the + * project. + */ + private void addSampleCode(IProject project, String sourceFolder, + Map<String, Object> parameters, Map<String, String> dictionary, + IProgressMonitor monitor) throws CoreException, IOException { + // create the java package directories. + IFolder pkgFolder = project.getFolder(sourceFolder); + String packageName = (String) parameters.get(PARAM_PACKAGE); + + // The PARAM_ACTIVITY key will be absent if no activity should be created, + // in which case activityName will be null. + String activityName = (String) parameters.get(PARAM_ACTIVITY); + + Map<String, Object> java_activity_parameters = new HashMap<String, Object>(parameters); + java_activity_parameters.put(PARAM_IMPORT_RESOURCE_CLASS, ""); //$NON-NLS-1$ + + if (activityName != null) { + + String resourcePackageClass = null; + + // An activity name can be of the form ".package.Class", ".Class" or FQDN. + // The initial dot is ignored, as it is always added later in the templates. + int lastDotIndex = activityName.lastIndexOf('.'); + + if (lastDotIndex != -1) { + + // Resource class + if (lastDotIndex > 0) { + resourcePackageClass = packageName + '.' + SdkConstants.FN_RESOURCE_BASE; + } + + // Package name + if (activityName.startsWith(".")) { //$NON-NLS-1$ + packageName += activityName.substring(0, lastDotIndex); + } else { + packageName = activityName.substring(0, lastDotIndex); + } + + // Activity Class name + activityName = activityName.substring(lastDotIndex + 1); + } + + java_activity_parameters.put(PARAM_ACTIVITY, activityName); + java_activity_parameters.put(PARAM_PACKAGE, packageName); + if (resourcePackageClass != null) { + String importResourceClass = "\nimport " + resourcePackageClass + ";"; //$NON-NLS-1$ // $NON-NLS-2$ + java_activity_parameters.put(PARAM_IMPORT_RESOURCE_CLASS, importResourceClass); + } + } + + String[] components = packageName.split(AdtConstants.RE_DOT); + for (String component : components) { + pkgFolder = pkgFolder.getFolder(component); + if (!pkgFolder.exists()) { + pkgFolder.create(true /* force */, true /* local */, + new SubProgressMonitor(monitor, 10)); + } + } + + if (activityName != null) { + // create the main activity Java file + String activityJava = activityName + SdkConstants.DOT_JAVA; + IFile file = pkgFolder.getFile(activityJava); + if (!file.exists()) { + copyFile(JAVA_ACTIVITY_TEMPLATE, file, java_activity_parameters, monitor, false); + } + + // create the layout file (if we're creating an + IFolder layoutfolder = project.getFolder(RES_DIRECTORY).getFolder(LAYOUT_DIRECTORY); + file = layoutfolder.getFile(MAIN_LAYOUT_XML); + if (!file.exists()) { + copyFile(LAYOUT_TEMPLATE, file, parameters, monitor, true); + dictionary.put(STRING_HELLO_WORLD, String.format("Hello World, %1$s!", + activityName)); + } + } + } + + private void copySampleCode(IProject project, File sampleDir, + Map<String, Object> parameters, Map<String, String> dictionary, + IProgressMonitor monitor) throws CoreException { + // Copy the sampleDir into the project directory recursively + IFileSystem fileSystem = EFS.getLocalFileSystem(); + IFileStore sourceDir = new ReadWriteFileStore( + fileSystem.getStore(sampleDir.toURI())); + IFileStore destDir = new ReadWriteFileStore( + fileSystem.getStore(AdtUtils.getAbsolutePath(project))); + sourceDir.copy(destDir, EFS.OVERWRITE, null); + } + + /** + * In a sample we never duplicate source files as read-only. + * This creates a store that read files attributes and doesn't set the r-o flag. + */ + private static class ReadWriteFileStore extends FileStoreAdapter { + + public ReadWriteFileStore(IFileStore store) { + super(store); + } + + // Override when reading attributes + @Override + public IFileInfo fetchInfo(int options, IProgressMonitor monitor) throws CoreException { + IFileInfo info = super.fetchInfo(options, monitor); + info.setAttribute(EFS.ATTRIBUTE_READ_ONLY, false); + return info; + } + + // Override when writing attributes + @Override + public void putInfo(IFileInfo info, int options, IProgressMonitor storeMonitor) + throws CoreException { + info.setAttribute(EFS.ATTRIBUTE_READ_ONLY, false); + super.putInfo(info, options, storeMonitor); + } + + @Deprecated + @Override + public IFileStore getChild(IPath path) { + IFileStore child = super.getChild(path); + if (!(child instanceof ReadWriteFileStore)) { + child = new ReadWriteFileStore(child); + } + return child; + } + + @Override + public IFileStore getChild(String name) { + return new ReadWriteFileStore(super.getChild(name)); + } + } + + /** + * Adds a file to the root of the project + * @param project the project to add the file to. + * @param destName the name to write the file as + * @param source the file to add. It'll keep the same filename once copied into the project. + * @param monitor the monitor to report progress to + * @throws FileNotFoundException if the file to be added does not exist + * @throws CoreException if writing the file does not work + */ + public static void addLocalFile(IProject project, File source, String destName, + IProgressMonitor monitor) throws FileNotFoundException, CoreException { + IFile dest = project.getFile(destName); + if (dest.exists() == false) { + FileInputStream stream = new FileInputStream(source); + dest.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); + } + } + + /** + * Adds the given folder to the project's class path. + * + * @param javaProject The Java Project to update. + * @param sourceFolders Template Parameters. + * @param monitor An existing monitor. + * @throws JavaModelException if the classpath could not be set. + */ + private void setupSourceFolders(IJavaProject javaProject, String[] sourceFolders, + IProgressMonitor monitor) throws JavaModelException { + IProject project = javaProject.getProject(); + + // get the list of entries. + IClasspathEntry[] entries = javaProject.getRawClasspath(); + + // remove the project as a source folder (This is the default) + entries = removeSourceClasspath(entries, project); + + // add the source folders. + for (String sourceFolder : sourceFolders) { + IFolder srcFolder = project.getFolder(sourceFolder); + + // remove it first in case. + entries = removeSourceClasspath(entries, srcFolder); + entries = ProjectHelper.addEntryToClasspath(entries, + JavaCore.newSourceEntry(srcFolder.getFullPath())); + } + + javaProject.setRawClasspath(entries, new SubProgressMonitor(monitor, 10)); + } + + + /** + * Removes the corresponding source folder from the class path entries if + * found. + * + * @param entries The class path entries to read. A copy will be returned. + * @param folder The parent source folder to remove. + * @return A new class path entries array. + */ + private IClasspathEntry[] removeSourceClasspath(IClasspathEntry[] entries, IContainer folder) { + if (folder == null) { + return entries; + } + IClasspathEntry source = JavaCore.newSourceEntry(folder.getFullPath()); + int n = entries.length; + for (int i = n - 1; i >= 0; i--) { + if (entries[i].equals(source)) { + IClasspathEntry[] newEntries = new IClasspathEntry[n - 1]; + if (i > 0) System.arraycopy(entries, 0, newEntries, 0, i); + if (i < n - 1) System.arraycopy(entries, i + 1, newEntries, i, n - i - 1); + n--; + entries = newEntries; + } + } + return entries; + } + + + /** + * Copies the given file from our resource folder to the new project. + * Expects the file to the US-ASCII or UTF-8 encoded. + * + * @throws CoreException from IFile if failing to create the new file. + * @throws MalformedURLException from URL if failing to interpret the URL. + * @throws FileNotFoundException from RandomAccessFile. + * @throws IOException from RandomAccessFile.length() if can't determine the + * length. + */ + private void copyFile(String resourceFilename, IFile destFile, + Map<String, Object> parameters, IProgressMonitor monitor, boolean reformat) + throws CoreException, IOException { + + // Read existing file. + String template = AdtPlugin.readEmbeddedTextFile( + TEMPLATES_DIRECTORY + resourceFilename); + + // Replace all keyword parameters + template = replaceParameters(template, parameters); + + if (reformat) { + // Guess the formatting style based on the file location + XmlFormatStyle style = EclipseXmlPrettyPrinter + .getForFile(destFile.getProjectRelativePath()); + if (style != null) { + template = reformat(style, template); + } + } + + // Save in the project as UTF-8 + InputStream stream = new ByteArrayInputStream(template.getBytes("UTF-8")); //$NON-NLS-1$ + destFile.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); + } + + /** + * Replaces placeholders found in a string with values. + * + * @param str the string to search for placeholders. + * @param parameters a map of <placeholder, Value> to search for in the string + * @return A new String object with the placeholder replaced by the values. + */ + private String replaceParameters(String str, Map<String, Object> parameters) { + + if (parameters == null) { + AdtPlugin.log(IStatus.ERROR, + "NPW replace parameters: null parameter map. String: '%s'", str); //$NON-NLS-1$ + return str; + } else if (str == null) { + AdtPlugin.log(IStatus.ERROR, + "NPW replace parameters: null template string"); //$NON-NLS-1$ + return str; + } + + for (Entry<String, Object> entry : parameters.entrySet()) { + if (entry != null && entry.getValue() instanceof String) { + Object value = entry.getValue(); + if (value == null) { + AdtPlugin.log(IStatus.ERROR, + "NPW replace parameters: null value for key '%s' in template '%s'", //$NON-NLS-1$ + entry.getKey(), + str); + } else { + str = str.replaceAll(entry.getKey(), (String) value); + } + } + } + + return str; + } + + private static class WorksetAdder implements Runnable { + private final IJavaProject mProject; + private final IWorkingSet[] mWorkingSets; + + private WorksetAdder(IJavaProject project, IWorkingSet[] workingSets) { + mProject = project; + mWorkingSets = workingSets; + } + + @Override + public void run() { + if (mWorkingSets.length > 0 && mProject != null + && mProject.exists()) { + PlatformUI.getWorkbench().getWorkingSetManager() + .addToWorkingSets(mProject, mWorkingSets); + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java new file mode 100644 index 000000000..ff03b338f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE; +import static com.android.SdkConstants.OS_SDK_TOOLS_LIB_FOLDER; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; + +import org.eclipse.jdt.ui.actions.OpenJavaPerspectiveAction; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.IWizardPage; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.ui.INewWizard; +import org.eclipse.ui.IWorkbench; + +import java.io.File; + + +/** + * A "New Android Project" Wizard. + * <p/> + * Note: this class is public so that it can be accessed from unit tests. + * It is however an internal class. Its API may change without notice. + * It should semantically be considered as a private final class. + * <p/> + * Do not derive from this class. + */ +public class NewProjectWizard extends Wizard implements INewWizard { + private static final String PROJECT_LOGO_LARGE = "icons/android-64.png"; //$NON-NLS-1$ + + private NewProjectWizardState mValues; + private ProjectNamePage mNamePage; + private SdkSelectionPage mSdkPage; + private SampleSelectionPage mSamplePage; + private ApplicationInfoPage mPropertiesPage; + private final Mode mMode; + private IStructuredSelection mSelection; + + /** Constructs a new wizard default project wizard */ + public NewProjectWizard() { + this(Mode.ANY); + } + + protected NewProjectWizard(Mode mode) { + mMode = mode; + switch (mMode) { + case SAMPLE: + setWindowTitle("New Android Sample Project"); + break; + case TEST: + setWindowTitle("New Android Test Project"); + break; + default: + setWindowTitle("New Android Project"); + break; + } + } + + @Override + public void addPages() { + mValues = new NewProjectWizardState(mMode); + + if (mMode != Mode.SAMPLE) { + mNamePage = new ProjectNamePage(mValues); + + if (mSelection != null) { + mNamePage.init(mSelection, AdtUtils.getActivePart()); + } + + addPage(mNamePage); + } + + if (mMode == Mode.TEST) { + addPage(new TestTargetPage(mValues)); + } + + mSdkPage = new SdkSelectionPage(mValues); + addPage(mSdkPage); + + if (mMode != Mode.TEST) { + // Sample projects can be created when entering the new/existing wizard, or + // the sample wizard + mSamplePage = new SampleSelectionPage(mValues); + addPage(mSamplePage); + } + + if (mMode != Mode.SAMPLE) { + // Project properties are entered in all project types except sample projects + mPropertiesPage = new ApplicationInfoPage(mValues); + addPage(mPropertiesPage); + } + } + + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) { + mSelection = selection; + + setHelpAvailable(false); // TODO have help + ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE); + setDefaultPageImageDescriptor(desc); + + // Trigger a check to see if the SDK needs to be reloaded (which will + // invoke onSdkLoaded asynchronously as needed). + AdtPlugin.getDefault().refreshSdk(); + } + + @Override + public boolean performFinish() { + File file = new File(AdtPlugin.getOsSdkFolder(), OS_SDK_TOOLS_LIB_FOLDER + File.separator + + FN_PROJECT_PROGUARD_FILE); + if (!file.exists()) { + AdtPlugin.displayError("Tools Out of Date?", + String.format("It looks like you do not have the latest version of the " + + "SDK Tools installed. Make sure you update via the SDK Manager " + + "first. (Could not find %1$s)", file.getPath())); + return false; + } + + NewProjectCreator creator = new NewProjectCreator(mValues, getContainer()); + if (!(creator.createAndroidProjects())) { + return false; + } + + // Open the default Java Perspective + OpenJavaPerspectiveAction action = new OpenJavaPerspectiveAction(); + action.run(); + return true; + } + + @Override + public IWizardPage getNextPage(IWizardPage page) { + if (page == mNamePage) { + // Skip the test target selection page unless creating a test project + if (mValues.mode != Mode.TEST) { + return mSdkPage; + } + } else if (page == mSdkPage) { + if (mValues.mode == Mode.SAMPLE) { + return mSamplePage; + } else if (mValues.mode != Mode.TEST) { + return mPropertiesPage; + } else { + // Done with wizard when creating from existing or creating test projects + return null; + } + } else if (page == mSamplePage) { + // Nothing more to be entered for samples + return null; + } + + return super.getNextPage(page); + } + + /** + * Returns the package name currently set by the wizard + * + * @return the current package name, or null + */ + public String getPackageName() { + return mValues.packageName; + } + + // TBD: Call setDialogSettings etc to store persistent state between wizard invocations. +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizardState.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizardState.java new file mode 100644 index 000000000..06c0300b7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizardState.java @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import com.android.SdkConstants; +import com.android.annotations.Nullable; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.common.xml.ManifestData.Activity; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.internal.project.ProjectProperties.PropertyType; +import com.android.utils.Pair; +import com.android.xml.AndroidManifest; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.ui.IWorkingSet; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * The {@link NewProjectWizardState} holds the state used by the various pages + * in the {@link NewProjectWizard} and its variations, and it can also be used + * to pass project information to the {@link NewProjectCreator}. + */ +public class NewProjectWizardState { + /** The mode to run the wizard in: creating test, or sample, or plain project */ + public Mode mode; + + /** + * If true, the project should be created from an existing codebase (pointed + * to by the {@link #projectLocation} or in the case of sample projects, the + * {@link #chosenSample}. Otherwise, create a brand new project from scratch. + */ + public boolean useExisting; + + /** + * Whether new projects should be created into the default project location + * (e.g. in the Eclipse workspace) or not + */ + public boolean useDefaultLocation = true; + + /** The build target SDK */ + public IAndroidTarget target; + /** True if the user has manually modified the target */ + public boolean targetModifiedByUser; + + /** The location to store projects into */ + public File projectLocation = new File(Platform.getLocation().toOSString()); + /** True if the project location name has been manually edited by the user */ + public boolean projectLocationModifiedByUser; + + /** The name of the project */ + public String projectName = ""; //$NON-NLS-1$ + /** True if the project name has been manually edited by the user */ + public boolean projectNameModifiedByUser; + + /** The application name */ + public String applicationName; + /** True if the application name has been manually edited by the user */ + public boolean applicationNameModifiedByUser; + + /** The package path */ + public String packageName; + /** True if the package name has been manually edited by the user */ + public boolean packageNameModifiedByUser; + + /** True if a new activity should be created */ + public boolean createActivity; + + /** The name of the new activity to be created */ + public String activityName; + /** True if the activity name has been manually edited by the user */ + public boolean activityNameModifiedByUser; + + /** The minimum SDK version to use with the project (may be null or blank) */ + public String minSdk; + /** True if the minimum SDK version has been manually edited by the user */ + public boolean minSdkModifiedByUser; + /** + * A list of paths to each of the available samples for the current SDK. + * The pair is (String: sample display name => File: sample directory). + * Note we want a list, not a map since we might have duplicates. + * */ + public List<Pair<String, File>> samples = new ArrayList<Pair<String, File>>(); + /** Path to the currently chosen sample */ + public File chosenSample; + + /** The name of the source folder, relative to the project root */ + public String sourceFolder = SdkConstants.FD_SOURCES; + /** The set of chosen working sets to use when creating the project */ + public IWorkingSet[] workingSets = new IWorkingSet[0]; + + /** + * A reference to a different project that the current test project will be + * testing. + */ + public IProject testedProject; + /** + * If true, this test project should be testing itself, otherwise it will be + * testing the project pointed to by {@link #testedProject}. + */ + public boolean testingSelf; + + // NOTE: These apply only to creating paired projects; when isTest is true + // we're using + // the normal fields above + /** + * If true, create a test project along with this plain project which will + * be testing the plain project. (This flag only applies when creating + * normal projects.) + */ + public boolean createPairProject; + /** + * The application name of the test application (only applies when + * {@link #createPairProject} is true) + */ + public String testApplicationName; + /** + * True if the testing application name has been modified by the user (only + * applies when {@link #createPairProject} is true) + */ + public boolean testApplicationNameModified; + /** + * The package name of the test application (only applies when + * {@link #createPairProject} is true) + */ + public String testPackageName; + /** + * True if the testing package name has been modified by the user (only + * applies when {@link #createPairProject} is true) + */ + public boolean testPackageModified; + /** + * The project name of the test project (only applies when + * {@link #createPairProject} is true) + */ + public String testProjectName; + /** + * True if the testing project name has been modified by the user (only + * applies when {@link #createPairProject} is true) + */ + public boolean testProjectModified; + /** Package name of the tested app */ + public String testTargetPackageName; + + /** + * Copy project into workspace? This flag only applies when importing + * projects (creating projects from existing source) + */ + public boolean copyIntoWorkspace; + + /** + * List of projects to be imported. Null if not importing projects. + */ + @Nullable + public List<ImportedProject> importProjects; + + /** + * Creates a new {@link NewProjectWizardState} + * + * @param mode the mode to run the wizard in + */ + public NewProjectWizardState(Mode mode) { + this.mode = mode; + if (mode == Mode.SAMPLE) { + useExisting = true; + } else if (mode == Mode.TEST) { + createActivity = false; + } + } + + /** + * Extract information (package name, application name, minimum SDK etc) from + * the given Android project. + * + * @param path the path to the project to extract information from + */ + public void extractFromAndroidManifest(Path path) { + String osPath = path.append(SdkConstants.FN_ANDROID_MANIFEST_XML).toOSString(); + if (!(new File(osPath).exists())) { + return; + } + + ManifestData manifestData = AndroidManifestHelper.parseForData(osPath); + if (manifestData == null) { + return; + } + + String newPackageName = null; + Activity activity = null; + String newActivityName = null; + String minSdkVersion = null; + try { + newPackageName = manifestData.getPackage(); + minSdkVersion = manifestData.getMinSdkVersionString(); + + // try to get the first launcher activity. If none, just take the first activity. + activity = manifestData.getLauncherActivity(); + if (activity == null) { + Activity[] activities = manifestData.getActivities(); + if (activities != null && activities.length > 0) { + activity = activities[0]; + } + } + } catch (Exception e) { + // ignore exceptions + } + + if (newPackageName != null && newPackageName.length() > 0) { + packageName = newPackageName; + } + + if (activity != null) { + newActivityName = AndroidManifest.extractActivityName(activity.getName(), + newPackageName); + } + + if (newActivityName != null && newActivityName.length() > 0) { + activityName = newActivityName; + // we are "importing" an existing activity, not creating a new one + createActivity = false; + + // If project name and application names are empty, use the activity + // name as a default. If the activity name has dots, it's a part of a + // package specification and only the last identifier must be used. + if (newActivityName.indexOf('.') != -1) { + String[] ids = newActivityName.split(AdtConstants.RE_DOT); + newActivityName = ids[ids.length - 1]; + } + if (projectName == null || projectName.length() == 0 || + !projectNameModifiedByUser) { + projectName = newActivityName; + projectNameModifiedByUser = false; + } + if (applicationName == null || applicationName.length() == 0 || + !applicationNameModifiedByUser) { + applicationNameModifiedByUser = false; + applicationName = newActivityName; + } + } else { + activityName = ""; //$NON-NLS-1$ + + // There is no activity name to use to fill in the project and application + // name. However if there's a package name, we can use this as a base. + if (newPackageName != null && newPackageName.length() > 0) { + // Package name is a java identifier, so it's most suitable for + // an application name. + + if (applicationName == null || applicationName.length() == 0 || + !applicationNameModifiedByUser) { + applicationName = newPackageName; + } + + // For the project name, remove any dots + newPackageName = newPackageName.replace('.', '_'); + if (projectName == null || projectName.length() == 0 || + !projectNameModifiedByUser) { + projectName = newPackageName; + } + + } + } + + if (mode == Mode.ANY && useExisting) { + updateSdkTargetToMatchProject(path.toFile()); + } + + minSdk = minSdkVersion; + minSdkModifiedByUser = false; + } + + /** + * Try to find an SDK Target that matches the current MinSdkVersion. + * + * There can be multiple targets with the same sdk api version, so don't change + * it if it's already at the right version. Otherwise pick the first target + * that matches. + */ + public void updateSdkTargetToMatchMinSdkVersion() { + IAndroidTarget currentTarget = target; + if (currentTarget != null && currentTarget.getVersion().equals(minSdk)) { + return; + } + + Sdk sdk = Sdk.getCurrent(); + if (sdk != null) { + IAndroidTarget[] targets = sdk.getTargets(); + for (IAndroidTarget t : targets) { + if (t.getVersion().equals(minSdk)) { + target = t; + return; + } + } + } + } + + /** + * Updates the SDK to reflect the SDK required by the project at the given + * location + * + * @param location the location of the project + */ + public void updateSdkTargetToMatchProject(File location) { + // Select the target matching the manifest's sdk or build properties, if any + IAndroidTarget foundTarget = null; + // This is the target currently in the UI + IAndroidTarget currentTarget = target; + String projectPath = location.getPath(); + + // If there's a current target defined, we do not allow to change it when + // operating in the create-from-sample mode -- since the available sample list + // is tied to the current target, so changing it would invalidate the project we're + // trying to load in the first place. + if (!targetModifiedByUser) { + ProjectProperties p = ProjectProperties.load(projectPath, + PropertyType.PROJECT); + if (p != null) { + String v = p.getProperty(ProjectProperties.PROPERTY_TARGET); + IAndroidTarget desiredTarget = Sdk.getCurrent().getTargetFromHashString(v); + // We can change the current target if: + // - we found a new desired target + // - there is no current target + // - or the current target can't run the desired target + if (desiredTarget != null && + (currentTarget == null || !desiredTarget.canRunOn(currentTarget))) { + foundTarget = desiredTarget; + } + } + + Sdk sdk = Sdk.getCurrent(); + IAndroidTarget[] targets = null; + if (sdk != null) { + targets = sdk.getTargets(); + } + if (targets == null) { + targets = new IAndroidTarget[0]; + } + + if (foundTarget == null && minSdk != null) { + // Otherwise try to match the requested min-sdk-version if we find an + // exact match, regardless of the currently selected target. + for (IAndroidTarget existingTarget : targets) { + if (existingTarget != null && + existingTarget.getVersion().equals(minSdk)) { + foundTarget = existingTarget; + break; + } + } + } + + if (foundTarget == null) { + // Or last attempt, try to match a sample project location and use it + // if we find an exact match, regardless of the currently selected target. + for (IAndroidTarget existingTarget : targets) { + if (existingTarget != null && + projectPath.startsWith(existingTarget.getLocation())) { + foundTarget = existingTarget; + break; + } + } + } + } + + if (foundTarget != null) { + target = foundTarget; + } + } + + /** + * Type of project being offered/created by the wizard + */ + public enum Mode { + /** Create a sample project. Testing options are not presented. */ + SAMPLE, + + /** + * Create a test project, either testing itself or some other project. + * Note that even if in the {@link #ANY} mode, a test project can be + * created as a *paired* project with the main project, so this flag + * only means that we are creating *just* a test project + */ + TEST, + + /** + * Create an Android project, which can be a plain project, optionally + * with a paired test project, or a sample project (the first page + * contains toggles for choosing which + */ + ANY; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewSampleProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewSampleProjectWizard.java new file mode 100644 index 000000000..6b6a4c29e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewSampleProjectWizard.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; + +/** + * A "New Sample Android Project" Wizard. + * <p/> + * This displays the new project wizard pre-configured for samples only. + */ +public class NewSampleProjectWizard extends NewProjectWizard { + /** + * Creates a new wizard for creating a sample Android project + */ + public NewSampleProjectWizard() { + super(Mode.SAMPLE); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewTestProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewTestProjectWizard.java new file mode 100644 index 000000000..e0959f4db --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewTestProjectWizard.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; + +/** + * A "New Test Android Project" Wizard. + * <p/> + * This is really the {@link NewProjectWizard} that only displays the "test project" pages. + */ +public class NewTestProjectWizard extends NewProjectWizard { + /** + * Creates a new wizard for creating an Android Test Project + */ + public NewTestProjectWizard() { + super(Mode.TEST); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ProjectNamePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ProjectNamePage.java new file mode 100644 index 000000000..d04ea897f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ProjectNamePage.java @@ -0,0 +1,606 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE; +import static com.android.SdkConstants.OS_SDK_TOOLS_LIB_FOLDER; +import static com.android.ide.eclipse.adt.AdtUtils.capitalize; +import static com.android.ide.eclipse.adt.internal.wizards.newproject.ApplicationInfoPage.ACTIVITY_NAME_SUFFIX; +import static com.android.utils.SdkUtils.stripWhitespace; + +import com.android.SdkConstants; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.common.xml.ManifestData.Activity; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.VersionCheck; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; + +import org.eclipse.core.filesystem.URIUtil; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.IWizardPage; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.osgi.util.TextProcessor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionEvent; +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.Composite; +import org.eclipse.swt.widgets.DirectoryDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkingSet; + +import java.io.File; +import java.net.URI; +import java.util.Locale; + +/** + * Initial page shown when creating projects which asks for the project name, + * the the location of the project, working sets, etc. + */ +public class ProjectNamePage extends WizardPage implements SelectionListener, ModifyListener { + private final NewProjectWizardState mValues; + /** Flag used when setting button/text state manually to ignore listener updates */ + private boolean mIgnore; + /** Last user-browsed location, static so that it be remembered for the whole session */ + private static String sCustomLocationOsPath = ""; //$NON-NLS-1$ + private static boolean sAutoComputeCustomLocation = true; + + private Text mProjectNameText; + private Text mLocationText; + private Button mCreateSampleRadioButton; + private Button mCreateNewButton; + private Button mUseDefaultCheckBox; + private Button mBrowseButton; + private Label mLocationLabel; + private WorkingSetGroup mWorkingSetGroup; + /** + * Whether we've made sure the Tools are up to date (enough that all the + * resources required by the New Project wizard are present -- we don't + * necessarily check for newer versions than that here; that's done by + * {@link VersionCheck}, though that check doesn't <b>enforce</b> an update + * since it needs to allow the user to proceed to access the SDK manager + * etc.) + */ + private boolean mCheckedSdkUptodate; + + /** + * Create the wizard. + * @param values current wizard state + */ + ProjectNamePage(NewProjectWizardState values) { + super("projectNamePage"); //$NON-NLS-1$ + mValues = values; + + setTitle("Create Android Project"); + setDescription("Select project name and type of project"); + mWorkingSetGroup = new WorkingSetGroup(); + setWorkingSets(new IWorkingSet[0]); + } + + void init(IStructuredSelection selection, IWorkbenchPart activePart) { + setWorkingSets(WorkingSetHelper.getSelectedWorkingSet(selection, activePart)); + } + + /** + * Create contents of the wizard. + * @param parent the parent to add the page to + */ + @Override + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NULL); + container.setLayout(new GridLayout(3, false)); + + Label nameLabel = new Label(container, SWT.NONE); + nameLabel.setText("Project Name:"); + + mProjectNameText = new Text(container, SWT.BORDER); + mProjectNameText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); + mProjectNameText.addModifyListener(this); + + if (mValues.mode != Mode.TEST) { + mCreateNewButton = new Button(container, SWT.RADIO); + mCreateNewButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1)); + mCreateNewButton.setText("Create new project in workspace"); + mCreateNewButton.addSelectionListener(this); + + // TBD: Should we hide this completely, and make samples something you only invoke + // from the "New Sample Project" wizard? + mCreateSampleRadioButton = new Button(container, SWT.RADIO); + mCreateSampleRadioButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, + 3, 1)); + mCreateSampleRadioButton.setText("Create project from existing sample"); + mCreateSampleRadioButton.addSelectionListener(this); + } + + Label separator = new Label(container, SWT.SEPARATOR | SWT.HORIZONTAL); + separator.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 3, 1)); + + mUseDefaultCheckBox = new Button(container, SWT.CHECK); + mUseDefaultCheckBox.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1)); + mUseDefaultCheckBox.setText("Use default location"); + mUseDefaultCheckBox.addSelectionListener(this); + + mLocationLabel = new Label(container, SWT.NONE); + mLocationLabel.setText("Location:"); + + mLocationText = new Text(container, SWT.BORDER); + mLocationText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mLocationText.addModifyListener(this); + + mBrowseButton = new Button(container, SWT.NONE); + mBrowseButton.setText("Browse..."); + mBrowseButton.addSelectionListener(this); + + Composite group = mWorkingSetGroup.createControl(container); + group.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false, 3, 1)); + + setControl(container); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + + if (visible) { + try { + mIgnore = true; + if (mValues.projectName != null) { + mProjectNameText.setText(mValues.projectName); + mProjectNameText.setFocus(); + } + if (mValues.mode == Mode.ANY || mValues.mode == Mode.TEST) { + if (mValues.useExisting) { + assert false; // This is now handled by the separate import wizard + } else if (mCreateNewButton != null) { + mCreateNewButton.setSelection(true); + } + } else if (mValues.mode == Mode.SAMPLE) { + mCreateSampleRadioButton.setSelection(true); + } + if (mValues.projectLocation != null) { + mLocationText.setText(mValues.projectLocation.getPath()); + } + mUseDefaultCheckBox.setSelection(mValues.useDefaultLocation); + updateLocationState(); + } finally { + mIgnore = false; + } + } + + validatePage(); + } + + @Override + public void modifyText(ModifyEvent e) { + if (mIgnore) { + return; + } + + Object source = e.getSource(); + + if (source == mProjectNameText) { + onProjectFieldModified(); + if (!mValues.useDefaultLocation && !mValues.projectLocationModifiedByUser) { + updateLocationPathField(null); + } + } else if (source == mLocationText) { + mValues.projectLocationModifiedByUser = true; + if (!mValues.useDefaultLocation) { + File f = new File(mLocationText.getText().trim()); + mValues.projectLocation = f; + if (f.exists() && f.isDirectory() && !f.equals(mValues.projectLocation)) { + updateLocationPathField(mValues.projectLocation.getPath()); + } + } + } + + validatePage(); + } + + private void onProjectFieldModified() { + mValues.projectName = mProjectNameText.getText().trim(); + mValues.projectNameModifiedByUser = true; + + if (!mValues.applicationNameModifiedByUser) { + mValues.applicationName = capitalize(mValues.projectName); + if (!mValues.testApplicationNameModified) { + mValues.testApplicationName = + ApplicationInfoPage.suggestTestApplicationName(mValues.applicationName); + } + } + if (!mValues.activityNameModifiedByUser) { + String name = capitalize(mValues.projectName); + mValues.activityName = stripWhitespace(name) + ACTIVITY_NAME_SUFFIX; + } + if (!mValues.testProjectModified) { + mValues.testProjectName = + ApplicationInfoPage.suggestTestProjectName(mValues.projectName); + } + if (!mValues.projectLocationModifiedByUser) { + updateLocationPathField(null); + } + } + + @Override + public void widgetSelected(SelectionEvent e) { + if (mIgnore) { + return; + } + + Object source = e.getSource(); + + if (source == mCreateNewButton && mCreateNewButton != null + && mCreateNewButton.getSelection()) { + mValues.useExisting = false; + if (mValues.mode == Mode.SAMPLE) { + // Only reset the mode if we're toggling from sample back to create new + // or create existing. We can only come to the sample state when we're in + // ANY mode. (In particular, we don't want to switch to ANY if you're + // in test mode. + mValues.mode = Mode.ANY; + } + updateLocationState(); + } else if (source == mCreateSampleRadioButton && mCreateSampleRadioButton.getSelection()) { + mValues.useExisting = true; + mValues.useDefaultLocation = true; + if (!mUseDefaultCheckBox.getSelection()) { + try { + mIgnore = true; + mUseDefaultCheckBox.setSelection(true); + } finally { + mIgnore = false; + } + } + mValues.mode = Mode.SAMPLE; + updateLocationState(); + } else if (source == mUseDefaultCheckBox) { + mValues.useDefaultLocation = mUseDefaultCheckBox.getSelection(); + updateLocationState(); + } else if (source == mBrowseButton) { + onOpenDirectoryBrowser(); + } + + validatePage(); + } + + /** + * 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) + * or in new project mode with the "use default location" turned off. + */ + private void updateLocationState() { + boolean isNewProject = !mValues.useExisting; + boolean isCreateFromSample = mValues.mode == Mode.SAMPLE; + boolean useDefault = mValues.useDefaultLocation && !isCreateFromSample; + boolean locationEnabled = (!isNewProject || !useDefault) && !isCreateFromSample; + + mUseDefaultCheckBox.setEnabled(isNewProject); + mLocationLabel.setEnabled(locationEnabled); + mLocationText.setEnabled(locationEnabled); + mBrowseButton.setEnabled(locationEnabled); + + updateLocationPathField(null); + } + + /** + * Display a directory browser and update the location path field with the selected path + */ + private void onOpenDirectoryBrowser() { + + String existingDir = mLocationText.getText().trim(); + + // Disable the path if it doesn't exist + if (existingDir.length() == 0) { + existingDir = null; + } else { + File f = new File(existingDir); + if (!f.exists()) { + existingDir = null; + } + } + + DirectoryDialog directoryDialog = new DirectoryDialog(mLocationText.getShell()); + directoryDialog.setMessage("Browse for folder"); + directoryDialog.setFilterPath(existingDir); + String dir = directoryDialog.open(); + + if (dir != null) { + updateLocationPathField(dir); + validatePage(); + } + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + + /** + * Returns the working sets to which the new project should be added. + * + * @return the selected working sets to which the new project should be added + */ + private IWorkingSet[] getWorkingSets() { + return mWorkingSetGroup.getSelectedWorkingSets(); + } + + /** + * Sets the working sets to which the new project should be added. + * + * @param workingSets the initial selected working sets + */ + private void setWorkingSets(IWorkingSet[] workingSets) { + assert workingSets != null; + mWorkingSetGroup.setWorkingSets(workingSets); + } + + /** + * Updates the location directory path field. + * <br/> + * When custom user selection is enabled, use the absDir argument if not null and also + * save it internally. If absDir is null, restore the last saved absDir. This allows the + * user selection to be remembered when the user switches from default to custom. + * <br/> + * When custom user selection is disabled, use the workspace default location with the + * current project name. This does not change the internally cached absDir. + * + * @param absDir A new absolute directory path or null to use the default. + */ + private void updateLocationPathField(String absDir) { + boolean isNewProject = !mValues.useExisting || mValues.mode == Mode.SAMPLE; + boolean useDefault = mValues.useDefaultLocation; + boolean customLocation = !isNewProject || !useDefault; + + if (!mIgnore) { + try { + mIgnore = true; + if (customLocation) { + if (absDir != null) { + // We get here if the user selected a directory with the "Browse" button. + // Disable auto-compute of the custom location unless the user selected + // the exact same path. + sAutoComputeCustomLocation = sAutoComputeCustomLocation && + absDir.equals(sCustomLocationOsPath); + sCustomLocationOsPath = TextProcessor.process(absDir); + } else if (sAutoComputeCustomLocation || + (!isNewProject && !new File(sCustomLocationOsPath).isDirectory())) { + // As a default import location, just suggest the home directory; the user + // needs to point to a project to import. + // TODO: Open file chooser automatically? + sCustomLocationOsPath = System.getProperty("user.home"); //$NON-NLS-1$ + } + if (!mLocationText.getText().equals(sCustomLocationOsPath)) { + mLocationText.setText(sCustomLocationOsPath); + mValues.projectLocation = new File(sCustomLocationOsPath); + } + } else { + String value = Platform.getLocation().append(mValues.projectName).toString(); + value = TextProcessor.process(value); + if (!mLocationText.getText().equals(value)) { + mLocationText.setText(value); + mValues.projectLocation = new File(value); + } + } + } finally { + mIgnore = false; + } + } + + if (mValues.useExisting && mValues.projectLocation != null + && mValues.projectLocation.exists() && mValues.mode != Mode.SAMPLE) { + mValues.extractFromAndroidManifest(new Path(mValues.projectLocation.getPath())); + if (!mValues.projectNameModifiedByUser && mValues.projectName != null) { + try { + mIgnore = true; + mProjectNameText.setText(mValues.projectName); + } finally { + mIgnore = false; + } + } + } + } + + private void validatePage() { + IStatus status = null; + + // Validate project name -- unless we're creating a sample, in which case + // the user will get a chance to pick the name on the Sample page + if (mValues.mode != Mode.SAMPLE) { + status = validateProjectName(mValues.projectName); + } + + if (status == null || status.getSeverity() != IStatus.ERROR) { + IStatus validLocation = validateLocation(); + if (validLocation != null) { + status = validLocation; + } + } + + if (!mCheckedSdkUptodate) { + // Ensure that we have a recent enough version of the Tools that the right templates + // are available + File file = new File(AdtPlugin.getOsSdkFolder(), OS_SDK_TOOLS_LIB_FOLDER + + File.separator + FN_PROJECT_PROGUARD_FILE); + if (!file.exists()) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("You do not have the latest version of the " + + "SDK Tools installed: Please update. (Missing %1$s)", file.getPath())); + } else { + mCheckedSdkUptodate = true; + } + } + + // -- update UI & enable finish if there's no error + setPageComplete(status == null || status.getSeverity() != IStatus.ERROR); + if (status != null) { + setMessage(status.getMessage(), + status.getSeverity() == IStatus.ERROR + ? IMessageProvider.ERROR : IMessageProvider.WARNING); + } else { + setErrorMessage(null); + setMessage(null); + } + } + + private IStatus validateLocation() { + if (mValues.mode == Mode.SAMPLE) { + // Samples are always created in the default directory + return null; + } + + // Validate location + Path path = new Path(mValues.projectLocation.getPath()); + if (!mValues.useExisting) { + if (!mValues.useDefaultLocation) { + // If not using the default value validate the location. + URI uri = URIUtil.toURI(path.toOSString()); + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IProject handle = workspace.getRoot().getProject(mValues.projectName); + IStatus locationStatus = workspace.validateProjectLocationURI(handle, uri); + if (!locationStatus.isOK()) { + return locationStatus; + } + // The location is valid as far as Eclipse is concerned (i.e. mostly not + // an existing workspace project.) Check it either doesn't exist or is + // a directory that is empty. + File f = path.toFile(); + if (f.exists() && !f.isDirectory()) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "A directory name must be specified."); + } else if (f.isDirectory()) { + // However if the directory exists, we should put a + // warning if it is not empty. We don't put an error + // (we'll ask the user again for confirmation before + // using the directory.) + String[] l = f.list(); + if (l != null && l.length != 0) { + return new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID, + "The selected output directory is not empty."); + } + } + } else { + // Otherwise validate the path string is not empty + if (mValues.projectLocation.getPath().length() == 0) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "A directory name must be specified."); + } + File dest = path.toFile(); + if (dest.exists()) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format( + "There is already a file or directory named \"%1$s\" in the selected location.", + mValues.projectName)); + } + } + } else { + // Must be an existing directory + File f = path.toFile(); + if (!f.isDirectory()) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "An existing directory name must be specified."); + } + + // Check there's an android manifest in the directory + String osPath = path.append(SdkConstants.FN_ANDROID_MANIFEST_XML).toOSString(); + File manifestFile = new File(osPath); + if (!manifestFile.isFile()) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format( + "Choose a valid Android code directory\n" + + "(%1$s not found in %2$s.)", + SdkConstants.FN_ANDROID_MANIFEST_XML, f.getName())); + } + + // Parse it and check the important fields. + ManifestData manifestData = AndroidManifestHelper.parseForData(osPath); + if (manifestData == null) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("File %1$s could not be parsed.", osPath)); + } + String packageName = manifestData.getPackage(); + if (packageName == null || packageName.length() == 0) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("No package name defined in %1$s.", osPath)); + } + Activity[] activities = manifestData.getActivities(); + if (activities == null || activities.length == 0) { + // This is acceptable now as long as no activity needs to be + // created + if (mValues.createActivity) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("No activity name defined in %1$s.", osPath)); + } + } + + // If there's already a .project, tell the user to use import instead. + if (path.append(".project").toFile().exists()) { //$NON-NLS-1$ + return new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID, + "An Eclipse project already exists in this directory.\n" + + "Consider using File > Import > Existing Project instead."); + } + } + + return null; + } + + public static IStatus validateProjectName(String projectName) { + if (projectName == null || projectName.length() == 0) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Project name must be specified"); + } else { + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IStatus nameStatus = workspace.validateName(projectName, IResource.PROJECT); + if (!nameStatus.isOK()) { + return nameStatus; + } else { + // Note: the case-sensitiveness of the project name matters and can cause a + // conflict *later* when creating the project resource, so let's check it now. + for (IProject existingProj : workspace.getRoot().getProjects()) { + if (projectName.equalsIgnoreCase(existingProj.getName())) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "A project with that name already exists in the workspace"); + } + } + } + } + + return null; + } + + @Override + public IWizardPage getNextPage() { + // Sync working set data to the value object, since the WorkingSetGroup + // doesn't let us add listeners to do this lazily + mValues.workingSets = getWorkingSets(); + + return super.getNextPage(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SampleSelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SampleSelectionPage.java new file mode 100644 index 000000000..197247083 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SampleSelectionPage.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import com.android.SdkConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; +import com.android.sdklib.IAndroidTarget; +import com.android.utils.Pair; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.viewers.ArrayContentProvider; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.IBaseLabelProvider; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.Text; + +import java.io.File; + +/** Page where the user can select a sample to "instantiate" */ +class SampleSelectionPage extends WizardPage implements SelectionListener, ModifyListener { + private final NewProjectWizardState mValues; + private boolean mIgnore; + + private Table mTable; + private TableViewer mTableViewer; + private IAndroidTarget mCurrentSamplesTarget; + private Text mSampleProjectName; + + /** + * Create the wizard. + */ + SampleSelectionPage(NewProjectWizardState values) { + super("samplePage"); //$NON-NLS-1$ + setTitle("Select Sample"); + setDescription("Select which sample to create"); + mValues = values; + } + + /** + * Create contents of the wizard. + */ + @Override + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NULL); + container.setLayout(new GridLayout(2, false)); + + mTableViewer = new TableViewer(container, SWT.BORDER | SWT.FULL_SELECTION); + mTable = mTableViewer.getTable(); + GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1); + gridData.heightHint = 300; + mTable.setLayoutData(gridData); + mTable.addSelectionListener(this); + + setControl(container); + + Label projectLabel = new Label(container, SWT.NONE); + projectLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + projectLabel.setText("Project Name:"); + + mSampleProjectName = new Text(container, SWT.BORDER); + mSampleProjectName.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mSampleProjectName.addModifyListener(this); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + + if (visible) { + if (mValues.projectName != null) { + try { + mIgnore = true; + mSampleProjectName.setText(mValues.projectName); + } finally { + mIgnore = false; + } + } + + // Update samples list if the SDK target has changed (or if it hasn't yet + // been populated) + if (mCurrentSamplesTarget != mValues.target) { + mCurrentSamplesTarget = mValues.target; + updateSamples(); + } + + validatePage(); + } + } + + private void updateSamples() { + IBaseLabelProvider labelProvider = new ColumnLabelProvider() { + @Override + public Image getImage(Object element) { + return AdtPlugin.getAndroidLogo(); + } + + @Override + public String getText(Object element) { + if (element instanceof Pair<?, ?>) { + Object name = ((Pair<?, ?>) element).getFirst(); + return name.toString(); + } + return element.toString(); // Fallback. Should not happen. + } + }; + + mTableViewer.setContentProvider(new ArrayContentProvider()); + mTableViewer.setLabelProvider(labelProvider); + + if (mValues.samples != null && mValues.samples.size() > 0) { + Object[] samples = mValues.samples.toArray(); + mTableViewer.setInput(samples); + + mTable.select(0); + selectSample(mValues.samples.get(0).getSecond()); + extractNamesFromAndroidManifest(); + } + } + + private void selectSample(File sample) { + mValues.chosenSample = sample; + if (sample != null && !mValues.projectNameModifiedByUser) { + mValues.projectName = sample.getName(); + if (SdkConstants.FD_SAMPLE.equals(mValues.projectName) && + sample.getParentFile() != null) { + mValues.projectName = sample.getParentFile().getName() + '_' + mValues.projectName; + } + try { + mIgnore = true; + mSampleProjectName.setText(mValues.projectName); + } finally { + mIgnore = false; + } + updatedProjectName(); + } + } + + @SuppressWarnings("unchecked") + @Override + public void widgetSelected(SelectionEvent e) { + if (mIgnore) { + return; + } + + if (e.getSource() == mTable) { + extractNamesFromAndroidManifest(); + int index = mTable.getSelectionIndex(); + if (index >= 0) { + Object[] roots = (Object[]) mTableViewer.getInput(); + selectSample(((Pair<String, File>) roots[index]).getSecond()); + } else { + selectSample(null); + } + } else { + assert false : e.getSource(); + } + + validatePage(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + + @Override + public void modifyText(ModifyEvent e) { + if (mIgnore) { + return; + } + + if (e.getSource() == mSampleProjectName) { + mValues.projectName = mSampleProjectName.getText().trim(); + mValues.projectNameModifiedByUser = true; + updatedProjectName(); + } + + validatePage(); + } + + private void updatedProjectName() { + if (mValues.useDefaultLocation) { + mValues.projectLocation = Platform.getLocation().toFile(); + } + } + + /** + * A sample was selected. Update the location field, manifest and validate. + * Extract names from an android manifest. + * This is done only if the user selected the "use existing source" and a manifest xml file + * can actually be found in the custom user directory. + */ + private void extractNamesFromAndroidManifest() { + if (mValues.chosenSample == null || !mValues.chosenSample.isDirectory()) { + return; + } + + Path path = new Path(mValues.chosenSample.getPath()); + mValues.extractFromAndroidManifest(path); + } + + @Override + public boolean isPageComplete() { + if (mValues.mode != Mode.SAMPLE) { + return true; + } + + // Ensure that when creating samples, the Finish button isn't enabled until + // the user has reached and completed this page + if (mValues.chosenSample == null) { + return false; + } + + return super.isPageComplete(); + } + + private void validatePage() { + IStatus status = null; + if (mValues.samples == null || mValues.samples.size() == 0) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "The chosen SDK does not contain any samples"); + } else if (mValues.chosenSample == null) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, "Choose a sample"); + } else if (!mValues.chosenSample.exists()) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("Sample does not exist: %1$s", mValues.chosenSample.getPath())); + } else { + status = ProjectNamePage.validateProjectName(mValues.projectName); + } + + // -- update UI & enable finish if there's no error + setPageComplete(status == null || status.getSeverity() != IStatus.ERROR); + if (status != null) { + setMessage(status.getMessage(), + status.getSeverity() == IStatus.ERROR + ? IMessageProvider.ERROR : IMessageProvider.WARNING); + } else { + setErrorMessage(null); + setMessage(null); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SdkSelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SdkSelectionPage.java new file mode 100644 index 000000000..6cafcf057 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SdkSelectionPage.java @@ -0,0 +1,487 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import com.android.SdkConstants; +import com.android.annotations.Nullable; +import com.android.ide.common.sdk.LoadStatus; +import com.android.ide.common.xml.AndroidManifestParser; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; +import com.android.io.FileWrapper; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkManager; +import com.android.sdkuilib.internal.widgets.SdkTargetSelector; +import com.android.utils.NullLogger; +import com.android.utils.Pair; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Group; + +import java.io.File; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Pattern; + +/** A page in the New Project wizard where you select the target SDK */ +class SdkSelectionPage extends WizardPage implements ITargetChangeListener { + private final NewProjectWizardState mValues; + private boolean mIgnore; + private SdkTargetSelector mSdkTargetSelector; + + /** + * Create the wizard. + */ + SdkSelectionPage(NewProjectWizardState values) { + super("sdkSelection"); //$NON-NLS-1$ + mValues = values; + + setTitle("Select Build Target"); + AdtPlugin.getDefault().addTargetListener(this); + } + + @Override + public void dispose() { + AdtPlugin.getDefault().removeTargetListener(this); + super.dispose(); + } + + /** + * Create contents of the wizard. + */ + @Override + public void createControl(Composite parent) { + Group group = new Group(parent, SWT.SHADOW_ETCHED_IN); + // Layout has 1 column + group.setLayout(new GridLayout()); + group.setLayoutData(new GridData(GridData.FILL_BOTH)); + group.setFont(parent.getFont()); + group.setText("Build Target"); + + // The selector is created without targets. They are added below in the change listener. + mSdkTargetSelector = new SdkTargetSelector(group, null); + + mSdkTargetSelector.setSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mIgnore) { + return; + } + + mValues.target = mSdkTargetSelector.getSelected(); + mValues.targetModifiedByUser = true; + onSdkTargetModified(); + validatePage(); + } + }); + + onSdkLoaded(); + + setControl(group); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (mValues.mode == Mode.SAMPLE) { + setDescription("Choose an SDK to select a sample from"); + } else { + setDescription("Choose an SDK to target"); + } + try { + mIgnore = true; + if (mValues.target != null) { + mSdkTargetSelector.setSelection(mValues.target); + } + } finally { + mIgnore = false; + } + + validatePage(); + } + + @Override + public boolean isPageComplete() { + // Ensure that the Finish button isn't enabled until + // the user has reached and completed this page + if (mValues.target == null) { + return false; + } + + return super.isPageComplete(); + } + + /** + * Called when an SDK target is modified. + * + * Also changes the minSdkVersion field to reflect the sdk api level that has + * just been selected. + */ + private void onSdkTargetModified() { + if (mIgnore) { + return; + } + + IAndroidTarget target = mValues.target; + + // Update the minimum SDK text field? + // We do if one of two conditions are met: + if (target != null) { + boolean setMinSdk = false; + AndroidVersion version = target.getVersion(); + int apiLevel = version.getApiLevel(); + // 1. Has the user not manually edited the SDK field yet? If so, keep + // updating it to the selected value. + if (!mValues.minSdkModifiedByUser) { + setMinSdk = true; + } else { + // 2. Is the API level set to a higher level than the newly selected + // target SDK? If so, change it down to the new lower value. + String s = mValues.minSdk; + if (s.length() > 0) { + try { + int currentApi = Integer.parseInt(s); + if (currentApi > apiLevel) { + setMinSdk = true; + } + } catch (NumberFormatException nfe) { + // User may have typed something invalid -- ignore + } + } + } + if (setMinSdk) { + String minSdk; + if (version.isPreview()) { + minSdk = version.getCodename(); + } else { + minSdk = Integer.toString(apiLevel); + } + mValues.minSdk = minSdk; + } + } + + loadSamplesForTarget(target); + } + + /** + * 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) + File previouslyChosenSample = mValues.chosenSample; + + mValues.samples.clear(); + mValues.chosenSample = null; + + if (target != null) { + // Get the sample root path and recompute the list of samples + String samplesRootPath = target.getPath(IAndroidTarget.SAMPLES); + + File root = new File(samplesRootPath); + findSamplesManifests(root, root, null, null, mValues.samples); + + Sdk sdk = Sdk.getCurrent(); + if (sdk != null) { + // Parse the extras to see if we can find samples that are + // compatible with the selected target API. + // First we need an SdkManager that suppresses all output. + SdkManager sdkman = sdk.getNewSdkManager(NullLogger.getLogger()); + + Map<File, String> extras = sdkman.getExtraSamples(); + for (Entry<File, String> entry : extras.entrySet()) { + File path = entry.getKey(); + String name = entry.getValue(); + + // Case where the sample is at the root of the directory and not + // in a per-sample sub-directory. + if (path.getName().equals(SdkConstants.FD_SAMPLE)) { + findSampleManifestInDir( + path, path, name, target.getVersion(), mValues.samples); + } + + // Scan sub-directories + findSamplesManifests( + path, path, name, target.getVersion(), mValues.samples); + } + } + + if (mValues.samples.isEmpty()) { + return; + } else { + Collections.sort(mValues.samples, new Comparator<Pair<String, File>>() { + @Override + public int compare(Pair<String, File> o1, Pair<String, File> o2) { + // Compare the display name of the sample + return o1.getFirst().compareTo(o2.getFirst()); + } + }); + } + + // Try to find the old selection. + if (previouslyChosenSample != null) { + String previouslyChosenName = previouslyChosenSample.getName(); + for (int i = 0, n = mValues.samples.size(); i < n; i++) { + File file = mValues.samples.get(i).getSecond(); + if (file.getName().equals(previouslyChosenName)) { + mValues.chosenSample = file; + break; + } + } + } + } + } + + /** + * 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. + * + * @param rootDir The "samples" root directory. Doesn't change during recursion. + * @param currDir The directory being scanned. Caller must initially set it to {@code rootDir}. + * @param extraName Optional name appended to the samples display name. Typically used to + * indicate a sample comes from a given extra package. + * @param targetVersion Optional target version filter. If non null, only samples that are + * compatible with the given target will be listed. + * @param samplesPaths A non-null list filled by this method with all samples found. The + * pair is (String: sample display name => File: sample directory). + */ + private void findSamplesManifests( + File rootDir, + File currDir, + @Nullable String extraName, + @Nullable AndroidVersion targetVersion, + List<Pair<String, File>> samplesPaths) { + if (!currDir.isDirectory()) { + return; + } + + for (File f : currDir.listFiles()) { + if (f.isDirectory()) { + findSampleManifestInDir(f, rootDir, extraName, targetVersion, samplesPaths); + + // 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(rootDir, f, extraName, targetVersion, samplesPaths); + } + } + } + } + + private void findSampleManifestInDir( + File sampleDir, + File rootDir, + String extraName, + AndroidVersion targetVersion, + List<Pair<String, File>> samplesPaths) { + // Assume this is a sample if it contains an android manifest. + File manifestFile = new File(sampleDir, SdkConstants.FN_ANDROID_MANIFEST_XML); + if (manifestFile.isFile()) { + try { + ManifestData data = + AndroidManifestParser.parse(new FileWrapper(manifestFile)); + if (data != null) { + boolean accept = false; + if (targetVersion == null) { + accept = true; + } else if (targetVersion != null) { + int i = data.getMinSdkVersion(); + if (i != ManifestData.MIN_SDK_CODENAME) { + accept = i <= targetVersion.getApiLevel(); + } else { + String s = data.getMinSdkVersionString(); + if (s != null) { + accept = s.equals(targetVersion.getCodename()); + } + } + } + + if (accept) { + String name = getSampleDisplayName(extraName, rootDir, sampleDir); + samplesPaths.add(Pair.of(name, sampleDir)); + } + } + } catch (Exception e) { + // Ignore. Don't use a sample which manifest doesn't parse correctly. + AdtPlugin.log(IStatus.INFO, + "NPW ignoring malformed manifest %s", //$NON-NLS-1$ + manifestFile.getAbsolutePath()); + } + } + } + + /** + * Compute the sample name compared to its root directory. + */ + private String getSampleDisplayName(String extraName, File rootDir, File sampleDir) { + String name = null; + if (!rootDir.equals(sampleDir)) { + String path = sampleDir.getPath(); + int n = rootDir.getPath().length(); + 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); + } + name = path.replaceAll(Pattern.quote(File.separator), " > "); //$NON-NLS-1$ + } + } + if (name == null && + rootDir.equals(sampleDir) && + sampleDir.getName().equals(SdkConstants.FD_SAMPLE) && + extraName != null) { + // This is an old-style extra with one single sample directory. Just use the + // extra's name as the same name. + return extraName; + } + if (name == null) { + // Otherwise try to use the sample's directory name as the sample name. + while (sampleDir != null && + (name == null || + SdkConstants.FD_SAMPLE.equals(name) || + SdkConstants.FD_SAMPLES.equals(name))) { + name = sampleDir.getName(); + sampleDir = sampleDir.getParentFile(); + } + } + if (name == null) { + if (extraName != null) { + // In the unlikely case nothing worked and we have an extra name, use that. + return extraName; + } else { + name = "Sample"; // fallback name... should not happen. //$NON-NLS-1$ + } + } + if (extraName != null) { + name = name + " [" + extraName + ']'; //$NON-NLS-1$ + } + + return name; + } + + private void validatePage() { + String error = null; + + if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADING) { + error = "The SDK is still loading; please wait."; + } + + if (error == null && mValues.target == null) { + error = "An SDK Target must be specified."; + } + + if (error == null && mValues.mode == Mode.SAMPLE) { + // Make sure this SDK target contains samples + if (mValues.samples == null || mValues.samples.size() == 0) { + error = "This target has no samples. Please select another target."; + } + } + + // -- update UI & enable finish if there's no error + setPageComplete(error == null); + if (error != null) { + setMessage(error, IMessageProvider.ERROR); + } else { + setErrorMessage(null); + setMessage(null); + } + } + + // ---- Implements ITargetChangeListener ---- + @Override + public void onSdkLoaded() { + if (mSdkTargetSelector == null) { + return; + } + + // Update the sdk target selector with the new targets + + // get the targets from the sdk + IAndroidTarget[] targets = null; + if (Sdk.getCurrent() != null) { + targets = Sdk.getCurrent().getTargets(); + } + mSdkTargetSelector.setTargets(targets); + + // If there's only one target, select it. + // This will invoke the selection listener on the selector defined above. + if (targets != null && targets.length == 1) { + mValues.target = targets[0]; + mSdkTargetSelector.setSelection(mValues.target); + onSdkTargetModified(); + } else if (targets != null) { + // Pick the highest available platform by default (see issue #17505 + // for related discussion.) + IAndroidTarget initialTarget = null; + for (IAndroidTarget target : targets) { + if (target.isPlatform() + && !target.getVersion().isPreview() + && (initialTarget == null || + target.getVersion().getApiLevel() > + initialTarget.getVersion().getApiLevel())) { + initialTarget = target; + } + } + if (initialTarget != null) { + mValues.target = initialTarget; + try { + mIgnore = true; + mSdkTargetSelector.setSelection(mValues.target); + } finally { + mIgnore = false; + } + onSdkTargetModified(); + } + } + + validatePage(); + } + + @Override + public void onProjectTargetChange(IProject changedProject) { + // Ignore + } + + @Override + public void onTargetLoaded(IAndroidTarget target) { + // Ignore + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/TestTargetPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/TestTargetPage.java new file mode 100644 index 000000000..f1c188ae9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/TestTargetPage.java @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import com.android.ide.common.xml.ManifestData; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.jdt.core.IJavaModel; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.ui.JavaElementLabelProvider; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionEvent; +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.Composite; +import org.eclipse.ui.dialogs.FilteredList; + +/** + * Page shown when creating a test project which lets you choose between testing + * yourself and testing a different project + */ +class TestTargetPage extends WizardPage implements SelectionListener { + private final NewProjectWizardState mValues; + /** Flag used when setting button/text state manually to ignore listener updates */ + private boolean mIgnore; + private String mLastExistingPackageName; + + private Button mCurrentRadioButton; + private Button mExistingRadioButton; + private FilteredList mProjectList; + private boolean mPageShown; + + /** + * Create the wizard. + */ + TestTargetPage(NewProjectWizardState values) { + super("testTargetPage"); //$NON-NLS-1$ + setTitle("Select Test Target"); + setDescription("Choose a project to test"); + mValues = values; + } + + /** + * Create contents of the wizard. + */ + @Override + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NULL); + + setControl(container); + container.setLayout(new GridLayout(2, false)); + + mCurrentRadioButton = new Button(container, SWT.RADIO); + mCurrentRadioButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1)); + mCurrentRadioButton.setText("This project"); + mCurrentRadioButton.addSelectionListener(this); + + mExistingRadioButton = new Button(container, SWT.RADIO); + mExistingRadioButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1)); + mExistingRadioButton.setText("An existing Android project:"); + mExistingRadioButton.addSelectionListener(this); + + ILabelProvider labelProvider = new JavaElementLabelProvider( + JavaElementLabelProvider.SHOW_DEFAULT); + mProjectList = new FilteredList(container, + SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL | SWT.SINGLE, labelProvider, + true /*ignoreCase*/, false /*allowDuplicates*/, true /* matchEmptyString*/); + mProjectList.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); + mProjectList.addSelectionListener(this); + } + + private void initializeList() { + ProjectChooserHelper helper = new ProjectChooserHelper(getShell(), null /*filter*/); + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + IJavaModel javaModel = JavaCore.create(workspaceRoot); + IJavaProject[] androidProjects = helper.getAndroidProjects(javaModel); + mProjectList.setElements(androidProjects); + if (mValues.testedProject != null) { + for (IJavaProject project : androidProjects) { + if (project.getProject() == mValues.testedProject) { + mProjectList.setSelection(new Object[] { project }); + break; + } + } + } else { + // No initial selection: force the user to choose + mProjectList.setSelection(new int[0]); + } + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + mPageShown = true; + + if (visible) { + try { + mIgnore = true; + mCurrentRadioButton.setSelection(mValues.testingSelf); + mExistingRadioButton.setSelection(!mValues.testingSelf); + mProjectList.setEnabled(!mValues.testingSelf); + + if (mProjectList.isEmpty()) { + initializeList(); + } + if (!mValues.testingSelf) { + mProjectList.setFocus(); + IProject project = getSelectedProject(); + if (project != null) { + // The FilteredList seems to -insist- on selecting the first item + // in the list, even when the selection is explicitly set to an empty + // array. This means the user is looking at a selection, so we need + // to also go ahead and select this item in the model such that the + // two agree, even if we would have preferred to have no initial + // selection. + mValues.testedProject = project; + } + } + } finally { + mIgnore = false; + } + } + + validatePage(); + } + + @Override + public void widgetSelected(SelectionEvent e) { + if (mIgnore) { + return; + } + + Object source = e.getSource(); + if (source == mExistingRadioButton) { + mProjectList.setEnabled(true); + mValues.testingSelf = false; + setExistingProject(getSelectedProject()); + mProjectList.setFocus(); + } else if (source == mCurrentRadioButton) { + mProjectList.setEnabled(false); + mValues.testingSelf = true; + mValues.testedProject = null; + } else { + // The event must be from the project list, which unfortunately doesn't + // pass itself as the selection event, it passes a reference to some internal + // table widget that it uses, so we check for this case last + IProject project = getSelectedProject(); + if (project != mValues.testedProject) { + setExistingProject(project); + } + } + + validatePage(); + } + + private IProject getSelectedProject() { + Object[] selection = mProjectList.getSelection(); + IProject project = selection != null && selection.length == 1 + ? ((IJavaProject) selection[0]).getProject() : null; + return project; + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + + private void setExistingProject(IProject project) { + mValues.testedProject = project; + + // Try to update the application, package, sdk target and minSdkVersion accordingly + if (project != null && + (!mValues.applicationNameModifiedByUser || + !mValues.packageNameModifiedByUser || + !mValues.targetModifiedByUser || + !mValues.minSdkModifiedByUser)) { + ManifestData manifestData = AndroidManifestHelper.parseForData(project); + if (manifestData != null) { + String appName = String.format("%1$sTest", project.getName()); + String packageName = manifestData.getPackage(); + String minSdkVersion = manifestData.getMinSdkVersionString(); + IAndroidTarget sdkTarget = null; + if (Sdk.getCurrent() != null) { + sdkTarget = Sdk.getCurrent().getTarget(project); + } + + if (packageName == null) { + packageName = ""; //$NON-NLS-1$ + } + mLastExistingPackageName = packageName; + + if (!mValues.projectNameModifiedByUser) { + mValues.projectName = appName; + } + + if (!mValues.applicationNameModifiedByUser) { + mValues.applicationName = appName; + } + + if (!mValues.packageNameModifiedByUser) { + packageName += ".test"; //$NON-NLS-1$ + mValues.packageName = packageName; + } + + if (!mValues.targetModifiedByUser && sdkTarget != null) { + mValues.target = sdkTarget; + } + + if (!mValues.minSdkModifiedByUser) { + if (minSdkVersion != null || sdkTarget != null) { + mValues.minSdk = minSdkVersion; + } + if (sdkTarget == null) { + mValues.updateSdkTargetToMatchMinSdkVersion(); + } + } + } + } + + updateTestTargetPackageField(mLastExistingPackageName); + } + + /** + * Updates the test target package name + * + * When using the "self-test" option, the packageName argument is ignored and the + * current value from the project package is used. + * + * Otherwise the packageName is used if it is not null. + */ + private void updateTestTargetPackageField(String packageName) { + if (mValues.testingSelf) { + mValues.testTargetPackageName = mValues.packageName; + } else if (packageName != null) { + mValues.testTargetPackageName = packageName; + } + } + + @Override + public boolean isPageComplete() { + // Ensure that the user sees the page and makes a selection + if (!mPageShown) { + return false; + } + + return super.isPageComplete(); + } + + private void validatePage() { + String error = null; + + if (!mValues.testingSelf) { + if (mValues.testedProject == null) { + error = "Please select an existing Android project as a test target."; + } else if (mValues.projectName.equals(mValues.testedProject.getName())) { + error = "The main project name and the test project name must be different."; + } + } + + // -- update UI & enable finish if there's no error + setPageComplete(error == null); + if (error != null) { + setMessage(error, IMessageProvider.ERROR); + } else { + setErrorMessage(null); + setMessage(null); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetGroup.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetGroup.java new file mode 100644 index 000000000..fb33a08b4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetGroup.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * Copyright (c) 2000, 2009 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ + +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import org.eclipse.jdt.internal.ui.JavaPlugin; +import org.eclipse.jdt.internal.ui.wizards.NewWizardMessages; +import org.eclipse.jdt.internal.ui.workingsets.IWorkingSetIDs; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Group; +import org.eclipse.ui.IWorkingSet; +import org.eclipse.ui.dialogs.WorkingSetConfigurationBlock; + +/** + * Copied from + * org.eclipse.jdt.ui.wizards.NewJavaProjectWizardPageOne$WorkingSetGroup + * + * Creates the working set group with controls that allow + * the selection of working sets + */ +@SuppressWarnings("restriction") +public class WorkingSetGroup { + + private WorkingSetConfigurationBlock fWorkingSetBlock; + private Button mEnableButton; + + public WorkingSetGroup() { + String[] workingSetIds = new String[] { + IWorkingSetIDs.JAVA, IWorkingSetIDs.RESOURCE + }; + fWorkingSetBlock = new WorkingSetConfigurationBlock(workingSetIds, JavaPlugin.getDefault() + .getDialogSettings()); + } + + public Composite createControl(Composite composite) { + Group workingSetGroup = new Group(composite, SWT.NONE); + workingSetGroup.setFont(composite.getFont()); + workingSetGroup.setText(NewWizardMessages.NewJavaProjectWizardPageOne_WorkingSets_group); + workingSetGroup.setLayout(new GridLayout(1, false)); + + fWorkingSetBlock.createContent(workingSetGroup); + + // WorkingSetGroup is implemented in such a way that the checkbox it contains + // can only be programmatically set if there's an existing working set associated + // *before* we construct the control. However the control is created when the + // wizard is opened, not when the page is first shown. + // + // One choice is to duplicate the class in our project. + // Or find the checkbox we want and trigger it manually. + mEnableButton = findCheckbox(workingSetGroup); + + return workingSetGroup; + } + + public void setWorkingSets(IWorkingSet[] workingSets) { + fWorkingSetBlock.setWorkingSets(workingSets); + } + + public IWorkingSet[] getSelectedWorkingSets() { + try { + return fWorkingSetBlock.getSelectedWorkingSets(); + } catch (Throwable t) { + // Test scenarios; no UI is created, which the fWorkingSetBlock assumes + // (it dereferences the enabledButton) + return new IWorkingSet[0]; + } + } + + public boolean isChecked() { + return mEnableButton == null ? false : mEnableButton.getSelection(); + } + + public void setChecked(boolean state) { + if (mEnableButton != null) { + mEnableButton.setSelection(state); + } + } + + /** + * Finds the first button of style Checkbox in the given parent composite. + * Returns null if not found. + */ + private Button findCheckbox(Composite parent) { + for (Control control : parent.getChildren()) { + if (control instanceof Button && (control.getStyle() & SWT.CHECK) == SWT.CHECK) { + return (Button) control; + } else if (control instanceof Composite) { + Button found = findCheckbox((Composite) control); + if (found != null) { + return found; + } + } + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetHelper.java new file mode 100755 index 000000000..428bfd331 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetHelper.java @@ -0,0 +1,130 @@ +/******************************************************************************* + * Copyright (c) 2000, 2009 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ + +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import org.eclipse.jdt.internal.ui.packageview.PackageExplorerPart; +import org.eclipse.jdt.internal.ui.workingsets.IWorkingSetIDs; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITreeSelection; +import org.eclipse.jface.viewers.TreePath; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkingSet; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * This class contains a helper method to deal with working sets. + * <p/> + * Copied from org.eclipse.jdt.ui.wizards.NewJavaProjectWizardPageOne + */ +@SuppressWarnings("restriction") +public final class WorkingSetHelper { + + private static final IWorkingSet[] EMPTY_WORKING_SET_ARRAY = new IWorkingSet[0]; + + /** This class is never instantiated. */ + private WorkingSetHelper() { + } + + public static IWorkingSet[] getSelectedWorkingSet(IStructuredSelection selection, + IWorkbenchPart activePart) { + IWorkingSet[] selected= getSelectedWorkingSet(selection); + if (selected != null && selected.length > 0) { + for (int i= 0; i < selected.length; i++) { + if (!isValidWorkingSet(selected[i])) + return EMPTY_WORKING_SET_ARRAY; + } + return selected; + } + + if (!(activePart instanceof PackageExplorerPart)) + return EMPTY_WORKING_SET_ARRAY; + + PackageExplorerPart explorerPart= (PackageExplorerPart) activePart; + if (explorerPart.getRootMode() == PackageExplorerPart.PROJECTS_AS_ROOTS) { + //Get active filter + IWorkingSet filterWorkingSet= explorerPart.getFilterWorkingSet(); + if (filterWorkingSet == null) + return EMPTY_WORKING_SET_ARRAY; + + if (!isValidWorkingSet(filterWorkingSet)) + return EMPTY_WORKING_SET_ARRAY; + + return new IWorkingSet[] {filterWorkingSet}; + } else { + //If we have been gone into a working set return the working set + Object input= explorerPart.getViewPartInput(); + if (!(input instanceof IWorkingSet)) + return EMPTY_WORKING_SET_ARRAY; + + IWorkingSet workingSet= (IWorkingSet)input; + if (!isValidWorkingSet(workingSet)) + return EMPTY_WORKING_SET_ARRAY; + + return new IWorkingSet[] {workingSet}; + } + } + + private static IWorkingSet[] getSelectedWorkingSet(IStructuredSelection selection) { + if (!(selection instanceof ITreeSelection)) + return EMPTY_WORKING_SET_ARRAY; + + ITreeSelection treeSelection= (ITreeSelection) selection; + if (treeSelection.isEmpty()) + return EMPTY_WORKING_SET_ARRAY; + + List<?> elements = treeSelection.toList(); + if (elements.size() == 1) { + Object element= elements.get(0); + TreePath[] paths= treeSelection.getPathsFor(element); + if (paths.length != 1) + return EMPTY_WORKING_SET_ARRAY; + + TreePath path= paths[0]; + if (path.getSegmentCount() == 0) + return EMPTY_WORKING_SET_ARRAY; + + Object candidate= path.getSegment(0); + if (!(candidate instanceof IWorkingSet)) + return EMPTY_WORKING_SET_ARRAY; + + IWorkingSet workingSetCandidate= (IWorkingSet) candidate; + if (isValidWorkingSet(workingSetCandidate)) + return new IWorkingSet[] { workingSetCandidate }; + + return EMPTY_WORKING_SET_ARRAY; + } + + ArrayList<Object> result = new ArrayList<Object>(); + for (Iterator<?> iterator = elements.iterator(); iterator.hasNext();) { + Object element= iterator.next(); + if (element instanceof IWorkingSet && isValidWorkingSet((IWorkingSet) element)) { + result.add(element); + } + } + return result.toArray(new IWorkingSet[result.size()]); + } + + + private static boolean isValidWorkingSet(IWorkingSet workingSet) { + String id= workingSet.getId(); + if (!IWorkingSetIDs.JAVA.equals(id) && !IWorkingSetIDs.RESOURCE.equals(id)) + return false; + + if (workingSet.isAggregateWorkingSet()) + return false; + + return true; + } +} |