com.jgoodies.forms.builder.DefaultFormBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.jgoodies.forms.builder.DefaultFormBuilder.java

Source

/*
 * Copyright (c) 2002-2014 JGoodies Software GmbH. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of JGoodies Software GmbH nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jgoodies.forms.builder;

import java.awt.Color;
import java.awt.Component;
import java.util.ResourceBundle;

import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.Border;

import com.jgoodies.common.internal.StringResourceAccessor;
import com.jgoodies.common.swing.MnemonicUtils;
import com.jgoodies.forms.layout.ConstantSize;
import com.jgoodies.forms.layout.FormLayout;
import com.jgoodies.forms.layout.FormSpecs;
import com.jgoodies.forms.layout.RowSpec;

/**
 * Provides a means to build form-oriented panels quickly and consistently
 * using the {@link FormLayout}. This builder combines frequently used
 * panel building steps: add a new row, add a label, proceed to the next
 * data column, then add a component.<p>
 *
 * The extra value lies in the {@code #append} methods that
 * append gap rows and component rows if necessary and then add
 * the given components. They are built upon the superclass behavior
 * {@code #appendRow} and the set of {@code #add} methods.
 * A set of component appenders allows to add a textual label and
 * associated component in a single step.<p>
 *
 * This builder can map resource keys to internationalized (i15d) texts
 * when creating text labels, titles and titled separators. Therefore
 * you must specify a {@code ResourceBundle} in the constructor.
 * The builder methods throw an {@code IllegalStateException} if one
 * of the mapping builder methods is invoked and no bundle has been set.<p>
 *
 * You can configure the build process by setting a leading column,
 * enabling the row grouping and by modifying the gaps between normal
 * lines and between paragraphs. The leading column will be honored
 * if the cursor proceeds to the next row. All appended components
 * start in the specified lead column, except appended separators that
 * span all columns.<p>
 *
 * It is temptive to use the DefaultFormBuilder all the time and
 * to let it add rows automatically. Use a simpler style if it increases
 * the code readability. Explicit row specifications and cell constraints
 * make your layout easier to understand - but harder to maintain.
 * See also the accompanying tutorial sources and the Tips &amp; Tricks
 * that are part of the Forms documentation.<p>
 *
 * Sometimes a form consists of many standardized rows but has a few
 * rows that require a customization. The DefaultFormBuilder can do everything
 * that the superclasses {@link com.jgoodies.forms.builder.AbstractFormBuilder}
 * and {@link com.jgoodies.forms.builder.PanelBuilder} can do;
 * among other things: appending new rows and moving the cursor.
 * Again, ask yourself if the DefaultFormBuilder is the appropriate builder.
 * As a rule of thumb you should have more components than builder commands.
 * There are different ways to add custom rows. Find below example code
 * that presents and compares the pros and cons of three approaches.<p>
 *
 * The texts for labels and titles can be <em>marked texts</em>,
 * i.e. strings with an optional mnemonic marker.
 * See the {@link MnemonicUtils} class comment for details.<p>
 *
 * <strong>Example:</strong>
 * <pre>
 * public void build() {
 *     FormLayout layout = new FormLayout(
 *         "right:max(40dlu;pref), 3dlu, 80dlu, 7dlu, " // 1st major colum
 *       + "right:max(40dlu;pref), 3dlu, 80dlu",        // 2nd major column
 *         "");                                         // add rows dynamically
 *     DefaultFormBuilder builder = new DefaultFormBuilder(layout)
 *           .border(Borders.DIALOG);
 *
 *     builder.appendSeparator("Flange");
 *
 *     builder.append("Identifier", identifierField);
 *     builder.nextLine();
 *
 *     builder.append("PTI [kW]",   new JTextField());
 *     builder.append("Power [kW]", new JTextField());
 *
 *     builder.append("s [mm]",     new JTextField());
 *     builder.nextLine();
 *
 *     builder.appendSeparator("Diameters");
 *
 *     builder.append("da [mm]",    new JTextField());
 *     builder.append("di [mm]",    new JTextField());
 *
 *     builder.append("da2 [mm]",   new JTextField());
 *     builder.append("di2 [mm]",   new JTextField());
 *
 *     builder.append("R [mm]",     new JTextField());
 *     builder.append("D [mm]",     new JTextField());
 *
 *     builder.appendSeparator("Criteria");
 *
 *     builder.append("Location",   buildLocationComboBox());
 *     builder.append("k-factor",   new JTextField());
 *
 *     builder.appendSeparator("Bolts");
 *
 *     builder.append("Material",   ViewerUIFactory.buildMaterialComboBox());
 *     builder.nextLine();
 *
 *     builder.append("Numbers",    new JTextField());
 *     builder.nextLine();
 *
 *     builder.append("ds [mm]",    new JTextField());
 * }
 * </pre><p>
 *
 * <strong>Custom Row Example:</strong>
 * <pre>
 * public JComponent buildPanel() {
 *     initComponents();
 *
 *     FormLayout layout = new FormLayout(
 *             "right:pref, 3dlu, default:grow",
 *             "");
 *     DefaultFormBuilder builder = new DefaultFormBuilder(layout)
 *            .border(Borders.DIALOG)
 *          .rowGroupingEnabled(true);
 *
 *     // In this approach, we add a gap and a custom row.
 *     // The advantage of this approach is, that we can express
 *     // the row spec and comment area cell constraints freely.
 *     // The disadvantage is the misalignment of the leading label.
 *     // Also the row's height may be inconsistent with other rows.
 *     builder.appendSeparator("Single Custom Row");
 *     builder.append("Name", name1Field);
 *     builder.appendLineGapRow();
 *     builder.appendRow(RowSpec.decode("top:31dlu")); // Assumes line is 14, gap is 3
 *     builder.nextLine(2);
 *     builder.append("Comment");
 *     builder.add(new JScrollPane(comment1Area),
 *                 CC.xy(builder.getColumn(), builder.getRow(), "fill, fill"));
 *     builder.nextLine();
 *
 *     // In this approach, we append a standard row with gap before it.
 *     // The advantage is, that the leading label is aligned well.
 *     // The disadvantage is that the comment area now spans
 *     // multiple cells and is slightly less flexible.
 *     // Also the row's height may be inconsistent with other rows.
 *     builder.appendSeparator("Standard + Custom Row");
 *     builder.append("Name", name2Field);
 *     builder.append("Comment");
 *     builder.appendRow(RowSpec.decode("17dlu")); // Assumes line is 14, gap is 3
 *     builder.add(new JScrollPane(comment2Area),
 *                 CC.xywh(builder.getColumn(), builder.getRow(), 1, 2));
 *     builder.nextLine(2);
 *
 *     // In this approach, we append two standard rows with associated gaps.
 *     // The advantage is, that the leading label is aligned well,
 *     // and the height is consistent with other rows.
 *     // The disadvantage is that the comment area now spans
 *     // multiple cells and is slightly less flexible.
 *     builder.appendSeparator("Two Standard Rows");
 *     builder.append("Name", name3Field);
 *     builder.append("Comment");
 *     builder.nextLine();
 *     builder.append("");
 *     builder.nextRow(-2);
 *     builder.add(new JScrollPane(comment3Area),
 *                 CC.xywh(builder.getColumn(), builder.getRow(), 1, 3));
 *
 *     return builder.build();
 * }
 * </pre><p>
 *
 * TODO: Consider adding a method for appending a component that spans the
 * remaining columns in the current row. Method name candidates are
 * {@code #appendFullSpan} and {@code #appendRemaining}.
 *
 * @author   Karsten Lentzsch
 * @version $Revision: 1.16 $
 * @since 1.0.3
 *
 * @see   com.jgoodies.forms.builder.AbstractFormBuilder
 * @see   com.jgoodies.forms.layout.FormSpecs
 * @see   com.jgoodies.forms.layout.FormLayout
 */
