Question

I am trying to implement a comboBox with multi-select feature. There will be a checkBox associated with each of the string and user can select one or many from the list. In GXT 2, we had CheckBoxListView which makes things easier.

I had follwing idea of implementing it.

Using a Grid which can have one column as CheckBox and other column as the string that i wnat to display and then adding the store of this grid to the CheckBoxStore. But, as the ListStore of Grid and ComboBoxes are not same, i tried it but no success, because both store are different and acceptd different properties.

There should be an alternate way like using ListView.But, i am not getting how can i use CheckBox in ListView

Need Help

Was it helpful?

Solution

This is my code.. Not perfect, but sort of temporary workaround..

import java.util.ArrayList;
import java.util.List;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArrayString;
import client.grid.model.IComboBoxProperties;
import client.model.ComboBoxModel;
import com.sencha.gxt.cell.core.client.form.CheckBoxCell;
import com.sencha.gxt.cell.core.client.form.ComboBoxCell.TriggerAction;
import com.sencha.gxt.core.client.Style.HideMode;
import com.sencha.gxt.data.shared.LabelProvider;
import com.sencha.gxt.data.shared.ListStore;
import com.sencha.gxt.widget.core.client.Window;
import com.sencha.gxt.widget.core.client.event.ExpandEvent;
import com.sencha.gxt.widget.core.client.event.HideEvent;
import com.sencha.gxt.widget.core.client.event.HideEvent.HideHandler;
import com.sencha.gxt.widget.core.client.form.ComboBox;
import com.sencha.gxt.widget.core.client.grid.ColumnConfig;
import com.sencha.gxt.widget.core.client.grid.ColumnModel;
import com.sencha.gxt.widget.core.client.grid.Grid;


public class MultiSelectBox extends ComboBox<ComboBoxModel>{

 private Window checkBoxListHolder;
 private Grid<ComboBoxModel> checkBoxGrid;
 private ListStore<ComboBoxModel> listStore;
 private boolean readOnly;
 private ColumnConfig<ComboBoxModel, String> cc1;
 private ColumnConfig<ComboBoxModel, Boolean> cc2;
 private ComboBoxModel setData;
 IComboBoxProperties props = GWT.create(IComboBoxProperties.class); 

public MultiSelectBox(ListStore<ComboBoxModel> store,
        LabelProvider<? super ComboBoxModel> labelProvider) {
    super(store, labelProvider);
    this.listStore=getStore();
    this.checkBoxGrid=getCheckBoxGrid();

    checkBoxListHolder = new Window(){

    };
    checkBoxListHolder.setClosable(false);
    checkBoxListHolder.setHeaderVisible(false);

    checkBoxListHolder.setResizable(false);
    checkBoxListHolder.setAutoHide(true);
    checkBoxListHolder.getButtonBar().setVisible(false);
    checkBoxGrid.setVisible(true);

    checkBoxGrid.focus();
    checkBoxListHolder.add(checkBoxGrid);
    checkBoxListHolder.addHideHandler(new HideHandler() {

        @Override
        public void onHide(HideEvent event) {
             checkBoxGrid.getView().refresh(false);
             checkBoxGrid.getStore().commitChanges();
             clear();
             setValue(parseCheckedValues(checkBoxGrid));
             getCell().setTriggerAction(TriggerAction.ALL);

        }
    }); 

    addExpandHandler(new ExpandEvent.ExpandHandler() {

        @Override
        public void onExpand(ExpandEvent event) {
             if (checkBoxListHolder.isVisible()) {
                 checkBoxGrid.getView().refresh(false);

                    checkBoxGrid.getStore().commitChanges();
                    checkBoxListHolder.hide();
                } else {
                    if(getValue()!=null)
                    {
                        setData=getValue();
                        updateGrid(setData);

                    }
                    else
                    {
                        updateGridStore(getStore());
                    }
                    checkBoxGrid.getStore().commitChanges();
                    checkBoxGrid.getView().refresh(false);
                    checkBoxListHolder.setPosition(getAbsoluteLeft(), getAbsoluteTop()+getOffsetHeight());
                     checkBoxListHolder.setSize(String.valueOf(getOffsetWidth()), "200");
                    checkBoxListHolder.show();

                }

             collapse();

        }
    });


}

