/*
* 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.
*/
/*
* References:
* org.eclipse.jdt.internal.ui.wizards.JavaProjectWizard
* org.eclipse.jdt.internal.ui.wizards.JavaProjectWizardFirstPage
*/
package com.android.ide.eclipse.adt.internal.wizards.newproject;
import com.android.ide.eclipse.adt.AdtPlugin;
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.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectCreationPage.IMainInfo;
import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectCreationPage.MainInfo;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.xml.ManifestData;
import com.android.sdkuilib.internal.widgets.SdkTargetSelector;
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.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaConventions;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.osgi.util.TextProcessor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
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.Control;
import org.eclipse.swt.widgets.DirectoryDialog;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
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.ArrayList;
import java.util.regex.Pattern;
/**
* NewAndroidProjectCreationPage is a project creation page that provides the
* following fields:
* <ul>
* <li> Project name
* <li> SDK Target
* <li> Application name
* <li> Package name
* <li> Activity name
* </ul>
* 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 NewTestProjectCreationPage extends WizardPage {
// constants
static final String TEST_PAGE_NAME = "newAndroidTestProjectPage"; //$NON-NLS-1$
/** Initial value for all name fields (project, activity, application, package). Used
* whenever a value is requested before controls are created. */
private static final String INITIAL_NAME = ""; //$NON-NLS-1$
/** Initial value for the Use Default Location check box. */
private static final boolean INITIAL_USE_DEFAULT_LOCATION = true;
/** Initial value for the Create Test Project check box. */
private static final boolean INITIAL_CREATE_TEST_PROJECT = false;
/** Pattern for characters accepted in a project name. Since this will be used as a
* directory name, we're being a bit conservative on purpose. It cannot start with a space. */
private static final Pattern sProjectNamePattern = Pattern.compile("^[\\w][\\w. -]*$"); //$NON-NLS-1$
/** Last user-browsed location, static so that it be remembered for the whole session */
private static String sCustomLocationOsPath = ""; //$NON-NLS-1$
private final int MSG_NONE = 0;
private final int MSG_WARNING = 1;
private final int MSG_ERROR = 2;
/** Structure with the externally visible information from this Test Project page. */
private final TestInfo mInfo = new TestInfo();
/** Structure with the externally visible information from the Main Project page.
* This is null if there's no such page, meaning the test project page is standalone. */
private IMainInfo mMainInfo;
// widgets
private Text mProjectNameField;
private Text mPackageNameField;
private Text mApplicationNameField;
private Button mUseDefaultLocation;
private Label mLocationLabel;
private Text mLocationPathField;
private Button mBrowseButton;
private Text mMinSdkVersionField;
private SdkTargetSelector mSdkTargetSelector;
private ITargetChangeListener mSdkTargetChangeListener;
private Button mCreateTestProjectField;
private Text mTestedProjectNameField;
private Button mProjectBrowseButton;
private ProjectChooserHelper mProjectChooserHelper;
private Button mTestSelfProjectRadio;
private Button mTestExistingProjectRadio;
/** A list of composites that are disabled when the "Create Test Project" toggle is off. */
private ArrayList<Composite> mToggleComposites = new ArrayList<Composite>();
private boolean mInternalProjectNameUpdate;
private boolean mInternalLocationPathUpdate;
private boolean mInternalPackageNameUpdate;
private boolean mInternalApplicationNameUpdate;
private boolean mInternalMinSdkVersionUpdate;
private boolean mInternalSdkTargetUpdate;
private IProject mExistingTestedProject;
private boolean mProjectNameModifiedByUser;
private boolean mApplicationNameModifiedByUser;
private boolean mPackageNameModifiedByUser;
private boolean mMinSdkVersionModifiedByUser;
private boolean mSdkTargetModifiedByUser;
private Label mTestTargetPackageLabel;
private String mLastExistingPackageName;
private WorkingSetGroup mWorkingSetGroup;
/**
* Creates a new project creation wizard page.
*/
public NewTestProjectCreationPage() {
super(TEST_PAGE_NAME);
setPageComplete(false);
setTitle("New Android Test Project");
setDescription("Creates a new Android Test Project resource.");
mWorkingSetGroup= new WorkingSetGroup();
setWorkingSets(new IWorkingSet[0]);
}
public void init(IStructuredSelection selection, IWorkbenchPart activePart) {
setWorkingSets(WorkingSetHelper.getSelectedWorkingSet(selection, activePart));
}
// --- Getters used by NewProjectWizard ---
/**
* Structure that collects all externally visible information from this page.
* This is used by the calling wizard to actually do the work or by other pages.
*/
public class TestInfo {
/** Returns true if a new Test Project should be created. */
public boolean getCreateTestProject() {
return mCreateTestProjectField == null ? true : mCreateTestProjectField.getSelection();
}
/**
* Returns the current project location path as entered by the user, or its
* anticipated initial value. Note that if the default has been returned the
* path in a project description used to create a project should not be set.
*
* @return the project location path or its anticipated initial value.
*/
public IPath getLocationPath() {
return new Path(getProjectLocation());
}
/** Returns the value of the project name field with leading and trailing spaces removed. */
public String getProjectName() {
return mProjectNameField == null ? INITIAL_NAME : mProjectNameField.getText().trim();
}
/** Returns the value of the package name field with spaces trimmed. */
public String getPackageName() {
return mPackageNameField == null ? INITIAL_NAME : mPackageNameField.getText().trim();
}
/** Returns the value of the test target package name field with spaces trimmed. */
public String getTargetPackageName() {
return mTestTargetPackageLabel == null ? INITIAL_NAME
: mTestTargetPackageLabel.getText().trim();
}
/** Returns the value of the min sdk version field with spaces trimmed. */
public String getMinSdkVersion() {
return mMinSdkVersionField == null ? "" : mMinSdkVersionField.getText().trim(); //$NON-NLS-1$
}
/** Returns the value of the application name field with spaces trimmed. */
public String getApplicationName() {
// Return the name of the activity as default application name.
return mApplicationNameField == null ? "" : mApplicationNameField.getText().trim(); //$NON-NLS-1$
}
/** Returns the value of the Use Default Location field. */
public boolean useDefaultLocation() {
return mUseDefaultLocation == null ? INITIAL_USE_DEFAULT_LOCATION
: mUseDefaultLocation.getSelection();
}
/** Returns the the default "src" constant. */
public String getSourceFolder() {
return SdkConstants.FD_SOURCES;
}
/** Returns the current sdk target or null if none has been selected yet. */
public IAndroidTarget getSdkTarget() {
return mSdkTargetSelector == null ? null : mSdkTargetSelector.getSelected();
}
public boolean isTestingSelf() {
return mMainInfo == null &&
(mTestSelfProjectRadio == null ? false : mTestSelfProjectRadio.getSelection());
}
public boolean isTestingMain() {
return mMainInfo != null;
}
public boolean isTestingExisting() {
return mMainInfo == null &&
(mTestExistingProjectRadio == null ? false
: mTestExistingProjectRadio.getSelection());
}
public IProject getExistingTestedProject() {
return mExistingTestedProject;
}
}
/**
* Returns a {@link TestInfo} structure that collects all externally visible information
* from this page. This is used by the calling wizard to actually do the work or by other pages.
*/
public TestInfo getTestInfo() {
return mInfo;
}
/**
* Grabs the {@link MainInfo} structure with visible parameters from the main project page.
* This may be null.
*/
public void setMainInfo(IMainInfo mainInfo) {
mMainInfo = mainInfo;
}
// --- UI creation ---
/**
* Creates the top level control for this dialog page under the given parent
* composite.
*
* @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite)
*/
public void createControl(Composite parent) {
final ScrolledComposite scrolledComposite = new ScrolledComposite(parent, SWT.V_SCROLL);
scrolledComposite.setFont(parent.getFont());
scrolledComposite.setExpandHorizontal(true);
scrolledComposite.setExpandVertical(true);
initializeDialogUnits(parent);
final Composite composite = new Composite(scrolledComposite, SWT.NULL);
composite.setFont(parent.getFont());
scrolledComposite.setContent(composite);
composite.setLayout(new GridLayout());
composite.setLayoutData(new GridData(GridData.FILL_BOTH));
createToggleTestProject(composite);
createTestProjectGroup(composite);
createLocationGroup(composite);
createTestTargetGroup(composite);
createTargetGroup(composite);
createPropertiesGroup(composite);
createWorkingSetGroup(composite);
// Update state the first time
enableLocationWidgets();
scrolledComposite.addControlListener(new ControlAdapter() {
@Override
public void controlResized(ControlEvent e) {
Rectangle r = scrolledComposite.getClientArea();
scrolledComposite.setMinSize(composite.computeSize(r.width, SWT.DEFAULT));
}
});
// Show description the first time
setErrorMessage(null);
setMessage(null);
setControl(scrolledComposite);
// Validate. This will complain about the first empty field.
validatePageComplete();
}
/**
* Overrides @DialogPage.setVisible(boolean) to put the focus in the project name when
* the dialog is made visible and to also update the enabled/disabled state of some
* controls (doing so in createControl doesn't always change their state somehow.)
*/
@Override
public void setVisible(boolean visible) {
super.setVisible(visible);
if (visible) {
mProjectNameField.setFocus();
validatePageComplete();
onCreateTestProjectToggle();
onExistingProjectChanged();
}
}
@Override
public void dispose() {
if (mSdkTargetChangeListener != null) {
AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener);
mSdkTargetChangeListener = null;
}
super.dispose();
}
/**
* Creates the "create test project" checkbox but only if there's a main page in the wizard.
*
* @param parent the parent composite
*/
private final void createToggleTestProject(Composite parent) {
if (mMainInfo != null) {
mCreateTestProjectField = new Button(parent, SWT.CHECK);
mCreateTestProjectField.setText("Create a Test Project");
mCreateTestProjectField.setToolTipText("Select this if you also want to create a Test Project.");
mCreateTestProjectField.setSelection(INITIAL_CREATE_TEST_PROJECT);
mCreateTestProjectField.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onCreateTestProjectToggle();
}
});
}
}
/**
* Creates the group for the project name:
* [label: "Project Name"] [text field]
*
* @param parent the parent composite
*/
private final void createTestProjectGroup(Composite parent) {
Composite group = new Composite(parent, SWT.NONE);
GridLayout layout = new GridLayout();
layout.numColumns = 2;
group.setLayout(layout);
group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mToggleComposites.add(group);
// --- test project name ---
// new project label
String tooltip = "Name of the Eclipse test project to create. It cannot be empty.";
Label label = new Label(group, SWT.NONE);
label.setText("Test Project Name:");
label.setFont(parent.getFont());
label.setToolTipText(tooltip);
// new project name entry field
mProjectNameField = new Text(group, SWT.BORDER);
GridData data = new GridData(GridData.FILL_HORIZONTAL);
mProjectNameField.setToolTipText(tooltip);
mProjectNameField.setLayoutData(data);
mProjectNameField.setFont(parent.getFont());
mProjectNameField.addListener(SWT.Modify, new Listener() {
public void handleEvent(Event event) {
if (!mInternalProjectNameUpdate) {
mProjectNameModifiedByUser = true;
}
updateLocationPathField(null);
}
});
}
private final void createLocationGroup(Composite parent) {
// --- project location ---
Group group = new Group(parent, SWT.SHADOW_ETCHED_IN);
group.setLayout(new GridLayout(3, /* num columns */
false /* columns of not equal size */));
group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
group.setFont(parent.getFont());
group.setText("Content");
mToggleComposites.add(group);
mUseDefaultLocation = new Button(group, SWT.CHECK);
GridData gd = new GridData(GridData.FILL_HORIZONTAL);
gd.horizontalSpan = 3;
mUseDefaultLocation.setLayoutData(gd);
mUseDefaultLocation.setText("Use default location");
mUseDefaultLocation.setSelection(INITIAL_USE_DEFAULT_LOCATION);
mUseDefaultLocation.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
super.widgetSelected(e);
enableLocationWidgets();
validatePageComplete();
}
});
mLocationLabel = new Label(group, SWT.NONE);
mLocationLabel.setText("Location:");
mLocationPathField = new Text(group, SWT.BORDER);
GridData data = new GridData(GridData.FILL, /* horizontal alignment */
GridData.BEGINNING, /* vertical alignment */
true, /* grabExcessHorizontalSpace */
false, /* grabExcessVerticalSpace */
1, /* horizontalSpan */
1); /* verticalSpan */
mLocationPathField.setLayoutData(data);
mLocationPathField.setFont(parent.getFont());
mLocationPathField.addListener(SWT.Modify, new Listener() {
public void handleEvent(Event event) {
onLocationPathFieldModified();
}
});
mBrowseButton = new Button(group, SWT.PUSH);
mBrowseButton.setText("Browse...");
setButtonLayoutData(mBrowseButton);
mBrowseButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onOpenDirectoryBrowser();
}
});
}
/**
* Creates the group for Test Target options.
*
* There are two different modes here:
* <ul>
* <li>When mMainInfo exists, this is part of a new Android Project. In which case
* the new test is tied to the soon-to-be main project and there is actually no choice.
* <li>When mMainInfo does not exist, this is a standalone new test project. In this case
* we offer 2 options for the test target: self test or against an existing Android project.
* </ul>
*
* @param parent the parent composite
*/
private final void createTestTargetGroup(Composite parent) {
Group group = new Group(parent, SWT.SHADOW_ETCHED_IN);
GridLayout layout = new GridLayout();
layout.numColumns = 3;
group.setLayout(layout);
group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
group.setFont(parent.getFont());
group.setText("Test Target");
mToggleComposites.add(group);
if (mMainInfo == null) {
// Standalone mode: choose between self-test and existing-project test
Label label = new Label(group, SWT.NONE);
label.setText("Select the project to test:");
GridData gd = new GridData(GridData.FILL_HORIZONTAL);
gd.horizontalSpan = 3;
label.setLayoutData(gd);
mTestSelfProjectRadio = new Button(group, SWT.RADIO);
mTestSelfProjectRadio.setText("This project");
mTestSelfProjectRadio.setSelection(false);
gd = new GridData(GridData.FILL_HORIZONTAL);
gd.horizontalSpan = 3;
mTestSelfProjectRadio.setLayoutData(gd);
mTestExistingProjectRadio = new Button(group, SWT.RADIO);
mTestExistingProjectRadio.setText("An existing Android project");
mTestExistingProjectRadio.setSelection(mMainInfo == null);
mTestExistingProjectRadio.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onExistingProjectChanged();
}
});
String tooltip = "The existing Android Project that is being tested.";
mTestedProjectNameField = new Text(group, SWT.BORDER);
mTestedProjectNameField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mTestedProjectNameField.setToolTipText(tooltip);
mTestedProjectNameField.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent e) {
onProjectFieldUpdated();
}
});
mProjectBrowseButton = new Button(group, SWT.NONE);
mProjectBrowseButton.setText("Browse...");
mProjectBrowseButton.setToolTipText("Allows you to select the Android project to test.");
mProjectBrowseButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onProjectBrowse();
}
});
mProjectChooserHelper = new ProjectChooserHelper(parent.getShell(), null /*filter*/);
} else {
// Part of NPW mode: no selection.
}
// package label line
Label label = new Label(group, SWT.NONE);
label.setText("Test Target Package:");
mTestTargetPackageLabel = new Label(group, SWT.NONE);
GridData gd = new GridData(GridData.FILL_HORIZONTAL);
gd.horizontalSpan = 2;
mTestTargetPackageLabel.setLayoutData(gd);
}
/**
* Creates the target group.
* It only contains an SdkTargetSelector.
*/
private void createTargetGroup(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");
mToggleComposites.add(group);
// The selector is created without targets. They are added below in the change listener.
mSdkTargetSelector = new SdkTargetSelector(group, null);
mSdkTargetChangeListener = new ITargetChangeListener() {
public void onSdkLoaded() {
// 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
if (targets != null && targets.length == 1) {
mSdkTargetSelector.setSelection(targets[0]);
}
}
public void onProjectTargetChange(IProject changedProject) {
// Ignore
}
public void onTargetLoaded(IAndroidTarget target) {
// Ignore
}
};
AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener);
// Invoke it once to initialize the targets
mSdkTargetChangeListener.onSdkLoaded();
mSdkTargetSelector.setSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onSdkTargetModified();
updateLocationPathField(null);
validatePageComplete();
}
});
}
/**
* Creates the group for the project properties:
* - Package name [text field]
* - Activity name [text field]
* - Application name [text field]
*
* @param parent the parent composite
*/
private final void createPropertiesGroup(Composite parent) {
// package specification group
Group group = new Group(parent, SWT.SHADOW_ETCHED_IN);
GridLayout layout = new GridLayout();
layout.numColumns = 2;
group.setLayout(layout);
group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
group.setFont(parent.getFont());
group.setText("Properties");
mToggleComposites.add(group);
// new application label
Label label = new Label(group, SWT.NONE);
label.setText("Application name:");
label.setFont(parent.getFont());
label.setToolTipText("Name of the Application. This is a free string. It can be empty.");
// new application name entry field
mApplicationNameField = new Text(group, SWT.BORDER);
GridData data = new GridData(GridData.FILL_HORIZONTAL);
mApplicationNameField.setToolTipText("Name of the Application. This is a free string. It can be empty.");
mApplicationNameField.setLayoutData(data);
mApplicationNameField.setFont(parent.getFont());
mApplicationNameField.addListener(SWT.Modify, new Listener() {
public void handleEvent(Event event) {
if (!mInternalApplicationNameUpdate) {
mApplicationNameModifiedByUser = true;
}
}
});
// new package label
label = new Label(group, SWT.NONE);
label.setText("Package name:");
label.setFont(parent.getFont());
label.setToolTipText("Namespace of the Package to create. This must be a Java namespace with at least two components.");
// new package name entry field
mPackageNameField = new Text(group, SWT.BORDER);
data = new GridData(GridData.FILL_HORIZONTAL);
mPackageNameField.setToolTipText("Namespace of the Package to create. This must be a Java namespace with at least two components.");
mPackageNameField.setLayoutData(data);
mPackageNameField.setFont(parent.getFont());
mPackageNameField.addListener(SWT.Modify, new Listener() {
public void handleEvent(Event event) {
if (!mInternalPackageNameUpdate) {
mPackageNameModifiedByUser = true;
}
onPackageNameFieldModified();
}
});
// min sdk version label
label = new Label(group, SWT.NONE);
label.setText("Min SDK Version:");
label.setFont(parent.getFont());
label.setToolTipText("The minimum SDK version number that the application requires. Must be an integer > 0. It can be empty.");
// min sdk version entry field
mMinSdkVersionField = new Text(group, SWT.BORDER);
data = new GridData(GridData.FILL_HORIZONTAL);
label.setToolTipText("The minimum SDK version number that the application requires. Must be an integer > 0. It can be empty.");
mMinSdkVersionField.setLayoutData(data);
mMinSdkVersionField.setFont(parent.getFont());
mMinSdkVersionField.addListener(SWT.Modify, new Listener() {
public void handleEvent(Event event) {
onMinSdkVersionFieldModified();
validatePageComplete();
}
});
}
private void createWorkingSetGroup(final Composite composite) {
Composite group = mWorkingSetGroup.createControl(composite);
group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mToggleComposites.add(group);
}
//--- Internal getters & setters ------------------
/** Returns the location path field value with spaces trimmed. */
private String getLocationPathFieldValue() {
return mLocationPathField == null ? "" : mLocationPathField.getText().trim(); //$NON-NLS-1$
}
/** Returns the current project location, depending on the Use Default Location check box. */
private String getProjectLocation() {
if (mInfo.useDefaultLocation()) {
return Platform.getLocation().toString();
} else {
return getLocationPathFieldValue();
}
}
/**
* Creates a project resource handle for the current project name field
* value.
* <p>
* This method does not create the project resource; this is the
* responsibility of <code>IProject::create</code> invoked by the new
* project resource wizard.
* </p>
*
* @return the new project resource handle
*/
private IProject getProjectHandle() {
return ResourcesPlugin.getWorkspace().getRoot().getProject(mInfo.getProjectName());
}
// --- UI Callbacks ----
/**
* Callback invoked when the user toggles the "Test target: Existing Android Project"
* checkbox. It enables or disable the UI to select an existing project.
*/
private void onExistingProjectChanged() {
if (mInfo.isTestingExisting()) {
boolean enabled = mTestExistingProjectRadio.getSelection();
mTestedProjectNameField.setEnabled(enabled);
mProjectBrowseButton.setEnabled(enabled);
setExistingProject(mInfo.getExistingTestedProject());
validatePageComplete();
}
}
/**
* Tries to load the defaults from the main page if possible.
*/
private void useMainProjectInformation() {
if (mInfo.isTestingMain() && mMainInfo != null) {
useMainWorkingSets();
String projName = String.format("%1$sTest", mMainInfo.getProjectName());
String appName = String.format("%1$sTest", mMainInfo.getApplicationName());
String packageName = mMainInfo.getPackageName();
if (packageName == null) {
packageName = ""; //$NON-NLS-1$
}
updateTestTargetPackageField(packageName);
if (!mProjectNameModifiedByUser) {
mInternalProjectNameUpdate = true;
mProjectNameField.setText(projName); //$NON-NLS-1$
mInternalProjectNameUpdate = false;
}
if (!mApplicationNameModifiedByUser) {
mInternalApplicationNameUpdate = true;
mApplicationNameField.setText(appName);
mInternalApplicationNameUpdate = false;
}
if (!mPackageNameModifiedByUser) {
mInternalPackageNameUpdate = true;
packageName += ".test"; //$NON-NLS-1$
mPackageNameField.setText(packageName);
mInternalPackageNameUpdate = false;
}
if (!mSdkTargetModifiedByUser) {
mInternalSdkTargetUpdate = true;
mSdkTargetSelector.setSelection(mMainInfo.getSdkTarget());
mInternalSdkTargetUpdate = false;
}
if (!mMinSdkVersionModifiedByUser) {
mInternalMinSdkVersionUpdate = true;
mMinSdkVersionField.setText(mMainInfo.getMinSdkVersion());
mInternalMinSdkVersionUpdate = false;
}
}
}
private void useMainWorkingSets() {
IWorkingSet[] workingSets = mMainInfo.getSelectedWorkingSets();
if (workingSets != null) {
// getSelectedWorkingSets returns an empty list if the working set feature is disabled.
if (workingSets.length > 0) {
mWorkingSetGroup.setChecked(true);
}
mWorkingSetGroup.setWorkingSets(workingSets);
}
}
/**
* Callback invoked when the user edits the project text field.
*/
private void onProjectFieldUpdated() {
String project = mTestedProjectNameField.getText();
// Is this a valid project?
IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null /*javaModel*/);
for (IJavaProject p : projects) {
if (p.getProject().getName().equals(project)) {
setExistingProject(p.getProject());
return;
}
}
}
/**
* Callback called when the user uses the "Browse Projects" button.
*/
private void onProjectBrowse() {
IJavaProject p = mProjectChooserHelper.chooseJavaProject(mTestedProjectNameField.getText(),
null /*message*/);
if (p != null) {
setExistingProject(p.getProject());
mTestedProjectNameField.setText(mExistingTestedProject.getName());
}
}
private void setExistingProject(IProject project) {
mExistingTestedProject = project;
// Try to update the application, package, sdk target and minSdkVersion accordingly
if (project != null &&
(!mApplicationNameModifiedByUser ||
!mPackageNameModifiedByUser ||
!mSdkTargetModifiedByUser ||
!mMinSdkVersionModifiedByUser)) {
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 (!mProjectNameModifiedByUser) {
mInternalProjectNameUpdate = true;
mProjectNameField.setText(appName);
mInternalProjectNameUpdate = false;
}
if (!mApplicationNameModifiedByUser) {
mInternalApplicationNameUpdate = true;
mApplicationNameField.setText(appName);
mInternalApplicationNameUpdate = false;
}
if (!mPackageNameModifiedByUser) {
mInternalPackageNameUpdate = true;
packageName += ".test"; //$NON-NLS-1$
mPackageNameField.setText(packageName); //$NON-NLS-1$
mInternalPackageNameUpdate = false;
}
if (!mSdkTargetModifiedByUser && sdkTarget != null) {
mInternalSdkTargetUpdate = true;
mSdkTargetSelector.setSelection(sdkTarget);
mInternalSdkTargetUpdate = false;
}
if (!mMinSdkVersionModifiedByUser) {
mInternalMinSdkVersionUpdate = true;
if (minSdkVersion != null) {
mMinSdkVersionField.setText(minSdkVersion);
}
if (sdkTarget == null) {
updateSdkSelectorToMatchMinSdkVersion();
}
mInternalMinSdkVersionUpdate = false;
}
}
}
updateTestTargetPackageField(mLastExistingPackageName);
validatePageComplete();
}
/**
* Display a directory browser and update the location path field with the selected path
*/
private void onOpenDirectoryBrowser() {
String existing_dir = getLocationPathFieldValue();
// Disable the path if it doesn't exist
if (existing_dir.length() == 0) {
existing_dir = null;
} else {
File f = new File(existing_dir);
if (!f.exists()) {
existing_dir = null;
}
}
DirectoryDialog dd = new DirectoryDialog(mLocationPathField.getShell());
dd.setMessage("Browse for folder");
dd.setFilterPath(existing_dir);
String abs_dir = dd.open();
if (abs_dir != null) {
updateLocationPathField(abs_dir);
validatePageComplete();
}
}
/**
* Callback when the "create test project" checkbox is changed.
* It enables or disables all UI groups accordingly.
*/
private void onCreateTestProjectToggle() {
boolean enabled = mInfo.getCreateTestProject();
for (Composite c : mToggleComposites) {
enableControl(c, enabled);
}
mSdkTargetSelector.setEnabled(enabled);
if (enabled) {
useMainProjectInformation();
}
validatePageComplete();
}
/** Enables or disables controls; recursive for composite controls. */
private void enableControl(Control c, boolean enabled) {
c.setEnabled(enabled);
if (c instanceof Composite)
for (Control c2 : ((Composite) c).getChildren()) {
enableControl(c2, enabled);
}
}
/**
* 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 enableLocationWidgets() {
boolean use_default = mInfo.useDefaultLocation();
boolean location_enabled = !use_default;
mLocationLabel.setEnabled(location_enabled);
mLocationPathField.setEnabled(location_enabled);
mBrowseButton.setEnabled(location_enabled);
updateLocationPathField(null);
}
/**
* Updates the location directory path field.
* <br/>
* When custom user selection is enabled, use the abs_dir argument if not null and also
* save it internally. If abs_dir is null, restore the last saved abs_dir. 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 abs_dir.
*
* @param abs_dir A new absolute directory path or null to use the default.
*/
private void updateLocationPathField(String abs_dir) {
boolean use_default = mInfo.useDefaultLocation();
boolean custom_location = !use_default;
if (!mInternalLocationPathUpdate) {
mInternalLocationPathUpdate = true;
if (custom_location) {
if (abs_dir != null) {
// We get here if the user selected a directory with the "Browse" button.
sCustomLocationOsPath = TextProcessor.process(abs_dir);
}
if (!mLocationPathField.getText().equals(sCustomLocationOsPath)) {
mLocationPathField.setText(sCustomLocationOsPath);
}
} else {
String value = Platform.getLocation().append(mInfo.getProjectName()).toString();
value = TextProcessor.process(value);
if (!mLocationPathField.getText().equals(value)) {
mLocationPathField.setText(value);
}
}
validatePageComplete();
mInternalLocationPathUpdate = false;
}
}
/**
* The location path field is either modified internally (from updateLocationPathField)
* or manually by the user when the custom_location mode is not set.
*
* Ignore the internal modification. When modified by the user, memorize the choice and
* validate the page.
*/
private void onLocationPathFieldModified() {
if (!mInternalLocationPathUpdate) {
// When the updates doesn't come from updateLocationPathField, it must be the user
// editing the field manually, in which case we want to save the value internally
String newPath = getLocationPathFieldValue();
sCustomLocationOsPath = newPath;
validatePageComplete();
}
}
/**
* The package name field is either modified internally (from extractNamesFromAndroidManifest)
* or manually by the user when the custom_location mode is not set.
*
* Ignore the internal modification. When modified by the user, memorize the choice and
* validate the page.
*/
private void onPackageNameFieldModified() {
updateTestTargetPackageField(null);
validatePageComplete();
}
/**
* Changes the {@link #mTestTargetPackageLabel} field.
*
* 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 (mInfo.isTestingSelf()) {
mTestTargetPackageLabel.setText(mInfo.getPackageName());
} else if (packageName != null) {
mTestTargetPackageLabel.setText(packageName);
}
}
/**
* Called when the min sdk version field has been modified.
*
* Ignore the internal modifications. When modified by the user, try to match
* a target with the same API level.
*/
private void onMinSdkVersionFieldModified() {
if (mInternalMinSdkVersionUpdate || mInternalSdkTargetUpdate) {
return;
}
updateSdkSelectorToMatchMinSdkVersion();
mMinSdkVersionModifiedByUser = true;
}
/**
* 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.
*/
private void updateSdkSelectorToMatchMinSdkVersion() {
String minSdkVersion = mInfo.getMinSdkVersion();
IAndroidTarget curr_target = mInfo.getSdkTarget();
if (curr_target != null && curr_target.getVersion().equals(minSdkVersion)) {
return;
}
for (IAndroidTarget target : mSdkTargetSelector.getTargets()) {
if (target.getVersion().equals(minSdkVersion)) {
mSdkTargetSelector.setSelection(target);
return;
}
}
}
/**
* 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 (mInternalMinSdkVersionUpdate || mInternalSdkTargetUpdate) {
return;
}
IAndroidTarget target = mInfo.getSdkTarget();
if (target != null) {
mInternalMinSdkVersionUpdate = true;
mMinSdkVersionField.setText(target.getVersion().getApiString());
mInternalMinSdkVersionUpdate = false;
}
mSdkTargetModifiedByUser = true;
}
/**
* Returns whether this page's controls currently all contain valid values.
*
* @return <code>true</code> if all controls are valid, and
* <code>false</code> if at least one is invalid
*/
private boolean validatePage() {
IWorkspace workspace = ResourcesPlugin.getWorkspace();
int status = MSG_NONE;
// there is nothing to validate if we're not going to create a test project
if (mInfo.getCreateTestProject()) {
status = validateProjectField(workspace);
if ((status & MSG_ERROR) == 0) {
status |= validateLocationPath(workspace);
}
if ((status & MSG_ERROR) == 0) {
status |= validateTestTarget();
}
if ((status & MSG_ERROR) == 0) {
status |= validateSdkTarget();
}
if ((status & MSG_ERROR) == 0) {
status |= validatePackageField();
}
if ((status & MSG_ERROR) == 0) {
status |= validateMinSdkVersionField();
}
}
if (status == MSG_NONE) {
setStatus(null, MSG_NONE);
}
// Return false if there's an error so that the finish button be disabled.
return (status & MSG_ERROR) == 0;
}
/**
* Validates the page and updates the Next/Finish buttons
*/
private void validatePageComplete() {
setPageComplete(validatePage());
}
/**
* Validates the test target (self, main project or existing project)
*
* @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
*/
private int validateTestTarget() {
if (mInfo.isTestingExisting() && mInfo.getExistingTestedProject() == null) {
return setStatus("Please select an existing Android project as a test target.",
MSG_ERROR);
}
return MSG_NONE;
}
/**
* Validates the project name field.
*
* @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
*/
private int validateProjectField(IWorkspace workspace) {
// Validate project field
String projectName = mInfo.getProjectName();
if (projectName.length() == 0) {
return setStatus("Project name must be specified", MSG_ERROR);
}
// Limit the project name to shell-agnostic characters since it will be used to
// generate the final package
if (!sProjectNamePattern.matcher(projectName).matches()) {
return setStatus("The project name must start with an alphanumeric characters, followed by one or more alphanumerics, digits, dots, dashes, underscores or spaces.",
MSG_ERROR);
}
IStatus nameStatus = workspace.validateName(projectName, IResource.PROJECT);
if (!nameStatus.isOK()) {
return setStatus(nameStatus.getMessage(), MSG_ERROR);
}
if (mMainInfo != null && projectName.equals(mMainInfo.getProjectName())) {
return setStatus("The main project name and the test project name must be different.",
MSG_ERROR);
}
if (getProjectHandle().exists()) {
return setStatus("A project with that name already exists in the workspace",
MSG_ERROR);
}
return MSG_NONE;
}
/**
* Validates the location path field.
*
* @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
*/
private int validateLocationPath(IWorkspace workspace) {
Path path = new Path(getProjectLocation());
if (!mInfo.useDefaultLocation()) {
// If not using the default value validate the location.
URI uri = URIUtil.toURI(path.toOSString());
IStatus locationStatus = workspace.validateProjectLocationURI(getProjectHandle(),
uri);
if (!locationStatus.isOK()) {
return setStatus(locationStatus.getMessage(), MSG_ERROR);
} else {
// 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 setStatus("A directory name must be specified.", MSG_ERROR);
} 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.length != 0) {
return setStatus("The selected output directory is not empty.",
MSG_WARNING);
}
}
}
} else {
// Otherwise validate the path string is not empty
if (getProjectLocation().length() == 0) {
return setStatus("A directory name must be specified.", MSG_ERROR);
}
File dest = path.append(mInfo.getProjectName()).toFile();
if (dest.exists()) {
return setStatus(String.format("There is already a file or directory named \"%1$s\" in the selected location.",
mInfo.getProjectName()), MSG_ERROR);
}
}
return MSG_NONE;
}
/**
* Validates the sdk target choice.
*
* @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
*/
private int validateSdkTarget() {
if (mInfo.getSdkTarget() == null) {
return setStatus("An SDK Target must be specified.", MSG_ERROR);
}
return MSG_NONE;
}
/**
* Validates the sdk target choice.
*
* @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
*/
private int validateMinSdkVersionField() {
// If the min sdk version is empty, it is always accepted.
if (mInfo.getMinSdkVersion().length() == 0) {
return MSG_NONE;
}
if (mInfo.getSdkTarget() != null &&
mInfo.getSdkTarget().getVersion().equals(mInfo.getMinSdkVersion()) == false) {
return setStatus("The API level for the selected SDK target does not match the Min SDK version.",
mInfo.getSdkTarget().getVersion().isPreview() ? MSG_ERROR : MSG_WARNING);
}
return MSG_NONE;
}
/**
* Validates the package name field.
*
* @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
*/
private int validatePackageField() {
// Validate package field
String packageName = mInfo.getPackageName();
if (packageName.length() == 0) {
return setStatus("Project package name must be specified.", MSG_ERROR);
}
// Check it's a valid package string
int result = MSG_NONE;
IStatus status = JavaConventions.validatePackageName(packageName, "1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$
if (!status.isOK()) {
result = setStatus(String.format("Project package: %s", status.getMessage()),
status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING);
}
// 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 (result != MSG_ERROR && packageName.indexOf('.') == -1) {
return setStatus("Project package name must have at least two identifiers.", MSG_ERROR);
}
// Check that the target package name is valid too
packageName = mInfo.getTargetPackageName();
if (packageName.length() == 0) {
return setStatus("Target package name must be specified.", MSG_ERROR);
}
// Check it's a valid package string
status = JavaConventions.validatePackageName(packageName, "1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$
if (!status.isOK()) {
result = setStatus(String.format("Target package: %s", status.getMessage()),
status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING);
}
if (result != MSG_ERROR && packageName.indexOf('.') == -1) {
return setStatus("Target name must have at least two identifiers.", MSG_ERROR);
}
return result;
}
/**
* Sets the error message for the wizard with the given message icon.
*
* @param message The wizard message type, one of MSG_ERROR or MSG_WARNING.
* @return As a convenience, always returns messageType so that the caller can return
* immediately.
*/
private int setStatus(String message, int messageType) {
if (message == null) {
setErrorMessage(null);
setMessage(null);
} else if (!message.equals(getMessage())) {
setMessage(message, messageType == MSG_WARNING ? WizardPage.WARNING : WizardPage.ERROR);
}
return messageType;
}
/**
* 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
*/
public IWorkingSet[] getWorkingSets() {
return mWorkingSetGroup.getSelectedWorkingSets();
}
/**
* Sets the working sets to which the new project should be added.
*
* @param workingSets the initial selected working sets
*/
public void setWorkingSets(IWorkingSet[] workingSets) {
assert workingSets != null;
mWorkingSetGroup.setWorkingSets(workingSets);
}
}