public final class DefaultFormBuilder extends I15dPanelBuilder {

    /**
     * Holds the row specification that is reused to describe rows
     * that are intended for labels and components.
     *
     * @see #setDefaultRowSpec(RowSpec)
     */
    private RowSpec defaultRowSpec = FormSpecs.PREF_ROWSPEC;

    /**
     * Holds the row specification that is reused to describe
     * the constant gaps between component lines.
     *
     * @see #setLineGapSize(ConstantSize)
     */
    private RowSpec lineGapSpec = FormSpecs.LINE_GAP_ROWSPEC;

    /**
     * Holds the row specification that describes the constant gaps
     * between paragraphs.
     *
     * @see #setParagraphGapSize(ConstantSize)
     */
    private RowSpec paragraphGapSpec = FormSpecs.PARAGRAPH_GAP_ROWSPEC;

    /**
     * Holds the offset of the leading column - often 0 or 1.
     *
     * @see #getLeadingColumnOffset()
     * @see #setLeadingColumnOffset(int)
     * @see #getLeadingColumn()
     */
    private int leadingColumnOffset = 0;

    /**
     * Determines whether new data rows are being grouped or not.
     *
     * @see #isRowGroupingEnabled()
     * @see #setRowGroupingEnabled(boolean)
     */
    private boolean rowGroupingEnabled = false;