   private  Grid<ComboBoxModel> getCheckBoxGrid()
   {

       ListStore<ComboBoxModel> gridStore = new ListStore<ComboBoxModel>(props.key());


      for(int i=0;i<listStore.size();i++)
       {
           gridStore.add(new ComboBoxModel(listStore.get(i).getKey(),listStore.get(i).getValue(),false));
       }
      gridStore.commitChanges();
       List<ColumnConfig<ComboBoxModel,?>> configs =  getColumnConfig();
       ColumnModel<ComboBoxModel> cm = new ColumnModel<ComboBoxModel>(configs); 

        final Grid<ComboBoxModel> grid = new Grid<ComboBoxModel>(gridStore, cm){

        };
        grid.setBorders(false);
        grid.getView().setStripeRows(false);
        grid.getView().setAutoFill(true);
        grid.getView().setColumnLines(false);
        grid.setHideHeaders(true);

       return grid;
   }
   private List<ColumnConfig<ComboBoxModel,?>> getColumnConfig(){
       List<ColumnConfig<ComboBoxModel,?>> columns = new ArrayList<ColumnConfig<ComboBoxModel,?>>();
          cc2 =new ColumnConfig<ComboBoxModel,Boolean>(props.checked(),20,"Select");
          cc2.setCell(new CheckBoxCell(){

          });
          columns.add(cc2);

       cc1 = new ColumnConfig<ComboBoxModel,String>(props.getValue(),50,"Choose Properties");
       columns.add(cc1);
       return columns;
   }


private ComboBoxModel parseCheckedValues(Grid<ComboBoxModel> grid) {

    ListStore<ComboBoxModel> list = grid.getStore();
    ComboBoxModel m = new ComboBoxModel();
    String buf="";
    String keys="";
    if (list != null) {
        for(int i=0;i<list.size();i++){
            if(list.get(i).getChecked().booleanValue())
            {
                buf=buf+list.get(i).getValue();
                buf=buf+",";
                keys=keys+list.get(i).getKey();
                keys=keys+",";
            }
        }
        if (buf.length() > 0 && buf.charAt(buf.length()-1)==',') {
            buf = buf.substring(0, buf.length()-1);
            }
        if (keys.length() > 0 && keys.charAt(keys.length()-1)==',') {
            keys = keys.substring(0, keys.length()-1);
            }

    }
    m.setKey(keys);
    m.setValue(buf);
    return m;
}

public JsArrayString getSelectedItems() {
    JsArrayString result = (JsArrayString) JsArrayString.createArray();
    ListStore<ComboBoxModel> store=checkBoxGrid.getStore();
    if (store != null){
        for(int i=0;i<store.size();i++){
            if(store.get(i).getChecked().booleanValue())
            {
                result.push(store.get(i).getKey());
            }
        }

    }
    return result;
}

 public List<ComboBoxModel> getSelectedItemCombos() {
    List<ComboBoxModel> list = new ArrayList<ComboBoxModel>();
      ListStore<ComboBoxModel> store=checkBoxGrid.getStore();
    if(store!=null){
        for(int i=0;i<store.size();i++){
            if(store.get(i).getChecked().booleanValue())
            {
                list.add(store.get(i));
            }
        }

    }
    return list;        
   } 
public void setCheckedItems(List<ComboBoxModel> list){

    ListStore<ComboBoxModel> liststore = checkBoxGrid.getStore();
    for(int i=0;i<liststore.size();i++)
    {
        for(int j=0;j<list.size();j++)
        {
            if(checkBoxGrid.getStore().get(i).getKey().equals(list.get(j).getKey()) && checkBoxGrid.getStore().get(i).getValue().equals(list.get(j).getValue()))
            {
                checkBoxGrid.getStore().get(i).setChecked(true);
                break;
            }
            else
                checkBoxGrid.getStore().get(i).setChecked(false);
        }
    }
       checkBoxGrid.getStore().commitChanges();
       setValue(parseCheckedValues(checkBoxGrid));
}




public void clearCheckedItems(){

    for (int i =0; i< checkBoxGrid.getStore().size(); i++){
         checkBoxGrid.getStore().get(i).setChecked(false);
    }

}
private ArrayList<ComboBoxModel> getSelectedValues(ComboBoxModel model)
{
    ArrayList<ComboBoxModel> list = new ArrayList<ComboBoxModel>();
    String [] values=model.getValue().split(",");
    String [] keys = model.getKey().split(",");
    int i=0;
    int len=values.length;
    for(i=0;i<len;i++)
    {
        list.add(new ComboBoxModel(keys[i],values[i],true));
    }


    return list;
}
private void updateGrid(ComboBoxModel model)
{
    String [] values=model.getValue().split(",");
    String [] keys = model.getKey().split(",");
    int i=0;
    int len=values.length;
    ListStore<ComboBoxModel> list = checkBoxGrid.getStore();

for(i=0;i<list.size();i++)
{

    for(int j=0;j<len;j++)
    {
        if(checkBoxGrid.getStore().get(i).getKey().equals(keys[j]) && checkBoxGrid.getStore().get(i).getValue().equals(values[j]))
        {
            checkBoxGrid.getStore().get(i).setChecked(true);
            break;
        }
        else
            checkBoxGrid.getStore().get(i).setChecked(false);
    }

}
checkBoxGrid.getStore().commitChanges();

}

public boolean isReadOnly() {
    return readOnly;
}

public void setReadOnly(boolean readOnly) {
    this.readOnly = readOnly;
}

public ListStore<ComboBoxModel> getListStore() {
    return listStore;
}

public void setListStore(ListStore<ComboBoxModel> listStore) {
    this.listStore = listStore;
}
private void updateGridStore(ListStore<ComboBoxModel> store)
{
     checkBoxGrid.getStore().clear();
    for(int i=0;i<store.size();i++)
       {
           checkBoxGrid.getStore().add(newComboBoxModel(listStore.get(i).getKey(),listStore.get(i).getValue(),false));
       }
    checkBoxGrid.getStore().commitChanges();
}
}

