/*******************************************************************************
 * Copyright (c) 2012, 2020 Original authors and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Original authors and others - initial API and implementation
 *     Dirk Fauth <dirk.fauth@googlemail.com> - Bug 454111
 *     Ryan McHale <rpmc22@gmail.com> - Bug 484716
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.widget;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.nebula.widgets.nattable.edit.EditConstants;
import org.eclipse.nebula.widgets.nattable.style.CellStyleAttributes;
import org.eclipse.nebula.widgets.nattable.style.CellStyleUtil;
import org.eclipse.nebula.widgets.nattable.style.HorizontalAlignmentEnum;
import org.eclipse.nebula.widgets.nattable.style.IStyle;
import org.eclipse.nebula.widgets.nattable.style.VerticalAlignmentEnum;
import org.eclipse.nebula.widgets.nattable.ui.matcher.LetterOrDigitKeyEventMatcher;
import org.eclipse.nebula.widgets.nattable.util.ArrayUtil;
import org.eclipse.nebula.widgets.nattable.util.GUIHelper;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.ShellListener;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;

/**
 * Customized combobox control that supports editing directly in the text field
 * and selecting items from the dropdown.
 *
 * <p>
 * This control supports the ability for multi select in the dropdown of the
 * combo which is not available for the SWT Combo control. This feature was
 * added with Nebula NatTable 1.0.0
 *
 * <p>
 * The following style bits are supported by this control.
 *
 * @see SWT#BORDER (if a border should be added to the Text control)
 * @see SWT#READ_ONLY (default for Text control, if this is missing, the Text
 *      control can be edited)
 * @see SWT#CHECK (if the items in the combo should be showed with checkboxes)
 * @see SWT#MULTI (if multi selection is allowed)
 */
public class NatCombo extends Composite {

    /**
     * Default String that is used to separate values in the String
     * representation showed in the text control if multiselect is supported.
     */
    public static final String DEFAULT_MULTI_SELECT_VALUE_SEPARATOR = ", "; //$NON-NLS-1$
    /**
     * Default String that is used to prefix the generated String representation
     * showed in the text control if multiselect is supported.
     */
    public static final String DEFAULT_MULTI_SELECT_PREFIX = "["; //$NON-NLS-1$
    /**
     * String that is used to suffix the generated String representation showed
     * in the text control if multiselect is supported.
     */
    public static final String DEFAULT_MULTI_SELECT_SUFFIX = "]"; //$NON-NLS-1$
    /**
     * The default number of visible items on open the combo.
     */
    public static final int DEFAULT_NUM_OF_VISIBLE_ITEMS = 5;

    /**
     * The IStyle that is used for rendering the Text and the combo control. The
     * important configurations used are horizontal alignment, background and
     * foreground color and font.
     */
    protected final IStyle cellStyle;

    /**
     * The maximum number of visible items of the combo. Setting this value to
     * -1 will result in always showing all items at once.
     */
    protected int maxVisibleItems;

    /**
     * The items that are showed within the combo transformed to a
     * java.util.List. Needed for indexed operations in the dropdown
     */
    protected java.util.List<String> itemList;

    /**
     * Map used to hold the selection state of items in the drop. Needed to
     * maintain state when filtering
     *
     * @since 1.4
     */
    protected Map<String, Boolean> selectionStateMap;

    /**
     * The text control allowing filtering of options
     *
     * @since 1.4
     */
    protected Text filterBox;

    /**
     * The text control of this NatCombo, allowing to enter values directly.
     */
    protected Text text;

    /**
     * The Shell containing the dropdown of this NatCombo
     */
    protected Shell dropdownShell;

    /**
     * The Table control used for the combo component of this NatCombo
     */
    protected Table dropdownTable;

    /**
     * The Table control used for the combo component of this NatCombo
     *
     * @since 1.4
     */
    protected TableViewer dropdownTableViewer;

    /**
     * The image that is shown at the right edge of the text control if the
     * NatCombo is opened.
     */
    protected Image iconImage;

    /**
     * The style bits that where set on creation time. Needed in case the
     * dropdown shell was disposed and needs to be created again.
     *
     * @since 2.0 - Renamed from style to widgetStyle to avoid possible
     *        confusions with Widget#style.
     */
    protected final int widgetStyle;

    /**
     * Flag that indicated whether this NatCombo supports filtering of the
     * values in the dropdown control
     *
     * @since 1.4
     */
    protected boolean showDropdownFilter;

    /**
     * Flag that indicates whether this ComboBoxCellEditor supports free editing
     * in the text control of the NatCombo or not. By default free editing is
     * disabled.
     */
    protected boolean freeEdit;

    /**
     * Flag that indicates whether this NatCombo supports multiselect or not. By
     * default multiselect is disabled.
     */
    protected boolean multiselect;

    /**
     * Flag that indicates whether checkboxes should be shown for the items in
     * the dropdown.
     */
    protected boolean useCheckbox;

    /**
     * String that is used to separate values in the String representation
     * showed in the text control if multiselect is supported.
     */
    protected String multiselectValueSeparator = DEFAULT_MULTI_SELECT_VALUE_SEPARATOR;
    /**
     * String that is used to prefix the generated String representation showed
     * in the text control if multiselect is supported. Needed to visualize the
     * multiselection to the user.
     */
    protected String multiselectTextPrefix = DEFAULT_MULTI_SELECT_PREFIX;
    /**
     * String that is used to suffix the generated String representation showed
     * in the text control if multiselect is supported. Needed to visualize the
     * multiselection to the user.
     */
    protected String multiselectTextSuffix = DEFAULT_MULTI_SELECT_SUFFIX;

    /**
     * Flag that tells whether the NatCombo has focus or not. The flag is set by
     * the FocusListenerWrapper that is set as focus listener on both, the Text
     * control and the dropdown table control. This flag is necessary as the
     * NatCombo has focus if either of both controls have focus.
     */
    private boolean hasFocus = false;
    /**
     * The list of FocusListener that contains the listeners that will be
     * informed if the NatCombo control gains or looses focus. We keep our own
     * list of listeners because the two controls that are combined in this
     * control share the same focus.
     */
    private List<FocusListener> focusListener = new ArrayList<>();

