Package org.gradle.gradleplugin.userinterface.swing.generic

Source Code of org.gradle.gradleplugin.userinterface.swing.generic.TaskTreeComponent$ProjectTreeNode

/*
* 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;

import org.gradle.foundation.ProjectView;
import org.gradle.foundation.TaskView;
import org.gradle.foundation.visitors.TaskTreePopulationVisitor;
import org.gradle.gradleplugin.foundation.GradlePluginLord;
import org.gradle.gradleplugin.foundation.filters.ProjectAndTaskFilter;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Enumeration;

/**
* This displays a tree of projects, subprojects, and tasks. You implement the Interaction to detemine how to handle
* right clicks and double clicking tasks. To use this, call populate and pass it a filter (allows you to change exactly
* what is displayed). There are several functions to obtaining the selected items, plus you can get the tree directly
* for any advanced functionality.
*
* @author mhunsicker
*/
public class TaskTreeComponent {
    private GradlePluginLord gradlePluginLord;
    private Interaction interaction;

    private JTree tree;
    private DefaultTreeModel model;
    private TaskTreeBaseNode rootNode;

    private boolean isPopulated;

    private TaskTreeComponent.Renderer renderer;

    public interface Interaction {
        void rightClick(JTree tree, int x, int y);

        /**
         * Notification that a project was invoked (double-clicked). Do whatever you like, such as execute its default
         * task.
         *
         * @param project the project that was invoked.
         */
        void projectInvoked(ProjectView project);

        /**
         * Notification that a task was invoked (double-clicked). Do whatever you like, such as execute it.
         *
         * @param task the task that was invoked.
         * @param isCtrlKeyDown true if the CTRL key was pressed at the time
         */
        void taskInvoked(TaskView task, boolean isCtrlKeyDown);
    }

    public TaskTreeComponent(GradlePluginLord gradlePluginLord, Interaction interaction) {
        this.gradlePluginLord = gradlePluginLord;
        this.interaction = interaction;

        createTreePanel();
    }