OTHER TIPS

I have implemented MultiSelectComboBox with almost all functions for GXT 3.X Versions.

Here is my code:

import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.cell.client.Cell;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.HasKeyDownHandlers;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.safecss.shared.SafeStyles;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Image;
import com.sencha.gxt.core.client.IdentityValueProvider;
import com.sencha.gxt.core.client.Style;
import com.sencha.gxt.core.client.dom.XElement;
import com.sencha.gxt.data.shared.LabelProvider;
import com.sencha.gxt.data.shared.ListStore;
import com.sencha.gxt.data.shared.ModelKeyProvider;
import com.sencha.gxt.widget.core.client.event.RowClickEvent;
import com.sencha.gxt.widget.core.client.event.RowMouseDownEvent;
import com.sencha.gxt.widget.core.client.form.TextField;
import com.sencha.gxt.widget.core.client.grid.CheckBoxSelectionModel;
import com.sencha.gxt.widget.core.client.grid.ColumnConfig;
import com.sencha.gxt.widget.core.client.grid.ColumnModel;
import com.sencha.gxt.widget.core.client.grid.Grid;
import com.sencha.gxt.widget.core.client.menu.Menu;
import com.sencha.gxt.widget.core.client.selection.SelectionChangedEvent;
import com.sencha.gxt.widget.core.client.tips.QuickTip;
import java.util.ArrayList;
import java.util.List;

public class MultiSelectComboBox<T> extends HorizontalPanel implements HasKeyDownHandlers {

    private static final Icons ICONS = GWT.create(Icons.class);
    private static final int SINGLE_ENTRY_HEIGHT = 22;
    private static final int IMAGE_WIDTH = 17;

    private TextField textField;
    private Menu menu;
    private Grid<SimpleValue> grid;
    private ModelKeyProvider<T> keyProvider;
    private LabelProvider<T> labelProvider;
    private List<T> values;
    private ListStore<SimpleValue> store;
    private GridClickHandler clickHandler;
    private CheckBoxSelectionModel<SimpleValue> sm;
    private String noSelectionLabel;
    private int listWidth;
    private int width;

    private MultiSelectComboBox(final ListStore<SimpleValue> store, ModelKeyProvider<T> keyProvider, LabelProvider<T> labelProvider, List<T> values, String noSelectionLabel, int width, int listWidth) {
        this.store = store;
        this.keyProvider = keyProvider;
        this.labelProvider = labelProvider;
        this.values = values;
        this.noSelectionLabel = noSelectionLabel;
        this.width = width;
        this.listWidth = listWidth;
        init();
    }

    public List<T> getValues() {
        List<T> selectedItems = new ArrayList<>();
        for (SimpleValue data : sm.getSelectedItems()) {
            selectedItems.add(findRealValueInternal(data.getKey()));
        }
        return selectedItems;
    }

    public void setValue(final T value) {
        setValues(new ArrayList<T>() {{add(value);}});
    }

    public void setValues(List<T> values) {
        for (T value : values) {
            boolean alreadyExists = findRealValueInternal(keyProvider.getKey(value)) != null;
            if (!alreadyExists) {
                values.add(value);
                grid.getStore().add(SimpleValue.create(value, keyProvider, labelProvider));
            }
        }
        for (T value : values) {
            SimpleValue simpleValue = findSimpleValueInternal(keyProvider.getKey(value));
            if (!sm.isSelected(simpleValue)) {
                sm.select(simpleValue, true);
            }
        }
    }

    public void addAll(List<T> list) {
        for (T v : list) {
            add(v);
        }
        grid.setHeight(values.size() * SINGLE_ENTRY_HEIGHT);
    }

    public void add(T value) {
        boolean alreadyExists = findRealValueInternal(keyProvider.getKey(value))  != null;
        if (!alreadyExists) {
            values.add(value);
            grid.getStore().add(SimpleValue.create(value, keyProvider, labelProvider));
        }
    }

    public void clearStore() {
        values.clear();
        grid.getStore().clear();
    }

    private SimpleValue findSimpleValueInternal(String key) {
        SimpleValue value = null;
        for (SimpleValue v : grid.getStore().getAll()) {
            if (v.getKey().equals(key)) {
                value = v;
                break;
            }
        }
        return value;
    }

    public boolean isValid() {
        return textField.isValid();
    }

