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;
32 import org.jetbrains.annotations.NotNull;
33 import org.jetbrains.annotations.Nullable;
36 import javax.swing.text.AttributeSet;
37 import javax.swing.text.BadLocationException;
38 import javax.swing.text.PlainDocument;
40 import java.awt.event.FocusAdapter;
41 import java.awt.event.FocusEvent;
42 import java.awt.event.KeyAdapter;
43 import java.awt.event.KeyEvent;
44 import java.beans.PropertyChangeListener;
45 import java.beans.PropertyChangeSupport;
46 import java.util.ListIterator;
47 import java.util.NoSuchElementException;
48 import java.util.regex.Matcher;
49 import java.util.regex.Pattern;
50 import java.util.regex.PatternSyntaxException;
52 public abstract class SpeedSearchBase<Comp extends JComponent> {
53 private static final Logger LOG = Logger.getInstance("#com.intellij.ui.SpeedSearchBase");
54 private SearchPopup mySearchPopup;
55 private JLayeredPane myPopupLayeredPane;
56 protected final Comp myComponent;
57 private final ToolWindowManagerListener myWindowManagerListener = new MyToolWindowManagerListener();
58 private final PropertyChangeSupport myChangeSupport = new PropertyChangeSupport(this);
59 private String myRecentEnteredPrefix;
60 private SpeedSearchComparator myComparator = new SpeedSearchComparator();
62 private static final Key SPEED_SEARCH_COMPONENT_MARKER = new Key("SPEED_SEARCH_COMPONENT_MARKER");
63 @NonNls protected static final String ENTERED_PREFIX_PROPERTY_NAME = "enteredPrefix";
65 public SpeedSearchBase(Comp component) {
66 myComponent = component;
68 myComponent.addFocusListener(new FocusAdapter() {
69 public void focusLost(FocusEvent e) {
70 manageSearchPopup(null);
73 myComponent.addKeyListener(new KeyAdapter() {
74 public void keyTyped(KeyEvent e) {
78 public void keyPressed(KeyEvent e) {
83 component.putClientProperty(SPEED_SEARCH_COMPONENT_MARKER, this);
86 public static boolean hasActiveSpeedSearch(JComponent component) {
87 SpeedSearchBase speedSearch = (SpeedSearchBase)component.getClientProperty(SPEED_SEARCH_COMPONENT_MARKER);
88 return speedSearch != null && speedSearch.mySearchPopup != null && speedSearch.mySearchPopup.isVisible();
92 * Returns visual (view) selection index.
94 protected abstract int getSelectedIndex();
96 protected abstract Object[] getAllElements();
98 protected abstract String getElementText(Object element);
101 * Should convert given view index to model index
103 protected int convertIndexToModel(final int viewIndex) {
108 * @param element Element to select. Don't forget to convert model index to view index if needed (i.e. table.convertRowIndexToView(modelIndex), etc).
109 * @param selectedText search text
111 protected abstract void selectElement(Object element, String selectedText);
113 protected ListIterator<Object> getElementIterator(int startingIndex) {
114 final Object[] allElements = getAllElements();
115 return new ViewIterator(this, startingIndex < 0 ? allElements.length : startingIndex);
118 public void addChangeListener(PropertyChangeListener listener) {
119 myChangeSupport.addPropertyChangeListener(listener);
122 public void removeChangeListener(PropertyChangeListener listener) {
123 myChangeSupport.removePropertyChangeListener(listener);
126 private void fireStateChanged() {
127 String enteredPrefix = getEnteredPrefix();
128 myChangeSupport.firePropertyChange(ENTERED_PREFIX_PROPERTY_NAME, myRecentEnteredPrefix, enteredPrefix);
129 myRecentEnteredPrefix = enteredPrefix;
132 protected boolean isMatchingElement(Object element, String pattern) {
133 String str = getElementText(element);
134 return str != null && compare(str, pattern);
137 protected boolean compare(String text, String pattern) {
138 return myComparator.doCompare(pattern, text);
141 public SpeedSearchComparator getComparator() {
145 public void setComparator(final SpeedSearchComparator comparator) {
146 myComparator = comparator;
149 public static class SpeedSearchComparator {
150 private Matcher myRecentSearchMatcher;
151 private String myRecentSearchText;
152 private boolean myShouldMatchFromTheBeginning;
154 public SpeedSearchComparator() {
158 public SpeedSearchComparator(boolean shouldMatchFromTheBeginning) {
159 myShouldMatchFromTheBeginning = shouldMatchFromTheBeginning;
162 public boolean doCompare(String pattern, String text) {
163 if (myRecentSearchText != null &&
164 myRecentSearchText.equals(pattern)
166 myRecentSearchMatcher.reset(text);
167 return myRecentSearchMatcher.find();
170 myRecentSearchText = pattern;
171 @NonNls final StringBuilder buf = StringBuilderSpinAllocator.alloc();
174 translatePattern(buf, pattern);
177 boolean allLowercase = pattern.equals(pattern.toLowerCase());
178 final Pattern recentSearchPattern = Pattern.compile(buf.toString(), allLowercase ? Pattern.CASE_INSENSITIVE : 0);
179 return (myRecentSearchMatcher = recentSearchPattern.matcher(text)).find();
181 catch (PatternSyntaxException ex) {
182 myRecentSearchText = null;
186 StringBuilderSpinAllocator.dispose(buf);
193 public void translatePattern(final StringBuilder buf, final String pattern) {
194 if (myShouldMatchFromTheBeginning) buf.append('^'); // match from the line start
195 final int len = pattern.length();
196 for (int i = 0; i < len; ++i) {
197 translateCharacter(buf, pattern.charAt(i));
200 if (buf.length() > 0 && "*^".indexOf(buf.charAt(buf.length() - 1)) == -1) buf.append(')');
203 public String getRecentSearchText() {
204 return myRecentSearchText;
207 public Matcher getRecentSearchMatcher() {
208 return myRecentSearchMatcher;
211 public void translateCharacter(final StringBuilder buf, final char ch) {
213 buf.append("(\\w|:)"); // ':' for xml tags
215 else if ("{}[].+^$()?".indexOf(ch) != -1) {
216 // do not bother with other metachars
220 if (Character.isUpperCase(ch)) {
221 if (buf.length() > 0 && "*^".indexOf(buf.charAt(buf.length() - 1)) == -1) buf.append(')');
223 buf.append("[A-Za-z_]*");
226 if (buf.length() > 0 && "*^".indexOf(buf.charAt(buf.length() - 1)) != -1) buf.append('(');
229 if (buf.length() == 0 || buf.length() > 0 && "^".indexOf(buf.charAt(buf.length() - 1)) != -1) buf.append('(');
235 private Object findNextElement(String s) {
236 final String _s = s.trim();
237 final int selectedIndex = getSelectedIndex();
238 final ListIterator<?> it = getElementIterator(selectedIndex + 1);
239 final Object current;
240 if (it.hasPrevious()) {
241 current = it.previous();
245 while (it.hasNext()) {
246 final Object element = it.next();
247 if (isMatchingElement(element, _s)) return element;
249 return isMatchingElement(current, _s) ? current : null;
253 private Object findPreviousElement(String s) {
254 final String _s = s.trim();
255 final int selectedIndex = getSelectedIndex();
256 if (selectedIndex < 0) return null;
257 final ListIterator<?> it = getElementIterator(selectedIndex);
258 final Object current;
264 while (it.hasPrevious()) {
265 final Object element = it.previous();
266 if (isMatchingElement(element, _s)) return element;
268 return selectedIndex != -1 && isMatchingElement(current, _s) ? current : null;
272 private Object findElement(String s) {
273 final String _s = s.trim();
274 int selectedIndex = getSelectedIndex();
275 if (selectedIndex < 0) {
278 final ListIterator<Object> it = getElementIterator(selectedIndex);
279 while (it.hasNext()) {
280 final Object element = it.next();
281 if (isMatchingElement(element, _s)) return element;
283 if (selectedIndex > 0) {
284 while (it.hasPrevious()) it.previous();
285 while (it.hasNext() && it.nextIndex() != selectedIndex) {
286 final Object element = it.next();
287 if (isMatchingElement(element, _s)) return element;
294 private Object findFirstElement(String s) {
295 final String _s = s.trim();
296 for (ListIterator<?> it = getElementIterator(0); it.hasNext();) {
297 final Object element = it.next();
298 if (isMatchingElement(element, _s)) return element;
304 private Object findLastElement(String s) {
305 final String _s = s.trim();
306 for (ListIterator<?> it = getElementIterator(-1); it.hasPrevious();) {
307 final Object element = it.previous();
308 if (isMatchingElement(element, _s)) return element;
313 private void processKeyEvent(KeyEvent e) {
314 if (e.isAltDown()) return;
315 if (mySearchPopup != null) {
316 mySearchPopup.processKeyEvent(e);
319 if (!isSpeedSearchEnabled()) return;
320 if (e.getID() == KeyEvent.KEY_TYPED) {
321 if (!UIUtil.isReallyTypedEvent(e)) return;
323 char c = e.getKeyChar();
324 if (Character.isLetterOrDigit(c) || c == '_' || c == '*' || c == '/' || c == ':') {
325 manageSearchPopup(new SearchPopup(String.valueOf(c)));
332 public Comp getComponent() {
336 protected boolean isSpeedSearchEnabled() {
341 public String getEnteredPrefix() {
342 return mySearchPopup != null ? mySearchPopup.mySearchField.getText() : null;
345 public void refreshSelection() {
346 if ( mySearchPopup != null ) mySearchPopup.refreshSelection();
349 private class SearchPopup extends JPanel {
350 private final SearchField mySearchField;
352 public SearchPopup(String initialString) {
353 final Color foregroundColor = UIUtil.getToolTipForeground();
354 Color color1 = UIUtil.getToolTipBackground();
355 mySearchField = new SearchField();
356 final JLabel searchLabel = new JLabel(" " + UIBundle.message("search.popup.search.for.label") + " ");
357 searchLabel.setFont(searchLabel.getFont().deriveFont(Font.BOLD));
358 searchLabel.setForeground(foregroundColor);
359 mySearchField.setBorder(null);
360 mySearchField.setBackground(color1.brighter());
361 mySearchField.setForeground(foregroundColor);
363 mySearchField.setDocument(new PlainDocument() {
364 public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
367 oldText = getText(0, getLength());
369 catch (BadLocationException e1) {
373 String newText = oldText.substring(0, offs) + str + oldText.substring(offs);
374 super.insertString(offs, str, a);
375 if (findElement(newText) == null) {
376 mySearchField.setForeground(Color.RED);
379 mySearchField.setForeground(foregroundColor);
383 mySearchField.setText(initialString);
385 setBorder(BorderFactory.createLineBorder(Color.gray, 1));
386 setBackground(color1.brighter());
387 setLayout(new BorderLayout());
388 add(searchLabel, BorderLayout.WEST);
389 add(mySearchField, BorderLayout.EAST);
390 Object element = findElement(mySearchField.getText());
391 updateSelection(element);
394 public void processKeyEvent(KeyEvent e) {
395 mySearchField.processKeyEvent(e);
396 if (e.isConsumed()) {
397 int keyCode = e.getKeyCode();
398 String s = mySearchField.getText();
400 if (keyCode == KeyEvent.VK_UP) {
401 element = findPreviousElement(s);
403 else if (keyCode == KeyEvent.VK_DOWN) {
404 element = findNextElement(s);
406 else if (keyCode == KeyEvent.VK_HOME) {
407 element = findFirstElement(s);
409 else if (keyCode == KeyEvent.VK_END) {
410 element = findLastElement(s);
413 element = findElement(s);
415 updateSelection(element);
419 public void refreshSelection () {
420 updateSelection(findElement(mySearchField.getText()));
423 private void updateSelection(Object element) {
424 if (element != null) {
425 selectElement(element, mySearchField.getText());
426 mySearchField.setForeground(Color.black);
429 mySearchField.setForeground(Color.red);
431 if (mySearchPopup != null) {
432 mySearchPopup.setSize(mySearchPopup.getPreferredSize());
433 mySearchPopup.validate();
440 private class SearchField extends JTextField {
445 public Dimension getPreferredSize() {
446 Dimension dim = super.getPreferredSize();
447 dim.width = getFontMetrics(getFont()).stringWidth(getText()) + 10;
452 * I made this method public in order to be able to call it from the outside.
453 * This is needed for delegating calls.
455 public void processKeyEvent(KeyEvent e) {
456 int i = e.getKeyCode();
457 if (i == KeyEvent.VK_BACK_SPACE && getDocument().getLength() == 0) {
462 i == KeyEvent.VK_ENTER ||
463 i == KeyEvent.VK_ESCAPE ||
464 i == KeyEvent.VK_PAGE_UP ||
465 i == KeyEvent.VK_PAGE_DOWN ||
466 i == KeyEvent.VK_LEFT ||
467 i == KeyEvent.VK_RIGHT
469 manageSearchPopup(null);
470 if (i == KeyEvent.VK_ESCAPE) {
477 i == KeyEvent.VK_HOME ||
478 i == KeyEvent.VK_END ||
479 i == KeyEvent.VK_UP ||
480 i == KeyEvent.VK_DOWN
486 super.processKeyEvent(e);
487 if (i == KeyEvent.VK_BACK_SPACE) {
493 private void manageSearchPopup(SearchPopup searchPopup) {
494 final Project project;
495 if (ApplicationManager.getApplication() != null && !ApplicationManager.getApplication().isDisposed()) {
496 project = PlatformDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(myComponent));
502 if (mySearchPopup != null) {
503 myPopupLayeredPane.remove(mySearchPopup);
504 myPopupLayeredPane.validate();
505 myPopupLayeredPane.repaint();
506 myPopupLayeredPane = null;
508 if (project != null) {
509 ((ToolWindowManagerEx)ToolWindowManager.getInstance(project)).removeToolWindowManagerListener(myWindowManagerListener);
512 else if (searchPopup != null) {
513 FeatureUsageTracker.getInstance().triggerFeatureUsed("ui.tree.speedsearch");
516 if (!myComponent.isShowing()) {
517 mySearchPopup = null;
520 mySearchPopup = searchPopup;
525 if (mySearchPopup == null || !myComponent.isDisplayable()) return;
527 if (project != null) {
528 ((ToolWindowManagerEx)ToolWindowManager.getInstance(project)).addToolWindowManagerListener(myWindowManagerListener);
530 JRootPane rootPane = myComponent.getRootPane();
531 if (rootPane != null) {
532 myPopupLayeredPane = rootPane.getLayeredPane();
535 myPopupLayeredPane = null;
537 if (myPopupLayeredPane == null) {
538 LOG.error(toString() + " in " + String.valueOf(myComponent));
541 myPopupLayeredPane.add(mySearchPopup, JLayeredPane.POPUP_LAYER);
542 if (myPopupLayeredPane == null) return; // See # 27482. Somewho it does happen...
543 Point lPaneP = myPopupLayeredPane.getLocationOnScreen();
544 Point componentP = myComponent.getLocationOnScreen();
545 Rectangle r = myComponent.getVisibleRect();
546 Dimension prefSize = mySearchPopup.getPreferredSize();
547 Window window = (Window)SwingUtilities.getAncestorOfClass(Window.class, myComponent);
549 if (window instanceof JDialog) {
550 windowP = ((JDialog)window).getContentPane().getLocationOnScreen();
552 else if (window instanceof JFrame) {
553 windowP = ((JFrame)window).getContentPane().getLocationOnScreen();
556 windowP = window.getLocationOnScreen();
558 int y = r.y + componentP.y - lPaneP.y - prefSize.height;
559 y = Math.max(y, windowP.y - lPaneP.y);
560 mySearchPopup.setLocation(componentP.x - lPaneP.x + r.x, y);
561 mySearchPopup.setSize(prefSize);
562 mySearchPopup.setVisible(true);
563 mySearchPopup.validate();
566 private class MyToolWindowManagerListener extends ToolWindowManagerAdapter {
567 public void stateChanged() {
568 manageSearchPopup(null);
572 protected class ViewIterator implements ListIterator {
573 private SpeedSearchBase mySpeedSearch;
574 private int myCurrentIndex;
575 private Object[] myElements;
577 public ViewIterator(@NotNull final SpeedSearchBase speedSearch, final int startIndex) {
578 mySpeedSearch = speedSearch;
579 myCurrentIndex = startIndex;
580 myElements = speedSearch.getAllElements();
582 if (startIndex < 0 || startIndex > myElements.length) {
583 throw new IndexOutOfBoundsException("Index: " + startIndex);
588 public boolean hasPrevious() {
589 return myCurrentIndex != 0;
593 public Object previous() {
594 final int i = myCurrentIndex - 1;
595 if (i < 0) throw new NoSuchElementException();
596 final Object previous = myElements[mySpeedSearch.convertIndexToModel(i)];
602 public int nextIndex() {
603 return myCurrentIndex;
607 public int previousIndex() {
608 return myCurrentIndex - 1;
612 public boolean hasNext() {
613 return myCurrentIndex != myElements.length;
617 public Object next() {
618 if (myCurrentIndex + 1 > myElements.length) throw new NoSuchElementException();
619 return myElements[mySpeedSearch.convertIndexToModel(myCurrentIndex++)];
623 public void remove() {
624 throw new UnsupportedOperationException("Not implemented in: " + getClass().getCanonicalName());
628 public void set(Object o) {
629 throw new UnsupportedOperationException("Not implemented in: " + getClass().getCanonicalName());
633 public void add(Object o) {
634 throw new UnsupportedOperationException("Not implemented in: " + getClass().getCanonicalName());