/*
* Copyright 2010 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.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.foundation.CommandLineAssistant;
import org.gradle.foundation.ProjectView;
import org.gradle.foundation.TaskView;
import org.gradle.gradleplugin.foundation.GradlePluginLord;
import org.gradle.gradleplugin.foundation.request.ExecutionRequest;
import org.gradle.gradleplugin.foundation.request.RefreshTaskListRequest;
import org.gradle.gradleplugin.foundation.request.Request;
import org.gradle.gradleplugin.foundation.filters.AllowAllProjectAndTaskFilter;
import org.gradle.gradleplugin.foundation.filters.BasicFilterEditor;
import org.gradle.gradleplugin.foundation.filters.BasicProjectAndTaskFilter;
import org.gradle.gradleplugin.foundation.settings.SettingsNode;
import org.gradle.gradleplugin.userinterface.AlternateUIInteraction;
import org.gradle.gradleplugin.userinterface.swing.generic.SwingAddMultipleFavoritesInteraction;
import org.gradle.gradleplugin.userinterface.swing.generic.TaskTreeComponent;
import org.gradle.gradleplugin.userinterface.swing.generic.Utility;
import org.gradle.gradleplugin.userinterface.swing.generic.filter.ProjectAndTaskFilterDialog;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JToggleButton;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.Iterator;
import java.util.List;
/**
* This displays a tree of projects and tasks.
*
* @author mhunsicker
*/
public class TaskTreeTab implements GradleTab, GradlePluginLord.GeneralPluginObserver, GradlePluginLord.RequestObserver {
private final Logger logger = Logging.getLogger(TaskTreeTab.class);
private static final String SHOW_DESCRIPTION = "show-description";
private static final String BLANK_PNG = "blank.png"; //a blank image used as a spacer on the context menu.
private static final String EXECUTE_PNG = "execute.png";
private JPanel mainPanel;
private GradlePluginLord gradlePluginLord;
private AlternateUIInteraction alternateUIInteraction;
private TaskTreeComponent treeComponent;
private JPopupMenu popupMenu;
private JMenuItem addToFavoritesMenuItem;
private JMenuItem executeMenuItem;
private JMenuItem executeOnlyThisMenuItem;
private JMenuItem filterOutMenuItem;
private JMenuItem editFileMenuItem;
private JMenuItem copyTaskNameMenuItem;
private JButton refreshButton;
private JButton executeButton;
private JToggleButton toggleFilterButton;
private JButton editFilterButton;
private JCheckBox showDescriptionCheckBox;
private BasicFilterEditor editor;
private boolean isRefreshing;
private Color defaultTreeBackground;
private Color workingBackgroundColor = UIManager.getDefaults().getColor("Panel.background"); //just something to provide better feedback that we're working.
private JScrollPane treeScrollPane;
private SettingsNode settingsNode;
public TaskTreeTab(GradlePluginLord gradlePluginLord, SettingsNode settingsNode, AlternateUIInteraction alternateUIInteraction) {
this.gradlePluginLord = gradlePluginLord;
this.settingsNode = settingsNode;
this.alternateUIInteraction = alternateUIInteraction;
gradlePluginLord.addGeneralPluginObserver( this, true );
gradlePluginLord.addRequestObserver( this, true );
initializeFilterEditor();
}
/**
* This initializes our filter editor. We create a filter, serialize in our settings and then use that to create the
* editor. Lastly, we add an observer to the editor so we can save our changes immediately (useful for IDE
* integration where we don't control the settings).
*/
private void initializeFilterEditor() {
BasicProjectAndTaskFilter filter = new BasicProjectAndTaskFilter();
filter.serializeIn(settingsNode);
editor = new BasicFilterEditor(filter);
editor.addFilterEditorObserver(new BasicFilterEditor.FilterEditorObserver() {
public void filterChanged() { //whenever changes are made, save them.
editor.createFilter().serializeOut(settingsNode);
}
}, false);
}
public String getName() {
return "Task Tree";
}
public Component createComponent() {
setupUI();
enableThingsAppropriately();
return mainPanel;
}
/**
* Notification that this component is about to be shown. Do whatever initialization you choose.
*/
public void aboutToShow() {
resetShowDescription(); //make sure that our setting is pushed to the tree's setting.
//when we start up, refresh our list.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
if (gradlePluginLord.isSetupComplete()) {
refresh();
}
else {
showTextInViewport("Cannot show tasks until configuration is complete. See Setup tab.");
}
}
});
}
public void setupUI() {
mainPanel = new JPanel(new BorderLayout());
mainPanel.add(createTopPanel(), BorderLayout.NORTH);
mainPanel.add(createTreePanel(), BorderLayout.CENTER);
setupPopupMenu();
}
private Component createTopPanel() {
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));
refreshButton = Utility.createButton(getClass(), "refresh.png", "Refreshes the task tree", new AbstractAction("Refresh") {
public void actionPerformed(ActionEvent e) {
refresh();
}
});
executeButton = Utility.createButton(getClass(), EXECUTE_PNG, "Execute the selected tasks", new AbstractAction("Execute") {
public void actionPerformed(ActionEvent e) {
executeSelectedTasks();
}
});
toggleFilterButton = Utility.createToggleButton( getClass(), "filter.png", "Toggles the view to show either everything or only the filtered items", new AbstractAction("Filter") {
public void actionPerformed(ActionEvent e) {
populate();
}
});
toggleFilterButton.setSelected(true);
editFilterButton = Utility.createButton(getClass(), "edit-filter.png", "Edits the filter to control what is visible", new AbstractAction("Edit Filter...") {
public void actionPerformed(ActionEvent e) {
configureFilter();
}
});
showDescriptionCheckBox = new JCheckBox("Description", true);
showDescriptionCheckBox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
resetShowDescription();
}
});
showDescriptionCheckBox.setSelected(settingsNode.getValueOfChildAsBoolean(SHOW_DESCRIPTION, showDescriptionCheckBox.isSelected()));
panel.add(refreshButton);
panel.add(Box.createHorizontalStrut(10));
panel.add(executeButton);
panel.add(Box.createHorizontalStrut(10));
panel.add(toggleFilterButton);
panel.add(Box.createHorizontalStrut(10));
panel.add(showDescriptionCheckBox);
panel.add(Box.createHorizontalGlue());
panel.add(editFilterButton);
panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
return panel;
}
private Component createTreePanel() {
treeComponent = new TaskTreeComponent(gradlePluginLord, new TaskTreeComponent.Interaction() {
public void rightClick(JTree tree, int x, int y) {
enableThingsAppropriately();
popupMenu.show(tree, x, y);
}
public void taskInvoked(TaskView task, boolean isCtrlKeyDown) {
if (isCtrlKeyDown) {
gradlePluginLord.addExecutionRequestToQueue(task, false, "-a");
}
else {
gradlePluginLord.addExecutionRequestToQueue(task, false);
}
}
public void projectInvoked(ProjectView project) {
executeDefaultTasksInProject(project);
}
});
treeComponent.getTree().addTreeSelectionListener(new TreeSelectionListener() {
public void valueChanged(TreeSelectionEvent e) {
enableThingsAppropriately();
}
});
defaultTreeBackground = treeComponent.getTree().getBackground();
treeScrollPane = new JScrollPane();
treeComponent.getTree().setBackground(workingBackgroundColor); //change the color to better indicate that
showTextInViewport("Has not built projects/tasks yet.");
return treeScrollPane;
}
/**
* Replaces the tree with a label of text. This is used when there's nothing in the tree, but perhaps a 'working' or
* error message.
*
* @param text the text to display
*/
private void showTextInViewport(String text) {
treeScrollPane.getViewport().removeAll();
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));
panel.add(Box.createHorizontalGlue());
panel.add(new JLabel(text));
panel.add(Box.createHorizontalGlue());
treeScrollPane.getViewport().add(panel);
treeScrollPane.revalidate();
}
/**
* Puts the tree in the main view. This is used once we've gathered the projects and tasks and want to display them
* in the tree.
*/
private void showTreeInViewport() {
treeScrollPane.getViewport().removeAll();
treeScrollPane.getViewport().add(treeComponent.getTree());
treeScrollPane.revalidate();
}
public void executionRequestAdded( ExecutionRequest request )
{
//we don't really care
}
public void refreshRequestAdded( RefreshTaskListRequest request )
{
//when someone adds a refresh request, update the UI to reflect this.
isRefreshing = true;
enableThingsAppropriately();
treeComponent.getTree().setBackground(workingBackgroundColor);
showTextInViewport("Refreshing projects and tasks.");
}
/**
* Notification that a command is about to be executed. This is mostly useful for IDE's that may need to save their files.
*
* @param request the request that's about to be executed.
* @author mhunsicker
*/
public void aboutToExecuteRequest( Request request )
{
//we don't really care
}
/**
* Notification that the command has completed execution.
*
* @param request the original request containing the command that was executed
* @param result the result of the command
* @param output the output from gradle executing the command
*/
public void requestExecutionComplete( Request request, int result, String output )
{
if( request instanceof RefreshTaskListRequest )
{
isRefreshing = false;
enableThingsAppropriately();
if( result != 0 ) { //if something went wrong, let the user know
showTextInViewport("Error");
}
}
}
/**
* Call this to repopulate the tree. Useful if new tasks have been created.
*/
private void refresh() {
gradlePluginLord.addRefreshRequestToQueue();
}
/**
* This populates (and repopulates) the tree.
*/
private void populate() {
if (toggleFilterButton.isSelected()) {
treeComponent.populate(editor.createFilter());
}
else {
treeComponent.populate(new AllowAllProjectAndTaskFilter());
}
//reset the background to indicate that we're populated
treeComponent.getTree().setBackground(defaultTreeBackground);
showTreeInViewport();
}
private void executeSelectedTasks(String... additionCommandLineOptions) {
List<TaskView> taskViews = treeComponent.getSelectedTasks();
String singleCommandLine = CommandLineAssistant.combineTasks( taskViews, additionCommandLineOptions );
if( singleCommandLine == null ) {
return;
}
gradlePluginLord.addExecutionRequestToQueue( singleCommandLine, singleCommandLine, false );
}
/**
* Notification that we're about to reload the projects and tasks.
*/
public void startingProjectsAndTasksReload() {
treeComponent.getTree().setBackground(workingBackgroundColor);
showTextInViewport("Building projects/tasks.");
}
/**
* Notification that the projects and tasks have been reloaded. You may want to repopulate or update your views.
*
* @param wasSuccessful true if they were successfully reloaded. False if an error occurred so we no longer can show
* the projects and tasks (probably an error in a .gradle file).
*/
public void projectsAndTasksReloaded(boolean wasSuccessful) {
isRefreshing = false;
enableThingsAppropriately();
if (!wasSuccessful) {
showTextInViewport("Error");
}
else {
populate();
}
}
/**
* Builds the popup menu
*/
private void setupPopupMenu() {
popupMenu = new JPopupMenu();
executeMenuItem = Utility.createMenuItem( this.getClass(), "Execute", EXECUTE_PNG, new AbstractAction() {
public void actionPerformed(ActionEvent e) {
executeSelectedTasks();
}
});
popupMenu.add(executeMenuItem);
executeOnlyThisMenuItem = Utility.createMenuItem( this.getClass(), "Execute Ignoring Dependencies (-a)", BLANK_PNG, new AbstractAction() {
public void actionPerformed(ActionEvent e) {
executeSelectedTasks("-a");
}
});
popupMenu.add(executeOnlyThisMenuItem);
popupMenu.addSeparator();
addToFavoritesMenuItem = Utility.createMenuItem( this.getClass(), "Add To Favorites", BLANK_PNG, new AbstractAction() {
public void actionPerformed(ActionEvent e) {
addSelectedToFavorites();
}
});
popupMenu.add(addToFavoritesMenuItem);
filterOutMenuItem = Utility.createMenuItem( this.getClass(), "Hide", BLANK_PNG, new AbstractAction() {
public void actionPerformed(ActionEvent e) {
hideSelection();
}
});
popupMenu.add(filterOutMenuItem);
editFileMenuItem = Utility.createMenuItem( this.getClass(), "Edit File", BLANK_PNG, new AbstractAction() {
public void actionPerformed(ActionEvent e) {
editSelectedFiles();
}
});
popupMenu.add(editFileMenuItem);
copyTaskNameMenuItem = Utility.createMenuItem( this.getClass(), "Copy Task Name", BLANK_PNG, new AbstractAction() {
public void actionPerformed(ActionEvent e) {
copySelectedTaskNames();
}
});
popupMenu.addSeparator();
popupMenu.add(copyTaskNameMenuItem);
}
/**
* Enables buttons and menu items based on what is selected.
*/
private void enableThingsAppropriately() {
boolean hasSelection = treeComponent.getTree().getSelectionPath() != null;
boolean hasTaskSelection = treeComponent.hasTasksSelected();
boolean canDoThings = !isRefreshing && treeComponent.isPopulated() && hasSelection; //can't be refreshing, is populated, and hasSelections
refreshButton.setEnabled(!isRefreshing);
addToFavoritesMenuItem.setEnabled(canDoThings);
executeMenuItem.setEnabled(canDoThings);
executeOnlyThisMenuItem.setEnabled(canDoThings);
executeButton.setEnabled(canDoThings);
if (alternateUIInteraction.doesSupportEditingOpeningFiles()) //I'll allow this to be dynamic. If we start supporting editing while running (say a user configured a setting to use a specific external tool), then we'll allow it.
{
editFileMenuItem.setVisible(true);
boolean hasProjectsSelected = treeComponent.hasProjectsSelected();
editFileMenuItem.setEnabled(hasProjectsSelected && canDoThings);
} else {
editFileMenuItem.setVisible(false); //just hide it if we don't support this
}
copyTaskNameMenuItem.setVisible( !isRefreshing && hasTaskSelection );
}
/**
* Adds whatever is selected to the favorites.
*/
private void addSelectedToFavorites() {
List<TaskView> tasks = treeComponent.getSelectedTasks();
gradlePluginLord.getFavoritesEditor().addMutlipleFavorites( tasks, false, new SwingAddMultipleFavoritesInteraction( SwingUtilities.getWindowAncestor(mainPanel) ) );
}
/**
* This displays a dialog that allows the user to determine what shows up in the tree. We give the filter dialog a
* filter rather than handing it out editor so teh user can cancel. That is, the dialog uses its own editor which it
* modifies freely and throws away. This way, if the user cancels, we dodon't have to deal with restoring the
* previous values in our local editor.
*/
private void configureFilter() {
ProjectAndTaskFilterDialog dialog = new ProjectAndTaskFilterDialog(SwingUtilities.getWindowAncestor(mainPanel), gradlePluginLord);
BasicProjectAndTaskFilter newFilter = dialog.show(editor.createFilter());
if (newFilter != null) //if the user didn't cancel...
{
editor.initializeFromFilter(newFilter);
populate();
}
}
/**
* Call this to filter out the currently selected items.
*/
private void hideSelection() {
TaskTreeComponent.MultipleSelection multipleSelection = treeComponent.getSelectedProjectsAndTasks();
if (!multipleSelection.projects.isEmpty() || !multipleSelection.tasks.isEmpty()) {
editor.hideProjects(multipleSelection.projects);
editor.hideTasks(multipleSelection.tasks);
populate(); //unfortunately, we have to repopulate now.
}
}
/**
* This resets whether the description is shown or not based on the check box. The tree component does the real
* work.
*/
private void resetShowDescription() {
settingsNode.setValueOfChildAsBoolean(SHOW_DESCRIPTION, showDescriptionCheckBox.isSelected()); //save it immediately
treeComponent.setShowDescription(showDescriptionCheckBox.isSelected());
}
/**
* This opens the selected files. This gets the 'parent' of this to do it for us. This facilitates using this inside
* an IDE (you get the IDE to open it).
*/
private void editSelectedFiles() {
TaskTreeComponent.MultipleSelection tasks = treeComponent.getSelectedProjectsAndTasks();
Iterator<ProjectView> iterator = tasks.projects.iterator();
while (iterator.hasNext()) {
ProjectView projectView = iterator.next();
File file = projectView.getBuildFile();
if( file != null ) {
alternateUIInteraction.editFile(file, -1 );
}
}
}
/**
* This executes all default tasks in the specified project.
*
* @param project the project to execute.
*/
private void executeDefaultTasksInProject(ProjectView project) {
Iterator<TaskView> iterator = project.getDefaultTasks().iterator();
while (iterator.hasNext()) {
TaskView task = iterator.next();
gradlePluginLord.addExecutionRequestToQueue(task, false);
}
}
/**
* Copies the selected tasks names to the clipboard
*/
private void copySelectedTaskNames() {
String names = getSelectedTaskNames();
if (names.length() == 0) {
return;
}
Toolkit.getDefaultToolkit().getSystemClipboard().setContents( new StringSelection( names ), null );
}
/**
* This puts all the selected task names in a space-delimited String
* @return a string of all the tasks
*/
private String getSelectedTaskNames() {
List<TaskView> tasks = treeComponent.getSelectedTasks();
if( tasks.isEmpty() ) {
return null;
}
StringBuilder taskString = new StringBuilder();
Iterator<TaskView> iterator = tasks.iterator();
while( iterator.hasNext() )
{
TaskView taskView = iterator.next();
taskString.append( taskView.getFullTaskName() );
if( iterator.hasNext() ) {
taskString.append( ' ' );
}
}
return taskString.toString();
}
}