/*
* Copyright 2009 the original author or authors.
*
* 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 org.gradle.gradleplugin.userinterface.swing.generic.tabs;
import org.gradle.initialization.DefaultCommandLineConverter;
import org.gradle.StartParameter;
import org.gradle.api.logging.LogLevel;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.gradleplugin.foundation.GradlePluginLord;
import org.gradle.gradleplugin.foundation.settings.SettingsNode;
import org.gradle.gradleplugin.userinterface.swing.generic.OutputUILord;
import org.gradle.gradleplugin.userinterface.swing.generic.Utility;
import org.gradle.logging.internal.LoggingCommandLineConverter;
import javax.swing.AbstractAction;
import javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.ButtonModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JTextField;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.*;
/**
* This tab contains general settings for the plugin.
*
* @author mhunsicker
*/
public class SetupTab implements GradleTab, GradlePluginLord.SettingsObserver {
private final Logger logger = Logging.getLogger(SetupTab.class);
private static final String STACK_TRACE_LEVEL_CLIENT_PROPERTY = "stack-trace-level-client-property";
private static final String SETUP = "setup";
private static final String STACK_TRACE_LEVEL = "stack-trace-level";
private static final String SHOW_OUTPUT_ON_ERROR = "show-output-on-error";
private static final String LOG_LEVEL = "log-level";
private static final String CURRENT_DIRECTORY = "current-directory";
private static final String CUSTOM_GRADLE_EXECUTOR = "custom-gradle-executor";
private GradlePluginLord gradlePluginLord;
private OutputUILord outputUILord;
private SettingsNode settingsNode;
private JPanel mainPanel;
private JRadioButton showNoStackTraceRadioButton;
private JRadioButton showStackTrackRadioButton;
private JRadioButton showFullStackTrackRadioButton;
private JComboBox logLevelComboBox;
private JCheckBox onlyShowOutputOnErrorCheckBox;
private ButtonGroup stackTraceButtonGroup;
private JTextField currentDirectoryTextField;
private JCheckBox useCustomGradleExecutorCheckBox;
private JTextField customGradleExecutorField;
private JButton browseForCustomGradleExecutorButton;
private JPanel customPanelPlaceHolder;
public SetupTab(GradlePluginLord gradlePluginLord, OutputUILord outputUILord, SettingsNode settingsNode) {
this.gradlePluginLord = gradlePluginLord;
this.outputUILord = outputUILord;
this.settingsNode = settingsNode.addChildIfNotPresent(SETUP);
}
public String getName() {
return "Setup";
}
public Component createComponent() {
setupUI();
return mainPanel;
}
/**
* Notification that this component is about to be shown. Do whatever initialization you choose.
*/
public void aboutToShow() {
updatePluginLordSettings();
gradlePluginLord.addSettingsObserver( this, true );
}
private void setupUI() {
mainPanel = new JPanel();
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
mainPanel.add(createCurrentDirectoryPanel());
mainPanel.add(Box.createVerticalStrut(10));
mainPanel.add(createLogLevelPanel());
mainPanel.add(Box.createVerticalStrut(10));
mainPanel.add(createStackTracePanel());
mainPanel.add(Box.createVerticalStrut(10));
mainPanel.add(createOptionsPanel());
mainPanel.add(Box.createVerticalStrut(10));
mainPanel.add(createCustomExecutorPanel());
mainPanel.add(Box.createVerticalStrut(10));
//add a panel that can be used to add custom things to the setup tab
customPanelPlaceHolder = new JPanel( new BorderLayout() );
mainPanel.add( customPanelPlaceHolder );
//Glue alone doesn't work in this situation. This forces everything to the top.
JPanel expandingPanel = new JPanel(new BorderLayout());
expandingPanel.add(Box.createVerticalGlue(), BorderLayout.CENTER);
mainPanel.add(expandingPanel);
mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
}
private Component createCurrentDirectoryPanel() {
currentDirectoryTextField = new JTextField();
currentDirectoryTextField.setEditable(false);
String currentDirectory = settingsNode.getValueOfChild(CURRENT_DIRECTORY, null);
if (currentDirectory == null || "".equals(currentDirectory.trim())) {
currentDirectory = gradlePluginLord.getCurrentDirectory().getAbsolutePath();
}
currentDirectoryTextField.setText(currentDirectory);
gradlePluginLord.setCurrentDirectory(new File(currentDirectory));
JButton browseButton = new JButton(new AbstractAction("Browse...") {
public void actionPerformed(ActionEvent e) {
File file = browseForDirectory( gradlePluginLord.getCurrentDirectory() );
if (file != null) {
setCurrentDirectory( file );
}
}
});
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panel.add(Utility.addLeftJustifiedComponent(new JLabel("Current Directory")));
panel.add(createSideBySideComponent(currentDirectoryTextField, browseButton));
return panel;
}
private void setCurrentDirectory(File file) {
if( file == null ) {
currentDirectoryTextField.setText("");
settingsNode.setValueOfChild(CURRENT_DIRECTORY, "" );
} else {
currentDirectoryTextField.setText( file.getAbsolutePath() );
settingsNode.setValueOfChild(CURRENT_DIRECTORY, file.getAbsolutePath());
}
if( gradlePluginLord.setCurrentDirectory(file) )
{
//refresh the tasks only if we actually changed the current directory
gradlePluginLord.addRefreshRequestToQueue();
}
}
/**
* this creates a panel where the right component is its preferred size. This is useful for putting on
* a button on the right and a text field on the left.
*/
public static JComponent createSideBySideComponent(Component leftComponent, Component rightComponent) {
JPanel xLayoutPanel = new JPanel();
xLayoutPanel.setLayout(new BoxLayout(xLayoutPanel, BoxLayout.X_AXIS));
Dimension preferredSize = leftComponent.getPreferredSize();
leftComponent.setMaximumSize(new Dimension(Integer.MAX_VALUE, preferredSize.height));
xLayoutPanel.add(leftComponent);
xLayoutPanel.add(Box.createHorizontalStrut(5));
xLayoutPanel.add(rightComponent);
return xLayoutPanel;
}
/**
* Browses for a file using the text value from the text field as the current value.
* @param fileTextField where we get the current value
* @return
*/
private File browseForDirectory(File initialFile ) {
if( initialFile == null ) {
initialFile = new File( System.getProperty("user.dir") );
}
JFileChooser chooser = new JFileChooser(initialFile);
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
chooser.setMultiSelectionEnabled(false);
File file = null;
if (chooser.showOpenDialog(mainPanel) == JFileChooser.APPROVE_OPTION) {
file = chooser.getSelectedFile();
}
return file;
}
/**
* Creates a panel that has a combo box to select a log level
*/
private Component createLogLevelPanel() {
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
logLevelComboBox = new JComboBox(getLogLevelWrappers());
panel.add(Utility.addLeftJustifiedComponent(new JLabel("Log Level")));
panel.add(Utility.addLeftJustifiedComponent(logLevelComboBox));
//initialize our value
String logLevelName = settingsNode.getValueOfChild(LOG_LEVEL, null);
LogLevel logLevel = gradlePluginLord.getLogLevel();
if (logLevelName != null) {
try {
logLevel = LogLevel.valueOf(logLevelName);
}
catch (IllegalArgumentException e) //this may happen if the enum changes. We don't want this to stop the whole UI
{
logger.error("Converting log level text to log level enum '" + logLevelName + "'", e);
}
}
gradlePluginLord.setLogLevel(logLevel);
setLogLevelComboBoxSetting(logLevel);
logLevelComboBox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
LogLevelWrapper wrapper = (LogLevelWrapper) logLevelComboBox.getSelectedItem();
if (wrapper != null) {
gradlePluginLord.setLogLevel(wrapper.logLevel);
settingsNode.setValueOfChild(LOG_LEVEL, wrapper.logLevel.name());
}
}
});
return panel;
}
/**
* This creates an array of wrapper objects suitable for passing to the constructor of the log level combo box.
*/
private Vector<LogLevelWrapper> getLogLevelWrappers() {
Collection<LogLevel> collection = new LoggingCommandLineConverter().getLogLevels();
Vector<LogLevelWrapper> wrappers = new Vector<LogLevelWrapper>();
Iterator<LogLevel> iterator = collection.iterator();
while (iterator.hasNext()) {
LogLevel level = iterator.next();
wrappers.add(new LogLevelWrapper(level));
}
Collections.sort(wrappers, new Comparator<LogLevelWrapper>() {
public int compare(LogLevelWrapper o1, LogLevelWrapper o2) {
return o1.toString().compareToIgnoreCase(o2.toString());
}
});
return wrappers;
}
/**
* This exists solely for overriding toString to something nicer. We'll captilize the first letter. The rest become
* lower case. Ultimately, this should probably move into LogLevel. We'll also put the log level shortcut in parenthesis
*/
private class LogLevelWrapper {
private LogLevel logLevel;
private String toString;
private LogLevelWrapper(LogLevel logLevel) {
this.logLevel = logLevel;
String temp = logLevel.toString().toLowerCase().replace('_', ' '); //replace underscores in the name with spaces
this.toString = Character.toUpperCase(temp.charAt(0)) + temp.substring(1);
//add the command line character to the end (so if an error message says use a log level, you can easily translate)
String commandLineCharacter = new LoggingCommandLineConverter().getLogLevelCommandLine( logLevel );
if( commandLineCharacter != null && !commandLineCharacter.equals( "" ))
{
this.toString += " (-" + commandLineCharacter + ")";
}
}
public String toString() {
return toString;
}
}
/**
* Sets the log level combo box to the specified log level.
*
* @param logLevel the log level in question.
*/
private void setLogLevelComboBoxSetting(LogLevel logLevel) {
DefaultComboBoxModel model = (DefaultComboBoxModel) logLevelComboBox.getModel();
for (int index = 0; index < model.getSize(); index++) {
LogLevelWrapper wrapper = (LogLevelWrapper) model.getElementAt(index);
if (wrapper.logLevel == logLevel) {
logLevelComboBox.setSelectedIndex(index);
return;
}
}
}
/**
* Creates a panel with stack trace level radio buttons that allow you to specify how much info is given when an
* error occurs.
*/
private Component createStackTracePanel() {
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panel.setBorder(BorderFactory.createTitledBorder("Stack Trace Output"));
showNoStackTraceRadioButton = new JRadioButton("Exceptions Only");
showStackTrackRadioButton = new JRadioButton("Standard Stack Trace (-" + DefaultCommandLineConverter.STACKTRACE + ")"); //add the command line character to the end (so if an error message says use a stack trace level, you can easily translate)
showFullStackTrackRadioButton = new JRadioButton("Full Stack Trace (-" + DefaultCommandLineConverter.FULL_STACKTRACE + ")" );
showNoStackTraceRadioButton.putClientProperty(STACK_TRACE_LEVEL_CLIENT_PROPERTY, StartParameter.ShowStacktrace.INTERNAL_EXCEPTIONS);
showStackTrackRadioButton.putClientProperty(STACK_TRACE_LEVEL_CLIENT_PROPERTY, StartParameter.ShowStacktrace.ALWAYS);
showFullStackTrackRadioButton.putClientProperty(STACK_TRACE_LEVEL_CLIENT_PROPERTY, StartParameter.ShowStacktrace.ALWAYS_FULL);
stackTraceButtonGroup = new ButtonGroup();
stackTraceButtonGroup.add(showNoStackTraceRadioButton);
stackTraceButtonGroup.add(showStackTrackRadioButton);
stackTraceButtonGroup.add(showFullStackTrackRadioButton);
showNoStackTraceRadioButton.setSelected(true);
ActionListener radioButtonListener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
updateStackTraceSetting(true);
}
};
showNoStackTraceRadioButton.addActionListener(radioButtonListener);
showStackTrackRadioButton.addActionListener(radioButtonListener);
showFullStackTrackRadioButton.addActionListener(radioButtonListener);
panel.add(Utility.addLeftJustifiedComponent(showNoStackTraceRadioButton));
panel.add(Utility.addLeftJustifiedComponent(showStackTrackRadioButton));
panel.add(Utility.addLeftJustifiedComponent(showFullStackTrackRadioButton));
String stackTraceLevel = settingsNode.getValueOfChild(STACK_TRACE_LEVEL, getSelectedStackTraceLevel().name());
if (stackTraceLevel != null) {
try {
setSelectedStackTraceLevel(StartParameter.ShowStacktrace.valueOf(stackTraceLevel));
updateStackTraceSetting(false); //false because we're serializing this in
}
catch (Exception e) { //this can happen if the stack trace levels change because you're moving between versions.
logger.error("Converting stack trace level text to stack trace level enum '" + stackTraceLevel + "'", e);
}
}
return panel;
}
/**
* This stores the current stack trace setting (based on the UI controls) in the plugin.
*/
private void updateStackTraceSetting(boolean saveSetting) {
StartParameter.ShowStacktrace stackTraceLevel = getSelectedStackTraceLevel();
gradlePluginLord.setStackTraceLevel(stackTraceLevel);
if (saveSetting) {
settingsNode.setValueOfChild(STACK_TRACE_LEVEL, stackTraceLevel.name());
}
}
/**
* Sets the selected strack trace level on the radio buttons. The radio buttons store their stack trace level as a
* client property and I'll look for a match using that. This way, we don't have to edit this if new levels are
* created.
*
* @param newStackTraceLevel the new stack trace level.
*/
private void setSelectedStackTraceLevel(StartParameter.ShowStacktrace newStackTraceLevel) {
Enumeration<AbstractButton> buttonEnumeration = stackTraceButtonGroup.getElements();
while (buttonEnumeration.hasMoreElements()) {
JRadioButton radioButton = (JRadioButton) buttonEnumeration.nextElement();
StartParameter.ShowStacktrace level = (StartParameter.ShowStacktrace) radioButton.getClientProperty(STACK_TRACE_LEVEL_CLIENT_PROPERTY);
if (newStackTraceLevel == level) {
radioButton.setSelected(true);
return;
}
}
}
/**
* Returns the currently selected stack trace level. The radio buttons store their stack trace level as a client
* property so once we get the selected button, we know the level. This way, we don't have to edit this if new
* levels are created. Unfortunately, Swing doesn't have an easy way to get the actual button from the group.
*
* @return the selected stack trace level
*/
private StartParameter.ShowStacktrace getSelectedStackTraceLevel() {
ButtonModel selectedButtonModel = stackTraceButtonGroup.getSelection();
if (selectedButtonModel != null) {
Enumeration<AbstractButton> buttonEnumeration = stackTraceButtonGroup.getElements();
while (buttonEnumeration.hasMoreElements()) {
JRadioButton radioButton = (JRadioButton) buttonEnumeration.nextElement();
if (radioButton.getModel() == selectedButtonModel) {
StartParameter.ShowStacktrace level = (StartParameter.ShowStacktrace) radioButton.getClientProperty(STACK_TRACE_LEVEL_CLIENT_PROPERTY);
return level;
}
}
}
return StartParameter.ShowStacktrace.INTERNAL_EXCEPTIONS;
}
private Component createOptionsPanel() {
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
onlyShowOutputOnErrorCheckBox = new JCheckBox("Only Show Output When Errors Occur");
onlyShowOutputOnErrorCheckBox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
updateShowOutputOnErrorsSetting();
settingsNode.setValueOfChildAsBoolean(SHOW_OUTPUT_ON_ERROR, onlyShowOutputOnErrorCheckBox.isSelected());
}
});
//initialize its default value
boolean valueAsBoolean = settingsNode.getValueOfChildAsBoolean(SHOW_OUTPUT_ON_ERROR, onlyShowOutputOnErrorCheckBox.isSelected());
onlyShowOutputOnErrorCheckBox.setSelected(valueAsBoolean);
updateShowOutputOnErrorsSetting();
panel.add(Utility.addLeftJustifiedComponent(onlyShowOutputOnErrorCheckBox));
return panel;
}
private void updateShowOutputOnErrorsSetting()
{
boolean value = onlyShowOutputOnErrorCheckBox.isSelected();
outputUILord.setOnlyShowOutputOnErrors(value);
}
private Component createCustomExecutorPanel() {
useCustomGradleExecutorCheckBox = new JCheckBox("Use Custom Gradle Executor");
customGradleExecutorField = new JTextField();
customGradleExecutorField.setEditable(false);
browseForCustomGradleExecutorButton = new JButton(new AbstractAction("Browse...") {
public void actionPerformed(ActionEvent e) {
browseForCustomGradleExecutor();
}
});
String customExecutorPath = settingsNode.getValueOfChild(CUSTOM_GRADLE_EXECUTOR, null);
if (customExecutorPath == null) {
setCustomGradleExecutor(null);
}
else {
setCustomGradleExecutor(new File(customExecutorPath));
}
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panel.add(Utility.addLeftJustifiedComponent(useCustomGradleExecutorCheckBox));
JComponent sideBySideComponent = createSideBySideComponent(customGradleExecutorField, browseForCustomGradleExecutorButton);
sideBySideComponent.setBorder(BorderFactory.createEmptyBorder(0, 30, 0, 0)); //indent it
panel.add(sideBySideComponent);
useCustomGradleExecutorCheckBox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (useCustomGradleExecutorCheckBox.isSelected()) { //if they checked it, browse for a custom executor immediately
browseForCustomGradleExecutor();
}
else {
setCustomGradleExecutor(null);
}
}
});
return panel;
}
/**
* Call this to browse for a custom gradle executor.
*/
private void browseForCustomGradleExecutor() {
File startingDirectory = new File(System.getProperty("user.home"));
File currentFile = gradlePluginLord.getCustomGradleExecutor();
if (currentFile != null) {
startingDirectory = currentFile.getAbsoluteFile();
}
else {
if (gradlePluginLord.getCurrentDirectory() != null) {
startingDirectory = gradlePluginLord.getCurrentDirectory();
}
}
JFileChooser chooser = new JFileChooser(startingDirectory);
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
chooser.setMultiSelectionEnabled(false);
File file = null;
if (chooser.showOpenDialog(mainPanel) == JFileChooser.APPROVE_OPTION) {
file = chooser.getSelectedFile();
}
if (file != null) {
setCustomGradleExecutor(file);
}
else { //if they canceled, and they have no custom gradle executor specified, then we must clear things
//This will reset the UI back to 'not using a custom executor'. We can't have them check the
//field and not have a value here.
if (gradlePluginLord.getCustomGradleExecutor() == null) {
setCustomGradleExecutor(null);
}
}
}
/**
* Call this to set a custom gradle executor. We'll enable all fields appropriately and setup the foundation settings. We'll also fire off a refresh.
*
* @param file the file to use as a custom executor. Null not to use one.
*/
private void setCustomGradleExecutor(File file) {
String storagePath;
boolean isUsingCustom = false;
if (file == null) {
isUsingCustom = false;
storagePath = null;
} else {
isUsingCustom = true;
storagePath = file.getAbsolutePath();
}
//set the executor in the foundation
if( gradlePluginLord.setCustomGradleExecutor(file) )
{
//refresh the tasks only if we actually changed the executor
gradlePluginLord.addRefreshRequestToQueue();
}
//set the UI values
useCustomGradleExecutorCheckBox.setSelected(isUsingCustom);
customGradleExecutorField.setText(storagePath);
//enable the UI appropriately.
browseForCustomGradleExecutorButton.setEnabled(isUsingCustom);
customGradleExecutorField.setEnabled(isUsingCustom);
//store the settings
settingsNode.setValueOfChild(CUSTOM_GRADLE_EXECUTOR, storagePath);
}
/**
This adds the specified component to the setup panel. It is added below the last
'default' item. You can only add 1 component here, so if you need to add multiple
things, you'll have to handle adding that to yourself to the one component.
@param component the component to add.
*/
public void setCustomPanel( JComponent component ) {
customPanelPlaceHolder.add( component, BorderLayout.CENTER );
customPanelPlaceHolder.invalidate();
mainPanel.validate();
}
/**
* Notification that some settings have changed for the plugin. Settings such as current directory, gradle home
* directory, etc. This is useful for UIs that need to update their UIs when this is changed by other means.
*/
public void settingsChanged() {
updatePluginLordSettings();
}
/**
* Called upon start up and whenever GradlePluginLord settings are changed. We'll update our values.
* Note: this actually gets called several times in a row for each settings during initialization. Its
* not optimal, but functional and I didn't want to deal with numerous, specific-field notifications.
*/
private void updatePluginLordSettings() {
setCustomGradleExecutor( gradlePluginLord.getCustomGradleExecutor() );
setCurrentDirectory( gradlePluginLord.getCurrentDirectory() );
setSelectedStackTraceLevel(gradlePluginLord.getStackTraceLevel());
setLogLevelComboBoxSetting(gradlePluginLord.getLogLevel());
}
}