// Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
// Licensed under the Apache License, Version 2.0 (see LICENSE).
package com.twitter.intellij.pants.settings;
import com.google.common.annotations.VisibleForTesting;
import com.intellij.openapi.externalSystem.service.settings.AbstractExternalProjectSettingsControl;
import com.intellij.openapi.externalSystem.service.settings.ExternalSystemSettingsControlCustomizer;
import com.intellij.openapi.externalSystem.util.ExternalSystemUiUtil;
import com.intellij.openapi.externalSystem.util.PaintAwarePanel;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.ui.CheckBoxList;
import com.intellij.ui.components.JBCheckBox;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.GridBag;
import com.intellij.util.ui.StatusText;
import com.intellij.util.ui.UIUtil;
import com.twitter.intellij.pants.PantsBundle;
import com.twitter.intellij.pants.util.PantsUtil;
import org.jetbrains.annotations.NotNull;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
public class PantsProjectSettingsControl extends AbstractExternalProjectSettingsControl<PantsProjectSettings> {
@VisibleForTesting
protected CheckBoxList<String> myTargetSpecsBox = new CheckBoxList<>();
private JBCheckBox myLibsWithSourcesCheckBox = new JBCheckBox(PantsBundle.message("pants.settings.text.with.sources.and.docs"));
private JBCheckBox myEnableIncrementalImportCheckBox = new JBCheckBox(PantsBundle.message("pants.settings.text.with.incremental.import"));
private JBCheckBox myUseIdeaProjectJdkCheckBox = new JBCheckBox(PantsBundle.message("pants.settings.text.with.jdk.enforcement"));
@VisibleForTesting
protected Set<String> errors = new HashSet<>();
// Key to keep track whether target specs are requested for the same project path.
private String lastPath = "";
private PantsProjectSettings mySettings;
public PantsProjectSettingsControl(@NotNull PantsProjectSettings settings) {
super(null, settings, new ExternalSystemSettingsControlCustomizer(true, true));
mySettings = settings;
}
@Override
protected void fillExtraControls(@NotNull PaintAwarePanel content, int indentLevel) {
myLibsWithSourcesCheckBox.setSelected(mySettings.isLibsWithSources());
myEnableIncrementalImportCheckBox.setSelected(mySettings.isEnableIncrementalImport());
myUseIdeaProjectJdkCheckBox.setSelected(mySettings.isUseIdeaProjectJdk());
mySettings.getTargetSpecs().forEach(spec -> myTargetSpecsBox.addItem(spec, spec, true));
List<JComponent> boxes = ContainerUtil.newArrayList(
myLibsWithSourcesCheckBox,
myEnableIncrementalImportCheckBox,
myUseIdeaProjectJdkCheckBox,
new JBLabel(PantsBundle.message("pants.settings.text.targets")),
new JBScrollPane(myTargetSpecsBox)
);
GridBag lineConstraints = ExternalSystemUiUtil.getFillLineConstraints(indentLevel);
for (JComponent component : boxes) {
content.add(component, lineConstraints);
}
}
// It is silly `CheckBoxList` does not provide an iterator.
private List<String> getSelectedTargetSpecsFromBoxes() {
List<String> selectedSpecs = new ArrayList<>();
for (int i = 0; i < myTargetSpecsBox.getModel().getSize(); i++) {
JCheckBox checkBox = (JCheckBox) myTargetSpecsBox.getModel().getElementAt(i);
if (checkBox.isSelected()) {
selectedSpecs.add(checkBox.getText());
}
}
return selectedSpecs;
}
@Override
protected boolean isExtraSettingModified() {
PantsProjectSettings newSettings = new PantsProjectSettings(
getSelectedTargetSpecsFromBoxes(),
// Project path is not visible to user, so it will stay the same.
getInitialSettings().getExternalProjectPath(),
myLibsWithSourcesCheckBox.isSelected(),
myEnableIncrementalImportCheckBox.isSelected(),
myUseIdeaProjectJdkCheckBox.isSelected()
);
return !newSettings.equals(getInitialSettings());
}
@Override
protected void resetExtraSettings(boolean isDefaultModuleCreation) {
}
public void onProjectPathChanged(@NotNull final String projectPath) {
if (lastPath.equals(projectPath)) {
return;
}
lastPath = projectPath;
myTargetSpecsBox.clear();
errors.clear();
final VirtualFile file = VirtualFileManager.getInstance().findFileByUrl(VfsUtil.pathToUrl(projectPath));
if (file == null || !PantsUtil.isPantsProjectFile(file)) {
myTargetSpecsBox.setEnabled(true);
errors.add(String.format("Pants project not found given project path: %s", projectPath));
return;
}
if (file.isDirectory()) {
myTargetSpecsBox.setEnabled(false);
Optional<String> relativeProjectPath = PantsUtil.getRelativeProjectPath(file.getPath());
if (!relativeProjectPath.isPresent()) {
errors.add(String.format("Fail to find relative path from %s to build root.", file.getPath()));
return;
}
String spec = relativeProjectPath.get() + "/::";
myTargetSpecsBox.setEmptyText(spec);
myTargetSpecsBox.addItem(spec, spec, true);
myLibsWithSourcesCheckBox.setEnabled(true);
}
else if (PantsUtil.isExecutable(file.getPath())) {
myTargetSpecsBox.setEnabled(false);
myTargetSpecsBox.setEmptyText(file.getName());
myLibsWithSourcesCheckBox.setSelected(false);
myLibsWithSourcesCheckBox.setEnabled(false);
}
else {
myTargetSpecsBox.setEnabled(true);
myTargetSpecsBox.setEmptyText(StatusText.DEFAULT_EMPTY_TEXT);
myLibsWithSourcesCheckBox.setEnabled(true);
ProgressManager.getInstance().run(new Task.Modal(getProject(), PantsBundle.message("pants.getting.target.list"), false) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
loadTargets(projectPath);
}
});
}
}
private void loadTargets(final String projectPath) {
if (!PantsUtil.isBUILDFilePath(projectPath)) {
return;
}
final Collection<String> targets = PantsUtil.listAllTargets(projectPath);
UIUtil.invokeLaterIfNeeded(
() -> {
myTargetSpecsBox.clear();
targets.forEach(s -> myTargetSpecsBox.addItem(s, s, false));
}
);
}
@Override
protected void applyExtraSettings(@NotNull PantsProjectSettings settings) {
settings.setLibsWithSources(myLibsWithSourcesCheckBox.isSelected());
settings.setEnableIncrementalImport(myEnableIncrementalImportCheckBox.isSelected());
settings.setUseIdeaProjectJdk(myUseIdeaProjectJdkCheckBox.isSelected());
settings.setTargetSpecs(getSelectedTargetSpecsFromBoxes());
}
@Override
public boolean validate(@NotNull PantsProjectSettings settings) throws ConfigurationException {
final String projectUrl = VfsUtil.pathToUrl(settings.getExternalProjectPath());
final VirtualFile file = VirtualFileManager.getInstance().findFileByUrl(projectUrl);
if (file == null) {
throw new ConfigurationException(PantsBundle.message("pants.error.file.not.exists", projectUrl));
}
if (PantsUtil.isExecutable(file.getPath())) {
return true;
}
if (!PantsUtil.isPantsProjectFile(file)) {
throw new ConfigurationException(PantsBundle.message("pants.error.not.build.file.path.or.directory"));
}
if (PantsUtil.isBUILDFileName(file.getName()) && myTargetSpecsBox.getSelectedIndices().length == 0) {
throw new ConfigurationException(PantsBundle.message("pants.error.no.targets.are.selected"));
}
if (!errors.isEmpty()) {
String errorMessage = String.join("\n", errors);
errors.clear();
throw new ConfigurationException(errorMessage);
}
return true;
}
}