org.pentaho.gwt.widgets.client.listbox.CustomListBox.java Source code

Java tutorial

Introduction

Here is the source code for org.pentaho.gwt.widgets.client.listbox.CustomListBox.java

Source

/*!
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * 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 Lesser General Public License for more details.
 *
 * Copyright (c) 2002-2013 Pentaho Corporation..  All rights reserved.
 */

package org.pentaho.gwt.widgets.client.listbox;

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

import org.pentaho.gwt.widgets.client.utils.ElementUtils;
import org.pentaho.gwt.widgets.client.utils.Rectangle;
import org.pentaho.gwt.widgets.client.utils.string.StringUtils;

import com.allen_sauer.gwt.dnd.client.DragController;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.ChangeListener;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.FocusListener;
import com.google.gwt.user.client.ui.FocusPanel;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.KeyboardListener;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.MouseListener;
import com.google.gwt.user.client.ui.PopupListener;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;

/**
 * 
 * ComplexListBox is a List-style widget can contain custom list-items made (images + text, text + checkboxes) This list
 * is displayed as a drop-down style component by default. If the visibleRowCount property is set higher than 1
 * (default), the list is rendered as a multi-line list box.
 * 
 * <P>
 * Usage:
 * 
 * <p>
 * 
 * <pre>
 * ComplexListBox list = new ComplexListBox();
 * 
 * list.addItem( &quot;Alberta&quot; );
 * list.addItem( &quot;Atlanta&quot; );
 * list.addItem( &quot;San Francisco&quot; );
 * list.addItem( new DefaultListItem( &quot;Testing&quot;, new Image( &quot;16x16sample.png&quot; ) ) );
 * list.addItem( new DefaultListItem( &quot;Testing 2&quot;, new CheckBox() ) );
 * 
 * list.setVisibleRowCount( 6 ); // turns representation from drop-down to list
 * 
 * list.addChangeListener( new ChangeListener() {
 *   public void onChange( Widget widget ) {
 *     System.out.println( &quot;&quot; + list.getSelectedIdex() );
 *   }
 * } );
 * </pre>
 * 
 * User: NBaker Date: Mar 9, 2009 Time: 11:01:57 AM
 * 
 */
