changedUpdate exception
[fedora-idea.git] / platform / platform-impl / src / com / intellij / ui / SpeedSearchBase.java
blob205bb1bf6da8106685a591376d23920f6da1f868
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.
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;
33 import javax.swing.*;
34 import javax.swing.text.AttributeSet;
35 import javax.swing.text.BadLocationException;
36 import javax.swing.text.PlainDocument;
37 import java.awt.*;
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);
68 });
69 myComponent.addKeyListener(new KeyAdapter() {
70 public void keyTyped(KeyEvent e) {
71 processKeyEvent(e);
74 public void keyPressed(KeyEvent e) {
75 processKeyEvent(e);
77 });
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() {
119 return myComparator;
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() {
132 this(true);
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();
146 else {
147 myRecentSearchText = pattern;
148 @NonNls final StringBuilder buf = StringBuilderSpinAllocator.alloc();
150 try {
151 translatePattern(buf, pattern);
153 try {
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;
162 finally {
163 StringBuilderSpinAllocator.dispose(buf);
166 return false;
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) {
179 if (ch == '*' ) {
180 buf.append("(\\w|:)"); // ':' for xml tags
182 else if ("{}[].+^$()?".indexOf(ch) != -1) {
183 // do not bother with other metachars
184 buf.append('\\');
186 if (Character.isUpperCase(ch)) {
187 // for camel humps
188 buf.append("[a-z]*");
190 buf.append(ch);
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) {
223 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;
233 return null;
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;
242 return null;
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;
252 return null;
255 private void processKeyEvent(KeyEvent e) {
256 if (e.isAltDown()) return;
257 if (mySearchPopup != null) {
258 mySearchPopup.processKeyEvent(e);
259 return;
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)));
268 e.consume();
274 public Comp getComponent() {
275 return myComponent;
278 protected boolean isSpeedSearchEnabled() {
279 return true;
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 {
306 String oldText;
307 try {
308 oldText = getText(0, getLength());
310 catch (BadLocationException e1) {
311 oldText = "";
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);
319 else {
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();
340 Object element;
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);
353 else {
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);
369 else {
370 mySearchField.setForeground(Color.red);
372 if (mySearchPopup != null) {
373 mySearchPopup.setSize(mySearchPopup.getPreferredSize());
374 mySearchPopup.validate();
377 fireStateChanged();
381 private class SearchField extends JTextField {
382 SearchField() {
383 setFocusable(false);
386 public Dimension getPreferredSize() {
387 Dimension dim = super.getPreferredSize();
388 dim.width = getFontMetrics(getFont()).stringWidth(getText()) + 10;
389 return dim;
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) {
399 e.consume();
400 return;
402 if (
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) {
412 e.consume();
414 return;
416 super.processKeyEvent(e);
417 if (
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
424 e.consume();
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));
434 else {
435 project = null;
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;
455 else {
456 mySearchPopup = searchPopup;
459 fireStateChanged();
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();
470 else {
471 myPopupLayeredPane = null;
473 if (myPopupLayeredPane == null) {
474 LOG.error(toString() + " in " + String.valueOf(myComponent));
475 return;
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);
484 Point windowP;
485 if (window instanceof JDialog) {
486 windowP = ((JDialog)window).getContentPane().getLocationOnScreen();
488 else if (window instanceof JFrame) {
489 windowP = ((JFrame)window).getContentPane().getLocationOnScreen();
491 else {
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);