    @SuppressWarnings("GWTStyleCheck")
    private void init() {
        menu = new Menu();

        initGrid();
        menu.add(grid);
        textField = new TextField();
        textField.setReadOnly(true);
        textField.setValue(noSelectionLabel);
        textField.setWidth(width);
        textField.addStyleName("multiComboTextFieldStyle");

        textField.addKeyDownHandler(new KeyDownHandler() {
            @Override
            public void onKeyDown(KeyDownEvent event) {
                if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
                    textField.finishEditing();
                    textField.sinkEvents(KeyCodes.KEY_ENTER);
                }
            }
        });

        Image image = new Image(ICONS.arrowDown());
        image.addStyleName("multiComboButtonStyle");
        image.addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent clickEvent) {
                menu.showAt(textField.getAbsoluteLeft(), textField.getAbsoluteTop() + textField.getOffsetHeight());
            }
        });

        add(textField);
        add(image);
    }

    @SuppressWarnings({"unchecked", "GWTStyleCheck"})
    private void initGrid() {
        IdentityValueProvider<SimpleValue> identity = new IdentityValueProvider<>();
        sm = new CheckBoxSelectionModel<SimpleValue>(identity) {
            @Override
            protected void onRowClick(RowClickEvent event) {}

            @Override
            protected void onRowMouseDown(RowMouseDownEvent event) {
                boolean left = event.getEvent().getButton() == Event.BUTTON_LEFT;
                XElement target = event.getEvent().getEventTarget().cast();
                if (left && this.getAppearance().isRowChecker(target)) {
                    controlSelection(listStore.get(event.getRowIndex()));
                }
            }
        };
        sm.setSelectionMode(Style.SelectionMode.MULTI);
        sm.addSelectionChangedHandler(new SelectionChangedEvent.SelectionChangedHandler<SimpleValue>() {
            @Override
            public void onSelectionChanged(SelectionChangedEvent<SimpleValue> simpleValueSelectionChangedEvent) {
                setValuesToTextField();
            }
        });

        ColumnModel columnModel = new ColumnModel(getConfigs());
        grid = new Grid<>(store, columnModel, new ZGridView<SimpleValue>());
        grid.setColumnReordering(true);
        grid.getView().setColumnLines(true);
        grid.setSelectionModel(sm);
        grid.setWidth(listWidth);
        grid.setHeight(values.size() * SINGLE_ENTRY_HEIGHT);
        grid.setHideHeaders(true);
        grid.getView().setColumnLines(false);
        grid.addStyleName("z-grid-style");

        new QuickTip(grid);

        clickHandler = new GridClickHandler() {
            @Override
            public void onClick(Cell.Context context, Object value) {
                controlSelection((SimpleValue)((Object[])value)[0]);
            }
        };
    }

    @SuppressWarnings("unchecked")
    private List<ColumnConfig> getConfigs() {
        List<ColumnConfig> configs = new ArrayList<>();

        ColumnConfig<SimpleValue, String> columnConfig = new ColumnConfig(new ZEmptyValueProvider(), listWidth - sm.getColumn().getWidth());
        columnConfig.setCell(new AbstractCell<String>("click") {
            @Override
            public void render(Context context, String value, SafeHtmlBuilder sb) {
                String htmlValue = store.get(context.getIndex()).getLabel();
                sb.append(SafeHtmlUtils.fromTrustedString(htmlValue != null ? "<div style=\"font-size: 12px;\">" + htmlValue + "</div>": ""));
            }

            @Override
            public void onBrowserEvent(Context context, Element parent, String value, NativeEvent event, ValueUpdater<String> valueUpdater) {
                super.onBrowserEvent(context, parent, value, event, valueUpdater);
                if (("click").equals(event.getType())) {
                    if (clickHandler != null) {
                        Object[] objects = new Object[1];
                        objects[0] = store.get(context.getIndex());
                        clickHandler.onClick(context, objects);
                    }
                }
            }
        });
        columnConfig.setColumnStyle(new SafeStyles() {
            @Override
            public String asString() {
                return "cursor: pointer;";
            }
        });
        columnConfig.setColumnStyle(new SafeStyles() {
            @Override
            public String asString() {
                return "border: none;";
            }
        });
        configs.add(sm.getColumn());
        configs.add(columnConfig);

        return configs;
    }

    private void controlSelection(SimpleValue model) {
        if (model != null) {
            if (sm.isSelected(model)) {
                sm.deselect(model);
            } else {
                sm.select(model, true);
            }
        }
    }

    private void setValuesToTextField() {
        if (sm.getSelectedItems().size() > 0) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < sm.getSelectedItems().size(); i++) {
                sb.append(sm.getSelectedItems().get(i).getLabel());
                if (i != sm.getSelectedItems().size() - 1) {
                    sb.append(", ");
                }
            }
            textField.setValue(sb.toString());
            textField.setToolTip(sb.toString());
        } else {
            textField.setValue(noSelectionLabel);
            textField.removeToolTip();
        }
    }

    private T findRealValueInternal(String key) {
        T value = null;
        for (T v : values) {
            if (key.equals(keyProvider.getKey(v))) {
                value = v;
                break;
            }
        }
        return value;
    }

    @Override
    public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
        return addDomHandler(handler, KeyDownEvent.getType());
    }

    public void setEnabled(boolean enabled) {
        textField.setEnabled(enabled);
    }

    public void setToolTip(String tooltip) {
        textField.setToolTip(tooltip);
    }

    public static class Builder<T> {
        private ModelKeyProvider<T> keyProvider;
        private LabelProvider<T> labelProvider;
        private String noSelectionLabel;
        private List<T> values = new ArrayList<>();
        private int listWidth;
        private int width = 130;

        public Builder<T> keyProvider(ModelKeyProvider<T> keyProvider) {
            this.keyProvider = keyProvider;
            return this;
        }

        public Builder labelProvider(LabelProvider<T> labelProvider) {
            this.labelProvider = labelProvider;
            return this;
        }

        public Builder noSelectionLabel(String noSelectionLabel) {
            this.noSelectionLabel = noSelectionLabel;
            return this;
        }

        public Builder values(List<T> values) {
            this.values = values;
            return this;
        }

        public Builder listWidth(int listWidth) {
            this.listWidth = listWidth;
            return this;
        }

        public Builder width(int width) {
            this.width = width;
            return this;
        }

        @SuppressWarnings("unchecked")
        public MultiSelectComboBox build() {
            if (keyProvider == null) {
                throw new IllegalStateException("KeyProvider is required");
            }
            if (labelProvider == null) {
                throw new IllegalStateException("LabelProvider is required");
            }
            if (noSelectionLabel == null) {
                noSelectionLabel = "";
            }
            ListStore<SimpleValue> store = new ListStore<>(new ModelKeyProvider<SimpleValue>() {
                @Override
                public String getKey(SimpleValue item) {
                    return item.getKey();
                }
            });
            if (values.size() > 0) {
                for (T obj : values) {
                    store.add(SimpleValue.create(obj, keyProvider, labelProvider));
                }
            }
            return new MultiSelectComboBox(store, keyProvider, labelProvider, values, noSelectionLabel, width - IMAGE_WIDTH, listWidth == 0 ? width : listWidth - IMAGE_WIDTH);
        }
    }

    public static class SimpleValue {
        private String key;
        private String label;

        public SimpleValue() {}

        public static <T> SimpleValue create(T obj, ModelKeyProvider<T> keyProvider, LabelProvider<T> labelProvider) {
            SimpleValue v = new SimpleValue();
            v.setKey(keyProvider.getKey(obj));
            v.setLabel(labelProvider.getLabel(obj));
            return v;
        }

        public String getKey() {
            return key;
        }

        public void setKey(String key) {
            this.key = key;
        }

        public String getLabel() {
            return label;
        }

        public void setLabel(String label) {
            this.label = label;
        }
    }
}

