001package org.jdesktop.swingx;
002
003import java.awt.Insets;
004import java.awt.event.ActionEvent;
005import java.awt.event.ActionListener;
006import java.awt.event.KeyAdapter;
007import java.awt.event.KeyEvent;
008import java.beans.PropertyChangeEvent;
009import java.beans.PropertyChangeListener;
010
011import javax.swing.AbstractAction;
012import javax.swing.JButton;
013import javax.swing.JPopupMenu;
014import javax.swing.KeyStroke;
015import javax.swing.SwingUtilities;
016import javax.swing.Timer;
017import javax.swing.text.Document;
018
019import org.jdesktop.beans.JavaBean;
020import org.jdesktop.swingx.plaf.SearchFieldAddon;
021import org.jdesktop.swingx.plaf.LookAndFeelAddons;
022import org.jdesktop.swingx.plaf.TextUIWrapper;
023import org.jdesktop.swingx.plaf.UIManagerExt;
024import org.jdesktop.swingx.prompt.BuddyButton;
025import org.jdesktop.swingx.search.NativeSearchFieldSupport;
026import org.jdesktop.swingx.search.RecentSearches;
027
028/**
029 * A text field with a find icon in which the user enters text that identifies
030 * items to search for.
031 * 
032 * JXSearchField almost looks and behaves like a native Windows Vista search
033 * box, a Mac OS X search field, or a search field like the one used in Mozilla
034 * Thunderbird 2.0 - depending on the current look and feel.
035 * 
036 * JXSearchField is a text field that contains a find button and a cancel
037 * button. The find button normally displays a lens icon appropriate for the
038 * current look and feel. The cancel button is used to clear the text and
039 * therefore only visible when text is present. It normally displays a 'x' like
040 * icon. Text can also be cleared, using the 'Esc' key.
041 * 
042 * The position of the find and cancel buttons can be customized by either
043 * changing the search fields (text) margin or button margin, or by changing the
044 * {@link LayoutStyle}.
045 * 
046 * JXSearchField supports two different search modes: {@link SearchMode#INSTANT}
047 * and {@link SearchMode#REGULAR}.
048 * 
049 * A search can be performed by registering an {@link ActionListener}. The
050 * {@link ActionEvent}s command property contains the text to search for. The
051 * search should be cancelled, when the command text is empty or null.
052 * 
053 * @see RecentSearches
054 * @author Peter Weishapl <petw@gmx.net>
055 * 
056 */
057@JavaBean
058public class JXSearchField extends JXTextField {
059        /**
060         * The default instant search delay.
061         */
062        private static final int DEFAULT_INSTANT_SEARCH_DELAY = 180;
063        /**
064         * The key used to invoke the cancel action.
065         */
066        private static final KeyStroke CANCEL_KEY = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
067
068        /**
069         * Defines, how the find and cancel button are layouted.
070         */
071        public enum LayoutStyle {
072                /**
073                 * <p>
074                 * In VISTA layout style, the find button is placed on the right side of
075                 * the search field. If text is entered, the find button is replaced by
076                 * the cancel button when the actual search mode is
077                 * {@link SearchMode#INSTANT}. When the search mode is
078                 * {@link SearchMode#REGULAR} the find button will always stay visible
079                 * and the cancel button will never be shown. However, 'Escape' can
080                 * still be pressed to clear the text.
081                 * </p>
082                 */
083                VISTA,
084                /**
085                 * <p>
086                 * In MAC layout style, the find button is placed on the left side of
087                 * the search field and the cancel button on the right side. The cancel
088                 * button is only visible when text is present.
089                 * </p>
090                 */
091                MAC
092        };
093
094        /**
095         * Defines when action events are posted.
096         */
097        public enum SearchMode {
098                /**
099                 * <p>
100                 * In REGULAR search mode, an action event is fired, when the user
101                 * presses enter or clicks the find button.
102                 * </p>
103                 * <p>
104                 * However, if a find popup menu is set and layout style is
105                 * {@link LayoutStyle#MAC}, no action will be fired, when the find
106                 * button is clicked, because instead the popup menu is shown. A search
107                 * can therefore only be triggered, by pressing the enter key.
108                 * </p>
109                 * <p>
110                 * The find button can have a rollover and a pressed icon, defined by
111                 * the "SearchField.rolloverIcon" and "SearchField.pressedIcon" UI
112                 * properties. When a find popup menu is set,
113                 * "SearchField.popupRolloverIcon" and "SearchField.popupPressedIcon"
114                 * are used.
115                 * </p>
116                 * 
117                 */
118                REGULAR,
119                /**
120                 * In INSTANT search mode, an action event is fired, when the user
121                 * presses enter or changes the search text.
122                 * 
123                 * The action event is delayed about the number of milliseconds
124                 * specified by {@link JXSearchField#getInstantSearchDelay()}.
125                 * 
126                 * No rollover and pressed icon is used for the find button.
127                 */
128                INSTANT
129        }
130
131        // ensure at least the default ui is registered
132        static {
133                LookAndFeelAddons.contribute(new SearchFieldAddon());
134        }
135
136        private JButton findButton;
137
138        private JButton cancelButton;
139
140        private JButton popupButton;
141
142        private LayoutStyle layoutStyle;
143
144        private SearchMode searchMode = SearchMode.INSTANT;
145
146        private boolean useSeperatePopupButton;
147
148        private boolean useSeperatePopupButtonSet;
149
150        private boolean layoutStyleSet;
151
152        private int instantSearchDelay = DEFAULT_INSTANT_SEARCH_DELAY;
153
154        private boolean promptFontStyleSet;
155
156        private Timer instantSearchTimer;
157
158        private String recentSearchesSaveKey;
159
160        private RecentSearches recentSearches;
161
162        /**
163         * Creates a new search field with a default prompt.
164         */
165        public JXSearchField() {
166                this(UIManagerExt.getString("SearchField.prompt"));
167        }
168
169        /**
170         * Creates a new search field with the given prompt and
171         * {@link SearchMode#INSTANT}.
172         * 
173         * @param prompt
174         */
175        public JXSearchField(String prompt) {
176                super(prompt);
177                // use the native search field if possible.
178                setUseNativeSearchFieldIfPossible(true);
179                // install default actions
180                setCancelAction(new ClearAction());
181                setFindAction(new FindAction());
182
183                // We cannot register the ClearAction through the Input- and
184                // ActionMap because ToolTipManager registers the escape key with an
185                // action that hides the tooltip every time the tooltip is changed and
186                // then the ClearAction will never be called.
187                addKeyListener(new KeyAdapter() {
188                        @Override
189            public void keyPressed(KeyEvent e) {
190                                if (CANCEL_KEY.equals(KeyStroke.getKeyStroke(e.getKeyCode(), e.getModifiers()))) {
191                                        getCancelAction().actionPerformed(
192                                                        new ActionEvent(JXSearchField.this, e.getID(), KeyEvent.getKeyText(e.getKeyCode())));
193                                }
194                        }
195                });
196
197                // Map specific native properties to general JXSearchField properties.
198                addPropertyChangeListener(NativeSearchFieldSupport.FIND_POPUP_PROPERTY, new PropertyChangeListener() {
199                        @Override
200            public void propertyChange(PropertyChangeEvent evt) {
201                                JPopupMenu oldPopup = (JPopupMenu) evt.getOldValue();
202                                firePropertyChange("findPopupMenu", oldPopup, evt.getNewValue());
203                        }
204                });
205                addPropertyChangeListener(NativeSearchFieldSupport.CANCEL_ACTION_PROPERTY, new PropertyChangeListener() {
206                        @Override
207            public void propertyChange(PropertyChangeEvent evt) {
208                                ActionListener oldAction = (ActionListener) evt.getOldValue();
209                                firePropertyChange("cancelAction", oldAction, evt.getNewValue());
210                        }
211                });
212                addPropertyChangeListener(NativeSearchFieldSupport.FIND_ACTION_PROPERTY, new PropertyChangeListener() {
213                        @Override
214            public void propertyChange(PropertyChangeEvent evt) {
215                                ActionListener oldAction = (ActionListener) evt.getOldValue();
216                                firePropertyChange("findAction", oldAction, evt.getNewValue());
217                        }
218                });
219        }
220
221        /**
222         * Returns the current {@link SearchMode}.
223         * 
224         * @return the current {@link SearchMode}.
225         */
226        public SearchMode getSearchMode() {
227                return searchMode;
228        }
229
230        /**
231         * Returns <code>true</code> if the current {@link SearchMode} is
232         * {@link SearchMode#INSTANT}.
233         * 
234         * @return <code>true</code> if the current {@link SearchMode} is
235         *         {@link SearchMode#INSTANT}
236         */
237        public boolean isInstantSearchMode() {
238                return SearchMode.INSTANT.equals(getSearchMode());
239        }
240
241        /**
242         * Returns <code>true</code> if the current {@link SearchMode} is
243         * {@link SearchMode#REGULAR}.
244         * 
245         * @return <code>true</code> if the current {@link SearchMode} is
246         *         {@link SearchMode#REGULAR}
247         */
248        public boolean isRegularSearchMode() {
249                return SearchMode.REGULAR.equals(getSearchMode());
250        }
251
252        /**
253         * Sets the current search mode. See {@link SearchMode} for a description of
254         * the different search modes.
255         * 
256         * @param searchMode
257         *            {@link SearchMode#INSTANT} or {@link SearchMode#REGULAR}
258         */
259        public void setSearchMode(SearchMode searchMode) {
260                firePropertyChange("searchMode", this.searchMode, this.searchMode = searchMode);
261        }
262
263        /**
264         * Get the instant search delay in milliseconds. The default delay is 50
265         * Milliseconds.
266         * 
267         * @see {@link #setInstantSearchDelay(int)}
268         * @return the instant search delay in milliseconds
269         */
270        public int getInstantSearchDelay() {
271                return instantSearchDelay;
272        }
273
274        /**
275         * Set the instant search delay in milliseconds. In
276         * {@link SearchMode#INSTANT}, when the user changes the text, an action
277         * event will be fired after the specified instant search delay.
278         * 
279         * It is recommended to use a instant search delay to avoid the firing of
280         * unnecessary events. For example when the user replaces the whole text
281         * with a different text the search fields underlying {@link Document}
282         * typically fires 2 document events. The first one, because the old text is
283         * removed and the second one because the new text is inserted. If the
284         * instant search delay is 0, this would result in 2 action events being
285         * fired. When a instant search delay is used, the first document event
286         * typically is ignored, because the second one is fired before the delay is
287         * over, which results in a correct behavior because only the last and only
288         * relevant event will be delivered.
289         * 
290         * @param instantSearchDelay
291         */
292        public void setInstantSearchDelay(int instantSearchDelay) {
293                firePropertyChange("instantSearchDelay", this.instantSearchDelay, this.instantSearchDelay = instantSearchDelay);
294        }
295
296        /**
297         * Get the current {@link LayoutStyle}.
298         * 
299         * @return
300         */
301        public LayoutStyle getLayoutStyle() {
302                return layoutStyle;
303        }
304
305        /**
306         * Returns <code>true</code> if the current {@link LayoutStyle} is
307         * {@link LayoutStyle#VISTA}.
308         * 
309         * @return
310         */
311        public boolean isVistaLayoutStyle() {
312                return LayoutStyle.VISTA.equals(getLayoutStyle());
313        }
314
315        /**
316         * Returns <code>true</code> if the current {@link LayoutStyle} is
317         * {@link LayoutStyle#MAC}.
318         * 
319         * @return
320         */
321        public boolean isMacLayoutStyle() {
322                return LayoutStyle.MAC.equals(getLayoutStyle());
323        }
324
325        /**
326         * Set the current {@link LayoutStyle}. See {@link LayoutStyle} for a
327         * description of how this affects layout and behavior of the search field.
328         * 
329         * @param layoutStyle
330         *            {@link LayoutStyle#MAC} or {@link LayoutStyle#VISTA}
331         */
332        public void setLayoutStyle(LayoutStyle layoutStyle) {
333                layoutStyleSet = true;
334                firePropertyChange("layoutStyle", this.layoutStyle, this.layoutStyle = layoutStyle);
335        }
336
337        /**
338         * Set the margin space around the search field's text.
339         * 
340         * @see javax.swing.text.JTextComponent#setMargin(java.awt.Insets)
341         */
342        @Override
343    public void setMargin(Insets m) {
344                super.setMargin(m);
345        }
346
347        /**
348         * Returns the cancel action, or an instance of {@link ClearAction}, if
349         * none has been set.
350         * 
351         * @return the cancel action
352         */
353        public final ActionListener getCancelAction() {
354                ActionListener a = NativeSearchFieldSupport.getCancelAction(this);
355                if (a == null) {
356                        a = new ClearAction();
357                }
358                return a;
359        }
360
361        /**
362         * Sets the action that is invoked, when the user presses the 'Esc' key or
363         * clicks the cancel button.
364         * 
365         * @param cancelAction
366         */
367        public final void setCancelAction(ActionListener cancelAction) {
368                NativeSearchFieldSupport.setCancelAction(this, cancelAction);
369        }
370
371        /**
372         * Returns the cancel button.
373         * 
374         * Calls {@link #createCancelButton()} to create the cancel button and
375         * registers an {@link ActionListener} that delegates actions to the
376         * {@link ActionListener} returned by {@link #getCancelAction()}, if
377         * needed.
378         * 
379         * @return the cancel button
380         */
381        public final JButton getCancelButton() {
382                if (cancelButton == null) {
383                        cancelButton = createCancelButton();
384                        cancelButton.addActionListener(new ActionListener() {
385                                @Override
386                public void actionPerformed(ActionEvent e) {
387                                        getCancelAction().actionPerformed(e);
388                                }
389                        });
390                }
391                return cancelButton;
392        }
393
394        /**
395         * Creates and returns the cancel button.
396         * 
397         * Override to use a custom cancel button.
398         * 
399         * @see #getCancelButton()
400         * @return the cancel button
401         */
402        protected JButton createCancelButton() {
403                BuddyButton btn = new BuddyButton();
404
405                return btn;
406        }
407
408        /**
409         * Returns the action that is invoked when the enter key is pressed or the
410         * find button is clicked. If no action has been set, a new instance of
411         * {@link FindAction} will be returned.
412         * 
413         * @return the find action
414         */
415        public final ActionListener getFindAction() {
416                ActionListener a = NativeSearchFieldSupport.getFindAction(this);
417                if (a == null) {
418                        a = new FindAction();
419                }
420                return a;
421        }
422
423        /**
424         * Sets the action that is invoked when the enter key is pressed or the find
425         * button is clicked.
426         * 
427         * @return the find action
428         */
429        public final void setFindAction(ActionListener findAction) {
430                NativeSearchFieldSupport.setFindAction(this, findAction);
431        }
432
433        /**
434         * Returns the find button.
435         * 
436         * Calls {@link #createFindButton()} to create the find button and registers
437         * an {@link ActionListener} that delegates actions to the
438         * {@link ActionListener} returned by {@link #getFindAction()}, if needed.
439         * 
440         * @return the find button
441         */
442        public final JButton getFindButton() {
443                if (findButton == null) {
444                        findButton = createFindButton();
445                        findButton.addActionListener(new ActionListener() {
446                                @Override
447                public void actionPerformed(ActionEvent e) {
448                                        getFindAction().actionPerformed(e);
449                                }
450                        });
451                }
452                return findButton;
453        }
454
455        /**
456         * Creates and returns the find button. The buttons action is set to the
457         * action returned by {@link #getSearchAction()}.
458         * 
459         * Override to use a custom find button.
460         * 
461         * @see #getFindButton()
462         * @return the find button
463         */
464        protected JButton createFindButton() {
465                BuddyButton btn = new BuddyButton();
466
467                return btn;
468        }
469
470        /**
471         * Returns the popup button. If a find popup menu is set, it will be
472         * displayed when this button is clicked.
473         * 
474         * This button will only be visible, if {@link #isUseSeperatePopupButton()}
475         * returns <code>true</code>. Otherwise the popup menu will be displayed
476         * when the find button is clicked.
477         * 
478         * @return the popup button
479         */
480        public final JButton getPopupButton() {
481                if (popupButton == null) {
482                        popupButton = createPopupButton();
483                }
484                return popupButton;
485        }
486
487        /**
488         * Creates and returns the popup button. Override to use a custom popup
489         * button.
490         * 
491         * @see #getPopupButton()
492         * @return the popup button
493         */
494        protected JButton createPopupButton() {
495                return new BuddyButton();
496        }
497
498        /**
499         * Returns <code>true</code> if the popup button should be visible and
500         * used for displaying the find popup menu. Otherwise, the find popup menu
501         * will be displayed when the find button is clicked.
502         * 
503         * @return <code>true</code> if the popup button should be used
504         */
505        public boolean isUseSeperatePopupButton() {
506                return useSeperatePopupButton;
507        }
508
509        /**
510         * Set if the popup button should be used for displaying the find popup
511         * menu.
512         * 
513         * @param useSeperatePopupButton
514         */
515        public void setUseSeperatePopupButton(boolean useSeperatePopupButton) {
516                useSeperatePopupButtonSet = true;
517                firePropertyChange("useSeperatePopupButton", this.useSeperatePopupButton,
518                                this.useSeperatePopupButton = useSeperatePopupButton);
519        }
520
521        public boolean isUseNativeSearchFieldIfPossible() {
522                return NativeSearchFieldSupport.isSearchField(this);
523        }
524
525        public void setUseNativeSearchFieldIfPossible(boolean useNativeSearchFieldIfPossible) {
526                TextUIWrapper.getDefaultWrapper().uninstall(this);
527                NativeSearchFieldSupport.setSearchField(this, useNativeSearchFieldIfPossible);
528                TextUIWrapper.getDefaultWrapper().install(this, true);
529                updateUI();
530        }
531
532        /**
533         * Updates the cancel, find and popup buttons enabled state in addition to
534         * setting the search fields editable state.
535         * 
536         * @see #updateButtonState()
537         * @see javax.swing.text.JTextComponent#setEditable(boolean)
538         */
539        @Override
540    public void setEditable(boolean b) {
541                super.setEditable(b);
542                updateButtonState();
543        }
544
545        /**
546         * Updates the cancel, find and popup buttons enabled state in addition to
547         * setting the search fields enabled state.
548         * 
549         * @see #updateButtonState()
550         * @see javax.swing.text.JTextComponent#setEnabled(boolean)
551         */
552        @Override
553    public void setEnabled(boolean enabled) {
554                super.setEnabled(enabled);
555                updateButtonState();
556        }
557
558        /**
559         * Enables the cancel action if this search field is editable and enabled,
560         * otherwise it will be disabled. Enabled the search action and popup button
561         * if this search field is enabled, otherwise it will be disabled.
562         */
563        protected void updateButtonState() {
564                getCancelButton().setEnabled(isEditable() & isEnabled());
565                getFindButton().setEnabled(isEnabled());
566                getPopupButton().setEnabled(isEnabled());
567        }
568
569        /**
570         * Sets the popup menu that will be displayed when the popup button is
571         * clicked. If a find popup menu is set and
572         * {@link #isUseSeperatePopupButton()} returns <code>false</code>, the
573         * popup button will be displayed instead of the find button. Otherwise the
574         * popup button will be displayed in addition to the find button.
575         * 
576         * The find popup menu is managed using {@link NativeSearchFieldSupport} to
577         * achieve compatibility with the native search field support provided by
578         * the Mac Look And Feel since Mac OS 10.5.
579         * 
580         * If a recent searches save key has been set and therefore a recent
581         * searches popup menu is installed, this method does nothing. You must
582         * first remove the recent searches save key, by calling
583         * {@link #setRecentSearchesSaveKey(String)} with a <code>null</code>
584         * parameter.
585         * 
586         * @see #setRecentSearchesSaveKey(String)
587         * @see RecentSearches
588         * @param findPopupMenu
589         *            the popup menu, which will be displayed when the popup button
590         *            is clicked
591         */
592        public void setFindPopupMenu(JPopupMenu findPopupMenu) {
593                if (isManagingRecentSearches()) {
594                        return;
595                }
596
597                NativeSearchFieldSupport.setFindPopupMenu(this, findPopupMenu);
598        }
599
600        /**
601         * Returns the find popup menu.
602         * 
603         * @see #setFindPopupMenu(JPopupMenu)
604         * @return the find popup menu
605         */
606        public JPopupMenu getFindPopupMenu() {
607                return NativeSearchFieldSupport.getFindPopupMenu(this);
608        }
609
610        /**
611         * TODO
612         * 
613         * @return
614         */
615        public final boolean isManagingRecentSearches() {
616                return recentSearches != null;
617        }
618
619        private boolean isValidRecentSearchesKey(String key) {
620                return key != null && key.length() > 0;
621        }
622
623        /**
624         * Returns the key used to persist recent searches.
625         * 
626         * @see #setRecentSearchesSaveKey(String)
627         * @return
628         */
629        public String getRecentSearchesSaveKey() {
630                return recentSearchesSaveKey;
631        }
632
633        /**
634         * Installs and manages a recent searches popup menu as the find popup menu,
635         * if <code>recentSearchesSaveKey</code> is not null. Otherwise, removes
636         * the popup menu and stops managing recent searches.
637         * 
638         * @see #setFindAction(ActionListener)
639         * @see #isManagingRecentSearches()
640         * @see RecentSearches
641         * 
642         * @param recentSearchesSaveKey
643         *            this key is used to persist the recent searches.
644         */
645        public void setRecentSearchesSaveKey(String recentSearchesSaveKey) {
646                String oldName = getRecentSearchesSaveKey();
647                this.recentSearchesSaveKey = recentSearchesSaveKey;
648
649                if (recentSearches != null) {
650                        // set null before uninstalling. otherwise the popup menu is not
651                        // allowed to be changed.
652                        RecentSearches rs = recentSearches;
653                        recentSearches = null;
654                        rs.uninstall(this);
655                }
656
657                if (isValidRecentSearchesKey(recentSearchesSaveKey)) {
658                        recentSearches = new RecentSearches(recentSearchesSaveKey);
659                        recentSearches.install(this);
660                }
661
662                firePropertyChange("recentSearchesSaveKey", oldName, this.recentSearchesSaveKey);
663        }
664
665        /**
666         * TODO
667         * 
668         * @return
669         */
670        public RecentSearches getRecentSearches() {
671                return recentSearches;
672        }
673
674        /**
675         * Returns the {@link Timer} used to delay the firing of action events in
676         * instant search mode when the user enters text.
677         * 
678         * This timer calls {@link #postActionEvent()}.
679         * 
680         * @return the {@link Timer} used to delay the firing of action events
681         */
682        public Timer getInstantSearchTimer() {
683                if (instantSearchTimer == null) {
684                        instantSearchTimer = new Timer(0, new ActionListener() {
685                                @Override
686                public void actionPerformed(ActionEvent e) {
687                                        postActionEvent();
688                                }
689                        });
690                        instantSearchTimer.setRepeats(false);
691                }
692                return instantSearchTimer;
693        }
694
695        /**
696         * Returns <code>true</code> if this search field is the focus owner or
697         * the find popup menu is visible.
698         * 
699         * This is a hack to make the search field paint the focus indicator in Mac
700         * OS X Aqua when the find popup menu is visible.
701         * 
702         * @return <code>true</code> if this search field is the focus owner or
703         *         the find popup menu is visible
704         */
705        @Override
706    public boolean hasFocus() {
707                if (getFindPopupMenu() != null && getFindPopupMenu().isVisible()) {
708                        return true;
709                }
710                return super.hasFocus();
711        }
712
713        /**
714         * Overriden to also update the find popup menu if set.
715         */
716        @Override
717    public void updateUI() {
718                super.updateUI();
719                if (getFindPopupMenu() != null) {
720                        SwingUtilities.updateComponentTreeUI(getFindPopupMenu());
721                }
722        }
723
724        /**
725         * Hack to enable the UI delegate to set default values depending on the
726         * current Look and Feel, without overriding custom values.
727         */
728        @Override
729    public void setPromptFontStyle(Integer fontStyle) {
730                super.setPromptFontStyle(fontStyle);
731                promptFontStyleSet = true;
732        }
733
734        /**
735         * Hack to enable the UI delegate to set default values depending on the
736         * current Look and Feel, without overriding custom values.
737         * 
738         * @param propertyName
739         *            the name of the property to change
740         * @param value
741         *            the new value of the property
742         */
743        public void customSetUIProperty(String propertyName, Object value) {
744                customSetUIProperty(propertyName, value, false);
745        }
746
747        /**
748         * Hack to enable the UI delegate to set default values depending on the
749         * current Look and Feel, without overriding custom values.
750         * 
751         * @param propertyName
752         *            the name of the property to change
753         * @param value
754         *            the new value of the property
755         * @param override
756         *            override custom values
757         */
758        public void customSetUIProperty(String propertyName, Object value, boolean override) {
759                if (propertyName == "useSeperatePopupButton") {
760                        if (!useSeperatePopupButtonSet || override) {
761                                setUseSeperatePopupButton(((Boolean) value).booleanValue());
762                                useSeperatePopupButtonSet = false;
763                        }
764                } else if (propertyName == "layoutStyle") {
765                        if (!layoutStyleSet || override) {
766                                setLayoutStyle(LayoutStyle.valueOf(value.toString()));
767                                layoutStyleSet = false;
768                        }
769                } else if (propertyName == "promptFontStyle") {
770                        if (!promptFontStyleSet || override) {
771                                setPromptFontStyle((Integer) value);
772                                promptFontStyleSet = false;
773                        }
774                } else {
775                        throw new IllegalArgumentException();
776                }
777        }
778
779        /**
780         * Overriden to prevent any delayed {@link ActionEvent}s from being sent
781         * after posting this action.
782         * 
783         * For example, if the current {@link SearchMode} is
784         * {@link SearchMode#INSTANT} and the instant search delay is greater 0. The
785         * user enters some text and presses enter. This method will be invoked
786         * immediately because the users presses enter. However, this method would
787         * be invoked after the instant search delay, if we would not prevent it
788         * here.
789         */
790        @Override
791    public void postActionEvent() {
792                getInstantSearchTimer().stop();
793                super.postActionEvent();
794        }
795
796        /**
797         * Invoked when the the cancel button or the 'Esc' key is pressed. Sets the
798         * text in the search field to <code>null</code>.
799         * 
800         */
801        class ClearAction extends AbstractAction {
802                public ClearAction() {
803                        putValue(SHORT_DESCRIPTION, "Clear Search Text");
804                }
805
806                /**
807                 * Calls {@link #clear()}.
808                 */
809                @Override
810        public void actionPerformed(ActionEvent e) {
811                        clear();
812                }
813
814                /**
815                 * Sets the search field's text to <code>null</code> and requests the
816                 * focus for the search field.
817                 */
818                public void clear() {
819                        setText(null);
820                        requestFocusInWindow();
821                }
822        }
823
824        /**
825         * Invoked when the find button is pressed.
826         */
827        public class FindAction extends AbstractAction {
828                public FindAction() {
829                }
830
831                /**
832                 * In regular search mode posts an action event if the search field is
833                 * the focus owner.
834                 * 
835                 * Also requests the focus for the search field and selects the whole
836                 * text.
837                 */
838                @Override
839        public void actionPerformed(ActionEvent e) {
840                        if (isFocusOwner() && isRegularSearchMode()) {
841                                postActionEvent();
842                        }
843                        requestFocusInWindow();
844                        selectAll();
845                }
846        }
847}