/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.gwt.gen2.table.client;
import com.google.gwt.gen2.event.shared.HandlerRegistration;
import com.google.gwt.gen2.table.client.TableModelHelper.ColumnSortInfo;
import com.google.gwt.gen2.table.client.TableModelHelper.ColumnSortList;
import com.google.gwt.gen2.table.event.client.ColumnSortEvent;
import com.google.gwt.gen2.table.event.client.ColumnSortHandler;
import com.google.gwt.gen2.table.event.client.HasColumnSortHandlers;
import com.google.gwt.gen2.table.override.client.OverrideDOM;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
* A variation of the {@link com.google.gwt.gen2.table.override.client.Grid}
* that supports sorting and row movement.
*/
public class SortableGrid extends SelectionGrid implements
HasColumnSortHandlers {
/**
* The column sorter defines an algorithm to sort columns.
*/
public abstract static class ColumnSorter {
/**
* Override this method to implement a custom column sorting algorithm.
*
* @param grid the grid that the sorting will be applied to
* @param sortList the list of columns to sort by
* @param callback the callback object when sorting is complete
*/
public abstract void onSortColumn(SortableGrid grid,
ColumnSortList sortList, SortableGrid.ColumnSorterCallback callback);
}
/**
* The default {@link ColumnSorter} used if no other {@link ColumnSorter} is
* specified. This column sorted uses a quicksort algorithm to sort columns.
*/
private static class DefaultColumnSorter extends ColumnSorter {
@Override
public void onSortColumn(SortableGrid grid, ColumnSortList sortList,
SortableGrid.ColumnSorterCallback callback) {
// Get the primary column and sort order
int column = sortList.getPrimaryColumn();
boolean ascending = sortList.isPrimaryAscending();
// Get all of the cell elements
SelectionGridCellFormatter formatter = grid.getSelectionGridCellFormatter();
int rowCount = grid.getRowCount();
List<Element> tdElems = new ArrayList<Element>(rowCount);
for (int i = 0; i < rowCount; i++) {
tdElems.add(formatter.getRawElement(i, column));
}
// Sort the cell elements
if (ascending) {
Collections.sort(tdElems, new Comparator<Element>() {
public int compare(Element o1, Element o2) {
return o1.getInnerText().compareTo(o2.getInnerText());
}
});
} else {
Collections.sort(tdElems, new Comparator<Element>() {
public int compare(Element o1, Element o2) {
return o2.getInnerText().compareTo(o1.getInnerText());
}
});
}
// Convert tdElems to trElems, reversing if needed
Element[] trElems = new Element[rowCount];
for (int i = 0; i < rowCount; i++) {
trElems[i] = DOM.getParent(tdElems.get(i));
}
// Use the callback to complete the sorting
callback.onSortingComplete(trElems);
}
}
/**
* Callback that is called when the row sorting is complete.
*/
public class ColumnSorterCallback {
/**
* An array of the tr elements that should be reselected.
*/
private Element[] selectedRows;
/**
* Construct a new {@link ColumnSorterCallback}.
*/
protected ColumnSorterCallback(Element[] selectedRows) {
this.selectedRows = selectedRows;
}
/**
* Set the order of all rows after a column sort request.
*
* This method takes an array of the row indexes in this Grid, ordered
* according to their new positions in the Grid.
*
* @param trIndexes the row index in their new order
*/
public void onSortingComplete(int[] trIndexes) {
// Convert indexes to row elements
SelectionGridRowFormatter formatter = getSelectionGridRowFormatter();
Element[] trElems = new Element[trIndexes.length];
for (int i = 0; i < trElems.length; i++) {
trElems[i] = formatter.getRawElement(trIndexes[i]);
}
// Call main callback method
onSortingComplete(trElems);
}
/**
* Set the order of all rows after a column sort request.
*
* This method takes an array of the row elements in this Grid, ordered
* according to their new positions in the Grid.
*
* @param trElems the row elements in their new order
*/
public void onSortingComplete(Element[] trElems) {
// Move the rows to their new positions
applySort(trElems);
// Fire the listeners
onSortingComplete();
}
/**
* Trigger the callback, but do not change the ordering of the rows.
*
* Use this method if your {@link ColumnSorter} rearranges the columns
* manually.
*/
public void onSortingComplete() {
// Reselect things that need reselecting
for (int i = 0; i < selectedRows.length; i++) {
int rowIndex = getRowIndex(selectedRows[i]);
if (rowIndex >= 0) {
selectRow(rowIndex, false);
}
}
fireColumnSorted();
}
}
/**
* The class used to do column sorting.
*/
private ColumnSorter columnSorter = null;
/**
* Information about the sorted columns.
*/
private ColumnSortList columnSortList = new ColumnSortList();
/**
* Constructor.
*/
public SortableGrid() {
super();
}
/**
* Constructs a {@link SortableGrid} with the requested size.
*
* @param rows the number of rows
* @param columns the number of columns
* @throws IndexOutOfBoundsException
*/
public SortableGrid(int rows, int columns) {
this();
resize(rows, columns);
}
public HandlerRegistration addColumnSortHandler(ColumnSortHandler handler) {
return addHandler(ColumnSortEvent.TYPE, handler);
}
/**
* @return the column sorter used to sort columns
*/
public ColumnSorter getColumnSorter() {
return getColumnSorter(false);
}
/**
* @return the {@link ColumnSortList} of previously sorted columns
*/
public ColumnSortList getColumnSortList() {
return columnSortList;
}
/**
* Move a row up (relative to the screen) one index to a lesser index.
*
* @param row the row index to move
* @throws IndexOutOfBoundsException
*/
public void moveRowDown(int row) {
swapRows(row, row + 1);
}
/**
* Move a row down (relative to the screen) one index to a greater index.
*
* @param row the row index to move
* @throws IndexOutOfBoundsException
*/
public void moveRowUp(int row) {
swapRows(row, row - 1);
}
/**
* Completely reverse the order of all rows in the table.
*/
public void reverseRows() {
int lastRow = numRows - 1;
for (int i = 0; i < numRows / 2; i++) {
swapRowsRaw(i, lastRow);
lastRow--;
}
// Set the column sorting as reversed
for (ColumnSortInfo sortInfo : columnSortList) {
sortInfo.setAscending(!sortInfo.isAscending());
}
fireColumnSorted();
}
/**
* Set the {@link ColumnSorter}.
*
* @param sorter the new {@link ColumnSorter}
*/
public void setColumnSorter(ColumnSorter sorter) {
this.columnSorter = sorter;
}
/**
* Set the current {@link ColumnSortList} and fire an event.
*
* @param columnSortList the new {@link ColumnSortList}
*/
public void setColumnSortList(ColumnSortList columnSortList) {
setColumnSortList(columnSortList, true);
}
/**
* Set the current {@link ColumnSortList} and optionally fire an event.
*
* @param columnSortList the new {@link ColumnSortList}
* @param fireEvents true to trigger the onSort event
*/
public void setColumnSortList(ColumnSortList columnSortList,
boolean fireEvents) {
assert columnSortList != null : "columnSortList cannot be null";
this.columnSortList = columnSortList;
if (fireEvents) {
fireColumnSorted();
}
}
/**
* Sort the grid according to the specified column. If the column is already
* sorted, reverse sort it.
*
* @param column the column to sort
* @throws IndexOutOfBoundsException
*/
public void sortColumn(int column) {
if (column == columnSortList.getPrimaryColumn()) {
sortColumn(column, !columnSortList.isPrimaryAscending());
} else {
sortColumn(column, true);
}
}
/**
* Sort the grid according to the specified column.
*
* @param column the column to sort
* @param ascending sort the column in ascending order
* @throws IndexOutOfBoundsException
*/
public void sortColumn(int column, boolean ascending) {
// Verify the column bounds
if (column < 0) {
throw new IndexOutOfBoundsException(
"Cannot access a column with a negative index: " + column);
} else if (column >= numColumns) {
throw new IndexOutOfBoundsException("Column index: " + column
+ ", Column size: " + numColumns);
}
// Add the sorting to the list of sorted columns
columnSortList.add(new ColumnSortInfo(column, ascending));
// Use the onSort method to actually sort the column
Element[] selectedRows = getSelectedRowsMap().values().toArray(
new Element[0]);
deselectAllRows();
getColumnSorter(true).onSortColumn(this, columnSortList,
new SortableGrid.ColumnSorterCallback(selectedRows));
}
/**
* Swap the positions of two rows.
*
* @param row1 the first row to swap
* @param row2 the second row to swap
* @throws IndexOutOfBoundsException
*/
public void swapRows(int row1, int row2) {
checkRowBounds(row1);
checkRowBounds(row2);
swapRowsRaw(row1, row2);
}
/**
* Fire column sorted event to listeners.
*/
protected void fireColumnSorted() {
fireEvent(new ColumnSortEvent(columnSortList));
}
/**
* Get the {@link ColumnSorter}. Optionally create a
* {@link DefaultColumnSorter} if the user hasn't specified one already.
*
* @param createAsNeeded create a default sorter if needed
* @return the column sorter
*/
protected ColumnSorter getColumnSorter(boolean createAsNeeded) {
if ((columnSorter == null) && createAsNeeded) {
columnSorter = new DefaultColumnSorter();
}
return columnSorter;
}
/**
* Swap two rows without checking the cell bounds.
*
* @param row1 the first row to swap
* @param row2 the second row to swap
*/
protected void swapRowsRaw(int row1, int row2) {
Element tbody = getBodyElement();
if (row1 == row2 + 1) {
// Just move row1 up one
Element tr = getSelectionGridRowFormatter().getRawElement(row1);
int index = OverrideDOM.getRowIndex(tr);
DOM.removeChild(tbody, tr);
DOM.insertChild(tbody, tr, index - 1);
} else if (row2 == row1 + 1) {
// Just move row2 up one
Element tr = getSelectionGridRowFormatter().getRawElement(row2);
int index = OverrideDOM.getRowIndex(tr);
DOM.removeChild(tbody, tr);
DOM.insertChild(tbody, tr, index - 1);
} else if (row1 == row2) {
// Do nothing if rows are the same
return;
} else {
// Remove both rows
Element tr1 = getSelectionGridRowFormatter().getRawElement(row1);
Element tr2 = getSelectionGridRowFormatter().getRawElement(row2);
int index1 = OverrideDOM.getRowIndex(tr1);
int index2 = OverrideDOM.getRowIndex(tr2);
DOM.removeChild(tbody, tr1);
DOM.removeChild(tbody, tr2);
// Reinsert them into the table
if (row1 > row2) {
DOM.insertChild(tbody, tr1, index2);
DOM.insertChild(tbody, tr2, index1);
} else if (row1 < row2) {
DOM.insertChild(tbody, tr2, index1);
DOM.insertChild(tbody, tr1, index2);
}
}
// Update the selected rows table
Map<Integer, Element> selectedRows = getSelectedRowsMap();
Element tr1 = selectedRows.remove(new Integer(row1));
Element tr2 = selectedRows.remove(new Integer(row2));
if (tr1 != null) {
selectedRows.put(new Integer(row2), tr1);
}
if (tr2 != null) {
selectedRows.put(new Integer(row1), tr2);
}
}
/**
* Set the order of all rows after a column sort request.
*
* This method takes an array of the row elements in this Grid, ordered
* according to their new positions in the Grid.
*
* @param trElems the row elements in their new order
*/
void applySort(Element[] trElems) {
// Move the rows to their new positions
Element bodyElem = getBodyElement();
for (int i = trElems.length - 1; i >= 0; i--) {
if (trElems[i] != null) {
DOM.removeChild(bodyElem, trElems[i]);
DOM.insertChild(bodyElem, trElems[i], 0);
}
}
}
}