/*
* Copyright 2011 JBoss 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 org.drools.guvnor.client.widgets.drools.decoratedgrid;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.CellValue.CellState;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.CellValue.GroupedCellValue;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.data.Coordinate;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.data.DynamicData;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.data.DynamicDataRow;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.data.GroupedDynamicDataRow;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.data.RowMapper;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.AppendRowEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.CellStateChangedEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.CellStateChangedEvent.CellStateOperation;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.ColumnResizeEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.CopyRowsEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.DeleteColumnEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.DeleteRowEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.InsertInternalColumnEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.InsertRowEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.MoveColumnsEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.PasteRowsEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.RowGroupingChangeEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.SelectedCellChangeEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.SetColumnVisibilityEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.SetInternalModelEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.SortDataEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.ToggleMergingEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.UpdateColumnDataEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.UpdateColumnDefinitionEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.UpdateModelEvent;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.UpdateSelectedCellsEvent;
import com.google.gwt.cell.client.Cell.Context;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.TableCellElement;
import com.google.gwt.dom.client.TableElement;
import com.google.gwt.dom.client.TableRowElement;
import com.google.gwt.dom.client.TableSectionElement;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.AbstractImagePrototype;
import com.google.gwt.user.client.ui.Widget;
/**
* An abstract grid of data. Implementations can choose the orientation to
* render "rows" and "columns" (e.g. some may transpose the normal meaning to
* provide a horizontal implementation of normally vertical tabular data)
*/
public abstract class AbstractMergableGridWidget<M, T> extends Widget
implements
ToggleMergingEvent.Handler,
DeleteRowEvent.Handler,
InsertRowEvent.Handler,
AppendRowEvent.Handler,
CopyRowsEvent.Handler,
PasteRowsEvent.Handler,
DeleteColumnEvent.Handler,
SetInternalModelEvent.Handler<M, T>,
InsertInternalColumnEvent.Handler<T>,
SetColumnVisibilityEvent.Handler,
UpdateColumnDataEvent.Handler,
UpdateColumnDefinitionEvent.Handler,
ColumnResizeEvent.Handler,
MoveColumnsEvent.Handler,
SortDataEvent.Handler,
UpdateSelectedCellsEvent.Handler,
CellStateChangedEvent.Handler {
/**
* Container for a details of a selected cell
*/
public static class CellSelectionDetail {
private Coordinate c;
private int offsetX;
private int offsetY;
private int height;
private int width;
CellSelectionDetail(Coordinate c,
int offsetX,
int offsetY,
int height,
int width) {
this.c = c;
this.offsetX = offsetX;
this.offsetY = offsetY;
this.height = height;
this.width = width;
}
public int getHeight() {
return height;
}
public int getOffsetX() {
return offsetX;
}
public int getOffsetY() {
return offsetY;
}
public int getWidth() {
return width;
}
public Coordinate getCoordinate() {
return c;
}
}
// Enum to support keyboard navigation
public enum MOVE_DIRECTION {
LEFT, RIGHT, UP, DOWN, NONE
}
//GWT disable text selection in an HTMLTable.
//event.stopPropogation() doesn't prevent text selection
private native static void disableTextSelectInternal(Element e,
boolean disable)/*-{
if (disable) {
e.ondrag = function() {
return false;
};
e.onselectstart = function() {
return false;
};
e.style.MozUserSelect = "none"
} else {
e.ondrag = null;
e.onselectstart = null;
e.style.MozUserSelect = "text"
}
}-*/;
// Selections store the actual grid data selected (irrespective of
// merged cells). So a merged cell spanning 2 rows is stored as 2
// selections. Selections are ordered by row number so we can
// iterate top to bottom.
protected TreeSet<CellValue< ? extends Comparable< ? >>> selections = new TreeSet<CellValue< ? extends Comparable< ? >>>(
new Comparator<CellValue< ? extends Comparable< ? >>>() {
public int compare(CellValue< ? extends Comparable< ? >> o1,
CellValue< ? extends Comparable< ? >> o2) {
return o1.getPhysicalCoordinate().getRow()
- o2.getPhysicalCoordinate().getRow();
}
} );
// TABLE elements
protected TableElement table;
protected TableSectionElement tbody;
// Resources
protected ResourcesProvider<T> resources;
protected EventBus eventBus;
protected String selectorGroupedCellsHtml;
protected String selectorUngroupedCellsHtml;
// Data and columns to render
protected List<DynamicColumn<T>> columns;
protected DynamicData data;
protected RowMapper rowMapper;
protected AbstractCellFactory<T> cellFactory;
protected AbstractCellValueFactory<T, ? > cellValueFactory;
protected CellTableDropDownDataValueMapProvider dropDownManager;
//Properties for multi-cell selection
protected CellValue< ? > rangeOriginCell;
protected CellValue< ? > rangeExtentCell;
protected MOVE_DIRECTION rangeDirection = MOVE_DIRECTION.NONE;
protected boolean bDragOperationPrimed = false;
//Rows that have been copied in a copy-paste operation
private List<DynamicDataRow> copiedRows = new ArrayList<DynamicDataRow>();
protected static final RowGroupingChangeEvent ROW_GROUPING_EVENT = new RowGroupingChangeEvent();
protected final boolean isReadOnly;
/**
* A grid of cells.
*/
public AbstractMergableGridWidget(ResourcesProvider<T> resources,
AbstractCellFactory<T> cellFactory,
AbstractCellValueFactory<T, ? > cellValueFactory,
CellTableDropDownDataValueMapProvider dropDownManager,
boolean isReadOnly,
EventBus eventBus) {
this.resources = resources;
this.cellFactory = cellFactory;
this.cellValueFactory = cellValueFactory;
this.dropDownManager = dropDownManager;
this.isReadOnly = isReadOnly;
this.eventBus = eventBus;
ImageResource selectorGroupedCells = resources.collapseCellsIcon();
ImageResource selectorUngroupedCells = resources.expandCellsIcon();
this.selectorGroupedCellsHtml = makeImageHtml( selectorGroupedCells );
this.selectorUngroupedCellsHtml = makeImageHtml( selectorUngroupedCells );
// Create some elements to contain the grid
table = Document.get().createTableElement();
tbody = Document.get().createTBodyElement();
table.setClassName( resources.cellTable() );
table.setCellPadding( 0 );
table.setCellSpacing( 0 );
setElement( table );
table.appendChild( tbody );
// Events in which we're interested (note, if a Cell<?> appears not to
// work I've probably forgotten some events. Might be a better way of
// doing this, but I copied CellTable<?, ?>'s lead
sinkEvents( Event.getTypeInt( "click" )
| Event.getTypeInt( "dblclick" )
| Event.getTypeInt( "mousedown" )
| Event.getTypeInt( "mouseup" )
| Event.getTypeInt( "mousemove" )
| Event.getTypeInt( "mouseout" )
| Event.getTypeInt( "change" )
| Event.getTypeInt( "keypress" )
| Event.getTypeInt( "keydown" ) );
//Prevent text selection
disableTextSelectInternal( table,
true );
//Wire-up events
eventBus.addHandler( ToggleMergingEvent.TYPE,
this );
eventBus.addHandler( DeleteRowEvent.TYPE,
this );
eventBus.addHandler( InsertRowEvent.TYPE,
this );
eventBus.addHandler( AppendRowEvent.TYPE,
this );
eventBus.addHandler( CopyRowsEvent.TYPE,
this );
eventBus.addHandler( PasteRowsEvent.TYPE,
this );
eventBus.addHandler( DeleteColumnEvent.TYPE,
this );
eventBus.addHandler( SetColumnVisibilityEvent.TYPE,
this );
eventBus.addHandler( UpdateColumnDataEvent.TYPE,
this );
eventBus.addHandler( UpdateColumnDefinitionEvent.TYPE,
this );
eventBus.addHandler( ColumnResizeEvent.TYPE,
this );
eventBus.addHandler( MoveColumnsEvent.TYPE,
this );
eventBus.addHandler( SortDataEvent.TYPE,
this );
eventBus.addHandler( UpdateSelectedCellsEvent.TYPE,
this );
eventBus.addHandler( CellStateChangedEvent.TYPE,
this );
}
private static String makeImageHtml(ImageResource image) {
return AbstractImagePrototype.create( image ).getHTML();
}
/**
* Redraw the whole table
*/
abstract void redraw();
/**
* Redraw table columns. Partial redraw
*
* @param startRedrawIndex
* Start column index (inclusive)
* @param endRedrawIndex
* End column index (inclusive)
*/
abstract void redrawColumns(int startRedrawIndex,
int endRedrawIndex);
//Apply grouping by collapsing applicable rows
private void applyModelGrouping(CellValue< ? > startCell,
boolean bRedraw) {
data.applyModelGrouping( startCell );
//Partial redraw
if ( bRedraw ) {
int startRowIndex = startCell.getCoordinate().getRow();
GroupedDynamicDataRow groupedRow = (GroupedDynamicDataRow) data.get( startRowIndex );
int minRedrawRow = findMinRedrawRow( startRowIndex - (startRowIndex > 0 ? 1 : 0) );
int maxRedrawRow = findMaxRedrawRow( startRowIndex + (startRowIndex < data.size() - 1 ? 1 : 0) );
for ( int iRow = 0; iRow < groupedRow.getChildRows().size() - 1; iRow++ ) {
deleteRowElement( startRowIndex );
}
redrawRows( minRedrawRow,
maxRedrawRow );
eventBus.fireEvent( ROW_GROUPING_EVENT );
}
}
//Check whether two values are equal or both null
private boolean equalOrNull(Object o1,
Object o2) {
if ( o1 == null && o2 == null ) {
return true;
}
if ( o1 != null && o2 == null ) {
return false;
}
if ( o1 == null && o2 != null ) {
return false;
}
return o1.equals( o2 );
}
// Given a base row find the maximum row that needs to be re-rendered based
// upon each columns merged cells; where each merged cell passes through the
// base row
private int findMaxRedrawRow(int baseRowIndex) {
if ( data.size() == 0 ) {
return 0;
}
// These should never happen, but it's a safe-guard
if ( baseRowIndex < 0 ) {
baseRowIndex = 0;
}
if ( baseRowIndex > data.size() - 1 ) {
baseRowIndex = data.size() - 1;
}
int maxRedrawRow = baseRowIndex;
DynamicDataRow baseRow = data.get( baseRowIndex );
for ( int iCol = 0; iCol < baseRow.size(); iCol++ ) {
int iRow = baseRowIndex;
CellValue< ? extends Comparable< ? >> cell = baseRow.get( iCol );
while ( cell.getRowSpan() != 1 && iRow < data.size() - 1 ) {
iRow++;
DynamicDataRow row = data.get( iRow );
cell = row.get( iCol );
}
maxRedrawRow = (iRow > maxRedrawRow ? iRow : maxRedrawRow);
}
return maxRedrawRow;
}
//Find the bottom coordinate of a merged cell
private Coordinate findMergedCellExtent(Coordinate c) {
if ( c.getRow() == data.size() - 1 ) {
return c;
}
Coordinate nc = new Coordinate( c.getRow() + 1,
c.getCol() );
CellValue< ? > newCell = data.get( nc );
while ( newCell.getRowSpan() == 0 && nc.getRow() < data.size() - 1 ) {
nc = new Coordinate( nc.getRow() + 1,
nc.getCol() );
newCell = data.get( nc );
}
if ( newCell.getRowSpan() != 0 ) {
nc = new Coordinate( nc.getRow() - 1,
nc.getCol() );
}
return nc;
}
// Given a base row find the minimum row that needs to be re-rendered based
// upon each columns merged cells; where each merged cell passes through the
// base row
private int findMinRedrawRow(int baseRowIndex) {
if ( data.size() == 0 ) {
return 0;
}
// These should never happen, but it's a safe-guard
if ( baseRowIndex < 0 ) {
baseRowIndex = 0;
}
if ( baseRowIndex > data.size() - 1 ) {
baseRowIndex = data.size() - 1;
}
int minRedrawRow = baseRowIndex;
DynamicDataRow baseRow = data.get( baseRowIndex );
for ( int iCol = 0; iCol < baseRow.size(); iCol++ ) {
int iRow = baseRowIndex;
CellValue< ? extends Comparable< ? >> cell = baseRow.get( iCol );
while ( cell.getRowSpan() != 1 && iRow > 0 ) {
iRow--;
DynamicDataRow row = data.get( iRow );
cell = row.get( iCol );
}
minRedrawRow = (iRow < minRedrawRow ? iRow : minRedrawRow);
}
return minRedrawRow;
}
//Get the next cell when selection moves in the specified direction
private Coordinate getNextCell(Coordinate c,
MOVE_DIRECTION dir) {
int step = 0;
Coordinate nc = c;
switch ( dir ) {
case LEFT :
// Move left
step = c.getCol() > 0 ? 1 : 0;
if ( step > 0 ) {
nc = new Coordinate( c.getRow(),
c.getCol() - step );
// Skip hidden columns
while ( nc.getCol() > 0
&& !columns.get( nc.getCol() ).isVisible() ) {
nc = new Coordinate( c.getRow(),
nc.getCol() - step );
}
//Move to top of a merged cells
CellValue< ? > newCell = data.get( nc );
while ( newCell.getRowSpan() == 0 ) {
nc = new Coordinate( nc.getRow() - 1,
nc.getCol() );
newCell = data.get( nc );
}
}
break;
case RIGHT :
// Move right
step = c.getCol() < columns.size() - 1 ? 1 : 0;
if ( step > 0 ) {
nc = new Coordinate( c.getRow(),
c.getCol() + step );
// Skip hidden columns
while ( nc.getCol() < columns.size() - 2
&& !columns.get( nc.getCol() ).isVisible() ) {
nc = new Coordinate( c.getRow(),
nc.getCol() + step );
}
//If the next column is not visible don't move
if ( !columns.get( nc.getCol() ).isVisible() ) {
nc = c;
break;
}
//Move to top of a merged cells
CellValue< ? > newCell = data.get( nc );
while ( newCell.getRowSpan() == 0 ) {
nc = new Coordinate( nc.getRow() - 1,
nc.getCol() );
newCell = data.get( nc );
}
}
break;
case UP :
// Move up
step = c.getRow() > 0 ? 1 : 0;
if ( step > 0 ) {
nc = new Coordinate( c.getRow() - step,
c.getCol() );
//Move to top of a merged cells
CellValue< ? > newCell = data.get( nc );
while ( newCell.getRowSpan() == 0 ) {
nc = new Coordinate( nc.getRow() - step,
nc.getCol() );
newCell = data.get( nc );
}
}
break;
case DOWN :
// Move down
step = c.getRow() < data.size() - 1 ? 1 : 0;
if ( step > 0 ) {
nc = new Coordinate( c.getRow() + step,
c.getCol() );
//Move to top of a merged cells
CellValue< ? > newCell = data.get( nc );
while ( newCell.getRowSpan() == 0 && nc.getRow() < data.size() - 1 ) {
nc = new Coordinate( nc.getRow() + step,
nc.getCol() );
newCell = data.get( nc );
}
if ( newCell.getRowSpan() == 0 && nc.getRow() == data.size() - 1 ) {
nc = c;
}
}
}
return nc;
}
// Re-index columns
private void reindexColumns() {
for ( int iCol = 0; iCol < columns.size(); iCol++ ) {
DynamicColumn<T> col = columns.get( iCol );
col.setColumnIndex( iCol );
}
}
//Remove grouping by expanding applicable rows
private void removeModelGrouping(CellValue< ? > startCell,
boolean bRedraw) {
List<DynamicDataRow> expandedRow = data.removeModelGrouping( startCell );
//Partial redraw
if ( bRedraw ) {
int startRowIndex = startCell.getCoordinate().getRow();
int minRedrawRow = findMinRedrawRow( startRowIndex - (startRowIndex > 0 ? 1 : 0) );
int maxRedrawRow = findMaxRedrawRow( startRowIndex + (startRowIndex < data.size() - 2 ? 1 : 0) );
for ( int iRow = 0; iRow < expandedRow.size() - 1; iRow++ ) {
createEmptyRowElement( startRowIndex );
}
redrawRows( minRedrawRow,
maxRedrawRow );
eventBus.fireEvent( ROW_GROUPING_EVENT );
}
}
//Clear all selections
protected void clearSelection() {
// De-select any previously selected cells
for ( CellValue< ? extends Comparable< ? >> cell : this.selections ) {
cell.removeState( CellState.SELECTED );
deselectCell( cell );
}
// Clear collection
selections.clear();
rangeDirection = MOVE_DIRECTION.NONE;
SelectedCellChangeEvent scce = new SelectedCellChangeEvent();
eventBus.fireEvent( scce );
}
abstract void createEmptyRowElement(int index);
abstract void createRowElement(int index,
DynamicDataRow rowData);
abstract void deleteRowElement(int index);
//Check whether "Grouping" widget has been clicked
protected boolean isGroupWidgetClicked(Event event,
Element target) {
String eventType = event.getType();
if ( eventType.equals( "mousedown" ) ) {
String tagName = target.getTagName();
if ( "img".equalsIgnoreCase( tagName ) ) {
return true;
}
}
return false;
}
/**
* Redraw table rows. Partial redraw
*
* @param startRedrawIndex
* Start row index (inclusive)
* @param endRedrawIndex
* End row index (inclusive)
*/
abstract void redrawRows(int startRedrawIndex,
int endRedrawIndex);
abstract void removeRowElement(int index);
/**
* Remove styling indicating a selected state
*
* @param cell
*/
abstract void deselectCell(CellValue< ? extends Comparable< ? >> cell);
/**
* Extend selection from the first cell selected to the cell specified
*
* @param end
* Extent of selection
*/
void extendSelection(Coordinate end) {
if ( rangeOriginCell == null ) {
throw new IllegalArgumentException( "origin has not been set. Unable to extend selection" );
}
if ( end == null ) {
throw new IllegalArgumentException( "end cannot be null" );
}
clearSelection();
CellValue< ? > endCell = data.get( end );
selectRange( rangeOriginCell,
endCell );
if ( rangeOriginCell.getCoordinate().getRow() > endCell.getCoordinate().getRow() ) {
rangeExtentCell = selections.first();
rangeDirection = MOVE_DIRECTION.UP;
} else {
rangeExtentCell = selections.last();
rangeDirection = MOVE_DIRECTION.DOWN;
}
}
/**
* Extend selection in the specified direction
*
* @param dir
* Direction to extend the selection
*/
void extendSelection(MOVE_DIRECTION dir) {
if ( selections.size() > 0 ) {
CellValue< ? > activeCell = (rangeExtentCell == null ? rangeOriginCell : rangeExtentCell);
Coordinate nc = getNextCell( activeCell.getCoordinate(),
dir );
clearSelection();
rangeDirection = dir;
rangeExtentCell = data.get( nc );
selectRange( rangeOriginCell,
rangeExtentCell );
}
}
/**
* Retrieve the extents of a cell
*
* @param cv
* The cell for which to retrieve the extents
* @return
*/
CellSelectionDetail getSelectedCellExtents(CellValue< ? extends Comparable< ? >> cv) {
if ( cv == null ) {
throw new IllegalArgumentException( "cv cannot be null" );
}
// Cells in hidden columns do not have extents
if ( !columns.get( cv.getCoordinate().getCol() ).isVisible() ) {
return null;
}
Coordinate hc = cv.getHtmlCoordinate();
TableRowElement tre = tbody.getRows().getItem( hc.getRow() ).<TableRowElement> cast();
TableCellElement tce = tre.getCells().getItem( hc.getCol() ).<TableCellElement> cast();
int offsetX = tce.getOffsetLeft();
int offsetY = tce.getOffsetTop();
int w = tce.getOffsetWidth();
int h = tce.getOffsetHeight();
CellSelectionDetail e = new CellSelectionDetail( cv.getCoordinate(),
offsetX,
offsetY,
h,
w );
return e;
}
/**
* Group a merged cell. If the cell is not merged across at least two rows
* or the cell is not the top of the merged range no action is taken.
*
* @param start
* Coordinate of top of merged group.
*/
void groupCells(Coordinate start) {
if ( start == null ) {
throw new IllegalArgumentException( "start cannot be null" );
}
CellValue< ? > startCell = data.get( start );
//Start cell needs to be top of a merged range
if ( startCell.getRowSpan() <= 1 && !startCell.isGrouped() ) {
return;
}
clearSelection();
if ( startCell.isGrouped() ) {
removeModelGrouping( startCell,
true );
} else {
applyModelGrouping( startCell,
true );
}
}
/**
* Hide a column
*/
abstract void hideColumn(int index);
/**
* Move the selected cell
*
* @param dir
* Direction to move the selection
*/
void moveSelection(MOVE_DIRECTION dir) {
if ( selections.size() > 0 ) {
CellValue< ? > activeCell = (rangeExtentCell == null ? rangeOriginCell : rangeExtentCell);
Coordinate nc = getNextCell( activeCell.getCoordinate(),
dir );
startSelecting( nc );
rangeDirection = dir;
}
}
/**
* Resize a column
*
* @param col
* @param width
*/
abstract void resizeColumn(DynamicColumn< ? > col,
int width);
/**
* Add styling to cell to indicate a selected state
*
* @param cell
*/
abstract void selectCell(CellValue< ? extends Comparable< ? >> cell);
/**
* Select a range of cells
*
* @param startCell
* The first cell to select
* @param endCell
* The last cell to select
*/
void selectRange(CellValue< ? > startCell,
CellValue< ? > endCell) {
int col = startCell.getCoordinate().getCol();
//Ensure startCell precedes endCell
if ( startCell.getCoordinate().getRow() > endCell.getCoordinate().getRow() ) {
CellValue< ? > swap = startCell;
startCell = endCell;
endCell = swap;
}
//Ensure startCell is at the top of a merged cell
while ( startCell.getRowSpan() == 0 ) {
startCell = data.get( startCell.getCoordinate().getRow() - 1 ).get( col );
}
//Ensure endCell is at the bottom of a merged cell
Coordinate nc = findMergedCellExtent( endCell.getCoordinate() );
endCell = data.get( nc );
//Select range
for ( int iRow = startCell.getCoordinate().getRow(); iRow <= endCell.getCoordinate().getRow(); iRow++ ) {
CellValue< ? > cell = data.get( iRow ).get( col );
selections.add( cell );
// Redraw selected cell
cell.addState( CellState.SELECTED );
selectCell( cell );
}
//Set extent of selected range according to the direction of selection
switch ( rangeDirection ) {
case DOWN :
this.rangeExtentCell = this.selections.last();
break;
case UP :
this.rangeExtentCell = this.selections.first();
break;
}
}
/**
* Show a column
*/
abstract void showColumn(int index);
/**
* Select a single cell. If the cell is merged the selection is extended to
* include all merged cells.
*
* @param start
* The physical coordinate of the cell
*/
void startSelecting(Coordinate start) {
if ( start == null ) {
throw new IllegalArgumentException( "start cannot be null" );
}
clearSelection();
//Raise event signalling change in selection
CellSelectionDetail ce = getSelectedCellExtents( data.get( start ) );
SelectedCellChangeEvent scce = new SelectedCellChangeEvent( ce );
eventBus.fireEvent( scce );
CellValue< ? > startCell = data.get( start );
selectRange( startCell,
startCell );
rangeOriginCell = startCell;
rangeExtentCell = null;
}
public void setData(DynamicData data) {
this.data = data;
this.rowMapper = new RowMapper( data );
}
public void setColumns(List<DynamicColumn<T>> columns) {
this.columns = columns;
reindexColumns();
}
public void onToggleMerging(ToggleMergingEvent event) {
clearSelection();
if ( event.isMerged() ) {
if ( !data.isMerged() ) {
data.setMerged( true );
redraw();
}
} else {
if ( data.isMerged() ) {
data.setMerged( false );
redraw();
eventBus.fireEvent( ROW_GROUPING_EVENT );
}
}
}
public void onDeleteRow(DeleteRowEvent event) {
int index = rowMapper.mapToMergedRow( event.getIndex() );
// Clear any selections
clearSelection();
//Delete row data
data.deleteRow( index );
removeRowElement( index );
// Partial redraw
if ( data.isMerged() ) {
if ( data.size() > 0 ) {
int minRedrawRow = findMinRedrawRow( index - 1 );
int maxRedrawRow = findMaxRedrawRow( index - 1 ) + 1;
if ( maxRedrawRow > data.size() - 1 ) {
maxRedrawRow = data.size() - 1;
}
redrawRows( minRedrawRow,
maxRedrawRow );
}
}
}
public void onInsertRow(InsertRowEvent event) {
int index = rowMapper.mapToMergedRow( event.getIndex() );
DynamicDataRow rowData = cellValueFactory.makeUIRowData();
insertRow( index,
rowData );
}
public void onAppendRow(AppendRowEvent event) {
int index = data.size();
DynamicDataRow rowData = cellValueFactory.makeUIRowData();
insertRow( index,
rowData );
}
public void onCopyRows(CopyRowsEvent event) {
copiedRows.clear();
//Determine set of *unique* logical (grouped) rows from absolute indexes
SortedSet<Integer> uniqueLogicalRowIndexes = new TreeSet<Integer>();
for ( Integer iRow : event.getRowIndexes() ) {
uniqueLogicalRowIndexes.add( rowMapper.mapToMergedRow( iRow ) );
}
for ( Integer iRow : uniqueLogicalRowIndexes ) {
copiedRows.add( data.get( iRow ) );
}
}
public void onPasteRows(PasteRowsEvent event) {
if ( copiedRows == null || copiedRows.size() == 0 ) {
return;
}
int iRow = rowMapper.mapToMergedRow( event.getTargetRowIndex() );
for ( DynamicDataRow sourceRowData : copiedRows ) {
//Clone the row, other than RowNumber column
insertRow( iRow,
cloneRow( sourceRowData ) );
iRow++;
}
}
private DynamicDataRow cloneRow(DynamicDataRow sourceRowData) {
if ( sourceRowData instanceof GroupedDynamicDataRow ) {
return cloneDynamicDataRow( (GroupedDynamicDataRow) sourceRowData );
}
return cloneDynamicDataRow( sourceRowData );
}
private DynamicDataRow cloneDynamicDataRow(DynamicDataRow sourceRowData) {
DynamicDataRow rowData = cellValueFactory.makeUIRowData();
for ( int iCol = 0; iCol < sourceRowData.size(); iCol++ ) {
CellValue< ? extends Comparable< ? >> cell = sourceRowData.get( iCol );
rowData.get( iCol ).setValue( cell.getValue() );
}
return rowData;
}
@SuppressWarnings({"rawtypes", "unchecked"})
private DynamicDataRow cloneDynamicDataRow(GroupedDynamicDataRow sourceRowData) {
GroupedDynamicDataRow rowData = new GroupedDynamicDataRow();
for ( int iCol = 0; iCol < sourceRowData.size(); iCol++ ) {
CellValue< ? extends Comparable< ? >> sourceCell = sourceRowData.get( iCol );
GroupedCellValue cell = sourceCell.convertToGroupedCell();
if ( sourceCell instanceof GroupedCellValue ) {
GroupedCellValue groupedSourceCell = (GroupedCellValue) sourceCell;
if ( groupedSourceCell.isGrouped() ) {
cell.addState( CellState.GROUPED );
}
if ( groupedSourceCell.isOtherwise() ) {
cell.addState( CellState.OTHERWISE );
}
}
rowData.add( cell );
}
for ( DynamicDataRow childRow : sourceRowData.getChildRows() ) {
rowData.addChildRow( cloneRow( childRow ) );
}
return rowData;
}
private void insertRow(int index,
DynamicDataRow rowData) {
// Clear any selections
clearSelection();
int minRedrawRow = index;
int maxRedrawRow = index;
// Find rows that need to be (re)drawn before the new row is inserted
if ( data.isMerged() ) {
if ( index < data.size() ) {
minRedrawRow = findMinRedrawRow( index );
maxRedrawRow = findMaxRedrawRow( index );
}
}
data.addRow( index,
rowData );
// Check extents as these could have changed after the row is inserted
if ( data.isMerged() ) {
if ( index < data.size() ) {
minRedrawRow = Math.min( minRedrawRow,
findMinRedrawRow( index ) );
maxRedrawRow = Math.max( maxRedrawRow,
findMaxRedrawRow( index ) );
} else {
minRedrawRow = Math.min( minRedrawRow,
findMinRedrawRow( index ) );
maxRedrawRow = index;
}
}
// Partial redraw
if ( !data.isMerged() ) {
// Only new row when not merged
createRowElement( index,
rowData );
} else {
// Affected rows when merged
createEmptyRowElement( index );
redrawRows( minRedrawRow,
maxRedrawRow );
}
}
public void onDeleteColumn(DeleteColumnEvent event) {
int firstColumnIndex = event.getFirstColumnIndex();
boolean bRedraw = event.redraw();
boolean bRedrawSidebar = false;
//Expand any merged cells in column
for ( int iCol = 0; iCol < event.getNumberOfColumns(); iCol++ ) {
for ( int iRow = 0; iRow < data.size(); iRow++ ) {
CellValue< ? > cv = data.get( iRow ).get( firstColumnIndex + iCol );
if ( cv.isGrouped() ) {
removeModelGrouping( cv,
false );
bRedrawSidebar = true;
}
}
}
// Clear any selections
clearSelection();
// Delete columns and data from grid
for ( int iCol = 0; iCol < event.getNumberOfColumns(); iCol++ ) {
columns.remove( firstColumnIndex );
data.deleteColumn( firstColumnIndex );
}
reindexColumns();
// Redraw
if ( bRedraw ) {
redraw();
if ( bRedrawSidebar ) {
eventBus.fireEvent( ROW_GROUPING_EVENT );
}
}
}
public void onInsertInternalColumn(InsertInternalColumnEvent<T> event) {
int index = event.getIndex();
boolean bRedraw = event.redraw();
// Clear any selections
clearSelection();
// Add column definitions and data
for ( int iCol = 0; iCol < event.getColumns().size(); iCol++ ) {
DynamicColumn<T> column = event.getColumns().get( iCol );
List<CellValue< ? extends Comparable< ? >>> columnData = event.getColumnsData().get( iCol );
columns.add( index + iCol,
column );
data.addColumn( index + iCol,
columnData,
column.isVisible() );
}
reindexColumns();
// Redraw
if ( bRedraw ) {
redrawColumns( index,
columns.size() - 1 );
}
}
public void onSetInternalModel(SetInternalModelEvent<M, T> event) {
this.dropDownManager.setData( event.getData() );
this.setColumns( event.getColumns() );
this.setData( event.getData() );
this.redraw();
}
public void onSetColumnVisibility(SetColumnVisibilityEvent event) {
int index = event.getIndex();
boolean isVisible = event.isVisible();
if ( isVisible && !columns.get( index ).isVisible() ) {
columns.get( index ).setVisible( isVisible );
data.setColumnVisibility( index,
isVisible );
showColumn( index );
} else if ( !isVisible && columns.get( index ).isVisible() ) {
columns.get( index ).setVisible( isVisible );
data.setColumnVisibility( index,
isVisible );
hideColumn( index );
}
}
public void onUpdateColumnData(UpdateColumnDataEvent event) {
int iRowIndex = 0;
int iColIndex = event.getIndex();
List<CellValue< ? extends Comparable< ? >>> columnData = event.getColumnData();
for ( int iRow = 0; iRow < data.size(); iRow++ ) {
DynamicDataRow row = data.get( iRow );
CellValue< ? extends Comparable< ? >> cell = columnData.get( iRowIndex );
if ( row instanceof GroupedDynamicDataRow ) {
GroupedDynamicDataRow groupedRow = (GroupedDynamicDataRow) row;
//Setting value on a GroupedCellValue causes all children to assume the same value
groupedRow.get( iColIndex ).setValue( cell.getValue() );
//So set the children's values accordingly
for ( int iGroupedRow = 0; iGroupedRow < groupedRow.getChildRows().size(); iGroupedRow++ ) {
cell = columnData.get( iRowIndex );
groupedRow.getChildRows().get( iGroupedRow ).get( iColIndex ).setValue( cell.getValue() );
iRowIndex++;
}
} else {
row.get( iColIndex ).setValue( cell.getValue() );
iRowIndex++;
}
}
data.assertModelMerging();
redrawColumns( iColIndex,
columns.size() - 1 );
}
public void onUpdateColumnDefinition(UpdateColumnDefinitionEvent event) {
int index = event.getColumnIndex();
DynamicColumn<T> column = columns.get( index );
column.setCell( event.getCell() );
column.setSystemControlled( event.isSystemControlled() );
column.setSortable( event.isSortable() );
}
public void onColumnResize(final ColumnResizeEvent event) {
resizeColumn( event.getColumn(),
event.getWidth() );
}
public void onMoveColumns(MoveColumnsEvent event) {
int sourceColumnIndex = event.getSourceColumnIndex();
int targetColumnIndex = event.getTargetColumnIndex();
int numberOfColumns = event.getNumberOfColumns();
//Work-out what columns need redrawing
int startRedrawIndex = sourceColumnIndex;
int endRedrawIndex = targetColumnIndex;
if ( targetColumnIndex < sourceColumnIndex ) {
startRedrawIndex = targetColumnIndex;
endRedrawIndex = sourceColumnIndex + numberOfColumns - 1;
}
//Move source columns and data to destination
if ( targetColumnIndex > sourceColumnIndex ) {
for ( int iCol = 0; iCol < numberOfColumns; iCol++ ) {
this.columns.add( targetColumnIndex,
this.columns.remove( sourceColumnIndex ) );
for ( int iRow = 0; iRow < data.size(); iRow++ ) {
DynamicDataRow row = data.get( iRow );
row.move( targetColumnIndex,
sourceColumnIndex );
}
}
} else if ( targetColumnIndex < sourceColumnIndex ) {
for ( int iCol = 0; iCol < numberOfColumns; iCol++ ) {
this.columns.add( targetColumnIndex,
this.columns.remove( sourceColumnIndex ) );
for ( int iRow = 0; iRow < data.size(); iRow++ ) {
DynamicDataRow row = data.get( iRow );
row.move( targetColumnIndex,
sourceColumnIndex );
}
sourceColumnIndex++;
targetColumnIndex++;
}
}
//Redraw the affected columns
reindexColumns();
data.assertModelMerging();
redrawColumns( startRedrawIndex,
endRedrawIndex );
}
public void onSortData(SortDataEvent event) {
//Remove grouping, if applicable
if ( data.isGrouped() ) {
ToggleMergingEvent tme = new ToggleMergingEvent( false );
eventBus.fireEvent( tme );
}
//Sort data
List<SortConfiguration> sortConfiguration = event.getSortConfiguration();
data.sort( sortConfiguration );
redraw();
//Copy data and raise event for underlying model to update itself
List<List<CellValue< ? extends Comparable< ? >>>> changedData = new ArrayList<List<CellValue< ? extends Comparable< ? >>>>();
for ( DynamicDataRow row : data ) {
List<CellValue< ? extends Comparable< ? >>> changedRow = new ArrayList<CellValue< ? extends Comparable< ? >>>();
changedData.add( changedRow );
//[size() - 1] to exclude the Analysis column data
for ( int iCol = 0; iCol < row.size() - 1; iCol++ ) {
CellValue< ? extends Comparable< ? >> changedCell = row.get( iCol );
changedRow.add( changedCell );
}
}
UpdateModelEvent dce = new UpdateModelEvent( new Coordinate( 0,
0 ),
changedData );
eventBus.fireEvent( dce );
}
@SuppressWarnings("rawtypes")
public void onUpdateSelectedCells(UpdateSelectedCellsEvent event) {
Comparable< ? > value = event.getValue();
Map<Coordinate, List<List<CellValue< ? extends Comparable< ? >>>>> changedData = new HashMap<Coordinate, List<List<CellValue< ? extends Comparable< ? >>>>>();
List<List<CellValue< ? extends Comparable< ? >>>> changedBlock;
List<CellValue< ? extends Comparable< ? >>> changedRow;
Coordinate firstSelection = selections.first().getCoordinate();
//If selections span multiple cells, any of which are grouped we should ungroup them
boolean bUngroupCells = false;
if ( selections.size() > 1 ) {
for ( CellValue< ? extends Comparable< ? >> cell : selections ) {
if ( cell instanceof GroupedCellValue ) {
bUngroupCells = true;
break;
}
}
}
//---Update selected cells (before ungrouping otherwise selections would need to be expanded too)---
changedBlock = new ArrayList<List<CellValue< ? extends Comparable< ? >>>>();
for ( CellValue< ? extends Comparable< ? >> cell : selections ) {
changedRow = new ArrayList<CellValue< ? extends Comparable< ? >>>();
Coordinate c = cell.getCoordinate();
if ( !columns.get( c.getCol() ).isSystemControlled() ) {
data.set( c,
value );
if ( value != null ) {
cell.removeState( CellState.OTHERWISE );
}
//Copy data that is changing for an event to update the underlying model
if ( cell instanceof GroupedCellValue ) {
GroupedCellValue gcv = (GroupedCellValue) cell;
for ( int iChildValueIndex = 0; iChildValueIndex < gcv.getGroupedCells().size(); iChildValueIndex++ ) {
changedRow.add( data.get( c ) );
changedBlock.add( changedRow );
}
} else {
changedRow.add( data.get( c ) );
changedBlock.add( changedRow );
}
}
}
Coordinate originSelected = new Coordinate( rowMapper.mapToAbsoluteRow( firstSelection.getRow() ),
firstSelection.getCol() );
changedData.put( originSelected,
changedBlock );
//---Clear dependent cells' values---
final Context context = new Context( 0,
firstSelection.getCol(),
null );
final Set<Integer> dependentColumnIndexes = this.dropDownManager.getDependentColumnIndexes( context );
for ( Integer dependentColumnIndex : dependentColumnIndexes ) {
changedBlock = new ArrayList<List<CellValue< ? extends Comparable< ? >>>>();
for ( CellValue< ? extends Comparable< ? >> cell : selections ) {
changedRow = new ArrayList<CellValue< ? extends Comparable< ? >>>();
Coordinate dc = new Coordinate( cell.getCoordinate().getRow(),
dependentColumnIndex );
if ( !columns.get( dc.getCol() ).isSystemControlled() ) {
data.set( dc,
null );
if ( value != null ) {
cell.removeState( CellState.OTHERWISE );
}
//Copy data that is changing for an event to update the underlying model
if ( cell instanceof GroupedCellValue ) {
GroupedCellValue gcv = (GroupedCellValue) cell;
for ( int iChildValueIndex = 0; iChildValueIndex < gcv.getGroupedCells().size(); iChildValueIndex++ ) {
changedRow.add( data.get( dc ) );
changedBlock.add( changedRow );
}
} else {
changedRow.add( data.get( dc ) );
changedBlock.add( changedRow );
}
}
}
Coordinate originDependent = new Coordinate( rowMapper.mapToAbsoluteRow( firstSelection.getRow() ),
dependentColumnIndex );
changedData.put( originDependent,
changedBlock );
}
//Ungroup if applicable
if ( bUngroupCells ) {
for ( CellValue< ? extends Comparable< ? >> cell : selections ) {
if ( cell instanceof GroupedCellValue ) {
//Removing merging partially redraws the grid
removeModelGrouping( cell,
true );
}
}
} else if ( data.isMerged() || selections.size() > 1 ) {
//If the data is merged changes to the cells' value can cause the need for a greater range of
//rows to be redrawn as a cell's new value could cause the merged span to increase. This is also
//the only mechanism available to update multiple individual cells' values when multiple
//cells are selected.
data.assertModelMerging();
// Partial redraw
int baseRowIndex = selections.first().getCoordinate().getRow();
int minRedrawRow = findMinRedrawRow( baseRowIndex );
int maxRedrawRow = findMaxRedrawRow( baseRowIndex );
// When merged cells become unmerged (if their value is
// cleared need to ensure the re-draw range is at least
// as large as the selection range
if ( maxRedrawRow < selections.last().getCoordinate().getRow() ) {
maxRedrawRow = selections.last().getCoordinate().getRow();
}
redrawRows( minRedrawRow,
maxRedrawRow );
} else {
//Redraw a single row
int baseRowIndex = selections.first().getCoordinate().getRow();
redrawRows( baseRowIndex,
baseRowIndex );
}
//Re-select applicable cells, following change to merge
startSelecting( firstSelection );
//Raise event for underlying model to update itself, converting logical row to physical
UpdateModelEvent dce = new UpdateModelEvent( changedData );
eventBus.fireEvent( dce );
}
@SuppressWarnings("rawtypes")
public void onCellStateChanged(CellStateChangedEvent event) {
Set<CellStateOperation> states = event.getStates();
boolean bUngroupCells = false;
Coordinate selection = selections.first().getCoordinate();
List<List<CellValue< ? extends Comparable< ? >>>> changedData = new ArrayList<List<CellValue< ? extends Comparable< ? >>>>();
//If selections span multiple cells, any of which are grouped we should ungroup them
if ( selections.size() > 1 ) {
for ( CellValue< ? extends Comparable< ? >> cell : selections ) {
if ( cell instanceof GroupedCellValue ) {
bUngroupCells = true;
break;
}
}
}
// Update underlying data (update before ungrouping as selections would need to be expanded too)
for ( CellValue< ? extends Comparable< ? >> cell : selections ) {
Coordinate c = cell.getCoordinate();
if ( !columns.get( c.getCol() ).isSystemControlled() ) {
CellValue< ? extends Comparable< ? >> cv = data.get( c );
for ( CellStateOperation state : states ) {
switch ( state.getOperation() ) {
case ADD :
cv.addState( state.getState() );
if ( state.getState() == CellState.OTHERWISE ) {
cv.setValue( null );
}
break;
case REMOVE :
cv.removeState( state.getState() );
break;
}
}
//Copy data that is changing for an event to update the underlying model
if ( cell instanceof GroupedCellValue ) {
GroupedCellValue gcv = (GroupedCellValue) cell;
for ( int iChildValueIndex = 0; iChildValueIndex < gcv.getGroupedCells().size(); iChildValueIndex++ ) {
List<CellValue< ? extends Comparable< ? >>> changedRow = new ArrayList<CellValue< ? extends Comparable< ? >>>();
changedRow.add( data.get( c ) );
changedData.add( changedRow );
}
} else {
List<CellValue< ? extends Comparable< ? >>> changedRow = new ArrayList<CellValue< ? extends Comparable< ? >>>();
changedRow.add( data.get( c ) );
changedData.add( changedRow );
}
}
}
//Ungroup if applicable
if ( bUngroupCells ) {
for ( CellValue< ? extends Comparable< ? >> cell : selections ) {
if ( cell instanceof GroupedCellValue ) {
//Removing merging partially redraws the grid
removeModelGrouping( cell,
true );
}
}
} else {
data.assertModelMerging();
// Partial redraw
int baseRowIndex = selections.first().getCoordinate().getRow();
int minRedrawRow = findMinRedrawRow( baseRowIndex );
int maxRedrawRow = findMaxRedrawRow( baseRowIndex );
// When merged cells become unmerged (if their value is
// cleared need to ensure the re-draw range is at least
// as large as the selection range
if ( maxRedrawRow < selections.last().getCoordinate().getRow() ) {
maxRedrawRow = selections.last().getCoordinate().getRow();
}
redrawRows( minRedrawRow,
maxRedrawRow );
}
//Re-select applicable cells, following change to merge
startSelecting( selection );
//Raise event for underlying model to update itself
UpdateModelEvent dce = new UpdateModelEvent( selections.first().getCoordinate(),
changedData );
eventBus.fireEvent( dce );
}
}