/*
* Copyright 2010 JBoss, a divison Red Hat, 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.jboss.errai.widgets.client;
import static com.google.gwt.user.client.DOM.setStyleAttribute;
import static com.google.gwt.user.client.ui.RootPanel.getBodyElement;
import static java.lang.Double.parseDouble;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import org.jboss.errai.widgets.client.format.WSCellFormatter;
import org.jboss.errai.widgets.client.format.WSCellSimpleTextCell;
import org.jboss.errai.widgets.client.format.WSCellTitle;
import org.jboss.errai.widgets.client.listeners.CellChangeEvent;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
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.dom.client.ScrollEvent;
import com.google.gwt.event.dom.client.ScrollHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.FocusPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasVerticalAlignment;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.RequiresResize;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
/**
* A Grid/Table implementation for working with structured data.
*/
@SuppressWarnings( { "UnusedDeclaration", "ConstantConditions" })
public class WSGrid extends Composite implements RequiresResize {
private static final int CELL_HEIGHT_PX = 18;
private FocusPanel focusPanel;
private VerticalPanel innerPanel;
private WSAbstractGrid titleBar;
private WSAbstractGrid dataGrid;
private int cols;
private Stack<WSCell> selectionList = new Stack<WSCell>();
private ArrayList<Integer> colSizes = new ArrayList<Integer>();
private Map<Integer, Boolean> sortedColumns = new HashMap<Integer, Boolean>();
private WSCell sortedColumnHeader;
private FocusManager fm = new DefaultFocusManager(this);
private int fillX;
private int fillY;
private boolean forwardDirY = true;
private boolean forwardDirX = true;
private boolean currentFocusColumn;
private boolean _leftGrow = false;
private boolean _resizeArmed = false;
private boolean _resizing = false;
private boolean _rangeSelect = false;
private boolean _mergedCells = false;
private boolean resizeOnAttach = false;
private boolean rowSelectionOnly = false;
private WSGrid wsGrid = this;
private PopupPanel resizeLine = new PopupPanel() {
@Override
public void onBrowserEvent(Event event) {
wsGrid.onBrowserEvent(event);
}
};
private List<ChangeHandler> cellChangeHandlers = new LinkedList<ChangeHandler>();
private List<ChangeHandler> afterCellChangeHandlers = new LinkedList<ChangeHandler>();
public WSGrid() {
this(true, true);
this.fm = new DefaultFocusManager(this);
}
public WSGrid(FocusManager fm) {
this(true, true);
this.fm = fm;
}
private int _startpos = 0;
public WSGrid(boolean scrollable, boolean editable) {
innerPanel = new VerticalPanel();
innerPanel.setSpacing(0);
focusPanel = new FocusPanel(innerPanel);
initWidget(focusPanel);
titleBar = new WSAbstractGrid(false, GridType.TITLEBAR);
innerPanel.add(titleBar);
innerPanel.setCellVerticalAlignment(titleBar, HasVerticalAlignment.ALIGN_BOTTOM);
titleBar.setStylePrimaryName("WSGrid-header");
if (editable)
dataGrid = new WSAbstractGrid(scrollable, GridType.EDITABLE_GRID);
else
dataGrid = new WSAbstractGrid(scrollable, GridType.NONEDITABLE_GRID);
innerPanel.add(dataGrid);
innerPanel.setCellVerticalAlignment(dataGrid, HasVerticalAlignment.ALIGN_TOP);
dataGrid.setStylePrimaryName("WSGrid-datagrid");
resizeLine.setWidth("5px");
resizeLine.setHeight("800px");
resizeLine.setStyleName("WSGrid-resize-line");
resizeLine.sinkEvents(Event.MOUSEEVENTS);
dataGrid.getScrollPanel().addScrollHandler(new ScrollHandler() {
public void onScroll(ScrollEvent event) {
titleBar.getScrollPanel().setHorizontalScrollPosition(dataGrid.getScrollPanel().getHorizontalScrollPosition());
}
});
/**
* This is the handler that is responsible for resizing columns.
*/
focusPanel.addMouseDownHandler(new MouseDownHandler() {
public void onMouseDown(MouseDownEvent event) {
if (!selectionList.isEmpty() && selectionList.lastElement().isEdit()) {
return;
}
if (_resizeArmed) {
if (!_resizing) {
_resizing = true;
disableTextSelection(getBodyElement(), true);
_startpos = event.getClientX();
resizeLine.show();
resizeLine.setPopupPosition(event.getClientX(), 0);
}
}
}
});
/**
* This handler traces the mouse movement, drawing the vertical resizing
* indicator.
*/
focusPanel.addMouseMoveHandler(new MouseMoveHandler() {
public void onMouseMove(MouseMoveEvent event) {
if (_resizing) {
setStyleAttribute(resizeLine.getElement(), "left", (event.getClientX()) + "px");
}
}
});
/**
* This is the mouse release handler that resizes a column once the left
* mouse button is released in a resizing operation.
*/
focusPanel.addMouseUpHandler(new MouseUpHandler() {
public void onMouseUp(MouseUpEvent event) {
if (_resizing) {
cancelResize();
WSCell currentFocus = selectionList.isEmpty() ? null : selectionList.lastElement();
if (currentFocus == null)
return;
int selCol = (_leftGrow ? currentFocus.col - 1 : currentFocus.col);
if (selCol == -1)
return;
int width = colSizes.get(selCol);
int offset = event.getClientX();
/**
* If we didn't move at all, don't calculate a new size.
*/
if (offset - _startpos == 0)
return;
width -= (_startpos -= event.getClientX());
setColumnWidth(selCol, width);
}
_rangeSelect = false;
}
});
/**
* This handler is responsible for all the keyboard input handlers for
* the grid.
*/
focusPanel.addKeyDownHandler(new KeyDownHandler() {
public void onKeyDown(KeyDownEvent event) {
final WSCell currentFocus;
int offsetX;
int offsetY;
/**
* Is there currently anything selected?
*/
if (selectionList.isEmpty()) {
/**
* No.
*/
return;
} else {
/**
* Set currentFocus to the last element in the
* selectionList.
*/
currentFocus = selectionList.lastElement();
/**
* Yes. Let's check to see if this cell spans. If so, we
* will set the offsetX value to the colspan value so
* navigation behaves.
*/
offsetX = currentFocus.isColSpan() ? currentFocus.getColspan() : 1;
offsetY = currentFocus.isRowSpan() ? currentFocus.getRowspan() : 1;
}
/**
* If we're currently editing a cell, then we ignore these
* operations.
*/
if (currentFocus.edit) {
return;
}
event.preventDefault();
switch (event.getNativeKeyCode()) {
/**
* If tab is pressed, we want to advance to the next cell. If we
* are at the end of the line, go to the first cell of the next
* line. If shift is depressed, then perform the inverse.
*/
case KeyCodes.KEY_TAB:
blurAll();
if (event.getNativeEvent().getShiftKey()) {
if (currentFocus.getCol() == 0 && currentFocus.getRow() > 0) {
dataGrid.tableIndex.get(currentFocus.getRow() - offsetX).get(cols - offsetX).focus();
} else {
dataGrid.tableIndex.get(currentFocus.getRow()).get(
currentFocus.getCol() - (currentFocus.isColSpan() ? currentFocus.getLeftwareColspan() : 1))
.focus();
}
} else {
if (currentFocus.getCol() == cols - offsetX && currentFocus.getRow() < dataGrid.tableIndex.size()) {
dataGrid.tableIndex.get(currentFocus.getRow() + offsetX).get(0).focus();
} else {
dataGrid.tableIndex.get(currentFocus.getRow()).get(currentFocus.getCol() + offsetX).focus();
}
}
break;
/**
* If the up key is pressed, we should advance to the cell
* directly above the currently selected cell. If we are at the
* top of the grid, then nothing should happen.
*/
case 63232:
case KeyCodes.KEY_UP:
if (currentFocus.getRow() > 0) {
if (!event.getNativeEvent().getShiftKey()) {
blurAll();
}
if (fm.isInitialised()) {
fm.moveUpwards();
} else {
dataGrid.tableIndex.get(
currentFocus.getRow() - (currentFocus.isRowSpan() ? currentFocus.getUpwardRowspan() : 1))
.get(currentFocus.getCol()).focus();
}
}
break;
/**
* If the right key is pressed, we should advance to the cell
* directly to the right of the currently selected cell. If we
* are at the rightmost part of the grid, then nothing should
* happen.
*/
case 63235:
case KeyCodes.KEY_RIGHT:
if (currentFocus.getCol() < cols - offsetX) {
/**
* Blur all columns if shift is not being depressed.
*/
if (!event.getNativeEvent().getShiftKey()) {
blurAll();
}
if (currentFocusColumn) {
titleBar.tableIndex.get(currentFocus.getRow()).get(currentFocus.getCol() + offsetX).focus();
} else {
if (fm.isInitialised()) {
fm.moveRight();
} else {
dataGrid.tableIndex.get(currentFocus.getRow()).get(currentFocus.getCol() + offsetX).focus();
}
}
}
break;
/**
* If either "Enter" or the down key is pressed we should
* advance to the cell directly below the current cell. If we
* are at the bottom of the grid, depending on the mode of the
* grid, either nothing should happen, or a new line should be
* added.
*/
case 63233:
case KeyCodes.KEY_ENTER:
case KeyCodes.KEY_DOWN:
if (currentFocus.getRow() < dataGrid.tableIndex.size() - offsetX) {
if (!event.getNativeEvent().getShiftKey()) {
blurAll();
}
if (fm.isInitialised()) {
fm.moveDownwards();
} else {
dataGrid.tableIndex.get(currentFocus.getRow() + offsetY).get(currentFocus.getCol()).focus();
}
}
break;
/**
* If the left key is pressed we should advance to the cell
* directly to the left of the currently selected cell. If we
* are at the leftmost part of the grid, nothing should happen.
*/
case 63234:
case KeyCodes.KEY_LEFT:
if (currentFocus.getCol() > 0) {
if (!event.getNativeEvent().getShiftKey()) {
blurAll();
}
if (currentFocusColumn) {
titleBar.tableIndex.get(currentFocus.getRow()).get(currentFocus.getCol() - offsetX).focus();
} else {
if (fm.isInitialised()) {
fm.moveLeft();
} else {
int delta = currentFocus.getCol()
- (currentFocus.isColSpan() ? currentFocus.getLeftwareColspan() : 1);
if (delta < 0) {
currentFocus.focus();
} else {
dataGrid.tableIndex.get(currentFocus.getRow()).get(delta).focus();
}
}
}
}
break;
case KeyCodes.KEY_CTRL:
case 16:
case 91:
break;
case KeyCodes.KEY_BACKSPACE:
case 63272:
case KeyCodes.KEY_DELETE:
if (currentFocus.grid.type != GridType.TITLEBAR) {
for (WSCell c : selectionList) {
c.setValue("");
}
} else {
/**
* Wipe the whole column.
*/
for (int i = 0; i < dataGrid.tableIndex.size(); i++) {
dataGrid.tableIndex.get(i).get(currentFocus.getCol()).setValue("");
}
}
break;
case 32: // spacebar
Timer t = new Timer() {
public void run() {
currentFocus.edit();
}
};
t.schedule(15);
break;
default:
currentFocus.setValue("");
currentFocus.edit();
}
}
});
focusPanel.addMouseOutHandler(new MouseOutHandler() {
public void onMouseOut(MouseOutEvent mouseOutEvent) {
_rangeSelect = false;
}
});
}
public void setScrollable(boolean scrollable) {
dataGrid.setScrollable(scrollable);
}
public void setEditable(boolean editable) {
if (editable)
dataGrid.setType(GridType.EDITABLE_GRID);
else
dataGrid.setType(GridType.NONEDITABLE_GRID);
}
public int getRowCount() {
return dataGrid.getRowCount();
}
public void removeRow(int row) {
dataGrid.removeRow(row);
}
public void setColumnHeader(int row, int column, String html) {
cols = titleBar.ensureRowsAndCols(row + 1, column + 1);
WSCell wsc = titleBar.getTableIndex().get(row).get(column);
wsc.setValue(new WSCellTitle(wsc, html));
}
public void setCell(int row, int column, String html) {
int col = dataGrid.ensureRowsAndCols(row + 1, column + 1);
if (col > cols)
cols = col;
dataGrid.getTableIndex().get(row).get(column).setValue(html);
}
public void setCell(int row, int column, WSCellFormatter formatter) {
int col = dataGrid.ensureRowsAndCols(row + 1, column + 1);
if (col > cols)
cols = col;
dataGrid.getTableIndex().get(row).get(column).setValue(formatter);
}
/**
* Return the total number of columns in the grid.
*
* @return -
*/
public int getCols() {
return cols;
}
/**
* Blur all currently selected columns.
*/
public void blurAll() {
Stack<WSCell> stk = new Stack<WSCell>();
stk.addAll(selectionList);
for (WSCell cell : stk) {
cell.blur();
}
selectionList.clear();
fillX = 0;
fillY = 0;
forwardDirX = forwardDirY = true;
fm.reset();
}
public void setRowHeight(int row, int height) {
dataGrid.setRowHeight(row, height);
}
/**
* Sets a column width (in pixels). Columns start from 0.
*
* @param column
* - the column
* @param width
* - the width in pixels
*/
public void setColumnWidth(int column, int width) {
colSizes.set(column, width);
if (column >= cols)
return;
if (isAttached()) {
titleBar.getTable().getColumnFormatter().setWidth(column, width + "px");
dataGrid.getTable().getColumnFormatter().setWidth(column, width + "px");
titleBar.tableIndex.get(0).get(column).setWidth(width + "px");
WSCell c;
for (int cX = 0; cX < dataGrid.tableIndex.size(); cX++) {
if (dataGrid.tableIndex.size() == 0) {
// Classic problem in the hosted mode
break;
}
if (dataGrid.tableIndex.get(cX).size() == 0) {
// Classic problem in the hosted mode
break;
}
c = dataGrid.tableIndex.get(cX).get(column);
/**
* Check to see if this cell is merged with other cells. If so,
* we need to accumulate the proper column width.
*/
if (c.isColSpan()) {
int spanSize = width + 1;
for (int span = c.getColspan() - 1; span > 0; span--) {
spanSize += colSizes.get(column + span) + 2;
}
c.setWidth((spanSize + 1) + "px");
} else {
c.setWidth(width + "px");
}
}
} else {
resizeOnAttach = true;
}
}
private void updateColumnSizes() {
int col = 0;
for (Integer size : colSizes) {
setColumnWidth(col++, size);
}
}
/**
* Returns an instance of the WSCell based on the row and col specified.
*
* @param row
* - the row
* @param col
* - the column
* @return Instance of WSCell.
*/
public WSCell getCell(int row, int col) {
return dataGrid.getCell(row, col);
}
/**
* Highlights a vertical column.
*
* @param col
* - the column
*/
private void selectColumn(int col) {
for (ArrayList<WSCell> row : titleBar.getTableIndex()) {
row.get(col).addStyleDependentName("hcolselect");
}
for (ArrayList<WSCell> row : dataGrid.getTableIndex()) {
row.get(col).addStyleDependentName("colselect");
}
}
/**
* Blurs a vertical column.
*
* @param col
* - the column
*/
private void blurColumn(int col) {
sortedColumns.put(col, false);
for (ArrayList<WSCell> row : titleBar.getTableIndex()) {
row.get(col).removeStyleDependentName("hcolselect");
}
for (ArrayList<WSCell> row : dataGrid.getTableIndex()) {
row.get(col).removeStyleDependentName("colselect");
}
}
public void clear() {
cols = 0;
this.titleBar.clear();
this.dataGrid.clear();
this.selectionList.clear();
this.colSizes.clear();
this.sortedColumns.clear();
this._mergedCells = false;
}
/**
* This is the actual grid implementation.
*/
public class WSAbstractGrid extends Composite {
private ScrollPanel scrollPanel;
private FlexTable table;
private ArrayList<ArrayList<WSCell>> tableIndex;
private GridType type;
public WSAbstractGrid() {
this(false, GridType.EDITABLE_GRID);
}
public WSAbstractGrid(GridType type) {
this(false, type);
}
public WSAbstractGrid(boolean scrollable, GridType type) {
this.type = type;
table = new FlexTable();
table.setStylePrimaryName("WSGrid");
table.insertRow(0);
scrollPanel = new ScrollPanel();
initWidget(scrollPanel);
scrollPanel.setAlwaysShowScrollBars(scrollable);
setScrollable(scrollable);
scrollPanel.add(table);
tableIndex = new ArrayList<ArrayList<WSCell>>();
tableIndex.add(new ArrayList<WSCell>());
setHeight("20px");
}
public void setType(GridType type) {
this.type = type;
}
public void setScrollable(boolean scrollable) {
if (!scrollable) {
setStyleAttribute(scrollPanel.getElement(), "overflow", "hidden");
scrollPanel.setHeight("20px");
table.setHeight("20px");
} else {
setStyleAttribute(scrollPanel.getElement(), "overflow", "scroll");
}
}
public void clear() {
scrollPanel.remove(table);
table = new FlexTable();
table.setStylePrimaryName("WSGrid");
table.insertRow(0);
scrollPanel.add(table);
tableIndex.clear();
tableIndex.add(new ArrayList<WSCell>());
}
public void addCell(int row, String w) {
int currentColSize = table.getCellCount(row);
table.addCell(row);
table.setWidget(row, currentColSize, new WSCell(this, new WSCellSimpleTextCell(w), row, currentColSize));
}
public int getRowCount() {
return table.getRowCount();
}
public void addRow() {
table.insertRow(table.getRowCount());
for (int i = 0; i < cols; i++) {
addCell(table.getRowCount() - 1, "");
}
}
public void removeRow(int row) {
table.removeRow(row);
tableIndex.remove(row);
// Need to update the rows for all the cells following the deleted
// row.
int size = tableIndex.size();
for (int i = row; i < size; i++) {
ArrayList<WSCell> currRow = tableIndex.get(i);
int numCols = currRow.size();
for (int j = 0; j < numCols; j++)
currRow.get(j).row--;
}
}
public int ensureRowsAndCols(int rows, int cols) {
if (colSizes.size() < cols) {
for (int i = 0; i < cols; i++) {
colSizes.add(125);
}
}
if (table.getRowCount() == 0) {
addRow();
}
while (table.getRowCount() < rows) {
addRow();
}
for (int r = 0; r < table.getRowCount(); r++) {
if (table.getCellCount(r) < cols) {
int growthDelta = cols - table.getCellCount(r);
for (int c = 0; c < growthDelta; c++) {
table.getColumnFormatter().setWidth(c, colSizes.get(c) + "px");
addCell(r, "");
}
assert table.getCellCount(r) == cols : "New size is wrong: " + table.getCellCount(r);
}
}
return cols == 0 ? 1 : cols;
}
public FlexTable getTable() {
return table;
}
public void setHeight(String height) {
if (scrollPanel != null)
scrollPanel.setHeight(height);
}
public void setWidth(String width) {
if (scrollPanel != null)
scrollPanel.setWidth(width);
}
public int getOffsetHeight() {
if (scrollPanel != null)
return scrollPanel.getOffsetHeight();
else
return table.getOffsetHeight();
}
public int getOffsetWidth() {
if (scrollPanel != null)
return scrollPanel.getOffsetWidth();
else
return table.getOffsetWidth();
}
@Override
protected void onAttach() {
super.onAttach();
}
public ArrayList<ArrayList<WSCell>> getTableIndex() {
return tableIndex;
}
public void setRowHeight(int row, int height) {
if (row > tableIndex.size())
return;
ArrayList<WSCell> rowData = tableIndex.get(row);
String ht = height + "px";
for (int i = 0; i < cols; i++) {
rowData.get(i).setHeight(ht);
}
}
public WSCell getCell(int row, int col) {
return tableIndex.get(row).get(col);
}
public void sort(int col, boolean ascending) {
boolean secondPass = isEmpty(valueAt(0, col));
_sort(col, ascending, 0, tableIndex.size() - 1);
if (secondPass) {
// resort
_sort(col, ascending, 0, tableIndex.size() - 1);
}
}
private void _sort(int col, boolean ascending, int low, int high) {
if (low < high) {
int p = _sort_partition(col, ascending, low, high);
_sort(col, ascending, low, p);
_sort(col, ascending, p + 1, high);
}
}
private int _sort_partition(int col, boolean ascending, int low, int high) {
WSCell pvtStr = cellAt(low, col);
int i = low - 1;
int j = high + 1;
if (ascending) {
while (i < j) {
i++;
while (_sort_lt(cellAt(i, col), pvtStr)) {
i++;
}
j--;
while (_sort_gt(cellAt(j, col), pvtStr)) {
j--;
}
if (i < j)
_sort_swap(i, j);
}
} else {
while (i < j) {
i++;
while (_sort_gt(cellAt(i, col), pvtStr)) {
i++;
}
j--;
while (_sort_lt(cellAt(j, col), pvtStr)) {
j--;
}
if (i < j)
_sort_swap(i, j);
}
}
return j;
}
private boolean _sort_gt(WSCell l, WSCell r) {
if (l == null)
return false;
if (l.numeric && r.numeric) {
return parseDouble(l.getValue()) > parseDouble(r.getValue());
} else {
String ll = l.getValue();
String rr = r.getValue();
// Internet Explorer is very annoying
if (_msie_compatibility) {
ll = ll.equals(" ") ? "" : ll;
rr = rr.equals(" ") ? "" : rr;
}
for (int i = 0; i < ll.length() && i < rr.length(); i++) {
if (ll.charAt(i) > rr.charAt(i)) {
return true;
} else if (ll.charAt(i) < rr.charAt(i)) {
return false;
}
}
return isEmpty(ll) && !isEmpty(rr);
}
}
private boolean _sort_lt(WSCell l, WSCell r) {
if (l == null)
return false;
if (l.numeric && r.numeric) {
return parseDouble(l.getValue()) < parseDouble(r.getValue());
} else {
String ll = l.getValue();
String rr = r.getValue();
// Internet Explorer is very annoying
if (_msie_compatibility) {
ll = ll.equals(" ") ? "" : ll;
rr = rr.equals(" ") ? "" : rr;
}
for (int i = 0; i < ll.length() && i < rr.length(); i++) {
if (ll.charAt(i) < rr.charAt(i)) {
return true;
} else if (ll.charAt(i) > rr.charAt(i)) {
return false;
}
}
return isEmpty(ll) && !isEmpty(rr);
}
}
private void _sort_swap(int i, int j) {
WSCellFormatter t;
WSCell c1;
WSCell c2;
for (int z = 0; z < cols; z++) {
c1 = cellAt(i, z);
c2 = cellAt(j, z);
t = c1.getCellFormat();
c1.setValue(c2.getCellFormat());
c2.setValue(t);
c1.setOriginalRow(j);
c2.setOriginalRow(i);
}
}
public WSCell cellAt(int row, int col) {
return tableIndex.size() > row ? tableIndex.get(row).get(col) : null;
}
public WSCellFormatter cellFmtAt(int row, int col) {
return cellAt(row, col).cellFormat;
}
public String valueAt(int row, int col) {
return tableIndex.get(row).get(col).cellFormat.getTextValue();
}
public void setValueAt(int row, int col, String html) {
cellAt(row, col).setValue(html);
}
public void setValueAt(int row, int col, WSCellFormatter cellFmt) {
cellAt(row, col).setValue(cellFmt);
}
public ScrollPanel getScrollPanel() {
return scrollPanel;
}
} // end WSAbstractGrid
private boolean _msie_compatibility = getUserAgent().contains("msie");
public class WSCell extends Composite {
protected SimplePanel panel;
protected WSCellFormatter cellFormat;
protected boolean numeric;
protected boolean edit;
protected int originalRow;
protected int row;
protected int col;
protected int rowspan = 1;
protected int colspan = 1;
protected WSAbstractGrid grid;
public WSCell(WSAbstractGrid grid, WSCellFormatter cellFormat, int row, int column) {
this.grid = grid;
panel = new SimplePanel();
panel.setStyleName("WSCell-panel");
if (grid.tableIndex.size() - 1 < row) {
while (grid.tableIndex.size() - 1 < row) {
grid.tableIndex.add(new ArrayList<WSCell>());
}
}
ArrayList<WSCell> cols = grid.tableIndex.get(row);
if (cols.size() == 0 || cols.size() - 1 < column) {
cols.add(this);
} else {
cols.set(column, this);
}
this.cellFormat = cellFormat;
panel.add(cellFormat.getWidget(wsGrid));
this.row = this.originalRow = row;
this.col = column;
initWidget(panel);
setWidth(colSizes.get(column) + "px");
setStyleName("WSCell");
sinkEvents(Event.MOUSEEVENTS | Event.FOCUSEVENTS | Event.ONCLICK | Event.ONDBLCLICK);
if (_msie_compatibility) {
if (cellFormat.getTextValue() == null || cellFormat.getTextValue().equals("")) {
cellFormat.setValue(" ");
}
disableTextSelectInternal(panel.getElement(), true);
}
}
public WSCell() {
}
/**
* Calling this method will place the cell into edit mode.
*/
public void edit() {
String text = cellFormat.getTextValue();
if (_msie_compatibility && text.equals(" "))
cellFormat.setValue("");
edit = cellFormat.edit(this);
}
/**
* Ends a current edit operation.
*/
public void stopedit() {
if (edit) {
edit = false;
if (!_msie_compatibility)
focusPanel.setFocus(true);
}
}
public void cancelEdit() {
cellFormat.cancelEdit();
}
/**
* Blurs the current cell.
*/
public void blur() {
if (edit)
cellFormat.stopedit();
removeStyleDependentName("selected");
if (rowSelectionOnly) {
for (int i = 0; i < cols; i++) {
WSCell c = grid.getCell(row, i);
if (i == 0) {
c.removeStyleDependentName("rowselect-left");
} else if (i + 1 == cols) {
c.removeStyleDependentName("rowselect-right");
} else {
c.removeStyleDependentName("rowselect");
}
}
}
if (currentFocusColumn) {
blurColumn(col);
currentFocusColumn = false;
} else {
selectionList.remove(this);
}
}
public void notifyCellUpdate(Object newValue) {
fireAllCellChangeHandlers(this, newValue);
}
public void notifyCellAfterUpdate() {
fireAllAfterCellChangeHandlers(this);
}
/**
* Focuses the current cell.
*/
public void focus() {
WSCell currentFocus = selectionList.isEmpty() ? null : selectionList.lastElement();
boolean isFocus = currentFocus == this;
if (selectionList.isEmpty()) {
fm.setStartCell(this);
}
if (!selectionList.contains(this))
selectionList.add(this);
if (grid.type == GridType.TITLEBAR) {
currentFocusColumn = true;
if (!isFocus) {
selectColumn(col);
}
} else {
int scrollPos = grid.getScrollPanel().getScrollPosition();
int scrollPosH = grid.getScrollPanel().getHorizontalScrollPosition();
int cellHeight = getOffsetHeight();
int cellWidth = getOffsetWidth();
int bottomCell = DOM.getAbsoluteTop(getElement()) + cellHeight
- DOM.getAbsoluteTop(grid.getScrollPanel().getElement()) + scrollPos;
int rightCell = DOM.getAbsoluteLeft(getElement()) + cellWidth
- DOM.getAbsoluteLeft(grid.getScrollPanel().getElement()) + scrollPosH;
int bottomVisible = grid.getScrollPanel().getOffsetHeight() + scrollPos - 19;
int topVisible = bottomVisible - grid.getScrollPanel().getOffsetHeight() + 2;
int rightVisible = grid.getScrollPanel().getOffsetWidth() + scrollPosH - 19;
int leftVisible = rightVisible - grid.getScrollPanel().getOffsetWidth() + 2;
if (bottomCell >= bottomVisible) {
final int startPos = scrollPos;
scrollPos += (bottomCell - bottomVisible) + 18;
final int endPos = scrollPos;
final int multiplier = ((endPos - startPos) / 100);
final int threshold = endPos - 50 - (multiplier * 10);
final double decelRate = ((double) (endPos - startPos)) / (250 + (multiplier * 100));
Timer smoothScroll = new Timer() {
int i = startPos;
double vel = 5.0 + multiplier;
int absoluteVel = (int) Math.round(vel);
double decel = decelRate;
@Override
public void run() {
if ((i += absoluteVel) >= endPos) {
i = endPos;
cancel();
}
if (i > threshold) {
if (vel > 1) {
vel -= decelRate;
absoluteVel = (int) Math.round(vel);
if (absoluteVel < 1)
absoluteVel = 1;
}
}
grid.scrollPanel.setScrollPosition(i);
}
};
smoothScroll.scheduleRepeating(1);
} else if (bottomCell - cellHeight <= (topVisible)) {
final int startPos = scrollPos;
scrollPos -= getOffsetHeight();
final int endPos = scrollPos;
final int multiplier = ((endPos - startPos) / 100);
final double decelRate = ((double) (startPos - endPos)) / (250 + (multiplier * 100));
final int threshold = endPos + 50 + (multiplier * 10);
Timer smoothScroll = new Timer() {
int i = startPos;
double vel = 5.0 + multiplier;
int absoluteVel = (int) Math.round(vel);
double decel = decelRate;
@Override
public void run() {
if ((i -= absoluteVel) <= endPos) {
i = endPos;
cancel();
}
if (i < threshold) {
if (vel > 1) {
vel -= decelRate;
absoluteVel = (int) Math.round(vel);
if (absoluteVel < 1)
absoluteVel = 1;
}
}
grid.scrollPanel.setScrollPosition(i);
}
};
smoothScroll.scheduleRepeating(1);
} else if (rightCell >= (rightVisible)) {
if (scrollPosH % cellWidth != 0) {
scrollPosH += (scrollPosH % cellWidth);
}
grid.getScrollPanel().setHorizontalScrollPosition(scrollPosH + getOffsetWidth());
} else if (rightCell - cellWidth <= (leftVisible)) {
if (scrollPosH % cellWidth != 0) {
scrollPosH -= (scrollPosH % cellWidth);
}
grid.getScrollPanel().setHorizontalScrollPosition(scrollPosH - getOffsetWidth());
}
}
if (grid.type != GridType.TITLEBAR && rowSelectionOnly) {
for (int i = 0; i < cols; i++) {
WSCell c = grid.getCell(row, i);
if (!selectionList.contains(c)) {
if (i == 0) {
c.addStyleDependentName("rowselect-left");
} else if (i + 1 == cols) {
c.addStyleDependentName("rowselect-right");
} else {
c.addStyleDependentName("rowselect");
}
}
}
if (col == 0) {
addStyleDependentName("rowselect-left");
} else if (col + 1 == cols) {
addStyleDependentName("rowselect-right");
} else {
addStyleDependentName("rowselect");
}
} else {
addStyleDependentName("selected");
}
}
/**
* Focuses a range between this cell and the currently selected cell.
*/
public void focusRange() {
WSCell cell = fm.getStartCell();
int startSelX = cell.col;
int startSelY = cell.row;
fillX = col - startSelX;
fillY = row - startSelY;
int startX = startSelX;
int startY = startSelY;
if (fillX < 0) {
startX = startSelX + fillX;
fillX *= -1;
forwardDirX = false;
}
if (fillY < 0) {
startY = startSelY + fillY;
fillY *= -1;
forwardDirY = false;
}
int endX = startX + fillX;
int endY = startY + fillY;
int x = startX;
while (x < endX) {
if (forwardDirX) {
x = x + fm.moveRight();
} else {
x = x + fm.moveLeft();
}
}
;
int y = startY;
while (y < endY) {
if (forwardDirY) {
y = y + fm.moveDownwards();
} else {
y = y + fm.moveUpwards();
}
}
;
}
public int getOriginalRow() {
return originalRow;
}
public void setOriginalRow(int originalRow) {
this.originalRow = originalRow;
}
public int getRow() {
return row;
}
public int getCol() {
return col;
}
public boolean isEdit() {
return edit;
}
public void setValue(String html) {
html = html.trim();
if (_msie_compatibility && html.length() == 0) {
cellFormat.setValue(" ");
} else {
cellFormat.setValue(html);
panel.clear();
panel.add(cellFormat.getWidget(wsGrid));
}
numeric = isNumeric(html);
}
public void setValue(WSCellFormatter formatter) {
if (_msie_compatibility && (formatter.getTextValue() == null || formatter.getTextValue().length() == 0)) {
formatter.setValue(" ");
}
this.cellFormat = formatter;
panel.clear();
Widget w = formatter.getWidget(wsGrid);
panel.add(w);
numeric = isNumeric(formatter.getTextValue());
}
public String getValue() {
return cellFormat.getTextValue();
}
public WSCellFormatter getCellFormat() {
return cellFormat;
}
public void mergeColumns(int cols) {
if (cols < 2)
return;
_mergedCells = true;
final int _row = row;
for (int i = 1; i < cols; i++) {
grid.table.removeCell(row, col + 1);
final WSCell cell = this;
final int _col = col + i;
grid.tableIndex.get(row).set(col + i, new WSGrid.WSCell() {
{
this.grid = cell.grid;
this.col = _col;
this.row = _row;
initWidget(new HTML());
}
@Override
public void blur() {
cell.blur();
}
@Override
public void focus() {
cell.focus();
}
@Override
public int getColspan() {
return cell.colspan - (col - cell.col);
}
@Override
public int getRowspan() {
return cell.rowspan - (row - cell.row);
}
@Override
public int getUpwardRowspan() {
return row - cell.row + 1;
}
@Override
public int getLeftwareColspan() {
return col - cell.col + 1;
}
@Override
public boolean isColSpan() {
return cell.isColSpan();
}
@Override
public boolean isRowSpan() {
return cell.isRowSpan();
}
});
}
grid.table.getFlexCellFormatter().setColSpan(row, col, cols);
colspan = cols;
updateColumnSizes();
}
public void mergeRows(int rows) {
if (rows < 2)
return;
_mergedCells = true;
FlexTable table = grid.table;
for (int i = 1; i < rows; i++) {
for (int c = col; c < (col + colspan); c++) {
final WSCell cell = this;
final int _row = row + i;
table.removeCell(_row, col);
final int _col = c;
grid.tableIndex.get(_row).set(c, new WSCell() {
{
this.grid = cell.grid;
this.col = _col;
this.row = _row;
initWidget(new HTML());
}
@Override
public void blur() {
cell.blur();
}
@Override
public void focus() {
cell.focus();
}
@Override
public int getColspan() {
return cell.colspan - (col - cell.col);
}
@Override
public int getRowspan() {
return cell.rowspan - (row - cell.row);
}
@Override
public int getUpwardRowspan() {
return row - cell.row + 1;
}
@Override
public int getLeftwareColspan() {
return col - cell.col + 1;
}
@Override
public boolean isColSpan() {
return cell.isColSpan();
}
@Override
public boolean isRowSpan() {
return cell.isRowSpan();
}
});
grid.table.getFlexCellFormatter().setRowSpan(row, col, rows);
rowspan = rows;
setHeight(((rows * CELL_HEIGHT_PX)) + "px");
updateColumnSizes();
}
}
}
public boolean isColSpan() {
return colspan > 1;
}
public int getColspan() {
return colspan;
}
public boolean isRowSpan() {
return rowspan > 1;
}
public int getRowspan() {
return rowspan;
}
public int getLeftwareColspan() {
return 1;
}
public int getUpwardRowspan() {
return 1;
}
public void setHeight(String height) {
super.setHeight(height);
panel.setHeight(height);
cellFormat.setHeight(height);
grid.table.getCellFormatter().setHeight(row, col, height);
}
@Override
public void onBrowserEvent(Event event) {
int leftG = getAbsoluteLeft() + 10;
int rightG = getAbsoluteLeft() + colSizes.get(col) - 10;
switch (event.getTypeInt()) {
case Event.ONMOUSEOVER:
if (!_resizing) {
// addStyleDependentName("hover");
if (_rangeSelect) {
focusRange();
}
}
break;
case Event.ONMOUSEOUT:
// if (!_resizing) removeStyleDependentName("hover");
if (grid.type == GridType.TITLEBAR)
_resizeArmed = false;
break;
case Event.ONMOUSEMOVE:
if (!_resizing && grid.type == GridType.TITLEBAR) {
if (event.getClientX() < leftG) {
addStyleDependentName("resize-left");
_resizeArmed = true;
_leftGrow = true;
} else if (event.getClientX() > rightG) {
addStyleDependentName("resize-right");
_resizeArmed = true;
_leftGrow = false;
} else {
removeStyleDependentName("resize-left");
removeStyleDependentName("resize-right");
_resizeArmed = false;
}
}
break;
case Event.ONMOUSEDOWN:
if (edit) {
return;
}
_rangeSelect = true;
if (event.getShiftKey()) {
focusRange();
break;
} else if (!event.getMetaKey() && !event.getCtrlKey() && !selectionList.isEmpty()
&& selectionList.lastElement() != this) {
blurAll();
}
focus();
break;
case Event.ONMOUSEUP:
_rangeSelect = false;
break;
case Event.ONFOCUS:
break;
case Event.ONCLICK:
if (grid.type == GridType.TITLEBAR) {
if (_mergedCells) {
WSModalDialog dialog = new WSModalDialog("Unable to sort");
dialog.getCancelButton().setVisible(false);
dialog.ask("The table cannot be sorted because it contains merged cells", null);
dialog.showModal();
return;
}
boolean asc = getColumnSortOrder(col);
if (sortedColumnHeader != null) {
WSCell old = sortedColumnHeader;
sortedColumnHeader = this;
old.cellFormat.getWidget(wsGrid);
} else {
sortedColumnHeader = this;
}
cellFormat.getWidget(wsGrid);
sortedColumns.put(col, !asc);
dataGrid.sort(col, asc);
}
break;
case Event.ONDBLCLICK:
switch (grid.type) {
case EDITABLE_GRID:
edit();
break;
case TITLEBAR:
break;
}
break;
}
}
}
/**
* Ends resizing operation.
*/
private void cancelResize() {
resizeLine.hide();
_resizing = _resizeArmed = false;
}
/**
* Returns the offsite high of the title row
*
* @return The offset height in pixels
*/
public int getTitlebarOffsetHeight() {
return titleBar.getOffsetHeight();
}
/**
* Sets the height of the grid.
*
* @param height
* CSS height string.
*/
public void setHeight(String height) {
focusPanel.setHeight(height);
}
/**
* Sets the height of the grid in pixels
*
* @param height
* The height in pixels
*/
public void setPreciseHeight(int height) {
int offsetHeight = height - getTitlebarOffsetHeight() - 10;
setHeight(height + "px");
if (offsetHeight > 20) {
dataGrid.getScrollPanel().setHeight((offsetHeight - 20) + "px");
}
// ERRAI-72
else if (offsetHeight < 0) {
dataGrid.getScrollPanel().setHeight(20 + "px");
} else {
dataGrid.getScrollPanel().setHeight(offsetHeight + "px");
}
}
/**
* Set thes the width of the grid.
*
* @param width
* The CSS width string.
*/
public void setWidth(String width) {
focusPanel.setWidth(width);
}
/**
* Sets the width of the grid in pixels.
*
* @param width
* The width in pixels.
*/
public void setPreciseWidth(int width) {
setWidth(width + "px");
titleBar.getScrollPanel().setWidth(width - 20 + "px");
dataGrid.getScrollPanel().setWidth(width + "px");
}
@Override
public void setPixelSize(int width, int height) {
setPreciseWidth(width);
setPreciseHeight(height);
}
public void sizeToParent() {
DeferredCommand.addCommand(new Command() {
public void execute() {
if (getParent().isVisible())
setPixelSize(getParent().getOffsetWidth() - 5, getParent().getOffsetHeight() - 15);
}
});
}
/**
* Increase or decrease the width by a relative amount. A positive value
* will increase the size of the grid, while a negative value will shrink
* the size.
*
* @param amount
* Size in pixels.
*/
public void growWidth(int amount) {
int newWidth = dataGrid.getScrollPanel().getOffsetWidth() + amount;
setWidth(newWidth + "px");
titleBar.getScrollPanel().setWidth(newWidth - 20 + "px");
dataGrid.getScrollPanel().setWidth(newWidth + "px");
}
public Map<Integer, Boolean> getSortedColumns() {
return sortedColumns;
}
/**
* If there is a sorted column, this will return an instance of the header
* cell for that sorted column.
*
* @return An instance of WSCell.
*/
public WSCell getSortedColumnHeader() {
return sortedColumnHeader;
}
/**
* Returns the sort order of the specified column. The boolean value
* <tt>true</tt> if the order is ascending, <tt>false</em> if it's decending or not sorted at all.
*
* @param col
* The column.
* @return true if ascending
*/
public boolean getColumnSortOrder(int col) {
if (sortedColumns.containsKey(col)) {
return sortedColumns.get(col);
} else {
sortedColumns.put(col, true);
return true;
}
}
/**
* Registers a {@link ChangeHandler} with the grid.
*
* @param handler
* -
*/
public void addCellChangeHandler(ChangeHandler handler) {
cellChangeHandlers.add(handler);
}
public void addAfterCellChangeHandler(ChangeHandler handler) {
afterCellChangeHandlers.add(handler);
}
/**
* Removes a {@link ChangeHandler} from the grid.
*
* @param handler
* -
*/
public void removeCellChangeHandler(ChangeHandler handler) {
cellChangeHandlers.remove(handler);
}
public void removeAfterCellChangeHandler(ChangeHandler handler) {
cellChangeHandlers.remove(handler);
}
private void fireAllCellChangeHandlers(WSCell cell, Object newValue) {
for (ChangeHandler c : cellChangeHandlers)
c.onChange(new CellChangeEvent(cell, newValue));
}
private void fireAllAfterCellChangeHandlers(WSCell cell) {
for (ChangeHandler c : afterCellChangeHandlers)
c.onChange(new CellChangeEvent(cell, cell.getCellFormat().getValue()));
}
public void mergeSelected() {
WSCell start;
// TODO Not sure this should use Focus Manager
int cellX = fm.getStartCell().col;
int cellY = fm.getStartCell().row;
if (!forwardDirX) {
cellX -= fillX;
}
if (!forwardDirY) {
cellY -= fillY;
}
start = dataGrid.cellAt(cellY, cellX);
start.mergeColumns(fillX + 1);
start.mergeRows(fillY + 1);
blurAll();
}
@Override
protected void onAttach() {
super.onAttach();
int titleHeight = titleBar.getOffsetHeight();
if (titleHeight > 0) // workaround
// https://jira.jboss.org/jira/browse/ERRAI-52
innerPanel.setCellHeight(titleBar, titleHeight + "px");
if (resizeOnAttach) {
for (int i = 0; i < colSizes.size(); i++) {
setColumnWidth(i, colSizes.get(i));
}
}
}
public void onResize() {
setPixelSize(getParent().getOffsetWidth(), getParent().getOffsetHeight());
}
private static final int NONEDITABLE = 0;
private static final int EDITABLE = 1;
private static final int TITLEGRID = 1 << 1;
public enum GridType {
NONEDITABLE_GRID(NONEDITABLE), EDITABLE_GRID(EDITABLE), TITLEBAR(TITLEGRID);
private int options;
GridType(int options) {
this.options = options;
}
public boolean isEditable() {
return (EDITABLE & options) != 0;
}
public boolean isTitleGrid() {
return (TITLEGRID & options) != 0;
}
}
public static void disableTextSelection(Element elem, boolean disable) {
disableTextSelectInternal(elem, disable);
}
public Stack<WSCell> getSelectionList() {
return selectionList;
}
public boolean isRowSelectionOnly() {
return rowSelectionOnly;
}
public void setRowSelectionOnly(boolean rowSelectionOnly) {
this.rowSelectionOnly = rowSelectionOnly;
}
private native static void disableTextSelectInternal(Element e, boolean disable)/*-{
if (disable) {
e.ondrag = function () { return false; };
e.onselectstart = function () { return false; };
} else {
e.ondrag = null;
e.onselectstart = null;
}
}-*/;
public static native String getUserAgent() /*-{
return navigator.userAgent.toLowerCase();
}-*/;
public static native boolean isNumeric(String input) /*-{
return (input - 0) == input && input.length > 0;
}-*/;
public static native boolean isEmpty(String input) /*-{
return input == null || input == "";
}-*/;
}