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