    // Instance Creation ****************************************************

    /**
     * Constructs a {@code DefaultFormBuilder} for the given
     * layout.
     *
     * @param layout   the {@code FormLayout} to be used
     *
     * @throws NullPointerException if {@code layout} is {@code null}
     */
    public DefaultFormBuilder(FormLayout layout) {
        this(layout, new JPanel(null));
    }

    /**
     * Constructs a {@code DefaultFormBuilder} for the given
     * layout and panel.
     *
     * @param layout    the {@code FormLayout} to be used
     * @param container     the layout container
     *
     * @throws NullPointerException if {@code layout} or {@code container} is {@code null}
     */
    public DefaultFormBuilder(FormLayout layout, JPanel container) {
        this(layout, (StringResourceAccessor) null, container);
    }

    /**
     * Constructs a {@code DefaultFormBuilder} for the given
     * layout and resource bundle.
     *
     * @param layout    the {@code FormLayout} to be used
     * @param bundle    the {@code ResourceBundle} used to lookup i15d
     * strings
     *
     * @throws NullPointerException if {@code layout} is {@code null}
     */
    public DefaultFormBuilder(FormLayout layout, ResourceBundle bundle) {
        super(layout, bundle);
    }

    /**
     * Constructs a {@code DefaultFormBuilder} for the given
     * layout, resource bundle, and panel.
     *
     * @param layout    the {@code FormLayout} to be used
     * @param container the layout container
     * @param bundle    the {@code ResourceBundle} used to lookup i15d
     *     strings
     *
     * @throws NullPointerException if {@code layout} or {@code container} is {@code null}
     */
    public DefaultFormBuilder(FormLayout layout, ResourceBundle bundle, JPanel container) {
        super(layout, bundle, container);
    }

    /**
     * Constructs a {@code DefaultFormBuilder} for the given
     * layout and resource bundle.
     *
     * @param layout       the {@code FormLayout} to be used
     * @param localizer    used to lookup i15d strings
     *
     * @throws NullPointerException if {@code layout} is {@code null}
     */
    public DefaultFormBuilder(FormLayout layout, StringResourceAccessor localizer) {
        super(layout, localizer);
    }

    /**
     * Constructs a {@code DefaultFormBuilder} for the given
     * layout, resource bundle, and panel.
     *
     * @param layout       the {@code FormLayout} to be used
     * @param container    the layout container
     * @param localizer    used to lookup i15d strings
     *
     * @throws NullPointerException if {@code layout} or {@code container} is {@code null}
     */
    public DefaultFormBuilder(FormLayout layout, StringResourceAccessor localizer, JPanel container) {
        super(layout, localizer, container);
    }

    // Frequently Used Panel Properties ***************************************

    @Override
    public DefaultFormBuilder background(Color background) {
        super.background(background);
        return this;
    }

    @Override
    public DefaultFormBuilder border(Border border) {
        super.border(border);
        return this;
    }

    @Override
    public DefaultFormBuilder border(String emptyBorderSpec) {
        super.border(emptyBorderSpec);
        return this;
    }

    @Override
    public DefaultFormBuilder opaque(boolean b) {
        super.opaque(b);
        return this;
    }

    // Modern (Cascading) Style Configuration *********************************

