mobi.hsz.idea.gitignore.ui.untrackFiles.UntrackFilesDialog.java Source code

Java tutorial

Introduction

Here is the source code for mobi.hsz.idea.gitignore.ui.untrackFiles.UntrackFilesDialog.java

Source

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2017 hsz Jakub Chrzanowski <jakub@hsz.mobi>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package mobi.hsz.idea.gitignore.ui.untrackFiles;

import com.intellij.dvcs.repo.Repository;
import com.intellij.ide.CommonActionsManager;
import com.intellij.ide.DefaultTreeExpander;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.actionSystem.ActionToolbar;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.CheckboxTree;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.components.JBLabel;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashMap;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.tree.TreeModelAdapter;
import com.intellij.util.ui.tree.TreeUtil;
import mobi.hsz.idea.gitignore.IgnoreBundle;
import mobi.hsz.idea.gitignore.util.Utils;
import mobi.hsz.idea.gitignore.util.exec.ExternalExec;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import java.awt.*;
import java.util.ArrayList;
import java.util.Map;

import static mobi.hsz.idea.gitignore.IgnoreManager.RefreshTrackedIgnoredListener.TRACKED_IGNORED_REFRESH;

/**
 * Dialog that lists all untracked but indexed files in a tree view, allows select specific files
 * and perform command to untrack them.
 *
 * @author Jakub Chrzanowski <jakub@hsz.mobi>
 * @since 1.7
 */
public class UntrackFilesDialog extends DialogWrapper {
    /** Current project. */
    @NotNull
    private final Project project;

    /** A list of the tracked but ignored files. */
    @NotNull
    private final HashMap<VirtualFile, Repository> files;

    /** Templates tree root node. */
    @NotNull
    private final FileTreeNode root;

    /** Map of the tree view {@link FileTreeNode} nodes. */
    @NotNull
    private final Map<VirtualFile, FileTreeNode> nodes = ContainerUtil.newHashMap();

    /** Commands editor with syntax highlight. */
    private Editor commands;

    /** {@link Document} related to the {@link Editor} feature. */
    private Document commandsDocument;

    /** Templates tree with checkbox feature. */
    private CheckboxTree tree;

    /** Tree expander responsible for expanding and collapsing tree structure. */
    private DefaultTreeExpander treeExpander;

    /** Listener that checks if files list has been changed and rewrites commands in {@link #commandsDocument}. */
    @NotNull
    private TreeModelListener treeModelListener = new TreeModelAdapter() {
        /**
         * Invoked after a tree has changed.
         *
         * @param event the event object specifying changed nodes
         * @param type  the event type specifying a kind of changes
         */
        @Override
        protected void process(TreeModelEvent event, EventType type) {
            final String text = getCommandsText();

            ApplicationManager.getApplication().runWriteAction(new Runnable() {
                @Override
                public void run() {
                    commandsDocument.setText(text);
                }
            });

        }
    };

    /**
     * Constructor.
     *
     * @param project current project
     * @param files files map to present
     */
    public UntrackFilesDialog(@NotNull Project project, @NotNull HashMap<VirtualFile, Repository> files) {
        super(project, false);
        this.project = project;
        this.files = files;
        this.root = createDirectoryNodes(project.getBaseDir(), null);

        setTitle(IgnoreBundle.message("dialog.untrackFiles.title"));
        setOKButtonText(IgnoreBundle.message("global.ok"));
        setCancelButtonText(IgnoreBundle.message("global.cancel"));
        init();
    }

    /**
     * Builds recursively nested {@link FileTreeNode} nodes structure.
     *
     * @param file       current {@link VirtualFile} instance
     * @param repository {@link Repository} of given file
     * @return leaf
     */
    @NotNull
    private FileTreeNode createDirectoryNodes(@NotNull VirtualFile file, @Nullable Repository repository) {
        final FileTreeNode node = nodes.get(file);
        if (node != null) {
            return node;
        }

        final FileTreeNode newNode = new FileTreeNode(project, file, repository);
        nodes.put(file, newNode);

        if (nodes.size() != 1) {
            final VirtualFile parent = file.getParent();
            if (parent != null) {
                createDirectoryNodes(parent, null).add(newNode);
            }
        }

        return newNode;
    }

