Package org.jdesktop.swingx

Source Code of org.jdesktop.swingx.JXSearchField

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 <petw@gmx.net>
*
*/
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 {
    /**
     * <p>
     * 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.
     * </p>
     */
    VISTA,
    /**
     * <p>
     * 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.
     * </p>
     */
    MAC
  };

  /**
   * Defines when action events are posted.
   */
  public enum SearchMode {
    /**
     * <p>
     * In REGULAR search mode, an action event is fired, when the user
     * presses enter or clicks the find button.
     * </p>
     * <p>
     * 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.
     * </p>
     * <p>
     * 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.
     * </p>
     *
     */
    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 <code>true</code> if the current {@link SearchMode} is
   * {@link SearchMode#INSTANT}.
   *
   * @return <code>true</code> if the current {@link SearchMode} is
   *         {@link SearchMode#INSTANT}
   */
  public boolean isInstantSearchMode() {
    return SearchMode.INSTANT.equals(getSearchMode());
  }

  /**
   * Returns <code>true</code> if the current {@link SearchMode} is
   * {@link SearchMode#REGULAR}.
   *
   * @return <code>true</code> 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 <code>true</code> if the current {@link LayoutStyle} is
   * {@link LayoutStyle#VISTA}.
   *
   * @return
   */
  public boolean isVistaLayoutStyle() {
    return LayoutStyle.VISTA.equals(getLayoutStyle());
  }

  /**
   * Returns <code>true</code> 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 <code>true</code>. 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 <code>true</code> 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 <code>true</code> 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 <code>false</code>, 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 <code>null</code>
   * 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 <code>recentSearchesSaveKey</code> 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 <code>true</code> 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 <code>true</code> 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 <code>null</code>.
   *
   */
  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 <code>null</code> 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();
    }
  }
}
TOP

Related Classes of org.jdesktop.swingx.JXSearchField

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.