package com.haskforce.settings; import com.haskforce.utils.ExecUtil; import com.haskforce.utils.GuiUtil; import com.haskforce.utils.NotificationUtil; import com.intellij.compiler.options.CompilerConfigurable; import com.intellij.notification.NotificationType; import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.TextFieldWithBrowseButton; import com.intellij.openapi.util.Condition; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.TextAccessor; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import scala.runtime.AbstractFunction1; import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.util.Arrays; /** * The "Haskell Compiler" section in Preferences->Compiler. */ public class HaskellCompilerConfigurable extends CompilerConfigurable { public static final String HASKELL_COMPILER_ID = "Haskell compiler"; // Swing components. private JPanel mainPanel; // GHC Binary components. private TextFieldWithBrowseButton ghcPath; private JLabel ghcVersion; // Cabal binary components. private TextFieldWithBrowseButton cabalPath; private JLabel cabalVersion; // Cabal configure flags private com.intellij.ui.RawCommandLineEditor cabalFlags; // Build configuration components. private JCheckBox profilingBuild; private JCheckBox cabalSandbox; private JCheckBox installCabalDependencies; private JCheckBox enableTests; private JRadioButton buildWithCabal; private TextFieldWithBrowseButton stackPath; private JRadioButton buildWithStack; private JLabel stackVersion; private TextFieldWithBrowseButton stackFile; private com.intellij.ui.RawCommandLineEditor stackFlags; private ButtonGroup buildWith = new ButtonGroup(); // Data container for settings. private final HaskellBuildSettings mySettings; @SuppressWarnings("FieldCanBeLocal") private final Project myProject; public HaskellCompilerConfigurable(@NotNull final Project inProject) { super(inProject); myProject = inProject; mySettings = HaskellBuildSettings.getInstance(myProject); stackPath.setText(mySettings.getStackPath()); GuiUtil.addFolderListener(stackPath, "stack"); stackFile.setText(mySettings.getStackFile()); GuiUtil.addFolderListener(stackFile, "stack.yaml", inProject, new Condition<VirtualFile>() { @Override public boolean value(VirtualFile virtualFile) { String ext = virtualFile.getExtension(); return ext != null && Arrays.asList("yaml", "yml").contains(ext.toLowerCase()); } }); ghcPath.setText(mySettings.getGhcPath()); GuiUtil.addFolderListener(ghcPath, "ghc"); cabalPath.setText(mySettings.getCabalPath()); GuiUtil.addFolderListener(cabalPath, "cabal"); cabalSandbox.setSelected(mySettings.isCabalSandboxEnabled()); installCabalDependencies.setSelected(mySettings.isInstallCabalDependenciesEnabled()); enableTests.setSelected(mySettings.isEnableTestsEnabled()); initializeBuildWithButtons(); updateVersionInfoFields(); } private void initializeBuildWithButtons() { buildWith.add(buildWithStack); buildWith.add(buildWithCabal); boolean stackEnabled = mySettings.isStackEnabled(); buildWithStack.setSelected(stackEnabled); setEnabledStackFields(stackEnabled); // Cabal and Stack can't be enabled simultaneously, prefer Stack. boolean cabalEnabled = !stackEnabled && mySettings.isCabalEnabled(); buildWithCabal.setSelected(cabalEnabled); setEnabledCabalFields(cabalEnabled); buildWithStack.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { setEnabledStackFields(true); } }); buildWithCabal.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { setEnabledCabalFields(true); } }); } /** * The setEnabledStack/CabalFields methods will toggle the other fields * as enabled so that the Stack and Cabal fields won't be enabled simultaneously. */ private void setEnabledStackFields(boolean enabled) { setEnabledStackFields(enabled, true); } private void setEnabledStackFields(boolean enabled, boolean toggle) { stackPath.setEnabled(enabled); stackFlags.setEnabled(enabled); stackFile.setEnabled(enabled); if (toggle) setEnabledCabalFields(!enabled, false); } private void setEnabledCabalFields(boolean enabled) { setEnabledCabalFields(enabled, true); } private void setEnabledCabalFields(boolean enabled, boolean toggle) { ghcPath.setEnabled(enabled); cabalPath.setEnabled(enabled); cabalFlags.setEnabled(enabled); profilingBuild.setEnabled(enabled); installCabalDependencies.setEnabled(enabled); cabalSandbox.setEnabled(enabled); enableTests.setEnabled(enabled); if (toggle) setEnabledStackFields(!enabled, false); } @NotNull @Override public String getId() { return HASKELL_COMPILER_ID; } @Nullable @Override public Runnable enableSearch(String s) { return null; } @Nls @Override public String getDisplayName() { return HASKELL_COMPILER_ID; } @Nullable @Override public String getHelpTopic() { return null; } /** * Constructs the compiler panel in Settings->Compiler. Also responsible * for filling in previous values or constructing sane default values. */ @Nullable @Override public JComponent createComponent() { return mainPanel; } /** * Enables the apply button if anything changed. */ @Override public boolean isModified() { return !(ghcCabalStackUnchanged() && cabalFlags.getText().equals(mySettings.getCabalFlags()) && profilingBuild.isSelected() == mySettings.isProfilingEnabled() && buildWithCabal.isSelected() == mySettings.isCabalEnabled() && cabalSandbox.isSelected() == mySettings.isCabalSandboxEnabled() && installCabalDependencies.isSelected() == mySettings.isInstallCabalDependenciesEnabled() && enableTests.isSelected() == mySettings.isEnableTestsEnabled() && buildWithStack.isSelected() == mySettings.isStackEnabled() && stackFlags.getText().equals(mySettings.getStackFlags())); } /** * Returns true if the ghc and cabal paths are unchanged. */ private boolean ghcCabalStackUnchanged() { return ghcPath.getText().equals(mySettings.getGhcPath()) && cabalPath.getText().equals(mySettings.getCabalPath()) && stackPath.getText().equals(mySettings.getStackPath()) && stackFile.getText().equals(mySettings.getStackFile()); } /** * Triggered when the user pushes the apply button. */ @Override public void apply() throws ConfigurationException { validate(); saveState(); updateVersionInfoFields(); } /** * Triggered when the user pushes the cancel button. */ @Override public void reset() { restoreState(); } @Override public void disposeUIResources() { } /** * Persistent save of the current state. */ private void saveState() { // Save to disk and to communicate with build server. mySettings.setProfilingBuild(profilingBuild.isSelected()); mySettings.setUseCabal(buildWithCabal.isSelected() && !buildWithStack.isSelected()); mySettings.setUseCabalSandbox(cabalSandbox.isSelected()); mySettings.setInstallCabalDependencies(installCabalDependencies.isSelected()); mySettings.setEnableTests(enableTests.isSelected()); mySettings.setGhcPath(ghcPath.getText()); mySettings.setCabalPath(cabalPath.getText()); mySettings.setCabalFlags(cabalFlags.getText()); mySettings.setUseStack(buildWithStack.isSelected()); mySettings.setStackPath(stackPath.getText()); mySettings.setStackFlags(stackFlags.getText()); mySettings.setStackFile(stackFile.getText()); } private void validate() throws ConfigurationException { if (buildWithCabal.isSelected()) { validateExecutable("cabal", cabalPath); validateExecutable("ghc", ghcPath); } if (buildWithStack.isSelected()) { validateExecutable("stack", stackPath); validateFileExists("stack.yaml", stackFile); } } private void validateExecutable(String name, TextAccessor field) throws ConfigurationException { if (new File(field.getText()).canExecute() || new File(myProject.getBasePath(), field.getText()).exists()) return; throw new ConfigurationException("Not a valid '" + name + "' executable: '" + field.getText() + "'"); } private void validateFileExists(String name, TextAccessor field) throws ConfigurationException { if (new File(field.getText()).exists() || new File(myProject.getBasePath(), field.getText()).exists()) return; throw new ConfigurationException("'" + name + "' file does not exist: '" + field.getText() + "'"); } /** * Updates the version info fields for all files configured. */ private void updateVersionInfoFields() { updateVersionInfoField("ghc", ghcPath.getText(), "--numeric-version", ghcVersion); updateVersionInfoField("cabal", cabalPath.getText(), "--numeric-version", cabalVersion); updateVersionInfoField("stack", stackPath.getText(), "--numeric-version", stackVersion); } private void updateVersionInfoField(final String name, String exePath, String versionFlag, final JLabel versionField) { if (exePath.isEmpty()) { versionField.setText(""); return; } ExecUtil.readCommandLine(null, exePath, versionFlag).fold( new AbstractFunction1<ExecUtil.ExecError, Void>() { @Override public Void apply(ExecUtil.ExecError e) { NotificationUtil.displaySimpleNotification( NotificationType.ERROR, myProject, name, e.getMessage() ); return null; } }, new AbstractFunction1<String, Void>() { @Override public Void apply(String version) { versionField.setText(version); return null; } } ); } /** * Restore components to the initial state. */ private void restoreState() { ghcPath.setText(mySettings.getGhcPath()); cabalPath.setText(mySettings.getCabalPath()); cabalFlags.setText(mySettings.getCabalFlags()); profilingBuild.setSelected(mySettings.isProfilingEnabled()); buildWithCabal.setSelected(mySettings.isCabalEnabled()); cabalSandbox.setSelected(mySettings.isCabalSandboxEnabled()); installCabalDependencies.setSelected(mySettings.isInstallCabalDependenciesEnabled()); enableTests.setSelected(mySettings.isEnableTestsEnabled()); buildWithStack.setSelected(mySettings.isStackEnabled()); stackPath.setText(mySettings.getStackPath()); stackFlags.setText(mySettings.getStackFlags()); stackFile.setText(mySettings.getStackFile()); } }