# CustomComboBox.java

    /*
    ** Module   : CustomComboBox.java
    ** Abstract : CustomComboBox is a combo box that is used data grid as its model.
    **
    ** Copyright (c) 2017-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ----------------Description----------------------
    ** 001 SBI 20170601 Created initial version.
    ** 002 CA  20180607 Changed to allow HTML table cells.
    ** 003 SBI 20231117 Reused GridHandle as the grid data model for this combobox.
    */
    /*
    ** 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;

    import java.lang.reflect.Array;
    import java.util.*;
    import java.util.logging.*;

    import com.google.common.base.Predicate;
    import com.google.gwt.core.client.GWT;
    import com.google.gwt.core.client.Scheduler;
    import com.google.gwt.event.dom.client.*;
    import com.google.gwt.event.logical.shared.*;
    import com.google.gwt.uibinder.client.*;
    import com.google.gwt.user.cellview.client.DataGrid;
    import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.*;
    import com.google.gwt.dom.client.*;
    import com.google.gwt.user.client.Event;
    import com.google.gwt.user.client.Event.*;
    import com.google.gwt.user.client.ui.*;
    import com.google.gwt.view.client.*;
    import com.google.web.bindery.event.shared.*;

    import org.gwtbootstrap3.client.ui.TextBox;

    /**
     *
     * Grid based combo box. It displays a grid drop down.
     *
     * @param    <TRowObject>
     *           The grid row object type
     */
    public class CustomComboBox<TRowObject>
    extends Composite
    {
       /** The logger */
       private static Logger logger = Logger.getLogger("CustomComboBox");

       /** The GWT UI creator */
       interface Binder extends UiBinder<Widget, CustomComboBox>
       {}

       /** The instance of the GWT UI creator */
       private static final Binder binder = GWT.create(Binder.class);

       /** The widget container */
       @UiField
       HorizontalPanel container;

       /** The text box */
       @UiField
       TextBox entry;

       /** The dropdown button*/
       @UiField
       Button dropdown;

       /** The dropdown panel */
       private PopupPanel popup;

       /** The selection handler */
       private HandlerRegistration selectionModelHandler;

       /** The parser for text entry presentation values */
       private EntryValue<TRowObject> entryTextParser;

       /** The grid handle that manages the grid placed on the dropdown panel */
       private GridHandle<TRowObject> gridHandle;

       /**
        * Creates the instance of the grid combobox.
        *
        * @param    popupWidth
        *           The css width
        * @param    popupHeight
        *           The css height
        */
       @UiConstructor
       public CustomComboBox(String popupWidth, String popupHeight)
       {
          initWidget(binder.createAndBindUi(this));
          entry.setReadOnly(true);
          entry.addKeyDownHandler(new KeyDownHandler()
          {
             @Override
             public void onKeyDown(KeyDownEvent event)
             {
                NativeEvent nativeEvent = event.getNativeEvent();
                int keyCode = nativeEvent.getKeyCode();
                if (keyCode == KeyCodes.KEY_ENTER)
                {
                   togglePopup();
                }
             }
          });
          popup = new PopupPanel(true)
          {
             @Override
             protected void onPreviewNativeEvent(NativePreviewEvent event)
             {
                super.onPreviewNativeEvent(event);
                Event nativeEvent = Event.as(event.getNativeEvent());
                int type = nativeEvent.getTypeInt();
                switch (type) {
                   case Event.ONKEYDOWN:
                      int keyCode = nativeEvent.getKeyCode();
                      if (keyCode == KeyCodes.KEY_ENTER || keyCode == KeyCodes.KEY_ESCAPE)
                      {
                         hidePopup();
                      }
                      break;
                   case Event.ONDBLCLICK:
                      hidePopup();
                      break;
                  default:
                     break;
                }
             }
          };
          popup.addStyleName("custom-combobox-popup");
          popup.setHeight(popupHeight);
          popup.setWidth(popupWidth);
          popup.addAutoHidePartner(dropdown.getElement());
          popup.addCloseHandler(new CloseHandler<PopupPanel>()
          {

             @Override
             public void onClose(CloseEvent<PopupPanel> event)
             {
                toggleDropdownStyle();
             }
          });
       }

       /**
        * Defines the presentation method to parse a row into its presentation value.
        *
        * @param    <TRowObject>
        *           The row type
        */
       public interface EntryValue<TRowObject>
       {
          /**
           * Returns the presentation value of the set of selected rows.
           *
           * @param    rows
           *           The given set of rows
           *
           * @return   The row's presentation value
           */
          String toEntryValue(Set<TRowObject> rows);
       }

       /**
        * Returns the combobox text entry parser.
        *
        * @return   The text entry parser
        */
       public EntryValue<TRowObject> getEntryTextParser()
       {
          return entryTextParser;
       }

       /**
        * Sets the combobox text entry parser.
        *
        * @param    entryTextParser
        *           The text entry parser
        */
       public void setEntryTextParser(EntryValue<TRowObject> entryTextParser)
       {
          this.entryTextParser = entryTextParser;
       }

       public void setGridHandle(GridHandle<TRowObject> gridHandle)
       {
          GridHandle<TRowObject> oldGridHandle = this.gridHandle;

          this.gridHandle = gridHandle;

          //clear data
          if (oldGridHandle != null && oldGridHandle != gridHandle)
          {
             oldGridHandle.clearGrid();
             oldGridHandle.clearSelection();
          }

          if (this.selectionModelHandler != null)
          {
             this.selectionModelHandler.removeHandler();
          }

          selectionModelHandler = this.gridHandle.getSelectionModel().addSelectionChangeHandler(new SelectionChangeEvent.Handler()
          {

             @Override
             public void onSelectionChange(SelectionChangeEvent event)
             {
                Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand()
                {
                   @Override
                   public void execute()
                   {
                      Set<TRowObject> selectedRows = CustomComboBox.this.gridHandle.getSelected();
                      String value;
                      if (!selectedRows.isEmpty())
                      {
                         value = getEntryTextParser().toEntryValue(selectedRows);
                      }
                      else
                      {
                         value = "";
                      }
                      entry.setValue(value);
                   }
                });
             }

          });
          //place the grid on the popup panel
          DataGrid grid = this.gridHandle.getGrid();
          popup.add(grid);
          popup.addAutoHidePartner(grid.getElement());
          grid.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.BOUND_TO_SELECTION);
       }

       /**
        * Set data.
        *
        * @param    data
        *           The table rows
        */
       public void setData(TRowObject[] data)
       {
          gridHandle.setRows(data);
       }

       /**
        * Gets the selected set.
        *
        * @return   The selected values
        */
       public Set<TRowObject> getValue()
       {
          if (gridHandle != null)
          {
             return gridHandle.getSelected();
          }

          return null;
       }

       /**
        * Sets the selected value.
        *
        * @param    row
        *           The selected row
        */
       public void setValue(TRowObject row)
       {
          if (gridHandle != null)
          {
             gridHandle.select(row);
          }
       }

       /**
        * Resets the current selection
        */
       public void resetValue()
       {
          //clear selection model
          if (gridHandle != null)
          {
             this.gridHandle.clearSelection();
          }
       }

       /**
        * Defines the dropdown button handler.
        *
        * @param    event
        *           The click event
        */
       @UiHandler("dropdown")
       void onDropDown(ClickEvent event)
       {
          togglePopup();
       }

       /**
        * Sets the selected value by item selector.
        *
        * @param    itemSelector
        *           The item selector
        */
       public void selectItem(Predicate<TRowObject> itemSelector)
       {
          ListDataProvider<TRowObject> dataProvider = getDataProvider();
          for (TRowObject value : dataProvider.getList())
          {
             if (itemSelector.apply(value))
             {
                resetValue();
                setValue(value);
                break;
             }
          }
       }

       /**
        * Returns the data grid.
        *
        * @return   The data grid.
        */
       public DataGrid<TRowObject> getDataGrid()
       {
          if (gridHandle != null)
          {
             return (DataGrid<TRowObject>) gridHandle.getGrid();
          }

          return null;
       }

       /**
        * Returns the selection model.
        *
        * @return   The selection model
        */
       public SelectionModel<TRowObject> getSelectionModel()
       {
          if (gridHandle != null)
          {
             return gridHandle.getSelectionModel();
          }

          return null;
       }

       /**
        * Returns the data provider.
        *
        * @return   The data provider
        */
       public ListDataProvider<TRowObject> getDataProvider()
       {
          if (gridHandle != null)
          {
             return gridHandle.getDataProvider();
          }

          return null;
       }

       /**
        * Requests the input focus for its data grid.
        */
       private void focusDataGrid()
       {
          DataGrid<TRowObject> grid = getDataGrid();
          if (grid != null)
          {
             Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand()
             {
                @Override
                public void execute()
                {
                   logger.info("grid is in focus");
                   grid.setFocus(true);
                }
             });
          }
       }

       /**
        * Displays the dropdown.
        */
       private void displayPopup()
       {
          popup.setPopupPositionAndShow(new PopupPanel.PositionCallback()
          {

             @Override
             public void setPosition(int offsetWidth, int offsetHeight)
             {
                int left = container.getAbsoluteLeft();
                int top = container.getAbsoluteTop() + container.getOffsetHeight();
                popup.setPopupPosition(left, top);
             }
          });

          toggleDropdownStyle();
       }

       /**
        * Switches the dropdown to be visible or hidden.
        */
       private void togglePopup()
       {
          Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand()
          {
             @Override
             public void execute()
             {
                if (hasElement(Document.get().getBody(), popup.getElement()))
                {
                   popup.hide();
                }
                else
                {
                   displayPopup();
                   focusDataGrid();
                }
             }
          });
       }

       /**
        * Hides the dropdown.
        */
       private void hidePopup()
       {
          Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand()
          {
             @Override
             public void execute()
             {
                if (hasElement(Document.get().getBody(), popup.getElement()))
                {
                   popup.hide();
                }
             }
          });
       }

       /**
        * Changes the presentation icon for the dropdown button.
        */
       private void toggleDropdownStyle()
       {
          Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand()
          {
             @Override
             public void execute()
             {
                toggleClass(dropdown.getElement(), "glyphicon-triangle-bottom");
                toggleClass(dropdown.getElement(), "glyphicon-triangle-top");
             }
          });
       }

       /**
        * Imports the jQuery toggleClass function.
        *
        * @param    e
        *           The html element
        * @param    style
        *           The css class
        */
       private native void toggleClass(final Element e, final String style) /*-{
       $wnd.jQuery(e).toggleClass(style);
       }-*/;

       /**
        * Tests if the given container holds the target element.
        *
        * @param    container
        *           The given container
        * @param    e
        *           The target element
        *
        * @return   The true if the given container has the target element, otherwise false.
        */
       private native boolean hasElement(final Element container, final Element e) /*-{
        return $wnd.jQuery.contains(container, e);
       }-*/;
    }