diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ProjectNamePage.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ProjectNamePage.java | 606 |
1 files changed, 606 insertions, 0 deletions
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(); + } +} |