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);
      }
   }

}