/*
* 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 liveplugin.toolwindow;
import com.intellij.ide.DefaultTreeExpander;
import com.intellij.ide.DeleteProvider;
import com.intellij.ide.actions.CollapseAllAction;
import com.intellij.ide.actions.ExpandAllAction;
import com.intellij.ide.ui.customization.CustomizationUtil;
import com.intellij.ide.util.treeView.AbstractTreeBuilder;
import com.intellij.ide.util.treeView.AbstractTreeStructure;
import com.intellij.ide.util.treeView.NodeDescriptor;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.fileChooser.FileSystemTree;
import com.intellij.openapi.fileChooser.actions.VirtualFileDeleteProvider;
import com.intellij.openapi.fileChooser.ex.FileChooserKeys;
import com.intellij.openapi.fileChooser.ex.FileNodeDescriptor;
import com.intellij.openapi.fileChooser.ex.FileSystemTreeImpl;
import com.intellij.openapi.fileChooser.ex.RootFileElement;
import com.intellij.openapi.fileChooser.impl.FileTreeBuilder;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.project.ProjectManagerListener;
import com.intellij.openapi.ui.SimpleToolWindowPanel;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowAnchor;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.pom.Navigatable;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentFactory;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.util.EditSourceOnDoubleClickHandler;
import com.intellij.util.EditSourceOnEnterKeyHandler;
import com.intellij.util.ui.tree.TreeUtil;
import liveplugin.IDEUtil;
import liveplugin.Icons;
import liveplugin.LivePluginAppComponent;
import liveplugin.pluginrunner.RunPluginAction;
import liveplugin.pluginrunner.TestPluginAction;
import liveplugin.toolwindow.addplugin.AddExamplePluginAction;
import liveplugin.toolwindow.addplugin.AddNewPluginAction;
import liveplugin.toolwindow.addplugin.AddPluginFromPathAction;
import liveplugin.toolwindow.settingsmenu.AddIDEAJarsAsDependencies;
import liveplugin.toolwindow.settingsmenu.AddPluginJarAsDependency;
import liveplugin.toolwindow.settingsmenu.RunAllPluginsOnIDEStartAction;
import liveplugin.toolwindow.settingsmenu.languages.*;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.tree.DefaultTreeModel;
import java.awt.*;
import java.io.File;
import java.util.*;
import java.util.List;
import static com.intellij.openapi.util.Condition.NOT_NULL;
import static com.intellij.util.containers.ContainerUtil.filter;
import static com.intellij.util.containers.ContainerUtil.map;
import static java.util.Arrays.asList;
import static liveplugin.LivePluginAppComponent.PLUGIN_EXAMPLES_PATH;
public class PluginToolWindowManager {
private static final String PLUGINS_TOOL_WINDOW_ID = "Plugins";
private static final Map<Project, PluginToolWindow> toolWindowsByProject = new HashMap<>();
public static AnAction addFromGistAction; // TODO refactor this!!!
public static AnAction addFromGitHubAction;
@Nullable public static PluginToolWindow getToolWindowFor(@Nullable Project project) {
if (project == null) return null;
return toolWindowsByProject.get(project);
}
static void reloadPluginTreesInAllProjects() {
for (Map.Entry<Project, PluginToolWindow> entry : toolWindowsByProject.entrySet()) {
Project project = entry.getKey();
PluginToolWindow toolWindow = entry.getValue();
toolWindow.reloadPluginRoots(project);
}
}
private static void putToolWindow(PluginToolWindow pluginToolWindow, Project project) {
toolWindowsByProject.put(project, pluginToolWindow);
}
private static PluginToolWindow removeToolWindow(Project project) {
return toolWindowsByProject.remove(project);
}
public static void addRoots(FileChooserDescriptor descriptor, List<VirtualFile> virtualFiles) {
virtualFiles = new ArrayList<>(filter(virtualFiles, NOT_NULL));
// Adding file parent is a hack to suppress size == 1 checks in com.intellij.openapi.fileChooser.ex.RootFileElement.
// Otherwise, if there is only one plugin, tree will show files in plugin directory instead of plugin folder.
// (Note that this code is also used by "Copy from Path" action.)
if (virtualFiles.size() == 1) {
VirtualFile parent = virtualFiles.get(0).getParent();
if (parent != null) {
descriptor.setRoots(parent);
} else {
descriptor.setRoots(virtualFiles);
}
} else {
descriptor.setRoots(virtualFiles);
}
}
public void init() {
ProjectManager.getInstance().addProjectManagerListener(new ProjectManagerListener() {
@Override public void projectOpened(Project project) {
PluginToolWindow pluginToolWindow = new PluginToolWindow();
pluginToolWindow.registerWindowFor(project);
putToolWindow(pluginToolWindow, project);
}
@Override public void projectClosed(Project project) {
PluginToolWindow pluginToolWindow = removeToolWindow(project);
if (pluginToolWindow != null) pluginToolWindow.unregisterWindowFrom(project);
}
@Override public boolean canCloseProject(Project project) {
return true;
}
@Override public void projectClosing(Project project) {
}
});
}
public static class PluginToolWindow {
private Ref<FileSystemTree> myFsTreeRef = new Ref<>();
private SimpleToolWindowPanel panel;
private ToolWindow toolWindow;
private static void installPopupMenuInto(FileSystemTree fsTree) {
AnAction action = new NewElementPopupAction();
action.registerCustomShortcutSet(new CustomShortcutSet(shortcutsOf("NewElement")), fsTree.getTree());
CustomizationUtil.installPopupHandler(fsTree.getTree(), "LivePlugin.Popup", ActionPlaces.UNKNOWN);
}
private static Shortcut[] shortcutsOf(String actionId) {
return KeymapManager.getInstance().getActiveKeymap().getShortcuts(actionId);
}
private static FileChooserDescriptor createFileChooserDescriptor() {
FileChooserDescriptor descriptor = new FileChooserDescriptor(true, true, true, false, true, true) {
@Override public Icon getIcon(VirtualFile file) {
for (String path : LivePluginAppComponent.pluginIdToPathMap().values()) {
if (file.getPath().toLowerCase().equals(path.toLowerCase())) return Icons.PLUGIN_ICON;
}
return super.getIcon(file);
}
@Override public String getName(VirtualFile virtualFile) {
return virtualFile.getName();
}
@NotNull @Override public String getComment(VirtualFile virtualFile) {
return "";
}
};
descriptor.withShowFileSystemRoots(false);
descriptor.withTreeRootVisible(false);
Collection<String> pluginPaths = LivePluginAppComponent.pluginIdToPathMap().values();
List<VirtualFile> virtualFiles = map(pluginPaths, path -> VirtualFileManager.getInstance().findFileByUrl("file://" + path));
addRoots(descriptor, virtualFiles);
return descriptor;
}
static Collection<VirtualFile> findPluginRootsFor(VirtualFile[] files) {
Set<VirtualFile> selectedPluginRoots = new HashSet<>();
for (VirtualFile file : files) {
VirtualFile root = pluginFolderOf(file);
if (root != null) selectedPluginRoots.add(root);
}
return selectedPluginRoots;
}
private static VirtualFile pluginFolderOf(VirtualFile file) {
if (file.getParent() == null) return null;
File pluginsRoot = new File(LivePluginAppComponent.pluginsRootPath());
// comparing files because string comparison was observed not work on windows (e.g. "c:/..." and "C:/...")
if (!FileUtil.filesEqual(new File(file.getParent().getPath()), pluginsRoot))
return pluginFolderOf(file.getParent());
else
return file;
}
private static AnAction withIcon(Icon icon, AnAction action) {
action.getTemplatePresentation().setIcon(icon);
return action;
}
public void registerWindowFor(Project project) {
ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(project);
toolWindow = toolWindowManager.registerToolWindow(PLUGINS_TOOL_WINDOW_ID, false, ToolWindowAnchor.RIGHT, project, true);
toolWindow.setIcon(Icons.PLUGIN_TOOLWINDOW_ICON);
toolWindow.getContentManager().addContent(createContent(project));
}
public void unregisterWindowFrom(Project project) {
ToolWindowManager.getInstance(project).unregisterToolWindow(PLUGINS_TOOL_WINDOW_ID);
}
private Content createContent(Project project) {
FileSystemTree fsTree = createFsTree(project);
myFsTreeRef = Ref.create(fsTree);
installPopupMenuInto(fsTree);
JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(fsTree.getTree());
panel = new MySimpleToolWindowPanel(true, myFsTreeRef);
panel.add(scrollPane);
panel.setToolbar(createToolBar());
return ContentFactory.SERVICE.getInstance().createContent(panel, "", false);
}
public void reloadPluginRoots(Project project) {
// the only reason to create new instance of tree here is that
// I couldn't find a way to force tree to update it's roots
FileSystemTree fsTree = createFsTree(project);
myFsTreeRef.set(fsTree);
installPopupMenuInto(fsTree);
JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myFsTreeRef.get().getTree());
panel.remove(0);
panel.add(scrollPane, 0);
}
private static FileSystemTree createFsTree(Project project) {
MyTree myTree = new MyTree(project);
// must be installed before adding tree to FileSystemTreeImpl
EditSourceOnDoubleClickHandler.install(myTree);
FileSystemTree result = new FileSystemTreeImpl(project, createFileChooserDescriptor(), myTree, null, null, null) {
@Override
protected AbstractTreeBuilder createTreeBuilder(JTree tree, DefaultTreeModel treeModel, AbstractTreeStructure treeStructure, Comparator<NodeDescriptor> comparator, FileChooserDescriptor descriptor, @Nullable Runnable onInitialized) {
return new FileTreeBuilder(tree, treeModel, treeStructure, comparator, descriptor, onInitialized) {
@Override protected boolean isAutoExpandNode(NodeDescriptor nodeDescriptor) {
return nodeDescriptor.getElement() instanceof RootFileElement;
}
};
}
};
// must be installed after adding tree to FileSystemTreeImpl
EditSourceOnEnterKeyHandler.install(myTree);
return result;
}
private JComponent createToolBar() {
DefaultActionGroup actionGroup = new DefaultActionGroup();
actionGroup.add(withIcon(Icons.ADD_PLUGIN_ICON, createAddPluginsGroup()));
actionGroup.add(new DeletePluginAction());
actionGroup.add(new RunPluginAction());
actionGroup.add(new TestPluginAction());
actionGroup.addSeparator();
actionGroup.add(new RefreshPluginsPanelAction());
actionGroup.add(withIcon(Icons.EXPAND_ALL_ICON, new ExpandAllAction()));
actionGroup.add(withIcon(Icons.COLLAPSE_ALL_ICON, new CollapseAllAction()));
actionGroup.addSeparator();
actionGroup.add(withIcon(Icons.SETTINGS_ICON, createSettingsGroup()));
actionGroup.add(withIcon(Icons.HELP_ICON, new ShowHelpAction()));
// this is a "hack" to force drop-down box appear below button
// (see com.intellij.openapi.actionSystem.ActionPlaces#isToolbarPlace implementation for details)
String place = ActionPlaces.EDITOR_TOOLBAR;
JPanel toolBarPanel = new JPanel(new GridLayout());
toolBarPanel.add(ActionManager.getInstance().createActionToolbar(place, actionGroup, true).getComponent());
return toolBarPanel;
}
private static AnAction createSettingsGroup() {
DefaultActionGroup actionGroup = new DefaultActionGroup("Settings", true) {
@Override public boolean disableIfNoVisibleChildren() {
// without this IntelliJ calls update() on first action in the group
// even if the action group is collapsed
return false;
}
};
actionGroup.add(new AddPluginJarAsDependency());
actionGroup.add(new AddIDEAJarsAsDependencies());
actionGroup.add(new Separator());
actionGroup.add(new RunAllPluginsOnIDEStartAction());
actionGroup.add(new Separator());
actionGroup.add(new AddKotlinLibsAsDependency());
actionGroup.add(new AddScalaLibsAsDependency());
actionGroup.add(new AddClojureLibsAsDependency());
actionGroup.add(new DownloadScalaLibs());
actionGroup.add(new DownloadClojureLibs());
actionGroup.add(new DownloadKotlinCompilerLib());
return actionGroup;
}
private static AnAction createAddPluginsGroup() {
DefaultActionGroup actionGroup = new DefaultActionGroup("Add Plugin", true);
actionGroup.add(new AddNewPluginAction());
actionGroup.add(new AddPluginFromPathAction());
if (addFromGistAction != null) {
actionGroup.add(addFromGistAction);
}
if (addFromGitHubAction != null) {
actionGroup.add(addFromGitHubAction);
}
actionGroup.add(createAddPluginsExamplesGroup());
return actionGroup;
}
private static AnAction createAddPluginsExamplesGroup() {
final DefaultActionGroup actionGroup = new DefaultActionGroup("Examples", true);
actionGroup.add(new DumbAwareAction("Add All") {
@Override public void actionPerformed(@NotNull AnActionEvent e) {
AnAction[] actions = actionGroup.getChildActionsOrStubs();
for (AnAction action : actions) {
if (action instanceof AddExamplePluginAction) {
IDEUtil.runAction(action, "ADD_ALL_EXAMPLES");
}
}
}
});
actionGroup.addSeparator();
actionGroup.add(new AddExamplePluginAction(PLUGIN_EXAMPLES_PATH + "helloWorld/", asList("plugin.groovy")));
actionGroup.add(new AddExamplePluginAction(PLUGIN_EXAMPLES_PATH + "registerAction/", asList("plugin.groovy")));
actionGroup.add(new AddExamplePluginAction(PLUGIN_EXAMPLES_PATH + "popupMenu/", asList("plugin.groovy")));
actionGroup.add(new AddExamplePluginAction(PLUGIN_EXAMPLES_PATH + "popupSearch/", asList("plugin.groovy")));
actionGroup.add(new AddExamplePluginAction(PLUGIN_EXAMPLES_PATH + "toolWindow/", asList("plugin.groovy")));
actionGroup.add(new AddExamplePluginAction(PLUGIN_EXAMPLES_PATH + "toolbarWidget/", asList("plugin.groovy")));
actionGroup.add(new AddExamplePluginAction(PLUGIN_EXAMPLES_PATH + "textEditor/", asList("plugin.groovy")));
actionGroup.add(new AddExamplePluginAction(PLUGIN_EXAMPLES_PATH + "transformSelectedText/", asList("plugin.groovy")));
actionGroup.add(new AddExamplePluginAction(PLUGIN_EXAMPLES_PATH + "insertNewLineAbove/", asList("plugin.groovy")));
actionGroup.add(new AddExamplePluginAction(PLUGIN_EXAMPLES_PATH + "inspection/", asList("plugin.groovy")));
actionGroup.add(new AddExamplePluginAction(PLUGIN_EXAMPLES_PATH + "intention/", asList("plugin.groovy")));
actionGroup.add(new AddExamplePluginAction(PLUGIN_EXAMPLES_PATH + "projectFilesStats/", asList("plugin.groovy")));
actionGroup.add(new AddExamplePluginAction(PLUGIN_EXAMPLES_PATH + "miscUtil/", asList("plugin.groovy", "util/AClass.groovy")));
actionGroup.add(new AddExamplePluginAction(PLUGIN_EXAMPLES_PATH + "additionalClasspath/", asList("plugin.groovy")));
actionGroup.add(new AddExamplePluginAction(PLUGIN_EXAMPLES_PATH + "integrationTest/", asList("plugin.groovy", "plugin-test.groovy")));
actionGroup.add(new AddExamplePluginAction(PLUGIN_EXAMPLES_PATH + "helloScala/", asList("plugin.scala")));
actionGroup.add(new AddExamplePluginAction(PLUGIN_EXAMPLES_PATH + "helloClojure/", asList("plugin.clj")));
return actionGroup;
}
public List<String> selectedPluginIds() {
Collection<VirtualFile> rootFiles = findPluginRootsFor(myFsTreeRef.get().getSelectedFiles());
return map(rootFiles, virtualFile -> virtualFile.getName());
}
public boolean isActive() {
return toolWindow.isActive();
}
}
private static class MySimpleToolWindowPanel extends SimpleToolWindowPanel {
private final Ref<FileSystemTree> fileSystemTree;
public MySimpleToolWindowPanel(boolean vertical, Ref<FileSystemTree> fileSystemTree) {
super(vertical);
this.fileSystemTree = fileSystemTree;
}
/**
* Provides context for actions in plugin tree popup popup menu.
* Without it they would be disabled or won't work.
* <p/>
* Used by
* {@link com.intellij.openapi.fileChooser.actions.NewFileAction},
* {@link com.intellij.openapi.fileChooser.actions.NewFolderAction},
* {@link com.intellij.openapi.fileChooser.actions.FileDeleteAction}
*/
@Override public Object getData(@NonNls String dataId) {
// this is used by create directory/file to get context in which they're executed
// (without this they would be disabled or won't work)
if (dataId.equals(FileSystemTree.DATA_KEY.getName())) return fileSystemTree.get();
if (dataId.equals(FileChooserKeys.NEW_FILE_TYPE.getName())) return IDEUtil.GROOVY_FILE_TYPE;
if (dataId.equals(FileChooserKeys.DELETE_ACTION_AVAILABLE.getName())) return true;
if (dataId.equals(PlatformDataKeys.VIRTUAL_FILE_ARRAY.getName())) return fileSystemTree.get().getSelectedFiles();
if (dataId.equals(PlatformDataKeys.TREE_EXPANDER.getName())) return new DefaultTreeExpander(fileSystemTree.get().getTree());
return super.getData(dataId);
}
}
private static class MyTree extends Tree implements DataProvider {
private final Project project;
private final DeleteProvider deleteProvider = new FileDeleteProviderWithRefresh();
private MyTree(Project project) {
this.project = project;
getEmptyText().setText("No plugins to show");
setRootVisible(false);
}
@Nullable @Override public Object getData(@NonNls String dataId) {
if (PlatformDataKeys.NAVIGATABLE_ARRAY.is(dataId)) { // need this to be able to open files in toolwindow on double-click/enter
List<FileNodeDescriptor> nodeDescriptors = TreeUtil.collectSelectedObjectsOfType(this, FileNodeDescriptor.class);
List<Navigatable> navigatables = new ArrayList<>();
for (FileNodeDescriptor nodeDescriptor : nodeDescriptors) {
navigatables.add(new OpenFileDescriptor(project, nodeDescriptor.getElement().getFile()));
}
return navigatables.toArray(new Navigatable[navigatables.size()]);
} else if (PlatformDataKeys.DELETE_ELEMENT_PROVIDER.is(dataId)) {
return deleteProvider;
} else {
return null;
}
}
}
private static class FileDeleteProviderWithRefresh implements DeleteProvider {
private final DeleteProvider fileDeleteProvider = new VirtualFileDeleteProvider();
@Override public void deleteElement(@NotNull DataContext dataContext) {
fileDeleteProvider.deleteElement(dataContext);
RefreshPluginsPanelAction.refreshPluginTree();
}
@Override public boolean canDeleteElement(@NotNull DataContext dataContext) {
return fileDeleteProvider.canDeleteElement(dataContext);
}
}
}