use editor font in editor popups - more components fixed
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / navigation / GotoTargetHandler.java
1 /*
2  * Copyright 2000-2015 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.codeInsight.navigation;
18
19 import com.intellij.codeInsight.CodeInsightActionHandler;
20 import com.intellij.codeInsight.hint.HintManager;
21 import com.intellij.featureStatistics.FeatureUsageTracker;
22 import com.intellij.find.FindUtil;
23 import com.intellij.ide.util.EditSourceUtil;
24 import com.intellij.ide.util.PsiElementListCellRenderer;
25 import com.intellij.ide.util.gotoByName.ChooseByNameBase;
26 import com.intellij.navigation.ItemPresentation;
27 import com.intellij.navigation.NavigationItem;
28 import com.intellij.openapi.application.ApplicationManager;
29 import com.intellij.openapi.editor.Editor;
30 import com.intellij.openapi.extensions.Extensions;
31 import com.intellij.openapi.progress.ProgressManager;
32 import com.intellij.openapi.project.DumbService;
33 import com.intellij.openapi.project.IndexNotReadyException;
34 import com.intellij.openapi.project.Project;
35 import com.intellij.openapi.ui.popup.JBPopup;
36 import com.intellij.openapi.ui.popup.PopupChooserBuilder;
37 import com.intellij.openapi.util.Computable;
38 import com.intellij.openapi.util.Ref;
39 import com.intellij.pom.Navigatable;
40 import com.intellij.psi.PsiElement;
41 import com.intellij.psi.PsiFile;
42 import com.intellij.psi.PsiNamedElement;
43 import com.intellij.ui.CollectionListModel;
44 import com.intellij.ui.JBListWithHintProvider;
45 import com.intellij.ui.popup.AbstractPopup;
46 import com.intellij.ui.popup.HintUpdateSupply;
47 import com.intellij.usages.UsageView;
48 import com.intellij.util.ArrayUtil;
49 import com.intellij.util.Function;
50 import com.intellij.util.Processor;
51 import com.intellij.util.containers.HashSet;
52 import org.jetbrains.annotations.NonNls;
53 import org.jetbrains.annotations.NotNull;
54 import org.jetbrains.annotations.Nullable;
55
56 import javax.swing.*;
57 import java.awt.*;
58 import java.util.*;
59 import java.util.List;
60
61 public abstract class GotoTargetHandler implements CodeInsightActionHandler {
62   private static final PsiElementListCellRenderer ourDefaultTargetElementRenderer = new DefaultPsiElementListCellRenderer();
63   private final DefaultListCellRenderer myActionElementRenderer = new ActionCellRenderer();
64
65   @Override
66   public boolean startInWriteAction() {
67     return false;
68   }
69
70   @Override
71   public void invoke(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
72     FeatureUsageTracker.getInstance().triggerFeatureUsed(getFeatureUsedKey());
73
74     try {
75       GotoData gotoData = getSourceAndTargetElements(editor, file);
76       if (gotoData != null && gotoData.source != null) {
77         show(project, editor, file, gotoData);
78       }
79     }
80     catch (IndexNotReadyException e) {
81       DumbService.getInstance(project).showDumbModeNotification("Navigation is not available here during index update");
82     }
83   }
84
85   @NonNls
86   protected abstract String getFeatureUsedKey();
87
88   @Nullable
89   protected abstract GotoData getSourceAndTargetElements(Editor editor, PsiFile file);
90
91   private void show(@NotNull final Project project,
92                     @NotNull Editor editor,
93                     @NotNull PsiFile file,
94                     @NotNull final GotoData gotoData) {
95     final PsiElement[] targets = gotoData.targets;
96     final List<AdditionalAction> additionalActions = gotoData.additionalActions;
97
98     if (targets.length == 0 && additionalActions.isEmpty()) {
99       HintManager.getInstance().showErrorHint(editor, getNotFoundMessage(project, editor, file));
100       return;
101     }
102
103     if (targets.length == 1 && additionalActions.isEmpty()) {
104       Navigatable descriptor = targets[0] instanceof Navigatable ? (Navigatable)targets[0] : EditSourceUtil.getDescriptor(targets[0]);
105       if (descriptor != null && descriptor.canNavigate()) {
106         navigateToElement(descriptor);
107       }
108       return;
109     }
110
111     for (PsiElement eachTarget : targets) {
112       gotoData.renderers.put(eachTarget, createRenderer(gotoData, eachTarget));
113     }
114
115     final String name = ((PsiNamedElement)gotoData.source).getName();
116     final String title = getChooserTitle(gotoData.source, name, targets.length);
117
118     if (shouldSortTargets()) {
119       Arrays.sort(targets, createComparator(gotoData.renderers, gotoData));
120     }
121
122     List<Object> allElements = new ArrayList<Object>(targets.length + additionalActions.size());
123     Collections.addAll(allElements, targets);
124     allElements.addAll(additionalActions);
125
126     final JBListWithHintProvider list = new JBListWithHintProvider(new CollectionListModel<Object>(allElements)) {
127       @Override
128       protected PsiElement getPsiElementForHint(final Object selectedValue) {
129         return selectedValue instanceof PsiElement ? (PsiElement) selectedValue : null;
130       }
131     };
132
133     list.setFont(ChooseByNameBase.getEditorFont());
134     
135     list.setCellRenderer(new DefaultListCellRenderer() {
136       @Override
137       public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
138         if (value == null) return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
139         if (value instanceof AdditionalAction) {
140           return myActionElementRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
141         }
142         PsiElementListCellRenderer renderer = getRenderer(value, gotoData.renderers, gotoData);
143         return renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
144       }
145     });
146
147     final Runnable runnable = new Runnable() {
148       @Override
149       public void run() {
150         int[] ids = list.getSelectedIndices();
151         if (ids == null || ids.length == 0) return;
152         Object[] selectedElements = list.getSelectedValues();
153         for (Object element : selectedElements) {
154           if (element instanceof AdditionalAction) {
155             ((AdditionalAction)element).execute();
156           }
157           else {
158             Navigatable nav = element instanceof Navigatable ? (Navigatable)element : EditSourceUtil.getDescriptor((PsiElement)element);
159             try {
160               if (nav != null && nav.canNavigate()) {
161                 navigateToElement(nav);
162               }
163             }
164             catch (IndexNotReadyException e) {
165               DumbService.getInstance(project).showDumbModeNotification("Navigation is not available while indexing");
166             }
167           }
168         }
169       }
170     };
171
172     final PopupChooserBuilder builder = new PopupChooserBuilder(list);
173     builder.setFilteringEnabled(new Function<Object, String>() {
174       @Override
175       public String fun(Object o) {
176         if (o instanceof AdditionalAction) {
177           return ((AdditionalAction)o).getText();
178         }
179         return getRenderer(o, gotoData.renderers, gotoData).getElementText((PsiElement)o);
180       }
181     });
182
183     final Ref<UsageView> usageView = new Ref<UsageView>();
184     final JBPopup popup = builder.
185       setTitle(title).
186       setItemChoosenCallback(runnable).
187       setMovable(true).
188       setCancelCallback(new Computable<Boolean>() {
189         @Override
190         public Boolean compute() {
191           HintUpdateSupply.hideHint(list);
192           return true;
193         }
194       }).
195       setCouldPin(new Processor<JBPopup>() {
196         @Override
197         public boolean process(JBPopup popup) {
198           usageView.set(FindUtil.showInUsageView(gotoData.source, gotoData.targets, getFindUsagesTitle(gotoData.source, name, gotoData.targets.length), project));
199           popup.cancel();
200           return false;
201         }
202       }).
203       setAdText(getAdText(gotoData.source, targets.length)).
204       createPopup();
205
206     builder.getScrollPane().setBorder(null);
207     builder.getScrollPane().setViewportBorder(null);
208
209     if (gotoData.listUpdaterTask != null) {
210       gotoData.listUpdaterTask.init((AbstractPopup)popup, list, usageView);
211       ProgressManager.getInstance().run(gotoData.listUpdaterTask);
212     }
213     popup.showInBestPositionFor(editor);
214   }
215
216   private static PsiElementListCellRenderer getRenderer(Object value,
217                                                         Map<Object, PsiElementListCellRenderer> targetsWithRenderers,
218                                                         GotoData gotoData) {
219     PsiElementListCellRenderer renderer = targetsWithRenderers.get(value);
220     if (renderer == null) {
221       renderer = gotoData.getRenderer(value);
222     }
223     if (renderer != null) {
224       return renderer;
225     }
226     else {
227       return ourDefaultTargetElementRenderer;
228     }
229   }
230
231   protected static Comparator<PsiElement> createComparator(final Map<Object, PsiElementListCellRenderer> targetsWithRenderers,
232                                                            final GotoData gotoData) {
233     return new Comparator<PsiElement>() {
234       @Override
235       public int compare(PsiElement o1, PsiElement o2) {
236         return getComparingObject(o1).compareTo(getComparingObject(o2));
237       }
238
239       private Comparable getComparingObject(PsiElement o1) {
240         return getRenderer(o1, targetsWithRenderers, gotoData).getComparingObject(o1);
241       }
242     };
243   }
244
245   protected static PsiElementListCellRenderer createRenderer(GotoData gotoData, PsiElement eachTarget) {
246     PsiElementListCellRenderer renderer = null;
247     for (GotoTargetRendererProvider eachProvider : Extensions.getExtensions(GotoTargetRendererProvider.EP_NAME)) {
248       renderer = eachProvider.getRenderer(eachTarget, gotoData);
249       if (renderer != null) break;
250     }
251     if (renderer == null) {
252       renderer = ourDefaultTargetElementRenderer;
253     }
254     return renderer;
255   }
256
257
258   protected void navigateToElement(Navigatable descriptor) {
259     descriptor.navigate(true);
260   }
261
262   protected boolean shouldSortTargets() {
263     return true;
264   }
265
266   @NotNull
267   protected abstract String getChooserTitle(PsiElement sourceElement, String name, int length);
268   @NotNull
269   protected String getFindUsagesTitle(PsiElement sourceElement, String name, int length) {
270     return getChooserTitle(sourceElement, name, length);
271   }
272
273   @NotNull
274   protected abstract String getNotFoundMessage(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file);
275
276   @Nullable
277   protected String getAdText(PsiElement source, int length) {
278     return null;
279   }
280
281   public interface AdditionalAction {
282     @NotNull
283     String getText();
284
285     Icon getIcon();
286
287     void execute();
288   }
289
290   public static class GotoData {
291     @NotNull public final PsiElement source;
292     public PsiElement[] targets;
293     public final List<AdditionalAction> additionalActions;
294
295     private boolean hasDifferentNames;
296     public ListBackgroundUpdaterTask listUpdaterTask;
297     protected final Set<String> myNames;
298     public Map<Object, PsiElementListCellRenderer> renderers = new HashMap<Object, PsiElementListCellRenderer>();
299
300     public GotoData(@NotNull PsiElement source, @NotNull PsiElement[] targets, @NotNull List<AdditionalAction> additionalActions) {
301       this.source = source;
302       this.targets = targets;
303       this.additionalActions = additionalActions;
304
305       myNames = new HashSet<String>();
306       for (PsiElement target : targets) {
307         if (target instanceof PsiNamedElement) {
308           myNames.add(((PsiNamedElement)target).getName());
309           if (myNames.size() > 1) break;
310         }
311       }
312
313       hasDifferentNames = myNames.size() > 1;
314     }
315
316     public boolean hasDifferentNames() {
317       return hasDifferentNames;
318     }
319
320     public boolean addTarget(final PsiElement element) {
321       if (ArrayUtil.find(targets, element) > -1) return false;
322       targets = ArrayUtil.append(targets, element);
323       renderers.put(element, createRenderer(this, element));
324       if (!hasDifferentNames && element instanceof PsiNamedElement) {
325         final String name = ApplicationManager.getApplication().runReadAction(new Computable<String>() {
326           @Override
327           public String compute() {
328             return ((PsiNamedElement)element).getName();
329           }
330         });
331         myNames.add(name);
332         hasDifferentNames = myNames.size() > 1;
333       }
334       return true;
335     }
336
337     public PsiElementListCellRenderer getRenderer(Object value) {
338       return renderers.get(value);
339     }
340   }
341
342   private static class DefaultPsiElementListCellRenderer extends PsiElementListCellRenderer {
343     @Override
344     public String getElementText(final PsiElement element) {
345       if (element instanceof PsiNamedElement) {
346         String name = ((PsiNamedElement)element).getName();
347         if (name != null) {
348           return name;
349         }
350       }
351       return element.getContainingFile().getName();
352     }
353
354     @Override
355     protected String getContainerText(final PsiElement element, final String name) {
356       if (element instanceof NavigationItem) {
357         final ItemPresentation presentation = ((NavigationItem)element).getPresentation();
358         return presentation != null ? presentation.getLocationString():null;
359       }
360
361       return null;
362     }
363
364     @Override
365     protected int getIconFlags() {
366       return 0;
367     }
368   }
369
370   private static class ActionCellRenderer extends DefaultListCellRenderer {
371     @Override
372     public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
373       Component result = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
374       if (value != null) {
375         AdditionalAction action = (AdditionalAction)value;
376         setText(action.getText());
377         setIcon(action.getIcon());
378       }
379       return result;
380     }
381   }
382 }