blob: 982904c9c62937fe07c79c82e3d3dee94aed7fe6 [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.ide.hierarchy;
import com.intellij.icons.AllIcons;
import com.intellij.ide.IdeBundle;
import com.intellij.ide.OccurenceNavigator;
import com.intellij.ide.OccurenceNavigatorSupport;
import com.intellij.ide.PsiCopyPasteManager;
import com.intellij.ide.dnd.DnDAction;
import com.intellij.ide.dnd.DnDDragStartBean;
import com.intellij.ide.dnd.DnDManager;
import com.intellij.ide.dnd.DnDSource;
import com.intellij.ide.dnd.aware.DnDAwareTree;
import com.intellij.ide.projectView.impl.ProjectViewTree;
import com.intellij.ide.dnd.TransferableWrapper;
import com.intellij.ide.util.scopeChooser.EditScopesDialog;
import com.intellij.ide.util.treeView.NodeDescriptor;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.ComboBoxAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.Navigatable;
import com.intellij.psi.*;
import com.intellij.psi.search.scope.packageSet.NamedScope;
import com.intellij.psi.search.scope.packageSet.NamedScopesHolder;
import com.intellij.ui.ScreenUtil;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.util.Alarm;
import com.intellij.util.EditSourceOnDoubleClickHandler;
import com.intellij.util.NullableFunction;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import java.awt.*;
import java.io.File;
import java.text.MessageFormat;
import java.util.*;
import java.util.List;
public abstract class HierarchyBrowserBaseEx extends HierarchyBrowserBase implements OccurenceNavigator {
private static final Logger LOG = Logger.getInstance("#com.intellij.ide.hierarchy.HierarchyBrowserBaseEx");
@NonNls private static final String HELP_ID = "reference.toolWindows.hierarchy";
protected final Hashtable<String, HierarchyTreeBuilder> myBuilders = new Hashtable<String, HierarchyTreeBuilder>();
protected final Hashtable<String, JTree> myType2TreeMap = new Hashtable<String, JTree>();
private final RefreshAction myRefreshAction = new RefreshAction();
private final Alarm myAlarm = new Alarm(Alarm.ThreadToUse.SHARED_THREAD);
private SmartPsiElementPointer mySmartPsiElementPointer;
private final CardLayout myCardLayout;
private final JPanel myTreePanel;
protected String myCurrentViewType;
private boolean myCachedIsValidBase = false;
private final HashMap<String, OccurenceNavigator> myOccurrenceNavigators = new HashMap<String, OccurenceNavigator>();
private static final OccurenceNavigator EMPTY_NAVIGATOR = new OccurenceNavigator() {
@Override
public boolean hasNextOccurence() {
return false;
}
@Override
public boolean hasPreviousOccurence() {
return false;
}
@Override
public OccurenceInfo goNextOccurence() {
return null;
}
@Override
public OccurenceInfo goPreviousOccurence() {
return null;
}
@Override
public String getNextOccurenceActionName() {
return "";
}
@Override
public String getPreviousOccurenceActionName() {
return "";
}
};
public static final String SCOPE_PROJECT = IdeBundle.message("hierarchy.scope.project");
public static final String SCOPE_ALL = IdeBundle.message("hierarchy.scope.all");
public static final String SCOPE_TEST = IdeBundle.message("hierarchy.scope.test");
public static final String SCOPE_CLASS = IdeBundle.message("hierarchy.scope.this.class");
protected final Map<String, String> myType2ScopeMap = new HashMap<String, String>();
public HierarchyBrowserBaseEx(@NotNull Project project, @NotNull PsiElement element) {
super(project);
setHierarchyBase(element);
myCardLayout = new CardLayout();
myTreePanel = new JPanel(myCardLayout);
createTrees(myType2TreeMap);
final HierarchyBrowserManager.State state = HierarchyBrowserManager.getInstance(project).getState();
for (String type : myType2TreeMap.keySet()) {
myType2ScopeMap.put(type, state.SCOPE != null ? state.SCOPE : SCOPE_ALL);
}
final Enumeration<String> keys = myType2TreeMap.keys();
while (keys.hasMoreElements()) {
final String key = keys.nextElement();
final JTree tree = myType2TreeMap.get(key);
myOccurrenceNavigators.put(key, new OccurenceNavigatorSupport(tree) {
@Override
@Nullable
protected Navigatable createDescriptorForNode(DefaultMutableTreeNode node) {
final HierarchyNodeDescriptor descriptor = getDescriptor(node);
if (descriptor == null) return null;
PsiElement psiElement = getOpenFileElementFromDescriptor(descriptor);
if (psiElement == null || !psiElement.isValid()) return null;
final VirtualFile virtualFile = psiElement.getContainingFile().getVirtualFile();
if (virtualFile == null) return null;
return new OpenFileDescriptor(psiElement.getProject(), virtualFile, psiElement.getTextOffset());
}
@Override
public String getNextOccurenceActionName() {
return getNextOccurenceActionNameImpl();
}
@Override
public String getPreviousOccurenceActionName() {
return getPrevOccurenceActionNameImpl();
}
});
myTreePanel.add(ScrollPaneFactory.createScrollPane(tree), key);
}
final JPanel legendPanel = createLegendPanel();
final JPanel contentPanel;
if (legendPanel != null) {
contentPanel = new JPanel(new BorderLayout());
contentPanel.add(myTreePanel, BorderLayout.CENTER);
contentPanel.add(legendPanel, BorderLayout.SOUTH);
}
else {
contentPanel = myTreePanel;
}
buildUi(createToolbar(getActionPlace(), HELP_ID).getComponent(), contentPanel);
}
@Nullable
protected PsiElement getOpenFileElementFromDescriptor(@NotNull HierarchyNodeDescriptor descriptor) {
return getElementFromDescriptor(descriptor);
}
@Override
@Nullable
protected abstract PsiElement getElementFromDescriptor(@NotNull HierarchyNodeDescriptor descriptor);
@NotNull
protected abstract String getPrevOccurenceActionNameImpl();
@NotNull
protected abstract String getNextOccurenceActionNameImpl();
protected abstract void createTrees(@NotNull Map<String, JTree> trees);
@Nullable
protected abstract JPanel createLegendPanel();
protected abstract boolean isApplicableElement(@NotNull PsiElement element);
@Nullable
protected abstract HierarchyTreeStructure createHierarchyTreeStructure(@NotNull String type, @NotNull PsiElement psiElement);
@Nullable
protected abstract Comparator<NodeDescriptor> getComparator();
@NotNull
protected abstract String getActionPlace();
@NotNull
protected abstract String getBrowserDataKey();
protected final JTree createTree(boolean dndAware) {
final Tree tree;
final NullableFunction<Object, PsiElement> toPsiConverter = new NullableFunction<Object, PsiElement>() {
@Override
public PsiElement fun(Object o) {
if (o instanceof HierarchyNodeDescriptor) {
return ((HierarchyNodeDescriptor)o).getContainingFile();
}
return null;
}
};
if (dndAware) {
tree = new DnDAwareTree(new DefaultTreeModel(new DefaultMutableTreeNode(""))) {
@Override
public void removeNotify() {
super.removeNotify();
if (ScreenUtil.isStandardAddRemoveNotify(this))
myRefreshAction.unregisterCustomShortcutSet(this);
}
@Override
public boolean isFileColorsEnabled() {
return ProjectViewTree.isFileColorsEnabledFor(this);
}
@Override
public Color getFileColorFor(Object object) {
return ProjectViewTree.getColorForObject(object, myProject, toPsiConverter);
}
};
if (!ApplicationManager.getApplication().isHeadlessEnvironment()) {
DnDManager.getInstance().registerSource(new DnDSource() {
@Override
public boolean canStartDragging(final DnDAction action, final Point dragOrigin) {
return getSelectedElements().length > 0;
}
@Override
public DnDDragStartBean startDragging(final DnDAction action, final Point dragOrigin) {
return new DnDDragStartBean(new TransferableWrapper() {
@Override
public TreeNode[] getTreeNodes() {
return tree.getSelectedNodes(TreeNode.class, null);
}
@Override
public PsiElement[] getPsiElements() {
return getSelectedElements();
}
@Override
public List<File> asFileList() {
return PsiCopyPasteManager.asFileList(getPsiElements());
}
});
}
@Override
public Pair<Image, Point> createDraggedImage(final DnDAction action, final Point dragOrigin) {
return null;
}
@Override
public void dragDropEnd() {
}
@Override
public void dropActionChanged(final int gestureModifiers) {
}
}, tree);
}
}
else {
tree = new Tree(new DefaultTreeModel(new DefaultMutableTreeNode(""))) {
@Override
public void removeNotify() {
super.removeNotify();
if (ScreenUtil.isStandardAddRemoveNotify(this))
myRefreshAction.unregisterCustomShortcutSet(this);
}
@Override
public boolean isFileColorsEnabled() {
return ProjectViewTree.isFileColorsEnabledFor(this);
}
@Override
public Color getFileColorFor(Object object) {
return ProjectViewTree.getColorForObject(object, myProject, toPsiConverter);
}
};
}
configureTree(tree);
EditSourceOnDoubleClickHandler.install(tree);
myRefreshAction.registerShortcutOn(tree);
return tree;
}
protected void setHierarchyBase(@NotNull PsiElement element) {
mySmartPsiElementPointer = SmartPointerManager.getInstance(myProject).createSmartPsiElementPointer(element);
}
private void restoreCursor() {
myAlarm.cancelAllRequests();
setCursor(Cursor.getDefaultCursor());
}
private void setWaitCursor() {
myAlarm.addRequest(new Runnable() {
@Override
public void run() {
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
}
}, 100);
}
public final void changeView(@NotNull final String typeName) {
myCurrentViewType = typeName;
final PsiElement element = mySmartPsiElementPointer.getElement();
if (element == null || !isApplicableElement(element)) {
return;
}
if (myContent != null) {
final String displayName = getContentDisplayName(typeName, element);
if (displayName != null) {
myContent.setDisplayName(displayName);
}
}
myCardLayout.show(myTreePanel, typeName);
if (!myBuilders.containsKey(typeName)) {
try {
setWaitCursor();
// create builder
final JTree tree = myType2TreeMap.get(typeName);
final DefaultTreeModel model = new DefaultTreeModel(new DefaultMutableTreeNode(""));
tree.setModel(model);
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
final HierarchyTreeStructure structure = createHierarchyTreeStructure(typeName, element);
if (structure == null) {
return;
}
final Comparator<NodeDescriptor> comparator = getComparator();
final HierarchyTreeBuilder builder = new HierarchyTreeBuilder(myProject, tree, model, structure, comparator);
myBuilders.put(typeName, builder);
Disposer.register(this, builder);
final HierarchyNodeDescriptor descriptor = structure.getBaseDescriptor();
builder.select(descriptor, new Runnable() {
@Override
public void run() {
builder.expand(descriptor, null);
}
});
}
finally {
restoreCursor();
}
}
getCurrentTree().requestFocus();
}
@Nullable
protected String getContentDisplayName(@NotNull String typeName, @NotNull PsiElement element) {
if (element instanceof PsiNamedElement) {
return MessageFormat.format(typeName, ((PsiNamedElement)element).getName());
}
return null;
}
@Override
protected void appendActions(@NotNull DefaultActionGroup actionGroup, String helpID) {
prependActions(actionGroup);
actionGroup.add(myRefreshAction);
super.appendActions(actionGroup, helpID);
}
protected void prependActions(final DefaultActionGroup actionGroup) {
}
@Override
public boolean hasNextOccurence() {
return getOccurrenceNavigator().hasNextOccurence();
}
private OccurenceNavigator getOccurrenceNavigator() {
if (myCurrentViewType == null) {
return EMPTY_NAVIGATOR;
}
final OccurenceNavigator navigator = myOccurrenceNavigators.get(myCurrentViewType);
return navigator != null ? navigator : EMPTY_NAVIGATOR;
}
@Override
public boolean hasPreviousOccurence() {
return getOccurrenceNavigator().hasPreviousOccurence();
}
@Override
public OccurenceInfo goNextOccurence() {
return getOccurrenceNavigator().goNextOccurence();
}
@Override
public OccurenceInfo goPreviousOccurence() {
return getOccurrenceNavigator().goPreviousOccurence();
}
@Override
public String getNextOccurenceActionName() {
return getOccurrenceNavigator().getNextOccurenceActionName();
}
@Override
public String getPreviousOccurenceActionName() {
return getOccurrenceNavigator().getPreviousOccurenceActionName();
}
@Override
protected HierarchyTreeBuilder getCurrentBuilder() {
return myBuilders.get(myCurrentViewType);
}
protected final boolean isValidBase() {
if (PsiDocumentManager.getInstance(myProject).getUncommittedDocuments().length > 0) {
return myCachedIsValidBase;
}
final PsiElement element = mySmartPsiElementPointer.getElement();
myCachedIsValidBase = element != null && isApplicableElement(element) && element.isValid();
return myCachedIsValidBase;
}
@Override
protected JTree getCurrentTree() {
if (myCurrentViewType == null) return null;
return myType2TreeMap.get(myCurrentViewType);
}
public String getCurrentViewType() {
return myCurrentViewType;
}
@Override
public Object getData(final String dataId) {
if (getBrowserDataKey().equals(dataId)) {
return this;
}
if (PlatformDataKeys.HELP_ID.is(dataId)) {
return HELP_ID;
}
return super.getData(dataId);
}
private void disposeBuilders() {
final Collection<HierarchyTreeBuilder> builders = myBuilders.values();
for (final HierarchyTreeBuilder builder : builders) {
Disposer.dispose(builder);
}
myBuilders.clear();
}
protected void doRefresh(boolean currentBuilderOnly) {
if (currentBuilderOnly) LOG.assertTrue(myCurrentViewType != null);
if (!isValidBase()) return;
if (getCurrentBuilder() == null) return; // seems like we are in the middle of refresh already
final Ref<Pair<List<Object>, List<Object>>> storedInfo = new Ref<Pair<List<Object>, List<Object>>>();
if (myCurrentViewType != null) {
final HierarchyTreeBuilder builder = getCurrentBuilder();
storedInfo.set(builder.storeExpandedAndSelectedInfo());
}
final PsiElement element = mySmartPsiElementPointer.getElement();
if (element == null || !isApplicableElement(element)) {
return;
}
final String currentViewType = myCurrentViewType;
if (currentBuilderOnly) {
Disposer.dispose(getCurrentBuilder());
myBuilders.remove(myCurrentViewType);
}
else {
disposeBuilders();
}
setHierarchyBase(element);
validate();
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
changeView(currentViewType);
final HierarchyTreeBuilder builder = getCurrentBuilder();
builder.restoreExpandedAndSelectedInfo(storedInfo.get());
}
});
}
protected String getCurrentScopeType() {
if (myCurrentViewType == null) return null;
return myType2ScopeMap.get(myCurrentViewType);
}
protected class AlphaSortAction extends ToggleAction {
public AlphaSortAction() {
super(IdeBundle.message("action.sort.alphabetically"), IdeBundle.message("action.sort.alphabetically"),
AllIcons.ObjectBrowser.Sorted);
}
@Override
public final boolean isSelected(final AnActionEvent event) {
return HierarchyBrowserManager.getInstance(myProject).getState().SORT_ALPHABETICALLY;
}
@Override
public final void setSelected(final AnActionEvent event, final boolean flag) {
final HierarchyBrowserManager hierarchyBrowserManager = HierarchyBrowserManager.getInstance(myProject);
hierarchyBrowserManager.getState().SORT_ALPHABETICALLY = flag;
final Comparator<NodeDescriptor> comparator = getComparator();
final Collection<HierarchyTreeBuilder> builders = myBuilders.values();
for (final HierarchyTreeBuilder builder : builders) {
builder.setNodeDescriptorComparator(comparator);
}
}
@Override
public final void update(final AnActionEvent event) {
super.update(event);
final Presentation presentation = event.getPresentation();
presentation.setEnabled(isValidBase());
}
}
protected static class BaseOnThisElementAction extends AnAction {
private final String myActionId;
private final String myBrowserDataKey;
public BaseOnThisElementAction(String text, String actionId, String browserDataKey) {
super(text);
myActionId = actionId;
myBrowserDataKey = browserDataKey;
}
@Override
public final void actionPerformed(final AnActionEvent event) {
final DataContext dataContext = event.getDataContext();
final HierarchyBrowserBaseEx browser = (HierarchyBrowserBaseEx)dataContext.getData(myBrowserDataKey);
if (browser == null) return;
final PsiElement selectedElement = browser.getSelectedElement();
if (selectedElement == null || !browser.isApplicableElement(selectedElement)) return;
final String currentViewType = browser.myCurrentViewType;
Disposer.dispose(browser);
browser.setHierarchyBase(selectedElement);
browser.validate();
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
browser.changeView(correctViewType(browser, currentViewType));
}
});
}
protected String correctViewType(HierarchyBrowserBaseEx browser, String viewType) {
return viewType;
}
@Override
public final void update(final AnActionEvent event) {
final Presentation presentation = event.getPresentation();
registerCustomShortcutSet(ActionManager.getInstance().getAction(myActionId).getShortcutSet(), null);
final DataContext dataContext = event.getDataContext();
final HierarchyBrowserBaseEx browser = (HierarchyBrowserBaseEx)dataContext.getData(myBrowserDataKey);
if (browser == null) {
presentation.setVisible(false);
presentation.setEnabled(false);
return;
}
presentation.setVisible(true);
final PsiElement selectedElement = browser.getSelectedElement();
if (selectedElement == null || !browser.isApplicableElement(selectedElement)) {
presentation.setEnabled(false);
presentation.setVisible(false);
return;
}
presentation.setEnabled(isEnabled(browser, selectedElement));
String nonDefaultText = getNonDefaultText(browser, selectedElement);
if (nonDefaultText != null) {
presentation.setText(nonDefaultText);
}
}
protected boolean isEnabled(@NotNull HierarchyBrowserBaseEx browser, @NotNull PsiElement element) {
return !element.equals(browser.mySmartPsiElementPointer.getElement()) && element.isValid();
}
@Nullable
protected String getNonDefaultText(@NotNull HierarchyBrowserBaseEx browser, @NotNull PsiElement element) {
return null;
}
}
private class RefreshAction extends com.intellij.ide.actions.RefreshAction {
public RefreshAction() {
super(IdeBundle.message("action.refresh"), IdeBundle.message("action.refresh"), AllIcons.Actions.Refresh);
}
@Override
public final void actionPerformed(final AnActionEvent e) {
doRefresh(false);
}
@Override
public final void update(final AnActionEvent event) {
final Presentation presentation = event.getPresentation();
presentation.setEnabled(isValidBase());
}
}
public class ChangeScopeAction extends ComboBoxAction {
@Override
public final void update(final AnActionEvent e) {
final Presentation presentation = e.getPresentation();
final Project project = CommonDataKeys.PROJECT.getData(e.getDataContext());
if (project == null) return;
presentation.setEnabled(isEnabled());
presentation.setText(getCurrentScopeType());
}
protected boolean isEnabled(){
return true;
}
@Override
@NotNull
protected final DefaultActionGroup createPopupActionGroup(final JComponent button) {
final DefaultActionGroup group = new DefaultActionGroup();
for(String name: getValidScopeNames()) {
group.add(new MenuAction(name));
}
group.add(new ConfigureScopesAction());
return group;
}
private Collection<String> getValidScopeNames() {
List<String> result = new ArrayList<String>();
result.add(SCOPE_PROJECT);
result.add(SCOPE_TEST);
result.add(SCOPE_ALL);
result.add(SCOPE_CLASS);
final NamedScopesHolder[] holders = NamedScopesHolder.getAllNamedScopeHolders(myProject);
for (NamedScopesHolder holder : holders) {
NamedScope[] scopes = holder.getEditableScopes(); //predefined scopes already included
for (NamedScope scope : scopes) {
result.add(scope.getName());
}
}
return result;
}
private void selectScope(final String scopeType) {
myType2ScopeMap.put(myCurrentViewType, scopeType);
HierarchyBrowserManager.getInstance(myProject).getState().SCOPE = scopeType;
// invokeLater is called to update state of button before long tree building operation
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
doRefresh(true); // scope is kept per type so other builders doesn't need to be refreshed
}
});
}
@Override
public final JComponent createCustomComponent(final Presentation presentation) {
final JPanel panel = new JPanel(new GridBagLayout());
panel.add(new JLabel(IdeBundle.message("label.scope")),
new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.WEST, GridBagConstraints.BOTH, new Insets(0, 5, 0, 0), 0, 0));
panel.add(super.createCustomComponent(presentation),
new GridBagConstraints(1, 0, 1, 1, 1, 1, GridBagConstraints.WEST, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
return panel;
}
private final class MenuAction extends AnAction {
private final String myScopeType;
public MenuAction(final String scopeType) {
super(scopeType);
myScopeType = scopeType;
}
@Override
public final void actionPerformed(final AnActionEvent e) {
selectScope(myScopeType);
}
}
private final class ConfigureScopesAction extends AnAction {
private ConfigureScopesAction() {
super("Configure...");
}
@Override
public void actionPerformed(AnActionEvent e) {
EditScopesDialog.showDialog(myProject, null);
if (!getValidScopeNames().contains(myType2ScopeMap.get(myCurrentViewType))) {
selectScope(SCOPE_ALL);
}
}
}
}
// will throw PCE during update when canceled
public void setProgressIndicator(@NotNull ProgressIndicator indicator) {
for (HierarchyTreeBuilder builder : myBuilders.values()) {
builder.setProgressIndicator(indicator);
}
}
}