org.jdesktop.swingx.JXSearchField Maven / Gradle / Ivy
package org.jdesktop.swingx;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.text.Document;
import org.jdesktop.beans.JavaBean;
import org.jdesktop.swingx.plaf.SearchFieldAddon;
import org.jdesktop.swingx.plaf.LookAndFeelAddons;
import org.jdesktop.swingx.plaf.TextUIWrapper;
import org.jdesktop.swingx.plaf.UIManagerExt;
import org.jdesktop.swingx.prompt.BuddyButton;
import org.jdesktop.swingx.search.NativeSearchFieldSupport;
import org.jdesktop.swingx.search.RecentSearches;
/**
* A text field with a find icon in which the user enters text that identifies
* items to search for.
*
* JXSearchField almost looks and behaves like a native Windows Vista search
* box, a Mac OS X search field, or a search field like the one used in Mozilla
* Thunderbird 2.0 - depending on the current look and feel.
*
* JXSearchField is a text field that contains a find button and a cancel
* button. The find button normally displays a lens icon appropriate for the
* current look and feel. The cancel button is used to clear the text and
* therefore only visible when text is present. It normally displays a 'x' like
* icon. Text can also be cleared, using the 'Esc' key.
*
* The position of the find and cancel buttons can be customized by either
* changing the search fields (text) margin or button margin, or by changing the
* {@link LayoutStyle}.
*
* JXSearchField supports two different search modes: {@link SearchMode#INSTANT}
* and {@link SearchMode#REGULAR}.
*
* A search can be performed by registering an {@link ActionListener}. The
* {@link ActionEvent}s command property contains the text to search for. The
* search should be cancelled, when the command text is empty or null.
*
* @see RecentSearches
* @author Peter Weishapl
*
*/
@JavaBean
public class JXSearchField extends JXTextField {
/**
* The default instant search delay.
*/
private static final int DEFAULT_INSTANT_SEARCH_DELAY = 180;
/**
* The key used to invoke the cancel action.
*/
private static final KeyStroke CANCEL_KEY = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
/**
* Defines, how the find and cancel button are layouted.
*/
public enum LayoutStyle {
/**
*
* In VISTA layout style, the find button is placed on the right side of
* the search field. If text is entered, the find button is replaced by
* the cancel button when the actual search mode is
* {@link SearchMode#INSTANT}. When the search mode is
* {@link SearchMode#REGULAR} the find button will always stay visible
* and the cancel button will never be shown. However, 'Escape' can
* still be pressed to clear the text.
*
*/
VISTA,
/**
*
* In MAC layout style, the find button is placed on the left side of
* the search field and the cancel button on the right side. The cancel
* button is only visible when text is present.
*
*/
MAC
};
/**
* Defines when action events are posted.
*/
public enum SearchMode {
/**
*
* In REGULAR search mode, an action event is fired, when the user
* presses enter or clicks the find button.
*
*
* However, if a find popup menu is set and layout style is
* {@link LayoutStyle#MAC}, no action will be fired, when the find
* button is clicked, because instead the popup menu is shown. A search
* can therefore only be triggered, by pressing the enter key.
*
*
* The find button can have a rollover and a pressed icon, defined by
* the "SearchField.rolloverIcon" and "SearchField.pressedIcon" UI
* properties. When a find popup menu is set,
* "SearchField.popupRolloverIcon" and "SearchField.popupPressedIcon"
* are used.
*
*
*/
REGULAR,
/**
* In INSTANT search mode, an action event is fired, when the user
* presses enter or changes the search text.
*
* The action event is delayed about the number of milliseconds
* specified by {@link JXSearchField#getInstantSearchDelay()}.
*
* No rollover and pressed icon is used for the find button.
*/
INSTANT
}
// ensure at least the default ui is registered
static {
LookAndFeelAddons.contribute(new SearchFieldAddon());
}
private JButton findButton;
private JButton cancelButton;
private JButton popupButton;
private LayoutStyle layoutStyle;
private SearchMode searchMode = SearchMode.INSTANT;
private boolean useSeperatePopupButton;
private boolean useSeperatePopupButtonSet;
private boolean layoutStyleSet;
private int instantSearchDelay = DEFAULT_INSTANT_SEARCH_DELAY;
private boolean promptFontStyleSet;
private Timer instantSearchTimer;
private String recentSearchesSaveKey;
private RecentSearches recentSearches;
/**
* Creates a new search field with a default prompt.
*/
public JXSearchField() {
this(UIManagerExt.getString("SearchField.prompt"));
}
/**
* Creates a new search field with the given prompt and
* {@link SearchMode#INSTANT}.
*
* @param prompt
*/
public JXSearchField(String prompt) {
super(prompt);
// use the native search field if possible.
setUseNativeSearchFieldIfPossible(true);
// install default actions
setCancelAction(new ClearAction());
setFindAction(new FindAction());
// We cannot register the ClearAction through the Input- and
// ActionMap because ToolTipManager registers the escape key with an
// action that hides the tooltip every time the tooltip is changed and
// then the ClearAction will never be called.
addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (CANCEL_KEY.equals(KeyStroke.getKeyStroke(e.getKeyCode(), e.getModifiers()))) {
getCancelAction().actionPerformed(
new ActionEvent(JXSearchField.this, e.getID(), KeyEvent.getKeyText(e.getKeyCode())));
}
}
});
// Map specific native properties to general JXSearchField properties.
addPropertyChangeListener(NativeSearchFieldSupport.FIND_POPUP_PROPERTY, new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
JPopupMenu oldPopup = (JPopupMenu) evt.getOldValue();
firePropertyChange("findPopupMenu", oldPopup, evt.getNewValue());
}
});
addPropertyChangeListener(NativeSearchFieldSupport.CANCEL_ACTION_PROPERTY, new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
ActionListener oldAction = (ActionListener) evt.getOldValue();
firePropertyChange("cancelAction", oldAction, evt.getNewValue());
}
});
addPropertyChangeListener(NativeSearchFieldSupport.FIND_ACTION_PROPERTY, new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
ActionListener oldAction = (ActionListener) evt.getOldValue();
firePropertyChange("findAction", oldAction, evt.getNewValue());
}
});
}
/**
* Returns the current {@link SearchMode}.
*
* @return the current {@link SearchMode}.
*/
public SearchMode getSearchMode() {
return searchMode;
}
/**
* Returns true
if the current {@link SearchMode} is
* {@link SearchMode#INSTANT}.
*
* @return true
if the current {@link SearchMode} is
* {@link SearchMode#INSTANT}
*/
public boolean isInstantSearchMode() {
return SearchMode.INSTANT.equals(getSearchMode());
}
/**
* Returns true
if the current {@link SearchMode} is
* {@link SearchMode#REGULAR}.
*
* @return true
if the current {@link SearchMode} is
* {@link SearchMode#REGULAR}
*/
public boolean isRegularSearchMode() {
return SearchMode.REGULAR.equals(getSearchMode());
}
/**
* Sets the current search mode. See {@link SearchMode} for a description of
* the different search modes.
*
* @param searchMode
* {@link SearchMode#INSTANT} or {@link SearchMode#REGULAR}
*/
public void setSearchMode(SearchMode searchMode) {
firePropertyChange("searchMode", this.searchMode, this.searchMode = searchMode);
}
/**
* Get the instant search delay in milliseconds. The default delay is 50
* Milliseconds.
*
* @see {@link #setInstantSearchDelay(int)}
* @return the instant search delay in milliseconds
*/
public int getInstantSearchDelay() {
return instantSearchDelay;
}
/**
* Set the instant search delay in milliseconds. In
* {@link SearchMode#INSTANT}, when the user changes the text, an action
* event will be fired after the specified instant search delay.
*
* It is recommended to use a instant search delay to avoid the firing of
* unnecessary events. For example when the user replaces the whole text
* with a different text the search fields underlying {@link Document}
* typically fires 2 document events. The first one, because the old text is
* removed and the second one because the new text is inserted. If the
* instant search delay is 0, this would result in 2 action events being
* fired. When a instant search delay is used, the first document event
* typically is ignored, because the second one is fired before the delay is
* over, which results in a correct behavior because only the last and only
* relevant event will be delivered.
*
* @param instantSearchDelay
*/
public void setInstantSearchDelay(int instantSearchDelay) {
firePropertyChange("instantSearchDelay", this.instantSearchDelay, this.instantSearchDelay = instantSearchDelay);
}
/**
* Get the current {@link LayoutStyle}.
*
* @return
*/
public LayoutStyle getLayoutStyle() {
return layoutStyle;
}
/**
* Returns true
if the current {@link LayoutStyle} is
* {@link LayoutStyle#VISTA}.
*
* @return
*/
public boolean isVistaLayoutStyle() {
return LayoutStyle.VISTA.equals(getLayoutStyle());
}
/**
* Returns true
if the current {@link LayoutStyle} is
* {@link LayoutStyle#MAC}.
*
* @return
*/
public boolean isMacLayoutStyle() {
return LayoutStyle.MAC.equals(getLayoutStyle());
}
/**
* Set the current {@link LayoutStyle}. See {@link LayoutStyle} for a
* description of how this affects layout and behavior of the search field.
*
* @param layoutStyle
* {@link LayoutStyle#MAC} or {@link LayoutStyle#VISTA}
*/
public void setLayoutStyle(LayoutStyle layoutStyle) {
layoutStyleSet = true;
firePropertyChange("layoutStyle", this.layoutStyle, this.layoutStyle = layoutStyle);
}
/**
* Set the margin space around the search field's text.
*
* @see javax.swing.text.JTextComponent#setMargin(java.awt.Insets)
*/
@Override
public void setMargin(Insets m) {
super.setMargin(m);
}
/**
* Returns the cancel action, or an instance of {@link ClearAction}, if
* none has been set.
*
* @return the cancel action
*/
public final ActionListener getCancelAction() {
ActionListener a = NativeSearchFieldSupport.getCancelAction(this);
if (a == null) {
a = new ClearAction();
}
return a;
}
/**
* Sets the action that is invoked, when the user presses the 'Esc' key or
* clicks the cancel button.
*
* @param cancelAction
*/
public final void setCancelAction(ActionListener cancelAction) {
NativeSearchFieldSupport.setCancelAction(this, cancelAction);
}
/**
* Returns the cancel button.
*
* Calls {@link #createCancelButton()} to create the cancel button and
* registers an {@link ActionListener} that delegates actions to the
* {@link ActionListener} returned by {@link #getCancelAction()}, if
* needed.
*
* @return the cancel button
*/
public final JButton getCancelButton() {
if (cancelButton == null) {
cancelButton = createCancelButton();
cancelButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
getCancelAction().actionPerformed(e);
}
});
}
return cancelButton;
}
/**
* Creates and returns the cancel button.
*
* Override to use a custom cancel button.
*
* @see #getCancelButton()
* @return the cancel button
*/
protected JButton createCancelButton() {
BuddyButton btn = new BuddyButton();
return btn;
}
/**
* Returns the action that is invoked when the enter key is pressed or the
* find button is clicked. If no action has been set, a new instance of
* {@link FindAction} will be returned.
*
* @return the find action
*/
public final ActionListener getFindAction() {
ActionListener a = NativeSearchFieldSupport.getFindAction(this);
if (a == null) {
a = new FindAction();
}
return a;
}
/**
* Sets the action that is invoked when the enter key is pressed or the find
* button is clicked.
*
* @return the find action
*/
public final void setFindAction(ActionListener findAction) {
NativeSearchFieldSupport.setFindAction(this, findAction);
}
/**
* Returns the find button.
*
* Calls {@link #createFindButton()} to create the find button and registers
* an {@link ActionListener} that delegates actions to the
* {@link ActionListener} returned by {@link #getFindAction()}, if needed.
*
* @return the find button
*/
public final JButton getFindButton() {
if (findButton == null) {
findButton = createFindButton();
findButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
getFindAction().actionPerformed(e);
}
});
}
return findButton;
}
/**
* Creates and returns the find button. The buttons action is set to the
* action returned by {@link #getSearchAction()}.
*
* Override to use a custom find button.
*
* @see #getFindButton()
* @return the find button
*/
protected JButton createFindButton() {
BuddyButton btn = new BuddyButton();
return btn;
}
/**
* Returns the popup button. If a find popup menu is set, it will be
* displayed when this button is clicked.
*
* This button will only be visible, if {@link #isUseSeperatePopupButton()}
* returns true
. Otherwise the popup menu will be displayed
* when the find button is clicked.
*
* @return the popup button
*/
public final JButton getPopupButton() {
if (popupButton == null) {
popupButton = createPopupButton();
}
return popupButton;
}
/**
* Creates and returns the popup button. Override to use a custom popup
* button.
*
* @see #getPopupButton()
* @return the popup button
*/
protected JButton createPopupButton() {
return new BuddyButton();
}
/**
* Returns true
if the popup button should be visible and
* used for displaying the find popup menu. Otherwise, the find popup menu
* will be displayed when the find button is clicked.
*
* @return true
if the popup button should be used
*/
public boolean isUseSeperatePopupButton() {
return useSeperatePopupButton;
}
/**
* Set if the popup button should be used for displaying the find popup
* menu.
*
* @param useSeperatePopupButton
*/
public void setUseSeperatePopupButton(boolean useSeperatePopupButton) {
useSeperatePopupButtonSet = true;
firePropertyChange("useSeperatePopupButton", this.useSeperatePopupButton,
this.useSeperatePopupButton = useSeperatePopupButton);
}
public boolean isUseNativeSearchFieldIfPossible() {
return NativeSearchFieldSupport.isSearchField(this);
}
public void setUseNativeSearchFieldIfPossible(boolean useNativeSearchFieldIfPossible) {
TextUIWrapper.getDefaultWrapper().uninstall(this);
NativeSearchFieldSupport.setSearchField(this, useNativeSearchFieldIfPossible);
TextUIWrapper.getDefaultWrapper().install(this, true);
updateUI();
}
/**
* Updates the cancel, find and popup buttons enabled state in addition to
* setting the search fields editable state.
*
* @see #updateButtonState()
* @see javax.swing.text.JTextComponent#setEditable(boolean)
*/
@Override
public void setEditable(boolean b) {
super.setEditable(b);
updateButtonState();
}
/**
* Updates the cancel, find and popup buttons enabled state in addition to
* setting the search fields enabled state.
*
* @see #updateButtonState()
* @see javax.swing.text.JTextComponent#setEnabled(boolean)
*/
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
updateButtonState();
}
/**
* Enables the cancel action if this search field is editable and enabled,
* otherwise it will be disabled. Enabled the search action and popup button
* if this search field is enabled, otherwise it will be disabled.
*/
protected void updateButtonState() {
getCancelButton().setEnabled(isEditable() & isEnabled());
getFindButton().setEnabled(isEnabled());
getPopupButton().setEnabled(isEnabled());
}
/**
* Sets the popup menu that will be displayed when the popup button is
* clicked. If a find popup menu is set and
* {@link #isUseSeperatePopupButton()} returns false
, the
* popup button will be displayed instead of the find button. Otherwise the
* popup button will be displayed in addition to the find button.
*
* The find popup menu is managed using {@link NativeSearchFieldSupport} to
* achieve compatibility with the native search field support provided by
* the Mac Look And Feel since Mac OS 10.5.
*
* If a recent searches save key has been set and therefore a recent
* searches popup menu is installed, this method does nothing. You must
* first remove the recent searches save key, by calling
* {@link #setRecentSearchesSaveKey(String)} with a null
* parameter.
*
* @see #setRecentSearchesSaveKey(String)
* @see RecentSearches
* @param findPopupMenu
* the popup menu, which will be displayed when the popup button
* is clicked
*/
public void setFindPopupMenu(JPopupMenu findPopupMenu) {
if (isManagingRecentSearches()) {
return;
}
NativeSearchFieldSupport.setFindPopupMenu(this, findPopupMenu);
}
/**
* Returns the find popup menu.
*
* @see #setFindPopupMenu(JPopupMenu)
* @return the find popup menu
*/
public JPopupMenu getFindPopupMenu() {
return NativeSearchFieldSupport.getFindPopupMenu(this);
}
/**
* TODO
*
* @return
*/
public final boolean isManagingRecentSearches() {
return recentSearches != null;
}
private boolean isValidRecentSearchesKey(String key) {
return key != null && key.length() > 0;
}
/**
* Returns the key used to persist recent searches.
*
* @see #setRecentSearchesSaveKey(String)
* @return
*/
public String getRecentSearchesSaveKey() {
return recentSearchesSaveKey;
}
/**
* Installs and manages a recent searches popup menu as the find popup menu,
* if recentSearchesSaveKey
is not null. Otherwise, removes
* the popup menu and stops managing recent searches.
*
* @see #setFindAction(ActionListener)
* @see #isManagingRecentSearches()
* @see RecentSearches
*
* @param recentSearchesSaveKey
* this key is used to persist the recent searches.
*/
public void setRecentSearchesSaveKey(String recentSearchesSaveKey) {
String oldName = getRecentSearchesSaveKey();
this.recentSearchesSaveKey = recentSearchesSaveKey;
if (recentSearches != null) {
// set null before uninstalling. otherwise the popup menu is not
// allowed to be changed.
RecentSearches rs = recentSearches;
recentSearches = null;
rs.uninstall(this);
}
if (isValidRecentSearchesKey(recentSearchesSaveKey)) {
recentSearches = new RecentSearches(recentSearchesSaveKey);
recentSearches.install(this);
}
firePropertyChange("recentSearchesSaveKey", oldName, this.recentSearchesSaveKey);
}
/**
* TODO
*
* @return
*/
public RecentSearches getRecentSearches() {
return recentSearches;
}
/**
* Returns the {@link Timer} used to delay the firing of action events in
* instant search mode when the user enters text.
*
* This timer calls {@link #postActionEvent()}.
*
* @return the {@link Timer} used to delay the firing of action events
*/
public Timer getInstantSearchTimer() {
if (instantSearchTimer == null) {
instantSearchTimer = new Timer(0, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
postActionEvent();
}
});
instantSearchTimer.setRepeats(false);
}
return instantSearchTimer;
}
/**
* Returns true
if this search field is the focus owner or
* the find popup menu is visible.
*
* This is a hack to make the search field paint the focus indicator in Mac
* OS X Aqua when the find popup menu is visible.
*
* @return true
if this search field is the focus owner or
* the find popup menu is visible
*/
@Override
public boolean hasFocus() {
if (getFindPopupMenu() != null && getFindPopupMenu().isVisible()) {
return true;
}
return super.hasFocus();
}
/**
* Overriden to also update the find popup menu if set.
*/
@Override
public void updateUI() {
super.updateUI();
if (getFindPopupMenu() != null) {
SwingUtilities.updateComponentTreeUI(getFindPopupMenu());
}
}
/**
* Hack to enable the UI delegate to set default values depending on the
* current Look and Feel, without overriding custom values.
*/
@Override
public void setPromptFontStyle(Integer fontStyle) {
super.setPromptFontStyle(fontStyle);
promptFontStyleSet = true;
}
/**
* Hack to enable the UI delegate to set default values depending on the
* current Look and Feel, without overriding custom values.
*
* @param propertyName
* the name of the property to change
* @param value
* the new value of the property
*/
public void customSetUIProperty(String propertyName, Object value) {
customSetUIProperty(propertyName, value, false);
}
/**
* Hack to enable the UI delegate to set default values depending on the
* current Look and Feel, without overriding custom values.
*
* @param propertyName
* the name of the property to change
* @param value
* the new value of the property
* @param override
* override custom values
*/
public void customSetUIProperty(String propertyName, Object value, boolean override) {
if (propertyName == "useSeperatePopupButton") {
if (!useSeperatePopupButtonSet || override) {
setUseSeperatePopupButton(((Boolean) value).booleanValue());
useSeperatePopupButtonSet = false;
}
} else if (propertyName == "layoutStyle") {
if (!layoutStyleSet || override) {
setLayoutStyle(LayoutStyle.valueOf(value.toString()));
layoutStyleSet = false;
}
} else if (propertyName == "promptFontStyle") {
if (!promptFontStyleSet || override) {
setPromptFontStyle((Integer) value);
promptFontStyleSet = false;
}
} else {
throw new IllegalArgumentException();
}
}
/**
* Overriden to prevent any delayed {@link ActionEvent}s from being sent
* after posting this action.
*
* For example, if the current {@link SearchMode} is
* {@link SearchMode#INSTANT} and the instant search delay is greater 0. The
* user enters some text and presses enter. This method will be invoked
* immediately because the users presses enter. However, this method would
* be invoked after the instant search delay, if we would not prevent it
* here.
*/
@Override
public void postActionEvent() {
getInstantSearchTimer().stop();
super.postActionEvent();
}
/**
* Invoked when the the cancel button or the 'Esc' key is pressed. Sets the
* text in the search field to null
.
*
*/
class ClearAction extends AbstractAction {
public ClearAction() {
putValue(SHORT_DESCRIPTION, "Clear Search Text");
}
/**
* Calls {@link #clear()}.
*/
@Override
public void actionPerformed(ActionEvent e) {
clear();
}
/**
* Sets the search field's text to null
and requests the
* focus for the search field.
*/
public void clear() {
setText(null);
requestFocusInWindow();
}
}
/**
* Invoked when the find button is pressed.
*/
public class FindAction extends AbstractAction {
public FindAction() {
}
/**
* In regular search mode posts an action event if the search field is
* the focus owner.
*
* Also requests the focus for the search field and selects the whole
* text.
*/
@Override
public void actionPerformed(ActionEvent e) {
if (isFocusOwner() && isRegularSearchMode()) {
postActionEvent();
}
requestFocusInWindow();
selectAll();
}
}
}