/*
* Copyright 2000-2007 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.jam.view.treetable;
import com.intellij.ide.util.treeView.AbstractTreeBuilder;
import com.intellij.ide.util.treeView.NodeDescriptor;
import com.intellij.jam.view.tree.JamAbstractTreeBuilder;
import com.intellij.jam.view.tree.JamNodeDescriptor;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.ui.PopupHandler;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.dualView.TreeTableView;
import com.intellij.util.EditSourceOnDoubleClickHandler;
import com.intellij.util.Function;
import com.intellij.util.Icons;
import com.intellij.util.PairProcessor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.SoftArrayHashMap;
import com.intellij.util.ui.ColumnInfo;
import com.intellij.util.ui.Tree;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.tree.TreeUtil;
import com.intellij.util.ui.treetable.ListTreeTableModelOnColumns;
import com.intellij.util.ui.treetable.TreeTableCellRenderer;
import com.intellij.util.ui.treetable.TreeTableModel;
import com.intellij.util.ui.treetable.TreeTableTree;
import com.intellij.util.xml.ui.CommittablePanel;
import com.intellij.util.xml.ui.EmptyPane;
import com.intellij.util.xml.ui.StripeTableCellRenderer;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.table.*;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.util.*;
import java.util.List;
/**
* @author peter
*/
public abstract class JamTreeTableView implements CommittablePanel, DataProvider {
private static final DefaultTableCellRenderer LOADING_NODE_RENDERER = new DefaultTableCellRenderer() {
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
final Component cellRendererComponent = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
setText("");
return cellRendererComponent;
}
};
@NonNls private static final String TREE = "tree";
@NonNls private static final String EMPTY = "empty";
private final CardLayout myCardLayout;
private final JPanel myContentPanel = new JPanel();
private final EmptyPane myEmptyPane = new EmptyPane("If you see this text, please, submit a bug");
private final JPanel myPanel = new PanelToViewDataDelegator();
private final TreeTableView myTreeTableView;
private final JamAbstractTreeBuilder myBuilder;
private final Project myProject;
private final JamNodeDescriptor myRootDescriptor;
private final ListTreeTableModelOnColumns myModel;
private final SoftArrayHashMap<Object,List<Object>> myCache = new SoftArrayHashMap<Object, List<Object>>();
private boolean myTreeShowing = false;
public JamTreeTableView(final Project project, final JamNodeDescriptor rootDescriptor) {
myProject = project;
myRootDescriptor = rootDescriptor;
final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
myModel = new ListTreeTableModelOnColumns(rootNode, ColumnInfo.EMPTY_ARRAY) {
public boolean isCellEditable(Object node, int column) {
try {
return super.isCellEditable(getJamNodeDescriptor(node), column);
}
catch (LoadingNodeException e) {
return false;
}
}
public void setValueAt(final Object aValue, Object node, int column) {
try {
final ColumnInfo columnInfo = getColumnInfos()[column];
final JamNodeDescriptor descriptor = getJamNodeDescriptor(node);
getCachedColumnValues(((DefaultMutableTreeNode) node).getUserObjectPath()).set(column, aValue);
JamTreeTableView.this.setValueAt(columnInfo, descriptor, aValue);
}
catch (LoadingNodeException e) {
}
}
public Object getValueAt(Object value, int column) {
return getCachedColumnValues(((DefaultMutableTreeNode) value).getUserObjectPath()).get(column);
}
};
myTreeTableView = new MyTreeTableView(myModel);
myBuilder = new JamAbstractTreeBuilder(project, getTree(), myModel, rootDescriptor) {
protected boolean updateNodeDescriptor(final NodeDescriptor descriptor) {
final boolean result = super.updateNodeDescriptor(descriptor);
if (!descriptor.equals(myRootDescriptor) || getTree().isRootVisible()) {
if (((JamNodeDescriptor) descriptor).isValid()) {
cacheNode((JamNodeDescriptor)descriptor);
}
}
return result;
}
public boolean isAutoExpandNode(NodeDescriptor nodeDescriptor) {
return false;
}
};
Disposer.register(this, myBuilder);
myTreeTableView.setDragEnabled(false);
myTreeTableView.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
myTreeTableView.setRowHeight(Icons.CLASS_ICON.getIconHeight());
final JTableHeader header = myTreeTableView.getTableHeader();
header.addMouseMotionListener(new MouseMotionAdapter() {
public void mouseMoved(MouseEvent e) {
updateTooltip(e);
}
});
header.addMouseListener(new MouseAdapter() {
public void mouseEntered(MouseEvent e) {
updateTooltip(e);
}
});
header.setReorderingAllowed(false);
getTree().setShowsRootHandles(true);
UIUtil.setLineStyleAngled(getTree());
getTree().setCellRenderer(new JamToolTipRenderer());
getTree().getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
EditSourceOnDoubleClickHandler.install(myTreeTableView);
myBuilder.init();
myPanel.setLayout(new BorderLayout());
myPanel.add(myContentPanel, BorderLayout.CENTER);
myCardLayout = new CardLayout();
myContentPanel.setLayout(myCardLayout);
myContentPanel.add(myEmptyPane.getComponent(), EMPTY);
myContentPanel.add(ScrollPaneFactory.createScrollPane(myTreeTableView), TREE);
}
protected final void updateTooltip(final MouseEvent e) {
final int i = myTreeTableView.columnAtPoint(e.getPoint());
if (i >= 0) {
myTreeTableView.getTableHeader().setToolTipText(getColumnInfos()[i].getTooltipText());
}
}
protected void setValueAt(final ColumnInfo columnInfo, final JamNodeDescriptor descriptor, final Object aValue) {
columnInfo.setValue(descriptor, aValue);
reset();
}
protected final void init() {
final JComponent toolbar = createToolbar();
if (toolbar != null) {
myPanel.add(toolbar, BorderLayout.NORTH);
}
final ActionGroup actionGroup = createActionGroup(true);
if (actionGroup != null) {
PopupHandler.installPopupHandler(myEmptyPane.getComponent(), actionGroup, ActionPlaces.J2EE_ATTRIBUTES_VIEW_POPUP, ActionManager.getInstance());
PopupHandler.installPopupHandler(myTreeTableView, actionGroup, ActionPlaces.J2EE_ATTRIBUTES_VIEW_POPUP, ActionManager.getInstance());
}
reset();
TreeUtil.expandAll(getTree());
}
public final TreeTableView getTreeTableView() {
return myTreeTableView;
}
public final Project getProject() {
return myProject;
}
public final Tree getTree() {
return myTreeTableView.getTree();
}
public final void refreshTreeTable() {
boolean showTree = isShowTree();
final boolean visibilityChanged = showTree != myTreeShowing;
if (visibilityChanged || showTree) {
TreePath[] selection = myTreeTableView.getTree().getSelectionPaths();
cacheValues();
myBuilder.setWaiting(false);
myBuilder.updateFromRoot();
if (!getTree().isExpanded(0)) {
TreeUtil.expandRootChildIfOnlyOne(getTree());
}
myTreeTableView.getSelectionModel().clearSelection();
if (selection != null) {
for (TreePath treePath : selection) {
final int row = myTreeTableView.getTree().getRowForPath(treePath);
myTreeTableView.getSelectionModel().addSelectionInterval(row, row);
}
}
}
if (!showTree) {
//noinspection HardCodedStringLiteral
myEmptyPane.setText("<html>" + getEmptyPaneText() + "</html>");
}
if (visibilityChanged) {
myTreeShowing = showTree;
if (showTree) {
TreeUtil.expandRootChildIfOnlyOne(getTree());
myCardLayout.show(myContentPanel, TREE);
}
else {
myCardLayout.show(myContentPanel, EMPTY);
}
myContentPanel.requestFocus();
}
}
protected void setColumnsPreferredWidth() {
final ColumnInfo[] columnInfos = getColumnInfos();
TableColumnModel columnModel = myTreeTableView.getColumnModel();
final JTableHeader header = myTreeTableView.getTableHeader();
final FontMetrics fontMetrics = header.getFontMetrics(header.getFont());
int maxPreferredWidth = 0;
TableColumn treeColumn = null;
for (int i = 0; i < columnInfos.length; i++) {
final ColumnInfo columnInfo = columnInfos[i];
final TableColumn column = columnModel.getColumn(i);
final String name = columnInfo.getName();
final int minWidth = StringUtil.isNotEmpty(name.trim()) ? fontMetrics.stringWidth(name) + 15 : 0;
column.setMinWidth(minWidth);
if (TreeTableModel.class.isAssignableFrom(columnInfo.getColumnClass())) {
treeColumn = column;
final int preferredWidth = myTreeTableView.getTree().getSize().width;
column.setPreferredWidth(preferredWidth);
if (preferredWidth > minWidth) {
column.setMinWidth(preferredWidth);
}
}
else if (!(columnInfo instanceof JamSpacerColumnInfo)) {
final int fixedWidth = columnInfo.getWidth(myTreeTableView);
if (fixedWidth > 0) {
final int realWidth = Math.max(minWidth, fixedWidth);
column.setMaxWidth(realWidth);
column.setMinWidth(realWidth);
column.setPreferredWidth(realWidth);
}
else {
int preferredWidth = minWidth;
final String preferredValue = columnInfo.getPreferredStringValue();
if (preferredValue != null) {
preferredWidth = Math.max(preferredWidth, fontMetrics.stringWidth(preferredValue));
}
column.setPreferredWidth(preferredWidth + columnInfo.getAdditionalWidth());
final String maxValue = columnInfo.getMaxStringValue();
if (maxValue != null) {
column.setMaxWidth(fontMetrics.stringWidth(preferredValue) + columnInfo.getAdditionalWidth());
}
}
}
maxPreferredWidth = Math.max(maxPreferredWidth, column.getPreferredWidth());
}
if (treeColumn != null) {
if (treeColumn.getPreferredWidth() < 4 * maxPreferredWidth) {
treeColumn.setPreferredWidth(4 * maxPreferredWidth);
}
}
}
@Nullable
protected ActionGroup createActionGroup(final boolean isPopup) {
return isPopup? createPopupActionGroup() : createToolbarActions();
}
@Nullable
protected ActionGroup createPopupActionGroup() {
return null;
}
@Nullable
protected ActionGroup createToolbarActions() {
return null;
}
@Nullable
protected JComponent createToolbar() {
final ActionGroup actionGroup = createActionGroup(false);
if (actionGroup == null) return null;
final JComponent component = ActionManager.getInstance().createActionToolbar(ActionPlaces.PROJECT_VIEW_TOOLBAR, actionGroup, true).getComponent();
component.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Color.darkGray), component.getBorder()));
return component;
}
public void dispose() {
}
public JComponent getComponent() {
return myPanel;
}
public void reset() {
final boolean columnsChanged = myModel.setColumns(createColumnInfos());
refreshTreeTable();
final AbstractTableModel tableModel = (AbstractTableModel)myTreeTableView.getModel();
if (columnsChanged) {
tableModel.fireTableStructureChanged();
setColumnsPreferredWidth();
}
if (myTreeShowing) {
tableModel.fireTableDataChanged();
}
}
public void commit() {
}
protected void cacheValues() {
myCache.clear();
}
protected void recacheColumn(final int columnIndex) {
assert columnIndex >= 0 && columnIndex < getColumnInfos().length;
final ColumnInfo columnInfo = getColumnInfos()[columnIndex];
myCache.processLeafEntries(new PairProcessor<Object, List<Object>>() {
public boolean process(final Object o, final List<Object> objects) {
objects.set(columnIndex, columnInfo.valueOf(o));
return true;
}
});
}
private static JamNodeDescriptor getJamNodeDescriptor(final Object nodeDescriptor) throws LoadingNodeException{
final DefaultMutableTreeNode node = (DefaultMutableTreeNode)nodeDescriptor;
if (AbstractTreeBuilder.isLoadingNode(node)) {
throw new LoadingNodeException();
}
return (JamNodeDescriptor)node.getUserObject();
}
private static class LoadingNodeException extends Exception {
}
protected final void setCachedValue(Object value, int column, Object... path) {
final JamNodeDescriptor[] path1 = getDescriptorPath(path);
if (path1 == null) return;
getCachedColumnValues(path1).set(column, value);
}
private static JamNodeDescriptor[] getDescriptorPath(JamNodeDescriptor descriptor) {
LinkedList<JamNodeDescriptor> list = new LinkedList<JamNodeDescriptor>();
while (descriptor != null) {
list.addFirst(descriptor);
descriptor = (JamNodeDescriptor)descriptor.getParent();
}
return list.toArray(new JamNodeDescriptor[list.size()]);
}
@Nullable
private JamNodeDescriptor[] getDescriptorPath(final Object... path) {
assert corresponds(myRootDescriptor, path[0]);
JamNodeDescriptor[] result = new JamNodeDescriptor[path.length];
result[0] = myRootDescriptor;
DefaultMutableTreeNode parent = myBuilder.getRootNode();
for (int i = 1; i < path.length; i++) {
final Object pathElement = path[i];
final DefaultMutableTreeNode treeNode = parent == null ? null : myBuilder.getNodeForElement(pathElement);
if (treeNode == null || !parent.equals(treeNode.getParent())) {
result[i] = ContainerUtil.find(result[i - 1].getChildren(), new Condition<JamNodeDescriptor>() {
public boolean value(final JamNodeDescriptor object) {
return corresponds(object, pathElement);
}
});
parent = treeNode;
}
else {
result[i] = (JamNodeDescriptor)treeNode.getUserObject();
parent = null;
}
if (result[i] == null) {
return null;
}
}
return result;
}
private static boolean corresponds(final JamNodeDescriptor object, final Object pathElement) {
return Comparing.equal(object.getElement(), pathElement);
}
private List<Object> getCachedColumnValues(final Object[] path) {
if (!myCache.containsKey(path)) {
myCache.put(path, new ArrayList<Object>(getEmptyColumnValues()));
}
return myCache.get(path);
}
protected void cacheNode(final JamNodeDescriptor node) {
final JamNodeDescriptor[] path = getDescriptorPath(node);
if (!myCache.containsKey(path)) {
myCache.put(path, getColumnValues(node));
}
}
private List<Object> getColumnValues(final JamNodeDescriptor descriptor) {
return ContainerUtil.map2List(getColumnInfos(), new Function<ColumnInfo, Object>() {
public Object fun(final ColumnInfo s) {
return s.valueOf(descriptor);
}
});
}
protected List<Object> getEmptyColumnValues() {
return Arrays.asList(new Object[getColumnInfos().length]);
}
protected ColumnInfo[] getColumnInfos() {
return myModel.getColumnInfos();
}
protected abstract boolean isShowTree();
@NotNull
protected String getEmptyPaneText() {
return "";
}
protected abstract ColumnInfo[] createColumnInfos();
@Nullable
private Object getSelectedElement() {
NodeDescriptor descriptor = getSelectedDescriptor();
return descriptor == null ? null : descriptor.getElement();
}
@Nullable
public final NodeDescriptor getSelectedDescriptor() {
final TreePath path = getSelectedPath();
if (path != null) {
Object lastPathComponent = path.getLastPathComponent();
if (lastPathComponent instanceof DefaultMutableTreeNode) {
try {
return getJamNodeDescriptor(lastPathComponent);
}
catch (LoadingNodeException e) {
}
}
}
return null;
}
@Nullable
private TreePath getSelectedPath() {
final TreePath[] paths = getTree() == null ? null : getTree().getSelectionPaths();
return paths != null && paths.length == 1 ? paths[0] : null;
}
public Object getData(String dataId) {
if (DataConstants.PSI_ELEMENT.equals(dataId)) {
Object element = getSelectedElement();
return element instanceof PsiElement && ((PsiElement)element).isValid() ? element : null;
}
else if (DataConstants.PSI_FILE.equals(dataId)) {
Object element = getSelectedElement();
return element instanceof PsiFile ? element : null;
}
else if (DataConstants.PROJECT.equals(dataId)) {
return myProject;
}
NodeDescriptor descriptor = getSelectedDescriptor();
if (descriptor instanceof JamNodeDescriptor) {
return ((JamNodeDescriptor)descriptor).getDataForElement(dataId);
}
return null;
}
protected void setCachedValues(final Object value, final Collection<Integer> columns, final Object... url) {
for (final int column : columns) {
setCachedValue(value, column, url);
}
}
private class PanelToViewDataDelegator extends JPanel implements DataProvider {
public Object getData(String dataId) {
return JamTreeTableView.this.getData(dataId);
}
}
private static class MyTreeTableView extends TreeTableView {
public MyTreeTableView(final ListTreeTableModelOnColumns model) {
super(model);
}
public TreeTableCellRenderer createTableRenderer(TreeTableModel treeTableModel) {
final TreeTableCellRenderer tableRenderer = super.createTableRenderer(treeTableModel);
tableRenderer.setDefaultBorder(null);
final TreeTableTree tree = getTree();
return new TreeTableCellRenderer(this, tree) {
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
final Component component1 = tableRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
tree.setCellFocused(true);
return component1;
}
};
}
protected final Object getRowElement(final int row) {
try {
return getJamNodeDescriptor(super.getRowElement(row));
}
catch (LoadingNodeException e) {
return null;
}
}
public TableCellRenderer getCellRenderer(int row, int column) {
try {
final JamNodeDescriptor node = getJamNodeDescriptor(super.getRowElement(row));
return getColumnInfo(column).getCustomizedRenderer(node, new StripeTableCellRenderer(super.getCellRenderer(row, column)));
}
catch (LoadingNodeException e) {
return LOADING_NODE_RENDERER;
}
}
}
}