blob: 63ee0a0005ccd84ec4e2be0a9c5752b9e708d5c9 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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.android.tools.idea.editors.hprof.views;
import com.android.tools.idea.actions.EditMultipleSourcesAction;
import com.android.tools.idea.actions.PsiFileAndLineNavigation;
import com.android.tools.idea.editors.allocations.ColumnTreeBuilder;
import com.android.tools.perflib.heap.ClassObj;
import com.android.tools.perflib.heap.Heap;
import com.android.tools.perflib.heap.Instance;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.*;
import com.android.tools.idea.editors.hprof.views.nodedata.HeapNode;
import com.android.tools.idea.editors.hprof.views.nodedata.HeapClassObjNode;
import com.android.tools.idea.editors.hprof.views.nodedata.HeapPackageNode;
import com.intellij.openapi.actionSystem.ex.CheckboxAction;
import com.intellij.openapi.actionSystem.ex.ComboBoxAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.ui.ColoredTreeCellRenderer;
import com.intellij.ui.JBColor;
import com.intellij.ui.PopupHandler;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.ui.components.JBList;
import com.intellij.ui.TreeSpeedSearch;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.util.PlatformIcons;
import com.intellij.util.containers.Convertor;
import com.intellij.util.containers.HashSet;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.util.*;
import java.util.List;
public class ClassesTreeView implements DataProvider {
public static final String TREE_NAME = "HprofClassesTree";
@NotNull private Project myProject;
@NotNull private Tree myTree;
@NotNull private DefaultTreeModel myTreeModel;
@NotNull private HeapPackageNode myRoot;
@NotNull private JComponent myColumnTree;
@Nullable private Comparator<HeapNode> myComparator;
private int mySelectedHeapId;
@NotNull private ListIndex myListIndex;
@NotNull private TreeIndex myTreeIndex;
@NotNull private DisplayMode myDisplayMode;
public ClassesTreeView(@NotNull Project project,
@NotNull DefaultActionGroup editorActionGroup,
@NotNull final SelectionModel selectionModel) {
myProject = project;
myRoot = new HeapPackageNode(null, "");
myTreeModel = new DefaultTreeModel(myRoot);
myTree = new Tree(myTreeModel);
myTree.setName(TREE_NAME);
myDisplayMode = DisplayMode.LIST;
myTree.setRootVisible(false);
myTree.setShowsRootHandles(false);
myTree.setLargeModel(true);
myTree.putClientProperty(DataManager.CLIENT_PROPERTY_DATA_PROVIDER, this);
JBList contextActionList = new JBList(new EditMultipleSourcesAction());
JBPopupFactory.getInstance().createListPopupBuilder(contextActionList);
final DefaultActionGroup popupGroup = new DefaultActionGroup(new EditMultipleSourcesAction());
myTree.addMouseListener(new PopupHandler() {
@Override
public void invokePopup(Component comp, int x, int y) {
ActionManager.getInstance().createActionPopupMenu(ActionPlaces.UNKNOWN, popupGroup).getComponent().show(comp, x, y);
}
});
editorActionGroup.addAction(new ComboBoxAction() {
@NotNull
@Override
protected DefaultActionGroup createPopupActionGroup(JComponent button) {
return new DefaultActionGroup(new CheckboxAction(DisplayMode.LIST.toString()) {
@Override
public boolean isSelected(AnActionEvent e) {
return myDisplayMode == DisplayMode.LIST;
}
@Override
public void setSelected(AnActionEvent e, boolean state) {
if (state) {
myDisplayMode = DisplayMode.LIST;
myTree.setShowsRootHandles(false);
myListIndex.buildList(myRoot);
restoreViewState(selectionModel);
}
}
}, new CheckboxAction(DisplayMode.TREE.toString()) {
@Override
public boolean isSelected(AnActionEvent e) {
return myDisplayMode == DisplayMode.TREE;
}
@Override
public void setSelected(AnActionEvent e, boolean state) {
if (state) {
myDisplayMode = DisplayMode.TREE;
myTree.setShowsRootHandles(true);
myTreeIndex.buildTree(mySelectedHeapId);
restoreViewState(selectionModel);
}
}
});
}
@Override
public void update(AnActionEvent e) {
super.update(e);
getTemplatePresentation().setText(myDisplayMode.toString());
e.getPresentation().setText(myDisplayMode.toString());
}
});
myListIndex = new ListIndex();
myTreeIndex = new TreeIndex();
selectionModel.addListener(myListIndex); // Add list index first, since that always updates; and tree index depends on it.
selectionModel.addListener(myTreeIndex);
selectionModel.addListener(new SelectionModel.SelectionListener() {
@Override
public void onHeapChanged(@NotNull Heap heap) {
mySelectedHeapId = heap.getId();
assert myListIndex.myHeapId == mySelectedHeapId;
if (myDisplayMode == DisplayMode.LIST) {
myListIndex.buildList(myRoot);
}
else if (myDisplayMode == DisplayMode.TREE) {
myTreeIndex.buildTree(mySelectedHeapId);
}
restoreViewState(selectionModel);
}
@Override
public void onClassObjChanged(@Nullable ClassObj classObj) {
}
@Override
public void onInstanceChanged(@Nullable Instance instance) {
}
});
myTree.addTreeSelectionListener(new TreeSelectionListener() {
@Override
public void valueChanged(TreeSelectionEvent e) {
TreePath path = e.getPath();
if (!e.isAddedPath()) {
return;
}
if (path == null || path.getPathCount() < 2) {
selectionModel.setClassObj(null);
return;
}
assert path.getLastPathComponent() instanceof HeapNode;
HeapNode heapNode = (HeapNode)path.getLastPathComponent();
if (heapNode instanceof HeapClassObjNode) {
selectionModel.setClassObj(((HeapClassObjNode)heapNode).getClassObj());
}
}
});
ColumnTreeBuilder builder = new ColumnTreeBuilder(myTree).addColumn(
new ColumnTreeBuilder.ColumnBuilder()
.setName("Class Name")
.setPreferredWidth(800)
.setHeaderAlignment(SwingConstants.LEFT)
.setComparator(new Comparator<HeapNode>() {
@Override
public int compare(HeapNode a, HeapNode b) {
int valueA = a instanceof HeapPackageNode ? 0 : 1;
int valueB = b instanceof HeapPackageNode ? 0 : 1;
if (valueA != valueB) {
return valueA - valueB;
}
return compareNames(a, b);
}
})
.setRenderer(new ColoredTreeCellRenderer() {
@Override
public void customizeCellRenderer(@NotNull JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
if (value instanceof HeapClassObjNode) {
ClassObj clazz = ((HeapClassObjNode)value).getClassObj();
String name = clazz.getClassName();
String pkg = null;
int i = name.lastIndexOf(".");
if (i != -1) {
pkg = name.substring(0, i);
name = name.substring(i + 1);
}
append(name, SimpleTextAttributes.REGULAR_ATTRIBUTES);
if (pkg != null) {
append(" (" + pkg + ")", new SimpleTextAttributes(Font.PLAIN, JBColor.GRAY));
}
setTransparentIconBackground(true);
setIcon(PlatformIcons.CLASS_ICON);
// TODO reformat anonymous classes (ANONYMOUS_CLASS_ICON) to match IJ.
}
else if (value instanceof HeapNode) {
append(((HeapNode)value).getSimpleName(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
setTransparentIconBackground(true);
setIcon(PlatformIcons.PACKAGE_ICON);
}
else {
append("This should not be rendered");
}
}
})
).addColumn(
new ColumnTreeBuilder.ColumnBuilder()
.setName("Total Count")
.setPreferredWidth(100)
.setHeaderAlignment(SwingConstants.RIGHT)
.setComparator(new Comparator<HeapNode>() {
@Override
public int compare(HeapNode a, HeapNode b) {
int result = a.getTotalCount() - b.getTotalCount();
return result == 0 ? compareNames(a, b) : result;
}
})
.setRenderer(new ColoredTreeCellRenderer() {
@Override
public void customizeCellRenderer(@NotNull JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
if (value instanceof HeapNode) {
append(Integer.toString(((HeapNode)value).getTotalCount()));
}
setTextAlign(SwingConstants.RIGHT);
}
})
).addColumn(
new ColumnTreeBuilder.ColumnBuilder()
.setName("Heap Count")
.setPreferredWidth(100)
.setHeaderAlignment(SwingConstants.RIGHT)
.setComparator(new Comparator<HeapNode>() {
@Override
public int compare(HeapNode a, HeapNode b) {
int result = a.getHeapInstancesCount(mySelectedHeapId) - b.getHeapInstancesCount(mySelectedHeapId);
return result == 0 ? compareNames(a, b) : result;
}
})
.setRenderer(new ColoredTreeCellRenderer() {
@Override
public void customizeCellRenderer(@NotNull JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
if (value instanceof HeapNode) {
append(Integer.toString(((HeapNode)value).getHeapInstancesCount(mySelectedHeapId)));
}
setTextAlign(SwingConstants.RIGHT);
}
})
).addColumn(
new ColumnTreeBuilder.ColumnBuilder()
.setName("Sizeof")
.setPreferredWidth(80)
.setHeaderAlignment(SwingConstants.RIGHT)
.setComparator(new Comparator<HeapNode>() {
@Override
public int compare(HeapNode a, HeapNode b) {
int sizeA = a.getInstanceSize();
int sizeB = b.getInstanceSize();
if (sizeA < 0 && sizeB < 0) {
return compareNames(a, b);
}
int result = sizeA - sizeB;
return result == 0 ? compareNames(a, b) : result;
}
})
.setRenderer(new ColoredTreeCellRenderer() {
@Override
public void customizeCellRenderer(@NotNull JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
if (value instanceof HeapClassObjNode) {
append(Integer.toString(((HeapClassObjNode)value).getInstanceSize()));
}
setTextAlign(SwingConstants.RIGHT);
}
})
).addColumn(
new ColumnTreeBuilder.ColumnBuilder()
.setName("Shallow Size")
.setPreferredWidth(100)
.setHeaderAlignment(SwingConstants.RIGHT)
.setComparator(new Comparator<HeapNode>() {
@Override
public int compare(HeapNode a, HeapNode b) {
int result = a.getShallowSize(mySelectedHeapId) - b.getShallowSize(mySelectedHeapId);
return result == 0 ? compareNames(a, b) : result;
}
}).setRenderer(new ColoredTreeCellRenderer() {
@Override
public void customizeCellRenderer(@NotNull JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
if (value instanceof HeapNode) {
append(Integer.toString(((HeapNode)value).getShallowSize(mySelectedHeapId)));
}
setTextAlign(SwingConstants.RIGHT);
}
})
).addColumn(
new ColumnTreeBuilder.ColumnBuilder()
.setName("Retained Size")
.setPreferredWidth(120)
.setHeaderAlignment(SwingConstants.RIGHT)
.setInitialOrder(SortOrder.DESCENDING)
.setComparator(new Comparator<HeapNode>() {
@Override
public int compare(HeapNode a, HeapNode b) {
long result = a.getRetainedSize() - b.getRetainedSize();
return result == 0 ? compareNames(a, b) : (result > 0 ? 1 : -1);
}
})
.setRenderer(new ColoredTreeCellRenderer() {
@Override
public void customizeCellRenderer(@NotNull JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
if (value instanceof HeapNode) {
append(Long.toString(((HeapNode)value).getRetainedSize()));
}
setTextAlign(SwingConstants.RIGHT);
}
})
);
//noinspection NullableProblems
builder.setTreeSorter(new ColumnTreeBuilder.TreeSorter<HeapNode>() {
@Override
public void sort(@NotNull Comparator<HeapNode> comparator, @NotNull SortOrder sortOrder) {
if (myComparator != comparator) {
myComparator = comparator;
selectionModel.setSelectionLocked(true);
TreePath selectionPath = myTree.getSelectionPath();
sortTree(myRoot);
myTreeModel.nodeStructureChanged(myRoot);
myTree.setSelectionPath(selectionPath);
myTree.scrollPathToVisible(selectionPath);
selectionModel.setSelectionLocked(false);
}
}
});
myColumnTree = builder.build();
installTreeSpeedSearch();
}
@NotNull
public JComponent getComponent() {
return myColumnTree;
}
private void installTreeSpeedSearch() {
new TreeSpeedSearch(myTree, new Convertor<TreePath, String>() {
@Override
public String convert(TreePath e) {
Object o = e.getLastPathComponent();
if (o instanceof HeapNode) {
if (o instanceof HeapClassObjNode) {
return ((HeapClassObjNode)o).getSimpleName();
}
else if (o instanceof HeapPackageNode) {
return ((HeapPackageNode)o).getFullName();
}
}
return o.toString();
}
}, true);
}
private void sortTree(@NotNull HeapPackageNode parent) {
if (parent.isLeaf() || myComparator == null) {
return;
}
List<HeapNode> children = parent.getChildren();
Collections.sort(children, myComparator);
for (HeapNode child : children) {
if (child instanceof HeapPackageNode) {
sortTree((HeapPackageNode)child);
}
}
}
private static int compareNames(@NotNull HeapNode a, @NotNull HeapNode b) {
int comparisonResult = a.getSimpleName()
.compareToIgnoreCase(b.getSimpleName());
if (comparisonResult == 0) {
return a.getFullName().compareToIgnoreCase(b.getFullName());
}
return comparisonResult;
}
private void restoreViewState(@NotNull final SelectionModel selectionModel) {
ClassObj classToSelect = selectionModel.getClassObj();
TreeNode nodeToSelect = null;
if (classToSelect != null) {
nodeToSelect = findClassObjNode(classToSelect);
}
sortTree(myRoot);
myTreeModel.nodeStructureChanged(myRoot);
final TreeNode targetNode = nodeToSelect;
if (targetNode != null) {
// If the new heap has the selected class (from a previous heap), then select it and scroll to it.
myColumnTree.revalidate();
final TreePath pathToSelect = new TreePath(myTreeModel.getPathToRoot(targetNode));
myTree.setSelectionPath(pathToSelect);
// This is kind of clunky, but the viewport doesn't know how big the tree is until it repaints.
// We need to do this because the contents of this tree has been more or less completely replaced.
// Unfortunately, calling repaint() only queues it, so we actually need an extra frame to select the node.
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
myTree.scrollPathToVisible(pathToSelect);
}
});
}
else {
selectionModel.setClassObj(null);
if (myTree.getRowCount() > 0) {
myTree.scrollRowToVisible(0);
}
}
}
@Nullable
private HeapClassObjNode findClassObjNode(@NotNull ClassObj targetClass) {
if (myDisplayMode == DisplayMode.LIST) {
for (int i = 0; i < myRoot.getChildCount(); ++i) {
TreeNode child = myRoot.getChildAt(i);
assert child instanceof HeapClassObjNode;
if (((HeapClassObjNode)child).getClassObj() == targetClass) {
return (HeapClassObjNode)child;
}
}
}
else if (myDisplayMode == DisplayMode.TREE) {
HeapPackageNode currentNode = myRoot;
String[] packages = targetClass.getClassName().split("\\.");
assert packages.length > 0;
int currentPackageIndex = 0;
while (currentPackageIndex < packages.length - 1) {
if (currentNode.getSubPackages().containsKey(packages[currentPackageIndex])) {
currentNode = currentNode.getSubPackages().get(packages[currentPackageIndex]);
++currentPackageIndex;
}
else {
return null;
}
}
for (int i = 0; i < currentNode.getChildCount(); ++i) {
TreeNode childTreeNode = currentNode.getChildAt(i);
assert childTreeNode instanceof HeapNode;
HeapNode child = (HeapNode)childTreeNode;
if (child instanceof HeapClassObjNode && ((HeapClassObjNode)child).getClassObj() == targetClass) {
return (HeapClassObjNode)child;
}
}
}
return null;
}
@Nullable
@Override
public Object getData(@NonNls String dataId) {
if (CommonDataKeys.NAVIGATABLE_ARRAY.is(dataId)) {
return getTargetFiles();
}
else if (CommonDataKeys.PROJECT.is(dataId)) {
return myProject;
}
return null;
}
@Nullable
private PsiFileAndLineNavigation[] getTargetFiles() {
TreePath path = myTree.getSelectionPath();
if (path.getPathCount() < 2) {
return null;
}
assert path.getLastPathComponent() instanceof HeapNode;
HeapNode node = (HeapNode)path.getLastPathComponent();
if (node instanceof HeapClassObjNode) {
ClassObj classObj = ((HeapClassObjNode)node).getClassObj();
String className = classObj.getClassName();
int arrayIndex = className.indexOf("[");
if (arrayIndex >= 0) {
className = className.substring(0, arrayIndex);
}
return PsiFileAndLineNavigation.wrappersForClassName(myProject, className, 1);
}
return null;
}
private static class ListIndex implements SelectionModel.SelectionListener {
ArrayList<HeapClassObjNode> myClasses = new ArrayList<HeapClassObjNode>();
private int myHeapId = -1;
@Override
public void onHeapChanged(@NotNull Heap heap) {
if (myHeapId != heap.getId()) {
myHeapId = heap.getId();
myClasses.clear();
// Find the union of the classObjs this heap has instances of, plus the classObjs themselves that are allocated on this heap.
HashSet<ClassObj> entriesSet = new HashSet<ClassObj>(heap.getClasses().size() + heap.getInstancesCount());
for (ClassObj classObj : heap.getClasses()) {
entriesSet.add(classObj);
}
for (Instance instance : heap.getInstances()) {
entriesSet.add(instance.getClassObj());
}
for (ClassObj classObj : entriesSet) {
myClasses.add(new HeapClassObjNode(classObj, myHeapId));
}
}
}
@Override
public void onClassObjChanged(@Nullable ClassObj classObj) {
}
@Override
public void onInstanceChanged(@Nullable Instance instance) {
}
public void buildList(@NotNull HeapNode root) {
root.removeAllChildren();
for (HeapClassObjNode heapClassObjNode : myClasses) {
heapClassObjNode.removeFromParent();
root.add(heapClassObjNode);
}
}
}
private class TreeIndex implements SelectionModel.SelectionListener {
private int myHeapId = -1;
@Override
public void onHeapChanged(@NotNull Heap heap) {
// TODO save the expansion state
if (myDisplayMode == DisplayMode.TREE) {
assert myListIndex.myHeapId == heap.getId();
buildTree(heap.getId());
}
}
@Override
public void onClassObjChanged(@Nullable ClassObj classObj) {
}
@Override
public void onInstanceChanged(@Nullable Instance instance) {
}
public void buildTree(int heapId) {
if (myHeapId != heapId) {
myHeapId = heapId;
myRoot.clear();
for (HeapClassObjNode heapClassObjNode : myListIndex.myClasses) {
myRoot.classifyClassObj(heapClassObjNode);
}
myRoot.update(mySelectedHeapId);
}
myRoot.buildTree();
}
}
private enum DisplayMode {
LIST("Class List View"),
TREE("Package Tree View");
@NotNull
private String myName;
DisplayMode(@NotNull String name) {
myName = name;
}
@Override
public String toString() {
return myName;
}
}
}