    /**
     * Sets the row specification that shall be used for component rows.
     * It is {@link FormSpecs#PREF_ROWSPEC} by default.
     *
     * @param defaultRowSpec   the RowSpec to be used for component rows
     */
    public DefaultFormBuilder defaultRowSpec(RowSpec defaultRowSpec) {
        this.defaultRowSpec = defaultRowSpec;
        return this;
    }

    /**
     * Sets the size of gaps between component lines using the given
     * constant size.<p>
     *
     * <strong>Examples:</strong><pre>
     * .lineGapSize(Sizes.ZERO);
     * .lineGapSize(Sizes.DLUY9);
     * .lineGapSize(Sizes.pixel(1));
     * </pre>
     *
     * @param lineGapSize   the {@code ConstantSize} that describes
     *     the size of the gaps between component lines
     */
    public DefaultFormBuilder lineGapSize(ConstantSize lineGapSize) {
        RowSpec rowSpec = RowSpec.createGap(lineGapSize);
        this.lineGapSpec = rowSpec;
        return this;
    }

    /**
     * Sets the size of gaps between paragraphs using the given
     * constant size.<p>
     *
     * <strong>Examples:</strong><pre>
     * .setParagraphGapSize(Sizes.DLUY14);
     * .setParagraphGapSize(Sizes.dluY(22));
     * .setParagraphGapSize(Sizes.pixel(42));
     * </pre>
     *
     * @param paragraphGapSize   the {@code ConstantSize} that describes
     *     the size of the gaps between paragraphs
     */
    public DefaultFormBuilder paragraphGapSize(ConstantSize paragraphGapSize) {
        RowSpec rowSpec = RowSpec.createGap(paragraphGapSize);
        this.paragraphGapSpec = rowSpec;
        return this;
    }

    /**
     * Sets the offset of the leading column, often 0 or 1.
     *
     * @param columnOffset  the new offset of the leading column
     */
    public DefaultFormBuilder leadingColumnOffset(int columnOffset) {
        this.leadingColumnOffset = columnOffset;
        return this;
    }

    /**
     * Enables or disables the grouping of new data rows.
     *
     * @param enabled  indicates grouping enabled, false disabled
     */
    public DefaultFormBuilder rowGroupingEnabled(boolean enabled) {
        rowGroupingEnabled = enabled;
        return this;
    }

    // Old Style Configuration ************************************************

    /**
     * Returns the row specification that is used for component rows.
     *
     * @return the {@code RowSpec} used for component rows
     *
     * @since 1.2
     * @deprecated Obsolete; will be deleted from the next version
     */
    @Deprecated
    public RowSpec getDefaultRowSpec() {
        return defaultRowSpec;
    }

    /**
     * Sets the row specification that shall be used for component rows.
     * It is {@link FormSpecs#PREF_ROWSPEC} by default.
     *
     * @param defaultRowSpec   the RowSpec to be used for component rows
     *
     * @since 1.2
     * @deprecated Replaced by {@link #defaultRowSpec(RowSpec)}
     */
    @Deprecated
    public void setDefaultRowSpec(RowSpec defaultRowSpec) {
        this.defaultRowSpec = defaultRowSpec;
    }

    /**
     * Returns the row specification that is used to separate component row.
     *
     * @return the {@code RowSpec} that is used to separate component rows
     * @deprecated Obsolete; will be deleted from the next version
     */
    @Deprecated
    public RowSpec getLineGapSpec() {
        return lineGapSpec;
    }

    /**
     * Sets the size of gaps between component lines using the given
     * constant size.<p>
     *
     * <strong>Examples:</strong><pre>
     * .setLineGapSize(Sizes.ZERO);
     * .setLineGapSize(Sizes.DLUY9);
     * .setLineGapSize(Sizes.pixel(1));
     * </pre>
     *
     * @param lineGapSize   the {@code ConstantSize} that describes
     *     the size of the gaps between component lines
     * @deprecated Replaced by {@link #lineGapSize(ConstantSize)}
     */
    @Deprecated
    public void setLineGapSize(ConstantSize lineGapSize) {
        RowSpec rowSpec = RowSpec.createGap(lineGapSize);
        this.lineGapSpec = rowSpec;
    }

