blob: 4ce2384597ae25518bdaad9c9cd760099a43d8a3 [file] [log] [blame]
/*
* Copyright 2000-2012 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.ui.treeStructure;
import com.intellij.ide.util.treeView.AbstractTreeBuilder;
import com.intellij.ide.util.treeView.NodeRenderer;
import com.intellij.ide.util.treeView.TreeVisitor;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionPopupMenu;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.ui.TreeUIHelper;
import com.intellij.util.ui.EmptyIcon;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.tree.TreeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.text.Position;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;
public class SimpleTree extends Tree implements CellEditorListener {
private static final SimpleNode NULL_NODE = new NullNode();
private static final int INVALID = -1;
private ActionGroup myPopupGroup;
private String myPlace;
private JComponent myEditorComponent;
private boolean myEscapePressed;
private int myEditingRow;
private boolean myIgnoreSelectionChange;
private int myMinHeightInRows = 5;
private Icon myExpandedHandle;
private Icon myCollapsedHandle;
private Icon myEmptyHandle;
public SimpleTree() {
setModel(new DefaultTreeModel(new PatchedDefaultMutableTreeNode()));
TreeUtil.installActions(this);
configureUiHelper(TreeUIHelper.getInstance());
addMouseListener(new MyMouseListener());
setCellRenderer(new NodeRenderer());
setEditable(false);
getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
ToolTipManager.sharedInstance().registerComponent(this);
getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
public void valueChanged(TreeSelectionEvent e) {
if (!myIgnoreSelectionChange && hasSingleSelection()) {
getNodeFor(getSelectionPath()).handleSelection(SimpleTree.this);
}
}
});
addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER && hasSingleSelection()) {
handleDoubleClickOrEnter(getSelectionPath(), e);
}
if (e.getKeyCode() == KeyEvent.VK_F2 && e.getModifiers() == 0) {
e.consume(); // ignore start editing by F2
}
}
});
UIUtil.setLineStyleAngled(this);
if (SystemInfo.isWindows && !SystemInfo.isWinVistaOrNewer) {
setUI(new BasicTreeUI()); // In WindowsXP UI handles are not shown :(
}
setOpaque(UIUtil.isUnderGTKLookAndFeel());
}
public SimpleTree(TreeModel aModel) {
this();
setModel(aModel);
}
protected void configureUiHelper(final TreeUIHelper helper) {
helper.installTreeSpeedSearch(this);
}
@Override
public TreePath getNextMatch(final String prefix, final int startingRow, final Position.Bias bias) {
// turn JTree Speed Search off
return null;
}
public boolean accept(AbstractTreeBuilder builder, final SimpleNodeVisitor visitor) {
return builder.accept(SimpleNode.class, new TreeVisitor<SimpleNode>() {
public boolean visit(@NotNull SimpleNode node) {
return visitor.accept(node);
}
}) != null;
}
public void setPopupGroup(ActionGroup aPopupGroup, String aPlace) {
myPopupGroup = aPopupGroup;
myPlace = aPlace;
}
public SimpleNode getNodeFor(int row) {
return getNodeFor(getPathForRow(row));
}
public SimpleNode getNodeFor(TreePath aPath) {
if (aPath == null) {
return NULL_NODE;
}
DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode)aPath.getLastPathComponent();
if (treeNode == null) {
return NULL_NODE;
}
final Object userObject = treeNode.getUserObject();
if (userObject instanceof SimpleNode) {
return (SimpleNode)userObject;
}
else {
return NULL_NODE;
}
}
@Nullable
public TreePath getPathFor(SimpleNode node) {
final TreeNode nodeWithObject = TreeUtil.findNodeWithObject((DefaultMutableTreeNode)getModel().getRoot(), node);
if (nodeWithObject != null) {
return TreeUtil.getPathFromRoot(nodeWithObject);
}
return null;
}
@Nullable
public SimpleNode getSelectedNode() {
if (isSelectionEmpty()) {
return null;
}
return getNodeFor(getSelectionPath());
}
public boolean isSelectionEmpty() {
final TreePath selection = super.getSelectionPath();
return selection == null || getNodeFor(selection) == NULL_NODE;
}
public SimpleNode[] getSelectedNodesIfUniform() {
List<SimpleNode> result = new ArrayList<SimpleNode>();
TreePath[] selectionPaths = getSelectionPaths();
if (selectionPaths != null) {
SimpleNode lastNode = null;
for (TreePath selectionPath : selectionPaths) {
SimpleNode nodeFor = getNodeFor(selectionPath);
if (lastNode != null && lastNode.getClass() != nodeFor.getClass()) {
return new SimpleNode[0];
}
result.add(nodeFor);
lastNode = nodeFor;
}
}
return result.toArray(new SimpleNode[result.size()]);
}
public void setSelectedNode(AbstractTreeBuilder builder, SimpleNode node, boolean expand) {
builder.select(node.getElement(), null, false);
}
protected void paintChildren(Graphics g) {
super.paintChildren(g);
g.setColor(UIManager.getColor("Tree.line"));
for (int row = 0; row < getRowCount(); row++) {
final TreePath path = getPathForRow(row);
if (!getNodeFor(path).shouldHaveSeparator()) {
continue;
}
final Rectangle bounds = getRowBounds(row);
int x = (int)bounds.getMaxX();
int y = (int)(bounds.getY() + bounds.height / 2);
g.drawLine(x, y, getWidth() - 5, y);
}
}
public void doClick(int row) {
setSelectionRow(row);
}
// From FTree:
public void cancelEditing() {
if (isEditing()) {
cellEditor.cancelCellEditing();
doStopEditing();
}
}
public void editingStopped(ChangeEvent e) {
doStopEditing();
}
public void editingCanceled(ChangeEvent e) {
doStopEditing();
}
public JComponent getEditorComponent() {
return myEditorComponent;
}
public boolean isEditing() {
return myEditorComponent != null;
}
public TreePath getEditingPath() {
if (isEditing()) {
return getPathForRow(myEditingRow);
}
return super.getEditingPath();
}
public boolean isPathEditable(TreePath path) {
return true;
}
@Override
public final boolean isFileColorsEnabled() {
return false;
}
@Override
protected boolean paintNodes() {
return true;
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (isEditing()) {
Rectangle editedNodeRect = getRowBounds(myEditingRow);
if (editedNodeRect == null) return;
g.setColor(getBackground());
g.fillRect(editedNodeRect.x, editedNodeRect.y, editedNodeRect.width, editedNodeRect.height);
}
}
public void setCellEditor(TreeCellEditor aCellEditor) {
if (cellEditor != null) {
cellEditor.removeCellEditorListener(this);
}
super.setCellEditor(aCellEditor);
if (cellEditor != null) {
cellEditor.addCellEditorListener(this);
}
}
public boolean stopEditing() {
boolean result = isEditing();
if (result) {
if (!cellEditor.stopCellEditing()) {
cellEditor.cancelCellEditing();
}
doStopEditing();
}
return result;
}
public void startEditingAtPath(final TreePath path) {
if (path != null && isVisible(path)) {
if (isEditing() && !stopEditing()) {
return;
}
startEditing(path);
}
}
private void startEditing(final TreePath path) {
CellEditor editor = getCellEditor();
if (editor != null && editor.isCellEditable(null) && isPathEditable(path)) {
getSelectionModel().clearSelection();
getSelectionModel().setSelectionPath(path);
myEditingRow = getRowForPath(path);
myEditorComponent = (JComponent)getCellEditor()
.getTreeCellEditorComponent(this, path.getLastPathComponent(), isPathSelected(path), isExpanded(path),
treeModel.isLeaf(path.getLastPathComponent()), myEditingRow);
putEditor(path);
if (myEditorComponent.isFocusable()) {
myEditorComponent.requestFocusInWindow();
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
scrollPathToVisible(path);
}
});
}
}
private void putEditor(TreePath path) {
add(myEditorComponent);
Rectangle nodeBounds = getPathBounds(path);
Dimension editorPrefSize = myEditorComponent.getPreferredSize();
if (editorPrefSize.height > nodeBounds.height) {
nodeBounds.y -= (editorPrefSize.height - nodeBounds.height) / 2;
nodeBounds.height = editorPrefSize.height;
}
myEditorComponent.setBounds(nodeBounds);
myEscapePressed = false;
}
private void doStopEditing() {
if (isEditing()) {
remove(myEditorComponent);
myEditorComponent = null;
setSelectionRow(myEditingRow);
myEditingRow = INVALID;
repaint();
}
}
public boolean isEscapePressed() {
return myEscapePressed;
}
public void setEscapePressed() {
myEscapePressed = true;
}
public void addSelectionPath(TreePath path) {
myIgnoreSelectionChange = true;
super.addSelectionPath(path);
myIgnoreSelectionChange = false;
}
public void addSelectionPaths(TreePath[] path) {
myIgnoreSelectionChange = true;
super.addSelectionPaths(path);
myIgnoreSelectionChange = false;
}
private boolean isSelected(TreePath path) {
TreePath[] selectionPaths = getSelectionPaths();
if (selectionPaths != null) {
for (TreePath selectionPath : selectionPaths) {
if (path.equals(selectionPath)) {
return true;
}
}
}
return false;
}
public boolean isMultipleSelection() {
return getSelectionRows() != null && getSelectionRows().length > 1;
}
private void handleDoubleClickOrEnter(final TreePath treePath, final InputEvent e) {
Runnable runnable = new Runnable() {
public void run() {
getNodeFor(treePath).handleDoubleClickOrEnter(SimpleTree.this, e);
}
};
ApplicationManager.getApplication().invokeLater(runnable, ModalityState.stateForComponent(this));
}
// TODO: move to some util?
public static boolean isDoubleClick(MouseEvent e) {
return e != null && e.getClickCount() > 0 && e.getClickCount() % 2 == 0;
}
protected ActionGroup getPopupGroup() {
return myPopupGroup;
}
protected void invokeContextMenu(final MouseEvent e) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
final ActionPopupMenu menu = ActionManager.getInstance().createActionPopupMenu(myPlace, myPopupGroup);
menu.getComponent().show(e.getComponent(), e.getPoint().x, e.getPoint().y);
}
});
}
private class MyMouseListener extends MouseAdapter {
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger()) {
invokePopup(e);
}
else if (isDoubleClick(e)) {
handleDoubleClickOrEnter(getClosestPathForLocation(e.getX(), e.getY()), e);
/*
if (!TreeWizardPopupImpl.isLocationInExpandControl(SimpleTree.this, getSelectionPath(), e.getX(), e.getY())) {
TreePath treePath = getClosestPathForLocation(e.getX(), e.getY());
handleDoubleClickOrEnter(treePath, e);
}
*/
}
}
public void mouseReleased(MouseEvent e) {
invokePopup(e);
}
public void mouseClicked(MouseEvent e) {
invokePopup(e);
}
private void invokePopup(final MouseEvent e) {
if (e.isPopupTrigger() && insideTreeItemsArea(e)) {
selectPathUnderCursorIfNeeded(e);
if (myPopupGroup != null) {
invokeContextMenu(e);
}
}
}
private void selectPathUnderCursorIfNeeded(final MouseEvent e) {
TreePath pathForLocation = getClosestPathForLocation(e.getX(), e.getY());
if (!isSelected(pathForLocation)) {
setSelectionPath(pathForLocation);
}
}
private boolean insideTreeItemsArea(MouseEvent e) {
Rectangle rowBounds = getRowBounds(getRowCount() - 1);
if (rowBounds == null) {
return false;
}
double lastItemBottomLine = rowBounds.getMaxY();
return e.getY() <= lastItemBottomLine;
}
}
public boolean select(AbstractTreeBuilder aBuilder, final SimpleNodeVisitor aVisitor, boolean shouldExpand) {
return aBuilder.select(SimpleNode.class, new TreeVisitor<SimpleNode>() {
public boolean visit(@NotNull SimpleNode node) {
return aVisitor.accept(node);
}
}, null, false);
}
private void debugTree(AbstractTreeBuilder aBuilder) {
TreeUtil.traverseDepth((TreeNode)aBuilder.getTree().getModel().getRoot(), new TreeUtil.Traverse() {
public boolean accept(Object node) {
System.out.println("Node: " + node);
return true;
}
});
}
private boolean hasSingleSelection() {
return !isSelectionEmpty() && getSelectionPaths().length == 1;
}
public DefaultTreeModel getBuilderModel() {
return (DefaultTreeModel)getModel();
}
public Dimension getPreferredScrollableViewportSize() {
return super.getPreferredSize();
}
public NodeRenderer getRenderer() {
return (NodeRenderer)getCellRenderer();
}
public String toString() {
return getClass().getName() + '#' + System.identityHashCode(this);
}
public final void setMinSizeInRows(int rows) {
myMinHeightInRows = rows;
}
public Dimension getMinimumSize() {
Dimension superSize = super.getMinimumSize();
if (myMinHeightInRows == -1) return superSize;
int rowCount = getRowCount();
if (rowCount == 0) return superSize;
double rowHeight = getRowBounds(0).getHeight();
return new Dimension(superSize.width, (int)(rowHeight * myMinHeightInRows));
}
public final int getToggleClickCount() {
SimpleNode node = getSelectedNode();
if (node != null) {
if (!node.expandOnDoubleClick()) return -1;
}
return super.getToggleClickCount();
}
@Override
public void processKeyEvent(final KeyEvent e) {
super.processKeyEvent(e);
}
private int getBoxWidth(TreePath path) {
final Object root = getModel().getRoot();
if (!isRootVisible()) {
if (path.getPathCount() == 2) {
final TreePath parent = path.getParentPath();
if (parent.getLastPathComponent() == root && !getShowsRootHandles()) {
return 0;
}
}
}
return getBoxWidth(this);
}
private static int getBoxWidth(JTree tree) {
BasicTreeUI basicTreeUI = (BasicTreeUI)tree.getUI();
int boxWidth;
if (basicTreeUI.getExpandedIcon() != null) {
boxWidth = basicTreeUI.getExpandedIcon().getIconWidth();
}
else {
boxWidth = 8;
}
return boxWidth;
}
@Override
public void updateUI() {
super.updateUI();
myExpandedHandle = null;
myCollapsedHandle = null;
myExpandedHandle = null;
}
public Icon getHandleIcon(DefaultMutableTreeNode node, TreePath path) {
if (node.getChildCount() == 0) return getEmptyHandle();
return isExpanded(path) ? getExpandedHandle() : getCollapsedHandle();
}
public Icon getExpandedHandle() {
if (myExpandedHandle == null) {
myExpandedHandle = UIUtil.getTreeExpandedIcon();
}
return myExpandedHandle;
}
public Icon getCollapsedHandle() {
if (myCollapsedHandle == null) {
myCollapsedHandle = UIUtil.getTreeCollapsedIcon();
}
return myCollapsedHandle;
}
public Icon getEmptyHandle() {
if (myEmptyHandle == null) {
final Icon expand = getExpandedHandle();
myEmptyHandle = expand != null ? EmptyIcon.create(expand) : EmptyIcon.create(0);
}
return myEmptyHandle;
}
}