001/*
002 * Copyright (c) 2002-2007 JGoodies Karsten Lentzsch. All Rights Reserved.
003 *
004 * Redistribution and use in source and binary forms, with or without
005 * modification, are permitted provided that the following conditions are met:
006 *
007 *  o Redistributions of source code must retain the above copyright notice,
008 *    this list of conditions and the following disclaimer.
009 *
010 *  o Redistributions in binary form must reproduce the above copyright notice,
011 *    this list of conditions and the following disclaimer in the documentation
012 *    and/or other materials provided with the distribution.
013 *
014 *  o Neither the name of JGoodies Karsten Lentzsch nor the names of
015 *    its contributors may be used to endorse or promote products derived
016 *    from this software without specific prior written permission.
017 *
018 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
019 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
020 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
021 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
022 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
025 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
026 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
027 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
028 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030
031package com.jgoodies.forms.factories;
032
033import java.awt.Color;
034import java.awt.Component;
035import java.awt.Container;
036import java.awt.Dimension;
037import java.awt.Font;
038import java.awt.FontMetrics;
039import java.awt.Insets;
040import java.awt.LayoutManager;
041
042import javax.swing.JComponent;
043import javax.swing.JLabel;
044import javax.swing.JPanel;
045import javax.swing.JSeparator;
046import javax.swing.SwingConstants;
047import javax.swing.UIManager;
048
049import com.jgoodies.forms.layout.Sizes;
050import com.jgoodies.forms.util.Utilities;
051
052/**
053 * A singleton implementation of the {@link ComponentFactory} interface
054 * that creates UI components as required by the
055 * {@link com.jgoodies.forms.builder.PanelBuilder}.<p>
056 *
057 * The texts used in methods <code>#createLabel(String)</code> and
058 * <code>#createTitle(String)</code> can contain an optional mnemonic marker.
059 * The mnemonic and mnemonic index are indicated by a single ampersand
060 * (<tt>&amp;</tt>). For example <tt>&quot;&amp;Save&quot;</tt>,
061 * or <tt>&quot;Save&nbsp;&amp;as&quot;</tt>. To use the ampersand itself
062 * duplicate it, for example <tt>&quot;Look&amp;&amp;Feel&quot;</tt>.
063 *
064 * @author Karsten Lentzsch
065 * @version $Revision$
066 */
067
068public final class DefaultComponentFactory implements ComponentFactory {
069
070    /**
071     * Holds the single instance of this class.
072     */
073    private static final DefaultComponentFactory INSTANCE = new DefaultComponentFactory();
074
075    /**
076     * The character used to indicate the mnemonic position for labels.
077     */
078    private static final char MNEMONIC_MARKER = '&';
079
080    // Instance *************************************************************
081
082    private DefaultComponentFactory() {
083        // Suppresses default constructor, ensuring non-instantiability.
084    }
085
086    /**
087     * Returns the sole instance of this factory class.
088     *
089     * @return the sole instance of this factory class
090     */
091    public static DefaultComponentFactory getInstance() {
092        return INSTANCE;
093    }
094
095    // Component Creation ***************************************************
096
097    /**
098     * Creates and returns a label with an optional mnemonic.
099     *
100     * <pre>
101     * createLabel("Name");       // No mnemonic
102     * createLabel("N&amp;ame");      // Mnemonic is 'a'
103     * createLabel("Save &amp;as");   // Mnemonic is the second 'a'
104     * createLabel("Look&amp;&amp;Feel"); // No mnemonic, text is Look&amp;Feel
105     * </pre>
106     *
107     * @param textWithMnemonic  the label's text -
108     *     may contain an ampersand (<tt>&amp;</tt>) to mark a mnemonic
109     * @return an label with optional mnemonic
110     */
111    @Override
112    public JLabel createLabel(String textWithMnemonic) {
113        JLabel label = new JLabel();
114        setTextAndMnemonic(label, textWithMnemonic);
115        return label;
116    }
117
118    /**
119     * Creates and returns a title label that uses the foreground color
120     * and font of a <code>TitledBorder</code>.
121     *
122     * <pre>
123     * createTitle("Name");       // No mnemonic
124     * createTitle("N&amp;ame");      // Mnemonic is 'a'
125     * createTitle("Save &amp;as");   // Mnemonic is the second 'a'
126     * createTitle("Look&amp;&amp;Feel"); // No mnemonic, text is Look&amp;Feel
127     * </pre>
128     *
129     * @param textWithMnemonic  the label's text -
130     *     may contain an ampersand (<tt>&amp;</tt>) to mark a mnemonic
131     * @return an emphasized title label
132     */
133    @Override
134    public JLabel createTitle(String textWithMnemonic) {
135        JLabel label = new TitleLabel();
136        setTextAndMnemonic(label, textWithMnemonic);
137        label.setVerticalAlignment(SwingConstants.CENTER);
138        return label;
139    }
140
141    /**
142     * Creates and returns a labeled separator with the label in the left-hand
143     * side. Useful to separate paragraphs in a panel; often a better choice
144     * than a <code>TitledBorder</code>.
145     *
146     * <pre>
147     * createSeparator("Name");       // No mnemonic
148     * createSeparator("N&amp;ame");      // Mnemonic is 'a'
149     * createSeparator("Save &amp;as");   // Mnemonic is the second 'a'
150     * createSeparator("Look&amp;&amp;Feel"); // No mnemonic, text is Look&amp;Feel
151     * </pre>
152     *
153     * @param textWithMnemonic  the label's text -
154     *     may contain an ampersand (<tt>&amp;</tt>) to mark a mnemonic
155     * @return a title label with separator on the side
156     */
157    public JComponent createSeparator(String textWithMnemonic) {
158        return createSeparator(textWithMnemonic, SwingConstants.LEFT);
159    }
160
161    /**
162     * Creates and returns a labeled separator. Useful to separate
163     * paragraphs in a panel, which is often a better choice than a
164     * <code>TitledBorder</code>.
165     *
166     * <pre>
167     * final int LEFT = SwingConstants.LEFT;
168     * createSeparator("Name",       LEFT); // No mnemonic
169     * createSeparator("N&amp;ame",      LEFT); // Mnemonic is 'a'
170     * createSeparator("Save &amp;as",   LEFT); // Mnemonic is the second 'a'
171     * createSeparator("Look&amp;&amp;Feel", LEFT); // No mnemonic, text is Look&amp;Feel
172     * </pre>
173     *
174     * @param textWithMnemonic  the label's text -
175     *     may contain an ampersand (<tt>&amp;</tt>) to mark a mnemonic
176     * @param alignment text alignment, one of <code>SwingConstants.LEFT</code>,
177     *     <code>SwingConstants.CENTER</code>, <code>SwingConstants.RIGHT</code>
178     * @return a separator with title label
179     */
180    @Override
181    public JComponent createSeparator(String textWithMnemonic, int alignment) {
182        if (textWithMnemonic == null || textWithMnemonic.length() == 0) {
183            return new JSeparator();
184        }
185        JLabel title = createTitle(textWithMnemonic);
186        title.setHorizontalAlignment(alignment);
187        return createSeparator(title);
188    }
189
190    /**
191     * Creates and returns a labeled separator. Useful to separate
192     * paragraphs in a panel, which is often a better choice than a
193     * <code>TitledBorder</code>.
194     *
195     * <p>The label's position is determined by the label's horizontal alignment,
196     * which must be one of:
197     * <code>SwingConstants.LEFT</code>,
198     * <code>SwingConstants.CENTER</code>,
199     * <code>SwingConstants.RIGHT</code>.</p>
200     *
201     * <p>TODO: Since this method has been marked public in version 1.0.6,
202     * we need to precisely describe the semantic of this method.</p>
203     *
204     * <p>TODO: Check if we can relax the constraint for the label alignment
205     * and also accept LEADING and TRAILING.</p>
206     *
207     * @param label       the title label component
208     * @return a separator with title label
209     * @exception NullPointerException if the label is <code>null</code>
210     *
211     * @since 1.0.6
212     */
213    public JComponent createSeparator(JLabel label) {
214        if (label == null) {
215            throw new NullPointerException("The label must not be null.");
216        }
217
218        JPanel panel = new JPanel(
219                new TitledSeparatorLayout(!Utilities.isLafAqua()));
220        panel.setOpaque(false);
221        panel.add(label);
222        panel.add(new JSeparator());
223        if (label.getHorizontalAlignment() == SwingConstants.CENTER) {
224            panel.add(new JSeparator());
225        }
226        return panel;
227    }
228
229    // Helper Code ***********************************************************
230
231    /**
232     * Sets the text of the given label and optionally a mnemonic.
233     * The given text may contain an ampersand (<tt>&amp;</tt>)
234     * to mark a mnemonic and its position. Such a marker indicates
235     * that the character that follows the ampersand shall be the mnemonic.
236     * If you want to use the ampersand itself duplicate it, for example
237     * <tt>&quot;Look&amp;&amp;Feel&quot</tt>.
238     *
239     * @param label             the label that gets a mnemonic
240     * @param textWithMnemonic  the text with optional mnemonic marker
241     */
242    private static void setTextAndMnemonic(JLabel label,
243            String textWithMnemonic) {
244        int markerIndex = textWithMnemonic.indexOf(MNEMONIC_MARKER);
245        // No marker at all
246        if (markerIndex == -1) {
247            label.setText(textWithMnemonic);
248            return;
249        }
250        int mnemonicIndex = -1;
251        int begin = 0;
252        int end;
253        int length = textWithMnemonic.length();
254        int quotedMarkers = 0;
255        StringBuffer buffer = new StringBuffer();
256        do {
257            // Check whether the next index has a mnemonic marker, too
258            if (markerIndex + 1 < length && textWithMnemonic
259                    .charAt(markerIndex + 1) == MNEMONIC_MARKER) {
260                end = markerIndex + 1;
261                quotedMarkers++;
262            } else {
263                end = markerIndex;
264                if (mnemonicIndex == -1) {
265                    mnemonicIndex = markerIndex - quotedMarkers;
266                }
267            }
268            buffer.append(textWithMnemonic.substring(begin, end));
269            begin = end + 1;
270            markerIndex = begin < length
271                    ? textWithMnemonic.indexOf(MNEMONIC_MARKER, begin)
272                    : -1;
273        } while (markerIndex != -1);
274        buffer.append(textWithMnemonic.substring(begin));
275
276        String text = buffer.toString();
277        label.setText(text);
278        if (mnemonicIndex != -1 && mnemonicIndex < text.length()) {
279            label.setDisplayedMnemonic(text.charAt(mnemonicIndex));
280            label.setDisplayedMnemonicIndex(mnemonicIndex);
281        }
282    }
283
284    /**
285     * A label that uses the TitleBorder font and color.
286     */
287    @SuppressWarnings("serial")
288    private static final class TitleLabel extends JLabel {
289
290        private TitleLabel() {
291            // Just invoke the super constructor.
292        }
293
294        /**
295         * TODO: For the Synth-based L&amp;f we should consider asking
296         * a <code>TitledBorder</code> instance for its font and color using
297         * <code>#getTitleFont</code> and <code>#getTitleColor</code> resp.
298         */
299        @Override
300        public void updateUI() {
301            super.updateUI();
302            Color foreground = getTitleColor();
303            if (foreground != null) {
304                setForeground(foreground);
305            }
306            setFont(getTitleFont());
307        }
308
309        private Color getTitleColor() {
310            return UIManager.getColor("TitledBorder.titleColor");
311        }
312
313        /**
314         * Looks up and returns the font used for title labels.
315         * Since Mac Aqua uses an inappropriate titled border font,
316         * we use a bold label font instead. Actually if the title
317         * is used in a titled separator, the bold weight is questionable.
318         * It seems that most native Aqua tools use a plain label in
319         * titled separators.
320         *
321         * @return the font used for title labels
322         */
323        private Font getTitleFont() {
324            return Utilities.isLafAqua()
325                    ? UIManager.getFont("Label.font").deriveFont(Font.BOLD)
326                    : UIManager.getFont("TitledBorder.font");
327        }
328
329    }
330
331    /**
332     * A layout for the title label and separator(s) in titled separators.
333     */
334    private static final class TitledSeparatorLayout implements LayoutManager {
335
336        private final boolean centerSeparators;
337
338        /**
339         * Constructs a TitledSeparatorLayout that either centers the separators
340         * or aligns them along the font baseline of the title label.
341         *
342         * @param centerSeparators  true to center, false to align along
343         *     the font baseline of the title label
344         */
345        private TitledSeparatorLayout(boolean centerSeparators) {
346            this.centerSeparators = centerSeparators;
347        }
348
349        /**
350         * Does nothing. This layout manager looks up the components
351         * from the layout container and used the component's index
352         * in the child array to identify the label and separators.
353         *
354         * @param name the string to be associated with the component
355         * @param comp the component to be added
356         */
357        @Override
358        public void addLayoutComponent(String name, Component comp) {
359            // Does nothing.
360        }
361
362        /**
363         * Does nothing. This layout manager looks up the components
364         * from the layout container and used the component's index
365         * in the child array to identify the label and separators.
366         *
367         * @param comp the component to be removed
368         */
369        @Override
370        public void removeLayoutComponent(Component comp) {
371            // Does nothing.
372        }
373
374        /**
375         * Computes and returns the minimum size dimensions
376         * for the specified container. Forwards this request
377         * to <code>#preferredLayoutSize</code>.
378         *
379         * @param parent the component to be laid out
380         * @return the container's minimum size.
381         * @see #preferredLayoutSize(Container)
382         */
383        @Override
384        public Dimension minimumLayoutSize(Container parent) {
385            return preferredLayoutSize(parent);
386        }
387
388        /**
389         * Computes and returns the preferred size dimensions
390         * for the specified container. Returns the title label's
391         * preferred size.
392         *
393         * @param parent the component to be laid out
394         * @return the container's preferred size.
395         * @see #minimumLayoutSize(Container)
396         */
397        @Override
398        public Dimension preferredLayoutSize(Container parent) {
399            Component label = getLabel(parent);
400            Dimension labelSize = label.getPreferredSize();
401            Insets insets = parent.getInsets();
402            int width = labelSize.width + insets.left + insets.right;
403            int height = labelSize.height + insets.top + insets.bottom;
404            return new Dimension(width, height);
405        }
406
407        /**
408         * Lays out the specified container.
409         *
410         * @param parent the container to be laid out
411         */
412        @Override
413        public void layoutContainer(Container parent) {
414            synchronized (parent.getTreeLock()) {
415                // Look up the parent size and insets
416                Dimension size = parent.getSize();
417                Insets insets = parent.getInsets();
418                int width = size.width - insets.left - insets.right;
419
420                // Look up components and their sizes
421                JLabel label = getLabel(parent);
422                Dimension labelSize = label.getPreferredSize();
423                int labelWidth = labelSize.width;
424                int labelHeight = labelSize.height;
425                Component separator1 = parent.getComponent(1);
426                int separatorHeight = separator1.getPreferredSize().height;
427
428                FontMetrics metrics = label.getFontMetrics(label.getFont());
429                int ascent = metrics.getMaxAscent();
430                int hGapDlu = centerSeparators ? 3 : 1;
431                int hGap = Sizes.dialogUnitXAsPixel(hGapDlu, label);
432                int vOffset = centerSeparators
433                        ? 1 + (labelHeight - separatorHeight) / 2
434                        : ascent - separatorHeight / 2;
435
436                int alignment = label.getHorizontalAlignment();
437                int y = insets.top;
438                if (alignment == SwingConstants.LEFT) {
439                    int x = insets.left;
440                    label.setBounds(x, y, labelWidth, labelHeight);
441                    x += labelWidth;
442                    x += hGap;
443                    int separatorWidth = size.width - insets.right - x;
444                    separator1.setBounds(x, y + vOffset, separatorWidth,
445                            separatorHeight);
446                } else if (alignment == SwingConstants.RIGHT) {
447                    int x = insets.left + width - labelWidth;
448                    label.setBounds(x, y, labelWidth, labelHeight);
449                    x -= hGap;
450                    x--;
451                    int separatorWidth = x - insets.left;
452                    separator1.setBounds(insets.left, y + vOffset,
453                            separatorWidth, separatorHeight);
454                } else {
455                    int xOffset = (width - labelWidth - 2 * hGap) / 2;
456                    int x = insets.left;
457                    separator1.setBounds(x, y + vOffset, xOffset - 1,
458                            separatorHeight);
459                    x += xOffset;
460                    x += hGap;
461                    label.setBounds(x, y, labelWidth, labelHeight);
462                    x += labelWidth;
463                    x += hGap;
464                    Component separator2 = parent.getComponent(2);
465                    int separatorWidth = size.width - insets.right - x;
466                    separator2.setBounds(x, y + vOffset, separatorWidth,
467                            separatorHeight);
468                }
469            }
470        }
471
472        private JLabel getLabel(Container parent) {
473            return (JLabel) parent.getComponent(0);
474        }
475
476    }
477
478}