    private void createTreePanel() {
        rootNode = new TaskTreeBaseNode();

        model = new DefaultTreeModel(rootNode);
        tree = new JTree(model);

        tree.setRootVisible(false);
        tree.setShowsRootHandles(true);

        renderer = new Renderer();
        tree.setCellRenderer(renderer);

        ToolTipManager.sharedInstance().registerComponent(tree);

        tree.setToggleClickCount(
                99)//prevents double clicks from expanding/collapsing the tree. We want to treat them as double-clicks

        tree.addMouseListener(new MyMouseListener());

        //make hitting Enter and CTRL Enter on a node equal executing it (the first node at least)
        tree.registerKeyboardAction(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                executeFirstSelectedNode(false);
            }
        }, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);

        tree.registerKeyboardAction(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                executeFirstSelectedNode(true);
            }
        }, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.CTRL_MASK), JComponent.WHEN_IN_FOCUSED_WINDOW);
    }

    public JTree getTree() {
        return tree;
    }

    public void setTreeCellRenderer(TreeCellRenderer renderer) {
        tree.setCellRenderer(renderer);
    }

    /**
     * This renders our projects and tasks. This removes the icon and optionally shows the description in a different
     * color. Since there's quite a bit of code for handling rendering tree cells, I'm just going to mooch off of the
     * DefaultTreeCellRenderer. I'll just modify it's behavior a little (I probably don't need that or the description
     * since it's not going to draw a selection or highlight).
     */
    private class Renderer implements TreeCellRenderer {
        private JPanel panel;
        private DefaultTreeCellRenderer nameRenderer;
        private DefaultTreeCellRenderer descriptionRenderer;
        private Color descriptionColor;
        private boolean showDescription = true;
        private Component seperator;
        private Font normalFont;
        private Font boldFont;

        private Renderer() {
            setupRendererUI();
            setShowDescription(true);

            descriptionColor = Color.blue;
        }

        private void setupRendererUI() {
            panel = new JPanel();
            panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));

            nameRenderer = new DefaultTreeCellRenderer();
            descriptionRenderer = new DefaultTreeCellRenderer();

            panel.add(nameRenderer);
            seperator = Box.createHorizontalStrut(10);
            panel.add(seperator);
            panel.add(descriptionRenderer);

            panel.setOpaque(false);

            setupFonts();
        }

        /**
         * Setup the fonts. On some platforms, bold is the typical version. We explicitly don't want that. So we'll make
         * the fonts plain and use the bold for our own purposes (indicating default tasks).
         */
        private void setupFonts() {
            normalFont = nameRenderer.getFont().deriveFont(Font.PLAIN);
            boldFont = normalFont.deriveFont(Font.BOLD);

            nameRenderer.setFont(normalFont);
            descriptionRenderer.setFont(normalFont);
        }

        public boolean showDescription() {
            return showDescription;
        }

        //the easiest thing to do is just hide the description and its separator

        public void setShowDescription(boolean showDescription) {
            this.showDescription = showDescription;
            seperator.setVisible(showDescription);
            descriptionRenderer.setVisible(showDescription);
            seperator.invalidate();
            nameRenderer.invalidate();
            descriptionRenderer.invalidate();
            panel.invalidate();

            //have to tell the tree each node changed. This is so it will recalculate its size. Without this, if the description is
            //initially disabled, the tree is populated and expanded, then description is enabled, nothing shows up because the tree
            //caches the node's size for some dumb reason.
            Enumeration enumeration = rootNode.breadthFirstEnumeration();
            while (enumeration.hasMoreElements()) {
                TaskTreeBaseNode treeNode = (TaskTreeBaseNode) enumeration.nextElement();
                model.nodeChanged(treeNode);
            }

            tree.repaint();
        }

        public Component getTreeCellRendererComponent(JTree tree, Object value, boolean isSelected, boolean expanded,
                                                      boolean leaf, int row, boolean hasFocus) {
            TaskTreeBaseNode node = (TaskTreeBaseNode) value;

            String description = node.getDescription();

            //we've already added these components to our panel. We know they're just labels. Calling getTreeCell... just sets their text and colors correctly.
            this.nameRenderer.getTreeCellRendererComponent(tree, node.toString(), isSelected, expanded, leaf, row,
                    hasFocus);
            this.descriptionRenderer.getTreeCellRendererComponent(tree, description, isSelected, expanded, leaf, row,
                    false);

            //set the tooltip. This must be on the component we return not our sub renderers
            panel.setToolTipText(description);

            //just remove the icon entirely
            nameRenderer.setIcon(null);
            this.descriptionRenderer.setIcon(null);

            if (node.isBold()) {
                nameRenderer.setFont(boldFont);
            } else {
                nameRenderer.setFont(normalFont);
            }

            //set the description color. If its selected, make it the name renderer's color
            //so we know the colors won't conflict (they do on Windows XP).
            if (!isSelected) {
                this.descriptionRenderer.setForeground(descriptionColor);
            } else {
                this.descriptionRenderer.setForeground(nameRenderer.getForeground());
            }

            nameRenderer.invalidate();
            descriptionRenderer.invalidate();
            seperator.invalidate();
            panel.invalidate();
            panel.validate();

            return panel;
        }
    }

    private class MyMouseListener extends MouseAdapter {
        public void mousePressed(MouseEvent e) {
            if (e.getButton() == MouseEvent.BUTTON3) {  //This is here just to select a node that we right click on.
                //The tree really needs to handle this for us.
                Point point = e.getPoint();
                int row = tree.getRowForLocation(point.x, point.y);
                if (row != -1) {
                    if (tree.isRowSelected(row)) {
                        return;
                    //if its already selected, just leave it alone. This prevents us from changing selecting when a user right-clicks on one of many selected items.

                    //we need to determine if we move the selection, or just add it to the existing selection.
                    if (isAddToSelectionKey(e)) {
                        tree.addSelectionRow(row);
                    } else {
                        tree.setSelectionRow(row);
                    }
                }
            }
        }

        private boolean isAddToSelectionKey(
                MouseEvent e) {  //this is actually OS-specific, but for now, I'll just use CTRL.
            return (e.getModifiers() & MouseEvent.CTRL_MASK) != 0;
        }

        public void mouseClicked(MouseEvent e) {
            if (e.getClickCount() == 1) {
                if (e.getButton() == MouseEvent.BUTTON3) {
                    Point point = e.getPoint();
                    interaction.rightClick(tree, point.x, point.y);
                }
            } else if (e.getClickCount() == 2) {
                TaskTreeBaseNode node = getNodeAtPoint(e.getPoint());
                if (node != null) {
                    boolean isCtrlKeyDown = (e.getModifiers() & MouseEvent.CTRL_MASK) != 0;
                    node.executeTask(isCtrlKeyDown);
                }
            }
        }
    }

    /**
     * This populates (and repopulates) the tree. This is surprisingly tedious in an effort to make the tree collapse as
     * little as possible.
     */
    public void populate(ProjectAndTaskFilter filter) {
        TaskTreePopulationVisitor.visitProjectAndTasks(gradlePluginLord.getProjects(), new PopulateTreeVisitor(),
                filter, rootNode);

        model.reload();
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {  //this expands the 'root' project
                tree.expandRow(0);
            }
        });

        isPopulated = true;
    }

    public boolean isPopulated() {
        return isPopulated;
    }

    /**
     * This visitor populates the tree as we walk projects and tasks. This jumpts through quite a bit of hoops in an
     * effort to keep the tree from collapsing. It still does, but not completely. In order to keep it from collapsing,
     * you must track some additional information that frankly the tree could and should do for you.
     */
    private class PopulateTreeVisitor implements TaskTreePopulationVisitor.Visitor<TaskTreeBaseNode, TaskTreeNode> {
        /**
         * This is called for each project.
         *
         * @param project the project
         * @param parentProjectObject whatever you handed back from a prior call to visitProject if this is a sub
         * project. Otherwise, it'll be whatever was passed into the visitPojectsAndTasks function.
         * @return an object that will be handed back to you for each of this project's tasks.
         */
        public TaskTreeBaseNode visitProject(ProjectView project, int indexOfProject,
                                             TaskTreeBaseNode parentProjectObject) {
            ProjectTreeNode projectTreeNode = findProjectChild(parentProjectObject, project.getName());
            if (projectTreeNode == null) {
                projectTreeNode = new ProjectTreeNode(project);
            }

            int actualIndex = parentProjectObject.getIndex(projectTreeNode);
            if (actualIndex != indexOfProject) //this will be -1 for a new node
            {
                if (actualIndex != -1) //only try to remove it if its already there. Swing doesn't like this otherwise.
                {
                    model.removeNodeFromParent(projectTreeNode);
                }

                insertChildNode(parentProjectObject, projectTreeNode, indexOfProject);
            }

            return projectTreeNode;
        }

        private ProjectTreeNode findProjectChild(TaskTreeBaseNode parentNode, String projectName) {
            for (int index = 0; index < parentNode.getChildCount(); index++) {
                TreeNode child = parentNode.getChildAt(index);
                if (child instanceof ProjectTreeNode) {
                    if (((ProjectTreeNode) child).getProject().getName().equals(projectName)) {
                        return (ProjectTreeNode) child;
                    }
                }
            }
            return null;
        }

        /**
         * This is called for each task.
         *
         * @param task the task
         * @param indexOfTask index
         * @param tasksProject the project for this task
         */
        public TaskTreeNode visitTask(TaskView task, int indexOfTask, ProjectView tasksProject,
                                      TaskTreeBaseNode parentTreeNode) {
            TaskTreeNode taskTreeNode = findTaskChild((ProjectTreeNode) parentTreeNode, task.getName());

            if (taskTreeNode == null) {
                taskTreeNode = new TaskTreeNode(task);
            }

            int actualIndex = parentTreeNode.getIndex(taskTreeNode);
            if (actualIndex != indexOfTask) //this will be -1 for a new node
            {
                if (actualIndex != -1) //only try to remove it if its already there. Swing doesn't like this otherwise.
                {
                    model.removeNodeFromParent(taskTreeNode);
                }

                insertChildNode(parentTreeNode, taskTreeNode, indexOfTask);
            }

            return taskTreeNode;
        }

        //This only exists so we can call insert or add appropriately.
        //The stupid tree isn't smart enough to do this for you.

        private void insertChildNode(DefaultMutableTreeNode parent, DefaultMutableTreeNode child, int index) {
            if (parent.getChildCount() < index) {
                parent.add(child);
                model.nodesWereInserted(parent, new int[]{parent.getChildCount() - 1});
            } else {
                parent.insert(child, index);
                model.nodesWereInserted(parent, new int[]{index});
            }
        }

        private TaskTreeNode findTaskChild(ProjectTreeNode parentNode, String taskName) {
            for (int index = 0; index < parentNode.getChildCount(); index++) {
                TreeNode child = parentNode.getChildAt(index);
                if (child instanceof TaskTreeNode) {
                    if (((TaskTreeNode) child).getTask().getName().equals(taskName)) {
                        return (TaskTreeNode) child;
                    }
                }
            }
            return null;
        }

        /**
         * This is called when a project has been visited completely and is just a notification giving you an
         * opportunity to do whatever you like.
         *
         * Here, we're going to remove any nodes that aren't in either of the lists. This is when a task or project is
         * hidden or when things simply change.
         *
         * @param parentProjectObject the object that represents the parent of the project and task objects below
         * @param projectObjects a list of whatever you returned from visitProject
         * @param taskObjects a list of whatever you returned from visitTask
         */
        public void completedVisitingProject(TaskTreeBaseNode parentProjectObject,
                                             List<TaskTreeBaseNode> projectObjects, List<TaskTreeNode> taskObjects) {
            int index = 0;
            while (index < parentProjectObject.getChildCount()) {
                TaskTreeBaseNode child = (TaskTreeBaseNode) parentProjectObject.getChildAt(index);
                if (!projectObjects.contains(child) && !taskObjects.contains(child)) {
                    model.removeNodeFromParent(child);
                } else {
                    index++;
                }
            }
        }
    }

    private void expandNode(TreeNode node) {
        tree.expandPath(new TreePath(node));
    }

    /**
     * This is a basic tree node. All nodes in this tree must extend this. This is so we don't have to deal with all the
     * differing types of things that may be in this tree.
     */
    public class TaskTreeBaseNode extends DefaultMutableTreeNode {
        public void executeTask(boolean isCtrlKeyDown) {
        }  //do nothing by default.

        public String toString() {
            return "hidden-root";
        //by default, its the root.

        public String getDescription() {
            return null;
        }

        public boolean isBold() {
            return false;
        }
    }

    /**
     * This represents a project.
     */
    public class ProjectTreeNode extends TaskTreeBaseNode {
        private ProjectView project;

        private ProjectTreeNode(ProjectView project) {
            this.project = project;
        }

        public String toString() {
            return project.getName();
        }

        @Override
        public void executeTask(boolean isCtrlKeyDown) {
            interaction.projectInvoked(project);
        }

        @Override
        public String getDescription() {
            return project.getDescription();
        }

        public ProjectView getProject() {
            return project;
        }
    }

    /**
     * This represents a single task.
     */
    public class TaskTreeNode extends TaskTreeBaseNode {
        private TaskView task;

        private TaskTreeNode(TaskView task) {
            this.task = task;
        }

        public String toString() {
            return task.getName();
        }

        @Override
        public void executeTask(boolean isCtrlKeyDown) {
            interaction.taskInvoked(task, isCtrlKeyDown);
        }

        public TaskView getTask() {
            return task;
        }

        @Override
        public String getDescription() {
            return task.getDescription();
        }

        @Override
        public boolean isBold() {
            return task.isDefault();
        }
    }

    /**
     * Returns the node at the specified point.
     */
    public TaskTreeBaseNode getNodeAtPoint(Point point) {
        int row = tree.getRowForLocation(point.x, point.y);
        if (row == -1) {
            return null;
        }

        TreePath path = tree.getPathForLocation(point.x, point.y);
        if (path == null) {
            return null;
        }
        return (TaskTreeBaseNode) path.getLastPathComponent();
    }

    /**
     * @return the first selected node or null if nothing is selected.
     */
    public TaskTreeBaseNode getFirstSelectedNode() {
        TreePath path = tree.getSelectionPath();
        if (path == null) {
            return null;
        }

        return (TaskTreeBaseNode) path.getLastPathComponent();
    }

    /**
     * @return a list of TaskTabTreeNode based on what is selected in the tree or an empty list if nothing is selected.
     */
    public List<TaskTreeBaseNode> getSelectedNodes() {
        TreePath[] treePaths = tree.getSelectionPaths();
        if (treePaths == null) {
            return Collections.emptyList();
        }

        List<TaskTreeBaseNode> nodes = new ArrayList<TaskTreeBaseNode>();

        for (int index = 0; index < treePaths.length; index++) {
            TreePath treePath = treePaths[index];
            nodes.add((TaskTreeBaseNode) treePath.getLastPathComponent());
        }

        return nodes;
    }

    /**
     * Determines if we have any projects selected. This ignores selected tasks.
     *
     * @return true if we have projects selected, false otherwise.
     */
    public boolean hasProjectsSelected() {
        TreePath[] treePaths = tree.getSelectionPaths();
        if (treePaths == null) {
            return false;
        }

        for (int index = 0; index < treePaths.length; index++) {
            TreePath treePath = treePaths[index];

            Object o = treePath.getLastPathComponent();
            if (o instanceof ProjectTreeNode) {
                return true;
            }
        }

        return false;
    }

    /**
     * Determines if we have any tasks selected. This ignores selected projects.
     *
     * @return true if we have tasks selected, false otherwise.
     */
    public boolean hasTasksSelected() {
        TreePath[] treePaths = tree.getSelectionPaths();
        if (treePaths == null) {
            return false;
        }

        for (int index = 0; index < treePaths.length; index++) {
            TreePath treePath = treePaths[index];

            Object o = treePath.getLastPathComponent();
            if (o instanceof TaskTreeNode) {
                return true;
            }
        }

        return false;
    }

    /**
     * This returns a list of selected tasks. This ignores selected projects.
     *
     * @return the selected tasks. Will return an empty list if no tasks are selected.
     */
    public List<TaskView> getSelectedTasks() {
        TreePath[] treePaths = tree.getSelectionPaths();
        if (treePaths == null) {
            return Collections.emptyList();
        }

        List<TaskView> tasks = new ArrayList<TaskView>();

        for (int index = 0; index < treePaths.length; index++) {
            TreePath treePath = treePaths[index];

            Object o = treePath.getLastPathComponent();
            if (o instanceof TaskTreeNode) {
                tasks.add(((TaskTreeNode) o).task);
            }
        }

        return tasks;
    }

    /**
     * Object to hold onto mutliple selections, but not just multiples of the same type of node. This separates the
     * selected nodes by type. You can have multiple projects and tasks selected.
     */
    public class MultipleSelection {
        public List<ProjectView> projects = new ArrayList<ProjectView>();
        public List<TaskView> tasks = new ArrayList<TaskView>();
    }

    /**
     * This returns the current selection broken up into projects and tasks.
     *
     * @return the selected projects and tasks. This never returns null and the contained lists are never null.
     */
    public MultipleSelection getSelectedProjectsAndTasks() {
        MultipleSelection multipleSelection = new MultipleSelection();

        TreePath[] treePaths = tree.getSelectionPaths();
        if (treePaths == null) {
            return multipleSelection;
        }

        for (int index = 0; index < treePaths.length; index++) {
            TreePath treePath = treePaths[index];

            Object o = treePath.getLastPathComponent();
            if (o instanceof TaskTreeNode) {
                multipleSelection.tasks.add(((TaskTreeNode) o).getTask());
            } else if (o instanceof ProjectTreeNode) {
                multipleSelection.projects.add(((ProjectTreeNode) o).getProject());
            }
        }

        return multipleSelection;
    }

    public boolean showDescription() {
        return renderer.showDescription();
    }

    public void setShowDescription(boolean showDescription) {
        this.renderer.setShowDescription(showDescription);
    }

    private void executeFirstSelectedNode(boolean isCtrlKeyDown) {
        TaskTreeComponent.TaskTreeBaseNode node = getFirstSelectedNode();
        if (node != null) {
            node.executeTask(isCtrlKeyDown);
        }
    }
}
TOP

Related Classes of org.gradle.gradleplugin.userinterface.swing.generic.TaskTreeComponent$ProjectTreeNode

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.