1
package org.jdesktop.swingx;
3
import java.awt.Insets;
4
import java.awt.event.ActionEvent;
5
import java.awt.event.ActionListener;
6
import java.awt.event.KeyAdapter;
7
import java.awt.event.KeyEvent;
8
import java.beans.PropertyChangeEvent;
9
import java.beans.PropertyChangeListener;
11
import javax.swing.AbstractAction;
12
import javax.swing.JButton;
13
import javax.swing.JPopupMenu;
14
import javax.swing.KeyStroke;
15
import javax.swing.SwingUtilities;
16
import javax.swing.Timer;
17
import javax.swing.text.Document;
19
import org.jdesktop.swingx.plaf.SearchFieldAddon;
20
import org.jdesktop.swingx.plaf.LookAndFeelAddons;
21
import org.jdesktop.swingx.plaf.TextUIWrapper;
22
import org.jdesktop.swingx.plaf.UIManagerExt;
23
import org.jdesktop.swingx.prompt.BuddyButton;
24
import org.jdesktop.swingx.search.NativeSearchFieldSupport;
25
import org.jdesktop.swingx.search.RecentSearches;
28
* A text field with a find icon in which the user enters text that identifies
29
* items to search for.
31
* JXSearchField almost looks and behaves like a native Windows Vista search
32
* box, a Mac OS X search field, or a search field like the one used in Mozilla
33
* Thunderbird 2.0 - depending on the current look and feel.
35
* JXSearchField is a text field that contains a find button and a cancel
36
* button. The find button normally displays a lens icon appropriate for the
37
* current look and feel. The cancel button is used to clear the text and
38
* therefore only visible when text is present. It normally displays a 'x' like
39
* icon. Text can also be cleared, using the 'Esc' key.
41
* The position of the find and cancel buttons can be customized by either
42
* changing the search fields (text) margin or button margin, or by changing the
43
* {@link LayoutStyle}.
45
* JXSearchField supports two different search modes: {@link SearchMode#INSTANT}
46
* and {@link SearchMode#REGULAR}.
48
* A search can be performed by registering an {@link ActionListener}. The
49
* {@link ActionEvent}s command property contains the text to search for. The
50
* search should be cancelled, when the command text is empty or null.
53
* @author Peter Weishapl <petw@gmx.net>
56
public class JXSearchField extends JXTextField {
58
* The default instant search delay.
60
private static final int DEFAULT_INSTANT_SEARCH_DELAY = 180;
62
* The key used to invoke the cancel action.
64
private static final KeyStroke CANCEL_KEY = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
67
* Defines, how the find and cancel button are layouted.
69
public enum LayoutStyle {
72
* In VISTA layout style, the find button is placed on the right side of
73
* the search field. If text is entered, the find button is replaced by
74
* the cancel button when the actual search mode is
75
* {@link SearchMode#INSTANT}. When the search mode is
76
* {@link SearchMode#REGULAR} the find button will always stay visible
77
* and the cancel button will never be shown. However, 'Escape' can
78
* still be pressed to clear the text.
84
* In MAC layout style, the find button is placed on the left side of
85
* the search field and the cancel button on the right side. The cancel
86
* button is only visible when text is present.
93
* Defines when action events are posted.
95
public enum SearchMode {
98
* In REGULAR search mode, an action event is fired, when the user
99
* presses enter or clicks the find button.
102
* However, if a find popup menu is set and layout style is
103
* {@link LayoutStyle#MAC}, no action will be fired, when the find
104
* button is clicked, because instead the popup menu is shown. A search
105
* can therefore only be triggered, by pressing the enter key.
108
* The find button can have a rollover and a pressed icon, defined by
109
* the "SearchField.rolloverIcon" and "SearchField.pressedIcon" UI
110
* properties. When a find popup menu is set,
111
* "SearchField.popupRolloverIcon" and "SearchField.popupPressedIcon"
118
* In INSTANT search mode, an action event is fired, when the user
119
* presses enter or changes the search text.
121
* The action event is delayed about the number of milliseconds
122
* specified by {@link JXSearchField#getInstantSearchDelay()}.
124
* No rollover and pressed icon is used for the find button.
129
// ensure at least the default ui is registered
131
LookAndFeelAddons.contribute(new SearchFieldAddon());
134
private JButton findButton;
136
private JButton cancelButton;
138
private JButton popupButton;
140
private LayoutStyle layoutStyle;
142
private SearchMode searchMode = SearchMode.INSTANT;
144
private boolean useSeperatePopupButton;
146
private boolean useSeperatePopupButtonSet;
148
private boolean layoutStyleSet;
150
private int instantSearchDelay = DEFAULT_INSTANT_SEARCH_DELAY;
152
private boolean promptFontStyleSet;
154
private Timer instantSearchTimer;
156
private String recentSearchesSaveKey;
158
private RecentSearches recentSearches;
161
* Creates a new search field with a default prompt.
163
public JXSearchField() {
164
this(UIManagerExt.getString("SearchField.prompt"));
168
* Creates a new search field with the given prompt and
169
* {@link SearchMode#INSTANT}.
173
public JXSearchField(String prompt) {
175
// use the native search field if possible.
176
setUseNativeSearchFieldIfPossible(true);
177
// install default actions
178
setCancelAction(new ClearAction());
179
setFindAction(new FindAction());
181
// We cannot register the ClearAction through the Input- and
182
// ActionMap because ToolTipManager registers the escape key with an
183
// action that hides the tooltip every time the tooltip is changed and
184
// then the ClearAction will never be called.
185
addKeyListener(new KeyAdapter() {
186
public void keyPressed(KeyEvent e) {
187
if (CANCEL_KEY.equals(KeyStroke.getKeyStroke(e.getKeyCode(), e.getModifiers()))) {
188
getCancelAction().actionPerformed(
189
new ActionEvent(JXSearchField.this, e.getID(), KeyEvent.getKeyText(e.getKeyCode())));
194
// Map specific native properties to general JXSearchField properties.
195
addPropertyChangeListener(NativeSearchFieldSupport.FIND_POPUP_PROPERTY, new PropertyChangeListener() {
196
public void propertyChange(PropertyChangeEvent evt) {
197
JPopupMenu oldPopup = (JPopupMenu) evt.getOldValue();
198
firePropertyChange("findPopupMenu", oldPopup, evt.getNewValue());
201
addPropertyChangeListener(NativeSearchFieldSupport.CANCEL_ACTION_PROPERTY, new PropertyChangeListener() {
202
public void propertyChange(PropertyChangeEvent evt) {
203
ActionListener oldAction = (ActionListener) evt.getOldValue();
204
firePropertyChange("cancelAction", oldAction, evt.getNewValue());
207
addPropertyChangeListener(NativeSearchFieldSupport.FIND_ACTION_PROPERTY, new PropertyChangeListener() {
208
public void propertyChange(PropertyChangeEvent evt) {
209
ActionListener oldAction = (ActionListener) evt.getOldValue();
210
firePropertyChange("findAction", oldAction, evt.getNewValue());
216
* Returns the current {@link SearchMode}.
218
* @return the current {@link SearchMode}.
220
public SearchMode getSearchMode() {
225
* Returns <code>true</code> if the current {@link SearchMode} is
226
* {@link SearchMode#INSTANT}.
228
* @return <code>true</code> if the current {@link SearchMode} is
229
* {@link SearchMode#INSTANT}
231
public boolean isInstantSearchMode() {
232
return SearchMode.INSTANT.equals(getSearchMode());
236
* Returns <code>true</code> if the current {@link SearchMode} is
237
* {@link SearchMode#REGULAR}.
239
* @return <code>true</code> if the current {@link SearchMode} is
240
* {@link SearchMode#REGULAR}
242
public boolean isRegularSearchMode() {
243
return SearchMode.REGULAR.equals(getSearchMode());
247
* Sets the current search mode. See {@link SearchMode} for a description of
248
* the different search modes.
251
* {@link SearchMode#INSTANT} or {@link SearchMode#REGULAR}
253
public void setSearchMode(SearchMode searchMode) {
254
firePropertyChange("searchMode", this.searchMode, this.searchMode = searchMode);
258
* Get the instant search delay in milliseconds. The default delay is 50
261
* @see {@link #setInstantSearchDelay(int)}
262
* @return the instant search delay in milliseconds
264
public int getInstantSearchDelay() {
265
return instantSearchDelay;
269
* Set the instant search delay in milliseconds. In
270
* {@link SearchMode#INSTANT}, when the user changes the text, an action
271
* event will be fired after the specified instant search delay.
273
* It is recommended to use a instant search delay to avoid the firing of
274
* unnecessary events. For example when the user replaces the whole text
275
* with a different text the search fields underlying {@link Document}
276
* typically fires 2 document events. The first one, because the old text is
277
* removed and the second one because the new text is inserted. If the
278
* instant search delay is 0, this would result in 2 action events being
279
* fired. When a instant search delay is used, the first document event
280
* typically is ignored, because the second one is fired before the delay is
281
* over, which results in a correct behavior because only the last and only
282
* relevant event will be delivered.
284
* @param instantSearchDelay
286
public void setInstantSearchDelay(int instantSearchDelay) {
287
firePropertyChange("instantSearchDelay", this.instantSearchDelay, this.instantSearchDelay = instantSearchDelay);
291
* Get the current {@link LayoutStyle}.
295
public LayoutStyle getLayoutStyle() {
300
* Returns <code>true</code> if the current {@link LayoutStyle} is
301
* {@link LayoutStyle#VISTA}.
305
public boolean isVistaLayoutStyle() {
306
return LayoutStyle.VISTA.equals(getLayoutStyle());
310
* Returns <code>true</code> if the current {@link LayoutStyle} is
311
* {@link LayoutStyle#MAC}.
315
public boolean isMacLayoutStyle() {
316
return LayoutStyle.MAC.equals(getLayoutStyle());
320
* Set the current {@link LayoutStyle}. See {@link LayoutStyle} for a
321
* description of how this affects layout and behavior of the search field.
324
* {@link LayoutStyle#MAC} or {@link LayoutStyle#VISTA}
326
public void setLayoutStyle(LayoutStyle layoutStyle) {
327
layoutStyleSet = true;
328
firePropertyChange("layoutStyle", this.layoutStyle, this.layoutStyle = layoutStyle);
332
* Set the margin space around the search field's text.
334
* @see javax.swing.text.JTextComponent#setMargin(java.awt.Insets)
336
public void setMargin(Insets m) {
341
* Returns the cancel action, or an instance of {@link ClearAction}, if
344
* @return the cancel action
346
public final ActionListener getCancelAction() {
347
ActionListener a = NativeSearchFieldSupport.getCancelAction(this);
349
a = new ClearAction();
355
* Sets the action that is invoked, when the user presses the 'Esc' key or
356
* clicks the cancel button.
358
* @param cancelAction
360
public final void setCancelAction(ActionListener cancelAction) {
361
NativeSearchFieldSupport.setCancelAction(this, cancelAction);
365
* Returns the cancel button.
367
* Calls {@link #createCancelButton()} to create the cancel button and
368
* registers an {@link ActionListener} that delegates actions to the
369
* {@link ActionListener} returned by {@link #getCancelAction()}, if
372
* @return the cancel button
374
public final JButton getCancelButton() {
375
if (cancelButton == null) {
376
cancelButton = createCancelButton();
377
cancelButton.addActionListener(new ActionListener() {
378
public void actionPerformed(ActionEvent e) {
379
getCancelAction().actionPerformed(e);
387
* Creates and returns the cancel button.
389
* Override to use a custom cancel button.
391
* @see #getCancelButton()
392
* @return the cancel button
394
protected JButton createCancelButton() {
395
BuddyButton btn = new BuddyButton();
401
* Returns the action that is invoked when the enter key is pressed or the
402
* find button is clicked. If no action has been set, a new instance of
403
* {@link FindAction} will be returned.
405
* @return the find action
407
public final ActionListener getFindAction() {
408
ActionListener a = NativeSearchFieldSupport.getFindAction(this);
410
a = new FindAction();
416
* Sets the action that is invoked when the enter key is pressed or the find
419
* @return the find action
421
public final void setFindAction(ActionListener findAction) {
422
NativeSearchFieldSupport.setFindAction(this, findAction);
426
* Returns the find button.
428
* Calls {@link #createFindButton()} to create the find button and registers
429
* an {@link ActionListener} that delegates actions to the
430
* {@link ActionListener} returned by {@link #getFindAction()}, if needed.
432
* @return the find button
434
public final JButton getFindButton() {
435
if (findButton == null) {
436
findButton = createFindButton();
437
findButton.addActionListener(new ActionListener() {
438
public void actionPerformed(ActionEvent e) {
439
getFindAction().actionPerformed(e);
447
* Creates and returns the find button. The buttons action is set to the
448
* action returned by {@link #getSearchAction()}.
450
* Override to use a custom find button.
452
* @see #getFindButton()
453
* @return the find button
455
protected JButton createFindButton() {
456
BuddyButton btn = new BuddyButton();
462
* Returns the popup button. If a find popup menu is set, it will be
463
* displayed when this button is clicked.
465
* This button will only be visible, if {@link #isUseSeperatePopupButton()}
466
* returns <code>true</code>. Otherwise the popup menu will be displayed
467
* when the find button is clicked.
469
* @return the popup button
471
public final JButton getPopupButton() {
472
if (popupButton == null) {
473
popupButton = createPopupButton();
479
* Creates and returns the popup button. Override to use a custom popup
482
* @see #getPopupButton()
483
* @return the popup button
485
protected JButton createPopupButton() {
486
return new BuddyButton();
490
* Returns <code>true</code> if the popup button should be visible and
491
* used for displaying the find popup menu. Otherwise, the find popup menu
492
* will be displayed when the find button is clicked.
494
* @return <code>true</code> if the popup button should be used
496
public boolean isUseSeperatePopupButton() {
497
return useSeperatePopupButton;
501
* Set if the popup button should be used for displaying the find popup
504
* @param useSeperatePopupButton
506
public void setUseSeperatePopupButton(boolean useSeperatePopupButton) {
507
useSeperatePopupButtonSet = true;
508
firePropertyChange("useSeperatePopupButton", this.useSeperatePopupButton,
509
this.useSeperatePopupButton = useSeperatePopupButton);
512
public boolean isUseNativeSearchFieldIfPossible() {
513
return NativeSearchFieldSupport.isSearchField(this);
516
public void setUseNativeSearchFieldIfPossible(boolean useNativeSearchFieldIfPossible) {
517
TextUIWrapper.getDefaultWrapper().uninstall(this);
518
NativeSearchFieldSupport.setSearchField(this, useNativeSearchFieldIfPossible);
519
TextUIWrapper.getDefaultWrapper().install(this, true);
524
* Updates the cancel, find and popup buttons enabled state in addition to
525
* setting the search fields editable state.
527
* @see #updateButtonState()
528
* @see javax.swing.text.JTextComponent#setEditable(boolean)
530
public void setEditable(boolean b) {
531
super.setEditable(b);
536
* Updates the cancel, find and popup buttons enabled state in addition to
537
* setting the search fields enabled state.
539
* @see #updateButtonState()
540
* @see javax.swing.text.JTextComponent#setEnabled(boolean)
542
public void setEnabled(boolean enabled) {
543
super.setEnabled(enabled);
548
* Enables the cancel action if this search field is editable and enabled,
549
* otherwise it will be disabled. Enabled the search action and popup button
550
* if this search field is enabled, otherwise it will be disabled.
552
protected void updateButtonState() {
553
getCancelButton().setEnabled(isEditable() & isEnabled());
554
getFindButton().setEnabled(isEnabled());
555
getPopupButton().setEnabled(isEnabled());
559
* Sets the popup menu that will be displayed when the popup button is
560
* clicked. If a find popup menu is set and
561
* {@link #isUseSeperatePopupButton()} returns <code>false</code>, the
562
* popup button will be displayed instead of the find button. Otherwise the
563
* popup button will be displayed in addition to the find button.
565
* The find popup menu is managed using {@link NativeSearchFieldSupport} to
566
* achieve compatibility with the native search field support provided by
567
* the Mac Look And Feel since Mac OS 10.5.
569
* If a recent searches save key has been set and therefore a recent
570
* searches popup menu is installed, this method does nothing. You must
571
* first remove the recent searches save key, by calling
572
* {@link #setRecentSearchesSaveKey(String)} with a <code>null</code>
575
* @see #setRecentSearchesSaveKey(String)
576
* @see RecentSearches
577
* @param findPopupMenu
578
* the popup menu, which will be displayed when the popup button
581
public void setFindPopupMenu(JPopupMenu findPopupMenu) {
582
if (isManagingRecentSearches()) {
586
NativeSearchFieldSupport.setFindPopupMenu(this, findPopupMenu);
590
* Returns the find popup menu.
592
* @see #setFindPopupMenu(JPopupMenu)
593
* @return the find popup menu
595
public JPopupMenu getFindPopupMenu() {
596
return NativeSearchFieldSupport.getFindPopupMenu(this);
604
public final boolean isManagingRecentSearches() {
605
return recentSearches != null;
608
private boolean isValidRecentSearchesKey(String key) {
609
return key != null && key.length() > 0;
613
* Returns the key used to persist recent searches.
615
* @see #setRecentSearchesSaveKey(String)
618
public String getRecentSearchesSaveKey() {
619
return recentSearchesSaveKey;
623
* Installs and manages a recent searches popup menu as the find popup menu,
624
* if <code>recentSearchesSaveKey</code> is not null. Otherwise, removes
625
* the popup menu and stops managing recent searches.
627
* @see #setFindAction(ActionListener)
628
* @see #isManagingRecentSearches()
629
* @see RecentSearches
631
* @param recentSearchesSaveKey
632
* this key is used to persist the recent searches.
634
public void setRecentSearchesSaveKey(String recentSearchesSaveKey) {
635
String oldName = getRecentSearchesSaveKey();
636
this.recentSearchesSaveKey = recentSearchesSaveKey;
638
if (recentSearches != null) {
639
// set null before uninstalling. otherwise the popup menu is not
640
// allowed to be changed.
641
RecentSearches rs = recentSearches;
642
recentSearches = null;
646
if (isValidRecentSearchesKey(recentSearchesSaveKey)) {
647
recentSearches = new RecentSearches(recentSearchesSaveKey);
648
recentSearches.install(this);
651
firePropertyChange("recentSearchesSaveKey", oldName, this.recentSearchesSaveKey);
659
public RecentSearches getRecentSearches() {
660
return recentSearches;
664
* Returns the {@link Timer} used to delay the firing of action events in
665
* instant search mode when the user enters text.
667
* This timer calls {@link #postActionEvent()}.
669
* @return the {@link Timer} used to delay the firing of action events
671
public Timer getInstantSearchTimer() {
672
if (instantSearchTimer == null) {
673
instantSearchTimer = new Timer(0, new ActionListener() {
674
public void actionPerformed(ActionEvent e) {
678
instantSearchTimer.setRepeats(false);
680
return instantSearchTimer;
684
* Returns <code>true</code> if this search field is the focus owner or
685
* the find popup menu is visible.
687
* This is a hack to make the search field paint the focus indicator in Mac
688
* OS X Aqua when the find popup menu is visible.
690
* @return <code>true</code> if this search field is the focus owner or
691
* the find popup menu is visible
693
public boolean hasFocus() {
694
if (getFindPopupMenu() != null && getFindPopupMenu().isVisible()) {
697
return super.hasFocus();
701
* Overriden to also update the find popup menu if set.
703
public void updateUI() {
705
if (getFindPopupMenu() != null) {
706
SwingUtilities.updateComponentTreeUI(getFindPopupMenu());
711
* Hack to enable the UI delegate to set default values depending on the
712
* current Look and Feel, without overriding custom values.
714
public void setPromptFontStyle(Integer fontStyle) {
715
super.setPromptFontStyle(fontStyle);
716
promptFontStyleSet = true;
720
* Hack to enable the UI delegate to set default values depending on the
721
* current Look and Feel, without overriding custom values.
723
* @param propertyName
724
* the name of the property to change
726
* the new value of the property
728
public void customSetUIProperty(String propertyName, Object value) {
729
customSetUIProperty(propertyName, value, false);
733
* Hack to enable the UI delegate to set default values depending on the
734
* current Look and Feel, without overriding custom values.
736
* @param propertyName
737
* the name of the property to change
739
* the new value of the property
741
* override custom values
743
public void customSetUIProperty(String propertyName, Object value, boolean override) {
744
if (propertyName == "useSeperatePopupButton") {
745
if (!useSeperatePopupButtonSet || override) {
746
setUseSeperatePopupButton(((Boolean) value).booleanValue());
747
useSeperatePopupButtonSet = false;
749
} else if (propertyName == "layoutStyle") {
750
if (!layoutStyleSet || override) {
751
setLayoutStyle(LayoutStyle.valueOf(value.toString()));
752
layoutStyleSet = false;
754
} else if (propertyName == "promptFontStyle") {
755
if (!promptFontStyleSet || override) {
756
setPromptFontStyle((Integer) value);
757
promptFontStyleSet = false;
760
throw new IllegalArgumentException();
765
* Overriden to prevent any delayed {@link ActionEvent}s from being sent
766
* after posting this action.
768
* For example, if the current {@link SearchMode} is
769
* {@link SearchMode#INSTANT} and the instant search delay is greater 0. The
770
* user enters some text and presses enter. This method will be invoked
771
* immediately because the users presses enter. However, this method would
772
* be invoked after the instant search delay, if we would not prevent it
775
public void postActionEvent() {
776
getInstantSearchTimer().stop();
777
super.postActionEvent();
781
* Invoked when the the cancel button or the 'Esc' key is pressed. Sets the
782
* text in the search field to <code>null</code>.
785
class ClearAction extends AbstractAction {
786
public ClearAction() {
787
putValue(SHORT_DESCRIPTION, "Clear Search Text");
791
* Calls {@link #clear()}.
793
public void actionPerformed(ActionEvent e) {
798
* Sets the search field's text to <code>null</code> and requests the
799
* focus for the search field.
801
public void clear() {
803
requestFocusInWindow();
808
* Invoked when the find button is pressed.
810
public class FindAction extends AbstractAction {
811
public FindAction() {
815
* In regular search mode posts an action event if the search field is
818
* Also requests the focus for the search field and selects the whole
821
public void actionPerformed(ActionEvent e) {
822
if (isFocusOwner() && isRegularSearchMode()) {
825
requestFocusInWindow();