/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* 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 com.intellij.ide.projectView.impl;
import com.intellij.ide.DataManager;
import com.intellij.ide.PsiCopyPasteManager;
import com.intellij.ide.SelectInTarget;
import com.intellij.ide.dnd.*;
import com.intellij.ide.dnd.aware.DnDAwareTree;
import com.intellij.ide.projectView.BaseProjectTreeBuilder;
import com.intellij.ide.projectView.ProjectView;
import com.intellij.ide.projectView.impl.nodes.AbstractModuleNode;
import com.intellij.ide.projectView.impl.nodes.AbstractProjectNode;
import com.intellij.ide.projectView.impl.nodes.ModuleGroupNode;
import com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode;
import com.intellij.ide.util.treeView.*;
import com.intellij.injected.editor.VirtualFileWindow;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.openapi.wm.ToolWindowId;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.pom.Navigatable;
import com.intellij.problems.WolfTheProblemSolver;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.refactoring.move.MoveHandler;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.ReflectionUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashMap;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.tree.TreeUtil;
import consulo.ide.projectView.impl.ProjectViewPaneOptionProvider;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public abstract class AbstractProjectViewPane extends UserDataHolderBase implements DataProvider, Disposable, BusyObject {
public static final ExtensionPointName<AbstractProjectViewPane> EP_NAME = ExtensionPointName.create("com.intellij.projectViewPane");
@NotNull
protected final Project myProject;
private Runnable myTreeChangeListener;
protected DnDAwareTree myTree;
protected AbstractTreeStructure myTreeStructure;
private AbstractTreeBuilder myTreeBuilder;
// subId->Tree state; key may be null
private final Map<String, TreeState> myReadTreeState = new HashMap<>();
private String mySubId;
@NonNls
private static final String ELEMENT_SUBPANE = "subPane";
@NonNls
private static final String ATTRIBUTE_SUBID = "subId";
private DnDTarget myDropTarget;
private DnDSource myDragSource;
private DnDManager myDndManager;
private void queueUpdateByProblem() {
if (Registry.is("projectView.showHierarchyErrors")) {
if (myTreeBuilder != null) {
myTreeBuilder.queueUpdate();
}
}
}
protected AbstractProjectViewPane(@NotNull Project project) {
myProject = project;
WolfTheProblemSolver.ProblemListener problemListener = new WolfTheProblemSolver.ProblemListener() {
@Override
public void problemsAppeared(@NotNull VirtualFile file) {
queueUpdateByProblem();
}
@Override
public void problemsChanged(@NotNull VirtualFile file) {
queueUpdateByProblem();
}
@Override
public void problemsDisappeared(@NotNull VirtualFile file) {
queueUpdateByProblem();
}
};
WolfTheProblemSolver.getInstance(project).addProblemListener(problemListener, this);
Disposer.register(project, this);
}
protected final void fireTreeChangeListener() {
if (myTreeChangeListener != null) myTreeChangeListener.run();
}
public final void setTreeChangeListener(@NotNull Runnable listener) {
myTreeChangeListener = listener;
}
public final void removeTreeChangeListener() {
myTreeChangeListener = null;
}
public abstract String getTitle();
public abstract Icon getIcon();
@NotNull
public abstract String getId();
@Nullable
public final String getSubId() {
return mySubId;
}
public final void setSubId(@Nullable String subId) {
if (Comparing.strEqual(mySubId, subId)) return;
saveExpandedPaths();
mySubId = subId;
}
public boolean isInitiallyVisible() {
return true;
}
public boolean supportsManualOrder() {
return false;
}
/**
* @return all supported sub views IDs.
* should return empty array if there is no subViews as in Project/Packages view.
*/
@NotNull
public String[] getSubIds() {
return ArrayUtil.EMPTY_STRING_ARRAY;
}
@NotNull
public String getPresentableSubIdName(@NotNull final String subId) {
throw new IllegalStateException("should not call");
}
public abstract JComponent createComponent();
public JComponent getComponentToFocus() {
return myTree;
}
public void expand(@Nullable final Object[] path, final boolean requestFocus) {
if (getTreeBuilder() == null || path == null) return;
getTreeBuilder().buildNodeForPath(path);
DefaultMutableTreeNode node = getTreeBuilder().getNodeForPath(path);
if (node == null) {
return;
}
TreePath treePath = new TreePath(node.getPath());
myTree.expandPath(treePath);
if (requestFocus) {
IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> {
IdeFocusManager.getGlobalInstance().requestFocus(myTree, true);
});
}
TreeUtil.selectPath(myTree, treePath);
}
@Override
public void dispose() {
if (myDndManager != null) {
if (myDropTarget != null) {
myDndManager.unregisterTarget(myDropTarget, myTree);
myDropTarget = null;
}
if (myDragSource != null) {
myDndManager.unregisterSource(myDragSource, myTree);
myDragSource = null;
}
myDndManager = null;
}
setTreeBuilder(null);
myTree = null;
myTreeStructure = null;
}
@NotNull
public abstract ActionCallback updateFromRoot(boolean restoreExpandedPaths);
public abstract void select(Object element, VirtualFile file, boolean requestFocus);
public void selectModule(final Module module, final boolean requestFocus) {
doSelectModuleOrGroup(module, requestFocus);
}
private void doSelectModuleOrGroup(final Object toSelect, final boolean requestFocus) {
ToolWindowManager windowManager = ToolWindowManager.getInstance(myProject);
final Runnable runnable = () -> {
ProjectView projectView = ProjectView.getInstance(myProject);
if (requestFocus) {
projectView.changeView(getId(), getSubId());
}
((BaseProjectTreeBuilder)getTreeBuilder()).selectInWidth(toSelect, requestFocus, node -> node instanceof AbstractModuleNode ||
node instanceof ModuleGroupNode ||
node instanceof AbstractProjectNode);
};
if (requestFocus) {
windowManager.getToolWindow(ToolWindowId.PROJECT_VIEW).activate(runnable);
}
else {
runnable.run();
}
}
public void selectModuleGroup(ModuleGroup moduleGroup, boolean requestFocus) {
doSelectModuleOrGroup(moduleGroup, requestFocus);
}
public TreePath[] getSelectionPaths() {
return myTree == null ? null : myTree.getSelectionPaths();
}
public void addToolbarActions(DefaultActionGroup actionGroup) {
}
public void addToolbarActionsImpl(DefaultActionGroup actionGroup) {
addToolbarActions(actionGroup);
for (ProjectViewPaneOptionProvider provider : ProjectViewPaneOptionProvider.EX_NAME.getExtensions()) {
provider.addToolbarActions(this, actionGroup);
}
}
@NotNull
protected <T extends NodeDescriptor> List<T> getSelectedNodes(final Class<T> nodeClass) {
TreePath[] paths = getSelectionPaths();
if (paths == null) return Collections.emptyList();
final ArrayList<T> result = new ArrayList<>();
for (TreePath path : paths) {
Object lastPathComponent = path.getLastPathComponent();
if (lastPathComponent instanceof DefaultMutableTreeNode) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode)lastPathComponent;
Object userObject = node.getUserObject();
if (userObject != null && ReflectionUtil.isAssignable(nodeClass, userObject.getClass())) {
result.add((T)userObject);
}
}
}
return result;
}
@Override
public Object getData(String dataId) {
if (CommonDataKeys.NAVIGATABLE_ARRAY.is(dataId)) {
TreePath[] paths = getSelectionPaths();
if (paths == null) return null;
final ArrayList<Navigatable> navigatables = new ArrayList<>();
for (TreePath path : paths) {
Object lastPathComponent = path.getLastPathComponent();
if (lastPathComponent instanceof DefaultMutableTreeNode) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode)lastPathComponent;
Object userObject = node.getUserObject();
if (userObject instanceof Navigatable) {
navigatables.add((Navigatable)userObject);
}
else if (node instanceof Navigatable) {
navigatables.add((Navigatable)node);
}
}
}
if (navigatables.isEmpty()) {
return null;
}
else {
return navigatables.toArray(new Navigatable[navigatables.size()]);
}
}
if (myTreeStructure instanceof AbstractTreeStructureBase) {
return ((AbstractTreeStructureBase)myTreeStructure).getDataFromProviders(getSelectedNodes(AbstractTreeNode.class), dataId);
}
return null;
}
// used for sorting tabs in the tabbed pane
public abstract int getWeight();
public abstract SelectInTarget createSelectInTarget();
public final TreePath getSelectedPath() {
return TreeUtil.getSelectedPathIfOne(myTree);
}
public final NodeDescriptor getSelectedDescriptor() {
final DefaultMutableTreeNode node = getSelectedNode();
if (node == null) return null;
Object userObject = node.getUserObject();
if (userObject instanceof NodeDescriptor) {
return (NodeDescriptor)userObject;
}
return null;
}
public final DefaultMutableTreeNode getSelectedNode() {
TreePath path = getSelectedPath();
return path == null ? null : ObjectUtils.tryCast(path.getLastPathComponent(), DefaultMutableTreeNode.class);
}
public final Object getSelectedElement() {
final Object[] elements = getSelectedElements();
return elements.length == 1 ? elements[0] : null;
}
@NotNull
public final PsiElement[] getSelectedPSIElements() {
List<PsiElement> psiElements = new ArrayList<>();
for (Object element : getSelectedElements()) {
final PsiElement psiElement = getPSIElement(element);
if (psiElement != null) {
psiElements.add(psiElement);
}
}
return PsiUtilCore.toPsiElementArray(psiElements);
}
@Nullable
protected PsiElement getPSIElement(@Nullable final Object element) {
if (element instanceof PsiElement) {
PsiElement psiElement = (PsiElement)element;
if (psiElement.isValid()) {
return psiElement;
}
}
return null;
}
@Nullable
protected Module getNodeModule(@Nullable final Object element) {
if (element instanceof PsiElement) {
PsiElement psiElement = (PsiElement)element;
return ModuleUtilCore.findModuleForPsiElement(psiElement);
}
return null;
}
@NotNull
public final Object[] getSelectedElements() {
TreePath[] paths = getSelectionPaths();
if (paths == null) return PsiElement.EMPTY_ARRAY;
ArrayList<Object> list = new ArrayList<>(paths.length);
for (TreePath path : paths) {
Object lastPathComponent = path.getLastPathComponent();
Object element = getElementFromTreeNode(lastPathComponent);
if (element instanceof Object[]) {
Collections.addAll(list, (Object[])element);
}
else if (element != null) {
list.add(element);
}
}
return ArrayUtil.toObjectArray(list);
}
@Nullable
public Object getElementFromTreeNode(@Nullable final Object treeNode) {
if (treeNode instanceof DefaultMutableTreeNode) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode)treeNode;
return exhumeElementFromNode(node);
}
return null;
}
private TreeNode[] getSelectedTreeNodes() {
TreePath[] paths = getSelectionPaths();
if (paths == null) return null;
final List<TreeNode> result = new ArrayList<>();
for (TreePath path : paths) {
Object lastPathComponent = path.getLastPathComponent();
if (lastPathComponent instanceof DefaultMutableTreeNode) {
result.add((TreeNode)lastPathComponent);
}
}
return result.toArray(new TreeNode[result.size()]);
}
protected Object exhumeElementFromNode(final DefaultMutableTreeNode node) {
return extractUserObject(node);
}
public static Object extractUserObject(DefaultMutableTreeNode node) {
Object userObject = node.getUserObject();
Object element = null;
if (userObject instanceof AbstractTreeNode) {
AbstractTreeNode descriptor = (AbstractTreeNode)userObject;
element = descriptor.getValue();
}
else if (userObject instanceof NodeDescriptor) {
NodeDescriptor descriptor = (NodeDescriptor)userObject;
element = descriptor.getElement();
if (element instanceof AbstractTreeNode) {
element = ((AbstractTreeNode)element).getValue();
}
}
else if (userObject != null) {
element = userObject;
}
return element;
}
public AbstractTreeBuilder getTreeBuilder() {
return myTreeBuilder;
}
public void readExternal(Element element) throws InvalidDataException {
List<Element> subPanes = element.getChildren(ELEMENT_SUBPANE);
for (Element subPane : subPanes) {
String subId = subPane.getAttributeValue(ATTRIBUTE_SUBID);
TreeState treeState = TreeState.createFrom(subPane);
if (!treeState.isEmpty()) {
myReadTreeState.put(subId, treeState);
}
}
for (ProjectViewPaneOptionProvider provider : ProjectViewPaneOptionProvider.EX_NAME.getExtensions()) {
KeyWithDefaultValue key = provider.getKey();
String valueOfKey = JDOMExternalizerUtil.readField(element, key.toString());
if (valueOfKey != null) {
putUserData(key, provider.parseValue(valueOfKey));
}
}
}
public void writeExternal(Element element) throws WriteExternalException {
saveExpandedPaths();
for (String subId : myReadTreeState.keySet()) {
TreeState treeState = myReadTreeState.get(subId);
Element subPane = new Element(ELEMENT_SUBPANE);
if (subId != null) {
subPane.setAttribute(ATTRIBUTE_SUBID, subId);
}
treeState.writeExternal(subPane);
element.addContent(subPane);
}
for (ProjectViewPaneOptionProvider provider : ProjectViewPaneOptionProvider.EX_NAME.getExtensions()) {
KeyWithDefaultValue key = provider.getKey();
Object value = getUserData(key);
//noinspection unchecked
String stringValue = provider.toString(value);
if (stringValue != null) {
JDOMExternalizerUtil.writeField(element, key.toString(), stringValue);
}
}
}
@Override
@SuppressWarnings("unchecked")
public <T> T getUserData(@NotNull Key<T> key) {
T value = super.getUserData(key);
if (value == null && key instanceof KeyWithDefaultValue) {
return (T)((KeyWithDefaultValue)key).getDefaultValue();
}
return value;
}
protected void saveExpandedPaths() {
if (myTree != null) {
TreeState treeState = TreeState.createOn(myTree);
if (!treeState.isEmpty()) {
myReadTreeState.put(getSubId(), treeState);
}
}
}
public final void restoreExpandedPaths() {
TreeState treeState = myReadTreeState.get(getSubId());
if (treeState != null && !treeState.isEmpty()) {
treeState.applyTo(myTree);
}
}
public void installComparator() {
installComparator(getTreeBuilder());
}
public void installComparator(AbstractTreeBuilder treeBuilder) {
final ProjectView projectView = ProjectView.getInstance(myProject);
treeBuilder.setNodeDescriptorComparator(new GroupByTypeComparator(projectView, getId()));
}
public JTree getTree() {
return myTree;
}
@NotNull
public PsiDirectory[] getSelectedDirectories() {
List<PsiDirectory> directories = ContainerUtil.newArrayList();
for (PsiDirectoryNode node : getSelectedNodes(PsiDirectoryNode.class)) {
PsiDirectory directory = node.getValue();
if (directory != null) {
directories.add(directory);
Object parentValue = node.getParent().getValue();
if (parentValue instanceof PsiDirectory && Registry.is("projectView.choose.directory.on.compacted.middle.packages")) {
while (true) {
directory = directory.getParentDirectory();
if (directory == null || directory.equals(parentValue)) {
break;
}
directories.add(directory);
}
}
}
}
if (!directories.isEmpty()) {
return directories.toArray(new PsiDirectory[directories.size()]);
}
final PsiElement[] elements = getSelectedPSIElements();
if (elements.length == 1) {
final PsiElement element = elements[0];
if (element instanceof PsiDirectory) {
return new PsiDirectory[]{(PsiDirectory)element};
}
else if (element instanceof PsiDirectoryContainer) {
return ((PsiDirectoryContainer)element).getDirectories();
}
else {
final PsiFile containingFile = element.getContainingFile();
if (containingFile != null) {
final PsiDirectory psiDirectory = containingFile.getContainingDirectory();
if (psiDirectory != null) {
return new PsiDirectory[]{psiDirectory};
}
final VirtualFile file = containingFile.getVirtualFile();
if (file instanceof VirtualFileWindow) {
final VirtualFile delegate = ((VirtualFileWindow)file).getDelegate();
final PsiFile delegatePsiFile = containingFile.getManager().findFile(delegate);
if (delegatePsiFile != null && delegatePsiFile.getContainingDirectory() != null) {
return new PsiDirectory[]{delegatePsiFile.getContainingDirectory()};
}
}
return PsiDirectory.EMPTY_ARRAY;
}
}
}
else {
final DefaultMutableTreeNode selectedNode = getSelectedNode();
if (selectedNode != null) {
return getSelectedDirectoriesInAmbiguousCase(selectedNode);
}
}
return PsiDirectory.EMPTY_ARRAY;
}
@NotNull
protected PsiDirectory[] getSelectedDirectoriesInAmbiguousCase(@NotNull final DefaultMutableTreeNode node) {
final Object userObject = node.getUserObject();
if (userObject instanceof AbstractModuleNode) {
final Module module = ((AbstractModuleNode)userObject).getValue();
if (module != null) {
final ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module);
final VirtualFile[] sourceRoots = moduleRootManager.getSourceRoots();
List<PsiDirectory> dirs = new ArrayList<>(sourceRoots.length);
final PsiManager psiManager = PsiManager.getInstance(myProject);
for (final VirtualFile sourceRoot : sourceRoots) {
final PsiDirectory directory = psiManager.findDirectory(sourceRoot);
if (directory != null) {
dirs.add(directory);
}
}
return dirs.toArray(new PsiDirectory[dirs.size()]);
}
}
return PsiDirectory.EMPTY_ARRAY;
}
// Drag'n'Drop stuff
@Nullable
public static PsiElement[] getTransferedPsiElements(Transferable transferable) {
try {
final Object transferData = transferable.getTransferData(DnDEventImpl.ourDataFlavor);
if (transferData instanceof TransferableWrapper) {
return ((TransferableWrapper)transferData).getPsiElements();
}
return null;
}
catch (Exception e) {
return null;
}
}
@Nullable
public static TreeNode[] getTransferedTreeNodes(Transferable transferable) {
try {
final Object transferData = transferable.getTransferData(DnDEventImpl.ourDataFlavor);
if (transferData instanceof TransferableWrapper) {
return ((TransferableWrapper)transferData).getTreeNodes();
}
return null;
}
catch (Exception e) {
return null;
}
}
protected void enableDnD() {
if (!ApplicationManager.getApplication().isHeadlessEnvironment()) {
myDropTarget = new ProjectViewDropTarget(myTree, new Retriever() {
@Override
public PsiElement getPsiElement(@Nullable TreeNode node) {
return getPSIElement(getElementFromTreeNode(node));
}
@Override
public Module getModule(TreeNode treeNode) {
return getNodeModule(getElementFromTreeNode(treeNode));
}
}, myProject) {
@Override
public void cleanUpOnLeave() {
beforeDnDLeave();
super.cleanUpOnLeave();
}
@Override
public boolean update(DnDEvent event) {
beforeDnDUpdate();
return super.update(event);
}
};
myDragSource = new MyDragSource();
myDndManager = DnDManager.getInstance();
myDndManager.registerSource(myDragSource, myTree);
myDndManager.registerTarget(myDropTarget, myTree);
}
}
protected void beforeDnDUpdate() {
}
protected void beforeDnDLeave() {
}
public void setTreeBuilder(final AbstractTreeBuilder treeBuilder) {
if (treeBuilder != null) {
Disposer.register(this, treeBuilder);
// needs refactoring for project view first
// treeBuilder.setCanYieldUpdate(true);
}
myTreeBuilder = treeBuilder;
}
public boolean supportsFoldersAlwaysOnTop() {
return true;
}
public boolean supportsSortByType() {
return true;
}
private class MyDragSource implements DnDSource {
@Override
public boolean canStartDragging(DnDAction action, Point dragOrigin) {
if ((action.getActionId() & DnDConstants.ACTION_COPY_OR_MOVE) == 0) return false;
final Object[] elements = getSelectedElements();
final PsiElement[] psiElements = getSelectedPSIElements();
DataContext dataContext = DataManager.getInstance().getDataContext(myTree);
return psiElements.length > 0 || canDragElements(elements, dataContext, action.getActionId());
}
@Override
public DnDDragStartBean startDragging(DnDAction action, Point dragOrigin) {
final PsiElement[] psiElements = getSelectedPSIElements();
final TreeNode[] nodes = getSelectedTreeNodes();
return new DnDDragStartBean(new TransferableWrapper() {
@Override
public List<File> asFileList() {
return PsiCopyPasteManager.asFileList(psiElements);
}
@Override
public TreeNode[] getTreeNodes() {
return nodes;
}
@Override
public PsiElement[] getPsiElements() {
return psiElements;
}
});
}
@Override
public Pair<Image, Point> createDraggedImage(DnDAction action, Point dragOrigin) {
final TreePath[] paths = getSelectionPaths();
if (paths == null) return null;
final int count = paths.length;
final JLabel label = new JLabel(String.format("%s item%s", count, count == 1 ? "" : "s"));
label.setOpaque(true);
label.setForeground(myTree.getForeground());
label.setBackground(myTree.getBackground());
label.setFont(myTree.getFont());
label.setSize(label.getPreferredSize());
final BufferedImage image = UIUtil.createImage(label.getWidth(), label.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = (Graphics2D)image.getGraphics();
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f));
label.paint(g2);
g2.dispose();
return new Pair<>(image, new Point(-image.getWidth(null), -image.getHeight(null)));
}
@Override
public void dragDropEnd() {
}
@Override
public void dropActionChanged(int gestureModifiers) {
}
}
private static boolean canDragElements(Object[] elements, DataContext dataContext, int dragAction) {
for (Object element : elements) {
if (element instanceof Module) {
return true;
}
}
return dragAction == DnDConstants.ACTION_MOVE && MoveHandler.canMove(dataContext);
}
@NotNull
public Project getProject() {
return myProject;
}
@NotNull
@Override
public ActionCallback getReady(@NotNull Object requestor) {
if (myTreeBuilder == null || myTreeBuilder.isDisposed()) return ActionCallback.REJECTED;
return myTreeBuilder.getUi().getReady(requestor);
}
}