    /**
     * Sets the size of gaps between paragraphs using the given
     * constant size.<p>
     *
     * <strong>Examples:</strong><pre>
     * .setParagraphGapSize(Sizes.DLUY14);
     * .setParagraphGapSize(Sizes.dluY(22));
     * .setParagraphGapSize(Sizes.pixel(42));
     * </pre>
     *
     * @param paragraphGapSize   the {@code ConstantSize} that describes
     *     the size of the gaps between paragraphs
     * @deprecated Replaced by {@link #lineGapSize(ConstantSize)}
     */
    @Deprecated
    public void setParagraphGapSize(ConstantSize paragraphGapSize) {
        RowSpec rowSpec = RowSpec.createGap(paragraphGapSize);
        this.paragraphGapSpec = rowSpec;
    }

    /**
     * Returns the offset of the leading column, often 0 or 1.
     *
     * @return the offset of the leading column
     * @deprecated Obsolete; will be deleted from the next version
     */
    @Deprecated
    public int getLeadingColumnOffset() {
        return leadingColumnOffset;
    }

    /**
     * Sets the offset of the leading column, often 0 or 1.
     *
     * @param columnOffset  the new offset of the leading column
     * @deprecated Replaced by {@link #leadingColumnOffset(int)}
     */
    @Deprecated
    public void setLeadingColumnOffset(int columnOffset) {
        this.leadingColumnOffset = columnOffset;
    }

    /**
     * Returns whether new data rows are being grouped or not.
     *
     * @return true indicates grouping enabled, false disabled
     * @deprecated Obsolete; will be deleted from the next version
     */
    @Deprecated
    public boolean isRowGroupingEnabled() {
        return rowGroupingEnabled;
    }

    /**
     * Enables or disables the grouping of new data rows.
     *
     * @param enabled  indicates grouping enabled, false disabled
     * @deprecated Replaced by {@link #rowGroupingEnabled(boolean)}
     */
    @Deprecated
    public void setRowGroupingEnabled(boolean enabled) {
        rowGroupingEnabled = enabled;
    }

    // Appending Rows ********************************************************

    /**
     * Appends a row with this builder's line gap size.
     *
     * @see #lineGapSize(ConstantSize)
     * @see #appendRow(String)
     */
    public final void appendLineGapRow() {
        appendRow(lineGapSpec);
    }

    // Filling Columns ******************************************************

    /**
     * Adds a component to the panel using the default constraints
     * with a column span of 1. Then proceeds to the next data column.
     *
     * @param component   the component to add
     */
    public void append(Component component) {
        append(component, 1);
    }

    /**
     * Adds a component to the panel using the default constraints with
     * the given columnSpan. Proceeds to the next data column.
     *
     * @param component the component to append
     * @param columnSpan    the column span used to add
     */
    public void append(Component component, int columnSpan) {
        ensureCursorColumnInGrid();
        ensureHasGapRow(lineGapSpec);
        ensureHasComponentLine();

        add(component, createLeftAdjustedConstraints(columnSpan));
        nextColumn(columnSpan + 1);
    }

    /**
     * Adds two components to the panel; each component will span a single
     * data column. Proceeds to the next data column.
     *
     * @param c1    the first component to add
     * @param c2    the second component to add
     */
    public void append(Component c1, Component c2) {
        append(c1);
        append(c2);
    }

    /**
     * Adds three components to the panel; each component will span a single
     * data column. Proceeds to the next data column.
     *
     * @param c1    the first component to add
     * @param c2    the second component to add
     * @param c3    the third component to add
     */
    public void append(Component c1, Component c2, Component c3) {
        append(c1);
        append(c2);
        append(c3);
    }

    // Appending Labels with optional components ------------------------------

    /**
     * Adds a text label to the panel and proceeds to the next column.
     *
     * @param textWithMnemonic  the label's text - may mark a mnemonic
     * @return the added label
     */
    public JLabel append(String textWithMnemonic) {
        JLabel label = getComponentFactory().createLabel(textWithMnemonic);
        append(label);
        return label;
    }

