GridHelper.java
/*
** Module : GridHelper.java
** Abstract : Grid helper.
**
** Copyright (c) 2017-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------Description-----------------------------------
** 001 HC 20170612 Initial version.
** 002 CA 20180607 Changed to allow HTML table cells.
** 003 GBB 20231117 Support for column width.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.admin.client.widget;
import com.google.common.base.Predicate;
import com.google.common.collect.*;
import com.google.gwt.cell.client.*;
import com.google.gwt.core.client.*;
import com.google.gwt.dom.client.*;
import com.google.gwt.regexp.shared.*;
import com.google.gwt.resources.client.*;
import com.google.gwt.safehtml.shared.*;
import com.google.gwt.user.cellview.client.*;
import com.google.gwt.user.client.ui.*;
import com.google.gwt.view.client.*;
import org.gwtbootstrap3.client.ui.gwt.DataGrid;
import java.util.*;
import java.util.function.*;
import java.util.function.Function;
/**
* The class makes work with the GWT's grid more pleasant.
*/
public class GridHelper
{
/**
* Creates a new default instance of {@link DataGrid}.
*
* @param <TRowObject>
* The row type.
*
* @return see above
*/
public static <TRowObject> DataGrid<TRowObject> create()
{
return new DataGrid<>(Integer.MAX_VALUE,
new ResourcesAdapter(GWT.create(DataGrid.Resources.class)));
}
/**
* Initializes an instance of {@link DataGrid}. The initialization includes creation of columns
* according to the column definitions, creation of {@link ListDataProvider}, creation of
* default (single) selection model, setting up sorting and wrapping everything in
* a {@link GridHandle} which the method returns.
*
* @param grid
* The grid to init.
* @param columns
* The column definitions.
* @param <TRowObject>
* The row type.
*
* @return see above
*/
public static <TRowObject> GridHandle<TRowObject>
initListGrid(DataGrid grid, ColumnInfo<TRowObject> ... columns)
{
return initListGrid(grid, false, columns);
}
/**
* Initializes an instance of {@link DataGrid}. The initialization includes creation of columns
* according to the column definitions, creation of {@link ListDataProvider}, creation of
* the selection model, setting up sorting and wrapping everything in
* a {@link GridHandle} which the method returns.
*
* @param grid
* The grid to init.
* @param multiSelection
* Multi selection flag.
* @param columns
* The column definitions.
* @param <TRowObject>
* The row type.
*
* @return see above
*/
public static <TRowObject> GridHandle<TRowObject>
initListGrid(DataGrid grid, boolean multiSelection, ColumnInfo<TRowObject> ... columns)
{
return initListGrid(grid, multiSelection, true, columns);
}
/**
* Initializes an instance of {@link DataGrid}. The initialization includes creation of columns
* according to the column definitions, creation of {@link ListDataProvider}, creation of
* the selection model, setting up sorting and wrapping everything in
* a {@link GridHandle} which the method returns.
*
* @param grid
* The grid to init.
* @param multiSelection
* Multi selection flag.
* @param sortable
* Sortable flag. When <code>false</code> none of the columns will be made sortable.
* @param columns
* The column definitions.
* @param <TRowObject>
* The row type.
*
* @return see above
*/
public static <TRowObject> GridHandle<TRowObject>
initListGrid(DataGrid grid,
boolean multiSelection,
boolean sortable,
ColumnInfo<TRowObject> ... columns)
{
ListDataProvider<TRowObject> dataProvider = new ListDataProvider<>();
dataProvider.addDataDisplay(grid);
ColumnSortEvent.ListHandler<TRowObject> sortHandler = null;
for (ColumnInfo<TRowObject> column : columns)
{
Column<TRowObject, ?> gridCol = null;
if (column.isHtmlCell())
{
gridCol = new Column<TRowObject, SafeHtml>(new SafeHtmlCell()
{
@Override
public Set<String> getConsumedEvents()
{
HashSet<String> events = new HashSet<>();
events.add("dblclick");
return events;
}
})
{
@Override
public SafeHtml getValue(final TRowObject object)
{
return (SafeHtml) column.getValue(object);
}
@Override
public void onBrowserEvent(Cell.Context context, Element elem,
TRowObject object, NativeEvent event)
{
super.onBrowserEvent(context, elem, object, event);
if (column.dblclickHandler != null && "dblclick".equals(event.getType()))
{
column.dblclickHandler.accept(object);
}
}
};
}
else
{
gridCol = new Column<TRowObject, String>(new TextCell()
{
@Override
public Set<String> getConsumedEvents()
{
HashSet<String> events = new HashSet<>();
events.add("dblclick");
return events;
}
})
{
@Override
public String getValue(final TRowObject object)
{
return (String) column.getValue(object);
}
@Override
public void onBrowserEvent(Cell.Context context, Element elem,
TRowObject object, NativeEvent event)
{
super.onBrowserEvent(context, elem, object, event);
if (column.dblclickHandler != null && "dblclick".equals(event.getType()))
{
column.dblclickHandler.accept(object);
}
}
};
}
if (!sortable)
{
grid.addColumn(gridCol, column.getHeaderLabel());
continue;
}
boolean colSortable = column.getComparator() != null;
gridCol.setSortable(colSortable);
grid.addColumn(gridCol, column.getHeaderLabel());
if (colSortable)
{
if (sortHandler == null)
{
sortHandler = new ColumnSortEvent.ListHandler<>(dataProvider.getList());
// make the first sortable column sorted
ColumnSortList columnSortList = grid.getColumnSortList();
columnSortList.push(new ColumnSortList.ColumnSortInfo(gridCol, true));
}
sortHandler.setComparator(gridCol, (o1, o2) -> column.getComparator().apply(o1, o2));
grid.addColumnSortHandler(sortHandler);
}
if (column.widthInPixels > 0)
{
grid.setColumnWidth(gridCol, column.widthInPixels, Style.Unit.PX);
}
else if (column.widthInPercents > 0)
{
grid.setColumnWidth(gridCol, column.widthInPercents, Style.Unit.PCT);
}
}
SetSelectionModel<TRowObject> selModel = multiSelection ?
new MultiSelectionModel<>() :
new SingleSelectionModel<>();
grid.setSelectionModel(selModel);
grid.setHover(true);
return new GridHandle<>(grid, dataProvider, selModel);
}
/**
* Setups the given grid data provider with new data that satisfy the given filter criteria.
*
* @param <TRowObject>
* The row type
* @param dataGrid
* The given grid
* @param dataProvider
* The given grid data provider
* @param records
* The new data
* @param filterExp
* The given filter criteria
*/
public static <TRowObject> void updateDataProvider(DataGrid<TRowObject> dataGrid,
ListDataProvider<TRowObject> dataProvider,
TRowObject[] records, RegExp filterExp)
{
dataProvider.getList().clear();
Predicate<TRowObject> filterRecords = new Predicate<TRowObject>()
{
@Override
public boolean apply(TRowObject record)
{
if (filterExp == null)
{
return true;
}
for (int i = 0; i < dataGrid.getColumnCount(); i++)
{
Column<TRowObject, ?> column = dataGrid.getColumn(i);
if (filterExp.test(column.getValue(record).toString()))
{
return true;
}
}
return false;
}
};
dataProvider.getList().addAll(new ArrayList<TRowObject>(
Collections2.filter(Arrays.asList(records), filterRecords)));
dataProvider.flush();
ColumnSortEvent.fire(dataGrid, dataGrid.getColumnSortList());
}
/**
* A helper method that updates the data provider with the supplied records.
*
* @param dataGrid
* The grid instance.
* @param dataProvider
* The related data provider.
* @param records
* The new records to insert into the data provider.
* @param <TRowObject>
* The row type.
*/
public static <TRowObject> void updateDataProvider(DataGrid<TRowObject> dataGrid,
ListDataProvider<TRowObject> dataProvider,
TRowObject[] records)
{
dataProvider.getList().clear();
if (records == null)
{
return;
}
for (int i = 0; i < records.length; i++)
{
dataProvider.getList().add(records[i]);
}
dataProvider.flush();
ColumnSortEvent.fire(dataGrid, dataGrid.getColumnSortList());
}
/**
* Holds grid column definition. The class is used as the source of information when
* initializing a {@link DataGrid} using the GridHelper.initListGrid methods.
*
* @param <TRowObject>
* The row type.
*/
public static class ColumnInfo<TRowObject>
{
/** Header label */
private String headerLabel;
/** Value getter */
private Function<TRowObject, ?> valueGetter;
/** Row comparator */
private BiFunction<TRowObject, TRowObject, Integer> comparator;
/** Double click handler */
private Consumer<TRowObject> dblclickHandler;
/** Flag indentifying this cell contains HTML code. */
private boolean htmlCell = false;
/** Width in pixels */
private int widthInPixels;
/** Width in percents */
private double widthInPercents;
/**
* Ctor.
*
* @param headerLabel
* Column header label.
* @param valueGetter
* Value getter.
* @param htmlCell
* Flag marking this column as containing HTML code.
*/
public ColumnInfo(String headerLabel,
Function<TRowObject, SafeHtml> valueGetter,
boolean htmlCell)
{
this.headerLabel = headerLabel;
this.valueGetter = valueGetter;
this.comparator = null;
this.dblclickHandler = null;
initComparator();
this.htmlCell = htmlCell;
}
/**
* Ctor.
*
* @param headerLabel
* Column header label.
* @param valueGetter
* Value getter.
*/
public ColumnInfo(String headerLabel, Function<TRowObject, String> valueGetter)
{
this(headerLabel, valueGetter, null, null);
}
/**
* Ctor.
*
* @param headerLabel
* Column header label.
* @param valueGetter
* Value getter.
* @param comparator
* Row comparator.
*/
public ColumnInfo(String headerLabel,
Function<TRowObject, String> valueGetter,
BiFunction<TRowObject, TRowObject, Integer> comparator)
{
this(headerLabel, valueGetter, comparator, null);
}
/**
* Ctor.
*
* @param headerLabel
* Column header label.
* @param valueGetter
* Value getter.
* @param comparator
* Row comparator.
* @param dblclickHandler
* Doubleclick handler.
*/
public ColumnInfo(String headerLabel,
Function<TRowObject, String> valueGetter,
BiFunction<TRowObject, TRowObject, Integer> comparator,
Consumer<TRowObject> dblclickHandler)
{
this.headerLabel = headerLabel;
this.valueGetter = valueGetter;
this.comparator = comparator;
this.dblclickHandler = dblclickHandler;
initComparator();
}
/**
* Get the state of the {@link #htmlCell} flag.
*
* @return See above.
*/
public boolean isHtmlCell()
{
return htmlCell;
}
/**
* Returns the column header label.
*
* @return see above
*/
public String getHeaderLabel()
{
return headerLabel;
}
/**
* Use the {@link #valueGetter} to compute the value for this column.
*
* @param object
* The object with the rows's data.
*
* @return The computed data for this column.
*/
public Object getValue(TRowObject object)
{
return valueGetter.apply(object);
}
/**
* Returns column width in pixels.
*
* @return See above.
*/
public int getWidthInPixels()
{
return widthInPixels;
}
/**
* Sets column width in pixels.
*
* @param widthInPixels
* Column width in pixels.
*/
public void setWidthInPixels(int widthInPixels)
{
this.widthInPixels = widthInPixels;
}
/**
* Returns column width in %.
*
* @return See above.
*/
public double getWidthInPercents()
{
return widthInPercents;
}
/**
* Sets column width in %.
*
* @param widthInPercents
* Column width in %.
*/
public void setWidthInPercents(double widthInPercents)
{
this.widthInPercents = widthInPercents;
}
/**
* Returns the row comparator.
*
* @return see above
*/
public BiFunction<TRowObject, TRowObject, Integer> getComparator()
{
return comparator;
}
/**
* Initializes the default row comparator.
*/
private void initComparator()
{
if(comparator == null)
{
comparator = (tRowObject, tRowObject2) ->
{
Object str1 = valueGetter.apply(tRowObject);
Object str2 = valueGetter.apply(tRowObject2);
if (htmlCell)
{
str1 = ((SafeHtml) str1).asString();
str2 = ((SafeHtml) str2).asString();
}
else
{
str1 = (String) str1;
str2 = (String) str2;
}
//noinspection NonJREEmulationClassesInClientCode
return ((String) str1).compareTo((String) str2);
};
}
}
}
/**
* A resource adapter used to override some of the grid's css styles.
*/
private static class ResourcesAdapter
implements DataGrid.Resources
{
/** The default resources */
private final DataGrid.Resources resources;
/** The style adapter */
private final StyleAdapter style;
/**
* Ctor.
*
* @param resources
* The default resources.
*/
public ResourcesAdapter(final DataGrid.Resources resources) {
this.resources = resources;
this.style = new StyleAdapter();
}
@Override
public ImageResource dataGridLoading() {
return resources.dataGridLoading();
}
@Override
public ImageResource dataGridSortAscending() {
return resources.dataGridSortAscending();
}
@Override
public ImageResource dataGridSortDescending() {
return resources.dataGridSortDescending();
}
@Override
public DataGrid.Style dataGridStyle() {
return style;
}
}
/**
* The class provides custom css styles.
*/
private static class StyleAdapter
implements DataGrid.Style
{
/** boostrap styles prefix */
private static final String B = "gwtb3-";
/** an empty/dummy style */
private static final String DUMMY = B + "d";
@Override
public boolean ensureInjected() {
return true;
}
@Override
public String getText() {
return B;
}
@Override
public String getName() {
return B;
}
@Override
public String dataGridCell() {
return B + "cell";
}
@Override
public String dataGridEvenRow() {
return "even";
}
@Override
public String dataGridEvenRowCell() {
return DUMMY;
}
@Override
public String dataGridFirstColumn() {
return DUMMY; // Bootstrap3 uses "smart selectors"
}
@Override
public String dataGridFirstColumnFooter() {
return DUMMY;
}
@Override
public String dataGridFirstColumnHeader() {
return DUMMY;
}
@Override
public String dataGridFooter() {
return DUMMY;
}
@Override
public String dataGridHeader() {
return DUMMY;
}
@Override
public String dataGridHoveredRow() {
return "active";
}
@Override
public String dataGridHoveredRowCell() {
return "active";
}
@Override
public String dataGridKeyboardSelectedCell() {
return DUMMY;
}
@Override
public String dataGridKeyboardSelectedRow() {
return "key-active";
}
@Override
public String dataGridKeyboardSelectedRowCell() {
return "key-active";
}
@Override
public String dataGridLastColumn() {
return DUMMY;
}
@Override
public String dataGridLastColumnFooter() {
return DUMMY;
}
@Override
public String dataGridLastColumnHeader() {
return DUMMY;
}
@Override
public String dataGridOddRow() {
return "odd";
}
@Override
public String dataGridOddRowCell() {
return DUMMY;
}
@Override
public String dataGridSelectedRow() {
return "info";
}
@Override
public String dataGridSelectedRowCell() {
return DUMMY;
}
@Override
public String dataGridSortableHeader() {
return DUMMY;
}
@Override
public String dataGridSortedHeaderAscending() {
return DUMMY;
}
@Override
public String dataGridSortedHeaderDescending() {
return DUMMY;
}
@Override
public String dataGridWidget() {
return "table";
}
}
}