Usage:

MultiSelectComboBox<String> combo = new MultiSelectComboBox.Builder<String>()
            .values(Arrays.asList(Enum.values()))
            .noSelectionLabel("All Values")
            .width(150)
            .listWidth(180)
            .keyProvider(new ModelKeyProvider<String>() {
                @Override
                public String getKey(String s) {
                    return s;
                }
            })
            .labelProvider(new LabelProvider<String>() {
                @Override
                public String getLabel(String s) {
                    return s;
                }
            })
            .build();

FWIW I implemented something for a toolbar.

MultiSelectButton is a TextButton with a menu full of check boxes and a summary is placed in the button label (EG: "Selected: 4/6").

I have implemented my own version starting from the David Lekishvili's version and trying to improve it on some aspects (tested with GXT-3.1.4 and GWT-2.7.0).

Here is the code :

MultiSelectComboBox.java

import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.cell.client.Cell;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.safecss.shared.SafeStyles;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.sencha.gxt.core.client.IdentityValueProvider;
import com.sencha.gxt.core.client.Style;
import com.sencha.gxt.core.client.dom.XElement;
import com.sencha.gxt.data.shared.LabelProvider;
import com.sencha.gxt.data.shared.ListStore;
import com.sencha.gxt.data.shared.ModelKeyProvider;
import com.sencha.gxt.data.shared.Store;
import com.sencha.gxt.widget.core.client.event.FocusEvent;
import com.sencha.gxt.widget.core.client.event.RowClickEvent;
import com.sencha.gxt.widget.core.client.event.RowMouseDownEvent;
import com.sencha.gxt.widget.core.client.event.TriggerClickEvent;
import com.sencha.gxt.widget.core.client.form.CheckBox;
import com.sencha.gxt.widget.core.client.form.TextField;
import com.sencha.gxt.widget.core.client.grid.*;
import com.sencha.gxt.widget.core.client.menu.Menu;
import com.sencha.gxt.widget.core.client.selection.SelectionChangedEvent;
import com.sencha.gxt.widget.core.client.tips.QuickTip;

