/*
* Ext GWT 2.2.4 - Ext for GWT
* Copyright(c) 2007-2010, Ext JS, LLC.
* licensing@extjs.com
*
* http://extjs.com/license
*/
package com.extjs.gxt.ui.client.widget.grid;
import java.util.HashMap;
import java.util.Map;
import com.extjs.gxt.ui.client.GXT;
import com.extjs.gxt.ui.client.Style.SortDir;
import com.extjs.gxt.ui.client.aria.FocusFrame;
import com.extjs.gxt.ui.client.core.El;
import com.extjs.gxt.ui.client.data.ModelData;
import com.extjs.gxt.ui.client.data.ModelProcessor;
import com.extjs.gxt.ui.client.data.ModelStringProvider;
import com.extjs.gxt.ui.client.event.BaseEvent;
import com.extjs.gxt.ui.client.event.ComponentEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.GridEvent;
import com.extjs.gxt.ui.client.store.ListStore;
import com.extjs.gxt.ui.client.widget.BoxComponent;
import com.extjs.gxt.ui.client.widget.grid.GridSelectionModel.Callback;
import com.extjs.gxt.ui.client.widget.grid.GridSelectionModel.Cell;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Accessibility;
/**
* This class represents the primary interface of a component based grid
* control. The grid requires a <code>ListStore</code> and
* <code>ColumnModel</code> when constructed. Each model in the store will be
* rendered as a row in the grid. Any updates to the store are automatically
* pushed to the grid. This includes inserting, removing, sorting and filter.
*
* <p />
* Grid support several ways to manage column widths.
*
* <ol>
* <li>The most basic approach is to simply give pixel widths to each column.
* Columns widths will match the specified values.</li>
* <li>A column can be set to "fill" all available space. As the width of the
* grid changes, or columns are resized, the "filling" column's width is
* adjusted so that the column's fill the available width with no horizontal
* scrolling. See @link {@link Grid#setAutoExpandColumn(String)}.</li>
* <li>Grid can resize columns based on a "weight". As the width of the grid, or
* columns change, the "weight" is used to allocate the extra space, or the
* space needed to be reduced. Use {@link GridView#setAutoFill(boolean)} to
* enable this feature. With auto fill, the calculations are only run once.
* After the grid is rendered, the columns widths will not be adjusted when
* available width changes. You can use @link
* {@link GridView#setForceFit(boolean)} to always run the width calculations on
* any changes to available width or column sizes. Columns can be "fixed" which
* prevents their columns widths to be adjusted by the grid "weight"
* calculations. See @link {@link ColumnConfig#setFixed(boolean)}.</li>
* </ol>
*
* <p />
* When state is enabled (default is false), Grid will save and restore the
* column width, column hidden state, sort direction, and sort field. To enable
* state, see {@link #setStateful(boolean)}. When the store uses a
* <code>PagingListLoader</code> the offset and limit parameter are saved with
* the Grid's state. These 2 values can be retrieved and used to make the first
* load request to return the user to the same location they left the grid.
*
* Code snippet:
*
* <pre>
PagingLoadConfig config = new BasePagingLoadConfig();
config.setOffset(0);
config.setLimit(50);
Map<String, Object> state = grid.getState();
if (state.containsKey("offset")) {
int offset = (Integer)state.get("offset");
int limit = (Integer)state.get("limit");
config.setOffset(offset);
config.setLimit(limit);
}
if (state.containsKey("sortField")) {
config.setSortField((String)state.get("sortField"));
config.setSortDir(SortDir.valueOf((String)state.get("sortDir")));
}
loader.load(config);
* </pre>
*
* <dl>
* <dt><b>Events:</b></dt>
*
* <dd><b>CellClick</b> : GridEvent(grid, rowIndex, cellIndex, event)<br>
* <div>Fires after a cell is clicked.</div>
* <ul>
* <li>grid : this</li>
* <li>rowIndex : row index</li>
* <li>cellIndex : cell index</li>
* <li>event : the dom event</li>
* </ul>
* </dd>
*
* <dd><b>CellDoubleClick</b> : GridEvent(grid, rowIndex, cellIndex, event)<br>
* <div>Fires after a cell is double clicked.</div>
* <ul>
* <li>grid : this</li>
* <li>rowIndex : row index</li>
* <li>cellIndex : cell index</li>
* <li>event : the dom event</li>
* </ul>
* </dd>
*
* <dd><b>CellMouseDown</b> : GridEvent(grid, rowIndex, cellIndex, event)<br>
* <div>Fires before a cell is clicked.</div>
* <ul>
* <li>grid : this</li>
* <li>rowIndex : row index</li>
* <li>cellIndex : cell index</li>
* <li>event : the dom event</li>
* </ul>
* </dd>
*
* <dd><b>RowClick</b> : GridEvent(grid, rowIndex, cellIndex, event)<br>
* <div>Fires after a row is clicked.</div>
* <ul>
* <li>grid : this</li>
* <li>rowIndex : the row index</li>
* <li>cellIndex : cell index</li>
* <li>index : the cell index</li>
* <li>event : the dom event</li>
* </ul>
* </dd>
*
* <dd><b>RowDoubleClick</b> : GridEvent(grid, rowIndex, cellIndex, event)<br>
* <div>Fires after a row is double clicked.</div>
* <ul>
* <li>grid : this</li>
* <li>rowIndex : the row index</li>
* <li>index : the cell index</li>
* <li>event : the dom event</li>
* </ul>
* </dd>
*
* <dd><b>RowMouseDown</b> : GridEvent(grid, rowIndex, colIndex, event)<br>
* <div>Fires before a row is clicked.</div>
* <ul>
* <li>grid : this</li>
* <li>rowIndex : row index</li>
* <li>colIndex : column index</li>
* <li>event : the dom event</li>
* </ul>
* </dd>
*
* <dd><b>HeaderClick</b> : GridEvent(grid, rowIndex, colIndex, event)<br>
* <div>Fires a header is clicked.</div>
* <ul>
* <li>grid : this</li>
* <li>rowIndex : row index</li>
* <li>colIndex : column index</li>
* <li>event : the dom event</li>
* </ul>
* </dd>
*
* <dd><b>HeaderDoubleClick</b> : GridEvent(grid, rowIndex, colIndex, event)<br>
* <div>Fires a header is double clicked.</div>
* <ul>
* <li>grid : this</li>
* <li>rowIndex : row index</li>
* <li>colIndex : column index</li>
* <li>event : the dom event</li>
* </ul>
* </dd>
*
* <dd><b>HeaderMouseDown</b> : GridEvent(grid, rowIndex, colIndex, event)<br>
* <div>Fires before a header is clicked.</div>
* <ul>
* <li>grid : this</li>
* <li>rowIndex : row index</li>
* <li>colIndex : column index</li>
* <li>event : the dom event</li>
* </ul>
* </dd>
*
* <dd><b>ContextMenu</b> : GridEvent(grid)<br>
* <div>Fires before the grid's context menu is shown. Listeners can cancel the
* action by calling {@link BaseEvent#setCancelled(boolean)}.</div>
* <ul>
* <li>grid : this</li>
* </ul>
* </dd>
*
* <dd><b>HeaderContextMenu</b> : GridEvent(grid, colIndex, menu)<br>
* <div>Fires right before the header's context menu is displayed. Listeners can
* cancel the action by calling {@link BaseEvent#setCancelled(boolean)}.</div>
* <ul>
* <li>grid : this</li>
* <li>colIndex : the column index</li>
* <li>menu : the context menu</li>
* </ul>
* </dd>
*
* <dd><b>BodyScroll</b> : GridEvent(grid, srollLeft, scrollTop)<br>
* <div>Fires when the body element is scrolled.</div>
* <ul>
* <li>grid : this</li>
* <li>scrollLeft : scrollLeft</li>
* <li>scrollTop : scrollTop</li>
* </ul>
* </dd>
*
* <dd><b>ColumnResize</b> : GridEvent(grid, colIndex, width)<br>
* <div>Fires when the user resizes a column.</div>
* <ul>
* <li>grid : this</li>
* <li>colIndex : the column index</li>
* <li>width : the new column width</li>
* </ul>
* </dd>
*
* <dd><b>ColumnMove</b> : GridEvent(grid, colIndex, size)<br>
* <div>Fires when the user moves a column.</div>
* <ul>
* <li>grid : this</li>
* <li>oldIndex : the old column index</li>
* <li>newIndex : the new column index</li>
* </ul>
* </dd>
*
* <dd><b>SortChange</b> : GridEvent(grid, sortInfo)<br>
* <div>Fires when the grid's store sort changes.</div>
* <ul>
* <li>grid : this</li>
* <li>sortInfo : the sort field and direction</li>
* </ul>
* </dd>
*
* <dd><b>ViewReady</b> : GridEvent(grid)<br>
* <div>Fires when the grid's view is ready.</div>
* <ul>
* <li>grid : this</li>
* </ul>
* </dd>
*
* <dd><b>Reconfigure</b> : GridEvent(grid)<br>
* <div>Fires when the grid gets reconfigured.</div>
* <ul>
* <li>grid : this</li>
* </ul>
* </dd>
* </dl>
*
* <dl>
* <dt>Inherited Events:</dt>
* <dd>BoxComponent Move</dd>
* <dd>BoxComponent Resize</dd>
* <dd>Component Enable</dd>
* <dd>Component Disable</dd>
* <dd>Component BeforeHide</dd>
* <dd>Component Hide</dd>
* <dd>Component BeforeShow</dd>
* <dd>Component Show</dd>
* <dd>Component Attach</dd>
* <dd>Component Detach</dd>
* <dd>Component BeforeRender</dd>
* <dd>Component Render</dd>
* <dd>Component BrowserEvent</dd>
* <dd>Component BeforeStateRestore</dd>
* <dd>Component StateRestore</dd>
* <dd>Component BeforeStateSave</dd>
* <dd>Component SaveState</dd>
* </dl>
*
* @param <M> the model type
*/
public class Grid<M extends ModelData> extends BoxComponent {
protected ColumnModel cm;
protected EditorSupport<M> editSupport;
protected GridSelectionModel<M> sm;
protected ListStore<M> store;
protected ModelStringProvider<M> stringProvider;
protected GridView view;
protected boolean viewReady;
private String autoExpandColumn;
private int autoExpandMax = 500;
private int autoExpandMin = 25;
private boolean columnLines;
private boolean enableColumnReorder;
private boolean enableColumnResize = true;
private boolean hideHeaders;
private int lazyRowRender = 10;
private boolean loadMask;
private int minColumnWidth = 25;
private ModelProcessor<M> modelProcessor;
private boolean stripeRows;
private boolean trackMouseOver = true;
private Map<String, String> states = new HashMap<String, String>();
/**
* Creates a new grid.
*
* @param store the data store
* @param cm the column model
*/
public Grid(ListStore<M> store, ColumnModel cm) {
this.store = store;
this.cm = cm;
this.view = new GridView();
disabledStyle = null;
baseStyle = "x-grid-panel";
setSelectionModel(new GridSelectionModel<M>());
disableTextSelection(true);
}
protected Grid() {
}
@Override
public void disableTextSelection(boolean disable) {
disableTextSelection = disable ? 1 : 0;
}
/**
* Returns the auto expand column id.
*
* @return the auto expand column id
*/
public String getAutoExpandColumn() {
return autoExpandColumn;
}
/**
* Returns the auto expand maximum width.
*
* @return the max width in pixels
*/
public int getAutoExpandMax() {
return autoExpandMax;
}
/**
* Returns the auto expand minimum width.
*
* @return the minimum width in pixels
*/
public int getAutoExpandMin() {
return autoExpandMin;
}
/**
* Returns the column model.
*
* @return the column model
*/
public ColumnModel getColumnModel() {
return cm;
}
/**
* Returns the time in ms after the rows get rendered.
*
* @return the lazy row rendering time
*/
public int getLazyRowRender() {
return lazyRowRender;
}
/**
* Returns the minimum column width.
*
* @return the min width in pixels
*/
public int getMinColumnWidth() {
return minColumnWidth;
}
/**
* Returns the model processor.
*
* @return the model processor
*/
public ModelProcessor<M> getModelProcessor() {
return modelProcessor;
}
/**
* Returns the grid's selection model.
*
* @return the selection model
*/
public GridSelectionModel<M> getSelectionModel() {
return sm;
}
/**
* Returns the grid's store.
*
* @return the store
*/
public ListStore<M> getStore() {
return store;
}
/**
* Returns the grid's view.
*
* @return the grid view
*/
public GridView getView() {
return view;
}
/**
* Returns true if column lines are enabled.
*
* @return true if column lines are enabled
*/
public boolean isColumnLines() {
return columnLines;
}
/**
* Returns true if column reordering is enabled.
*
* @return true if enabled
*/
public boolean isColumnReordering() {
return enableColumnReorder;
}
/**
* Returns true if column resizing is enabled.
*
* @return true if resizing is enabled
*/
public boolean isColumnResize() {
return enableColumnResize;
}
/**
* Returns true if the header is hidden.
*
* @return true for hidden
*/
public boolean isHideHeaders() {
return hideHeaders;
}
/**
* Returns true if the load mask in enabled.
*
* @return the load mask state
*/
public boolean isLoadMask() {
return loadMask;
}
/**
* Returns true if row striping is enabled.
*
* @return the strip row state
*/
public boolean isStripeRows() {
return stripeRows;
}
/**
* Returns true if rows are highlighted on mouse over.
*
* @return the track mouse state
*/
public boolean isTrackMouseOver() {
return trackMouseOver;
}
/**
* Returns true if the view is ready.
*
* @return the view ready state
*/
public boolean isViewReady() {
return viewReady;
}
@Override
@SuppressWarnings({"unchecked", "rawtypes"})
public void onComponentEvent(ComponentEvent ce) {
super.onComponentEvent(ce);
GridEvent ge = (GridEvent) ce;
switch (ce.getEventTypeInt()) {
case Event.ONCLICK:
onClick(ge);
break;
case Event.ONDBLCLICK:
onDoubleClick(ge);
break;
case Event.ONMOUSEDOWN:
onMouseDown(ge);
break;
case Event.ONMOUSEUP:
onMouseUp(ge);
break;
case Event.ONFOCUS:
onFocus(ce);
break;
case Event.ONBLUR:
onBlur(ce);
break;
}
view.handleComponentEvent(ge);
}
/**
* Reconfigures the grid to use a different Store and Column Model. The View
* will be bound to the new objects and refreshed.
*
* @param store the new store
* @param cm the new column model
*/
public void reconfigure(ListStore<M> store, ColumnModel cm) {
if (loadMask && rendered) {
mask(GXT.MESSAGES.loadMask_msg());
}
if (rendered) {
view.initData(store, cm);
}
this.store = store;
this.cm = cm;
// rebind the sm
setSelectionModel(sm);
if (isViewReady()) {
view.refresh(true);
}
if (loadMask && rendered) {
unmask();
}
fireEvent(Events.Reconfigure);
}
/**
* The id of a column in this grid that should expand to fill unused space
* (pre-render). This id can not be 0.
*
* @param autoExpandColumn the auto expand column id
*/
public void setAutoExpandColumn(String autoExpandColumn) {
this.autoExpandColumn = autoExpandColumn;
}
/**
* The maximum width the autoExpandColumn can have (if enabled) (defaults to
* 500, pre-render).
*
* @param autoExpandMax the auto expand max
*/
public void setAutoExpandMax(int autoExpandMax) {
this.autoExpandMax = autoExpandMax;
}
/**
* The minimum width the autoExpandColumn can have (if enabled)(pre-render).
*
* @param autoExpandMin the auto expand min width
*/
public void setAutoExpandMin(int autoExpandMin) {
this.autoExpandMin = autoExpandMin;
}
/**
* True to enable column separation lines (defaults to false).
*
* @param columnLines true to enable column separation lines
*/
public void setColumnLines(boolean columnLines) {
this.columnLines = columnLines;
if (rendered) {
el().setStyleName("x-grid-with-col-lines", columnLines);
}
}
/**
* True to enable column reordering via drag and drop (defaults to false).
*
* @param enableColumnReorder true to enable
*/
public void setColumnReordering(boolean enableColumnReorder) {
this.enableColumnReorder = enableColumnReorder;
}
/**
* Sets whether columns may be resized (defaults to true).
*
* @param enableColumnResize true to allow column resizing
*/
public void setColumnResize(boolean enableColumnResize) {
this.enableColumnResize = enableColumnResize;
}
/**
* Sets whether the header should be hidden (defaults to false).
*
* @param hideHeaders true to hide the header
*/
public void setHideHeaders(boolean hideHeaders) {
this.hideHeaders = hideHeaders;
}
/**
* Sets the time in ms after the row gets rendered (defaults to 10). 0 means
* that the rows get rendered as soon as the grid gets rendered.
*
* @param lazyRowRender the time in ms after the rows get rendered.
*/
public void setLazyRowRender(int lazyRowRender) {
this.lazyRowRender = lazyRowRender;
}
/**
* Sets whether a load mask should be displayed during load operations
* (defaults to false).
*
* @param loadMask true to show a mask
*/
public void setLoadMask(boolean loadMask) {
this.loadMask = loadMask;
}
/**
* The minimum width a column can be resized to (defaults to 25).
*
* @param minColumnWidth the min column width
*/
public void setMinColumnWidth(int minColumnWidth) {
this.minColumnWidth = minColumnWidth;
}
/**
* Sets the grid's model processor.
*
* @see ModelProcessor
* @param modelProcessor
*/
public void setModelProcessor(ModelProcessor<M> modelProcessor) {
this.modelProcessor = modelProcessor;
}
/**
* Sets the grid selection model.
*
* @param sm the selection model
*/
public void setSelectionModel(GridSelectionModel<M> sm) {
if (this.sm != null) {
this.sm.bindGrid(null);
}
this.sm = sm;
if (sm != null) {
sm.bindGrid(this);
}
}
/**
* Sets the binder's string provider.
*
* @param stringProvider the string provider
*/
public void setStringProvider(ModelStringProvider<M> stringProvider) {
this.stringProvider = stringProvider;
}
/**
* True to stripe the rows (defaults to false).
*
* @param stripeRows true to strip rows
*/
public void setStripeRows(boolean stripeRows) {
this.stripeRows = stripeRows;
}
/**
* True to highlight rows when the mouse is over (defaults to true).
*
* @param trackMouseOver true to highlight rows on mouse over
*/
public void setTrackMouseOver(boolean trackMouseOver) {
this.trackMouseOver = trackMouseOver;
}
/**
* Sets the view's grid (pre-render).
*
* @param view the view
*/
public void setView(GridView view) {
this.view = view;
// rebind the sm
setSelectionModel(sm);
}
protected void afterRender() {
view.render();
super.afterRender();
if (lazyRowRender > 0) {
Timer t = new Timer() {
@Override
public void run() {
afterRenderView();
}
};
t.schedule(lazyRowRender);
} else {
afterRenderView();
}
}
protected void afterRenderView() {
viewReady = true;
view.afterRender();
onAfterRenderView();
for (String key : states.keySet()) {
setAriaState(key, states.get(key));
}
fireEvent(Events.ViewReady);
}
@Override
protected void applyState(Map<String, Object> state) {
super.applyState(state);
if (isStateful()) {
for (ColumnConfig c : cm.getColumns()) {
String id = c.getId();
if (state.containsKey("hidden" + id)) {
c.setHidden((Boolean) state.get("hidden" + id));
}
if (state.containsKey("width" + id)) {
c.setWidth((Integer) state.get("width" + id));
}
}
doApplyStoreState(state);
}
}
@Override
protected ComponentEvent createComponentEvent(Event event) {
return view.createComponentEvent(event);
}
protected void doApplyStoreState(Map<String, Object> state) {
String sortField = (String) state.get("sortField");
if (store.getLoader() == null && sortField != null) {
String sortDir = (String) state.get("sortDir");
SortDir dir = SortDir.findDir(sortDir);
store.sort(sortField, dir);
}
}
@Override
protected void doAttachChildren() {
super.doAttachChildren();
view.doAttach();
}
@Override
protected void doDetachChildren() {
super.doDetachChildren();
view.doDetach();
}
protected EditorSupport<M> getEditSupport() {
return new EditorSupport<M>();
}
@Override
protected El getFocusEl() {
if (isViewReady()) {
return view.focusEl;
} else {
return super.getFocusEl();
}
}
@Override
protected void notifyHide() {
super.notifyHide();
view.notifyHide();
}
@Override
protected void notifyShow() {
super.notifyShow();
view.notifyShow();
}
protected void onAfterRenderView() {
}
protected void onBlur(ComponentEvent ce) {
if (GXT.isFocusManagerEnabled()) {
FocusFrame.get().unframe();
}
}
protected void onClick(GridEvent<M> e) {
if (e.getRowIndex() != -1) {
fireEvent(Events.RowClick, e);
if (e.getColIndex() != -1) {
fireEvent(Events.CellClick, e);
}
}
}
@Override
protected void onDisable() {
super.onDisable();
mask();
}
protected void onDoubleClick(GridEvent<M> e) {
if (e.getRowIndex() != -1) {
fireEvent(Events.RowDoubleClick, e);
if (e.getColIndex() != -1) {
fireEvent(Events.CellDoubleClick, e);
}
}
}
@Override
protected void onEnable() {
super.onEnable();
unmask();
}
protected void onFocus(ComponentEvent ce) {
if (GXT.isFocusManagerEnabled()) {
if (getSelectionModel().selectedHeader != null) {
FocusFrame.get().frame(getSelectionModel().selectedHeader);
} else {
FocusFrame.get().frame(this);
}
}
}
protected void onMouseDown(GridEvent<M> e) {
if (isDisableTextSelection() && GXT.isWebKit) {
String tagName = e.getEvent().getEventTarget().<Element> cast().getTagName();
if (!"input".equalsIgnoreCase(tagName) && !"textarea".equalsIgnoreCase(tagName)) {
e.preventDefault();
}
}
if (e.getRowIndex() != -1) {
fireEvent(Events.RowMouseDown, e);
if (e.getColIndex() != -1) {
fireEvent(Events.CellMouseDown, e);
}
}
}
protected void onMouseUp(GridEvent<M> e) {
if (e.getRowIndex() != -1) {
fireEvent(Events.RowMouseUp, e);
if (e.getColIndex() != -1) {
fireEvent(Events.CellMouseUp, e);
}
}
}
@Override
protected void onRender(Element target, int index) {
setElement(DOM.createDiv(), target, index);
super.onRender(target, index);
el().setStyleAttribute("position", "relative");
setColumnLines(isColumnLines());
view.init(this);
el().setTabIndex(0);
el().setElementAttribute("hideFocus", "true");
if (GXT.isAriaEnabled()) {
Accessibility.setRole(getElement(), "grid");
setAriaState("aria-readonly", "true");
setAriaState("aria-multiselectable", "true");
}
}
@Override
protected void onResize(int width, int height) {
super.onResize(width, height);
if (viewReady) {
view.calculateVBar(true);
} else {
view.layout();
}
}
@Override
protected void setAriaRole(String roleName) {
if (isViewReady()) {
Accessibility.setRole(view.focusEl.dom, roleName);
}
}
protected void setAriaState(String stateName, String stateValue) {
if (isViewReady()) {
Accessibility.setState(view.focusEl.dom, stateName, stateValue);
} else {
states.put(stateName, stateValue);
}
}
protected Cell walkCells(int row, int col, int step, Callback callback, boolean acceptNavs) {
boolean first = true;
int clen = cm.getColumnCount();
int rlen = store.getCount();
if (step < 0) {
if (col < 0) {
if (GXT.isFocusManagerEnabled()) {
return new Cell(row, 0);
}
row--;
first = false;
}
while (row >= 0) {
if (!first) {
col = clen - 1;
}
first = false;
while (col >= 0) {
if (callback.isSelectable(row, col, acceptNavs)) {
return new Cell(row, col);
}
col--;
}
row--;
}
} else {
if (col == clen && GXT.isFocusManagerEnabled()) {
return new Cell(row, col - 1);
}
if (col >= clen) {
row++;
first = false;
}
while (row < rlen) {
if (!first) {
col = 0;
}
first = false;
while (col < clen) {
if (callback.isSelectable(row, col, acceptNavs)) {
return new Cell(row, col);
}
col++;
}
row++;
}
}
return null;
}
}