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

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.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 
 * 
 */
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() { 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() { public void propertyChange(PropertyChangeEvent evt) { JPopupMenu oldPopup = (JPopupMenu) evt.getOldValue(); firePropertyChange("findPopupMenu", oldPopup, evt.getNewValue()); } }); addPropertyChangeListener(NativeSearchFieldSupport.CANCEL_ACTION_PROPERTY, new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { ActionListener oldAction = (ActionListener) evt.getOldValue(); firePropertyChange("cancelAction", oldAction, evt.getNewValue()); } }); addPropertyChangeListener(NativeSearchFieldSupport.FIND_ACTION_PROPERTY, new PropertyChangeListener() { 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) */ 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() { 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() { 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) */ 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) */ 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() { 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 */ public boolean hasFocus() { if (getFindPopupMenu() != null && getFindPopupMenu().isVisible()) { return true; } return super.hasFocus(); } /** * Overriden to also update the find popup menu if set. */ 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. */ 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. */ 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()}. */ 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. */ public void actionPerformed(ActionEvent e) { if (isFocusOwner() && isRegularSearchMode()) { postActionEvent(); } requestFocusInWindow(); selectAll(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy