blob: a8ccfd887b9053642cbfdce427fcf0a4fe5c5f28 [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.allocations;
import com.android.annotations.Nullable;
import com.google.common.collect.ImmutableList;
import com.intellij.ui.ColoredTreeCellRenderer;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.table.JBTable;
import com.intellij.util.ui.tree.WideSelectionTreeUI;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
import javax.swing.tree.AbstractLayoutCache;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.util.*;
import java.util.List;
/**
* A ColumnTree is an alternative to TreeTables with slightly less functionality but
* with a much more simple model. A column tree is composed by two things: A tree and
* a set of headers. Each node in the tree renders all the "columns". This is
* achieved by doing the following:
* - The tree is given a renderer that is a JPanel with a custom layout manager.
* - Each component of this panel is the renderer for each "Column"
* - The layout manager given to the panel uses the columns to decide how to align
* the components for each column
* - The TreeUI is changed on the tree to create components that extend the whole
* width of the tree.
*
* To set up your component do this:
* JComponent node = new ColumnTreeBuilder(myTree)
* .addColumn(new ColumnTreeBuilder.ColumnBuilder()
* .setName("Column 1")
* .setRenderer(new MyColumnOneRenderer()))
* .addColumn(new ColumnTreeBuilder.ColumnBuilder()
* .setName("Column 2")
* .setRenderer(new MyColumnTwoRenderer()))
* .build()
*/
public class ColumnTreeBuilder {
@NotNull
private final JTree myTree;
@NotNull
private final ColumnTreeCellRenderer myCellRenderer;
@NotNull
private final JTable myTable;
@NotNull
private final TableRowSorter<TableModel> myRowSorter;
@NotNull
private final DefaultTableModel myTableModel;
@Nullable
private TreeSorter myTreeSorter;
@NotNull
private List<ColumnBuilder> myColumBuilders;
public ColumnTreeBuilder(@NotNull JTree tree) {
myTree = tree;
myTableModel = new DefaultTableModel();
myTable = new JBTable(myTableModel);
myCellRenderer = new ColumnTreeCellRenderer(myTree, myTable.getColumnModel());
myRowSorter = new TableRowSorter<TableModel>(myTable.getModel());
myColumBuilders = new LinkedList<ColumnBuilder>();
}
/**
* Sets the tree sorter to call when a column wants to be sorted.
*/
public ColumnTreeBuilder setTreeSorter(@NotNull TreeSorter sorter) {
myTreeSorter = sorter;
return this;
}
public JComponent build() {
boolean showsRootHandles = myTree.getShowsRootHandles(); // Stash this value since it'll get stomped WideSelectionTreeUI.
myTree.setUI(new ColumnTreeUI());
myTree.setShowsRootHandles(showsRootHandles);
myTree.setCellRenderer(myCellRenderer);
myTable.getColumnModel().addColumnModelListener(new TableColumnModelListener() {
@Override
public void columnAdded(TableColumnModelEvent tableColumnModelEvent) {
}
@Override
public void columnRemoved(TableColumnModelEvent tableColumnModelEvent) {
}
@Override
public void columnMoved(TableColumnModelEvent tableColumnModelEvent) {
}
@Override
public void columnMarginChanged(ChangeEvent changeEvent) {
myTree.revalidate();
myTree.repaint();
}
@Override
public void columnSelectionChanged(ListSelectionEvent listSelectionEvent) {
}
});
myTable.setRowSorter(myRowSorter);
myRowSorter.addRowSorterListener(new RowSorterListener() {
@Override
public void sorterChanged(RowSorterEvent event) {
if (myTreeSorter != null && !myRowSorter.getSortKeys().isEmpty()) {
RowSorter.SortKey key = myRowSorter.getSortKeys().get(0);
Comparator<?> comparator = myRowSorter.getComparator(key.getColumn());
Enumeration<TreePath> expanded = myTree.getExpandedDescendants(new TreePath(myTree.getModel().getRoot()));
comparator = key.getSortOrder() == SortOrder.ASCENDING ? comparator : Collections.reverseOrder(comparator);
myTreeSorter.sort(comparator, key.getSortOrder());
if (expanded != null) {
while (expanded.hasMoreElements()) {
myTree.expandPath(expanded.nextElement());
}
}
}
}
});
myTable.setAutoResizeMode(JTable.AUTO_RESIZE_NEXT_COLUMN);
for (ColumnBuilder column : myColumBuilders) {
column.create(myTableModel);
}
for (ColumnBuilder column : myColumBuilders) {
column.configure(myTable, myRowSorter, myCellRenderer);
}
JPanel panel = new TreeWrapperPanel(myTable, myTree);
JTableHeader header = myTable.getTableHeader();
header.setReorderingAllowed(false);
JViewport viewport = new JViewport();
viewport.setView(header);
JBScrollPane scrollPane = new JBScrollPane(panel);
scrollPane.setColumnHeader(viewport);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
return scrollPane;
}
public ColumnTreeBuilder addColumn(ColumnBuilder column) {
myColumBuilders.add(column);
return this;
}
public interface TreeSorter<T> {
void sort(Comparator<T> comparator, SortOrder order);
}
/**
* A custom layout manager to use while rendering a tree node.
* It uses the given column model to know the size of the columns.
*/
private static class ColumnLayout implements LayoutManager {
@NotNull
private final TableColumnModel myColumnModel;
@NotNull
private final JTree myTree;
public ColumnLayout(@NotNull JTree tree, @NotNull TableColumnModel columnModel) {
myTree = tree;
myColumnModel = columnModel;
}
@Override
public void addLayoutComponent(String name, Component comp) {
}
@Override
public void removeLayoutComponent(Component comp) {
}
@Override
public Dimension preferredLayoutSize(Container parent) {
return parent.getSize();
}
@Override
public Dimension minimumLayoutSize(Container parent) {
return parent.getSize();
}
@Override
public void layoutContainer(Container parent) {
int size = parent.getComponentCount();
int columns = myColumnModel.getColumnCount();
assert size == columns;
int padding = myTree.getWidth();
for (int i = 0; i < size && i < columns; i++) {
padding -= myColumnModel.getColumn(i).getWidth();
}
// The parent component has no fixed start position, but it fills the whole width, so we fill in reverse.
int offset = parent.getWidth();
for (int i = size - 1; i >= 0; i--) {
Component component = parent.getComponent(i);
TableColumn column = myColumnModel.getColumn(i);
int width = Math.min(column.getWidth() + padding, offset);
component.setBounds(offset - width, 0, width, parent.getHeight());
offset -= width;
padding = 0;
}
}
}
/**
* The cell renderer to use on the tree. It's a simple panel that uses the ColumnLayout
* to align its "columns". When rendered, it calls to its child renderers to set up their
* widgets.
*/
private static class ColumnTreeCellRenderer extends JPanel implements TreeCellRenderer {
public ColumnTreeCellRenderer(JTree tree, TableColumnModel columnModel) {
super(new ColumnLayout(tree, columnModel));
}
@Override
public Component getTreeCellRendererComponent(JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
setFont(tree.getFont());
for (int i = 0; i < getComponentCount(); i++) {
Component component = getComponent(i);
if (component instanceof ColoredTreeCellRenderer) {
((ColoredTreeCellRenderer)component).getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
}
}
return this;
}
@Override
public Dimension getPreferredSize() {
Dimension dimension = new Dimension();
for (int i = 0; i < getComponentCount(); i++) {
Dimension size = getComponent(i).getPreferredSize();
dimension.width += size.width;
dimension.height = Math.max(dimension.height, size.height);
}
return dimension;
}
}
/**
* A custom TreeUI that is needed to adjust the components created by each node.
* The width of each component on the tree must be all the way to the right, which
* means that we need to know how much indentation is added at which point, and
* this is known by the UI (see getRowX).
*/
private static class ColumnTreeUI extends WideSelectionTreeUI {
private int myWidth = -1;
@Override
public void paint(Graphics g, JComponent c) {
if (myWidth != c.getWidth()) {
treeState.invalidateSizes();
myWidth = c.getWidth();
}
super.paint(g, c);
}
@Override
protected AbstractLayoutCache.NodeDimensions createNodeDimensions() {
return new NodeDimensionsHandler() {
@Override
public Rectangle getNodeDimensions(Object value, int row, int depth, boolean expanded, Rectangle size) {
Rectangle dimensions = super.getNodeDimensions(value, row, depth, expanded, size);
dimensions.width = tree.getWidth() - getRowX(row, depth);
return dimensions;
}
};
}
}
public static class ColumnBuilder {
private String myName;
private int myWidth;
private int myHeaderAlignment;
private Comparator<?> myComparator;
private ColoredTreeCellRenderer myRenderer;
private SortOrder myInitialOrder = SortOrder.UNSORTED;
public ColumnBuilder setName(String name) {
myName = name;
return this;
}
public ColumnBuilder setPreferredWidth(int width) {
myWidth = width;
return this;
}
public ColumnBuilder setHeaderAlignment(int alignment) {
myHeaderAlignment = alignment;
return this;
}
public void create(DefaultTableModel model) {
model.addColumn(myName);
}
public void configure(JTable table, TableRowSorter<TableModel> sorter, ColumnTreeCellRenderer renderer) {
TableColumn column = table.getColumn(myName);
column.setPreferredWidth(myWidth);
final TableCellRenderer tableCellRenderer = table.getTableHeader().getDefaultRenderer();
column.setHeaderRenderer(new DefaultTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
Component component = tableCellRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if (component instanceof JLabel) {
((JLabel)component).setHorizontalAlignment(myHeaderAlignment);
}
return component;
}
});
if (myComparator != null) {
sorter.setComparator(column.getModelIndex(), myComparator);
if (myInitialOrder != SortOrder.UNSORTED) {
sorter.setSortKeys(ImmutableList.of(new RowSorter.SortKey(column.getModelIndex(), myInitialOrder)));
}
}
else {
sorter.setSortable(column.getModelIndex(), false);
}
assert myRenderer != null;
renderer.add(myRenderer);
}
public ColumnBuilder setComparator(@NotNull Comparator<?> comparator) {
myComparator = comparator;
return this;
}
public ColumnBuilder setRenderer(@NotNull ColoredTreeCellRenderer renderer) {
myRenderer = renderer;
return this;
}
public ColumnBuilder setInitialOrder(@NotNull SortOrder initialOrder) {
myInitialOrder = initialOrder;
return this;
}
}
private static class TreeWrapperPanel extends JPanel implements Scrollable {
private final JTree myTree;
public TreeWrapperPanel(JTable table, JTree tree) {
super(new BorderLayout());
myTree = tree;
add(table, BorderLayout.NORTH);
add(myTree, BorderLayout.CENTER);
}
@Override
public Dimension getPreferredScrollableViewportSize() {
return myTree.getPreferredScrollableViewportSize();
}
@Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
return myTree.getScrollableUnitIncrement(visibleRect, orientation, direction);
}
@Override
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
return myTree.getScrollableUnitIncrement(visibleRect, orientation, direction);
}
@Override
public boolean getScrollableTracksViewportWidth() {
return myTree.getScrollableTracksViewportWidth();
}
@Override
public boolean getScrollableTracksViewportHeight() {
return myTree.getScrollableTracksViewportHeight();
}
}
}