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
;
61 import javax
.swing
.event
.DocumentEvent
;
62 import javax
.swing
.event
.ListSelectionEvent
;
63 import javax
.swing
.event
.ListSelectionListener
;
64 import javax
.swing
.text
.DefaultEditorKit
;
66 import java
.awt
.event
.*;
67 import java
.lang
.ref
.Reference
;
68 import java
.lang
.ref
.WeakReference
;
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
140 protected ChooseByNameBase(Project project
, ChooseByNameModel model
, String initialText
, final PsiElement context
) {
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
) {
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;
169 public Object
getData(String dataId
) {
170 if (PlatformDataKeys
.HELP_ID
.is(dataId
)) {
171 return myModel
.getHelpId();
173 if (!myListIsUpToDate
) {
176 if (LangDataKeys
.PSI_ELEMENT
.is(dataId
)) {
177 Object element
= getChosenElement();
179 if (element
instanceof PsiElement
) {
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
)) {
205 public void registerHint(JBPopup h
) {
206 if (myHint
!= null && myHint
.isVisible() && 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() {
228 public void hideHint() {
229 if (myHint
!= null) {
234 public JBPopup
getHint() {
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){
243 updateProcessor
.updatePopup(element
);
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
));
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 (")"));
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
);
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() {
348 if (!JBPopupFactory
.getInstance().isChildPopupFocused(e
.getComponent())) {
357 myCheckBox
.addItemListener(new ItemListener() {
358 public void itemStateChanged(ItemEvent e
) {
362 myCheckBox
.setFocusable(false);
364 myTextField
.getDocument().addDocumentListener(new DocumentAdapter() {
365 protected void textChanged(DocumentEvent e
) {
366 clearPosponedOkAction(false);
371 myTextField
.addKeyListener(new KeyAdapter() {
372 public void keyPressed(KeyEvent e
) {
373 if (!myListScrollPane
.isVisible()) {
376 final int keyCode
= e
.getKeyCode();
378 case KeyEvent
.VK_DOWN
:
379 ListScrollingUtil
.moveDown(myList
, e
.getModifiersEx());
382 ListScrollingUtil
.moveUp(myList
, e
.getModifiersEx());
384 case KeyEvent
.VK_PAGE_UP
:
385 ListScrollingUtil
.movePageUp(myList
);
387 case KeyEvent
.VK_PAGE_DOWN
:
388 ListScrollingUtil
.movePageDown(myList
);
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());
410 myTextField
.addActionListener(new ActionListener() {
411 public void actionPerformed(ActionEvent actionEvent
) {
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());
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()) {
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();
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;
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
);
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
) {
573 }, KeyStroke
.getKeyStroke(KeyEvent
.VK_ESCAPE
, 0),
574 JComponent
.WHEN_IN_FOCUSED_WINDOW
577 myList
.registerKeyboardAction(new AbstractAction() {
578 public void actionPerformed(ActionEvent e
) {
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();
601 throw new IllegalStateException("cannot find parent window: project=" + myProject
+
602 (myProject
!= null ?
"; open=" + myProject
.isOpen() : "") +
603 "; window=" + window
);
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() {
619 final String text
= myTextField
.getText();
620 if (!isShowListForEmptyPattern() && (text
== null || text
.trim().length() == 0)) {
623 myCard
.show(myCardContainer
, CHECK_BOX_CARD
);
626 final Runnable request
= new Runnable() {
628 final CalcElementsCallback callback
= new CalcElementsCallback() {
629 public void run(final Set
<?
> elements
) {
630 synchronized (myRebuildMutex
) {
631 ApplicationManager
.getApplication().assertIsDispatchThread();
632 if (myDisposedFlag
) {
636 setElementsToList(pos
, elements
);
638 myListIsUpToDate
= true;
639 choosenElementMightChange();
641 if (postRunnable
!= null) {
648 cancelCalcElementsThread();
650 myCalcElementsThread
= new CalcElementsThread(text
, myCheckBox
.isSelected(), callback
, modalityState
, postRunnable
== null);
651 ApplicationManager
.getApplication().executeOnPooledThread(myCalcElementsThread
);
656 myAlarm
.addRequest(request
, delay
, ModalityState
.stateForComponent(myTextField
));
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()) {
677 myTextField
.setForeground(Color
.red
);
678 myListUpdater
.cancelAll();
680 clearPosponedOkAction(false);
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
>();
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()) {
713 myListUpdater
.appendToModel(commands
, pos
);
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()));
726 private int detectBestStatisticalPosition() {
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
);
736 int stats
= StatisticsManager
.getInstance().getUseCount(new StatisticsInfo(statContext
, text
));
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
{
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
) {
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
) {
796 this.element
= element
;
799 public void apply() {
800 if (idx
< myListModel
.size()) {
801 myListModel
.add(idx
, element
);
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() {
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() {
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);
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
);
843 doPostponedOkIfNeeded();
845 if (!myDisposedFlag
) {
852 private void doPostponedOkIfNeeded() {
853 if (myPosponedOkAction
!= null) {
854 if (getChosenElement() != null) {
856 clearPosponedOkAction(myDisposedFlag
);
862 private void clearPosponedOkAction(boolean success
) {
863 if (myPosponedOkAction
!= null) {
865 myPosponedOkAction
.setDone();
867 myPosponedOkAction
.setRejected();
871 myPosponedOkAction
= null;
874 protected abstract void showList();
876 protected abstract void hideList();
878 protected abstract void close(boolean isOk
);
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
);
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() {
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();
940 protected void processKeyEvent(KeyEvent e
) {
941 final KeyStroke keyStroke
= KeyStroke
.getKeyStrokeForEvent(e
);
942 if (myCompletionKeyStroke
!= null && keyStroke
.equals(myCompletionKeyStroke
)) {
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() {
950 fillInCommonPrefix(pattern
);
953 rebuildList(0, 0, postRunnable
, ModalityState
.current());
956 if (backStroke
!= null && keyStroke
.equals(backStroke
)) {
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());
968 if (forwardStroke
!= null && keyStroke
.equals(forwardStroke
)) {
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());
981 super.processKeyEvent(e
);
983 catch (NullPointerException e1
) {
984 if (!Patches
.SUN_BUG_6322854
) {
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
;
1006 while (commonPrefix
.length() > 0) {
1007 if (string
.startsWith(commonPrefix
)) {
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();
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());
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;
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();
1077 showCard(SEARCHING_CARD
, 200);
1079 final Set
<Object
> elements
= new LinkedHashSet
<Object
>();
1080 Runnable action
= new Runnable() {
1083 ensureNamesLoaded(myCheckboxState
);
1084 addElementsByPattern(elements
, myPattern
);
1085 for (Object elem
: elements
) {
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
) {
1100 ApplicationManager
.getApplication().runReadAction(action
);
1103 myShowCardAlarm
.cancelAllRequests();
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
;
1114 cardToShow
= elements
.isEmpty() ? NOT_FOUND_CARD
: CHECK_BOX_CARD
;
1116 showCard(cardToShow
, 0);
1118 myElements
= elements
;
1120 ApplicationManager
.getApplication().invokeLater(new Runnable() {
1122 myCallback
.run(myElements
);
1124 }, myModalityState
);
1127 private void showCard(final String card
, final int delay
) {
1128 myShowCardAlarm
.cancelAllRequests();
1129 myShowCardAlarm
.addRequest(new Runnable() {
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
);
1146 throw new ProcessCanceledException();
1148 Collections
.sort(namesList
, new MatchesComparator(pattern
));
1150 boolean overflow
= false;
1151 List
<Object
> sameNameElements
= new SmartList
<Object
>();
1153 for (String name
: namesList
) {
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
) {
1174 else if (elements
.length
== 1 && matchesQualifier(elements
[0], qualifierPattern
)) {
1175 elementsArray
.add(elements
[0]);
1176 if (elementsArray
.size() >= myMaximumListSizeLimit
) {
1184 elementsArray
.add(EXTRA_ELEM
);
1188 private void cancel() {
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) {
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;
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;
1243 } catch (Exception e
) {
1244 // Do nothing. No matches appears valid result for "bad" pattern
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
);
1267 for (String name
: names
) {
1268 if (calcElementsThread
!= null && calcElementsThread
.myCancelled
) {
1271 if (matches(pattern
, matcher
, 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;
1284 if (myModel
instanceof CustomMatcherModel
) {
1285 if (((CustomMatcherModel
)myModel
).matches(name
, pattern
)) {
1289 else if (pattern
.length() == 0 || matcher
.matches(name
)) {
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
) {
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
));