/*
* Copyright 2012 Red Hat, Inc. and/or its affiliates.
*
* 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.kie.workbench.common.widgets.decoratedgrid.client.widget;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.Style.Visibility;
import com.google.gwt.dom.client.TableElement;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.logical.shared.HasResizeHandlers;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.CellPanel;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.Widget;
import org.kie.workbench.common.widgets.decoratedgrid.client.widget.events.AfterColumnDeleted;
import org.kie.workbench.common.widgets.decoratedgrid.client.widget.events.AfterColumnInserted;
import org.kie.workbench.common.widgets.decoratedgrid.client.widget.events.DeleteColumnEvent;
import org.kie.workbench.common.widgets.decoratedgrid.client.widget.events.InsertInternalColumnEvent;
import org.kie.workbench.common.widgets.decoratedgrid.client.widget.events.MoveColumnsEvent;
import org.kie.workbench.common.widgets.decoratedgrid.client.widget.events.SetColumnVisibilityEvent;
import org.kie.workbench.common.widgets.decoratedgrid.client.widget.events.SetInternalModelEvent;
import org.kie.workbench.common.widgets.decoratedgrid.client.widget.events.UpdateColumnDefinitionEvent;
/**
* An abstract "Header" widget to decorate a <code>DecoratedGridWidget</code>
* @param <M> The domain model represented by the Header
* @param <T> The type of domain columns represented by the Header
*/
public abstract class AbstractDecoratedGridHeaderWidget<M, T> extends CellPanel
implements
HasResizeHandlers,
SetInternalModelEvent.Handler<M, T>,
DeleteColumnEvent.Handler,
InsertInternalColumnEvent.Handler<T>,
SetColumnVisibilityEvent.Handler,
UpdateColumnDefinitionEvent.Handler,
MoveColumnsEvent.Handler {
/**
* Container class for information relating to re-size operations
*/
public class ResizerInformation {
private boolean isResizePrimed = false;
private boolean isResizing = false;
private int resizeColumnLeft = 0;
private DynamicColumn<T> resizeColumn = null;
private int resizeColumnWidth = 0;
/**
* @param resizeColumn the resizeColumn to set
*/
public void setResizeColumn( DynamicColumn<T> resizeColumn ) {
this.resizeColumn = resizeColumn;
}
/**
* @param resizeColumnLeft the resizeColumnLeft to set
*/
public void setResizeColumnLeft( int resizeColumnLeft ) {
this.resizeColumnLeft = resizeColumnLeft;
}
/**
* @param isResizePrimed the bResizePrimed to set
*/
public void setResizePrimed( boolean isResizePrimed ) {
this.isResizePrimed = isResizePrimed;
}
}
private static final int MIN_COLUMN_WIDTH = 16;
protected Panel panel;
//Model
protected M model;
protected List<DynamicColumn<T>> sortableColumns = new ArrayList<DynamicColumn<T>>();
protected final boolean isReadOnly;
// Resources
protected ResourcesProvider<T> resources;
protected EventBus eventBus;
// Column resizing
private ResizerInformation resizerInfo = new ResizerInformation();
private DivElement resizer;
private UIObject parent;
/**
* Construct a "Header" for the provided DecoratedGridWidget
* @param resources
* @param eventBus
*/
public AbstractDecoratedGridHeaderWidget( ResourcesProvider<T> resources,
boolean isReadOnly,
EventBus eventBus ) {
if ( resources == null ) {
throw new IllegalArgumentException( "resources cannot be null" );
}
if ( eventBus == null ) {
throw new IllegalArgumentException( "eventBus cannot be null" );
}
this.resources = resources;
this.isReadOnly = isReadOnly;
this.eventBus = eventBus;
// Container DIV in which the components will live
Element tre = DOM.createTR();
Element tce = DOM.createTD();
Element div = DOM.createDiv();
div.getStyle().setPosition( Position.RELATIVE );
getBody().getParentElement().<TableElement>cast().setCellSpacing( 0 );
getBody().getParentElement().<TableElement>cast().setCellPadding( 0 );
tce.appendChild( div );
tre.appendChild( tce );
getBody().appendChild( tre );
// Widgets within the container
panel = new ScrollPanel();
// We don't want scroll bars on the ScrollPanel so hide any overflow
panel.getElement().getStyle().setOverflow( Overflow.HIDDEN );
panel.add( getHeaderWidget() );
add( panel,
div );
// Column resizing
resizer = DOM.createDiv().<DivElement>cast();
resizer.addClassName( resources.headerResizer() );
resizer.getStyle().setTop( 0,
Unit.PX );
// Add the resizer to the outer most container, otherwise it gets
// truncated by the ScrollPanel as it hides any overflow
div.appendChild( resizer );
addDomHandler( new MouseMoveHandler() {
public void onMouseMove( MouseMoveEvent event ) {
int mx = event.getClientX();
if ( resizerInfo.isResizing ) {
if ( mx - resizerInfo.resizeColumnLeft < MIN_COLUMN_WIDTH ) {
event.preventDefault();
return;
}
setResizerDimensions( event.getX() );
resizerInfo.resizeColumnWidth = mx - resizerInfo.resizeColumnLeft;
resizeColumn( resizerInfo.resizeColumn,
resizerInfo.resizeColumnWidth );
// Second call to set dimensions as a column resize can add (or remove) a scroll bar
// to (or from) the Decision Table and our resizer needs to be redrawn accordingly.
// Just having the call to set dimensions after the column has been resized added
// excess flicker to movement of the resizer.
setResizerDimensions( event.getX() );
event.preventDefault();
} else {
resizerInfo = getResizerInformation( mx );
}
}
},
MouseMoveEvent.getType() );
addDomHandler( new MouseDownHandler() {
public void onMouseDown( MouseDownEvent event ) {
if ( !resizerInfo.isResizePrimed ) {
return;
}
int mx = event.getX();
resizerInfo.isResizing = true;
setResizerDimensions( mx );
resizer.getStyle().setVisibility( Visibility.VISIBLE );
event.preventDefault();
}
},
MouseDownEvent.getType() );
addDomHandler( new MouseUpHandler() {
public void onMouseUp( MouseUpEvent event ) {
if ( !resizerInfo.isResizing ) {
return;
}
resizerInfo.isResizing = false;
resizerInfo.isResizePrimed = false;
resizer.getStyle().setVisibility( Visibility.HIDDEN );
event.preventDefault();
}
},
MouseUpEvent.getType() );
addDomHandler( new MouseOutHandler() {
public void onMouseOut( MouseOutEvent event ) {
if ( !resizerInfo.isResizing ) {
return;
}
resizerInfo.isResizing = false;
resizerInfo.isResizePrimed = false;
resizer.getStyle().setVisibility( Visibility.HIDDEN );
event.preventDefault();
}
},
MouseOutEvent.getType() );
//Wire-up other event handlers
eventBus.addHandler( DeleteColumnEvent.TYPE,
this );
eventBus.addHandler( SetColumnVisibilityEvent.TYPE,
this );
eventBus.addHandler( UpdateColumnDefinitionEvent.TYPE,
this );
eventBus.addHandler( MoveColumnsEvent.TYPE,
this );
}
public HandlerRegistration addResizeHandler( ResizeHandler handler ) {
if ( handler == null ) {
throw new IllegalArgumentException( "handler cannot be null" );
}
return addHandler( handler,
ResizeEvent.getType() );
}
/**
* Redraw entire header
*/
public abstract void redraw();
/**
* Set scroll position to enable some degree of synchronisation between
* DecisionTable and DecisionTableHeader
* @param position
*/
public abstract void setScrollPosition( int position );
@Override
public void setWidth( String width ) {
// Set the width of our ScrollPanel too; to prevent the containing
// DIV from extending it's width to accommodate the increase in size
super.setWidth( width );
panel.setWidth( width );
}
// Bit of a hack to ensure the resizer is the correct size. The
// Decision Table itself could be contained in an outer most DIV
// that hides any overflow however the horizontal scrollbar
// would be rendered inside the DIV and hence still be covered
// by the resizer.
private void setResizerDimensions( final int position ) {
resizer.getStyle().setHeight( parent.getElement().getClientHeight(),
Unit.PX );
resizer.getStyle().setLeft( position,
Unit.PX );
}
void setSidebar( UIObject parent ) {
this.parent = parent;
}
/**
* Get the Widget that should be wrapped by the scroll panel and resize
* handlers. The widget renders the actual "header" embedded within the
* decorations provided by this class: scroll-bars and resizing support.
* @return
*/
protected abstract Widget getHeaderWidget();
/**
* Given the X-coordinate check whether resizing of any column should be
* enabled. The ResizerInformation return value contains necessary
* information for this decorating class to perform column-resizing.
* @param mx the MouseMoveEvent.event.getClientX() coordinate
* @return
*/
protected abstract ResizerInformation getResizerInformation( int mx );
/**
* Resize the Header column
* @param resizeColumn
* @param resizeColumnWidth
*/
protected abstract void resizeColumn( DynamicColumn<T> resizeColumn,
int resizeColumnWidth );
/**
* Update sort order. The column clicked becomes the primary sort column.
* and the other, previously sorted, columns degrade in priority
* @param column
*/
protected void updateSortOrder( DynamicColumn<T> column ) {
int sortIndex;
TreeMap<Integer, DynamicColumn<T>> sortedColumns = new TreeMap<Integer, DynamicColumn<T>>();
switch ( column.getSortIndex() ) {
case -1:
//A new column is added to the sort group
for ( DynamicColumn<T> sortedColumn : sortableColumns ) {
if ( sortedColumn.getSortDirection() != SortDirection.NONE ) {
sortedColumns.put( sortedColumn.getSortIndex(),
sortedColumn );
}
}
sortIndex = 1;
for ( DynamicColumn<T> sortedColumn : sortedColumns.values() ) {
sortedColumn.setSortIndex( sortIndex );
sortIndex++;
}
column.setSortIndex( 0 );
column.setSortDirection( SortDirection.ASCENDING );
break;
case 0:
//The existing "lead" column's sort direction is changed
if ( column.getSortDirection() == SortDirection.ASCENDING ) {
column.setSortDirection( SortDirection.DESCENDING );
} else if ( column.getSortDirection() == SortDirection.DESCENDING ) {
column.setSortDirection( SortDirection.NONE );
column.clearSortIndex();
for ( DynamicColumn<T> sortedColumn : sortableColumns ) {
if ( sortedColumn.getSortDirection() != SortDirection.NONE ) {
sortedColumns.put( sortedColumn.getSortIndex(),
sortedColumn );
}
}
sortIndex = 0;
for ( DynamicColumn<T> sortedColumn : sortedColumns.values() ) {
sortedColumn.setSortIndex( sortIndex );
sortIndex++;
}
}
break;
default:
//An existing column is promoted to "lead"
for ( DynamicColumn<T> sortedColumn : sortableColumns ) {
if ( sortedColumn.getSortDirection() != SortDirection.NONE ) {
if ( !sortedColumn.equals( column ) ) {
sortedColumns.put( sortedColumn.getSortIndex() + 1,
sortedColumn );
}
}
}
column.setSortIndex( 0 );
sortIndex = 1;
for ( DynamicColumn<T> sortedColumn : sortedColumns.values() ) {
sortedColumn.setSortIndex( sortIndex );
sortIndex++;
}
break;
}
}
/**
* Get the column sorting configuration. The list contains an entry for each
* column on which the data should be sorted.
* @return
*/
protected List<SortConfiguration> getSortConfiguration() {
List<SortConfiguration> sortConfiguration = new ArrayList<SortConfiguration>();
List<DynamicColumn<T>> columns = sortableColumns;
for ( DynamicColumn<T> column : columns ) {
SortConfiguration sc = column.getSortConfiguration();
if ( sc.getSortIndex() != -1 ) {
sortConfiguration.add( sc );
}
}
return sortConfiguration;
}
public void onDeleteColumn( final DeleteColumnEvent event ) {
for ( int iCol = 0; iCol < event.getNumberOfColumns(); iCol++ ) {
sortableColumns.remove( event.getFirstColumnIndex() );
}
Scheduler.get().scheduleFinally( new Command() {
public void execute() {
redraw();
eventBus.fireEvent( new AfterColumnDeleted(
event.getFirstColumnIndex(),
event.getNumberOfColumns()
) );
}
} );
}
public void onInsertInternalColumn( final InsertInternalColumnEvent<T> event ) {
int iCol = event.getIndex();
for ( DynamicColumn<T> column : event.getColumns() ) {
sortableColumns.add( iCol++,
column );
}
Scheduler.get().scheduleFinally( new Command() {
public void execute() {
redraw();
eventBus.fireEvent( new AfterColumnInserted(event.getIndex()) );
}
} );
}
public void onSetColumnVisibility( SetColumnVisibilityEvent event ) {
Scheduler.get().scheduleFinally( new Command() {
public void execute() {
redraw();
}
} );
}
public void onUpdateColumnDefinition( UpdateColumnDefinitionEvent event ) {
Scheduler.get().scheduleFinally( new Command() {
public void execute() {
redraw();
}
} );
}
public void onMoveColumns( MoveColumnsEvent event ) {
int sourceColumnIndex = event.getSourceColumnIndex();
int targetColumnIndex = event.getTargetColumnIndex();
int numberOfColumns = event.getNumberOfColumns();
//Move source columns to destination
if ( targetColumnIndex > sourceColumnIndex ) {
for ( int iCol = 0; iCol < numberOfColumns; iCol++ ) {
this.sortableColumns.add( targetColumnIndex,
this.sortableColumns.remove( sourceColumnIndex ) );
}
} else if ( targetColumnIndex < sourceColumnIndex ) {
for ( int iCol = 0; iCol < numberOfColumns; iCol++ ) {
this.sortableColumns.add( targetColumnIndex,
this.sortableColumns.remove( sourceColumnIndex ) );
sourceColumnIndex++;
targetColumnIndex++;
}
}
//Redraw
Scheduler.get().scheduleFinally( new Command() {
public void execute() {
redraw();
}
} );
}
}