/*
 * @(#)${NAME}
 *
 * Copyright 2002 - 2004 JIDE Software Inc. All rights reserved.
 */
package com.jidesoft.swing;

import com.jidesoft.plaf.UIDefaultsLookup;
import com.jidesoft.popup.JidePopup;
import com.jidesoft.swing.event.SearchableEvent;
import com.jidesoft.swing.event.SearchableListener;
import com.jidesoft.utils.DefaultWildcardSupport;
import com.jidesoft.utils.WildcardSupport;

import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
 * JList, JTable and JTree are three data-rich components. They can be used to display a huge amount of data so
 * searching function will be very a useful feature in those components. <code>Searchable</code> is such a class that
 * can make JList, JTable and JTree searchable. User can simply type in any string they want to search for and use arrow
 * keys to navigate to next or previous occurrence.
 * <p/>
 * <code>Searchable</code> is a base abstract class. <code>ListSearchable</code>, <code>TableSearchable</code> and
 * <code>TreeSearchable</code> are implementations to make JList, JTable and JTree searchable respectively. For each
 * implementation, there are five methods need to be implemented. <ul> <li><code>protected abstract int
 * getSelectedIndex()</code> <li><code>protected abstract void setSelectedIndex(int index, boolean incremental)</code>
 * <li><code>protected abstract int getElementCount()</code> <li><code>protected abstract Object getElementAt(int
 * index)</code> <li><code>protected abstract String convertElementToString(Object element)</code> </ul>
 * <p/>
 * Please look at the javadoc of each method to learn more details.
 * <p/>
 * The keys used by this class are fully customizable. Subclass can override the methods such as {@link
 * #isActivateKey(java.awt.event.KeyEvent)}, {@link #isDeactivateKey(java.awt.event.KeyEvent)}, {@link
 * #isFindFirstKey(java.awt.event.KeyEvent)},{@link #isFindLastKey(java.awt.event.KeyEvent)}, {@link
 * #isFindNextKey(java.awt.event.KeyEvent)}, {@link #isFindPreviousKey(java.awt.event.KeyEvent)} to provide its own set
 * of keys.
 * <p/>
 * In addition to press up/down arrow to find next occurrence or previous occurrence of particular string, there are
 * several other features that are very handy.
 * <p/>
 * Multiple selection feature - If you press CTRL key and hold it while pressing up and down arrow, it will find
 * next/previous occurrence while keeping existing selections. <br> Select all feature - If you type in a searching text
 * and press CTRL+A, all the occurrences of that searching string will be selected. This is a very handy feature. For
 * example you want to delete all rows in a table whose name column begins with "old". So you can type in "old" and
 * press CTRL+A, now all rows beginning with "old" will be selected. Pressing delete will delete all of them. <br> Basic
 * regular expression support - It allows '?' to match any letter or digit, or '*' to match several letters or digits.
 * Even though it's possible to implement full regular expression support, we don't want to do that. The reason is the
 * regular expression is very complex, it's probably not a good idea to let user type in such a complex expression in a
 * small popup window. However if your user is very familiar with regular expression, you can add the feature to
 * <code>Searchable</code>. All you need to do is to override {@link #compare(String, String)} method and implement by
 * yourself.
 * <p/>
 * As this is an abstract class, please refer to to javadoc of {@link ListSearchable},{@link TreeSearchable}, and {@link
 * TableSearchable} to find out how to use it with JList, JTree and JTable respectively.
 * <p/>
 * This component has a timer. If user types very fast, it will accumulate them together and generate only one searching
 * action. The timer can be controlled by {@link #setSearchingDelay(int)}.
 * <p/>
 * By default we will use lightweight popup for the sake of performance. But if you use heavyweight component which
 * could obscure the lightweight popup, you can call {@link #setHeavyweightComponentEnabled(boolean)} to true so that
 * heavyweight popup will be used.
 * <p/>
 * When a <code>Searchable</code> is installed on a component, component.getClientProperty(Searchable.CLIENT_PROPERTY_SEARCHABLE)
 * will give you the Searchable instance. You can use static method {@link #getSearchable(javax.swing.JComponent)} to
 * get it too.
 * <p/>
 * Last but not the least, only one Searchable is allowed on a component. If you install another one, it will remove the
 * first one and then install the new one.
 */
public abstract class Searchable {

    private final PropertyChangeSupport _propertyChangeSupport = new PropertyChangeSupport(this);

    protected final JComponent _component;

    private SearchPopup _popup;
    private JLayeredPane _layeredPane;

    private boolean _heavyweightComponentEnabled;

    /**
     * optional SearchableProvider
     */
    private SearchableProvider _searchableProvider;
    private Pattern _pattern;
    private String _searchText;
    private String _previousSearchText;

    private boolean _fromStart = true;
    private boolean _caseSensitive = false;
    private boolean _repeats = false;
    private boolean _wildcardEnabled = true;
    private boolean _countMatch;
    protected int _matchCount;
    private WildcardSupport _wildcardSupport = null;
    private Color _mismatchForeground;
    private Color _foreground = null;
    private Color _background = null;
    protected ComponentListener _componentListener;
    protected KeyListener _keyListener;
    protected FocusListener _focusListener;
    private SearchableListener _searchableListener;

    public static final String PROPERTY_SEARCH_TEXT = "searchText";

    private int _cursor = -1;

    private String _searchLabel = null;

    /**
     * The popup location
     */
    private int _popupLocation = SwingConstants.TOP;

    private int _searchingDelay = 0;
    private int _popupTimeout = 0;
    private Timer _popupTimer;

    private boolean _reverseOrder = false;

    /**
     * A list of event listeners for this component.
     */
    protected EventListenerList listenerList = new EventListenerList();

    private Component _popupLocationRelativeTo;

    /**
     * The client property for Searchable instance. When Searchable is installed on a component, this client property
     * has the Searchable.
     */
    public static final String CLIENT_PROPERTY_SEARCHABLE = "Searchable";

    private Set<Integer> _selection;

    private boolean _processModelChangeEvent = true;
    private boolean _hideSearchPopupOnEvent = true;

    /**
     * Creates a Searchable.
     *
     * @param component component where the Searchable will be installed.
     */
    public Searchable(JComponent component) {
        Searchable searchable = getSearchable(component);
        if (searchable != null) {
            SearchableUtils.uninstallSearchable(searchable);
        }
        _previousSearchText = null;
        _component = component;
        _selection = new HashSet<Integer>();
        installListeners();
        updateClientProperty(_component, this);
    }

    /**
     * Creates a Searchable.
     *
     * @param component          component where the Searchable will be installed.
     * @param searchableProvider the Searchable Provider.
     */
    public Searchable(JComponent component, SearchableProvider searchableProvider) {
        Searchable searchable = getSearchable(component);
        if (searchable != null) {
            SearchableUtils.uninstallSearchable(searchable);
        }
        _searchableProvider = searchableProvider;
        _previousSearchText = null;
        _component = component;
        _selection = new HashSet<Integer>();
        installListeners();
        updateClientProperty(_component, this);
    }

    /**
     * Gets the selected index in the component. The concrete implementation should call methods on the component to
     * retrieve the current selected index. If the component supports multiple selection, it's OK just return the index
     * of the first selection. <p>Here are some examples. In the case of JList, the index is the row index. In the case
     * of JTree, the index is the row index too. In the case of JTable, depending on the selection mode, the index could
     * be row index (in row selection mode), could be column index (in column selection mode) or could the cell index
     * (in cell selection mode).
     *
     * @return the selected index.
     */
    protected abstract int getSelectedIndex();

    /**
     * Sets the selected index. The concrete implementation should call methods on the component to select the element
     * at the specified index. The incremental flag is used to do multiple select. If the flag is true, the element at
     * the index should be added to current selection. If false, you should clear previous selection and then select the
     * element.
     *
     * @param index       the index to be selected
     * @param incremental a flag to enable multiple selection. If the flag is true, the element at the index should be
     *                    added to current selection. If false, you should clear previous selection and then select the
     *                    element.
     */
    protected abstract void setSelectedIndex(int index, boolean incremental);

    /**
     * Sets the selected index. The reason we have this method is just for back compatibility. All the method do is just
     * to invoke {@link #setSelectedIndex(int, boolean)}.
     * <p/>
     * Please do NOT try to override this method. Always override {@link #setSelectedIndex(int, boolean)} instead.
     *
     * @param index       the index to be selected
     * @param incremental a flag to enable multiple selection. If the flag is true, the element at the index should be
     *                    added to current selection. If false, you should clear previous selection and then select the
     *                    element.
     */
    public void adjustSelectedIndex(int index, boolean incremental) {
        setSelectedIndex(index, incremental);
    }

    /**
     * Gets the total element count in the component. Different concrete implementation could have different
     * interpretation of the count. This is totally OK as long as it's consistent in all the methods. For example, the
     * index parameter in other methods should be always a valid value within the total count.
     *
     * @return the total element count.
     */
    protected abstract int getElementCount();

    /**
     * Gets the element at the specified index. The element could be any data structure that internally used in the
     * component. The convertElementToString method will give you a chance to convert the element to string which is
     * used to compare with the string that user types in.
     *
     * @param index the index
     * @return the element at the specified index.
     */
    protected abstract Object getElementAt(int index);

    /**
     * Converts the element that returns from getElementAt() to string.
     *
     * @param element the element to be converted
     * @return the string representing the element in the component.
     */
    protected abstract String convertElementToString(Object element);

    /**
     * Converts the element to String.
     * <p/>
     * This method will invoke {@link #convertElementToString(Object)} only. This method is added to provide a public
     * method for ShrinkSearchSupport without breaking the existing code of the customers.
     *
     * @param element the element to be converted
     * @return the string representing the element in the component.
     * @since 3.4.5
     */
    public String convertToString(Object element) {
        return convertElementToString(element);
    }

    /**
     * Get the flag indicating if the search popup should be hidden on the component's event.
     * <p/>
     * By default, the value is true so that the search popup will be hidden anyway when the component get related
     * events. However, you could set this flag to false if you don't want to hide the search popup in some scenarios.
     * For example, JIDE ComboBoxShrinkSearchableSupport will set this flag to false temporarily when it tries to shrink
     * the list.
     *
     * @return true if the search popup is hidden on event. Otherwise false.
     */
    public boolean isHideSearchPopupOnEvent() {
        return _hideSearchPopupOnEvent;
    }

    /**
     * Set the flag indicating if the search popup should be hidden on the component's event.
     *
     * @param hideSearchPopupOnEvent the flag
     * @see #isHideSearchPopupOnEvent()
     */
    public void setHideSearchPopupOnEvent(boolean hideSearchPopupOnEvent) {
        _hideSearchPopupOnEvent = hideSearchPopupOnEvent;
    }

    /**
     * A text field for searching text.
     */
    protected class SearchField extends JTextField {
        SearchField() {
            JideSwingUtilities.setComponentTransparent(this);
        }

        @Override
        public Dimension getPreferredSize() {
            Dimension size = super.getPreferredSize();
            size.width = getFontMetrics(getFont()).stringWidth(getText()) + 4;
            return size;
        }

        @Override
        public void processKeyEvent(KeyEvent e) {
            int keyCode = e.getKeyCode();
            if (keyCode == KeyEvent.VK_BACK_SPACE && getDocument().getLength() == 0) {
                e.consume();
                return;
            }
            final boolean isNavigationKey = isNavigationKey(e);
            if (isDeactivateKey(e) && !isNavigationKey) {
                hidePopup();
                if (keyCode == KeyEvent.VK_ESCAPE)
                    e.consume();
                return;
            }
            super.processKeyEvent(e);
            if (keyCode == KeyEvent.VK_BACK_SPACE || isNavigationKey)
                e.consume();
            if (isSelectAllKey(e)) {
                e.consume();
            }
        }
    }

    /**
     * The popup panel for search label and search text field.
     */
    private class DefaultSearchPopup extends SearchPopup {
        private JLabel _label;
        private JLabel _noMatch;

        public DefaultSearchPopup(String text) {
            initComponents(text);
        }

        private void initComponents(String text) {
            final Color foreground = Searchable.this.getForeground();
            final Color background = Searchable.this.getBackground();

            // setup the label
            _label = new JLabel(getSearchLabel());
            _label.setForeground(foreground);
            _label.setVerticalAlignment(JLabel.BOTTOM);

            _noMatch = new JLabel();
            _noMatch.setForeground(getMismatchForeground());
            _noMatch.setVerticalAlignment(JLabel.BOTTOM);

            //setup text field
            _textField = new SearchField();
            _textField.setFocusable(false);
            _textField.setBorder(BorderFactory.createEmptyBorder());
            _textField.setForeground(foreground);
            _textField.setCursor(getCursor());
            _textField.getDocument().addDocumentListener(new DocumentListener() {
                private Timer timer = new Timer(200, new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        applyText();
                    }
                });

                public void insertUpdate(DocumentEvent e) {
                    startTimer();
                }

                public void removeUpdate(DocumentEvent e) {
                    startTimer();
                }

                public void changedUpdate(DocumentEvent e) {
                    startTimer();
                }

                protected void applyText() {
                    String text = _textField.getText();
                    firePropertyChangeEvent(text);
                    if (text.length() != 0) {
                        int found = findFromCursor(text);
                        if (found == -1) {
                            _textField.setForeground(getMismatchForeground());
                        }
                        else {
                            _textField.setForeground(foreground);
                        }
                        select(found, null, text);
                    }
                    else {
                        _textField.setForeground(foreground);
                        _noMatch.setText("");
                        updatePopupBounds();
                        hidePopup();
                    }
                }

                void startTimer() {
                    updatePopupBounds();
                    if (getSearchingDelay() > 0) {
                        timer.setInitialDelay(getSearchingDelay());
                        if (timer.isRunning()) {
                            timer.restart();
                        }
                        else {
                            timer.setRepeats(false);
                            timer.start();
                        }
                    }
                    else {
                        applyText();
                    }
                }
            });
            _textField.setText(text);

            setBackground(background);
            setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(UIDefaultsLookup.getColor("controlShadow"), 1),
                    BorderFactory.createEmptyBorder(0, 6, 1, 8)));
            setLayout(new BorderLayout(2, 0));
            Dimension size = _label.getPreferredSize();
            size.height = _textField.getPreferredSize().height;
            _label.setPreferredSize(size);
            add(_label, BorderLayout.BEFORE_LINE_BEGINS);
            add(_textField, BorderLayout.CENTER);
            add(_noMatch, BorderLayout.AFTER_LINE_ENDS);
            setPopupBorder(BorderFactory.createEmptyBorder());
        }

        @Override
        protected void select(int index, KeyEvent e, String searchingText) {
            if (index != -1) {
                boolean incremental = e != null && isIncrementalSelectKey(e);
                setSelectedIndex(index, incremental);
                Searchable.this.setCursor(index, incremental);
                _textField.setForeground(Searchable.this.getForeground());
                _noMatch.setText("");
            }
            else {
                _textField.setForeground(getMismatchForeground());
                _noMatch.setText(getResourceString("Searchable.noMatch"));
            }
            updatePopupBounds();
            if (index != -1) {
                Object element = getElementAt(index);
                fireSearchableEvent(new SearchableEvent(Searchable.this, SearchableEvent.SEARCHABLE_MATCH, searchingText, element, convertElementToString(element)));
            }
            else {
                fireSearchableEvent(new SearchableEvent(Searchable.this, SearchableEvent.SEARCHABLE_NOMATCH, searchingText));
            }
        }

        private void updatePopupBounds() {
            if (_popup != null) {
                _textField.invalidate();
                try {
                    if (!isHeavyweightComponentEnabled()) {
                        Dimension size = _noMatch.getPreferredSize();
                        size.width += _label.getPreferredSize().width;
                        size.width += new JLabel(_textField.getText()).getPreferredSize().width + 24;
                        size.height = _popup.getSize().height;
                        _popup.setSize(size);
                        _popup.validate();
                    }
                    else {
                        _popup.packPopup();
                    }
                }
                catch (Exception e) { // catch any potential exception
                    // see bug report at http://www.jidesoft.com/forum/viewtopic.php?p=8557#8557
                }
            }
        }
    }

    /**
     * Hides the popup.
     */
    public void hidePopup() {
        if (_popup != null) {
            if (isHeavyweightComponentEnabled()) {
                _popup.hidePopupImmediately();
            }
            else {
                if (_layeredPane != null) {
                    _layeredPane.remove(_popup);
                    _layeredPane.validate();
                    _layeredPane.repaint();
                    _layeredPane = null;
                }
            }
            _popup = null;
            _searchableProvider = null;
            _previousSearchText = null;
            fireSearchableEvent(new SearchableEvent(Searchable.this, SearchableEvent.SEARCHABLE_END, "", getCurrentIndex(), _previousSearchText));
        }
        setCursor(-1);
    }

    public SearchableProvider getSearchableProvider() {
        return _searchableProvider;
    }

    public void setSearchableProvider(SearchableProvider searchableProvider) {
        _searchableProvider = searchableProvider;
    }

    /**
     * Installs necessary listeners to the component. This method will be called automatically when Searchable is
     * created.
     */
    public void installListeners() {
        if (_componentListener == null) {
            _componentListener = createComponentListener();
        }
        _component.addComponentListener(_componentListener);
        Component scrollPane = JideSwingUtilities.getScrollPane(_component);
        if (scrollPane != null) {
            scrollPane.addComponentListener(_componentListener);
        }

        if (_keyListener == null) {
            _keyListener = createKeyListener();
        }
        JideSwingUtilities.insertKeyListener(getComponent(), _keyListener, 0);

        if (_focusListener == null) {
            _focusListener = createFocusListener();
        }
        getComponent().addFocusListener(_focusListener);
        if (_searchableListener == null) {
            _searchableListener = new SearchableListener() {
                public void searchableEventFired(SearchableEvent e) {
                    if (e.getID() == SearchableEvent.SEARCHABLE_START) {
                        if (getPopupTimeout() > 0) {
                            _popupTimer = new Timer(getPopupTimeout(), new ActionListener() {
                                public void actionPerformed(ActionEvent e) {
                                    if (isPopupVisible()) {
                                        hidePopup();
                                    }
                                }
                            });
                            _popupTimer.setRepeats(false);
                            _popupTimer.start();
                        }
                    }
                    else if (_popupTimer != null) {
                        if (e.getID() == SearchableEvent.SEARCHABLE_END) {
                            _popupTimer.stop();
                        }
                        else {
                            _popupTimer.restart();
                        }
                    }
                }
            };
        }
        addSearchableListener(_searchableListener);
    }

    /**
     * Creates a component listener that updates the popup when component is hidden, moved or resized.
     *
     * @return a ComponentListener.
     */
    protected ComponentListener createComponentListener() {
        return new ComponentAdapter() {
            @Override
            public void componentHidden(ComponentEvent e) {
                super.componentHidden(e);
                boolean passive = _searchableProvider == null || _searchableProvider.isPassive();
                if (passive) {
                    hidePopup();
                }
            }

            @Override
            public void componentResized(ComponentEvent e) {
                super.componentResized(e);
                boolean passive = _searchableProvider == null || _searchableProvider.isPassive();
                if (passive) {
                    updateSizeAndLocation();
                }
            }

            @Override
            public void componentMoved(ComponentEvent e) {
                super.componentMoved(e);
                boolean passive = _searchableProvider == null || _searchableProvider.isPassive();
                if (passive) {
                    updateSizeAndLocation();
                }
            }
        };
    }

    /**
     * Creates the KeyListener and listen to key typed in the component.
     *
     * @return the KeyListener.
     */
    protected KeyListener createKeyListener() {
        return new KeyAdapter() {
            @Override
            public void keyTyped(KeyEvent e) {
                boolean passive = _searchableProvider == null || _searchableProvider.isPassive();
                if (passive) {
                    keyTypedOrPressed(e);
                }
            }

            @Override
            public void keyPressed(KeyEvent e) {
                boolean passive = _searchableProvider == null || _searchableProvider.isPassive();
                if (passive) {
                    keyTypedOrPressed(e);
                }
            }
        };
    }

    /**
     * Creates a FocusListener. We use it to hide the popup when the component loses focus.
     *
     * @return a FocusListener.
     */
    protected FocusListener createFocusListener() {
        return new FocusAdapter() {
            @Override
            public void focusLost(FocusEvent focusevent) {
                boolean passive = _searchableProvider == null || _searchableProvider.isPassive();
                if (passive) {
                    hidePopup();
                }
            }
        };
    }

    /**
     * Uninstall the listeners that installed before. This method is never called because we don't have the control of
     * the life cycle of the component. However you can call this method if you don't want the component to be
     * searchable any more.
     */
    public void uninstallListeners() {
        if (_componentListener != null) {
            getComponent().removeComponentListener(_componentListener);
            Component scrollPane = JideSwingUtilities.getScrollPane(getComponent());
            if (scrollPane != null) {
                scrollPane.removeComponentListener(_componentListener);
            }
            _componentListener = null;
        }

        if (_keyListener != null) {
            getComponent().removeKeyListener(_keyListener);
            _keyListener = null;
        }

        if (_focusListener != null) {
            getComponent().removeFocusListener(_focusListener);
            _focusListener = null;
        }

        if (_searchableListener != null) {
            removeSearchableListener(_searchableListener);
            _searchableListener = null;
        }
    }

    /**
     * Adds the property change listener. The only property change event that will be fired is the "searchText" property
     * which will be fired when user types in a different search text in the popup.
     *
     * @param propertychangelistener the listener
     */
    public void addPropertyChangeListener(PropertyChangeListener propertychangelistener) {
        _propertyChangeSupport.addPropertyChangeListener(propertychangelistener);
    }

    /**
     * Removes the property change listener.
     *
     * @param propertychangelistener the listener
     */
    public void removePropertyChangeListener(PropertyChangeListener propertychangelistener) {
        _propertyChangeSupport.removePropertyChangeListener(propertychangelistener);
    }

    public void firePropertyChangeEvent(String searchingText) {
        if (!searchingText.equals(_previousSearchText)) {
            _propertyChangeSupport.firePropertyChange(PROPERTY_SEARCH_TEXT, _previousSearchText, searchingText);
            fireSearchableEvent(new SearchableEvent(this, SearchableEvent.SEARCHABLE_CHANGE, searchingText, getCurrentIndex(), _previousSearchText));
            _previousSearchText = searchingText;
            if (searchingText.length() == 0) {
                searchingTextEmpty();
            }
        }
    }

    /**
     * Actions to take on searching text empty scenario
     */
    protected void searchingTextEmpty() {
        // do nothing
    }

    /**
     * Checks if the element matches the searching text.
     *
     * @param element       the element to be checked
     * @param searchingText the searching text
     * @return true if matches.
     */
    protected boolean compare(Object element, String searchingText) {
        String text = convertElementToString(element);
        return text != null && compare(isCaseSensitive() ? text : text.toLowerCase(), searchingText);
    }

    /**
     * Checks if the element string matches the searching text. Different from {@link #compare(Object, String)}, this
     * method is after the element has been converted to string using {@link #convertElementToString(Object)}.
     *
     * @param text          the text to be checked
     * @param searchingText the searching text
     * @return true if matches.
     */
    protected boolean compare(String text, String searchingText) {
        if (searchingText == null || searchingText.trim().length() == 0) {
            return true;
        }

        if (!isWildcardEnabled()) {
            return searchingText != null &&
                    (searchingText.equals(text) || searchingText.length() > 0 && (isFromStart() ? text.startsWith(searchingText) : text.indexOf(searchingText) != -1));
        }
        else {
            // use the previous pattern since nothing changed.
            if (_searchText != null && _searchText.equals(searchingText) && _pattern != null) {
                return _pattern.matcher(text).find();
            }

            WildcardSupport wildcardSupport = getWildcardSupport();
            String s = wildcardSupport.convert(searchingText);
            if (searchingText.equals(s)) {
                return isFromStart() ? text.startsWith(searchingText) : text.indexOf(searchingText) != -1;
            }
            _searchText = searchingText;

            try {
                _pattern = Pattern.compile(isFromStart() ? "^" + s : s, isCaseSensitive() ? 0 : Pattern.CASE_INSENSITIVE);
                return _pattern.matcher(text).find();
            }
            catch (PatternSyntaxException e) {
                return false;
            }
        }
    }


    /**
     * Gets the cursor which is the index of current location when searching. The value will be used in findNext and
     * findPrevious.
     *
     * @return the current position of the cursor.
     */
    public int getCursor() {
        return _cursor;
    }

    /**
     * Sets the cursor which is the index of current location when searching. The value will be used in findNext and
     * findPrevious.
     *
     * @param cursor the new position of the cursor.
     */
    public void setCursor(int cursor) {
        setCursor(cursor, false);
    }

    /**
     * Sets the cursor which is the index of current location when searching. The value will be used in findNext and
     * findPrevious. We will call this method automatically inside this class. However, if you ever call {@link
     * #setSelectedIndex(int, boolean)} method from your code, you should call this method with the same parameters.
     *
     * @param cursor      the new position of the cursor.
     * @param incremental a flag to enable multiple selection. If the flag is true, the element at the index should be
     *                    added to current selection. If false, you should clear previous selection and then select the
     *                    element.
     */
    public void setCursor(int cursor, boolean incremental) {
        if (!incremental || _cursor < 0) _selection.clear();
        if (_cursor >= 0) _selection.add(cursor);
        _cursor = cursor;
    }

    /**
     * Highlight all matching cases in the target.
     * <p/>
     * In default implementation, it will just search all texts in the target to highlight all. If you have a really
     * huge text to search, you may want to override this method to have a lazy behavior on visible areas only.
     */
    protected void highlightAll() {
        int firstIndex = -1;
        int index = getSelectedIndex();
        String text = getSearchingText();

        while (index != -1) {
            int newIndex = findNext(text);
            if (index == newIndex) {
                index = -1;
            }
            else {
                index = newIndex;
            }
            if (index != -1) {
                if (firstIndex == -1) {
                    firstIndex = index;
                }
                select(index, text);
            }
        }
        // now select the first one
        if (firstIndex != -1) {
            select(firstIndex, text);
        }
    }

    /**
     * Cancel highlight all.
     * <p/>
     * By default, it does nothing. However, if you want to override {@link #highlightAll()}, you may want to override
     * this method to notify your Searchable that the highlightAll button is to be released.
     */
    protected void cancelHighlightAll() {

    }

    /**
     * Select the index for the searching text.
     *
     * @param index         the start offset
     * @param searchingText the searching text presented in the searchable event to be fired here.
     */
    protected void select(int index, String searchingText) {
        if (index != -1) {
            setSelectedIndex(index, true);
            setCursor(index, true);
            Object element = getElementAt(index);
            fireSearchableEvent(new SearchableEvent(this, SearchableEvent.SEARCHABLE_MATCH, searchingText, element, convertElementToString(element)));
        }
        else {
            setSelectedIndex(-1, false);
            fireSearchableEvent(new SearchableEvent(this, SearchableEvent.SEARCHABLE_NOMATCH, searchingText));
        }
    }

    /**
     * Finds the next matching index from the cursor.
     *
     * @param s the searching text
     * @return the next index that the element matches the searching text.
     */
    public int findNext(String s) {
        String str = isCaseSensitive() ? s : s.toLowerCase();
        int count = getElementCount();
        if (count == 0)
            return s.length() > 0 ? -1 : 0;
        int selectedIndex = getCurrentIndex();
        for (int i = selectedIndex + 1; i < count; i++) {
            Object element = getElementAt(i);
            if (compare(element, str))
                return i;
        }

        if (isRepeats()) {
            for (int i = 0; i < selectedIndex; i++) {
                Object element = getElementAt(i);
                if (compare(element, str))
                    return i;
            }
        }

        return selectedIndex == -1 ? -1 : (compare(getElementAt(selectedIndex), str) ? selectedIndex : -1);
    }

    protected int getCurrentIndex() {
        if (_selection.contains(getSelectedIndex())) {
            return _cursor != -1 ? _cursor : getSelectedIndex();
        }
        else {
            _selection.clear();
            return getSelectedIndex();
        }
    }

    /**
     * Finds the previous matching index from the cursor.
     *
     * @param s the searching text
     * @return the previous index that the element matches the searching text.
     */
    public int findPrevious(String s) {
        String str = isCaseSensitive() ? s : s.toLowerCase();
        int count = getElementCount();
        if (count == 0)
            return s.length() > 0 ? -1 : 0;
        int selectedIndex = getCurrentIndex();
        for (int i = selectedIndex - 1; i >= 0; i--) {
            Object element = getElementAt(i);
            if (compare(element, str))
                return i;
        }

        if (isRepeats()) {
            for (int i = count - 1; i >= selectedIndex; i--) {
                Object element = getElementAt(i);
                if (compare(element, str))
                    return i;
            }
        }
        return selectedIndex == -1 ? -1 : (compare(getElementAt(selectedIndex), str) ? selectedIndex : -1);
    }

    /**
     * Finds the next matching index from the cursor. If it reaches the end, it will restart from the beginning. However
     * is the reverseOrder flag is true, it will finds the previous matching index from the cursor. If it reaches the
     * beginning, it will restart from the end.
     *
     * @param s the searching text
     * @return the next index that the element matches the searching text.
     */
    public int findFromCursor(String s) {
        if (isCountMatch()) {
            boolean reverse = isReverseOrder();
            setReverseOrder(false);
            int selectedIndex = getCurrentIndex();
            if (selectedIndex < 0) {
                selectedIndex = 0;
            }
            int oldIndex;
            int newIndex = -1;
            _matchCount = -1;
            do {
                setSelectedIndex(newIndex, false);
                oldIndex = newIndex;
                newIndex = findNext(s);
                _matchCount++;
            }
            while (newIndex > oldIndex);
            setSelectedIndex(selectedIndex, false);
            setReverseOrder(reverse);
        }

        if (isReverseOrder()) {
            return reverseFindFromCursor(s);
        }

        String str = isCaseSensitive() ? s : s.toLowerCase();
        int selectedIndex = getCurrentIndex();
        if (selectedIndex < 0)
            selectedIndex = 0;
        int count = getElementCount();
        if (count == 0)
            return -1; // no match

        // find from cursor
        for (int i = selectedIndex; i < count; i++) {
            Object element = getElementAt(i);
            if (compare(element, str))
                return i;
        }

        // if not found, start over from the beginning
        for (int i = 0; i < selectedIndex; i++) {
            Object element = getElementAt(i);
            if (compare(element, str))
                return i;
        }

        return -1;
    }

    /**
     * Finds the previous matching index from the cursor. If it reaches the beginning, it will restart from the end.
     *
     * @param s the searching text
     * @return the next index that the element matches the searching text.
     */
    public int reverseFindFromCursor(String s) {
        if (!isReverseOrder()) {
            return findFromCursor(s);
        }

        String str = isCaseSensitive() ? s : s.toLowerCase();
        int selectedIndex = getCurrentIndex();
        if (selectedIndex < 0)
            selectedIndex = 0;
        int count = getElementCount();
        if (count == 0)
            return -1; // no match

        // find from cursor to beginning
        for (int i = selectedIndex; i >= 0; i--) {
            Object element = getElementAt(i);
            if (compare(element, str))
                return i;
        }

        // if not found, start over from the end
        for (int i = count - 1; i >= selectedIndex; i--) {
            Object element = getElementAt(i);
            if (compare(element, str))
                return i;
        }

        return -1;
    }

    /**
     * Finds the first element that matches the searching text.
     *
     * @param s the searching text
     * @return the first element that matches with the searching text.
     */
    public int findFirst(String s) {
        String str = isCaseSensitive() ? s : s.toLowerCase();
        int count = getElementCount();
        if (count == 0)
            return s.length() > 0 ? -1 : 0;

        for (int i = 0; i < count; i++) {
            int index = getIndex(count, i);
            Object element = getElementAt(index);
            if (compare(element, str))
                return index;
        }

        return -1;
    }

    /**
     * Finds the last element that matches the searching text.
     *
     * @param s the searching text
     * @return the last element that matches the searching text.
     */
    public int findLast(String s) {
        String str = isCaseSensitive() ? s : s.toLowerCase();
        int count = getElementCount();
        if (count == 0)
            return s.length() > 0 ? -1 : 0;
        for (int i = count - 1; i >= 0; i--) {
            Object element = getElementAt(i);
            if (compare(element, str))
                return i;
        }
        return -1;
    }

    /**
     * This method is called when a key is typed or pressed.
     *
     * @param e the KeyEvent.
     */
    protected void keyTypedOrPressed(KeyEvent e) {
        if (_searchableProvider != null && _searchableProvider.isPassive()) {
            _searchableProvider.processKeyEvent(e);
            return;
        }

        if (isActivateKey(e)) {
            String searchingText = "";
            if (e.getID() == KeyEvent.KEY_TYPED) {
                if (JideSwingUtilities.isMenuShortcutKeyDown(e)) { // if ctrl key is pressed
                    return;
                }
                if (e.isAltDown()) {
                    return;
                }

                searchingText = String.valueOf(e.getKeyChar());
            }
            showPopup(searchingText);
            if (e.getKeyCode() != KeyEvent.VK_ENTER) {
                e.consume();
            }
        }
    }

    private int getIndex(int count, int index) {
        return isReverseOrder() ? count - index - 1 : index;
    }

    /**
     * Shows the search popup. By default, the search popup will be visible automatically when user types in the first
     * key (in the case of JList, JTree, JTable) or types in designated keystroke (in the case of JTextComponent). So
     * this method is only used when you want to show the popup manually.
     *
     * @param searchingText the searching text
     */
    public void showPopup(String searchingText) {
        if (_searchableProvider == null) {
            fireSearchableEvent(new SearchableEvent(this, SearchableEvent.SEARCHABLE_START, searchingText));
            showPopup(createSearchPopup(searchingText));
            _searchableProvider = new SearchableProvider() {
                public String getSearchingText() {
                    return _popup != null ? _popup.getSearchingText() : "";
                }

                public boolean isPassive() {
                    return true;
                }

                public void processKeyEvent(KeyEvent e) {
                    if (_popup != null) {
                        _popup.processKeyEvent(e);
                    }
                }
            };
        }
    }

    /**
     * Creates the popup to hold the searching text.
     *
     * @param searchingText the searching text
     * @return the searching popup.
     */
    protected SearchPopup createSearchPopup(String searchingText) {
        return new DefaultSearchPopup(searchingText);
    }

    /**
     * Gets the searching text.
     *
     * @return the searching text.
     */
    public String getSearchingText() {
        return _searchableProvider != null ? _searchableProvider.getSearchingText() : "";
    }

    private void showPopup(SearchPopup searchpopup) {
        JRootPane rootPane = _component.getRootPane();
        if (rootPane != null)
            _layeredPane = rootPane.getLayeredPane();
        else {
            _layeredPane = null;
        }

        if (_layeredPane == null || isHeavyweightComponentEnabled()) {
            _popup = searchpopup;
            Point location = updateSizeAndLocation();
            if (location != null) {
                searchpopup.showPopup(location.x, location.y);
                _popup.setVisible(true);
            }
            else {
                _popup = null;
            }
        }
        else {
            if (_popup != null && _layeredPane != null) {
                _layeredPane.remove(_popup);
                _layeredPane.validate();
                _layeredPane.repaint();
                _layeredPane = null;
            }
            else if (!_component.isShowing())
                _popup = null;
            else
                _popup = searchpopup;

            if (_popup == null || !_component.isDisplayable())
                return;

            if (_layeredPane == null) {
                System.err.println("Failed to find layeredPane.");
                return;
            }

            _layeredPane.add(_popup, JLayeredPane.POPUP_LAYER);

            updateSizeAndLocation();
            _popup.setVisible(true);
            _popup.validate();
        }

        if (_popup != null) {
            _popup.addPopupMenuListener(new PopupMenuListener() {
                @Override
                public void popupMenuWillBecomeVisible(PopupMenuEvent e) {

                }

                @Override
                public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
                    // clear up the internal cached values so that a new search popup will be shown after this.
                    _popup = null;
                    _searchableProvider = null;
                }

                @Override
                public void popupMenuCanceled(PopupMenuEvent e) {

                }
            });
        }
    }

    private Point updateSizeAndLocation() {
        Component component = getPopupLocationRelativeTo();
        if (component == null) {
            component = JideSwingUtilities.getScrollPane(_component);
        }
        if (component == null) {
            component = _component;
        }

        Point componentLocation;
        if (_popup != null) {
            Dimension size = _popup.getPreferredSize();
            switch (getPopupLocation()) {
                case SwingConstants.BOTTOM:
                    try {
                        componentLocation = component.getLocationOnScreen();
                        componentLocation.y += component.getHeight();
                        if (!isHeavyweightComponentEnabled()) {
                            SwingUtilities.convertPointFromScreen(componentLocation, _layeredPane);
                            if ((componentLocation.y + size.height > _layeredPane.getHeight())) {
                                componentLocation.y = _layeredPane.getHeight() - size.height;
                            }
                        }
                    }
                    catch (IllegalComponentStateException e) {
                        return null; // can't get the location so just return.
                    }
                    break;
                case SwingConstants.TOP:
                default:
                    try {
                        componentLocation = component.getLocationOnScreen();
                        if (!isHeavyweightComponentEnabled()) {
                            SwingUtilities.convertPointFromScreen(componentLocation, _layeredPane);
                        }
                        componentLocation.y -= size.height;
                        if ((componentLocation.y < 0)) {
                            componentLocation.y = 0;
                        }
                    }
                    catch (IllegalComponentStateException e) {
                        return null; // can't get the location so just return.
                    }
                    break;
            }
            if (!isHeavyweightComponentEnabled()) {
                _popup.setLocation(componentLocation);
                _popup.setSize(size);
            }
            else {
                _popup.packPopup();
            }
            return componentLocation;
        }
        else {
            return null;
        }
    }

    /**
     * Checks if the key is used as a key to find the first occurrence.
     *
     * @param e the key event
     * @return true if the key in KeyEvent is a key to find the firstoccurrencee. By default, home key is used.
     */
    protected boolean isFindFirstKey(KeyEvent e) {
        return e.getKeyCode() == KeyEvent.VK_HOME;
    }

    /**
     * Checks if the key is used as a key to find the last occurrence.
     *
     * @param e the key event
     * @return true if the key in KeyEvent is a key to find the last occurrence. By default, end key is used.
     */
    protected boolean isFindLastKey(KeyEvent e) {
        return e.getKeyCode() == KeyEvent.VK_END;
    }

    /**
     * Checks if the key is used as a key to find the previous occurrence.
     *
     * @param e the key event
     * @return true if the key in KeyEvent is a key to find the previous occurrence. By default, up arrow key is used.
     */
    protected boolean isFindPreviousKey(KeyEvent e) {
        return e.getKeyCode() == KeyEvent.VK_UP;
    }

    /**
     * Checks if the key is used as a key to find the next occurrence.
     *
     * @param e the key event
     * @return true if the key in KeyEvent is a key to find the next occurrence. By default, down arrow key is used.
     */
    protected boolean isFindNextKey(KeyEvent e) {
        return e.getKeyCode() == KeyEvent.VK_DOWN;
    }

    /**
     * Checks if the key is used as a navigation key. Navigation keys are keys which are used to navigate to other
     * occurrences of the searching string.
     *
     * @param e the key event
     * @return true if the key in KeyEvent is a navigation key.
     */
    protected boolean isNavigationKey(KeyEvent e) {
        return isFindFirstKey(e) || isFindLastKey(e) || isFindNextKey(e) || isFindPreviousKey(e);
    }

    /**
     * Checks if the key in KeyEvent should activate the search popup.
     *
     * @param e the key event
     * @return true if the keyChar is visible except space and tab.
     */
    protected boolean isActivateKey(KeyEvent e) {
        char keyChar = e.getKeyChar();
        return e.getID() == KeyEvent.KEY_TYPED && keyChar > KeyEvent.VK_SPACE && keyChar != KeyEvent.VK_DELETE;
    }

    /**
     * Checks if the key in KeyEvent should hide the search popup. If this method return true and the key is not used
     * for navigation purpose ({@link #isNavigationKey(java.awt.event.KeyEvent)} return false), the popup will be
     * hidden.
     *
     * @param e the key event
     * @return true if the keyCode in the KeyEvent is escape key, enter key, or any of the arrow keys such as page up,
     * page down, home, end, left, right, up and down.
     */
    protected boolean isDeactivateKey(KeyEvent e) {
        int keyCode = e.getKeyCode();
        return keyCode == KeyEvent.VK_ENTER || keyCode == KeyEvent.VK_ESCAPE
                || keyCode == KeyEvent.VK_PAGE_UP || keyCode == KeyEvent.VK_PAGE_DOWN
                || keyCode == KeyEvent.VK_HOME || keyCode == KeyEvent.VK_END
                || keyCode == KeyEvent.VK_LEFT || keyCode == KeyEvent.VK_RIGHT
                || keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_DOWN;
    }

    /**
     * Checks if the key will trigger selecting all.
     *
     * @param e the key event
     * @return true if the key in KeyEvent is a key to trigger selecting all.
     */
    protected boolean isSelectAllKey(KeyEvent e) {
        return JideSwingUtilities.isMenuShortcutKeyDown(e) && e.getKeyCode() == KeyEvent.VK_A;
    }

    /**
     * Checks if the key will trigger incremental selection.
     *
     * @param e the key event
     * @return true if the key in KeyEvent is a key to trigger incremental selection. By default, ctrl down key is used.
     */
    protected boolean isIncrementalSelectKey(KeyEvent e) {
        return JideSwingUtilities.isMenuShortcutKeyDown(e);
    }

    /**
     * Gets the foreground color when the searching text doesn't match with any of the elements in the component.
     *
     * @return the foreground color for mismatch. If you never call {@link #setMismatchForeground(java.awt.Color)}. red
     * color will be used.
     */
    public Color getMismatchForeground() {
        if (_mismatchForeground == null) {
            return Color.RED;
        }
        else {
            return _mismatchForeground;
        }
    }

    /**
     * Sets the foreground for mismatch.
     *
     * @param mismatchForeground mismatch forground
     */
    public void setMismatchForeground(Color mismatchForeground) {
        _mismatchForeground = mismatchForeground;
    }

    /**
     * Checks if the case is sensitive during searching.
     *
     * @return true if the searching is case sensitive.
     */
    public boolean isCaseSensitive() {
        return _caseSensitive;
    }

    /**
     * Sets the case sensitive flag. By default, it's false meaning it's a case insensitive search.
     *
     * @param caseSensitive the flag if searching is case sensitive
     */
    public void setCaseSensitive(boolean caseSensitive) {
        _caseSensitive = caseSensitive;
    }

    /**
     * If it returns a positive number, it will wait for that many ms before doing the search. When the searching is
     * complex, this flag will be useful to make the searching efficient. In the other words, if user types in several
     * keys very quickly, there will be only one search. If it returns 0 or negative number, each key will generate a
     * search.
     *
     * @return the number of ms delay before searching starts.
     */
    public int getSearchingDelay() {
        return _searchingDelay;
    }

    /**
     * If this flag is set to a positive number, it will wait for that many ms before doing the search. When the
     * searching is complex, this flag will be useful to make the searching efficient. In the other words, if user types
     * in several keys very quickly, there will be only one search. If this flag is set to 0 , each key will generate a
     * search with no delay. If this flag is set to a negative number, there are different behaviors. SearchableBar will
     * not generate any search while typing, but others will generate a search with no delay as well as it is set to 0.
     *
     * @param searchingDelay the number of ms delay before searching start.
     */
    public void setSearchingDelay(int searchingDelay) {
        _searchingDelay = searchingDelay;
    }

    /**
     * Checks if restart from the beginning when searching reaches the end or restart from the end when reaches
     * beginning. Default is false.
     *
     * @return true or false.
     */
    public boolean isRepeats() {
        return _repeats;
    }

    /**
     * Sets the repeat flag. By default, it's false meaning it will stop searching when reaching the end or reaching the
     * beginning.
     *
     * @param repeats the repeat flag
     */
    public void setRepeats(boolean repeats) {
        _repeats = repeats;
    }

    /**
     * Gets the foreground color used inn the search popup.
     *
     * @return the foreground. By default it will use the foreground of tooltip.
     */
    public Color getForeground() {
        if (_foreground == null) {
            return UIDefaultsLookup.getColor("ToolTip.foreground");
        }
        else {
            return _foreground;
        }
    }

    /**
     * Sets the foreground color used by popup.
     *
     * @param foreground the foreground
     */
    public void setForeground(Color foreground) {
        _foreground = foreground;
    }

    /**
     * Gets the background color used inn the search popup.
     *
     * @return the background. By default it will use the background of tooltip.
     */
    public Color getBackground() {
        if (_background == null) {
            return UIDefaultsLookup.getColor("ToolTip.background");
        }
        else {
            return _background;
        }
    }

    /**
     * Sets the background color used by popup.
     *
     * @param background the background
     */
    public void setBackground(Color background) {
        _background = background;
    }

    /**
     * Checks if it supports wildcard in searching text. By default it is true which means user can type in "*" or "?"
     * to match with any characters or any character. If it's false, it will treat "*" or "?" as a regular character.
     *
     * @return true if it supports wildcard.
     */
    public boolean isWildcardEnabled() {
        return _wildcardEnabled;
    }

    /**
     * Enable or disable the usage of wildcard.
     *
     * @param wildcardEnabled the flag if wildcard is enabled
     * @see #isWildcardEnabled()
     */
    public void setWildcardEnabled(boolean wildcardEnabled) {
        _wildcardEnabled = wildcardEnabled;
    }

    /**
     * Gets the WildcardSupport. If user never sets it, {@link DefaultWildcardSupport} will be used.
     *
     * @return the WildcardSupport.
     */
    public WildcardSupport getWildcardSupport() {
        if (_wildcardSupport == null) {
            _wildcardSupport = new DefaultWildcardSupport();
        }
        return _wildcardSupport;
    }

    /**
     * Sets the WildcardSupport. This class allows you to define what wildcards to use and how to convert the wildcard
     * strings to a regular expression string which is eventually used to search.
     *
     * @param wildcardSupport the new WildCardSupport.
     */
    public void setWildcardSupport(WildcardSupport wildcardSupport) {
        _wildcardSupport = wildcardSupport;
    }

    /**
     * Gets the current text that appears in the search popup. By default it is "Search for: ".
     *
     * @return the text that appears in the search popup.
     */
    public String getSearchLabel() {
        if (_searchLabel == null) {
            return getResourceString("Searchable.searchFor");
        }
        else {
            return _searchLabel;
        }
    }

    /**
     * Sets the text that appears in the search popup.
     *
     * @param searchLabel the search label
     */
    public void setSearchLabel(String searchLabel) {
        _searchLabel = searchLabel;
    }

    /**
     * Adds the specified listener to receive searchable events from this searchable.
     *
     * @param l the searchable listener
     */
    public void addSearchableListener(SearchableListener l) {
        listenerList.add(SearchableListener.class, l);
    }

    /**
     * Removes the specified searchable listener so that it no longer receives searchable events.
     *
     * @param l the searchable listener
     */
    public void removeSearchableListener(SearchableListener l) {
        listenerList.remove(SearchableListener.class, l);
    }

    /**
     * Returns an array of all the <code>SearchableListener</code>s added to this <code>SearchableGroup</code> with
     * <code>addSearchableListener</code>.
     *
     * @return all of the <code>SearchableListener</code>s added or an empty array if no listeners have been added
     * @see #addSearchableListener
     */
    public SearchableListener[] getSearchableListeners() {
        return listenerList.getListeners(SearchableListener.class);
    }

    /**
     * Returns if a given listener is already installed.
     *
     * @param l the listener
     * @return true if the listener is already installed. Otherwise false.
     * @since 3.2.3
     */
    public boolean isSearchableListenerInstalled(SearchableListener l) {
        SearchableListener[] listeners = getSearchableListeners();
        for (SearchableListener listener : listeners) {
            if (listener == l) {
                return true;
            }
        }
        return false;
    }

    /**
     * Fires a searchable event.
     *
     * @param e the event
     */
    protected void fireSearchableEvent(SearchableEvent e) {
        Object[] listeners = listenerList.getListenerList();
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == SearchableListener.class) {
                ((SearchableListener) listeners[i + 1]).searchableEventFired(e);
            }
        }
    }

    /**
     * Gets the actual component which installed this Searchable.
     *
     * @return the actual component which installed this Searchable.
     */
    public Component getComponent() {
        return _component;
    }

    /**
     * Gets the popup location. It could be either {@link SwingConstants#TOP} or {@link SwingConstants#BOTTOM}.
     *
     * @return the popup location.
     */
    public int getPopupLocation() {
        return _popupLocation;
    }

    /**
     * Sets the popup location.
     *
     * @param popupLocation the popup location. The valid values are either {@link SwingConstants#TOP} or {@link
     *                      SwingConstants#BOTTOM}.
     */
    public void setPopupLocation(int popupLocation) {
        _popupLocation = popupLocation;
    }

    public abstract class SearchPopup extends JidePopup {
        protected SearchField _textField;

        @Override
        public void processKeyEvent(KeyEvent e) {
            _textField.processKeyEvent(e);
            if (e.isConsumed()) {
                String text = getSearchingText();
                if (text.length() == 0) {
                    return;
                }

                if (isSelectAllKey(e)) {
                    selectAll(e, text);
                    return;
                }

                int found;
                if (isFindPreviousKey(e)) {
                    found = findPrevious(text);
                    select(found, e, text);
                }
                else if (isFindNextKey(e)) {
                    found = findNext(text);
                    select(found, e, text);
                }
                else if (isFindFirstKey(e)) {
                    found = findFirst(text);
                    select(found, e, text);
                }
                else if (isFindLastKey(e)) {
                    found = findLast(text);
                    select(found, e, text);
                }
//                else {
//                    found = findFromCursor(text);
//                }
            }
            if (e.getKeyCode() != KeyEvent.VK_ENTER) {
                e.consume();
            }
        }

        private void selectAll(KeyEvent e, String text) {
            boolean oldReverseOrder = isReverseOrder(); // keep the old reverse order and we will set it back.
            if (oldReverseOrder) {
                setReverseOrder(false);
            }

            int index = findFirst(text);
            if (index != -1) {
                setSelectedIndex(index, false); // clear side effect of ctrl-a will select all items
                Searchable.this.setCursor(index); // as setSelectedIndex is used directly, we have to manually set the cursor value.
            }


            boolean oldRepeats = isRepeats(); // set repeats to false and set it back later.
            if (oldRepeats) {
                setRepeats(false);
            }

            while (index != -1) {
                int newIndex = findNext(text);
                if (index == newIndex) {
                    index = -1;
                }
                else {
                    index = newIndex;
                }
                if (index == -1) {
                    break;
                }
                select(index, e, text);
            }

            if (oldRepeats) {
                setRepeats(oldRepeats);
            }

            if (oldReverseOrder) {
                setReverseOrder(oldReverseOrder);
            }
        }

        public String getSearchingText() {
            return _textField != null ? _textField.getText() : "";
        }

        abstract protected void select(int index, KeyEvent e, String searchingText);
    }

    /**
     * Checks the searching order. By default the searchable starts searching from top to bottom. If this flag is false,
     * it searches from bottom to top.
     *
     * @return the reverseOrder flag.
     */
    public boolean isReverseOrder() {
        return _reverseOrder;
    }

    /**
     * Sets the searching order. By default the searchable starts searching from top to bottom. If this flag is false,
     * it searches from bottom to top.
     *
     * @param reverseOrder the flag if searching from top to bottom or from bottom to top
     */
    public void setReverseOrder(boolean reverseOrder) {
        _reverseOrder = reverseOrder;
    }

    /**
     * Gets the localized string from resource bundle. Subclass can override it to provide its own string. Available
     * keys are defined in swing.properties that begin with "Searchable.".
     *
     * @param key the resource string key
     * @return the localized string.
     */
    protected String getResourceString(String key) {
        return Resource.getResourceBundle(_component != null ? _component.getLocale() : Locale.getDefault()).getString(key);
    }

    /**
     * Check if the searchable popup is visible.
     *
     * @return true if visible. Otherwise, false.
     */
    public boolean isPopupVisible() {
        return _popup != null;
    }

    public boolean isHeavyweightComponentEnabled() {
        return _heavyweightComponentEnabled;
    }

    public void setHeavyweightComponentEnabled(boolean heavyweightComponentEnabled) {
        _heavyweightComponentEnabled = heavyweightComponentEnabled;
    }


    /**
     * Gets the component that the location of the popup relative to.
     *
     * @return the component that the location of the popup relative to.
     */
    public Component getPopupLocationRelativeTo() {
        return _popupLocationRelativeTo;
    }

    /**
     * Sets the location of the popup relative to the specified component. Then based on the value of {@link
     * #getPopupLocation()}. If you never set, we will use the searchable component or its scroll pane (if exists) as
     * the popupLocationRelativeTo component.
     *
     * @param popupLocationRelativeTo the relative component
     */
    public void setPopupLocationRelativeTo(Component popupLocationRelativeTo) {
        _popupLocationRelativeTo = popupLocationRelativeTo;
    }

    /**
     * This is a property of how to compare searching text with the data. If it is true, it will use {@link
     * String#startsWith(String)} to do the comparison. Otherwise, it will use {@link String#indexOf(String)} to do the
     * comparison.
     *
     * @return true or false.
     */
    public boolean isFromStart() {
        return _fromStart;
    }

    /**
     * Sets the fromStart property.
     *
     * @param fromStart true if the comparison matches from the start of the text only. Otherwise false. The difference
     *                  is if true, it will use String's <code>startWith</code> method to match. If false, it will use
     *                  <code>indedxOf</code> method.
     */
    public void setFromStart(boolean fromStart) {
        hidePopup();
        _fromStart = fromStart;
    }

    /**
     * Gets the Searchable installed on the component. Null is no Searchable was installed.
     *
     * @param component the component
     * @return the Searchable installed. Null is no Searchable was installed.
     */
    public static Searchable getSearchable(JComponent component) {
        Object clientProperty = component.getClientProperty(CLIENT_PROPERTY_SEARCHABLE);
        if (clientProperty instanceof Searchable) {
            return ((Searchable) clientProperty);
        }
        else {
            return null;
        }
    }

    private void updateClientProperty(JComponent component, Searchable searchable) {
        if (component != null) {
            Object clientProperty = _component.getClientProperty(CLIENT_PROPERTY_SEARCHABLE);
            if (clientProperty instanceof Searchable) {
                ((Searchable) clientProperty).uninstallListeners();
            }
            component.putClientProperty(CLIENT_PROPERTY_SEARCHABLE, searchable);
        }
    }

    /**
     * Get the flag if we should process model change event.
     * <p/>
     * By default, the value is true, which means the model change event should be processed.
     * <p/>
     * In <code>ListShrinkSearchableSupport</code> case, since we will fire this event while applying filters. This flag
     * will be switched to false before we fire the event and set it back to true.
     * <p/>
     * In normal case, please do not set this flag.
     *
     * @return true if we should process model change event. Otherwise false.
     */
    public boolean isProcessModelChangeEvent() {
        return _processModelChangeEvent;
    }

    /**
     * Set the flag if we should process model change event.
     * <p/>
     * In normal case, please do not set this flag.
     * <p/>
     *
     * @param processModelChangeEvent the flag
     * @see #isProcessModelChangeEvent()
     */
    public void setProcessModelChangeEvent(boolean processModelChangeEvent) {
        _processModelChangeEvent = processModelChangeEvent;
    }

    /**
     * Gets the timeout for showing the popup.
     *
     * @return the popup timeout.
     * @see #setPopupTimeout(int)
     */
    public int getPopupTimeout() {
        return _popupTimeout;
    }

    /**
     * Sets the timeout for showing the popup.
     * <p/>
     * By default, the timeout value is 0, which means no timeout. You could set it to a positive value to automatically
     * hide the search popup after an idle time.
     *
     * @param popupTimeout the timeout in milliseconds
     */
    public void setPopupTimeout(int popupTimeout) {
        _popupTimeout = popupTimeout;
    }

    /**
     * Gets the flag indicating if the Searchable should count all matches for every search.
     *
     * @return true if should count all matches. Otherwise false.
     * @see #setCountMatch(boolean)
     * @since 3.5.2
     */
    public boolean isCountMatch() {
        return _countMatch;
    }

    /**
     * Sets the flag indicating if the Searchable should count all matches for every search.
     * <p/>
     * By default, the flag is false to keep performance high.
     *
     * @param countMatch the flag
     * @since 3.5.2
     */
    public void setCountMatch(boolean countMatch) {
        _countMatch = countMatch;
    }

    int getMatchCount() {
        return _matchCount;
    }

    /**
     * <code>findAll</code> uses the Searchable to find all the element indices that match the searching string.
     *
     * @param s the searching string.
     * @return the list of indices.
     */
    public java.util.List<Integer> findAll(String s) {
        String str = isCaseSensitive() ? s : s.toLowerCase();
        java.util.List<Integer> list = new ArrayList<Integer>();
        for (int i = 0, count = getElementCount(); i < count; i++) {
            Object elementAt = getElementAt(i);
            if (compare(elementAt, str)) {
                list.add(i);
            }
        }
        return list;
    }

    /**
     * Gets the element at the specified index as string using {@link #convertElementToString(Object)} method.
     *
     * @param index the index.
     * @return the element at the index converted to string.
     */
    public String getElementAtAsString(int index) {
        return convertElementToString(getElementAt(index));
    }

    protected void textChanged(String text) {
        if (text == null || text.length() == 0) {
            firePropertyChangeEvent("");
            return;
        }
        int found = findFromCursor(text);
        if (found == -1) {
            firePropertyChangeEvent(text);
            fireSearchableEvent(new SearchableEvent(this, SearchableEvent.SEARCHABLE_NOMATCH, text));
        }
        else {
            firePropertyChangeEvent(text);
            Object element = getElementAt(found);
            fireSearchableEvent(new SearchableEvent(this, SearchableEvent.SEARCHABLE_MATCH, text, element, convertElementToString(element)));
        }
    }

    /**
     * Finds the first element that matches the searching text exactly.
     *
     * @param s the searching text
     * @return the first element that matches with the searching text.
     * @since 3.6.1
     */
    public int findFirstExactly(String s) {
        String str = isCaseSensitive() ? s : s.toLowerCase();
        int count = getElementCount();
        if (count == 0)
            return s.length() > 0 ? -1 : 0;

        for (int i = 0; i < count; i++) {
            int index = getIndex(count, i);
            Object element = getElementAt(index);
            String text = convertElementToString(element);
            if (JideSwingUtilities.equals(text, str))
                return index;
        }

        return -1;
    }
}
