ec7382d24b0918ed017fdf8dba20255639faacbe
[idea/community.git] / platform / lang-impl / src / com / intellij / ide / util / gotoByName / ChooseByNamePopup.java
1 /*
2  * Copyright 2000-2014 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.gotoByName;
18
19 import com.intellij.featureStatistics.FeatureUsageTracker;
20 import com.intellij.ide.ui.UISettings;
21 import com.intellij.openapi.application.ApplicationManager;
22 import com.intellij.openapi.application.ModalityState;
23 import com.intellij.openapi.keymap.KeymapUtil;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.ui.popup.ComponentPopupBuilder;
26 import com.intellij.openapi.ui.popup.JBPopupFactory;
27 import com.intellij.openapi.util.Computable;
28 import com.intellij.openapi.util.Key;
29 import com.intellij.openapi.util.SystemInfo;
30 import com.intellij.openapi.util.text.StringUtil;
31 import com.intellij.psi.PsiElement;
32 import com.intellij.psi.statistics.StatisticsInfo;
33 import com.intellij.psi.statistics.StatisticsManager;
34 import com.intellij.ui.ScreenUtil;
35 import org.jetbrains.annotations.NonNls;
36 import org.jetbrains.annotations.NotNull;
37 import org.jetbrains.annotations.Nullable;
38
39 import javax.swing.*;
40 import java.awt.*;
41 import java.awt.event.InputEvent;
42 import java.awt.event.KeyEvent;
43 import java.awt.event.MouseListener;
44 import java.util.List;
45 import java.util.Set;
46 import java.util.regex.Matcher;
47 import java.util.regex.Pattern;
48
49 public class ChooseByNamePopup extends ChooseByNameBase implements ChooseByNamePopupComponent {
50   public static final Key<ChooseByNamePopup> CHOOSE_BY_NAME_POPUP_IN_PROJECT_KEY = new Key<ChooseByNamePopup>("ChooseByNamePopup");
51   private Component myOldFocusOwner = null;
52   private boolean myShowListForEmptyPattern = false;
53   private final boolean myMayRequestCurrentWindow;
54   private final ChooseByNamePopup myOldPopup;
55   private ActionMap myActionMap;
56   private InputMap myInputMap;
57   private String myAdText;
58
59   protected ChooseByNamePopup(@Nullable final Project project,
60                               @NotNull ChooseByNameModel model,
61                               @NotNull ChooseByNameItemProvider provider,
62                               @Nullable ChooseByNamePopup oldPopup,
63                               @Nullable final String predefinedText,
64                               boolean mayRequestOpenInCurrentWindow,
65                               int initialIndex) {
66     super(project, model, provider, oldPopup != null ? oldPopup.getEnteredText() : predefinedText, initialIndex);
67     myOldPopup = oldPopup;
68     if (oldPopup != null) { //inherit old focus owner
69       myOldFocusOwner = oldPopup.myPreviouslyFocusedComponent;
70     }
71     myMayRequestCurrentWindow = mayRequestOpenInCurrentWindow;
72     myAdText = myMayRequestCurrentWindow ? "Press " +
73                                            KeymapUtil.getKeystrokeText(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.SHIFT_MASK)) +
74                                            " to open in current window" : null;
75   }
76
77   public String getEnteredText() {
78     return myTextField.getText();
79   }
80
81   public int getSelectedIndex() {
82     return myList.getSelectedIndex();
83   }
84
85   @Override
86   protected void initUI(final Callback callback, final ModalityState modalityState, boolean allowMultipleSelection) {
87     super.initUI(callback, modalityState, allowMultipleSelection);
88     if (myOldPopup != null) {
89       myTextField.setCaretPosition(myOldPopup.myTextField.getCaretPosition());
90     }
91     if (myInitialText != null) {
92       int selStart = myOldPopup == null ? 0 : myOldPopup.myTextField.getSelectionStart();
93       int selEnd = myOldPopup == null ? myInitialText.length() : myOldPopup.myTextField.getSelectionEnd();
94       if (selEnd > selStart) {
95         myTextField.select(selStart, selEnd);
96       }
97       rebuildList(myInitialIndex, 0, ModalityState.current(), null);
98     }
99     if (myOldFocusOwner != null) {
100       myPreviouslyFocusedComponent = myOldFocusOwner;
101       myOldFocusOwner = null;
102     }
103
104     if (myInputMap != null && myActionMap != null) {
105       for (KeyStroke keyStroke : myInputMap.keys()) {
106         Object key = myInputMap.get(keyStroke);
107         myTextField.getInputMap().put(keyStroke, key);
108         myTextField.getActionMap().put(key, myActionMap.get(key));
109       }
110     }
111   }
112
113   @Override
114   public boolean isOpenInCurrentWindowRequested() {
115     return super.isOpenInCurrentWindowRequested() && myMayRequestCurrentWindow;
116   }
117
118   @Override
119   protected boolean isCheckboxVisible() {
120     return true;
121   }
122
123   @Override
124   protected boolean isShowListForEmptyPattern(){
125     return myShowListForEmptyPattern;
126   }
127
128   public void setShowListForEmptyPattern(boolean showListForEmptyPattern) {
129     myShowListForEmptyPattern = showListForEmptyPattern;
130   }
131
132   @Override
133   protected boolean isCloseByFocusLost() {
134     return UISettings.getInstance().HIDE_NAVIGATION_ON_FOCUS_LOSS;
135   }
136
137   @Override
138   protected void showList() {
139     final JLayeredPane layeredPane = myTextField.getRootPane().getLayeredPane();
140
141     Rectangle bounds = new Rectangle(myTextFieldPanel.getLocationOnScreen(), myTextField.getSize());
142     bounds.y += myTextFieldPanel.getHeight() + (SystemInfo.isMac ? 3 : 1);
143
144     final Dimension preferredScrollPaneSize = myListScrollPane.getPreferredSize();
145     if (myList.getModel().getSize() == 0) {
146       preferredScrollPaneSize.height = UIManager.getFont("Label.font").getSize();
147     }
148
149     preferredScrollPaneSize.width = Math.max(myTextFieldPanel.getWidth(), preferredScrollPaneSize.width);
150
151     Rectangle preferredBounds = new Rectangle(bounds.x, bounds.y, preferredScrollPaneSize.width, preferredScrollPaneSize.height);
152     Rectangle original = new Rectangle(preferredBounds);
153
154     ScreenUtil.fitToScreen(preferredBounds);
155     if (original.width > preferredBounds.width) {
156       int height = myListScrollPane.getHorizontalScrollBar().getPreferredSize().height;
157       preferredBounds.height += height;
158     }
159
160     myListScrollPane.setVisible(true);
161     myListScrollPane.setBorder(null);
162     String adText = getAdText();
163     if (myDropdownPopup == null) {
164       ComponentPopupBuilder builder = JBPopupFactory.getInstance().createComponentPopupBuilder(myListScrollPane, myListScrollPane);
165       builder.setFocusable(false)
166         .setLocateWithinScreenBounds(false)
167         .setRequestFocus(false)
168         .setCancelKeyEnabled(false)
169         .setFocusOwners(new JComponent[]{myTextField})
170         .setBelongsToGlobalPopupStack(false)
171         .setModalContext(false)
172         .setAdText(adText)
173         .setMayBeParent(true);
174       builder.setCancelCallback(new Computable<Boolean>() {
175         @Override
176         public Boolean compute() {
177           return Boolean.TRUE;
178         }
179       });
180       myDropdownPopup = builder.createPopup();
181       myDropdownPopup.setLocation(preferredBounds.getLocation());
182       myDropdownPopup.setSize(preferredBounds.getSize());
183       myDropdownPopup.show(layeredPane);
184     }
185     else {
186       myDropdownPopup.setLocation(preferredBounds.getLocation());
187
188       // in 'focus follows mouse' mode, to avoid focus escaping to editor, don't reduce popup size when list size is reduced
189       final Dimension currentSize = myDropdownPopup.getSize();
190       if (UISettings.getInstance().HIDE_NAVIGATION_ON_FOCUS_LOSS ||
191           preferredBounds.width > currentSize.width || preferredBounds.height > currentSize.height) {
192         myDropdownPopup.setSize(preferredBounds.getSize());
193       }
194     }
195   }
196
197   @Override
198   protected void hideList() {
199     if (myDropdownPopup != null) {
200       myDropdownPopup.cancel();
201       myDropdownPopup = null;
202     }
203   }
204
205   @Override
206   public void close(final boolean isOk) {
207     if (checkDisposed()){
208       return;
209     }
210
211     if (isOk) {
212       myModel.saveInitialCheckBoxState(myCheckBox.isSelected());
213
214       final List<Object> chosenElements = getChosenElements();
215       if (chosenElements != null) {
216         if (myActionListener instanceof MultiElementsCallback) {
217           ((MultiElementsCallback)myActionListener).elementsChosen(chosenElements);
218         }
219         else {
220           for (Object element : chosenElements) {
221             myActionListener.elementChosen(element);
222             String text = myModel.getFullName(element);
223             if (text != null) {
224               StatisticsManager.getInstance().incUseCount(new StatisticsInfo(statisticsContext(), text));
225             }
226           }
227         }
228       }
229       else {
230         return;
231       }
232
233       if (!chosenElements.isEmpty()) {
234         final String enteredText = getTrimmedText();
235         if (enteredText.indexOf('*') >= 0) {
236           FeatureUsageTracker.getInstance().triggerFeatureUsed("navigation.popup.wildcards");
237         }
238         else {
239           for (Object element : chosenElements) {
240             final String name = myModel.getElementName(element);
241             if (name != null) {
242               if (!StringUtil.startsWithIgnoreCase(name, enteredText)) {
243                 FeatureUsageTracker.getInstance().triggerFeatureUsed("navigation.popup.camelprefix");
244                 break;
245               }
246             }
247           }
248         }
249       }
250       else {
251         return;
252       }
253     }
254
255     setDisposed(true);
256     myAlarm.cancelAllRequests();
257     if (myProject != null) {
258       myProject.putUserData(CHOOSE_BY_NAME_POPUP_IN_PROJECT_KEY, null);
259     }
260
261     cleanupUI(isOk);
262     if (ApplicationManager.getApplication().isUnitTestMode()) return;
263     if (myActionListener != null) {
264       myActionListener.onClose();
265     }
266   }
267
268   private void cleanupUI(boolean ok) {
269     if (myTextPopup != null) {
270       if (ok) {
271         myTextPopup.closeOk(null);
272       }
273       else {
274         myTextPopup.cancel();
275       }
276       myTextPopup = null;
277     }
278
279     if (myDropdownPopup != null) {
280       if (ok) {
281         myDropdownPopup.closeOk(null);
282       }
283       else {
284         myDropdownPopup.cancel();
285       }
286       myDropdownPopup = null;
287     }
288   }
289
290   public static ChooseByNamePopup createPopup(final Project project, final ChooseByNameModel model, final PsiElement context) {
291     return createPopup(project, model, new DefaultChooseByNameItemProvider(context), null);
292   }
293
294   public static ChooseByNamePopup createPopup(final Project project, final ChooseByNameModel model, final PsiElement context,
295                                               @Nullable final String predefinedText) {
296     return createPopup(project, model, new DefaultChooseByNameItemProvider(context), predefinedText, false, 0);
297   }
298
299   public static ChooseByNamePopup createPopup(final Project project, final ChooseByNameModel model, final PsiElement context,
300                                               @Nullable final String predefinedText,
301                                               boolean mayRequestOpenInCurrentWindow, final int initialIndex) {
302     return createPopup(project, model, new DefaultChooseByNameItemProvider(context), predefinedText, mayRequestOpenInCurrentWindow,
303                        initialIndex);
304   }
305
306   public static ChooseByNamePopup createPopup(final Project project,
307                                               @NotNull ChooseByNameModel model,
308                                               @NotNull ChooseByNameItemProvider provider) {
309     return createPopup(project, model, provider, null);
310   }
311
312   public static ChooseByNamePopup createPopup(final Project project,
313                                               @NotNull ChooseByNameModel model,
314                                               @NotNull ChooseByNameItemProvider provider,
315                                               @Nullable final String predefinedText) {
316     return createPopup(project, model, provider, predefinedText, false, 0);
317   }
318
319   public static ChooseByNamePopup createPopup(final Project project,
320                                               @NotNull final ChooseByNameModel model,
321                                               @NotNull ChooseByNameItemProvider provider,
322                                               @Nullable final String predefinedText,
323                                               boolean mayRequestOpenInCurrentWindow,
324                                               final int initialIndex) {
325     final ChooseByNamePopup oldPopup = project == null ? null : project.getUserData(CHOOSE_BY_NAME_POPUP_IN_PROJECT_KEY);
326     if (oldPopup != null) {
327       oldPopup.close(false);
328     }
329     ChooseByNamePopup newPopup = new ChooseByNamePopup(project, model, provider, oldPopup, predefinedText, mayRequestOpenInCurrentWindow, initialIndex) {
330       @NotNull
331       @Override
332       protected Set<Object> filter(@NotNull Set<Object> elements) {
333         return model instanceof EdtSortingModel ? super.filter(((EdtSortingModel)model).sort(elements)) : super.filter(elements);
334       }
335     };
336
337     if (project != null) {
338       project.putUserData(CHOOSE_BY_NAME_POPUP_IN_PROJECT_KEY, newPopup);
339     }
340     return newPopup;
341   }
342
343   private static final Pattern patternToDetectLinesAndColumns = Pattern.compile("([^:]+)(?::|@|,|)\\[?(\\d+)?(?:(?:\\D)(\\d+)?)?\\]?");
344   public static final Pattern patternToDetectAnonymousClasses = Pattern.compile("([\\.\\w]+)((\\$[\\d]+)*(\\$)?)");
345   private static final Pattern patternToDetectMembers = Pattern.compile("(.+)(#)(.*)");
346
347   @Override
348   public String transformPattern(String pattern) {
349     final ChooseByNameModel model = getModel();
350     return getTransformedPattern(pattern, model);
351   }
352
353   public static String getTransformedPattern(String pattern, ChooseByNameModel model) {
354     Pattern regex = null;
355     if (pattern.indexOf(':') != -1 ||
356         pattern.indexOf(',') != -1 ||
357         pattern.indexOf(';') != -1 ||
358         //pattern.indexOf('#') != -1 ||
359         pattern.indexOf('@') != -1) { // quick test if reg exp should be used
360       regex = patternToDetectLinesAndColumns;
361     }
362
363     if (model instanceof GotoClassModel2) {
364       if (pattern.indexOf('#') != -1) {
365         regex = patternToDetectMembers;
366       }
367
368       if (pattern.indexOf('$') != -1) {
369         regex = patternToDetectAnonymousClasses;
370       }
371     }
372
373     if (regex != null) {
374       final Matcher matcher = regex.matcher(pattern);
375       if (matcher.matches()) {
376         pattern = matcher.group(1);
377       }
378     }
379
380     return pattern;
381   }
382
383   public int getLinePosition() {
384     return getLineOrColumn(true);
385   }
386
387   private int getLineOrColumn(final boolean line) {
388     final Matcher matcher = patternToDetectLinesAndColumns.matcher(getTrimmedText());
389     if (matcher.matches()) {
390       final int groupNumber = line ? 2 : 3;
391       try {
392         if (groupNumber <= matcher.groupCount()) {
393           final String group = matcher.group(groupNumber);
394           if (group != null) return Integer.parseInt(group) - 1;
395         }
396         if (!line && getLineOrColumn(true) != -1) return 0;
397       }
398       catch (NumberFormatException ignored) {
399       }
400     }
401
402     return -1;
403   }
404
405   @Nullable
406   public String getPathToAnonymous() {
407     final Matcher matcher = patternToDetectAnonymousClasses.matcher(getTrimmedText());
408     if (matcher.matches()) {
409       String path = matcher.group(2);
410       if (path != null) {
411         path = path.trim();
412         if (path.endsWith("$") && path.length() >= 2) {
413           path = path.substring(0, path.length() - 2);
414         }
415         if (!path.isEmpty()) return path;
416       }
417     }
418
419     return null;
420   }
421
422   public int getColumnPosition() {
423     return getLineOrColumn(false);
424   }
425
426   @Nullable
427   public String getMemberPattern() {
428     final String enteredText = getTrimmedText();
429     final int index = enteredText.lastIndexOf('#');
430     if (index == -1) {
431       return null;
432     }
433
434     String name = enteredText.substring(index + 1).trim();
435     return StringUtil.isEmpty(name) ? null : name;
436   }
437
438   public void registerAction(@NonNls String aActionName, KeyStroke keyStroke, Action aAction) {
439     if (myInputMap == null) myInputMap = new InputMap();
440     if (myActionMap == null) myActionMap = new ActionMap();
441     myInputMap.put(keyStroke, aActionName);
442     myActionMap.put(aActionName, aAction);
443   }
444
445   public String getAdText() {
446     return myAdText;
447   }
448
449   public void setAdText(final String adText) {
450     myAdText = adText;
451   }
452
453   public void addMouseClickListener(MouseListener listener) {
454     myList.addMouseListener(listener);
455   }
456
457   public Object getSelectionByPoint(Point point) {
458     final int index = myList.locationToIndex(point);
459     return index > -1 ? myList.getModel().getElementAt(index) : null;
460   }
461
462   public void repaintList() {
463     myList.repaint();
464   }
465 }