blob: d04ea897f6730bb6b928b5c9a0c083b3c74a60a3 [file] [log] [blame]
/*
* 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();
}
}