    /**
     * List of KeyListener that should be added to the dropdown table once it is
     * created. Kept locally because the table creation is deferred to the first
     * access.
     */
    private List<KeyListener> keyListener = new ArrayList<>();
    /**
     * List of TraverseListener that should be added to the dropdown table once
     * it is created. Kept locally because the table creation is deferred to the
     * first access.
     */
    private List<TraverseListener> traverseListener = new ArrayList<>();
    /**
     * List of MouseListener that should be added to the dropdown table once it
     * is created. Kept locally because the table creation is deferred to the
     * first access.
     */
    private List<MouseListener> mouseListener = new ArrayList<>();
    /**
     * List of SelectionListener that should be added to the dropdown table once
     * it is created. Kept locally because the table creation is deferred to the
     * first access.
     */
    private List<SelectionListener> selectionListener = new ArrayList<>();
    /**
     * List of ShellListener that should be added to the dropdown table once it
     * is created. Kept locally because the table creation is deferred to the
     * first access.
     */
    private List<ShellListener> shellListener = new ArrayList<>();

    /**
     * Creates a new NatCombo using the given IStyle for rendering, showing the
     * default number of items at once in the dropdown. Creating the NatCombo
     * with this constructor, there is no free edit and no multiple selection
     * enabled.
     *
     * @param parent
     *            A widget that will be the parent of this NatCombo
     * @param cellStyle
     *            Style configuration containing horizontal alignment, font,
     *            foreground and background color information.
     * @param style
     *            The style for the Text Control to construct. Uses this style
     *            adding internal styles via ConfigRegistry.
     */
    public NatCombo(Composite parent, IStyle cellStyle, int style) {
        this(parent, cellStyle, DEFAULT_NUM_OF_VISIBLE_ITEMS, style, GUIHelper.getImage("down_2")); //$NON-NLS-1$
    }

    /**
     * Creates a new NatCombo using the given IStyle for rendering, showing the
     * given amount of items at once in the dropdown. Creating the NatCombo with
     * this constructor, there is no free edit and no multiple selection
     * enabled.
     *
     * @param parent
     *            A widget that will be the parent of this NatCombo
     * @param cellStyle
     *            Style configuration containing horizontal alignment, font,
     *            foreground and background color information.
     * @param maxVisibleItems
     *            the max number of items the drop down will show before
     *            introducing a scroll bar.
     * @param style
     *            The style for the Text Control to construct. Uses this style
     *            adding internal styles via ConfigRegistry.
     */
    public NatCombo(Composite parent, IStyle cellStyle, int maxVisibleItems, int style) {
        this(parent, cellStyle, maxVisibleItems, style, GUIHelper.getImage("down_2")); //$NON-NLS-1$
    }

    /**
     * Creates a new NatCombo using the given IStyle for rendering, showing the
     * given amount of items at once in the dropdown. Creating the NatCombo with
     * this constructor, there is no free edit and no multiple selection
     * enabled.
     *
     * @param parent
     *            A widget that will be the parent of this NatCombo
     * @param cellStyle
     *            Style configuration containing horizontal alignment, font,
     *            foreground and background color information.
     * @param maxVisibleItems
     *            the max number of items the drop down will show before
     *            introducing a scroll bar.
     * @param style
     *            The style for the Text Control to construct. Uses this style
     *            adding internal styles via ConfigRegistry.
     *
     * @param showDropdownFilter
     *            Flag indicating whether the dropdown filter is displayed
     *
     * @since 1.4
     */
    public NatCombo(Composite parent, IStyle cellStyle, int maxVisibleItems, int style, boolean showDropdownFilter) {
        this(parent, cellStyle, maxVisibleItems, style, GUIHelper.getImage("down_2"), showDropdownFilter); //$NON-NLS-1$
    }

    /**
     * Creates a new NatCombo using the given IStyle for rendering, showing the
     * given amount of items at once in the dropdown.
     *
     * @param parent
     *            A widget that will be the parent of this NatCombo
     * @param cellStyle
     *            Style configuration containing horizontal alignment, font,
     *            foreground and background color information.
     * @param maxVisibleItems
     *            the max number of items the drop down will show before
     *            introducing a scroll bar.
     * @param style
     *            The style for the {@link Text} Control to construct. Uses this
     *            style adding internal styles via ConfigRegistry.
     * @param iconImage
     *            The image to use as overlay to the {@link Text} Control if the
     *            dropdown is visible. Using this image will indicate that the
     *            control is an open combo to the user.
     */
    public NatCombo(Composite parent, IStyle cellStyle, int maxVisibleItems, int style, Image iconImage) {
        this(parent, cellStyle, maxVisibleItems, style, GUIHelper.getImage("down_2"), false); //$NON-NLS-1$
    }

    /**
     * Creates a new NatCombo using the given IStyle for rendering, showing the
     * given amount of items at once in the dropdown.
     *
     * @param parent
     *            A widget that will be the parent of this NatCombo
     * @param cellStyle
     *            Style configuration containing horizontal alignment, font,
     *            foreground and background color information.
     * @param maxVisibleItems
     *            the max number of items the drop down will show before
     *            introducing a scroll bar.
     * @param style
     *            The style for the {@link Text} Control to construct. Uses this
     *            style adding internal styles via ConfigRegistry.
     * @param iconImage
     *            The image to use as overlay to the {@link Text} Control if the
     *            dropdown is visible. Using this image will indicate that the
     *            control is an open combo to the user.
     *
     * @param showDropdownFilter
     *            Flag indicating whether the dropdown filter is displayed
     *
     * @since 1.4
     */
    public NatCombo(Composite parent, IStyle cellStyle, int maxVisibleItems, int style, Image iconImage, boolean showDropdownFilter) {
        super(parent, SWT.NONE);

        this.cellStyle = cellStyle;
        this.maxVisibleItems = maxVisibleItems;
        this.iconImage = iconImage;

        this.widgetStyle = style;

        this.showDropdownFilter = showDropdownFilter;
        this.freeEdit = (style & SWT.READ_ONLY) == 0;
        this.multiselect = (style & SWT.MULTI) != 0;
        this.useCheckbox = (style & SWT.CHECK) != 0;

        GridLayout gridLayout = new GridLayout(2, false);
        gridLayout.marginWidth = 0;
        gridLayout.marginHeight = 0;
        gridLayout.horizontalSpacing = 0;
        setLayout(gridLayout);

        createTextControl(style);

        // typically the dropdown shell should be hidden when the focus is lost
        // but in case the NatCombo is the first control in a shell, the text
        // control will get the focus immediately after the shell lost focus.
        // as handling with focus listeners in such a case fails, we add a move
        // listener that will update the position of the dropdown shell if the
        // parent shell moves
        final Listener moveListener = event -> calculateBounds();
        getShell().addListener(SWT.Move, moveListener);

        addDisposeListener(e -> {
            if (NatCombo.this.dropdownShell != null) {
                NatCombo.this.dropdownShell.dispose();
            }
            NatCombo.this.text.dispose();
            NatCombo.this.getShell().removeListener(SWT.Move, moveListener);
        });
    }