import java.util.ArrayList;
import java.util.List;

/**
 * Custom GXT component allowing selection of several elements in a combobox along with filtering of values
 * Custom rendering of values can be defined by providing a custom AbstractCell
 */
public class MultiSelectComboBox<T> extends HorizontalPanel {

    /**
     * Number of values that will be displayed in the drop down list before adding a vertical scrollbar
     */
    private static final int NB_VALUES_WITHOUT_SCROLL = 15;

    private static final int SINGLE_ENTRY_HEIGHT = 22;

    /**
     * Field displaying label of every checked values
     */
    private MultiComboBoxTriggerField selectedItemsField = new MultiComboBoxTriggerField();

    /**
     * Menu contains the "select all checkbox" and the filtering text field
     */
    private Menu menu = new Menu();

    /**
     * Grid with only one column containing the combobox elements
     */
    private Grid<T> grid;

    private CheckBox selectAllCheckBox = new CheckBox();

    private ModelKeyProvider<T> keyProvider;

    private LabelProvider<T> labelProvider;

    private CheckBoxSelectionModel<T> sm;

    private ColumnConfig<T, String> columnConfig;

    /**
     * Cell used for rendering of values in the combobox drop down list
     */
    private AbstractCell cell;

    /**
     * Label to display in case no element is selected in the drop down list
     */
    private String noSelectionLabel = new String("");

    /**
     * Combobox field default width
     */
    private int selectedFieldWidth = 150;

    /**
     * Combobox drop down list default width
     */
    private int listWidth = 185;

    /**
     * This default cell is the one used in case no custom cell has been specified in constructor
     */
    private AbstractCell defaultCell = new AbstractCell() {
        @Override
        public void render(Cell.Context context, Object value, SafeHtmlBuilder sb) {
            String label = labelProvider.getLabel(grid.getStore().get(context.getIndex()));
            sb.append(SafeHtmlUtils.fromTrustedString(label != null ? "<div style=\"font-size: 12px;\">" + label + "</div>" : ""));
        }
    };

    /**
     * Default constructor
     *
     * @param keyProvider
     * @param labelProvider
     */
    public MultiSelectComboBox(ModelKeyProvider<T> keyProvider, LabelProvider<T> labelProvider) {
        this.keyProvider = keyProvider;
        this.labelProvider = labelProvider;

        cell = defaultCell;

        init();
    }

    /**
     * Constructor with a custom width for the drop down list
     *
     * @param keyProvider
     * @param labelProvider
     */
    public MultiSelectComboBox(ModelKeyProvider<T> keyProvider, LabelProvider<T> labelProvider, int listWidth) {
        this.keyProvider = keyProvider;
        this.labelProvider = labelProvider;
        this.listWidth = listWidth;

        cell = defaultCell;

        init();
    }

    /**
     * Constructor with a custom cell defined for values rendering
     *
     * @param keyProvider
     * @param labelProvider
     * @param customCell
     */
    public MultiSelectComboBox(ModelKeyProvider<T> keyProvider, LabelProvider<T> labelProvider, AbstractCell customCell) {
        this.keyProvider = keyProvider;
        this.labelProvider = labelProvider;

        cell = customCell;

        init();
    }

    private void init() {
        StyleResources.INSTANCE.multiSelectComboStyle().ensureInjected();

        initGrid();

        // Filter field
        final TextField filterField = new TextField();
        filterField.setEmptyText(IpWebParameters.INSTANCE.filterLabel());

        final Store.StoreFilter<T> filter = new Store.StoreFilter<T>() {
            @Override
            public boolean select(Store<T> store, T parent, T item) {
                return labelProvider.getLabel(item).toUpperCase().contains(filterField.getText().toUpperCase());
            }
        };

        // Filter values when the user fill in the filter field
        filterField.addKeyUpHandler(new KeyUpHandler() {
            @Override
            public void onKeyUp(KeyUpEvent event) {
                if (filterField.getText().length() > 1) {
                    grid.getStore().removeFilters();
                    grid.getStore().addFilter(filter);
                    grid.getStore().setEnableFilters(true);
                } else {
                    grid.getStore().removeFilters();
                }
            }
        });

        selectAllCheckBox.setStyleName(StyleResources.INSTANCE.multiSelectComboStyle().selectAllCheckbox());
        selectAllCheckBox.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
            @Override
            public void onValueChange(ValueChangeEvent<Boolean> event) {
                if (sm.isSelectAllChecked()) {
                    sm.setSelectAllChecked(false);
                } else {
                    sm.setSelectAllChecked(true);
                }
            }
        });

        HorizontalPanel filterPanel = new HorizontalPanel();
        filterPanel.add(selectAllCheckBox);
        filterPanel.add(filterField);

        // Menu contains filter fields and checkable items
        menu.add(filterPanel);
        menu.add(grid);

        selectedItemsField.addStyleName(StyleResources.INSTANCE.multiSelectComboStyle().multiComboTextField());