    /**
     * Adds a text label and component to the panel.
     * Then proceeds to the next data column.<p>
     *
     * The created label is labeling the given component; so the component
     * gets the focus if the (optional) label mnemonic is pressed.
     *
     * @param textWithMnemonic  the label's text - may mark a mnemonic
     * @param component         the component to add
     * @return the added label
     */
    public JLabel append(String textWithMnemonic, Component component) {
        return append(textWithMnemonic, component, 1);
    }

    /**
     * Adds a text label and component to the panel; the component will span
     * the specified number columns. Proceeds to the next data column,
     * and goes to the next line if the boolean flag is set.<p>
     *
     * The created label is labeling the given component; so the component
     * gets the focus if the (optional) label mnemonic is pressed.
     *
     * @param textWithMnemonic  the label's text - may mark a mnemonic
     * @param c                 the component to add
     * @param nextLine          true forces a next line
     * @return the added label
     * @see JLabel#setLabelFor(java.awt.Component)
     */
    public JLabel append(String textWithMnemonic, Component c, boolean nextLine) {
        JLabel label = append(textWithMnemonic, c);
        if (nextLine) {
            nextLine();
        }
        return label;
    }

    /**
     * Adds a text label and component to the panel; the component will span
     * the specified number columns. Proceeds to the next data column.<p>
     *
     * The created label is labeling the given component; so the component
     * gets the focus if the (optional) label mnemonic is pressed.
     *
     * @param textWithMnemonic  the label's text - may mark a mnemonic
     * @param c                 the component to add
     * @param columnSpan        number of columns the component shall span
     * @return the added label
     * @see JLabel#setLabelFor(java.awt.Component)
     */
    public JLabel append(String textWithMnemonic, Component c, int columnSpan) {
        JLabel label = append(textWithMnemonic);
        label.setLabelFor(c);
        append(c, columnSpan);
        return label;
    }

    /**
     * Adds a text label and two components to the panel; each component
     * will span a single column. Proceeds to the next data column.<p>
     *
     * The created label is labeling the first component; so the component
     * gets the focus if the (optional) label mnemonic is pressed.
     *
     * @param textWithMnemonic  the label's text - may mark a mnemonic
     * @param c1    the first component to add
     * @param c2    the second component to add
     * @return the added label
     */
    public JLabel append(String textWithMnemonic, Component c1, Component c2) {
        JLabel label = append(textWithMnemonic, c1);
        append(c2);
        return label;
    }

    /**
     * Adds a text label and two components to the panel; each component
     * will span a single column. Proceeds to the next data column.<p>
     *
     * The created label is labeling the first component; so the component
     * gets the focus if the (optional) label mnemonic is pressed.
     *
     * @param textWithMnemonic  the label's text - may mark a mnemonic
     * @param c1      the first component to add
     * @param c2      the second component to add
     * @param colSpan the column span for the second component
     * @return the created label
     */
    public JLabel append(String textWithMnemonic, Component c1, Component c2, int colSpan) {
        JLabel label = append(textWithMnemonic, c1);
        append(c2, colSpan);
        return label;
    }

    /**
     * Adds a text label and three components to the panel; each component
     * will span a single column. Proceeds to the next data column.<p>
     *
     * The created label is labeling the first component; so the component
     * gets the focus if the (optional) label mnemonic is pressed.
     *
     * @param textWithMnemonic  the label's text - may mark a mnemonic
     * @param c1    the first component to add
     * @param c2    the second component to add
     * @param c3    the third component to add
     * @return the added label
     */
    public JLabel append(String textWithMnemonic, Component c1, Component c2, Component c3) {
        JLabel label = append(textWithMnemonic, c1, c2);
        append(c3);
        return label;
    }

    /**
     * Adds a text label and four components to the panel; each component
     * will span a single column. Proceeds to the next data column.<p>
     *
     * The created label is labeling the first component; so the component
     * gets the focus if the (optional) label mnemonic is pressed.
     *
     * @param textWithMnemonic  the label's text - may mark a mnemonic
     * @param c1    the first component to add
     * @param c2    the second component to add
     * @param c3    the third component to add
     * @param c4    the fourth component to add
     * @return the added label
     */
    public JLabel append(String textWithMnemonic, Component c1, Component c2, Component c3, Component c4) {
        JLabel label = append(textWithMnemonic, c1, c2, c3);
        append(c4);
        return label;
    }

