2 * Copyright (C) 2007 The Android Open Source Project
4 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.eclipse.org/org/documents/epl-v10.php
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
19 * org.eclipse.jdt.internal.ui.wizards.JavaProjectWizard
20 * org.eclipse.jdt.internal.ui.wizards.JavaProjectWizardFirstPage
23 package com.android.ide.eclipse.adt.internal.wizards.newproject;
25 import com.android.ide.eclipse.adt.AdtPlugin;
26 import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
27 import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper;
28 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
29 import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
30 import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectCreationPage.IMainInfo;
31 import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectCreationPage.MainInfo;
32 import com.android.sdklib.IAndroidTarget;
33 import com.android.sdklib.SdkConstants;
34 import com.android.sdklib.xml.ManifestData;
35 import com.android.sdkuilib.internal.widgets.SdkTargetSelector;
37 import org.eclipse.core.filesystem.URIUtil;
38 import org.eclipse.core.resources.IProject;
39 import org.eclipse.core.resources.IResource;
40 import org.eclipse.core.resources.IWorkspace;
41 import org.eclipse.core.resources.ResourcesPlugin;
42 import org.eclipse.core.runtime.IPath;
43 import org.eclipse.core.runtime.IStatus;
44 import org.eclipse.core.runtime.Path;
45 import org.eclipse.core.runtime.Platform;
46 import org.eclipse.jdt.core.IJavaProject;
47 import org.eclipse.jdt.core.JavaConventions;
48 import org.eclipse.jface.wizard.WizardPage;
49 import org.eclipse.osgi.util.TextProcessor;
50 import org.eclipse.swt.SWT;
51 import org.eclipse.swt.custom.ScrolledComposite;
52 import org.eclipse.swt.events.ControlAdapter;
53 import org.eclipse.swt.events.ControlEvent;
54 import org.eclipse.swt.events.ModifyEvent;
55 import org.eclipse.swt.events.ModifyListener;
56 import org.eclipse.swt.events.SelectionAdapter;
57 import org.eclipse.swt.events.SelectionEvent;
58 import org.eclipse.swt.graphics.Rectangle;
59 import org.eclipse.swt.layout.GridData;
60 import org.eclipse.swt.layout.GridLayout;
61 import org.eclipse.swt.widgets.Button;
62 import org.eclipse.swt.widgets.Composite;
63 import org.eclipse.swt.widgets.Control;
64 import org.eclipse.swt.widgets.DirectoryDialog;
65 import org.eclipse.swt.widgets.Event;
66 import org.eclipse.swt.widgets.Group;
67 import org.eclipse.swt.widgets.Label;
68 import org.eclipse.swt.widgets.Listener;
69 import org.eclipse.swt.widgets.Text;
73 import java.util.ArrayList;
74 import java.util.regex.Pattern;
77 * NewAndroidProjectCreationPage is a project creation page that provides the
82 * <li> Application name
86 * Note: this class is public so that it can be accessed from unit tests.
87 * It is however an internal class. Its API may change without notice.
88 * It should semantically be considered as a private final class.
89 * Do not derive from this class.
91 public class NewTestProjectCreationPage extends WizardPage {
94 static final String TEST_PAGE_NAME = "newAndroidTestProjectPage"; //$NON-NLS-1$
96 /** Initial value for all name fields (project, activity, application, package). Used
97 * whenever a value is requested before controls are created. */
98 private static final String INITIAL_NAME = ""; //$NON-NLS-1$
99 /** Initial value for the Use Default Location check box. */
100 private static final boolean INITIAL_USE_DEFAULT_LOCATION = true;
101 /** Initial value for the Create Test Project check box. */
102 private static final boolean INITIAL_CREATE_TEST_PROJECT = false;
105 /** Pattern for characters accepted in a project name. Since this will be used as a
106 * directory name, we're being a bit conservative on purpose. It cannot start with a space. */
107 private static final Pattern sProjectNamePattern = Pattern.compile("^[\\w][\\w. -]*$"); //$NON-NLS-1$
108 /** Last user-browsed location, static so that it be remembered for the whole session */
109 private static String sCustomLocationOsPath = ""; //$NON-NLS-1$
111 private final int MSG_NONE = 0;
112 private final int MSG_WARNING = 1;
113 private final int MSG_ERROR = 2;
115 /** Structure with the externally visible information from this Test Project page. */
116 private final TestInfo mInfo = new TestInfo();
117 /** Structure with the externally visible information from the Main Project page.
118 * This is null if there's no such page, meaning the test project page is standalone. */
119 private IMainInfo mMainInfo;
122 private Text mProjectNameField;
123 private Text mPackageNameField;
124 private Text mApplicationNameField;
125 private Button mUseDefaultLocation;
126 private Label mLocationLabel;
127 private Text mLocationPathField;
128 private Button mBrowseButton;
129 private Text mMinSdkVersionField;
130 private SdkTargetSelector mSdkTargetSelector;
131 private ITargetChangeListener mSdkTargetChangeListener;
132 private Button mCreateTestProjectField;
133 private Text mTestedProjectNameField;
134 private Button mProjectBrowseButton;
135 private ProjectChooserHelper mProjectChooserHelper;
136 private Button mTestSelfProjectRadio;
137 private Button mTestExistingProjectRadio;
139 /** A list of composites that are disabled when the "Create Test Project" toggle is off. */
140 private ArrayList<Composite> mToggleComposites = new ArrayList<Composite>();
142 private boolean mInternalProjectNameUpdate;
143 private boolean mInternalLocationPathUpdate;
144 private boolean mInternalPackageNameUpdate;
145 private boolean mInternalApplicationNameUpdate;
146 private boolean mInternalMinSdkVersionUpdate;
147 private boolean mInternalSdkTargetUpdate;
148 private IProject mExistingTestedProject;
149 private boolean mProjectNameModifiedByUser;
150 private boolean mApplicationNameModifiedByUser;
151 private boolean mPackageNameModifiedByUser;
152 private boolean mMinSdkVersionModifiedByUser;
153 private boolean mSdkTargetModifiedByUser;
155 private Label mTestTargetPackageLabel;
157 private String mLastExistingPackageName;
161 * Creates a new project creation wizard page.
163 public NewTestProjectCreationPage() {
164 super(TEST_PAGE_NAME);
165 setPageComplete(false);
166 setTitle("New Android Test Project");
167 setDescription("Creates a new Android Test Project resource.");
170 // --- Getters used by NewProjectWizard ---
173 * Structure that collects all externally visible information from this page.
174 * This is used by the calling wizard to actually do the work or by other pages.
176 public class TestInfo {
178 /** Returns true if a new Test Project should be created. */
179 public boolean getCreateTestProject() {
180 return mCreateTestProjectField == null ? true : mCreateTestProjectField.getSelection();
184 * Returns the current project location path as entered by the user, or its
185 * anticipated initial value. Note that if the default has been returned the
186 * path in a project description used to create a project should not be set.
188 * @return the project location path or its anticipated initial value.
190 public IPath getLocationPath() {
191 return new Path(getProjectLocation());
194 /** Returns the value of the project name field with leading and trailing spaces removed. */
195 public String getProjectName() {
196 return mProjectNameField == null ? INITIAL_NAME : mProjectNameField.getText().trim();
199 /** Returns the value of the package name field with spaces trimmed. */
200 public String getPackageName() {
201 return mPackageNameField == null ? INITIAL_NAME : mPackageNameField.getText().trim();
204 /** Returns the value of the test target package name field with spaces trimmed. */
205 public String getTargetPackageName() {
206 return mTestTargetPackageLabel == null ? INITIAL_NAME
207 : mTestTargetPackageLabel.getText().trim();
210 /** Returns the value of the min sdk version field with spaces trimmed. */
211 public String getMinSdkVersion() {
212 return mMinSdkVersionField == null ? "" : mMinSdkVersionField.getText().trim(); //$NON-NLS-1$
215 /** Returns the value of the application name field with spaces trimmed. */
216 public String getApplicationName() {
217 // Return the name of the activity as default application name.
218 return mApplicationNameField == null ? "" : mApplicationNameField.getText().trim(); //$NON-NLS-1$
221 /** Returns the value of the Use Default Location field. */
222 public boolean useDefaultLocation() {
223 return mUseDefaultLocation == null ? INITIAL_USE_DEFAULT_LOCATION
224 : mUseDefaultLocation.getSelection();
227 /** Returns the the default "src" constant. */
228 public String getSourceFolder() {
229 return SdkConstants.FD_SOURCES;
232 /** Returns the current sdk target or null if none has been selected yet. */
233 public IAndroidTarget getSdkTarget() {
234 return mSdkTargetSelector == null ? null : mSdkTargetSelector.getSelected();
237 public boolean isTestingSelf() {
238 return mMainInfo == null &&
239 (mTestSelfProjectRadio == null ? false : mTestSelfProjectRadio.getSelection());
242 public boolean isTestingMain() {
243 return mMainInfo != null;
246 public boolean isTestingExisting() {
247 return mMainInfo == null &&
248 (mTestExistingProjectRadio == null ? false
249 : mTestExistingProjectRadio.getSelection());
252 public IProject getExistingTestedProject() {
253 return mExistingTestedProject;
258 * Returns a {@link TestInfo} structure that collects all externally visible information
259 * from this page. This is used by the calling wizard to actually do the work or by other pages.
261 public TestInfo getTestInfo() {
266 * Grabs the {@link MainInfo} structure with visible parameters from the main project page.
269 public void setMainInfo(IMainInfo mainInfo) {
270 mMainInfo = mainInfo;
273 // --- UI creation ---
276 * Creates the top level control for this dialog page under the given parent
279 * @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite)
281 public void createControl(Composite parent) {
282 final ScrolledComposite scrolledComposite = new ScrolledComposite(parent, SWT.V_SCROLL);
283 scrolledComposite.setFont(parent.getFont());
284 scrolledComposite.setExpandHorizontal(true);
285 scrolledComposite.setExpandVertical(true);
286 initializeDialogUnits(parent);
288 final Composite composite = new Composite(scrolledComposite, SWT.NULL);
289 composite.setFont(parent.getFont());
290 scrolledComposite.setContent(composite);
292 composite.setLayout(new GridLayout());
293 composite.setLayoutData(new GridData(GridData.FILL_BOTH));
295 createToggleTestProject(composite);
296 createTestProjectGroup(composite);
297 createLocationGroup(composite);
298 createTestTargetGroup(composite);
299 createTargetGroup(composite);
300 createPropertiesGroup(composite);
302 // Update state the first time
303 enableLocationWidgets();
305 scrolledComposite.addControlListener(new ControlAdapter() {
307 public void controlResized(ControlEvent e) {
308 Rectangle r = scrolledComposite.getClientArea();
309 scrolledComposite.setMinSize(composite.computeSize(r.width, SWT.DEFAULT));
313 // Show description the first time
314 setErrorMessage(null);
316 setControl(scrolledComposite);
318 // Validate. This will complain about the first empty field.
319 validatePageComplete();
323 * Overrides @DialogPage.setVisible(boolean) to put the focus in the project name when
324 * the dialog is made visible and to also update the enabled/disabled state of some
325 * controls (doing so in createControl doesn't always change their state somehow.)
328 public void setVisible(boolean visible) {
329 super.setVisible(visible);
331 mProjectNameField.setFocus();
332 validatePageComplete();
333 onCreateTestProjectToggle();
334 onExistingProjectChanged();
339 public void dispose() {
341 if (mSdkTargetChangeListener != null) {
342 AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener);
343 mSdkTargetChangeListener = null;
350 * Creates the "create test project" checkbox but only if there's a main page in the wizard.
352 * @param parent the parent composite
354 private final void createToggleTestProject(Composite parent) {
356 if (mMainInfo != null) {
357 mCreateTestProjectField = new Button(parent, SWT.CHECK);
358 mCreateTestProjectField.setText("Create a Test Project");
359 mCreateTestProjectField.setToolTipText("Select this if you also want to create a Test Project.");
360 mCreateTestProjectField.setSelection(INITIAL_CREATE_TEST_PROJECT);
361 mCreateTestProjectField.addSelectionListener(new SelectionAdapter() {
363 public void widgetSelected(SelectionEvent e) {
364 onCreateTestProjectToggle();
371 * Creates the group for the project name:
372 * [label: "Project Name"] [text field]
374 * @param parent the parent composite
376 private final void createTestProjectGroup(Composite parent) {
377 Composite group = new Composite(parent, SWT.NONE);
378 GridLayout layout = new GridLayout();
379 layout.numColumns = 2;
380 group.setLayout(layout);
381 group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
383 mToggleComposites.add(group);
385 // --- test project name ---
388 String tooltip = "Name of the Eclipse test project to create. It cannot be empty.";
389 Label label = new Label(group, SWT.NONE);
390 label.setText("Test Project Name:");
391 label.setFont(parent.getFont());
392 label.setToolTipText(tooltip);
394 // new project name entry field
395 mProjectNameField = new Text(group, SWT.BORDER);
396 GridData data = new GridData(GridData.FILL_HORIZONTAL);
397 mProjectNameField.setToolTipText(tooltip);
398 mProjectNameField.setLayoutData(data);
399 mProjectNameField.setFont(parent.getFont());
400 mProjectNameField.addListener(SWT.Modify, new Listener() {
401 public void handleEvent(Event event) {
402 if (!mInternalProjectNameUpdate) {
403 mProjectNameModifiedByUser = true;
405 updateLocationPathField(null);
410 private final void createLocationGroup(Composite parent) {
412 // --- project location ---
414 Group group = new Group(parent, SWT.SHADOW_ETCHED_IN);
415 group.setLayout(new GridLayout(3, /* num columns */
416 false /* columns of not equal size */));
417 group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
418 group.setFont(parent.getFont());
419 group.setText("Content");
421 mToggleComposites.add(group);
423 mUseDefaultLocation = new Button(group, SWT.CHECK);
424 GridData gd = new GridData(GridData.FILL_HORIZONTAL);
425 gd.horizontalSpan = 3;
426 mUseDefaultLocation.setLayoutData(gd);
427 mUseDefaultLocation.setText("Use default location");
428 mUseDefaultLocation.setSelection(INITIAL_USE_DEFAULT_LOCATION);
430 mUseDefaultLocation.addSelectionListener(new SelectionAdapter() {
432 public void widgetSelected(SelectionEvent e) {
433 super.widgetSelected(e);
434 enableLocationWidgets();
435 validatePageComplete();
440 mLocationLabel = new Label(group, SWT.NONE);
441 mLocationLabel.setText("Location:");
443 mLocationPathField = new Text(group, SWT.BORDER);
444 GridData data = new GridData(GridData.FILL, /* horizontal alignment */
445 GridData.BEGINNING, /* vertical alignment */
446 true, /* grabExcessHorizontalSpace */
447 false, /* grabExcessVerticalSpace */
448 1, /* horizontalSpan */
449 1); /* verticalSpan */
450 mLocationPathField.setLayoutData(data);
451 mLocationPathField.setFont(parent.getFont());
452 mLocationPathField.addListener(SWT.Modify, new Listener() {
453 public void handleEvent(Event event) {
454 onLocationPathFieldModified();
458 mBrowseButton = new Button(group, SWT.PUSH);
459 mBrowseButton.setText("Browse...");
460 setButtonLayoutData(mBrowseButton);
461 mBrowseButton.addSelectionListener(new SelectionAdapter() {
463 public void widgetSelected(SelectionEvent e) {
464 onOpenDirectoryBrowser();
470 * Creates the group for Test Target options.
472 * There are two different modes here:
474 * <li>When mMainInfo exists, this is part of a new Android Project. In which case
475 * the new test is tied to the soon-to-be main project and there is actually no choice.
476 * <li>When mMainInfo does not exist, this is a standalone new test project. In this case
477 * we offer 2 options for the test target: self test or against an existing Android project.
480 * @param parent the parent composite
482 private final void createTestTargetGroup(Composite parent) {
484 Group group = new Group(parent, SWT.SHADOW_ETCHED_IN);
485 GridLayout layout = new GridLayout();
486 layout.numColumns = 3;
487 group.setLayout(layout);
488 group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
489 group.setFont(parent.getFont());
490 group.setText("Test Target");
492 mToggleComposites.add(group);
494 if (mMainInfo == null) {
495 // Standalone mode: choose between self-test and existing-project test
497 Label label = new Label(group, SWT.NONE);
498 label.setText("Select the project to test:");
499 GridData gd = new GridData(GridData.FILL_HORIZONTAL);
500 gd.horizontalSpan = 3;
501 label.setLayoutData(gd);
503 mTestSelfProjectRadio = new Button(group, SWT.RADIO);
504 mTestSelfProjectRadio.setText("This project");
505 mTestSelfProjectRadio.setSelection(false);
506 gd = new GridData(GridData.FILL_HORIZONTAL);
507 gd.horizontalSpan = 3;
508 mTestSelfProjectRadio.setLayoutData(gd);
510 mTestExistingProjectRadio = new Button(group, SWT.RADIO);
511 mTestExistingProjectRadio.setText("An existing Android project");
512 mTestExistingProjectRadio.setSelection(mMainInfo == null);
513 mTestExistingProjectRadio.addSelectionListener(new SelectionAdapter() {
515 public void widgetSelected(SelectionEvent e) {
516 onExistingProjectChanged();
520 String tooltip = "The existing Android Project that is being tested.";
522 mTestedProjectNameField = new Text(group, SWT.BORDER);
523 mTestedProjectNameField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
524 mTestedProjectNameField.setToolTipText(tooltip);
525 mTestedProjectNameField.addModifyListener(new ModifyListener() {
526 public void modifyText(ModifyEvent e) {
527 onProjectFieldUpdated();
531 mProjectBrowseButton = new Button(group, SWT.NONE);
532 mProjectBrowseButton.setText("Browse...");
533 mProjectBrowseButton.setToolTipText("Allows you to select the Android project to test.");
534 mProjectBrowseButton.addSelectionListener(new SelectionAdapter() {
536 public void widgetSelected(SelectionEvent e) {
541 mProjectChooserHelper = new ProjectChooserHelper(parent.getShell(), null /*filter*/);
543 // Part of NPW mode: no selection.
547 // package label line
549 Label label = new Label(group, SWT.NONE);
550 label.setText("Test Target Package:");
551 mTestTargetPackageLabel = new Label(group, SWT.NONE);
552 GridData gd = new GridData(GridData.FILL_HORIZONTAL);
553 gd.horizontalSpan = 2;
554 mTestTargetPackageLabel.setLayoutData(gd);
558 * Creates the target group.
559 * It only contains an SdkTargetSelector.
561 private void createTargetGroup(Composite parent) {
562 Group group = new Group(parent, SWT.SHADOW_ETCHED_IN);
563 // Layout has 1 column
564 group.setLayout(new GridLayout());
565 group.setLayoutData(new GridData(GridData.FILL_BOTH));
566 group.setFont(parent.getFont());
567 group.setText("Build Target");
569 mToggleComposites.add(group);
571 // The selector is created without targets. They are added below in the change listener.
572 mSdkTargetSelector = new SdkTargetSelector(group, null);
574 mSdkTargetChangeListener = new ITargetChangeListener() {
575 public void onSdkLoaded() {
576 // Update the sdk target selector with the new targets
578 // get the targets from the sdk
579 IAndroidTarget[] targets = null;
580 if (Sdk.getCurrent() != null) {
581 targets = Sdk.getCurrent().getTargets();
583 mSdkTargetSelector.setTargets(targets);
585 // If there's only one target, select it
586 if (targets != null && targets.length == 1) {
587 mSdkTargetSelector.setSelection(targets[0]);
591 public void onProjectTargetChange(IProject changedProject) {
595 public void onTargetLoaded(IAndroidTarget target) {
600 AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener);
602 // Invoke it once to initialize the targets
603 mSdkTargetChangeListener.onSdkLoaded();
605 mSdkTargetSelector.setSelectionListener(new SelectionAdapter() {
607 public void widgetSelected(SelectionEvent e) {
608 onSdkTargetModified();
609 updateLocationPathField(null);
610 validatePageComplete();
616 * Creates the group for the project properties:
617 * - Package name [text field]
618 * - Activity name [text field]
619 * - Application name [text field]
621 * @param parent the parent composite
623 private final void createPropertiesGroup(Composite parent) {
624 // package specification group
625 Group group = new Group(parent, SWT.SHADOW_ETCHED_IN);
626 GridLayout layout = new GridLayout();
627 layout.numColumns = 2;
628 group.setLayout(layout);
629 group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
630 group.setFont(parent.getFont());
631 group.setText("Properties");
633 mToggleComposites.add(group);
635 // new application label
636 Label label = new Label(group, SWT.NONE);
637 label.setText("Application name:");
638 label.setFont(parent.getFont());
639 label.setToolTipText("Name of the Application. This is a free string. It can be empty.");
641 // new application name entry field
642 mApplicationNameField = new Text(group, SWT.BORDER);
643 GridData data = new GridData(GridData.FILL_HORIZONTAL);
644 mApplicationNameField.setToolTipText("Name of the Application. This is a free string. It can be empty.");
645 mApplicationNameField.setLayoutData(data);
646 mApplicationNameField.setFont(parent.getFont());
647 mApplicationNameField.addListener(SWT.Modify, new Listener() {
648 public void handleEvent(Event event) {
649 if (!mInternalApplicationNameUpdate) {
650 mApplicationNameModifiedByUser = true;
656 label = new Label(group, SWT.NONE);
657 label.setText("Package name:");
658 label.setFont(parent.getFont());
659 label.setToolTipText("Namespace of the Package to create. This must be a Java namespace with at least two components.");
661 // new package name entry field
662 mPackageNameField = new Text(group, SWT.BORDER);
663 data = new GridData(GridData.FILL_HORIZONTAL);
664 mPackageNameField.setToolTipText("Namespace of the Package to create. This must be a Java namespace with at least two components.");
665 mPackageNameField.setLayoutData(data);
666 mPackageNameField.setFont(parent.getFont());
667 mPackageNameField.addListener(SWT.Modify, new Listener() {
668 public void handleEvent(Event event) {
669 if (!mInternalPackageNameUpdate) {
670 mPackageNameModifiedByUser = true;
672 onPackageNameFieldModified();
676 // min sdk version label
677 label = new Label(group, SWT.NONE);
678 label.setText("Min SDK Version:");
679 label.setFont(parent.getFont());
680 label.setToolTipText("The minimum SDK version number that the application requires. Must be an integer > 0. It can be empty.");
682 // min sdk version entry field
683 mMinSdkVersionField = new Text(group, SWT.BORDER);
684 data = new GridData(GridData.FILL_HORIZONTAL);
685 label.setToolTipText("The minimum SDK version number that the application requires. Must be an integer > 0. It can be empty.");
686 mMinSdkVersionField.setLayoutData(data);
687 mMinSdkVersionField.setFont(parent.getFont());
688 mMinSdkVersionField.addListener(SWT.Modify, new Listener() {
689 public void handleEvent(Event event) {
690 onMinSdkVersionFieldModified();
691 validatePageComplete();
697 //--- Internal getters & setters ------------------
699 /** Returns the location path field value with spaces trimmed. */
700 private String getLocationPathFieldValue() {
701 return mLocationPathField == null ? "" : mLocationPathField.getText().trim(); //$NON-NLS-1$
704 /** Returns the current project location, depending on the Use Default Location check box. */
705 private String getProjectLocation() {
706 if (mInfo.useDefaultLocation()) {
707 return Platform.getLocation().toString();
709 return getLocationPathFieldValue();
714 * Creates a project resource handle for the current project name field
717 * This method does not create the project resource; this is the
718 * responsibility of <code>IProject::create</code> invoked by the new
719 * project resource wizard.
722 * @return the new project resource handle
724 private IProject getProjectHandle() {
725 return ResourcesPlugin.getWorkspace().getRoot().getProject(mInfo.getProjectName());
728 // --- UI Callbacks ----
731 * Callback invoked when the user toggles the "Test target: Existing Android Project"
732 * checkbox. It enables or disable the UI to select an existing project.
734 private void onExistingProjectChanged() {
735 if (mInfo.isTestingExisting()) {
736 boolean enabled = mTestExistingProjectRadio.getSelection();
737 mTestedProjectNameField.setEnabled(enabled);
738 mProjectBrowseButton.setEnabled(enabled);
739 setExistingProject(mInfo.getExistingTestedProject());
740 validatePageComplete();
745 * Tries to load the defaults from the main page if possible.
747 private void useMainProjectInformation() {
748 if (mInfo.isTestingMain() && mMainInfo != null) {
750 String projName = String.format("%1$sTest", mMainInfo.getProjectName());
751 String appName = String.format("%1$sTest", mMainInfo.getApplicationName());
753 String packageName = mMainInfo.getPackageName();
754 if (packageName == null) {
755 packageName = ""; //$NON-NLS-1$
758 updateTestTargetPackageField(packageName);
760 if (!mProjectNameModifiedByUser) {
761 mInternalProjectNameUpdate = true;
762 mProjectNameField.setText(projName); //$NON-NLS-1$
763 mInternalProjectNameUpdate = false;
766 if (!mApplicationNameModifiedByUser) {
767 mInternalApplicationNameUpdate = true;
768 mApplicationNameField.setText(appName);
769 mInternalApplicationNameUpdate = false;
772 if (!mPackageNameModifiedByUser) {
773 mInternalPackageNameUpdate = true;
774 packageName += ".test"; //$NON-NLS-1$
775 mPackageNameField.setText(packageName);
776 mInternalPackageNameUpdate = false;
779 if (!mSdkTargetModifiedByUser) {
780 mInternalSdkTargetUpdate = true;
781 mSdkTargetSelector.setSelection(mMainInfo.getSdkTarget());
782 mInternalSdkTargetUpdate = false;
785 if (!mMinSdkVersionModifiedByUser) {
786 mInternalMinSdkVersionUpdate = true;
787 mMinSdkVersionField.setText(mMainInfo.getMinSdkVersion());
788 mInternalMinSdkVersionUpdate = false;
794 * Callback invoked when the user edits the project text field.
796 private void onProjectFieldUpdated() {
797 String project = mTestedProjectNameField.getText();
799 // Is this a valid project?
800 IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null /*javaModel*/);
801 for (IJavaProject p : projects) {
802 if (p.getProject().getName().equals(project)) {
803 setExistingProject(p.getProject());
810 * Callback called when the user uses the "Browse Projects" button.
812 private void onProjectBrowse() {
813 IJavaProject p = mProjectChooserHelper.chooseJavaProject(mTestedProjectNameField.getText(),
816 setExistingProject(p.getProject());
817 mTestedProjectNameField.setText(mExistingTestedProject.getName());
821 private void setExistingProject(IProject project) {
822 mExistingTestedProject = project;
824 // Try to update the application, package, sdk target and minSdkVersion accordingly
825 if (project != null &&
826 (!mApplicationNameModifiedByUser ||
827 !mPackageNameModifiedByUser ||
828 !mSdkTargetModifiedByUser ||
829 !mMinSdkVersionModifiedByUser)) {
831 ManifestData manifestData = AndroidManifestHelper.parseForData(project);
832 if (manifestData != null) {
833 String appName = String.format("%1$sTest", project.getName());
834 String packageName = manifestData.getPackage();
835 String minSdkVersion = manifestData.getMinSdkVersionString();
836 IAndroidTarget sdkTarget = null;
837 if (Sdk.getCurrent() != null) {
838 sdkTarget = Sdk.getCurrent().getTarget(project);
841 if (packageName == null) {
842 packageName = ""; //$NON-NLS-1$
844 mLastExistingPackageName = packageName;
846 if (!mProjectNameModifiedByUser) {
847 mInternalProjectNameUpdate = true;
848 mProjectNameField.setText(appName);
849 mInternalProjectNameUpdate = false;
852 if (!mApplicationNameModifiedByUser) {
853 mInternalApplicationNameUpdate = true;
854 mApplicationNameField.setText(appName);
855 mInternalApplicationNameUpdate = false;
858 if (!mPackageNameModifiedByUser) {
859 mInternalPackageNameUpdate = true;
860 packageName += ".test"; //$NON-NLS-1$
861 mPackageNameField.setText(packageName); //$NON-NLS-1$
862 mInternalPackageNameUpdate = false;
865 if (!mSdkTargetModifiedByUser && sdkTarget != null) {
866 mInternalSdkTargetUpdate = true;
867 mSdkTargetSelector.setSelection(sdkTarget);
868 mInternalSdkTargetUpdate = false;
871 if (!mMinSdkVersionModifiedByUser) {
872 mInternalMinSdkVersionUpdate = true;
873 if (minSdkVersion != null) {
874 mMinSdkVersionField.setText(minSdkVersion);
876 if (sdkTarget == null) {
877 updateSdkSelectorToMatchMinSdkVersion();
879 mInternalMinSdkVersionUpdate = false;
884 updateTestTargetPackageField(mLastExistingPackageName);
885 validatePageComplete();
889 * Display a directory browser and update the location path field with the selected path
891 private void onOpenDirectoryBrowser() {
893 String existing_dir = getLocationPathFieldValue();
895 // Disable the path if it doesn't exist
896 if (existing_dir.length() == 0) {
899 File f = new File(existing_dir);
905 DirectoryDialog dd = new DirectoryDialog(mLocationPathField.getShell());
906 dd.setMessage("Browse for folder");
907 dd.setFilterPath(existing_dir);
908 String abs_dir = dd.open();
910 if (abs_dir != null) {
911 updateLocationPathField(abs_dir);
912 validatePageComplete();
917 * Callback when the "create test project" checkbox is changed.
918 * It enables or disables all UI groups accordingly.
920 private void onCreateTestProjectToggle() {
921 boolean enabled = mInfo.getCreateTestProject();
922 for (Composite c : mToggleComposites) {
923 enableControl(c, enabled);
925 mSdkTargetSelector.setEnabled(enabled);
928 useMainProjectInformation();
930 validatePageComplete();
933 /** Enables or disables controls; recursive for composite controls. */
934 private void enableControl(Control c, boolean enabled) {
935 c.setEnabled(enabled);
936 if (c instanceof Composite)
937 for (Control c2 : ((Composite) c).getChildren()) {
938 enableControl(c2, enabled);
943 * Enables or disable the location widgets depending on the user selection:
944 * the location path is enabled when using the "existing source" mode (i.e. not new project)
945 * or in new project mode with the "use default location" turned off.
947 private void enableLocationWidgets() {
948 boolean use_default = mInfo.useDefaultLocation();
949 boolean location_enabled = !use_default;
951 mLocationLabel.setEnabled(location_enabled);
952 mLocationPathField.setEnabled(location_enabled);
953 mBrowseButton.setEnabled(location_enabled);
955 updateLocationPathField(null);
959 * Updates the location directory path field.
961 * When custom user selection is enabled, use the abs_dir argument if not null and also
962 * save it internally. If abs_dir is null, restore the last saved abs_dir. This allows the
963 * user selection to be remembered when the user switches from default to custom.
965 * When custom user selection is disabled, use the workspace default location with the
966 * current project name. This does not change the internally cached abs_dir.
968 * @param abs_dir A new absolute directory path or null to use the default.
970 private void updateLocationPathField(String abs_dir) {
971 boolean use_default = mInfo.useDefaultLocation();
972 boolean custom_location = !use_default;
974 if (!mInternalLocationPathUpdate) {
975 mInternalLocationPathUpdate = true;
976 if (custom_location) {
977 if (abs_dir != null) {
978 // We get here if the user selected a directory with the "Browse" button.
979 sCustomLocationOsPath = TextProcessor.process(abs_dir);
981 if (!mLocationPathField.getText().equals(sCustomLocationOsPath)) {
982 mLocationPathField.setText(sCustomLocationOsPath);
985 String value = Platform.getLocation().append(mInfo.getProjectName()).toString();
986 value = TextProcessor.process(value);
987 if (!mLocationPathField.getText().equals(value)) {
988 mLocationPathField.setText(value);
991 validatePageComplete();
992 mInternalLocationPathUpdate = false;
997 * The location path field is either modified internally (from updateLocationPathField)
998 * or manually by the user when the custom_location mode is not set.
1000 * Ignore the internal modification. When modified by the user, memorize the choice and
1001 * validate the page.
1003 private void onLocationPathFieldModified() {
1004 if (!mInternalLocationPathUpdate) {
1005 // When the updates doesn't come from updateLocationPathField, it must be the user
1006 // editing the field manually, in which case we want to save the value internally
1007 String newPath = getLocationPathFieldValue();
1008 sCustomLocationOsPath = newPath;
1009 validatePageComplete();
1014 * The package name field is either modified internally (from extractNamesFromAndroidManifest)
1015 * or manually by the user when the custom_location mode is not set.
1017 * Ignore the internal modification. When modified by the user, memorize the choice and
1018 * validate the page.
1020 private void onPackageNameFieldModified() {
1021 updateTestTargetPackageField(null);
1022 validatePageComplete();
1026 * Changes the {@link #mTestTargetPackageLabel} field.
1028 * When using the "self-test" option, the packageName argument is ignored and the
1029 * current value from the project package is used.
1031 * Otherwise the packageName is used if it is not null.
1033 private void updateTestTargetPackageField(String packageName) {
1034 if (mInfo.isTestingSelf()) {
1035 mTestTargetPackageLabel.setText(mInfo.getPackageName());
1037 } else if (packageName != null) {
1038 mTestTargetPackageLabel.setText(packageName);
1043 * Called when the min sdk version field has been modified.
1045 * Ignore the internal modifications. When modified by the user, try to match
1046 * a target with the same API level.
1048 private void onMinSdkVersionFieldModified() {
1049 if (mInternalMinSdkVersionUpdate || mInternalSdkTargetUpdate) {
1053 updateSdkSelectorToMatchMinSdkVersion();
1055 mMinSdkVersionModifiedByUser = true;
1059 * Try to find an SDK Target that matches the current MinSdkVersion.
1061 * There can be multiple targets with the same sdk api version, so don't change
1062 * it if it's already at the right version. Otherwise pick the first target
1065 private void updateSdkSelectorToMatchMinSdkVersion() {
1066 String minSdkVersion = mInfo.getMinSdkVersion();
1068 IAndroidTarget curr_target = mInfo.getSdkTarget();
1069 if (curr_target != null && curr_target.getVersion().equals(minSdkVersion)) {
1073 for (IAndroidTarget target : mSdkTargetSelector.getTargets()) {
1074 if (target.getVersion().equals(minSdkVersion)) {
1075 mSdkTargetSelector.setSelection(target);
1082 * Called when an SDK target is modified.
1084 * Also changes the minSdkVersion field to reflect the sdk api level that has
1085 * just been selected.
1087 private void onSdkTargetModified() {
1088 if (mInternalMinSdkVersionUpdate || mInternalSdkTargetUpdate) {
1092 IAndroidTarget target = mInfo.getSdkTarget();
1094 if (target != null) {
1095 mInternalMinSdkVersionUpdate = true;
1096 mMinSdkVersionField.setText(target.getVersion().getApiString());
1097 mInternalMinSdkVersionUpdate = false;
1100 mSdkTargetModifiedByUser = true;
1104 * Returns whether this page's controls currently all contain valid values.
1106 * @return <code>true</code> if all controls are valid, and
1107 * <code>false</code> if at least one is invalid
1109 private boolean validatePage() {
1110 IWorkspace workspace = ResourcesPlugin.getWorkspace();
1112 int status = MSG_NONE;
1114 // there is nothing to validate if we're not going to create a test project
1115 if (mInfo.getCreateTestProject()) {
1116 status = validateProjectField(workspace);
1117 if ((status & MSG_ERROR) == 0) {
1118 status |= validateLocationPath(workspace);
1120 if ((status & MSG_ERROR) == 0) {
1121 status |= validateTestTarget();
1123 if ((status & MSG_ERROR) == 0) {
1124 status |= validateSdkTarget();
1126 if ((status & MSG_ERROR) == 0) {
1127 status |= validatePackageField();
1129 if ((status & MSG_ERROR) == 0) {
1130 status |= validateMinSdkVersionField();
1133 if (status == MSG_NONE) {
1134 setStatus(null, MSG_NONE);
1137 // Return false if there's an error so that the finish button be disabled.
1138 return (status & MSG_ERROR) == 0;
1142 * Validates the page and updates the Next/Finish buttons
1144 private void validatePageComplete() {
1145 setPageComplete(validatePage());
1149 * Validates the test target (self, main project or existing project)
1151 * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
1153 private int validateTestTarget() {
1154 if (mInfo.isTestingExisting() && mInfo.getExistingTestedProject() == null) {
1155 return setStatus("Please select an existing Android project as a test target.",
1163 * Validates the project name field.
1165 * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
1167 private int validateProjectField(IWorkspace workspace) {
1168 // Validate project field
1169 String projectName = mInfo.getProjectName();
1170 if (projectName.length() == 0) {
1171 return setStatus("Project name must be specified", MSG_ERROR);
1174 // Limit the project name to shell-agnostic characters since it will be used to
1175 // generate the final package
1176 if (!sProjectNamePattern.matcher(projectName).matches()) {
1177 return setStatus("The project name must start with an alphanumeric characters, followed by one or more alphanumerics, digits, dots, dashes, underscores or spaces.",
1181 IStatus nameStatus = workspace.validateName(projectName, IResource.PROJECT);
1182 if (!nameStatus.isOK()) {
1183 return setStatus(nameStatus.getMessage(), MSG_ERROR);
1186 if (mMainInfo != null && projectName.equals(mMainInfo.getProjectName())) {
1187 return setStatus("The main project name and the test project name must be different.",
1191 if (getProjectHandle().exists()) {
1192 return setStatus("A project with that name already exists in the workspace",
1200 * Validates the location path field.
1202 * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
1204 private int validateLocationPath(IWorkspace workspace) {
1205 Path path = new Path(getProjectLocation());
1206 if (!mInfo.useDefaultLocation()) {
1207 // If not using the default value validate the location.
1208 URI uri = URIUtil.toURI(path.toOSString());
1209 IStatus locationStatus = workspace.validateProjectLocationURI(getProjectHandle(),
1211 if (!locationStatus.isOK()) {
1212 return setStatus(locationStatus.getMessage(), MSG_ERROR);
1214 // The location is valid as far as Eclipse is concerned (i.e. mostly not
1215 // an existing workspace project.) Check it either doesn't exist or is
1216 // a directory that is empty.
1217 File f = path.toFile();
1218 if (f.exists() && !f.isDirectory()) {
1219 return setStatus("A directory name must be specified.", MSG_ERROR);
1220 } else if (f.isDirectory()) {
1221 // However if the directory exists, we should put a warning if it is not
1222 // empty. We don't put an error (we'll ask the user again for confirmation
1223 // before using the directory.)
1224 String[] l = f.list();
1225 if (l.length != 0) {
1226 return setStatus("The selected output directory is not empty.",
1232 // Otherwise validate the path string is not empty
1233 if (getProjectLocation().length() == 0) {
1234 return setStatus("A directory name must be specified.", MSG_ERROR);
1237 File dest = path.append(mInfo.getProjectName()).toFile();
1238 if (dest.exists()) {
1239 return setStatus(String.format("There is already a file or directory named \"%1$s\" in the selected location.",
1240 mInfo.getProjectName()), MSG_ERROR);
1248 * Validates the sdk target choice.
1250 * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
1252 private int validateSdkTarget() {
1253 if (mInfo.getSdkTarget() == null) {
1254 return setStatus("An SDK Target must be specified.", MSG_ERROR);
1260 * Validates the sdk target choice.
1262 * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
1264 private int validateMinSdkVersionField() {
1266 // If the min sdk version is empty, it is always accepted.
1267 if (mInfo.getMinSdkVersion().length() == 0) {
1271 if (mInfo.getSdkTarget() != null &&
1272 mInfo.getSdkTarget().getVersion().equals(mInfo.getMinSdkVersion()) == false) {
1273 return setStatus("The API level for the selected SDK target does not match the Min SDK version.",
1274 mInfo.getSdkTarget().getVersion().isPreview() ? MSG_ERROR : MSG_WARNING);
1281 * Validates the package name field.
1283 * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
1285 private int validatePackageField() {
1286 // Validate package field
1287 String packageName = mInfo.getPackageName();
1288 if (packageName.length() == 0) {
1289 return setStatus("Project package name must be specified.", MSG_ERROR);
1292 // Check it's a valid package string
1293 int result = MSG_NONE;
1294 IStatus status = JavaConventions.validatePackageName(packageName, "1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$
1295 if (!status.isOK()) {
1296 result = setStatus(String.format("Project package: %s", status.getMessage()),
1297 status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING);
1300 // The Android Activity Manager does not accept packages names with only one
1301 // identifier. Check the package name has at least one dot in them (the previous rule
1302 // validated that if such a dot exist, it's not the first nor last characters of the
1304 if (result != MSG_ERROR && packageName.indexOf('.') == -1) {
1305 return setStatus("Project package name must have at least two identifiers.", MSG_ERROR);
1308 // Check that the target package name is valid too
1309 packageName = mInfo.getTargetPackageName();
1310 if (packageName.length() == 0) {
1311 return setStatus("Target package name must be specified.", MSG_ERROR);
1314 // Check it's a valid package string
1315 status = JavaConventions.validatePackageName(packageName, "1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$
1316 if (!status.isOK()) {
1317 result = setStatus(String.format("Target package: %s", status.getMessage()),
1318 status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING);
1321 if (result != MSG_ERROR && packageName.indexOf('.') == -1) {
1322 return setStatus("Target name must have at least two identifiers.", MSG_ERROR);
1329 * Sets the error message for the wizard with the given message icon.
1331 * @param message The wizard message type, one of MSG_ERROR or MSG_WARNING.
1332 * @return As a convenience, always returns messageType so that the caller can return
1335 private int setStatus(String message, int messageType) {
1336 if (message == null) {
1337 setErrorMessage(null);
1339 } else if (!message.equals(getMessage())) {
1340 setMessage(message, messageType == MSG_WARNING ? WizardPage.WARNING : WizardPage.ERROR);