nl.strohalm.cyclos.mobile.client.ui.widgets.DataList.java Source code

Java tutorial

Introduction

Here is the source code for nl.strohalm.cyclos.mobile.client.ui.widgets.DataList.java

Source

/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
    
Cyclos is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
    
Cyclos 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 General Public License for more details.
    
You should have received a copy of the GNU General Public License
along with Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
    
 */
package nl.strohalm.cyclos.mobile.client.ui.widgets;

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

import nl.strohalm.cyclos.mobile.client.CyclosMobile;
import nl.strohalm.cyclos.mobile.client.Messages;
import nl.strohalm.cyclos.mobile.client.ui.Icon;
import nl.strohalm.cyclos.mobile.client.utils.BaseAsyncCallback;
import nl.strohalm.cyclos.mobile.client.utils.ComponentEventHelper;
import nl.strohalm.cyclos.mobile.client.utils.IterableJsArray;
import nl.strohalm.cyclos.mobile.client.utils.ResultPage;
import nl.strohalm.cyclos.mobile.client.utils.ScreenHelper;

import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.cell.client.Cell.Context;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.event.dom.client.ScrollEvent;
import com.google.gwt.event.dom.client.ScrollHandler;
import com.google.gwt.event.logical.shared.AttachEvent;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.cellview.client.AbstractPager;
import com.google.gwt.user.cellview.client.CellList;
import com.google.gwt.user.cellview.client.HasKeyboardPagingPolicy.KeyboardPagingPolicy;
import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.view.client.AsyncDataProvider;
import com.google.gwt.view.client.HasData;
import com.google.gwt.view.client.HasRows;
import com.google.gwt.view.client.NoSelectionModel;
import com.google.gwt.view.client.Range;
import com.google.gwt.view.client.SelectionChangeEvent;
import com.google.gwt.view.client.SelectionChangeEvent.Handler;

/**
 * A single column list of cells which fetches large data asynchronously on scroll event.
 */
public abstract class DataList<T extends JavaScriptObject> extends Composite {

    protected FlowPanel listContainer;
    protected FlowPanel mainContainer;
    protected List<T> values;
    protected CellList<T> cellList;
    protected AbstractCell<T> cell;
    protected ScrollingPager pager;
    protected AsyncDataProvider<T> asyncDataProvider;
    protected SimplePanel loadingPanel;
    protected FlowPanel scrollPanel;
    protected boolean loadingEnabled;
    protected Widget emptyListWidget;
    protected static int PAGE_SIZE = 25;

    public DataList() {
        values = new ArrayList<T>();
        mainContainer = new FlowPanel();
        listContainer = new FlowPanel();
        loadingEnabled = true;
        createCell();
        createCellList();
        createDataProvider();
        createContainer();
        createPager();
        createLoadingPanel();
        createScrollPanel();

        initWidget(mainContainer);
    }

    /**
     * Creates the container widget
     * which will be scrollable
     */
    private void createContainer() {
        listContainer = new FlowPanel();
        Widget header = addHeader();
        if (header != null) {
            listContainer.add(header);
        }
        listContainer.add(cellList);
        Widget footer = addFooter();
        if (footer != null) {
            listContainer.add(footer);
        }
    }

    /**
     * May be override in order to add a header to the widget<br>
     * This widget will be scrolled within cells   
     */
    protected Widget addHeader() {
        return null;
    }

    /**
     * May be override in order to add a footer to the widget<br> 
     * This widget will be scrolled within cells 
     */
    protected Widget addFooter() {
        return null;
    }

    /**
     * Creates the pager which will be used for pagination purposes
     */
    private void createPager() {
        pager = new ScrollingPager();
        pager.getElement().setId("data-list");
        pager.setWidget(listContainer);
        pager.setDisplay(cellList);

        // Adjust pager to fit in screen and generate scroll if needed
        Window.addResizeHandler(new ResizeHandler() {
            @Override
            public void onResize(ResizeEvent event) {
                adjustPager(pager);
            }
        });
        pager.addAttachHandler(new AttachEvent.Handler() {
            @Override
            public void onAttachOrDetach(AttachEvent event) {
                adjustPager(pager);
            }
        });

        // Initialize widget
        mainContainer.add(pager);
    }

    /**
     * Creates a loading data panel
     */
    private void createLoadingPanel() {
        loadingPanel = new SimplePanel();
        loadingPanel.setStyleName("loading-data");
        loadingPanel.setVisible(false);

        LabelField text = new LabelField(Messages.Accessor.get().loadingData());
        text.addStyleName("loading-data-text");

        loadingPanel.setWidget(text);
        listContainer.add(loadingPanel);
    }

