001package org.jdesktop.swingx.plaf;
002
003import static javax.swing.BorderFactory.createEmptyBorder;
004
005import java.awt.Dimension;
006import java.awt.Graphics;
007import java.awt.Graphics2D;
008import java.awt.Insets;
009import java.awt.Point;
010import java.awt.Rectangle;
011import java.awt.TextComponent;
012import java.awt.Component.BaselineResizeBehavior;
013import java.awt.event.FocusAdapter;
014import java.awt.event.FocusEvent;
015import java.lang.reflect.Method;
016
017import javax.accessibility.Accessible;
018import javax.swing.JComponent;
019import javax.swing.SwingUtilities;
020import javax.swing.border.Border;
021import javax.swing.plaf.ComponentUI;
022import javax.swing.plaf.TextUI;
023import javax.swing.text.BadLocationException;
024import javax.swing.text.Caret;
025import javax.swing.text.EditorKit;
026import javax.swing.text.Highlighter;
027import javax.swing.text.JTextComponent;
028import javax.swing.text.Position;
029import javax.swing.text.View;
030import javax.swing.text.DefaultHighlighter.DefaultHighlightPainter;
031import javax.swing.text.Position.Bias;
032
033import org.jdesktop.swingx.painter.Painter;
034import org.jdesktop.swingx.prompt.PromptSupport;
035import org.jdesktop.swingx.prompt.PromptSupport.FocusBehavior;
036
037/**
038 * <p>
039 * Abstract {@link TextUI} class that delegates most work to another
040 * {@link TextUI} and additionally renders a prompt text as specified in the
041 * {@link JTextComponent}s client properties by {@link PromptSupport}.
042 * <p>
043 * Subclasses of this class must provide a prompt component used for rendering
044 * the prompt text.
045 * </p>
046 * 
047 * @author Peter Weishapl <petw@gmx.net>
048 * 
049 */
050public abstract class PromptTextUI extends TextUI {
051    protected class PainterHighlighter implements Highlighter {
052        private final Painter painter;
053
054        private JTextComponent c;
055
056        public PainterHighlighter(Painter painter) {
057            this.painter = painter;
058        }
059
060        /**
061         * {@inheritDoc}
062         */
063        @Override
064        public Object addHighlight(int p0, int p1, HighlightPainter p)
065                throws BadLocationException {
066            return new Object();
067        }
068
069        /**
070         * {@inheritDoc}
071         */
072        @Override
073        public void changeHighlight(Object tag, int p0, int p1)
074                throws BadLocationException {
075
076        }
077
078        /**
079         * {@inheritDoc}
080         */
081        @Override
082        public void deinstall(JTextComponent c) {
083            c = null;
084        }
085
086        /**
087         * {@inheritDoc}
088         */
089        @Override
090        public Highlight[] getHighlights() {
091            return null;
092        }
093
094        /**
095         * {@inheritDoc}
096         */
097        @Override
098        public void install(JTextComponent c) {
099            this.c = c;
100        }
101
102        /**
103         * {@inheritDoc}
104         */
105        @Override
106        public void paint(Graphics g) {
107            Graphics2D g2d = (Graphics2D) g.create();
108
109            try {
110                painter.paint(g2d, c, c.getWidth(), c.getHeight());
111            } finally {
112                g2d.dispose();
113            }
114        }
115
116        /**
117         * {@inheritDoc}
118         */
119        @Override
120        public void removeAllHighlights() {
121            // TODO Auto-generated method stub
122
123        }
124
125        /**
126         * {@inheritDoc}
127         */
128        @Override
129        public void removeHighlight(Object tag) {
130            // TODO Auto-generated method stub
131
132        }
133    }
134
135    static final FocusHandler focusHandler = new FocusHandler();
136
137    /**
138     * Delegate the hard work to this object.
139     */
140    protected final TextUI delegate;
141
142    /**
143     * This component ist painted when rendering the prompt text.
144     */
145    protected JTextComponent promptComponent;
146
147    /**
148     * Creates a new {@link PromptTextUI} which delegates most work to another
149     * {@link TextUI}.
150     * 
151     * @param delegate
152     */
153    public PromptTextUI(TextUI delegate) {
154        this.delegate = delegate;
155    }
156
157    /**
158     * Creates a component which should be used to render the prompt text.
159     * 
160     * @return
161     */
162    protected abstract JTextComponent createPromptComponent();
163
164    /**
165     * Calls TextUI#installUI(JComponent) on the delegate and installs a focus
166     * listener on <code>c</code> which repaints the component when it gains or
167     * loses the focus.
168     */
169    @Override
170    public void installUI(JComponent c) {
171        delegate.installUI(c);
172
173        JTextComponent txt = (JTextComponent) c;
174
175        // repaint to correctly highlight text if FocusBehavior is
176        // HIGHLIGHT_LABEL in Metal and Windows LnF
177        txt.addFocusListener(focusHandler);
178    }
179
180    /**
181     * Delegates, then uninstalls the focus listener.
182     */
183    @Override
184    public void uninstallUI(JComponent c) {
185        delegate.uninstallUI(c);
186        c.removeFocusListener(focusHandler);
187        promptComponent = null;
188    }
189
190    /**
191     * Creates a label component, if none has already been created. Sets the
192     * prompt components properties to reflect the given {@link JTextComponent}s
193     * properties and returns it.
194     * 
195     * @param txt
196     * @return the adjusted prompt component
197     */
198    public JTextComponent getPromptComponent(JTextComponent txt) {
199        if (promptComponent == null) {
200            promptComponent = createPromptComponent();
201        }
202        if (txt.isFocusOwner()
203                && PromptSupport.getFocusBehavior(txt) == FocusBehavior.HIDE_PROMPT) {
204            promptComponent.setText(null);
205        } else {
206            promptComponent.setText(PromptSupport.getPrompt(txt));
207        }
208
209        promptComponent.getHighlighter().removeAllHighlights();
210        if (txt.isFocusOwner()
211                && PromptSupport.getFocusBehavior(txt) == FocusBehavior.HIGHLIGHT_PROMPT) {
212            promptComponent.setForeground(txt.getSelectedTextColor());
213            try {
214                promptComponent.getHighlighter().addHighlight(0,
215                        promptComponent.getText().length(),
216                        new DefaultHighlightPainter(txt.getSelectionColor()));
217            } catch (BadLocationException e) {
218                e.printStackTrace();
219            }
220        } else {
221            promptComponent.setForeground(PromptSupport.getForeground(txt));
222        }
223
224        if (PromptSupport.getFontStyle(txt) == null) {
225            promptComponent.setFont(txt.getFont());
226        } else {
227            promptComponent.setFont(txt.getFont().deriveFont(
228                    PromptSupport.getFontStyle(txt)));
229        }
230
231        promptComponent.setBackground(PromptSupport.getBackground(txt));
232        promptComponent.setHighlighter(new PainterHighlighter(PromptSupport
233                .getBackgroundPainter(txt)));
234        promptComponent.setEnabled(txt.isEnabled());
235        promptComponent.setOpaque(txt.isOpaque());
236        promptComponent.setBounds(txt.getBounds());
237        Border b = txt.getBorder();
238        
239        if (b == null) {
240            promptComponent.setBorder(txt.getBorder());
241        } else {
242            Insets insets = b.getBorderInsets(txt);
243            promptComponent.setBorder(
244                    createEmptyBorder(insets.top, insets.left, insets.bottom, insets.right));
245        }
246        
247        promptComponent.setSelectedTextColor(txt.getSelectedTextColor());
248        promptComponent.setSelectionColor(txt.getSelectionColor());
249        promptComponent.setEditable(txt.isEditable());
250        promptComponent.setMargin(txt.getMargin());
251
252        return promptComponent;
253    }
254
255    /**
256     * When {@link #shouldPaintPrompt(JTextComponent)} returns true, the prompt
257     * component is retrieved by calling
258     * {@link #getPromptComponent(JTextComponent)} and it's preferred size is
259     * returned. Otherwise super{@link #getPreferredSize(JComponent)} is called.
260     */
261    @Override
262    public Dimension getPreferredSize(JComponent c) {
263        JTextComponent txt = (JTextComponent) c;
264        if (shouldPaintPrompt(txt)) {
265            return getPromptComponent(txt).getPreferredSize();
266        }
267        return delegate.getPreferredSize(c);
268    }
269
270    /**
271     * Delegates painting when {@link #shouldPaintPrompt(JTextComponent)}
272     * returns false. Otherwise the prompt component is retrieved by calling
273     * {@link #getPromptComponent(JTextComponent)} and painted. Then the caret
274     * of the given text component is painted.
275     */
276    @Override
277    public void paint(Graphics g, final JComponent c) {
278        JTextComponent txt = (JTextComponent) c;
279
280        if (shouldPaintPrompt(txt)) {
281            paintPromptComponent(g, txt);
282        } else {
283            delegate.paint(g, c);
284        }
285    }
286
287    protected void paintPromptComponent(Graphics g, JTextComponent txt) {
288        JTextComponent lbl = getPromptComponent(txt);
289        SwingUtilities.paintComponent(g, lbl, txt, 0, 0, txt.getWidth(), txt.getHeight());
290
291        if (txt.getCaret() != null) {
292            txt.getCaret().paint(g);
293        }
294    }
295
296    /**
297     * Returns if the prompt or the text field should be painted, depending on
298     * the state of <code>txt</code>.
299     * 
300     * @param txt
301     * @return true when <code>txt</code> contains not text, otherwise false
302     */
303    public boolean shouldPaintPrompt(JTextComponent txt) {
304        return txt.getText() == null || txt.getText().length() == 0;
305    }
306
307    /**
308     * Calls super.{@link #update(Graphics, JComponent)}, which in turn calls
309     * the paint method of this object.
310     */
311    @Override
312    public void update(Graphics g, JComponent c) {
313        super.update(g, c);
314    }
315
316    /**
317     * Delegate when {@link #shouldPaintPrompt(JTextComponent)} returns false.
318     * Otherwise get the prompt component's UI and delegate to it. This ensures
319     * that the {@link Caret} is painted on the correct position (this is
320     * important when the text is centered, so that the caret will not be
321     * painted inside the label text)
322     */
323    @Override
324    public Rectangle modelToView(JTextComponent t, int pos, Bias bias)
325            throws BadLocationException {
326        if (shouldPaintPrompt(t)) {
327            return getPromptComponent(t).getUI().modelToView(t, pos, bias);
328        } else {
329            return delegate.modelToView(t, pos, bias);
330        }
331    }
332
333    /**
334     * Calls {@link #modelToView(JTextComponent, int, Bias)} with
335     * {@link Bias#Forward}.
336     */
337    @Override
338    public Rectangle modelToView(JTextComponent t, int pos)
339            throws BadLocationException {
340        return modelToView(t, pos, Position.Bias.Forward);
341    }
342
343    // ********************* Delegate methods *************************///
344    // ****************************************************************///
345
346    @Override
347    public boolean contains(JComponent c, int x, int y) {
348        return delegate.contains(c, x, y);
349    }
350
351    @Override
352    public void damageRange(JTextComponent t, int p0, int p1, Bias firstBias,
353            Bias secondBias) {
354        delegate.damageRange(t, p0, p1, firstBias, secondBias);
355    }
356
357    @Override
358    public void damageRange(JTextComponent t, int p0, int p1) {
359        delegate.damageRange(t, p0, p1);
360    }
361
362    @Override
363    public boolean equals(Object obj) {
364        return delegate.equals(obj);
365    }
366
367    @Override
368    public Accessible getAccessibleChild(JComponent c, int i) {
369        return delegate.getAccessibleChild(c, i);
370    }
371
372    @Override
373    public int getAccessibleChildrenCount(JComponent c) {
374        return delegate.getAccessibleChildrenCount(c);
375    }
376
377    @Override
378    public EditorKit getEditorKit(JTextComponent t) {
379        return delegate.getEditorKit(t);
380    }
381
382    @Override
383    public Dimension getMaximumSize(JComponent c) {
384        return delegate.getMaximumSize(c);
385    }
386
387    @Override
388    public Dimension getMinimumSize(JComponent c) {
389        return delegate.getMinimumSize(c);
390    }
391
392    @Override
393    public int getNextVisualPositionFrom(JTextComponent t, int pos, Bias b,
394            int direction, Bias[] biasRet) throws BadLocationException {
395        return delegate
396                .getNextVisualPositionFrom(t, pos, b, direction, biasRet);
397    }
398
399    @Override
400    public View getRootView(JTextComponent t) {
401        return delegate.getRootView(t);
402    }
403
404    @Override
405    public String getToolTipText(JTextComponent t, Point pt) {
406        return delegate.getToolTipText(t, pt);
407    }
408
409    @Override
410    public int hashCode() {
411        return delegate.hashCode();
412    }
413
414    @Override
415    public String toString() {
416        return String.format("%s (%s)", getClass().getName(), delegate
417                .toString());
418    }
419
420    @Override
421    public int viewToModel(JTextComponent t, Point pt, Bias[] biasReturn) {
422        return delegate.viewToModel(t, pt, biasReturn);
423    }
424
425    @Override
426    public int viewToModel(JTextComponent t, Point pt) {
427        return delegate.viewToModel(t, pt);
428    }
429
430    /**
431     * Tries to call {@link ComponentUI#getBaseline(int, int)} on the delegate
432     * via Reflection. Workaround to maintain compatibility with Java 5. Ideally
433     * we should also override {@link #getBaselineResizeBehavior(JComponent)},
434     * but that's impossible since the {@link BaselineResizeBehavior} class,
435     * which does not exist in Java 5, is involved.
436     * 
437     * @return the baseline, or -2 if <code>getBaseline</code> could not be
438     *         invoked on the delegate.
439     */
440    @Override
441    public int getBaseline(JComponent c, int width, int height) {
442        try {
443            Method m = delegate.getClass().getMethod("getBaseline",
444                    JComponent.class, int.class, int.class);
445            Object o = m.invoke(delegate, new Object[] { c, width, height });
446            return (Integer) o;
447        } catch (Exception ex) {
448            // ignore
449            return -2;
450        }
451    }
452
453    /**
454     * Repaint the {@link TextComponent} when it loses or gains the focus.
455     */
456    private static final class FocusHandler extends FocusAdapter {
457        @Override
458        public void focusGained(FocusEvent e) {
459            e.getComponent().repaint();
460        }
461
462        @Override
463        public void focusLost(FocusEvent e) {
464            e.getComponent().repaint();
465        }
466    }
467}