/*
* Copyright (C) 2013 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.tools.idea.templates.Template;
import com.android.tools.idea.templates.TemplateManager;
import com.android.tools.idea.templates.TemplateMetadata;
import com.google.common.collect.*;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.util.Disposer;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBLabel;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.MouseInputAdapter;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.io.File;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import static com.android.tools.idea.templates.TemplateMetadata.*;
import static com.android.tools.idea.wizard.FormFactorApiComboBox.AndroidTargetComboBoxItem;
import static com.android.tools.idea.wizard.FormFactorUtils.*;
import static com.android.tools.idea.wizard.FormFactorUtils.FormFactor.MOBILE;
import static com.android.tools.idea.wizard.ScopedStateStore.Key;
import static com.android.tools.idea.wizard.ScopedStateStore.Scope.STEP;
import static com.android.tools.idea.wizard.ScopedStateStore.Scope.WIZARD;
import static com.android.tools.idea.wizard.ScopedStateStore.createKey;
import static com.android.tools.idea.wizard.WizardConstants.NEWLY_INSTALLED_API_KEY;
/**
* ConfigureAndroidModuleStep is the first page in the New Project wizard that sets project/module name, location, and other project-global
* parameters.
*/
public class ConfigureFormFactorStep extends DynamicWizardStepWithHeaderAndDescription {
public static final String MIN_SDK_STRING = "Minimum SDK";
private static final Key<String> API_FEEDBACK_KEY = createKey("API Feedback", STEP, String.class);
public static final Key<Integer> NUM_ENABLED_FORM_FACTORS_KEY = createKey("NumberOfEnabledFormFactors", WIZARD, Integer.class);
private JPanel myPanel;
private JPanel myFormFactorPanel;
private JBLabel myHelpMeChooseLabel = new JBLabel("Help Me Choose");
private List<FormFactor> myFormFactors = Lists.newArrayList();
private ChooseApiLevelDialog myChooseApiLevelDialog = new ChooseApiLevelDialog(null, -1);
private Disposable myDisposable;
private Map<FormFactor, FormFactorSdkControls> myFormFactorApiSelectors = Maps.newHashMap();
public ConfigureFormFactorStep(@NotNull Disposable disposable) {
super("Select the form factors your app will run on", "Different platforms require separate SDKs",
null, disposable);
myDisposable = disposable;
Disposer.register(disposable, myChooseApiLevelDialog.getDisposable());
setBodyComponent(myPanel);
}
@Override
public void init() {
super.init();
myHelpMeChooseLabel.addMouseListener(new MouseInputAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
myChooseApiLevelDialog = new ChooseApiLevelDialog(null, myState.get(getMinApiLevelKey(MOBILE)));
Disposer.register(myDisposable, myChooseApiLevelDialog.getDisposable());
myChooseApiLevelDialog.show();
if (myChooseApiLevelDialog.isOK()) {
int minApiLevel = myChooseApiLevelDialog.getSelectedApiLevel();
myFormFactorApiSelectors.get(MOBILE).setSelectedItem(Integer.toString(minApiLevel));
}
}
});
myHelpMeChooseLabel.setMaximumSize(new Dimension(250, 200));
register(API_FEEDBACK_KEY, myHelpMeChooseLabel, new ComponentBinding<String, JBLabel>() {
@Override
public void setValue(@Nullable String newValue, @NotNull JBLabel label) {
final JBLabel referenceLabel = label;
final String referenceString = newValue;
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
referenceLabel.setText(referenceString);
}
});
}
});
registerValueDeriver(API_FEEDBACK_KEY, new ValueDeriver<String>() {
@Nullable
@Override
public Set<Key<?>> getTriggerKeys() {
return makeSetOf(getTargetComboBoxKey(MOBILE));
}
@Nullable
@Override
public String deriveValue(ScopedStateStore state, Key changedKey, @Nullable String currentValue) {
AndroidTargetComboBoxItem selectedItem = state.get(getTargetComboBoxKey(MOBILE));
if (selectedItem == null) {
return currentValue;
}
Integer selectedApi = selectedItem.apiLevel;
return String.format(Locale.getDefault(), "<html>Lower API levels target more devices, but have fewer features available. " +
"By targeting API %d and later, your app will run on approximately <b>%.1f%%</b> of the " +
"devices that are active on the Google Play Store. " +
"<span color=\"#%s\">Help me choose.</span></html>", selectedApi,
myChooseApiLevelDialog.getSupportedDistributionForApiLevel(selectedApi) * 100,
Integer.toHexString(JBColor.blue.getRGB()).substring(2)
);
}
});
populateAdditionalFormFactors();
}
private void populateAdditionalFormFactors() {
TemplateManager manager = TemplateManager.getInstance();
List<File> applicationTemplates = manager.getTemplatesInCategory(Template.CATEGORY_APPLICATION);
GridLayoutManager gridLayoutManager = new GridLayoutManager(applicationTemplates.size() * 2 + 1, 2);
gridLayoutManager.setVGap(5);
gridLayoutManager.setHGap(10);
myFormFactorPanel.setLayout(gridLayoutManager);
GridConstraints c = new GridConstraints();
c.setVSizePolicy(GridConstraints.SIZEPOLICY_FIXED);
int row = 0;
for (File templateFile : applicationTemplates) {
TemplateMetadata metadata = manager.getTemplate(templateFile);
if (metadata == null || metadata.getFormFactor() == null) {
continue;
}
FormFactor formFactor = FormFactor.get(metadata.getFormFactor());
if (formFactor == null) {
continue;
}
myFormFactors.add(formFactor);
c.setRow(row);
c.setColumn(0);
c.setFill(GridConstraints.FILL_NONE);
c.setAnchor(GridConstraints.ANCHOR_WEST);
JCheckBox inclusionCheckBox = new JCheckBox(formFactor.toString());
myFormFactorPanel.add(inclusionCheckBox, c);
register(FormFactorUtils.getInclusionKey(formFactor), inclusionCheckBox);
FormFactorSdkControls controls = new FormFactorSdkControls(formFactor, metadata.getMinSdk());
FormFactorApiComboBox minSdkComboBox = controls.getMinSdkCombo();
minSdkComboBox.setName(formFactor.id + ".minSdk");
controls.layout(myFormFactorPanel, ++row, inclusionCheckBox.getIconTextGap());
minSdkComboBox.register(this);
myFormFactorApiSelectors.put(formFactor, controls);
// If we don't have any valid targets for the given form factor, disable that form factor
if (minSdkComboBox.getItemCount() == 0) {
inclusionCheckBox.setSelected(false);
inclusionCheckBox.setEnabled(false);
inclusionCheckBox.setText(inclusionCheckBox.getText() + " (Not Installed)");
} else if (row == 1) {
myState.put(FormFactorUtils.getInclusionKey(formFactor), true);
}
if (formFactor.equals(MOBILE)) {
c.setRow(++row);
c.setColumn(1);
c.setAnchor(GridConstraints.ANCHOR_NORTHWEST);
c.setFill(GridConstraints.FILL_NONE);
myFormFactorPanel.add(myHelpMeChooseLabel, c);
}
row++;
}
}
@Override
public void onEnterStep() {
super.onEnterStep();
if (myState.containsKey(NEWLY_INSTALLED_API_KEY)) {
FormFactorApiComboBox.loadInstalledVersions();
}
}
@Override
public void deriveValues(Set<Key> modified) {
super.deriveValues(modified);
// Persist the min API level choices on a per-form factor basis
int enabledFormFactors = 0;
for (FormFactor formFactor : myFormFactors) {
Boolean included = myState.get(getInclusionKey(formFactor));
// Disable api selection for non-enabled form factors and check to see if only one is selected
if (included != null) {
if (myFormFactorApiSelectors.containsKey(formFactor)) {
myFormFactorApiSelectors.get(formFactor).setEnabled(included);
}
if (included) {
enabledFormFactors++;
FormFactorSdkControls controls = myFormFactorApiSelectors.get(formFactor);
if (controls != null) {
controls.getMinSdkCombo().deriveValues(myState, modified);
}
}
}
}
myState.put(NUM_ENABLED_FORM_FACTORS_KEY, enabledFormFactors);
}
@Override
public boolean validate() {
setErrorHtml("");
Integer enabledFormFactors = myState.get(NUM_ENABLED_FORM_FACTORS_KEY);
if (enabledFormFactors == null || enabledFormFactors < 1) {
// Don't allow an empty project
setErrorHtml("At least one form factor must be selected.");
return false;
}
for (FormFactor formFactor : myFormFactors) {
Boolean included = myState.get(getInclusionKey(formFactor));
// Disable api selection for non-enabled form factors and check to see if only one is selected
if (included != null && included) {
if (myState.get(getMinApiKey(formFactor)) == null) {
// Don't allow the user to continue unless all minAPIs are chosen
setErrorHtml("Each form factor must have a Minimum SDK level selected.");
return false;
}
}
}
return true;
}
@NotNull
@Override
public String getStepName() {
return "Configure Form Factors";
}
@Nullable
public String getHelpText(@NotNull String param) {
if (param.equals(ATTR_MIN_API)) {
return "Choose the lowest version of Android that your application will support. Lower API levels target more devices, " +
"but means fewer features are available. By targeting API 8 and later, you reach approximately 95% of the market.";
} else if (param.equals(ATTR_TARGET_API)) {
return "Choose the highest API level that the application is known to work with. This attribute informs the system that you have " +
"tested against the target version and the system should not enable any compatibility behaviors to maintain your app's " +
"forward-compatibility with the target version. The application is still able to run on older versions (down to " +
"minSdkVersion). Your application may look dated if you are not targeting the current version.";
} else if (param.equals(ATTR_BUILD_API)) {
return "Choose a target API to compile your code against, from your installed SDKs. This is typically the most recent version, " +
"or the first version that supports all the APIs you want to directly access without reflection.";
} else {
return null;
}
}
@Override
public JComponent getPreferredFocusedComponent() {
return myPanel;
}
@Nullable
@Override
protected JComponent getHeader() {
return ConfigureAndroidProjectPath.buildConfigurationHeader();
}
@Override
@Nullable
protected JBColor getTitleTextColor() {
return WizardConstants.ANDROID_NPW_TITLE_COLOR;
}
private static final class FormFactorSdkControls {
private final JLabel myLabel;
private final FormFactorApiComboBox myMinSdkCombobox;
public FormFactorSdkControls(FormFactor formFactor, int minApi) {
myLabel = new JLabel(MIN_SDK_STRING);
myMinSdkCombobox = new FormFactorApiComboBox(formFactor, minApi);
}
public void setEnabled(boolean enabled) {
for (JComponent component : ImmutableSet.of(myLabel, myMinSdkCombobox)) {
component.setEnabled(enabled);
}
}
public void setSelectedItem(String item) {
ScopedDataBinder.setSelectedItem(myMinSdkCombobox, item);
}
public JLabel getLabel() {
return myLabel;
}
public FormFactorApiComboBox getMinSdkCombo() {
return myMinSdkCombobox;
}
private void layout(JPanel panel, int row, int ident) {
GridConstraints c = new GridConstraints();
c.setVSizePolicy(GridConstraints.SIZEPOLICY_FIXED);
c.setRow(row);
c.setColumn(0);
c.setFill(GridConstraints.FILL_NONE);
c.setAnchor(GridConstraints.ANCHOR_WEST);
c.setRow(row);
c.setIndent(ident);
panel.add(myLabel, c);
c.setIndent(0);
c.setColumn(1);
c.setFill(GridConstraints.FILL_HORIZONTAL);
panel.add(myMinSdkCombobox, c);
}
}
}