1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 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 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 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. 15 */ 16 package com.android.ide.eclipse.adt.internal.wizards.newproject; 17 18 import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE; 19 import static com.android.SdkConstants.OS_SDK_TOOLS_LIB_FOLDER; 20 import static com.android.ide.eclipse.adt.AdtUtils.capitalize; 21 import static com.android.ide.eclipse.adt.internal.wizards.newproject.ApplicationInfoPage.ACTIVITY_NAME_SUFFIX; 22 import static com.android.utils.SdkUtils.stripWhitespace; 23 24 import com.android.SdkConstants; 25 import com.android.ide.common.xml.ManifestData; 26 import com.android.ide.common.xml.ManifestData.Activity; 27 import com.android.ide.eclipse.adt.AdtPlugin; 28 import com.android.ide.eclipse.adt.internal.VersionCheck; 29 import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; 30 import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; 31 32 import org.eclipse.core.filesystem.URIUtil; 33 import org.eclipse.core.resources.IProject; 34 import org.eclipse.core.resources.IResource; 35 import org.eclipse.core.resources.IWorkspace; 36 import org.eclipse.core.resources.ResourcesPlugin; 37 import org.eclipse.core.runtime.IStatus; 38 import org.eclipse.core.runtime.Path; 39 import org.eclipse.core.runtime.Platform; 40 import org.eclipse.core.runtime.Status; 41 import org.eclipse.jface.dialogs.IMessageProvider; 42 import org.eclipse.jface.viewers.IStructuredSelection; 43 import org.eclipse.jface.wizard.IWizardPage; 44 import org.eclipse.jface.wizard.WizardPage; 45 import org.eclipse.osgi.util.TextProcessor; 46 import org.eclipse.swt.SWT; 47 import org.eclipse.swt.events.ModifyEvent; 48 import org.eclipse.swt.events.ModifyListener; 49 import org.eclipse.swt.events.SelectionEvent; 50 import org.eclipse.swt.events.SelectionListener; 51 import org.eclipse.swt.layout.GridData; 52 import org.eclipse.swt.layout.GridLayout; 53 import org.eclipse.swt.widgets.Button; 54 import org.eclipse.swt.widgets.Composite; 55 import org.eclipse.swt.widgets.DirectoryDialog; 56 import org.eclipse.swt.widgets.Label; 57 import org.eclipse.swt.widgets.Text; 58 import org.eclipse.ui.IWorkbenchPart; 59 import org.eclipse.ui.IWorkingSet; 60 61 import java.io.File; 62 import java.net.URI; 63 import java.util.Locale; 64 65 /** 66 * Initial page shown when creating projects which asks for the project name, 67 * the the location of the project, working sets, etc. 68 */ 69 public class ProjectNamePage extends WizardPage implements SelectionListener, ModifyListener { 70 private final NewProjectWizardState mValues; 71 /** Flag used when setting button/text state manually to ignore listener updates */ 72 private boolean mIgnore; 73 /** Last user-browsed location, static so that it be remembered for the whole session */ 74 private static String sCustomLocationOsPath = ""; //$NON-NLS-1$ 75 private static boolean sAutoComputeCustomLocation = true; 76 77 private Text mProjectNameText; 78 private Text mLocationText; 79 private Button mCreateSampleRadioButton; 80 private Button mCreateNewButton; 81 private Button mUseDefaultCheckBox; 82 private Button mBrowseButton; 83 private Label mLocationLabel; 84 private WorkingSetGroup mWorkingSetGroup; 85 /** 86 * Whether we've made sure the Tools are up to date (enough that all the 87 * resources required by the New Project wizard are present -- we don't 88 * necessarily check for newer versions than that here; that's done by 89 * {@link VersionCheck}, though that check doesn't <b>enforce</b> an update 90 * since it needs to allow the user to proceed to access the SDK manager 91 * etc.) 92 */ 93 private boolean mCheckedSdkUptodate; 94 95 /** 96 * Create the wizard. 97 * @param values current wizard state 98 */ 99 ProjectNamePage(NewProjectWizardState values) { 100 super("projectNamePage"); //$NON-NLS-1$ 101 mValues = values; 102 103 setTitle("Create Android Project"); 104 setDescription("Select project name and type of project"); 105 mWorkingSetGroup = new WorkingSetGroup(); 106 setWorkingSets(new IWorkingSet[0]); 107 } 108 109 void init(IStructuredSelection selection, IWorkbenchPart activePart) { 110 setWorkingSets(WorkingSetHelper.getSelectedWorkingSet(selection, activePart)); 111 } 112 113 /** 114 * Create contents of the wizard. 115 * @param parent the parent to add the page to 116 */ 117 @Override 118 public void createControl(Composite parent) { 119 Composite container = new Composite(parent, SWT.NULL); 120 container.setLayout(new GridLayout(3, false)); 121 122 Label nameLabel = new Label(container, SWT.NONE); 123 nameLabel.setText("Project Name:"); 124 125 mProjectNameText = new Text(container, SWT.BORDER); 126 mProjectNameText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); 127 mProjectNameText.addModifyListener(this); 128 129 if (mValues.mode != Mode.TEST) { 130 mCreateNewButton = new Button(container, SWT.RADIO); 131 mCreateNewButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1)); 132 mCreateNewButton.setText("Create new project in workspace"); 133 mCreateNewButton.addSelectionListener(this); 134 135 // TBD: Should we hide this completely, and make samples something you only invoke 136 // from the "New Sample Project" wizard? 137 mCreateSampleRadioButton = new Button(container, SWT.RADIO); 138 mCreateSampleRadioButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 139 3, 1)); 140 mCreateSampleRadioButton.setText("Create project from existing sample"); 141 mCreateSampleRadioButton.addSelectionListener(this); 142 } 143 144 Label separator = new Label(container, SWT.SEPARATOR | SWT.HORIZONTAL); 145 separator.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 3, 1)); 146 147 mUseDefaultCheckBox = new Button(container, SWT.CHECK); 148 mUseDefaultCheckBox.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1)); 149 mUseDefaultCheckBox.setText("Use default location"); 150 mUseDefaultCheckBox.addSelectionListener(this); 151 152 mLocationLabel = new Label(container, SWT.NONE); 153 mLocationLabel.setText("Location:"); 154 155 mLocationText = new Text(container, SWT.BORDER); 156 mLocationText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); 157 mLocationText.addModifyListener(this); 158 159 mBrowseButton = new Button(container, SWT.NONE); 160 mBrowseButton.setText("Browse..."); 161 mBrowseButton.addSelectionListener(this); 162 163 Composite group = mWorkingSetGroup.createControl(container); 164 group.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false, 3, 1)); 165 166 setControl(container); 167 } 168 169 @Override 170 public void setVisible(boolean visible) { 171 super.setVisible(visible); 172 173 if (visible) { 174 try { 175 mIgnore = true; 176 if (mValues.projectName != null) { 177 mProjectNameText.setText(mValues.projectName); 178 mProjectNameText.setFocus(); 179 } 180 if (mValues.mode == Mode.ANY || mValues.mode == Mode.TEST) { 181 if (mValues.useExisting) { 182 assert false; // This is now handled by the separate import wizard 183 } else if (mCreateNewButton != null) { 184 mCreateNewButton.setSelection(true); 185 } 186 } else if (mValues.mode == Mode.SAMPLE) { 187 mCreateSampleRadioButton.setSelection(true); 188 } 189 if (mValues.projectLocation != null) { 190 mLocationText.setText(mValues.projectLocation.getPath()); 191 } 192 mUseDefaultCheckBox.setSelection(mValues.useDefaultLocation); 193 updateLocationState(); 194 } finally { 195 mIgnore = false; 196 } 197 } 198 199 validatePage(); 200 } 201 202 @Override 203 public void modifyText(ModifyEvent e) { 204 if (mIgnore) { 205 return; 206 } 207 208 Object source = e.getSource(); 209 210 if (source == mProjectNameText) { 211 onProjectFieldModified(); 212 if (!mValues.useDefaultLocation && !mValues.projectLocationModifiedByUser) { 213 updateLocationPathField(null); 214 } 215 } else if (source == mLocationText) { 216 mValues.projectLocationModifiedByUser = true; 217 if (!mValues.useDefaultLocation) { 218 File f = new File(mLocationText.getText().trim()); 219 mValues.projectLocation = f; 220 if (f.exists() && f.isDirectory() && !f.equals(mValues.projectLocation)) { 221 updateLocationPathField(mValues.projectLocation.getPath()); 222 } 223 } 224 } 225 226 validatePage(); 227 } 228 229 private void onProjectFieldModified() { 230 mValues.projectName = mProjectNameText.getText().trim(); 231 mValues.projectNameModifiedByUser = true; 232 233 if (!mValues.applicationNameModifiedByUser) { 234 mValues.applicationName = capitalize(mValues.projectName); 235 if (!mValues.testApplicationNameModified) { 236 mValues.testApplicationName = 237 ApplicationInfoPage.suggestTestApplicationName(mValues.applicationName); 238 } 239 } 240 if (!mValues.activityNameModifiedByUser) { 241 String name = capitalize(mValues.projectName); 242 mValues.activityName = stripWhitespace(name) + ACTIVITY_NAME_SUFFIX; 243 } 244 if (!mValues.testProjectModified) { 245 mValues.testProjectName = 246 ApplicationInfoPage.suggestTestProjectName(mValues.projectName); 247 } 248 if (!mValues.projectLocationModifiedByUser) { 249 updateLocationPathField(null); 250 } 251 } 252 253 @Override 254 public void widgetSelected(SelectionEvent e) { 255 if (mIgnore) { 256 return; 257 } 258 259 Object source = e.getSource(); 260 261 if (source == mCreateNewButton && mCreateNewButton != null 262 && mCreateNewButton.getSelection()) { 263 mValues.useExisting = false; 264 if (mValues.mode == Mode.SAMPLE) { 265 // Only reset the mode if we're toggling from sample back to create new 266 // or create existing. We can only come to the sample state when we're in 267 // ANY mode. (In particular, we don't want to switch to ANY if you're 268 // in test mode. 269 mValues.mode = Mode.ANY; 270 } 271 updateLocationState(); 272 } else if (source == mCreateSampleRadioButton && mCreateSampleRadioButton.getSelection()) { 273 mValues.useExisting = true; 274 mValues.useDefaultLocation = true; 275 if (!mUseDefaultCheckBox.getSelection()) { 276 try { 277 mIgnore = true; 278 mUseDefaultCheckBox.setSelection(true); 279 } finally { 280 mIgnore = false; 281 } 282 } 283 mValues.mode = Mode.SAMPLE; 284 updateLocationState(); 285 } else if (source == mUseDefaultCheckBox) { 286 mValues.useDefaultLocation = mUseDefaultCheckBox.getSelection(); 287 updateLocationState(); 288 } else if (source == mBrowseButton) { 289 onOpenDirectoryBrowser(); 290 } 291 292 validatePage(); 293 } 294 295 /** 296 * Enables or disable the location widgets depending on the user selection: 297 * the location path is enabled when using the "existing source" mode (i.e. not new project) 298 * or in new project mode with the "use default location" turned off. 299 */ 300 private void updateLocationState() { 301 boolean isNewProject = !mValues.useExisting; 302 boolean isCreateFromSample = mValues.mode == Mode.SAMPLE; 303 boolean useDefault = mValues.useDefaultLocation && !isCreateFromSample; 304 boolean locationEnabled = (!isNewProject || !useDefault) && !isCreateFromSample; 305 306 mUseDefaultCheckBox.setEnabled(isNewProject); 307 mLocationLabel.setEnabled(locationEnabled); 308 mLocationText.setEnabled(locationEnabled); 309 mBrowseButton.setEnabled(locationEnabled); 310 311 updateLocationPathField(null); 312 } 313 314 /** 315 * Display a directory browser and update the location path field with the selected path 316 */ 317 private void onOpenDirectoryBrowser() { 318 319 String existingDir = mLocationText.getText().trim(); 320 321 // Disable the path if it doesn't exist 322 if (existingDir.length() == 0) { 323 existingDir = null; 324 } else { 325 File f = new File(existingDir); 326 if (!f.exists()) { 327 existingDir = null; 328 } 329 } 330 331 DirectoryDialog directoryDialog = new DirectoryDialog(mLocationText.getShell()); 332 directoryDialog.setMessage("Browse for folder"); 333 directoryDialog.setFilterPath(existingDir); 334 String dir = directoryDialog.open(); 335 336 if (dir != null) { 337 updateLocationPathField(dir); 338 validatePage(); 339 } 340 } 341 342 @Override 343 public void widgetDefaultSelected(SelectionEvent e) { 344 } 345 346 /** 347 * Returns the working sets to which the new project should be added. 348 * 349 * @return the selected working sets to which the new project should be added 350 */ 351 private IWorkingSet[] getWorkingSets() { 352 return mWorkingSetGroup.getSelectedWorkingSets(); 353 } 354 355 /** 356 * Sets the working sets to which the new project should be added. 357 * 358 * @param workingSets the initial selected working sets 359 */ 360 private void setWorkingSets(IWorkingSet[] workingSets) { 361 assert workingSets != null; 362 mWorkingSetGroup.setWorkingSets(workingSets); 363 } 364 365 /** 366 * Updates the location directory path field. 367 * <br/> 368 * When custom user selection is enabled, use the absDir argument if not null and also 369 * save it internally. If absDir is null, restore the last saved absDir. This allows the 370 * user selection to be remembered when the user switches from default to custom. 371 * <br/> 372 * When custom user selection is disabled, use the workspace default location with the 373 * current project name. This does not change the internally cached absDir. 374 * 375 * @param absDir A new absolute directory path or null to use the default. 376 */ 377 private void updateLocationPathField(String absDir) { 378 boolean isNewProject = !mValues.useExisting || mValues.mode == Mode.SAMPLE; 379 boolean useDefault = mValues.useDefaultLocation; 380 boolean customLocation = !isNewProject || !useDefault; 381 382 if (!mIgnore) { 383 try { 384 mIgnore = true; 385 if (customLocation) { 386 if (absDir != null) { 387 // We get here if the user selected a directory with the "Browse" button. 388 // Disable auto-compute of the custom location unless the user selected 389 // the exact same path. 390 sAutoComputeCustomLocation = sAutoComputeCustomLocation && 391 absDir.equals(sCustomLocationOsPath); 392 sCustomLocationOsPath = TextProcessor.process(absDir); 393 } else if (sAutoComputeCustomLocation || 394 (!isNewProject && !new File(sCustomLocationOsPath).isDirectory())) { 395 // As a default import location, just suggest the home directory; the user 396 // needs to point to a project to import. 397 // TODO: Open file chooser automatically? 398 sCustomLocationOsPath = System.getProperty("user.home"); //$NON-NLS-1$ 399 } 400 if (!mLocationText.getText().equals(sCustomLocationOsPath)) { 401 mLocationText.setText(sCustomLocationOsPath); 402 mValues.projectLocation = new File(sCustomLocationOsPath); 403 } 404 } else { 405 String value = Platform.getLocation().append(mValues.projectName).toString(); 406 value = TextProcessor.process(value); 407 if (!mLocationText.getText().equals(value)) { 408 mLocationText.setText(value); 409 mValues.projectLocation = new File(value); 410 } 411 } 412 } finally { 413 mIgnore = false; 414 } 415 } 416 417 if (mValues.useExisting && mValues.projectLocation != null 418 && mValues.projectLocation.exists() && mValues.mode != Mode.SAMPLE) { 419 mValues.extractFromAndroidManifest(new Path(mValues.projectLocation.getPath())); 420 if (!mValues.projectNameModifiedByUser && mValues.projectName != null) { 421 try { 422 mIgnore = true; 423 mProjectNameText.setText(mValues.projectName); 424 } finally { 425 mIgnore = false; 426 } 427 } 428 } 429 } 430 431 private void validatePage() { 432 IStatus status = null; 433 434 // Validate project name -- unless we're creating a sample, in which case 435 // the user will get a chance to pick the name on the Sample page 436 if (mValues.mode != Mode.SAMPLE) { 437 status = validateProjectName(mValues.projectName); 438 } 439 440 if (status == null || status.getSeverity() != IStatus.ERROR) { 441 IStatus validLocation = validateLocation(); 442 if (validLocation != null) { 443 status = validLocation; 444 } 445 } 446 447 if (!mCheckedSdkUptodate) { 448 // Ensure that we have a recent enough version of the Tools that the right templates 449 // are available 450 File file = new File(AdtPlugin.getOsSdkFolder(), OS_SDK_TOOLS_LIB_FOLDER 451 + File.separator + FN_PROJECT_PROGUARD_FILE); 452 if (!file.exists()) { 453 status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 454 String.format("You do not have the latest version of the " 455 + "SDK Tools installed: Please update. (Missing %1$s)", file.getPath())); 456 } else { 457 mCheckedSdkUptodate = true; 458 } 459 } 460 461 // -- update UI & enable finish if there's no error 462 setPageComplete(status == null || status.getSeverity() != IStatus.ERROR); 463 if (status != null) { 464 setMessage(status.getMessage(), 465 status.getSeverity() == IStatus.ERROR 466 ? IMessageProvider.ERROR : IMessageProvider.WARNING); 467 } else { 468 setErrorMessage(null); 469 setMessage(null); 470 } 471 } 472 473 private IStatus validateLocation() { 474 if (mValues.mode == Mode.SAMPLE) { 475 // Samples are always created in the default directory 476 return null; 477 } 478 479 // Validate location 480 Path path = new Path(mValues.projectLocation.getPath()); 481 if (!mValues.useExisting) { 482 if (!mValues.useDefaultLocation) { 483 // If not using the default value validate the location. 484 URI uri = URIUtil.toURI(path.toOSString()); 485 IWorkspace workspace = ResourcesPlugin.getWorkspace(); 486 IProject handle = workspace.getRoot().getProject(mValues.projectName); 487 IStatus locationStatus = workspace.validateProjectLocationURI(handle, uri); 488 if (!locationStatus.isOK()) { 489 return locationStatus; 490 } 491 // The location is valid as far as Eclipse is concerned (i.e. mostly not 492 // an existing workspace project.) Check it either doesn't exist or is 493 // a directory that is empty. 494 File f = path.toFile(); 495 if (f.exists() && !f.isDirectory()) { 496 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 497 "A directory name must be specified."); 498 } else if (f.isDirectory()) { 499 // However if the directory exists, we should put a 500 // warning if it is not empty. We don't put an error 501 // (we'll ask the user again for confirmation before 502 // using the directory.) 503 String[] l = f.list(); 504 if (l != null && l.length != 0) { 505 return new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID, 506 "The selected output directory is not empty."); 507 } 508 } 509 } else { 510 // Otherwise validate the path string is not empty 511 if (mValues.projectLocation.getPath().length() == 0) { 512 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 513 "A directory name must be specified."); 514 } 515 File dest = path.toFile(); 516 if (dest.exists()) { 517 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 518 String.format( 519 "There is already a file or directory named \"%1$s\" in the selected location.", 520 mValues.projectName)); 521 } 522 } 523 } else { 524 // Must be an existing directory 525 File f = path.toFile(); 526 if (!f.isDirectory()) { 527 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 528 "An existing directory name must be specified."); 529 } 530 531 // Check there's an android manifest in the directory 532 String osPath = path.append(SdkConstants.FN_ANDROID_MANIFEST_XML).toOSString(); 533 File manifestFile = new File(osPath); 534 if (!manifestFile.isFile()) { 535 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 536 String.format( 537 "Choose a valid Android code directory\n" + 538 "(%1$s not found in %2$s.)", 539 SdkConstants.FN_ANDROID_MANIFEST_XML, f.getName())); 540 } 541 542 // Parse it and check the important fields. 543 ManifestData manifestData = AndroidManifestHelper.parseForData(osPath); 544 if (manifestData == null) { 545 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 546 String.format("File %1$s could not be parsed.", osPath)); 547 } 548 String packageName = manifestData.getPackage(); 549 if (packageName == null || packageName.length() == 0) { 550 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 551 String.format("No package name defined in %1$s.", osPath)); 552 } 553 Activity[] activities = manifestData.getActivities(); 554 if (activities == null || activities.length == 0) { 555 // This is acceptable now as long as no activity needs to be 556 // created 557 if (mValues.createActivity) { 558 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 559 String.format("No activity name defined in %1$s.", osPath)); 560 } 561 } 562 563 // If there's already a .project, tell the user to use import instead. 564 if (path.append(".project").toFile().exists()) { //$NON-NLS-1$ 565 return new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID, 566 "An Eclipse project already exists in this directory.\n" + 567 "Consider using File > Import > Existing Project instead."); 568 } 569 } 570 571 return null; 572 } 573 574 public static IStatus validateProjectName(String projectName) { 575 if (projectName == null || projectName.length() == 0) { 576 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 577 "Project name must be specified"); 578 } else { 579 IWorkspace workspace = ResourcesPlugin.getWorkspace(); 580 IStatus nameStatus = workspace.validateName(projectName, IResource.PROJECT); 581 if (!nameStatus.isOK()) { 582 return nameStatus; 583 } else { 584 // Note: the case-sensitiveness of the project name matters and can cause a 585 // conflict *later* when creating the project resource, so let's check it now. 586 for (IProject existingProj : workspace.getRoot().getProjects()) { 587 if (projectName.equalsIgnoreCase(existingProj.getName())) { 588 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 589 "A project with that name already exists in the workspace"); 590 } 591 } 592 } 593 } 594 595 return null; 596 } 597 598 @Override 599 public IWizardPage getNextPage() { 600 // Sync working set data to the value object, since the WorkingSetGroup 601 // doesn't let us add listeners to do this lazily 602 mValues.workingSets = getWorkingSets(); 603 604 return super.getNextPage(); 605 } 606 } 607