com.haulmont.cuba.desktop.gui.components.DesktopFieldGroup.java Source code

Java tutorial

Introduction

Here is the source code for com.haulmont.cuba.desktop.gui.components.DesktopFieldGroup.java

Source

/*
 * Copyright (c) 2008-2016 Haulmont.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.haulmont.cuba.desktop.gui.components;

import com.haulmont.chile.core.model.MetaPropertyPath;
import com.haulmont.cuba.core.global.AppBeans;
import com.haulmont.cuba.desktop.App;
import com.haulmont.cuba.desktop.sys.DesktopToolTipManager;
import com.haulmont.cuba.desktop.sys.layout.LayoutAdapter;
import com.haulmont.cuba.desktop.sys.layout.MigLayoutHelper;
import com.haulmont.cuba.desktop.sys.vcl.CollapsiblePanel;
import com.haulmont.cuba.desktop.sys.vcl.ToolTipButton;
import com.haulmont.cuba.gui.ComponentsHelper;
import com.haulmont.cuba.gui.app.security.role.edit.UiPermissionDescriptor;
import com.haulmont.cuba.gui.app.security.role.edit.UiPermissionValue;
import com.haulmont.cuba.gui.components.*;
import com.haulmont.cuba.gui.components.Component;
import com.haulmont.cuba.gui.components.Formatter;
import com.haulmont.cuba.gui.data.CollectionDatasource;
import com.haulmont.cuba.gui.data.Datasource;
import net.miginfocom.layout.CC;
import net.miginfocom.layout.LC;
import net.miginfocom.swing.MigLayout;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
import java.util.*;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.haulmont.bali.util.Preconditions.checkNotNullArgument;

public class DesktopFieldGroup extends DesktopAbstractComponent<JPanel>
        implements FieldGroup, AutoExpanding, Component.UiPermissionAware {

    private final Logger log = LoggerFactory.getLogger(DesktopFieldGroup.class);

    public static final String DEFAULT_FIELD_WIDTH = "200px";

    protected String description;

    protected MigLayout layout;
    protected Datasource datasource;
    protected CollapsiblePanel collapsiblePanel;

    protected int rows = 0;
    protected int cols = 1;

    protected boolean editable = true;
    protected boolean borderVisible = false;

    protected FieldCaptionAlignment captionAlignment;

    protected Map<String, FieldConfig> fields = new HashMap<>();
    protected List<List<FieldConfig>> columnFieldMapping = new ArrayList<>();
    {
        columnFieldMapping.add(new ArrayList<>());
    }

    protected Map<Integer, Integer> columnFieldCaptionWidth = null;
    protected int fieldCaptionWidth = -1;

    protected boolean requestUpdateCaptionWidth = false;

    protected List<EditableChangeListener> editableChangeListeners = new ArrayList<>();

    protected FieldGroupFieldFactory fieldFactory;

    public DesktopFieldGroup() {
        LC lc = new LC();
        lc.hideMode(3); // Invisible components will not participate in the layout at all and it will for instance not take up a grid cell.
        lc.insets("0 0 0 0");
        if (LayoutAdapter.isDebug()) {
            lc.debug(1000);
        }

        layout = new MigLayout(lc);
        impl = new JPanel(layout);
        assignClassDebugProperty(impl);
        collapsiblePanel = new CollapsiblePanel(super.getComposition());
        assignClassDebugProperty(collapsiblePanel);
        collapsiblePanel.setBorderVisible(false);

        setWidth(Component.AUTO_SIZE);

        fieldFactory = AppBeans.get(FieldGroupFieldFactory.NAME);
    }

    /**
     * @return flat list of column fields
     */
    protected List<FieldConfig> getColumnOrderedFields() {
        return columnFieldMapping.stream().flatMap(List::stream).collect(Collectors.toList());
    }

    @Override
    public FieldConfig createField(String id) {
        return new FieldConfigImpl(id);
    }

    @Override
    public List<FieldConfig> getFields() {
        return getColumnOrderedFields();
    }

    @Override
    public List<FieldConfig> getFields(int column) {
        return Collections.unmodifiableList(columnFieldMapping.get(column));
    }

    @Override
    public FieldConfig getField(int column, int row) {
        return columnFieldMapping.get(column).get(row);
    }

    @Override
    public FieldConfig getField(String fieldId) {
        for (final Map.Entry<String, FieldConfig> entry : fields.entrySet()) {
            if (entry.getKey().equals(fieldId)) {
                return entry.getValue();
            }
        }
        return null;
    }

    @Override
    public FieldConfig getFieldNN(String fieldId) {
        FieldConfig fieldConfig = fields.get(fieldId);
        if (fieldConfig == null) {
            throw new IllegalArgumentException("Unable to find field with id " + fieldId);
        }

        return fieldConfig;
    }

    @Override
    public void add(Component childComponent) {
        throw new UnsupportedOperationException("Add component is not supported by FieldGroup component");
    }

    @Override
    public void remove(Component childComponent) {
        throw new UnsupportedOperationException("Remove component is not supported by FieldGroup component");
    }

    @Override
    public void removeAll() {
        throw new UnsupportedOperationException("Remove all components are not supported by FieldGroup component");
    }

    @Nullable
    @Override
    public Component getOwnComponent(String id) {
        FieldConfig fieldConfig = getField(id);
        if (fieldConfig != null && fieldConfig.isBound()) {
            return fieldConfig.getComponent();
        }
        return null;
    }

    @Nullable
    @Override
    public Component getComponent(String id) {
        return ComponentsHelper.getComponent(this, id);
    }

    @Override
    public Collection<Component> getComponents() {
        return ComponentsHelper.getComponents(this);
    }

    @Override
    public List<Component> getOwnComponents() {
        return getColumnOrderedFields().stream().filter(FieldConfig::isBound).map(FieldConfig::getComponent)
                .collect(Collectors.toList());
    }

    @Override
    public void addField(FieldConfig field) {
        addField(field, 0);
    }

    @Override
    public void addField(FieldConfig fc, int colIndex) {
        checkArgument(!fields.containsKey(fc.getId()), "Field '%s' is already registered", fc.getId());
        checkArgument(this == ((FieldConfigImpl) fc).getOwner(), "Field does not belong to this FieldGroup");

        if (colIndex < 0 || colIndex >= getColumns()) {
            throw new IllegalArgumentException(String
                    .format("Illegal column number %s, available amount of columns is %s", colIndex, getColumns()));
        }

        addFieldInternal(fc, colIndex, -1);
    }

    @Override
    public void addField(FieldConfig fc, int colIndex, int rowIndex) {
        checkArgument(!fields.containsKey(fc.getId()), "Field '%s' is already registered", fc.getId());
        checkArgument(this == ((FieldConfigImpl) fc).getOwner(), "Field does not belong to this FieldGroup");

        if (colIndex < 0 || colIndex >= getColumns()) {
            throw new IllegalArgumentException(String
                    .format("Illegal column number %s, available amount of columns is %s", colIndex, getColumns()));
        }
        List<FieldConfig> colFields = columnFieldMapping.get(colIndex);
        if (rowIndex < 0 || rowIndex > colFields.size()) {
            throw new IllegalArgumentException(
                    String.format("Illegal row number %s, available amount of rows is %s", rowIndex, getRows()));
        }

        addFieldInternal(fc, colIndex, rowIndex);
    }

    protected void addFieldInternal(FieldConfig fc, int colIndex, int rowIndex) {
        List<FieldConfig> colFields = columnFieldMapping.get(colIndex);
        if (rowIndex == -1) {
            rowIndex = colFields.size();
        }

        fields.put(fc.getId(), fc);
        colFields.add(rowIndex, fc);

        FieldConfigImpl fci = (FieldConfigImpl) fc;

        fci.setColumn(colIndex);
        fci.setManaged(true);

        if (fc.getComponent() != null) {
            managedFieldComponentAssigned(fci, fci.getAttachMode());

            impl.revalidate();
            impl.repaint();
        }
    }

    @Override
    public void removeField(String fieldId) {
        removeField(getFieldNN(fieldId));
    }

    @Override
    public void removeField(FieldConfig fc) {
        checkArgument(this == ((FieldConfigImpl) fc).getOwner(), "Field is not belong to this FieldGroup");

        if (fields.values().contains(fc)) {
            int colIndex = ((FieldConfigImpl) fc).getColumn();
            columnFieldMapping.get(colIndex).remove(fc);
            fields.remove(fc.getId());

            boolean wasBound = fc.isBound();
            if (fc.isBound()) {
                FieldConfigImpl fci = (FieldConfigImpl) fc;

                impl.remove(fci.getCompositionNN().getComposition());
                if (fci.getLabel() != null) {
                    impl.remove(fci.getLabel());
                }
                if (fci.getToolTipButton() != null) {
                    impl.remove(fci.getToolTipButton());
                }

                reattachColumnFields(colIndex);

                this.rows = detectRowsCount();
            }

            ((FieldConfigImpl) fc).setManaged(false);

            if (fc.getComponent() != null) {
                fc.getComponent().setParent(null);
            }

            if (wasBound) {
                impl.revalidate();
                impl.repaint();
            }
        }
    }

    @Override
    public void requestFocus() {
        for (FieldConfig fc : getColumnOrderedFields()) {
            Component component = fc.getComponent();
            if (component != null && component.isEnabled() && component.isVisible()
                    && component instanceof Focusable && ((Focusable) component).isFocusable()) {

                component.requestFocus();
                break;
            }
        }
    }

    @Override
    public void requestFocus(String fieldId) {
        FieldConfig field = getFieldNN(fieldId);
        Component componentField = field.getComponentNN();
        componentField.requestFocus();
    }

    @Override
    public Datasource getDatasource() {
        return datasource;
    }

    @Override
    public void setDatasource(final Datasource datasource) {
        if (this.datasource != null) {
            throw new UnsupportedOperationException(
                    "Changing datasource is not supported by the FieldGroup component");
        }

        this.datasource = datasource;

        assignAutoDebugId();
    }

    @Override
    public void bind() {
        bindDeclarativeFieldConfigs();
    }

    protected void bindDeclarativeFieldConfigs() {
        List<Integer> reattachColumns = new ArrayList<>();

        for (FieldConfig fc : getColumnOrderedFields()) {
            if (!fc.isCustom() && !fc.isBound()) {
                FieldConfigImpl fci = (FieldConfigImpl) fc;

                Datasource targetDs = fc.getTargetDatasource();
                if (targetDs == null) {
                    throw new IllegalStateException(String.format("Unable to get datasource for field '%s'", id));
                }

                FieldGroupFieldFactory.GeneratedField generatedField = fieldFactory.createField(fc);
                Component fieldComponent = generatedField.getComponent();

                fci.assignComponent(fieldComponent);
                fci.setAttachMode(generatedField.getAttachMode());

                setupFieldComponent(fci);

                DesktopAbstractComponent fieldImpl = (DesktopAbstractComponent) fieldComponent;
                fci.setComposition(fieldImpl);

                assignTypicalAttributes(fieldComponent);

                if (generatedField.getAttachMode() == FieldAttachMode.APPLY_DEFAULTS) {
                    applyFieldDefaults(fci);
                }

                int columnIndex = fci.getColumn();
                if (!reattachColumns.contains(columnIndex)) {
                    reattachColumns.add(columnIndex);
                }
            }
        }

        if (!reattachColumns.isEmpty()) {
            this.rows = detectRowsCount();

            for (Integer reattachColumnIndex : reattachColumns) {
                reattachColumnFields(reattachColumnIndex);
            }
        }

        impl.revalidate();
        impl.repaint();
    }

    protected void setupFieldComponent(FieldConfig fieldConfig) {
        Component fieldComponent = fieldConfig.getComponent();

        if (fieldComponent instanceof DesktopCheckBox) {
            fieldComponent.setAlignment(Alignment.MIDDLE_LEFT);
        }
    }

    protected void reattachColumnFields(int colIndex) {
        fields.values().stream().filter(FieldConfig::isBound).map(fieldConfig -> ((FieldConfigImpl) fieldConfig))
                .filter(fci -> fci.getColumn() == colIndex).forEach(fci -> {
                    impl.remove(fci.getCompositionNN().getComposition());
                    if (fci.getLabel() != null) {
                        impl.remove(fci.getLabel());
                    }
                    if (fci.getToolTipButton() != null) {
                        impl.remove(fci.getToolTipButton());
                    }
                });

        List<FieldConfig> columnFCs = columnFieldMapping.get(colIndex);
        int insertRowIndex = 0;
        for (FieldConfig fc : columnFCs) {
            if (fc.isBound()) {
                FieldConfigImpl fci = (FieldConfigImpl) fc;

                Component fieldComponent = fci.getComponentNN();
                JComponent composition = fieldComponent.unwrapComposition(JComponent.class);

                JLabel label = fci.getLabel();
                if (label != null) {
                    int preferredCaptionWidth = getPreferredCaptionWidth(colIndex);
                    if (preferredCaptionWidth > 0) {
                        label.setPreferredSize(new Dimension(preferredCaptionWidth, 25));
                        label.setMaximumSize(new Dimension(preferredCaptionWidth, 25));
                        label.setMinimumSize(new Dimension(preferredCaptionWidth, 25));
                    } else {
                        label.setPreferredSize(new Dimension(label.getPreferredSize().width, 25));
                    }
                    label.setVisible(fieldComponent.isVisible());

                    CC labelCc = new CC();
                    MigLayoutHelper.applyAlignment(labelCc, Alignment.TOP_LEFT);

                    impl.add(label, labelCc.cell(colIndex * 3, insertRowIndex, 1, 1));
                }

                ToolTipButton toolTipButton = fci.getToolTipButton();
                if (fci.getToolTipButton() != null) {
                    updateTooltipButton(fci, fieldComponent);

                    DesktopToolTipManager.getInstance().registerTooltip(toolTipButton);
                    impl.add(toolTipButton, new CC().cell(colIndex * 3 + 2, insertRowIndex, 1, 1).alignY("top"));
                }

                CC cell = new CC().cell(colIndex * 3 + 1, insertRowIndex, 1, 1);

                MigLayoutHelper.applyWidth(cell, (int) fieldComponent.getWidth(), fieldComponent.getWidthUnits(),
                        false);
                MigLayoutHelper.applyHeight(cell, (int) fieldComponent.getHeight(), fieldComponent.getHeightUnits(),
                        false);
                MigLayoutHelper.applyAlignment(cell, fieldComponent.getAlignment());

                composition.putClientProperty(getSwingPropertyId(), fci.getId());
                impl.add(composition, cell);

                insertRowIndex++;
            }
        }

        impl.validate();
        impl.repaint();
    }

    protected int detectRowsCount() {
        int rowsCount = 0;
        for (List<FieldConfig> fields : columnFieldMapping) {
            long boundCount = fields.stream().filter(FieldConfig::isBound).count();

            rowsCount = (int) Math.max(rowsCount, boundCount);
        }
        return Math.max(rowsCount, 1);
    }

    protected void managedFieldComponentAssigned(FieldConfigImpl fci, FieldAttachMode mode) {
        DesktopAbstractComponent fieldImpl = (DesktopAbstractComponent) fci.getComponentNN();
        fci.setComposition(fieldImpl);

        if (StringUtils.isNotEmpty(fci.getContextHelpText())
                || hasContextHelpIconClickListeners(fci.getComponentNN())) {
            updateTooltipButton(fci, fci.getComponentNN());
        }

        if (fieldImpl.getCaption() != null) {
            fci.getLabel().setText(fieldImpl.getCaption());
        }

        assignTypicalAttributes(fci.getComponentNN());

        if (mode == FieldAttachMode.APPLY_DEFAULTS) {
            applyFieldDefaults(fci);
        }

        this.rows = detectRowsCount();

        reattachColumnFields(fci.getColumn());
    }

    protected void applyFieldDefaults(FieldConfigImpl fci) {
        Component fieldComponent = fci.getComponentNN();

        if (fieldComponent instanceof Field) {
            Field cubaField = (Field) fieldComponent;

            if (fci.getTargetCaption() != null) {
                cubaField.setCaption(fci.getTargetCaption());
            }
            if (fci.getTargetDescription() != null) {
                cubaField.setDescription(fci.getTargetDescription());
            }
            if (fci.getTargetRequired() != null) {
                cubaField.setRequired(fci.getTargetRequired());
            }
            if (fci.getTargetRequiredMessage() != null) {
                cubaField.setRequiredMessage(fci.getTargetRequiredMessage());
            }
            if (fci.getTargetContextHelpText() != null) {
                cubaField.setContextHelpText(fci.getTargetContextHelpText());
            }
            if (fci.getTargetContextHelpTextHtmlEnabled() != null) {
                cubaField.setContextHelpTextHtmlEnabled(fci.getTargetContextHelpTextHtmlEnabled());
            }
            if (fci.getTargetContextHelpIconClickHandler() != null) {
                cubaField.setContextHelpIconClickHandler(fci.getTargetContextHelpIconClickHandler());
            }
            if (fci.getTargetEditable() != null) {
                cubaField.setEditable(fci.getTargetEditable());
            }
            if (fci.getTargetVisible() != null) {
                cubaField.setVisible(fci.getTargetVisible());
            }
            if (cubaField instanceof Component.Focusable && fci.getTargetTabIndex() != null) {
                ((Component.Focusable) cubaField).setTabIndex(fci.getTargetTabIndex());
            }
            for (Field.Validator validator : fci.getTargetValidators()) {
                cubaField.addValidator(validator);
            }

            if (fci.getTargetWidth() != null) {
                fieldComponent.setWidth(fci.getTargetWidth());
            } else {
                fieldComponent.setWidth(DEFAULT_FIELD_WIDTH);
            }
        } else {
            DesktopAbstractComponent composition = fci.getCompositionNN();
            if (fci.getTargetCaption() != null) {
                fci.getLabel().setText(fci.getTargetCaption());
            }
            if (fci.getTargetVisible() != null) {
                composition.setVisible(fci.getTargetVisible());
            }
            if (fci.getTargetWidth() != null) {
                composition.setWidth(fci.getTargetWidth());
            } else {
                composition.setWidth(DEFAULT_FIELD_WIDTH);
            }
        }

        if (fieldComponent instanceof Component.HasFormatter && fci.getTargetFormatter() != null) {
            ((Component.HasFormatter) fieldComponent).setFormatter(fci.getTargetFormatter());
        }

        if (StringUtils.isNotEmpty(fci.getTargetStylename())) {
            fieldComponent.setStyleName(fci.getTargetStylename());
        }

        App app = App.getInstance();
        if (app != null && app.isTestMode()) {
            fci.getCompositionNN().getComposition().setName(fci.getId());
        }
    }

    protected void doSetParentEnabled(FieldConfig fc, boolean enabled) {
        if (fc.getComponent() != null) {
            Component component = fc.getComponent();
            if (component instanceof DesktopAbstractComponent) {
                ((DesktopAbstractComponent) component).setParentEnabled(enabled);
            }

            FieldConfigImpl fci = (FieldConfigImpl) fc;
            if (fci.getLabel() != null) {
                fci.getLabel().setEnabled(enabled);
            }
        }
    }

    @Override
    public void updateEnabled() {
        super.updateEnabled();

        for (FieldConfig field : fields.values()) {
            doSetParentEnabled(field, parentEnabled && enabled);
        }
    }

    @Override
    public boolean isBorderVisible() {
        return borderVisible;
    }

    @Override
    public void setBorderVisible(boolean borderVisible) {
        this.borderVisible = borderVisible;
        collapsiblePanel.setBorderVisible(borderVisible);
    }

    @Override
    public FieldCaptionAlignment getCaptionAlignment() {
        return captionAlignment;
    }

    @Override
    public void setCaptionAlignment(FieldCaptionAlignment captionAlignment) {
        this.captionAlignment = captionAlignment;

        log.warn("setCaptionAlignment not implemented for desktop");
    }

    @Override
    public int getFieldCaptionWidth() {
        return fieldCaptionWidth;
    }

    @Override
    public void setFieldCaptionWidth(int fixedCaptionWidth) {
        this.fieldCaptionWidth = fixedCaptionWidth;

        updateCaptionWidths();
    }

    @Override
    public int getFieldCaptionWidth(int column) {
        if (columnFieldCaptionWidth != null) {
            Integer value = columnFieldCaptionWidth.get(column);
            return value != null ? value : -1;
        }
        return -1;
    }

    @Override
    public void setFieldCaptionWidth(int column, int width) {
        if (columnFieldCaptionWidth == null) {
            columnFieldCaptionWidth = new HashMap<>();
        }
        columnFieldCaptionWidth.put(column, width);

        updateCaptionWidths();
    }

    protected void updateCaptionWidths() {
        if (!requestUpdateCaptionWidth) {
            SwingUtilities.invokeLater(() -> {
                requestUpdateCaptionWidth = false;

                for (FieldConfig fieldConfig : fields.values()) {
                    JLabel label = ((FieldConfigImpl) fieldConfig).getLabel();

                    if (label != null) {
                        int col = ((FieldConfigImpl) fieldConfig).getColumn();
                        int preferredCaptionWidth = getPreferredCaptionWidth(col);

                        if (preferredCaptionWidth > 0) {
                            label.setPreferredSize(new Dimension(preferredCaptionWidth, 25));
                            label.setMaximumSize(new Dimension(preferredCaptionWidth, 25));
                            label.setMinimumSize(new Dimension(preferredCaptionWidth, 25));
                        }
                    }
                }
            });
            requestUpdateCaptionWidth = true;
        }
    }

    protected int getPreferredCaptionWidth(int col) {
        int preferredCaptionWidth = -1;
        if (fieldCaptionWidth > 0) {
            preferredCaptionWidth = fieldCaptionWidth;
        }
        if (columnFieldCaptionWidth != null && columnFieldCaptionWidth.containsKey(col)) {
            preferredCaptionWidth = columnFieldCaptionWidth.get(col);
        }
        return preferredCaptionWidth;
    }

    @Override
    public int getColumns() {
        return cols;
    }

    public int getRows() {
        return rows;
    }

    public DesktopAbstractComponent getCellComponent(int colIndex, int rowIndex) {
        if (colIndex < 0 || colIndex >= getColumns()) {
            throw new IllegalArgumentException(String
                    .format("Illegal column number %s, available amount of columns is %s", colIndex, getColumns()));
        }
        List<FieldConfig> colFields = columnFieldMapping.get(colIndex);
        if (rowIndex < 0 || rowIndex > colFields.size()) {
            throw new IllegalArgumentException(String
                    .format("Illegal column number %s, available amount of columns is %s", colIndex, getColumns()));
        }

        for (FieldConfig fieldConfig : fields.values()) {
            DesktopAbstractComponent composition = ((FieldConfigImpl) fieldConfig).getComposition();
            if (composition != null) {
                JComponent jComponent = composition.getComposition();
                Object componentConstraints = layout.getComponentConstraints(jComponent);
                if (componentConstraints instanceof CC) {
                    CC cc = (CC) componentConstraints;
                    if (cc.getCellY() == rowIndex) {
                        int ccColIndex = (cc.getCellX() - 1) / 3;
                        if (colIndex == ccColIndex) {
                            return composition;
                        }
                    }
                }
            }
        }
        return null;
    }

    @Override
    public void setColumns(int columns) {
        if (this.cols != columns) {
            this.cols = columns;

            List<List<FieldConfig>> oldColumnFields = this.columnFieldMapping;
            this.columnFieldMapping = new ArrayList<>();
            for (int i = 0; i < columns; i++) {
                if (i < oldColumnFields.size()) {
                    columnFieldMapping.add(oldColumnFields.get(i));
                } else {
                    columnFieldMapping.add(new ArrayList<>());
                }
            }
        }
    }

    @Override
    public float getColumnExpandRatio(int col) {
        return 0;
    }

    @Override
    public void setColumnExpandRatio(int col, float ratio) {
    }

    @Override
    public FieldGroupFieldFactory getFieldFactory() {
        return fieldFactory;
    }

    @Override
    public void setFieldFactory(FieldGroupFieldFactory fieldFactory) {
        this.fieldFactory = fieldFactory;
    }

    @Override
    public void addCustomField(String fieldId, CustomFieldGenerator fieldGenerator) {
        FieldConfig field = getField(fieldId);
        if (field == null) {
            throw new IllegalArgumentException(String.format("Field '%s' doesn't exist", fieldId));
        }
        addCustomField(field, fieldGenerator);
    }

    @Override
    public void addCustomField(FieldConfig fc, CustomFieldGenerator fieldGenerator) {
        if (!fc.isCustom()) {
            throw new IllegalStateException(String.format("Field '%s' must be defined as custom", fc.getId()));
        }

        FieldConfigImpl fci = (FieldConfigImpl) fc;

        Component fieldComponent = fieldGenerator.generateField(fc.getTargetDatasource(), fci.getTargetProperty());
        fc.setComponent(fieldComponent);
    }

    @Override
    public void setId(String id) {
        super.setId(id);

        if (id != null && App.getInstance().isTestMode()) {
            for (FieldConfig fc : fields.values()) {
                Component fieldComponent = fc.getComponent();
                if (fieldComponent != null) {
                    JComponent jComponent = DesktopComponentsHelper.getComposition(fieldComponent);
                    if (jComponent != null) {
                        jComponent.setName(fc.getId());
                    }
                }
            }
        }
    }

    protected void assignTypicalAttributes(Component c) {
        if (c instanceof BelongToFrame) {
            BelongToFrame belongToFrame = (BelongToFrame) c;
            if (belongToFrame.getFrame() == null) {
                belongToFrame.setFrame(getFrame());
            }
        }
        c.setParent(this);
    }

    @Override
    public boolean isEditable() {
        return editable;
    }

    @Override
    public void setEditable(boolean editable) {
        if (editable != isEditable()) {
            this.editable = editable;

            EditableChangeEvent event = new EditableChangeEvent(this);
            for (EditableChangeListener listener : new ArrayList<>(editableChangeListeners)) {
                listener.editableChanged(event);
            }
        }
    }

    @Override
    public String getCaption() {
        return collapsiblePanel.getCaption();
    }

    @Override
    public void setCaption(String caption) {
        collapsiblePanel.setCaption(caption);
    }

    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public JComponent getComposition() {
        return collapsiblePanel;
    }

    @Override
    public boolean expandsWidth() {
        return true;
    }

    @Override
    public boolean expandsHeight() {
        return false;
    }

    @Override
    public boolean isValid() {
        try {
            validate();
            return true;
        } catch (ValidationException e) {
            return false;
        }
    }

    @Override
    public void validate() throws ValidationException {
        if (!isVisible() || !isEditableWithParent() || !isEnabled()) {
            return;
        }

        Map<Component.Validatable, ValidationException> problemFields = null; // lazily initialized

        // validate column by column
        List<FieldConfig> fieldsByColumns = getColumnOrderedFields();

        for (FieldConfig fc : fieldsByColumns) {
            Component fieldComponent = fc.getComponent();

            // If has valid state
            if ((fieldComponent instanceof Validatable) && (fieldComponent instanceof Editable)) {
                // If editable
                try {
                    ((Validatable) fieldComponent).validate();
                } catch (ValidationException ex) {
                    if (problemFields == null) {
                        problemFields = new LinkedHashMap<>();
                    }
                    problemFields.put((Validatable) fieldComponent, ex);
                }
            }
        }

        if (problemFields != null && !problemFields.isEmpty()) {
            FieldsValidationException validationException = new FieldsValidationException();
            validationException.setProblemFields(problemFields);

            throw validationException;
        }
    }

    @Override
    protected String getAlternativeDebugId() {
        if (id != null) {
            return id;
        }
        if (datasource != null && StringUtils.isNotEmpty(datasource.getId())) {
            return "fieldGroup_" + datasource.getId();
        }

        return getClass().getSimpleName();
    }

    public void updateCaptionVisibility(DesktopAbstractComponent child) {
        FieldConfig field = fields.values().stream().filter(entry -> entry.getComponent() == child).findFirst()
                .orElse(null);

        FieldConfigImpl fci = (FieldConfigImpl) field;
        if (fci != null && fci.getLabel() != null) {
            fci.getLabel().setVisible(child.isComponentVisible());
        }
    }

    public void updateCaptionText(DesktopAbstractComponent child) {
        FieldConfig field = fields.values().stream().filter(entry -> entry.getComponent() == child).findFirst()
                .orElse(null);

        FieldConfigImpl fci = (FieldConfigImpl) field;
        if (fci != null && fci.getLabel() != null) {
            fci.getLabel().setText(child.getCaption());
        }
    }

    public void updateChildEnabled(DesktopAbstractComponent child) {
        FieldConfig field = fields.values().stream().filter(entry -> entry.getComponent() == child).findFirst()
                .orElse(null);

        FieldConfigImpl fci = (FieldConfigImpl) field;
        if (fci != null && fci.getLabel() != null) {
            fci.getLabel().setEnabled(child.isEnabledWithParent());
        }
    }

    public void updateContextHelp(DesktopAbstractComponent child) {
        FieldConfig field = fields.values().stream().filter(entry -> entry.getComponent() == child).findFirst()
                .orElse(null);

        FieldConfigImpl fci = (FieldConfigImpl) field;
        if (fci != null) {
            updateTooltipButton(fci, child);
        }
    }

    protected void updateTooltipButton(FieldConfigImpl fci, Component component) {
        ToolTipButton toolTipButton = fci.getToolTipButton();

        boolean hasContextHelpIconClickListeners = hasContextHelpIconClickListeners(component);
        String contextHelpText = DesktopComponentsHelper.getContextHelpText(fci.getContextHelpText(),
                BooleanUtils.isTrue(fci.isContextHelpTextHtmlEnabled()));

        ActionListener toolTipButtonActionListener = fci.getToolTipButtonActionListener();
        if (hasContextHelpIconClickListeners) {
            if (toolTipButtonActionListener == null) {
                toolTipButtonActionListener = e -> fireContextHelpIconClickEvent(component);
                toolTipButton.addActionListener(toolTipButtonActionListener);
            }

            toolTipButton.setToolTipText(null);
        } else {
            if (toolTipButtonActionListener != null) {
                toolTipButton.removeActionListener(toolTipButtonActionListener);
                fci.setToolTipButtonActionListener(null);
            }

            toolTipButton.setToolTipText(contextHelpText);
        }

        toolTipButton.setVisible(component.isVisible()
                && (StringUtils.isNotEmpty(contextHelpText) || hasContextHelpIconClickListeners));
    }

    protected boolean hasContextHelpIconClickListeners(Component component) {
        return component instanceof HasContextHelpClickHandler
                && ((HasContextHelpClickHandler) component).getContextHelpIconClickHandler() != null;
    }

    protected void fireContextHelpIconClickEvent(Component component) {
        if (component instanceof HasContextHelpClickHandler) {
            ContextHelpIconClickEvent event = new ContextHelpIconClickEvent((HasContextHelp) component);
            ((HasContextHelpClickHandler) component).fireContextHelpIconClickEvent(event);
        }
    }

    @Override
    public void applyPermission(UiPermissionDescriptor permissionDescriptor) {
        checkNotNullArgument(permissionDescriptor);

        final String subComponentId = permissionDescriptor.getSubComponentId();
        final UiPermissionValue permissionValue = permissionDescriptor.getPermissionValue();
        final String screenId = permissionDescriptor.getScreenId();

        if (subComponentId != null) {
            final FieldGroup.FieldConfig field = getField(subComponentId);
            if (field != null) {
                if (permissionValue == UiPermissionValue.HIDE) {
                    field.setVisible(false);
                } else if (permissionValue == UiPermissionValue.READ_ONLY) {
                    field.setEditable(false);
                }
            } else {
                log.info("Couldn't find suitable component {} in window {} for UI security rule", subComponentId,
                        screenId);
            }
        } else {
            final String actionHolderComponentId = permissionDescriptor.getActionHolderComponentId();
            FieldConfig fieldConfig = getField(actionHolderComponentId);
            if (fieldConfig == null || fieldConfig.getComponent() == null
                    || !((fieldConfig.getComponent() instanceof Component.SecuredActionsHolder))) {
                log.info("Couldn't find suitable component {} in window {} for UI security rule",
                        actionHolderComponentId, screenId);
                return;
            }

            Component fieldComponent = fieldConfig.getComponent();
            String actionId = permissionDescriptor.getActionId();
            ActionsPermissions permissions = ((SecuredActionsHolder) fieldComponent).getActionsPermissions();
            if (permissionValue == UiPermissionValue.HIDE) {
                permissions.addHiddenActionPermission(actionId);
            } else if (permissionValue == UiPermissionValue.READ_ONLY) {
                permissions.addDisabledActionPermission(actionId);
            }
        }
    }

    @Override
    public void addEditableChangeListener(EditableChangeListener listener) {
        checkNotNullArgument(listener);

        if (!editableChangeListeners.contains(listener)) {
            editableChangeListeners.add(listener);
        }
    }

    @Override
    public void removeEditableChangeListener(EditableChangeListener listener) {
        checkNotNullArgument(listener);

        editableChangeListeners.remove(listener);
    }

    public class FieldConfigImpl implements FieldConfig {
        protected String id;
        protected Element xmlDescriptor;
        protected int column;

        protected Component component;
        protected DesktopAbstractComponent composition;
        protected JLabel label = new JLabel();
        protected ToolTipButton toolTipButton = new ToolTipButton();

        protected boolean managed = false;

        protected String targetWidth;
        protected String targetStylename;
        protected Datasource targetDatasource;
        protected Boolean targetRequired;
        protected Boolean targetEditable;
        protected Boolean targetEnabled;
        protected Boolean targetVisible;
        protected String targetProperty;
        protected Integer targetTabIndex;
        protected String targetRequiredMessage;
        protected CollectionDatasource targetOptionsDatasource;
        protected String targetCaption;
        protected String targetDescription;
        protected String targetContextHelpText;
        protected Boolean targetContextHelpTextHtmlEnabled;
        protected Formatter targetFormatter;
        protected boolean isTargetCustom;

        protected ActionListener toolTipButtonActionListener;

        protected List<Field.Validator> targetValidators = new ArrayList<>(0);
        protected Consumer<ContextHelpIconClickEvent> targetContextHelpIconClickHandler;
        protected FieldAttachMode attachMode = FieldAttachMode.APPLY_DEFAULTS;

        public FieldConfigImpl(String id) {
            this.id = id;
        }

        @Override
        public String getId() {
            return id;
        }

        @Override
        public boolean isBound() {
            return component != null;
        }

        public FieldGroup getOwner() {
            return DesktopFieldGroup.this;
        }

        @Override
        public String getWidth() {
            if (composition != null && isWrapped()) {
                return ComponentsHelper.getComponentWidth(composition);
            }
            if (component != null) {
                return ComponentsHelper.getComponentWidth(component);
            }
            return targetWidth;
        }

        @Override
        public void setWidth(String width) {
            if (composition != null && isWrapped()) {
                composition.setWidth(width);
            } else if (component != null) {
                component.setWidth(width);
            } else {
                targetWidth = width;
            }
        }

        @Override
        public String getStyleName() {
            if (component != null) {
                return component.getStyleName();
            }
            return targetStylename;
        }

        @Override
        public void setStyleName(String stylename) {
            if (component != null) {
                component.setStyleName(stylename);

                if (composition != null && isWrapped()) {
                    composition.setStyleName(stylename);
                }
            } else {
                this.targetStylename = stylename;
            }
        }

        protected boolean isWrapped() {
            return component != null && !(component instanceof Field);
        }

        @Override
        public Datasource getTargetDatasource() {
            if (component instanceof DatasourceComponent) {
                return ((DatasourceComponent) component).getDatasource();
            }
            if (targetDatasource != null) {
                return targetDatasource;
            }
            return DesktopFieldGroup.this.datasource;
        }

        @Override
        public Datasource getDatasource() {
            if (component instanceof DatasourceComponent) {
                return ((DatasourceComponent) component).getDatasource();
            }

            return targetDatasource;
        }

        @Override
        public void setDatasource(Datasource datasource) {
            checkState(this.component == null, "FieldConfig is already bound to component");

            this.targetDatasource = datasource;
        }

        @Override
        public Boolean isRequired() {
            if (component instanceof Field) {
                return ((Field) component).isRequired();
            }
            return targetRequired;
        }

        @Override
        public void setRequired(Boolean required) {
            if (component instanceof Field) {
                checkNotNullArgument(required, "Unable to reset required flag for the bound FieldConfig");

                ((Field) component).setRequired(required);
            } else {
                this.targetRequired = required;
            }
        }

        @Override
        public Boolean isEditable() {
            if (component instanceof Editable) {
                return ((Field) component).isEditable();
            }
            return targetEditable;
        }

        @Override
        public void setEditable(Boolean editable) {
            if (component instanceof Editable) {
                checkNotNullArgument(editable, "Unable to reset editable flag for the bound FieldConfig");

                ((Editable) component).setEditable(editable);
            } else {
                this.targetEditable = editable;
            }
        }

        @Override
        public Boolean isEnabled() {
            if (component != null) {
                return component.isEnabled();
            }
            return targetEnabled;
        }

        @Override
        public void setEnabled(Boolean enabled) {
            if (component != null) {
                checkNotNullArgument(enabled, "Unable to reset enabled flag for the bound FieldConfig");

                component.setEnabled(enabled);

                if (composition != null && isWrapped()) {
                    composition.setEnabled(enabled);
                }
            } else {
                this.targetEnabled = enabled;
            }
        }

        @Override
        public Boolean isVisible() {
            if (component != null) {
                return component.isVisible();
            }
            return targetVisible;
        }

        @Override
        public void setVisible(Boolean visible) {
            if (component != null) {
                checkNotNullArgument(visible, "Unable to reset visible flag for the bound FieldConfig");

                component.setVisible(visible);

                if (composition != null && isWrapped()) {
                    composition.setVisible(visible);
                }
            } else {
                this.targetVisible = visible;
            }
        }

        @Override
        public String getProperty() {
            if (component instanceof DatasourceComponent) {
                MetaPropertyPath metaPropertyPath = ((DatasourceComponent) component).getMetaPropertyPath();
                return metaPropertyPath != null ? metaPropertyPath.toString() : null;
            }
            return targetProperty;
        }

        @Override
        public void setProperty(String property) {
            checkState(this.component == null, "Unable to change property for bound FieldConfig");

            this.targetProperty = property;
        }

        @Override
        public Integer getTabIndex() {
            return targetTabIndex;
        }

        @Override
        public void setTabIndex(Integer tabIndex) {
            if (component instanceof Focusable) {
                checkNotNullArgument(tabIndex, "Unable to reset tabIndex for the bound FieldConfig");

                ((Focusable) component).setTabIndex(tabIndex);
            } else {
                this.targetTabIndex = tabIndex;
            }
        }

        @Override
        public String getRequiredError() {
            return getRequiredMessage();
        }

        @Override
        public void setRequiredError(String requiredError) {
            setRequiredMessage(requiredError);
        }

        @Override
        public boolean isCustom() {
            return isTargetCustom;
        }

        @Override
        public void setCustom(boolean custom) {
            checkState(this.component == null, "Unable to change custom flag for bound FieldConfig");

            this.isTargetCustom = custom;
        }

        @Override
        public String getRequiredMessage() {
            if (component instanceof Field) {
                return ((Field) component).getRequiredMessage();
            }
            return targetRequiredMessage;
        }

        @Override
        public void setRequiredMessage(String requiredMessage) {
            if (component instanceof Field) {
                ((Field) component).setRequiredMessage(requiredMessage);
            } else {
                this.targetRequiredMessage = requiredMessage;
            }
        }

        @Nullable
        @Override
        public Component getComponent() {
            return component;
        }

        @Override
        public Component getComponentNN() {
            if (component == null) {
                throw new IllegalStateException("FieldConfig is not bound to a Component");
            }
            return component;
        }

        @Override
        public void setComponent(Component component) {
            checkState(this.component == null, "Unable to change component for bound FieldConfig");

            this.component = component;

            if (managed && component != null) {
                managedFieldComponentAssigned(this, FieldAttachMode.APPLY_DEFAULTS);
            }
        }

        @Override
        public void setComponent(Component component, FieldAttachMode mode) {
            checkState(this.component == null, "Unable to change component for bound FieldConfig");

            this.attachMode = mode;
            this.component = component;

            if (managed && component != null) {
                managedFieldComponentAssigned(this, mode);
            }
        }

        public void assignComponent(Component component) {
            checkState(this.component == null, "Unable to change component for bound FieldConfig");

            this.component = component;
        }

        public void setAttachMode(FieldAttachMode attachMode) {
            this.attachMode = attachMode;
        }

        @Override
        public void addValidator(Field.Validator validator) {
            if (component instanceof Field) {
                ((Field) component).addValidator(validator);
            } else {
                if (!targetValidators.contains(validator)) {
                    targetValidators.add(validator);
                }
            }
        }

        @Override
        public void removeValidator(Field.Validator validator) {
            if (component instanceof Field) {
                ((Field) component).removeValidator(validator);
            }
            targetValidators.remove(validator);
        }

        @Override
        public void setOptionsDatasource(CollectionDatasource optionsDatasource) {
            if (component instanceof OptionsField) {
                ((OptionsField) component).setOptionsDatasource(optionsDatasource);
            } else {
                this.targetOptionsDatasource = optionsDatasource;
            }
        }

        @Override
        public CollectionDatasource getOptionsDatasource() {
            if (component instanceof OptionsField) {
                return ((OptionsField) component).getOptionsDatasource();
            }
            return targetOptionsDatasource;
        }

        @Override
        public String getCaption() {
            if (component instanceof Field) {
                return ((Field) component).getCaption();
            }
            if (composition != null && isWrapped()) {
                return label.getText();
            }
            return targetCaption;
        }

        @Override
        public void setCaption(String caption) {
            if (component instanceof Field) {
                ((Field) component).setCaption(caption);
            } else if (composition != null && isWrapped()) {
                label.setText(caption);
            } else {
                this.targetCaption = caption;
            }
        }

        @Override
        public String getContextHelpText() {
            if (component instanceof Field) {
                return ((Field) component).getContextHelpText();
            }
            return targetContextHelpText;
        }

        @Override
        public void setContextHelpText(String contextHelpText) {
            if (component instanceof Field) {
                ((Field) component).setContextHelpText(contextHelpText);
            } else {
                this.targetContextHelpText = contextHelpText;
            }
        }

        @Override
        public Boolean isContextHelpTextHtmlEnabled() {
            if (component instanceof Field) {
                return ((Field) component).isContextHelpTextHtmlEnabled();
            }
            return targetContextHelpTextHtmlEnabled;
        }

        @Override
        public void setContextHelpTextHtmlEnabled(Boolean enabled) {
            if (component instanceof Field) {
                ((Field) component).setContextHelpTextHtmlEnabled(enabled);
            } else {
                this.targetContextHelpTextHtmlEnabled = enabled;
            }
        }

        @Override
        public Consumer<ContextHelpIconClickEvent> getContextHelpIconClickHandler() {
            if (component instanceof Field) {
                return ((Field) component).getContextHelpIconClickHandler();
            }
            return targetContextHelpIconClickHandler;
        }

        @Override
        public void setContextHelpIconClickHandler(Consumer<ContextHelpIconClickEvent> handler) {
            if (component instanceof Field) {
                ((Field) component).setContextHelpIconClickHandler(handler);
            } else {
                this.targetContextHelpIconClickHandler = handler;
            }
        }

        @Override
        public String getDescription() {
            if (component instanceof Field) {
                return ((Field) component).getDescription();
            }
            return targetDescription;
        }

        @Override
        public void setDescription(String description) {
            if (component instanceof Field) {
                ((Field) component).setDescription(description);
            } else {
                this.targetDescription = description;
            }
        }

        @Override
        public Formatter getFormatter() {
            if (component instanceof HasFormatter) {
                return ((HasFormatter) component).getFormatter();
            }
            return targetFormatter;
        }

        @Override
        public void setFormatter(Formatter formatter) {
            if (component instanceof HasFormatter) {
                ((HasFormatter) component).setFormatter(formatter);
            } else {
                this.targetFormatter = formatter;
            }
        }

        @Override
        public Element getXmlDescriptor() {
            return xmlDescriptor;
        }

        @Override
        public void setXmlDescriptor(Element element) {
            this.xmlDescriptor = element;
        }

        public int getColumn() {
            return column;
        }

        public void setColumn(int column) {
            this.column = column;
        }

        @Nullable
        public DesktopAbstractComponent getComposition() {
            return composition;
        }

        public DesktopAbstractComponent getCompositionNN() {
            if (composition == null) {
                throw new IllegalStateException("FieldConfig is not bound to a Component");
            }
            return composition;
        }

        public void setComposition(DesktopAbstractComponent composition) {
            checkState(this.composition == null, "Unable to change composition for bound FieldConfig");

            this.composition = composition;

            if (isWrapped()) {
                if (targetCaption != null) {
                    label.setText(targetCaption);
                } else {
                    label.setText(composition.getCaption());
                }
            }
        }

        public boolean isManaged() {
            return managed;
        }

        public void setManaged(boolean managed) {
            this.managed = managed;
        }

        public ActionListener getToolTipButtonActionListener() {
            return toolTipButtonActionListener;
        }

        public void setToolTipButtonActionListener(ActionListener toolTipButtonActionListener) {
            this.toolTipButtonActionListener = toolTipButtonActionListener;
        }

        public String getTargetWidth() {
            return targetWidth;
        }

        public void setTargetWidth(String targetWidth) {
            this.targetWidth = targetWidth;
        }

        public String getTargetStylename() {
            return targetStylename;
        }

        public void setTargetStylename(String targetStylename) {
            this.targetStylename = targetStylename;
        }

        public void setTargetDatasource(Datasource targetDatasource) {
            this.targetDatasource = targetDatasource;
        }

        public Boolean getTargetRequired() {
            return targetRequired;
        }

        public void setTargetRequired(Boolean targetRequired) {
            this.targetRequired = targetRequired;
        }

        public Boolean getTargetEditable() {
            return targetEditable;
        }

        public void setTargetEditable(Boolean targetEditable) {
            this.targetEditable = targetEditable;
        }

        public Boolean getTargetEnabled() {
            return targetEnabled;
        }

        public void setTargetEnabled(Boolean targetEnabled) {
            this.targetEnabled = targetEnabled;
        }

        public Boolean getTargetVisible() {
            return targetVisible;
        }

        public void setTargetVisible(Boolean targetVisible) {
            this.targetVisible = targetVisible;
        }

        public String getTargetProperty() {
            return targetProperty;
        }

        public void setTargetProperty(String targetProperty) {
            this.targetProperty = targetProperty;
        }

        public Integer getTargetTabIndex() {
            return targetTabIndex;
        }

        public void setTargetTabIndex(Integer targetTabIndex) {
            this.targetTabIndex = targetTabIndex;
        }

        public String getTargetRequiredMessage() {
            return targetRequiredMessage;
        }

        public void setTargetRequiredMessage(String targetRequiredMessage) {
            this.targetRequiredMessage = targetRequiredMessage;
        }

        public String getTargetContextHelpText() {
            return targetContextHelpText;
        }

        public void setTargetContextHelpText(String targetContextHelpText) {
            this.targetContextHelpText = targetContextHelpText;
        }

        public Boolean getTargetContextHelpTextHtmlEnabled() {
            return targetContextHelpTextHtmlEnabled;
        }

        public void setTargetContextHelpTextHtmlEnabled(Boolean targetContextHelpTextHtmlEnabled) {
            this.targetContextHelpTextHtmlEnabled = targetContextHelpTextHtmlEnabled;
        }

        public Consumer<ContextHelpIconClickEvent> getTargetContextHelpIconClickHandler() {
            return targetContextHelpIconClickHandler;
        }

        public void setTargetContextHelpIconClickHandler(
                Consumer<ContextHelpIconClickEvent> targetContextHelpIconClickHandler) {
            this.targetContextHelpIconClickHandler = targetContextHelpIconClickHandler;
        }

        public CollectionDatasource getTargetOptionsDatasource() {
            return targetOptionsDatasource;
        }

        public void setTargetOptionsDatasource(CollectionDatasource targetOptionsDatasource) {
            this.targetOptionsDatasource = targetOptionsDatasource;
        }

        public String getTargetCaption() {
            return targetCaption;
        }

        public void setTargetCaption(String targetCaption) {
            this.targetCaption = targetCaption;
        }

        public String getTargetDescription() {
            return targetDescription;
        }

        public void setTargetDescription(String targetDescription) {
            this.targetDescription = targetDescription;
        }

        public Formatter getTargetFormatter() {
            return targetFormatter;
        }

        public void setTargetFormatter(Formatter targetFormatter) {
            this.targetFormatter = targetFormatter;
        }

        public List<Field.Validator> getTargetValidators() {
            return targetValidators;
        }

        public void setTargetValidators(List<Field.Validator> targetValidators) {
            this.targetValidators = targetValidators;
        }

        public FieldAttachMode getAttachMode() {
            return attachMode;
        }

        public JLabel getLabel() {
            return label;
        }

        public void setLabel(JLabel label) {
            this.label = label;
        }

        public ToolTipButton getToolTipButton() {
            return toolTipButton;
        }

        public void setToolTipButton(ToolTipButton toolTipButton) {
            this.toolTipButton = toolTipButton;
        }

        @Override
        public String toString() {
            return "FieldConfig: " + id;
        }
    }
}