blob: 44ec15003de92169479a668e5148208bcbb82326 [file] [log] [blame]
/*
* Copyright 2000-2013 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.openapi.keymap.impl.ui;
import com.intellij.icons.AllIcons;
import com.intellij.ide.DataManager;
import com.intellij.ide.ui.UISettings;
import com.intellij.ide.ui.search.SearchUtil;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.QuickList;
import com.intellij.openapi.actionSystem.impl.ActionMenu;
import com.intellij.openapi.keymap.KeyMapBundle;
import com.intellij.openapi.keymap.Keymap;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.keymap.impl.KeymapImpl;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.GraphicsConfig;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.*;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.ui.treeStructure.treetable.TreeTableModel;
import com.intellij.util.ui.EmptyIcon;
import com.intellij.util.ui.GraphicsUtil;
import com.intellij.util.ui.PlatformColors;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.tree.TreeUtil;
import com.intellij.util.ui.tree.WideSelectionTreeUI;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Set;
public class ActionsTree {
private static final Icon EMPTY_ICON = EmptyIcon.ICON_18;
private static final Icon OPEN_ICON = new DefaultTreeCellRenderer().getOpenIcon();
private static final Icon CLOSE_ICON = AllIcons.Nodes.Folder;
private final JTree myTree;
private DefaultMutableTreeNode myRoot;
private final JScrollPane myComponent;
private Keymap myKeymap;
private Group myMainGroup = new Group("", null, null);
private boolean myShowBoundActions = Registry.is("keymap.show.alias.actions");
@NonNls
private static final String ROOT = "ROOT";
private String myFilter = null;
public ActionsTree() {
myRoot = new DefaultMutableTreeNode(ROOT);
myTree = new Tree(new MyModel(myRoot)) {
@Override
public void paint(Graphics g) {
super.paint(g);
Rectangle visibleRect = getVisibleRect();
Rectangle clip = g.getClipBounds();
for (int row = 0; row < getRowCount(); row++) {
Rectangle rowBounds = getRowBounds(row);
rowBounds.x = 0;
rowBounds.width = Integer.MAX_VALUE;
if (rowBounds.intersects(clip)) {
Object node = getPathForRow(row).getLastPathComponent();
if (node instanceof DefaultMutableTreeNode) {
Object data = ((DefaultMutableTreeNode)node).getUserObject();
Rectangle fullRowRect = new Rectangle(visibleRect.x, rowBounds.y, visibleRect.width, rowBounds.height);
paintRowData(this, data, fullRowRect, (Graphics2D)g);
}
}
}
}
};
myTree.setRootVisible(false);
myTree.setShowsRootHandles(true);
myTree.putClientProperty(WideSelectionTreeUI.STRIPED_CLIENT_PROPERTY, Boolean.TRUE);
myTree.setCellRenderer(new KeymapsRenderer());
myTree.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
String description = getDescription(e);
if (description != null) {
ActionMenu.showDescriptionInStatusBar(true, myTree, description);
}
else {
ActionMenu.showDescriptionInStatusBar(false, myTree, null);
}
}
@Nullable
private String getDescription(@NotNull MouseEvent e) {
TreePath path = myTree.getPathForLocation(e.getX(), e.getY());
if (path == null) return null;
DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
if (node == null) return null;
Object userObject = node.getUserObject();
if (!(userObject instanceof String)) return null;
String actionId = (String)userObject;
AnAction action = ActionManager.getInstance().getActionOrStub(actionId);
if (action == null) return null;
return action.getTemplatePresentation().getDescription();
}
});
myTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
myComponent = ScrollPaneFactory.createScrollPane(myTree,
ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
}
public JComponent getComponent() {
return myComponent;
}
public void addTreeSelectionListener(TreeSelectionListener l) {
myTree.getSelectionModel().addTreeSelectionListener(l);
}
@Nullable
private Object getSelectedObject() {
TreePath selectionPath = myTree.getSelectionPath();
if (selectionPath == null) return null;
return ((DefaultMutableTreeNode)selectionPath.getLastPathComponent()).getUserObject();
}
@Nullable
public String getSelectedActionId() {
Object userObject = getSelectedObject();
if (userObject instanceof String) return (String)userObject;
if (userObject instanceof QuickList) return ((QuickList)userObject).getActionId();
return null;
}
@Nullable
public QuickList getSelectedQuickList() {
Object userObject = getSelectedObject();
if (!(userObject instanceof QuickList)) return null;
return (QuickList)userObject;
}
public void reset(Keymap keymap, final QuickList[] allQuickLists) {
reset(keymap, allQuickLists, myFilter, null);
}
public Group getMainGroup() {
return myMainGroup;
}
public JTree getTree(){
return myTree;
}
public void filter(final String filter, final QuickList[] currentQuickListIds) {
myFilter = filter;
reset(myKeymap, currentQuickListIds, filter, null);
}
private void reset(final Keymap keymap, final QuickList[] allQuickLists, String filter, @Nullable KeyboardShortcut shortcut) {
myKeymap = keymap;
final PathsKeeper pathsKeeper = new PathsKeeper();
pathsKeeper.storePaths();
myRoot.removeAllChildren();
ActionManager actionManager = ActionManager.getInstance();
Project project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(myComponent));
Group mainGroup = ActionsTreeUtil.createMainGroup(project, myKeymap, allQuickLists, filter, true, filter != null && filter.length() > 0 ?
ActionsTreeUtil.isActionFiltered(filter, true) :
(shortcut != null ? ActionsTreeUtil.isActionFiltered(actionManager, myKeymap, shortcut) : null));
if ((filter != null && filter.length() > 0 || shortcut != null) && mainGroup.initIds().isEmpty()){
mainGroup = ActionsTreeUtil.createMainGroup(project, myKeymap, allQuickLists, filter, false, filter != null && filter.length() > 0 ?
ActionsTreeUtil.isActionFiltered(filter, false) :
ActionsTreeUtil.isActionFiltered(actionManager, myKeymap, shortcut));
}
myRoot = ActionsTreeUtil.createNode(mainGroup);
myMainGroup = mainGroup;
MyModel model = (MyModel)myTree.getModel();
model.setRoot(myRoot);
model.nodeStructureChanged(myRoot);
pathsKeeper.restorePaths();
}
public void filterTree(final KeyboardShortcut keyboardShortcut, final QuickList [] currentQuickListIds) {
reset(myKeymap, currentQuickListIds, myFilter, keyboardShortcut);
}
private class MyModel extends DefaultTreeModel implements TreeTableModel {
protected MyModel(DefaultMutableTreeNode root) {
super(root);
}
@Override
public void setTree(JTree tree) {
}
public int getColumnCount() {
return 2;
}
public String getColumnName(int column) {
switch (column) {
case 0: return KeyMapBundle.message("action.column.name");
case 1: return KeyMapBundle.message("shortcuts.column.name");
}
return "";
}
public Object getValueAt(Object value, int column) {
if (!(value instanceof DefaultMutableTreeNode)) {
return "???";
}
if (column == 0) {
return value;
}
else if (column == 1) {
Object userObject = ((DefaultMutableTreeNode)value).getUserObject();
if (userObject instanceof QuickList) {
userObject = ((QuickList)userObject).getActionId();
}
if (userObject instanceof String) {
Shortcut[] shortcuts = myKeymap.getShortcuts((String)userObject);
return KeymapUtil.getShortcutsText(shortcuts);
}
else {
return "";
}
}
else {
return "???";
}
}
public Object getChild(Object parent, int index) {
return ((TreeNode)parent).getChildAt(index);
}
public int getChildCount(Object parent) {
return ((TreeNode)parent).getChildCount();
}
public Class getColumnClass(int column) {
if (column == 0) {
return TreeTableModel.class;
}
else {
return Object.class;
}
}
public boolean isCellEditable(Object node, int column) {
return column == 0;
}
public void setValueAt(Object aValue, Object node, int column) {
}
}
private static boolean isActionChanged(String actionId, Keymap oldKeymap, Keymap newKeymap) {
if (!newKeymap.canModify()) return false;
Shortcut[] oldShortcuts = oldKeymap.getShortcuts(actionId);
Shortcut[] newShortcuts = newKeymap.getShortcuts(actionId);
return !Comparing.equal(oldShortcuts, newShortcuts);
}
private static boolean isGroupChanged(Group group, Keymap oldKeymap, Keymap newKeymap) {
if (!newKeymap.canModify()) return false;
ArrayList children = group.getChildren();
for (Object child : children) {
if (child instanceof Group) {
if (isGroupChanged((Group)child, oldKeymap, newKeymap)) {
return true;
}
}
else if (child instanceof String) {
String actionId = (String)child;
if (isActionChanged(actionId, oldKeymap, newKeymap)) {
return true;
}
}
else if (child instanceof QuickList) {
String actionId = ((QuickList)child).getActionId();
if (isActionChanged(actionId, oldKeymap, newKeymap)) {
return true;
}
}
}
return false;
}
public void selectAction(String actionId) {
final JTree tree = myTree;
String path = myMainGroup.getActionQualifiedPath(actionId);
if (path == null) {
return;
}
final DefaultMutableTreeNode node = getNodeForPath(path);
if (node == null) {
return;
}
TreeUtil.selectInTree(node, true, tree);
}
@Nullable
private DefaultMutableTreeNode getNodeForPath(String path) {
Enumeration enumeration = ((DefaultMutableTreeNode)myTree.getModel().getRoot()).preorderEnumeration();
while (enumeration.hasMoreElements()) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode)enumeration.nextElement();
if (Comparing.equal(getPath(node), path)) {
return node;
}
}
return null;
}
private ArrayList<DefaultMutableTreeNode> getNodesByPaths(ArrayList<String> paths){
final ArrayList<DefaultMutableTreeNode> result = new ArrayList<DefaultMutableTreeNode>();
Enumeration enumeration = ((DefaultMutableTreeNode)myTree.getModel().getRoot()).preorderEnumeration();
while (enumeration.hasMoreElements()) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode)enumeration.nextElement();
final String path = getPath(node);
if (paths.contains(path)) {
result.add(node);
}
}
return result;
}
@Nullable
private String getPath(DefaultMutableTreeNode node) {
final Object userObject = node.getUserObject();
if (userObject instanceof String) {
String actionId = (String)userObject;
final TreeNode parent = node.getParent();
if (parent instanceof DefaultMutableTreeNode) {
final Object object = ((DefaultMutableTreeNode)parent).getUserObject();
if (object instanceof Group) {
return ((Group)object).getActionQualifiedPath(actionId);
}
}
return myMainGroup.getActionQualifiedPath(actionId);
}
if (userObject instanceof Group) {
return ((Group)userObject).getQualifiedPath();
}
if (userObject instanceof QuickList) {
return ((QuickList)userObject).getDisplayName();
}
return null;
}
public static Icon getEvenIcon(Icon icon) {
LayeredIcon layeredIcon = new LayeredIcon(2);
layeredIcon.setIcon(EMPTY_ICON, 0);
if (icon != null && icon.getIconHeight() <= EMPTY_ICON.getIconHeight() && icon.getIconWidth() <= EMPTY_ICON.getIconWidth()) {
layeredIcon
.setIcon(icon, 1, (-icon.getIconWidth() + EMPTY_ICON.getIconWidth()) / 2, (EMPTY_ICON.getIconHeight() - icon.getIconHeight()) / 2);
}
return layeredIcon;
}
private class PathsKeeper {
private ArrayList<String> myPathsToExpand;
private ArrayList<String> mySelectionPaths;
public void storePaths() {
myPathsToExpand = new ArrayList<String>();
mySelectionPaths = new ArrayList<String>();
DefaultMutableTreeNode root = (DefaultMutableTreeNode)myTree.getModel().getRoot();
TreePath path = new TreePath(root.getPath());
if (myTree.isPathSelected(path)){
addPathToList(root, mySelectionPaths);
}
if (myTree.isExpanded(path) || root.getChildCount() == 0){
addPathToList(root, myPathsToExpand);
_storePaths(root);
}
}
private void addPathToList(DefaultMutableTreeNode root, ArrayList<String> list) {
String path = getPath(root);
if (!StringUtil.isEmpty(path)) {
list.add(path);
}
}
private void _storePaths(DefaultMutableTreeNode root) {
ArrayList<TreeNode> childNodes = childrenToArray(root);
for (final Object childNode1 : childNodes) {
DefaultMutableTreeNode childNode = (DefaultMutableTreeNode)childNode1;
TreePath path = new TreePath(childNode.getPath());
if (myTree.isPathSelected(path)) {
addPathToList(childNode, mySelectionPaths);
}
if ((myTree.isExpanded(path) || childNode.getChildCount() == 0) && !childNode.isLeaf()) {
addPathToList(childNode, myPathsToExpand);
_storePaths(childNode);
}
}
}
public void restorePaths() {
final ArrayList<DefaultMutableTreeNode> nodesToExpand = getNodesByPaths(myPathsToExpand);
for (DefaultMutableTreeNode node : nodesToExpand) {
myTree.expandPath(new TreePath(node.getPath()));
}
if (myTree.getSelectionModel().getSelectionCount() == 0) {
final ArrayList<DefaultMutableTreeNode> nodesToSelect = getNodesByPaths(mySelectionPaths);
if (!nodesToSelect.isEmpty()) {
for (DefaultMutableTreeNode node : nodesToSelect) {
TreeUtil.selectInTree(node, false, myTree);
}
}
else {
myTree.setSelectionRow(0);
}
}
}
private ArrayList<TreeNode> childrenToArray(DefaultMutableTreeNode node) {
ArrayList<TreeNode> arrayList = new ArrayList<TreeNode>();
for(int i = 0; i < node.getChildCount(); i++){
arrayList.add(node.getChildAt(i));
}
return arrayList;
}
}
private class KeymapsRenderer extends ColoredTreeCellRenderer {
// Make sure that the text rendered by this method is 'searchable' via com.intellij.openapi.keymap.impl.ui.ActionsTree.filter method.
public void customizeCellRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
final boolean showIcons = UISettings.getInstance().SHOW_ICONS_IN_MENUS;
Keymap originalKeymap = myKeymap != null ? myKeymap.getParent() : null;
Icon icon = null;
String text;
boolean bound = false;
setToolTipText(null);
if (value instanceof DefaultMutableTreeNode) {
Object userObject = ((DefaultMutableTreeNode)value).getUserObject();
boolean changed;
if (userObject instanceof Group) {
Group group = (Group)userObject;
text = group.getName();
changed = originalKeymap != null && isGroupChanged(group, originalKeymap, myKeymap);
icon = group.getIcon();
if (icon == null){
icon = CLOSE_ICON;
}
}
else if (userObject instanceof String) {
String actionId = (String)userObject;
bound = myShowBoundActions && ((KeymapImpl)myKeymap).isActionBound(actionId);
AnAction action = ActionManager.getInstance().getActionOrStub(actionId);
if (action != null) {
text = action.getTemplatePresentation().getText();
if (text == null || text.length() == 0) { //fill dynamic presentation gaps
text = actionId;
}
Icon actionIcon = action.getTemplatePresentation().getIcon();
if (actionIcon != null) {
icon = actionIcon;
}
setToolTipText(action.getTemplatePresentation().getDescription());
}
else {
text = actionId;
}
changed = originalKeymap != null && isActionChanged(actionId, originalKeymap, myKeymap);
}
else if (userObject instanceof QuickList) {
QuickList list = (QuickList)userObject;
icon = AllIcons.Actions.QuickList;
text = list.getDisplayName();
changed = originalKeymap != null && isActionChanged(list.getActionId(), originalKeymap, myKeymap);
}
else if (userObject instanceof Separator) {
// TODO[vova,anton]: beautify
changed = false;
text = "-------------";
}
else {
throw new IllegalArgumentException("unknown userObject: " + userObject);
}
if (showIcons) {
setIcon(ActionsTree.getEvenIcon(icon));
}
Color foreground;
if (selected) {
foreground = UIUtil.getTreeSelectionForeground();
}
else {
if (changed) {
foreground = PlatformColors.BLUE;
}
else {
foreground = UIUtil.getTreeForeground();
}
if (bound) {
foreground = JBColor.MAGENTA;
}
}
SearchUtil.appendFragments(myFilter, text, Font.PLAIN, foreground,
selected ? UIUtil.getTreeSelectionBackground() : UIUtil.getTreeTextBackground(), this);
}
}
}
private void paintRowData(Tree tree, Object data, Rectangle bounds, Graphics2D g) {
Shortcut[] shortcuts = null;
Set<String> abbreviations = null;
if (data instanceof String) {
final String actionId = (String)data;
shortcuts = myKeymap.getShortcuts(actionId);
abbreviations = AbbreviationManager.getInstance().getAbbreviations(actionId);
}
else if (data instanceof QuickList) {
shortcuts = myKeymap.getShortcuts(((QuickList)data).getActionId());
}
final GraphicsConfig config = GraphicsUtil.setupAAPainting(g);
int totalWidth = 0;
final FontMetrics metrics = tree.getFontMetrics(tree.getFont());
if (shortcuts != null && shortcuts.length > 0) {
for (Shortcut shortcut : shortcuts) {
totalWidth += metrics.stringWidth(KeymapUtil.getShortcutText(shortcut));
totalWidth += 10;
}
totalWidth -= 5;
int x = bounds.x + bounds.width - totalWidth;
int fontHeight = (int)metrics.getMaxCharBounds(g).getHeight();
Color c1 = new Color(234, 200, 162);
Color c2 = new Color(208, 200, 66);
g.translate(0, bounds.y - 1);
for (Shortcut shortcut : shortcuts) {
int width = metrics.stringWidth(KeymapUtil.getShortcutText(shortcut));
UIUtil.drawSearchMatch(g, x, x + width, bounds.height, c1, c2);
g.setColor(Gray._50);
g.drawString(KeymapUtil.getShortcutText(shortcut), x, fontHeight);
x += width;
x += 10;
}
g.translate(0, -bounds.y + 1);
}
if (Registry.is("actionSystem.enableAbbreviations") && abbreviations != null && abbreviations.size() > 0) {
for (String abbreviation : abbreviations) {
totalWidth += metrics.stringWidth(abbreviation);
totalWidth += 10;
}
totalWidth -= 5;
int x = bounds.x + bounds.width - totalWidth;
int fontHeight = (int)metrics.getMaxCharBounds(g).getHeight();
Color c1 = new Color(206, 234, 176);
Color c2 = new Color(126, 208, 82);
g.translate(0, bounds.y - 1);
for (String abbreviation : abbreviations) {
int width = metrics.stringWidth(abbreviation);
UIUtil.drawSearchMatch(g, x, x + width, bounds.height, c1, c2);
g.setColor(Gray._50);
g.drawString(abbreviation, x, fontHeight);
x += width;
x += 10;
}
g.translate(0, -bounds.y + 1);
}
config.restore();
}
}