/* * 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.wizard; 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.google.common.base.Function; import com.google.common.base.Objects; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.base.Strings; 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.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.ScopedStateStore.Key; import static com.android.tools.idea.wizard.ScopedStateStore.createKey; /** * Wizard step for specifying template-specific parameters. */ public class TemplateParameterStep2 extends DynamicWizardStepWithHeaderAndDescription { 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()); 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; /** * 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("Choose options for your new file", null, formFactor.getIcon(), disposable); 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(); ComboBoxItem[] items = new ComboBoxItem[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 ComboBoxItem 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 ComboBoxItem(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) { Map<Parameter, Object> computedDefaultValues = ParameterDefaultValueComputer. newDefaultValuesMap(parameters, parametersWithNonDefaultValues, getImplicitParameters(), new DeduplicateValuesFunction()); 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); } 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(Parameter parameter, Module module) { ChooseClassAction browseAction = new ChooseClassAction(module.getProject(), parameter); 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(), 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) { 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(); return null; } }); } 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 getDescriptionText() { return myParameterDescription; } @Override public void deriveValues(Set<Key> modified) { super.deriveValues(modified); if (myCurrentTemplate != null) { updateStateWithDefaults(myCurrentTemplate.getParameters()); updateControlsVisibility(); } } 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 ComboBoxItem) { ComboBoxItem selectedItem = (ComboBoxItem)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 ? null : myThumbnailsCache.apply(newValue); Icon icon = thumbnail != null ? thumbnail.orNull() : null; component.setIcon(icon); component.setVisible(thumbnail != 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; } 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 ComboBoxItem(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)); } } 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()); } } finally { myUpdatingDefaults = false; } } private Map<Parameter, Object> refreshParameterDefaults(Collection<Parameter> parameters, Map<String, Object> defaultValues) { 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"; } @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 static boolean isActivitySubclass(@NotNull PsiClass classDecl) { for (PsiClass superClass : classDecl.getSupers()) { String typename = superClass.getQualifiedName(); if ("android.app.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)) { return Visibility.VISIBLE; } } return Visibility.NOT_VISIBLE; } } private class ChooseClassAction implements ActionListener { private final Project myProject; private Parameter myParameter; public ChooseClassAction(Project project, Parameter parameter) { myProject = project; myParameter = parameter; } @Override public void actionPerformed(ActionEvent e) { final OnlyShowActivities filter = new OnlyShowActivities(); TreeClassChooser chooser = TreeClassChooserFactory.getInstance(myProject) .createWithInnerClassesScopeChooser("Select Activity", GlobalSearchScope.projectScope(myProject), 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(myProject).findClass(targetClassName, GlobalSearchScope.allScope(myProject)); 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; int extensionOffset = value.length() - 4; boolean hasExtension = value.charAt(extensionOffset) == '.'; //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; } } }