001package org.jdesktop.swingx.plaf;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005
006import javax.swing.JComponent;
007import javax.swing.JTextArea;
008import javax.swing.JTextField;
009import javax.swing.UIDefaults;
010import javax.swing.plaf.TextUI;
011import javax.swing.plaf.basic.BasicTextUI;
012import javax.swing.text.JTextComponent;
013
014import org.jdesktop.swingx.JXSearchField;
015import org.jdesktop.swingx.prompt.BuddySupport;
016
017/**
018 * TODO:
019 * 
020 * @author Peter Weishapl <petw@gmx.net>
021 * 
022 * @param <UI>
023 */
024public abstract class TextUIWrapper<UI extends TextUI> {
025    private static final DefaultWrapper defaultWrapper = new DefaultWrapper();
026
027    public static final TextUIWrapper<? extends PromptTextUI> getDefaultWrapper() {
028        return defaultWrapper;
029    }
030
031    private Class<UI> wrapperClass;
032
033    protected TextUIWrapper(Class<UI> wrapperClass) {
034        this.wrapperClass = wrapperClass;
035    }
036
037    /**
038     * <p>
039     * Wraps and replaces the current UI of the given <code>textComponent</code>, by calling
040     * {@link #wrapUI(JTextComponent)} if necessary.
041     * </p>
042     * 
043     * @param textComponent
044     * @param stayOnUIChange
045     *            if <code>true</code>, a {@link PropertyChangeListener} is registered, which
046     *            listens for UI changes and wraps any new UI object.
047     */
048    public final void install(final JTextComponent textComponent, boolean stayOnUIChange) {
049        replaceUIIfNeeded(textComponent);
050        if (stayOnUIChange) {
051            uiChangeHandler.install(textComponent);
052        }
053    }
054
055    /**
056     * Wraps and replaces the text components current UI by calling {@link #wrapUI(TextUI)}, if the
057     * text components current UI is not an instance of the given wrapper class.
058     * 
059     * @param textComponent
060     * @return <code>true</code> if the UI has been replaced
061     */
062    protected boolean replaceUIIfNeeded(JTextComponent textComponent) {
063        if (wrapperClass.isAssignableFrom(textComponent.getUI().getClass())) {
064            return false;
065        }
066
067        textComponent.setUI(wrapUI(textComponent));
068
069        return true;
070    }
071
072    /**
073     * Override to return the appropriate UI wrapper object for the given {@link TextUI}.
074     * 
075     * @param textUI
076     * @return the wrapping UI
077     */
078    public abstract UI wrapUI(JTextComponent textComponent);
079
080    /**
081     * Returns the wrapper class.
082     * 
083     * @return the wrapper class
084     */
085    public Class<UI> getWrapperClass() {
086        return wrapperClass;
087    }
088
089    /**
090     * <p>
091     * Removes the {@link PropertyChangeListener}, which listens for "UI" property changes (if
092     * installed) and then calls {@link JComponent#updateUI()} on the <code>textComponent</code> to
093     * set the UI object provided by the current {@link UIDefaults}.
094     * </p>
095     * 
096     * @param textComponent
097     */
098    public final void uninstall(final JTextComponent textComponent) {
099        uiChangeHandler.uninstall(textComponent);
100        textComponent.updateUI();
101    }
102
103    private final TextUIChangeHandler uiChangeHandler = new TextUIChangeHandler();
104
105    private final class TextUIChangeHandler extends AbstractUIChangeHandler {
106        @Override
107        public void propertyChange(PropertyChangeEvent evt) {
108            JTextComponent txt = (JTextComponent) evt.getSource();
109
110            replaceUIIfNeeded(txt);
111        }
112    }
113
114    public static final class DefaultWrapper extends TextUIWrapper<PromptTextUI> {
115        private DefaultWrapper() {
116            super(PromptTextUI.class);
117        }
118
119        /**
120         * <p>
121         * Creates a new {@link PromptTextUI}, which wraps the given <code>textComponent</code>s UI.
122         * </p>
123         * <p>
124         * If the UI is already of type {@link PromptTextUI}, it will be returned. If
125         * <code>textComponent</code> is of type {@link JXSearchField} a new {@link SearchFieldUI}
126         * object will be returned. If <code>textComponent</code> is of type {@link JTextField} or
127         * {@link JTextArea} a {@link BuddyTextFieldUI} or {@link PromptTextAreaUI} will be
128         * returned, respectively. If the UI is of any other type, a
129         * {@link IllegalArgumentException} will be thrown.
130         * </p>
131         * 
132         * @param textComponent
133         *            wrap this components UI
134         * @return a {@link PromptTextUI} which wraps the <code>textComponent</code>s UI.
135         */
136        @Override
137        public PromptTextUI wrapUI(JTextComponent textComponent) {
138            TextUI textUI = textComponent.getUI();
139
140            if (textUI instanceof PromptTextUI) {
141                return (PromptTextUI) textUI;
142            } else if (textComponent instanceof JXSearchField) {
143                return new SearchFieldUI(textUI);
144            } else if (textComponent instanceof JTextField) {
145                return new BuddyTextFieldUI(textUI);
146            } else if (textComponent instanceof JTextArea) {
147                return new PromptTextAreaUI(textUI);
148            }
149            throw new IllegalArgumentException("ui implementation not supported: "
150                    + textUI.getClass());
151        }
152
153        /**
154         * Every time the UI needs to be replaced we also need to make sure, that all buddy
155         * components are also in the component hierarchy. (That's because {@link BasicTextUI}
156         * removes all our buddies upon UI changes).
157         */
158        @Override
159        protected boolean replaceUIIfNeeded(JTextComponent textComponent) {
160            boolean replaced = super.replaceUIIfNeeded(textComponent);
161
162            if (replaced && textComponent instanceof JTextField) {
163                BuddySupport.ensureBuddiesAreInComponentHierarchy((JTextField) textComponent);
164            }
165            return replaced;
166        }
167    }
168}