blob: b8b189dd451b3bba8a08c1693b3734a7b1878d7c [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.SdkConstants;
import com.android.annotations.VisibleForTesting;
import com.android.builder.model.SourceProvider;
import com.android.sdklib.AndroidVersion;
import com.android.tools.idea.templates.Parameter;
import com.android.tools.idea.templates.Template;
import com.android.tools.idea.templates.TemplateMetadata;
import com.android.tools.idea.ui.ComboBoxItemWithApiTag;
import com.android.tools.idea.ui.LabelWithEditLink;
import com.android.tools.idea.ui.TextFieldWithLaunchBrowserButton;
import com.android.tools.idea.templates.StringEvaluator;
import com.android.tools.idea.wizard.dynamic.DynamicWizardStepWithDescription;
import com.android.tools.idea.wizard.dynamic.ScopedStateStore;
import com.google.common.base.*;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.*;
import com.intellij.ide.util.ClassFilter;
import com.intellij.ide.util.TreeClassChooser;
import com.intellij.ide.util.TreeClassChooserFactory;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.event.DocumentAdapter;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.JavaCodeFragment;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiClassUtil;
import com.intellij.ui.*;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import com.intellij.uiDesigner.core.Spacer;
import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.text.Document;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.List;
import static com.android.tools.idea.wizard.dynamic.ScopedStateStore.Key;
import static com.android.tools.idea.wizard.dynamic.ScopedStateStore.createKey;
/**
* Wizard step for specifying template-specific parameters.
*/
public class TemplateParameterStep2 extends DynamicWizardStepWithDescription {
public static final Logger LOG = Logger.getInstance(TemplateParameterStep2.class);
public static final int COLUMN_COUNT = 3;
private static final Key<File> KEY_TEMPLATE_ICON = createKey("page.template.icon", ScopedStateStore.Scope.STEP, File.class);
private final Function<Parameter, Key<?>> myParameterToKey;
private final Map<String, Object> myPresetParameters = Maps.newHashMap();
@NotNull private final Key<String> myPackageNameKey;
private final LoadingCache<File, Optional<Icon>> myThumbnailsCache = CacheBuilder.newBuilder().build(new TemplateIconLoader());
@NotNull private final FormFactorUtils.FormFactor myFormFactor; // TODO: Use for icon
private final SourceProvider[] mySourceProviders;
private JLabel myTemplateIcon;
private JPanel myTemplateParameters;
private JLabel myTemplateDescription;
private JPanel myRootPanel;
private JLabel myParameterDescription;
private JSeparator myFooterSeparator;
private Map<String, Object> myParameterDefaultValues = Maps.newHashMap();
private TemplateEntry myCurrentTemplate;
private JComboBox mySourceSet;
private JLabel mySourceSetLabel;
private boolean myUpdatingDefaults = false;
private Map<Parameter, List<JComponent>> myParameterComponents = new WeakHashMap<Parameter, List<JComponent>>();
private final StringEvaluator myEvaluator = new StringEvaluator();
private Map<String, WizardParameterFactory> myExternalWizardParameterFactoryMap = null;
private Map<JComponent, Parameter> myDataComponentParameters = new WeakHashMap<JComponent, Parameter>();
/**
* Creates a new template parameters wizard step.
*
* @param presetParameters some parameter values may be predefined outside of this step.
* User will not be allowed to change their values.
*/
public TemplateParameterStep2(@NotNull FormFactorUtils.FormFactor formFactor, Map<String, Object> presetParameters,
@Nullable Disposable disposable, @NotNull Key<String> packageNameKey,
SourceProvider[] sourceProviders) {
super(disposable);
myFormFactor = formFactor;
mySourceProviders = sourceProviders;
myPresetParameters.putAll(presetParameters);
myPackageNameKey = packageNameKey;
myParameterToKey = CacheBuilder.newBuilder().weakKeys().build(CacheLoader.from(new ParameterKeyFunction()));
myRootPanel.setBorder(createBodyBorder());
myTemplateDescription.setBorder(BorderFactory.createEmptyBorder(0, 0, myTemplateDescription.getFont().getSize(), 0));
setBodyComponent(myRootPanel);
}
private static JComponent createTextFieldWithBrowse(Parameter parameter) {
String sourceUrl = parameter.sourceUrl;
if (sourceUrl == null) {
LOG.warn(String.format("Source URL is missing for parameter %1$s", parameter.name));
sourceUrl = "";
}
return new TextFieldWithLaunchBrowserButton(sourceUrl);
}
public void setPresetValue(@NotNull String key, @Nullable Object value) {
myPresetParameters.put(key, value);
invokeUpdate(null);
}
private static JComponent createEnumCombo(Parameter parameter) {
List<Element> options = parameter.getOptions();
ComboBoxItemWithApiTag[] items = new ComboBoxItemWithApiTag[options.size()];
int initialSelection = -1;
int i = 0;
assert !options.isEmpty();
for (Element option : options) {
//noinspection unchecked
items[i++] = createItemForOption(parameter, option);
String isDefault = option.getAttribute(Template.ATTR_DEFAULT);
if (isDefault != null && !isDefault.isEmpty() && Boolean.valueOf(isDefault)) {
initialSelection = i - 1;
}
}
@SuppressWarnings("UndesirableClassUsage")
JComboBox comboBox = new JComboBox(items);
comboBox.setSelectedIndex(initialSelection);
return comboBox;
}
public static ComboBoxItemWithApiTag createItemForOption(Parameter parameter, Element option) {
String optionId = option.getAttribute(SdkConstants.ATTR_ID);
assert optionId != null && !optionId.isEmpty() : SdkConstants.ATTR_ID;
NodeList childNodes = option.getChildNodes();
assert childNodes.getLength() == 1 && childNodes.item(0).getNodeType() == Node.TEXT_NODE;
String optionLabel = childNodes.item(0).getNodeValue().trim();
int minSdk = getIntegerOptionValue(option, TemplateMetadata.ATTR_MIN_API, parameter.name, 1);
int minBuildApi = getIntegerOptionValue(option, TemplateMetadata.ATTR_MIN_BUILD_API, parameter.name, 1);
return new ComboBoxItemWithApiTag(optionId, optionLabel, minSdk, minBuildApi);
}
private static int getIntegerOptionValue(Element option, String attribute, @Nullable String parameterName, int defaultValue) {
String stringValue = option.getAttribute(attribute);
try {
return StringUtil.isEmpty(stringValue) ? defaultValue : Integer.parseInt(stringValue);
}
catch (Exception e) {
LOG.warn(String.format("Invalid %1$s value (%2$s) for option %3$s in parameter %4$s", attribute, stringValue,
option.getAttribute(SdkConstants.ATTR_ID), parameterName), e);
return defaultValue;
}
}
private static int addComponent(JComponent parent, JComponent component, int row, int column, boolean isLast) {
GridConstraints gridConstraints = new GridConstraints();
gridConstraints.setRow(row);
gridConstraints.setColumn(column);
boolean isGreedyComponent = component instanceof JTextField || component instanceof Spacer ||
component instanceof LabelWithEditLink || component instanceof TextAccessor ||
component instanceof EditorComboBox;
int columnSpan = (isLast && isGreedyComponent) ? COLUMN_COUNT - column : 1;
gridConstraints.setColSpan(columnSpan);
gridConstraints.setAnchor(GridConstraints.ALIGN_LEFT);
gridConstraints.setHSizePolicy(isGreedyComponent
? GridConstraints.SIZEPOLICY_CAN_GROW | GridConstraints.SIZEPOLICY_WANT_GROW
: GridConstraints.SIZEPOLICY_CAN_SHRINK);
gridConstraints.setVSizePolicy(component instanceof Spacer
? GridConstraints.SIZEPOLICY_CAN_GROW | GridConstraints.SIZEPOLICY_WANT_GROW
: GridConstraints.SIZEPOLICY_FIXED);
gridConstraints.setFill(GridConstraints.FILL_HORIZONTAL);
parent.add(component, gridConstraints);
if (isLast && !isGreedyComponent && column < COLUMN_COUNT - 1) {
addComponent(parent, new Spacer(), row, column + 1, true);
}
return columnSpan;
}
private Map<Parameter, Object> getParameterObjectMap(Collection<Parameter> parameters,
Map<Parameter, Object> parametersWithDefaultValues,
Map<Parameter, Object> parametersWithNonDefaultValues)
throws CircularParameterDependencyException {
ParameterDefaultValueComputer computer =
new ParameterDefaultValueComputer(parameters, parametersWithNonDefaultValues, getImplicitParameters(),
new DeduplicateValuesFunction());
Map<Parameter, Object> computedDefaultValues = computer.getParameterValues();
Map<Parameter, Object> parameterValues = Maps.newHashMap(parametersWithDefaultValues);
for (Map.Entry<Parameter, Object> entry : computedDefaultValues.entrySet()) {
if (!parametersWithNonDefaultValues.keySet().contains(entry.getKey()) && entry.getValue() != null) {
parameterValues.put(entry.getKey(), entry.getValue());
}
}
return parameterValues;
}
private Map<String, Object> getImplicitParameters() {
ImmutableMap.Builder<String, Object> builder = new ImmutableMap.Builder<String, Object>();
for (Key<String> parameter : AddAndroidActivityPath.IMPLICIT_PARAMETERS) {
String value = myState.get(parameter);
if (value != null) {
builder.put(parameter.name, value);
}
}
return builder.build();
}
@Override
public boolean isStepVisible() {
return myState.get(AddAndroidActivityPath.KEY_SELECTED_TEMPLATE) != null;
}
@NotNull
private List<JComponent> createComponents(final Parameter parameter) {
JLabel label = new JLabel(parameter.name + ":");
final JComponent dataComponent;
if (AddAndroidActivityPath.PACKAGE_NAME_PARAMETERS.contains(parameter.id)) {
Module module = getModule();
if (module != null) {
dataComponent = createPackageEntry(parameter, module);
}
else {
dataComponent = new LabelWithEditLink();
}
}
else if (AddAndroidActivityPath.CLASS_NAME_PARAMETERS.contains(parameter.id)) {
Module module = getModule();
if (module != null) {
dataComponent = createClassEntry(parameter, module);
}
else {
dataComponent = new JTextField();
}
}
else {
switch (parameter.type) {
case BOOLEAN:
label = null;
dataComponent = new JCheckBox(parameter.name);
break;
case ENUM:
dataComponent = createEnumCombo(parameter);
break;
case EXTERNAL:
dataComponent = createTextFieldWithBrowse(parameter);
break;
case STRING:
dataComponent = new JTextField();
break;
case SEPARATOR:
return Collections.<JComponent>singletonList(new JSeparator(SwingConstants.HORIZONTAL));
case CUSTOM:
JComponent createdComponent = null;
try {
WizardParameterFactory factory = getExternalFactory(parameter.externalTypeName);
if (factory != null) {
createdComponent = factory.createComponent(parameter.externalTypeName, parameter);
}
if (createdComponent == null) {
LOG.error(String.format("Bad registration for custom wizard type %1$s. See ExternalWizardParameterFactory extension point.",
Strings.isNullOrEmpty(parameter.externalTypeName) ? "(null)" : parameter.externalTypeName));
createdComponent = new JTextField();
}
}
catch(Exception e) {
LOG.error(String.format("Exception creating class %1$s", parameter.externalTypeName), e);
createdComponent = new JTextField();
}
dataComponent = createdComponent;
break;
default:
throw new IllegalStateException(parameter.type.toString());
}
}
if (!StringUtil.isEmpty(parameter.help) && dataComponent.getAccessibleContext() != null) {
dataComponent.getAccessibleContext().setAccessibleDescription(parameter.help);
}
register(parameter, dataComponent);
if (label != null) {
label.setLabelFor(dataComponent);
}
return label != null ? Arrays.asList(label, dataComponent) : Arrays.asList(dataComponent);
}
@Nullable
private WizardParameterFactory getExternalFactory(String uiTypeName) {
if (Strings.isNullOrEmpty(uiTypeName)) {
return null;
}
if (myExternalWizardParameterFactoryMap == null) {
Map<String,WizardParameterFactory> externalWizardParameterFactoryHashMap = new HashMap<String,WizardParameterFactory>();
WizardParameterFactory[] factories = Extensions.getExtensions(WizardParameterFactory.EP_NAME);
for(WizardParameterFactory factory : factories) {
String[] types = factory.getSupportedTypes();
if (types != null) {
for(String type : types) {
if (externalWizardParameterFactoryHashMap.containsKey(type)) {
LOG.error("Duplicate ExternalWizardParameterFactory registration on Type:" + type);
continue;
}
externalWizardParameterFactoryHashMap.put(type, factory);
}
}
}
myExternalWizardParameterFactoryMap = externalWizardParameterFactoryHashMap;
}
return myExternalWizardParameterFactoryMap.get(uiTypeName);
}
private JComponent createClassEntry(@NotNull Parameter parameter, @NotNull Module module) {
ChooseClassAction browseAction = new ChooseClassAction(parameter, module);
String historyKey = AddAndroidActivityPath.getRecentHistoryKey(parameter.id);
// Need to add empty entry to the history, otherwise it will select entry used last
RecentsManager.getInstance(module.getProject()).registerRecentEntry(historyKey, "");
ReferenceEditorComboWithBrowseButton control =
new ReferenceEditorComboWithBrowseButton(browseAction, "", module.getProject(), true,
new OnlyShowActivities(module), historyKey);
if (!StringUtil.isEmpty(control.getText())) {
control.prependItem("");
control.setText("");
}
addJBDocumentListener(control.getChildComponent().getDocument(), control);
// Discourage from growing
control.setPreferredSize(new Dimension(1, 1));
return control;
}
private JComponent createPackageEntry(@NotNull Parameter parameter, @NotNull Module module) {
Project project = module.getProject();
com.intellij.openapi.editor.Document doc =
JavaReferenceEditorUtil.createDocument("", project, false, JavaCodeFragment.VisibilityChecker.PROJECT_SCOPE_VISIBLE);
assert doc != null;
final EditorComboBox textField = new EditorComboBox(doc, project, StdFileTypes.JAVA);
final List<String> recentEntries = AddAndroidActivityPath.getParameterValueHistory(parameter, project);
if (recentEntries != null) {
textField.setHistory(ArrayUtil.toStringArray(recentEntries));
}
addJBDocumentListener(doc, textField);
textField.setPreferredSize(new Dimension(1, 1));
return textField;
}
private void addJBDocumentListener(com.intellij.openapi.editor.Document doc, final JComponent textField) {
DocumentAdapter listener = new DocumentAdapter() {
@Override
public void documentChanged(DocumentEvent event) {
saveState(textField);
}
};
Disposable disposable = getDisposable();
if (disposable != null) {
doc.addDocumentListener(listener, disposable);
}
else {
doc.addDocumentListener(listener);
}
}
@SuppressWarnings("unchecked")
private void register(Parameter parameter, JComponent dataComponent) {
myDataComponentParameters.put(dataComponent, parameter);
Key<?> key = getParameterKey(parameter);
if (dataComponent instanceof JCheckBox) {
register((Key<Boolean>)key, (JCheckBox)dataComponent);
}
else if (dataComponent instanceof EditorComboBox) { // Should be above JComboBox
register((Key<String>)key, (EditorComboBox)dataComponent, new ComponentBinding<String, EditorComboBox>() {
@Override
public void setValue(@Nullable String newValue, @NotNull EditorComboBox component) {
String text = Strings.nullToEmpty(newValue);
component.prependItem(text);
component.setText(text);
}
@Nullable
@Override
public String getValue(@NotNull EditorComboBox component) {
return component.getText();
}
});
}
else if (dataComponent instanceof JComboBox) {
register(key, (JComboBox)dataComponent);
}
else if (dataComponent instanceof JTextField) {
register((Key<String>)key, (JTextField)dataComponent);
}
else if (dataComponent instanceof TextFieldWithBrowseButton) {
register((Key<String>)key, (TextFieldWithBrowseButton)dataComponent);
}
else if (dataComponent instanceof LabelWithEditLink) {
register((Key<String>)key, (LabelWithEditLink)dataComponent, new ComponentBinding<String, LabelWithEditLink>() {
@Override
public void setValue(@Nullable String newValue, @NotNull LabelWithEditLink component) {
component.setText(Strings.nullToEmpty(newValue));
}
@Nullable
@Override
public String getValue(@NotNull LabelWithEditLink component) {
return component.getText();
}
@Nullable
@Override
public Document getDocument(@NotNull LabelWithEditLink component) {
return component.getDocument();
}
});
}
else if (dataComponent instanceof TextAccessor) {
register((Key<String>)key, dataComponent, new ComponentBinding<String, JComponent>() {
@Override
public void setValue(@Nullable String newValue, @NotNull JComponent component) {
((TextAccessor)component).setText(Strings.nullToEmpty(newValue));
}
@Nullable
@Override
public String getValue(@NotNull JComponent component) {
return ((TextAccessor)component).getText();
}
});
}
else {
WizardParameterFactory factory = getExternalFactory(parameter.externalTypeName);
if (factory != null) {
register((Key<String>)key, dataComponent, factory.createBinding(dataComponent, parameter));
}
else {
throw new IllegalArgumentException(dataComponent.getClass().getName());
}
}
}
@Override
protected JLabel getDescriptionLabel() {
return myParameterDescription;
}
@Override
public void deriveValues(Set<Key> modified) {
super.deriveValues(modified);
if (myCurrentTemplate != null) {
updateStateWithDefaults(myCurrentTemplate.getParameters());
updateControlsEnabled();
updateControlsVisibility();
}
}
private void updateControlsEnabled() {
if (myUpdatingDefaults) {
return;
}
Map<String, Object> contextValues = getContextValues();
for (Parameter parameter : myCurrentTemplate.getParameters()) {
String enabledStr = parameter.enabled;
if (!StringUtil.isEmpty(enabledStr)) {
boolean enabled = myEvaluator.evaluateBooleanExpression(enabledStr, contextValues, true);
List<JComponent> components = myParameterComponents.get(parameter);
if (components != null) {
for (JComponent component : components) {
Parameter componentParameter = myDataComponentParameters.get(component);
if (!enabled && componentParameter != null) {
myState.remove(getParameterKey(componentParameter));
updateStateWithDefaults(Sets.newHashSet(componentParameter));
}
component.setEnabled(enabled);
}
}
}
}
}
private void updateControlsVisibility() {
if (myUpdatingDefaults) {
return;
}
Map<String, Object> contextValues = getContextValues();
for (Parameter parameter : myCurrentTemplate.getParameters()) {
String visibility = parameter.visibility;
if (!StringUtil.isEmpty(visibility)) {
boolean visible = myEvaluator.evaluateBooleanExpression(visibility, contextValues, true);
List<JComponent> components = myParameterComponents.get(parameter);
if (components != null) {
for (JComponent component : components) {
component.setVisible(visible);
}
}
}
}
}
private Map<String, Object> getContextValues() {
Map<String, Object> values = Maps.newHashMap();
for (Key key : myState.getAllKeys()) {
values.put(key.name, myState.get(key));
}
return values;
}
@Override
public boolean validate() {
setErrorHtml(null);
AndroidVersion minApi = myState.get(AddAndroidActivityPath.KEY_MIN_SDK);
Integer buildApi = myState.get(AddAndroidActivityPath.KEY_BUILD_SDK);
TemplateEntry templateEntry = myState.get(AddAndroidActivityPath.KEY_SELECTED_TEMPLATE);
if (templateEntry == null) {
return false;
}
for (Parameter param : templateEntry.getParameters()) {
if (param != null) {
Object value = getStateParameterValue(param);
String error = param.validate(getProject(), getModule(), myState.get(AddAndroidActivityPath.KEY_SOURCE_PROVIDER),
myState.get(myPackageNameKey), value != null ? value : "");
if (error != null) {
// Highlight?
setErrorHtml(error);
return false;
}
// Check to see that the selection's constraints are met if this is a combo box
if (value instanceof ComboBoxItemWithApiTag) {
ComboBoxItemWithApiTag selectedItem = (ComboBoxItemWithApiTag)value;
if (minApi != null && selectedItem.minApi > minApi.getFeatureLevel()) {
setErrorHtml(String.format("The \"%s\" option for %s requires a minimum API level of %d",
selectedItem.label, param.name, selectedItem.minApi));
return false;
}
if (buildApi != null && selectedItem.minBuildApi > buildApi) {
setErrorHtml(String.format("The \"%s\" option for %s requires a minimum API level of %d",
selectedItem.label, param.name, selectedItem.minBuildApi));
return false;
}
}
}
}
return true;
}
@Override
public void init() {
super.init();
if (mySourceProviders.length > 0) {
myState.put(AddAndroidActivityPath.KEY_SOURCE_PROVIDER, mySourceProviders[0]);
myState.put(AddAndroidActivityPath.KEY_SOURCE_PROVIDER_NAME, mySourceProviders[0].getName());
}
register(AddAndroidActivityPath.KEY_SELECTED_TEMPLATE, (JComponent)myTemplateDescription.getParent(),
new ComponentBinding<TemplateEntry, JComponent>() {
@Override
public void setValue(@Nullable TemplateEntry newValue, @NotNull JComponent component) {
setSelectedTemplate(newValue);
}
}
);
register(KEY_DESCRIPTION, myFooterSeparator, new ComponentBinding<String, JSeparator>() {
@Override
public void setValue(@Nullable String newValue, @NotNull JSeparator component) {
component.setVisible(!StringUtil.isEmpty(newValue));
}
});
register(KEY_TEMPLATE_ICON, myTemplateIcon, new ComponentBinding<File, JLabel>() {
@Override
public void setValue(@Nullable File newValue, @NotNull JLabel component) {
Optional<Icon> thumbnail = newValue == null ? Optional.<Icon>absent() : myThumbnailsCache.getUnchecked(newValue);
Icon icon = thumbnail.orNull();
component.setIcon(icon);
component.setVisible(icon != null);
}
});
registerValueDeriver(KEY_TEMPLATE_ICON, new ValueDeriver<File>() {
@Nullable
@Override
public File deriveValue(@NotNull ScopedStateStore state, @Nullable Key changedKey, @Nullable File currentValue) {
return getTemplateIconPath(state.get(AddAndroidActivityPath.KEY_SELECTED_TEMPLATE));
}
});
registerValueDeriver(AddAndroidActivityPath.KEY_SOURCE_PROVIDER_NAME, new ValueDeriver<String>() {
@Nullable
@Override
public String deriveValue(@NotNull ScopedStateStore state, @Nullable Key changedKey, @Nullable String currentValue) {
SourceProvider sourceProvider = state.get(AddAndroidActivityPath.KEY_SOURCE_PROVIDER);
return sourceProvider == null ? null : sourceProvider.getName();
}
});
}
@Nullable
private File getTemplateIconPath(@Nullable TemplateEntry entry) {
if (entry == null) {
return null;
}
final Map<String, Parameter> parameterIds = Maps.newHashMap();
for (Parameter parameter : entry.getParameters()) {
parameterIds.put(parameter.id, parameter);
}
String path = entry.getMetadata().getThumbnailPath(new Function<String, Object>() {
@Override
public Object apply(String input) {
return getStateParameterValue(parameterIds.get(input));
}
});
if (!StringUtil.isEmpty(path)) {
File file = new File(entry.getTemplateDir(), FileUtilRt.toSystemDependentName(path, '/'));
return file.isFile() ? file : null;
}
else {
return null;
}
}
private void setSelectedTemplate(@Nullable TemplateEntry template) {
if (template == null) {
return;
}
TemplateMetadata metadata = template.getMetadata();
myTemplateIcon.setText(template.getTitle());
String string = ImportUIUtil.makeHtmlString(metadata.getDescription());
myTemplateDescription.setText(string);
updateControls(template);
}
private void updateControls(@Nullable TemplateEntry entry) {
if (Objects.equal(myCurrentTemplate, entry)) {
return;
}
myCurrentTemplate = entry;
final Set<Parameter> parameters;
if (entry != null) {
updateStateWithDefaults(entry.getParameters());
parameters = ImmutableSet.copyOf(filterNonUIParameters(entry));
}
else {
parameters = ImmutableSet.of();
}
for (Component component : myTemplateParameters.getComponents()) {
myTemplateParameters.remove(component);
if (component instanceof JComponent) {
deregister((JComponent)component);
}
}
int lastRow = addParameterComponents(parameters.size() + 1, parameters);
addSourceSetControls(lastRow);
}
private int addParameterComponents(final int rowCount, final Set<Parameter> parameters) {
CellLocation location = new CellLocation();
myTemplateParameters.removeAll();
GridLayoutManager layout = new GridLayoutManager(rowCount + 1, COLUMN_COUNT);
layout.setSameSizeHorizontally(false);
myTemplateParameters.setLayout(layout);
for (final Parameter parameter : parameters) {
addComponents(parameter, location);
}
if (location.column > 0) {
//add spacers before moving to the next row.
if (location.column < COLUMN_COUNT) {
addComponent(myTemplateParameters, new Spacer(), location.row, location.column, true);
}
location.row++;
}
return location.row;
}
private static class CellLocation {
public int row = 0, column = 0;
}
private void addSourceSetControls(int row) {
if (mySourceProviders.length > 1) {
if (mySourceSetLabel == null) {
mySourceSetLabel = new JLabel("Target Source Set:");
//noinspection UndesirableClassUsage
mySourceSet = new JComboBox();
register(AddAndroidActivityPath.KEY_SOURCE_PROVIDER, mySourceSet);
setControlDescription(mySourceSet, "The selected folder contains multiple source sets, " +
"this can include source sets that do not yet exist on disk. " +
"Please select the target source set in which to create the files.");
}
mySourceSet.removeAllItems();
for (SourceProvider sourceProvider : mySourceProviders) {
//noinspection unchecked
mySourceSet.addItem(new ComboBoxItemWithApiTag(sourceProvider, sourceProvider.getName(), 0, 0));
}
addComponent(myTemplateParameters, mySourceSetLabel, row, 0, false);
addComponent(myTemplateParameters, mySourceSet, row, 1, true);
}
}
private Iterable<Parameter> filterNonUIParameters(TemplateEntry entry) {
return Iterables.filter(entry.getParameters(), new Predicate<Parameter>() {
@Override
public boolean apply(Parameter input) {
return input != null && !StringUtil.isEmpty(input.name) && !myPresetParameters.containsKey(input.id);
}
});
}
@VisibleForTesting
protected void updateStateWithDefaults(Collection<Parameter> parameters) {
if (myUpdatingDefaults) {
return;
}
myUpdatingDefaults = true;
try {
for (Parameter parameter : parameters) {
if (myPresetParameters.containsKey(parameter.id)) {
myState.unsafePut(getParameterKey(parameter), myPresetParameters.get(parameter.id));
}
}
try {
Map<Parameter, Object> parameterDefaults = refreshParameterDefaults(parameters, myParameterDefaultValues);
for (Map.Entry<Parameter, Object> entry : parameterDefaults.entrySet()) {
myState.unsafePut(getParameterKey(entry.getKey()), entry.getValue());
myParameterDefaultValues.put(entry.getKey().id, entry.getValue());
}
}
catch (CircularParameterDependencyException exception) {
LOG.error("Circular dependency between parameters in template %1$s, participating parameters: %2$s", exception,
myCurrentTemplate.getTitle(),
Joiner.on(", ").join(exception.getParameterIds()));
}
}
finally {
myUpdatingDefaults = false;
}
}
private Map<Parameter, Object> refreshParameterDefaults(Collection<Parameter> parameters, Map<String, Object> defaultValues)
throws CircularParameterDependencyException {
final Map<Parameter, Object> parametersAtDefault = Maps.newHashMap();
final Map<Parameter, Object> parametersAtNonDefault = Maps.newHashMap();
for (Parameter parameter : parameters) {
if (isDefaultParameterValue(parameter, defaultValues)) {
parametersAtDefault.put(parameter, defaultValues.get(parameter.id));
}
else {
parametersAtNonDefault.put(parameter, getStateParameterValue(parameter));
}
}
return getParameterObjectMap(parameters, parametersAtDefault, parametersAtNonDefault);
}
@NotNull
public Key<?> getParameterKey(@NotNull Parameter parameter) {
//noinspection ConstantConditions
return myParameterToKey.apply(parameter);
}
@Nullable
private Object getStateParameterValue(Parameter parameter) {
if (myPresetParameters.containsKey(parameter.id)) {
return myPresetParameters.get(parameter.id);
}
else {
return myState.get(getParameterKey(parameter));
}
}
private boolean isDefaultParameterValue(Parameter parameter, Map<String, Object> defaultValues) {
Object stateValue = getStateParameterValue(parameter);
if (stateValue == null) {
return true;
}
else {
Object defaultValue = defaultValues.get(parameter.id);
return Objects.equal(defaultValue, stateValue);
}
}
private void addComponents(Parameter parameter, CellLocation location) {
List<JComponent> keyComponents = createComponents(parameter);
// If a group of components take a full row, we ensure we are on
// a fresh row at the start, and also ensure we end on a new row
// for the next component.
// We only group components together on the same row if both
// component sets indicate they allow it.
// Right now, our indication for requiring a full row is simply
// if the # of components is > 1. Only checkbox is allowed to
// share a row.
boolean isFullRow = keyComponents.size() > 1;
// We start a new row if these components are "fullrow" while on an previously used row
// or if there isn't enough space.
if ((isFullRow && location.column > 0)
|| location.column + keyComponents.size() > COLUMN_COUNT) {
// Add spacers before moving to the next row.
if (location.column < COLUMN_COUNT) {
addComponent(myTemplateParameters, new Spacer(), location.row, location.column, true);
}
location.column = 0;
location.row++;
}
// For any component that didn't return a label (checkbox for now), we manually add a null label here to keep the layout the same.
if (location.column == 0 && keyComponents.size() == 1 && keyComponents.get(0) instanceof JCheckBox) {
location.column += addComponent(myTemplateParameters, new JLabel(), location.row, location.column, false);
}
myParameterComponents.put(parameter, keyComponents);
for (Iterator<JComponent> iterator = keyComponents.iterator(); iterator.hasNext(); ) {
JComponent keyComponent = iterator.next();
location.column += addComponent(myTemplateParameters, keyComponent, location.row, location.column,
isFullRow && !iterator.hasNext() );
setControlDescription(keyComponent, parameter.help);
}
if (isFullRow) {
location.row++;
location.column = 0;
}
}
@NotNull
@Override
public String getStepName() {
return "Template parameters";
}
@NotNull
@Override
protected String getStepTitle() {
return "Customize the Activity";
}
@Nullable
@Override
protected String getStepDescription() {
return null;
}
@Nullable
@Override
protected Icon getStepIcon() {
return myFormFactor.getIcon();
}
@Override
public JComponent getPreferredFocusedComponent() {
for (Component component : myTemplateParameters.getComponents()) {
if (!(component instanceof JLabel) && component.isFocusable()) {
return (JComponent)component;
}
}
return myTemplateParameters;
}
private static class ParameterKeyFunction implements Function<Parameter, Key<?>> {
@Override
public Key<?> apply(Parameter input) {
final Class<?> clazz;
switch (input.type) {
case BOOLEAN:
clazz = Boolean.class;
break;
case ENUM:
case EXTERNAL:
case STRING:
case SEPARATOR:
case CUSTOM:
clazz = String.class;
break;
default:
throw new IllegalStateException(input.type.toString());
}
assert input.id != null;
return createKey(input.id, ScopedStateStore.Scope.PATH, clazz);
}
}
private static class OnlyShowActivities implements JavaCodeFragment.VisibilityChecker {
private final Module myModule;
public OnlyShowActivities(Module module) {
myModule = module;
}
private static boolean isActivitySubclass(@NotNull PsiClass classDecl) {
for (PsiClass superClass : classDecl.getSupers()) {
String typename = superClass.getQualifiedName();
if (SdkConstants.CLASS_ACTIVITY.equals(typename) || isActivitySubclass(superClass)) {
return true;
}
}
return false;
}
@Override
public Visibility isDeclarationVisible(PsiElement declaration, @Nullable PsiElement place) {
if (declaration instanceof PsiClass) {
PsiClass classDecl = (PsiClass)declaration;
if (PsiClassUtil.isRunnableClass(classDecl, true, true) &&
isActivitySubclass(classDecl) && isOnClasspath(classDecl)) {
return Visibility.VISIBLE;
}
}
return Visibility.NOT_VISIBLE;
}
private boolean isOnClasspath(@NotNull PsiClass classDecl) {
GlobalSearchScope scope = myModule.getModuleWithDependenciesAndLibrariesScope(false);
VirtualFile file = classDecl.getContainingFile().getVirtualFile();
return scope.contains(file);
}
}
private class ChooseClassAction implements ActionListener {
private Parameter myParameter;
@NotNull private final Module myModule;
public ChooseClassAction(@NotNull Parameter parameter, @NotNull Module module) {
myParameter = parameter;
myModule = module;
}
@Override
public void actionPerformed(ActionEvent e) {
final OnlyShowActivities filter = new OnlyShowActivities(myModule);
Project project = myModule.getProject();
TreeClassChooser chooser = TreeClassChooserFactory.getInstance(project)
.createWithInnerClassesScopeChooser("Select Activity", GlobalSearchScope.projectScope(project), new ClassFilter() {
@Override
public boolean isAccepted(PsiClass aClass) {
return filter.isDeclarationVisible(aClass, null) ==
JavaCodeFragment.VisibilityChecker.Visibility.VISIBLE;
}
}, null
);
//noinspection unchecked
Key<String> key = (Key<String>)getParameterKey(myParameter);
final String targetClassName = myState.get(key);
if (targetClassName != null) {
final PsiClass aClass = JavaPsiFacade.getInstance(project).findClass(targetClassName, GlobalSearchScope.allScope(project));
if (aClass != null) {
chooser.selectDirectory(aClass.getContainingFile().getContainingDirectory());
}
}
chooser.showDialog();
PsiClass aClass = chooser.getSelected();
if (aClass != null) {
myState.put(key, aClass.getQualifiedName());
}
}
}
private static class TemplateIconLoader extends CacheLoader<File, Optional<Icon>> {
@Nullable
@Override
public Optional<Icon> load(@NotNull File key) {
Logger log = Logger.getInstance(ActivityGalleryStep.class);
try {
if (key.isFile()) {
BufferedImage image = ImageIO.read(key);
if (image != null) {
return Optional.<Icon>of(new ImageIcon(image.getScaledInstance(256, 256, Image.SCALE_SMOOTH)));
}
else {
log.error("File " + key.getAbsolutePath() + " exists but is not a valid image");
}
}
else {
log.error("Image file " + key.getAbsolutePath() + " was not found");
}
}
catch (IOException e) {
log.warn(e);
}
return Optional.absent();
}
}
private class DeduplicateValuesFunction implements ParameterDefaultValueComputer.Deduplicator {
private final Project project;
private final Module module;
private final SourceProvider provider;
private final String packageName;
private DeduplicateValuesFunction() {
project = getProject();
module = getModule();
provider = myState.get(AddAndroidActivityPath.KEY_SOURCE_PROVIDER);
packageName = myState.get(myPackageNameKey);
}
@Override
@Nullable
public String deduplicate(@NotNull Parameter parameter, @Nullable String value) {
if (StringUtil.isEmpty(value) || !parameter.constraints.contains(Parameter.Constraint.UNIQUE)) {
return value;
}
String suggested = value;
String extension = FileUtilRt.getExtension(value);
boolean hasExtension = !extension.isEmpty();
int extensionOffset = value.length() - extension.length();
//noinspection ForLoopThatDoesntUseLoopVariable
for (int i = 2; !parameter.uniquenessSatisfied(project, module, provider, packageName, suggested); i++) {
if (hasExtension) {
suggested = value.substring(0, extensionOffset) + i + value.substring(extensionOffset);
}
else {
suggested = value + i;
}
}
return suggested;
}
}
}