/*
* 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.Set;
import org.drools.guvnor.client.messages.Constants;
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.DynamicDataRow;
import org.drools.guvnor.client.widgets.drools.decoratedgrid.events.UpdateSelectedCellsEvent;
import com.google.gwt.cell.client.Cell;
import com.google.gwt.cell.client.Cell.Context;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.TableCellElement;
import com.google.gwt.dom.client.TableRowElement;
import com.google.gwt.dom.client.TableSectionElement;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
/**
* A Vertical implementation of MergableGridWidget, that renders columns as erm,
* columns and rows as rows. Supports merging of cells between rows.
*/
public abstract class AbstractVerticalMergableGridWidget<M, T> extends AbstractMergableGridWidget<M, T> {
//Deferred binding creates an appropriate class depending on browser
private CellHeightCalculatorImpl cellHeightCalculator = GWT.create( CellHeightCalculatorImpl.class );
public AbstractVerticalMergableGridWidget(ResourcesProvider<T> resources,
AbstractCellFactory<T> cellFactory,
AbstractCellValueFactory<T, ? > cellValueFactory,
CellTableDropDownDataValueMapProvider dropDownManager, boolean isReadOnly,
EventBus eventBus) {
super( resources,
cellFactory,
cellValueFactory,
dropDownManager,
isReadOnly,
eventBus );
}
@Override
public void onBrowserEvent(Event event) {
String eventType = event.getType();
// Get the event target
EventTarget eventTarget = event.getEventTarget();
if ( !Element.is( eventTarget ) ) {
return;
}
Element target = event.getEventTarget().cast();
//Check whether "group" widget has been clicked
boolean bGroupWidgetClick = isGroupWidgetClicked( event,
target );
// Find the cell where the event occurred.
TableCellElement eventTableCell = findNearestParentCell( target );
if ( eventTableCell == null ) {
return;
}
int htmlCol = eventTableCell.getCellIndex();
Element trElem = eventTableCell.getParentElement();
if ( trElem == null ) {
return;
}
TableRowElement tr = TableRowElement.as( trElem );
int htmlRow = tr.getSectionRowIndex();
// Convert HTML coordinates to physical coordinates
CellValue< ? > htmlCell = data.get( htmlRow ).get( htmlCol );
Coordinate eventPhysicalCoordinate = htmlCell.getPhysicalCoordinate();
CellValue< ? > eventPhysicalCell = data.get( eventPhysicalCoordinate.getRow() ).get( eventPhysicalCoordinate.getCol() );
//Event handlers
if ( eventType.equals( "mousedown" ) ) {
handleMousedownEvent( event,
eventPhysicalCoordinate,
bGroupWidgetClick );
return;
} else if ( eventType.equals( "mousemove" ) ) {
handleMousemoveEvent( event,
eventPhysicalCoordinate );
return;
} else if ( eventType.equals( "mouseup" ) ) {
handleMouseupEvent( event,
eventPhysicalCoordinate );
return;
} else if ( eventType.equals( "keydown" ) ) {
handleKeyboardNavigationEvent( event );
if ( event.getKeyCode() == KeyCodes.KEY_ENTER ) {
// Enter key is a special case; as the selected cell needs to be
// sent events and not the cell that GWT deemed the target for
// events.
switch ( rangeDirection ) {
case UP :
eventPhysicalCell = selections.first();
break;
case DOWN :
eventPhysicalCell = selections.last();
break;
}
eventPhysicalCoordinate = eventPhysicalCell.getCoordinate();
eventTableCell = tbody.getRows().getItem( eventPhysicalCell.getHtmlCoordinate().getRow() ).getCells().getItem( eventPhysicalCell.getHtmlCoordinate().getCol() );
}
}
// Pass event and physical cell to Cell Widget for handling
Cell<CellValue< ? extends Comparable< ? >>> cellWidget = columns.get( eventPhysicalCoordinate.getCol() ).getCell();
// Implementations of AbstractCell aren't forced to initialise consumed events
Set<String> consumedEvents = cellWidget.getConsumedEvents();
if ( consumedEvents != null && consumedEvents.contains( eventType ) ) {
Context context = new Context( eventPhysicalCoordinate.getRow(),
eventPhysicalCoordinate.getCol(),
eventPhysicalCoordinate );
//The element containing the cell's HTML is nested inside two DIVs
Element parent = eventTableCell.getFirstChildElement().getFirstChildElement();
cellWidget.onBrowserEvent( context,
parent,
eventPhysicalCell,
event,
null );
}
}
@Override
protected void redraw() {
TableSectionElement nbody = Document.get().createTBodyElement();
for ( int iRow = 0; iRow < data.size(); iRow++ ) {
DynamicDataRow rowData = data.get( iRow );
TableRowElement tre = Document.get().createTRElement();
tre.setClassName( getRowStyle( iRow ) );
populateTableRowElement( tre,
rowData );
nbody.appendChild( tre );
}
// Update table to DOM
table.replaceChild( nbody,
tbody );
tbody = nbody;
}
@Override
void redrawColumns(int startRedrawIndex,
int endRedrawIndex) {
if ( startRedrawIndex < 0 ) {
throw new IllegalArgumentException( "startRedrawIndex cannot be less than zero." );
}
if ( startRedrawIndex > columns.size() ) {
throw new IllegalArgumentException( "startRedrawIndex cannot be greater than the number of defined columns." );
}
if ( endRedrawIndex < 0 ) {
throw new IllegalArgumentException( "endRedrawIndex cannot be less than zero." );
}
if ( endRedrawIndex > columns.size() ) {
throw new IllegalArgumentException( "endRedrawIndex cannot be greater than the number of defined columns." );
}
if ( startRedrawIndex > endRedrawIndex ) {
throw new IllegalArgumentException( "startRedrawIndex cannot be greater than endRedrawIndex." );
}
for ( int iRow = 0; iRow < data.size(); iRow++ ) {
TableRowElement tre = tbody.getRows().getItem( iRow );
DynamicDataRow rowData = data.get( iRow );
redrawTableRowElement( rowData,
tre,
startRedrawIndex,
endRedrawIndex );
}
}
@Override
public void resizeColumn(DynamicColumn< ? > col,
int width) {
if ( col == null ) {
throw new IllegalArgumentException( "col cannot be null" );
}
if ( width < 0 ) {
throw new IllegalArgumentException( "width cannot be less than zero" );
}
col.setWidth( width );
int iCol = col.getColumnIndex();
for ( DynamicDataRow row : data ) {
CellValue< ? extends Comparable< ? >> cell = row
.get( iCol );
Coordinate c = cell.getHtmlCoordinate();
TableRowElement tre = tbody.getRows().getItem( c.getRow() );
TableCellElement tce = tre.getCells().getItem( c.getCol() );
DivElement div = tce.getFirstChild().<DivElement> cast();
DivElement divText = tce.getFirstChild().getFirstChild().<DivElement> cast();
// Set widths
tce.getStyle().setWidth( width,
Unit.PX );
div.getStyle().setWidth( width,
Unit.PX );
divText.getStyle().setWidth( width,
Unit.PX );
}
}
// Find the cell that contains the element. Note that the TD element is not
// the parent. The parent is the div inside the TD cell.
private TableCellElement findNearestParentCell(Element elem) {
while ( (elem != null)
&& (elem != table) ) {
String tagName = elem.getTagName();
if ( "td".equalsIgnoreCase( tagName )
|| "th".equalsIgnoreCase( tagName ) ) {
return elem.cast();
}
elem = elem.getParentElement();
}
return null;
}
// Row styles need to be re-applied after inserting and deleting rows
private void fixRowStyles(int iRow) {
while ( iRow < tbody.getChildCount() ) {
Element e = Element.as( tbody.getChild( iRow ) );
TableRowElement tre = TableRowElement.as( e );
tre.setClassName( getRowStyle( iRow ) );
iRow++;
}
}
// Get style applicable to row
private String getRowStyle(int iRow) {
String evenRowStyle = resources.cellTableEvenRow();
String oddRowStyle = resources.cellTableOddRow();
boolean isEven = iRow % 2 == 0;
String trClasses = isEven ? evenRowStyle : oddRowStyle;
return trClasses;
}
//Handle "Key Down" events relating to keyboard navigation
private void handleKeyboardNavigationEvent(Event event) {
if ( event.getKeyCode() == KeyCodes.KEY_DELETE && !isReadOnly ) {
for ( CellValue< ? > cell : selections ) {
cell.removeState( CellState.OTHERWISE );
}
eventBus.fireEvent( new UpdateSelectedCellsEvent( null ) );
return;
} else if ( event.getKeyCode() == KeyCodes.KEY_RIGHT
|| (event.getKeyCode() == KeyCodes.KEY_TAB && !event.getShiftKey()) ) {
moveSelection( MOVE_DIRECTION.RIGHT );
event.preventDefault();
return;
} else if ( event.getKeyCode() == KeyCodes.KEY_LEFT
|| (event.getKeyCode() == KeyCodes.KEY_TAB && event.getShiftKey()) ) {
moveSelection( MOVE_DIRECTION.LEFT );
event.preventDefault();
return;
} else if ( event.getKeyCode() == KeyCodes.KEY_UP ) {
if ( event.getShiftKey() ) {
extendSelection( MOVE_DIRECTION.UP );
} else {
moveSelection( MOVE_DIRECTION.UP );
}
event.preventDefault();
return;
} else if ( event.getKeyCode() == KeyCodes.KEY_DOWN ) {
if ( event.getShiftKey() ) {
extendSelection( MOVE_DIRECTION.DOWN );
} else {
moveSelection( MOVE_DIRECTION.DOWN );
}
event.preventDefault();
return;
}
}
//Handle "Mouse Down" events
private void handleMousedownEvent(Event event,
Coordinate eventCoordinate,
boolean bGroupWidgetClick) {
if ( event.getButton() == NativeEvent.BUTTON_LEFT ) {
if ( bGroupWidgetClick ) {
groupCells( eventCoordinate );
} else if ( event.getShiftKey() ) {
// Shift-click range selection
extendSelection( eventCoordinate );
return;
} else {
//Start of potential mouse-drag select operation
startSelecting( eventCoordinate );
bDragOperationPrimed = true;
return;
}
}
}
//Handle "Mouse Move" events
private void handleMousemoveEvent(Event event,
Coordinate eventCoordinate) {
if ( event.getButton() == NativeEvent.BUTTON_LEFT ) {
if ( bDragOperationPrimed ) {
extendSelection( eventCoordinate );
return;
}
}
}
//Handle "Mouse Up" events
private void handleMouseupEvent(Event event,
Coordinate eventCoordinate) {
bDragOperationPrimed = false;
}
// Build a TableCellElement
@SuppressWarnings("rawtypes")
private TableCellElement makeTableCellElement(int iCol,
DynamicDataRow rowData) {
TableCellElement tce = null;
// Column to handle rendering
DynamicColumn<T> column = columns.get( iCol );
CellValue< ? extends Comparable< ? >> cellData = rowData.get( iCol );
int rowSpan = cellData.getRowSpan();
if ( rowSpan > 0 ) {
// Use Elements rather than Templates as it's easier to set attributes that need to be dynamic
tce = Document.get().createTDElement();
DivElement div = Document.get().createDivElement();
DivElement divText = Document.get().createDivElement();
tce.addClassName( resources.cellTableCell() );
tce.addClassName( resources.cellTableColumn( column.getModelColumn() ) );
div.setClassName( resources.cellTableCellDiv() );
divText.addClassName( resources.cellTableTextDiv() );
// Set widths
int colWidth = column.getWidth();
div.getStyle().setWidth( colWidth,
Unit.PX );
divText.getStyle().setWidth( colWidth,
Unit.PX );
tce.getStyle().setWidth( colWidth,
Unit.PX );
// Set heights, TD includes border, DIV does not
int divHeight = cellHeightCalculator.calculateHeight( rowSpan );
div.getStyle().setHeight( divHeight,
Unit.PX );
tce.setRowSpan( rowSpan );
//Styling depending upon state
if ( cellData.isOtherwise() ) {
tce.addClassName( resources.cellTableCellOtherwise() );
} else {
tce.removeClassName( resources.cellTableCellOtherwise() );
}
if ( cellData instanceof GroupedCellValue ) {
GroupedCellValue gcv = (GroupedCellValue) cellData;
if ( gcv.hasMultipleValues() ) {
tce.addClassName( resources.cellTableCellMultipleValues() );
}
} else {
tce.removeClassName( resources.cellTableCellMultipleValues() );
}
if ( cellData.isSelected() ) {
tce.addClassName( resources.cellTableCellSelected() );
} else {
tce.removeClassName( resources.cellTableCellSelected() );
}
// Render the cell and set inner HTML
SafeHtmlBuilder cellBuilder = new SafeHtmlBuilder();
if ( !cellData.isOtherwise() ) {
Coordinate c = cellData.getCoordinate();
Context context = new Context( c.getRow(),
c.getCol(),
c );
column.render( context,
rowData,
cellBuilder );
} else {
cellBuilder.appendEscaped( "<otherwise>" );
}
divText.setInnerHTML( cellBuilder.toSafeHtml().asString() );
// Construct the table
tce.appendChild( div );
div.appendChild( divText );
tce.setTabIndex( 0 );
//Add on "Grouping" widget, if applicable
if ( rowSpan > 1 || cellData.isGrouped() ) {
Element de = DOM.createDiv();
DivElement divGroup = DivElement.as( de );
divGroup.setTitle( Constants.INSTANCE.groupCells() );
divGroup.addClassName( resources.cellTableGroupDiv() );
if ( cellData.isGrouped() ) {
divGroup.setInnerHTML( selectorUngroupedCellsHtml );
} else {
divGroup.setInnerHTML( selectorGroupedCellsHtml );
}
div.appendChild( divGroup );
}
}
return tce;
}
// Populate the content of a TableRowElement. This is used to populate
// new, empty, TableRowElements with complete rows for insertion into an
// HTML table based upon visible columns
private TableRowElement populateTableRowElement(TableRowElement tre,
DynamicDataRow rowData) {
tre.getStyle().setHeight( resources.rowHeight(),
Unit.PX );
for ( int iCol = 0; iCol < columns.size(); iCol++ ) {
DynamicColumn<T> column = columns.get( iCol );
if ( column.isVisible() ) {
TableCellElement tce = makeTableCellElement( iCol,
rowData );
if ( tce != null ) {
tre.appendChild( tce );
}
}
}
return tre;
}
// Redraw a row adding new cells if necessary. This is used to populate part
// of a row from the given index onwards, when a new column has been
// inserted. It is important the indexes on the underlying data have
// been set correctly before calling as they are used to determine the
// correct HTML element in which to render a cell.
private void redrawTableRowElement(DynamicDataRow rowData,
TableRowElement tre,
int startColIndex,
int endColIndex) {
for ( int iCol = startColIndex; iCol <= endColIndex; iCol++ ) {
// Only redraw visible columns
DynamicColumn< ? > column = columns.get( iCol );
if ( column.isVisible() ) {
int maxColumnIndex = tre.getCells().getLength() - 1;
int requiredColumnIndex = rowData.get( iCol ).getHtmlCoordinate().getCol();
if ( requiredColumnIndex > maxColumnIndex ) {
// Make a new TD element
TableCellElement newCell = makeTableCellElement( iCol,
rowData );
if ( newCell != null ) {
tre.appendChild( newCell );
}
} else {
// Reuse an existing TD element
TableCellElement newCell = makeTableCellElement( iCol,
rowData );
if ( newCell != null ) {
TableCellElement oldCell = tre.getCells().getItem( requiredColumnIndex );
tre.replaceChild( newCell,
oldCell );
}
}
}
}
}
@Override
protected void createEmptyRowElement(int index) {
tbody.insertRow( index );
fixRowStyles( index );
}
@Override
protected void createRowElement(int index,
DynamicDataRow rowData) {
TableRowElement tre = tbody.insertRow( index );
populateTableRowElement( tre,
rowData );
fixRowStyles( index );
}
@Override
protected void deleteRowElement(int index) {
Node tre = tbody.getChild( index );
tbody.removeChild( tre );
}
@Override
protected void redrawRows(int startRedrawIndex,
int endRedrawIndex) {
if ( startRedrawIndex < 0 ) {
throw new IllegalArgumentException( "startRedrawIndex cannot be less than zero." );
}
if ( startRedrawIndex > data.size() ) {
throw new IllegalArgumentException( "startRedrawIndex cannot be greater than the number of rows in the table." );
}
if ( endRedrawIndex < 0 ) {
throw new IllegalArgumentException( "endRedrawIndex cannot be less than zero." );
}
if ( endRedrawIndex > data.size() ) {
throw new IllegalArgumentException( "endRedrawIndex cannot be greater than the number of rows in the table." );
}
if ( startRedrawIndex > endRedrawIndex ) {
throw new IllegalArgumentException( "startRedrawIndex cannot be greater than endRedrawIndex." );
}
//Redraw replacement rows
for ( int iRow = startRedrawIndex; iRow <= endRedrawIndex; iRow++ ) {
DynamicDataRow rowData = data.get( iRow );
TableRowElement tre = Document.get().createTRElement();
populateTableRowElement( tre,
rowData );
tbody.replaceChild( tre,
tbody.getChild( iRow ) );
}
fixRowStyles( startRedrawIndex );
}
@Override
protected void removeRowElement(int index) {
if ( index < 0 ) {
throw new IllegalArgumentException( "Index cannot be less than zero." );
}
if ( index > data.size() ) {
throw new IllegalArgumentException( "Index cannot be greater than the number of rows." );
}
tbody.deleteRow( index );
fixRowStyles( index );
}
@Override
@SuppressWarnings({"rawtypes"})
void deselectCell(CellValue< ? extends Comparable< ? >> cell) {
if ( cell == null ) {
throw new IllegalArgumentException( "cell cannot be null" );
}
Coordinate hc = cell.getHtmlCoordinate();
TableRowElement tre = tbody.getRows().getItem( hc.getRow() )
.<TableRowElement> cast();
TableCellElement tce = tre.getCells().getItem( hc.getCol() )
.<TableCellElement> cast();
//Merging, grouping etc could have led to the selected HTML cell disappearing
if ( tce != null ) {
String cellSelectedStyle = resources.cellTableCellSelected();
String cellMultipleValuesStyle = resources.cellTableCellMultipleValues();
String cellOtherwiseStyle = resources.cellTableCellOtherwise();
tce.removeClassName( cellSelectedStyle );
//Re-apply applicable styling
if ( cell.isOtherwise() ) {
tce.addClassName( cellOtherwiseStyle );
}
if ( cell instanceof GroupedCellValue ) {
GroupedCellValue gcv = (GroupedCellValue) cell;
if ( gcv.hasMultipleValues() ) {
tce.addClassName( cellMultipleValuesStyle );
}
}
}
}
@Override
void hideColumn(int index) {
if ( index < 0 ) {
throw new IllegalArgumentException( "index cannot be less than zero" );
}
if ( index > columns.size() ) {
throw new IllegalArgumentException( "index cannot be greater than the number of rows" );
}
for ( int iRow = 0; iRow < data.size(); iRow++ ) {
DynamicDataRow rowData = data.get( iRow );
CellValue< ? extends Comparable< ? >> cell = rowData.get( index );
if ( cell.getRowSpan() > 0 ) {
Coordinate hc = cell.getHtmlCoordinate();
TableRowElement tre = tbody.getRows().getItem( hc.getRow() );
TableCellElement tce = tre.getCells().getItem( hc.getCol() );
tre.removeChild( tce );
}
}
}
@Override
void selectCell(CellValue< ? extends Comparable< ? >> cell) {
if ( cell == null ) {
throw new IllegalArgumentException( "cell cannot be null" );
}
Coordinate hc = cell.getHtmlCoordinate();
TableRowElement tre = tbody.getRows().getItem( hc.getRow() )
.<TableRowElement> cast();
TableCellElement tce = tre.getCells().getItem( hc.getCol() )
.<TableCellElement> cast();
//Cell selected style takes precedence
String cellSelectedStyle = resources.cellTableCellSelected();
String cellOtherwiseStyle = resources.cellTableCellOtherwise();
String cellMultipleValuesStyle = resources.cellTableCellMultipleValues();
tce.removeClassName( cellMultipleValuesStyle );
tce.removeClassName( cellOtherwiseStyle );
tce.addClassName( cellSelectedStyle );
tce.focus();
}
@Override
void showColumn(int index) {
if ( index < 0 ) {
throw new IllegalArgumentException( "index cannot be less than zero" );
}
if ( index > columns.size() ) {
throw new IllegalArgumentException( "index cannot be greater than the number of rows" );
}
for ( int iRow = 0; iRow < data.size(); iRow++ ) {
DynamicDataRow rowData = data.get( iRow );
TableCellElement tce = makeTableCellElement( index,
rowData );
if ( tce != null ) {
CellValue< ? extends Comparable< ? >> cell = rowData.get( index );
Coordinate hc = cell.getHtmlCoordinate();
TableRowElement tre = tbody.getRows().getItem( hc.getRow() );
TableCellElement ntce = tre.insertCell( hc.getCol() );
tre.replaceChild( tce,
ntce );
}
}
}
}