//        selectedItemsField.addStyleOnOver(selectedItemsField.getElement(), StyleResources.INSTANCE.multiSelectComboStyle().multiComboTextField());

        selectedItemsField.setReadOnly(false);
        selectedItemsField.setValue(noSelectionLabel);
        selectedItemsField.setWidth(selectedFieldWidth);

        selectedItemsField.addTriggerClickHandler(new TriggerClickEvent.TriggerClickHandler() {
            @Override
            public void onTriggerClick(TriggerClickEvent event) {
                menu.showAt(selectedItemsField.getAbsoluteLeft(), selectedItemsField.getAbsoluteTop() + selectedItemsField.getOffsetHeight());
            }
        });

        selectedItemsField.addFocusHandler(new FocusEvent.FocusHandler() {
            @Override
            public void onFocus(FocusEvent event) {
                menu.showAt(selectedItemsField.getAbsoluteLeft(), selectedItemsField.getAbsoluteTop() + selectedItemsField.getOffsetHeight());
            }
        });

        add(selectedItemsField);
    }

    /**
     * Creation of the grid object required for rendering of values in the drop down list
     */
    private void initGrid() {
        IdentityValueProvider<T> identity = new IdentityValueProvider<>();

        // Check elements when clicking on the checkbox or on the row
        sm = new CheckBoxSelectionModel<T>(identity) {
            @Override
            protected void onRowClick(RowClickEvent event) {
                boolean left = event.getEvent().getButton() == Event.BUTTON_LEFT;
                XElement target = event.getEvent().getEventTarget().cast();
                if (left && !this.getAppearance().isRowChecker(target)) {
                    controlSelection(listStore.get(event.getRowIndex()));
                }
            }

            @Override
            protected void onRowMouseDown(RowMouseDownEvent event) {
                boolean left = event.getEvent().getButton() == Event.BUTTON_LEFT;
                XElement target = event.getEvent().getEventTarget().cast();
                if (left && this.getAppearance().isRowChecker(target)) {
                    controlSelection(listStore.get(event.getRowIndex()));
                }
            }
        };

        // allow the user to select multiple values
        sm.setSelectionMode(Style.SelectionMode.MULTI);

        // Manage selectAll check box depending on already selected values
        sm.addSelectionChangedHandler(new SelectionChangedEvent.SelectionChangedHandler<T>() {
            @Override
            public void onSelectionChanged(SelectionChangedEvent<T> simpleValueSelectionChangedEvent) {
                // automatically check or uncheck the "select all checkbox" depending on current user selection
                if (selectAllCheckBox.getValue() && !isAllSelected()) {
                    selectAllCheckBox.setValue(false);
                } else if (!selectAllCheckBox.getValue() && isAllSelected()) {
                    selectAllCheckBox.setValue(true);
                }

                setValuesToTextField();
            }
        });


        ColumnModel columnModel = new ColumnModel(getColumnConfig());
        ListStore<T> store = new ListStore<>(keyProvider);
        grid = new Grid<>(store, columnModel, new GridView<T>());
        grid.setSelectionModel(sm);
        grid.setAllowTextSelection(false);
        grid.setHideHeaders(true);
        grid.setBorders(false);

        // Define grid view properties
        grid.getView().setColumnLines(false);
        grid.getView().setAdjustForHScroll(false);
        grid.getView().setTrackMouseOver(false);

        refreshGridHeight();

        new QuickTip(grid);
    }

    /**
     * @return the list of currently selected items
     */
    public List<T> getSelectedItems() {
        return sm.getSelectedItems();
    }

    /**
     * Add a list of checkable item in the drop down list
     *
     * @param list
     */
    public void addAll(List<T> list) {
        for (T v : list) {
            add(v);
        }
    }

    /**
     * Add a checkable item in the drop down list
     *
     * @param item the item to add
     */
    public void add(T item) {
        boolean alreadyExists = false;
        if (!alreadyExists) {
            grid.getStore().add(item);
        }

        refreshGridHeight();
        refreshColumnWidth();
    }

    /**
     * @return all items contained in the store
     */
    public List<T> getAllItems() {
        return grid.getStore().getAll();
    }

    public void clearStore() {
        grid.getStore().clear();
    }

    private List<ColumnConfig> getColumnConfig() {
        List<ColumnConfig> configs = new ArrayList<>();

        // Width depends on the presence of a scrollbar
        columnConfig = new ColumnConfig(new IdentityValueProvider<T>());

        columnConfig.setCell(cell);

        columnConfig.setColumnStyle(new SafeStyles() {
            @Override
            public String asString() {
                return "border: none;";
            }
        });

        configs.add(sm.getColumn());
        configs.add(columnConfig);

        return configs;
    }

    /**
     * Adjust drop down list height depending on the number of elements in the list. A scrollbar is added if this number exceeds NB_VALUES_WITHOUT_SCROLL
     */
    private void refreshGridHeight() {
        if (getAllItems().size() > NB_VALUES_WITHOUT_SCROLL) {
            grid.setHeight(NB_VALUES_WITHOUT_SCROLL * SINGLE_ENTRY_HEIGHT);
        } else {
            grid.setHeight(getAllItems().size() * SINGLE_ENTRY_HEIGHT);
        }
    }

    /**
     * Refresh width of columns. Width depends on the presence of a scrollbar
     */
    private void refreshColumnWidth() {
        int columnWidth = listWidth - sm.getColumn().getWidth() - 25;
        if (getAllItems().size() < NB_VALUES_WITHOUT_SCROLL) {
            columnWidth = listWidth - sm.getColumn().getWidth();
        }
        columnConfig.setWidth(columnWidth);
    }

    /**
     * Switch the selection status of the given item
     *
     * @param item
     */
    private void controlSelection(T item) {
        if (item != null) {
            if (sm.isSelected(item)) {
                sm.deselect(item);
            } else {
                sm.select(item, true);
            }
        }
    }

    /**
     * Manage the label to display in the "selected item field". Label of each selected values is displayed or "All" if all elements are selected
     */
    private void setValuesToTextField() {
        if (sm.getSelectedItems().size() > 0) {
            if (isAllSelected()) {
                selectedItemsField.setValue(IpWebParameters.INSTANCE.all());
                selectedItemsField.setToolTip(IpWebParameters.INSTANCE.all());
            } else {
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < sm.getSelectedItems().size(); i++) {
                    sb.append(labelProvider.getLabel(sm.getSelectedItems().get(i)));
                    if (i != sm.getSelectedItems().size() - 1) {
                        sb.append(", ");
                    }
                }
                selectedItemsField.setValue(sb.toString());
                selectedItemsField.setToolTip(sb.toString());
            }
        } else {
            selectedItemsField.setValue(noSelectionLabel);
            selectedItemsField.removeToolTip();
        }
    }

    public void setEnabled(boolean enabled) {
        selectedItemsField.setEnabled(enabled);
    }

    /**
     * Set the matching model item as checked
     *
     * @param item the item to check
     */
    public void select(T item) {
        if (!sm.isSelected(item)) {
            sm.select(true, item);
        }
    }

    /**
     * Set the matching model items as checked
     *
     * @param items the item to check
     */
    public void select(List<T> items) {
        for (T item : items) {
            this.select(item);
        }
    }

    /**
     * Check all items in the drop down list
     */
    public void selectAll() {
        for (T item : getAllItems()) {
            this.select(item);
        }
    }

    /**
     * deselect all items in the drop down list
     */
    public void deselectAll() {
        sm.deselectAll();
    }

    /**
     * @return true if all items are selected
     */
    public boolean isAllSelected() {
        return sm.getSelection().size() == getAllItems().size();
    }

    public interface ComboStyle extends CssResource {
        String multiComboTextField();

        String selectAllCheckbox();
    }

    public interface StyleResources extends ClientBundle {
        StyleResources INSTANCE = GWT.create(StyleResources.class);

        @Source("multiselectcombo.css")
        ComboStyle multiSelectComboStyle();
    }

}

