/* * Copyright 2003-2016 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 jetbrains.mps.ide.findusages.view.treeholder.treeview; import com.intellij.icons.AllIcons.General; import com.intellij.ide.CommonActionsManager; import com.intellij.ide.DefaultTreeExpander; import com.intellij.ide.OccurenceNavigator; import com.intellij.ide.OccurenceNavigatorSupport; import com.intellij.ide.TreeExpander; import com.intellij.openapi.actionSystem.ActionGroup; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.ActionPlaces; import com.intellij.openapi.actionSystem.ActionToolbar; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.actionSystem.ToggleAction; import com.intellij.pom.Navigatable; import com.intellij.ui.ScrollPaneFactory; import com.intellij.usageView.UsageViewBundle; import com.intellij.util.ui.tree.TreeUtil; import jetbrains.mps.icons.MPSIcons.Actions; import jetbrains.mps.ide.findusages.CantLoadSomethingException; import jetbrains.mps.ide.findusages.CantSaveSomethingException; import jetbrains.mps.ide.findusages.model.CategoryKind; import jetbrains.mps.ide.findusages.model.SearchResults; import jetbrains.mps.ide.findusages.view.icons.IconManager; import jetbrains.mps.ide.findusages.view.icons.Icons; import jetbrains.mps.ide.findusages.view.treeholder.tree.DataTree; import jetbrains.mps.ide.findusages.view.treeholder.tree.DataTreeChangesNotifier; import jetbrains.mps.ide.findusages.view.treeholder.tree.IChangeListener; import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.AbstractResultNodeData; import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.BaseNodeData; import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.ModelNodeData; import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.ModuleNodeData; import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.NodeNodeData; import jetbrains.mps.ide.findusages.view.treeholder.treeview.UsagesTree.UsagesTreeNode; import jetbrains.mps.ide.findusages.view.treeholder.treeview.path.PathItemRole; import jetbrains.mps.ide.project.ProjectHelper; import jetbrains.mps.ide.ui.tree.TreeHighlighterExtension; import jetbrains.mps.project.Project; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jdom.Element; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SNodeReference; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.border.EmptyBorder; import javax.swing.tree.DefaultMutableTreeNode; import java.awt.BorderLayout; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; public class UsagesTreeComponent extends JPanel implements IChangeListener { private static final Logger LOG = LogManager.getLogger(UsagesTreeComponent.class); private static final String CONTENTS = "contents"; private static final String VIEW_OPTIONS = "view_options"; private static final String NODE_REPRESENTATOR = "node_representator"; private static final String CLASS_NAME = "class_name"; private final Project myProject; private INodeRepresentator myNodeRepresentator = null; private UsagesTree myTree; private final DataTree myContents; private Set<PathItemRole> myPathProvider = new HashSet<>(); private ViewToolbar myViewToolbar; private ActionsToolbar myActionsToolbar; private ViewOptions myViewOptions = new ViewOptions(); private ViewOptions myDefaultOptions; private boolean mySearchedNodesButtonsVisible = true; private boolean myAdditionalInfoButtonVisible = true; private OccurenceNavigatorSupport myOccurrenceNavigator; public UsagesTreeComponent(ViewOptions defaultOptions, Project mpsProject, DataTreeChangesNotifier changeDispatch) { super(new BorderLayout()); myProject = mpsProject; myContents = new DataTree(changeDispatch); myTree = new UsagesTree(mpsProject); myOccurrenceNavigator = new OccurenceNavigatorSupport(myTree) { @Override protected Navigatable createDescriptorForNode(DefaultMutableTreeNode node) { if (node.getChildCount() > 0) { return null; } if (!(node instanceof UsagesTreeNode)) { return null; } UsagesTreeNode treeNode = (UsagesTreeNode) node; if (treeNode.getUserObject() == null) { return null; } final BaseNodeData data = treeNode.getUserObject().getData(); return toNavigatable(data); } @Override public String getNextOccurenceActionName() { return UsageViewBundle.message("action.next.occurrence"); } @Override public String getPreviousOccurenceActionName() { return UsageViewBundle.message("action.previous.occurrence"); } }; TreeHighlighterExtension.attachHighlighters(myTree, ProjectHelper.toIdeaProject(mpsProject)); myTree.setBorder(new EmptyBorder(3, 5, 3, 5)); JScrollPane treePane = ScrollPaneFactory.createScrollPane(myTree); myPathProvider.add(PathItemRole.ROLE_MAIN_RESULTS); myPathProvider.add(PathItemRole.ROLE_TARGET_NODE); myViewToolbar = new ViewToolbar(myTree); myActionsToolbar = new ActionsToolbar(); myDefaultOptions = defaultOptions; myViewOptions.setValues(myDefaultOptions); setComponentsViewOptions(myViewOptions); add(treePane, BorderLayout.CENTER); myContents.addChangeListener(this); } public void dispose() { myContents.removeChangeListeners(this); myContents.dispose(); myTree.dispose(); } public void setContents(final SearchResults contents) { // XXX no idea if there's real need to have read action here, just refactored ModelAccess static out of DataTree here. myProject.getModelAccess().runReadAction(() -> myContents.setContents(contents, myNodeRepresentator)); } public OccurenceNavigator getOccurenceNavigator() { return myOccurrenceNavigator; } @Override public void changed() { myTree.setContents(myContents, myPathProvider); } public void addPathComponent(PathItemRole role) { if (!myPathProvider.contains(role)) { myPathProvider.add(role); } myTree.setResultPathProvider(myPathProvider); } public void removePathComponent(PathItemRole role) { myPathProvider.remove(role); myTree.setResultPathProvider(myPathProvider); } //MUST be called in construction time, introduced for "to do" functionality public void setCustomRepresentator(INodeRepresentator nodeRepresentator) { myNodeRepresentator = nodeRepresentator; myViewToolbar.recreateToolbar(); myViewToolbar.setViewOptions(myViewOptions); } public void setComponentsViewOptions(ViewOptions options) { myViewToolbar.setViewOptions(options); myActionsToolbar.setViewOptions(options); myTree.setShowPopupMenu(options.myShowPopupMenu); } public void getComponentsViewOptions(ViewOptions options) { myViewToolbar.getViewOptions(options); myActionsToolbar.getViewOptions(options); options.myShowPopupMenu = myTree.isShowPopupMenu(); } public void read(Element element, Project project) throws CantLoadSomethingException { myNodeRepresentator = null; Element nodeRepresentatorXML = element.getChild(NODE_REPRESENTATOR); if (nodeRepresentatorXML != null) { String className = nodeRepresentatorXML.getAttributeValue(CLASS_NAME); if (className != null) { try { Class nodeRepresentatorClass = Class.forName(className); myNodeRepresentator = (INodeRepresentator) nodeRepresentatorClass.newInstance(); //noinspection ConstantConditions myNodeRepresentator.read(nodeRepresentatorXML, project); } catch (Throwable t) { LOG.error("Can't instantiate node representator " + className, t); throw new CantLoadSomethingException("Can't instantiate node representator " + className, t); } } } Element viewOptionsXML = element.getChild(VIEW_OPTIONS); myViewOptions.read(viewOptionsXML, project); setComponentsViewOptions(myViewOptions); Element contentsXML = element.getChild(CONTENTS); myContents.read(contentsXML, project); myTree.setContents(myContents, myPathProvider); } public void write(Element element, Project project) throws CantSaveSomethingException { Element nodeRepresentatorXML = new Element(NODE_REPRESENTATOR); if (myNodeRepresentator != null) { nodeRepresentatorXML.setAttribute(CLASS_NAME, myNodeRepresentator.getClass().getName()); //noinspection ConstantConditions myNodeRepresentator.write(nodeRepresentatorXML, project); } element.addContent(nodeRepresentatorXML); Element viewOptionsXML = new Element(VIEW_OPTIONS); getComponentsViewOptions(myViewOptions); myViewOptions.write(viewOptionsXML, project); element.addContent(viewOptionsXML); Element contentsXML = new Element(CONTENTS); myContents.write(contentsXML, project); element.addContent(contentsXML); } public Set<SModel> getIncludedModels() { return myContents.getIncludedModels(myProject.getRepository()); } public Set<SModel> getAllModels() { return myContents.getAllModels(myProject.getRepository()); } public List<SNodeReference> getIncludedResultNodes() { return myContents.getIncludedResultNodes(); } public List<SNodeReference> getAllResultNodes() { return myContents.getAllResultNodes(); } public ActionGroup getActionsToolbar() { return myActionsToolbar.getActions(); } public JComponent getViewToolbar() { return myViewToolbar; } public UsagesTree getTree() { return myTree; } private Navigatable toNavigatable(final BaseNodeData data) { if (!(data instanceof AbstractResultNodeData)) { return null; } return new Navigatable() { @Override public void navigate(boolean requestFocus) { boolean useProjectTree = !(data instanceof NodeNodeData); if(data instanceof NodeNodeData) { // Show nodes directly in editor instead of project pane useProjectTree = false; } if(data instanceof ModelNodeData || data instanceof ModuleNodeData) { // Leave focus in UsagesView or it became unusable requestFocus = false; } ((AbstractResultNodeData) data).navigate(myProject, useProjectTree, requestFocus); } @Override public boolean canNavigate() { return true; } @Override public boolean canNavigateToSource() { return true; } }; } class ViewToolbar extends JPanel { private final JComponent myTargetComponent; private PathOptionsToolbar myPathOptionsToolbar; private ViewOptionsToolbar myViewOptionsToolbar; private JComponent myToolbar = null; public ViewToolbar(JComponent targetComponent) { myTargetComponent = targetComponent; myPathOptionsToolbar = new PathOptionsToolbar(); myViewOptionsToolbar = new ViewOptionsToolbar(); recreateToolbar(); } private void recreateToolbar() { if (myToolbar != null) { remove(myToolbar); } DefaultActionGroup actionGroup = new DefaultActionGroup(); myPathOptionsToolbar.recreateActions(); actionGroup.addAll(myPathOptionsToolbar.getActions()); actionGroup.addSeparator(); actionGroup.addAll(myViewOptionsToolbar.getActions()); ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, actionGroup, false); actionToolbar.setTargetComponent(myTargetComponent); myToolbar = actionToolbar.getComponent(); add(myToolbar); } public void setViewOptions(ViewOptions options) { myPathOptionsToolbar.setViewOptions(options); myViewOptionsToolbar.setViewOptions(options); recreateToolbar(); } public void getViewOptions(ViewOptions options) { myPathOptionsToolbar.getViewOptions(options); myViewOptionsToolbar.getViewOptions(options); } class ViewOptionsToolbar { private MyBaseToggleAction myAdditionalInfoNeededButton; private MyBaseToggleAction myShowSearchedNodesButton; private MyBaseToggleAction myGroupSearchedNodesButton; private DefaultActionGroup myActions; public ViewOptionsToolbar() { myAdditionalInfoNeededButton = new MyBaseToggleAction("Additional node info", "", General.Information) { @Override public boolean isSelected(AnActionEvent e) { return myTree.isAdditionalInfoNeeded(); } @Override public void doSetSelected(AnActionEvent e, boolean state) { myTree.setAdditionalInfoNeeded(state); } }; myShowSearchedNodesButton = new MyBaseToggleAction("Show searched nodes", "", Actions.SearchedNodes) { @Override public boolean isSelected(AnActionEvent e) { return myTree.isShowSearchedNodes(); } @Override public void doSetSelected(AnActionEvent e, boolean state) { myTree.setShowSearchedNodes(state); if (!myTree.isShowSearchedNodes() && myGroupSearchedNodesButton.isSelected(null)) { myGroupSearchedNodesButton.doSetSelected(null, false); } } }; myGroupSearchedNodesButton = new MyBaseToggleAction("Group searched nodes", "", Actions.GroupSearched) { @Override public boolean isSelected(AnActionEvent e) { return myTree.isGroupSearchedNodes(); } @Override public void doSetSelected(AnActionEvent e, boolean state) { myTree.startAdjusting(); myTree.setGroupSearchedNodes(state); if (state) { myTree.setShowSearchedNodes(true); } myTree.finishAdjusting(); } }; myActions = new DefaultActionGroup(); myActions.addAction(myAdditionalInfoNeededButton); myActions.addAction(myShowSearchedNodesButton); myActions.addAction(myGroupSearchedNodesButton); } public ActionGroup getActions() { return myActions; } public void setViewOptions(ViewOptions options) { myTree.startAdjusting(); myAdditionalInfoNeededButton.doSetSelected(null, options.myInfo); myShowSearchedNodesButton.doSetSelected(null, options.myShowSearchedNodes); myGroupSearchedNodesButton.doSetSelected(null, options.myGroupSearchedNodes); mySearchedNodesButtonsVisible = options.mySearchedNodesButtonsVisible; if (!mySearchedNodesButtonsVisible) { myActions.remove(myShowSearchedNodesButton); myActions.remove(myGroupSearchedNodesButton); } myAdditionalInfoButtonVisible = options.myAdditionalInfoButtonVisible; if (!myAdditionalInfoButtonVisible) { myActions.remove(myAdditionalInfoNeededButton); } myTree.finishAdjusting(); } public void getViewOptions(ViewOptions options) { options.myCount = true; options.myInfo = myAdditionalInfoNeededButton.isSelected(null); options.myShowSearchedNodes = myShowSearchedNodesButton.isSelected(null); options.myGroupSearchedNodes = myGroupSearchedNodesButton.isSelected(null); options.mySearchedNodesButtonsVisible = mySearchedNodesButtonsVisible; options.myAdditionalInfoButtonVisible = myAdditionalInfoButtonVisible; } } class PathOptionsToolbar { private List<MyBaseToggleAction> myCategoryPathButtons = new ArrayList<>(); private MyBaseToggleAction myModulePathButton; private MyBaseToggleAction myModelPathButton; private MyBaseToggleAction myRootPathButton; private MyBaseToggleAction myNamedConceptPathButton; private DefaultActionGroup myActions; public PathOptionsToolbar() { recreateActions(); } private void recreateActions() { List<CategoryKind> categoryKinds = Collections.singletonList( new CategoryKind(CategoryKind.DEFAULT_CATEGORY_KIND.getName(), General.Filter, CategoryKind.DEFAULT_CATEGORY_KIND.getTooltip()) ); if (myNodeRepresentator != null) { categoryKinds = ((INodeRepresentator<?>) myNodeRepresentator).getCategoryKinds(); } myCategoryPathButtons.clear(); for (CategoryKind kind : categoryKinds) { myCategoryPathButtons.add(new MyBasePathToggleAction( PathItemRole.getCategoryRole(kind), kind.getTooltip(), IconManager.getIconForCategoryKind(kind))); } myModulePathButton = new MyBasePathToggleAction(PathItemRole.ROLE_MODULE, "Group by module", Icons.MODULE_ICON); myModelPathButton = new MyBasePathToggleAction(PathItemRole.ROLE_MODEL, "Group by model", Icons.MODEL_ICON); myRootPathButton = new MyBaseToggleAction("Group by root node", "", Icons.ROOT_ICON) { @Override public boolean isSelected(AnActionEvent e) { return myPathProvider.contains(PathItemRole.ROLE_ROOT); } @Override public void doSetSelected(AnActionEvent e, boolean state) { if (state) { addPathComponent(PathItemRole.ROLE_ROOT); } else { myTree.startAdjusting(); if (myNamedConceptPathButton.isSelected(null)) { myNamedConceptPathButton.doSetSelected(null, false); } removePathComponent(PathItemRole.ROLE_ROOT); myTree.finishAdjusting(); } } }; myNamedConceptPathButton = new MyBaseToggleAction("Group by path", "", Icons.PATH_ICON) { @Override public boolean isSelected(AnActionEvent e) { return myPathProvider.contains(PathItemRole.ROLE_ROOT_TO_TARGET_NODE); } @Override public void doSetSelected(AnActionEvent e, boolean state) { if (state) { myTree.startAdjusting(); if (!myRootPathButton.isSelected(null)) { myRootPathButton.doSetSelected(null, true); } addPathComponent(PathItemRole.ROLE_ROOT_TO_TARGET_NODE); myTree.finishAdjusting(); } else { removePathComponent(PathItemRole.ROLE_ROOT_TO_TARGET_NODE); } } }; myActions = new DefaultActionGroup(); for (MyBaseToggleAction categoryPathButton : myCategoryPathButtons) { myActions.addAction(categoryPathButton); } myActions.addAction(myModulePathButton); myActions.addAction(myModelPathButton); myActions.addAction(myRootPathButton); myActions.addAction(myNamedConceptPathButton); } public void setViewOptions(ViewOptions options) { myTree.startAdjusting(); int size = Math.min(myCategoryPathButtons.size(), options.myCategories.length); for (int i = 0; i < size; i++) { myCategoryPathButtons.get(i).doSetSelected(null, options.myCategories[i]); } myModulePathButton.doSetSelected(null, options.myModule); myModelPathButton.doSetSelected(null, options.myModel); myRootPathButton.doSetSelected(null, options.myRoot); myNamedConceptPathButton.doSetSelected(null, options.myNamedPath); myTree.finishAdjusting(); } public void getViewOptions(ViewOptions options) { options.myCategories = new boolean[myCategoryPathButtons.size()]; for (int i = 0; i < myCategoryPathButtons.size(); i++) { options.myCategories[i] = myCategoryPathButtons.get(i).isSelected(null); } options.myModule = myModulePathButton.isSelected(null); options.myModel = myModelPathButton.isSelected(null); options.myRoot = myRootPathButton.isSelected(null); options.myNamedPath = myNamedConceptPathButton.isSelected(null); } public ActionGroup getActions() { return myActions; } } class MyBasePathToggleAction extends MyBaseToggleAction { private PathItemRole myPathItemRole = null; public MyBasePathToggleAction(PathItemRole itemRole, String name, Icon icon) { super(name, "", icon); myPathItemRole = itemRole; } @Override public boolean isSelected(AnActionEvent e) { return myPathProvider.contains(myPathItemRole); } @Override public void doSetSelected(AnActionEvent e, boolean state) { if (myPathItemRole == null) { return; } if (state) { addPathComponent(myPathItemRole); } else { removePathComponent(myPathItemRole); } } } } class ActionsToolbar { private DefaultActionGroup myActions; private MyBaseToggleAction myAutoscrollButton; public ActionsToolbar() { myActions = new DefaultActionGroup(); final CommonActionsManager actionsManager = CommonActionsManager.getInstance(); final TreeExpander treeExpander = new DefaultTreeExpander(myTree) { @Override public void collapseAll() { super.collapseAll(); TreeUtil.expand(myTree, 2); } }; myActions.add(actionsManager.createExpandAllAction(treeExpander, myTree)); myActions.add(actionsManager.createCollapseAllAction(treeExpander, myTree)); myActions.add(actionsManager.createPrevOccurenceAction(getOccurenceNavigator())); myActions.add(actionsManager.createNextOccurenceAction(getOccurenceNavigator())); myAutoscrollButton = new MyBaseToggleAction("Autoscroll to source", "", Icons.AUTOSCROLL_ICON) { @Override public boolean isSelected(AnActionEvent e) { return myTree.isAutoscroll(); } @Override public void doSetSelected(AnActionEvent e, boolean state) { myTree.setAutoscroll(state); } }; myActions.addAction(myAutoscrollButton); } public void setViewOptions(ViewOptions options) { myAutoscrollButton.doSetSelected(null, options.myAutoscrolls); } public void getViewOptions(ViewOptions options) { options.myAutoscrolls = myAutoscrollButton.isSelected(null); } public ActionGroup getActions() { return myActions; } } private abstract class MyBaseToggleAction extends ToggleAction { protected MyBaseToggleAction(String text, String description, Icon icon) { super(text, description, icon); } @Override public final void setSelected(AnActionEvent e, boolean state) { doSetSelected(e, state); getComponentsViewOptions(myViewOptions); myDefaultOptions.setValues(myViewOptions); } public abstract void doSetSelected(AnActionEvent e, boolean state); } }