    /**
     * Creates a scrolling button panel only if browser 
     * does not support native scrolling for this widget
     */
    private void createScrollPanel() {
        if (!ScreenHelper.supportsTouch()) {
            scrollPanel = new FlowPanel();
            scrollPanel.setStyleName("scroll-data");

            SimplePanel up = new SimplePanel();
            up.setStyleName("scroll-data-image");
            up.setWidget(Icon.UP.image());

            SimplePanel down = new SimplePanel();
            down.setStyleName("scroll-data-image");
            down.setWidget(Icon.DOWN.image());

            // Add scrolling events
            ComponentEventHelper.addScrollEvents(up, down, pager.getElement().getId());

            scrollPanel.add(up);
            scrollPanel.add(down);

            mainContainer.add(scrollPanel);
        }
    }

    /**
     * Displays the loading panel if available
     */
    private void displayLoading() {
        if (loadingEnabled) {
            loadingPanel.setVisible(true);
        }
    }

    /**
     * Hides the scroll panel if available
     */
    protected void hideScroll() {
        if (scrollPanel != null) {
            scrollPanel.setVisible(false);
        }
    }

    /**
     * Hides the loading panel if available
     */
    private void hideLoading() {
        if (loadingEnabled) {
            // If exists data hide the panel using delay
            // to let the user see the message
            if (values != null && values.size() > 0) {
                Timer t = new Timer() {
                    @Override
                    public void run() {
                        // Hide loading panel
                        loadingPanel.setVisible(false);
                    }
                };
                t.schedule(2000);
            } else {
                // Otherwise hide the panel
                loadingPanel.setVisible(false);
            }
        }
    }

    /**
     * Adjust pager sizes to fit in the menu container.<br>
     * May be override in order to use custom sizes
     */
    protected void adjustPager(AbstractPager pager) {
        // Adjust the height, to generate scroll
        int height = CyclosMobile.get().getMainLayout().getMainHeight();
        if (height != 0) {
            pager.setSize("100%", Math.abs(height) + "px");
        }
    }

    /**
     * Creates a {#CellList} used to display the data
     */
    private void createCellList() {
        cellList = new CellList<T>(cell);
        cellList.setPageSize(PAGE_SIZE);
        cellList.setRowCount(PAGE_SIZE);
        cellList.setKeyboardPagingPolicy(KeyboardPagingPolicy.INCREASE_RANGE);
        cellList.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.BOUND_TO_SELECTION);

        // Sets widget when no data available
        cellList.setEmptyListWidget(getEmptyListWidget());
        // Sets loading widget
        cellList.setLoadingIndicator(getLoadingWidget());

