blob: 69f5ea757c2c1c1f24f312790b96fea7dd85378c [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.structure.gradle;
import com.android.sdklib.AndroidTargetHash;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.BuildToolInfo;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.repository.descriptors.PkgType;
import com.android.sdklib.repository.local.LocalBuildToolPkgInfo;
import com.android.sdklib.repository.local.LocalPkgInfo;
import com.android.sdklib.repository.local.LocalSdk;
import com.android.tools.idea.gradle.parser.BuildFileKey;
import com.android.tools.idea.gradle.parser.BuildFileKeyType;
import com.android.tools.idea.gradle.parser.GradleBuildFile;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Splitter;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableMap;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.ui.TextBrowseFolderListener;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBTextField;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import org.jetbrains.android.sdk.AndroidSdkData;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public class KeyValuePane extends JPanel implements DocumentListener, ItemListener {
/**
* Listener class that gets called any time the value for the given key is modified in the UI. This should be used to mark that
* value is "dirty" and ensure it gets written out to the build file.
*/
public interface ModificationListener {
void modified(@NotNull BuildFileKey key);
}
private final BiMap<BuildFileKey, JComponent> myProperties = HashBiMap.create();
private boolean myIsUpdating;
private Map<BuildFileKey, Object> myCurrentBuildFileObject;
private Map<BuildFileKey, Object> myCurrentModelObject;
private final Project myProject;
private final ModificationListener myListener;
/**
* This structure lets us define known values to populate combo boxes for some keys. The user can choose one of those known values from
* the combo box, or enter a custom value. This structure is a map, where the key to the map is the BuildFileKey and the value is a
* sub-map. This sub-map lets us show different strings in the combo box in the UI than what we actually read and write to the underlying
* build file. For example, you can have a targetSdkVersion of 20 in the build file, but it will show that in the UI as
* "API 20: Android 4.4 (KitKat Wear)". This sub-map is bi-directional, and has keys of the values that appear in the build file, and
* values as what appears in the UI. For simple cases where the UI shows the same thing that appears in the build file, this can be
* an identity mapping.
*/
private final Map<BuildFileKey, BiMap<String, String>> myKeysWithKnownValues;
public KeyValuePane(@NotNull Project project, @NotNull ModificationListener listener) {
myProject = project;
myListener = listener;
LocalSdk sdk = null;
AndroidSdkData androidSdkData = AndroidSdkUtils.tryToChooseAndroidSdk();
if (androidSdkData != null) {
sdk = androidSdkData.getLocalSdk();
}
// Use immutable maps with builders for our built-in value maps because ImmutableBiMap ensures that iteration order is the same as
// insertion order.
ImmutableBiMap.Builder<String, String> buildToolsMapBuilder = ImmutableBiMap.builder();
ImmutableBiMap.Builder<String, String> apisMapBuilder = ImmutableBiMap.builder();
ImmutableBiMap.Builder<String, String> compiledApisMapBuilder = ImmutableBiMap.builder();
if (sdk != null) {
LocalPkgInfo[] buildToolsPackages = sdk.getPkgsInfos(PkgType.PKG_BUILD_TOOLS);
for (LocalPkgInfo buildToolsPackage : buildToolsPackages) {
if (!(buildToolsPackage instanceof LocalBuildToolPkgInfo)) {
continue;
}
BuildToolInfo buildToolInfo = ((LocalBuildToolPkgInfo)buildToolsPackage).getBuildToolInfo();
if (buildToolInfo == null) {
continue;
}
String buildToolVersion = buildToolInfo.getRevision().toString();
buildToolsMapBuilder.put(buildToolVersion, buildToolVersion);
}
for (IAndroidTarget target : sdk.getTargets()) {
if (target.isPlatform()) {
AndroidVersion version = target.getVersion();
String codename = version.getCodename();
String apiString, platformString;
if (codename != null) {
apiString = codename;
platformString = AndroidTargetHash.getPlatformHashString(version);
}
else {
platformString = apiString = Integer.toString(version.getApiLevel());
}
String label = AndroidSdkUtils.getTargetLabel(target);
apisMapBuilder.put(apiString, label);
compiledApisMapBuilder.put(platformString, label);
}
}
}
BiMap<String, String> installedBuildTools = buildToolsMapBuilder.build();
BiMap<String, String> installedApis = apisMapBuilder.build();
BiMap<String, String> installedCompileApis = compiledApisMapBuilder.build();
BiMap<String, String> javaCompatibility = ImmutableBiMap.of("JavaVersion.VERSION_1_6", "1.6", "JavaVersion.VERSION_1_7", "1.7");
myKeysWithKnownValues = ImmutableMap.<BuildFileKey, BiMap<String, String>>builder()
.put(BuildFileKey.MIN_SDK_VERSION, installedApis)
.put(BuildFileKey.TARGET_SDK_VERSION, installedApis)
.put(BuildFileKey.COMPILE_SDK_VERSION, installedCompileApis)
.put(BuildFileKey.BUILD_TOOLS_VERSION, installedBuildTools)
.put(BuildFileKey.SOURCE_COMPATIBILITY, javaCompatibility)
.put(BuildFileKey.TARGET_COMPATIBILITY, javaCompatibility)
.build();
}
/**
* Sets the current object as seen by parsing the build file directly. This controls what the user explicitly sets through the build file.
* Any keys that are set to null are unset in the build file and will take on default values when the build file is executed.
*/
public void setCurrentBuildFileObject(@Nullable Map<BuildFileKey, Object> currentBuildFileObject) {
myCurrentBuildFileObject = currentBuildFileObject;
}
/**
* Sets the current object as seen by querying the Gradle model after the build file is evaluated. This shows the user what the build file
* will actually do, showing the default values of keys (as supplied by the plugin) that are otherwise not explicitly set in the file.
*/
public void setCurrentModelObject(@Nullable Map<BuildFileKey, Object> currentModelObject) {
myCurrentModelObject = currentModelObject;
}
public void init(GradleBuildFile gradleBuildFile, Collection<BuildFileKey>properties) {
GridLayoutManager layout = new GridLayoutManager(properties.size() + 1, 2);
setLayout(layout);
GridConstraints constraints = new GridConstraints();
constraints.setAnchor(GridConstraints.ANCHOR_WEST);
constraints.setVSizePolicy(GridConstraints.SIZEPOLICY_FIXED);
for (BuildFileKey property : properties) {
constraints.setColumn(0);
constraints.setFill(GridConstraints.FILL_NONE);
constraints.setHSizePolicy(GridConstraints.SIZEPOLICY_FIXED);
JBLabel label = new JBLabel(property.getDisplayName());
add(label, constraints);
constraints.setColumn(1);
constraints.setFill(GridConstraints.FILL_HORIZONTAL);
constraints.setHSizePolicy(GridConstraints.SIZEPOLICY_WANT_GROW);
JComponent component;
switch(property.getType()) {
case BOOLEAN: {
constraints.setFill(GridConstraints.FILL_NONE);
ComboBox comboBox = getComboBox(false);
comboBox.addItem("");
comboBox.addItem("true");
comboBox.addItem("false");
comboBox.setPrototypeDisplayValue("(false) ");
component = comboBox;
break;
}
case FILE:
case FILE_AS_STRING: {
JBTextField textField = new JBTextField();
TextFieldWithBrowseButton fileField = new TextFieldWithBrowseButton(textField);
FileChooserDescriptor d = new FileChooserDescriptor(true, false, false, true, false, false);
d.setShowFileSystemRoots(true);
fileField.addBrowseFolderListener(new TextBrowseFolderListener(d));
fileField.getTextField().getDocument().addDocumentListener(this);
component = fileField;
break;
}
case REFERENCE: {
constraints.setFill(GridConstraints.FILL_NONE);
ComboBox comboBox = getComboBox(true);
if (hasKnownValues(property)) {
for (String s : myKeysWithKnownValues.get(property).values()) {
comboBox.addItem(s);
}
}
// If there are no hardcoded values, the combo box's values will get populated later when the panel for the container reference
// type wakes up and notifies us of its current values.
component = comboBox;
break;
}
case CLOSURE:
case STRING:
case INTEGER:
default: {
if (hasKnownValues(property)) {
constraints.setFill(GridConstraints.FILL_NONE);
ComboBox comboBox = getComboBox(true);
for (String s : myKeysWithKnownValues.get(property).values()) {
comboBox.addItem(s);
}
component = comboBox;
}
else {
JBTextField textField = new JBTextField();
textField.getDocument().addDocumentListener(this);
component = textField;
}
break;
}
}
add(component, constraints);
label.setLabelFor(component);
myProperties.put(property, component);
constraints.setRow(constraints.getRow() + 1);
}
constraints.setColumn(0);
constraints.setVSizePolicy(GridConstraints.FILL_VERTICAL);
constraints.setHSizePolicy(GridConstraints.SIZEPOLICY_FIXED);
add(new JBLabel(""), constraints);
updateUiFromCurrentObject();
}
public void updateReferenceValues(@NotNull BuildFileKey containerProperty, @NotNull Iterable<String> values) {
BuildFileKey itemType = containerProperty.getItemType();
if (itemType == null) {
return;
}
ComboBox comboBox = (ComboBox)myProperties.get(itemType);
if (comboBox == null) {
return;
}
myIsUpdating = true;
try {
String currentValue = comboBox.getEditor().getItem().toString();
comboBox.removeAllItems();
for (String value : values) {
comboBox.addItem(value);
}
comboBox.setSelectedItem(currentValue);
} finally {
myIsUpdating = false;
}
}
private ComboBox getComboBox(boolean editable) {
ComboBox comboBox = new ComboBox();
comboBox.addItemListener(this);
comboBox.setEditor(new ComboBoxEditor() {
private final JBTextField myTextField = new JBTextField();
@Override
public Component getEditorComponent() {
return myTextField;
}
@Override
public void setItem(Object o) {
myTextField.setText(o != null ? o.toString() : "");
}
@Override
public Object getItem() {
return myTextField.getText();
}
@Override
public void selectAll() {
myTextField.selectAll();
}
@Override
public void addActionListener(ActionListener actionListener) {
}
@Override
public void removeActionListener(ActionListener actionListener) {
}
});
comboBox.setEditable(true);
JBTextField editorComponent = (JBTextField)comboBox.getEditor().getEditorComponent();
editorComponent.setEditable(editable);
editorComponent.getDocument().addDocumentListener(this);
return comboBox;
}
/**
* Reads the state of the UI form objects and writes them into the currently selected object in the list, setting the dirty bit as
* appropriate.
*/
private void updateCurrentObjectFromUi() {
if (myIsUpdating || myCurrentBuildFileObject == null) {
return;
}
for (Map.Entry<BuildFileKey, JComponent> entry : myProperties.entrySet()) {
BuildFileKey key = entry.getKey();
JComponent component = entry.getValue();
Object currentValue = myCurrentBuildFileObject.get(key);
Object newValue;
BuildFileKeyType type = key.getType();
switch(type) {
case BOOLEAN: {
ComboBox comboBox = (ComboBox)component;
JBTextField editorComponent = (JBTextField)comboBox.getEditor().getEditorComponent();
int index = comboBox.getSelectedIndex();
if (index == 2) {
newValue = Boolean.FALSE;
editorComponent.setForeground(JBColor.BLACK);
}
else if (index == 1) {
newValue = Boolean.TRUE;
editorComponent.setForeground(JBColor.BLACK);
}
else {
newValue = null;
editorComponent.setForeground(JBColor.GRAY);
}
break;
}
case FILE:
case FILE_AS_STRING: {
newValue = ((TextFieldWithBrowseButton)component).getText();
if ("".equals(newValue)) {
newValue = null;
}
if (newValue != null) {
newValue = new File(newValue.toString());
}
break;
}
case INTEGER: {
try {
if (hasKnownValues(key)) {
String newStringValue = ((ComboBox)component).getEditor().getItem().toString();
newStringValue = getMappedValue(myKeysWithKnownValues.get(key).inverse(), newStringValue);
newValue = Integer.valueOf(newStringValue);
}
else {
newValue = Integer.valueOf(((JBTextField)component).getText());
}
}
catch (Exception e) {
newValue = null;
}
break;
}
case REFERENCE: {
newValue = ((ComboBox)component).getEditor().getItem();
String newStringValue = (String)newValue;
if (hasKnownValues(key)) {
newStringValue = getMappedValue(myKeysWithKnownValues.get(key).inverse(), newStringValue);
}
if (newStringValue != null && newStringValue.isEmpty()) {
newStringValue = null;
}
String prefix = getReferencePrefix(key);
if (newStringValue != null && !newStringValue.startsWith(prefix)) {
newStringValue = prefix + newStringValue;
}
newValue = newStringValue;
break;
}
case CLOSURE:
case STRING:
default: {
if (hasKnownValues(key)) {
String newStringValue = ((ComboBox)component).getEditor().getItem().toString();
newStringValue = getMappedValue(myKeysWithKnownValues.get(key).inverse(), newStringValue);
if (newStringValue.isEmpty()) {
newStringValue = null;
}
newValue = newStringValue;
}
else {
newValue = ((JBTextField)component).getText();
if ("".equals(newValue)) {
newValue = null;
}
}
if (type == BuildFileKeyType.CLOSURE && newValue != null) {
List newListValue = new ArrayList();
for (String s : Splitter.on(',').omitEmptyStrings().trimResults().split((String)newValue)) {
newListValue.add(key.getValueFactory().parse(s, myProject));
}
newValue = newListValue;
}
break;
}
}
if (!Objects.equal(currentValue, newValue)) {
if (newValue == null) {
myCurrentBuildFileObject.remove(key);
} else {
myCurrentBuildFileObject.put(key, newValue);
}
if (GradleBuildFile.shouldWriteValue(currentValue, newValue)) {
myListener.modified(key);
}
}
}
}
/**
* Updates the form UI objects to reflect the currently selected object. Clears the objects and disables them if there is no selected
* object.
*/
public void updateUiFromCurrentObject() {
myIsUpdating = true;
for (Map.Entry<BuildFileKey, JComponent> entry : myProperties.entrySet()) {
BuildFileKey key = entry.getKey();
JComponent component = entry.getValue();
Object value = myCurrentBuildFileObject != null ? myCurrentBuildFileObject.get(key) : null;
final Object modelValue = myCurrentModelObject != null ? myCurrentModelObject.get(key) : null;
switch(key.getType()) {
case BOOLEAN: {
ComboBox comboBox = (ComboBox)component;
String text = formatDefaultValue(modelValue);
comboBox.removeItemAt(0);
comboBox.insertItemAt(text, 0);
JBTextField editorComponent = (JBTextField)comboBox.getEditor().getEditorComponent();
if (Boolean.FALSE.equals(value)) {
comboBox.setSelectedIndex(2);
editorComponent.setForeground(JBColor.BLACK);
} else if (Boolean.TRUE.equals(value)) {
comboBox.setSelectedIndex(1);
editorComponent.setForeground(JBColor.BLACK);
} else {
comboBox.setSelectedIndex(0);
editorComponent.setForeground(JBColor.GRAY);
}
break;
}
case FILE:
case FILE_AS_STRING: {
TextFieldWithBrowseButton fieldWithButton = (TextFieldWithBrowseButton)component;
fieldWithButton.setText(value != null ? value.toString() : "");
JBTextField textField = (JBTextField)fieldWithButton.getTextField();
textField.getEmptyText().setText(formatDefaultValue(modelValue));
break;
}
case REFERENCE: {
String stringValue = (String)value;
if (hasKnownValues(key) && stringValue != null) {
stringValue = getMappedValue(myKeysWithKnownValues.get(key), stringValue);
}
String prefix = getReferencePrefix(key);
if (stringValue == null) {
stringValue = "";
}
else if (stringValue.startsWith(prefix)) {
stringValue = stringValue.substring(prefix.length());
}
ComboBox comboBox = (ComboBox)component;
JBTextField textField = (JBTextField)comboBox.getEditor().getEditorComponent();
textField.getEmptyText().setText(formatDefaultValue(modelValue));
comboBox.setSelectedItem(stringValue);
break;
}
case CLOSURE:
if (value instanceof List) {
value = Joiner.on(", ").join((List)value);
}
// Fall through to INTEGER/STRING/default case
case INTEGER:
case STRING:
default: {
if (hasKnownValues(key)) {
if (value != null) {
value = getMappedValue(myKeysWithKnownValues.get(key), value.toString());
}
ComboBox comboBox = (ComboBox)component;
comboBox.setSelectedItem(value != null ? value.toString() : "");
JBTextField textField = (JBTextField)comboBox.getEditor().getEditorComponent();
textField.getEmptyText().setText(formatDefaultValue(modelValue));
}
else {
JBTextField textField = (JBTextField)component;
textField.setText(value != null ? value.toString() : "");
textField.getEmptyText().setText(formatDefaultValue(modelValue));
}
break;
}
}
component.setEnabled(myCurrentBuildFileObject != null);
}
myIsUpdating = false;
}
@NotNull
private static String formatDefaultValue(@Nullable Object modelValue) {
if (modelValue == null) {
return "";
}
String s = modelValue.toString();
return !s.isEmpty() ? "(" + s + ")" : "";
}
@NotNull
private static String getMappedValue(@NotNull BiMap<String, String> map, @NotNull String value) {
if (map.containsKey(value)) {
value = map.get(value);
}
return value;
}
private boolean hasKnownValues(BuildFileKey key) {
return myKeysWithKnownValues.containsKey(key);
}
@Override
public void insertUpdate(@NotNull DocumentEvent documentEvent) {
updateCurrentObjectFromUi();
}
@Override
public void removeUpdate(@NotNull DocumentEvent documentEvent) {
updateCurrentObjectFromUi();
}
@Override
public void changedUpdate(@NotNull DocumentEvent documentEvent) {
updateCurrentObjectFromUi();
}
@Override
public void itemStateChanged(ItemEvent event) {
if (event.getStateChange() == ItemEvent.SELECTED) {
updateCurrentObjectFromUi();
}
}
@NotNull
private static String getReferencePrefix(@NotNull BuildFileKey key) {
BuildFileKey containerType = key.getContainerType();
if (containerType != null) {
String path = containerType.getPath();
String lastLeaf = path.substring(path.lastIndexOf('/') + 1);
return lastLeaf + ".";
} else {
return "";
}
}
}