All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jidesoft.swing.Searchable Maven / Gradle / Ivy

/*
 * @(#)${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. Searchable 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.
 * 

* Searchable is a base abstract class. ListSearchable, TableSearchable and * TreeSearchable are implementations to make JList, JTable and JTree searchable respectively. For each * implementation, there are five methods need to be implemented.

  • protected abstract int * getSelectedIndex()
  • protected abstract void setSelectedIndex(int index, boolean incremental) *
  • protected abstract int getElementCount()
  • protected abstract Object getElementAt(int * index)
  • protected abstract String convertElementToString(Object element)
*

* Please look at the javadoc of each method to learn more details. *

* 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. *

* 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. *

* 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.
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.
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 * Searchable. All you need to do is to override {@link #compare(String, String)} method and implement by * yourself. *

* 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. *

* 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)}. *

* 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. *

* When a Searchable 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. *

* 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 _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(); 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(); 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.

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)}. *

* 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. *

* 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. *

* 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. *

* 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. *

* 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 SearchableListeners added to this SearchableGroup with * addSearchableListener. * * @return all of the SearchableListeners 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 startWith method to match. If false, it will use * indedxOf 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. *

* By default, the value is true, which means the model change event should be processed. *

* In ListShrinkSearchableSupport 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. *

* 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. *

* In normal case, please do not set this flag. *

* * @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. *

* 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. *

* 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; } /** * findAll 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 findAll(String s) { String str = isCaseSensitive() ? s : s.toLowerCase(); java.util.List list = new ArrayList(); 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; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy