001: /*
002: * Copyright (c) 2002-2004 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:
031: package com.jgoodies.forms.factories;
032:
033: import java.awt.Color;
034: import java.awt.Component;
035: import java.awt.Container;
036: import java.awt.Dimension;
037: import java.awt.Font;
038: import java.awt.FontMetrics;
039: import java.awt.Insets;
040: import java.awt.LayoutManager;
041: import java.beans.PropertyChangeEvent;
042: import java.beans.PropertyChangeListener;
043:
044: import javax.swing.JComponent;
045: import javax.swing.JLabel;
046: import javax.swing.JPanel;
047: import javax.swing.JSeparator;
048: import javax.swing.LookAndFeel;
049: import javax.swing.SwingConstants;
050: import javax.swing.UIManager;
051:
052: import com.jgoodies.forms.layout.Sizes;
053:
054: /**
055: * A singleton implementaton of the {@link ComponentFactory} interface that
056: * creates UI components as required by the
057: * {@link com.jgoodies.forms.builder.PanelBuilder}.
058: * <p>
059: *
060: * The texts used in methods <code>#createLabel(String)</code> and
061: * <code>#createTitle(String)</code> can contain an optional mnemonic marker.
062: * The mnemonic and mnemonic index are indicated by a single ampersand (<tt>&</tt>).
063: * For example <tt>"&Save"</tt>, or
064: * <tt>"Save &as"</tt>. To use the ampersand itself
065: * duplicate it, for example <tt>"Look&&Feel"</tt>.
066: *
067: * @author Karsten Lentzsch
068: * @version $Revision: 1.2 $
069: */
070:
071: public class DefaultComponentFactory implements ComponentFactory {
072:
073: /**
074: * Holds the single instance of this class.
075: */
076: private static final DefaultComponentFactory INSTANCE = new DefaultComponentFactory();
077:
078: /**
079: * The character used to indicate the mnemonic position for labels.
080: */
081: private static final char MNEMONIC_MARKER = '&';
082:
083: // Instance *************************************************************
084:
085: private DefaultComponentFactory() {
086: // Suppresses default constructor, ensuring non-instantiability.
087: }
088:
089: /**
090: * Returns the sole instance of this factory class.
091: *
092: * @return the sole instance of this factory class
093: */
094: public static DefaultComponentFactory getInstance() {
095: return INSTANCE;
096: }
097:
098: // Component Creation ***************************************************
099:
100: /**
101: * Creates and returns a label with an optional mnemonic.
102: * <p>
103: *
104: * <pre>
105: * createLabel("Name"); // No mnemonic
106: * createLabel("N&ame"); // Mnemonic is 'a'
107: * createLabel("Save &as"); // Mnemonic is the second 'a'
108: * createLabel("Look&&Feel"); // No mnemonic, text is Look&Feel
109: * </pre>
110: *
111: * @param textWithMnemonic
112: * the label's text - may contain a mnemonic
113: * @return an label with optional mnemonic
114: */
115: public JLabel createLabel(String textWithMnemonic) {
116: JLabel label = new JLabel();
117: setTextAndMnemonic(label, textWithMnemonic);
118: return label;
119: }
120:
121: /**
122: * Creates and returns a label that uses the foreground color and font of a
123: * <code>TitledBorder</code>.
124: * <p>
125: *
126: * <pre>
127: * createTitle("Name"); // No mnemonic
128: * createTitle("N&ame"); // Mnemonic is 'a'
129: * createTitle("Save &as"); // Mnemonic is the second 'a'
130: * createTitle("Look&&Feel"); // No mnemonic, text is Look&Feel
131: * </pre>
132: *
133: * @param textWithMnemonic
134: * the title's text - may contain a mnemonic
135: * @return an emphasized title label
136: */
137: public JLabel createTitle(String textWithMnemonic) {
138: JLabel label = new TitleLabel();
139: setTextAndMnemonic(label, textWithMnemonic);
140: label.setVerticalAlignment(SwingConstants.CENTER);
141: return label;
142: }
143:
144: /**
145: * Creates and returns a labeled separator with the label in the left-hand
146: * side. Useful to separate paragraphs in a panel; often a better choice
147: * than a <code>TitledBorder</code>.
148: *
149: * @param text
150: * the title's text
151: * @return a title label with separator on the side
152: */
153: public JComponent createSeparator(String text) {
154: return createSeparator(text, SwingConstants.LEFT);
155: }
156:
157: /**
158: * Creates and returns a labeled separator. Useful to separate paragraphs in
159: * a panel, which is often a better choice than a <code>TitledBorder</code>.
160: *
161: * @param text
162: * the title's text
163: * @param alignment
164: * text alignment: left, center, right
165: * @return a separator with title label
166: */
167: public JComponent createSeparator(String text, int alignment) {
168: if (text == null || text.length() == 0) {
169: return new JSeparator();
170: }
171: JLabel title = createTitle(text);
172: title.setHorizontalAlignment(alignment);
173: return createSeparator(title);
174: }
175:
176: /**
177: * Creates and returns a labeled separator. Useful to separate paragraphs in
178: * a panel, which is often a better choice than a <code>TitledBorder</code>.
179: * <p>
180: *
181: * The label's position is determined by the label's horizontal alignment.
182: *
183: * @param label
184: * the title label component
185: * @return a separator with title label
186: * @throws NullPointerException
187: * if the label is <code>null</code>
188: */
189: private JComponent createSeparator(JLabel label) {
190: if (label == null)
191: throw new NullPointerException(
192: "The label must not be null.");
193:
194: JPanel panel = new JPanel(new TitledSeparatorLayout(
195: !isLafAqua()));
196: panel.setOpaque(false);
197: panel.add(label);
198: panel.add(new JSeparator());
199: if (label.getHorizontalAlignment() == SwingConstants.CENTER) {
200: panel.add(new JSeparator());
201: }
202: return panel;
203: }
204:
205: // Helper Code ***********************************************************
206:
207: /**
208: * Sets the text of the given label and optionally a mnemonic. The given
209: * text may contain mnemonic markers <b>&&</b>, where such a marker
210: * indicates that the following character shall be the mnemonic. If you want
211: * to use the <b>\&</b> charachter, just put two together, for example
212: * "&&&&".
213: *
214: * @param label
215: * the label that gets a mnemonic
216: * @param textWithMnemonic
217: * the text with optional mnemonic marker
218: */
219: private static void setTextAndMnemonic(JLabel label,
220: String textWithMnemonic) {
221: int markerIndex = textWithMnemonic.indexOf(MNEMONIC_MARKER);
222: // No marker at all
223: if (markerIndex == -1) {
224: label.setText(textWithMnemonic);
225: return;
226: }
227: int mnemonicIndex = -1;
228: int begin = 0;
229: int end;
230: int length = textWithMnemonic.length();
231: StringBuffer buffer = new StringBuffer();
232: do {
233: // Check whether the next index has a mnemonic marker, too
234: if (markerIndex + 1 < length
235: && textWithMnemonic.charAt(markerIndex + 1) == MNEMONIC_MARKER) {
236: end = markerIndex + 1;
237: } else {
238: end = markerIndex;
239: if (mnemonicIndex == -1)
240: mnemonicIndex = markerIndex;
241: }
242: buffer.append(textWithMnemonic.substring(begin, end));
243: begin = end + 1;
244: markerIndex = begin < length ? textWithMnemonic.indexOf(
245: MNEMONIC_MARKER, begin) : -1;
246: } while (markerIndex != -1);
247: buffer.append(textWithMnemonic.substring(begin));
248:
249: label.setText(buffer.toString());
250: if ((mnemonicIndex != -1) && (mnemonicIndex + 1 < length)) {
251: label.setDisplayedMnemonic(textWithMnemonic
252: .charAt(mnemonicIndex + 1));
253: label.setDisplayedMnemonicIndex(mnemonicIndex);
254: }
255: }
256:
257: // A label that uses the TitleBorder font and color
258: private static class TitleLabel extends JLabel {
259:
260: private TitleLabel() {
261: // Just invoke the super constructor.
262: }
263:
264: private TitleLabel(String text) {
265: super (text);
266: }
267:
268: /**
269: * TODO: Consider asking a <code>TitledBorder</code> instance for its
270: * font and font color use <code>#getTitleFont</code> and
271: * <code>#getTitleColor</code> for the Synth-based looks.
272: */
273: public void updateUI() {
274: super .updateUI();
275: Color foreground = UIManager
276: .getColor("TitledBorder.titleColor");
277: if (foreground != null)
278: setForeground(foreground);
279: setFont(getTitleFont());
280: }
281:
282: /**
283: * Looks up and returns the font used for title labels. Since Mac Aqua
284: * uses an inappropriate titled border font, we use a bold label font
285: * instead. Actually if the title is used in a titled separator, the
286: * bold weight is questionable. It seems that most native Aqua tools use
287: * a plain label in titled separators.
288: *
289: * @return the font used for title labels
290: */
291: private Font getTitleFont() {
292: return isLafAqua() ? UIManager.getFont("Label.font")
293: .deriveFont(Font.BOLD) : UIManager
294: .getFont("TitledBorder.font");
295: }
296:
297: }
298:
299: // A layout for the title label and separator(s) in titled separators.
300: private static class TitledSeparatorLayout implements LayoutManager {
301:
302: private final boolean centerSeparators;
303:
304: /**
305: * Constructs a TitledSeparatorLayout that either centers the separators
306: * or aligns them along the font baseline of the title label.
307: *
308: * @param centerSeparators
309: * true to center, false to align along the font baseline of
310: * the title label
311: */
312: private TitledSeparatorLayout(boolean centerSeparators) {
313: this .centerSeparators = centerSeparators;
314: }
315:
316: /**
317: * Does nothing. This layout manager looks up the components from the
318: * layout container and used the component's index in the child array to
319: * identify the label and separators.
320: *
321: * @param name
322: * the string to be associated with the component
323: * @param comp
324: * the component to be added
325: */
326: public void addLayoutComponent(String name, Component comp) {
327: // Does nothing.
328: }
329:
330: /**
331: * Does nothing. This layout manager looks up the components from the
332: * layout container and used the component's index in the child array to
333: * identify the label and separators.
334: *
335: * @param comp
336: * the component to be removed
337: */
338: public void removeLayoutComponent(Component comp) {
339: // Does nothing.
340: }
341:
342: /**
343: * Computes and returns the minimum size dimensions for the specified
344: * container. Forwards this request to <code>#preferredLayoutSize</code>.
345: *
346: * @param parent
347: * the component to be laid out
348: * @return the container's minimum size.
349: * @see #preferredLayoutSize(Container)
350: */
351: public Dimension minimumLayoutSize(Container parent) {
352: return preferredLayoutSize(parent);
353: }
354:
355: /**
356: * Computes and returns the preferred size dimensions for the specified
357: * container. Returns the title label's preferred size.
358: *
359: * @param parent
360: * the component to be laid out
361: * @return the container's preferred size.
362: * @see #minimumLayoutSize(Container)
363: */
364: public Dimension preferredLayoutSize(Container parent) {
365: Component label = getLabel(parent);
366: Dimension labelSize = label.getPreferredSize();
367: Insets insets = parent.getInsets();
368: int width = labelSize.width + insets.left + insets.right;
369: int height = labelSize.height + insets.top + insets.bottom;
370: return new Dimension(width, height);
371: }
372:
373: /**
374: * Lays out the specified container.
375: *
376: * @param parent
377: * the container to be laid out
378: */
379: public void layoutContainer(Container parent) {
380: synchronized (parent.getTreeLock()) {
381: // Look up the parent size and insets
382: Dimension size = parent.getSize();
383: Insets insets = parent.getInsets();
384: int width = size.width - insets.left - insets.right;
385:
386: // Look up components and their sizes
387: JLabel label = getLabel(parent);
388: Dimension labelSize = label.getPreferredSize();
389: int labelWidth = labelSize.width;
390: int labelHeight = labelSize.height;
391: Component separator1 = parent.getComponent(1);
392: int separatorHeight = separator1.getPreferredSize().height;
393:
394: FontMetrics metrics = label.getFontMetrics(label
395: .getFont());
396: int ascent = metrics.getMaxAscent();
397: int hGapDlu = centerSeparators ? 3 : 1;
398: int hGap = Sizes.dialogUnitXAsPixel(hGapDlu, label);
399: int vOffset = centerSeparators ? 1 + (labelHeight - separatorHeight) / 2
400: : ascent - separatorHeight / 2;
401:
402: int alignment = label.getHorizontalAlignment();
403: int y = insets.top;
404: if (alignment == JLabel.LEFT) {
405: int x = insets.left;
406: label.setBounds(x, y, labelWidth, labelHeight);
407: x += labelWidth;
408: x += hGap;
409: int separatorWidth = size.width - insets.right - x;
410: separator1.setBounds(x, y + vOffset,
411: separatorWidth, separatorHeight);
412: } else if (alignment == JLabel.RIGHT) {
413: int x = insets.left + width - labelWidth;
414: label.setBounds(x, y, labelWidth, labelHeight);
415: x -= hGap;
416: x--;
417: int separatorWidth = x - insets.left;
418: separator1.setBounds(insets.left, y + vOffset,
419: separatorWidth, separatorHeight);
420: } else {
421: int xOffset = (width - labelWidth - 2 * hGap) / 2;
422: int x = insets.left;
423: separator1.setBounds(x, y + vOffset, xOffset - 1,
424: separatorHeight);
425: x += xOffset;
426: x += hGap;
427: label.setBounds(x, y, labelWidth, labelHeight);
428: x += labelWidth;
429: x += hGap;
430: Component separator2 = parent.getComponent(2);
431: int separatorWidth = size.width - insets.right - x;
432: separator2.setBounds(x, y + vOffset,
433: separatorWidth, separatorHeight);
434: }
435: }
436: }
437:
438: private JLabel getLabel(Container parent) {
439: return (JLabel) parent.getComponent(0);
440: }
441:
442: }
443:
444: // TODO: Move the code below this line to a new class
445: // com.jgoodies.forms.util.Utilities
446:
447: // Caching and Lazily Computing the Laf State *****************************
448:
449: /**
450: * Holds the cached result of the Aqua l&f check. Is invalidated by the
451: * <code>LookAndFeelChangeHandler</code> if the look&feel changes.
452: */
453: private static Boolean cachedIsLafAqua;
454:
455: /**
456: * Describes whether the <code>LookAndFeelChangeHandler</code> has been
457: * registered with the <code>UIManager</code> or not. It is registered
458: * lazily when the first cached l&f state is computed.
459: */
460: private static boolean changeHandlerRegistered = false;
461:
462: private synchronized static void ensureLookAndFeelChangeHandlerRegistered() {
463: if (!changeHandlerRegistered) {
464: UIManager
465: .addPropertyChangeListener(new LookAndFeelChangeHandler());
466: changeHandlerRegistered = true;
467: }
468: }
469:
470: /**
471: * Lazily checks and answers whether the Aqua look&feel is active.
472: *
473: * @return true if the current look&feel is Aqua
474: */
475: private static boolean isLafAqua() {
476: if (cachedIsLafAqua == null) {
477: cachedIsLafAqua = Boolean.valueOf(computeIsLafAqua());
478: ensureLookAndFeelChangeHandlerRegistered();
479: }
480: return cachedIsLafAqua.booleanValue();
481: }
482:
483: /**
484: * Computes and answers whether the Aqua look&feel is active.
485: *
486: * @return true if the current look&feel is Aqua
487: */
488: private static boolean computeIsLafAqua() {
489: LookAndFeel laf = UIManager.getLookAndFeel();
490: return laf.getName().startsWith("Mac OS X Aqua");
491: }
492:
493: // Listens to changes of the Look and Feel and invalidates the cache
494: private static class LookAndFeelChangeHandler implements
495: PropertyChangeListener {
496:
497: /**
498: * Invalidates the cached laf states if the look&feel changes.
499: *
500: * @param evt
501: * describes the property change
502: */
503: public void propertyChange(PropertyChangeEvent evt) {
504: cachedIsLafAqua = null;
505: }
506: }
507:
508: }
|