05b214914d10bacc7da2548f730c5cf876391376
[fedora-idea.git] / platform / lang-impl / src / com / intellij / ide / util / gotoByName / ChooseByNameBase.java
blob05b214914d10bacc7da2548f730c5cf876391376
1 /*
2 * Copyright 2000-2009 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.intellij.ide.util.gotoByName;
19 import com.intellij.Patches;
20 import com.intellij.ide.IdeBundle;
21 import com.intellij.ide.actions.CopyReferenceAction;
22 import com.intellij.ide.ui.UISettings;
23 import com.intellij.openapi.actionSystem.*;
24 import com.intellij.openapi.application.ApplicationManager;
25 import com.intellij.openapi.application.ModalityState;
26 import com.intellij.openapi.diagnostic.Logger;
27 import com.intellij.openapi.editor.colors.EditorColorsManager;
28 import com.intellij.openapi.editor.colors.EditorColorsScheme;
29 import com.intellij.openapi.keymap.KeymapManager;
30 import com.intellij.openapi.progress.ProcessCanceledException;
31 import com.intellij.openapi.project.Project;
32 import com.intellij.openapi.ui.popup.JBPopup;
33 import com.intellij.openapi.ui.popup.JBPopupFactory;
34 import com.intellij.openapi.util.ActionCallback;
35 import com.intellij.openapi.util.Comparing;
36 import com.intellij.openapi.util.Pair;
37 import com.intellij.openapi.util.registry.Registry;
38 import com.intellij.openapi.util.text.StringUtil;
39 import com.intellij.openapi.wm.IdeFocusManager;
40 import com.intellij.openapi.wm.WindowManager;
41 import com.intellij.openapi.wm.ex.WindowManagerEx;
42 import com.intellij.psi.PsiElement;
43 import com.intellij.psi.codeStyle.NameUtil;
44 import com.intellij.psi.statistics.StatisticsInfo;
45 import com.intellij.psi.statistics.StatisticsManager;
46 import com.intellij.psi.util.proximity.PsiProximityComparator;
47 import com.intellij.ui.DocumentAdapter;
48 import com.intellij.ui.ListScrollingUtil;
49 import com.intellij.ui.popup.PopupOwner;
50 import com.intellij.ui.popup.PopupUpdateProcessor;
51 import com.intellij.util.Alarm;
52 import com.intellij.util.Function;
53 import com.intellij.util.SmartList;
54 import com.intellij.util.containers.ContainerUtil;
55 import com.intellij.util.diff.Diff;
56 import com.intellij.util.ui.UIUtil;
57 import org.jetbrains.annotations.NonNls;
58 import org.jetbrains.annotations.Nullable;
60 import javax.swing.*;
61 import javax.swing.event.DocumentEvent;
62 import javax.swing.event.ListSelectionEvent;
63 import javax.swing.event.ListSelectionListener;
64 import javax.swing.text.DefaultEditorKit;
65 import java.awt.*;
66 import java.awt.event.*;
67 import java.lang.ref.Reference;
68 import java.lang.ref.WeakReference;
69 import java.util.*;
70 import java.util.List;
72 public abstract class ChooseByNameBase{
73 private static final Logger LOG = Logger.getInstance("#com.intellij.ide.util.gotoByName.ChooseByNameBase");
75 protected final Project myProject;
76 protected final ChooseByNameModel myModel;
77 protected final String myInitialText;
78 private final Reference<PsiElement> myContext;
80 protected Component myPreviouslyFocusedComponent;
82 protected JPanelProvider myTextFieldPanel;// Located in the layered pane
83 protected JTextField myTextField;
84 private JPanel myCardContainer;
85 private CardLayout myCard;
86 protected JCheckBox myCheckBox;
87 /** the tool area of the popup, it is just after card box */
88 protected JComponent myToolArea;
90 protected JScrollPane myListScrollPane; // Located in the layered pane
91 protected JList myList;
92 private DefaultListModel myListModel;
93 private List<Pair<String, Integer>> myHistory;
94 private List<Pair<String, Integer>> myFuture;
96 protected ChooseByNamePopupComponent.Callback myActionListener;
98 protected final Alarm myAlarm = new Alarm();
100 private final ListUpdater myListUpdater = new ListUpdater();
102 private volatile boolean myListIsUpToDate = false;
103 protected boolean myDisposedFlag = false;
104 private ActionCallback myPosponedOkAction;
106 private final String[][] myNames = new String[2][];
107 private CalcElementsThread myCalcElementsThread;
108 private static int VISIBLE_LIST_SIZE_LIMIT = 10;
109 private static final int MAXIMUM_LIST_SIZE_LIMIT = 30;
110 private int myMaximumListSizeLimit = MAXIMUM_LIST_SIZE_LIMIT;
111 @NonNls private static final String NOT_FOUND_IN_PROJECT_CARD = "syslib";
112 @NonNls private static final String NOT_FOUND_CARD = "nfound";
113 @NonNls private static final String CHECK_BOX_CARD = "chkbox";
114 @NonNls private static final String SEARCHING_CARD = "searching";
115 private static final int REBUILD_DELAY = 300;
117 private final Alarm myHideAlarm = new Alarm();
119 private static class MatchesComparator implements Comparator<String> {
120 private final String myOriginalPattern;
122 private MatchesComparator(final String originalPattern) {
123 myOriginalPattern = originalPattern.trim();
126 public int compare(final String a, final String b) {
127 boolean aStarts = a.startsWith(myOriginalPattern);
128 boolean bStarts = b.startsWith(myOriginalPattern);
129 if (aStarts && bStarts) return a.compareToIgnoreCase(b);
130 if (aStarts && !bStarts) return -1;
131 if (bStarts && !aStarts) return 1;
132 return a.compareToIgnoreCase(b);
137 * @param initialText initial text which will be in the lookup text field
138 * @param context
140 protected ChooseByNameBase(Project project, ChooseByNameModel model, String initialText, final PsiElement context) {
141 myProject = project;
142 myModel = model;
143 myInitialText = initialText;
144 myContext = new WeakReference<PsiElement>(context);
148 * Set tool area. The method may be called only before invoke.
149 * @param toolArea a tool area component
151 public void setToolArea(JComponent toolArea) {
152 if(myCard != null) {
153 throw new IllegalStateException("Tool area is modifiable only before invoke()");
155 myToolArea = toolArea;
158 public void invoke(final ChooseByNamePopupComponent.Callback callback, final ModalityState modalityState, boolean allowMultipleSelection) {
159 initUI(callback, modalityState, allowMultipleSelection);
162 public class JPanelProvider extends JPanel implements DataProvider {
163 JBPopup myHint = null;
164 boolean myFocusRequested = false;
166 JPanelProvider() {
169 public Object getData(String dataId) {
170 if (PlatformDataKeys.HELP_ID.is(dataId)) {
171 return myModel.getHelpId();
173 if (!myListIsUpToDate) {
174 return null;
176 if (LangDataKeys.PSI_ELEMENT.is(dataId)) {
177 Object element = getChosenElement();
179 if (element instanceof PsiElement) {
180 return element;
183 if (element instanceof DataProvider) {
184 return ((DataProvider)element).getData(dataId);
187 else if (LangDataKeys.PSI_ELEMENT_ARRAY.is(dataId)) {
188 final List<Object> chosenElements = getChosenElements();
189 if (chosenElements != null) {
190 List<PsiElement> result = new ArrayList<PsiElement>();
191 for (Object element : chosenElements) {
192 if (element instanceof PsiElement) {
193 result.add((PsiElement)element);
196 return result.toArray(new PsiElement[result.size()]);
199 else if (PlatformDataKeys.DOMINANT_HINT_AREA_RECTANGLE.is(dataId)) {
200 return getBounds();
202 return null;
205 public void registerHint(JBPopup h) {
206 if (myHint != null && myHint.isVisible() && myHint != h){
207 myHint.cancel();
209 myHint = h;
212 public boolean focusRequested() {
213 boolean focusRequested = myFocusRequested;
215 myFocusRequested = false;
217 return focusRequested;
220 public void requestFocus() {
221 myFocusRequested = true;
224 public void unregisterHint() {
225 myHint = null;
228 public void hideHint() {
229 if (myHint != null) {
230 myHint.cancel();
234 public JBPopup getHint() {
235 return myHint;
238 public void updateHint(PsiElement element) {
239 if (myHint == null || !myHint.isVisible()) return;
240 final PopupUpdateProcessor updateProcessor = myHint.getUserData(PopupUpdateProcessor.class);
241 if (updateProcessor != null){
242 myHint.cancel();
243 updateProcessor.updatePopup(element);
249 * @param callback
250 * @param modalityState - if not null rebuilds list in given {@link ModalityState}
251 * @param allowMultipleSelection
253 protected void initUI(final ChooseByNamePopupComponent.Callback callback, final ModalityState modalityState, boolean allowMultipleSelection) {
254 myPreviouslyFocusedComponent = WindowManagerEx.getInstanceEx().getFocusedComponent(myProject);
256 myActionListener = callback;
257 myTextFieldPanel = new JPanelProvider();
258 myTextFieldPanel.setLayout(new BoxLayout(myTextFieldPanel, BoxLayout.Y_AXIS));
259 final JPanel hBox = new JPanel();
260 hBox.setLayout(new BoxLayout(hBox, BoxLayout.X_AXIS));
262 if (myModel.getPromptText() != null) {
263 JLabel label = new JLabel(" " + myModel.getPromptText());
264 label.setFont(UIUtil.getLabelFont().deriveFont(Font.BOLD));
265 hBox.add(label);
268 myCard = new CardLayout();
269 myCardContainer = new JPanel(myCard);
271 final JPanel checkBoxPanel = new JPanel();
272 checkBoxPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 5));
273 myCheckBox = new JCheckBox(myModel.getCheckBoxName());
274 myCheckBox.setSelected(myModel.loadInitialCheckBoxState());
276 if (myModel.getPromptText() != null){
277 checkBoxPanel.setLayout(new BoxLayout(checkBoxPanel, BoxLayout.X_AXIS));
278 checkBoxPanel.add (new JLabel (" ("));
279 checkBoxPanel.add (myCheckBox);
280 checkBoxPanel.add (new JLabel (")"));
281 } else {
282 checkBoxPanel.setLayout(new BoxLayout(checkBoxPanel, BoxLayout.LINE_AXIS));
283 checkBoxPanel.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
284 checkBoxPanel.add (new JLabel (")"));
285 checkBoxPanel.add (myCheckBox);
286 checkBoxPanel.add (new JLabel (" ("));
288 checkBoxPanel.setVisible(myModel.getCheckBoxName() != null);
289 JPanel panel = new JPanel(new BorderLayout());
290 panel.add(checkBoxPanel, BorderLayout.CENTER);
291 myCardContainer.add(panel, CHECK_BOX_CARD);
292 myCardContainer.add(new JLabel(" (" + myModel.getNotInMessage() + ")"), NOT_FOUND_IN_PROJECT_CARD);
293 myCardContainer.add(new JLabel(" " + IdeBundle.message("label.choosebyname.no.matches.found")), NOT_FOUND_CARD);
294 myCardContainer.add(new JLabel(" " + IdeBundle.message("label.choosebyname.searching")), SEARCHING_CARD);
295 myCard.show(myCardContainer, CHECK_BOX_CARD);
297 //myCaseCheckBox = new JCheckBox("Ignore case");
298 //myCaseCheckBox.setMnemonic('g');
299 //myCaseCheckBox.setSelected(true);
301 //myCamelCheckBox = new JCheckBox("Camel words");
302 //myCamelCheckBox.setMnemonic('w');
303 //myCamelCheckBox.setSelected(true);
305 if (isCheckboxVisible()) {
306 hBox.add(myCardContainer);
307 //hBox.add(myCheckBox);
308 //hBox.add(myCaseCheckBox);
309 //hBox.add(myCamelCheckBox);
311 if(myToolArea != null) {
312 // if too area was set, add it to hbox
313 hBox.add(myToolArea);
315 myTextFieldPanel.add(hBox);
317 myHistory = new ArrayList<Pair<String, Integer>>();
318 myFuture = new ArrayList<Pair<String, Integer>>();
319 myTextField = new MyTextField();
320 myTextField.setText(myInitialText);
322 final ActionMap actionMap = new ActionMap();
323 actionMap.setParent(myTextField.getActionMap());
324 actionMap.put(DefaultEditorKit.copyAction, new AbstractAction() {
325 public void actionPerformed(ActionEvent e) {
326 if (myTextField.getSelectedText() != null) {
327 actionMap.getParent().get(DefaultEditorKit.copyAction).actionPerformed(e);
328 return;
330 final Object chosenElement = getChosenElement();
331 if (chosenElement instanceof PsiElement) {
332 CopyReferenceAction.doCopy((PsiElement)chosenElement, myProject);
336 myTextField.setActionMap(actionMap);
338 myTextFieldPanel.add(myTextField);
339 EditorColorsScheme scheme = EditorColorsManager.getInstance().getGlobalScheme();
340 Font editorFont = new Font(scheme.getEditorFontName(), Font.PLAIN, scheme.getEditorFontSize());
341 myTextField.setFont(editorFont);
343 if (isCloseByFocusLost()) {
344 myTextField.addFocusListener(new FocusAdapter() {
345 public void focusLost(final FocusEvent e) {
346 myHideAlarm.addRequest(new Runnable() {
347 public void run() {
348 if (!JBPopupFactory.getInstance().isChildPopupFocused(e.getComponent())) {
349 hideHint();
352 }, 200);
357 myCheckBox.addItemListener(new ItemListener() {
358 public void itemStateChanged(ItemEvent e) {
359 rebuildList();
362 myCheckBox.setFocusable(false);
364 myTextField.getDocument().addDocumentListener(new DocumentAdapter() {
365 protected void textChanged(DocumentEvent e) {
366 clearPosponedOkAction(false);
367 rebuildList();
371 myTextField.addKeyListener(new KeyAdapter() {
372 public void keyPressed(KeyEvent e) {
373 if (!myListScrollPane.isVisible()) {
374 return;
376 final int keyCode = e.getKeyCode();
377 switch (keyCode) {
378 case KeyEvent.VK_DOWN:
379 ListScrollingUtil.moveDown(myList, e.getModifiersEx());
380 break;
381 case KeyEvent.VK_UP:
382 ListScrollingUtil.moveUp(myList, e.getModifiersEx());
383 break;
384 case KeyEvent.VK_PAGE_UP:
385 ListScrollingUtil.movePageUp(myList);
386 break;
387 case KeyEvent.VK_PAGE_DOWN:
388 ListScrollingUtil.movePageDown(myList);
389 break;
390 case KeyEvent.VK_ENTER:
391 // Diagnostics for Enter key problems with IdeaVim installed
392 if (myTextField.getActionMap().get("notify-field-accept") == null){
393 LOG.error("Text field has no action for 'notify-field-accept' input map entry");
394 for (Object o : myTextField.getActionMap().allKeys()) {
395 if (o instanceof String && ((String)o).contains("vim")){
396 LOG.error("Text field action map contains ExEditorKit action: " + o);
400 if (myList.getSelectedValue() == EXTRA_ELEM) {
401 myMaximumListSizeLimit += MAXIMUM_LIST_SIZE_LIMIT;
402 rebuildList(myList.getSelectedIndex(), REBUILD_DELAY, null, ModalityState.current());
403 e.consume();
405 break;
410 myTextField.addActionListener(new ActionListener() {
411 public void actionPerformed(ActionEvent actionEvent) {
412 doClose(true);
416 myListModel = new DefaultListModel();
417 myList = new JList(myListModel);
418 myList.setFocusable(false);
419 myList.setSelectionMode(allowMultipleSelection ? ListSelectionModel.MULTIPLE_INTERVAL_SELECTION :
420 ListSelectionModel.SINGLE_SELECTION);
421 myList.addMouseListener(new MouseAdapter() {
422 public void mouseClicked(MouseEvent e) {
423 if (!myTextField.hasFocus()) {
424 myTextField.requestFocus();
427 if (e.getClickCount() == 2) {
428 if (myList.getSelectedValue() == EXTRA_ELEM) {
429 myMaximumListSizeLimit += MAXIMUM_LIST_SIZE_LIMIT;
430 rebuildList(myList.getSelectedIndex(), REBUILD_DELAY, null, ModalityState.current());
431 e.consume();
433 else {
434 doClose(true);
439 myList.setCellRenderer(myModel.getListCellRenderer());
440 myList.setFont(editorFont);
442 myList.addListSelectionListener(new ListSelectionListener() {
443 public void valueChanged(ListSelectionEvent e) {
444 choosenElementMightChange();
445 updateDocumentation();
449 myListScrollPane = new JScrollPane(myList);
451 if (!UIUtil.isMotifLookAndFeel()) {
452 UIUtil.installPopupMenuBorder(myTextFieldPanel);
454 UIUtil.installPopupMenuColorAndFonts(myTextFieldPanel);
456 showTextFieldPanel();
458 if (modalityState != null) {
459 rebuildList(0, 0, null, modalityState);
463 private void hideHint() {
464 if (!myTextFieldPanel.focusRequested()) {
465 doClose(false);
466 myTextFieldPanel.hideHint();
471 * Default rebuild list. It uses {@link #REBUILD_DELAY} and current modality state.
473 public void rebuildList() {
474 // TODO this method is public, because the chooser does not listed for the model.
475 rebuildList(0, REBUILD_DELAY, null, ModalityState.current());
478 private void updateDocumentation() {
479 final JBPopup hint = myTextFieldPanel.getHint();
480 final Object element = getChosenElement();
481 if (hint != null) {
482 if (element instanceof PsiElement) {
483 myTextFieldPanel.updateHint((PsiElement)element);
484 } else if (element instanceof DataProvider) {
485 final Object o = ((DataProvider)element).getData(LangDataKeys.PSI_ELEMENT.getName());
486 if (o instanceof PsiElement) {
487 myTextFieldPanel.updateHint((PsiElement)o);
493 private void doClose(final boolean ok) {
494 if (myDisposedFlag) return;
496 if (posponeCloseWhenListReady(ok)) return;
498 cancelListUpdater();
499 close(ok);
501 clearPosponedOkAction(ok);
504 protected void cancelListUpdater() {
505 myListUpdater.cancelAll();
508 private boolean posponeCloseWhenListReady(boolean ok) {
509 if (!Registry.is("actionSystem.fixLostTyping")) return false;
511 final String text = myTextField.getText();
512 if (ok && !myListIsUpToDate && text != null && text.trim().length() > 0) {
513 myPosponedOkAction = new ActionCallback();
514 IdeFocusManager.getInstance(myProject).suspendKeyProcessingUntil(myPosponedOkAction);
515 return true;
518 return false;
521 private synchronized void ensureNamesLoaded(boolean checkboxState) {
522 int index = checkboxState ? 1 : 0;
523 if (myNames[index] != null) return;
525 Window window = (Window)SwingUtilities.getAncestorOfClass(Window.class, myTextField);
526 //LOG.assertTrue (myTextField != null);
527 //LOG.assertTrue (window != null);
528 Window ownerWindow = null;
529 if (window != null) {
530 window.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
531 ownerWindow = window.getOwner();
532 if (ownerWindow != null) {
533 ownerWindow.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
536 myNames[index] = myModel.getNames(checkboxState);
538 if (window != null) {
539 window.setCursor(Cursor.getDefaultCursor());
540 if (ownerWindow != null) {
541 ownerWindow.setCursor(Cursor.getDefaultCursor());
546 protected abstract boolean isCheckboxVisible();
548 protected abstract boolean isShowListForEmptyPattern();
550 protected abstract boolean isCloseByFocusLost();
552 protected void showTextFieldPanel() {
553 final JLayeredPane layeredPane = getLayeredPane();
554 final Dimension preferredTextFieldPanelSize = myTextFieldPanel.getPreferredSize();
555 final int x = (layeredPane.getWidth() - preferredTextFieldPanelSize.width) / 2;
556 final int paneHeight = layeredPane.getHeight();
557 final int y = paneHeight / 3 - preferredTextFieldPanelSize.height / 2;
560 myTextFieldPanel.setBounds(x, y, preferredTextFieldPanelSize.width, preferredTextFieldPanelSize.height);
561 layeredPane.add(myTextFieldPanel, Integer.valueOf(500));
562 layeredPane.moveToFront(myTextFieldPanel);
563 VISIBLE_LIST_SIZE_LIMIT = Math.max
564 (10, (paneHeight - (y + preferredTextFieldPanelSize.height)) / (preferredTextFieldPanelSize.height / 2) - 1);
566 // I'm registering KeyListener to close popup only by KeyTyped event.
567 // If react on KeyPressed then sometime KeyTyped goes into underlying editor.
568 // It causes typing of Enter into it.
569 myTextFieldPanel.registerKeyboardAction(new AbstractAction() {
570 public void actionPerformed(ActionEvent e) {
571 doClose(false);
573 }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
574 JComponent.WHEN_IN_FOCUSED_WINDOW
577 myList.registerKeyboardAction(new AbstractAction() {
578 public void actionPerformed(ActionEvent e) {
579 doClose(false);
581 }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
582 JComponent.WHEN_IN_FOCUSED_WINDOW
585 IdeFocusManager.getInstance(myProject).requestFocus(myTextField, true);
587 myTextFieldPanel.validate();
588 myTextFieldPanel.paintImmediately(0, 0, myTextFieldPanel.getWidth(), myTextFieldPanel.getHeight());
591 private JLayeredPane getLayeredPane() {
592 JLayeredPane layeredPane;
593 final Window window = WindowManager.getInstance().suggestParentWindow(myProject);
594 if (window instanceof JFrame) {
595 layeredPane = ((JFrame)window).getLayeredPane();
597 else if (window instanceof JDialog) {
598 layeredPane = ((JDialog)window).getLayeredPane();
600 else {
601 throw new IllegalStateException("cannot find parent window: project=" + myProject +
602 (myProject != null ? "; open=" + myProject.isOpen() : "") +
603 "; window=" + window);
605 return layeredPane;
608 private final Object myRebuildMutex = new Object ();
610 protected void rebuildList(final int pos, final int delay, final Runnable postRunnable, final ModalityState modalityState) {
611 ApplicationManager.getApplication().assertIsDispatchThread();
612 myListIsUpToDate = false;
613 myAlarm.cancelAllRequests();
614 myListUpdater.cancelAll();
616 cancelCalcElementsThread();
617 ApplicationManager.getApplication().invokeLater(new Runnable() {
618 public void run() {
619 final String text = myTextField.getText();
620 if (!isShowListForEmptyPattern() && (text == null || text.trim().length() == 0)) {
621 myListModel.clear();
622 hideList();
623 myCard.show(myCardContainer, CHECK_BOX_CARD);
624 return;
626 final Runnable request = new Runnable() {
627 public void run() {
628 final CalcElementsCallback callback = new CalcElementsCallback() {
629 public void run(final Set<?> elements) {
630 synchronized (myRebuildMutex) {
631 ApplicationManager.getApplication().assertIsDispatchThread();
632 if (myDisposedFlag) {
633 return;
636 setElementsToList(pos, elements);
638 myListIsUpToDate = true;
639 choosenElementMightChange();
641 if (postRunnable != null) {
642 postRunnable.run();
648 cancelCalcElementsThread();
650 myCalcElementsThread = new CalcElementsThread(text, myCheckBox.isSelected(), callback, modalityState, postRunnable == null);
651 ApplicationManager.getApplication().executeOnPooledThread(myCalcElementsThread);
655 if (delay > 0) {
656 myAlarm.addRequest(request, delay, ModalityState.stateForComponent(myTextField));
658 else {
659 request.run();
662 }, modalityState);
665 private void cancelCalcElementsThread() {
666 if (myCalcElementsThread != null) {
667 myCalcElementsThread.cancel();
668 myCalcElementsThread = null;
672 private void setElementsToList(int pos, Set<?> elements) {
673 myListUpdater.cancelAll();
674 if (myDisposedFlag) return;
675 if (elements.isEmpty()) {
676 myListModel.clear();
677 myTextField.setForeground(Color.red);
678 myListUpdater.cancelAll();
679 hideList();
680 clearPosponedOkAction(false);
681 return;
684 Object[] oldElements = myListModel.toArray();
685 Object[] newElements = elements.toArray();
686 Diff.Change change = Diff.buildChanges(oldElements, newElements);
688 if (change == null) return; // Nothing changed
690 List<Cmd> commands = new ArrayList<Cmd>();
691 int inserted = 0;
692 int deleted = 0;
693 while (change != null) {
694 if (change.deleted > 0) {
695 final int start = change.line0 + inserted - deleted;
696 commands.add(new RemoveCmd(start, start + change.deleted - 1));
699 if (change.inserted > 0) {
700 for (int i = 0; i < change.inserted; i++) {
701 commands.add(new InsertCmd(change.line0 + i + inserted - deleted, newElements[change.line1 + i]));
705 deleted += change.deleted;
706 inserted += change.inserted;
707 change = change.link;
710 myTextField.setForeground(UIUtil.getTextFieldForeground());
711 if (!commands.isEmpty()) {
712 showList();
713 myListUpdater.appendToModel(commands, pos);
715 else {
716 if (pos == 0) {
717 pos = detectBestStatisticalPosition();
720 ListScrollingUtil.selectItem(myList, Math.min(pos, myListModel.size() - 1));
721 myList.setVisibleRowCount(Math.min(VISIBLE_LIST_SIZE_LIMIT, myList.getModel().getSize()));
722 showList();
726 private int detectBestStatisticalPosition() {
727 int best = 0;
728 int bestPosition = 0;
729 final int count = myListModel.getSize();
731 final String statContext = statisticsContext();
732 for (int i = 0; i < count; i++) {
733 final Object modelElement = myListModel.getElementAt(i);
734 String text = EXTRA_ELEM.equals(modelElement) ? null : myModel.getFullName(modelElement);
735 if (text != null) {
736 int stats = StatisticsManager.getInstance().getUseCount(new StatisticsInfo(statContext, text));
737 if (stats > best) {
738 best = stats;
739 bestPosition = i;
744 return bestPosition;
747 @NonNls
748 protected String statisticsContext() {
749 return "choose_by_name#"+myModel.getPromptText()+"#"+ myCheckBox.isSelected() + "#" + myTextField.getText();
752 private String getQualifierPattern(String pattern) {
753 final String[] separators = myModel.getSeparators();
754 int lastSeparatorOccurence = 0;
755 for (String separator : separators) {
756 lastSeparatorOccurence = Math.max(lastSeparatorOccurence, pattern.lastIndexOf(separator));
758 return pattern.substring(0, lastSeparatorOccurence);
761 public String getNamePattern(String pattern) {
762 final String[] separators = myModel.getSeparators();
763 int lastSeparatorOccurence = 0;
764 for (String separator : separators) {
765 final int idx = pattern.lastIndexOf(separator);
766 lastSeparatorOccurence = Math.max(lastSeparatorOccurence, idx == -1 ? idx : idx + separator.length());
769 return pattern.substring(lastSeparatorOccurence);
772 private interface Cmd {
773 void apply();
776 private class RemoveCmd implements Cmd {
777 private final int start;
778 private final int end;
780 private RemoveCmd(final int start, final int end) {
781 this.start = start;
782 this.end = end;
785 public void apply() {
786 myListModel.removeRange(start, end);
790 private class InsertCmd implements Cmd {
791 private final int idx;
792 private final Object element;
794 private InsertCmd(final int idx, final Object element) {
795 this.idx = idx;
796 this.element = element;
799 public void apply() {
800 if (idx < myListModel.size()) {
801 myListModel.add(idx, element);
803 else {
804 myListModel.addElement(element);
809 private class ListUpdater {
810 private final Alarm myAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD);
811 private static final int DELAY = 10;
812 private static final int MAX_BLOCKING_TIME = 30;
813 private final List<Cmd> myCommands = Collections.synchronizedList(new ArrayList<Cmd>());
815 public void cancelAll() {
816 myCommands.clear();
817 myAlarm.cancelAllRequests();
820 public void appendToModel(final List<Cmd> commands, final int selectionPos) {
821 myAlarm.cancelAllRequests();
822 myCommands.addAll(commands);
824 if (myCommands.isEmpty() || myDisposedFlag) return;
825 myAlarm.addRequest(new Runnable() {
826 public void run() {
827 if (myDisposedFlag) return;
828 final long startTime = System.currentTimeMillis();
829 while (!myCommands.isEmpty() && System.currentTimeMillis() - startTime < MAX_BLOCKING_TIME) {
830 final Cmd cmd = myCommands.remove(0);
831 cmd.apply();
834 myList.setVisibleRowCount(Math.min(VISIBLE_LIST_SIZE_LIMIT, myList.getModel().getSize()));
835 if (!myListModel.isEmpty()) {
836 int pos = selectionPos == 0 ? detectBestStatisticalPosition() : selectionPos;
837 ListScrollingUtil.selectItem(myList, Math.min(pos, myListModel.size() - 1));
840 if (!myCommands.isEmpty()) {
841 myAlarm.addRequest(this, DELAY);
842 } else {
843 doPostponedOkIfNeeded();
845 if (!myDisposedFlag) {
846 showList();
849 }, DELAY);
852 private void doPostponedOkIfNeeded() {
853 if (myPosponedOkAction != null) {
854 if (getChosenElement() != null) {
855 doClose(true);
856 clearPosponedOkAction(myDisposedFlag);
862 private void clearPosponedOkAction(boolean success) {
863 if (myPosponedOkAction != null) {
864 if (success) {
865 myPosponedOkAction.setDone();
866 } else {
867 myPosponedOkAction.setRejected();
871 myPosponedOkAction = null;
874 protected abstract void showList();
876 protected abstract void hideList();
878 protected abstract void close(boolean isOk);
880 @Nullable
881 public Object getChosenElement() {
882 final List<Object> elements = getChosenElements();
883 return elements != null && elements.size() == 1 ? elements.get(0) : null;
886 protected List<Object> getChosenElements() {
887 if (myListIsUpToDate) {
888 List<Object> values = new ArrayList<Object>(Arrays.asList(myList.getSelectedValues()));
889 values.remove(EXTRA_ELEM);
890 return values;
893 final String text = myTextField.getText();
894 final boolean checkBoxState = myCheckBox.isSelected();
895 //ensureNamesLoaded(checkBoxState);
896 final String[] names = checkBoxState ? myNames[1] : myNames[0];
897 if (names == null) return Collections.emptyList();
899 Object uniqueElement = null;
901 for (final String name : names) {
902 if (text.equalsIgnoreCase(name)) {
903 final Object[] elements = myModel.getElementsByName(name, checkBoxState, text);
904 if (elements.length > 1) return Collections.emptyList();
905 if (elements.length == 0) continue;
906 if (uniqueElement != null) return Collections.emptyList();
907 uniqueElement = elements[0];
910 return uniqueElement == null ? Collections.emptyList() : Collections.singletonList(uniqueElement);
913 protected void choosenElementMightChange() {
916 private final class MyTextField extends JTextField implements PopupOwner {
917 private final KeyStroke myCompletionKeyStroke;
918 private final KeyStroke forwardStroke;
919 private final KeyStroke backStroke;
921 private MyTextField() {
922 super(40);
923 enableEvents(AWTEvent.KEY_EVENT_MASK);
924 myCompletionKeyStroke = getShortcut(IdeActions.ACTION_CODE_COMPLETION);
925 forwardStroke = getShortcut(IdeActions.ACTION_GOTO_FORWARD);
926 backStroke = getShortcut(IdeActions.ACTION_GOTO_BACK);
930 private KeyStroke getShortcut(String actionCodeCompletion) {
931 final Shortcut[] shortcuts = KeymapManager.getInstance().getActiveKeymap().getShortcuts(actionCodeCompletion);
932 for (final Shortcut shortcut : shortcuts) {
933 if (shortcut instanceof KeyboardShortcut) {
934 return ((KeyboardShortcut)shortcut).getFirstKeyStroke();
937 return null;
940 protected void processKeyEvent(KeyEvent e) {
941 final KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(e);
942 if (myCompletionKeyStroke != null && keyStroke.equals(myCompletionKeyStroke)) {
943 e.consume();
944 final String pattern = myTextField.getText();
945 final String oldText = myTextField.getText();
946 final int oldPos = myList.getSelectedIndex();
947 myHistory.add(Pair.create(oldText, oldPos));
948 final Runnable postRunnable = new Runnable() {
949 public void run() {
950 fillInCommonPrefix(pattern);
953 rebuildList(0, 0, postRunnable, ModalityState.current());
954 return;
956 if (backStroke != null && keyStroke.equals(backStroke)) {
957 e.consume();
958 if (!myHistory.isEmpty()) {
959 final String oldText = myTextField.getText();
960 final int oldPos = myList.getSelectedIndex();
961 final Pair<String, Integer> last = myHistory.remove(myHistory.size() - 1);
962 myTextField.setText(last.first);
963 myFuture.add(Pair.create(oldText, oldPos));
964 rebuildList(0, 0, null, ModalityState.current());
966 return;
968 if (forwardStroke != null && keyStroke.equals(forwardStroke)) {
969 e.consume();
970 if (!myFuture.isEmpty()) {
971 final String oldText = myTextField.getText();
972 final int oldPos = myList.getSelectedIndex();
973 final Pair<String, Integer> next = myFuture.remove(myFuture.size() - 1);
974 myTextField.setText(next.first);
975 myHistory.add(Pair.create(oldText, oldPos));
976 rebuildList(0, 0, null, ModalityState.current());
978 return;
980 try {
981 super.processKeyEvent(e);
983 catch (NullPointerException e1) {
984 if (!Patches.SUN_BUG_6322854) {
985 throw e1;
990 private void fillInCommonPrefix(final String pattern) {
991 final ArrayList<String> list = new ArrayList<String>();
992 getNamesByPattern(myCheckBox.isSelected(), null, list, pattern);
994 if (isComplexPattern(pattern)) return; //TODO: support '*'
995 final String oldText = myTextField.getText();
996 final int oldPos = myList.getSelectedIndex();
998 String commonPrefix = null;
999 if (!list.isEmpty()) {
1000 for (String name : list) {
1001 final String string = name.toLowerCase();
1002 if (commonPrefix == null) {
1003 commonPrefix = string;
1005 else {
1006 while (commonPrefix.length() > 0) {
1007 if (string.startsWith(commonPrefix)) {
1008 break;
1010 commonPrefix = commonPrefix.substring(0, commonPrefix.length() - 1);
1012 if (commonPrefix.length() == 0) break;
1015 commonPrefix = list.get(0).substring(0, commonPrefix.length());
1016 for (int i = 1; i < list.size(); i++) {
1017 final String string = list.get(i).substring(0, commonPrefix.length());
1018 if (!string.equals(commonPrefix)) {
1019 commonPrefix = commonPrefix.toLowerCase();
1020 break;
1024 if (commonPrefix == null) commonPrefix = "";
1025 final String newPattern = commonPrefix;
1027 myHistory.add(Pair.create(oldText, oldPos));
1028 myTextField.setText(newPattern);
1029 myTextField.setCaretPosition(newPattern.length());
1031 rebuildList();
1034 private boolean isComplexPattern(final String pattern) {
1035 if (pattern.indexOf('*') >= 0) return true;
1036 for (String s : myModel.getSeparators()) {
1037 if (pattern.contains(s)) return true;
1040 return false;
1043 @Nullable
1044 public Point getBestPopupPosition() {
1045 return new Point(myTextFieldPanel.getWidth(), getHeight());
1048 protected void paintComponent(final Graphics g) {
1049 UISettings.setupAntialiasing(g);
1050 super.paintComponent(g);
1054 private static final String EXTRA_ELEM = "...";
1056 private class CalcElementsThread implements Runnable {
1057 private final String myPattern;
1058 private boolean myCheckboxState;
1059 private final CalcElementsCallback myCallback;
1060 private final ModalityState myModalityState;
1062 private Set<Object> myElements = null;
1064 private volatile boolean myCancelled = false;
1065 private final boolean myCanCancel;
1067 private CalcElementsThread(String pattern, boolean checkboxState, CalcElementsCallback callback, ModalityState modalityState, boolean canCancel) {
1068 myPattern = pattern;
1069 myCheckboxState = checkboxState;
1070 myCallback = callback;
1071 myModalityState = modalityState;
1072 myCanCancel = canCancel;
1075 private final Alarm myShowCardAlarm = new Alarm();
1076 public void run() {
1077 showCard(SEARCHING_CARD, 200);
1079 final Set<Object> elements = new LinkedHashSet<Object>();
1080 Runnable action = new Runnable() {
1081 public void run() {
1082 try {
1083 ensureNamesLoaded(myCheckboxState);
1084 addElementsByPattern(elements, myPattern);
1085 for (Object elem : elements) {
1086 if (myCancelled) {
1087 break;
1089 if (elem instanceof PsiElement) {
1090 final PsiElement psiElement = (PsiElement)elem;
1091 psiElement.isWritable(); // That will cache writable flag in VirtualFile. Taking the action here makes it canceleable.
1095 catch (ProcessCanceledException e) {
1096 //OK
1100 ApplicationManager.getApplication().runReadAction(action);
1102 if (myCancelled) {
1103 myShowCardAlarm.cancelAllRequests();
1104 return;
1107 final String cardToShow;
1108 if (elements.isEmpty() && !myCheckboxState) {
1109 myCheckboxState = true;
1110 ApplicationManager.getApplication().runReadAction(action);
1111 cardToShow = elements.isEmpty() ? NOT_FOUND_CARD : NOT_FOUND_IN_PROJECT_CARD;
1113 else {
1114 cardToShow = elements.isEmpty() ? NOT_FOUND_CARD : CHECK_BOX_CARD;
1116 showCard(cardToShow, 0);
1118 myElements = elements;
1120 ApplicationManager.getApplication().invokeLater(new Runnable() {
1121 public void run() {
1122 myCallback.run(myElements);
1124 }, myModalityState);
1127 private void showCard(final String card, final int delay) {
1128 myShowCardAlarm.cancelAllRequests();
1129 myShowCardAlarm.addRequest(new Runnable() {
1130 public void run() {
1131 myCard.show(myCardContainer, card);
1133 }, delay, myModalityState);
1136 private void addElementsByPattern(Set<Object> elementsArray, String pattern) {
1137 String namePattern = getNamePattern(pattern);
1138 String qualifierPattern = getQualifierPattern(pattern);
1140 boolean empty = namePattern.length() == 0 || namePattern.equals("@"); // TODO[yole]: remove implicit dependency
1141 if (empty && !isShowListForEmptyPattern()) return;
1143 List<String> namesList = new ArrayList<String>();
1144 getNamesByPattern(myCheckboxState, this, namesList, namePattern);
1145 if (myCancelled) {
1146 throw new ProcessCanceledException();
1148 Collections.sort(namesList, new MatchesComparator(pattern));
1150 boolean overflow = false;
1151 List<Object> sameNameElements = new SmartList<Object>();
1152 All:
1153 for (String name : namesList) {
1154 if (myCancelled) {
1155 throw new ProcessCanceledException();
1157 final Object[] elements = myModel.getElementsByName(name, myCheckboxState, namePattern);
1158 if (elements.length > 1) {
1159 sameNameElements.clear();
1160 for (final Object element : elements) {
1161 if (matchesQualifier(element, qualifierPattern)) {
1162 sameNameElements.add(element);
1165 sortByProximity(sameNameElements);
1166 for (Object element : sameNameElements) {
1167 elementsArray.add(element);
1168 if (elementsArray.size() >= myMaximumListSizeLimit) {
1169 overflow = true;
1170 break All;
1174 else if (elements.length == 1 && matchesQualifier(elements[0], qualifierPattern)) {
1175 elementsArray.add(elements[0]);
1176 if (elementsArray.size() >= myMaximumListSizeLimit) {
1177 overflow = true;
1178 break;
1183 if (overflow) {
1184 elementsArray.add(EXTRA_ELEM);
1188 private void cancel() {
1189 if (myCanCancel) {
1190 myCancelled = true;
1195 private void sortByProximity(final List<Object> sameNameElements) {
1196 Collections.sort(sameNameElements, new PathProximityComparator(myModel, myContext.get()));
1199 private List<String> split(String s) {
1200 List<String> answer = new ArrayList<String>();
1201 for (String token : StringUtil.tokenize(s, StringUtil.join(myModel.getSeparators(), ""))) {
1202 if (token.length() > 0) {
1203 answer.add(token);
1207 return answer.isEmpty() ? Collections.singletonList(s) : answer;
1210 private boolean matchesQualifier(final Object element, final String qualifierPattern) {
1211 final String name = myModel.getFullName(element);
1212 if (name == null) return false;
1214 final List<String> suspects = split(name);
1215 final List<Pair<String, NameUtil.Matcher>> patternsAndMatchers = ContainerUtil.map2List(split(qualifierPattern), new Function<String, Pair<String, NameUtil.Matcher>>() {
1216 public Pair<String, NameUtil.Matcher> fun(String s) {
1217 final String pattern = getNamePattern(s);
1218 final NameUtil.Matcher matcher = buildPatternMatcher(pattern);
1220 return new Pair<String, NameUtil.Matcher>(pattern, matcher);
1224 int matchPosition = 0;
1226 try {
1227 patterns:
1228 for (Pair<String, NameUtil.Matcher> patternAndMatcher : patternsAndMatchers) {
1229 final String pattern = patternAndMatcher.first;
1230 final NameUtil.Matcher matcher = patternAndMatcher.second;
1231 if (pattern.length() > 0) {
1232 for (int j = matchPosition; j < suspects.size() - 1; j++) {
1233 String suspect = suspects.get(j);
1234 if (matches(pattern, matcher, suspect)) {
1235 matchPosition = j + 1;
1236 continue patterns;
1240 return false;
1243 } catch (Exception e) {
1244 // Do nothing. No matches appears valid result for "bad" pattern
1245 return false;
1248 return true;
1251 private void getNamesByPattern(final boolean checkboxState,
1252 CalcElementsThread calcElementsThread,
1253 final List<String> list,
1254 String pattern) throws ProcessCanceledException {
1255 if (!isShowListForEmptyPattern()) {
1256 LOG.assertTrue(pattern.length() > 0);
1259 if (pattern.startsWith("@")) {
1260 pattern = pattern.substring(1);
1263 final String[] names = checkboxState ? myNames[1] : myNames[0];
1264 final NameUtil.Matcher matcher = buildPatternMatcher(pattern);
1266 try {
1267 for (String name : names) {
1268 if (calcElementsThread != null && calcElementsThread.myCancelled) {
1269 break;
1271 if (matches(pattern, matcher, name)) {
1272 list.add(name);
1276 catch (Exception e) {
1277 // Do nothing. No matches appears valid result for "bad" pattern
1281 private boolean matches(String pattern, NameUtil.Matcher matcher, String name) {
1282 boolean matches = false;
1283 if (name != null) {
1284 if (myModel instanceof CustomMatcherModel) {
1285 if (((CustomMatcherModel)myModel).matches(name, pattern)) {
1286 matches = true;
1289 else if (pattern.length() == 0 || matcher.matches(name)) {
1290 matches = true;
1293 return matches;
1296 private NameUtil.Matcher buildPatternMatcher(String pattern) {
1297 return NameUtil.buildMatcher(pattern, 0, true, true, pattern.toLowerCase().equals(pattern));
1300 private interface CalcElementsCallback {
1301 void run(Set<?> elements);
1304 private static class PathProximityComparator implements Comparator<Object> {
1305 private final ChooseByNameModel myModel;
1306 private final PsiProximityComparator myProximityComparator;
1308 private PathProximityComparator(final ChooseByNameModel model, final PsiElement context) {
1309 myModel = model;
1310 myProximityComparator = new PsiProximityComparator(context);
1313 public int compare(final Object o1, final Object o2) {
1314 int rc = myProximityComparator.compare(o1, o2);
1315 if (rc != 0) return rc;
1317 return Comparing.compare(myModel.getFullName(o1), myModel.getFullName(o2));