@SuppressWarnings("deprecation")
public class CustomListBox extends HorizontalPanel
        implements ChangeListener, PopupListener, MouseListener, FocusListener, KeyboardListener, ListItemListener {
    protected List<ListItem> items = new ArrayList<ListItem>();
    protected int selectedIndex = -1;
    protected DropDownArrow arrow = new DropDownArrow();
    protected int visible = 1;
    private int maxDropVisible = 15;
    protected boolean editable = false;
    private VerticalPanel listPanel = new VerticalPanel();
    protected ScrollPanel listScrollPanel = new ScrollPanel();

    // Members for drop-down style
    protected FlexTable dropGrid = new FlexTable();
    protected boolean popupShowing = false;
    private DropPopupPanel popup;
    private PopupList popupVbox = new PopupList();
    protected FocusPanel fPanel = new FocusPanel();
    private ScrollPanel popupScrollPanel = new ScrollPanel();

    protected List<ChangeListener> listeners = new ArrayList<ChangeListener>();
    private final int spacing = 1;
    protected int maxHeight, maxWidth, averageHeight; // height and width of largest ListItem
    private String primaryStyleName;
    private String height, width;
    protected String popupHeight;
    private String popupWidth;
    protected boolean suppressLayout;

    private boolean enabled = true;
    private String val;
    private Command command;
    protected DragController dragController;
    protected boolean multiSelect;

    public CustomListBox() {

        dropGrid.getColumnFormatter().setWidth(0, "100%"); //$NON-NLS-1$
        dropGrid.setWidget(0, 1, arrow);
        dropGrid.getElement().getStyle().setProperty("tableLayout", "fixed");
        arrow.getElement().getParentElement().getStyle().setProperty("width", "20px");
        dropGrid.setCellPadding(0);
        dropGrid.setCellSpacing(1);
        updateUI();

        // Add List Panel to it's scrollPanel
        listScrollPanel.add(listPanel);
        listScrollPanel.setHeight("100%"); //$NON-NLS-1$
        listScrollPanel.setWidth("100%"); //$NON-NLS-1$
        listScrollPanel.getElement().getStyle().setProperty("overflowX", "hidden"); //$NON-NLS-1$ //$NON-NLS-2$
        // listScrollPanel.getElement().getStyle().setProperty("padding",spacing+"px");
        listPanel.setSpacing(spacing);
        listPanel.setWidth("100%"); //$NON-NLS-1$

        // default to drop-down
        fPanel.add(dropGrid);
        fPanel.setHeight("100%"); //$NON-NLS-1$
        super.add(fPanel);

        popupScrollPanel.add(popupVbox);
        popupScrollPanel.getElement().getStyle().setProperty("overflowX", "hidden"); //$NON-NLS-1$ //$NON-NLS-2$
        popupVbox.setWidth("100%"); //$NON-NLS-1$
        popupVbox.setSpacing(spacing);

        fPanel.addMouseListener(this);
        fPanel.addFocusListener(this);
        fPanel.addKeyboardListener(this);

        this.setStylePrimaryName("custom-list"); //$NON-NLS-1$

        setTdStyles(this.getElement());
        setTdStyles(listPanel.getElement());

        editableTextBox = new TextBox() {
            @Override
            public void onBrowserEvent(Event event) {
                // int code = event.getKeyCode();

                switch (DOM.eventGetType(event)) {
                case Event.ONKEYUP:
                    onChange(editableTextBox);
                    val = editableTextBox.getText();
                    // event.cancelBubble(true);
                    break;
                case Event.ONMOUSEUP:
                    super.onBrowserEvent(event);
                    event.cancelBubble(true);
                default:
                    return;
                }
            }
        };
        editableTextBox.setStylePrimaryName("custom-list-textbox");
    }

    public void setTableLayout(String tableLayout) {
        if (StringUtils.isEmpty(tableLayout)) {
            dropGrid.getElement().getStyle().clearProperty("tableLayout");
        } else {
            dropGrid.getElement().getStyle().setProperty("tableLayout", tableLayout);
        }
    }

    private native void setTdStyles(Element ele)/*-{
                                                var tds = ele.getElementsByTagName("td");
                                                for( var i=0; i< tds.length; i++){
                                                var td = tds[i];
                                                if(!td.style){
                                                td.className = "customListBoxTdFix";
                                                } else {
                                                td.style.padding = "0px";
                                                td.style.border = "none";
                                                }
                                                }
                                                }-*/;

    /**
     * Removes the passed in ListItem
     * 
     * @param listItem
     *          item to remove
     */
    public void remove(ListItem listItem) {
        this.items.remove(listItem);
        setSelectedIndex(0);

        if (suppressLayout == false) {
            updateUI();
        }
    }

    /**
     * Removes all items from the list.
     */
    public void removeAll() {
        this.items.clear();
        this.selectedIndex = -1;
        this.selectedItems.clear();

        if (this.suppressLayout == false) {
            for (ChangeListener l : listeners) {
                l.onChange(this);
            }
        }
        if (suppressLayout == false) {
            updateUI();
        }
    }

    /**
     * Removes all items from the list.
     */
    public void clear() {
        removeAll();
    }

    /**
     * Convenience method to support the more conventional method of child attachment
     * 
     * @param label
     */
    public void add(String label) {
        this.addItem(label);
    }

    /**
     * Adds the given ListItem to the list control.
     * 
     * @param item
     *          ListItem
     */
    public void addItem(ListItem item) {
        items.add(item);

        item.setListItemListener(this);

        // If first one added, set selectedIndex to 0
        if (items.size() == 1 && this.visible == 1) {
            setSelectedIndex(0);
        }
        if (suppressLayout == false) {
            updateUI();
        }

        if (dragController != null) {
            dragController.makeDraggable(item.getWidget());
        }
    }

    @Override
    public void add(Widget w) {
        addItem((ListItem) w);
    }

    /**
     * Call this method with true will suppress the re-laying out of the widget after every add/remove. This is useful
     * when adding a large batch of items to the listbox.
     */
    public void setSuppressLayout(boolean supress) {
        this.suppressLayout = supress;
        if (!suppressLayout) {

            if (selectedIndex < 0 && this.items.size() > 0) {
                this.setSelectedIndex(0); // notifies listeners
            } else {
                // just notify listeners something has changed.
                for (ChangeListener l : listeners) {
                    l.onChange(this);
                }
            }

            updateUI();
        }
    }

    /**
     * Convenience method creates a {@link: DefaultListItem} with the given text and adds it to the list control
     * 
     * @param label
     */
    public void addItem(String label) {
        DefaultListItem item = new DefaultListItem(label);
        items.add(item);
        item.setListItemListener(this);

        // If first one added, set selectedIndex to 0
        if (items.size() == 1) {
            setSelectedIndex(0);
        }
        if (suppressLayout == false) {
            updateUI();
        }
        if (dragController != null) {
            dragController.makeDraggable(item.getWidget());
        }
    }

    /**
     * Returns a list of current ListItems.
     * 
     * @return List of ListItems
     */
    public List<ListItem> getItems() {
        return items;
    }

    /**
     * Sets the number of items to be displayed at once in the lsit control. If set to 1 (default) the list is rendered as
     * a drop-down
     * 
     * @param visibleCount
     *          number of rows to be visible.
     */
    public void setVisibleRowCount(int visibleCount) {
        int prevCount = visible;
        this.visible = visibleCount;

        if (visible > 1 && prevCount == 1) {
            // switched from drop-down to list
            fPanel.remove(dropGrid);
            fPanel.add(listScrollPanel);
        } else if (visible == 1 && prevCount > 1) {
            // switched from list to drop-down
            fPanel.remove(listScrollPanel);
            fPanel.add(dropGrid);
        }

        if (suppressLayout == false) {
            updateUI();
        }
    }

    /**
     * Returns the number of rows visible in the list
     * 
     * @return number of visible rows.
     */
    public int getVisibleRowCount() {
        return visible;
    }

    protected void updateUI() {
        if (!this.isAttached()) {
            return;
        }
        if (visible > 1) {
            updateList();
        } else {
            updateDropDown();
        }
    }

    /**
     * Returns the number of rows to be displayed in the drop-down popup.
     * 
     * @return number of visible popup items.
     */
    public int getMaxDropVisible() {
        return maxDropVisible;
    }

    /**
     * Sets the number of items to be visible in the drop-down popup. If set lower than the number of items a scroll-bar
     * with provide access to hidden items.
     * 
     * @param maxDropVisible
     *          number of items visible in popup.
     */
    public void setMaxDropVisible(int maxDropVisible) {
        this.maxDropVisible = maxDropVisible;

        // Update the popup to respect this value
        if (maxHeight > 0) { // Items already added
            this.popupHeight = this.maxDropVisible * maxHeight + "px"; //$NON-NLS-1$
        }
    }

    protected TextBox editableTextBox;
    private SimplePanel selectedItemWrapper = new SimplePanel();

    protected void updateSelectedDropWidget() {
        Widget selectedWidget = new Label(""); //Default to show in case of empty sets? //$NON-NLS-1$
        boolean updateMade = true;
        if (editable == false) { // only show their widget if editable is false
            if (selectedIndex >= 0) {
                selectedWidget = items.get(selectedIndex).getWidgetForDropdown();
            } else if (items.size() > 0) {
                selectedWidget = items.get(0).getWidgetForDropdown();
            }
        } else {
            String previousVal = editableTextBox.getText();
            String newVal = "";

            if (this.val != null) {
                newVal = this.val;
            } else if (selectedIndex >= 0) {
                newVal = items.get(selectedIndex).getValue().toString();
            } else if (items.size() > 0) {
                newVal = items.get(0).getValue().toString();
            }

            if (previousVal.equals(newVal) == false) {
                editableTextBox.setText(newVal);
            }
            if (previousVal != null && previousVal.equals(newVal)) {
                updateMade = false;
            }
            editableTextBox.setWidth("100%"); //$NON-NLS-1$
            editableTextBox.sinkEvents(Event.KEYEVENTS);
            editableTextBox.sinkEvents(Event.MOUSEEVENTS);
            selectedWidget = editableTextBox;

        }
        this.setTdStyles(selectedWidget.getElement());
        // selectedItemWrapper.getElement().getStyle().setProperty("overflow", "hidden"); //$NON-NLS-1$ //$NON-NLS-2$
        selectedItemWrapper.clear();
        selectedItemWrapper.add(selectedWidget);
        dropGrid.setWidget(0, 0, selectedItemWrapper);
        if (editable && updateMade) {
            editableTextBox.setFocus(true);
            editableTextBox.selectAll();
        }
    }

    /**
     * Called by updateUI when the list is not a drop-down (visible row count > 1)
     */
    private void updateList() {
        listPanel.clear();
        maxHeight = 0;
        maxWidth = 0;

        // actually going to average up the heights
        for (ListItem li : this.items) {
            Widget w = li.getWidget();

            Rectangle rect = ElementUtils.getSize(w.getElement());
            // we only care about this if the user hasn't specified a height.
            if (height == null) {
                maxHeight += rect.height;
            }
            maxWidth = Math.max(maxWidth, rect.width);

            // Add it to the dropdown
            listPanel.add(w);
            listPanel.setCellWidth(w, "100%"); //$NON-NLS-1$
        }
        if (height == null && this.items.size() > 0) {
            maxHeight = Math.round(maxHeight / this.items.size());
        }

        // we only care about this if the user has specified a visible row count and no heihgt
        if (height == null) {
            int h = (this.visible * (maxHeight + spacing));
            this.listScrollPanel.setHeight(h + "px"); //$NON-NLS-1$
            fPanel.setHeight(h + "px");
        } else {
            this.listScrollPanel.setHeight(height);
            fPanel.setHeight(height);
        }
        if (width == null) {
            this.fPanel.setWidth(maxWidth + 40 + "px"); //20 is scrollbar space //$NON-NLS-1$
        }

    }

    /**
     * Called by updateUI when the list is a drop-down (visible row count = 1)
     */
    private void updateDropDown() {

        // Update Shown selection in grid
        updateSelectedDropWidget();

        // Update popup panel,
        // Calculate the size of the largest list item.
        popupVbox.clear();
        maxWidth = 0;
        averageHeight = 0; // Actually used to set the width of the arrow
        popupHeight = null;

        int totalHeight = 0;
        for (ListItem li : this.items) {
            Widget w = li.getWidget();

            Rectangle rect = ElementUtils.getSize(w.getElement());
            maxWidth = Math.max(maxWidth, rect.width);
            maxHeight = Math.max(maxHeight, rect.height);
            totalHeight += rect.height;

            // Add it to the dropdown
            popupVbox.add(w);
            popupVbox.setCellWidth(w, "100%"); //$NON-NLS-1$
        }

        // Average the height of the items
        if (items.size() > 0) {
            averageHeight = Math.round(totalHeight / items.size());
        }

        // Set the size of the drop-down based on the largest list item
        if (width == null) {
            dropGrid.setWidth((maxWidth + 60) + "px");
            this.popupWidth = maxWidth + 60 + "px"; //$NON-NLS-1$
        } else if (width.equals("100%")) { //$NON-NLS-1$
            dropGrid.setWidth("100%"); //$NON-NLS-1$
            this.popupWidth = maxWidth + (spacing * 4) + maxHeight + "px"; //$NON-NLS-1$
        } else {
            dropGrid.setWidth("100%"); //$NON-NLS-1$
            int w = -1;
            if (width.indexOf("px") > 0) { //$NON-NLS-1$
                w = Integer.parseInt(this.width.replace("px", "")); //$NON-NLS-1$ //$NON-NLS-2$
            } else if (width.indexOf("%") > 0) { //$NON-NLS-1$
                w = Integer.parseInt(this.width.replace("%", "")); //$NON-NLS-1$ //$NON-NLS-2$
            }
            selectedItemWrapper.setWidth((w - (averageHeight + (this.spacing * 6))) + "px"); //$NON-NLS-1$

        }

        // Store the the size of the popup to respect MaxDropVisible now that we know the item height
        // This cannot be set here as the popup is not visible :(

        if (maxDropVisible > 0 && items.size() > maxDropVisible) {
            // (Lesser of maxDropVisible or items size) * (Average item height + spacing value)
            this.popupHeight = (Math.min(this.maxDropVisible, this.items.size())
                    * (averageHeight + (this.spacing * 2))) + "px"; //$NON-NLS-1$
        } else {
            this.popupHeight = null; //ElementUtils.getSize(popupVbox.getElement()).height+ "px";
        }

    }

    /**
     * Used internally to hide/show drop-down popup.
     */
    protected void togglePopup() {
        if (popupShowing == false) {

            // This delayed instantiation works around a problem with the underlying GWT widgets that
            // throw errors positioning when the GWT app is loaded in a frame that's not visible.

            if (popup == null) {
                popup = new DropPopupPanel();
                popup.addPopupListener(this);
                popup.add(popupScrollPanel);
            }

            int x = this.getElement().getAbsoluteLeft();
            int y = this.getElement().getAbsoluteTop() + this.getElement().getOffsetHeight() + 1;
            int windowH = Window.getClientHeight();
            int windowW = Window.getClientWidth();

            Rectangle popupSize = ElementUtils.getSize(popup.getElement());
            if (y + popupSize.height > windowH) {
                y = windowH - popupSize.height;
            }
            if (x + popupSize.width > windowW) {
                x = windowW - popupSize.width;
            }

            popup.setPopupPosition(x, y);

            popup.show();

            // Set the size of the popup calculated in updateDropDown().
            if (this.popupHeight != null) {
                this.popupScrollPanel.getElement().getStyle().setProperty("height", this.popupHeight); //$NON-NLS-1$
            }

            if (this.popupWidth != null) {
                String w = Math.max(this.getElement().getOffsetWidth() - 2, this.maxWidth + 10) + "px";
                this.popupScrollPanel.getElement().getStyle().setProperty("width", w); //$NON-NLS-1$ //$NON-NLS-2$
                popup.getElement().getStyle().setProperty("width", w);
            }

            scrollSelectedItemIntoView();

            popupShowing = true;
        } else {
            popup.hide();
            // fPanel.setFocus(true);
        }
    }

    protected void scrollSelectedItemIntoView() {

        // Scroll to view currently selected widget
        // DOM.scrollIntoView(this.getSelectedItem().getWidget().getElement());
        // Side effect of the previous call scrolls the scrollpanel to the right. Compensate here
        // popupScrollPanel.setHorizontalScrollPosition(0);

        if (this.visible > 1) {
            this.listScrollPanel.ensureVisible(items.get(selectedIndex).getWidget());
            return;
        }

        // if the position of the selected item is greater than the height of the scroll area plus it's scroll offset
        if (((this.selectedIndex + 1) * this.averageHeight) > popupScrollPanel.getOffsetHeight()
                + popupScrollPanel.getScrollPosition()) {
            popupScrollPanel.setScrollPosition(
                    (((this.selectedIndex) * this.averageHeight) - popupScrollPanel.getOffsetHeight())
                            + averageHeight);
            return;
        }

        // if the position of the selected item is Less than the scroll offset
        if (((this.selectedIndex) * this.averageHeight) < popupScrollPanel.getScrollPosition()) {
            popupScrollPanel.setScrollPosition(((this.selectedIndex) * this.averageHeight));
        }
    }

    /**
     * Selects the given ListItem in the list.
     * 
     * @param item
     *          ListItem to be selected.
     */
    public void setSelectedItem(ListItem item) {
        if (items.contains(item) == false) {
            throw new RuntimeException("Item not in collection"); //$NON-NLS-1$
        }
        // Clear previously selected item
        if (selectedIndex > -1) {
            items.get(selectedIndex).onDeselect();
        }
        if (visible == 1) { // Drop-down mode
            if (popupShowing) {
                togglePopup();
            }
        }
        setSelectedIndex(items.indexOf(item));

    }

    protected List<ListItem> selectedItems = new ArrayList<ListItem>();

    protected void handleSelection(ListItem item, Event evt) {
        if (!evt.getCtrlKey() && !evt.getShiftKey() && !evt.getMetaKey()) {
            for (ListItem itm : selectedItems) {
                itm.onDeselect();
            }
            if (selectedIndex > -1) {
                items.get(selectedIndex).onDeselect();
            }
            selectedItems.clear();
            item.onSelect();
            if (!selectedItems.contains(item)) {
                selectedItems.add(item);
            }
            selectedIndex = items.indexOf(item);

            scrollSelectedItemIntoView();
        } else if (evt.getShiftKey()) {
            int idxOfNewSelection = items.indexOf(item);
            int startIdx = Math.min(selectedIndex, idxOfNewSelection);
            int endIndex = Math.max(selectedIndex, idxOfNewSelection);

            for (int i = startIdx; i <= endIndex; i++) {
                if (!selectedItems.contains(items.get(i))) {
                    selectedItems.add(items.get(i));
                }
                items.get(i).onSelect();
            }

        } else if (evt.getCtrlKey() || evt.getMetaKey()) {
            if (selectedItems.remove(item)) {
                item.onDeselect();
            } else {
                item.onSelect();

                if (!selectedItems.contains(item)) {
                    selectedItems.add(item);
                }
            }
        } else {
            if (!selectedItems.contains(item)) {
                selectedItems.add(item);
            }
        }

        if (this.suppressLayout == false) {
            for (ChangeListener l : listeners) {
                l.onChange(this);
            }
        }

    }

    /**
     * Selects the ListItem at the given index (zero-based)
     * 
     * @param idx
     *          index of ListItem to select
     */
    public void setSelectedIndex(int idx) {
        if (idx > items.size()) {
            throw new RuntimeException("Index out of bounds: " + idx); //$NON-NLS-1$
        }
        // De-Select the current
        if (selectedIndex > -1) {
            items.get(selectedIndex).onDeselect();
        }

        selectedItems.clear();
        if (idx >= 0 && items.size() > idx) {
            selectedItems.add(items.get(idx));
        }

        int prevIdx = selectedIndex;
        if (idx >= 0) {
            selectedIndex = idx;
            items.get(idx).onSelect();

            this.val = null;
            if (visible == 1 && this.isAttached()) {
                scrollSelectedItemIntoView();
            }
            updateSelectedDropWidget();
        }

        if (this.suppressLayout == false && prevIdx != idx) {
            for (ChangeListener l : listeners) {
                l.onChange(this);
            }
        }
    }

    public void setSelectedIndices(int[] indices) {
        if (multiSelect == false) {
            // throw new IllegalStateException("Cannot select more than one item in a combobox");
            if (indices.length > 0) {
                setSelectedIndex(indices[0]);
            }
            return;
        }
        for (ListItem item : selectedItems) {
            item.onDeselect();
        }
        selectedItems.clear();
        for (int i = 0; i < indices.length; i++) {
            int idx = indices[i];
            if (idx >= 0 && idx < items.size()) {
                items.get(idx).onSelect();
                selectedItems.add(items.get(idx));
            }
        }

        if (this.suppressLayout == false) {
            for (ChangeListener l : listeners) {
                l.onChange(this);
            }
        }
    }

    /**
     * Registers a ChangeListener with the list.
     * 
     * @param listener
     *          ChangeListner
     */
    public void addChangeListener(ChangeListener listener) {
        listeners.add(listener);
    }

    /**
     * Removes to given ChangeListener from list.
     * 
     * @param listener
     *          ChangeListener
     */
    public void removeChangeListener(ChangeListener listener) {
        this.listeners.remove(listener);
    }

    /**
     * Returns the selected index of the list (zero-based)
     * 
     * @return Integer index
     */
    public int getSelectedIndex() {
        return selectedIndex;
    }

    /**
     * Returns the number of listitems in the box
     * 
     * @return number of children
     */
    public int getSize() {
        return this.items.size();
    }

    /**
     * Returns the currently selected item
     * 
     * @return currently selected Item
     */
    public ListItem getSelectedItem() {
        if (selectedIndex < 0 || selectedIndex > items.size()) {
            return null;
        }

        return items.get(selectedIndex);
    }

    public List<ListItem> getSelectedItems() {
        return new ArrayList<ListItem>(selectedItems);
    }

    public int[] getSelectedIndices() {
        int[] selectedIndices = new int[selectedItems.size()];
        for (int i = 0; i < selectedItems.size(); i++) {
            selectedIndices[i] = items.indexOf(selectedItems.get(i));
        }
        return selectedIndices;
    }

    @Override
    public void setStylePrimaryName(String s) {
        super.setStylePrimaryName(s);
        this.primaryStyleName = s;

        // This may have came in late. Update ListItems
        for (ListItem item : items) {
            item.setStylePrimaryName(s);
        }
    }

    @Override
    protected void onAttach() {
        super.onAttach();
        updateUI();
    }

    @Override
    /**
     * Calling setHeight will implecitly change the list from a drop-down style to a list style.
     */
    public void setHeight(String s) {
        this.height = s;
        // user has specified height, focusPanel needs to be 100%;
        this.fPanel.setHeight(s);
        this.listScrollPanel.setHeight("100%"); //$NON-NLS-1$
        if (visible == 1) {
            this.setVisibleRowCount(15);
        }
        super.setHeight(s);
        updateUI();
    }

    @Override
    public void setWidth(String s) {
        fPanel.setWidth(s);
        this.listScrollPanel.setWidth("100%"); //$NON-NLS-1$
        this.width = s;
        this.popupWidth = s;
        if (s != null) {
            dropGrid.setWidth("100%"); //$NON-NLS-1$
        }
        super.setWidth(s);
        updateUI();
    }

    // ======================================= Listener methods ===================================== //

    public void onPopupClosed(PopupPanel popupPanel, boolean b) {
        this.popupShowing = false;
    }

    public void onMouseDown(Widget widget, int i, int i1) {
    }

    public void onMouseEnter(Widget widget) {
    }

    public void onMouseLeave(Widget widget) {
    }

    public void onMouseMove(Widget widget, int i, int i1) {
    }

    public void onMouseUp(Widget widget, int i, int i1) {
        if (isEnabled() == false) {
            return;
        }
        if (visible == 1) { // drop-down mode
            this.togglePopup();
        }
    }

    public void onFocus(Widget widget) {
        if (isEnabled() == false) {
            return;
        }
        // fPanel.setFocus(true);
    }

    public void onLostFocus(Widget widget) {
    }

    private int shiftOriginIdx = -1;

    public void onKeyDown(Widget widget, char c, int i) {
        if (c == 16) { // shift
            shiftOriginIdx = selectedIndex;
        }
    }

    public void onKeyPress(Widget widget, char c, int i) {
    }

    public void onKeyUp(Widget widget, char c, int i) {
        if (isEnabled() == false) {
            return;
        }
        if (c == 16) {
            shiftOriginIdx = -1;
        }
        boolean fireEvents = false;
        switch (c) {
        case 38: // UP
            if (selectedIndex > 0) {

                if (multiSelect && !Event.getCurrentEvent().getShiftKey()) {
                    for (ListItem itm : selectedItems) {
                        itm.onDeselect();
                    }
                    selectedItems.clear();
                    ListItem itm = items.get(selectedIndex - 1);
                    selectedIndex = selectedIndex - 1;
                    itm.onSelect();
                    if (!selectedItems.contains(itm)) {
                        selectedItems.add(itm);
                    }
                    fireEvents = true;
                    this.listScrollPanel.ensureVisible(itm.getWidget());
                } else if (multiSelect && Event.getCurrentEvent().getShiftKey()) {

                    ListItem itm = items.get(selectedIndex - 1);

                    if (!selectedItems.contains(itm)) {
                        selectedItems.add(itm);
                    }
                    itm.onSelect();
                    this.listScrollPanel.ensureVisible(itm.getWidget());

                    ListItem prevItem = items.get(selectedIndex);
                    if (selectedIndex != shiftOriginIdx && shiftOriginIdx < selectedIndex
                            && selectedItems.contains(prevItem)) {
                        selectedItems.remove(prevItem);
                        prevItem.onDeselect();
                    }

                    selectedIndex = selectedIndex - 1;

                    fireEvents = true;
                } else {
                    setSelectedIndex(selectedIndex - 1);
                    scrollSelectedItemIntoView();
                }
            }

            break;
        case 40: // Down
            if (selectedIndex < items.size() - 1) {
                if (multiSelect && !Event.getCurrentEvent().getShiftKey()) {
                    for (ListItem itm : selectedItems) {
                        itm.onDeselect();
                    }
                    selectedItems.clear();
                    ListItem itm = items.get(selectedIndex + 1);
                    selectedIndex = selectedIndex + 1;
                    itm.onSelect();
                    if (!selectedItems.contains(itm)) {
                        selectedItems.add(itm);
                    }
                    fireEvents = true;
                    this.listScrollPanel.ensureVisible(itm.getWidget());
                } else if (multiSelect && Event.getCurrentEvent().getShiftKey()) {

                    ListItem itm = items.get(selectedIndex + 1);

                    if (!selectedItems.contains(itm)) {
                        selectedItems.add(itm);
                    }
                    itm.onSelect();

                    ListItem prevItem = items.get(selectedIndex);
                    if (selectedIndex != shiftOriginIdx && shiftOriginIdx > selectedIndex
                            && selectedItems.contains(prevItem)) {
                        selectedItems.remove(prevItem);
                        prevItem.onDeselect();
                    }

                    this.listScrollPanel.ensureVisible(itm.getWidget());

                    selectedIndex = selectedIndex + 1;

                    fireEvents = true;
                } else {
                    setSelectedIndex(selectedIndex + 1);
                    scrollSelectedItemIntoView();
                }
            }
            break;
        case 27: // ESC
        case 13: // Enter
            if (popupShowing) {
                togglePopup();
            }
            break;
        case 65: // A
            if (Event.getCurrentEvent().getCtrlKey()) {
                for (ListItem item : items) {
                    item.onSelect();
                }
                selectedItems.clear();
                selectedItems.addAll(items);

                fireEvents = true;
            }
            break;

        }
        if (fireEvents && this.suppressLayout == false) {
            for (ChangeListener l : listeners) {
                l.onChange(this);
            }
        }
    }

    public void setCommand(Command command) {
        this.command = command;
    }

    // ====================================== Listener Implementations =========================== //

    public void itemSelected(ListItem listItem, Event event) {
        fPanel.setFocus(true);
        if (multiSelect) {
            handleSelection(listItem, event);
        } else {
            setSelectedItem(listItem);
        }
    }

    public void doAction(ListItem listItem) {
        if (command != null) {
            command.execute();
        }
    }

    // ======================================= Inner Classes ===================================== //

    /**
     * Panel used as a drop-down popup.
     */
    private class DropPopupPanel extends PopupPanel {
        public DropPopupPanel() {
            super(true);
            setStyleName("drop-popup"); //$NON-NLS-1$
        }

        @Override
        public boolean onEventPreview(Event event) {
            if (DOM.isOrHasChild(CustomListBox.this.getElement(), DOM.eventGetTarget(event))) {

                return true;
            }
            return super.onEventPreview(event);
        }
    }

    /**
     * Panel contained in the popup
     */
    private class PopupList extends VerticalPanel {

        public PopupList() {
            this.sinkEvents(Event.MOUSEEVENTS);
        }

        @Override
        public void onBrowserEvent(Event event) {
            super.onBrowserEvent(event);
        }
    }

    /**
     * This is the arrow rendered in the drop-down.
     */
    protected class DropDownArrow extends SimplePanel {
        private SimplePanel img;
        private boolean enabled = true;

        public DropDownArrow() {
            img = new SimplePanel();

            this.setStylePrimaryName("combo-arrow"); //$NON-NLS-1$
            super.add(img);
            ElementUtils.preventTextSelection(this.getElement());
        }

        public void setEnabled(boolean enabled) {
            if (this.enabled == enabled) {
                return;
            }
            this.enabled = enabled;
            if (enabled) {
                this.setStylePrimaryName("combo-arrow"); //$NON-NLS-1$
            } else {
                this.setStylePrimaryName("combo-arrow-disabled"); //$NON-NLS-1$
            }

        }
    }

    /**
     * Setting editable to true allows the user to specify their own value for the combobox.
     * 
     * @param editable
     */
    public void setEditable(boolean editable) {
        this.editable = editable;
        this.updateUI();
    }

    public boolean isEditable() {
        return this.editable;
    }

    /**
     * Returns the user-entered value in the case of an editable drop-down
     * 
     * @return Value user has entered
     */
    public String getValue() {
        if (!editable) {
            if (getSelectedItem() != null) {
                return getSelectedItem().getText();
            } else {
                return null;
            }
        } else {
            return (editableTextBox != null) ? editableTextBox.getText() : null;
        }
    }

    public void setValue(String text) {
        this.val = text;
        if (editable) {
            editableTextBox.setText(text);
            selectedIndex = -1;
            this.onChange(editableTextBox);
        } else {
            for (int i = 0; i < items.size(); i++) {
                if (items.get(i).getText().equals(text)) {
                    setSelectedIndex(i);
                    return;
                }
            }
        }
    }

    public void onChange(Widget sender) {
        for (ChangeListener l : listeners) {
            l.onChange(this);
        }
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
        if (editableTextBox != null) {
            editableTextBox.setEnabled(enabled);
        }
        arrow.setEnabled(enabled);
        this.setStylePrimaryName((this.enabled) ? "custom-list" : "custom-list-disabled"); //$NON-NLS-1$ //$NON-NLS-2$
    }

    public boolean isEnabled() {
        return this.enabled;
    }

    public Widget createProxy() {
        DefaultListItem item = new DefaultListItem();
        item.setText(getSelectedItem().getText());
        return item;
    }

    public void setDragController(DragController controller) {
        dragController = controller;
        if (this.items.size() > 0) {
            for (ListItem item : items) {
                dragController.makeDraggable(item.getWidget());
            }
        }
    }

    public boolean isMultiSelect() {
        return multiSelect;
    }

    public void setMultiSelect(boolean multiSelect) {
        this.multiSelect = multiSelect;
    }
}