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