/**
* Sencha GXT 3.1.0-beta - Sencha for GWT
* Copyright(c) 2007-2014, Sencha, Inc.
* licensing@sencha.com
*
* http://www.sencha.com/products/gxt/license/
*/
package com.sencha.gxt.widget.core.client.grid.filters;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.event.logical.shared.AttachEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.sencha.gxt.core.client.ValueProvider;
import com.sencha.gxt.core.client.util.DelayedTask;
import com.sencha.gxt.core.shared.event.GroupingHandlerRegistration;
import com.sencha.gxt.data.shared.Store;
import com.sencha.gxt.data.shared.Store.StoreFilter;
import com.sencha.gxt.data.shared.loader.BeforeLoadEvent;
import com.sencha.gxt.data.shared.loader.BeforeLoadEvent.BeforeLoadHandler;
import com.sencha.gxt.data.shared.loader.DataProxy;
import com.sencha.gxt.data.shared.loader.FilterConfig;
import com.sencha.gxt.data.shared.loader.FilterPagingLoadConfig;
import com.sencha.gxt.data.shared.loader.LoadEvent;
import com.sencha.gxt.data.shared.loader.LoadHandler;
import com.sencha.gxt.data.shared.loader.Loader;
import com.sencha.gxt.messages.client.DefaultMessages;
import com.sencha.gxt.widget.core.client.ComponentPlugin;
import com.sencha.gxt.widget.core.client.event.ActivateEvent;
import com.sencha.gxt.widget.core.client.event.ActivateEvent.ActivateHandler;
import com.sencha.gxt.widget.core.client.event.CheckChangeEvent;
import com.sencha.gxt.widget.core.client.event.CheckChangeEvent.CheckChangeHandler;
import com.sencha.gxt.widget.core.client.event.ColumnMoveEvent;
import com.sencha.gxt.widget.core.client.event.ColumnMoveEvent.ColumnMoveHandler;
import com.sencha.gxt.widget.core.client.event.DeactivateEvent;
import com.sencha.gxt.widget.core.client.event.DeactivateEvent.DeactivateHandler;
import com.sencha.gxt.widget.core.client.event.HeaderContextMenuEvent;
import com.sencha.gxt.widget.core.client.event.HeaderContextMenuEvent.HeaderContextMenuHandler;
import com.sencha.gxt.widget.core.client.event.ReconfigureEvent;
import com.sencha.gxt.widget.core.client.event.ReconfigureEvent.ReconfigureHandler;
import com.sencha.gxt.widget.core.client.event.UpdateEvent;
import com.sencha.gxt.widget.core.client.event.UpdateEvent.UpdateHandler;
import com.sencha.gxt.widget.core.client.event.ViewReadyEvent;
import com.sencha.gxt.widget.core.client.event.ViewReadyEvent.ViewReadyHandler;
import com.sencha.gxt.widget.core.client.grid.ColumnConfig;
import com.sencha.gxt.widget.core.client.grid.ColumnHeader;
import com.sencha.gxt.widget.core.client.grid.ColumnHeader.Head;
import com.sencha.gxt.widget.core.client.grid.ColumnHiddenChangeEvent;
import com.sencha.gxt.widget.core.client.grid.ColumnHiddenChangeEvent.ColumnHiddenChangeHandler;
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.CheckMenuItem;
import com.sencha.gxt.widget.core.client.menu.Menu;
import com.sencha.gxt.widget.core.client.menu.SeparatorMenuItem;
/**
* Provides an abstract base class that applies filters to the rows in a grid.
* <p/>
* A filter is applied to a grid to reduce the amount of information that is
* displayed, thus highlighting the information of interest to the user. A
* filter is generally invoked from a header menu.
* <p/>
* The filters can be applied locally using {@link Store} filtering or passed to
* a remote data source using a {@link FilterPagingLoadConfig}. To enable local
* filtering, use <code>setLocal(true)</code>. To enable remote filtering, use
* the {@link AbstractGridFilters#AbstractGridFilters(Loader)} form of the
* constructor.
* <p/>
* To add a filter to a {@link Grid} column, create an instance of a concrete
* subclass of {@link Filter}, passing to the constructor the
* {@link ValueProvider} for the column, then add the filter to a
* {@link GridFilters} using {@link GridFilters#addFilter(Filter)} and invoke
* {@link GridFilters#initPlugin(Grid)}. The filter then appears in the grid
* header menu item for any column that uses the supplied value provider.
* <p/>
* Derived classes must provide an implementation of {@link #getStore()}.
*
* @param <M> the model type
*/
public abstract class AbstractGridFilters<M> implements ComponentPlugin<Grid<M>> {
/**
* The default locale-sensitive messages used by this class.
*/
public class DefaultGridFilterMessages implements GridFilterMessages {
@Override
public String filterText() {
return DefaultMessages.getMessages().gridFilters_filterText();
}
}
/**
* The locale-sensitive messages used by this class.
*/
public static interface GridFilterMessages {
String filterText();
}
public interface GridFiltersAppearance {
String filteredStyle();
}
private class Handler implements UpdateHandler, ActivateHandler<Filter<M, ?>>, DeactivateHandler<Filter<M, ?>> {
@Override
public void onActivate(ActivateEvent<Filter<M, ?>> event) {
onStateChange(event.getItem());
}
@Override
public void onDeactivate(DeactivateEvent<Filter<M, ?>> event) {
onStateChange(event.getItem());
}
@SuppressWarnings("unchecked")
@Override
public void onUpdate(UpdateEvent event) {
onStateChange((Filter<M, ?>) event.getSource());
}
}
@SuppressWarnings("rawtypes")
private class LoaderHandler implements BeforeLoadHandler<FilterPagingLoadConfig>, LoadHandler {
@Override
public void onBeforeLoad(BeforeLoadEvent<FilterPagingLoadConfig> event) {
handleBeforeLoad(event);
}
@Override
public void onLoad(LoadEvent event) {
handleLoad(event);
}
}
private class HeaderUpdateHandler implements ColumnHiddenChangeHandler, ColumnMoveHandler, ViewReadyHandler, AttachEvent.Handler {
private boolean scheduled = false;
private void update() {
if (scheduled) {
return;
}
scheduled = true;
Scheduler.get().scheduleFinally(new ScheduledCommand() {
@Override
public void execute() {
scheduled = false;
updateColumnHeadings();
}
});
}
@Override
public void onColumnHiddenChange(ColumnHiddenChangeEvent event) {
update();
}
@Override
public void onColumnMove(ColumnMoveEvent event) {
update();
}
@Override
public void onViewReady(ViewReadyEvent event) {
update();
}
@Override
public void onAttachOrDetach(AttachEvent event) {
if (event.isAttached()) {
update();
}
}
}
private HeaderUpdateHandler columnHandler = new HeaderUpdateHandler();
protected ColumnModel<M> columnModel;
protected StoreFilter<M> currentFilter;
protected Grid<M> grid;
protected Store<M> store;
protected Loader<FilterPagingLoadConfig, ?> loader;
private final GridFiltersAppearance appearance;
private boolean autoReload = true;
private CheckMenuItem checkFilterItem;
private DelayedTask deferredUpdate = new DelayedTask() {
@Override
public void onExecute() {
reload();
}
};
private Map<String, Filter<M, ?>> filters;
private Map<Filter<M, ?>, HandlerRegistration> registrations;
private GroupingHandlerRegistration columnHandlerRegistration;
private Menu filterMenu;
private Handler handler = new Handler();
private LoaderHandler loadHandler = new LoaderHandler();
private boolean local = false;
private GridFilterMessages messages;
private SeparatorMenuItem separatorItem;
private int updateBuffer = 500;
/**
* Creates grid filters that are applied locally. Caller must also invoke
* <code>setLocal(true)</code>.
*/
public AbstractGridFilters() {
this(GWT.<GridFiltersAppearance> create(GridFiltersAppearance.class));
}
/**
* Creates grid filters that are applied locally. Caller must also invoke
* <code>setLocal(true)</code>.
*
* @param appearance the appearance
*/
public AbstractGridFilters(GridFiltersAppearance appearance) {
filters = new HashMap<String, Filter<M, ?>>();
registrations = new HashMap<Filter<M, ?>, HandlerRegistration>();
this.appearance = appearance;
}
/**
* Creates grid filters to be applied remotely. The grid filters are
* automatically added to the {@link FilterPagingLoadConfig} during the
* loading processing and passed by the {@link Loader} to the
* {@link DataProxy} so that they are available for the remote data source to
* use as needed.
*
* @param loader the remote loader
*/
public AbstractGridFilters(Loader<FilterPagingLoadConfig, ?> loader) {
this(loader, GWT.<GridFiltersAppearance> create(GridFiltersAppearance.class));
}
/**
* Creates grid filters to be applied remotely. The grid filters are
* automatically added to the {@link FilterPagingLoadConfig} during the
* loading processing and passed by the {@link Loader} to the
* {@link DataProxy} so that they are available for the remote data source to
* use as needed.
*
* @param loader the remote loader
* @param appearance the appearance
*/
@SuppressWarnings("unchecked")
public AbstractGridFilters(Loader<FilterPagingLoadConfig, ?> loader, GridFiltersAppearance appearance) {
this(appearance);
this.loader = loader;
loader.addBeforeLoadHandler(loadHandler);
loader.addLoadHandler(loadHandler);
}
/**
* Adds a filter.
*
* @param filter the filter
*/
public void addFilter(Filter<M, ?> filter) {
filters.put(filter.getValueProvider().getPath(), filter);
GroupingHandlerRegistration r = new GroupingHandlerRegistration();
r.add(filter.addUpdateHandler(handler));
r.add(filter.addActivateHandler(handler));
r.add(filter.addDeactivateHandler(handler));
registrations.put(filter, r);
}
/**
* Builds a query consisting of a list of loader filter configurations from a
* list of grid filters.
*
* @param filters
* @return a query consisting of a list of loader filter configurations
*/
public List<FilterConfig> buildQuery(List<Filter<M, ?>> filters) {
List<FilterConfig> configs = new ArrayList<FilterConfig>();
for (Filter<M, ?> f : filters) {
List<FilterConfig> temp = f.getFilterConfig();
for (FilterConfig tempConfig : temp) {
tempConfig.setField(f.getValueProvider().getPath());
configs.add(tempConfig);
}
}
return configs;
}
/**
* Removes filter related query parameters from the provided object.
*
* @param config the load config
*/
public void cleanParams(FilterPagingLoadConfig config) {
config.setFilters(new ArrayList<FilterConfig>());
}
/**
* Turns all filters off. This does not clear the configuration information
* (see {@link #removeAll}).
*/
public void clearFilters() {
for (Filter<M, ?> f : filters.values()) {
f.setActive(false, false);
}
}
public GridFiltersAppearance getAppearance() {
return appearance;
}
/**
* Returns the filter based on the value provider path.
*
* @param path the path
* @return the matching filter or null
*/
public Filter<M, ?> getFilter(String path) {
return filters.get(path);
}
/**
* Returns a list of the currently active filters.
*
* @return the list of active filters
*/
public List<Filter<M, ?>> getFilterData() {
List<Filter<M, ?>> configs = new ArrayList<Filter<M, ?>>();
for (Filter<M, ?> f : filters.values()) {
if (f.isActive()) {
configs.add(f);
}
}
return configs;
}
/**
* Returns the locale-sensitive messages used by this class.
*
* @return the locale-sensitive messages used by this class
*/
public GridFilterMessages getMessages() {
if (messages == null) {
messages = new DefaultGridFilterMessages();
}
return messages;
}
@Override
public void initPlugin(Grid<M> component) {
this.grid = component;
grid.addHeaderContextMenuHandler(new HeaderContextMenuHandler() {
@Override
public void onHeaderContextMenu(HeaderContextMenuEvent event) {
onContextMenu(event);
}
});
grid.addReconfigureHandler(new ReconfigureHandler() {
@Override
public void onReconfigure(ReconfigureEvent event) {
}
});
bindStore(getStore());
bindColumnModel(grid.getColumnModel());
}
/**
* Returns true if auto load is enabled.
*
* @return the auto load state
*/
public boolean isAutoReload() {
return autoReload;
}
/**
* Removes all filters.
*/
public void removeAll() {
List<Filter<M, ?>> temp = new ArrayList<Filter<M, ?>>(filters.values());
for (Filter<M, ?> f : temp) {
removeFilter(f);
}
}
/**
* Removes the given filter.
*
* @param filter the filter to be removed
*/
public void removeFilter(Filter<M, ?> filter) {
filters.remove(filter.getValueProvider().getPath());
HandlerRegistration r = registrations.remove(filter);
if (r != null) {
r.removeHandler();
}
}
/**
* Tree to reload the datasource when a filter change happens (defaults to
* true). Set this to false to prevent the datastore from being reloaded if
* there are changes to the filters.
*
* @param autoLoad true to enable auto reload
*/
public void setAutoReload(boolean autoLoad) {
this.autoReload = autoLoad;
}
/**
* Number of milliseconds to defer store updates since the last filter change
* (defaults to 500).
*
* @param updateBuffer the buffer in milliseconds
*/
public void setUpdateBuffer(int updateBuffer) {
this.updateBuffer = updateBuffer;
}
@SuppressWarnings("rawtypes")
public void updateColumnHeadings() {
int cols = grid.getColumnModel().getColumnCount();
for (int i = 0; i < cols; i++) {
ColumnConfig<M, ?> config = grid.getColumnModel().getColumn(i);
if (!config.isHidden()) {
ColumnHeader<M> header = grid.getView().getHeader();
if (header != null) {
Head h = header.getHead(i);
if (h != null && h.isRendered()) {
Filter<M, ?> f = getFilter(config.getValueProvider().getPath());
if (f != null) {
h.getElement().setClassName(appearance.filteredStyle(), f.isActive());
}
}
}
}
}
}
protected void bindColumnModel(ColumnModel<M> columnModel) {
if (this.columnModel != null && columnHandlerRegistration != null) {
columnHandlerRegistration.removeHandler();
columnHandlerRegistration = null;
}
this.columnModel = columnModel;
if (columnModel != null) {
columnHandlerRegistration = new GroupingHandlerRegistration();
columnHandlerRegistration.add(columnModel.addColumnHiddenChangeHandler(columnHandler));
columnHandlerRegistration.add(grid.addViewReadyHandler(columnHandler));
columnHandlerRegistration.add(columnModel.addColumnMoveHandler(columnHandler));
columnHandlerRegistration.add(grid.addAttachHandler(columnHandler));
}
}
protected void bindStore(Store<M> store) {
this.store = store;
}
protected Filter<M, ?> getMenuFilter(CheckChangeEvent<CheckMenuItem> event) {
CheckMenuItem item = event.getItem();
ColumnConfig<M, ?> config = grid.getColumnModel().getColumn((Integer) item.getData("index"));
return getFilter(config.getPath());
}
protected StoreFilter<M> getModelFilter() {
StoreFilter<M> storeFilter = new StoreFilter<M>() {
@Override
public boolean select(Store<M> store, M parent, M item) {
for (Filter<M, ?> f : filters.values()) {
if (f.isActivatable() && f.isActive() && !f.validateModel(item)) {
return false;
}
}
return true;
}
};
return storeFilter;
}
/**
* Returns the store used by the grid filters.
*
* @return the store used by the grid filters
*/
protected abstract Store<M> getStore();
protected void handleBeforeLoad(BeforeLoadEvent<FilterPagingLoadConfig> event) {
FilterPagingLoadConfig config = event.getLoadConfig();
cleanParams(config);
List<FilterConfig> filterConfigs = buildQuery(getFilterData());
config.setFilters(filterConfigs);
}
@SuppressWarnings("rawtypes")
protected void handleLoad(LoadEvent event) {
}
/**
* Returns true if the grid filters are to be applied locally.
*
* @return true if the grid filters are to be applied locally
*/
protected boolean isLocal() {
return local;
}
protected void onCheckChange(CheckChangeEvent<CheckMenuItem> event) {
getMenuFilter(event).setActive(event.getItem().isChecked(), false);
}
protected void onContextMenu(HeaderContextMenuEvent event) {
int column = event.getColumnIndex();
if (separatorItem == null) {
separatorItem = new SeparatorMenuItem();
}
separatorItem.removeFromParent();
if (checkFilterItem == null) {
checkFilterItem = new CheckMenuItem(DefaultMessages.getMessages().gridFilters_filterText());
checkFilterItem.addCheckChangeHandler(new CheckChangeHandler<CheckMenuItem>() {
@Override
public void onCheckChange(CheckChangeEvent<CheckMenuItem> event) {
AbstractGridFilters.this.onCheckChange(event);
}
});
}
checkFilterItem.setData("index", column);
Filter<M, ?> f = getFilter(grid.getColumnModel().getColumn(column).getValueProvider().getPath());
if (f != null) {
filterMenu = f.getMenu();
checkFilterItem.setChecked(f.isActive(), true);
checkFilterItem.setSubMenu(filterMenu);
Menu menu = event.getMenu();
menu.add(separatorItem);
menu.add(checkFilterItem);
}
}
protected void onStateChange(Filter<M, ?> filter) {
if (checkFilterItem != null && checkFilterItem.isAttached()) {
checkFilterItem.setChecked(filter.isActive(), true);
}
if ((autoReload || local)) {
deferredUpdate.delay(updateBuffer);
}
updateColumnHeadings();
}
protected void reload() {
if (local) {
if (currentFilter != null) {
store.removeFilter(currentFilter);
}
currentFilter = getModelFilter();
store.addFilter(currentFilter);
if (!store.isFiltered()) {
store.setEnableFilters(true);
}
} else {
deferredUpdate.cancel();
if (loader != null) {
loader.load();
}
}
}
/**
* True to use Store filter functions (local filtering) instead of the default
* server side filtering (defaults to false).
*
* @param local true for local
*/
protected void setLocal(boolean local) {
this.local = local;
}
}