/*
* 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();
}
}