file structure filtering tests
[idea/community.git] / platform / lang-impl / src / com / intellij / ide / util / FileStructurePopup.java
1 /*
2  * Copyright 2000-2011 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.ide.util;
17
18 import com.intellij.ide.DataManager;
19 import com.intellij.ide.IdeBundle;
20 import com.intellij.ide.structureView.StructureViewModel;
21 import com.intellij.ide.structureView.StructureViewTreeElement;
22 import com.intellij.ide.structureView.impl.common.PsiTreeElementBase;
23 import com.intellij.ide.structureView.newStructureView.StructureViewComponent;
24 import com.intellij.ide.structureView.newStructureView.TreeModelWrapper;
25 import com.intellij.ide.util.treeView.AbstractTreeNode;
26 import com.intellij.ide.util.treeView.NodeRenderer;
27 import com.intellij.ide.util.treeView.smartTree.*;
28 import com.intellij.navigation.ItemPresentation;
29 import com.intellij.openapi.Disposable;
30 import com.intellij.openapi.MnemonicHelper;
31 import com.intellij.openapi.actionSystem.*;
32 import com.intellij.openapi.application.AccessToken;
33 import com.intellij.openapi.application.ApplicationManager;
34 import com.intellij.openapi.command.CommandProcessor;
35 import com.intellij.openapi.editor.Editor;
36 import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory;
37 import com.intellij.openapi.keymap.KeymapUtil;
38 import com.intellij.openapi.project.Project;
39 import com.intellij.openapi.ui.popup.JBPopup;
40 import com.intellij.openapi.ui.popup.JBPopupFactory;
41 import com.intellij.openapi.util.*;
42 import com.intellij.openapi.util.text.StringUtil;
43 import com.intellij.openapi.wm.IdeFocusManager;
44 import com.intellij.psi.PsiDocumentManager;
45 import com.intellij.psi.PsiElement;
46 import com.intellij.psi.PsiFile;
47 import com.intellij.ui.*;
48 import com.intellij.ui.popup.AbstractPopup;
49 import com.intellij.ui.popup.PopupUpdateProcessor;
50 import com.intellij.ui.speedSearch.ElementFilter;
51 import com.intellij.ui.speedSearch.SpeedSearchUtil;
52 import com.intellij.ui.treeStructure.Tree;
53 import com.intellij.ui.treeStructure.filtered.FilteringTreeBuilder;
54 import com.intellij.ui.treeStructure.filtered.FilteringTreeStructure;
55 import com.intellij.util.Alarm;
56 import com.intellij.util.ArrayUtil;
57 import com.intellij.util.containers.ContainerUtil;
58 import com.intellij.util.containers.Convertor;
59 import com.intellij.util.containers.HashSet;
60 import com.intellij.util.ui.UIUtil;
61 import com.intellij.util.ui.tree.TreeUtil;
62 import org.jetbrains.annotations.Nls;
63 import org.jetbrains.annotations.NonNls;
64 import org.jetbrains.annotations.NotNull;
65 import org.jetbrains.annotations.Nullable;
66
67 import javax.swing.*;
68 import javax.swing.event.ChangeEvent;
69 import javax.swing.event.ChangeListener;
70 import javax.swing.event.TreeSelectionEvent;
71 import javax.swing.event.TreeSelectionListener;
72 import javax.swing.tree.DefaultMutableTreeNode;
73 import javax.swing.tree.TreePath;
74 import java.awt.*;
75 import java.awt.event.ActionEvent;
76 import java.awt.event.ActionListener;
77 import java.awt.event.MouseAdapter;
78 import java.awt.event.MouseEvent;
79 import java.util.*;
80 import java.util.List;
81
82 /**
83  * @author Konstantin Bulenkov
84  */
85 public class FileStructurePopup implements Disposable {
86   private static final Comparator<TextRange> TEXT_RANGE_COMPARATOR = new Comparator<TextRange>() {
87     @Override
88     public int compare(TextRange o1, TextRange o2) {
89       if (o1.getStartOffset() == o2.getStartOffset()) {
90         return o2.getEndOffset() - o1.getEndOffset(); //longer is better
91       }
92       return o1.getStartOffset() - o2.getStartOffset();
93     }
94   };
95   private final Editor myEditor;
96   private final Project myProject;
97   private final StructureViewModel myTreeModel;
98   private final StructureViewModel myBaseTreeModel;
99   private final TreeStructureActionsOwner myTreeActionsOwner;
100   private JBPopup myPopup;
101
102   @NonNls private static final String narrowDownPropertyKey = "FileStructurePopup.narrowDown";
103   private boolean myShouldNarrowDown = true;
104   private Tree myTree;
105   private FilteringTreeBuilder myAbstractTreeBuilder;
106   private String myTitle;
107   private TreeSpeedSearch mySpeedSearch;
108   private SmartTreeStructure myTreeStructure;
109   private int myPreferredWidth;
110   private final FilteringTreeStructure myFilteringStructure;
111   private PsiElement myInitialPsiElement;
112   private Map<Class, JCheckBox> myCheckBoxes = new HashMap<Class, JCheckBox>();
113   private String myTestSearchFilter;
114
115   public FileStructurePopup(StructureViewModel structureViewModel,
116                             @Nullable Editor editor,
117                             Project project,
118                             @NotNull final Disposable auxDisposable,
119                             final boolean applySortAndFilter) {
120     myProject = project;
121     myEditor = editor;
122     myBaseTreeModel = structureViewModel;
123     Disposer.register(this, auxDisposable);
124     if (applySortAndFilter) {
125       myTreeActionsOwner = new TreeStructureActionsOwner(myBaseTreeModel);
126       myTreeModel = new TreeModelWrapper(structureViewModel, myTreeActionsOwner);
127     }
128     else {
129       myTreeActionsOwner = null;
130       myTreeModel = structureViewModel;
131     }    
132
133     myTreeStructure = new SmartTreeStructure(project, myTreeModel){
134       public void rebuildTree() {
135         if (ApplicationManager.getApplication().isUnitTestMode() || !myPopup.isDisposed()) {
136           super.rebuildTree();
137         }
138       }
139
140       public boolean isToBuildChildrenInBackground(final Object element) {
141         return getRootElement() == element;
142       }
143
144       protected TreeElementWrapper createTree() {
145         return new StructureViewComponent.StructureViewTreeElementWrapper(myProject, myModel.getRoot(), myModel);
146       }
147
148       @Override
149       public String toString() {
150         return "structure view tree structure(model=" + myTreeModel + ")";
151       }
152     };
153     myTree = new JBTreeWithHintProvider(new DefaultMutableTreeNode(myTreeStructure.getRootElement())) {
154       @Override
155       protected PsiElement getPsiElementForHint(Object selectedValue) {
156         //noinspection ConstantConditions
157         return getPsi((FilteringTreeStructure.FilteringNode)((DefaultMutableTreeNode)selectedValue).getUserObject());
158       }
159     };
160     myTree.setCellRenderer(new NodeRenderer() {
161       @Override
162       protected void doAppend(@NotNull @Nls String fragment,
163                               @NotNull SimpleTextAttributes attributes,
164                               boolean isMainText,
165                               boolean selected) {
166         SpeedSearchUtil.appendFragmentsForSpeedSearch(myTree, fragment, attributes, selected, this);
167       }
168
169       @Override
170       public void doAppend(@NotNull String fragment, @NotNull SimpleTextAttributes attributes, boolean selected) {
171         SpeedSearchUtil.appendFragmentsForSpeedSearch(myTree, fragment, attributes, selected, this);
172       }
173
174       @Override
175       public void doAppend(String fragment, boolean selected) {
176         SpeedSearchUtil.appendFragmentsForSpeedSearch(myTree, fragment, SimpleTextAttributes.REGULAR_ATTRIBUTES, selected, this);
177       }
178     });
179     myTree.setRootVisible(false);
180     myTree.setShowsRootHandles(true);
181
182     mySpeedSearch = new MyTreeSpeedSearch();
183     mySpeedSearch.setComparator(new SpeedSearchComparator(false, true));
184
185     final FileStructurePopupFilter filter = new FileStructurePopupFilter();
186     myFilteringStructure = new FilteringTreeStructure(filter, myTreeStructure, ApplicationManager.getApplication().isUnitTestMode());
187     myAbstractTreeBuilder = new FilteringTreeBuilder(myTree, filter, myFilteringStructure, null) {
188       @Override
189       protected boolean validateNode(Object child) {
190         return StructureViewComponent.isValid(child);
191       }
192
193       @Override
194       public void revalidateTree() {
195         //myTree.revalidate();
196         //myTree.repaint();
197       }
198
199       @Override
200       public boolean isToEnsureSelectionOnFocusGained() {
201         return false;
202       }
203     };
204
205     myAbstractTreeBuilder.getUi().getUpdater().setPassThroughMode(true);
206     myInitialPsiElement = getCurrentElement(getPsiFile(myProject));
207     //myAbstractTreeBuilder.setCanYieldUpdate(true);
208     Disposer.register(this, myAbstractTreeBuilder);
209   }
210
211   public void show() {
212     //final long time = System.currentTimeMillis();
213     final ActionCallback treeHasBuilt = new ActionCallback();
214     IdeFocusManager.getInstance(myProject).typeAheadUntil(treeHasBuilt);
215     JComponent panel = createCenterPanel();
216     new MnemonicHelper().register(panel);
217     boolean shouldSetWidth = DimensionService.getInstance().getSize(getDimensionServiceKey(), myProject) == null;
218     myPopup = JBPopupFactory.getInstance().createComponentPopupBuilder(panel, null)
219       .setTitle(myTitle)
220       .setResizable(true)
221       .setModalContext(false)
222       .setFocusable(true)
223       .setMovable(true)
224       .setBelongsToGlobalPopupStack(true)
225       //.setCancelOnClickOutside(false) //for debug and snapshots
226       .setCancelKeyEnabled(false)
227       .setDimensionServiceKey(null, getDimensionServiceKey(), false)
228       .setCancelCallback(new Computable<Boolean>() {
229         @Override
230         public Boolean compute() {
231           DimensionService.getInstance().setLocation(getDimensionServiceKey(), myPopup.getLocationOnScreen(), myProject);
232           return true;
233         }
234       })
235       .createPopup();
236     
237     myTree.addTreeSelectionListener(new TreeSelectionListener() {
238       @Override
239       public void valueChanged(TreeSelectionEvent e) {
240         if (myPopup.isVisible()) {
241           final PopupUpdateProcessor updateProcessor = myPopup.getUserData(PopupUpdateProcessor.class);
242           if (updateProcessor != null) {
243             final AbstractTreeNode node = getSelectedNode();
244             updateProcessor.updatePopup(node);
245           }
246         }
247       }
248     });
249     Disposer.register(myPopup, this);
250     Disposer.register(myPopup, new Disposable() {
251       @Override
252       public void dispose() {
253         if (!treeHasBuilt.isDone()) {
254           treeHasBuilt.setRejected();
255         }
256       }
257     });
258     final Point location = DimensionService.getInstance().getLocation(getDimensionServiceKey(), myProject);
259     if (location != null) {
260       myPopup.showInScreenCoordinates(myEditor.getContentComponent(), location);
261     } else {
262       myPopup.showCenteredInCurrentWindow(myProject);
263     }
264
265     ((AbstractPopup)myPopup).setShowHints(true);
266     if (shouldSetWidth) {
267       myPopup.setSize(new Dimension(myPreferredWidth + 10, myPopup.getSize().height));
268     }
269
270     myAbstractTreeBuilder.expandAll(new Runnable() {
271       @Override
272       public void run() {
273         IdeFocusManager.getInstance(myProject).requestFocus(myTree, true);
274         myAbstractTreeBuilder.queueUpdate().doWhenDone(new Runnable() {
275           @Override
276           public void run() {
277             selectPsiElement(myInitialPsiElement);
278             treeHasBuilt.setDone();
279             //long t = System.currentTimeMillis() - time;
280             //System.out.println("Shown in " + t + "ms");
281           }
282         });
283       }
284     });
285     if (!ApplicationManager.getApplication().isUnitTestMode()) {
286       final Alarm alarm = new Alarm(Alarm.ThreadToUse.SHARED_THREAD, myPopup);
287       alarm.addRequest(new Runnable() {
288         String filter = "";
289
290         @Override
291         public void run() {
292           alarm.cancelAllRequests();
293           String prefix = mySpeedSearch.getEnteredPrefix();
294           myTree.getEmptyText().setText(StringUtil.isEmpty(prefix) ? "Nothing to show" : "Can't find '" + prefix + "'");
295           if (prefix == null) prefix = "";
296
297           if (!filter.equals(prefix)) {
298             filter = prefix;
299             myAbstractTreeBuilder.refilter(null, false, false).doWhenProcessed(new Runnable() {
300               @Override
301               public void run() {
302                 myTree.repaint();
303                 //if (mySpeedSearch.isPopupActive()) {
304                 //  mySpeedSearch.refreshSelection();
305                 //}
306               }
307             });
308           }
309           alarm.addRequest(this, 300);
310         }
311       }, 300);
312     }
313   }
314
315   @Nullable
316   public FilteringTreeStructure.FilteringNode selectPsiElement(PsiElement element) {
317     Set<PsiElement> parents = getAllParents(element);
318
319     FilteringTreeStructure.FilteringNode node = (FilteringTreeStructure.FilteringNode)myAbstractTreeBuilder.getRootElement();
320     while (node != null) {
321       boolean changed = false;
322       for (FilteringTreeStructure.FilteringNode n : node.children()) {
323         final PsiElement psiElement = getPsi(n);
324         if (psiElement != null && parents.contains(psiElement)) {
325           node = n;
326           changed = true;
327           break;
328         }
329       }
330       if (!changed) {
331         myAbstractTreeBuilder.getUi().select(node, null);
332         if (myAbstractTreeBuilder.getSelectedElements().isEmpty()) {
333           TreeUtil.selectFirstNode(myTree);
334         }
335         return node;
336       }
337     }
338     TreeUtil.selectFirstNode(myTree);
339     return null;
340   }
341
342   private static Set<PsiElement> getAllParents(PsiElement element) {
343     Set<PsiElement> parents = new java.util.HashSet<PsiElement>();
344
345     while (element != null) {
346       parents.add(element);
347       if (element instanceof PsiFile) break;
348       element = element.getParent();
349     }
350     return parents;
351   }
352
353   @Nullable
354   private PsiElement getPsi(FilteringTreeStructure.FilteringNode n) {
355     final Object delegate = n.getDelegate();
356     if (delegate instanceof StructureViewComponent.StructureViewTreeElementWrapper) {
357       final TreeElement value = ((StructureViewComponent.StructureViewTreeElementWrapper)delegate).getValue();
358       if (value instanceof StructureViewTreeElement) {
359         final Object element = ((StructureViewTreeElement)value).getValue();
360         if (element instanceof PsiElement) {
361           return (PsiElement)element;
362         }
363       }
364     }
365     return null;
366   }
367
368   @Nullable
369   protected PsiFile getPsiFile(final Project project) {
370     return PsiDocumentManager.getInstance(project).getPsiFile(myEditor.getDocument());
371   }
372
373   public void dispose() {
374   }
375
376   protected static String getDimensionServiceKey() {
377     return "StructurePopup";
378   }
379
380   @Nullable
381   public PsiElement getCurrentElement(@Nullable final PsiFile psiFile) {
382     if (psiFile == null) return null;
383
384     PsiDocumentManager.getInstance(myProject).commitAllDocuments();
385
386     Object elementAtCursor = myTreeModel.getCurrentEditorElement();
387     if (elementAtCursor instanceof PsiElement) {
388       return (PsiElement)elementAtCursor;
389     }
390
391     return null;
392   }
393
394   public JComponent createCenterPanel() {
395     List<FileStructureFilter> fileStructureFilters = new ArrayList<FileStructureFilter>();
396     List<FileStructureNodeProvider> fileStructureNodeProviders = new ArrayList<FileStructureNodeProvider>();
397     if (myTreeActionsOwner != null) {
398       for(Filter filter: myBaseTreeModel.getFilters()) {
399         if (filter instanceof FileStructureFilter) {
400           final FileStructureFilter fsFilter = (FileStructureFilter)filter;
401           myTreeActionsOwner.setActionIncluded(fsFilter, true);
402           fileStructureFilters.add(fsFilter);
403         }
404       }
405
406       if (myBaseTreeModel instanceof ProvidingTreeModel) {
407         for (NodeProvider provider : ((ProvidingTreeModel)myBaseTreeModel).getNodeProviders()) {
408           if (provider instanceof FileStructureNodeProvider) {
409             fileStructureNodeProviders.add((FileStructureNodeProvider)provider);
410           }
411         }
412       }
413     }
414     final JPanel panel = new JPanel(new BorderLayout());
415     JPanel comboPanel = new JPanel(new GridLayout(0, 2, 0, 0));
416
417     final Shortcut[] F4 = ActionManager.getInstance().getAction(IdeActions.ACTION_EDIT_SOURCE).getShortcutSet().getShortcuts();
418     final Shortcut[] ENTER = CustomShortcutSet.fromString("ENTER").getShortcuts();
419     final CustomShortcutSet shortcutSet = new CustomShortcutSet(ArrayUtil.mergeArrays(F4, ENTER));
420     new AnAction() {
421       public void actionPerformed(AnActionEvent e) {
422         final boolean succeeded = navigateSelectedElement();
423         if (succeeded) {
424           unregisterCustomShortcutSet(panel);
425         }
426       }
427     }.registerCustomShortcutSet(shortcutSet, panel);
428
429     new AnAction() {
430       public void actionPerformed(AnActionEvent e) {
431         if (mySpeedSearch != null && mySpeedSearch.isPopupActive()) {
432           mySpeedSearch.hidePopup();
433         } else {
434           myPopup.cancel();
435         }
436       }
437     }.registerCustomShortcutSet(CustomShortcutSet.fromString("ESCAPE"), myTree);
438     
439     myTree.addMouseListener(new MouseAdapter() {
440       @Override
441       public void mouseClicked(MouseEvent e) {
442         if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() > 1) {          
443           navigateSelectedElement();
444         }
445       }
446     });
447
448     for(FileStructureFilter filter: fileStructureFilters) {
449       addCheckbox(comboPanel, filter);
450     }
451
452     for (FileStructureNodeProvider provider : fileStructureNodeProviders) {
453       addCheckbox(comboPanel, provider);
454     }
455     myPreferredWidth = Math.max(comboPanel.getPreferredSize().width, 350);
456     panel.add(comboPanel, BorderLayout.NORTH);
457     JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myAbstractTreeBuilder.getTree());
458     scrollPane.setBorder(IdeBorderFactory.createBorder(SideBorder.TOP | SideBorder.BOTTOM));
459     panel.add(scrollPane, BorderLayout.CENTER);
460     panel.add(createSouthPanel(), BorderLayout.SOUTH);
461     DataManager.registerDataProvider(panel, new DataProvider() {
462       @Override
463       public Object getData(@NonNls String dataId) {
464         if (PlatformDataKeys.PROJECT.is(dataId)) {
465           return myProject;
466         }
467         if (LangDataKeys.PSI_ELEMENT.is(dataId)) {
468           final Object node = ContainerUtil.getFirstItem(myAbstractTreeBuilder.getSelectedElements());
469           if (!(node instanceof FilteringTreeStructure.FilteringNode)) return null;
470           return getPsi((FilteringTreeStructure.FilteringNode)node);
471         }
472         if (LangDataKeys.POSITION_ADJUSTER_POPUP.is(dataId)) {
473           return myPopup;
474         }
475         return null;
476       }
477     });
478     myFilteringStructure.rebuild();
479     return panel;
480   }
481
482   @Nullable
483   private AbstractTreeNode getSelectedNode() {
484     Object component = myTree.getSelectionPath().getLastPathComponent();
485     if (component instanceof DefaultMutableTreeNode) {
486       component = ((DefaultMutableTreeNode)component).getUserObject();
487       if (component instanceof FilteringTreeStructure.FilteringNode) {
488         component = ((FilteringTreeStructure.FilteringNode)component).getDelegate();
489         if (component instanceof AbstractTreeNode) {
490           return (AbstractTreeNode)component;
491         }
492       }
493     }
494     return null;
495   }
496
497   public boolean navigateSelectedElement() {
498     final Ref<Boolean> succeeded = new Ref<Boolean>();
499     final CommandProcessor commandProcessor = CommandProcessor.getInstance();
500     commandProcessor.executeCommand(myProject, new Runnable() {
501       public void run() {
502         final AbstractTreeNode selectedNode = getSelectedNode();
503         if (selectedNode != null) {
504           if (selectedNode.canNavigateToSource()) {
505             selectedNode.navigate(true);
506             succeeded.set(true);
507           }
508           else {
509             succeeded.set(false);
510           }
511         }
512         else {
513           succeeded.set(false);
514         }
515
516
517         IdeDocumentHistory.getInstance(myProject).includeCurrentCommandAsNavigation();
518       }
519     }, "Navigate", null);
520     if (succeeded.get()) {
521       myPopup.cancel();
522     }
523     return succeeded.get();
524   }
525
526   private JComponent createSouthPanel() {
527     final JCheckBox checkBox = new JCheckBox(IdeBundle.message("checkbox.narrow.down.on.typing"));
528     checkBox.setSelected(PropertiesComponent.getInstance().getBoolean(narrowDownPropertyKey, true));
529     checkBox.addChangeListener(new ChangeListener() {
530       public void stateChanged(ChangeEvent e) {
531         myShouldNarrowDown = checkBox.isSelected();
532         PropertiesComponent.getInstance().setValue(narrowDownPropertyKey, Boolean.toString(myShouldNarrowDown));
533
534         myAbstractTreeBuilder.queueUpdate();
535       }
536     });
537
538     checkBox.setFocusable(false);
539     UIUtil.applyStyle(UIUtil.ComponentStyle.MINI, checkBox);
540     final JPanel panel = new JPanel(new BorderLayout());
541     panel.add(checkBox, BorderLayout.WEST);
542     return panel;
543   }
544
545   private void addCheckbox(final JPanel panel, final TreeAction action) {
546     String text = action instanceof FileStructureFilter ? ((FileStructureFilter)action).getCheckBoxText() :
547                   action instanceof FileStructureNodeProvider ? ((FileStructureNodeProvider)action).getCheckBoxText() : null;    
548
549     if (text == null) return;
550
551     Shortcut[] shortcuts = action instanceof FileStructureFilter ?
552                           ((FileStructureFilter)action).getShortcut() : ((FileStructureNodeProvider)action).getShortcut();
553
554
555     
556     final JCheckBox chkFilter = new JCheckBox();
557     final boolean selected = getDefaultValue(action);
558     chkFilter.setSelected(selected);
559     myTreeActionsOwner.setActionIncluded(action, action instanceof FileStructureFilter ? !selected : selected);
560     chkFilter.addActionListener(new ActionListener() {
561       public void actionPerformed(final ActionEvent e) {
562         final boolean state = chkFilter.isSelected();
563         saveState(action, state);
564         myTreeActionsOwner.setActionIncluded(action, action instanceof FileStructureFilter ? !state : state);
565         //final String filter = mySpeedSearch.isPopupActive() ? mySpeedSearch.getEnteredPrefix() : null;
566         //mySpeedSearch.hidePopup();
567         Object selection = ContainerUtil.getFirstItem(myAbstractTreeBuilder.getSelectedElements());
568         if (selection instanceof FilteringTreeStructure.FilteringNode) {
569           selection = ((FilteringTreeStructure.FilteringNode)selection).getDelegate();
570         }
571         myTreeStructure.rebuildTree();
572         myFilteringStructure.rebuild();
573         
574         final Object sel = selection;
575         myAbstractTreeBuilder.refilter(sel, true, false).doWhenProcessed(new Runnable() {
576           @Override
577           public void run() {
578             if (mySpeedSearch.isPopupActive()) {
579               mySpeedSearch.refreshSelection();
580             }
581           }
582         });
583       }
584     });
585     chkFilter.setFocusable(false);
586
587     if (shortcuts.length > 0) {
588       text += " (" + KeymapUtil.getShortcutText(shortcuts[0]) + ")";
589       new AnAction() {
590         public void actionPerformed(final AnActionEvent e) {
591           chkFilter.doClick();
592         }
593       }.registerCustomShortcutSet(new CustomShortcutSet(shortcuts), myTree);
594     }
595     chkFilter.setText(text);
596     panel.add(chkFilter);
597     myCheckBoxes.put(action.getClass(), chkFilter);
598   }
599
600   private static boolean getDefaultValue(TreeAction action) {
601     if (action instanceof PropertyOwner) {
602       final String propertyName = ((PropertyOwner)action).getPropertyName();
603       return PropertiesComponent.getInstance().getBoolean(getPropertyName(propertyName), false);
604     }
605
606     return false;
607   }
608
609   private static void saveState(TreeAction action, boolean state) {
610     if (action instanceof PropertyOwner) {
611       final String propertyName = ((PropertyOwner)action).getPropertyName();
612       PropertiesComponent.getInstance().setValue(getPropertyName(propertyName), Boolean.toString(state));
613     }
614   }
615
616   public static String getPropertyName(String propertyName) {
617     return propertyName + ".file.structure.state";
618   }
619
620   public void setTitle(String title) {
621     myTitle = title;
622   }
623
624   public Tree getTree() {
625     return myTree;
626   }
627
628   public TreeSpeedSearch getSpeedSearch() {
629     return mySpeedSearch;
630   }
631
632   public FilteringTreeBuilder getTreeBuilder() {
633     return myAbstractTreeBuilder;
634   }
635
636   public void setSearchFilterForTests(String filter) {
637     myTestSearchFilter = filter;
638   }
639
640   public void setTreeActionState(Class<? extends TreeAction> action, boolean state) {
641     final JCheckBox checkBox = myCheckBoxes.get(action);
642     if (checkBox != null) {
643       checkBox.setSelected(state);
644       for (ActionListener listener : checkBox.getActionListeners()) {
645         listener.actionPerformed(new ActionEvent(this, 1, ""));
646       }
647     }
648   }
649
650   @Nullable
651   private static String getText(Object node) {
652     String text = String.valueOf(node);
653     if (text != null) {
654       if (node instanceof StructureViewComponent.StructureViewTreeElementWrapper) {
655         final TreeElement value = ((StructureViewComponent.StructureViewTreeElementWrapper)node).getValue();
656         if (value instanceof PsiTreeElementBase && ((PsiTreeElementBase)value).isSearchInLocationString()) {
657           final String string = ((PsiTreeElementBase)value).getLocationString();
658           if (!StringUtil.isEmpty(string)) {
659             return text + " (" + string + ")";
660           }
661         }
662       }
663       return text;
664     }
665
666     if (node instanceof StructureViewComponent.StructureViewTreeElementWrapper) {
667       final AccessToken token = ApplicationManager.getApplication().acquireReadActionLock();
668       try {
669         final ItemPresentation presentation = ((StructureViewComponent.StructureViewTreeElementWrapper)node).getValue().getPresentation();
670         return presentation.getPresentableText();
671       }
672       finally {
673         token.finish();
674       }
675     }
676
677     return null;
678   }
679
680   private class FileStructurePopupFilter implements ElementFilter {
681     private String myLastFilter = null;
682     private HashSet<Object> myVisibleParents = new HashSet<Object>();
683     private final boolean isUnitTest = ApplicationManager.getApplication().isUnitTestMode();
684
685     @Override
686     public boolean shouldBeShowing(Object value) {
687       if (!myShouldNarrowDown) return true;
688
689       String filter = getSearchPrefix();
690       if (!StringUtil.equals(myLastFilter, filter)) {
691         myVisibleParents.clear();
692         myLastFilter = filter;
693       }
694       if (filter != null) {
695         if (myVisibleParents.contains(value)) {
696           return true;
697         }
698
699         final String text = getText(value);
700         if (text == null) return false;
701
702         if (matches(text)) {
703           Object o = value;
704           while (o instanceof FilteringTreeStructure.FilteringNode && (o = ((FilteringTreeStructure.FilteringNode)o).getParent()) != null) {
705             myVisibleParents.add(o);
706           }
707           return true;
708         } else {
709           return false;
710         }
711         
712       } 
713       return true;
714     }
715
716     private boolean matches(@NotNull String text) {
717       if (isUnitTest) {
718         final SpeedSearchComparator comparator = mySpeedSearch.getComparator();
719         return StringUtil.isNotEmpty(myTestSearchFilter) && comparator.matchingFragments(myTestSearchFilter, text) != null;
720       }
721       return mySpeedSearch.matchingFragments(text) != null;
722     }
723
724   }
725
726   @Nullable
727   private String getSearchPrefix() {
728     if (ApplicationManager.getApplication().isUnitTestMode()) return myTestSearchFilter;
729
730     return mySpeedSearch != null && !StringUtil.isEmpty(mySpeedSearch.getEnteredPrefix())
731                     ? mySpeedSearch.getEnteredPrefix() : null;
732   }
733
734   public class MyTreeSpeedSearch extends TreeSpeedSearch {
735     public MyTreeSpeedSearch() {
736       super(FileStructurePopup.this.myTree, new Convertor<TreePath, String>() {
737         @Nullable
738         public String convert(TreePath path) {
739           final DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
740           final Object userObject = node.getUserObject();
741           if (userObject instanceof FilteringTreeStructure.FilteringNode) {
742             return FileStructurePopup.getText(((FilteringTreeStructure.FilteringNode)userObject).getDelegate());
743           }
744           return "";
745         }
746       }, true);
747     }
748
749     @Override
750     protected Point getComponentLocationOnScreen() {
751       return myPopup.getContent().getLocationOnScreen();
752     }
753
754     @Override
755     protected Rectangle getComponentVisibleRect() {
756       return myPopup.getContent().getVisibleRect();
757     }
758
759     @Override
760     public Object findElement(String s) {
761       List<ObjectWithWeight> elements = new ArrayList<ObjectWithWeight>();
762       s = s.trim();
763       final ListIterator<Object> it = getElementIterator(0);
764       while (it.hasNext()) {
765         final ObjectWithWeight o = new ObjectWithWeight(it.next(), s, getComparator());
766         if (!o.weights.isEmpty()) {
767           elements.add(o);
768         }
769       }
770       ObjectWithWeight cur = null;
771       ArrayList<ObjectWithWeight> current = new ArrayList<ObjectWithWeight>();
772       for (ObjectWithWeight element : elements) {
773         if (cur == null) {
774           cur = element;
775           current.add(cur);
776           continue;
777         }
778
779         final int i = element.compareWith(cur);
780         if (i == 0) {
781           current.add(element);
782         } else if (i < 0) {
783           cur = element;
784           current.clear();
785           current.add(cur);
786         }
787       }
788
789       return current.isEmpty() ? null : findClosestTo(myInitialPsiElement, current);
790     }
791
792     @Nullable
793     private Object findClosestTo(PsiElement path, ArrayList<ObjectWithWeight> paths) {
794       if (path == null || myInitialPsiElement == null) {
795         return paths.get(0).node;
796       }
797       final Set<PsiElement> parents = getAllParents(myInitialPsiElement);
798       ArrayList<ObjectWithWeight> cur = new ArrayList<ObjectWithWeight>();
799       int max = -1;
800       for (ObjectWithWeight p : paths) {
801         final Object last = ((TreePath)p.node).getLastPathComponent();
802         final List<PsiElement> elements = new ArrayList<PsiElement>();
803         final Object object = ((DefaultMutableTreeNode)last).getUserObject();
804         if (object instanceof FilteringTreeStructure.FilteringNode) {
805           FilteringTreeStructure.FilteringNode node = (FilteringTreeStructure.FilteringNode)object;
806           while (node != null) {
807             elements.add(getPsi(node));
808             node = node.getParentNode();
809           }
810           final int size = ContainerUtil.intersection(parents, elements).size();
811           if (size > max) {
812             max = size;
813             cur.clear();
814             cur.add(p);
815           } else if (size == max) {
816             cur.add(p);
817           }
818         }
819       }
820
821       Collections.sort(cur, new Comparator<ObjectWithWeight>() {
822         @Override
823         public int compare(ObjectWithWeight o1, ObjectWithWeight o2) {
824           final int i = o1.compareWith(o2);
825           return i != 0 ? i 
826                         : ((TreePath)o2.node).getPathCount() - ((TreePath)o1.node).getPathCount();
827         }
828       });
829       return cur.isEmpty() ? null : cur.get(0).node;
830     }
831
832     class ObjectWithWeight {
833       final Object node;
834       final List<TextRange> weights = new ArrayList<TextRange>();
835
836       ObjectWithWeight(Object element, String pattern, SpeedSearchComparator comparator) {
837         this.node = element;
838         final String text = getElementText(element);
839         if (text != null) {
840           final Iterable<TextRange> ranges = comparator.matchingFragments(pattern, text);
841           if (ranges != null) {
842             for (TextRange range : ranges) {
843               weights.add(range);
844             }
845           }
846         }
847         Collections.sort(weights, TEXT_RANGE_COMPARATOR);
848       }
849
850       int compareWith(ObjectWithWeight obj) {
851         final List<TextRange> w = obj.weights;
852         for (int i = 0; i < weights.size(); i++) {
853           if (i >= w.size()) return 1;
854           final int result = TEXT_RANGE_COMPARATOR.compare(weights.get(i), w.get(i));
855           if (result != 0) {
856             return result;
857           }
858         }
859
860         return 0;
861       }
862
863     }
864   }
865 }