RUBY-5758
[fedora-idea.git] / platform / lang-impl / src / com / intellij / ide / util / gotoByName / ChooseByNameBase.java
blob6ec50fe404c2741675f4df1b3bf2eb900c5e96b2
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 if (myList.getSelectedValue() == EXTRA_ELEM) {
392 myMaximumListSizeLimit += MAXIMUM_LIST_SIZE_LIMIT;
393 rebuildList(myList.getSelectedIndex(), REBUILD_DELAY, null, ModalityState.current());
394 e.consume();
396 break;
401 myTextField.addActionListener(new ActionListener() {
402 public void actionPerformed(ActionEvent actionEvent) {
403 doClose(true);
407 myListModel = new DefaultListModel();
408 myList = new JList(myListModel);
409 myList.setFocusable(false);
410 myList.setSelectionMode(allowMultipleSelection ? ListSelectionModel.MULTIPLE_INTERVAL_SELECTION :
411 ListSelectionModel.SINGLE_SELECTION);
412 myList.addMouseListener(new MouseAdapter() {
413 public void mouseClicked(MouseEvent e) {
414 if (!myTextField.hasFocus()) {
415 myTextField.requestFocus();
418 if (e.getClickCount() == 2) {
419 if (myList.getSelectedValue() == EXTRA_ELEM) {
420 myMaximumListSizeLimit += MAXIMUM_LIST_SIZE_LIMIT;
421 rebuildList(myList.getSelectedIndex(), REBUILD_DELAY, null, ModalityState.current());
422 e.consume();
424 else {
425 doClose(true);
430 myList.setCellRenderer(myModel.getListCellRenderer());
431 myList.setFont(editorFont);
433 myList.addListSelectionListener(new ListSelectionListener() {
434 public void valueChanged(ListSelectionEvent e) {
435 choosenElementMightChange();
436 updateDocumentation();
440 myListScrollPane = new JScrollPane(myList);
442 if (!UIUtil.isMotifLookAndFeel()) {
443 UIUtil.installPopupMenuBorder(myTextFieldPanel);
445 UIUtil.installPopupMenuColorAndFonts(myTextFieldPanel);
447 showTextFieldPanel();
449 if (modalityState != null) {
450 rebuildList(0, 0, null, modalityState);
454 private void hideHint() {
455 if (!myTextFieldPanel.focusRequested()) {
456 doClose(false);
457 myTextFieldPanel.hideHint();
462 * Default rebuild list. It uses {@link #REBUILD_DELAY} and current modality state.
464 public void rebuildList() {
465 // TODO this method is public, because the chooser does not listed for the model.
466 rebuildList(0, REBUILD_DELAY, null, ModalityState.current());
469 private void updateDocumentation() {
470 final JBPopup hint = myTextFieldPanel.getHint();
471 final Object element = getChosenElement();
472 if (hint != null) {
473 if (element instanceof PsiElement) {
474 myTextFieldPanel.updateHint((PsiElement)element);
475 } else if (element instanceof DataProvider) {
476 final Object o = ((DataProvider)element).getData(LangDataKeys.PSI_ELEMENT.getName());
477 if (o instanceof PsiElement) {
478 myTextFieldPanel.updateHint((PsiElement)o);
484 private void doClose(final boolean ok) {
485 if (myDisposedFlag) return;
487 if (posponeCloseWhenListReady(ok)) return;
489 cancelListUpdater();
490 close(ok);
492 clearPosponedOkAction(ok);
495 protected void cancelListUpdater() {
496 myListUpdater.cancelAll();
499 private boolean posponeCloseWhenListReady(boolean ok) {
500 if (!Registry.is("actionSystem.fixLostTyping")) return false;
502 final String text = myTextField.getText();
503 if (ok && !myListIsUpToDate && text != null && text.trim().length() > 0) {
504 myPosponedOkAction = new ActionCallback();
505 IdeFocusManager.getInstance(myProject).suspendKeyProcessingUntil(myPosponedOkAction);
506 return true;
509 return false;
512 private synchronized void ensureNamesLoaded(boolean checkboxState) {
513 int index = checkboxState ? 1 : 0;
514 if (myNames[index] != null) return;
516 Window window = (Window)SwingUtilities.getAncestorOfClass(Window.class, myTextField);
517 //LOG.assertTrue (myTextField != null);
518 //LOG.assertTrue (window != null);
519 Window ownerWindow = null;
520 if (window != null) {
521 window.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
522 ownerWindow = window.getOwner();
523 if (ownerWindow != null) {
524 ownerWindow.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
527 myNames[index] = myModel.getNames(checkboxState);
529 if (window != null) {
530 window.setCursor(Cursor.getDefaultCursor());
531 if (ownerWindow != null) {
532 ownerWindow.setCursor(Cursor.getDefaultCursor());
537 protected abstract boolean isCheckboxVisible();
539 protected abstract boolean isShowListForEmptyPattern();
541 protected abstract boolean isCloseByFocusLost();
543 protected void showTextFieldPanel() {
544 final JLayeredPane layeredPane = getLayeredPane();
545 final Dimension preferredTextFieldPanelSize = myTextFieldPanel.getPreferredSize();
546 final int x = (layeredPane.getWidth() - preferredTextFieldPanelSize.width) / 2;
547 final int paneHeight = layeredPane.getHeight();
548 final int y = paneHeight / 3 - preferredTextFieldPanelSize.height / 2;
551 myTextFieldPanel.setBounds(x, y, preferredTextFieldPanelSize.width, preferredTextFieldPanelSize.height);
552 layeredPane.add(myTextFieldPanel, Integer.valueOf(500));
553 layeredPane.moveToFront(myTextFieldPanel);
554 VISIBLE_LIST_SIZE_LIMIT = Math.max
555 (10, (paneHeight - (y + preferredTextFieldPanelSize.height)) / (preferredTextFieldPanelSize.height / 2) - 1);
557 // I'm registering KeyListener to close popup only by KeyTyped event.
558 // If react on KeyPressed then sometime KeyTyped goes into underlying editor.
559 // It causes typing of Enter into it.
560 myTextFieldPanel.registerKeyboardAction(new AbstractAction() {
561 public void actionPerformed(ActionEvent e) {
562 doClose(false);
564 }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
565 JComponent.WHEN_IN_FOCUSED_WINDOW
568 myList.registerKeyboardAction(new AbstractAction() {
569 public void actionPerformed(ActionEvent e) {
570 doClose(false);
572 }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
573 JComponent.WHEN_IN_FOCUSED_WINDOW
576 IdeFocusManager.getInstance(myProject).requestFocus(myTextField, true);
578 myTextFieldPanel.validate();
579 myTextFieldPanel.paintImmediately(0, 0, myTextFieldPanel.getWidth(), myTextFieldPanel.getHeight());
582 private JLayeredPane getLayeredPane() {
583 JLayeredPane layeredPane;
584 final Window window = WindowManager.getInstance().suggestParentWindow(myProject);
585 if (window instanceof JFrame) {
586 layeredPane = ((JFrame)window).getLayeredPane();
588 else if (window instanceof JDialog) {
589 layeredPane = ((JDialog)window).getLayeredPane();
591 else {
592 throw new IllegalStateException("cannot find parent window: project=" + myProject +
593 (myProject != null ? "; open=" + myProject.isOpen() : "") +
594 "; window=" + window);
596 return layeredPane;
599 private final Object myRebuildMutex = new Object ();
601 protected void rebuildList(final int pos, final int delay, final Runnable postRunnable, final ModalityState modalityState) {
602 ApplicationManager.getApplication().assertIsDispatchThread();
603 myListIsUpToDate = false;
604 myAlarm.cancelAllRequests();
605 myListUpdater.cancelAll();
607 cancelCalcElementsThread();
608 ApplicationManager.getApplication().invokeLater(new Runnable() {
609 public void run() {
610 final String text = myTextField.getText();
611 if (!isShowListForEmptyPattern() && (text == null || text.trim().length() == 0)) {
612 myListModel.clear();
613 hideList();
614 myCard.show(myCardContainer, CHECK_BOX_CARD);
615 return;
617 final Runnable request = new Runnable() {
618 public void run() {
619 final CalcElementsCallback callback = new CalcElementsCallback() {
620 public void run(final Set<?> elements) {
621 synchronized (myRebuildMutex) {
622 ApplicationManager.getApplication().assertIsDispatchThread();
623 if (myDisposedFlag) {
624 return;
627 setElementsToList(pos, elements);
629 myListIsUpToDate = true;
630 choosenElementMightChange();
632 if (postRunnable != null) {
633 postRunnable.run();
639 cancelCalcElementsThread();
641 myCalcElementsThread = new CalcElementsThread(text, myCheckBox.isSelected(), callback, modalityState, postRunnable == null);
642 ApplicationManager.getApplication().executeOnPooledThread(myCalcElementsThread);
646 if (delay > 0) {
647 myAlarm.addRequest(request, delay, ModalityState.stateForComponent(myTextField));
649 else {
650 request.run();
653 }, modalityState);
656 private void cancelCalcElementsThread() {
657 if (myCalcElementsThread != null) {
658 myCalcElementsThread.cancel();
659 myCalcElementsThread = null;
663 private void setElementsToList(int pos, Set<?> elements) {
664 myListUpdater.cancelAll();
665 if (myDisposedFlag) return;
666 if (elements.isEmpty()) {
667 myListModel.clear();
668 myTextField.setForeground(Color.red);
669 myListUpdater.cancelAll();
670 hideList();
671 clearPosponedOkAction(false);
672 return;
675 Object[] oldElements = myListModel.toArray();
676 Object[] newElements = elements.toArray();
677 Diff.Change change = Diff.buildChanges(oldElements, newElements);
679 if (change == null) return; // Nothing changed
681 List<Cmd> commands = new ArrayList<Cmd>();
682 int inserted = 0;
683 int deleted = 0;
684 while (change != null) {
685 if (change.deleted > 0) {
686 final int start = change.line0 + inserted - deleted;
687 commands.add(new RemoveCmd(start, start + change.deleted - 1));
690 if (change.inserted > 0) {
691 for (int i = 0; i < change.inserted; i++) {
692 commands.add(new InsertCmd(change.line0 + i + inserted - deleted, newElements[change.line1 + i]));
696 deleted += change.deleted;
697 inserted += change.inserted;
698 change = change.link;
701 myTextField.setForeground(UIUtil.getTextFieldForeground());
702 if (!commands.isEmpty()) {
703 showList();
704 myListUpdater.appendToModel(commands, pos);
706 else {
707 if (pos == 0) {
708 pos = detectBestStatisticalPosition();
711 ListScrollingUtil.selectItem(myList, Math.min(pos, myListModel.size() - 1));
712 myList.setVisibleRowCount(Math.min(VISIBLE_LIST_SIZE_LIMIT, myList.getModel().getSize()));
713 showList();
717 private int detectBestStatisticalPosition() {
718 int best = 0;
719 int bestPosition = 0;
720 final int count = myListModel.getSize();
722 final String statContext = statisticsContext();
723 for (int i = 0; i < count; i++) {
724 final Object modelElement = myListModel.getElementAt(i);
725 String text = EXTRA_ELEM.equals(modelElement) ? null : myModel.getFullName(modelElement);
726 if (text != null) {
727 int stats = StatisticsManager.getInstance().getUseCount(new StatisticsInfo(statContext, text));
728 if (stats > best) {
729 best = stats;
730 bestPosition = i;
735 return bestPosition;
738 @NonNls
739 protected String statisticsContext() {
740 return "choose_by_name#"+myModel.getPromptText()+"#"+ myCheckBox.isSelected() + "#" + myTextField.getText();
743 private String getQualifierPattern(String pattern) {
744 final String[] separators = myModel.getSeparators();
745 int lastSeparatorOccurence = 0;
746 for (String separator : separators) {
747 lastSeparatorOccurence = Math.max(lastSeparatorOccurence, pattern.lastIndexOf(separator));
749 return pattern.substring(0, lastSeparatorOccurence);
752 public String getNamePattern(String pattern) {
753 final String[] separators = myModel.getSeparators();
754 int lastSeparatorOccurence = 0;
755 for (String separator : separators) {
756 final int idx = pattern.lastIndexOf(separator);
757 lastSeparatorOccurence = Math.max(lastSeparatorOccurence, idx == -1 ? idx : idx + separator.length());
760 return pattern.substring(lastSeparatorOccurence);
763 private interface Cmd {
764 void apply();
767 private class RemoveCmd implements Cmd {
768 private final int start;
769 private final int end;
771 private RemoveCmd(final int start, final int end) {
772 this.start = start;
773 this.end = end;
776 public void apply() {
777 myListModel.removeRange(start, end);
781 private class InsertCmd implements Cmd {
782 private final int idx;
783 private final Object element;
785 private InsertCmd(final int idx, final Object element) {
786 this.idx = idx;
787 this.element = element;
790 public void apply() {
791 if (idx < myListModel.size()) {
792 myListModel.add(idx, element);
794 else {
795 myListModel.addElement(element);
800 private class ListUpdater {
801 private final Alarm myAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD);
802 private static final int DELAY = 10;
803 private static final int MAX_BLOCKING_TIME = 30;
804 private final List<Cmd> myCommands = Collections.synchronizedList(new ArrayList<Cmd>());
806 public void cancelAll() {
807 myCommands.clear();
808 myAlarm.cancelAllRequests();
811 public void appendToModel(final List<Cmd> commands, final int selectionPos) {
812 myAlarm.cancelAllRequests();
813 myCommands.addAll(commands);
815 if (myCommands.isEmpty() || myDisposedFlag) return;
816 myAlarm.addRequest(new Runnable() {
817 public void run() {
818 if (myDisposedFlag) return;
819 final long startTime = System.currentTimeMillis();
820 while (!myCommands.isEmpty() && System.currentTimeMillis() - startTime < MAX_BLOCKING_TIME) {
821 final Cmd cmd = myCommands.remove(0);
822 cmd.apply();
825 myList.setVisibleRowCount(Math.min(VISIBLE_LIST_SIZE_LIMIT, myList.getModel().getSize()));
826 if (!myListModel.isEmpty()) {
827 int pos = selectionPos == 0 ? detectBestStatisticalPosition() : selectionPos;
828 ListScrollingUtil.selectItem(myList, Math.min(pos, myListModel.size() - 1));
831 if (!myCommands.isEmpty()) {
832 myAlarm.addRequest(this, DELAY);
833 } else {
834 doPostponedOkIfNeeded();
836 if (!myDisposedFlag) {
837 showList();
840 }, DELAY);
843 private void doPostponedOkIfNeeded() {
844 if (myPosponedOkAction != null) {
845 if (getChosenElement() != null) {
846 doClose(true);
847 clearPosponedOkAction(myDisposedFlag);
853 private void clearPosponedOkAction(boolean success) {
854 if (myPosponedOkAction != null) {
855 if (success) {
856 myPosponedOkAction.setDone();
857 } else {
858 myPosponedOkAction.setRejected();
862 myPosponedOkAction = null;
865 protected abstract void showList();
867 protected abstract void hideList();
869 protected abstract void close(boolean isOk);
871 @Nullable
872 public Object getChosenElement() {
873 final List<Object> elements = getChosenElements();
874 return elements != null && elements.size() == 1 ? elements.get(0) : null;
877 protected List<Object> getChosenElements() {
878 if (myListIsUpToDate) {
879 List<Object> values = new ArrayList<Object>(Arrays.asList(myList.getSelectedValues()));
880 values.remove(EXTRA_ELEM);
881 return values;
884 final String text = myTextField.getText();
885 final boolean checkBoxState = myCheckBox.isSelected();
886 //ensureNamesLoaded(checkBoxState);
887 final String[] names = checkBoxState ? myNames[1] : myNames[0];
888 if (names == null) return Collections.emptyList();
890 Object uniqueElement = null;
892 for (final String name : names) {
893 if (text.equalsIgnoreCase(name)) {
894 final Object[] elements = myModel.getElementsByName(name, checkBoxState, text);
895 if (elements.length > 1) return Collections.emptyList();
896 if (elements.length == 0) continue;
897 if (uniqueElement != null) return Collections.emptyList();
898 uniqueElement = elements[0];
901 return uniqueElement == null ? Collections.emptyList() : Collections.singletonList(uniqueElement);
904 protected void choosenElementMightChange() {
907 private final class MyTextField extends JTextField implements PopupOwner {
908 private final KeyStroke myCompletionKeyStroke;
909 private final KeyStroke forwardStroke;
910 private final KeyStroke backStroke;
912 private MyTextField() {
913 super(40);
914 enableEvents(AWTEvent.KEY_EVENT_MASK);
915 myCompletionKeyStroke = getShortcut(IdeActions.ACTION_CODE_COMPLETION);
916 forwardStroke = getShortcut(IdeActions.ACTION_GOTO_FORWARD);
917 backStroke = getShortcut(IdeActions.ACTION_GOTO_BACK);
921 private KeyStroke getShortcut(String actionCodeCompletion) {
922 final Shortcut[] shortcuts = KeymapManager.getInstance().getActiveKeymap().getShortcuts(actionCodeCompletion);
923 for (final Shortcut shortcut : shortcuts) {
924 if (shortcut instanceof KeyboardShortcut) {
925 return ((KeyboardShortcut)shortcut).getFirstKeyStroke();
928 return null;
931 protected void processKeyEvent(KeyEvent e) {
932 final KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(e);
933 if (myCompletionKeyStroke != null && keyStroke.equals(myCompletionKeyStroke)) {
934 e.consume();
935 final String pattern = myTextField.getText();
936 final String oldText = myTextField.getText();
937 final int oldPos = myList.getSelectedIndex();
938 myHistory.add(Pair.create(oldText, oldPos));
939 final Runnable postRunnable = new Runnable() {
940 public void run() {
941 fillInCommonPrefix(pattern);
944 rebuildList(0, 0, postRunnable, ModalityState.current());
945 return;
947 if (backStroke != null && keyStroke.equals(backStroke)) {
948 e.consume();
949 if (!myHistory.isEmpty()) {
950 final String oldText = myTextField.getText();
951 final int oldPos = myList.getSelectedIndex();
952 final Pair<String, Integer> last = myHistory.remove(myHistory.size() - 1);
953 myTextField.setText(last.first);
954 myFuture.add(Pair.create(oldText, oldPos));
955 rebuildList(0, 0, null, ModalityState.current());
957 return;
959 if (forwardStroke != null && keyStroke.equals(forwardStroke)) {
960 e.consume();
961 if (!myFuture.isEmpty()) {
962 final String oldText = myTextField.getText();
963 final int oldPos = myList.getSelectedIndex();
964 final Pair<String, Integer> next = myFuture.remove(myFuture.size() - 1);
965 myTextField.setText(next.first);
966 myHistory.add(Pair.create(oldText, oldPos));
967 rebuildList(0, 0, null, ModalityState.current());
969 return;
971 try {
972 super.processKeyEvent(e);
974 catch (NullPointerException e1) {
975 if (!Patches.SUN_BUG_6322854) {
976 throw e1;
981 private void fillInCommonPrefix(final String pattern) {
982 final ArrayList<String> list = new ArrayList<String>();
983 getNamesByPattern(myCheckBox.isSelected(), null, list, pattern);
985 if (isComplexPattern(pattern)) return; //TODO: support '*'
986 final String oldText = myTextField.getText();
987 final int oldPos = myList.getSelectedIndex();
989 String commonPrefix = null;
990 if (!list.isEmpty()) {
991 for (String name : list) {
992 final String string = name.toLowerCase();
993 if (commonPrefix == null) {
994 commonPrefix = string;
996 else {
997 while (commonPrefix.length() > 0) {
998 if (string.startsWith(commonPrefix)) {
999 break;
1001 commonPrefix = commonPrefix.substring(0, commonPrefix.length() - 1);
1003 if (commonPrefix.length() == 0) break;
1006 commonPrefix = list.get(0).substring(0, commonPrefix.length());
1007 for (int i = 1; i < list.size(); i++) {
1008 final String string = list.get(i).substring(0, commonPrefix.length());
1009 if (!string.equals(commonPrefix)) {
1010 commonPrefix = commonPrefix.toLowerCase();
1011 break;
1015 if (commonPrefix == null) commonPrefix = "";
1016 final String newPattern = commonPrefix;
1018 myHistory.add(Pair.create(oldText, oldPos));
1019 myTextField.setText(newPattern);
1020 myTextField.setCaretPosition(newPattern.length());
1022 rebuildList();
1025 private boolean isComplexPattern(final String pattern) {
1026 if (pattern.indexOf('*') >= 0) return true;
1027 for (String s : myModel.getSeparators()) {
1028 if (pattern.contains(s)) return true;
1031 return false;
1034 @Nullable
1035 public Point getBestPopupPosition() {
1036 return new Point(myTextFieldPanel.getWidth(), getHeight());
1039 protected void paintComponent(final Graphics g) {
1040 UISettings.setupAntialiasing(g);
1041 super.paintComponent(g);
1045 private static final String EXTRA_ELEM = "...";
1047 private class CalcElementsThread implements Runnable {
1048 private final String myPattern;
1049 private boolean myCheckboxState;
1050 private final CalcElementsCallback myCallback;
1051 private final ModalityState myModalityState;
1053 private Set<Object> myElements = null;
1055 private volatile boolean myCancelled = false;
1056 private final boolean myCanCancel;
1058 private CalcElementsThread(String pattern, boolean checkboxState, CalcElementsCallback callback, ModalityState modalityState, boolean canCancel) {
1059 myPattern = pattern;
1060 myCheckboxState = checkboxState;
1061 myCallback = callback;
1062 myModalityState = modalityState;
1063 myCanCancel = canCancel;
1066 private final Alarm myShowCardAlarm = new Alarm();
1067 public void run() {
1068 showCard(SEARCHING_CARD, 200);
1070 final Set<Object> elements = new LinkedHashSet<Object>();
1071 Runnable action = new Runnable() {
1072 public void run() {
1073 try {
1074 ensureNamesLoaded(myCheckboxState);
1075 addElementsByPattern(elements, myPattern);
1076 for (Object elem : elements) {
1077 if (myCancelled) {
1078 break;
1080 if (elem instanceof PsiElement) {
1081 final PsiElement psiElement = (PsiElement)elem;
1082 psiElement.isWritable(); // That will cache writable flag in VirtualFile. Taking the action here makes it canceleable.
1086 catch (ProcessCanceledException e) {
1087 //OK
1091 ApplicationManager.getApplication().runReadAction(action);
1093 if (myCancelled) {
1094 myShowCardAlarm.cancelAllRequests();
1095 return;
1098 final String cardToShow;
1099 if (elements.isEmpty() && !myCheckboxState) {
1100 myCheckboxState = true;
1101 ApplicationManager.getApplication().runReadAction(action);
1102 cardToShow = elements.isEmpty() ? NOT_FOUND_CARD : NOT_FOUND_IN_PROJECT_CARD;
1104 else {
1105 cardToShow = elements.isEmpty() ? NOT_FOUND_CARD : CHECK_BOX_CARD;
1107 showCard(cardToShow, 0);
1109 myElements = elements;
1111 ApplicationManager.getApplication().invokeLater(new Runnable() {
1112 public void run() {
1113 myCallback.run(myElements);
1115 }, myModalityState);
1118 private void showCard(final String card, final int delay) {
1119 myShowCardAlarm.cancelAllRequests();
1120 myShowCardAlarm.addRequest(new Runnable() {
1121 public void run() {
1122 myCard.show(myCardContainer, card);
1124 }, delay, myModalityState);
1127 private void addElementsByPattern(Set<Object> elementsArray, String pattern) {
1128 String namePattern = getNamePattern(pattern);
1129 String qualifierPattern = getQualifierPattern(pattern);
1131 boolean empty = namePattern.length() == 0 || namePattern.equals("@"); // TODO[yole]: remove implicit dependency
1132 if (empty && !isShowListForEmptyPattern()) return;
1134 List<String> namesList = new ArrayList<String>();
1135 getNamesByPattern(myCheckboxState, this, namesList, namePattern);
1136 if (myCancelled) {
1137 throw new ProcessCanceledException();
1139 Collections.sort(namesList, new MatchesComparator(pattern));
1141 boolean overflow = false;
1142 List<Object> sameNameElements = new SmartList<Object>();
1143 All:
1144 for (String name : namesList) {
1145 if (myCancelled) {
1146 throw new ProcessCanceledException();
1148 final Object[] elements = myModel.getElementsByName(name, myCheckboxState, namePattern);
1149 if (elements.length > 1) {
1150 sameNameElements.clear();
1151 for (final Object element : elements) {
1152 if (matchesQualifier(element, qualifierPattern)) {
1153 sameNameElements.add(element);
1156 sortByProximity(sameNameElements);
1157 for (Object element : sameNameElements) {
1158 elementsArray.add(element);
1159 if (elementsArray.size() >= myMaximumListSizeLimit) {
1160 overflow = true;
1161 break All;
1165 else if (elements.length == 1 && matchesQualifier(elements[0], qualifierPattern)) {
1166 elementsArray.add(elements[0]);
1167 if (elementsArray.size() >= myMaximumListSizeLimit) {
1168 overflow = true;
1169 break;
1174 if (overflow) {
1175 elementsArray.add(EXTRA_ELEM);
1179 private void cancel() {
1180 if (myCanCancel) {
1181 myCancelled = true;
1186 private void sortByProximity(final List<Object> sameNameElements) {
1187 Collections.sort(sameNameElements, new PathProximityComparator(myModel, myContext.get()));
1190 private List<String> split(String s) {
1191 List<String> answer = new ArrayList<String>();
1192 for (String token : StringUtil.tokenize(s, StringUtil.join(myModel.getSeparators(), ""))) {
1193 if (token.length() > 0) {
1194 answer.add(token);
1198 return answer.isEmpty() ? Collections.singletonList(s) : answer;
1201 private boolean matchesQualifier(final Object element, final String qualifierPattern) {
1202 final String name = myModel.getFullName(element);
1203 if (name == null) return false;
1205 final List<String> suspects = split(name);
1206 final List<Pair<String, NameUtil.Matcher>> patternsAndMatchers = ContainerUtil.map2List(split(qualifierPattern), new Function<String, Pair<String, NameUtil.Matcher>>() {
1207 public Pair<String, NameUtil.Matcher> fun(String s) {
1208 final String pattern = getNamePattern(s);
1209 final NameUtil.Matcher matcher = buildPatternMatcher(pattern);
1211 return new Pair<String, NameUtil.Matcher>(pattern, matcher);
1215 int matchPosition = 0;
1217 try {
1218 patterns:
1219 for (Pair<String, NameUtil.Matcher> patternAndMatcher : patternsAndMatchers) {
1220 final String pattern = patternAndMatcher.first;
1221 final NameUtil.Matcher matcher = patternAndMatcher.second;
1222 if (pattern.length() > 0) {
1223 for (int j = matchPosition; j < suspects.size() - 1; j++) {
1224 String suspect = suspects.get(j);
1225 if (matches(pattern, matcher, suspect)) {
1226 matchPosition = j + 1;
1227 continue patterns;
1231 return false;
1234 } catch (Exception e) {
1235 // Do nothing. No matches appears valid result for "bad" pattern
1236 return false;
1239 return true;
1242 private void getNamesByPattern(final boolean checkboxState,
1243 CalcElementsThread calcElementsThread,
1244 final List<String> list,
1245 String pattern) throws ProcessCanceledException {
1246 if (!isShowListForEmptyPattern()) {
1247 LOG.assertTrue(pattern.length() > 0);
1250 if (pattern.startsWith("@")) {
1251 pattern = pattern.substring(1);
1254 final String[] names = checkboxState ? myNames[1] : myNames[0];
1255 final NameUtil.Matcher matcher = buildPatternMatcher(pattern);
1257 try {
1258 for (String name : names) {
1259 if (calcElementsThread != null && calcElementsThread.myCancelled) {
1260 break;
1262 if (matches(pattern, matcher, name)) {
1263 list.add(name);
1267 catch (Exception e) {
1268 // Do nothing. No matches appears valid result for "bad" pattern
1272 private boolean matches(String pattern, NameUtil.Matcher matcher, String name) {
1273 boolean matches = false;
1274 if (name != null) {
1275 if (myModel instanceof CustomMatcherModel) {
1276 if (((CustomMatcherModel)myModel).matches(name, pattern)) {
1277 matches = true;
1280 else if (pattern.length() == 0 || matcher.matches(name)) {
1281 matches = true;
1284 return matches;
1287 private NameUtil.Matcher buildPatternMatcher(String pattern) {
1288 return NameUtil.buildMatcher(pattern, 0, true, true, pattern.toLowerCase().equals(pattern));
1291 private interface CalcElementsCallback {
1292 void run(Set<?> elements);
1295 private static class PathProximityComparator implements Comparator<Object> {
1296 private final ChooseByNameModel myModel;
1297 private final PsiProximityComparator myProximityComparator;
1299 private PathProximityComparator(final ChooseByNameModel model, final PsiElement context) {
1300 myModel = model;
1301 myProximityComparator = new PsiProximityComparator(context);
1304 public int compare(final Object o1, final Object o2) {
1305 int rc = myProximityComparator.compare(o1, o2);
1306 if (rc != 0) return rc;
1308 return Comparing.compare(myModel.getFullName(o1), myModel.getFullName(o2));