| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.ui; |
| |
| import com.intellij.featureStatistics.FeatureUsageTracker; |
| import com.intellij.ide.DataManager; |
| import com.intellij.ide.ui.UISettings; |
| import com.intellij.openapi.actionSystem.AnAction; |
| import com.intellij.openapi.actionSystem.AnActionEvent; |
| import com.intellij.openapi.actionSystem.CommonDataKeys; |
| import com.intellij.openapi.actionSystem.CustomShortcutSet; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.SystemInfo; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.wm.ToolWindowManager; |
| import com.intellij.openapi.wm.ex.ToolWindowManagerAdapter; |
| import com.intellij.openapi.wm.ex.ToolWindowManagerEx; |
| import com.intellij.openapi.wm.ex.ToolWindowManagerListener; |
| import com.intellij.psi.codeStyle.NameUtil; |
| import com.intellij.ui.speedSearch.SpeedSearchSupply; |
| import com.intellij.util.ui.UIUtil; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import javax.swing.text.AttributeSet; |
| import javax.swing.text.BadLocationException; |
| import javax.swing.text.PlainDocument; |
| import java.awt.*; |
| import java.awt.event.FocusAdapter; |
| import java.awt.event.FocusEvent; |
| import java.awt.event.KeyAdapter; |
| import java.awt.event.KeyEvent; |
| import java.beans.PropertyChangeListener; |
| import java.beans.PropertyChangeSupport; |
| import java.util.ListIterator; |
| import java.util.NoSuchElementException; |
| |
| public abstract class SpeedSearchBase<Comp extends JComponent> extends SpeedSearchSupply { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.ui.SpeedSearchBase"); |
| private SearchPopup mySearchPopup; |
| private JLayeredPane myPopupLayeredPane; |
| protected final Comp myComponent; |
| private final ToolWindowManagerListener myWindowManagerListener = new MyToolWindowManagerListener(); |
| private final PropertyChangeSupport myChangeSupport = new PropertyChangeSupport(this); |
| private String myRecentEnteredPrefix; |
| private SpeedSearchComparator myComparator = new SpeedSearchComparator(false); |
| private boolean myClearSearchOnNavigateNoMatch = false; |
| |
| @NonNls protected static final String ENTERED_PREFIX_PROPERTY_NAME = "enteredPrefix"; |
| |
| public SpeedSearchBase(Comp component) { |
| myComponent = component; |
| |
| myComponent.addFocusListener(new FocusAdapter() { |
| @Override |
| public void focusLost(FocusEvent e) { |
| manageSearchPopup(null); |
| } |
| }); |
| myComponent.addKeyListener(new KeyAdapter() { |
| @Override |
| public void keyTyped(KeyEvent e) { |
| processKeyEvent(e); |
| } |
| |
| @Override |
| public void keyPressed(KeyEvent e) { |
| processKeyEvent(e); |
| } |
| }); |
| |
| new AnAction() { |
| @Override |
| public void actionPerformed(AnActionEvent e) { |
| final String prefix = getEnteredPrefix(); |
| assert prefix != null; |
| final String[] strings = NameUtil.splitNameIntoWords(prefix); |
| final String last = strings[strings.length - 1]; |
| final int i = prefix.lastIndexOf(last); |
| mySearchPopup.mySearchField.setText(prefix.substring(0, i).trim()); |
| } |
| |
| @Override |
| public void update(AnActionEvent e) { |
| e.getPresentation().setEnabled(isPopupActive() && !StringUtil.isEmpty(getEnteredPrefix())); |
| } |
| }.registerCustomShortcutSet(CustomShortcutSet.fromString(SystemInfo.isMac ? "meta BACK_SPACE" : "control BACK_SPACE"), myComponent); |
| |
| installSupplyTo(component); |
| } |
| |
| public static boolean hasActiveSpeedSearch(JComponent component) { |
| return getSupply(component) != null; |
| } |
| |
| public void setClearSearchOnNavigateNoMatch(boolean clearSearchOnNavigateNoMatch) { |
| myClearSearchOnNavigateNoMatch = clearSearchOnNavigateNoMatch; |
| } |
| |
| @Override |
| public boolean isPopupActive() { |
| return mySearchPopup != null && mySearchPopup.isVisible(); |
| } |
| |
| |
| @Override |
| public Iterable<TextRange> matchingFragments(@NotNull String text) { |
| if (!isPopupActive()) return null; |
| final SpeedSearchComparator comparator = getComparator(); |
| final String recentSearchText = comparator.getRecentSearchText(); |
| return StringUtil.isNotEmpty(recentSearchText) ? comparator.matchingFragments(recentSearchText, text) : null; |
| } |
| |
| /** |
| * Returns visual (view) selection index. |
| */ |
| protected abstract int getSelectedIndex(); |
| |
| protected abstract Object[] getAllElements(); |
| |
| @Nullable |
| protected abstract String getElementText(Object element); |
| |
| protected int getElementCount() { |
| return getAllElements().length; |
| } |
| |
| /** |
| * Should convert given view index to model index |
| */ |
| protected int convertIndexToModel(final int viewIndex) { |
| return viewIndex; |
| } |
| |
| /** |
| * @param element Element to select. Don't forget to convert model index to view index if needed (i.e. table.convertRowIndexToView(modelIndex), etc). |
| * @param selectedText search text |
| */ |
| protected abstract void selectElement(Object element, String selectedText); |
| |
| protected ListIterator<Object> getElementIterator(int startingIndex) { |
| return new ViewIterator(this, startingIndex < 0 ? getElementCount() : startingIndex); |
| } |
| |
| public void addChangeListener(@NotNull PropertyChangeListener listener) { |
| myChangeSupport.addPropertyChangeListener(listener); |
| } |
| |
| public void removeChangeListener(@NotNull PropertyChangeListener listener) { |
| myChangeSupport.removePropertyChangeListener(listener); |
| } |
| |
| private void fireStateChanged() { |
| String enteredPrefix = getEnteredPrefix(); |
| myChangeSupport.firePropertyChange(ENTERED_PREFIX_PROPERTY_NAME, myRecentEnteredPrefix, enteredPrefix); |
| myRecentEnteredPrefix = enteredPrefix; |
| } |
| |
| protected boolean isMatchingElement(Object element, String pattern) { |
| String str = getElementText(element); |
| return str != null && compare(str, pattern); |
| } |
| |
| protected boolean compare(String text, String pattern) { |
| return myComparator.matchingFragments(pattern, text) != null; |
| } |
| |
| public SpeedSearchComparator getComparator() { |
| return myComparator; |
| } |
| |
| public void setComparator(final SpeedSearchComparator comparator) { |
| myComparator = comparator; |
| } |
| |
| @Nullable |
| private Object findNextElement(String s) { |
| final String _s = s.trim(); |
| final int selectedIndex = getSelectedIndex(); |
| final ListIterator<?> it = getElementIterator(selectedIndex + 1); |
| final Object current; |
| if (it.hasPrevious()) { |
| current = it.previous(); |
| it.next(); |
| } |
| else current = null; |
| while (it.hasNext()) { |
| final Object element = it.next(); |
| if (isMatchingElement(element, _s)) return element; |
| } |
| |
| if (UISettings.getInstance().CYCLE_SCROLLING) { |
| final ListIterator<Object> i = getElementIterator(0); |
| while (i.hasNext()) { |
| final Object element = i.next(); |
| if (isMatchingElement(element, _s)) return element; |
| } |
| } |
| |
| return ( current != null && isMatchingElement(current, _s) ) ? current : null; |
| } |
| |
| @Nullable |
| private Object findPreviousElement(String s) { |
| final String _s = s.trim(); |
| final int selectedIndex = getSelectedIndex(); |
| if (selectedIndex < 0) return null; |
| final ListIterator<?> it = getElementIterator(selectedIndex); |
| final Object current; |
| if (it.hasNext()) { |
| current = it.next(); |
| it.previous(); |
| } |
| else current = null; |
| while (it.hasPrevious()) { |
| final Object element = it.previous(); |
| if (isMatchingElement(element, _s)) return element; |
| } |
| |
| if (UISettings.getInstance().CYCLE_SCROLLING) { |
| final ListIterator<Object> i = getElementIterator(getElementCount()); |
| while (i.hasPrevious()) { |
| final Object element = i.previous(); |
| if (isMatchingElement(element, _s)) return element; |
| } |
| } |
| |
| return selectedIndex != -1 && isMatchingElement(current, _s) ? current : null; |
| } |
| |
| @Nullable |
| protected Object findElement(String s) { |
| final String _s = s.trim(); |
| int selectedIndex = getSelectedIndex(); |
| if (selectedIndex < 0) { |
| selectedIndex = 0; |
| } |
| final ListIterator<Object> it = getElementIterator(selectedIndex); |
| while (it.hasNext()) { |
| final Object element = it.next(); |
| if (isMatchingElement(element, _s)) return element; |
| } |
| if (selectedIndex > 0) { |
| while (it.hasPrevious()) it.previous(); |
| while (it.hasNext() && it.nextIndex() != selectedIndex) { |
| final Object element = it.next(); |
| if (isMatchingElement(element, _s)) return element; |
| } |
| } |
| return null; |
| } |
| |
| @Nullable |
| private Object findFirstElement(String s) { |
| final String _s = s.trim(); |
| for (ListIterator<?> it = getElementIterator(0); it.hasNext();) { |
| final Object element = it.next(); |
| if (isMatchingElement(element, _s)) return element; |
| } |
| return null; |
| } |
| |
| @Nullable |
| private Object findLastElement(String s) { |
| final String _s = s.trim(); |
| for (ListIterator<?> it = getElementIterator(-1); it.hasPrevious();) { |
| final Object element = it.previous(); |
| if (isMatchingElement(element, _s)) return element; |
| } |
| return null; |
| } |
| |
| public void showPopup() { |
| manageSearchPopup(new SearchPopup("")); |
| } |
| |
| public void hidePopup() { |
| manageSearchPopup(null); |
| } |
| |
| protected void processKeyEvent(KeyEvent e) { |
| if (e.isAltDown()) return; |
| if (e.isShiftDown() && isNavigationKey(e.getKeyCode())) return; |
| if (mySearchPopup != null) { |
| mySearchPopup.processKeyEvent(e); |
| return; |
| } |
| if (!isSpeedSearchEnabled()) return; |
| if (e.getID() == KeyEvent.KEY_TYPED) { |
| if (!UIUtil.isReallyTypedEvent(e)) return; |
| |
| char c = e.getKeyChar(); |
| if (Character.isLetterOrDigit(c) || c == '_' || c == '*' || c == '/' || c == ':' || c == '.' || c == '#') { |
| manageSearchPopup(new SearchPopup(String.valueOf(c))); |
| e.consume(); |
| } |
| } |
| } |
| |
| |
| public Comp getComponent() { |
| return myComponent; |
| } |
| |
| protected boolean isSpeedSearchEnabled() { |
| return true; |
| } |
| |
| @Override |
| @Nullable |
| public String getEnteredPrefix() { |
| return mySearchPopup != null ? mySearchPopup.mySearchField.getText() : null; |
| } |
| |
| @Override |
| public void refreshSelection() { |
| if ( mySearchPopup != null ) mySearchPopup.refreshSelection(); |
| } |
| |
| private class SearchPopup extends JPanel { |
| private final SearchField mySearchField; |
| |
| public SearchPopup(String initialString) { |
| final Color foregroundColor = UIUtil.getToolTipForeground(); |
| Color color1 = new JBColor(UIUtil.getToolTipBackground().brighter(), Gray._111); |
| mySearchField = new SearchField(); |
| final JLabel searchLabel = new JLabel(" " + UIBundle.message("search.popup.search.for.label") + " "); |
| searchLabel.setFont(searchLabel.getFont().deriveFont(Font.BOLD)); |
| searchLabel.setForeground(foregroundColor); |
| mySearchField.setBorder(null); |
| mySearchField.setBackground(color1); |
| mySearchField.setForeground(foregroundColor); |
| |
| mySearchField.setDocument(new PlainDocument() { |
| @Override |
| public void insertString(int offs, String str, AttributeSet a) throws BadLocationException { |
| String oldText; |
| try { |
| oldText = getText(0, getLength()); |
| } |
| catch (BadLocationException e1) { |
| oldText = ""; |
| } |
| |
| String newText = oldText.substring(0, offs) + str + oldText.substring(offs); |
| super.insertString(offs, str, a); |
| if (findElement(newText) == null) { |
| mySearchField.setForeground(JBColor.RED); |
| } |
| else { |
| mySearchField.setForeground(foregroundColor); |
| } |
| } |
| }); |
| mySearchField.setText(initialString); |
| |
| setBorder(BorderFactory.createLineBorder(Color.gray, 1)); |
| setBackground(color1); |
| setLayout(new BorderLayout()); |
| add(searchLabel, BorderLayout.WEST); |
| add(mySearchField, BorderLayout.EAST); |
| Object element = findElement(mySearchField.getText()); |
| onSearchFieldUpdated(initialString); |
| updateSelection(element); |
| } |
| |
| @Override |
| public void processKeyEvent(KeyEvent e) { |
| mySearchField.processKeyEvent(e); |
| if (e.isConsumed()) { |
| String s = mySearchField.getText(); |
| onSearchFieldUpdated(s); |
| int keyCode = e.getKeyCode(); |
| Object element; |
| if (isUpDownHomeEnd(keyCode)) { |
| element = findTargetElement(keyCode, s); |
| if (myClearSearchOnNavigateNoMatch && element == null) { |
| manageSearchPopup(null); |
| element = findTargetElement(keyCode, ""); |
| } |
| } |
| else { |
| element = findElement(s); |
| } |
| updateSelection(element); |
| } |
| } |
| |
| @Nullable |
| private Object findTargetElement(int keyCode, String searchPrefix) { |
| if (keyCode == KeyEvent.VK_UP) { |
| return findPreviousElement(searchPrefix); |
| } |
| else if (keyCode == KeyEvent.VK_DOWN) { |
| return findNextElement(searchPrefix); |
| } |
| else if (keyCode == KeyEvent.VK_HOME) { |
| return findFirstElement(searchPrefix); |
| } |
| else { |
| assert keyCode == KeyEvent.VK_END; |
| return findLastElement(searchPrefix); |
| } |
| } |
| |
| public void refreshSelection () { |
| updateSelection(findElement(mySearchField.getText())); |
| } |
| |
| private void updateSelection(Object element) { |
| if (element != null) { |
| selectElement(element, mySearchField.getText()); |
| mySearchField.setForeground(UIUtil.getLabelForeground()); |
| } |
| else { |
| mySearchField.setForeground(JBColor.red); |
| } |
| if (mySearchPopup != null) { |
| mySearchPopup.setSize(mySearchPopup.getPreferredSize()); |
| mySearchPopup.validate(); |
| } |
| |
| fireStateChanged(); |
| } |
| } |
| |
| protected void onSearchFieldUpdated(String pattern) { |
| } |
| |
| private class SearchField extends JTextField { |
| SearchField() { |
| setFocusable(false); |
| } |
| |
| @Override |
| public Dimension getPreferredSize() { |
| Dimension dim = super.getPreferredSize(); |
| dim.width = getFontMetrics(getFont()).stringWidth(getText()) + 10; |
| return dim; |
| } |
| |
| /** |
| * I made this method public in order to be able to call it from the outside. |
| * This is needed for delegating calls. |
| */ |
| @Override |
| public void processKeyEvent(KeyEvent e) { |
| int i = e.getKeyCode(); |
| if (i == KeyEvent.VK_BACK_SPACE && getDocument().getLength() == 0) { |
| e.consume(); |
| return; |
| } |
| if ( |
| i == KeyEvent.VK_ENTER || |
| i == KeyEvent.VK_ESCAPE || |
| i == KeyEvent.VK_PAGE_UP || |
| i == KeyEvent.VK_PAGE_DOWN || |
| i == KeyEvent.VK_LEFT || |
| i == KeyEvent.VK_RIGHT |
| ) { |
| manageSearchPopup(null); |
| if (i == KeyEvent.VK_ESCAPE) { |
| e.consume(); |
| } |
| return; |
| } |
| |
| if (isUpDownHomeEnd(i)) { |
| e.consume(); |
| return; |
| } |
| |
| super.processKeyEvent(e); |
| if (i == KeyEvent.VK_BACK_SPACE) { |
| e.consume(); |
| } |
| } |
| |
| } |
| |
| private static boolean isUpDownHomeEnd(int keyCode) { |
| return keyCode == KeyEvent.VK_HOME || keyCode == KeyEvent.VK_END || keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_DOWN; |
| } |
| |
| private static boolean isPgUpPgDown(int keyCode) { |
| return keyCode == KeyEvent.VK_PAGE_UP || keyCode == KeyEvent.VK_PAGE_DOWN; |
| } |
| |
| private static boolean isNavigationKey(int keyCode) { |
| return isPgUpPgDown(keyCode) || isUpDownHomeEnd(keyCode); |
| } |
| |
| |
| private void manageSearchPopup(@Nullable SearchPopup searchPopup) { |
| Project project = null; |
| if (ApplicationManager.getApplication() != null && !ApplicationManager.getApplication().isDisposed()) { |
| project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(myComponent)); |
| } |
| if (project != null && project.isDefault()) { |
| project = null; |
| } |
| if (mySearchPopup != null) { |
| myPopupLayeredPane.remove(mySearchPopup); |
| myPopupLayeredPane.validate(); |
| myPopupLayeredPane.repaint(); |
| myPopupLayeredPane = null; |
| |
| if (project != null) { |
| ((ToolWindowManagerEx)ToolWindowManager.getInstance(project)).removeToolWindowManagerListener(myWindowManagerListener); |
| } |
| } |
| else if (searchPopup != null) { |
| FeatureUsageTracker.getInstance().triggerFeatureUsed("ui.tree.speedsearch"); |
| } |
| |
| if (!myComponent.isShowing()) { |
| mySearchPopup = null; |
| } |
| else { |
| mySearchPopup = searchPopup; |
| } |
| |
| fireStateChanged(); |
| |
| if (mySearchPopup == null || !myComponent.isDisplayable()) return; |
| |
| if (project != null) { |
| ((ToolWindowManagerEx)ToolWindowManager.getInstance(project)).addToolWindowManagerListener(myWindowManagerListener); |
| } |
| JRootPane rootPane = myComponent.getRootPane(); |
| if (rootPane != null) { |
| myPopupLayeredPane = rootPane.getLayeredPane(); |
| } |
| else { |
| myPopupLayeredPane = null; |
| } |
| if (myPopupLayeredPane == null) { |
| LOG.error(toString() + " in " + myComponent); |
| return; |
| } |
| myPopupLayeredPane.add(mySearchPopup, JLayeredPane.POPUP_LAYER); |
| if (myPopupLayeredPane == null) return; // See # 27482. Somewho it does happen... |
| Point lPaneP = myPopupLayeredPane.getLocationOnScreen(); |
| Point componentP = getComponentLocationOnScreen(); |
| Rectangle r = getComponentVisibleRect(); |
| Dimension prefSize = mySearchPopup.getPreferredSize(); |
| Window window = (Window)SwingUtilities.getAncestorOfClass(Window.class, myComponent); |
| Point windowP; |
| if (window instanceof JDialog) { |
| windowP = ((JDialog)window).getContentPane().getLocationOnScreen(); |
| } |
| else if (window instanceof JFrame) { |
| windowP = ((JFrame)window).getContentPane().getLocationOnScreen(); |
| } |
| else { |
| windowP = window.getLocationOnScreen(); |
| } |
| int y = r.y + componentP.y - lPaneP.y - prefSize.height; |
| y = Math.max(y, windowP.y - lPaneP.y); |
| mySearchPopup.setLocation(componentP.x - lPaneP.x + r.x, y); |
| mySearchPopup.setSize(prefSize); |
| mySearchPopup.setVisible(true); |
| mySearchPopup.validate(); |
| } |
| |
| protected Rectangle getComponentVisibleRect() { |
| return myComponent.getVisibleRect(); |
| } |
| |
| protected Point getComponentLocationOnScreen() { |
| return myComponent.getLocationOnScreen(); |
| } |
| |
| private class MyToolWindowManagerListener extends ToolWindowManagerAdapter { |
| @Override |
| public void stateChanged() { |
| manageSearchPopup(null); |
| } |
| } |
| |
| protected class ViewIterator implements ListIterator<Object> { |
| private final SpeedSearchBase mySpeedSearch; |
| private int myCurrentIndex; |
| private final Object[] myElements; |
| |
| public ViewIterator(@NotNull final SpeedSearchBase speedSearch, final int startIndex) { |
| mySpeedSearch = speedSearch; |
| myCurrentIndex = startIndex; |
| myElements = speedSearch.getAllElements(); |
| |
| if (startIndex < 0 || startIndex > myElements.length) { |
| throw new IndexOutOfBoundsException("Index: " + startIndex + " in: " + SpeedSearchBase.this.getClass()); |
| } |
| } |
| |
| @Override |
| public boolean hasPrevious() { |
| return myCurrentIndex != 0; |
| } |
| |
| @Override |
| public Object previous() { |
| final int i = myCurrentIndex - 1; |
| if (i < 0) throw new NoSuchElementException(); |
| final Object previous = myElements[mySpeedSearch.convertIndexToModel(i)]; |
| myCurrentIndex = i; |
| return previous; |
| } |
| |
| @Override |
| public int nextIndex() { |
| return myCurrentIndex; |
| } |
| |
| @Override |
| public int previousIndex() { |
| return myCurrentIndex - 1; |
| } |
| |
| @Override |
| public boolean hasNext() { |
| return myCurrentIndex != myElements.length; |
| } |
| |
| @Override |
| public Object next() { |
| if (myCurrentIndex + 1 > myElements.length) throw new NoSuchElementException(); |
| return myElements[mySpeedSearch.convertIndexToModel(myCurrentIndex++)]; |
| } |
| |
| @Override |
| public void remove() { |
| throw new UnsupportedOperationException("Not implemented in: " + getClass().getCanonicalName()); |
| } |
| |
| @Override |
| public void set(Object o) { |
| throw new UnsupportedOperationException("Not implemented in: " + getClass().getCanonicalName()); |
| } |
| |
| @Override |
| public void add(Object o) { |
| throw new UnsupportedOperationException("Not implemented in: " + getClass().getCanonicalName()); |
| } |
| } |
| } |