blob: b3ff3500e2e6061920c5f1d9765c75b4082629a9 [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.npw;
import com.android.annotations.VisibleForTesting;
import com.android.tools.idea.ui.LabelWithEditLink;
import com.android.tools.idea.wizard.WizardConstants;
import com.android.tools.idea.wizard.dynamic.DynamicWizardStepWithHeaderAndDescription;
import com.android.tools.idea.wizard.dynamic.ScopedStateStore;
import com.google.common.base.CharMatcher;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.fileChooser.FileChooserFactory;
import com.intellij.openapi.fileChooser.FileSaverDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileWrapper;
import org.jetbrains.android.util.AndroidUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Set;
import static com.android.tools.idea.wizard.dynamic.ScopedStateStore.Key;
/**
* ConfigureAndroidModuleStep is the first page in the New Project wizard that sets project/module name, location, and other project-global
* parameters.
*/
public class ConfigureAndroidProjectStep extends DynamicWizardStepWithHeaderAndDescription {
private static final String EXAMPLE_DOMAIN = "example.com";
public static final String SAVED_COMPANY_DOMAIN = "SAVED_COMPANY_DOMAIN";
private static final CharMatcher ILLEGAL_CHARACTER_MATCHER = CharMatcher.anyOf(WizardConstants.INVALID_FILENAME_CHARS);
@VisibleForTesting
static final Set<String> INVALID_MSFT_FILENAMES = ImmutableSet
.of("con", "prn", "aux", "clock$", "nul", "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9", "lpt1", "lpt2",
"lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", "$mft", "$mftmirr", "$logfile", "$volume", "$attrdef", "$bitmap", "$boot",
"$badclus", "$secure", "$upcase", "$extend", "$quota", "$objid", "$reparse");
private TextFieldWithBrowseButton myProjectLocation;
private JTextField myAppName;
private JPanel myPanel;
private JTextField myCompanyDomain;
private LabelWithEditLink myPackageName;
public ConfigureAndroidProjectStep(@NotNull Disposable disposable) {
this("Configure your new project", disposable);
}
public ConfigureAndroidProjectStep(String title, Disposable parentDisposable) {
super(title, null, parentDisposable);
setBodyComponent(myPanel);
}
@Override
public void init() {
register(WizardConstants.APPLICATION_NAME_KEY, myAppName);
register(WizardConstants.COMPANY_DOMAIN_KEY, myCompanyDomain);
register(WizardConstants.PACKAGE_NAME_KEY, myPackageName);
registerValueDeriver(WizardConstants.PACKAGE_NAME_KEY, PACKAGE_NAME_DERIVER);
register(WizardConstants.PROJECT_LOCATION_KEY, myProjectLocation);
registerValueDeriver(WizardConstants.PROJECT_LOCATION_KEY, myProjectLocationDeriver);
myProjectLocation.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
browseForFile();
}
});
myState.put(WizardConstants.APPLICATION_NAME_KEY, "My Application");
String savedCompanyDomain = PropertiesComponent.getInstance().getValue(SAVED_COMPANY_DOMAIN);
if (savedCompanyDomain == null) {
savedCompanyDomain = nameToPackage(System.getProperty("user.name"));
if (savedCompanyDomain != null) {
savedCompanyDomain = savedCompanyDomain + '.' + EXAMPLE_DOMAIN;
}
}
if (savedCompanyDomain == null) {
savedCompanyDomain = EXAMPLE_DOMAIN;
}
myState.put(WizardConstants.COMPANY_DOMAIN_KEY, savedCompanyDomain);
super.init();
}
private void browseForFile() {
FileSaverDescriptor fileSaverDescriptor = new FileSaverDescriptor("Project location", "Please choose a location for your project");
File currentPath = new File(myProjectLocation.getText());
File parentPath = currentPath.getParentFile();
if (parentPath == null) {
String homePath = System.getProperty("user.home");
parentPath = homePath == null ? new File("/") : new File(homePath);
}
VirtualFile parent = LocalFileSystem.getInstance().findFileByIoFile(parentPath);
String filename = currentPath.getName();
VirtualFileWrapper fileWrapper =
FileChooserFactory.getInstance().createSaveFileDialog(fileSaverDescriptor, (Project)null).save(parent, filename);
if (fileWrapper != null) {
myProjectLocation.setText(fileWrapper.getFile().getAbsolutePath());
}
}
@Override
public void deriveValues(Set<Key> modified) {
super.deriveValues(modified);
// Save the user edited value of the company domain
if (modified.contains(WizardConstants.COMPANY_DOMAIN_KEY)) {
String domain = myState.get(WizardConstants.COMPANY_DOMAIN_KEY);
if (domain != null && !domain.isEmpty() && myState.containsKey(WizardConstants.PACKAGE_NAME_KEY)) {
@SuppressWarnings("ConstantConditions")
String message = AndroidUtils.validateAndroidPackageName(myState.get(WizardConstants.PACKAGE_NAME_KEY));
if (message == null) {
PropertiesComponent.getInstance().setValue(SAVED_COMPANY_DOMAIN, domain);
}
}
}
}
@Override
public boolean validate() {
WizardUtils.ValidationResult locationValidationResult = WizardUtils.validateLocation(myState.get(WizardConstants.PROJECT_LOCATION_KEY));
setErrorHtml(locationValidationResult.isOk() ? "" : locationValidationResult.getFormattedMessage());
return validateAppName() && validatePackageName() && !locationValidationResult.isError();
}
protected boolean validateAppName() {
String appName = myState.get(WizardConstants.APPLICATION_NAME_KEY);
if (appName == null || appName.isEmpty()) {
setErrorHtml("Please enter an application name (shown in launcher)");
return false;
} else if (Character.isLowerCase(appName.charAt(0))) {
setErrorHtml("The application name for most apps begins with an uppercase letter");
}
return true;
}
protected boolean validatePackageName() {
String packageName = myState.get(WizardConstants.PACKAGE_NAME_KEY);
if (packageName == null) {
setErrorHtml("Please enter a package name (This package uniquely identifies your application)");
return false;
} else {
String message = AndroidUtils.validateAndroidPackageName(packageName);
if (message != null) {
setErrorHtml("Invalid package name: " + message);
return false;
}
}
return true;
}
@NotNull
@Override
public String getStepName() {
return "Create Android Project";
}
@Override
public JComponent getPreferredFocusedComponent() {
return myAppName;
}
@Nullable
public String getHelpText(@NotNull Key<?> key) {
if (key.equals(WizardConstants.APPLICATION_NAME_KEY)) {
return "The application name is shown in the Play store, as well as in the Manage Applications list in Settings.";
} else if (key.equals(WizardConstants.PACKAGE_NAME_KEY)) {
return "The package name must be a unique identifier for your application.\n It is typically not shown to users, " +
"but it <b>must</b> stay the same for the lifetime of your application; it is how multiple versions of the same application " +
"are considered the \"same app\".\nThis is typically the reverse domain name of your organization plus one or more " +
"application identifiers, and it must be a valid Java package name.";
} else {
return null;
}
}
@VisibleForTesting
static String nameToPackage(String name) {
name = name.replace('-', '_');
name = name.replaceAll("[^a-zA-Z0-9_]", "");
name = name.toLowerCase();
return name;
}
public static final ValueDeriver<String> PACKAGE_NAME_DERIVER = new ValueDeriver<String>() {
@Nullable
@Override
public Set<Key<?>> getTriggerKeys() {
return makeSetOf(WizardConstants.APPLICATION_NAME_KEY, WizardConstants.COMPANY_DOMAIN_KEY);
}
@Nullable
@Override
public String deriveValue(@NotNull ScopedStateStore state, Key changedKey, @Nullable String currentValue) {
String projectName = state.get(WizardConstants.APPLICATION_NAME_KEY);
if (projectName == null) {
projectName = "app";
}
projectName = nameToPackage(projectName);
String companyDomain = state.get(WizardConstants.COMPANY_DOMAIN_KEY);
if (companyDomain == null) {
companyDomain = EXAMPLE_DOMAIN;
}
ArrayList domainParts = Lists.newArrayList(companyDomain.split("\\."));
String reversedDomain = Joiner.on('.').skipNulls().join(Lists.reverse(Lists.transform(domainParts, new Function<String, String>() {
@Override
public String apply(String input) {
String name = nameToPackage(input);
return name.isEmpty() ? null : name;
}
})));
return reversedDomain + '.' + projectName;
}
};
private static final ValueDeriver<String> myProjectLocationDeriver = new ValueDeriver<String>() {
@Nullable
@Override
public Set<Key<?>> getTriggerKeys() {
return makeSetOf(WizardConstants.APPLICATION_NAME_KEY);
}
@Nullable
@Override
public String deriveValue(ScopedStateStore state, Key changedKey, @Nullable String currentValue) {
String name = state.get(WizardConstants.APPLICATION_NAME_KEY);
name = name == null ? "" : name;
name = name.replaceAll(WizardConstants.INVALID_FILENAME_CHARS, "");
name = name.replaceAll("\\s", "");
File baseDirectory = new File(NewProjectWizardState.getProjectFileDirectory());
File projectDir = new File(baseDirectory, name);
int i = 2;
while (projectDir.exists()) {
projectDir = new File(baseDirectory, name + i);
i++;
}
return projectDir.getPath();
}
};
@NotNull
@Override
protected WizardStepHeaderSettings getStepHeader() {
return ConfigureAndroidProjectPath.buildConfigurationHeader();
}
}