blob: 8de17ab73219217ff381dad6d0420a4ab61e0379 [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.util;
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.ide.DataManager;
import com.intellij.ide.DefaultTreeExpander;
import com.intellij.ide.IdeBundle;
import com.intellij.ide.TreeExpander;
import com.intellij.ide.structureView.StructureView;
import com.intellij.ide.structureView.StructureViewBuilder;
import com.intellij.ide.structureView.StructureViewModel;
import com.intellij.ide.structureView.StructureViewTreeElement;
import com.intellij.ide.structureView.impl.StructureViewComposite;
import com.intellij.ide.structureView.impl.common.PsiTreeElementBase;
import com.intellij.ide.structureView.newStructureView.StructureViewComponent;
import com.intellij.ide.structureView.newStructureView.TreeModelWrapper;
import com.intellij.ide.util.treeView.AbstractTreeNode;
import com.intellij.ide.util.treeView.NodeRenderer;
import com.intellij.ide.util.treeView.smartTree.*;
import com.intellij.navigation.ItemPresentation;
import com.intellij.navigation.LocationPresentation;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.MnemonicHelper;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.impl.EditorImpl;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.ui.*;
import com.intellij.ui.popup.AbstractPopup;
import com.intellij.ui.popup.PopupUpdateProcessor;
import com.intellij.ui.speedSearch.ElementFilter;
import com.intellij.ui.speedSearch.SpeedSearchUtil;
import com.intellij.ui.treeStructure.AlwaysExpandedTree;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.ui.treeStructure.filtered.FilteringTreeBuilder;
import com.intellij.ui.treeStructure.filtered.FilteringTreeStructure;
import com.intellij.util.Alarm;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.Convertor;
import com.intellij.util.containers.HashSet;
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 org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.event.*;
import java.lang.reflect.Field;
import java.util.*;
import java.util.List;
/**
* @author Konstantin Bulenkov
*/
public class FileStructurePopup implements Disposable {
private static final Logger LOG = Logger.getInstance("#com.intellij.ide.util.FileStructurePopup");
private final Editor myEditor;
private final Project myProject;
private final StructureViewModel myTreeModel;
private final StructureViewModel myBaseTreeModel;
private final TreeStructureActionsOwner myTreeActionsOwner;
private PsiFile myPsiFile;
private JBPopup myPopup;
@NonNls private static final String narrowDownPropertyKey = "FileStructurePopup.narrowDown";
private boolean myShouldNarrowDown = true;
private FileStructureTree myTree;
private FilteringTreeBuilder myAbstractTreeBuilder;
private String myTitle;
private TreeSpeedSearch mySpeedSearch;
private SmartTreeStructure myTreeStructure;
private int myPreferredWidth;
private final FilteringTreeStructure myFilteringStructure;
private PsiElement myInitialPsiElement;
private Map<Class, JCheckBox> myCheckBoxes = new HashMap<Class, JCheckBox>();
private List<JCheckBox> myAutoClicked = new ArrayList<JCheckBox>();
private String myTestSearchFilter;
private final ActionCallback myTreeHasBuilt = new ActionCallback();
private boolean myInitialNodeIsLeaf;
private final List<Pair<String, JCheckBox>> myTriggeredCheckboxes = new ArrayList<Pair<String, JCheckBox>>();
private final TreeExpander myTreeExpander;
private StructureView myStructureView;
public FileStructurePopup(StructureViewModel structureViewModel,
@Nullable Editor editor,
Project project,
@NotNull final Disposable auxDisposable,
final boolean applySortAndFilter) {
myProject = project;
myEditor = editor;
//Stop code analyzer to speedup EDT
DaemonCodeAnalyzer.getInstance(myProject).disableUpdateByTimer(this);
IdeFocusManager.getInstance(myProject).typeAheadUntil(myTreeHasBuilt);
//long l = System.currentTimeMillis();
if (editor instanceof EditorImpl) {
VirtualFile file = ((EditorImpl)editor).getVirtualFile();
FileEditor fileEditor = FileEditorManager.getInstance(myProject).getSelectedEditor(file);
if (fileEditor != null) {
StructureViewBuilder builder = fileEditor.getStructureViewBuilder();
myPsiFile = PsiManager.getInstance(project).findFile(file);
if (builder != null && myPsiFile != null) {
myStructureView = builder.createStructureView(fileEditor, project);
Disposer.register(this, myStructureView);
}
}
}
//System.out.println(System.currentTimeMillis() - l);
if (myStructureView instanceof StructureViewComposite) {
StructureViewComposite.StructureViewDescriptor[] views = ((StructureViewComposite)myStructureView).getStructureViews();
myBaseTreeModel = new StructureViewCompositeModel(myPsiFile, views);
Disposer.register(this, (Disposable)myBaseTreeModel);
} else {
myBaseTreeModel = structureViewModel;
}
Disposer.register(this, auxDisposable);
if (applySortAndFilter) {
myTreeActionsOwner = new TreeStructureActionsOwner(myBaseTreeModel);
myTreeModel = new TreeModelWrapper(myBaseTreeModel, myTreeActionsOwner);
}
else {
myTreeActionsOwner = null;
myTreeModel = structureViewModel;
}
myTreeStructure = new SmartTreeStructure(project, myTreeModel){
@Override
public void rebuildTree() {
if (ApplicationManager.getApplication().isUnitTestMode() || !myPopup.isDisposed()) {
super.rebuildTree();
}
}
@Override
public boolean isToBuildChildrenInBackground(final Object element) {
return getRootElement() == element;
}
@Override
protected TreeElementWrapper createTree() {
return new StructureViewComponent.StructureViewTreeElementWrapper(myProject, myModel.getRoot(), myModel);
}
@NonNls
@Override
public String toString() {
return "structure view tree structure(model=" + myTreeModel + ")";
}
};
myTree = new FileStructureTree(myTreeStructure.getRootElement(), Registry.is("fast.tree.expand.in.structure.view"));
myTree.setCellRenderer(new NodeRenderer() {
@Override
protected void doAppend(@NotNull @Nls String fragment,
@NotNull SimpleTextAttributes attributes,
boolean isMainText,
boolean selected) {
if (!isMainText ) {
super.doAppend(fragment, attributes, isMainText, selected);
} else {
SpeedSearchUtil.appendFragmentsForSpeedSearch(myTree, fragment, attributes, selected, this);
}
}
@Override
public void doAppend(@NotNull String fragment, @NotNull SimpleTextAttributes attributes, boolean selected) {
SpeedSearchUtil.appendFragmentsForSpeedSearch(myTree, fragment, attributes, selected, this);
}
@Override
public void doAppend(String fragment, boolean selected) {
SpeedSearchUtil.appendFragmentsForSpeedSearch(myTree, fragment, SimpleTextAttributes.REGULAR_ATTRIBUTES, selected, this);
}
});
mySpeedSearch = new MyTreeSpeedSearch();
mySpeedSearch.setComparator(new SpeedSearchComparator(false, true));
final FileStructurePopupFilter filter = new FileStructurePopupFilter();
myFilteringStructure = new FilteringTreeStructure(filter, myTreeStructure, ApplicationManager.getApplication().isUnitTestMode());
myAbstractTreeBuilder = new FilteringTreeBuilder(myTree, filter, myFilteringStructure, null) {
@Override
public void initRootNode() {
}
@Override
protected boolean validateNode(Object child) {
return StructureViewComponent.isValid(child);
}
@Override
public void revalidateTree() {
//myTree.revalidate();
//myTree.repaint();
}
@Override
public boolean isToEnsureSelectionOnFocusGained() {
return false;
}
};
myTreeExpander = new DefaultTreeExpander(myTree);
//myAbstractTreeBuilder.getUi().setPassthroughMode(true);
myAbstractTreeBuilder.getUi().getUpdater().setDelay(1);
myInitialPsiElement = getCurrentElement(getPsiFile(myProject));
//myAbstractTreeBuilder.setCanYieldUpdate(true);
Disposer.register(this, myAbstractTreeBuilder);
TreeUtil.installActions(myTree);
}
public void show() {
//final long time = System.currentTimeMillis();
JComponent panel = createCenterPanel();
new MnemonicHelper().register(panel);
boolean shouldSetWidth = DimensionService.getInstance().getSize(getDimensionServiceKey(), myProject) == null;
myPopup = JBPopupFactory.getInstance().createComponentPopupBuilder(panel, null)
.setTitle(myTitle)
.setResizable(true)
.setModalContext(false)
.setFocusable(true)
.setRequestFocus(true)
.setMovable(true)
.setBelongsToGlobalPopupStack(true)
//.setCancelOnClickOutside(false) //for debug and snapshots
.setCancelKeyEnabled(false)
.setDimensionServiceKey(null, getDimensionServiceKey(), false)
.setCancelCallback(new Computable<Boolean>() {
@Override
public Boolean compute() {
DimensionService.getInstance().setLocation(getDimensionServiceKey(), myPopup.getLocationOnScreen(), myProject);
return true;
}
})
.createPopup();
myTree.addTreeSelectionListener(new TreeSelectionListener() {
@Override
public void valueChanged(TreeSelectionEvent e) {
if (myPopup.isVisible()) {
final PopupUpdateProcessor updateProcessor = myPopup.getUserData(PopupUpdateProcessor.class);
if (updateProcessor != null) {
final AbstractTreeNode node = getSelectedNode();
updateProcessor.updatePopup(node);
}
}
}
});
Disposer.register(myPopup, this);
Disposer.register(myPopup, new Disposable() {
@Override
public void dispose() {
if (!myTreeHasBuilt.isDone()) {
myTreeHasBuilt.setRejected();
}
}
});
myTree.getEmptyText().setText("Loading...");
final Point location = DimensionService.getInstance().getLocation(getDimensionServiceKey(), myProject);
if (location != null && myEditor != null) {
myPopup.showInScreenCoordinates(myEditor.getContentComponent(), location);
} else {
myPopup.showCenteredInCurrentWindow(myProject);
}
((AbstractPopup)myPopup).setShowHints(true);
if (shouldSetWidth) {
myPopup.setSize(new Dimension(myPreferredWidth + 10, myPopup.getSize().height));
}
IdeFocusManager.getInstance(myProject).requestFocus(myTree, true);
SwingUtilities.windowForComponent(myPopup.getContent()).addWindowFocusListener(new WindowFocusListener() {
@Override
public void windowGainedFocus(WindowEvent e) {
}
@Override
public void windowLostFocus(WindowEvent e) {
myPopup.cancel();
}
});
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
myFilteringStructure.rebuild();
}
});
//noinspection SSBasedInspection
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
myAbstractTreeBuilder.queueUpdate().doWhenDone(new Runnable() {
@Override
public void run() {
myTreeHasBuilt.setDone();
//noinspection SSBasedInspection
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (selectPsiElement(myInitialPsiElement) == null) {
TreeUtil.ensureSelection(myAbstractTreeBuilder.getTree());
myAbstractTreeBuilder.revalidateTree();
}
}
});
}
});
installUpdater();
}
});
}
});
}
private void installUpdater() {
if (ApplicationManager.getApplication().isUnitTestMode()) {
return;
}
final Alarm alarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, myPopup);
alarm.addRequest(new Runnable() {
String filter = "";
@Override
public void run() {
alarm.cancelAllRequests();
String prefix = mySpeedSearch.getEnteredPrefix();
myTree.getEmptyText().setText(StringUtil.isEmpty(prefix) ? "Nothing to show" : "Can't find '" + prefix + "'");
if (prefix == null) prefix = "";
if (!filter.equals(prefix)) {
final boolean isBackspace = prefix.length() < filter.length();
filter = prefix;
myAbstractTreeBuilder.refilter(null, false, false).doWhenProcessed(new Runnable() {
@Override
public void run() {
//noinspection SSBasedInspection
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
myTree.repaint();
if (isBackspace && handleBackspace(filter)) {
return;
}
if (myFilteringStructure.getRootElement().getChildren().length == 0) {
for (JCheckBox box : myCheckBoxes.values()) {
if (!box.isSelected()) {
myAutoClicked.add(box);
myTriggeredCheckboxes.add(0, Pair.create(filter, box));
box.doClick();
filter = "";
break;
}
}
}
}
});
}
});
}
if (!alarm.isDisposed()) {
alarm.addRequest(this, 300);
}
}
}, 300);
}
private boolean handleBackspace(String filter) {
boolean clicked = false;
final Iterator<Pair<String, JCheckBox>> iterator = myTriggeredCheckboxes.iterator();
while (iterator.hasNext()) {
final Pair<String, JCheckBox> next = iterator.next();
if (next.getFirst().length() < filter.length()) break;
if (next.getFirst().length() >= filter.length()) {
iterator.remove();
next.getSecond().doClick();
clicked = true;
}
}
return clicked;
}
@Nullable
public FilteringTreeStructure.FilteringNode selectPsiElement(PsiElement element) {
Set<PsiElement> parents = getAllParents(element);
FilteringTreeStructure.FilteringNode node = (FilteringTreeStructure.FilteringNode)myAbstractTreeBuilder.getRootElement();
if (element != null && node != null && myStructureView instanceof StructureViewComposite) {
parents.remove(element.getContainingFile());
final List<FilteringTreeStructure.FilteringNode> fileNodes = node.children();
for (FilteringTreeStructure.FilteringNode fileNode : fileNodes) {
final FilteringTreeStructure.FilteringNode found = findNode(parents, fileNode);
if (found != null && found != fileNode) {
return found;
}
}
} else {
final FilteringTreeStructure.FilteringNode found = findNode(parents, node);
if (found == null) {
TreeUtil.ensureSelection(myTree);
}
return found;
}
TreeUtil.ensureSelection(myTree);
return null;
}
private FilteringTreeStructure.FilteringNode findNode(Set<PsiElement> parents, FilteringTreeStructure.FilteringNode node) {
while (node != null) {
boolean changed = false;
for (FilteringTreeStructure.FilteringNode n : node.children()) {
final PsiElement psiElement = getPsi(n);
if (psiElement != null && parents.contains(psiElement)) {
node = n;
changed = true;
break;
}
}
if (!changed) {
myAbstractTreeBuilder.select(node);
if (myAbstractTreeBuilder.getSelectedElements().isEmpty()) {
TreeUtil.selectFirstNode(myTree);
}
myInitialNodeIsLeaf = node.getChildren().length == 0;
return node;
}
}
return null;
}
private static Set<PsiElement> getAllParents(PsiElement element) {
Set<PsiElement> parents = new java.util.HashSet<PsiElement>();
while (element != null) {
parents.add(element);
if (element instanceof PsiFile) break;
element = element.getParent();
}
return parents;
}
@Nullable
private PsiElement getPsi(FilteringTreeStructure.FilteringNode n) {
final Object delegate = n.getDelegate();
if (delegate instanceof StructureViewComponent.StructureViewTreeElementWrapper) {
final TreeElement value = ((StructureViewComponent.StructureViewTreeElementWrapper)delegate).getValue();
if (value instanceof StructureViewTreeElement) {
final Object element = ((StructureViewTreeElement)value).getValue();
if (element instanceof PsiElement) {
return (PsiElement)element;
}
}
}
return null;
}
@Nullable
protected PsiFile getPsiFile(final Project project) {
return myEditor == null ? null : PsiDocumentManager.getInstance(project).getPsiFile(myEditor.getDocument());
}
@Override
public void dispose() {
}
@NonNls
protected static String getDimensionServiceKey() {
return "StructurePopup";
}
@Nullable
public PsiElement getCurrentElement(@Nullable final PsiFile psiFile) {
if (psiFile == null) return null;
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
Object elementAtCursor = myTreeModel.getCurrentEditorElement();
if (elementAtCursor instanceof PsiElement) {
return (PsiElement)elementAtCursor;
}
if (myEditor != null) {
return psiFile.getViewProvider().findElementAt(myEditor.getCaretModel().getOffset());
}
return null;
}
public JComponent createCenterPanel() {
List<FileStructureFilter> fileStructureFilters = new ArrayList<FileStructureFilter>();
List<FileStructureNodeProvider> fileStructureNodeProviders = new ArrayList<FileStructureNodeProvider>();
if (myTreeActionsOwner != null) {
for(Filter filter: myBaseTreeModel.getFilters()) {
if (filter instanceof FileStructureFilter) {
final FileStructureFilter fsFilter = (FileStructureFilter)filter;
myTreeActionsOwner.setActionIncluded(fsFilter, true);
fileStructureFilters.add(fsFilter);
}
}
if (myBaseTreeModel instanceof ProvidingTreeModel) {
for (NodeProvider provider : ((ProvidingTreeModel)myBaseTreeModel).getNodeProviders()) {
if (provider instanceof FileStructureNodeProvider) {
fileStructureNodeProviders.add((FileStructureNodeProvider)provider);
}
}
}
}
final JPanel panel = new JPanel(new BorderLayout());
JPanel comboPanel = new JPanel(new GridLayout(0, 2, 0, 0));
final Shortcut[] F4 = ActionManager.getInstance().getAction(IdeActions.ACTION_EDIT_SOURCE).getShortcutSet().getShortcuts();
final Shortcut[] ENTER = CustomShortcutSet.fromString("ENTER").getShortcuts();
final CustomShortcutSet shortcutSet = new CustomShortcutSet(ArrayUtil.mergeArrays(F4, ENTER));
new AnAction() {
@Override
public void actionPerformed(AnActionEvent e) {
final boolean succeeded = navigateSelectedElement();
if (succeeded) {
unregisterCustomShortcutSet(panel);
}
}
}.registerCustomShortcutSet(shortcutSet, panel);
new AnAction() {
@Override
public void actionPerformed(AnActionEvent e) {
if (mySpeedSearch != null && mySpeedSearch.isPopupActive()) {
mySpeedSearch.hidePopup();
} else {
myPopup.cancel();
}
}
}.registerCustomShortcutSet(CustomShortcutSet.fromString("ESCAPE"), myTree);
new ClickListener() {
@Override
public boolean onClick(MouseEvent e, int clickCount) {
final TreePath path = myTree.getPathForLocation(e.getX(), e.getY());
if (path == null) return false; // user wants to expand/collapse a node
navigateSelectedElement();
return true;
}
}.installOn(myTree);
for(FileStructureFilter filter: fileStructureFilters) {
addCheckbox(comboPanel, filter);
}
for (FileStructureNodeProvider provider : fileStructureNodeProviders) {
addCheckbox(comboPanel, provider);
}
myPreferredWidth = Math.max(comboPanel.getPreferredSize().width, 350);
panel.add(comboPanel, BorderLayout.NORTH);
JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myAbstractTreeBuilder.getTree());
scrollPane.setBorder(IdeBorderFactory.createBorder(SideBorder.TOP | SideBorder.BOTTOM));
panel.add(scrollPane, BorderLayout.CENTER);
panel.add(createSouthPanel(), BorderLayout.SOUTH);
DataManager.registerDataProvider(panel, new DataProvider() {
@Override
public Object getData(@NonNls String dataId) {
if (PlatformDataKeys.PROJECT.is(dataId)) {
return myProject;
}
if (LangDataKeys.PSI_ELEMENT.is(dataId)) {
final Object node = ContainerUtil.getFirstItem(myAbstractTreeBuilder.getSelectedElements());
if (!(node instanceof FilteringTreeStructure.FilteringNode)) return null;
return getPsi((FilteringTreeStructure.FilteringNode)node);
}
if (LangDataKeys.POSITION_ADJUSTER_POPUP.is(dataId)) {
return myPopup;
}
if (PlatformDataKeys.TREE_EXPANDER.is(dataId)) {
return myTreeExpander;
}
return null;
}
});
panel.addFocusListener(new FocusAdapter() {
@Override
public void focusLost(FocusEvent e) {
myPopup.cancel();
}
});
return panel;
}
@Nullable
private AbstractTreeNode getSelectedNode() {
final TreePath path = myTree.getSelectionPath();
if (path != null) {
Object component = path.getLastPathComponent();
if (component instanceof DefaultMutableTreeNode) {
component = ((DefaultMutableTreeNode)component).getUserObject();
if (component instanceof FilteringTreeStructure.FilteringNode) {
component = ((FilteringTreeStructure.FilteringNode)component).getDelegate();
if (component instanceof AbstractTreeNode) {
return (AbstractTreeNode)component;
}
}
}
}
return null;
}
public boolean navigateSelectedElement() {
final AbstractTreeNode selectedNode = getSelectedNode();
if (ApplicationManager.getApplication().isInternal()) {
String enteredPrefix = getSpeedSearch().getEnteredPrefix();
String itemText = getSpeedSearchText(selectedNode);
if (StringUtil.isNotEmpty(enteredPrefix) && StringUtil.isNotEmpty(itemText)) {
LOG.info("Chosen in file structure popup by prefix '" + enteredPrefix + "': '" + itemText + "'");
}
}
final Ref<Boolean> succeeded = new Ref<Boolean>();
final CommandProcessor commandProcessor = CommandProcessor.getInstance();
commandProcessor.executeCommand(myProject, new Runnable() {
@Override
public void run() {
if (selectedNode != null) {
if (selectedNode.canNavigateToSource()) {
myPopup.cancel();
selectedNode.navigate(true);
succeeded.set(true);
}
else {
succeeded.set(false);
}
}
else {
succeeded.set(false);
}
IdeDocumentHistory.getInstance(myProject).includeCurrentCommandAsNavigation();
}
}, "Navigate", null);
return succeeded.get();
}
private JComponent createSouthPanel() {
final JCheckBox checkBox = new JCheckBox(IdeBundle.message("checkbox.narrow.down.on.typing"));
checkBox.setSelected(PropertiesComponent.getInstance().getBoolean(narrowDownPropertyKey, true));
checkBox.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
myShouldNarrowDown = checkBox.isSelected();
PropertiesComponent.getInstance().setValue(narrowDownPropertyKey, Boolean.toString(myShouldNarrowDown));
if (mySpeedSearch.isPopupActive() && !StringUtil.isEmpty(mySpeedSearch.getEnteredPrefix())) {
myAbstractTreeBuilder.queueUpdate();
}
}
});
checkBox.setFocusable(false);
UIUtil.applyStyle(UIUtil.ComponentStyle.MINI, checkBox);
final JPanel panel = new JPanel(new BorderLayout());
panel.add(checkBox, BorderLayout.WEST);
return panel;
}
private void addCheckbox(final JPanel panel, final TreeAction action) {
String text = action instanceof FileStructureFilter ? ((FileStructureFilter)action).getCheckBoxText() :
action instanceof FileStructureNodeProvider ? ((FileStructureNodeProvider)action).getCheckBoxText() : null;
if (text == null) return;
Shortcut[] shortcuts = action instanceof FileStructureFilter ?
((FileStructureFilter)action).getShortcut() : ((FileStructureNodeProvider)action).getShortcut();
final JCheckBox chkFilter = new JCheckBox();
UIUtil.applyStyle(UIUtil.ComponentStyle.SMALL, chkFilter);
final boolean selected = getDefaultValue(action);
chkFilter.setSelected(selected);
myTreeActionsOwner.setActionIncluded(action, action instanceof FileStructureFilter ? !selected : selected);
chkFilter.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
final boolean state = chkFilter.isSelected();
if (!myAutoClicked.contains(chkFilter)) {
saveState(action, state);
}
myTreeActionsOwner.setActionIncluded(action, action instanceof FileStructureFilter ? !state : state);
//final String filter = mySpeedSearch.isPopupActive() ? mySpeedSearch.getEnteredPrefix() : null;
//mySpeedSearch.hidePopup();
Object selection = ContainerUtil.getFirstItem(myAbstractTreeBuilder.getSelectedElements());
if (selection instanceof FilteringTreeStructure.FilteringNode) {
selection = ((FilteringTreeStructure.FilteringNode)selection).getDelegate();
}
myTreeStructure.rebuildTree();
myFilteringStructure.rebuild();
final Object sel = selection;
final Runnable runnable = new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
myAbstractTreeBuilder.refilter(sel, true, false).doWhenProcessed(new Runnable() {
@Override
public void run() {
if (mySpeedSearch.isPopupActive()) {
mySpeedSearch.refreshSelection();
}
}
});
}
});
}
};
if (ApplicationManager.getApplication().isUnitTestMode()) {
runnable.run();
} else {
ApplicationManager.getApplication().invokeLater(runnable);
}
}
});
chkFilter.setFocusable(false);
if (shortcuts.length > 0) {
text += " (" + KeymapUtil.getShortcutText(shortcuts[0]) + ")";
new AnAction() {
@Override
public void actionPerformed(final AnActionEvent e) {
chkFilter.doClick();
}
}.registerCustomShortcutSet(new CustomShortcutSet(shortcuts), myTree);
}
chkFilter.setText(text);
panel.add(chkFilter);
myCheckBoxes.put(action.getClass(), chkFilter);
}
private static boolean getDefaultValue(TreeAction action) {
if (action instanceof PropertyOwner) {
final String propertyName = ((PropertyOwner)action).getPropertyName();
return PropertiesComponent.getInstance().getBoolean(getPropertyName(propertyName), false);
}
return false;
}
private static void saveState(TreeAction action, boolean state) {
if (action instanceof PropertyOwner) {
final String propertyName = ((PropertyOwner)action).getPropertyName();
PropertiesComponent.getInstance().setValue(getPropertyName(propertyName), Boolean.toString(state));
}
}
@NonNls
public static String getPropertyName(String propertyName) {
return propertyName + ".file.structure.state";
}
public void setTitle(String title) {
myTitle = title;
}
public Tree getTree() {
return myTree;
}
public TreeSpeedSearch getSpeedSearch() {
return mySpeedSearch;
}
public FilteringTreeBuilder getTreeBuilder() {
return myAbstractTreeBuilder;
}
public void setSearchFilterForTests(String filter) {
myTestSearchFilter = filter;
}
public void setTreeActionState(Class<? extends TreeAction> action, boolean state) {
final JCheckBox checkBox = myCheckBoxes.get(action);
if (checkBox != null) {
checkBox.setSelected(state);
for (ActionListener listener : checkBox.getActionListeners()) {
listener.actionPerformed(new ActionEvent(this, 1, ""));
}
}
}
@Nullable
public static String getSpeedSearchText(final Object userObject) {
String text = String.valueOf(userObject);
if (text != null) {
if (userObject instanceof StructureViewComponent.StructureViewTreeElementWrapper) {
final TreeElement value = ((StructureViewComponent.StructureViewTreeElementWrapper)userObject).getValue();
if (value instanceof PsiTreeElementBase && ((PsiTreeElementBase)value).isSearchInLocationString()) {
final String locationString = ((PsiTreeElementBase)value).getLocationString();
if (!StringUtil.isEmpty(locationString)) {
String locationPrefix = null;
String locationSuffix = null;
if (value instanceof LocationPresentation) {
locationPrefix = ((LocationPresentation)value).getLocationPrefix();
locationSuffix = ((LocationPresentation)value).getLocationSuffix();
}
return text +
StringUtil.notNullize(locationPrefix, LocationPresentation.DEFAULT_LOCATION_PREFIX) +
locationString +
StringUtil.notNullize(locationSuffix, LocationPresentation.DEFAULT_LOCATION_SUFFIX);
}
}
}
return text;
}
if (userObject instanceof StructureViewComponent.StructureViewTreeElementWrapper) {
return ApplicationManager.getApplication().runReadAction(new Computable<String>() {
@Nullable
@Override
public String compute() {
final ItemPresentation presentation =
((StructureViewComponent.StructureViewTreeElementWrapper)userObject).getValue().getPresentation();
return presentation.getPresentableText();
}
});
}
return null;
}
private class FileStructurePopupFilter implements ElementFilter {
private String myLastFilter = null;
private HashSet<Object> myVisibleParents = new HashSet<Object>();
private final boolean isUnitTest = ApplicationManager.getApplication().isUnitTestMode();
@Override
public boolean shouldBeShowing(Object value) {
if (!myShouldNarrowDown) return true;
String filter = getSearchPrefix();
if (!StringUtil.equals(myLastFilter, filter)) {
myVisibleParents.clear();
myLastFilter = filter;
}
if (filter != null) {
if (myVisibleParents.contains(value)) {
return true;
}
final String text = getSpeedSearchText(value);
if (text == null) return false;
if (matches(text)) {
Object o = value;
while (o instanceof FilteringTreeStructure.FilteringNode && (o = ((FilteringTreeStructure.FilteringNode)o).getParent()) != null) {
myVisibleParents.add(o);
}
return true;
} else {
return false;
}
}
return true;
}
private boolean matches(@NotNull String text) {
if (isUnitTest) {
final SpeedSearchComparator comparator = mySpeedSearch.getComparator();
return StringUtil.isNotEmpty(myTestSearchFilter) && comparator.matchingFragments(myTestSearchFilter, text) != null;
}
return mySpeedSearch.matchingFragments(text) != null;
}
}
@Nullable
private String getSearchPrefix() {
if (ApplicationManager.getApplication().isUnitTestMode()) return myTestSearchFilter;
return mySpeedSearch != null && !StringUtil.isEmpty(mySpeedSearch.getEnteredPrefix())
? mySpeedSearch.getEnteredPrefix() : null;
}
public class MyTreeSpeedSearch extends TreeSpeedSearch {
public MyTreeSpeedSearch() {
super(myTree, new Convertor<TreePath, String>() {
@Override
@Nullable
public String convert(TreePath path) {
final DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
final Object userObject = node.getUserObject();
if (userObject instanceof FilteringTreeStructure.FilteringNode) {
return getSpeedSearchText(((FilteringTreeStructure.FilteringNode)userObject).getDelegate());
}
return "";
}
}, true);
}
@Override
protected Point getComponentLocationOnScreen() {
return myPopup.getContent().getLocationOnScreen();
}
@Override
protected Rectangle getComponentVisibleRect() {
return myPopup.getContent().getVisibleRect();
}
@Override
public Object findElement(String s) {
final List<SpeedSearchObjectWithWeight> elements = SpeedSearchObjectWithWeight.findElement(s, this);
return elements.isEmpty() ? null : findClosestTo(myInitialPsiElement, elements);
}
@Nullable
private Object findClosestTo(PsiElement path, List<SpeedSearchObjectWithWeight> paths) {
if (path == null || myInitialPsiElement == null) {
return paths.get(0).node;
}
final Set<PsiElement> parents = getAllParents(myInitialPsiElement);
ArrayList<SpeedSearchObjectWithWeight> cur = new ArrayList<SpeedSearchObjectWithWeight>();
int max = -1;
for (SpeedSearchObjectWithWeight p : paths) {
final Object last = ((TreePath)p.node).getLastPathComponent();
final List<PsiElement> elements = new ArrayList<PsiElement>();
final Object object = ((DefaultMutableTreeNode)last).getUserObject();
if (object instanceof FilteringTreeStructure.FilteringNode) {
FilteringTreeStructure.FilteringNode node = (FilteringTreeStructure.FilteringNode)object;
FilteringTreeStructure.FilteringNode candidate = node;
while (node != null) {
elements.add(getPsi(node));
node = node.getParentNode();
}
final int size = ContainerUtil.intersection(parents, elements).size();
if (size == elements.size() - 1 && size == parents.size() - (myInitialNodeIsLeaf ? 1 : 0) && candidate.children().isEmpty()) {
return p.node;
}
if (size > max) {
max = size;
cur.clear();
cur.add(p);
} else if (size == max) {
cur.add(p);
}
}
}
Collections.sort(cur, new Comparator<SpeedSearchObjectWithWeight>() {
@Override
public int compare(SpeedSearchObjectWithWeight o1, SpeedSearchObjectWithWeight o2) {
final int i = o1.compareWith(o2);
return i != 0 ? i
: ((TreePath)o2.node).getPathCount() - ((TreePath)o1.node).getPathCount();
}
});
return cur.isEmpty() ? null : cur.get(0).node;
}
}
class FileStructureTree extends JBTreeWithHintProvider implements AlwaysExpandedTree {
private final boolean fast;
public FileStructureTree(Object rootElement, boolean fastExpand) {
super(new DefaultMutableTreeNode(rootElement));
if (fastExpand) {
boolean newValueIsSet;
try {
final Field field = JTree.class.getDeclaredField("expandedState");
field.setAccessible(true);
field.set(this, new Hashtable() {
@Override
public synchronized Object get(Object key) {
return Boolean.TRUE;
}
});
newValueIsSet = true;
}
catch (Exception e) {
newValueIsSet = false;
}
fast = newValueIsSet;
} else {
fast = false;
}
//TODO[kb]: hack expanded states in getUI().treeState
setRootVisible(false);
setShowsRootHandles(true);
setHorizontalAutoScrollingEnabled(false);
}
@Override
public boolean isAlwaysExpanded() {
return fast;
}
@Override
public boolean isExpanded(TreePath path) {
return fast || super.isExpanded(path);
}
@Override
public boolean isExpanded(int row) {
return fast || super.isExpanded(row);
}
@Override
protected PsiElement getPsiElementForHint(Object selectedValue) {
//noinspection ConstantConditions
return getPsi((FilteringTreeStructure.FilteringNode)((DefaultMutableTreeNode)selectedValue).getUserObject());
}
}
}