blob: 120f07b96fcaaca4741bc8c2c54a0d6671ca9373 [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.openapi.roots.ui.configuration.dependencyAnalysis;
import com.intellij.ProjectTopics;
import com.intellij.icons.AllIcons;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.ComboBoxAction;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleType;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.roots.ModuleRootAdapter;
import com.intellij.openapi.roots.ModuleRootEvent;
import com.intellij.openapi.roots.ModuleSourceOrderEntry;
import com.intellij.openapi.roots.OrderEntry;
import com.intellij.openapi.roots.ui.CellAppearanceEx;
import com.intellij.openapi.roots.ui.OrderEntryAppearanceService;
import com.intellij.openapi.roots.ui.configuration.ProjectStructureConfigurable;
import com.intellij.openapi.ui.MasterDetailsComponent;
import com.intellij.openapi.ui.NamedConfigurable;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.*;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.util.PathUtil;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.tree.TreeUtil;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import java.awt.*;
import java.util.ArrayList;
import java.util.HashMap;
/**
* The classpath details component
*/
public class AnalyzeDependenciesComponent extends MasterDetailsComponent {
/**
* Data key for order path element
*/
public static DataKey<ModuleDependenciesAnalyzer.OrderPathElement> ORDER_PATH_ELEMENT_KEY = DataKey.create("ORDER_PATH_ELEMENT");
/**
* The module being analyzed
*/
private final Module myModule;
/**
* The settings for UI mode
*/
private final AnalyzeDependenciesSettings mySettings;
/**
* The cached analyzed classpaths for this module
*/
private final HashMap<Pair<ClasspathType, Boolean>, ModuleDependenciesAnalyzer> myClasspaths =
new HashMap<Pair<ClasspathType, Boolean>, ModuleDependenciesAnalyzer>();
/**
* The message bus connection to use
*/
private MessageBusConnection myMessageBusConnection;
/**
* The constructor
*
* @param module the module to analyze
*/
public AnalyzeDependenciesComponent(Module module) {
myModule = module;
mySettings = AnalyzeDependenciesSettings.getInstance(myModule.getProject());
initTree();
init();
getSplitter().setProportion(0.3f);
myMessageBusConnection = myModule.getProject().getMessageBus().connect();
myMessageBusConnection.subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootAdapter() {
@Override
public void rootsChanged(ModuleRootEvent event) {
myClasspaths.clear();
updateTree();
}
});
}
/**
* {@inheritDoc}
*/
@Override
public void disposeUIResources() {
if (myMessageBusConnection != null) {
myMessageBusConnection.disconnect();
}
}
/**
* Initialize components
*/
private void init() {
myTree.setCellRenderer(new ColoredTreeCellRenderer() {
@Override
public void customizeCellRenderer(JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
//if(getBackground() == null) {
// setBackground(UIUtil.getTreeTextBackground());
//}
if (value instanceof MyNode && !(value instanceof MyRootNode)) {
final MyNode node = (MyNode)value;
PathNode<?> n = (PathNode<?>)node.getUserObject();
CellAppearanceEx a = n.getAppearance(selected, node.isDisplayInBold());
a.customize(this);
}
}
});
myTree.setShowsRootHandles(false);
myTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
reloadTree();
}
/**
* {@inheritDoc}
*/
@Override
protected ArrayList<AnAction> createActions(boolean fromPopup) {
if (!fromPopup) {
ArrayList<AnAction> rc = new ArrayList<AnAction>();
rc.add(new ClasspathTypeAction());
rc.add(new SdkFilterAction());
rc.add(new UrlModeAction());
return rc;
}
else {
return super.createActions(fromPopup);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void processRemovedItems() {
// no remove action so far
}
/**
* {@inheritDoc}
*/
@Override
protected boolean wasObjectStored(Object editableObject) {
// no modifications so far
return false;
}
/**
* {@inheritDoc}
*/
@Nls
@Override
public String getDisplayName() {
return "Classpath Details";
}
/**
* Reload tree
*/
public void reloadTree() {
myRoot.removeAllChildren();
ModuleDependenciesAnalyzer a = getAnalyzer();
if (mySettings.isUrlMode()) {
for (ModuleDependenciesAnalyzer.UrlExplanation urlExplanation : a.getUrls()) {
myRoot.add(new MyNode(new UrlNode(urlExplanation)));
}
}
else {
for (ModuleDependenciesAnalyzer.OrderEntryExplanation explanation : a.getOrderEntries()) {
myRoot.add(new MyNode(new OrderEntryNode(explanation)));
}
}
((DefaultTreeModel)myTree.getModel()).reload(myRoot);
}
/**
* @return the analyzer for the current settings
*/
public ModuleDependenciesAnalyzer getAnalyzer() {
final Pair<ClasspathType, Boolean> key = Pair.create(getClasspathType(), mySettings.isSdkIncluded());
ModuleDependenciesAnalyzer a = myClasspaths.get(key);
if (a == null) {
a = new ModuleDependenciesAnalyzer(myModule, !mySettings.isTest(), !mySettings.isRuntime(), mySettings.isSdkIncluded());
myClasspaths.put(key, a);
}
return a;
}
/**
* @return the current classpath type from settings
*/
private ClasspathType getClasspathType() {
return mySettings.isRuntime() ?
(mySettings.isTest() ? ClasspathType.TEST_RUNTIME : ClasspathType.PRODUCTION_RUNTIME) :
(mySettings.isTest() ? ClasspathType.TEST_COMPILE : ClasspathType.PRODUCTION_COMPILE);
}
/**
* Schedule updating the tree
*/
void updateTree() {
// TODO make loading in the background if there will be significant delays on big projects
reloadTree();
}
/**
* The action that allows navigating to the path element
*/
static class NavigateAction extends DumbAwareAction {
/**
* The constructor
*/
NavigateAction() {
super("Navigate to ...", "Navigate to place where path element is defined", null);
}
/**
* {@inheritDoc}
*/
@Override
public void actionPerformed(AnActionEvent e) {
final Module module = e.getData(LangDataKeys.MODULE);
if (module == null) {
return;
}
final ModuleDependenciesAnalyzer.OrderPathElement element = e.getData(ORDER_PATH_ELEMENT_KEY);
if (element != null && element instanceof ModuleDependenciesAnalyzer.OrderEntryPathElement) {
final ModuleDependenciesAnalyzer.OrderEntryPathElement o = (ModuleDependenciesAnalyzer.OrderEntryPathElement)element;
final OrderEntry entry = o.entry();
final Module m = entry.getOwnerModule();
ProjectStructureConfigurable.getInstance(module.getProject()).selectOrderEntry(m, entry);
}
}
}
/**
* Base class for nodes
*
* @param <T> the actual explanation type
*/
abstract class PathNode<T extends ModuleDependenciesAnalyzer.Explanation> extends NamedConfigurable<T> implements DataProvider {
/**
* The cut off length, after which URLs are not shown (only suffix)
*/
public static final int CUTOFF_LENGTH = 80;
/**
* The explanation
*/
protected final T myExplanation;
/**
* The tree with explanation
*/
private Tree myExplanationTree;
/**
* The constructor
*
* @param explanation the wrapped explanation
*/
public PathNode(T explanation) {
myExplanation = explanation;
}
/**
* {@inheritDoc}
*/
@Override
public T getEditableObject() {
return myExplanation;
}
/**
* @return a created tree component (to be used as
*/
private JComponent createTreeComponent() {
myExplanationTree = new Tree(new DefaultTreeModel(buildTree()));
myExplanationTree.setRootVisible(false);
myExplanationTree.setCellRenderer(new ExplanationTreeRenderer());
DataManager.registerDataProvider(myExplanationTree, this);
TreeUtil.expandAll(myExplanationTree);
final NavigateAction navigateAction = new NavigateAction();
navigateAction.registerCustomShortcutSet(new CustomShortcutSet(CommonShortcuts.DOUBLE_CLICK_1.getShortcuts()[0]), myExplanationTree);
DefaultActionGroup group = new DefaultActionGroup();
group.addAction(navigateAction);
PopupHandler.installUnknownPopupHandler(myExplanationTree, group, ActionManager.getInstance());
return new JBScrollPane(myExplanationTree);
}
/**
* {@inheritDoc}
*/
@Override
public Object getData(@NonNls String dataId) {
if (CommonDataKeys.PROJECT.is(dataId)) {
return myModule.getProject();
}
if (LangDataKeys.MODULE.is(dataId)) {
return myModule;
}
TreePath selectionPath = myExplanationTree.getSelectionPath();
DefaultMutableTreeNode node = selectionPath == null ? null : (DefaultMutableTreeNode)selectionPath.getLastPathComponent();
Object o = node == null ? null : node.getUserObject();
if (o instanceof ModuleDependenciesAnalyzer.OrderPathElement) {
if (ORDER_PATH_ELEMENT_KEY.is(dataId)) {
return o;
}
}
return null;
}
/**
* Build tree for the dependencies
*
* @return a tree model
*/
private DefaultMutableTreeNode buildTree() {
DefaultMutableTreeNode root = new DefaultMutableTreeNode("ROOT");
for (ModuleDependenciesAnalyzer.OrderPath orderPath : myExplanation.paths()) {
addDependencyPath(root, orderPath, 0);
}
return root;
}
/**
* Add the dependency path
*
* @param parent the parent to which path is added
* @param orderPath the order entry path
* @param i the position in the path
*/
private void addDependencyPath(DefaultMutableTreeNode parent,
ModuleDependenciesAnalyzer.OrderPath orderPath,
int i) {
if (i >= orderPath.entries().size()) {
return;
}
ModuleDependenciesAnalyzer.OrderPathElement e = orderPath.entries().get(i);
int sz = parent.getChildCount();
DefaultMutableTreeNode n;
if (sz == 0) {
n = null;
}
else {
n = (DefaultMutableTreeNode)parent.getChildAt(sz - 1);
if (!n.getUserObject().equals(e)) {
n = null;
}
}
if (n == null) {
n = new DefaultMutableTreeNode(e);
parent.add(n);
}
addDependencyPath(n, orderPath, i + 1);
}
/**
* {@inheritDoc}
*/
@Override
public void setDisplayName(String name) {
// do nothing
}
/**
* {@inheritDoc}
*/
@Override
public JComponent createOptionsPanel() {
final JComponent tree = createTreeComponent();
JPanel panel = new JPanel(new BorderLayout());
JLabel paths = new JLabel("Available Through Paths:");
paths.setDisplayedMnemonic('P');
paths.setLabelFor(tree);
panel.add(paths, BorderLayout.NORTH);
panel.add(tree, BorderLayout.CENTER);
return panel;
}
/**
* {@inheritDoc}
*/
@Override
public void disposeUIResources() {
//Do nothing
}
/**
* {@inheritDoc}
*/
@Override
public String getHelpTopic() {
return null;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isModified() {
return false;
}
/**
* {@inheritDoc}
*/
@Override
public void apply() throws ConfigurationException {
// Do nothing
}
/**
* {@inheritDoc}
*/
@Override
public void reset() {
//Do nothing
}
/**
* Get appearance for rendering in master list
*
* @param selected true if selected
* @param bold true if bold
* @return the result appearance
*/
public abstract CellAppearanceEx getAppearance(boolean selected, boolean bold);
/**
* @retrun the string cut so it would fit the banner (the prefix is dropped)
*/
protected String suffixForBanner(String p) {
if (p.length() > CUTOFF_LENGTH) {
p = "..." + p.substring(p.length() - CUTOFF_LENGTH);
}
return p;
}
/**
* @retrun the string cut so it would fit the banner (the suffix is dropped)
*/
protected String prefixForBanner(String p) {
if (p.length() > CUTOFF_LENGTH) {
p = p.substring(0, CUTOFF_LENGTH) + "...";
}
return p;
}
}
/**
* Cell renderer for explanation tree
*/
static class ExplanationTreeRenderer extends ColoredTreeCellRenderer {
@Override
public void customizeCellRenderer(JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
DefaultMutableTreeNode n = (DefaultMutableTreeNode)value;
final Object userObject = n.getUserObject();
if (!(userObject instanceof ModuleDependenciesAnalyzer.OrderPathElement)) {
return;
}
ModuleDependenciesAnalyzer.OrderPathElement e = (ModuleDependenciesAnalyzer.OrderPathElement)userObject;
final CellAppearanceEx appearance = e.getAppearance(selected);
appearance.customize(this);
}
}
/**
* The entry node in URL node
*/
class UrlNode extends PathNode<ModuleDependenciesAnalyzer.UrlExplanation> {
/**
* The constructor
*
* @param url the wrapped explanation
*/
public UrlNode(ModuleDependenciesAnalyzer.UrlExplanation url) {
super(url);
setNameFieldShown(false);
}
/**
* {@inheritDoc}
*/
@Override
public CellAppearanceEx getAppearance(boolean selected, final boolean isBold) {
return new CellAppearanceEx() {
@Override
public void customize(@NotNull SimpleColoredComponent component) {
component.setIcon(getIcon());
final Font font = UIUtil.getTreeFont();
if (isBold) {
component.setFont(font.deriveFont(Font.BOLD));
}
else {
component.setFont(font.deriveFont(Font.PLAIN));
}
final String p = PathUtil.toPresentableUrl(getEditableObject().url());
component.append(PathUtil.getFileName(p),
isBold ? SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES : SimpleTextAttributes.REGULAR_ATTRIBUTES);
component.append(" (" + PathUtil.getParentPath(p) + ")", SimpleTextAttributes.GRAYED_ATTRIBUTES);
}
@Override
public void customize(@NotNull final HtmlListCellRenderer renderer) {
throw new UnsupportedOperationException("Rendering in combo box not supported yet.");
}
@NotNull
@Override
public String getText() {
return getDisplayName();
}
};
}
/**
* {@inheritDoc}
*/
@Override
public String getBannerSlogan() {
final VirtualFile f = myExplanation.getLocalFile();
String p = f == null ? myExplanation.url() : f.getPath();
p = suffixForBanner(p);
return p;
}
/**
* {@inheritDoc}
*/
private Icon getIcon() {
return myExplanation.getIcon();
}
/**
* {@inheritDoc}
*/
@Nls
@Override
public String getDisplayName() {
return myExplanation.url();
}
}
/**
* The wrapper for order entries
*/
class OrderEntryNode extends PathNode<ModuleDependenciesAnalyzer.OrderEntryExplanation> {
/**
* The constructor
*
* @param orderEntryExplanation the explanation to wrap
*/
public OrderEntryNode(ModuleDependenciesAnalyzer.OrderEntryExplanation orderEntryExplanation) {
super(orderEntryExplanation);
setNameFieldShown(false);
}
/**
* {@inheritDoc}
*/
@Override
public CellAppearanceEx getAppearance(boolean selected, final boolean isBold) {
if (myExplanation.entry() instanceof ModuleSourceOrderEntry) {
ModuleSourceOrderEntry e = (ModuleSourceOrderEntry)myExplanation.entry();
if (e.getOwnerModule() == myModule) {
return new CellAppearanceEx() {
@Override
public void customize(@NotNull SimpleColoredComponent component) {
component.setIcon(ModuleType.get(myModule).getIcon());
component.append("<This Module>", SimpleTextAttributes.SYNTHETIC_ATTRIBUTES);
}
@Override
public void customize(@NotNull final HtmlListCellRenderer renderer) {
throw new UnsupportedOperationException("Rendering in combo box not supported yet.");
}
@NotNull
@Override
public String getText() {
return "<This Module>";
}
};
}
else {
return OrderEntryAppearanceService.getInstance().forModule(e.getOwnerModule());
}
}
return OrderEntryAppearanceService.getInstance().forOrderEntry(myModule.getProject(), myExplanation.entry(), selected);
}
/**
* {@inheritDoc}
*/
@Override
public String getBannerSlogan() {
if (myExplanation.entry() instanceof ModuleSourceOrderEntry) {
ModuleSourceOrderEntry e = (ModuleSourceOrderEntry)myExplanation.entry();
return prefixForBanner("Module " + e.getOwnerModule().getName());
}
else {
final String p = myExplanation.entry().getPresentableName() + " in module " + myExplanation.entry().getOwnerModule().getName();
return suffixForBanner(p);
}
}
/**
* {@inheritDoc}
*/
@Nls
@Override
public String getDisplayName() {
return myExplanation.entry().getPresentableName();
}
}
/**
* The action that allows including and excluding SDK entries from analysis
*/
private class SdkFilterAction extends ToggleAction {
/**
* The constructor
*/
public SdkFilterAction() {
super("Include SDK", "If selected, the SDK classes are included", AllIcons.General.Jdk);
}
/**
* {@inheritDoc}
*/
@Override
public boolean isSelected(AnActionEvent e) {
return mySettings.isSdkIncluded();
}
/**
* {@inheritDoc}
*/
@Override
public void setSelected(AnActionEvent e, boolean state) {
mySettings.setIncludeSdk(state);
updateTree();
}
}
/**
* The action that allows switching class path between URL and order entry modes
*/
private class UrlModeAction extends ToggleAction {
/**
* The constructor
*/
public UrlModeAction() {
super("Use URL mode", "If selected, the URLs are displayed, otherwise order entries", AllIcons.Nodes.PpFile);
}
/**
* {@inheritDoc}
*/
@Override
public boolean isSelected(AnActionEvent e) {
return mySettings.isUrlMode();
}
/**
* {@inheritDoc}
*/
@Override
public void setSelected(AnActionEvent e, boolean state) {
mySettings.setUrlMode(state);
updateTree();
}
}
/**
* Classpath type action for the analyze classpath
*/
private class ClasspathTypeAction extends ComboBoxAction {
/**
* The filter action group
*/
DefaultActionGroup myItems;
/**
* {@inheritDoc}
*/
@NotNull
@Override
protected DefaultActionGroup createPopupActionGroup(JComponent button) {
if (myItems == null) {
myItems = new DefaultActionGroup(null, true);
for (final ClasspathType classpathType : ClasspathType.values()) {
myItems.addAction(new DumbAwareAction(classpathType.getDescription()) {
@Override
public void actionPerformed(AnActionEvent e) {
mySettings.setRuntime(classpathType.isRuntime());
mySettings.setTest(classpathType.isTest());
updateTree();
}
});
}
}
return myItems;
}
/**
* {@inheritDoc}
*/
@Override
public void update(AnActionEvent e) {
final Presentation presentation = e.getPresentation();
updateText(presentation);
}
/**
* Update the text for the combobox
*
* @param presentation the presentaiton to update
*/
private void updateText(Presentation presentation) {
final ClasspathType classpathType = getClasspathType();
String t = classpathType.getDescription();
presentation.setText(t);
}
}
/**
* The enumeration type that represents classpath entry filter
*/
private static enum ClasspathType {
/**
* The production compile mode
*/
PRODUCTION_COMPILE(false, false, "Production Compile"),
/**
* The production runtime mode
*/
PRODUCTION_RUNTIME(false, true, "Production Runtime"),
/**
* The test runtime mode
*/
TEST_RUNTIME(true, true, "Test Runtime"),
/**
* The test compile mode
*/
TEST_COMPILE(true, false, "Test Compile");
/**
* true, if test mode
*/
final private boolean myIsTest;
/**
* true, if runtime mode
*/
final private boolean myIsRuntime;
/**
* The description text
*/
final private String myDescription;
/**
* The constructor
*
* @param isTest true if the test mode
* @param isRuntime true if the runtime ode
* @param description the description text
*/
ClasspathType(boolean isTest, boolean isRuntime, String description) {
myIsTest = isTest;
myIsRuntime = isRuntime;
myDescription = description;
}
/**
* @return true if the test mode
*/
public boolean isTest() {
return myIsTest;
}
/**
* @return true if the runtime mode
*/
public boolean isRuntime() {
return myIsRuntime;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return myDescription;
}
/**
* @return the description for the entry
*/
public String getDescription() {
return myDescription;
}
}
}