    /**
     * Sets the given items to be the items shown in the dropdown of this
     * NatCombo.
     *
     * @param items
     *            The array of items to set.
     */
    public void setItems(String[] items) {
        if (items != null) {
            this.itemList = Arrays.asList(items);
            this.selectionStateMap = new LinkedHashMap<>();
            for (String item : items) {
                this.selectionStateMap.put(item, Boolean.FALSE);
            }
            if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
                this.dropdownTableViewer.setInput(items);
            }
        }
    }

    /**
     * Creates the Text control of this NatCombo, adding styles, look&amp;feel
     * and needed listeners for the control only.
     *
     * @param style
     *            The style for the Text Control to construct. Uses this style
     *            adding internal styles via ConfigRegistry.
     */
    protected void createTextControl(int style) {
        int textStyle = style | HorizontalAlignmentEnum.getSWTStyle(this.cellStyle);
        this.text = new Text(this, textStyle);
        this.text.setBackground(this.cellStyle.getAttributeValue(CellStyleAttributes.BACKGROUND_COLOR));
        this.text.setForeground(this.cellStyle.getAttributeValue(CellStyleAttributes.FOREGROUND_COLOR));
        this.text.setFont(this.cellStyle.getAttributeValue(CellStyleAttributes.FONT));

        GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true);
        this.text.setLayoutData(gridData);

        this.text.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent event) {
                if (event.keyCode == SWT.ARROW_DOWN
                        || event.keyCode == SWT.ARROW_UP) {
                    showDropdownControl();

                    int selectionIndex = getDropdownTable().getSelectionIndex();
                    if (selectionIndex < 0) {
                        select(0);
                    } else {
                        // only visualize the selection in the dropdown, do not
                        // perform a selection
                        getDropdownTable().select(selectionIndex);
                    }

                    // ensure the arrow key events do not have any further
                    // effect
                    event.doit = false;
                } else if (!LetterOrDigitKeyEventMatcher.isLetterOrDigit(event.character)) {
                    if (NatCombo.this.freeEdit) {
                        // simply clear the selection in dropdownlist so the
                        // free value in text control will be used
                        if (!getDropdownTable().isDisposed()) {
                            getDropdownTable().deselectAll();
                            for (Map.Entry<String, Boolean> entry : NatCombo.this.selectionStateMap.entrySet()) {
                                entry.setValue(Boolean.FALSE);
                            }
                        }
                    } else {
                        showDropdownControl();
                    }
                }
            }
        });

        this.text.addMouseListener(new MouseAdapter() {

            @Override
            public void mouseDown(MouseEvent e) {
                if (!NatCombo.this.freeEdit) {
                    if (getDropdownTable().isDisposed()
                            || !getDropdownTable().isVisible()) {
                        showDropdownControl();
                    } else {
                        // if there is no free edit enabled, set the focus back
                        // to the dropdownlist so it handles key strokes itself
                        getDropdownTable().forceFocus();
                    }
                }
            }
        });

        this.text.addControlListener(new ControlListener() {
            @Override
            public void controlResized(ControlEvent e) {
                calculateBounds();
            }

            @Override
            public void controlMoved(ControlEvent e) {
                calculateBounds();
            }
        });

        this.text.addFocusListener(new FocusListenerWrapper());

        final Canvas iconCanvas = new Canvas(this, SWT.NONE) {

            @Override
            public Point computeSize(int wHint, int hHint, boolean changed) {
                Rectangle iconImageBounds = NatCombo.this.iconImage.getBounds();
                return new Point(iconImageBounds.width + 2, iconImageBounds.height + 2);
            }

        };

        gridData = new GridData(GridData.BEGINNING, SWT.FILL, false, true);
        iconCanvas.setLayoutData(gridData);

        iconCanvas.addPaintListener(event -> {
            GC gc = event.gc;

            Rectangle iconCanvasBounds = iconCanvas.getBounds();
            Rectangle iconImageBounds = NatCombo.this.iconImage.getBounds();
            int horizontalAlignmentPadding =
                    CellStyleUtil.getHorizontalAlignmentPadding(
                            HorizontalAlignmentEnum.CENTER, iconCanvasBounds, iconImageBounds.width);
            int verticalAlignmentPadding =
                    CellStyleUtil.getVerticalAlignmentPadding(
                            VerticalAlignmentEnum.MIDDLE, iconCanvasBounds, iconImageBounds.height);
            gc.drawImage(NatCombo.this.iconImage, horizontalAlignmentPadding, verticalAlignmentPadding);

            Color originalFg = gc.getForeground();
            gc.setForeground(GUIHelper.COLOR_WIDGET_BORDER);
            gc.drawRectangle(0, 0, iconCanvasBounds.width - 1, iconCanvasBounds.height - 1);
            gc.setForeground(originalFg);
        });

        iconCanvas.addMouseListener(new MouseAdapter() {

            @Override
            public void mouseDown(MouseEvent e) {
                if (NatCombo.this.dropdownShell != null && !NatCombo.this.dropdownShell.isDisposed()) {
                    if (NatCombo.this.dropdownShell.isVisible()) {
                        NatCombo.this.text.forceFocus();
                        hideDropdownControl();
                    } else {
                        showDropdownControl();
                    }
                } else {
                    showDropdownControl();
                }
            }
        });
    }

    /**
     * Create the dropdown control of this NatCombo, adding styles,
     * look&amp;feel and needed listeners for the control only.
     *
     * @param style
     *            The style for the Table Control to construct. Uses this style
     *            adding internal styles via ConfigRegistry.
     */
    protected void createDropdownControl(int style) {
        this.dropdownShell = new Shell(getShell(), SWT.MODELESS);

        // (SWT.V_SCROLL | SWT.NO_SCROLL) prevents appearance of unnecessary
        // horizontal scrollbar on mac
        // see: https://bugs.eclipse.org/bugs/show_bug.cgi?id=304128
        int scrollStyle = ((this.itemList != null && this.itemList.size() > this.maxVisibleItems)
                && this.maxVisibleItems > 0) ? (SWT.V_SCROLL | SWT.NO_SCROLL) : SWT.NO_SCROLL;
        int dropdownListStyle = style
                | scrollStyle
                | HorizontalAlignmentEnum.getSWTStyle(this.cellStyle)
                | SWT.FULL_SELECTION;

        this.dropdownTable = new Table(this.dropdownShell, dropdownListStyle);
        this.dropdownTableViewer = new TableViewer(this.dropdownTable);
        this.dropdownTable.setBackground(
                this.cellStyle.getAttributeValue(CellStyleAttributes.BACKGROUND_COLOR));
        this.dropdownTable.setForeground(
                this.cellStyle.getAttributeValue(CellStyleAttributes.FOREGROUND_COLOR));
        this.dropdownTable.setFont(
                this.cellStyle.getAttributeValue(CellStyleAttributes.FONT));

        // add a column to be able to resize the item width in the dropdown
        new TableColumn(this.dropdownTable, SWT.NONE);

        this.dropdownTableViewer.setContentProvider(ArrayContentProvider.getInstance());

        this.dropdownTableViewer.setLabelProvider(new LabelProvider() {

            @Override
            public boolean isLabelProperty(Object element, String property) {
                return false;
            }
        });

        this.dropdownTable.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent event) {
                if (event.keyCode == SWT.ARROW_DOWN) {
                    int selected = NatCombo.this.dropdownTable.getSelectionIndex();
                    if (selected < 0) {
                        // no selection before, select the first entry
                        if (!NatCombo.this.useCheckbox) {
                            select(0);
                        } else {
                            getDropdownTable().select(0);
                        }
                        event.doit = false;
                    }
                }
            }
        });

        this.dropdownTable.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                boolean selected = e.detail != SWT.CHECK;
                boolean isCtrlPressed = (e.stateMask & SWT.MODIFIER_MASK) == SWT.CTRL;
                TableItem chosenItem = (TableItem) e.item;

                // Given the ability to filter we need to find the item's
                // table index which may not match the index in the itemList
                int itemTableIndex = NatCombo.this.dropdownTable.indexOf(chosenItem);

                // This case handles check actions
                if (!selected) {
                    if (!chosenItem.getChecked()) {
                        NatCombo.this.selectionStateMap.put(chosenItem.getText(), Boolean.FALSE);
                    } else {
                        NatCombo.this.selectionStateMap.put(chosenItem.getText(), Boolean.TRUE);
                    }
                } else if (!NatCombo.this.useCheckbox) {
                    if (NatCombo.this.multiselect && isCtrlPressed) {
                        boolean isSelected = NatCombo.this.dropdownTable.isSelected(itemTableIndex);
                        NatCombo.this.selectionStateMap.put(chosenItem.getText(), isSelected);
                        if (NatCombo.this.useCheckbox) {
                            chosenItem.setChecked(isSelected);
                        }
                    } else {
                        // A single item was selected. Clear all previous state
                        for (String item : NatCombo.this.itemList) {
                            NatCombo.this.selectionStateMap.put(item, Boolean.FALSE);
                        }

                        // Set the state for the selected item
                        NatCombo.this.selectionStateMap.put(chosenItem.getText(), Boolean.TRUE);
                    }
                }

                updateTextControl(false);
            }
        });

        this.dropdownTable.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent event) {
                if ((event.keyCode == SWT.CR)
                        || (event.keyCode == SWT.KEYPAD_CR)) {
                    updateTextControl(true);
                } else if (event.keyCode == SWT.F2 && NatCombo.this.freeEdit) {
                    NatCombo.this.text.forceFocus();
                    hideDropdownControl();
                }
            }
        });

        this.dropdownTable.addFocusListener(new FocusListenerWrapper());

        FormLayout layout = new FormLayout();
        layout.spacing = 0;
        layout.marginHeight = 0;
        layout.marginWidth = 0;
        this.dropdownShell.setLayout(layout);

        FormData dropDownLayoutData = new FormData();
        dropDownLayoutData.left = new FormAttachment(0);
        dropDownLayoutData.right = new FormAttachment(100);
        dropDownLayoutData.bottom = new FormAttachment(100);

        if (this.showDropdownFilter) {
            this.filterBox = new Text(this.dropdownShell, SWT.BORDER);
            this.filterBox.setFont(this.cellStyle.getAttributeValue(CellStyleAttributes.FONT));
            this.filterBox.setEnabled(true);
            this.filterBox.setEditable(true);
            this.filterBox.addFocusListener(new FocusListenerWrapper());
            this.filterBox.addKeyListener(new KeyAdapter() {

                @Override
                public void keyReleased(KeyEvent e) {
                    if (null != NatCombo.this.dropdownTableViewer && !NatCombo.this.dropdownTable.isDisposed()) {
                        NatCombo.this.dropdownTableViewer.refresh();
                        calculateBounds();
                        setDropdownSelection(getTextAsArray());
                    }
                }
            });

            FormData data = new FormData();
            data.top = new FormAttachment(0);
            data.left = new FormAttachment(0);
            data.right = new FormAttachment(100);
            this.filterBox.setLayoutData(data);

            data = new FormData();
            dropDownLayoutData.top = new FormAttachment(this.filterBox, 0, SWT.BOTTOM);
            this.dropdownTable.setLayoutData(dropDownLayoutData);

            ViewerFilter viewerFilter = new ViewerFilter() {

                @Override
                public boolean select(Viewer viewer, Object parentElement, Object element) {
                    if (element instanceof String) {
                        return ((String) element).toLowerCase().contains(NatCombo.this.filterBox.getText().toLowerCase());
                    }
                    return false;
                }
            };
            this.dropdownTableViewer.addFilter(viewerFilter);
        } else {
            dropDownLayoutData.top = new FormAttachment(0);
            this.dropdownTable.setLayoutData(dropDownLayoutData);
        }

        if (this.itemList != null) {
            setItems(this.itemList.toArray(new String[] {}));
        }

        // apply the listeners that were registered before the creation of the
        // dropdown control
        applyDropdownListener();

        setDropdownSelection(getTextAsArray());
    }

    /**
     * This method will be called if an item of the dropdown control is selected
     * via mouse click or pressing enter. It will populate the text control with
     * the information gathered out of the selection in the dropdown control and
     * hide the dropdown if necessary.
     *
     * @param hideDropdown
     *            <code>true</code> if the dropdown should be hidden after
     *            updating the text control
     */
    protected void updateTextControl(boolean hideDropdown) {
        this.text.setText(getTransformedTextForSelection());
        if (hideDropdown) {
            hideDropdownControl();
        }
    }

    /**
     * Shows the dropdown of this NatCombo. Will always calculate the size of
     * the dropdown regarding the current size of the Text control.
     */
    public void showDropdownControl() {
        showDropdownControl(false);
    }

    /**
     * Shows the dropdown of this NatCombo. Will always calculate the size of
     * the dropdown regarding the current size of the Text control.
     *
     * @param focusOnText
     *            <code>true</code> if the focus should be set to the text
     *            control instead of the dropdown after opening the dropdown.
     */
    public void showDropdownControl(boolean focusOnText) {
        if (this.dropdownShell == null || this.dropdownShell.isDisposed()) {
            createDropdownControl(this.widgetStyle);
        }
        calculateBounds();
        this.dropdownShell.open();
        if (focusOnText) {
            this.text.forceFocus();
            this.text.setSelection(this.text.getText().length());
        }
    }

    /**
     * @return The {@link Table} control that is used in the dropdown. Will be
     *         created if it does not exist yet.
     * @since 1.5
     */
    protected Table getDropdownTable() {
        if (this.dropdownTable == null) {
            createDropdownControl(this.widgetStyle);
        }
        return this.dropdownTable;
    }

    /**
     * Hide the dropdown of this NatCombo.
     */
    public void hideDropdownControl() {
        if (this.dropdownShell != null && !this.dropdownShell.isDisposed()) {
            this.dropdownShell.setVisible(false);
        }
    }

    /**
     * Calculates the number of items that should be showed in the dropdown at
     * once. It is needed to calculate the height of the dropdown. If
     * maxVisibleItems is configured -1, this method always returns the number
     * of items in the list. Otherwise if will return the configured maximum
     * number of items to be visible at once or less if there are less than the
     * configured maximum.
     *
     * @return the number of items that should be showed in the dropdown at
     *         once.
     */
    protected int getVisibleItemCount() {
        int itemCount = getDropdownTable().getItemCount();
        if (itemCount > 0) {
            // if maxVisibleItems == -1 show all items at once
            // otherwise use the minimum for item count or max visible item
            // configuration
            int visibleItemCount = itemCount;
            if (this.maxVisibleItems > 0) {
                visibleItemCount = Math.min(itemCount, this.maxVisibleItems);
            }
            itemCount = visibleItemCount;
        }
        return itemCount;
    }

    /**
     * Calculates the size and location of the Shell that represents the
     * dropdown control of this NatCombo. Size and location will be calculated
     * dependent the position and size of the corresponding Text control and the
     * information showed in the dropdown.
     */
    protected void calculateBounds() {
        if (this.dropdownShell != null && !this.dropdownShell.isDisposed()) {
            Point size = getSize();
            // calculate the height by multiplying the number of visible items
            // with the item height of items in the list and adding 2*grid line
            // width to work around a calculation error regarding the descent of
            // the font metrics for the last shown item
            // Note: if there are no items to show in the combo, calculate with
            // the item count of 3 so an empty combo will open
            int listHeight = (getVisibleItemCount() > 0 ? getVisibleItemCount() : 3)
                    * this.dropdownTable.getItemHeight()
                    + this.dropdownTable.getGridLineWidth() * 2;

            // since introduced the TableColumn for real full row selection, we
            // call pack() to perform autoresize to ensure the width shows the
            // whole content
            this.dropdownTable.getColumn(0).pack();
            int listWidth = Math.max(
                    this.dropdownTable.computeSize(SWT.DEFAULT, listHeight, true).x, size.x);

            Point textPosition = this.text.toDisplay(this.text.getLocation());

            // by default the dropdown shell will be created below the cell in
            // the table
            int dropdownShellStartingY = textPosition.y + this.text.getBounds().height;

            int textBottomY = textPosition.y + this.text.getBounds().height + listHeight;
            // if the bottom of the drowdown is below the display, render it
            // above the cell
            if (textBottomY > Display.getCurrent().getBounds().height) {
                dropdownShellStartingY = textPosition.y - listHeight;
            }

            Rectangle parentBounds = getParent().getBounds();
            Point parentStart = getParent().toDisplay(parentBounds.x, parentBounds.y);
            int parentBottomY = parentStart.y + parentBounds.height - parentBounds.y;
            if (getParent().getHorizontalBar() != null && getParent().getHorizontalBar().isVisible()) {
                parentBottomY -= getParent().getHorizontalBar().getSize().y;
            }
            if (dropdownShellStartingY > parentBottomY) {
                dropdownShellStartingY = parentBottomY;
            }

            int filterTextBoxHeight = this.showDropdownFilter ? this.filterBox.computeSize(SWT.DEFAULT, SWT.DEFAULT).y : 0;
            Rectangle shellBounds = new Rectangle(
                    textPosition.x,
                    dropdownShellStartingY,
                    listWidth + (this.dropdownTable.getGridLineWidth() * 2),
                    listHeight + filterTextBoxHeight);

            this.dropdownShell.setBounds(shellBounds);

            calculateColumnWidth();
        }
    }

    /**
     * Calculates and applies the column width to ensure that the column has the
     * same width as the table itself, so selection is possible for the whole
     * row.
     */
    protected void calculateColumnWidth() {
        int width = this.dropdownTable.getBounds().width;
        // only reduce if a scrollbar is available and visible
        if (this.dropdownTable.getVerticalBar() != null
                && this.dropdownTable.getItemCount() > this.maxVisibleItems
                && this.maxVisibleItems > 0) {
            width -= this.dropdownTable.getVerticalBar().getSize().x;
        }
        this.dropdownTable.getColumn(0).setWidth(width);
    }

    /**
     * Returns the zero-relative index of the item which is currently selected
     * in the receiver, or -1 if no item is selected.
     * <p>
     * Note that this only returns useful results if this NatCombo supports
     * single selection or only one item is selected.
     *
     * @return the index of the selected item or -1
     */
    public int getSelectionIndex() {
        if (this.selectionStateMap != null) {
            for (String item : this.selectionStateMap.keySet()) {
                if (this.selectionStateMap.get(item)) {
                    return this.itemList.indexOf(item);
                }
            }
        } else if (!this.text.isDisposed()) {
            return this.itemList.indexOf(this.text.getText());
        }
        return -1;
    }

    /**
     * Returns the zero-relative indices of the items which are currently
     * selected in the receiver. The order of the indices is unspecified. The
     * array is empty if no items are selected.
     * <p>
     * Note: This is not the actual structure used by the receiver to maintain
     * its selection, so modifying the array will not affect the receiver.
     * </p>
     *
     * @return the array of indices of the selected items
     */
    public int[] getSelectionIndices() {
        if (this.selectionStateMap != null) {
            List<Integer> selectedIndices = new ArrayList<>();
            for (String item : this.selectionStateMap.keySet()) {
                if (this.selectionStateMap.get(item)) {
                    selectedIndices.add(this.itemList.indexOf(item));
                }
            }
            int[] indices = new int[selectedIndices.size()];
            for (int i = 0; i < selectedIndices.size(); i++) {
                indices[i] = selectedIndices.get(i);
            }
            return indices;
        } else {
            String[] selectedItems = getTextAsArray();
            int[] result = new int[selectedItems.length];
            for (int i = 0; i < selectedItems.length; i++) {
                result[i] = this.itemList.indexOf(selectedItems[i]);
            }
            return result;
        }
    }

    /**
     * Returns the number of selected items contained in the receiver.
     *
     * @return the number of selected items
     */
    public int getSelectionCount() {
        if (this.selectionStateMap != null) {
            List<Integer> selectedIndices = new ArrayList<>();
            for (String item : this.selectionStateMap.keySet()) {
                if (this.selectionStateMap.get(item)) {
                    selectedIndices.add(this.itemList.indexOf(item));
                }
            }
            return selectedIndices.size();
        } else {
            return getTextAsArray().length;
        }
    }

    /**
     * Returns an array of <code>String</code>s that are currently selected in
     * the receiver. The order of the items is unspecified. An empty array
     * indicates that no items are selected.
     * <p>
     * Note: This is not the actual structure used by the receiver to maintain
     * its selection, so modifying the array will not affect the receiver.
     * </p>
     *
     * @return an array representing the selection
     */
    public String[] getSelection() {
        String[] result = getTransformedSelection();
        if (result == null
                || (result.length == 0 && !this.text.isDisposed() && this.text.getText().length() > 0)) {
            result = getTextAsArray();
        }
        return result;
    }

    /**
     * Selects the items at the given zero-relative indices in the receiver. The
     * current selection is cleared before the new items are selected.
     * <p>
     * Indices that are out of range and duplicate indices are ignored. If the
     * receiver is single-select and multiple indices are specified, then all
     * indices are ignored.
     * <p>
     * The text control of this NatCombo will also be updated with the new
     * selected values.
     *
     * @param items
     *            the items to select
     */
    public void setSelection(String[] items) {
        String textValue = ""; //$NON-NLS-1$
        if (items != null) {
            if (!getDropdownTable().isDisposed()) {
                setDropdownSelection(items);
                if (this.freeEdit
                        && getDropdownTable().getSelectionCount() == 0) {
                    textValue = getTransformedText(items);
                } else {
                    textValue = getTransformedTextForSelection();
                }
            } else {
                textValue = getTransformedText(items);
            }
        }
        this.text.setText(textValue);

        if (this.multiselect) {
            this.text.setSelection(textValue.length() - this.multiselectTextSuffix.length());
        }
    }

    /**
     * Selects the item at the given zero-relative index in the receiver's list.
     * If the item at the index was already selected, it remains selected.
     * Indices that are out of range are ignored.
     *
     * @param index
     *            the index of the item to select
     */
    public void select(int index) {
        if (!getDropdownTable().isDisposed()) {
            getDropdownTable().select(index);
            for (int i = 0; i < this.itemList.size(); i++) {
                this.selectionStateMap.put(this.itemList.get(i), i == index);
            }
            this.text.setText(getTransformedTextForSelection());
        } else if (index >= 0) {
            this.text.setText(this.itemList.get(index));
        }
    }

    /**
     * Selects the items at the given zero-relative indices in the receiver. The
     * current selection is not cleared before the new items are selected.
     * <p>
     * If the item at a given index is not selected, it is selected. If the item
     * at a given index was already selected, it remains selected. Indices that
     * are out of range and duplicate indices are ignored. If the receiver is
     * single-select and multiple indices are specified, then all indices are
     * ignored.
     *
     * @param indices
     *            the array of indices for the items to select
     */
    public void select(int[] indices) {
        if (!getDropdownTable().isDisposed()) {
            getDropdownTable().select(indices);
            List<Integer> indicesList = ArrayUtil.asIntegerList(indices);
            for (int i = 0; i < this.itemList.size(); i++) {
                this.selectionStateMap.put(this.itemList.get(i), indicesList.contains(i));
            }
            this.text.setText(getTransformedTextForSelection());
        } else {
            String[] selectedItems = new String[indices.length];
            for (int i = 0; i < indices.length; i++) {
                if (indices[i] >= 0) {
                    selectedItems[i] = this.itemList.get(indices[i]);
                }
            }
            this.text.setText(getTransformedText(selectedItems));
        }
    }

    /**
     * @since 1.5
     */
    protected void applyDropdownListener() {
        for (KeyListener l : this.keyListener) {
            if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
                this.dropdownTable.addKeyListener(l);
            }
        }
        for (TraverseListener l : this.traverseListener) {
            if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
                this.dropdownTable.addTraverseListener(l);
            }
        }
        for (MouseListener l : this.mouseListener) {
            if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
                this.dropdownTable.addMouseListener(l);
            }
        }
        for (SelectionListener l : this.selectionListener) {
            if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
                this.dropdownTable.addSelectionListener(l);
            }
        }
        for (ShellListener l : this.shellListener) {
            if (this.dropdownShell != null && !this.dropdownShell.isDisposed()) {
                this.dropdownShell.addShellListener(l);
            }
        }
    }

    @Override
    public void addKeyListener(KeyListener listener) {
        if (listener != null) {
            if (this.text != null && !this.text.isDisposed()) {
                this.text.addKeyListener(listener);
            }
            if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
                this.dropdownTable.addKeyListener(listener);
            }
            this.keyListener.add(listener);
        }
    }

    @Override
    public void removeKeyListener(KeyListener listener) {
        if (this.text != null && !this.text.isDisposed()) {
            this.text.removeKeyListener(listener);
        }
        if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
            this.dropdownTable.removeKeyListener(listener);
        }
        this.keyListener.remove(listener);
    }

    @Override
    public void addTraverseListener(TraverseListener listener) {
        if (listener != null) {
            if (this.text != null && !this.text.isDisposed()) {
                this.text.addTraverseListener(listener);
            }
            if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
                this.dropdownTable.addTraverseListener(listener);
            }
            this.traverseListener.add(listener);
        }
    }

    @Override
    public void removeTraverseListener(TraverseListener listener) {
        if (this.text != null && !this.text.isDisposed()) {
            this.text.removeTraverseListener(listener);
        }
        if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
            this.dropdownTable.removeTraverseListener(listener);
        }
        this.traverseListener.remove(listener);
    }

    @Override
    public void addMouseListener(MouseListener listener) {
        // only add the mouse listener to the dropdown, as clicking in the text
        // control should not trigger anything else than it is handled by the
        // text control itself.
        if (listener != null) {
            if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
                this.dropdownTable.addMouseListener(listener);
            }
            this.mouseListener.add(listener);
        }
    }

    @Override
    public void removeMouseListener(MouseListener listener) {
        if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
            this.dropdownTable.removeMouseListener(listener);
        }
        this.mouseListener.remove(listener);
    }

    @Override
    public void notifyListeners(int eventType, Event event) {
        if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
            this.dropdownTable.notifyListeners(eventType, event);
        }
    }

    public void addSelectionListener(SelectionListener listener) {
        if (listener != null) {
            if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
                this.dropdownTable.addSelectionListener(listener);
            }
            this.selectionListener.add(listener);
        }
    }

    public void removeSelectionListener(SelectionListener listener) {
        if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
            this.dropdownTable.removeSelectionListener(listener);
        }
        this.selectionListener.remove(listener);
    }

    public void addShellListener(ShellListener listener) {
        if (listener != null) {
            if (this.dropdownShell != null && !this.dropdownShell.isDisposed()) {
                this.dropdownShell.addShellListener(listener);
            }
            this.shellListener.add(listener);
        }
    }

    public void removeShellListener(ShellListener listener) {
        if (this.dropdownShell != null && !this.dropdownShell.isDisposed()) {
            this.dropdownShell.removeShellListener(listener);
        }
        this.shellListener.remove(listener);
    }

    public void addTextControlListener(ControlListener listener) {
        if (listener != null && this.text != null && !this.text.isDisposed()) {
            this.text.addControlListener(listener);
        }
    }

    public void removeTextControlListener(ControlListener listener) {
        if (this.text != null && !this.text.isDisposed()) {
            this.text.removeControlListener(listener);
        }
    }

    @Override
    public boolean isFocusControl() {
        return this.hasFocus;
    }

    @Override
    public boolean forceFocus() {
        return this.text.forceFocus();
    }

    @Override
    public void addFocusListener(FocusListener listener) {
        if (listener != null) {
            this.focusListener.add(listener);
        }
    }

    @Override
    public void removeFocusListener(final FocusListener listener) {
        // The FocusListenerWrapper is executing the focusLost event
        // in a separate thread with 100ms delay to ensure that the NatCombo
        // lost focus. This is necessary because the NatCombo is a combination
        // of a text field and a table as dropdown which do not share the
        // same focus by default.
        this.focusListener.remove(listener);
    }

    /**
     * Transforms the selection in the Table control dropdown into a String[].
     * Doing this is necessary to provide a SWT List like interface regarding
     * selections for the NatCombo.
     *
     * @return Array containing all selected TableItem text attributes
     */
    protected String[] getTransformedSelection() {
        List<String> selectedItems = new ArrayList<>();
        for (String item : this.selectionStateMap.keySet()) {
            Boolean isSelected = this.selectionStateMap.get(item);
            if (isSelected != null && isSelected) {
                selectedItems.add(item);
            }
        }
        return selectedItems.toArray(new String[selectedItems.size()]);
    }

    /**
     * Transforms the given String array whose contents represents selected
     * items to a selection that can be handled by the underlying Table control
     * in the dropdown.
     *
     * @param selection
     *            The Strings that represent the selected items
     */
    protected void setDropdownSelection(String[] selection) {
        java.util.List<String> selectionList = Arrays.asList(selection);
        java.util.List<TableItem> selectedItems = new ArrayList<>();
        for (TableItem item : getDropdownTable().getItems()) {
            if (selectionList.contains(EditConstants.SELECT_ALL_ITEMS_VALUE)
                    || selectionList.contains(item.getText())) {
                this.selectionStateMap.put(item.getText(), Boolean.TRUE);
                if (this.useCheckbox) {
                    item.setChecked(true);
                } else {
                    selectedItems.add(item);
                }
            } else {
                this.selectionStateMap.put(item.getText(), Boolean.FALSE);
            }
        }
        getDropdownTable().setSelection(selectedItems.toArray(new TableItem[] {}));
    }

    /**
     * Will transform the text for the Text control of this NatCombo to an array
     * of Strings. This is necessary for the multiselect feature.
     *
     * <p>
     * Note that by default the multiselect String is specified to show with
     * enclosing [] brackets and values separated by ", ". If you need to change
     * this you need to set the corresponding values in this NatCombo.
     *
     * @return The text for the Text control of this NatCombo converted to an
     *         array of Strings.
     */
    protected String[] getTextAsArray() {
        if (!this.text.isDisposed()) {
            String transform = this.text.getText();
            if (transform.length() > 0) {
                if (this.multiselect) {
                    // for multiselect the String is defined by default in
                    // format [a, b, c]
                    // the prefix and suffix for multiselect String
                    // representation need to be removed
                    // in free edit mode we need to check if the format is used
                    int prefixLength = this.multiselectTextPrefix.length();
                    int suffixLength = this.multiselectTextSuffix.length();
                    if (this.freeEdit) {
                        if (!transform.startsWith(this.multiselectTextPrefix)) {
                            prefixLength = 0;
                        }
                        if (!transform.endsWith(this.multiselectTextSuffix)) {
                            suffixLength = 0;
                        }
                    }
                    transform = transform.substring(prefixLength, transform.length() - suffixLength);

                    // if the transform value length is still > 0, try to split
                    if (transform.length() > 0) {
                        return transform.split(this.multiselectValueSeparator);
                    }
                }
                return new String[] { transform };
            }
        }
        return new String[] {};
    }

    /**
     * Transforms the selection of the dropdown to a text representation that
     * can be added to the text control of this combo.
     *
     * <p>
     * Note that by default the multiselect String is specified to show with
     * enclosing [] brackets and values separated by ", ". If you need to change
     * this you need to set the corresponding values in this NatCombo.
     *
     * @return String representation for the selection within the combo.
     */
    protected String getTransformedTextForSelection() {
        String result = ""; //$NON-NLS-1$
        String[] selection = getTransformedSelection();
        if (selection != null) {
            result = getTransformedText(selection);
        }
        return result;
    }

    /**
     * Transforms the given array of Strings to a text representation that can
     * be added to the text control of this combo.
     * <p>
     * If this NatCombo is only configured to support single selection, than
     * only the first value in the array will be processed. Otherwise the result
     * will be processed by concatenating the values.
     * <p>
     * Note that by default the multiselect String is specified to show with
     * enclosing [] brackets and values separated by ", ". If you need to change
     * this you need to set the corresponding values in this NatCombo.
     *
     * @param values
     *            The values to build the text representation from.
     * @return String representation for the selection within the combo.
     */
    protected String getTransformedText(String[] values) {
        String result = ""; //$NON-NLS-1$
        if (this.multiselect) {
            for (int i = 0; i < values.length; i++) {
                String selection = values[i];
                result += selection;
                if ((i + 1) < values.length) {
                    result += this.multiselectValueSeparator;
                }
            }
            result = this.multiselectTextPrefix + result + this.multiselectTextSuffix;
        } else if (values.length > 0) {
            result = values[0];
        }
        return result;
    }

    /**
     * @param multiselectValueSeparator
     *            String that should be used to separate values in the String
     *            representation showed in the text control if multiselect is
     *            supported. <code>null</code> to use the default value
     *            separator.
     * @see NatCombo#DEFAULT_MULTI_SELECT_VALUE_SEPARATOR
     */
    public void setMultiselectValueSeparator(String multiselectValueSeparator) {
        if (multiselectValueSeparator == null) {
            this.multiselectValueSeparator = DEFAULT_MULTI_SELECT_VALUE_SEPARATOR;
        } else {
            this.multiselectValueSeparator = multiselectValueSeparator;
        }
    }

    /**
     * Set the prefix and suffix that will parenthesize the text that is created
     * out of the selected values if this NatCombo supports multiselection.
     *
     * @param multiselectTextPrefix
     *            String that should be used to prefix the generated String
     *            representation showed in the text control if multiselect is
     *            supported. <code>null</code> to use the default prefix.
     * @param multiselectTextSuffix
     *            String that should be used to suffix the generated String
     *            representation showed in the text control if multiselect is
     *            supported. <code>null</code> to use the default suffix.
     * @see NatCombo#DEFAULT_MULTI_SELECT_PREFIX
     * @see NatCombo#DEFAULT_MULTI_SELECT_SUFFIX
     */
    public void setMultiselectTextBracket(String multiselectTextPrefix, String multiselectTextSuffix) {
        if (multiselectTextPrefix == null) {
            this.multiselectTextPrefix = DEFAULT_MULTI_SELECT_PREFIX;
        } else {
            this.multiselectTextPrefix = multiselectTextPrefix;
        }

        if (multiselectTextSuffix == null) {
            this.multiselectTextSuffix = DEFAULT_MULTI_SELECT_SUFFIX;
        } else {
            this.multiselectTextSuffix = multiselectTextSuffix;
        }
    }

    /**
     * FocusListener that is used to ensure that the Text control and the
     * dropdown table control are sharing the same focus. If either of both
     * controls looses focus, the local focus flag is set to false and a delayed
     * background thread for focus lost is started. If the other control gains
     * focus, the local focus flag is set to true which skips the execution of
     * the delayed background thread. This means the NatCombo hasn't lost focus.
     *
     * @since 1.4
     */
    public class FocusListenerWrapper implements FocusListener {

        @Override
        public void focusLost(final FocusEvent e) {
            NatCombo.this.hasFocus = false;
            Display.getCurrent().timerExec(100, () -> {
                if (!NatCombo.this.hasFocus) {
                    List<FocusListener> copy = new ArrayList<>(NatCombo.this.focusListener);
                    for (FocusListener f : copy) {
                        f.focusLost(e);
                    }
                }
            });
        }

        @Override
        public void focusGained(FocusEvent e) {
            NatCombo.this.hasFocus = true;
            for (FocusListener f : NatCombo.this.focusListener) {
                f.focusGained(e);
            }
        }
    }

}