001package org.jdesktop.swingx.plaf;
002
003import java.awt.Component;
004import java.awt.Container;
005import java.awt.Dimension;
006import java.awt.Insets;
007import java.awt.Rectangle;
008import java.awt.event.ActionEvent;
009import java.awt.event.ActionListener;
010import java.beans.PropertyChangeEvent;
011import java.beans.PropertyChangeListener;
012
013import javax.swing.Icon;
014import javax.swing.JButton;
015import javax.swing.JComponent;
016import javax.swing.SwingUtilities;
017import javax.swing.UIManager;
018import javax.swing.event.DocumentEvent;
019import javax.swing.event.DocumentListener;
020import javax.swing.plaf.TextUI;
021import javax.swing.plaf.UIResource;
022import javax.swing.text.Document;
023
024import org.jdesktop.swingx.JXSearchField;
025import org.jdesktop.swingx.JXSearchField.LayoutStyle;
026import org.jdesktop.swingx.prompt.BuddySupport;
027import org.jdesktop.swingx.search.NativeSearchFieldSupport;
028
029/**
030 * The default {@link JXSearchField} UI delegate.
031 * 
032 * @author Peter Weishapl <petw@gmx.net>
033 * 
034 */
035public class SearchFieldUI extends BuddyTextFieldUI {
036    /**
037     * The search field that we're a UI delegate for. Initialized by the
038     * <code>installUI</code> method, and reset to null by
039     * <code>uninstallUI</code>.
040     * 
041     * @see #installUI
042     * @see #uninstallUI
043     */
044    protected JXSearchField searchField;
045
046    private Handler handler;
047
048    public static final Insets NO_INSETS = new Insets(0, 0, 0, 0);
049
050    public SearchFieldUI(TextUI delegate) {
051        super(delegate);
052    }
053
054    private Handler getHandler() {
055        if (handler == null) {
056            handler = new Handler();
057        }
058        return handler;
059    }
060
061    /**
062     * Calls {@link #installDefaults()}, adds the search, clear and popup button
063     * to the search field and registers a {@link PropertyChangeListener} ad
064     * {@link DocumentListener} and an {@link ActionListener} on the popup
065     * button.
066     */
067    @Override
068    public void installUI(JComponent c) {
069        searchField = (JXSearchField) c;
070
071        super.installUI(c);
072
073        installDefaults();
074        layoutButtons();
075
076        configureListeners();
077    }
078
079    private void configureListeners() {
080        if (isNativeSearchField()) {
081            popupButton().removeActionListener(getHandler());
082            searchField.removePropertyChangeListener(getHandler());
083        } else {
084            popupButton().addActionListener(getHandler());
085            searchField.addPropertyChangeListener(getHandler());
086        }
087
088        // add support for instant search mode in any case.
089        searchField.getDocument().addDocumentListener(getHandler());
090    }
091
092    private boolean isNativeSearchField() {
093        return NativeSearchFieldSupport.isNativeSearchField(searchField);
094    }
095
096    @Override
097    protected BuddyLayoutAndBorder createBuddyLayoutAndBorder() {
098        return new BuddyLayoutAndBorder() {
099            /**
100             * This does nothing, if the search field is rendered natively on
101             * Leopard.
102             */
103            @Override
104            protected void replaceBorderIfNecessary() {
105                if (!isNativeSearchField()) {
106                    super.replaceBorderIfNecessary();
107                }
108            }
109
110            /**
111             * Return zero, when the search field is rendered natively on
112             * Leopard, to make painting work correctly.
113             */
114            @Override
115            public Dimension preferredLayoutSize(Container parent) {
116                if (isNativeSearchField()) {
117                    return new Dimension();
118                } else {
119                    return super.preferredLayoutSize(parent);
120                }
121            }
122
123            /**
124             * Prevent 'jumping' when text is entered: Include the clear button,
125             * when layout style is Mac. When layout style is Vista: Take the
126             * clear button's preferred width if its either greater than the
127             * search button's pref. width or greater than the popup button's
128             * pref. width when a popup menu is installed and not using a
129             * seperate popup button.
130             */
131            @Override
132            public Insets getBorderInsets(Component c) {
133                Insets insets = super.getBorderInsets(c);
134                if (searchField != null && !isNativeSearchField()) {
135                    if (isMacLayoutStyle()) {
136                        if (!clearButton().isVisible()) {
137                            insets.right += clearButton().getPreferredSize().width;
138                        }
139                    } else {
140                        JButton refButton = popupButton();
141                        if (searchField.getFindPopupMenu() == null
142                                ^ searchField.isUseSeperatePopupButton()) {
143                            refButton = searchButton();
144                        }
145
146                        int clearWidth = clearButton().getPreferredSize().width;
147                        int refWidth = refButton.getPreferredSize().width;
148                        int overSize = clearButton().isVisible() ? refWidth
149                                - clearWidth : clearWidth - refWidth;
150                        if (overSize > 0) {
151                            insets.right += overSize;
152                        }
153                    }
154
155                }
156                return insets;
157            }
158        };
159    }
160
161    private void layoutButtons() {
162        BuddySupport.removeAll(searchField);
163
164        if (isNativeSearchField()) {
165            return;
166        }
167
168        if (isMacLayoutStyle()) {
169            BuddySupport.addLeft(searchButton(), searchField);
170        } else {
171            BuddySupport.addRight(searchButton(), searchField);
172        }
173
174        BuddySupport.addRight(clearButton(), searchField);
175
176        if (usingSeperatePopupButton()) {
177            BuddySupport.addRight(BuddySupport.createGap(getPopupOffset()),
178                    searchField);
179        }
180
181        if (usingSeperatePopupButton() || !isMacLayoutStyle()) {
182            BuddySupport.addRight(popupButton(), searchField);
183        } else {
184            BuddySupport.addLeft(popupButton(), searchField);
185        }
186    }
187
188    private boolean isMacLayoutStyle() {
189        return searchField.getLayoutStyle() == LayoutStyle.MAC;
190    }
191
192    /**
193     * Initialize the search fields various properties based on the
194     * corresponding "SearchField.*" properties from defaults table. The
195     * {@link JXSearchField}s layout is set to the value returned by
196     * <code>createLayout</code>. Also calls {@link #replaceBorderIfNecessary()}
197     * and {@link #updateButtons()}. This method is called by
198     * {@link #installUI(JComponent)}.
199     * 
200     * @see #installUI
201     * @see #createLayout
202     * @see JXSearchField#customSetUIProperty(String, Object)
203     */
204    protected void installDefaults() {
205        if (isNativeSearchField()) {
206            return;
207        }
208
209        if (UIManager.getBoolean("SearchField.useSeperatePopupButton")) {
210            searchField.customSetUIProperty("useSeperatePopupButton",
211                    Boolean.TRUE);
212        } else {
213            searchField.customSetUIProperty("useSeperatePopupButton",
214                    Boolean.FALSE);
215        }
216
217        searchField.customSetUIProperty("layoutStyle", UIManager
218                .get("SearchField.layoutStyle"));
219        searchField.customSetUIProperty("promptFontStyle", UIManager
220                .get("SearchField.promptFontStyle"));
221
222        if (shouldReplaceResource(searchField.getOuterMargin())) {
223            searchField.setOuterMargin(UIManager
224                    .getInsets("SearchField.buttonMargin"));
225        }
226
227        updateButtons();
228
229        if (shouldReplaceResource(clearButton().getIcon())) {
230            clearButton().setIcon(UIManager.getIcon("SearchField.clearIcon"));
231        }
232        if (shouldReplaceResource(clearButton().getPressedIcon())) {
233            clearButton().setPressedIcon(
234                    UIManager.getIcon("SearchField.clearPressedIcon"));
235        }
236        if (shouldReplaceResource(clearButton().getRolloverIcon())) {
237            clearButton().setRolloverIcon(
238                    UIManager.getIcon("SearchField.clearRolloverIcon"));
239        }
240
241        searchButton().setIcon(
242                getNewIcon(searchButton().getIcon(), "SearchField.icon"));
243
244        popupButton().setIcon(
245                getNewIcon(popupButton().getIcon(), "SearchField.popupIcon"));
246        popupButton().setRolloverIcon(
247                getNewIcon(popupButton().getRolloverIcon(),
248                        "SearchField.popupRolloverIcon"));
249        popupButton().setPressedIcon(
250                getNewIcon(popupButton().getPressedIcon(),
251                        "SearchField.popupPressedIcon"));
252    }
253
254    /**
255     * Removes all installed listeners, the layout and resets the search field
256     * original border and removes all children.
257     */
258    @Override
259    public void uninstallUI(JComponent c) {
260        super.uninstallUI(c);
261
262        searchField.removePropertyChangeListener(getHandler());
263        searchField.getDocument().removeDocumentListener(getHandler());
264        popupButton().removeActionListener(getHandler());
265
266        searchField.setLayout(null);
267        searchField.removeAll();
268        searchField = null;
269    }
270
271    /**
272     * Returns true if <code>o</code> is <code>null</code> or of instance
273     * {@link UIResource}.
274     * 
275     * @param o an object
276     * @return true if <code>o</code> is <code>null</code> or of instance
277     *         {@link UIResource}
278     */
279    protected boolean shouldReplaceResource(Object o) {
280        return o == null || o instanceof UIResource;
281    }
282
283    /**
284     * Convience method for only replacing icons if they have not been
285     * customized by the user. Returns the icon from the defaults table
286     * belonging to <code>resKey</code>, if
287     * {@link #shouldReplaceResource(Object)} with the <code>icon</code> as a
288     * parameter returns <code>true</code>. Otherwise returns <code>icon</code>.
289     * 
290     * @param icon the current icon
291     * @param resKey the resource key identifying the default icon
292     * @return the new icon
293     */
294    protected Icon getNewIcon(Icon icon, String resKey) {
295        Icon uiIcon = UIManager.getIcon(resKey);
296        if (shouldReplaceResource(icon)) {
297            return uiIcon;
298        }
299        return icon;
300    }
301
302    /**
303     * Convienence method.
304     * 
305     * @see JXSearchField#getCancelButton()
306     * @return the clear button
307     */
308    protected final JButton clearButton() {
309        return searchField.getCancelButton();
310    }
311
312    /**
313     * Convienence method.
314     * 
315     * @see JXSearchField#getFindButton()
316     * @return the search button
317     */
318    protected final JButton searchButton() {
319        return searchField.getFindButton();
320    }
321
322    /**
323     * Convienence method.
324     * 
325     * @see JXSearchField#getPopupButton()
326     * @return the popup button
327     */
328    protected final JButton popupButton() {
329        return searchField.getPopupButton();
330    }
331
332    /**
333     * Returns <code>true</code> if
334     * {@link JXSearchField#isUseSeperatePopupButton()} is <code>true</code> and
335     * a search popup menu has been set.
336     * 
337     * @return the popup button is used in addition to the search button
338     */
339    public boolean usingSeperatePopupButton() {
340        return searchField.isUseSeperatePopupButton()
341                && searchField.getFindPopupMenu() != null;
342    }
343
344    /**
345     * Returns the number of pixels between the popup button and the clear (or
346     * search) button as specified in the default table by
347     * 'SearchField.popupOffset'. Returns 0 if
348     * {@link #usingSeperatePopupButton()} returns <code>false</code>
349     * 
350     * @return number of pixels between the popup button and the clear (or
351     *         search) button
352     */
353    protected int getPopupOffset() {
354        if (usingSeperatePopupButton()) {
355            return UIManager.getInt("SearchField.popupOffset");
356        }
357        return 0;
358    }
359
360    /**
361     * Sets the visibility of the search, clear and popup buttons depending on
362     * the search mode, layout stye, search text, search popup menu and the use
363     * of a seperate popup button. Also resets the search buttons pressed and
364     * rollover icons if the search field is in regular search mode or clears
365     * the icons when the search field is in instant search mode.
366     */
367    protected void updateButtons() {
368        clearButton().setVisible(
369                (!searchField.isRegularSearchMode() || searchField
370                        .isMacLayoutStyle())
371                        && hasText());
372
373        boolean clearNotHere = (searchField.isMacLayoutStyle() || !clearButton()
374                .isVisible());
375
376        searchButton()
377                .setVisible(
378                        (searchField.getFindPopupMenu() == null || usingSeperatePopupButton())
379                                && clearNotHere);
380        popupButton().setVisible(
381                searchField.getFindPopupMenu() != null
382                        && (clearNotHere || usingSeperatePopupButton()));
383
384        if (searchField.isRegularSearchMode()) {
385            searchButton().setRolloverIcon(
386                    getNewIcon(searchButton().getRolloverIcon(),
387                            "SearchField.rolloverIcon"));
388            searchButton().setPressedIcon(
389                    getNewIcon(searchButton().getPressedIcon(),
390                            "SearchField.pressedIcon"));
391        } else {
392            // no action, therefore no rollover icon.
393            if (shouldReplaceResource(searchButton().getRolloverIcon())) {
394                searchButton().setRolloverIcon(null);
395            }
396            if (shouldReplaceResource(searchButton().getPressedIcon())) {
397                searchButton().setPressedIcon(null);
398            }
399        }
400    }
401
402    private boolean hasText() {
403        return searchField.getText() != null
404                && searchField.getText().length() > 0;
405    }
406
407    class Handler implements PropertyChangeListener, ActionListener,
408            DocumentListener {
409        @Override
410        public void propertyChange(PropertyChangeEvent evt) {
411            String prop = evt.getPropertyName();
412            Object src = evt.getSource();
413
414            if (src.equals(searchField)) {
415                if ("findPopupMenu".equals(prop) || "searchMode".equals(prop)
416                        || "useSeperatePopupButton".equals(prop)
417                        || "searchMode".equals(prop)
418                        || "layoutStyle".equals(prop)) {
419                    layoutButtons();
420                    updateButtons();
421                } else if ("document".equals(prop)) {
422                    Document doc = (Document) evt.getOldValue();
423                    if (doc != null) {
424                        doc.removeDocumentListener(this);
425                    }
426                    doc = (Document) evt.getNewValue();
427                    if (doc != null) {
428                        doc.addDocumentListener(this);
429                    }
430                }
431            }
432        }
433
434        /**
435         * Shows the search popup menu, if installed.
436         */
437        @Override
438        public void actionPerformed(ActionEvent e) {
439            if (searchField.getFindPopupMenu() != null) {
440                Component src = SearchFieldAddon.SEARCH_FIELD_SOURCE
441                        .equals(UIManager.getString("SearchField.popupSource")) ? searchField
442                        : (Component) e.getSource();
443
444                Rectangle r = SwingUtilities.getLocalBounds(src);
445                int popupWidth = searchField.getFindPopupMenu()
446                        .getPreferredSize().width;
447                int x = searchField.isVistaLayoutStyle()
448                        || usingSeperatePopupButton() ? r.x + r.width
449                        - popupWidth : r.x;
450                searchField.getFindPopupMenu().show(src, x, r.y + r.height);
451            }
452        }
453
454        @Override
455        public void changedUpdate(DocumentEvent e) {
456            update();
457        }
458
459        @Override
460        public void insertUpdate(DocumentEvent e) {
461            update();
462        }
463
464        @Override
465        public void removeUpdate(DocumentEvent e) {
466            update();
467        }
468
469        /**
470         * Called when the search text changes. Calls
471         * {@link JXSearchField#postActionEvent()} In instant search mode or
472         * starts the search field instant search timer if the instant search
473         * delay is greater 0.
474         */
475        private void update() {
476            if (searchField.isInstantSearchMode()) {
477                searchField.getInstantSearchTimer().stop();
478                // only use timer when delay greater 0.
479                if (searchField.getInstantSearchDelay() > 0) {
480                    searchField.getInstantSearchTimer().setInitialDelay(
481                            searchField.getInstantSearchDelay());
482                    searchField.getInstantSearchTimer().start();
483                } else {
484                    searchField.postActionEvent();
485                }
486            }
487
488            updateButtons();
489        }
490    }
491}