/*
* Copyright 2003-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 jetbrains.mps.ide.findusages.view.treeholder.treeview;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.KeyboardShortcut;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.ui.LayeredIcon;
import jetbrains.mps.icons.MPSIcons.Nodes;
import jetbrains.mps.ide.findusages.view.treeholder.tree.DataNode;
import jetbrains.mps.ide.findusages.view.treeholder.tree.DataTree;
import jetbrains.mps.ide.findusages.view.treeholder.tree.TextOptions;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.AbstractResultNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.BaseNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.ModelNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.NodeNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.treeview.path.PathItemRole;
import jetbrains.mps.ide.ui.tree.MPSTree;
import jetbrains.mps.ide.ui.tree.MPSTreeNode;
import jetbrains.mps.project.Project;
import jetbrains.mps.util.Computable;
import jetbrains.mps.util.ComputeRunnable;
import jetbrains.mps.workbench.action.ActionUtils;
import jetbrains.mps.workbench.action.BaseAction;
import org.jetbrains.annotations.Nullable;
import javax.swing.AbstractAction;
import javax.swing.Icon;
import javax.swing.KeyStroke;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.TreePath;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class UsagesTree extends MPSTree {
private static final String COMMAND_OPEN_NODE_IN_PROJECT = "open_node_in_project";
private static final String COMMAND_OPEN_NODE_IN_TREE = "open_node_in_tree";
private static final String COMMAND_INCLUDE = "include";
private static final String COMMAND_EXCLUDE = "exclude";
private DataTree myContents;
private HashSet<PathItemRole> myResultPathProvider = new HashSet<PathItemRole>();
private boolean myAdditionalInfoNeeded;
private boolean myShowSearchedNodes;
private boolean myGroupSearchedNodes;
private boolean myShowPopupMenu;
private int myIsAdjusting = 0;
private boolean myAutoscroll = false;
private Project myProject;
public UsagesTree(Project project) {
myProject = project;
myAdditionalInfoNeeded = false;
myResultPathProvider.add(PathItemRole.ROLE_MAIN_RESULTS);
myResultPathProvider.add(PathItemRole.ROLE_TARGET_NODE);
setRootVisible(false);
getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), COMMAND_OPEN_NODE_IN_PROJECT);
getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_F4, 0), COMMAND_OPEN_NODE_IN_PROJECT);
getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_F1, InputEvent.ALT_MASK), COMMAND_OPEN_NODE_IN_TREE);
KeyStroke deleteKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0);
getInputMap().put(deleteKeyStroke, COMMAND_EXCLUDE);
if (SystemInfo.isMac) {
//KeyStroke insertKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0);
//getInputMap().put(insertKeyStroke, COMMAND_INCLUDE);
} else {
KeyStroke insertKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0);
getInputMap().put(insertKeyStroke, COMMAND_INCLUDE);
}
addTreeExpansionListener(new TreeExpansionListener() {
@Override
public void treeExpanded(TreeExpansionEvent event) {
BaseNodeData data = ((UsagesTreeNode) event.getPath().getLastPathComponent()).getUserObject().getData();
data.setExpanded(true);
}
@Override
public void treeCollapsed(TreeExpansionEvent event) {
BaseNodeData data = ((UsagesTreeNode) event.getPath().getLastPathComponent()).getUserObject().getData();
data.setExpanded(false);
}
});
getActionMap().put(COMMAND_OPEN_NODE_IN_PROJECT, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
openCurrentNodeLink(false, false);
}
});
getActionMap().put(COMMAND_OPEN_NODE_IN_TREE, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
openCurrentNodeLink(true, true);
}
});
getActionMap().put(COMMAND_EXCLUDE, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
setCurrentNodesExclusion(true);
}
});
getActionMap().put(COMMAND_INCLUDE, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
setCurrentNodesExclusion(false);
}
});
addTreeSelectionListener(new TreeSelectionListener() {
@Override
public void valueChanged(TreeSelectionEvent e) {
if (myAutoscroll) {
openNewlySelectedNodeLink(e, false, false);
}
}
});
}
@Override
public boolean isDisposed() {
return super.isDisposed() || myProject.isDisposed();
}
public void startAdjusting() {
myIsAdjusting++;
}
public void finishAdjusting() {
myIsAdjusting--;
rebuildLater();
}
@Override
public void rebuildNow() {
UsagesTree.super.rebuildNow();
int i;
for (i = 0; i < getRootNode().getChildCount(); i++) {
Object[] path = {getRootNode(), getRootNode().getChildAt(i)};
TreePath treePath = new TreePath(path);
expandPath(treePath);
}
}
public void setContents(DataTree contents, Set<PathItemRole> pathProvider) {
myContents = contents;
myResultPathProvider.clear();
myResultPathProvider.addAll(pathProvider);
if (myIsAdjusting == 0) {
rebuildLater();
}
}
public void setContents(DataTree contents) {
myContents = contents;
if (myIsAdjusting == 0) {
rebuildLater();
}
}
public void setResultPathProvider(Set<PathItemRole> resultPathProvider) {
myResultPathProvider.clear();
myResultPathProvider.addAll(resultPathProvider);
if (myIsAdjusting == 0) {
rebuildLater();
}
}
public void setAdditionalInfoNeeded(boolean additionalInfoNeeded) {
myAdditionalInfoNeeded = additionalInfoNeeded;
if (myIsAdjusting == 0) {
rebuildLater();
}
}
public boolean isAdditionalInfoNeeded() {
return myAdditionalInfoNeeded;
}
public void setShowSearchedNodes(boolean showSearchedNodes) {
myShowSearchedNodes = showSearchedNodes;
if (myIsAdjusting == 0) {
rebuildLater();
}
}
public boolean isShowSearchedNodes() {
return myShowSearchedNodes;
}
public void setGroupSearchedNodes(boolean groupSearchedNodes) {
myGroupSearchedNodes = groupSearchedNodes;
if (myIsAdjusting == 0) {
rebuildLater();
}
}
public boolean isGroupSearchedNodes() {
return myGroupSearchedNodes;
}
public boolean isShowPopupMenu() {
return myShowPopupMenu;
}
public void setShowPopupMenu(boolean showPopupMenu) {
myShowPopupMenu = showPopupMenu;
}
public void setAll(DataTree contents, HashSet<PathItemRole> pathProvider) {
myContents = contents;
myResultPathProvider = pathProvider;
if (myIsAdjusting == 0) {
rebuildLater();
}
}
@Override
protected UsagesTreeNode rebuild() {
ComputeRunnable<UsagesTreeNode> cr = new ComputeRunnable<UsagesTreeNode>(new Computable<UsagesTreeNode>() {
@Override
public UsagesTreeNode compute() {
UsagesTreeNode root = new UsagesTreeNode();
if (myContents == null || myContents.getTreeRoot().getChildren().isEmpty()) {
// FIXME refactor UsagesTree construction so that it doesn't try to show tree before any content supplied.
// Now the tree is rebuilt on view options change (UsagesTreeComponent#setComponentsViewOptions())
return root;
}
if (myShowSearchedNodes) {
HashSet<PathItemRole> searchedNodesPathProvider = new HashSet<PathItemRole>();
searchedNodesPathProvider.add(PathItemRole.ROLE_MAIN_SEARCHED_NODES);
DataNode searchedNodesRoot = myContents.getTreeRoot().getChildren().get(0);
if (searchedNodesRoot.containsNodes(NodeNodeData.class)) {
if (myGroupSearchedNodes) {
searchedNodesPathProvider.add(PathItemRole.ROLE_ROOT);
searchedNodesPathProvider.add(PathItemRole.ROLE_ROOT_TO_TARGET_NODE);
}
searchedNodesPathProvider.add(PathItemRole.ROLE_TARGET_NODE);
} else if (searchedNodesRoot.containsNodes(ModelNodeData.class)) {
if (myGroupSearchedNodes) {
searchedNodesPathProvider.add(PathItemRole.ROLE_MODULE);
}
searchedNodesPathProvider.add(PathItemRole.ROLE_MODEL);
} else {
searchedNodesPathProvider.add(PathItemRole.ROLE_MODULE);
}
root.add(buildTree(searchedNodesRoot, searchedNodesPathProvider));
}
root.add(buildTree(myContents.getTreeRoot().getChildren().get(1), myResultPathProvider));
return root;
}
});
myProject.getModelAccess().runReadAction(cr);
return cr.getResult();
}
//this is not recursive
//use only for top-level nodes
private UsagesTreeNode buildTree(DataNode root, HashSet<PathItemRole> nodeCategories) {
List<UsagesTreeNode> children = buildSubtreeStructure(root, nodeCategories);
assert children.size() == 1;
UsagesTreeNode child = children.get(0);
mergeChildren(children);
buildCounters(child);
sortByCaption(children);
setUIProperties(child);
makeNodesHTML(child);
return child;
}
private void sortByCaption(List<UsagesTreeNode> children) {
Collections.sort(children, new Comparator<UsagesTreeNode>() {
private boolean isIgnored(UsagesTreeNode node) {
// need to keep order of non-root nodes as they seen in an editor (see MPS-6113)
BaseNodeData data = node.getUserObject().getData();
return (data instanceof NodeNodeData) && !((NodeNodeData) data).isRootNode();
}
@Override
public int compare(UsagesTreeNode o1, UsagesTreeNode o2) {
if (isIgnored(o1) || isIgnored(o2)) {
return 0;
}
String s1 = o1.getUserObject().getData().getPlainText();
String s2 = o2.getUserObject().getData().getPlainText();
return s1.compareTo(s2);
}
});
for (UsagesTreeNode child : children) {
sortByCaption(child.internalGetChildren());
}
}
private List<UsagesTreeNode> buildSubtreeStructure(DataNode root, HashSet<PathItemRole> nodeCategories) {
List<UsagesTreeNode> children = new ArrayList<UsagesTreeNode>();
for (DataNode child : root.getChildren()) {
children.addAll(buildSubtreeStructure(child, nodeCategories));
}
BaseNodeData data = root.getData();
if (nodeCategories.contains(data.getRole()) || data.isPathTail()) {
UsagesTreeNode node = new UsagesTreeNode(root);
for (UsagesTreeNode child : children) {
node.add(child);
}
children.clear();
children.add(node);
}
return children;
}
private int buildCounters(UsagesTreeNode root) {
int num = 0;
for (UsagesTreeNode child : root.internalGetChildren()) {
num += buildCounters(child);
}
root.setSubresultsCount(num);
if (root.getUserObject().getData().isResultNode()) {
num++;
}
return num;
}
private void mergeChildren(List<UsagesTreeNode> children) {
List<UsagesTreeNode> mergedChildren = new ArrayList<UsagesTreeNode>();
Map<Object, UsagesTreeNode> childMap = new LinkedHashMap<Object, UsagesTreeNode>();
for (UsagesTreeNode child : children) {
Object additionID = child.getUserObject().getData().getIdObject();
if (additionID == null) {
//we don't know what to do in the case of deleted nodes, so we won't merge them
mergedChildren.add(child);
} else {
UsagesTreeNode addToNode = childMap.get(additionID);
if (addToNode == null) {
childMap.put(additionID, child);
} else {
List<UsagesTreeNode> addition = new ArrayList<UsagesTreeNode>(child.internalGetChildren());
for (UsagesTreeNode additionChild : addition) {
addToNode.add(additionChild);
}
}
}
}
mergedChildren.addAll(childMap.values());
for (UsagesTreeNode child : mergedChildren) {
mergeChildren(child.internalGetChildren());
}
children.clear();
children.addAll(mergedChildren);
}
private void setUIProperties(UsagesTreeNode root) {
BaseNodeData data = root.getUserObject().getData();
Icon icon = data.getIcon(() -> myProject.getRepository());
if (data.isResultNode()) {
final LayeredIcon result = new LayeredIcon(2);
result.setIcon(icon, 0);
result.setIcon(Nodes.UsagesResultOverlay, 1);
icon = result;
}
root.setIcon(icon);
String invalid = data.isInvalid() ? "<font color=red>[Invalid]</font> " : "";
String caption = data.getText(new TextOptions(myAdditionalInfoNeeded, !root.isLeaf(), root.getSubresultsCount()));
if (data.isExcluded()) {
root.setText(invalid + "<font color=gray><s>" + caption + "</s></font>");
} else {
root.setText(invalid + caption);
}
int i;
for (i = 0; i < root.getChildCount(); i++) {
setUIProperties((UsagesTreeNode) root.getChildAt(i));
}
}
private void makeNodesHTML(UsagesTreeNode root) {
root.setText("<html>" + root.getText() + "</html>");
int i;
for (i = 0; i < root.getChildCount(); i++) {
UsagesTreeNode child = (UsagesTreeNode) root.getChildAt(i);
makeNodesHTML(child);
}
}
@Override
public UsagesTreeNode getCurrentNode() {
return (UsagesTreeNode) super.getCurrentNode();
}
public UsagesTreeNode[] getCurrentNodes() {
return getSelectedNodes(UsagesTreeNode.class, null);
}
private void setCurrentNodesExclusion(boolean isExcluded) {
Set<DataNode> nodes = new HashSet<>();
//we need to traverse UI tree nodes here as some child nodes of a UI node can correspond to non-child nodes of its data node
for (UsagesTreeNode node : getCurrentNodes()) {
Enumeration e = node.breadthFirstEnumeration();
while (e.hasMoreElements()) {
UsagesTreeNode n = ((UsagesTreeNode) e.nextElement());
nodes.add(n.getUserObject());
}
}
myContents.setExcluded(nodes, isExcluded);
}
private void setCurrentNodesOnlyExclusion() {
myContents.setExcluded(new HashSet<DataNode>(Arrays.asList(myContents.getTreeRoot())), true);
setCurrentNodesExclusion(false);
}
@Override
protected ActionGroup createPopupActionGroup(MPSTreeNode node) {
if (!myShowPopupMenu) {
return null;
}
BaseAction includeAction = new BaseAction("Include") {
{
String keyStroke = "INSERT";
KeyboardShortcut shortcut = new KeyboardShortcut(KeyStroke.getKeyStroke(keyStroke), null);
KeymapManager.getInstance().getKeymap(KeymapManager.DEFAULT_IDEA_KEYMAP).addShortcut(getActionId(), shortcut);
}
@Override
public void doExecute(AnActionEvent e, Map<String, Object> _params) {
setCurrentNodesExclusion(false);
e.getInputEvent().consume();
}
};
BaseAction excludeAction = new BaseAction("Exclude") {
{
String keyStroke = "DELETE";
KeyboardShortcut shortcut = new KeyboardShortcut(KeyStroke.getKeyStroke(keyStroke), null);
KeymapManager.getInstance().getKeymap(KeymapManager.DEFAULT_IDEA_KEYMAP).addShortcut(getActionId(), shortcut);
}
@Override
public void doExecute(AnActionEvent e, Map<String, Object> _params) {
setCurrentNodesExclusion(true);
e.getInputEvent().consume();
}
};
BaseAction includeSelectedOnlyAction = new BaseAction("Include selected only") {
@Override
public void doExecute(AnActionEvent e, Map<String, Object> _params) {
setCurrentNodesOnlyExclusion();
e.getInputEvent().consume();
}
};
return ActionUtils.groupFromActions(includeAction, excludeAction, includeSelectedOnlyAction);
}
private void openCurrentNodeLink(boolean inProjectIfPossible, boolean focus) {
UsagesTreeNode treeNode = getCurrentNode();
if (treeNode != null) {
treeNode.goByNodeLink(inProjectIfPossible, focus);
}
}
private void openNewlySelectedNodeLink(TreeSelectionEvent e, boolean inProjectIfPossible, boolean focus) {
TreePath path = e.getNewLeadSelectionPath();
if (path == null) {
return;
}
Object treeNode = path.getLastPathComponent();
if (treeNode instanceof UsagesTreeNode) {
((UsagesTreeNode) treeNode).goByNodeLink(inProjectIfPossible, focus);
}
}
private boolean isAliveResultNode(DataNode node) {
return node.getData().isResultNode() && !node.getData().isInvalid();
}
private UsagesTreeNode findFirstResultInSubtree(UsagesTreeNode root, boolean includeRoot) {
assert root != null;
if (includeRoot && isAliveResultNode(root.getUserObject())) {
return root;
}
for (MPSTreeNode node : root) {
UsagesTreeNode result = findFirstResultInSubtree(((UsagesTreeNode) node), true);
if (result != null) {
return result;
}
}
return null;
}
private UsagesTreeNode findLastResultInSubtree(UsagesTreeNode root, boolean includeRoot) {
assert root != null;
List<MPSTreeNode> children = new ArrayList<MPSTreeNode>();
for (MPSTreeNode node : root) {
children.add(node);
}
Collections.reverse(children);
for (MPSTreeNode node : children) {
UsagesTreeNode result = findLastResultInSubtree(((UsagesTreeNode) node), true);
if (result != null) {
return result;
}
}
if (includeRoot && isAliveResultNode(root.getUserObject())) {
return root;
}
return null;
}
public UsagesTreeNode findNextResult(UsagesTreeNode fromNode) {
assert fromNode != null;
//trying to do step into
UsagesTreeNode node = findFirstResultInSubtree(fromNode, false);
if (node != null) {
return node;
}
//go up until reach a node with child to the right from that we came from and try to get results from its children, the go to parent...
UsagesTreeNode current = fromNode;
while (true) {
UsagesTreeNode parent = (UsagesTreeNode) current.getParent();
if (parent == getResultsNode().getParent()) {
return null;
}
//step into next children of that parent
int nextIndex = parent.getIndex(current) + 1;
while (nextIndex < parent.getChildCount()) {
UsagesTreeNode firstResult = findFirstResultInSubtree((UsagesTreeNode) parent.getChildAt(nextIndex), true);
if (firstResult != null) {
return firstResult;
}
nextIndex++;
}
current = parent;
}
}
public UsagesTreeNode findPrevResult(UsagesTreeNode fromNode) {
assert fromNode != null;
//go up until reach a node with child result nodes to the left from that we came from or find a result node
UsagesTreeNode current = fromNode;
while (true) {
UsagesTreeNode parent = (UsagesTreeNode) current.getParent();
if (parent == null) {
return null;
}
//step into prev children of that parent
int prevIndex = parent.getIndex(current) - 1;
while (prevIndex >= 0) {
UsagesTreeNode lastResult = findLastResultInSubtree((UsagesTreeNode) parent.getChildAt(prevIndex), true);
if (lastResult != null) {
return lastResult;
}
prevIndex--;
}
DataNode userObject = parent.getUserObject();
if (userObject != null) {
if (isAliveResultNode(userObject)) {
return parent;
}
}
current = parent;
}
}
private UsagesTreeNode getResultsNode() {
int index = myShowSearchedNodes ? 1 : 0;
return (UsagesTreeNode) getRootNode().getChildAt(index);
}
@Nullable
private UsagesTreeNode getSearchedNodesNode() {
if (!myShowSearchedNodes) {
return null;
}
return (UsagesTreeNode) getRootNode().getChildAt(0);
}
public void setAutoscroll(boolean autoscroll) {
myAutoscroll = autoscroll;
if (getCurrentNode() != null) {
getCurrentNode().goByNodeLink(false, false);
}
}
public boolean isAutoscroll() {
return myAutoscroll;
}
public class UsagesTreeNode extends MPSTreeNode {
private int mySubresultsCount = 0;
public UsagesTreeNode() {
setNodeIdentifier("");
}
public UsagesTreeNode(DataNode userObj) {
super(userObj);
if (userObj != null) {
setNodeIdentifier(userObj.getData().getPlainText());
}
}
@Override
protected void updateErrorState() {
//disable for
}
@Override
public int getToggleClickCount() {
return isPathTail() ? -1 : 2;
}
private boolean isPathTail() {
return getUserObject() != null && getUserObject().getData().isPathTail();
}
@Override
public void doubleClick() {
if (isPathTail()) {
goByNodeLink(false, true);
}
}
/*package*/ void goByNodeLink(boolean inProjectIfPossible, boolean focus) {
BaseNodeData data = getUserObject().getData();
if (data instanceof AbstractResultNodeData) {
((AbstractResultNodeData) data).navigate(myProject, inProjectIfPossible, focus);
}
}
public int getSubresultsCount() {
return mySubresultsCount;
}
public void setSubresultsCount(int subresultsCount) {
mySubresultsCount = subresultsCount;
}
@Override
public DataNode getUserObject() {
return (DataNode) super.getUserObject();
}
List<UsagesTreeNode> internalGetChildren() {
return children == null ? Collections.emptyList() : children;
}
}
}