# InputDialog.java

    /*
    ** Module   : InputDialog.java
    ** Abstract : Generic input data dialog.
    **
    ** Copyright (c) 2017-2022, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --------------------------------Description-----------------------------------
    ** 001 HC  20170612 Initial version.
    ** 002 EVL 20170926 Javadoc fix.
    ** 003 OM  20190111 Got rid of compile warning related to ambiguous method call.
    ** 004 TJD 20220331 Removed deprecated Inputdialog.showFilterDialog()
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.admin.client.widget.dialog;

    import com.goldencode.p2j.admin.client.widget.*;
    import com.google.gwt.dom.client.*;
    import com.google.gwt.editor.client.*;
    import com.google.gwt.event.dom.client.*;
    import com.google.gwt.uibinder.client.*;
    import com.google.gwt.user.client.ui.*;
    import com.google.inject.*;
    import com.google.web.bindery.event.shared.*;

    import org.gwtbootstrap3.client.ui.Button;
    import org.gwtbootstrap3.client.ui.CheckBox;
    import org.gwtbootstrap3.client.ui.*;
    import org.gwtbootstrap3.client.ui.IntegerBox;
    import org.gwtbootstrap3.client.ui.LongBox;
    import org.gwtbootstrap3.client.ui.SuggestBox;
    import org.gwtbootstrap3.client.ui.TextArea;
    import org.gwtbootstrap3.client.ui.TextBox;
    import org.gwtbootstrap3.client.ui.base.*;
    import org.gwtbootstrap3.client.ui.base.ValueBoxBase;
    import org.gwtbootstrap3.client.ui.constants.*;
    import org.gwtbootstrap3.client.ui.form.validator.*;
    import org.gwtbootstrap3.client.ui.html.*;
    import org.gwtbootstrap3.extras.select.client.ui.*;
    import org.gwtbootstrap3.extras.select.client.ui.constants.*;

    import java.util.*;
    import java.util.function.*;

    /**
     * Generic input data dialog.
     */
    public class InputDialog
    extends Composite
    {
       /**
        * GWT binder.
        */
       interface Binder
       extends UiBinder<Modal, InputDialog>
       {}

       /** Widget */
       @UiField
       HTML bodyMessage;

       /** Widget */
       @UiField
       org.gwtbootstrap3.client.ui.html.Span title;

       /** Widget */
       @UiField
       Button bSave;

       /** Widget */
       @UiField
       Button bCancel;

       /** Widget */
       @UiField
       Modal modal;

       /** Widget */
       @UiField
       ModalFooter modalFooter;

       /** Widget */
       @UiField
       ButtonGroup extraButtonGroup;

       /** Widget */
       @UiField
       Div formsPanel;

       /** Widget */
       @UiField
       NavTabs navTabs;

       /** Size of the labels in the first column of fields. */
       private int labelSize = 4;

       /** Ato hide flag */
       private boolean autoHide;

       /** The handler that will be closed when the dialog is dismissed */
       private Consumer<Map<Field, Object>> actionHandler;

       /** Hide dialog handler registration */
       private HandlerRegistration hideHandlerReg;

       /** Map of fields to widgets */
       private Map<Field, HasValue> widgets = new HashMap<>();

       /** Mao of panel forms */
       private Map<Panel, Form> forms = new HashMap<>();

       /** Map of tabs */
       private Map<Panel, AnchorListItem> tabs = new HashMap<>();

       /** Currently displayed form */
       private Form activeForm;

       /** Currently displayed panel */
       private Panel activePanel;

       /** Text translation map */
       private Map<String, String> translationMap;

       /** If <code>true</code> the dialog will show the top level panels as tabs */
       private boolean tabbed;

       /**
        * Ctor.
        *
        * @param   binder
        *          GWT binder.
        */
       @Inject
       InputDialog(InputDialog.Binder binder)
       {
          initWidget(binder.createAndBindUi(this));
       }

       /**
        * Build the dialog.
        *
        * @param title
        *        Dialog title.
        * @param items
        *        Input form items.
        */
       public void build(String title, Item... items)
       {
          build(title, "", items);
       }

       /**
        * Build the dialog.
        *
        * @param title
        *        Dialog title.
        * @param helpText
        *        Help text to be displayed above the table or <code>null</code>
        * @param items
        *        Input form items.
        */
       public void build(String title, String helpText, Item... items)
       {
          this.title.setText(title);
          bodyMessage.setHTML(helpText);

          if (!hasLabels(items))
          {
             labelSize = 0;
          }

          buildWidgets(items);
          layout(items);

          for (Map.Entry<Panel, AnchorListItem> anchor : tabs.entrySet())
          {
             anchor.getValue().addClickHandler(event ->
             {
                Panel newPanel = anchor.getKey();
                if (newPanel == activePanel)
                {
                   return;
                }

                AnchorListItem oldTab = tabs.get(activePanel);
                AnchorListItem newTab = anchor.getValue();
                Form newForm = forms.get(anchor.getKey());

                if (oldTab != null)
                {
                   oldTab.setActive(false);
                }

                newTab.setActive(true);

                if (activeForm != null)
                {
                   activeForm.setVisible(false);
                }

                newForm.setVisible(true);
                activePanel = newPanel;
                activeForm = newForm;
             });
          }
       }

       /**
        * Adds extra buttons.
        *
        * @param   buttons
        *          Extra buttons array.
        */
       public void addButtons(Button ... buttons)
       {
          for (Button button : buttons)
          {
             extraButtonGroup.add(button);
          }
       }

       /**
        * Adds field validators.
        *
        * @param   f
        *          Field.
        * @param   validators
        *          Array of validators.
        */
       public void addValidators(Field f, Validator... validators)
       {
          for (Validator v : validators)
          {
             ((HasValidators) widgets.get(f)).addValidator(v);
          }
       }

       /**
        * Allows to override the default label of the save button.
        *
        * @param    saveButtonText
        *           The new label for the save button
        */
       public void setSaveButtonText(String saveButtonText)
       {
          this.bSave.setText(saveButtonText);
       }

       /**
        * Sets the default label size.
        *
        * @param   size
        *          New size.
        */
       public void setLabelSize(int size)
       {
          if (size < 1 || size > 12)
          {
             throw new RuntimeException("Unsupported label size!");
          }

          labelSize = size;
       }

       /**
        *  Sets the tabbed flag.
        *
        * @param   tabbed
        *          Tabbed flag.
        */
       public void setTabbed(boolean tabbed)
       {
          this.tabbed = tabbed;
       }

       /**
        * Returns the widget of a specified field.
        *
        * @param   f
        *          A field.
        *
        * @return  the widget or <code>null</code> if widget not found
        */
       public HasValue getWidget(Field f)
       {
          return widgets.get(f);
       }

       /**
        * Returns the value of a specified field.
        *
        * @param   field
        *          A field.
        * @param   defValue
        *          Value that will be returned when the field value is <code>null</code>.
        * @param   <T>
        *          Value type.
        *
        * @return  The field value
        */
       public <T> T getValue(Field field, T defValue)
       {
          HasValue w = getWidget(field);
          if (w == null)
          {
             throw new RuntimeException("Field does not exist!");
          }
          T v = (T) w.getValue();
          return v == null ? defValue : v;
       }

       /**
        * Sets field value.
        *
        * @param   field
        *          A field.
        * @param   value
        *          A value.
        */
       public void setValue(Field field, Object value)
       {
          setValue(field, value, false);
       }

       /**
        * Sets field value.
        *
        * @param   field
        *          A field.
        * @param   value
        *          A value.
        * @param   fireEvents
        *          When <code>true</code> the widget will trigger a value-change event.
        */
       public void setValue(Field field, Object value, boolean fireEvents)
       {
          HasValue w = getWidget(field);
          if (w == null)
          {
             throw new RuntimeException("Field does not exist!");
          }
          w.setValue(value, fireEvents);
       }

       /**
        * Get tab widget for the supplied panel.
        *
        * @param   p
        *          A panel.
        *
        * @return  tab widget
        */
       public AbstractListItem getTab(Panel p)
       {
          return tabs.get(p);
       }

       /**
        * Show the dialog. The dialog will close automatically when Save clicked.
        *
        * @param   parent
        *          Parent widget where this widget will be revealed in.
        * @param   actionHandler
        *          Invoked when Save or Cancel button is clicked.
        */
       public void show(HasWidgets parent, Consumer<Map<Field, Object>> actionHandler)
       {
          show(parent, true, actionHandler);
       }

       /**
        * Show the dialog.
        *
        * @param   parent
        *          Parent widget where this widget will be revealed in.
        * @param   autoHide
        *          When true, the dialog will hide when Save is clicked automatically.
        *          When false, dismiss() must be called by the user code to make the dialog go away.
        *          Note that the dialog always hides when Cancel is clicked.
        * @param   actionHandler
        *          Invoked when Save or Cancel button is clicked.
        */
       public void show(HasWidgets parent, boolean autoHide, Consumer<Map<Field, Object>> actionHandler)
       {
          this.autoHide = autoHide;
          this.actionHandler = actionHandler;
          parent.add(this);

          boolean activeModal = Document.get().getBody().hasClassName("modal-open");

          hideHandlerReg = modal.addHiddenHandler(evt ->
          {
             this.removeFromParent();
             hideHandlerReg.removeHandler();
             if (activeModal)
             {
                // when stack of modals are displayed the modal-open style must be restored
                // so that the modal under this one has the correct appearance (for example
                // the scroll bars are displayed and the modal can be scrolled when larger
                // than the visible area)
                Document.get().getBody().addClassName("modal-open");
             }
          });

          modal.show();
       }

       /**
        * Dismisses the dialog.
        */
       public void dismiss()
       {
          modal.hide();
       }

       /**
        * Click event handler.
        *
        * @param   event
        *          Click event.
        */
       @UiHandler("bSave")
       public void bSaveClick(ClickEvent event)
       {
          boolean valid = true;
          for (Form form : forms.values())
          {
             boolean formValid = form.validate(true);
             if (tabs != null && !tabs.isEmpty())
             {
                for (Map.Entry<Panel, Form> panel : forms.entrySet())
                {
                   if (panel.getValue() == form)
                   {
                      AnchorListItem anchor = tabs.get(panel.getKey());
                      if (anchor != null)
                      {
                         anchor.setIcon(formValid ? null : IconType.EXCLAMATION_TRIANGLE);
                         if (!formValid)
                         {
                            anchor.addStyleName("has-error");
                         }
                         else
                         {
                            anchor.removeStyleName("has-error");
                         }
                      }
                   }
                }
             }
             valid &= formValid;
          }

          if (!valid)
          {
             return;
          }

          Map<Field, Object> result = new HashMap<>();
          widgets.forEach((f, w) ->
          {
             result.put(f, w.getValue());
          });

          if (autoHide)
          {
             dismiss();
          }
          actionHandler.accept(result);
       }

       /**
        * Click event handler.
        *
        * @param   event
        *          Click event.
        */
       @UiHandler("bCancel")
       public void bCancelClick(ClickEvent event)
       {
          if (actionHandler != null)
          {
             dismiss();
             actionHandler.accept(null);
          }
       }

       /**
        * Text translation map.
        *
        * @return   the translation map
        */
       public Map<String, String> getTranslationMap()
       {
          return translationMap;
       }

       /**
        * Setups the translation map that holds pairs of a key and its presentation value.
        *
        * @param    translationMap
        *           The translation map
        */
       public void setTranslationMap(Map<String, String> translationMap)
       {
          this.translationMap = translationMap;
       }

       /**
        * Builds the dialog widgets from the input dialog model.
        *
        * @param   items
        *          The input dialog model.
        */
       private void buildWidgets(Item[] items)
       {
          for (Item item : items)
          {
             if (item instanceof WidgetItem)
             {
                continue;
             }

             if (item instanceof Group)
             {
                buildWidgets(((Group) item).subItems);
                continue;
             }

             Field f = (Field) item;

             if (f.value == null)
             {
                // the field type is determined from the value, so no null allowed
                throw new RuntimeException("Value must not be null!");
             }

             Class clazz = f.value instanceof Class ? (Class) f.value : f.value.getClass();
             HasValue rootWidget;

             if (f instanceof SelectField || f instanceof MultiSelectField)
             {
                boolean multi = f instanceof MultiSelectField;
                SelectBase select = multi ?
                new MultipleSelectWithValidation() :
                new SelectWithValidation();

                for (Object opt : f.options)
                {
                   select.add((IsWidget) opt);
                }

                if (f.value != null)
                {
                   select.setValue(f.value);
                }

                if (f.options.size() > 10)
                {
                   select.setLiveSearch(true);
                   select.setLiveSearchNormalize(true);
                   select.setLiveSearchStyle(LiveSearchStyle.CONTAINS);
                   select.setLiveSearchPlaceholder("Search");
                }

                if (f.header != null)
                {
                   select.setHeader(f.header);
                }

                rootWidget = select;
             }
             else if (clazz == String.class)
             {
                com.google.gwt.user.client.ui.ValueBoxBase<String> w;

                if (f.lines > 0)
                {
                   w = new TextArea();
                   ((TextArea) w).setVisibleLines(f.lines);
                }
                else
                {
                   w = new TextBox();
                }

                if (f.password)
                {
                   w.getElement().setPropertyString("type", "password");
                }

                if (f.value instanceof String)
                {
                   w.setValue((String) f.value);
                }

                rootWidget = w;

                if (f.suggestions != null && !f.suggestions.isEmpty())
                {
                   MultiWordSuggestOracle oracle = new MultiWordSuggestOracle();
                   oracle.addAll(f.suggestions);
                   oracle.setDefaultSuggestionsFromText(f.suggestions);
                   SuggestBox sb = new SuggestBox(oracle, (ValueBoxBase<String>) w);
                   rootWidget = sb;
                }
             }
             else if (clazz == Integer.class)
             {
                IntegerBox w = new IntegerBox();
                if (f.value instanceof Integer)
                {
                   w.setValue((Integer) f.value);
                }

                rootWidget = w;
             }
             else if (clazz == Long.class)
             {
                LongBox w = new LongBox();
                if (f.value instanceof Long)
                {
                   w.setValue((Long) f.value);
                }

                rootWidget = w;
             }
             else if (clazz == Boolean.class)
             {
                InlineCheckBox w = new InlineCheckBox();
                w.setText(f.label == null ? "" : f.label);
                if (f.value instanceof Boolean)
                {
                   w.setValue((Boolean) f.value);
                }

                rootWidget = w;
             }
             else if (f.value instanceof EnumCreator)
             {
                EnumRadioGroup.RadioStyles style = EnumRadioGroup.RadioStyles.RADIO_INLINE;
                if (!f.inline)
                {
                   style = EnumRadioGroup.RadioStyles.RADIO;
                }
                EnumRadioGroup<EnumCreator> w = new EnumRadioGroup<EnumCreator>(
                         "EnumRadioGroup-" + f.hashCode(),
                         (EnumCreator) f.value, style, true,
                         getTranslationMap());
                w.setValue((EnumCreator) f.value, false);
                rootWidget = w;
             }
             else
             {
                throw new RuntimeException("Type " + f.value.getClass() + " is not supported!");
             }

             if (rootWidget instanceof HasReadOnly)
             {
                ((HasReadOnly) rootWidget).setReadOnly(f.readOnly);
             }

             if (!f.readOnly)
             {
                if (!f.allowBlank)
                {
                   if (clazz == Integer.class)
                   {
                      ((HasValidators) rootWidget).addValidator(
                         createBlankValidator("An integer value is expected"));
                   }
                   else if (rootWidget instanceof HasValidators)
                   {
                      ((HasValidators) rootWidget).addValidator(createBlankValidator());
                   }
                }

                if (f.validators != null && rootWidget instanceof HasValidators)
                {
                   for (Validator v : f.validators)
                   {
                      ((HasValidators) rootWidget).addValidator(v);
                   }
                }
             }

             widgets.put(f, rootWidget);
          }
       }

       /**
        * Layouts the input dialog.
        *
        * @param   items
        *          The input dialog model.
        */
       private void layout(Item[] items)
       {
          if (tabbed)
          {
             // build tabs
             boolean first = true;
             for (Item it : items)
             {
                Panel panel = (Panel) it;

                AnchorListItem tab = new AnchorListItem();

                tab.setActive(first);
                if (first)
                {
                   activePanel = panel;
                   first = false;
                }

                tab.setText(panel.label);
                tabs.put(panel, tab);
                navTabs.add(tab);
             }

             // build forms
             first = true;
             for (Item it : items)
             {
                Panel panel = (Panel) it;

                Form form = new Form(FormType.HORIZONTAL);
                forms.put(panel, form);

                form.add(layoutPanel(form, panel, true));

                form.setVisible(first);
                if (first)
                {
                   activeForm = form;
                   first = false;
                }

                formsPanel.add(form);
             }

             navTabs.setVisible(true);
          }
          else
          {
             Form form = new Form(FormType.HORIZONTAL);
             forms.put(null, form);
             form.add(layoutPanel(form, new Panel(null, labelSize, items), false));
             formsPanel.add(form);
          }
       }

       /**
        * Layouts one panel, this is part of the main layout logic.
        *
        * @param   f
        *          Form item.
        * @param   panel
        *          Panel item.
        * @param   nolabel
        *          Whether labels should be visible.
        *
        * @return  The layout result in a form of UI widget.
        */
       private FieldSet layoutPanel(Form f, Panel panel, boolean nolabel)
       {
          FieldSet fieldSet = new FieldSet();
          if (panel.label != null && !nolabel)
          {
             fieldSet.add(new Legend(panel.label));
          }

          if (panel.subItems == null)
          {
             return fieldSet;
          }

          for (Item item : panel.subItems)
          {
             Span span = null;
             if (item instanceof Span)
             {
                span = (Span) item;
             }
             else if (item instanceof Field)
             {
                span = new Span(item);
             }

             if (span != null && span.subItems != null && span.subItems.length > 0)
             {
                fieldSet.add(layoutSpan(span, panel.labelSize != null ? panel.labelSize : labelSize));
             }
             else if (item instanceof Panel)
             {
                Row row = new Row();
                Column fullColl = new Column(ColumnSize.XS_12);
                row.add(fullColl);
                fullColl.add(fieldSet);

                fieldSet.add(layoutPanel(f, (Panel) item, false));
             }
          }

          return fieldSet;
       }

       /**
        * Layouts one line of widgets.
        *
        * @param   span
        *          The items on one line.
        * @param   labelSize
        *          Label size.
        *
        * @return  The layout result in a form of UI widget.
        */
       private Row layoutSpan(Span span, int labelSize)
       {
          if (span.subItems == null || span.subItems.length == 0)
          {
             return null;
          }

          ColumnSize[] colsizes = ColumnSize.values();

          List<UIObject> uiObjects = new ArrayList<>();
          List<UIObject> skip = new ArrayList<>();
          UIObject firstLabel = null;
          boolean first = true;
          for (Item item : span.subItems)
          {
             if (item instanceof WidgetItem)
             {
                uiObjects.add(((WidgetItem) item).widget);
                skip.add(((WidgetItem) item).widget);
             }
             else if (item instanceof Field)
             {
                Field f = (Field) item;
                HasValue w = widgets.get(f);

                IsWidget label = buildLabel(f, w);
                if (first && label != null)
                {
                   firstLabel = (UIObject) label;
                }
                else if (label != null)
                {
                   uiObjects.add((UIObject) label);
                }

                Widget wRoot = buildValidatorUI(f, w);
                uiObjects.add(wRoot);

                if (f.labelSize != null && f.labelSize >= 0 && f.labelSize <= 12)
                {
                   if (f.labelSize > 0)
                   {
                      ((UIObject) label).addStyleName(colsizes[f.labelSize - 1].getCssName());
                   }
                   skip.add((UIObject) label);

                   if (first)
                   {
                      labelSize = f.labelSize;
                   }
                }

                if (f.fieldSize != null && f.fieldSize >= 0 && f.fieldSize <= 12)
                {
                   if (f.fieldSize > 0)
                   {
                      wRoot.addStyleName(colsizes[f.fieldSize - 1].getCssName());
                   }
                   skip.add(wRoot);
                }

                first = false;
             }
          }

          int colSize = (12 - labelSize) / uiObjects.size();
          if (colSize < 1)
          {
             colSize = 1;
          }

          ColumnSize colsize = colsizes[colSize - 1];
          int remain = 12 - (colSize*uiObjects.size() + labelSize);

          Row row = new Row();
          FormGroup fg = new FormGroup();
          row.add(fg);

          UIObject firstWidget = uiObjects.isEmpty() ? null : uiObjects.get(0);

          if (firstLabel != null)
          {
             firstLabel.addStyleName(colsizes[labelSize - 1].getCssName());
             fg.add((IsWidget) firstLabel);
          }
          else if (firstWidget != null && !skip.contains(firstWidget))
          {
             firstWidget.addStyleName(ColumnOffset.values()[labelSize].getCssName());
          }

          for (UIObject obj : uiObjects)
          {
             ColumnSize effectiveSize = colsize;
             if (remain > 0)
             {
                effectiveSize = colsizes[colSize];
                remain--;
             }

             if (!skip.contains(obj))
             {
                obj.addStyleName(effectiveSize.getCssName());
             }

             fg.add((IsWidget) obj);
          }

          return row;
       }

       /**
        * Builds label widget.
        *
        * @param   f
        *          Field for which to build the label.
        * @param   w
        *          The field widget.
        *
        * @return  The built label widget.
        */
       private Widget buildLabel(Field f, HasValue w)
       {
          FormLabel label = null;
          if (!(w instanceof CheckBox))
          {
             if (f.label != null && !f.label.isEmpty())
             {
                label = new FormLabel();
                label.setText(f.label + ":");
             }
          }

          return label;
       }

       /**
        * Assembles the field widget into a set of widgets capable to validate and show validation
        * errors.
        *
        * @param   f
        *          Field for which to build the validation UI.
        * @param   w
        *          The field widget.
        *
        * @return  The built validation UI.
        */
       private Widget buildValidatorUI(Field f, HasValue w)
       {
          FlowPanel widgetPanel = new FlowPanel();

          if (f.inputAddon == null)
          {
             widgetPanel.add((IsWidget) w);
          }
          else
          {
             InputGroup iGroup = new InputGroup();
             iGroup.add((IsWidget) w);
             iGroup.add(f.inputAddon);
             widgetPanel.add(iGroup);
          }

          if (f.tooltip != null && f.tooltip.trim().length() > 0)
          {
             Icon icon = new Icon(IconType.INFO_CIRCLE);
             Tooltip tooltip = new Tooltip(icon, f.tooltip);
             widgetPanel.add(tooltip);
          }

          InlineHelpBlock hlp = new InlineHelpBlock();
          hlp.setIconType(IconType.EXCLAMATION_TRIANGLE);
          widgetPanel.add(hlp);

          return widgetPanel;
       }

       /**
        * Creates a blank validator with the default message.
        *
        * @return  a blank validator
        */
       private Validator createBlankValidator()
       {
          return createBlankValidator(null);
       }

       /**
        * Creates a blank validator with a custom message.
        *
        * @param    errMessage
        *           The error message
        *
        * @return   The blank validator
        */
       private Validator createBlankValidator(String errMessage)
       {
          return new SensitiveBlankValidatorWrapper(errMessage == null ?
                                                    new BlankValidator() :
                                                    new BlankValidator(errMessage));
       }

       /**
        * Checks whether the supplied input dialog model has any defined label.
        *
        * @param   items
        *          The input dialog model.
        *
        * @return  <code>true</code> when there is any label defined in the supplied model
        */
       private boolean hasLabels(Item... items)
       {
          for (Item item : items)
          {
             if (item instanceof Field)
             {
                Field f = (Field) item;
                if ((f.value == Boolean.class || f.value instanceof Boolean) && f.inline)
                {
                   // inline check boxes do not have labels
                   continue;
                }
                String label = f.label;
                if (label != null && !label.trim().isEmpty())
                {
                   return true;
                }
             }
             else if (item instanceof Group)
             {
                boolean res = hasLabels(((Group) item).subItems);
                if (res)
                {
                   return true;
                }
             }
          }

          return false;
       }

       /**
        * The root interface of the input dialog model.
        */
       public interface Item
       {
       }

       /**
        * The root interface for items having other sub items.
        */
       public abstract static class Group
       implements Item
       {
          /** sub items */
          Item[] subItems;
       }

       /**
        * Group of items spanning one line.
        */
       public static class Span
       extends Group
       {
          /**
           * Ctor.
           *
           * @param   items
           *          The items to be spanned on one line.
           */
          public Span(Item... items)
          {
             subItems = items;
          }
       }

       /**
        * Group of items positioned on a single form panel.
        */
       public static class Panel
       extends Group
       {
          /** Optional panel label */
          String label;

          /** the column size of the label area dedicated to the sub items */
          Integer labelSize;

          /**
           * Ctor.
           *
           * @param   label
           *          The panel label.
           * @param   items
           *          The items to be displayed on one form panel.
           */
          public Panel(String label, Item... items)
          {
             this.label = label;
             subItems = items;
          }

          /**
           * Ctor.
           *
           * @param   label
           *          The panel label.
           * @param   labelSize
           *          Labels size.
           * @param   items
           *          The items to be displayed on one form panel.
           */
          public Panel(String label, int labelSize, Item... items)
          {
             this.label = label;
             subItems = items;
             this.labelSize = labelSize;
          }
       }

       /**
        * A single input dialog field.
        *
        * @param   <ValType>
        *          The value type the field represents.
        */
       public static class Field<ValType>
       implements Item
       {
          /** Optional field label */
          String label = "";

          /** Label size */
          Integer labelSize;

          /** Field size */
          Integer fieldSize;

          /** Tooltip */
          String tooltip;

          /** The value to be displayed */
          ValType value;

          /** Whether blank value is accepted */
          boolean allowBlank = true;

          /** Any custom validators */
          Validator<ValType>[] validators = null;

          /** List of values to be displayed in a suggestion list box */
          List<String> suggestions;

          /** Additional input widget */
          AbstractInputGroupAddon inputAddon;

          /** List of options for selection fields */
          List<Option> options;

          /** Header to be displayed in the selection list for selection fields */
          String header;

          /** Whether the field should be laid out as inline, relevant for check and radio boxes */
          boolean inline = true;

          /** Number of lines for text area field */
          int lines = 0;

          /** Read only flag */
          boolean readOnly;

          /** Password flag */
          boolean password;

          /**
           * Default constructor.
           */
          public Field()
          {
          }

          /**
           * Constructor.
           *
           * @param   label
           *          Field label.
           * @param   value
           *          Initial field value.
           */
          public Field(String label, ValType value)
          {
             this(label, value, false);
          }

          /**
           * Constructor. The list of validators remains {@code null}.
           *
           * @param   label
           *          Field label.
           * @param   value
           *          Initial field value.
           * @param   allowBlank
           *          Whether blank value is allowed.
           */
          public Field(String label, ValType value, boolean allowBlank)
          {
             this.label = label;
             this.value = value;
             this.allowBlank = allowBlank;
          }

          /**
           * Constructor.
           *
           * @param   label
           *          Field label.
           * @param   value
           *          Initial field value.
           * @param   allowBlank
           *          Whether blank value is allowed.
           * @param   validators
           *          Custom validators.
           */
          public Field(String label,
                       ValType value,
                       boolean allowBlank,
                       Validator<ValType>... validators)
          {
             this.label = label;
             this.value = value;
             this.allowBlank = allowBlank;
             this.validators = validators;
          }

          /**
           * Suggestions setter.
           *
           * @param   suggestions
           *          List of suggestions.

           * @return  returns <code>this</code>
           */
          public Field withSuggestions(List<String> suggestions)
          {
             this.suggestions = suggestions;
             return this;
          }

          /**
           * Tooltip setter.
           *
           * @param   tooltip
           *          Field tooltip.
           *
           * @return  returns <code>this</code>
           */
          public Field withTooltip(String tooltip)
          {
             this.tooltip = tooltip;
             return this;
          }

          /**
           * Addon setter.
           *
           * @param   inputAddon
           *          Field addon widget.
           *
           * @return  returns <code>this</code>
           */
          public Field withAddon(AbstractInputGroupAddon inputAddon)
          {
             this.inputAddon = inputAddon;
             return this;
          }

          /**
           * Inline setter.
           *
           * @param   inline
           *          Inline flag.
           *
           * @return  returns <code>this</code>
           */
          public Field inline(boolean inline)
          {
             this.inline = inline;
             return this;
          }

          /**
           * Lines setter.
           *
           * @param   lines
           *          Text area number of lines.
           *
           * @return  returns <code>this</code>
           */
          public Field lines(int lines)
          {
             this.lines = lines;
             return this;
          }

          /**
           * Readonly setter.
           *
           * @param   readOnly
           *          Field read only flag.
           *
           * @return  returns <code>this</code>
           */
          public Field readOnly(boolean readOnly)
          {
             this.readOnly = readOnly;
             return this;
          }

          /**
           * Password flag setter.
           *
           * @param   password
           *          Field password flag.
           *
           * @return  returns <code>this</code>
           */
          public Field password(boolean password)
          {
             this.password = password;
             return this;
          }

          /**
           * Label size setter.
           *
           * @param   size
           *          Field label size.
           *
           * @return  returns <code>this</code>
           */
          public Field withLabelSize(int size)
          {
             this.labelSize = size;
             return this;
          }

          /**
           * Field size setter.
           *
           * @param   size
           *          Field size.
           *
           * @return  returns <code>this</code>
           */
          public Field withFieldSize(int size)
          {
             this.fieldSize = size;
             return this;
          }

          /**
           * Options builder.
           *
           * @param   optionsList
           *          List of options.
           *
           * @return  list of {@link Option} instances
           */
          protected List<Option> buildOptions(List<String> optionsList)
          {
             List<Option> optionList = new ArrayList<>(optionsList.size());
             for (String option : optionsList)
             {
                Option opt = new Option();
                opt.setValue(option);
                opt.setText(option);
                optionList.add(opt);
             }

             return optionList;
          }

          /**
           * Options builder.
           *
           * @param   optionsMap
           *          List of options map [value-display text].
           *
           * @return  list of {@link Option} instances
           */
          protected List<Option> buildOptions(Map<String, String> optionsMap)
          {
             List<Option> optionList = new ArrayList<>(optionsMap.size());
             for (Map.Entry<String, String> option : optionsMap.entrySet())
             {
                Option opt = new Option();
                opt.setValue(option.getKey());
                opt.setText(option.getValue());
                optionList.add(opt);
             }

             return optionList;
          }
       }

       /**
        * Select input field.
        */
       public static class SelectField
       extends Field<String>
       {
          /** Header to be displayed in the selection list box */
          String header;

          /**
           * Default ctor.
           */
          public SelectField()
          {
             super();
          }

          /**
           * Ctor.
           *
           * @param   label
           *          Field label.
           * @param   options
           *          Select field options.
           * @param   value
           *          The initial value.
           */
          public SelectField(String label, List<String> options, String value)
          {
             super(label, value, false);
             this.options = buildOptions(options);
          }

          /**
           * Ctor.
           *
           * @param   label
           *          Field label.
           * @param   options
           *          Select field options.
           * @param   value
           *          The initial value.
           */
          public SelectField(String label, Map<String, String> options, String value)
          {
             super(label, value, false);
             this.options = buildOptions(options);
          }

          /**
           * Ctor.
           *
           * @param   label
           *          Field label.
           * @param   options
           *          Select field options.
           * @param   value
           *          The initial value.
           * @param   header
           *          The select list box header.
           */
          public SelectField(String label, List<Option> options, String value, String header)
          {
             this(label, options, value, header, false);
          }

          /**
           * Ctor.
           *
           * @param   label
           *          Field label.
           * @param   options
           *          Select field options.
           * @param   value
           *          The initial value.
           * @param   header
           *          The select list box header.
           * @param   allowBlank
           *          Allow blank flag.
           */
          public SelectField(String label,
                             List<Option> options,
                             String value,
                             String header,
                             boolean allowBlank)
          {
             super(label, value, allowBlank);
             this.header = header;
             this.options = options;
          }
       }

       /**
        * Multi selection field.
        */
       public static class MultiSelectField
       extends Field<List<String>>
       {
          /**
           * Default ctor.
           */
          public MultiSelectField()
          {
             super();
          }

          /**
           * Ctor.
           *
           * @param   label
           *          Field label.
           * @param   options
           *          Select field options.
           * @param   value
           *          The initial value.
           * @param   allowBlank
           *          Allow blank flag.
           */
          public MultiSelectField(String label,
                                  List<String> options,
                                  List<String> value,
                                  boolean allowBlank)
          {
             super(label, value, allowBlank);
             this.options = buildOptions(options);
          }

          /**
           * Ctor.
           *
           * @param   label
           *          Field label.
           * @param   options
           *          Select field options.
           * @param   value
           *          The initial value.
           * @param   allowBlank
           *          Allow blank flag.
           */
          public MultiSelectField(String label,
                                  Map<String, String> options,
                                  List<String> value,
                                  boolean allowBlank)
          {
             super(label, value, allowBlank);
             this.options = buildOptions(options);
          }

          /**
           * Ctor.
           *
           * @param   label
           *          Field label.
           * @param   options
           *          Select field options.
           * @param   value
           *          The initial value.
           * @param   allowBlank
           *          Allow blank flag.
           * @param   header
           *          The select list box header.
           */
          public MultiSelectField(String label,
                                  List<Option> options,
                                  List<String> value,
                                  boolean allowBlank,
                                  String header)
          {
             super(label, value, allowBlank);
             this.options = options;
             this.header = header;
          }
       }

       /**
        * Field item wrapping arbitrary widget.
        */
       public static class WidgetItem
       extends Field
       {
          /** The field widget */
          UIObject widget;

          /**
           * Ctor.
           *
           * @param   widget
           *          The field widget.
           */
          public WidgetItem(UIObject widget)
          {
             this.widget = widget;
          }
       }

       /**
        * This class wraps any validator and relaxes its validation logic in such a way that
        * it always considers the disabled editor valid.
        */
       private static class SensitiveBlankValidatorWrapper
       implements Validator
       {
          /** The wrapped validator */
          private BlankValidator blankValidator;

          /**
           * Ctor.
           *
           * @param   blankValidator
           *          The wrapped validator.
           */
          public SensitiveBlankValidatorWrapper(BlankValidator blankValidator)
          {
             this.blankValidator = blankValidator;
          }

          /**
           * Priority value for this validator. Lower the number, higher the priority.
           *
           * @return  the priority.
           */
          @Override
          public int getPriority()
          {
             return blankValidator.getPriority();
          }

          /**
           * Validate the field.
           *
           * @param   editor the {@link Editor}.
           * @param   value  the value
           * @return  the list
           */
          @Override
          public List<EditorError> validate(Editor editor, Object value)
          {
             if (editor instanceof HasEnabled && !((HasEnabled) editor).isEnabled())
             {
                return Collections.emptyList();
             }

             return blankValidator.validate(editor, value);
          }
       }

    }