Files
openide/platform/lang-impl/src/com/intellij/ide/util/FileStructurePopup.java
Sergey Ignatov ae4940c100 i18n for java structure view
remove replacing "Show" with ""

GitOrigin-RevId: 6f947c605c177f72434aa668d8c85f25c133351e
2020-01-07 12:30:52 +00:00

1060 lines
40 KiB
Java

// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ide.util;
import com.intellij.CommonBundle;
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.icons.AllIcons;
import com.intellij.ide.*;
import com.intellij.ide.actions.ViewStructureAction;
import com.intellij.ide.dnd.aware.DnDAwareTree;
import com.intellij.ide.structureView.ModelListener;
import com.intellij.ide.structureView.StructureView;
import com.intellij.ide.structureView.StructureViewModel;
import com.intellij.ide.structureView.impl.common.PsiTreeElementBase;
import com.intellij.ide.structureView.newStructureView.StructureViewComponent;
import com.intellij.ide.structureView.newStructureView.TreeActionWrapper;
import com.intellij.ide.structureView.newStructureView.TreeActionsOwner;
import com.intellij.ide.structureView.newStructureView.TreeModelWrapper;
import com.intellij.ide.ui.UISettingsListener;
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.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.application.ReadAction;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.fileEditor.TextEditor;
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.*;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.pom.Navigatable;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.StubBasedPsiElement;
import com.intellij.psi.codeStyle.MinusculeMatcher;
import com.intellij.psi.codeStyle.NameUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.ui.*;
import com.intellij.ui.components.JBCheckBox;
import com.intellij.ui.popup.AbstractPopup;
import com.intellij.ui.popup.HintUpdateSupply;
import com.intellij.ui.popup.PopupUpdateProcessor;
import com.intellij.ui.scale.JBUIScale;
import com.intellij.ui.speedSearch.ElementFilter;
import com.intellij.ui.tree.AsyncTreeModel;
import com.intellij.ui.tree.StructureTreeModel;
import com.intellij.ui.tree.TreeVisitor;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.ui.treeStructure.filtered.FilteringTreeStructure;
import com.intellij.util.*;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.JBIterable;
import com.intellij.util.text.TextRangeUtil;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.TextTransferable;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.tree.TreeUtil;
import com.intellij.xml.util.XmlStringUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import org.jetbrains.concurrency.AsyncPromise;
import org.jetbrains.concurrency.Promise;
import org.jetbrains.concurrency.Promises;
import javax.swing.*;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.event.*;
import java.util.List;
import java.util.*;
import java.util.function.BiPredicate;
/**
* @author Konstantin Bulenkov
*/
public class FileStructurePopup implements Disposable, TreeActionsOwner {
private static final Logger LOG = Logger.getInstance(FileStructurePopup.class);
private static final String NARROW_DOWN_PROPERTY_KEY = "FileStructurePopup.narrowDown";
private final Project myProject;
private final FileEditor myFileEditor;
private final StructureViewModel myTreeModelWrapper;
private final StructureViewModel myTreeModel;
private final TreeStructureActionsOwner myTreeActionsOwner;
private JBPopup myPopup;
private String myTitle;
private final Tree myTree;
private final SmartTreeStructure myTreeStructure;
private final FilteringTreeStructure myFilteringStructure;
private final AsyncTreeModel myAsyncTreeModel;
private final StructureTreeModel myStructureTreeModel;
private final TreeSpeedSearch mySpeedSearch;
private final Object myInitialElement;
private final Map<Class, JBCheckBox> myCheckBoxes = new HashMap<>();
private final List<JBCheckBox> myAutoClicked = new ArrayList<>();
private String myTestSearchFilter;
private final ActionCallback myTreeHasBuilt = new ActionCallback();
private final List<Pair<String, JBCheckBox>> myTriggeredCheckboxes = new ArrayList<>();
private final TreeExpander myTreeExpander;
private final CopyPasteDelegator myCopyPasteDelegator;
private boolean myCanClose = true;
private boolean myDisposed;
/**
* @noinspection unused
* @deprecated use {@link #FileStructurePopup(Project, FileEditor, StructureViewModel)}
*/
@Deprecated
public FileStructurePopup(@NotNull Project project,
@NotNull FileEditor fileEditor,
@NotNull StructureView structureView,
boolean applySortAndFilter) {
this(project, fileEditor, ViewStructureAction.createStructureViewModel(project, fileEditor, structureView));
Disposer.register(this, structureView);
}
public FileStructurePopup(@NotNull Project project,
@NotNull FileEditor fileEditor,
@NotNull StructureViewModel treeModel) {
myProject = project;
myFileEditor = fileEditor;
myTreeModel = treeModel;
//Stop code analyzer to speedup EDT
DaemonCodeAnalyzer.getInstance(myProject).disableUpdateByTimer(this);
IdeFocusManager.getInstance(myProject).typeAheadUntil(myTreeHasBuilt, "FileStructurePopup");
myTreeActionsOwner = new TreeStructureActionsOwner(myTreeModel);
myTreeActionsOwner.setActionIncluded(Sorter.ALPHA_SORTER, true);
myTreeModelWrapper = new TreeModelWrapper(myTreeModel, myTreeActionsOwner);
Disposer.register(this, myTreeModelWrapper);
myTreeStructure = new SmartTreeStructure(project, myTreeModelWrapper) {
@Override
public void rebuildTree() {
if (!ApplicationManager.getApplication().isUnitTestMode() && myPopup.isDisposed()) {
return;
}
ProgressManager.getInstance().computePrioritized(() -> {
super.rebuildTree();
myFilteringStructure.rebuild();
return null;
});
}
@Override
public boolean isToBuildChildrenInBackground(@NotNull Object element) {
return getRootElement() == element;
}
@NotNull
@Override
protected TreeElementWrapper createTree() {
return StructureViewComponent.createWrapper(myProject, myModel.getRoot(), myModel);
}
@NonNls
@Override
public String toString() {
return "structure view tree structure(model=" + myTreeModelWrapper + ")";
}
};
FileStructurePopupFilter filter = new FileStructurePopupFilter();
myFilteringStructure = new FilteringTreeStructure(filter, myTreeStructure, false);
myStructureTreeModel = new StructureTreeModel<>(myFilteringStructure, this);
myAsyncTreeModel = new AsyncTreeModel(myStructureTreeModel, this);
myAsyncTreeModel.setRootImmediately(myStructureTreeModel.getRootImmediately());
myTree = new MyTree(myAsyncTreeModel);
StructureViewComponent.registerAutoExpandListener(myTree, myTreeModel);
ModelListener modelListener = () -> rebuild(false);
myTreeModel.addModelListener(modelListener);
Disposer.register(this, () -> myTreeModel.removeModelListener(modelListener));
myTree.setCellRenderer(new NodeRenderer());
myProject.getMessageBus().connect(this).subscribe(UISettingsListener.TOPIC, o -> rebuild(false));
myTree.setTransferHandler(new TransferHandler() {
@Override
public boolean importData(@NotNull TransferSupport support) {
String s = CopyPasteManager.getInstance().getContents(DataFlavor.stringFlavor);
if (s != null && !mySpeedSearch.isPopupActive()) {
mySpeedSearch.showPopup(s);
return true;
}
return false;
}
@Nullable
@Override
protected Transferable createTransferable(JComponent component) {
JBIterable<Pair<FilteringTreeStructure.FilteringNode, PsiElement>> pairs = JBIterable.of(myTree.getSelectionPaths())
.filterMap(TreeUtil::getLastUserObject)
.filter(FilteringTreeStructure.FilteringNode.class)
.filterMap(o -> o.getDelegate() instanceof PsiElement ? Pair.create(o, (PsiElement)o.getDelegate()) : null)
.collect();
if (pairs.isEmpty()) return null;
Set<PsiElement> psiSelection = pairs.map(Functions.pairSecond()).toSet();
String text = StringUtil.join(pairs, pair -> {
PsiElement psi = pair.second;
String defaultPresentation = pair.first.getPresentation().getPresentableText();
if (psi == null) return defaultPresentation;
for (PsiElement p = psi.getParent(); p != null; p = p.getParent()) {
if (psiSelection.contains(p)) return null;
}
return ObjectUtils.chooseNotNull(psi.getText(), defaultPresentation);
}, "\n");
String htmlText = "<body>\n" + text + "\n</body>";
return new TextTransferable(XmlStringUtil.wrapInHtml(htmlText), text);
}
@Override
public int getSourceActions(JComponent component) {
return COPY;
}
});
mySpeedSearch = new MyTreeSpeedSearch();
mySpeedSearch.setComparator(new SpeedSearchComparator(false, true) {
@NotNull
@Override
protected MinusculeMatcher createMatcher(@NotNull String pattern) {
return NameUtil.buildMatcher(pattern).withSeparators(" ()").build();
}
});
myTreeExpander = new DefaultTreeExpander(myTree);
myCopyPasteDelegator = new CopyPasteDelegator(myProject, myTree);
myInitialElement = myTreeModel.getCurrentEditorElement();
TreeUtil.installActions(myTree);
}
public void show() {
JComponent panel = createCenterPanel();
MnemonicHelper.init(panel);
myTree.addTreeSelectionListener(__ -> {
if (myPopup.isVisible()) {
PopupUpdateProcessor updateProcessor = myPopup.getUserData(PopupUpdateProcessor.class);
if (updateProcessor != null) {
AbstractTreeNode node = getSelectedNode();
updateProcessor.updatePopup(node);
}
}
});
myPopup = JBPopupFactory.getInstance().createComponentPopupBuilder(panel, myTree)
.setTitle(myTitle)
.setResizable(true)
.setModalContext(false)
.setFocusable(true)
.setRequestFocus(true)
.setMovable(true)
.setBelongsToGlobalPopupStack(true)
//.setCancelOnClickOutside(false) //for debug and snapshots
.setCancelOnOtherWindowOpen(true)
.setCancelKeyEnabled(false)
.setDimensionServiceKey(myProject, getDimensionServiceKey(), true)
.setCancelCallback(() -> myCanClose)
.setNormalWindowLevel(true)
.createPopup();
Disposer.register(myPopup, this);
Disposer.register(myPopup, () -> {
if (!myTreeHasBuilt.isDone()) {
myTreeHasBuilt.setRejected();
}
});
myTree.getEmptyText().setText(CommonBundle.getLoadingTreeNodeText());
myPopup.showCenteredInCurrentWindow(myProject);
((AbstractPopup)myPopup).setShowHints(true);
IdeFocusManager.getInstance(myProject).requestFocus(myTree, true);
rebuildAndSelect(false, myInitialElement).onProcessed(path -> UIUtil.invokeLaterIfNeeded(() -> {
TreeUtil.ensureSelection(myTree);
myTreeHasBuilt.setDone();
installUpdater();
}));
}
private void installUpdater() {
if (ApplicationManager.getApplication().isUnitTestMode() || myPopup.isDisposed()) {
return;
}
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) ? "Structure is empty" : "'" + prefix + "' not found");
if (prefix == null) prefix = "";
if (!filter.equals(prefix)) {
boolean isBackspace = prefix.length() < filter.length();
filter = prefix;
rebuild(true).onProcessed(ignore -> UIUtil.invokeLaterIfNeeded(() -> {
if (isDisposed()) return;
TreeUtil.promiseExpandAll(myTree);
if (isBackspace && handleBackspace(filter)) {
return;
}
if (myFilteringStructure.getRootElement().getChildren().length == 0) {
for (JBCheckBox 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;
Iterator<Pair<String, JBCheckBox>> iterator = myTriggeredCheckboxes.iterator();
while (iterator.hasNext()) {
Pair<String, JBCheckBox> next = iterator.next();
if (next.getFirst().length() < filter.length()) break;
iterator.remove();
next.getSecond().doClick();
clicked = true;
}
return clicked;
}
@NotNull
public Promise<TreePath> select(Object element) {
int[] stage = {1, 0}; // 1 - first pass, 2 - optimization applied, 3 - retry w/o optimization
TreePath[] deepestPath = {null};
TreeVisitor visitor = path -> {
Object last = path.getLastPathComponent();
Object userObject = StructureViewComponent.unwrapNavigatable(last);
Object value = StructureViewComponent.unwrapValue(last);
if (Comparing.equal(value, element) ||
userObject instanceof AbstractTreeNode && ((AbstractTreeNode)userObject).canRepresent(element)) {
return TreeVisitor.Action.INTERRUPT;
}
if (value instanceof PsiElement && element instanceof PsiElement) {
if (PsiTreeUtil.isAncestor((PsiElement)value, (PsiElement)element, true)) {
int count = path.getPathCount();
if (stage[1] == 0 || stage[1] < count) {
stage[1] = count;
deepestPath[0] = path;
}
}
else if (stage[0] != 3) {
stage[0] = 2;
return TreeVisitor.Action.SKIP_CHILDREN;
}
}
return TreeVisitor.Action.CONTINUE;
};
Function<TreePath, Promise<TreePath>> action = path -> {
myTree.expandPath(path);
TreeUtil.selectPath(myTree, path);
TreeUtil.ensureSelection(myTree);
return Promises.resolvedPromise(path);
};
Function<TreePath, Promise<TreePath>> fallback = new Function<TreePath, Promise<TreePath>>() {
@Override
public Promise<TreePath> fun(TreePath path) {
if (path == null && stage[0] == 2) {
// Some structure views merge unrelated psi elements into a structure node (MarkdownStructureViewModel).
// So turn off the isAncestor() optimization and retry once.
stage[0] = 3;
return myAsyncTreeModel.accept(visitor).thenAsync(this);
}
else {
TreePath adjusted = path == null ? deepestPath[0] : path;
if (path == null && adjusted != null && element instanceof PsiElement) {
Object minChild = findClosestPsiElement((PsiElement)element, adjusted, myAsyncTreeModel);
if (minChild != null) adjusted = adjusted.pathByAddingChild(minChild);
}
return adjusted == null ? Promises.rejectedPromise() : action.fun(adjusted);
}
}
};
return myAsyncTreeModel
.accept(visitor)
.thenAsync(fallback);
}
@TestOnly
public AsyncPromise<Void> rebuildAndUpdate() {
AsyncPromise<Void> result = new AsyncPromise<>();
TreeVisitor visitor = path -> {
AbstractTreeNode node = TreeUtil.getLastUserObject(AbstractTreeNode.class, path);
if (node != null) node.update();
return TreeVisitor.Action.CONTINUE;
};
rebuild(false).onProcessed(ignore1 -> myAsyncTreeModel.accept(visitor).onProcessed(ignore2 -> result.setResult(null)));
return result;
}
public boolean isDisposed() {
return myDisposed;
}
@Override
public void dispose() {
myDisposed = true;
}
private static boolean isShouldNarrowDown() {
return PropertiesComponent.getInstance().getBoolean(NARROW_DOWN_PROPERTY_KEY, true);
}
@NonNls
protected static String getDimensionServiceKey() {
return "StructurePopup";
}
@Nullable
public PsiElement getCurrentElement(@Nullable final PsiFile psiFile) {
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
Object elementAtCursor = myTreeModelWrapper.getCurrentEditorElement();
if (elementAtCursor instanceof PsiElement) {
return (PsiElement)elementAtCursor;
}
if (psiFile != null && myFileEditor instanceof TextEditor) {
return psiFile.getViewProvider().findElementAt(((TextEditor)myFileEditor).getEditor().getCaretModel().getOffset());
}
return null;
}
public JComponent createCenterPanel() {
List<FileStructureFilter> fileStructureFilters = new ArrayList<>();
List<FileStructureNodeProvider> fileStructureNodeProviders = new ArrayList<>();
if (myTreeActionsOwner != null) {
for (Filter filter : myTreeModel.getFilters()) {
if (filter instanceof FileStructureFilter) {
FileStructureFilter fsFilter = (FileStructureFilter)filter;
myTreeActionsOwner.setActionIncluded(fsFilter, true);
fileStructureFilters.add(fsFilter);
}
}
if (myTreeModel instanceof ProvidingTreeModel) {
for (NodeProvider provider : ((ProvidingTreeModel)myTreeModel).getNodeProviders()) {
if (provider instanceof FileStructureNodeProvider) {
fileStructureNodeProviders.add((FileStructureNodeProvider)provider);
}
}
}
}
int checkBoxCount = fileStructureNodeProviders.size() + fileStructureFilters.size();
JPanel panel = new JPanel(new BorderLayout());
panel.setPreferredSize(JBUI.size(540, 500));
JPanel chkPanel = new JPanel(new GridLayout(0, checkBoxCount > 0 && checkBoxCount % 4 == 0 ? checkBoxCount / 2 : 3,
JBUIScale.scale(UIUtil.DEFAULT_HGAP), 0));
chkPanel.setOpaque(false);
Shortcut[] F4 = ActionManager.getInstance().getAction(IdeActions.ACTION_EDIT_SOURCE).getShortcutSet().getShortcuts();
Shortcut[] ENTER = CustomShortcutSet.fromString("ENTER").getShortcuts();
CustomShortcutSet shortcutSet = new CustomShortcutSet(ArrayUtil.mergeArrays(F4, ENTER));
new DumbAwareAction() {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
boolean succeeded = navigateSelectedElement();
if (succeeded) {
unregisterCustomShortcutSet(panel);
}
}
}.registerCustomShortcutSet(shortcutSet, panel);
DumbAwareAction.create(e -> {
if (mySpeedSearch != null && mySpeedSearch.isPopupActive()) {
mySpeedSearch.hidePopup();
}
else {
myPopup.cancel();
}
}).registerCustomShortcutSet(CustomShortcutSet.fromString("ESCAPE"), myTree);
new ClickListener() {
@Override
public boolean onClick(@NotNull MouseEvent e, int clickCount) {
TreePath path = myTree.getClosestPathForLocation(e.getX(), e.getY());
Rectangle bounds = path == null ? null : myTree.getPathBounds(path);
if (bounds == null ||
bounds.x > e.getX() ||
bounds.y > e.getY() || bounds.y + bounds.height < e.getY()) return false;
navigateSelectedElement();
return true;
}
}.installOn(myTree);
for (FileStructureFilter filter : fileStructureFilters) {
addCheckbox(chkPanel, filter);
}
for (FileStructureNodeProvider provider : fileStructureNodeProviders) {
addCheckbox(chkPanel, provider);
}
JPanel topPanel = new JPanel(new BorderLayout());
topPanel.add(chkPanel, BorderLayout.WEST);
topPanel.add(createSettingsButton(), BorderLayout.EAST);
topPanel.setBackground(JBUI.CurrentTheme.Popup.toolbarPanelColor());
Dimension prefSize = topPanel.getPreferredSize();
prefSize.height = JBUI.CurrentTheme.Popup.toolbarHeight();
topPanel.setPreferredSize(prefSize);
topPanel.setBorder(JBUI.Borders.emptyLeft(UIUtil.DEFAULT_HGAP));
panel.add(topPanel, BorderLayout.NORTH);
JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myTree);
scrollPane.setBorder(IdeBorderFactory.createBorder(JBUI.CurrentTheme.Popup.toolbarBorderColor(), SideBorder.TOP | SideBorder.BOTTOM));
panel.add(scrollPane, BorderLayout.CENTER);
DataManager.registerDataProvider(panel, dataId -> {
if (CommonDataKeys.PROJECT.is(dataId)) {
return myProject;
}
if (PlatformDataKeys.FILE_EDITOR.is(dataId)) {
return myFileEditor;
}
if (OpenFileDescriptor.NAVIGATE_IN_EDITOR.is(dataId)) {
if (myFileEditor instanceof TextEditor) {
return ((TextEditor)myFileEditor).getEditor();
}
}
if (CommonDataKeys.PSI_ELEMENT.is(dataId)) {
return getSelectedElements().filter(PsiElement.class).first();
}
if (LangDataKeys.PSI_ELEMENT_ARRAY.is(dataId)) {
return PsiUtilCore.toPsiElementArray(getSelectedElements().filter(PsiElement.class).toList());
}
if (CommonDataKeys.NAVIGATABLE.is(dataId)) {
return getSelectedElements().filter(Navigatable.class).first();
}
if (CommonDataKeys.NAVIGATABLE_ARRAY.is(dataId)) {
List<Navigatable> result = getSelectedElements().filter(Navigatable.class).toList();
return result.isEmpty() ? null : result.toArray(new Navigatable[0]);
}
if (LangDataKeys.POSITION_ADJUSTER_POPUP.is(dataId)) {
return myPopup;
}
if (PlatformDataKeys.COPY_PROVIDER.is(dataId)) {
return myCopyPasteDelegator.getCopyProvider();
}
if (PlatformDataKeys.TREE_EXPANDER.is(dataId)) {
return myTreeExpander;
}
return null;
});
panel.addFocusListener(new FocusAdapter() {
@Override
public void focusLost(FocusEvent e) {
myPopup.cancel();
}
});
return panel;
}
@NotNull
private JBIterable<Object> getSelectedElements() {
return JBIterable.of(myTree.getSelectionPaths())
.filterMap(o -> StructureViewComponent.unwrapValue(o.getLastPathComponent()));
}
@NotNull
private JComponent createSettingsButton() {
JLabel label = new JLabel(AllIcons.General.GearPlain);
label.setBorder(JBUI.Borders.empty(0, 4));
label.setHorizontalAlignment(SwingConstants.RIGHT);
label.setVerticalAlignment(SwingConstants.CENTER);
List<AnAction> sorters = createSorters();
new ClickListener() {
@Override
public boolean onClick(@NotNull MouseEvent event, int clickCount) {
DefaultActionGroup group = new DefaultActionGroup();
if (!sorters.isEmpty()) {
group.addAll(sorters);
group.addSeparator();
}
//addGroupers(group);
//addFilters(group);
group.add(new ToggleAction(IdeBundle.message("checkbox.narrow.down.on.typing")) {
@Override
public boolean isSelected(@NotNull AnActionEvent e) {
return isShouldNarrowDown();
}
@Override
public void setSelected(@NotNull AnActionEvent e, boolean state) {
PropertiesComponent.getInstance().setValue(NARROW_DOWN_PROPERTY_KEY, Boolean.toString(state));
if (mySpeedSearch.isPopupActive() && !StringUtil.isEmpty(mySpeedSearch.getEnteredPrefix())) {
rebuild(true);
}
}
});
DataManager dataManager = DataManager.getInstance();
ListPopup popup = JBPopupFactory.getInstance().createActionGroupPopup(
null, group, dataManager.getDataContext(label), JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, false);
popup.addListener(new JBPopupListener() {
@Override
public void onClosed(@NotNull LightweightWindowEvent event) {
myCanClose = true;
}
});
myCanClose = false;
popup.showUnderneathOf(label);
return true;
}
}.installOn(label);
return label;
}
private List<AnAction> createSorters() {
List<AnAction> actions = new ArrayList<>();
for (Sorter sorter : myTreeModel.getSorters()) {
if (sorter.isVisible()) {
actions.add(new MyTreeActionWrapper(sorter));
}
}
return actions;
}
@Nullable
private static Object findClosestPsiElement(@NotNull PsiElement element,
@NotNull TreePath adjusted,
@NotNull TreeModel treeModel) {
TextRange range = element.getTextRange();
if (range == null) return null;
Object parent = adjusted.getLastPathComponent();
int minDistance = 0;
Object minChild = null;
for (int i = 0, count = treeModel.getChildCount(parent); i < count; i++) {
Object child = treeModel.getChild(parent, i);
Object value = StructureViewComponent.unwrapValue(child);
if (value instanceof StubBasedPsiElement && ((StubBasedPsiElement)value).getStub() != null) continue;
TextRange r = value instanceof PsiElement ? ((PsiElement)value).getTextRange() : null;
if (r == null) continue;
int distance = TextRangeUtil.getDistance(range, r);
if (minChild == null || distance < minDistance) {
minDistance = distance;
minChild = child;
}
}
return minChild;
}
private class MyTreeActionWrapper extends TreeActionWrapper {
private final TreeAction myAction;
MyTreeActionWrapper(TreeAction action) {
super(action, myTreeActionsOwner);
myAction = action;
myTreeActionsOwner.setActionIncluded(action, getDefaultValue(action));
}
@Override
public void update(@NotNull AnActionEvent e) {
super.update(e);
e.getPresentation().setIcon(null);
}
@Override
public void setSelected(@NotNull AnActionEvent e, boolean state) {
boolean actionState = TreeModelWrapper.shouldRevert(myAction) != state;
myTreeActionsOwner.setActionIncluded(myAction, actionState);
saveState(myAction, state);
rebuild(false).onProcessed(ignore -> {
if (mySpeedSearch.isPopupActive()) {
mySpeedSearch.refreshSelection();
}
});
}
}
@Nullable
private AbstractTreeNode getSelectedNode() {
TreePath path = myTree.getSelectionPath();
Object o = StructureViewComponent.unwrapNavigatable(path == null ? null : path.getLastPathComponent());
return o instanceof AbstractTreeNode ? (AbstractTreeNode)o : null;
}
private boolean navigateSelectedElement() {
AbstractTreeNode selectedNode = getSelectedNode();
if (ApplicationManager.getApplication().isInternal()) {
String enteredPrefix = mySpeedSearch.getEnteredPrefix();
String itemText = getSpeedSearchText(selectedNode);
if (StringUtil.isNotEmpty(enteredPrefix) && StringUtil.isNotEmpty(itemText)) {
LOG.info("Chosen in file structure popup by prefix '" + enteredPrefix + "': '" + itemText + "'");
}
}
Ref<Boolean> succeeded = new Ref<>();
CommandProcessor commandProcessor = CommandProcessor.getInstance();
commandProcessor.executeCommand(myProject, () -> {
if (selectedNode != null) {
if (selectedNode.canNavigateToSource()) {
selectedNode.navigate(true);
myPopup.cancel();
succeeded.set(true);
}
else {
succeeded.set(false);
}
}
else {
succeeded.set(false);
}
IdeDocumentHistory.getInstance(myProject).includeCurrentCommandAsNavigation();
}, "Navigate", null);
return succeeded.get();
}
private void addCheckbox(JPanel panel, TreeAction action) {
String text = action instanceof FileStructureFilter ? ((FileStructureFilter)action).getCheckBoxText() :
action instanceof FileStructureNodeProvider ? ((FileStructureNodeProvider)action).getCheckBoxText() : null;
if (text == null) return;
Shortcut[] shortcuts = extractShortcutFor(action);
JBCheckBox checkBox = new JBCheckBox();
checkBox.setOpaque(false);
UIUtil.applyStyle(UIUtil.ComponentStyle.SMALL, checkBox);
boolean selected = getDefaultValue(action);
checkBox.setSelected(selected);
boolean isRevertedStructureFilter = action instanceof FileStructureFilter && ((FileStructureFilter)action).isReverted();
myTreeActionsOwner.setActionIncluded(action, isRevertedStructureFilter != selected);
checkBox.addActionListener(__ -> {
boolean state = checkBox.isSelected();
if (!myAutoClicked.contains(checkBox)) {
saveState(action, state);
}
myTreeActionsOwner.setActionIncluded(action, isRevertedStructureFilter != state);
rebuild(false).onProcessed(ignore -> {
if (mySpeedSearch.isPopupActive()) {
mySpeedSearch.refreshSelection();
}
});
});
checkBox.setFocusable(false);
if (shortcuts.length > 0) {
text += " (" + KeymapUtil.getShortcutText(shortcuts[0]) + ")";
DumbAwareAction.create(e -> checkBox.doClick())
.registerCustomShortcutSet(new CustomShortcutSet(shortcuts), myTree);
}
checkBox.setText(text);
panel.add(checkBox);
myCheckBoxes.put(action.getClass(), checkBox);
}
@NotNull
private Promise<Void> rebuild(boolean refilterOnly) {
Object selection = JBIterable.of(myTree.getSelectionPaths())
.filterMap(o -> StructureViewComponent.unwrapValue(o.getLastPathComponent())).first();
return rebuildAndSelect(refilterOnly, selection).then(o -> null);
}
@NotNull
private Promise<TreePath> rebuildAndSelect(boolean refilterOnly, Object selection) {
AsyncPromise<TreePath> result = new AsyncPromise<>();
myStructureTreeModel.getInvoker().invoke(() -> {
if (refilterOnly) {
myFilteringStructure.refilter();
myStructureTreeModel.invalidate().onSuccess(
res ->
(selection == null ? myAsyncTreeModel.accept(o -> TreeVisitor.Action.CONTINUE) : select(selection))
.onError(ignore2 -> result.setError("rejected"))
.onSuccess(p -> UIUtil.invokeLaterIfNeeded(
() -> {
TreeUtil.expand(getTree(), myTreeModel instanceof StructureViewCompositeModel ? 3 : 2);
TreeUtil.ensureSelection(myTree);
mySpeedSearch.refreshSelection();
result.setResult(p);
})));
}
else {
myTreeStructure.rebuildTree();
myStructureTreeModel.invalidate().onSuccess(res -> rebuildAndSelect(true, selection).processed(result));
}
});
return result;
}
@NotNull
static Shortcut[] extractShortcutFor(@NotNull TreeAction action) {
if (action instanceof ActionShortcutProvider) {
String actionId = ((ActionShortcutProvider)action).getActionIdForShortcut();
return KeymapUtil.getActiveKeymapShortcuts(actionId).getShortcuts();
}
return action instanceof FileStructureFilter ?
((FileStructureFilter)action).getShortcut() : ((FileStructureNodeProvider)action).getShortcut();
}
private static boolean getDefaultValue(TreeAction action) {
String propertyName = action instanceof PropertyOwner ? ((PropertyOwner)action).getPropertyName() : action.getName();
return PropertiesComponent.getInstance().getBoolean(TreeStructureUtil.getPropertyName(propertyName), Sorter.ALPHA_SORTER.equals(action));
}
private static void saveState(TreeAction action, boolean state) {
String propertyName = action instanceof PropertyOwner ? ((PropertyOwner)action).getPropertyName() : action.getName();
PropertiesComponent.getInstance().setValue(TreeStructureUtil.getPropertyName(propertyName), state, Sorter.ALPHA_SORTER.equals(action));
}
public void setTitle(String title) {
myTitle = title;
}
@NotNull
public Tree getTree() {
return myTree;
}
@TestOnly
public TreeSpeedSearch getSpeedSearch() {
return mySpeedSearch;
}
@TestOnly
public void setSearchFilterForTests(String filter) {
myTestSearchFilter = filter;
}
public void setTreeActionState(Class<? extends TreeAction> action, boolean state) {
JBCheckBox 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(Object object) {
String text = String.valueOf(object);
Object value = StructureViewComponent.unwrapWrapper(object);
if (text != null) {
if (value instanceof PsiTreeElementBase && ((PsiTreeElementBase)value).isSearchInLocationString()) {
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;
}
// NB!: this point is achievable if the following method returns null
// see com.intellij.ide.util.treeView.NodeDescriptor.toString
if (value instanceof TreeElement) {
return ReadAction.compute(() -> ((TreeElement)value).getPresentation().getPresentableText());
}
return null;
}
@Override
public void setActionActive(String name, boolean state) {
}
@Override
public boolean isActionActive(String name) {
return false;
}
private class FileStructurePopupFilter implements ElementFilter {
private String myLastFilter;
private final Set<Object> myVisibleParents = new HashSet<>();
private final boolean isUnitTest = ApplicationManager.getApplication().isUnitTestMode();
@Override
public boolean shouldBeShowing(Object value) {
if (!isShouldNarrowDown()) return true;
String filter = getSearchPrefix();
if (!StringUtil.equals(myLastFilter, filter)) {
myVisibleParents.clear();
myLastFilter = filter;
}
if (filter != null) {
if (myVisibleParents.contains(value)) {
return true;
}
String text = getSpeedSearchText(value);
if (text == null) return false;
if (matches(filter, 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 filter, @NotNull String text) {
return (isUnitTest || mySpeedSearch.isPopupActive()) &&
StringUtil.isNotEmpty(filter) &&
mySpeedSearch.getComparator().matchingFragments(filter, text) != null;
}
}
@Nullable
private String getSearchPrefix() {
if (ApplicationManager.getApplication().isUnitTestMode()) return myTestSearchFilter;
return mySpeedSearch != null && !StringUtil.isEmpty(mySpeedSearch.getEnteredPrefix())
? mySpeedSearch.getEnteredPrefix() : null;
}
private class MyTreeSpeedSearch extends TreeSpeedSearch {
MyTreeSpeedSearch() {
super(myTree, path -> getSpeedSearchText(TreeUtil.getLastUserObject(path)), true);
}
@Override
protected Point getComponentLocationOnScreen() {
return myPopup.getContent().getLocationOnScreen();
}
@Override
protected Rectangle getComponentVisibleRect() {
return myPopup.getContent().getVisibleRect();
}
@Override
public Object findElement(String s) {
List<SpeedSearchObjectWithWeight> elements = SpeedSearchObjectWithWeight.findElement(s, this);
SpeedSearchObjectWithWeight best = ContainerUtil.getFirstItem(elements);
if (best == null) return null;
if (myInitialElement instanceof PsiElement) {
PsiElement initial = (PsiElement)myInitialElement;
// find children of the initial element
SpeedSearchObjectWithWeight bestForParent = find(initial, elements, FileStructurePopup::isParent);
if (bestForParent != null) return bestForParent.node;
// find siblings of the initial element
PsiElement parent = initial.getParent();
if (parent != null) {
SpeedSearchObjectWithWeight bestSibling = find(parent, elements, FileStructurePopup::isParent);
if (bestSibling != null) return bestSibling.node;
}
// find grand children of the initial element
SpeedSearchObjectWithWeight bestForAncestor = find(initial, elements, FileStructurePopup::isAncestor);
if (bestForAncestor != null) return bestForAncestor.node;
}
return best.node;
}
}
@Nullable
private static SpeedSearchObjectWithWeight find(@NotNull PsiElement element,
@NotNull List<? extends SpeedSearchObjectWithWeight> objects,
@NotNull BiPredicate<? super PsiElement, ? super TreePath> predicate) {
return ContainerUtil.find(objects, object -> predicate.test(element, ObjectUtils.tryCast(object.node, TreePath.class)));
}
private static boolean isElement(@NotNull PsiElement element, @Nullable TreePath path) {
return element.equals(StructureViewComponent.unwrapValue(TreeUtil.getLastUserObject(FilteringTreeStructure.FilteringNode.class, path)));
}
private static boolean isParent(@NotNull PsiElement parent, @Nullable TreePath path) {
return path != null && isElement(parent, path.getParentPath());
}
private static boolean isAncestor(@NotNull PsiElement ancestor, @Nullable TreePath path) {
while (path != null) {
if (isElement(ancestor, path)) return true;
path = path.getParentPath();
}
return false;
}
static class MyTree extends DnDAwareTree implements PlaceProvider<String> {
MyTree(TreeModel treeModel) {
super(treeModel);
setRootVisible(false);
setShowsRootHandles(true);
HintUpdateSupply.installHintUpdateSupply(this, o -> {
Object value = StructureViewComponent.unwrapValue(o);
return value instanceof PsiElement ? (PsiElement)value : null;
});
}
@Override
public String getPlace() {
return ActionPlaces.STRUCTURE_VIEW_POPUP;
}
}
}