API for custom matchers in ChooseByName popup
[fedora-idea.git] / lang-impl / src / com / intellij / ide / util / gotoByName / ChooseByNameBase.java
bloba8955fc64e822cefd90e2a891db4abe2c29af256
1 package com.intellij.ide.util.gotoByName;
3 import com.intellij.ide.IdeBundle;
4 import com.intellij.ide.actions.CopyReferenceAction;
5 import com.intellij.ide.ui.UISettings;
6 import com.intellij.openapi.actionSystem.*;
7 import com.intellij.openapi.application.ApplicationManager;
8 import com.intellij.openapi.application.ModalityState;
9 import com.intellij.openapi.diagnostic.Logger;
10 import com.intellij.openapi.editor.colors.EditorColorsManager;
11 import com.intellij.openapi.editor.colors.EditorColorsScheme;
12 import com.intellij.openapi.keymap.KeymapManager;
13 import com.intellij.openapi.progress.ProcessCanceledException;
14 import com.intellij.openapi.project.Project;
15 import com.intellij.openapi.ui.popup.JBPopup;
16 import com.intellij.openapi.ui.popup.JBPopupFactory;
17 import com.intellij.openapi.util.Comparing;
18 import com.intellij.openapi.util.Pair;
19 import com.intellij.openapi.util.SystemInfo;
20 import com.intellij.openapi.util.text.StringUtil;
21 import com.intellij.openapi.wm.WindowManager;
22 import com.intellij.openapi.wm.ex.WindowManagerEx;
23 import com.intellij.psi.PsiElement;
24 import com.intellij.psi.codeStyle.NameUtil;
25 import com.intellij.psi.statistics.StatisticsInfo;
26 import com.intellij.psi.statistics.StatisticsManager;
27 import com.intellij.psi.util.proximity.PsiProximityComparator;
28 import com.intellij.ui.DocumentAdapter;
29 import com.intellij.ui.ListScrollingUtil;
30 import com.intellij.ui.popup.PopupOwner;
31 import com.intellij.ui.popup.PopupUpdateProcessor;
32 import com.intellij.util.Alarm;
33 import com.intellij.util.SmartList;
34 import com.intellij.util.diff.Diff;
35 import com.intellij.util.ui.UIUtil;
36 import org.apache.oro.text.regex.*;
37 import org.jetbrains.annotations.NonNls;
38 import org.jetbrains.annotations.Nullable;
40 import javax.swing.*;
41 import javax.swing.event.DocumentEvent;
42 import javax.swing.event.ListSelectionEvent;
43 import javax.swing.event.ListSelectionListener;
44 import javax.swing.text.DefaultEditorKit;
45 import java.awt.*;
46 import java.awt.event.*;
47 import java.lang.ref.Reference;
48 import java.lang.ref.WeakReference;
49 import java.util.*;
50 import java.util.List;
52 public abstract class ChooseByNameBase{
53 private static final Logger LOG = Logger.getInstance("#com.intellij.ide.util.gotoByName.ChooseByNameBase");
55 protected final Project myProject;
56 protected final ChooseByNameModel myModel;
57 protected final String myInitialText;
58 private final Reference<PsiElement> myContext;
60 protected Component myPreviouslyFocusedComponent;
62 protected JPanelProvider myTextFieldPanel;// Located in the layered pane
63 protected JTextField myTextField;
64 private JPanel myCardContainer;
65 private CardLayout myCard;
66 protected JCheckBox myCheckBox;
67 /** the tool area of the popup, it is just after card box */
68 protected JComponent myToolArea;
70 protected JScrollPane myListScrollPane; // Located in the layered pane
71 protected JList myList;
72 private DefaultListModel myListModel;
73 private List<Pair<String, Integer>> myHistory;
74 private List<Pair<String, Integer>> myFuture;
76 protected ChooseByNamePopupComponent.Callback myActionListener;
78 protected final Alarm myAlarm = new Alarm();
80 private final ListUpdater myListUpdater = new ListUpdater();
82 private boolean myListIsUpToDate = false;
83 protected boolean myDisposedFlag = false;
85 private final String[][] myNames = new String[2][];
86 private CalcElementsThread myCalcElementsThread;
87 private static int VISIBLE_LIST_SIZE_LIMIT = 10;
88 private static final int MAXIMUM_LIST_SIZE_LIMIT = 30;
89 private int myMaximumListSizeLimit = MAXIMUM_LIST_SIZE_LIMIT;
90 @NonNls private static final String NOT_FOUND_IN_PROJECT_CARD = "syslib";
91 @NonNls private static final String NOT_FOUND_CARD = "nfound";
92 @NonNls private static final String CHECK_BOX_CARD = "chkbox";
93 @NonNls private static final String SEARCHING_CARD = "searching";
94 private static final int REBUILD_DELAY = 300;
96 private final Alarm myHideAlarm = new Alarm();
98 private static class MatchesComparator implements Comparator<String> {
99 private final String myOriginalPattern;
101 public MatchesComparator(final String originalPattern) {
102 myOriginalPattern = originalPattern.trim();
105 public int compare(final String a, final String b) {
106 boolean aStarts = a.startsWith(myOriginalPattern);
107 boolean bStarts = b.startsWith(myOriginalPattern);
108 if (aStarts && bStarts) return a.compareToIgnoreCase(b);
109 if (aStarts && !bStarts) return -1;
110 if (bStarts && !aStarts) return 1;
111 return a.compareToIgnoreCase(b);
116 * @param initialText initial text which will be in the lookup text field
117 * @param context
119 protected ChooseByNameBase(Project project, ChooseByNameModel model, String initialText, final PsiElement context) {
120 myProject = project;
121 myModel = model;
122 myInitialText = initialText;
123 myContext = new WeakReference<PsiElement>(context);
127 * @return get tool area
129 public JComponent getToolArea() {
130 return myToolArea;
134 * Set tool area. The method may be called only before invoke.
135 * @param toolArea a tool area component
137 public void setToolArea(JComponent toolArea) {
138 if(myCard != null) {
139 throw new IllegalStateException("Tool area is modifiable only before invoke()");
141 myToolArea = toolArea;
144 public void invoke(final ChooseByNamePopupComponent.Callback callback, final ModalityState modalityState, boolean allowMultipleSelection) {
145 initUI(callback, modalityState, allowMultipleSelection);
148 public class JPanelProvider extends JPanel implements DataProvider {
149 JBPopup myHint = null;
150 boolean myFocusRequested = false;
152 JPanelProvider(LayoutManager mgr) {
153 super(mgr);
156 JPanelProvider() {
159 public Object getData(String dataId) {
160 if (!myListIsUpToDate) {
161 return null;
163 if (dataId.equals(DataConstants.PSI_ELEMENT)) {
164 Object element = getChosenElement();
166 if (element instanceof PsiElement) {
167 return element;
170 if (element instanceof DataProvider) {
171 return ((DataProvider)element).getData(dataId);
174 else if (dataId.equals(DataConstants.PSI_ELEMENT_ARRAY)) {
175 final List<Object> chosenElements = getChosenElements();
176 if (chosenElements != null) {
177 List<PsiElement> result = new ArrayList<PsiElement>();
178 for (Object element : chosenElements) {
179 if (element instanceof PsiElement) {
180 result.add((PsiElement)element);
183 return result.toArray(new PsiElement[result.size()]);
186 else if (dataId.equals(DataConstants.DOMINANT_HINT_AREA_RECTANGLE)) {
187 return getBounds();
189 return null;
192 public void registerHint(JBPopup h) {
193 if (myHint != null && myHint.isVisible() && myHint != h){
194 myHint.cancel();
196 myHint = h;
199 public boolean focusRequested() {
200 boolean focusRequested = myFocusRequested;
202 myFocusRequested = false;
204 return focusRequested;
207 public void requestFocus() {
208 myFocusRequested = true;
211 public void unregisterHint() {
212 myHint = null;
215 public void hideHint() {
216 if (myHint != null) {
217 myHint.cancel();
221 public JBPopup getHint() {
222 return myHint;
225 public void updateHint(PsiElement element) {
226 if (myHint == null || !myHint.isVisible()) return;
227 final PopupUpdateProcessor updateProcessor = myHint.getUserData(PopupUpdateProcessor.class);
228 if (updateProcessor != null){
229 myHint.cancel();
230 updateProcessor.updatePopup(element);
236 * @param callback
237 * @param modalityState - if not null rebuilds list in given {@link ModalityState}
238 * @param allowMultipleSelection
240 protected void initUI(final ChooseByNamePopupComponent.Callback callback, final ModalityState modalityState, boolean allowMultipleSelection) {
241 myPreviouslyFocusedComponent = WindowManagerEx.getInstanceEx().getFocusedComponent(myProject);
243 myActionListener = callback;
244 myTextFieldPanel = new JPanelProvider();
245 myTextFieldPanel.setLayout(new BoxLayout(myTextFieldPanel, BoxLayout.Y_AXIS));
246 final JPanel hBox = new JPanel();
247 hBox.setLayout(new BoxLayout(hBox, BoxLayout.X_AXIS));
249 if (myModel.getPromptText() != null) {
250 JLabel label = new JLabel(" " + myModel.getPromptText());
251 label.setFont(UIUtil.getLabelFont().deriveFont(Font.BOLD));
252 hBox.add(label);
255 myCard = new CardLayout();
256 myCardContainer = new JPanel(myCard);
258 final JPanel checkBoxPanel = new JPanel();
259 checkBoxPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 5));
260 myCheckBox = new JCheckBox(myModel.getCheckBoxName());
261 myCheckBox.setSelected(myModel.loadInitialCheckBoxState());
263 if (myModel.getPromptText() != null){
264 checkBoxPanel.setLayout(new BoxLayout(checkBoxPanel, BoxLayout.X_AXIS));
265 checkBoxPanel.add (new JLabel (" ("));
266 checkBoxPanel.add (myCheckBox);
267 checkBoxPanel.add (new JLabel (")"));
268 } else {
269 checkBoxPanel.setLayout(new BoxLayout(checkBoxPanel, BoxLayout.LINE_AXIS));
270 checkBoxPanel.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
271 checkBoxPanel.add (new JLabel (")"));
272 checkBoxPanel.add (myCheckBox);
273 checkBoxPanel.add (new JLabel (" ("));
275 checkBoxPanel.setVisible(myModel.getCheckBoxName() != null);
276 JPanel panel = new JPanel(new BorderLayout());
277 panel.add(checkBoxPanel, BorderLayout.CENTER);
278 myCardContainer.add(panel, CHECK_BOX_CARD);
279 myCardContainer.add(new JLabel(" (" + myModel.getNotInMessage() + ")"), NOT_FOUND_IN_PROJECT_CARD);
280 myCardContainer.add(new JLabel(" " + IdeBundle.message("label.choosebyname.no.matches.found")), NOT_FOUND_CARD);
281 myCardContainer.add(new JLabel(" " + IdeBundle.message("label.choosebyname.searching")), SEARCHING_CARD);
282 myCard.show(myCardContainer, CHECK_BOX_CARD);
284 //myCaseCheckBox = new JCheckBox("Ignore case");
285 //myCaseCheckBox.setMnemonic('g');
286 //myCaseCheckBox.setSelected(true);
288 //myCamelCheckBox = new JCheckBox("Camel words");
289 //myCamelCheckBox.setMnemonic('w');
290 //myCamelCheckBox.setSelected(true);
292 if (isCheckboxVisible()) {
293 hBox.add(myCardContainer);
294 //hBox.add(myCheckBox);
295 //hBox.add(myCaseCheckBox);
296 //hBox.add(myCamelCheckBox);
298 if(myToolArea != null) {
299 // if too area was set, add it to hbox
300 hBox.add(myToolArea);
302 myTextFieldPanel.add(hBox);
304 myHistory = new ArrayList<Pair<String, Integer>>();
305 myFuture = new ArrayList<Pair<String, Integer>>();
306 myTextField = new MyTextField();
307 myTextField.setText(myInitialText);
309 final ActionMap actionMap = new ActionMap();
310 actionMap.setParent(myTextField.getActionMap());
311 actionMap.put(DefaultEditorKit.copyAction, new AbstractAction() {
312 public void actionPerformed(ActionEvent e) {
313 if (myTextField.getSelectedText() != null) {
314 actionMap.getParent().get(DefaultEditorKit.copyAction).actionPerformed(e);
315 return;
317 final Object chosenElement = getChosenElement();
318 if (chosenElement instanceof PsiElement) {
319 CopyReferenceAction.doCopy((PsiElement)chosenElement, myProject);
323 myTextField.setActionMap(actionMap);
325 myTextFieldPanel.add(myTextField);
326 EditorColorsScheme scheme = EditorColorsManager.getInstance().getGlobalScheme();
327 Font editorFont = new Font(scheme.getEditorFontName(), Font.PLAIN, scheme.getEditorFontSize());
328 myTextField.setFont(editorFont);
330 if (isCloseByFocusLost()) {
331 myTextField.addFocusListener(new FocusAdapter() {
332 public void focusLost(final FocusEvent e) {
333 myHideAlarm.addRequest(new Runnable() {
334 public void run() {
335 if (!JBPopupFactory.getInstance().isChildPopupFocused(e.getComponent())) {
336 hideHint();
339 }, 200);
344 myCheckBox.addItemListener(new ItemListener() {
345 public void itemStateChanged(ItemEvent e) {
346 rebuildList();
349 myCheckBox.setFocusable(false);
351 myTextField.getDocument().addDocumentListener(new DocumentAdapter() {
352 protected void textChanged(DocumentEvent e) {
353 rebuildList();
357 myTextField.addKeyListener(new KeyAdapter() {
358 public void keyPressed(KeyEvent e) {
359 if (!myListScrollPane.isVisible()) {
360 return;
362 final int keyCode = e.getKeyCode();
363 switch (keyCode) {
364 case KeyEvent.VK_DOWN:
365 ListScrollingUtil.moveDown(myList, e.getModifiersEx());
366 break;
367 case KeyEvent.VK_UP:
368 ListScrollingUtil.moveUp(myList, e.getModifiersEx());
369 break;
370 case KeyEvent.VK_PAGE_UP:
371 ListScrollingUtil.movePageUp(myList);
372 break;
373 case KeyEvent.VK_PAGE_DOWN:
374 ListScrollingUtil.movePageDown(myList);
375 break;
376 case KeyEvent.VK_ENTER:
377 if (myList.getSelectedValue() == EXTRA_ELEM) {
378 myMaximumListSizeLimit += MAXIMUM_LIST_SIZE_LIMIT;
379 rebuildList(myList.getSelectedIndex(), REBUILD_DELAY, null, ModalityState.current());
380 e.consume();
382 break;
387 myTextField.addActionListener(new ActionListener() {
388 public void actionPerformed(ActionEvent actionEvent) {
389 doClose(true);
393 myListModel = new DefaultListModel();
394 myList = new JList(myListModel);
395 myList.setFocusable(false);
396 myList.setSelectionMode(allowMultipleSelection ? ListSelectionModel.MULTIPLE_INTERVAL_SELECTION :
397 ListSelectionModel.SINGLE_SELECTION);
398 myList.addMouseListener(new MouseAdapter() {
399 public void mouseClicked(MouseEvent e) {
400 if (!myTextField.hasFocus()) {
401 myTextField.requestFocus();
404 if (e.getClickCount() == 2) {
405 if (myList.getSelectedValue() == EXTRA_ELEM) {
406 myMaximumListSizeLimit += MAXIMUM_LIST_SIZE_LIMIT;
407 rebuildList(myList.getSelectedIndex(), REBUILD_DELAY, null, ModalityState.current());
408 e.consume();
410 else {
411 doClose(true);
416 myList.setCellRenderer(myModel.getListCellRenderer());
417 myList.setFont(editorFont);
419 myList.addListSelectionListener(new ListSelectionListener() {
420 public void valueChanged(ListSelectionEvent e) {
421 choosenElementMightChange();
422 updateDocumentation();
426 myListScrollPane = new JScrollPane(myList);
428 if (!UIUtil.isMotifLookAndFeel()) {
429 UIUtil.installPopupMenuBorder(myTextFieldPanel);
431 UIUtil.installPopupMenuColorAndFonts(myTextFieldPanel);
433 showTextFieldPanel();
435 if (modalityState != null) {
436 rebuildList(0, 0, null, modalityState);
440 private void hideHint() {
441 if (!myTextFieldPanel.focusRequested()) {
442 doClose(false);
443 myTextFieldPanel.hideHint();
448 * Default rebuild list. It uses {@link #REBUILD_DELAY} and current modality state.
450 public void rebuildList() {
451 // TODO this method is public, because the chooser does not listed for the model.
452 rebuildList(0, REBUILD_DELAY, null, ModalityState.current());
455 private void updateDocumentation() {
456 final JBPopup hint = myTextFieldPanel.getHint();
457 final Object element = getChosenElement();
458 if (hint != null) {
459 if (element instanceof PsiElement) {
460 myTextFieldPanel.updateHint((PsiElement)element);
461 } else if (element instanceof DataProvider) {
462 final Object o = ((DataProvider)element).getData(DataConstants.PSI_ELEMENT);
463 if (o instanceof PsiElement) {
464 myTextFieldPanel.updateHint((PsiElement)o);
470 private void doClose(final boolean ok) {
471 myListUpdater.cancelAll();
472 close(ok);
475 private synchronized void ensureNamesLoaded(boolean checkboxState) {
476 int index = checkboxState ? 1 : 0;
477 if (myNames[index] != null) return;
479 Window window = (Window)SwingUtilities.getAncestorOfClass(Window.class, myTextField);
480 //LOG.assertTrue (myTextField != null);
481 //LOG.assertTrue (window != null);
482 Window ownerWindow = null;
483 if (window != null) {
484 window.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
485 ownerWindow = window.getOwner();
486 if (ownerWindow != null) {
487 ownerWindow.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
490 myNames[index] = myModel.getNames(checkboxState);
492 if (window != null) {
493 window.setCursor(Cursor.getDefaultCursor());
494 if (ownerWindow != null) {
495 ownerWindow.setCursor(Cursor.getDefaultCursor());
500 protected abstract boolean isCheckboxVisible();
502 protected abstract boolean isShowListForEmptyPattern();
504 protected abstract boolean isCloseByFocusLost();
506 protected void showTextFieldPanel() {
507 final JLayeredPane layeredPane = getLayeredPane();
508 final Dimension preferredTextFieldPanelSize = myTextFieldPanel.getPreferredSize();
509 final int x = (layeredPane.getWidth() - preferredTextFieldPanelSize.width) / 2;
510 final int paneHeight = layeredPane.getHeight();
511 final int y = paneHeight / 3 - preferredTextFieldPanelSize.height / 2;
514 myTextFieldPanel.setBounds(x, y, preferredTextFieldPanelSize.width, preferredTextFieldPanelSize.height);
515 layeredPane.add(myTextFieldPanel, Integer.valueOf(500));
516 layeredPane.moveToFront(myTextFieldPanel);
517 VISIBLE_LIST_SIZE_LIMIT = Math.max
518 (10, (paneHeight - (y + preferredTextFieldPanelSize.height)) / (preferredTextFieldPanelSize.height / 2) - 1);
520 // I'm registering KeyListener to close popup only by KeyTyped event.
521 // If react on KeyPressed then sometime KeyTyped goes into underlying editor.
522 // It causes typing of Enter into it.
523 myTextFieldPanel.registerKeyboardAction(new AbstractAction() {
524 public void actionPerformed(ActionEvent e) {
525 doClose(false);
527 }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
528 JComponent.WHEN_IN_FOCUSED_WINDOW
531 myList.registerKeyboardAction(new AbstractAction() {
532 public void actionPerformed(ActionEvent e) {
533 doClose(false);
535 }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
536 JComponent.WHEN_IN_FOCUSED_WINDOW
539 if (myTextField.requestFocusInWindow() || SystemInfo.isMac) {
540 myTextField.requestFocus();
543 myTextFieldPanel.validate();
544 myTextFieldPanel.paintImmediately(0, 0, myTextFieldPanel.getWidth(), myTextFieldPanel.getHeight());
547 private JLayeredPane getLayeredPane() {
548 JLayeredPane layeredPane;
549 final Window window = WindowManager.getInstance().suggestParentWindow(myProject);
550 if (window instanceof JFrame) {
551 layeredPane = ((JFrame)window).getLayeredPane();
553 else if (window instanceof JDialog) {
554 layeredPane = ((JDialog)window).getLayeredPane();
556 else {
557 throw new IllegalStateException("cannot find parent window: project=" + myProject +
558 (myProject != null ? "; open=" + myProject.isOpen() : "") +
559 "; window=" + window);
561 return layeredPane;
564 private final Object myRebuildMutex = new Object ();
566 protected void rebuildList(final int pos, final int delay, final Runnable postRunnable, final ModalityState modalityState) {
567 myListIsUpToDate = false;
568 myAlarm.cancelAllRequests();
569 myListUpdater.cancelAll();
571 tryToCancel();
572 ApplicationManager.getApplication().invokeLater(new Runnable() {
573 public void run() {
574 final String text = myTextField.getText();
575 if (!isShowListForEmptyPattern() && (text == null || text.trim().length() == 0)) {
576 myListModel.clear();
577 hideList();
578 myCard.show(myCardContainer, CHECK_BOX_CARD);
579 return;
581 final Runnable request = new Runnable() {
582 public void run() {
583 final CalcElementsCallback callback = new CalcElementsCallback() {
584 public void run(final Set<?> elements) {
585 synchronized (myRebuildMutex) {
586 ApplicationManager.getApplication().assertIsDispatchThread();
587 if (myDisposedFlag) {
588 return;
591 setElementsToList(pos, elements);
593 myListIsUpToDate = true;
594 choosenElementMightChange();
596 if (postRunnable != null) {
597 postRunnable.run();
603 tryToCancel();
605 myCalcElementsThread = new CalcElementsThread(text, myCheckBox.isSelected(), callback, modalityState);
606 myCalcElementsThread.setCanCancel(postRunnable == null);
607 ApplicationManager.getApplication().executeOnPooledThread(myCalcElementsThread);
611 if (delay > 0) {
612 myAlarm.addRequest(request, delay, ModalityState.stateForComponent(myTextField));
614 else {
615 request.run();
618 }, modalityState);
621 private void tryToCancel() {
622 if (myCalcElementsThread != null) {
623 myCalcElementsThread.cancel();
624 myCalcElementsThread = null;
628 private void setElementsToList(int pos, Set<?> elements) {
629 myListUpdater.cancelAll();
630 if (myDisposedFlag) return;
631 if (elements.isEmpty()) {
632 myListModel.clear();
633 myTextField.setForeground(Color.red);
634 myListUpdater.cancelAll();
635 hideList();
636 return;
639 Object[] oldElements = myListModel.toArray();
640 Object[] newElements = elements.toArray();
641 Diff.Change change = Diff.buildChanges(oldElements, newElements);
643 if (change == null) return; // Nothing changed
645 List<Cmd> commands = new ArrayList<Cmd>();
646 int inserted = 0;
647 int deleted = 0;
648 while (change != null) {
649 if (change.deleted > 0) {
650 final int start = change.line0 + inserted - deleted;
651 commands.add(new RemoveCmd(start, start + change.deleted - 1));
654 if (change.inserted > 0) {
655 for (int i = 0; i < change.inserted; i++) {
656 commands.add(new InsertCmd(change.line0 + i + inserted - deleted, newElements[change.line1 + i]));
660 deleted += change.deleted;
661 inserted += change.inserted;
662 change = change.link;
665 myTextField.setForeground(UIUtil.getTextFieldForeground());
666 if (!commands.isEmpty()) {
667 showList();
668 myListUpdater.appendToModel(commands, pos);
670 else {
671 if (pos == 0) {
672 pos = detectBestStatisticalPosition();
675 ListScrollingUtil.selectItem(myList, Math.min(pos, myListModel.size() - 1));
676 myList.setVisibleRowCount(Math.min(VISIBLE_LIST_SIZE_LIMIT, myList.getModel().getSize()));
677 showList();
681 private int detectBestStatisticalPosition() {
682 int best = 0;
683 int bestPosition = 0;
684 final int count = myListModel.getSize();
686 final String statContext = statisticsContext();
687 for (int i = 0; i < count; i++) {
688 final Object modelElement = myListModel.getElementAt(i);
689 String text = EXTRA_ELEM.equals(modelElement) ? null : myModel.getFullName(modelElement);
690 if (text != null) {
691 int stats = StatisticsManager.getInstance().getUseCount(new StatisticsInfo(statContext, text));
692 if (stats > best) {
693 best = stats;
694 bestPosition = i;
699 return bestPosition;
702 protected String statisticsContext() {
703 return "choose_by_name#"+myModel.getPromptText()+"#"+ myCheckBox.isSelected() + "#" + myTextField.getText();
706 private String getQualifierPattern(String pattern) {
707 final String[] separators = myModel.getSeparators();
708 int lastSeparatorOccurence = 0;
709 for (String separator : separators) {
710 lastSeparatorOccurence = Math.max(lastSeparatorOccurence, pattern.lastIndexOf(separator));
712 return pattern.substring(0, lastSeparatorOccurence);
715 public String getNamePattern(String pattern) {
716 final String[] separators = myModel.getSeparators();
717 int lastSeparatorOccurence = 0;
718 for (String separator : separators) {
719 final int idx = pattern.lastIndexOf(separator);
720 lastSeparatorOccurence = Math.max(lastSeparatorOccurence, idx == -1 ? idx : idx + separator.length());
723 return pattern.substring(lastSeparatorOccurence);
726 private interface Cmd {
727 void apply();
730 private class RemoveCmd implements Cmd {
731 private final int start;
732 private final int end;
734 public RemoveCmd(final int start, final int end) {
735 this.start = start;
736 this.end = end;
739 public void apply() {
740 myListModel.removeRange(start, end);
744 private class InsertCmd implements Cmd {
745 private final int idx;
746 private final Object element;
748 public InsertCmd(final int idx, final Object element) {
749 this.idx = idx;
750 this.element = element;
753 public void apply() {
754 if (idx < myListModel.size()) {
755 myListModel.add(idx, element);
757 else {
758 myListModel.addElement(element);
763 private class ListUpdater {
764 private final Alarm myAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD);
765 private static final int DELAY = 10;
766 private static final int MAX_BLOCKING_TIME = 30;
767 private final List<Cmd> myCommands = Collections.synchronizedList(new ArrayList<Cmd>());
769 public void cancelAll() {
770 myCommands.clear();
771 myAlarm.cancelAllRequests();
774 public void appendToModel(final List<Cmd> commands, final int selectionPos) {
775 myAlarm.cancelAllRequests();
776 myCommands.addAll(commands);
778 if (myCommands.isEmpty() || myDisposedFlag) return;
779 myAlarm.addRequest(new Runnable() {
780 public void run() {
781 if (myDisposedFlag) return;
782 final long startTime = System.currentTimeMillis();
783 while (!myCommands.isEmpty() && System.currentTimeMillis() - startTime < MAX_BLOCKING_TIME) {
784 final Cmd cmd = myCommands.remove(0);
785 cmd.apply();
788 myList.setVisibleRowCount(Math.min(VISIBLE_LIST_SIZE_LIMIT, myList.getModel().getSize()));
789 if (!myListModel.isEmpty()) {
790 int pos = selectionPos == 0 ? detectBestStatisticalPosition() : selectionPos;
791 ListScrollingUtil.selectItem(myList, Math.min(pos, myListModel.size() - 1));
794 if (!myCommands.isEmpty()) {
795 myAlarm.addRequest(this, DELAY);
797 showList();
799 }, DELAY);
803 protected abstract void showList();
805 protected abstract void hideList();
807 protected abstract void close(boolean isOk);
809 @Nullable
810 public Object getChosenElement() {
811 final List<Object> elements = getChosenElements();
812 return elements != null && elements.size() == 1 ? elements.get(0) : null;
815 protected List<Object> getChosenElements() {
816 if (myListIsUpToDate) {
817 List<Object> values = new ArrayList<Object>(Arrays.asList(myList.getSelectedValues()));
818 values.remove(EXTRA_ELEM);
819 return values;
822 final String text = myTextField.getText();
823 final boolean checkBoxState = myCheckBox.isSelected();
824 //ensureNamesLoaded(checkBoxState);
825 final String[] names = checkBoxState ? myNames[1] : myNames[0];
826 if (names == null) return Collections.emptyList();
828 Object uniqueElement = null;
830 for (final String name : names) {
831 if (text.equalsIgnoreCase(name)) {
832 final Object[] elements = myModel.getElementsByName(name, checkBoxState, text);
833 if (elements.length > 1) return Collections.emptyList();
834 if (elements.length == 0) continue;
835 if (uniqueElement != null) return Collections.emptyList();
836 uniqueElement = elements[0];
839 return uniqueElement == null ? Collections.emptyList() : Collections.singletonList(uniqueElement);
842 protected void choosenElementMightChange() {
845 private final class MyTextField extends JTextField implements PopupOwner {
846 private final KeyStroke myCompletionKeyStroke;
847 private final KeyStroke forwardStroke;
848 private final KeyStroke backStroke;
850 public MyTextField() {
851 super(40);
852 enableEvents(AWTEvent.KEY_EVENT_MASK);
853 myCompletionKeyStroke = getShortcut(IdeActions.ACTION_CODE_COMPLETION);
854 forwardStroke = getShortcut(IdeActions.ACTION_GOTO_FORWARD);
855 backStroke = getShortcut(IdeActions.ACTION_GOTO_BACK);
859 private KeyStroke getShortcut(String actionCodeCompletion) {
860 final Shortcut[] shortcuts = KeymapManager.getInstance().getActiveKeymap().getShortcuts(actionCodeCompletion);
861 for (final Shortcut shortcut : shortcuts) {
862 if (shortcut instanceof KeyboardShortcut) {
863 return ((KeyboardShortcut)shortcut).getFirstKeyStroke();
866 return null;
869 protected void processKeyEvent(KeyEvent e) {
870 final KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(e);
871 if (myCompletionKeyStroke != null && keyStroke.equals(myCompletionKeyStroke)) {
872 e.consume();
873 final String pattern = myTextField.getText();
874 final String oldText = myTextField.getText();
875 final int oldPos = myList.getSelectedIndex();
876 myHistory.add(Pair.create(oldText, oldPos));
877 final Runnable postRunnable = new Runnable() {
878 public void run() {
879 fillInCommonPrefix(pattern);
882 rebuildList(0, 0, postRunnable, ModalityState.current());
883 return;
885 if (backStroke != null && keyStroke.equals(backStroke)) {
886 e.consume();
887 if (!myHistory.isEmpty()) {
888 final String oldText = myTextField.getText();
889 final int oldPos = myList.getSelectedIndex();
890 final Pair<String, Integer> last = myHistory.remove(myHistory.size() - 1);
891 myTextField.setText(last.first);
892 myFuture.add(Pair.create(oldText, oldPos));
893 rebuildList(0, 0, null, ModalityState.current());
895 return;
897 if (forwardStroke != null && keyStroke.equals(forwardStroke)) {
898 e.consume();
899 if (!myFuture.isEmpty()) {
900 final String oldText = myTextField.getText();
901 final int oldPos = myList.getSelectedIndex();
902 final Pair<String, Integer> next = myFuture.remove(myFuture.size() - 1);
903 myTextField.setText(next.first);
904 myHistory.add(Pair.create(oldText, oldPos));
905 rebuildList(0, 0, null, ModalityState.current());
907 return;
909 super.processKeyEvent(e);
912 private void fillInCommonPrefix(final String pattern) {
913 final ArrayList<String> list = new ArrayList<String>();
914 getNamesByPattern(myCheckBox.isSelected(), null, list, pattern);
916 if (isComplexPattern(pattern)) return; //TODO: support '*'
917 final String oldText = myTextField.getText();
918 final int oldPos = myList.getSelectedIndex();
920 String commonPrefix = null;
921 if (!list.isEmpty()) {
922 for (String name : list) {
923 final String string = name.toLowerCase();
924 if (commonPrefix == null) {
925 commonPrefix = string;
927 else {
928 while (commonPrefix.length() > 0) {
929 if (string.startsWith(commonPrefix)) {
930 break;
932 commonPrefix = commonPrefix.substring(0, commonPrefix.length() - 1);
934 if (commonPrefix.length() == 0) break;
937 commonPrefix = list.get(0).substring(0, commonPrefix.length());
938 for (int i = 1; i < list.size(); i++) {
939 final String string = list.get(i).substring(0, commonPrefix.length());
940 if (!string.equals(commonPrefix)) {
941 commonPrefix = commonPrefix.toLowerCase();
942 break;
946 if (commonPrefix == null) commonPrefix = "";
947 final String newPattern = commonPrefix;
949 myHistory.add(Pair.create(oldText, oldPos));
950 myTextField.setText(newPattern);
951 myTextField.setCaretPosition(newPattern.length());
953 rebuildList();
956 private boolean isComplexPattern(final String pattern) {
957 if (pattern.indexOf('*') >= 0) return true;
958 for (String s : myModel.getSeparators()) {
959 if (pattern.contains(s)) return true;
962 return false;
965 @Nullable
966 public Point getBestPopupPosition() {
967 return new Point(myTextFieldPanel.getWidth(), getHeight());
970 protected void paintComponent(final Graphics g) {
971 UISettings.setupAntialiasing(g);
972 super.paintComponent(g);
976 private static final String EXTRA_ELEM = "...";
978 private class CalcElementsThread implements Runnable {
979 private final String myPattern;
980 private boolean myCheckboxState;
981 private final CalcElementsCallback myCallback;
982 private final ModalityState myModalityState;
984 private Set<Object> myElements = null;
986 private volatile boolean myCancelled = false;
987 private boolean myCanCancel = true;
989 public CalcElementsThread(String pattern, boolean checkboxState, CalcElementsCallback callback, ModalityState modalityState) {
990 myPattern = pattern;
991 myCheckboxState = checkboxState;
992 myCallback = callback;
993 myModalityState = modalityState;
996 private final Alarm myShowCardAlarm = new Alarm();
997 public void run() {
998 showCard(SEARCHING_CARD, 200);
1000 final Set<Object> elements = new LinkedHashSet<Object>();
1001 Runnable action = new Runnable() {
1002 public void run() {
1003 try {
1004 ensureNamesLoaded(myCheckboxState);
1005 addElementsByPattern(elements, myPattern);
1006 for (Object elem : elements) {
1007 if (myCancelled) throw new ProcessCanceledException();
1008 if (elem instanceof PsiElement) {
1009 final PsiElement psiElement = (PsiElement)elem;
1010 psiElement.isWritable(); // That will cache writable flag in VirtualFile. Taking the action here makes it canceleable.
1014 catch (ProcessCanceledException e) {
1015 //OK
1019 ApplicationManager.getApplication().runReadAction(action);
1021 if (myCancelled) {
1022 myShowCardAlarm.cancelAllRequests();
1023 return;
1026 final String cardToShow;
1027 if (elements.isEmpty() && !myCheckboxState) {
1028 myCheckboxState = true;
1029 ApplicationManager.getApplication().runReadAction(action);
1030 cardToShow = elements.isEmpty() ? NOT_FOUND_CARD : NOT_FOUND_IN_PROJECT_CARD;
1032 else {
1033 cardToShow = elements.isEmpty() ? NOT_FOUND_CARD : CHECK_BOX_CARD;
1035 showCard(cardToShow, 0);
1037 myElements = elements;
1039 ApplicationManager.getApplication().invokeLater(new Runnable() {
1040 public void run() {
1041 myCallback.run(myElements);
1043 }, myModalityState);
1046 private void showCard(final String card, final int delay) {
1047 myShowCardAlarm.cancelAllRequests();
1048 myShowCardAlarm.addRequest(new Runnable() {
1049 public void run() {
1050 myCard.show(myCardContainer, card);
1052 }, delay, myModalityState);
1055 public void setCanCancel(boolean canCancel) {
1056 myCanCancel = canCancel;
1059 private void addElementsByPattern(Set<Object> elementsArray, String pattern) {
1060 String namePattern = getNamePattern(pattern);
1061 String qualifierPattern = getQualifierPattern(pattern);
1063 boolean empty = namePattern.length() == 0 || namePattern.equals("@"); // TODO[yole]: remove implicit dependency
1064 if (empty && !isShowListForEmptyPattern()) return;
1066 List<String> namesList = new ArrayList<String>();
1067 getNamesByPattern(myCheckboxState, this, namesList, namePattern);
1068 if (myCancelled) {
1069 throw new ProcessCanceledException();
1071 Collections.sort(namesList, new MatchesComparator(pattern));
1073 boolean overflow = false;
1074 List<Object> sameNameElements = new SmartList<Object>();
1075 All:
1076 for (String name : namesList) {
1077 if (myCancelled) {
1078 throw new ProcessCanceledException();
1080 final Object[] elements = myModel.getElementsByName(name, myCheckboxState, namePattern);
1081 if (elements.length > 1) {
1082 sameNameElements.clear();
1083 for (final Object element : elements) {
1084 if (matchesQualifier(element, qualifierPattern)) {
1085 sameNameElements.add(element);
1088 sortByProximity(sameNameElements);
1089 for (Object element : sameNameElements) {
1090 elementsArray.add(element);
1091 if (elementsArray.size() >= myMaximumListSizeLimit) {
1092 overflow = true;
1093 break All;
1097 else if (elements.length == 1 && matchesQualifier(elements[0], qualifierPattern)) {
1098 elementsArray.add(elements[0]);
1099 if (elementsArray.size() >= myMaximumListSizeLimit) {
1100 overflow = true;
1101 break;
1106 if (overflow) {
1107 elementsArray.add(EXTRA_ELEM);
1111 private void cancel() {
1112 if (myCanCancel) {
1113 myCancelled = true;
1118 private void sortByProximity(final List<Object> sameNameElements) {
1119 Collections.sort(sameNameElements, new PathProximityComparator(myModel, myContext.get()));
1122 private List<String> split(String s) {
1123 for (String separator : myModel.getSeparators()) {
1124 final List<String> result = StringUtil.split(s, separator);
1125 if (!result.isEmpty()) return result;
1127 return Collections.singletonList(s);
1130 private boolean matchesQualifier(final Object element, final String qualifierPattern) {
1131 final String name = myModel.getFullName(element);
1132 if (name == null) return false;
1134 final List<String> suspects = split(name);
1135 final List<String> patterns = split(qualifierPattern);
1137 int matchPosition = 0;
1139 patterns:
1140 for (String pattern : patterns) {
1141 if (pattern.length() > 0) {
1142 for (int j = matchPosition; j < suspects.size() - 1; j++) {
1143 String suspect = suspects.get(j);
1144 if (StringUtil.startsWithIgnoreCase(suspect, pattern)) {
1145 matchPosition = j + 1;
1146 continue patterns;
1150 return false;
1154 return true;
1157 private void getNamesByPattern(final boolean checkboxState,
1158 CalcElementsThread calcElementsThread,
1159 final List<String> list,
1160 String pattern) throws ProcessCanceledException {
1161 if (!isShowListForEmptyPattern()) {
1162 LOG.assertTrue(pattern.length() > 0);
1165 if (pattern.startsWith("@")) {
1166 pattern = pattern.substring(1);
1169 final String[] names = checkboxState ? myNames[1] : myNames[0];
1170 final String regex = NameUtil.buildRegexp(pattern, 0, true, true);
1172 try {
1173 Perl5Compiler compiler = new Perl5Compiler();
1174 final Pattern compiledPattern = compiler.compile(regex);
1175 final PatternMatcher matcher = new Perl5Matcher();
1177 for (String name : names) {
1178 if (calcElementsThread != null && calcElementsThread.myCancelled) {
1179 throw new ProcessCanceledException();
1181 if (name != null) {
1182 if (myModel instanceof CustomMatcherModel) {
1183 if (((CustomMatcherModel)myModel).matches(name, pattern)) {
1184 list.add(name);
1187 else if (pattern.length() == 0 || matcher.matches(name, compiledPattern)) {
1188 list.add(name);
1193 catch (MalformedPatternException e) {
1194 // Do nothing. No matches appears valid result for "bad" pattern
1198 private interface CalcElementsCallback {
1199 void run(Set<?> elements);
1202 private static class PathProximityComparator implements Comparator<Object> {
1203 private final ChooseByNameModel myModel;
1204 private final PsiProximityComparator myProximityComparator;
1206 private PathProximityComparator(final ChooseByNameModel model, final PsiElement context) {
1207 myModel = model;
1208 myProximityComparator = new PsiProximityComparator(context);
1211 public int compare(final Object o1, final Object o2) {
1212 int rc = myProximityComparator.compare(o1, o2);
1213 if (rc != 0) return rc;
1215 return Comparing.compare(myModel.getFullName(o1), myModel.getFullName(o2));