    // Appending internationalized labels with optional components ------------

    /**
     * Adds an internationalized (i15d) text label to the panel using
     * the given resource key and proceeds to the next column.
     *
     * @param resourceKey      the resource key for the the label's text
     * @return the added label
     */
    public JLabel appendI15d(String resourceKey) {
        return append(getResourceString(resourceKey));
    }

    /**
     * Adds an internationalized (i15d) text label and component
     * to the panel. Then proceeds to the next data column.<p>
     *
     * The created label is labeling the given component; so the component
     * gets the focus if the (optional) label mnemonic is pressed.
     *
     * @param resourceKey  the resource key for the text to add
     * @param component  the component to add
     * @return the added label
     */
    public JLabel appendI15d(String resourceKey, Component component) {
        return append(getResourceString(resourceKey), component, 1);
    }

    /**
     * Adds an internationalized (i15d) text label and component
     * to the panel. Then proceeds to the next data column.
     * Goes to the next line if the boolean flag is set.<p>
     *
     * The created label is labeling the first component; so the component
     * gets the focus if the (optional) label mnemonic is pressed.
     *
     * @param resourceKey  the resource key for the text to add
     * @param component    the component to add
     * @param nextLine     true forces a next line
     * @return the added label
     */
    public JLabel appendI15d(String resourceKey, Component component, boolean nextLine) {
        return append(getResourceString(resourceKey), component, nextLine);
    }

    /**
     * Adds an internationalized (i15d) text label to the panel using
     * the given resource key; then proceeds to the next data column
     * and adds a component with the given column span.
     * Proceeds to the next data column.<p>
     *
     * The created label is labeling the first component; so the component
     * gets the focus if the (optional) label mnemonic is pressed.
     *
     * @param resourceKey  the resource key for the text to add
     * @param c           the component to add
     * @param columnSpan  number of columns the component shall span
     * @return the added label
     */
    public JLabel appendI15d(String resourceKey, Component c, int columnSpan) {
        return append(getResourceString(resourceKey), c, columnSpan);
    }

    /**
     * Adds an internationalized (i15d) text label and two components
     * to the panel; each component will span a single column.
     * Proceeds to the next data column.<p>
     *
     * The created label is labeling the first component; so the component
     * gets the focus if the (optional) label mnemonic is pressed.
     *
     * @param resourceKey  the resource key for the text to add
     * @param c1    the first component to add
     * @param c2    the second component to add
     * @return the added label
     */
    public JLabel appendI15d(String resourceKey, Component c1, Component c2) {
        return append(getResourceString(resourceKey), c1, c2);
    }

    /**
     * Adds an internationalized (i15d) text label and two components
     * to the panel; each component will span a single column.
     * Proceeds to the next data column.<p>
     *
     * The created label is labeling the first component; so the component
     * gets the focus if the (optional) label mnemonic is pressed.
     *
     * @param resourceKey  the resource key for the text to add
     * @param c1      the first component to add
     * @param c2      the second component to add
     * @param colSpan the column span for the second component
     * @return the added label
     */
    public JLabel appendI15d(String resourceKey, Component c1, Component c2, int colSpan) {
        return append(getResourceString(resourceKey), c1, c2, colSpan);
    }

    /**
     * Adds an internationalized (i15d) text label and three components
     * to the panel; each component will span a single column.
     * Proceeds to the next data column.<p>
     *
     * The created label is labeling the first component; so the component
     * gets the focus if the (optional) label mnemonic is pressed.
     *
     * @param resourceKey  the resource key for the text to add
     * @param c1    the first component to add
     * @param c2    the second component to add
     * @param c3    the third component to add
     * @return the added label
     */
    public JLabel appendI15d(String resourceKey, Component c1, Component c2, Component c3) {
        return append(getResourceString(resourceKey), c1, c2, c3);
    }

