file structure dialog speed search match highlighting
[idea/community.git] / platform / lang-impl / src / com / intellij / ide / util / FileStructureDialog.java
1 /*
2  * Copyright 2000-2009 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
17 package com.intellij.ide.util;
18
19 import com.intellij.ide.IdeBundle;
20 import com.intellij.ide.commander.CommanderPanel;
21 import com.intellij.ide.commander.ProjectListBuilder;
22 import com.intellij.ide.structureView.StructureViewBuilder;
23 import com.intellij.ide.structureView.StructureViewModel;
24 import com.intellij.ide.structureView.StructureViewTreeElement;
25 import com.intellij.ide.structureView.TreeBasedStructureViewBuilder;
26 import com.intellij.ide.structureView.newStructureView.TreeActionsOwner;
27 import com.intellij.ide.structureView.newStructureView.TreeModelWrapper;
28 import com.intellij.ide.util.treeView.AbstractTreeNode;
29 import com.intellij.ide.util.treeView.smartTree.Filter;
30 import com.intellij.ide.util.treeView.smartTree.SmartTreeStructure;
31 import com.intellij.ide.util.treeView.smartTree.Sorter;
32 import com.intellij.ide.util.treeView.smartTree.TreeElement;
33 import com.intellij.lang.LanguageStructureViewBuilder;
34 import com.intellij.openapi.Disposable;
35 import com.intellij.openapi.actionSystem.*;
36 import com.intellij.openapi.application.ApplicationManager;
37 import com.intellij.openapi.command.CommandProcessor;
38 import com.intellij.openapi.editor.Editor;
39 import com.intellij.openapi.fileEditor.OpenFileDescriptor;
40 import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory;
41 import com.intellij.openapi.keymap.KeymapUtil;
42 import com.intellij.openapi.project.Project;
43 import com.intellij.openapi.ui.DialogWrapper;
44 import com.intellij.openapi.util.Comparing;
45 import com.intellij.openapi.util.Disposer;
46 import com.intellij.openapi.util.Ref;
47 import com.intellij.openapi.vfs.VirtualFile;
48 import com.intellij.openapi.wm.ex.IdeFocusTraversalPolicy;
49 import com.intellij.pom.Navigatable;
50 import com.intellij.psi.PsiDocumentManager;
51 import com.intellij.psi.PsiElement;
52 import com.intellij.psi.PsiFile;
53 import com.intellij.psi.util.PsiUtilBase;
54 import com.intellij.ui.IdeBorderFactory;
55 import com.intellij.ui.ListScrollingUtil;
56 import com.intellij.ui.SideBorder;
57 import com.intellij.ui.SpeedSearchBase;
58 import com.intellij.util.ArrayUtil;
59 import com.intellij.util.containers.HashSet;
60 import org.jetbrains.annotations.NonNls;
61 import org.jetbrains.annotations.NotNull;
62 import org.jetbrains.annotations.Nullable;
63
64 import javax.swing.*;
65 import javax.swing.border.Border;
66 import javax.swing.event.ChangeEvent;
67 import javax.swing.event.ChangeListener;
68 import java.awt.*;
69 import java.awt.event.ActionEvent;
70 import java.awt.event.ActionListener;
71 import java.beans.PropertyChangeEvent;
72 import java.beans.PropertyChangeListener;
73 import java.util.ArrayList;
74 import java.util.List;
75 import java.util.Set;
76
77 public class FileStructureDialog extends DialogWrapper {
78   private final Editor myEditor;
79   private final Navigatable myNavigatable;
80   private final Project myProject;
81   private MyCommanderPanel myCommanderPanel;
82   private final StructureViewModel myTreeModel;
83   private final StructureViewModel myBaseTreeModel;
84   private SmartTreeStructure myTreeStructure;
85   private final MyTreeActionsOwner myTreeActionsOwner;
86
87   @NonNls private static final String ourPropertyKey = "FileStructure.narrowDown";
88   private boolean myShouldNarrowDown = false;
89
90   public FileStructureDialog(StructureViewModel structureViewModel,
91                              @Nullable Editor editor,
92                              Project project,
93                              Navigatable navigatable,
94                              @NotNull final Disposable auxDisposable,
95                              final boolean applySortAndFilter) {
96     super(project, true);
97     myProject = project;
98     myEditor = editor;
99     myNavigatable = navigatable;
100     myBaseTreeModel = structureViewModel;
101     if (applySortAndFilter) {
102       myTreeActionsOwner = new MyTreeActionsOwner();
103       myTreeModel = new TreeModelWrapper(structureViewModel, myTreeActionsOwner);
104     }
105     else {
106       myTreeActionsOwner = null;
107       myTreeModel = structureViewModel;
108     }
109
110     PsiFile psiFile = getPsiFile(project);
111
112     final PsiElement psiElement = getCurrentElement(psiFile);
113
114     //myDialog.setUndecorated(true);
115     init();
116
117     if (psiElement != null) {
118       if (structureViewModel.shouldEnterElement(psiElement)) {
119         myCommanderPanel.getBuilder().enterElement(psiElement, PsiUtilBase.getVirtualFile(psiElement));
120       }
121       else {
122         myCommanderPanel.getBuilder().selectElement(psiElement, PsiUtilBase.getVirtualFile(psiElement));
123       }
124     }
125
126     Disposer.register(myDisposable, auxDisposable);
127   }
128
129   protected PsiFile getPsiFile(final Project project) {
130     return PsiDocumentManager.getInstance(project).getPsiFile(myEditor.getDocument());
131   }
132
133   @Nullable
134   protected Border createContentPaneBorder() {
135     return null;
136   }
137
138   public void dispose() {
139     myCommanderPanel.dispose();
140     super.dispose();
141   }
142
143   protected String getDimensionServiceKey() {
144     return "#com.intellij.ide.util.FileStructureDialog";
145   }
146
147   public JComponent getPreferredFocusedComponent() {
148     return IdeFocusTraversalPolicy.getPreferredFocusedComponent(myCommanderPanel);
149   }
150
151   @Nullable
152   protected PsiElement getCurrentElement(@Nullable final PsiFile psiFile) {
153     if (psiFile == null) return null;
154
155     PsiDocumentManager.getInstance(myProject).commitAllDocuments();
156
157     Object elementAtCursor = myTreeModel.getCurrentEditorElement();
158     if (elementAtCursor instanceof PsiElement) {
159       return (PsiElement)elementAtCursor;
160     }
161
162     return null;
163   }
164
165   protected JComponent createCenterPanel() {
166     myCommanderPanel = new MyCommanderPanel(myProject);
167     myTreeStructure = new MyStructureTreeStructure();
168
169     List<FileStructureFilter> fileStructureFilters = new ArrayList<FileStructureFilter>();
170     if (myTreeActionsOwner != null) {
171       for(Filter filter: myBaseTreeModel.getFilters()) {
172         if (filter instanceof FileStructureFilter) {
173           final FileStructureFilter fsFilter = (FileStructureFilter)filter;
174           myTreeActionsOwner.setFilterIncluded(fsFilter, true);
175           fileStructureFilters.add(fsFilter);
176         }
177       }
178     }
179
180     PsiFile psiFile = getPsiFile(myProject);
181     boolean showRoot = isShowRoot(psiFile);
182     ProjectListBuilder projectListBuilder = new ProjectListBuilder(myProject, myCommanderPanel, myTreeStructure, null, showRoot) {
183       protected boolean nodeIsAcceptableForElement(AbstractTreeNode node, Object element) {
184         return Comparing.equal(((StructureViewTreeElement)node.getValue()).getValue(), element);
185       }
186
187       protected void refreshSelection() {
188         myCommanderPanel.scrollSelectionInView();
189         if (myShouldNarrowDown) {
190           myCommanderPanel.updateSpeedSearch();
191         }
192       }
193
194       protected List<AbstractTreeNode> getAllAcceptableNodes(final Object[] childElements, VirtualFile file) {
195         ArrayList<AbstractTreeNode> result = new ArrayList<AbstractTreeNode>();
196         for (Object childElement : childElements) {
197           result.add((AbstractTreeNode)childElement);
198         }
199         return result;
200       }
201     };
202     myCommanderPanel.setBuilder(projectListBuilder);
203     myCommanderPanel.setTitlePanelVisible(false);
204
205     new AnAction() {
206       public void actionPerformed(AnActionEvent e) {
207         final boolean succeeded = myCommanderPanel.navigateSelectedElement();
208         if (succeeded) {
209           unregisterCustomShortcutSet(myCommanderPanel);
210         }
211       }
212     }.registerCustomShortcutSet(ActionManager.getInstance().getAction(IdeActions.ACTION_EDIT_SOURCE).getShortcutSet(), myCommanderPanel);
213
214     myCommanderPanel.setPreferredSize(new Dimension(400, 500));
215
216     JPanel panel = new JPanel(new GridBagLayout());
217
218     addNarrowDownCheckbox(panel);
219
220     for(FileStructureFilter filter: fileStructureFilters) {
221       addFilterCheckbox(panel, filter);
222     }
223
224     myCommanderPanel.setBorder(IdeBorderFactory.createBorder(SideBorder.TOP));
225     panel.add(myCommanderPanel,
226               new GridBagConstraints(0, GridBagConstraints.RELATIVE, 1, 1, 1, 1, GridBagConstraints.WEST, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
227
228     return panel;
229   }
230
231   protected boolean isShowRoot(final PsiFile psiFile) {
232     StructureViewBuilder viewBuilder = LanguageStructureViewBuilder.INSTANCE.getStructureViewBuilder(psiFile);
233     return viewBuilder instanceof TreeBasedStructureViewBuilder && ((TreeBasedStructureViewBuilder)viewBuilder).isRootNodeShown();
234   }
235
236   private void addNarrowDownCheckbox(final JPanel panel) {
237     final JCheckBox checkBox = new JCheckBox(IdeBundle.message("checkbox.narrow.down.the.list.on.typing"));
238     checkBox.setSelected(PropertiesComponent.getInstance().isTrueValue(ourPropertyKey));
239     checkBox.addChangeListener(new ChangeListener() {
240       public void stateChanged(ChangeEvent e) {
241         myShouldNarrowDown = checkBox.isSelected();
242         PropertiesComponent.getInstance().setValue(ourPropertyKey, Boolean.toString(myShouldNarrowDown));
243
244         ProjectListBuilder builder = (ProjectListBuilder)myCommanderPanel.getBuilder();
245         if (builder == null) {
246           return;
247         }
248         builder.addUpdateRequest();
249       }
250     });
251
252     checkBox.setFocusable(false);
253     panel.add(checkBox,
254               new GridBagConstraints(0, GridBagConstraints.RELATIVE, 1, 1, 0, 0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0, 5, 0, 5), 0, 0));
255   }
256
257   private void addFilterCheckbox(final JPanel panel, final FileStructureFilter fileStructureFilter) {
258     final JCheckBox chkFilter = new JCheckBox();
259     chkFilter.addActionListener(new ActionListener() {
260       public void actionPerformed(final ActionEvent e) {
261         myTreeActionsOwner.setFilterIncluded(fileStructureFilter, !chkFilter.isSelected());
262         myTreeStructure.rebuildTree();
263         ProjectListBuilder builder = (ProjectListBuilder)myCommanderPanel.getBuilder();
264         if (builder != null) {
265           builder.updateList(true);
266         }
267       }
268     });
269     chkFilter.setFocusable(false);
270     String text = fileStructureFilter.getCheckBoxText();
271     final Shortcut[] shortcuts = fileStructureFilter.getShortcut();
272     if (shortcuts.length > 0) {
273       text += " (" + KeymapUtil.getShortcutText(shortcuts [0]) + ")";
274       new AnAction() {
275         public void actionPerformed(final AnActionEvent e) {
276           chkFilter.doClick();
277         }
278       }.registerCustomShortcutSet(new CustomShortcutSet(shortcuts), panel);
279     }
280     chkFilter.setText(text);
281     panel.add(chkFilter,
282                 new GridBagConstraints(0, GridBagConstraints.RELATIVE, 1, 1, 0, 0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0, 5, 0, 5), 0, 0));
283   }
284
285   @Nullable
286   protected JComponent createSouthPanel() {
287     return null;
288   }
289
290   public CommanderPanel getPanel() {
291     return myCommanderPanel;
292   }
293
294   private class MyCommanderPanel extends CommanderPanel implements DataProvider {
295     @Override
296     protected boolean shouldDrillDownOnEmptyElement(final AbstractTreeNode node) {
297       return false;
298     }
299
300     public MyCommanderPanel(Project _project) {
301       super(_project, false, true);
302       myList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
303       myListSpeedSearch.addChangeListener(new PropertyChangeListener() {
304         public void propertyChange(PropertyChangeEvent evt) {
305           ProjectListBuilder builder = (ProjectListBuilder)getBuilder();
306           if (builder == null) {
307             return;
308           }
309           builder.addUpdateRequest(hasPrefixShortened(evt));
310           ApplicationManager.getApplication().invokeLater(new Runnable() {
311             public void run() {
312               int index = myList.getSelectedIndex();
313               if (index != -1 && index < myList.getModel().getSize()) {
314                 myList.clearSelection();
315                 ListScrollingUtil.selectItem(myList, index);
316               }
317               else {
318                 ListScrollingUtil.ensureSelectionExists(myList);
319               }
320             }
321           });
322         }
323       });
324       myListSpeedSearch.setComparator(createSpeedSearchComparator());
325     }
326
327     private boolean hasPrefixShortened(final PropertyChangeEvent evt) {
328       return evt.getNewValue() != null && evt.getOldValue() != null &&
329              ((String)evt.getNewValue()).length() < ((String)evt.getOldValue()).length();
330     }
331
332     public boolean navigateSelectedElement() {
333       final Ref<Boolean> succeeded = new Ref<Boolean>();
334       final CommandProcessor commandProcessor = CommandProcessor.getInstance();
335       commandProcessor.executeCommand(myProject, new Runnable() {
336         public void run() {
337           succeeded.set(MyCommanderPanel.super.navigateSelectedElement());
338           IdeDocumentHistory.getInstance(myProject).includeCurrentCommandAsNavigation();
339         }
340       }, "Navigate", null);
341       if (succeeded.get()) {
342         close(CANCEL_EXIT_CODE);
343       }
344       return succeeded.get();
345     }
346
347     public Object getData(String dataId) {
348       Object selectedElement = myCommanderPanel.getSelectedValue();
349
350       if (selectedElement instanceof TreeElement) selectedElement = ((StructureViewTreeElement)selectedElement).getValue();
351
352       if (PlatformDataKeys.NAVIGATABLE.is(dataId)) {
353         return selectedElement instanceof Navigatable ? selectedElement : myNavigatable;
354       }
355
356       if (OpenFileDescriptor.NAVIGATE_IN_EDITOR.is(dataId)) return myEditor;
357
358       return getDataImpl(dataId);
359     }
360
361     public String getEnteredPrefix() {
362       return myListSpeedSearch.getEnteredPrefix();
363     }
364
365     public void updateSpeedSearch() {
366       myListSpeedSearch.refreshSelection();
367     }
368
369     public void scrollSelectionInView() {
370       int selectedIndex = myList.getSelectedIndex();
371       if (selectedIndex >= 0) {
372         ListScrollingUtil.ensureIndexIsVisible(myList, selectedIndex, 0);
373       }
374     }
375   }
376
377   private class MyStructureTreeStructure extends SmartTreeStructure {
378     public MyStructureTreeStructure() {
379       super(FileStructureDialog.this.myProject, myTreeModel);
380     }
381
382     public Object[] getChildElements(Object element) {
383       Object[] childElements = super.getChildElements(element);
384
385       if (!myShouldNarrowDown) {
386         return childElements;
387       }
388
389       String enteredPrefix = myCommanderPanel.getEnteredPrefix();
390       if (enteredPrefix == null) {
391         return childElements;
392       }
393
394       ArrayList<Object> filteredElements = new ArrayList<Object>(childElements.length);
395       SpeedSearchBase.SpeedSearchComparator speedSearchComparator = createSpeedSearchComparator();
396
397       for (Object child : childElements) {
398         if (child instanceof AbstractTreeNode) {
399           Object value = ((AbstractTreeNode)child).getValue();
400           if (value instanceof TreeElement) {
401             String name = ((TreeElement)value).getPresentation().getPresentableText();
402             if (name == null) {
403               continue;
404             }
405             if (!speedSearchComparator.doCompare(enteredPrefix, name)) {
406               continue;
407             }
408           }
409         }
410         filteredElements.add(child);
411       }
412       return ArrayUtil.toObjectArray(filteredElements);
413     }
414
415     public void rebuildTree() {
416       getChildElements(getRootElement());   // for some reason necessary to rebuild tree correctly
417       super.rebuildTree();
418     }
419   }
420
421   private static SpeedSearchBase.SpeedSearchComparator createSpeedSearchComparator() {
422     return new SpeedSearchBase.SpeedSearchComparator() {
423       public void translateCharacter(final StringBuilder buf, final char ch) {
424         if (ch == '*') {
425           if (buf.length() > 0 && "^*)(".indexOf(buf.charAt(buf.length() - 1)) == -1) buf.append(')');
426           buf.append(".*"); // overrides '*' handling to skip (,) in parameter lists
427         }
428         else {
429           if (ch == ':') {
430             if (buf.length() > 0 && "^*)(".indexOf(buf.charAt(buf.length() - 1)) == -1) buf.append(')');
431             buf.append(".*"); //    get:int should match any getter returning int
432             buf.append('(');
433           }
434           super.translateCharacter(buf, ch);
435         }
436       }
437     };
438   }
439
440   private class MyTreeActionsOwner implements TreeActionsOwner {
441     private final Set<Filter> myFilters = new HashSet<Filter>();
442
443     public void setActionActive(String name, boolean state) {
444     }
445
446     public boolean isActionActive(String name) {
447       for (final Sorter sorter : myBaseTreeModel.getSorters()) {
448         if (sorter.getName().equals(name)) {
449           if (!sorter.isVisible()) return true;
450         }
451       }
452       for(Filter filter: myFilters) {
453         if (filter.getName().equals(name)) return true;
454       }
455       return Sorter.ALPHA_SORTER_ID.equals(name);
456     }
457
458     public void setFilterIncluded(final FileStructureFilter filter, final boolean selected) {
459       if (selected) {
460         myFilters.add(filter);
461       }
462       else {
463         myFilters.remove(filter);
464       }
465     }
466   }
467 }