blob: de4119791ad411d905259780b0044a61810e6b84 [file] [log] [blame]
/*
* Copyright 2000-2009 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.popup.tree;
import com.intellij.icons.AllIcons;
import com.intellij.ide.util.treeView.AlphaComparator;
import com.intellij.ide.util.treeView.NodeRenderer;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.PopupStep;
import com.intellij.openapi.ui.popup.TreePopup;
import com.intellij.openapi.ui.popup.TreePopupStep;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.popup.WizardPopup;
import com.intellij.ui.treeStructure.SimpleTree;
import com.intellij.ui.treeStructure.filtered.FilteringTreeBuilder;
import com.intellij.ui.treeStructure.filtered.FilteringTreeStructure;
import com.intellij.util.Range;
import com.intellij.util.ui.tree.TreeUtil;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
public class TreePopupImpl extends WizardPopup implements TreePopup {
private MyTree myWizardTree;
private MouseMotionListener myMouseMotionListener;
private MouseListener myMouseListener;
private final List<TreePath> mySavedExpanded = new ArrayList<TreePath>();
private TreePath mySavedSelected;
private TreePath myShowingChildPath;
private TreePath myPendingChildPath;
private FilteringTreeBuilder myBuilder;
public TreePopupImpl(JBPopup parent, @NotNull TreePopupStep aStep, Object parentValue) {
super(parent, aStep);
setParentValue(parentValue);
}
public TreePopupImpl(@NotNull TreePopupStep aStep) {
this(null, aStep, null);
}
@Override
protected JComponent createContent() {
myWizardTree = new MyTree();
myWizardTree.getAccessibleContext().setAccessibleName("WizardTree");
myBuilder = new FilteringTreeBuilder(myWizardTree, this, getTreeStep().getStructure(), AlphaComparator.INSTANCE) {
@Override
protected boolean isSelectable(final Object nodeObject) {
return getTreeStep().isSelectable(nodeObject, nodeObject);
}
};
myBuilder.updateFromRoot();
myWizardTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
Action action = myWizardTree.getActionMap().get("toggleSelectionPreserveAnchor");
if (action != null) {
action.setEnabled(false);
}
myWizardTree.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SPACE) {
toggleExpansion(myWizardTree.getAnchorSelectionPath());
}
}
});
myWizardTree.setRootVisible(getTreeStep().isRootVisible());
myWizardTree.setShowsRootHandles(true);
ToolTipManager.sharedInstance().registerComponent(myWizardTree);
myWizardTree.setCellRenderer(new MyRenderer());
myMouseMotionListener = new MyMouseMotionListener();
myMouseListener = new MyMouseListener();
registerAction("select", KeyEvent.VK_ENTER, 0, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleSelect(true, null);
}
});
registerAction("toggleExpansion", KeyEvent.VK_SPACE, 0, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleExpansion(myWizardTree.getSelectionPath());
}
});
final Action oldExpandAction = getActionMap().get("selectChild");
getActionMap().put("selectChild", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
final TreePath path = myWizardTree.getSelectionPath();
if (path != null && 0 == myWizardTree.getModel().getChildCount(path.getLastPathComponent())) {
handleSelect(false, null);
return;
}
oldExpandAction.actionPerformed(e);
}
});
final Action oldCollapseAction = getActionMap().get("selectParent");
getActionMap().put("selectParent", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
final TreePath path = myWizardTree.getSelectionPath();
if (shouldHidePopup(path)) {
goBack();
return;
}
oldCollapseAction.actionPerformed(e);
}
});
return myWizardTree;
}
private boolean shouldHidePopup(TreePath path) {
if (getParent() == null) return false;
if (path == null) return false;
if (!myWizardTree.isCollapsed(path)) return false;
if (myWizardTree.isRootVisible()) {
return path.getPathCount() == 1;
}
return path.getPathCount() == 2;
}
@Override
protected ActionMap getActionMap() {
return myWizardTree.getActionMap();
}
@Override
protected InputMap getInputMap() {
return myWizardTree.getInputMap();
}
private void addListeners() {
myWizardTree.addMouseMotionListener(myMouseMotionListener);
myWizardTree.addMouseListener(myMouseListener);
}
@Override
public void dispose() {
mySavedExpanded.clear();
final Enumeration<TreePath> expanded = myWizardTree.getExpandedDescendants(new TreePath(myWizardTree.getModel().getRoot()));
if (expanded != null) {
while (expanded.hasMoreElements()) {
mySavedExpanded.add(expanded.nextElement());
}
}
mySavedSelected = myWizardTree.getSelectionPath();
myWizardTree.removeMouseMotionListener(myMouseMotionListener);
myWizardTree.removeMouseListener(myMouseListener);
super.dispose();
}
@Override
protected boolean beforeShow() {
addListeners();
expandAll();
collapseAll();
restoreExpanded();
if (mySavedSelected != null) {
myWizardTree.setSelectionPath(mySavedSelected);
}
return super.beforeShow();
}
@Override
protected void afterShow() {
selectFirstSelectableItem();
}
// TODO: not-tested code:
private void selectFirstSelectableItem() {
for (int i = 0; i < myWizardTree.getRowCount(); i++) {
TreePath path = myWizardTree.getPathForRow(i);
if (getTreeStep().isSelectable(path.getLastPathComponent(), extractUserObject(path.getLastPathComponent()))) {
myWizardTree.setSelectionPath(path);
break;
}
}
}
private void restoreExpanded() {
if (mySavedExpanded.isEmpty()) {
expandAll();
return;
}
for (TreePath each : mySavedExpanded) {
myWizardTree.expandPath(each);
}
}
private void expandAll() {
for (int i = 0; i < myWizardTree.getRowCount(); i++) {
myWizardTree.expandRow(i);
}
}
private void collapseAll() {
int row = myWizardTree.getRowCount() - 1;
while (row > 0) {
myWizardTree.collapseRow(row);
row--;
}
}
private TreePopupStep getTreeStep() {
return (TreePopupStep) myStep;
}
private class MyMouseMotionListener extends MouseMotionAdapter {
@Override
public void mouseMoved(MouseEvent e) {
final TreePath path = getPath(e);
if (path != null) {
myWizardTree.setSelectionPath(path);
notifyParentOnChildSelection();
if (getTreeStep().isSelectable(path.getLastPathComponent(), extractUserObject(path.getLastPathComponent()))) {
myWizardTree.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
if (myPendingChildPath == null || !myPendingChildPath.equals(path)) {
myPendingChildPath = path;
restartTimer();
}
return;
}
}
myWizardTree.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
}
private TreePath getPath(MouseEvent e) {
return myWizardTree.getClosestPathForLocation(e.getPoint().x, e.getPoint().y);
}
private class MyMouseListener extends MouseAdapter {
@Override
public void mousePressed(MouseEvent e) {
final TreePath path = getPath(e);
if (path == null) {
return;
}
if (e.getButton() != MouseEvent.BUTTON1) {
return;
}
final Object selected = path.getLastPathComponent();
if (getTreeStep().isSelectable(selected, extractUserObject(selected))) {
handleSelect(true, e);
}
else {
if (!isLocationInExpandControl(myWizardTree, path, e.getPoint().x, e.getPoint().y)) {
toggleExpansion(path);
}
}
}
@Override
public void mouseReleased(MouseEvent e) {
}
}
private void toggleExpansion(TreePath path) {
if (path == null) {
return;
}
if (getTreeStep().isSelectable(path.getLastPathComponent(), extractUserObject(path.getLastPathComponent()))) {
if (myWizardTree.isExpanded(path)) {
myWizardTree.collapsePath(path);
}
else {
myWizardTree.expandPath(path);
}
}
}
private void handleSelect(boolean handleFinalChoices, MouseEvent e) {
final boolean pathIsAlreadySelected = myShowingChildPath != null && myShowingChildPath.equals(myWizardTree.getSelectionPath());
if (pathIsAlreadySelected) return;
myPendingChildPath = null;
Object selected = myWizardTree.getLastSelectedPathComponent();
if (selected != null) {
final Object userObject = extractUserObject(selected);
if (getTreeStep().isSelectable(selected, userObject)) {
disposeChildren();
final boolean hasNextStep = myStep.hasSubstep(userObject);
if (!hasNextStep && !handleFinalChoices) {
myShowingChildPath = null;
return;
}
final PopupStep queriedStep = myStep.onChosen(userObject, handleFinalChoices);
if (queriedStep == PopupStep.FINAL_CHOICE || !hasNextStep) {
setFinalRunnable(myStep.getFinalRunnable());
setOk(true);
disposeAllParents(e);
}
else {
myShowingChildPath = myWizardTree.getSelectionPath();
handleNextStep(queriedStep, myShowingChildPath);
myShowingChildPath = null;
}
}
}
}
private void handleNextStep(PopupStep nextStep, Object parentValue) {
final Rectangle pathBounds = myWizardTree.getPathBounds(myWizardTree.getSelectionPath());
final Point point = new RelativePoint(myWizardTree, new Point(getContent().getWidth() + 2, (int) pathBounds.getY())).getScreenPoint();
myChild = createPopup(this, nextStep, parentValue);
myChild.show(getContent(), point.x - STEP_X_PADDING, point.y, true);
}
private class MyRenderer extends NodeRenderer {
@Override
public void customizeCellRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
final boolean shouldPaintSelected = (getTreeStep().isSelectable(value, extractUserObject(value)) && selected) || (getTreeStep().isSelectable(value, extractUserObject(value)) && hasFocus);
final boolean shouldPaintFocus =
!getTreeStep().isSelectable(value, extractUserObject(value)) && selected || shouldPaintSelected || hasFocus;
super.customizeCellRenderer(tree, value, shouldPaintSelected, expanded, leaf, row, shouldPaintFocus);
}
}
private static boolean isLocationInExpandControl(JTree aTree, TreePath path, int mouseX, int mouseY) {
Range<Integer> box = TreeUtil.getExpandControlRange(aTree, path);
return box != null && box.isWithin(mouseX);
}
@Override
protected void process(KeyEvent aEvent) {
myWizardTree.processKeyEvent(aEvent);
}
private Object extractUserObject(Object aNode) {
Object object = ((DefaultMutableTreeNode) aNode).getUserObject();
if (object instanceof FilteringTreeStructure.FilteringNode) {
return ((FilteringTreeStructure.FilteringNode) object).getDelegate();
}
return object;
}
private class MyTree extends SimpleTree {
@Override
public void processKeyEvent(KeyEvent e) {
e.setSource(this);
super.processKeyEvent(e);
}
@Override
public Dimension getPreferredSize() {
final Dimension pref = super.getPreferredSize();
return new Dimension(pref.width + 10, pref.height);
}
@Override
protected void paintChildren(Graphics g) {
super.paintChildren(g);
Rectangle visibleRect = getVisibleRect();
int rowForLocation = getClosestRowForLocation(0, visibleRect.y);
for (int i = rowForLocation; i < rowForLocation + getVisibleRowCount() + 1; i++) {
final TreePath eachPath = getPathForRow(i);
if (eachPath == null) continue;
final Object lastPathComponent = eachPath.getLastPathComponent();
final boolean hasNextStep = getTreeStep().hasSubstep(extractUserObject(lastPathComponent));
if (!hasNextStep) continue;
Icon icon = isPathSelected(eachPath) ?
AllIcons.Icons.Ide.NextStep :
AllIcons.Icons.Ide.NextStepGrayed;
final Rectangle rec = getPathBounds(eachPath);
int x = getSize().width - icon.getIconWidth() - 1;
int y = rec.y + (rec.height - icon.getIconWidth()) / 2;
icon.paintIcon(this, g, x, y);
}
}
}
private Project getProject() {
return getTreeStep().getProject();
}
@Override
protected void onAutoSelectionTimer() {
handleSelect(false, null);
}
@Override
protected JComponent getPreferredFocusableComponent() {
return myWizardTree;
}
@Override
protected void onSpeedSearchPatternChanged() {
myBuilder.refilter();
}
@Override
protected void onChildSelectedFor(Object value) {
TreePath path = (TreePath) value;
if (myWizardTree.getSelectionPath() != path) {
myWizardTree.setSelectionPath(path);
}
}
@Override
public boolean isModalContext() {
return true;
}
}