    /**
     * Adds an internationalized (i15d) text label and four components
     * to the panel; each component will span a single column.
     * Proceeds to the next data column.<p>
     *
     * The created label is labeling the first component; so the component
     * gets the focus if the (optional) label mnemonic is pressed.
     *
     * @param resourceKey  the resource key for the text to add
     * @param c1    the first component to add
     * @param c2    the second component to add
     * @param c3    the third component to add
     * @param c4    the third component to add
     * @return the added label
     */
    public JLabel appendI15d(String resourceKey, Component c1, Component c2, Component c3, Component c4) {
        return append(getResourceString(resourceKey), c1, c2, c3, c4);
    }

    // Adding Titles ----------------------------------------------------------

    /**
     * Adds a title label to the panel and proceeds to the next column.
     *
     * @param textWithMnemonic  the label's text - may mark a mnemonic
     * @return the added title label
     */
    public JLabel appendTitle(String textWithMnemonic) {
        JLabel titleLabel = getComponentFactory().createTitle(textWithMnemonic);
        append(titleLabel);
        return titleLabel;
    }

    /**
     * Adds an internationalized title label to the panel and
     * proceeds to the next column.
     *
     * @param resourceKey   the resource key for the title's text
     * @return the added title label
     */
    public JLabel appendI15dTitle(String resourceKey) {
        return appendTitle(getResourceString(resourceKey));
    }

    // Appending Separators ---------------------------------------------------

    /**
     * Adds a separator without text that spans all columns.
     *
     * @return the added titled separator
     */
    public JComponent appendSeparator() {
        return appendSeparator("");
    }

    /**
     * Adds a separator with the given text that spans all columns.
     *
     * @param text      the separator title text
     * @return the added titled separator
     */
    public JComponent appendSeparator(String text) {
        ensureCursorColumnInGrid();
        ensureHasGapRow(paragraphGapSpec);
        ensureHasComponentLine();

        setColumn(super.getLeadingColumn());
        int columnSpan = getColumnCount();
        setColumnSpan(getColumnCount());
        JComponent titledSeparator = addSeparator(text);
        setColumnSpan(1);
        nextColumn(columnSpan);
        return titledSeparator;
    }

    /**
     * Appends an internationalized titled separator for
     * the given resource key that spans all columns.
     *
     * @param resourceKey   the resource key for the separator title's text
     * @return the added titled separator
     */
    public JComponent appendI15dSeparator(String resourceKey) {
        return appendSeparator(getResourceString(resourceKey));
    }

    // Overriding Superclass Behavior ***************************************

    /**
     * Returns the leading column. Unlike the superclass this method
     * honors the column offset.
     *
     * @return the leading column
     */
    @Override
    protected int getLeadingColumn() {
        int column = super.getLeadingColumn();
        return column + leadingColumnOffset * getColumnIncrementSign();
    }

    // Adding Rows **********************************************************

    /**
     * Ensures that the cursor is in the grid. In case it's beyond the
     * form's right hand side, the cursor is moved to the leading column
     * of the next line.
     */
    private void ensureCursorColumnInGrid() {
        if (isLeftToRight() && getColumn() > getColumnCount() || !isLeftToRight() && getColumn() < 1) {
            nextLine();
        }
    }

    /**
     * Ensures that we have a gap row before the next component row.
     * Checks if the current row is the given {@code RowSpec}
     * and appends this row spec if necessary.
     *
     * @param gapRowSpec  the row specification to check for
     */
    private void ensureHasGapRow(RowSpec gapRowSpec) {
        if (getRow() == 1 || getRow() <= getRowCount()) {
            return;
        }

        if (getRow() <= getRowCount()) {
            RowSpec rowSpec = getCursorRowSpec();
            if (rowSpec == gapRowSpec) {
                return;
            }
        }
        appendRow(gapRowSpec);
        nextLine();
    }

    /**
     * Ensures that the form has a component row. Adds a component row
     * if the cursor is beyond the form's bottom.
     */
    private void ensureHasComponentLine() {
        if (getRow() <= getRowCount()) {
            return;
        }
        appendRow(defaultRowSpec);
        if (rowGroupingEnabled) {
            getLayout().addGroupedRow(getRow());
        }
    }

    /**
     * Looks up and returns the row specification of the current row.
     *
     * @return the row specification of the current row
     */
    private RowSpec getCursorRowSpec() {
        return getLayout().getRowSpec(getRow());
    }

}