package com.jetbrains.lang.dart.sdk;
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.icons.AllIcons;
import com.intellij.ide.actions.ShowSettingsUtilImpl;
import com.intellij.ide.browsers.BrowserSpecificSettings;
import com.intellij.ide.browsers.WebBrowser;
import com.intellij.ide.browsers.chrome.ChromeSettings;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.ex.ApplicationManagerEx;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.module.ModuleType;
import com.intellij.openapi.options.Configurable.NoScroll;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.options.SearchableConfigurable;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.IconLoader;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.ui.*;
import com.intellij.ui.components.JBCheckBox;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.treeStructure.treetable.TreeColumnInfo;
import com.intellij.util.ui.ColumnInfo;
import com.intellij.util.ui.UIUtil;
import com.intellij.xml.util.XmlStringUtil;
import com.jetbrains.lang.dart.DartBundle;
import com.jetbrains.lang.dart.flutter.FlutterUtil;
import com.jetbrains.lang.dart.ide.runner.client.DartiumUtil;
import gnu.trove.THashSet;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.text.JTextComponent;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.ExpandVetoException;
import java.io.File;
import java.util.*;
public class DartConfigurable implements SearchableConfigurable, NoScroll {
private static final String DART_SETTINGS_PAGE_ID = "dart.settings";
private static final String DART_SETTINGS_PAGE_NAME = DartBundle.message("dart.title");
private JPanel myMainPanel;
private JBCheckBox myEnableDartSupportCheckBox;
private JPanel mySettingsPanel;
private ComboboxWithBrowseButton mySdkPathComboWithBrowse;
private JBLabel myVersionLabel;
private JBCheckBox myCheckSdkUpdateCheckBox;
// disabled and unchecked, shown in UI instead of myCheckSdkUpdateCheckBox if selected Dart SDK is a part of a Flutter SDK
private JBCheckBox myCheckSdkUpdateCheckBoxFake;
private ComboBox mySdkUpdateChannelCombo;
private JButton myCheckSdkUpdateButton;
private ComboboxWithBrowseButton myDartiumPathComboWithBrowse;
private JButton myDartiumSettingsButton;
private JBCheckBox myCheckedModeCheckBox;
private JPanel myModulesPanel;
private CheckboxTreeTable myModulesCheckboxTreeTable;
private JBLabel myErrorLabel;
private final @NotNull Project myProject;
private boolean myInReset = false;
private boolean myDartSupportEnabledInitial;
private @Nullable DartSdk mySdkInitial;
private final @NotNull Collection<Module> myModulesWithDartSdkLibAttachedInitial = new THashSet<>();
private @Nullable WebBrowser myDartiumInitial;
private ChromeSettings myDartiumSettingsCurrent;
public DartConfigurable(final @NotNull Project project) {
myProject = project;
initEnableDartSupportCheckBox();
initDartSdkAndDartiumControls();
initModulesPanel();
myErrorLabel.setIcon(AllIcons.Actions.Lightning);
}
private void initEnableDartSupportCheckBox() {
myEnableDartSupportCheckBox.setText(DartBundle.message("enable.dart.support.for.project.0", myProject.getName()));
myEnableDartSupportCheckBox.addActionListener(e -> {
updateControlsEnabledState();
updateErrorLabel();
});
}
private void initDartSdkAndDartiumControls() {
final Computable<ChromeSettings> currentDartiumSettingsRetriever = () -> myDartiumSettingsCurrent;
final Computable<Boolean> isResettingControlsComputable = () -> myInReset;
DartSdkUtil.initDartSdkAndDartiumControls(myProject, mySdkPathComboWithBrowse, myVersionLabel, myDartiumPathComboWithBrowse,
currentDartiumSettingsRetriever, myDartiumSettingsButton,
isResettingControlsComputable);
final JTextComponent sdkEditor = (JTextComponent)mySdkPathComboWithBrowse.getComboBox().getEditor().getEditorComponent();
sdkEditor.getDocument().addDocumentListener(new DocumentAdapter() {
protected void textChanged(final DocumentEvent e) {
final String sdkHomePath = getTextFromCombo(mySdkPathComboWithBrowse);
if (!sdkHomePath.isEmpty()) {
final String version = DartSdkUtil.getSdkVersion(sdkHomePath);
if (version != null && (version.contains("-dev.") || version.contains("-edge."))) {
mySdkUpdateChannelCombo.setSelectedItem(DartSdkUpdateOption.StableAndDev);
}
}
updateControlsEnabledState();
updateErrorLabel();
}
});
final JTextComponent dartiumEditor = (JTextComponent)myDartiumPathComboWithBrowse.getComboBox().getEditor().getEditorComponent();
dartiumEditor.getDocument().addDocumentListener(new DocumentAdapter() {
protected void textChanged(final DocumentEvent e) {
updateErrorLabel();
}
});
myCheckSdkUpdateCheckBox.addActionListener(e -> {
final boolean enabled = myCheckSdkUpdateCheckBox.isSelected() && myCheckSdkUpdateCheckBox.isEnabled();
mySdkUpdateChannelCombo.setEnabled(enabled);
myCheckSdkUpdateButton.setEnabled(enabled);
if (enabled) {
IdeFocusManager.getInstance(myProject).requestFocus(mySdkUpdateChannelCombo, true);
}
});
mySdkUpdateChannelCombo.setModel(new DefaultComboBoxModel(DartSdkUpdateOption.OPTIONS_TO_SHOW_IN_COMBO));
mySdkUpdateChannelCombo.setRenderer(new ListCellRendererWrapper<DartSdkUpdateOption>() {
@Override
public void customize(JList list, DartSdkUpdateOption value, int index, boolean selected, boolean hasFocus) {
setText(value.getPresentableName());
}
});
myCheckSdkUpdateButton.addActionListener(e -> {
final Runnable runnable = this::checkSdkUpdate;
ApplicationManagerEx.getApplicationEx()
.runProcessWithProgressSynchronously(runnable, DartBundle.message("checking.dart.sdk.update"), true, myProject, myMainPanel);
});
}
private void checkSdkUpdate() {
final String currentSdkVersion = myVersionLabel.getText();
final DartSdkUpdateChecker.SdkUpdateInfo sdkUpdateInfo =
DartSdkUpdateChecker.getSdkUpdateInfo((DartSdkUpdateOption)mySdkUpdateChannelCombo.getSelectedItem());
ApplicationManager.getApplication().invokeLater(() -> {
if (sdkUpdateInfo == null) {
Messages.showErrorDialog(myProject,
DartBundle.message("dart.sdk.update.check.failed"),
DartBundle.message("dart.sdk.update.title"));
}
else {
final String message;
if (currentSdkVersion == null || currentSdkVersion.isEmpty()) {
message = DartBundle.message("dart.sdk.0.available.for.download", sdkUpdateInfo.myVersion, sdkUpdateInfo.myDownloadUrl);
}
else if (DartSdkUpdateChecker.compareDartSdkVersions(currentSdkVersion, sdkUpdateInfo.myVersion) >= 0) {
message = DartBundle.message("dart.sdk.0.is.up.to.date", currentSdkVersion);
}
else {
message =
DartBundle.message("new.dart.sdk.0.available.for.download..dialog", sdkUpdateInfo.myVersion, sdkUpdateInfo.myDownloadUrl);
}
Messages.showInfoMessage(myProject, message, DartBundle.message("dart.sdk.update.title"));
}
}, ModalityState.defaultModalityState(), myProject.getDisposed());
}
private void initModulesPanel() {
if (!DartSdkLibUtil.isIdeWithMultipleModuleSupport()) {
myModulesPanel.setVisible(false);
return;
}
final Module[] modules = ModuleManager.getInstance(myProject).getModules();
Arrays.sort(modules, Comparator.comparing(module -> module.getName().toLowerCase(Locale.US)));
final CheckedTreeNode rootNode = new CheckedTreeNode(myProject);
((DefaultTreeModel)myModulesCheckboxTreeTable.getTree().getModel()).setRoot(rootNode);
myModulesCheckboxTreeTable.getTree().setRootVisible(true);
myModulesCheckboxTreeTable.getTree().setShowsRootHandles(false);
for (final Module module : modules) {
rootNode.add(new CheckedTreeNode(module));
}
((DefaultTreeModel)myModulesCheckboxTreeTable.getTree().getModel()).reload(rootNode);
}
@Override
@NotNull
public String getId() {
return "dart.settings";
}
@Override
@Nls
public String getDisplayName() {
return DART_SETTINGS_PAGE_NAME;
}
@Override
@Nullable
public String getHelpTopic() {
return "settings.dart.settings";
}
@Override
@Nullable
public JComponent createComponent() {
return myMainPanel;
}
@Override
public boolean isModified() {
final String sdkHomePath = getTextFromCombo(mySdkPathComboWithBrowse);
final boolean sdkSelected = DartSdkUtil.isDartSdkHome(sdkHomePath);
// was disabled, now disabled (or no sdk selected) => not modified, do not care about other controls
if (!myDartSupportEnabledInitial && (!myEnableDartSupportCheckBox.isSelected() || !sdkSelected)) return false;
// was enabled, now disabled => modified
if (myDartSupportEnabledInitial && !myEnableDartSupportCheckBox.isSelected()) return true;
// was disabled, now enabled or was enabled, now enabled => need to check further
final String initialSdkHomePath = mySdkInitial == null ? "" : mySdkInitial.getHomePath();
if (sdkSelected && !sdkHomePath.equals(initialSdkHomePath)) return true;
final boolean flutter = FlutterUtil.getFlutterRoot(sdkHomePath) != null;
if (!flutter) {
final DartSdkUpdateOption sdkUpdateOption = myCheckSdkUpdateCheckBox.isSelected()
? (DartSdkUpdateOption)mySdkUpdateChannelCombo.getSelectedItem()
: DartSdkUpdateOption.DoNotCheck;
if (sdkUpdateOption != DartSdkUpdateOption.getDartSdkUpdateOption()) return true;
}
final String dartiumPath = getTextFromCombo(myDartiumPathComboWithBrowse);
final String dartiumPathInitial = myDartiumInitial == null ? null : myDartiumInitial.getPath();
if (!dartiumPath.isEmpty() && new File(dartiumPath).exists() && !dartiumPath.equals(dartiumPathInitial)) return true;
if (myDartiumInitial != null && !myDartiumSettingsCurrent.equals(myDartiumInitial.getSpecificSettings())) return true;
if (DartSdkLibUtil.isIdeWithMultipleModuleSupport()) {
final Module[] selectedModules = myModulesCheckboxTreeTable.getCheckedNodes(Module.class);
if (selectedModules.length != myModulesWithDartSdkLibAttachedInitial.size()) return true;
for (final Module module : selectedModules) {
if (!myModulesWithDartSdkLibAttachedInitial.contains(module)) return true;
}
}
else {
if (myDartSupportEnabledInitial != myEnableDartSupportCheckBox.isSelected()) return true;
}
return false;
}
@NotNull
private static String getTextFromCombo(@NotNull final ComboboxWithBrowseButton combo) {
return FileUtilRt.toSystemIndependentName(combo.getComboBox().getEditor().getItem().toString().trim());
}
@Override
public void reset() {
myInReset = true;
// remember initial state
mySdkInitial = DartSdk.getDartSdk(myProject);
myModulesWithDartSdkLibAttachedInitial.clear();
if (mySdkInitial != null) {
myModulesWithDartSdkLibAttachedInitial.addAll(DartSdkLibUtil.getModulesWithDartSdkEnabled(myProject));
}
myDartSupportEnabledInitial = !myModulesWithDartSdkLibAttachedInitial.isEmpty();
myDartiumInitial = DartiumUtil.getDartiumBrowser();
myDartiumSettingsCurrent = new ChromeSettings();
if (myDartiumInitial != null) {
final BrowserSpecificSettings browserSpecificSettings = myDartiumInitial.getSpecificSettings();
if (browserSpecificSettings instanceof ChromeSettings) {
myDartiumSettingsCurrent = (ChromeSettings)browserSpecificSettings.clone();
}
}
// reset UI
myEnableDartSupportCheckBox.setSelected(myDartSupportEnabledInitial);
final String sdkInitialPath = mySdkInitial == null ? "" : FileUtilRt.toSystemDependentName(mySdkInitial.getHomePath());
mySdkPathComboWithBrowse.getComboBox().getEditor().setItem(sdkInitialPath);
if (!sdkInitialPath.isEmpty()) {
ensureComboModelContainsCurrentItem(mySdkPathComboWithBrowse.getComboBox());
}
final DartSdkUpdateOption sdkUpdateOption = DartSdkUpdateOption.getDartSdkUpdateOption();
myCheckSdkUpdateCheckBox.setSelected(sdkUpdateOption != DartSdkUpdateOption.DoNotCheck);
mySdkUpdateChannelCombo.setSelectedItem(sdkUpdateOption);
final String dartiumInitialPath =
myDartiumInitial == null ? "" : FileUtilRt.toSystemDependentName(StringUtil.notNullize(myDartiumInitial.getPath()));
myDartiumPathComboWithBrowse.getComboBox().getEditor().setItem(dartiumInitialPath);
if (!dartiumInitialPath.isEmpty()) {
ensureComboModelContainsCurrentItem(myDartiumPathComboWithBrowse.getComboBox());
}
// we decided to save one line in settings and always use Dartium in checked mode
myCheckedModeCheckBox.setVisible(false);
DartiumUtil.setCheckedMode(myDartiumSettingsCurrent.getEnvironmentVariables(), true);
//final boolean checkedMode = myDartiumInitial == null || DartiumUtil.isCheckedMode(myDartiumSettingsCurrent.getEnvironmentVariables());
//myCheckedModeCheckBox.setSelected(checkedMode);
if (DartSdkLibUtil.isIdeWithMultipleModuleSupport()) {
final CheckedTreeNode rootNode = (CheckedTreeNode)myModulesCheckboxTreeTable.getTree().getModel().getRoot();
rootNode.setChecked(false);
final Enumeration children = rootNode.children();
while (children.hasMoreElements()) {
final CheckedTreeNode node = (CheckedTreeNode)children.nextElement();
node.setChecked(myModulesWithDartSdkLibAttachedInitial.contains((Module)node.getUserObject()));
}
}
updateControlsEnabledState();
updateErrorLabel();
myInReset = false;
}
private static void ensureComboModelContainsCurrentItem(@NotNull final JComboBox comboBox) {
final Object currentItem = comboBox.getEditor().getItem();
boolean contains = false;
for (int i = 0; i < comboBox.getModel().getSize(); i++) {
if (currentItem.equals(comboBox.getModel().getElementAt(i))) {
contains = true;
break;
}
}
if (!contains) {
((DefaultComboBoxModel)comboBox.getModel()).insertElementAt(currentItem, 0);
comboBox.setSelectedItem(currentItem); // to set focus on current item in combo popup
comboBox.getEditor().setItem(currentItem); // to set current item in combo itself
}
}
@Override
public void apply() throws ConfigurationException {
// similar to DartModuleBuilder.setupSdkAndDartium()
final Runnable runnable = () -> {
if (myEnableDartSupportCheckBox.isSelected()) {
final String sdkHomePath = getTextFromCombo(mySdkPathComboWithBrowse);
if (DartSdkUtil.isDartSdkHome(sdkHomePath)) {
DartSdkUtil.updateKnownSdkPaths(myProject, sdkHomePath);
DartSdkLibUtil.ensureDartSdkConfigured(myProject, sdkHomePath);
DaemonCodeAnalyzer.getInstance(myProject).restart();
final Module[] modules = DartSdkLibUtil.isIdeWithMultipleModuleSupport()
? myModulesCheckboxTreeTable.getCheckedNodes(Module.class)
: ModuleManager.getInstance(myProject).getModules();
DartSdkLibUtil.enableDartSdkForSpecifiedModulesAndDisableForOthers(myProject, modules);
}
final boolean flutter = FlutterUtil.getFlutterRoot(sdkHomePath) != null;
if (!flutter) {
final DartSdkUpdateOption sdkUpdateOption = myCheckSdkUpdateCheckBox.isSelected()
? (DartSdkUpdateOption)mySdkUpdateChannelCombo.getSelectedItem()
: DartSdkUpdateOption.DoNotCheck;
DartSdkUpdateOption.setDartSdkUpdateOption(sdkUpdateOption);
}
final String dartiumPath = getTextFromCombo(myDartiumPathComboWithBrowse);
DartiumUtil.applyDartiumSettings(dartiumPath, myDartiumSettingsCurrent);
}
else {
if (myModulesWithDartSdkLibAttachedInitial.size() > 0 && mySdkInitial != null) {
DartSdkLibUtil.disableDartSdk(myModulesWithDartSdkLibAttachedInitial);
}
}
};
ApplicationManager.getApplication().runWriteAction(runnable);
reset(); // because we rely on remembering initial state
}
@Override
public void disposeUIResources() {
mySdkInitial = null;
myModulesWithDartSdkLibAttachedInitial.clear();
myDartiumInitial = null;
myDartiumSettingsCurrent = null;
}
private void updateControlsEnabledState() {
UIUtil.setEnabled(mySettingsPanel, myEnableDartSupportCheckBox.isSelected(), true);
final boolean flutter = FlutterUtil.getFlutterRoot(getTextFromCombo(mySdkPathComboWithBrowse)) != null;
myCheckSdkUpdateCheckBox.setVisible(!flutter);
final boolean enabled = myCheckSdkUpdateCheckBox.isVisible() &&
myCheckSdkUpdateCheckBox.isEnabled() &&
myCheckSdkUpdateCheckBox.isSelected();
mySdkUpdateChannelCombo.setEnabled(enabled);
myCheckSdkUpdateButton.setEnabled(enabled);
myCheckSdkUpdateCheckBoxFake.setVisible(flutter);
myCheckSdkUpdateCheckBoxFake.setEnabled(false);
}
private void updateErrorLabel() {
final String message = getErrorMessage();
myErrorLabel
.setText(XmlStringUtil.wrapInHtml("<font color='#" + ColorUtil.toHex(JBColor.RED) + "'><left>" + message + "</left></font>"));
myErrorLabel.setVisible(message != null);
}
@Nullable
private String getErrorMessage() {
if (!myEnableDartSupportCheckBox.isSelected()) {
return null;
}
String message = DartSdkUtil.getErrorMessageIfWrongSdkRootPath(getTextFromCombo(mySdkPathComboWithBrowse));
if (message != null) return message;
message = DartiumUtil.getErrorMessageIfWrongDartiumPath(getTextFromCombo(myDartiumPathComboWithBrowse));
if (message != null) return message;
if (DartSdkLibUtil.isIdeWithMultipleModuleSupport()) {
final Module[] modules = myModulesCheckboxTreeTable.getCheckedNodes(Module.class);
if (modules.length == 0) {
return DartBundle.message("warning.no.modules.selected.dart.support.will.be.disabled");
}
}
return null;
}
private void createUIComponents() {
mySdkPathComboWithBrowse = new ComboboxWithBrowseButton(new ComboBox<>());
myDartiumPathComboWithBrowse = new ComboboxWithBrowseButton(new ComboBox<>());
final CheckboxTree.CheckboxTreeCellRenderer checkboxTreeCellRenderer = new CheckboxTree.CheckboxTreeCellRenderer() {
@Override
public void customizeRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
if (!(value instanceof CheckedTreeNode)) return;
final boolean dartSupportEnabled = myEnableDartSupportCheckBox.isSelected();
final CheckedTreeNode node = (CheckedTreeNode)value;
final Object userObject = node.getUserObject();
if (userObject instanceof Project) {
if (!dartSupportEnabled) {
//disabled state is also used as partially selected, that's why we do not change 'enabled' state if dartSupportEnabled
getCheckbox().setEnabled(false);
}
getTextRenderer().setEnabled(dartSupportEnabled);
getTextRenderer().append(DartBundle.message("project.0", ((Project)userObject).getName()));
}
else if (userObject instanceof Module) {
getCheckbox().setEnabled(dartSupportEnabled);
getTextRenderer().setEnabled(dartSupportEnabled);
final Icon moduleIcon = ModuleType.get((Module)userObject).getIcon();
getTextRenderer().setIcon(dartSupportEnabled ? moduleIcon : IconLoader.getDisabledIcon(moduleIcon));
getTextRenderer().append(((Module)userObject).getName());
}
}
};
myModulesCheckboxTreeTable = new CheckboxTreeTable(null, checkboxTreeCellRenderer, new ColumnInfo[]{new TreeColumnInfo("")});
myModulesCheckboxTreeTable.addCheckboxTreeListener(new CheckboxTreeAdapter() {
@Override
public void nodeStateChanged(@NotNull CheckedTreeNode node) {
updateErrorLabel();
}
});
//myModulesCheckboxTreeTable.setRowHeight(myModulesCheckboxTreeTable.getRowHeight() + 2);
myModulesCheckboxTreeTable.getTree().addTreeWillExpandListener(new TreeWillExpandListener() {
public void treeWillExpand(final TreeExpansionEvent event) throws ExpandVetoException {
}
public void treeWillCollapse(final TreeExpansionEvent event) throws ExpandVetoException {
throw new ExpandVetoException(event);
}
});
}
public static void openDartSettings(@NotNull final Project project) {
ShowSettingsUtilImpl.showSettingsDialog(project, DART_SETTINGS_PAGE_ID, "");
}
}