/* * 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.util; import com.intellij.ide.IdeBundle; import com.intellij.ide.util.PropertiesComponent; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.ActionPlaces; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CustomShortcutSet; import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.actionSystem.Presentation; import com.intellij.openapi.actionSystem.ToggleAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.VerticalFlowLayout; import com.intellij.openapi.util.IconLoader; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.SystemInfo; import com.intellij.ui.ColoredTreeCellRenderer; import com.intellij.ui.ScrollPaneFactory; import com.intellij.ui.SimpleTextAttributes; import com.intellij.ui.SpeedSearchComparator; import com.intellij.ui.TreeSpeedSearch; import com.intellij.ui.speedSearch.SpeedSearchUtil; import com.intellij.ui.treeStructure.Tree; import com.intellij.util.PlatformIcons; import com.intellij.util.containers.Convertor; import com.intellij.util.containers.FactoryMap; import com.intellij.util.containers.HashMap; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.tree.TreeUtil; import jetbrains.mps.ide.icons.IconManager; import jetbrains.mps.ide.project.ProjectHelper; import jetbrains.mps.smodel.SNodeUtil; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.model.SNodeReference; import org.jetbrains.mps.openapi.module.SRepository; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.KeyStroke; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.LinkedHashSet; import java.util.List; public class GroupedNodesChooser extends DialogWrapper { protected Tree myTree; private DefaultTreeModel myTreeModel; protected JComponent[] myOptionControls; private final ArrayList<MemberNode> mySelectedNodes = new ArrayList<MemberNode>(); private boolean mySorted = false; private boolean myShowContainers = true; private boolean myAllowEmptySelection = false; private boolean myAllowMultiSelection; protected final Project myProject; private SNodeReference[] myElements; private final HashMap<MemberNode, ParentNode> myNodeToParentMap = new HashMap<MemberNode, ParentNode>(); private final HashMap<SNodeReference, MemberNode> myElementToNodeMap = new HashMap<SNodeReference, MemberNode>(); private final ArrayList<ContainerNode> myContainerNodes = new ArrayList<ContainerNode>(); private LinkedHashSet<SNodeReference> mySelectedElements; @NonNls private static final String PROP_SORTED = "MPS.NodesChooser.sorted"; @NonNls private static final String PROP_SHOWCONTAINERS = "MPS.NodesChooser.showContainers"; public GroupedNodesChooser(SNodeReference[] elements, boolean allowEmptySelection, boolean allowMultiSelection, @NotNull Project project ) { super(project, true); myAllowEmptySelection = allowEmptySelection; myAllowMultiSelection = allowMultiSelection; myProject = project; myTree = new Tree(new DefaultTreeModel(new DefaultMutableTreeNode())); myOptionControls = null; resetElements(elements); init(); } public void resetElements(SNodeReference[] elements) { myElements = elements; mySelectedNodes.clear(); myNodeToParentMap.clear(); myElementToNodeMap.clear(); myContainerNodes.clear(); ProjectHelper.getModelAccess(myProject).runReadAction(() -> myTreeModel = buildModel()); myTree.setModel(myTreeModel); myTree.setRootVisible(false); doSort(); TreeUtil.expandAll(myTree); initOptions(); myTree.doLayout(); setOKActionEnabled(myElements != null && myElements.length > 0); } protected void initOptions() { myOptionControls = new JComponent[0]; } /** * should be invoked in read action */ private DefaultTreeModel buildModel() { final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(); final Ref<Integer> count = new Ref<Integer>(0); final SRepository projectRepo = ProjectHelper.getProjectRepository(myProject); final FactoryMap<Object, ParentNode> map = new FactoryMap<Object, ParentNode>() { @Override protected ParentNode create(final Object key) { if (key instanceof SNodeReference) { SNode el = ((SNodeReference) key).resolve(projectRepo); if (el != null) { final ContainerNode containerNode = new ContainerNode(rootNode, (SNodeReference) key, getText(el), getIcon(el), count); myContainerNodes.add(containerNode); return containerNode; } return new ParentNode(rootNode, null, "<unknown>", null, count); } if (key instanceof String) { return new ParentNode(rootNode, null, (String) key, null, count); } throw new IllegalArgumentException(); } }; for (SNodeReference object : myElements) { SNode node = object.resolve(projectRepo); Object group = getGroupNode(node); if (group == null) group = getGroupTitle(node); final ParentNode parentNode = map.get(group); final MemberNode elementNode = new MemberNode(parentNode, object, getText(node), getIcon(node), count); myNodeToParentMap.put(elementNode, parentNode); myElementToNodeMap.put(object, elementNode); } return new DefaultTreeModel(rootNode); } protected Icon getIcon(SNode node) { return IconManager.getIconFor(node); } protected String getText(SNode node) { return SNodeUtil.getPresentation(node); } @Nullable protected SNodeReference getGroupNode(SNode node) { SNode parent = node.getParent(); return parent != null ? new jetbrains.mps.smodel.SNodePointer(parent) : null; } @NotNull protected String getGroupTitle(SNode node) { return "Others"; } public void selectElements(SNodeReference[] elements) { ArrayList<TreePath> selectionPaths = new ArrayList<TreePath>(); for (SNodeReference element : elements) { MemberNode treeNode = myElementToNodeMap.get(element); if (treeNode != null) { selectionPaths.add(new TreePath(treeNode.getPath())); } } myTree.setSelectionPaths(selectionPaths.toArray(new TreePath[selectionPaths.size()])); } @Override protected Action[] createActions() { if (myAllowEmptySelection) { return new Action[]{getOKAction(), new SelectNoneAction(), getCancelAction()}; } else { return new Action[]{getOKAction(), getCancelAction()}; } } @Override protected void doHelpAction() { } protected void customizeOptionsPanel() { } @Override protected JComponent createSouthPanel() { JPanel panel = new JPanel(new GridBagLayout()); customizeOptionsPanel(); JPanel optionsPanel = new JPanel(new VerticalFlowLayout()); for (final JComponent component : myOptionControls) { optionsPanel.add(component); } panel.add( optionsPanel, new GridBagConstraints(0, 0, 1, 1, 1, 0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0, 0, 0, 5), 0, 0) ); if (myElements == null || myElements.length == 0) { setOKActionEnabled(false); } panel.add( super.createSouthPanel(), new GridBagConstraints(1, 0, 1, 1, 0, 0, GridBagConstraints.SOUTH, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0) ); return panel; } @Override protected JComponent createCenterPanel() { JPanel panel = new JPanel(new BorderLayout()); // Toolbar DefaultActionGroup group = new DefaultActionGroup(); fillToolbarActions(group); group.addSeparator(); ExpandAllAction expandAllAction = new ExpandAllAction(); expandAllAction.registerCustomShortcutSet( new CustomShortcutSet( KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, SystemInfo.isMac ? InputEvent.META_MASK : InputEvent.CTRL_MASK)), myTree); group.add(expandAllAction); CollapseAllAction collapseAllAction = new CollapseAllAction(); collapseAllAction.registerCustomShortcutSet( new CustomShortcutSet( KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, SystemInfo.isMac ? InputEvent.META_MASK : InputEvent.CTRL_MASK)), myTree); group.add(collapseAllAction); panel.add(ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, group, true).getComponent(), BorderLayout.NORTH); // Tree myTree.setCellRenderer(new ColoredTreeCellRenderer() { @Override public void customizeCellRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { if (value instanceof ElementNode) { ((ElementNode) value).renderTreeNode(this, tree); } } } ); UIUtil.setLineStyleAngled(myTree); myTree.setRootVisible(false); myTree.setShowsRootHandles(true); myTree.addKeyListener(new TreeKeyListener()); myTree.addTreeSelectionListener(new MyTreeSelectionListener()); if (!myAllowMultiSelection) { myTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); } if (getRootNode().getChildCount() > 0) { myTree.expandRow(0); myTree.setSelectionRow(1); } TreeUtil.expandAll(myTree); final TreeSpeedSearch treeSpeedSearch = new TreeSpeedSearch(myTree, new Convertor<TreePath, String>() { @Override @Nullable public String convert(TreePath path) { final ElementNode lastPathComponent = (ElementNode) path.getLastPathComponent(); if (lastPathComponent == null) return null; return lastPathComponent.getText(); } }); treeSpeedSearch.setComparator(new SpeedSearchComparator(false)); treeSpeedSearch.addChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { myTree.repaint(); // to update match highlighting } }); myTree.addMouseListener( new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { if (myTree.getPathForLocation(e.getX(), e.getY()) != null) { doOKAction(); } } } } ); TreeUtil.installActions(myTree); JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myTree); scrollPane.setPreferredSize(new Dimension(350, 450)); panel.add(scrollPane, BorderLayout.CENTER); return panel; } protected void fillToolbarActions(DefaultActionGroup group) { SortEmAction sortAction = new SortEmAction(); sortAction.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.ALT_MASK)), myTree); setSorted(PropertiesComponent.getInstance().isTrueValue(PROP_SORTED)); group.add(sortAction); ShowContainersAction showContainersAction = getShowContainersAction(); showContainersAction.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.ALT_MASK)), myTree); setShowContainers(PropertiesComponent.getInstance().isTrueValue(PROP_SHOWCONTAINERS)); group.add(showContainersAction); } @Override protected String getDimensionServiceKey() { return "#jetbrains.mps.ide.util.NodesChooser"; } @Override public JComponent getPreferredFocusedComponent() { return myTree; } public JComponent[] getOptionControls() { return myOptionControls; } @Nullable private LinkedHashSet<SNodeReference> getSelectedElementsList() { return getExitCode() == OK_EXIT_CODE ? mySelectedElements : null; } @Nullable public List<SNodeReference> getSelectedElements() { final LinkedHashSet<SNodeReference> list = getSelectedElementsList(); return list == null ? null : new ArrayList<SNodeReference>(list); } protected final boolean areElementsSelected() { return mySelectedElements != null && !mySelectedElements.isEmpty(); } private boolean isSorted() { return mySorted; } private void setSorted(boolean sorted) { if (mySorted == sorted) return; mySorted = sorted; doSort(); } private void doSort() { Pair<ElementNode, List<ElementNode>> pair = storeSelection(); Enumeration<ParentNode> children = getRootNodeChildren(); while (children.hasMoreElements()) { ParentNode classNode = children.nextElement(); sortNode(classNode, mySorted); myTreeModel.nodeStructureChanged(classNode); } restoreSelection(pair); } private static void sortNode(ParentNode node, boolean sorted) { ArrayList<MemberNode> arrayList = new ArrayList<MemberNode>(); Enumeration<MemberNode> children = node.children(); while (children.hasMoreElements()) { arrayList.add(children.nextElement()); } Collections.sort(arrayList, sorted ? new AlphaComparator() : new OrderComparator()); replaceChildren(node, arrayList); } private static void replaceChildren(final DefaultMutableTreeNode node, final Collection<? extends ElementNode> arrayList) { node.removeAllChildren(); for (ElementNode child : arrayList) { node.add(child); } } private void setShowContainers(boolean showContainers) { myShowContainers = showContainers; Pair<ElementNode, List<ElementNode>> selection = storeSelection(); DefaultMutableTreeNode root = getRootNode(); if (!myShowContainers || myContainerNodes.isEmpty()) { List<ParentNode> otherObjects = new ArrayList<ParentNode>(); Enumeration<ParentNode> children = getRootNodeChildren(); ParentNode newRoot = new ParentNode(null, null, getAllContainersNodeName(), null, new Ref<Integer>(0)); while (children.hasMoreElements()) { final ParentNode nextElement = children.nextElement(); if (nextElement instanceof ContainerNode) { final ContainerNode containerNode = (ContainerNode) nextElement; Enumeration<MemberNode> memberNodes = containerNode.children(); List<MemberNode> memberNodesList = new ArrayList<MemberNode>(); while (memberNodes.hasMoreElements()) { memberNodesList.add(memberNodes.nextElement()); } for (MemberNode memberNode : memberNodesList) { newRoot.add(memberNode); } } else { otherObjects.add(nextElement); } } replaceChildren(root, otherObjects); sortNode(newRoot, mySorted); if (newRoot.children().hasMoreElements()) root.add(newRoot); } else { Enumeration<ParentNode> children = getRootNodeChildren(); if (children.hasMoreElements()) { ParentNode allClassesNode = children.nextElement(); Enumeration<MemberNode> memberNodes = allClassesNode.children(); ArrayList<MemberNode> arrayList = new ArrayList<MemberNode>(); while (memberNodes.hasMoreElements()) { arrayList.add(memberNodes.nextElement()); } for (MemberNode memberNode : arrayList) { myNodeToParentMap.get(memberNode).add(memberNode); } } replaceChildren(root, myContainerNodes); } myTreeModel.nodeStructureChanged(root); TreeUtil.expandAll(myTree); restoreSelection(selection); } protected String getAllContainersNodeName() { return "All elements"; } private Enumeration<ParentNode> getRootNodeChildren() { return getRootNode().children(); } private DefaultMutableTreeNode getRootNode() { return (DefaultMutableTreeNode) myTreeModel.getRoot(); } private Pair<ElementNode, List<ElementNode>> storeSelection() { List<ElementNode> selectedNodes = new ArrayList<ElementNode>(); TreePath[] paths = myTree.getSelectionPaths(); if (paths != null) { for (TreePath path : paths) { selectedNodes.add((ElementNode) path.getLastPathComponent()); } } TreePath leadSelectionPath = myTree.getLeadSelectionPath(); return Pair.create(leadSelectionPath != null ? (ElementNode) leadSelectionPath.getLastPathComponent() : null, selectedNodes); } private void restoreSelection(Pair<ElementNode, List<ElementNode>> pair) { List<ElementNode> selectedNodes = pair.second; DefaultMutableTreeNode root = getRootNode(); ArrayList<TreePath> toSelect = new ArrayList<TreePath>(); for (ElementNode node : selectedNodes) { if (root.isNodeDescendant(node)) { toSelect.add(new TreePath(node.getPath())); } } if (!toSelect.isEmpty()) { myTree.setSelectionPaths(toSelect.toArray(new TreePath[toSelect.size()])); } ElementNode leadNode = pair.first; if (leadNode != null) { myTree.setLeadSelectionPath(new TreePath(leadNode.getPath())); } } @Override public void dispose() { PropertiesComponent instance = PropertiesComponent.getInstance(); instance.setValue(PROP_SORTED, Boolean.toString(isSorted())); instance.setValue(PROP_SHOWCONTAINERS, Boolean.toString(myShowContainers)); final Container contentPane = getContentPane(); if (contentPane != null) { contentPane.removeAll(); } mySelectedNodes.clear(); myElements = null; super.dispose(); } private class MyTreeSelectionListener implements TreeSelectionListener { @Override public void valueChanged(TreeSelectionEvent e) { TreePath[] paths = e.getPaths(); if (paths == null) return; for (int i = 0; i < paths.length; i++) { Object node = paths[i].getLastPathComponent(); if (node instanceof MemberNode) { final MemberNode memberNode = (MemberNode) node; if (e.isAddedPath(i)) { if (!mySelectedNodes.contains(memberNode)) { mySelectedNodes.add(memberNode); } } else { mySelectedNodes.remove(memberNode); } } } mySelectedElements = new LinkedHashSet<SNodeReference>(); for (MemberNode selectedNode : mySelectedNodes) { mySelectedElements.add(selectedNode.getElement()); } } } private abstract static class ElementNode extends DefaultMutableTreeNode { private final int myOrder; private final SNodeReference myElement; private final String myText; private Icon myIcon; public ElementNode(@Nullable DefaultMutableTreeNode parent, SNodeReference nodePointer, String text, Icon icon, Ref<Integer> order) { myIcon = icon; myOrder = order.get(); order.set(myOrder + 1); myText = text; myElement = nodePointer; if (parent != null) { parent.add(this); } } public SNodeReference getElement() { return myElement; } public int getOrder() { return myOrder; } public String getText() { return myText; } public void renderTreeNode(ColoredTreeCellRenderer coloredTreeCellRenderer, JTree tree) { SpeedSearchUtil.appendFragmentsForSpeedSearch(tree, getText(), getTextAttributes(tree), false, coloredTreeCellRenderer); coloredTreeCellRenderer.setIcon(myIcon); } protected SimpleTextAttributes getTextAttributes(JTree tree) { return new SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, tree.getForeground()); } } private static class MemberNode extends ElementNode { public MemberNode(ParentNode parent, SNodeReference element, String text, Icon icon, Ref<Integer> order) { super(parent, element, text, icon, order); } } private static class ParentNode extends ElementNode { public ParentNode(@Nullable DefaultMutableTreeNode parent, SNodeReference element, String text, Icon icon, Ref<Integer> order) { super(parent, null, text, icon, order); } } private static class ContainerNode extends ParentNode { public ContainerNode(DefaultMutableTreeNode parent, SNodeReference element, String text, Icon icon, Ref<Integer> order) { super(parent, element, text, icon, order); } } private class SelectNoneAction extends AbstractAction { public SelectNoneAction() { super(IdeBundle.message("action.select.none")); } @Override public void actionPerformed(ActionEvent e) { myTree.clearSelection(); doOKAction(); } } private class TreeKeyListener extends KeyAdapter { @Override public void keyPressed(KeyEvent e) { TreePath path = myTree.getLeadSelectionPath(); if (path == null) return; final Object lastComponent = path.getLastPathComponent(); if (e.getKeyCode() == KeyEvent.VK_ENTER) { if (lastComponent instanceof ParentNode) return; doOKAction(); e.consume(); } else if (e.getKeyCode() == KeyEvent.VK_INSERT) { if (lastComponent instanceof ElementNode) { final ElementNode node = (ElementNode) lastComponent; if (!mySelectedNodes.contains(node)) { if (node.getNextNode() != null) { myTree.setSelectionPath(new TreePath(node.getNextNode().getPath())); } } else { if (node.getNextNode() != null) { myTree.removeSelectionPath(new TreePath(node.getPath())); myTree.setSelectionPath(new TreePath(node.getNextNode().getPath())); myTree.repaint(); } } e.consume(); } } } } private class SortEmAction extends ToggleAction { public SortEmAction() { super(IdeBundle.message("action.sort.alphabetically"), IdeBundle.message("action.sort.alphabetically"), IconLoader.getIcon("/objectBrowser/sorted.png")); } @Override public boolean isSelected(AnActionEvent event) { return isSorted(); } @Override public void setSelected(AnActionEvent event, boolean flag) { setSorted(flag); } } protected ShowContainersAction getShowContainersAction() { return new ShowContainersAction("Show Groups", PlatformIcons.CLASS_ICON); } protected class ShowContainersAction extends ToggleAction { public ShowContainersAction(final String text, final Icon icon) { super(text, text, icon); } @Override public boolean isSelected(AnActionEvent event) { return myShowContainers; } @Override public void setSelected(AnActionEvent event, boolean flag) { setShowContainers(flag); } @Override public void update(AnActionEvent e) { super.update(e); Presentation presentation = e.getPresentation(); presentation.setEnabled(myContainerNodes.size() > 1); } } private class ExpandAllAction extends AnAction { public ExpandAllAction() { super(IdeBundle.message("action.expand.all"), IdeBundle.message("action.expand.all"), IconLoader.getIcon("/actions/expandall.png")); } @Override public void actionPerformed(AnActionEvent e) { TreeUtil.expandAll(myTree); } } private class CollapseAllAction extends AnAction { public CollapseAllAction() { super(IdeBundle.message("action.collapse.all"), IdeBundle.message("action.collapse.all"), IconLoader.getIcon("/actions/collapseall.png")); } @Override public void actionPerformed(AnActionEvent e) { TreeUtil.collapseAll(myTree, 1); } } private static class AlphaComparator implements Comparator<ElementNode> { @Override public int compare(ElementNode n1, ElementNode n2) { return n1.getText().compareToIgnoreCase(n2.getText()); } } private static class OrderComparator implements Comparator<ElementNode> { @Override public int compare(ElementNode n1, ElementNode n2) { return n1.getOrder() - n2.getOrder(); } } }