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.
16 package com
.intellij
.ui
;
18 import com
.intellij
.featureStatistics
.FeatureUsageTracker
;
19 import com
.intellij
.ide
.DataManager
;
20 import com
.intellij
.openapi
.actionSystem
.PlatformDataKeys
;
21 import com
.intellij
.openapi
.application
.ApplicationManager
;
22 import com
.intellij
.openapi
.diagnostic
.Logger
;
23 import com
.intellij
.openapi
.project
.Project
;
24 import com
.intellij
.openapi
.util
.Key
;
25 import com
.intellij
.openapi
.wm
.ToolWindowManager
;
26 import com
.intellij
.openapi
.wm
.ex
.ToolWindowManagerAdapter
;
27 import com
.intellij
.openapi
.wm
.ex
.ToolWindowManagerEx
;
28 import com
.intellij
.openapi
.wm
.ex
.ToolWindowManagerListener
;
29 import com
.intellij
.util
.StringBuilderSpinAllocator
;
30 import com
.intellij
.util
.ui
.UIUtil
;
31 import org
.jetbrains
.annotations
.NonNls
;
34 import javax
.swing
.text
.AttributeSet
;
35 import javax
.swing
.text
.BadLocationException
;
36 import javax
.swing
.text
.PlainDocument
;
38 import java
.awt
.event
.FocusAdapter
;
39 import java
.awt
.event
.FocusEvent
;
40 import java
.awt
.event
.KeyAdapter
;
41 import java
.awt
.event
.KeyEvent
;
42 import java
.beans
.PropertyChangeListener
;
43 import java
.beans
.PropertyChangeSupport
;
44 import java
.util
.regex
.Matcher
;
45 import java
.util
.regex
.Pattern
;
46 import java
.util
.regex
.PatternSyntaxException
;
48 public abstract class SpeedSearchBase
<Comp
extends JComponent
> {
49 private static final Logger LOG
= Logger
.getInstance("#com.intellij.ui.SpeedSearchBase");
50 private SearchPopup mySearchPopup
;
51 private JLayeredPane myPopupLayeredPane
;
52 protected final Comp myComponent
;
53 private final ToolWindowManagerListener myWindowManagerListener
= new MyToolWindowManagerListener();
54 private final PropertyChangeSupport myChangeSupport
= new PropertyChangeSupport(this);
55 private String myRecentEnteredPrefix
;
56 private SpeedSearchComparator myComparator
= new SpeedSearchComparator();
58 private static final Key SPEED_SEARCH_COMPONENT_MARKER
= new Key("SPEED_SEARCH_COMPONENT_MARKER");
59 @NonNls protected static final String ENTERED_PREFIX_PROPERTY_NAME
= "enteredPrefix";
61 public SpeedSearchBase(Comp component
) {
62 myComponent
= component
;
64 myComponent
.addFocusListener(new FocusAdapter() {
65 public void focusLost(FocusEvent e
) {
66 manageSearchPopup(null);
69 myComponent
.addKeyListener(new KeyAdapter() {
70 public void keyTyped(KeyEvent e
) {
74 public void keyPressed(KeyEvent e
) {
79 component
.putClientProperty(SPEED_SEARCH_COMPONENT_MARKER
, this);
82 public static boolean hasActiveSpeedSearch(JComponent component
) {
83 SpeedSearchBase speedSearch
= (SpeedSearchBase
)component
.getClientProperty(SPEED_SEARCH_COMPONENT_MARKER
);
84 return speedSearch
!= null && speedSearch
.mySearchPopup
!= null && speedSearch
.mySearchPopup
.isVisible();
87 protected abstract int getSelectedIndex();
89 protected abstract Object
[] getAllElements();
91 protected abstract String
getElementText(Object element
);
93 protected abstract void selectElement(Object element
, String selectedText
);
95 public void addChangeListener(PropertyChangeListener listener
) {
96 myChangeSupport
.addPropertyChangeListener(listener
);
99 public void removeChangeListener(PropertyChangeListener listener
) {
100 myChangeSupport
.removePropertyChangeListener(listener
);
103 private void fireStateChanged() {
104 String enteredPrefix
= getEnteredPrefix();
105 myChangeSupport
.firePropertyChange(ENTERED_PREFIX_PROPERTY_NAME
, myRecentEnteredPrefix
, enteredPrefix
);
106 myRecentEnteredPrefix
= enteredPrefix
;
109 protected boolean isMatchingElement(Object element
, String pattern
) {
110 String str
= getElementText(element
);
111 return str
!= null && compare(str
, pattern
);
114 protected boolean compare(String text
, String pattern
) {
115 return myComparator
.doCompare(pattern
, text
);
118 public SpeedSearchComparator
getComparator() {
122 public void setComparator(final SpeedSearchComparator comparator
) {
123 myComparator
= comparator
;
126 public static class SpeedSearchComparator
{
127 private Matcher myRecentSearchMatcher
;
128 private String myRecentSearchText
;
129 private boolean myShouldMatchFromTheBeginning
;
131 public SpeedSearchComparator() {
135 public SpeedSearchComparator(boolean shouldMatchFromTheBeginning
) {
136 myShouldMatchFromTheBeginning
= shouldMatchFromTheBeginning
;
139 public boolean doCompare(String pattern
, String text
) {
140 if (myRecentSearchText
!= null &&
141 myRecentSearchText
.equals(pattern
)
143 myRecentSearchMatcher
.reset(text
);
144 return myRecentSearchMatcher
.find();
147 myRecentSearchText
= pattern
;
148 @NonNls final StringBuilder buf
= StringBuilderSpinAllocator
.alloc();
151 translatePattern(buf
, pattern
);
154 boolean allLowercase
= pattern
.equals(pattern
.toLowerCase());
155 final Pattern recentSearchPattern
= Pattern
.compile(buf
.toString(), allLowercase ? Pattern
.CASE_INSENSITIVE
: 0);
156 return (myRecentSearchMatcher
= recentSearchPattern
.matcher(text
)).find();
158 catch (PatternSyntaxException ex
) {
159 myRecentSearchText
= null;
163 StringBuilderSpinAllocator
.dispose(buf
);
170 public void translatePattern(final StringBuilder buf
, final String pattern
) {
171 if (myShouldMatchFromTheBeginning
) buf
.append('^'); // match from the line start
172 final int len
= pattern
.length();
173 for (int i
= 0; i
< len
; ++i
) {
174 translateCharacter(buf
, pattern
.charAt(i
));
178 public void translateCharacter(final StringBuilder buf
, final char ch
) {
180 buf
.append("(\\w|:)"); // ':' for xml tags
182 else if ("{}[].+^$()?".indexOf(ch
) != -1) {
183 // do not bother with other metachars
186 if (Character
.isUpperCase(ch
)) {
188 buf
.append("[a-z]*");
194 private Object
findNextElement(String s
) {
195 String _s
= s
.trim();
196 Object
[] elements
= getAllElements();
197 if (elements
.length
== 0) return null;
198 int selectedIndex
= getSelectedIndex();
199 for (int i
= selectedIndex
+ 1; i
< elements
.length
; i
++) {
200 Object element
= elements
[i
];
201 if (isMatchingElement(element
, _s
)) return element
;
203 return selectedIndex
!= -1 ? elements
[selectedIndex
] : null; // return current
206 private Object
findPreviousElement(String s
) {
207 String _s
= s
.trim();
208 Object
[] elements
= getAllElements();
209 if (elements
.length
== 0) return null;
210 int selectedIndex
= getSelectedIndex();
211 for (int i
= selectedIndex
- 1; i
>= 0; i
--) {
212 Object element
= elements
[i
];
213 if (isMatchingElement(element
, _s
)) return element
;
215 return selectedIndex
!= -1 ? elements
[selectedIndex
] : null; // return current
218 private Object
findElement(String s
) {
219 String _s
= s
.trim();
220 Object
[] elements
= getAllElements();
221 int selectedIndex
= getSelectedIndex();
222 if (selectedIndex
< 0) {
225 for (int i
= selectedIndex
; i
< elements
.length
; i
++) {
226 Object element
= elements
[i
];
227 if (isMatchingElement(element
, _s
)) return element
;
229 for (int i
= 0; i
< selectedIndex
; i
++) {
230 Object element
= elements
[i
];
231 if (isMatchingElement(element
, _s
)) return element
;
236 private Object
findFirstElement(String s
) {
237 String _s
= s
.trim();
238 Object
[] elements
= getAllElements();
239 for (Object element
: elements
) {
240 if (isMatchingElement(element
, _s
)) return element
;
245 private Object
findLastElement(String s
) {
246 String _s
= s
.trim();
247 Object
[] elements
= getAllElements();
248 for (int i
= elements
.length
- 1; i
>= 0; i
--) {
249 Object element
= elements
[i
];
250 if (isMatchingElement(element
, _s
)) return element
;
255 private void processKeyEvent(KeyEvent e
) {
256 if (e
.isAltDown()) return;
257 if (mySearchPopup
!= null) {
258 mySearchPopup
.processKeyEvent(e
);
261 if (!isSpeedSearchEnabled()) return;
262 if (e
.getID() == KeyEvent
.KEY_TYPED
) {
263 if (!UIUtil
.isReallyTypedEvent(e
)) return;
265 char c
= e
.getKeyChar();
266 if (Character
.isLetterOrDigit(c
) || c
== '_' || c
== '*' || c
== '/' || c
== ':') {
267 manageSearchPopup(new SearchPopup(String
.valueOf(c
)));
274 public Comp
getComponent() {
278 protected boolean isSpeedSearchEnabled() {
282 public String
getEnteredPrefix() {
283 return mySearchPopup
!= null ? mySearchPopup
.mySearchField
.getText() : null;
286 public void refreshSelection() {
287 if ( mySearchPopup
!= null ) mySearchPopup
.refreshSelection();
290 private class SearchPopup
extends JPanel
{
291 private final SearchField mySearchField
;
293 public SearchPopup(String initialString
) {
294 final Color foregroundColor
= UIUtil
.getToolTipForeground();
295 Color color1
= UIUtil
.getToolTipBackground();
296 mySearchField
= new SearchField();
297 final JLabel searchLabel
= new JLabel(" " + UIBundle
.message("search.popup.search.for.label") + " ");
298 searchLabel
.setFont(searchLabel
.getFont().deriveFont(Font
.BOLD
));
299 searchLabel
.setForeground(foregroundColor
);
300 mySearchField
.setBorder(null);
301 mySearchField
.setBackground(color1
.brighter());
302 mySearchField
.setForeground(foregroundColor
);
304 mySearchField
.setDocument(new PlainDocument() {
305 public void insertString(int offs
, String str
, AttributeSet a
) throws BadLocationException
{
308 oldText
= getText(0, getLength());
310 catch (BadLocationException e1
) {
314 String newText
= oldText
.substring(0, offs
) + str
+ oldText
.substring(offs
);
315 super.insertString(offs
, str
, a
);
316 if (findElement(newText
) == null) {
317 mySearchField
.setForeground(Color
.RED
);
320 mySearchField
.setForeground(foregroundColor
);
324 mySearchField
.setText(initialString
);
326 setBorder(BorderFactory
.createLineBorder(Color
.gray
, 1));
327 setBackground(color1
.brighter());
328 setLayout(new BorderLayout());
329 add(searchLabel
, BorderLayout
.WEST
);
330 add(mySearchField
, BorderLayout
.EAST
);
331 Object element
= findElement(mySearchField
.getText());
332 updateSelection(element
);
335 public void processKeyEvent(KeyEvent e
) {
336 mySearchField
.processKeyEvent(e
);
337 if (e
.isConsumed()) {
338 int keyCode
= e
.getKeyCode();
339 String s
= mySearchField
.getText();
341 if (keyCode
== KeyEvent
.VK_UP
) {
342 element
= findPreviousElement(s
);
344 else if (keyCode
== KeyEvent
.VK_DOWN
) {
345 element
= findNextElement(s
);
347 else if (keyCode
== KeyEvent
.VK_HOME
) {
348 element
= findFirstElement(s
);
350 else if (keyCode
== KeyEvent
.VK_END
) {
351 element
= findLastElement(s
);
354 element
= findElement(s
);
356 updateSelection(element
);
360 public void refreshSelection () {
361 updateSelection(findElement(mySearchField
.getText()));
364 private void updateSelection(Object element
) {
365 if (element
!= null) {
366 selectElement(element
, mySearchField
.getText());
367 mySearchField
.setForeground(Color
.black
);
370 mySearchField
.setForeground(Color
.red
);
372 if (mySearchPopup
!= null) {
373 mySearchPopup
.setSize(mySearchPopup
.getPreferredSize());
374 mySearchPopup
.validate();
381 private class SearchField
extends JTextField
{
386 public Dimension
getPreferredSize() {
387 Dimension dim
= super.getPreferredSize();
388 dim
.width
= getFontMetrics(getFont()).stringWidth(getText()) + 10;
393 * I made this method public in order to be able to call it from the outside.
394 * This is needed for delegating calls.
396 public void processKeyEvent(KeyEvent e
) {
397 int i
= e
.getKeyCode();
398 if (i
== KeyEvent
.VK_BACK_SPACE
&& getDocument().getLength() == 0) {
403 i
== KeyEvent
.VK_ENTER
||
404 i
== KeyEvent
.VK_ESCAPE
||
405 i
== KeyEvent
.VK_PAGE_UP
||
406 i
== KeyEvent
.VK_PAGE_DOWN
||
407 i
== KeyEvent
.VK_LEFT
||
408 i
== KeyEvent
.VK_RIGHT
410 manageSearchPopup(null);
411 if (i
== KeyEvent
.VK_ESCAPE
) {
416 super.processKeyEvent(e
);
418 i
== KeyEvent
.VK_BACK_SPACE
||
419 i
== KeyEvent
.VK_HOME
||
420 i
== KeyEvent
.VK_END
||
421 i
== KeyEvent
.VK_UP
||
422 i
== KeyEvent
.VK_DOWN
429 private void manageSearchPopup(SearchPopup searchPopup
) {
430 final Project project
;
431 if (ApplicationManager
.getApplication() != null && !ApplicationManager
.getApplication().isDisposed()) {
432 project
= PlatformDataKeys
.PROJECT
.getData(DataManager
.getInstance().getDataContext(myComponent
));
438 if (mySearchPopup
!= null) {
439 myPopupLayeredPane
.remove(mySearchPopup
);
440 myPopupLayeredPane
.validate();
441 myPopupLayeredPane
.repaint();
442 myPopupLayeredPane
= null;
444 if (project
!= null) {
445 ((ToolWindowManagerEx
)ToolWindowManager
.getInstance(project
)).removeToolWindowManagerListener(myWindowManagerListener
);
448 else if (searchPopup
!= null) {
449 FeatureUsageTracker
.getInstance().triggerFeatureUsed("ui.tree.speedsearch");
452 if (!myComponent
.isShowing()) {
453 mySearchPopup
= null;
456 mySearchPopup
= searchPopup
;
461 if (mySearchPopup
== null || !myComponent
.isDisplayable()) return;
463 if (project
!= null) {
464 ((ToolWindowManagerEx
)ToolWindowManager
.getInstance(project
)).addToolWindowManagerListener(myWindowManagerListener
);
466 JRootPane rootPane
= myComponent
.getRootPane();
467 if (rootPane
!= null) {
468 myPopupLayeredPane
= rootPane
.getLayeredPane();
471 myPopupLayeredPane
= null;
473 if (myPopupLayeredPane
== null) {
474 LOG
.error(toString() + " in " + String
.valueOf(myComponent
));
477 myPopupLayeredPane
.add(mySearchPopup
, JLayeredPane
.POPUP_LAYER
);
478 if (myPopupLayeredPane
== null) return; // See # 27482. Somewho it does happen...
479 Point lPaneP
= myPopupLayeredPane
.getLocationOnScreen();
480 Point componentP
= myComponent
.getLocationOnScreen();
481 Rectangle r
= myComponent
.getVisibleRect();
482 Dimension prefSize
= mySearchPopup
.getPreferredSize();
483 Window window
= (Window
)SwingUtilities
.getAncestorOfClass(Window
.class, myComponent
);
485 if (window
instanceof JDialog
) {
486 windowP
= ((JDialog
)window
).getContentPane().getLocationOnScreen();
488 else if (window
instanceof JFrame
) {
489 windowP
= ((JFrame
)window
).getContentPane().getLocationOnScreen();
492 windowP
= window
.getLocationOnScreen();
494 int y
= r
.y
+ componentP
.y
- lPaneP
.y
- prefSize
.height
;
495 y
= Math
.max(y
, windowP
.y
- lPaneP
.y
);
496 mySearchPopup
.setLocation(componentP
.x
- lPaneP
.x
+ r
.x
, y
);
497 mySearchPopup
.setSize(prefSize
);
498 mySearchPopup
.setVisible(true);
499 mySearchPopup
.validate();
502 private class MyToolWindowManagerListener
extends ToolWindowManagerAdapter
{
503 public void stateChanged() {
504 manageSearchPopup(null);