/* * 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; /** * 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 { IProject handle = workspace.getRoot().getProject(projectName); if (handle.exists()) { 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(); } }