    /**
     * Creates center panel of {@link DialogWrapper}.
     *
     * @return panel
     */
    @Nullable
    @Override
    protected JComponent createCenterPanel() {
        final JPanel centerPanel = new JPanel(new BorderLayout());
        centerPanel.setPreferredSize(new Dimension(500, 400));

        final JPanel treePanel = new JPanel(new BorderLayout());
        centerPanel.add(treePanel, BorderLayout.CENTER);

        /* Scroll panel for the templates tree. */
        JScrollPane treeScrollPanel = createTreeScrollPanel();
        treePanel.add(treeScrollPanel, BorderLayout.CENTER);

        final JPanel northPanel = new JPanel(new GridBagLayout());
        northPanel.setBorder(IdeBorderFactory.createEmptyBorder(2, 0, 2, 0));
        northPanel.add(createTreeActionsToolbarPanel(treeScrollPanel).getComponent(),
                new GridBagConstraints(0, 0, 1, 1, 1, 1, GridBagConstraints.BASELINE_LEADING,
                        GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
        treePanel.add(northPanel, BorderLayout.NORTH);

        // Create commands preview section
        commandsDocument = EditorFactory.getInstance().createDocument(getCommandsText());
        commands = Utils.createPreviewEditor(commandsDocument, project, true);

        final JPanel commandsPanel = new JPanel(new BorderLayout());
        final JLabel commandsLabel = new JBLabel(IgnoreBundle.message("dialog.untrackFiles.commands.label"));
        commandsLabel.setBorder(IdeBorderFactory.createEmptyBorder(10, 0, 10, 0));
        commandsPanel.add(commandsLabel, BorderLayout.NORTH);

        JComponent commandsComponent = commands.getComponent();
        commandsComponent.setPreferredSize(new Dimension(0, 200));
        commandsPanel.add(commandsComponent, BorderLayout.CENTER);
        centerPanel.add(commandsPanel, BorderLayout.SOUTH);

        return centerPanel;
    }

    /**
     * Creates scroll panel with templates tree in it.
     *
     * @return scroll panel
     */
    private JScrollPane createTreeScrollPanel() {
        for (Map.Entry<VirtualFile, Repository> entry : files.entrySet()) {
            createDirectoryNodes(entry.getKey(), entry.getValue());
        }

        final FileTreeRenderer renderer = new FileTreeRenderer();

        tree = new CheckboxTree(renderer, root);
        tree.setCellRenderer(renderer);
        tree.setRootVisible(true);
        tree.setShowsRootHandles(false);
        UIUtil.setLineStyleAngled(tree);
        TreeUtil.installActions(tree);

        final JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(tree);
        scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        TreeUtil.expandAll(tree);

        tree.getModel().addTreeModelListener(treeModelListener);
        treeExpander = new DefaultTreeExpander(tree);

        return scrollPane;
    }

    /**
     * Creates tree toolbar panel with actions for working with templates tree.
     *
     * @param target templates tree
     * @return action toolbar
     */
    private ActionToolbar createTreeActionsToolbarPanel(@NotNull JComponent target) {
        final CommonActionsManager actionManager = CommonActionsManager.getInstance();
        final DefaultActionGroup actions = new DefaultActionGroup();
        actions.add(actionManager.createExpandAllAction(treeExpander, tree));
        actions.add(actionManager.createCollapseAllAction(treeExpander, tree));

        final ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN,
                actions, true);
        actionToolbar.setTargetComponent(target);

        return actionToolbar;
    }

    /**
     * This method is invoked by default implementation of "OK" action. It just closes dialog with
     * <code>OK_EXIT_CODE</code>. This is convenient place to override functionality of "OK" action.
     * Note that the method does nothing if "OK" action isn't enabled.
     */
    @Override
    protected void doOKAction() {
        super.doOKAction();

        HashMap<Repository, ArrayList<VirtualFile>> checked = getCheckedFiles();
        for (Map.Entry<Repository, ArrayList<VirtualFile>> entry : checked.entrySet()) {
            for (VirtualFile file : entry.getValue()) {
                ExternalExec.removeFileFromTracking(file, entry.getKey());
            }
        }

        project.getMessageBus().syncPublisher(TRACKED_IGNORED_REFRESH).refresh();
    }

    /**
     * Returns structured map of selected {@link VirtualFile} list sorted by {@link Repository}.
     *
     * @return sorted files map
     */
    @NotNull
    private HashMap<Repository, ArrayList<VirtualFile>> getCheckedFiles() {
        final HashMap<Repository, ArrayList<VirtualFile>> result = new HashMap<Repository, ArrayList<VirtualFile>>();

        FileTreeNode leaf = (FileTreeNode) root.getFirstLeaf();
        if (leaf == null) {
            return result;
        }

        do {
            if (!leaf.isChecked()) {
                continue;
            }

            final Repository repository = leaf.getRepository();
            final VirtualFile file = leaf.getFile();
            if (repository == null) {
                continue;
            }

            ArrayList<VirtualFile> list = ContainerUtil.getOrCreate(result, repository,
                    new ArrayList<VirtualFile>());
            list.add(file);

            result.put(repository, list);
        } while ((leaf = (FileTreeNode) leaf.getNextLeaf()) != null);

        return result;
    }

    /**
     * Returns ready to present commands list.
     *
     * @return commands list
     */
    @NotNull
    private String getCommandsText() {
        final StringBuilder builder = new StringBuilder();

        HashMap<Repository, ArrayList<VirtualFile>> checked = getCheckedFiles();
        for (Map.Entry<Repository, ArrayList<VirtualFile>> entry : checked.entrySet()) {
            VirtualFile repository = entry.getKey().getRoot();
            builder.append(
                    IgnoreBundle.message("dialog.untrackFiles.commands.repository", repository.getCanonicalPath()))
                    .append("\n");

            for (VirtualFile file : entry.getValue()) {
                builder.append(IgnoreBundle.message("dialog.untrackFiles.commands.command",
                        Utils.getRelativePath(repository, file))).append("\n");
            }

            builder.append("\n");
        }
        return builder.toString();
    }
}