        // Add a selection model for cell's selection        
        final NoSelectionModel<T> selectionModel = new NoSelectionModel<T>();
        cellList.setSelectionModel(selectionModel);
        selectionModel.addSelectionChangeHandler(new Handler() {
            @Override
            public void onSelectionChange(SelectionChangeEvent event) {
                onRowSelected(selectionModel.getLastSelectedObject());
            }
        });
    }

    /**
     * Creates a label by default as empty list widget.<br>
     * May be override in order to display a custom widget     
     */
    protected Widget getEmptyListWidget() {
        emptyListWidget = new Label(Messages.Accessor.get().notAvailableData());
        emptyListWidget.setStyleName("no-data");
        // This message should not be shown until the request has been made
        emptyListWidget.setVisible(false);
        return emptyListWidget;
    }

    /**
     * Creates a label by default as loading indicator widget.<br>
     * May be override in order to display a custom widget     
     */
    protected Widget getLoadingWidget() {
        Label label = new Label(Messages.Accessor.get().loadingData());
        label.setStyleName("no-data");
        return label;
    }

    /**
     * Creates the render representation of the model object
     */
    private void createCell() {
        cell = new AbstractCell<T>() {
            @Override
            public void render(Context context, T value, SafeHtmlBuilder sb) {
                Widget widget = onRender(context, value);
                if (widget != null) {
                    sb.appendHtmlConstant(widget.toString());
                }
            }
        };
    }

    /**
     * Creates the provider which fetches and updates the data
     */
    protected void createDataProvider() {
        asyncDataProvider = new AsyncDataProvider<T>() {

            @Override
            protected void onRangeChanged(final HasData<T> display) {

                // Get the new range.
                final Range range = display.getVisibleRange();
                final int start = range.getStart();

                // Calculate total pages count
                int diff = display.getRowCount() % PAGE_SIZE != 0 ? 1 : 0;
                int pages = (display.getRowCount() / PAGE_SIZE) + diff;

                // Current page
                int page = (int) Math.ceil(range.getLength() / (double) PAGE_SIZE);

                // No more data
                if (page > pages) {

                    // Hide loading message
                    hideLoading();

                    return;
                }

                // First page start counting from zero
                page = page - 1;

                BaseAsyncCallback<ResultPage<T>> callback = new BaseAsyncCallback<ResultPage<T>>() {

                    @Override
                    public void onFailure(Throwable caught) {
                        onDataError();
                        super.onFailure(caught);
                    }

                    @Override
                    public void onSuccess(ResultPage<T> result) {
                        // Invoke custom logic before render data
                        onDataLoaded(result);

                        // Update components and render data
                        List<T> newValues = convertToList(result.getElements());
                        values.addAll(newValues);

                        // Once the request has been made we are allowed
                        // to display the empty list widget if necessary
                        emptyListWidget.setVisible(true);

                        updateRowCount(result.getTotalCount(), true);
                        updateRowData(start, values);

                        // Hide loading message
                        hideLoading();

                        // Hide scroll if result is empty
                        if (result.getTotalCount() == 0) {
                            hideScroll();
                        }
                    }
                };
                onSearchData(page, PAGE_SIZE, callback);
            }
        };
        // Handle the cellList
        asyncDataProvider.addDataDisplay(cellList);
    }

    /**
     * May be override in order to execute custom logic on data loaded and before render it     
     */
    protected void onDataLoaded(ResultPage<T> result) {

    }

    /**
     * Is invoked when a problem occur retrieving data
     */
    private void onDataError() {
        // Hides any visible message
        hideLoading();
    }

    /**
     * Converts an JsArray to List which is necessary in GWT methods     
     */
    protected List<T> convertToList(JsArray<T> elements) {
        List<T> list = new ArrayList<T>();
        if (elements != null && elements.length() > 0) {
            IterableJsArray<T> iterable = new IterableJsArray<T>(elements);
            for (Iterator<T> it = iterable.iterator(); it.hasNext();) {
                T value = it.next();
                if (value != null) {
                    list.add(value);
                }
            }
        }
        return list;
    }

    /**
     * Returns the widget which will be rendered in the list.
     * @param value The model value, may be null so according checks should be done
     */
    protected abstract Widget onRender(Context context, T value);

    /**
     * Should invoke service methods to fetch the data     
     */
    protected abstract void onSearchData(int page, int length, AsyncCallback<ResultPage<T>> callback);

    /**
     * Called when a row is selected
     */
    protected void onRowSelected(T value) {

    }

    /**
     * Scrolling pager to fetch the data when scrolls down    
     */
    public class ScrollingPager extends AbstractPager {

        private int incrementSize = PAGE_SIZE;
        private int lastScrollPos = 0;
        private int lastPageSize = -1;
        private final ScrollPanel scrollPanel;

        public ScrollingPager() {
            // Create scroll widget
            scrollPanel = new ScrollPanel();
            initWidget(scrollPanel);

            // Do not let the scrollable take tab focus
            scrollPanel.getElement().setTabIndex(-1);

            // Add scroll events.
            scrollPanel.addScrollHandler(new ScrollHandler() {
                public void onScroll(ScrollEvent event) {
                    // If scrolling up, ignore the event
                    int oldScrollPos = lastScrollPos;
                    lastScrollPos = scrollPanel.getVerticalScrollPosition();

                    if (oldScrollPos >= lastScrollPos) {
                        return;
                    }

                    // Update display data
                    HasRows display = getDisplay();
                    if (display != null) {
                        int maxScrollTop = scrollPanel.getWidget().getOffsetHeight()
                                - scrollPanel.getOffsetHeight();
                        if (lastScrollPos >= maxScrollTop) {

                            // We are near the end, so increase the page size
                            int newPageSize = Math.min(display.getVisibleRange().getLength() + incrementSize,
                                    display.getRowCount());

                            if (lastPageSize != newPageSize) {
                                lastPageSize = newPageSize;
                                // Meanwhile display loading message
                                displayLoading();
                            }
                            display.setVisibleRange(0, newPageSize);
                        }
                    }
                }
            });
        }

        /**
         * Sets the widget to be scrolled           
         */
        public void setWidget(Widget widget) {
            scrollPanel.setWidget(widget);
        }

        /**
         * Set the number of rows by which the range is increased when the scrollbar
         * reaches the bottom.
         *
         * @param incrementSize the incremental number of rows
         */
        public void setIncrementSize(int incrementSize) {
            this.incrementSize = incrementSize;
        }

        @Override
        protected void onRangeOrRowCountChanged() {
            // Does nothing
        }
    }

}