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