MultiComboBoxTriggerField.java

import com.google.gwt.event.shared.HandlerRegistration;
import com.sencha.gxt.cell.core.client.form.TriggerFieldCell;
import com.sencha.gxt.widget.core.client.event.CollapseEvent;
import com.sencha.gxt.widget.core.client.event.ExpandEvent;
import com.sencha.gxt.widget.core.client.form.PropertyEditor;
import com.sencha.gxt.widget.core.client.form.TriggerField;

/**
 * Trigger Field for multi select combo box
 */
public class MultiComboBoxTriggerField extends TriggerField<String> implements ExpandEvent.HasExpandHandlers, CollapseEvent.HasCollapseHandlers {

    public MultiComboBoxTriggerField() {
        this(new TriggerFieldCell<String>(), new MultiComboBoxPropertyEditor());
    }

    protected MultiComboBoxTriggerField(TriggerFieldCell<String> cell, PropertyEditor<String> propertyEditor) {
        super(cell, propertyEditor);
    }

    @Override
    public HandlerRegistration addExpandHandler(ExpandEvent.ExpandHandler handler) {
        return addHandler(handler, ExpandEvent.getType());
    }

    @Override
    public HandlerRegistration addCollapseHandler(CollapseEvent.CollapseHandler handler) {
        return addHandler(handler, CollapseEvent.getType());
    }
}

MultiComboBoxPropertyEditor.java

import com.sencha.gxt.widget.core.client.form.PropertyEditor;

import java.text.ParseException;

/**
 * Property editor for multi select combo box
 */
public class MultiComboBoxPropertyEditor extends PropertyEditor<String> {

    @Override
    public String parse(CharSequence text) throws ParseException {
        return text.toString();
    }

    @Override
    public String render(String object) {
        return object;
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top