/*
* Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores
* CA 94065 USA or visit www.oracle.com if you need additional information or
* have any questions.
*/
package com.sun.lwuit.table;
import com.sun.lwuit.Component;
import com.sun.lwuit.Container;
import com.sun.lwuit.Display;
import com.sun.lwuit.Form;
import com.sun.lwuit.Graphics;
import com.sun.lwuit.Label;
import com.sun.lwuit.TextArea;
import com.sun.lwuit.TextField;
import com.sun.lwuit.events.ActionEvent;
import com.sun.lwuit.events.ActionListener;
import com.sun.lwuit.events.DataChangedListener;
import com.sun.lwuit.geom.Rectangle;
import com.sun.lwuit.plaf.Border;
import com.sun.lwuit.plaf.Style;
/**
* The table class represents a grid of data that can be used for rendering a grid
* of components/labels. The table reflects and updates the underlying model data.
*
* @author Shai Almog
*/
public class Table extends Container {
/**
* Constant denoting that inner borders should not be drawn at all
*/
public static final int INNER_BORDERS_NONE = 0;
/**
* Constant denoting that only inner borders rows should be drawn
*/
public static final int INNER_BORDERS_ROWS = 1;
/**
* Constant denoting that only inner borders columns should be drawn
*/
public static final int INNER_BORDERS_COLS = 2;
/**
* Constant denoting that inner borders should be drawn fully
*/
public static final int INNER_BORDERS_ALL = 3;
private TableModel model;
private Listener listener = new Listener();
private boolean drawBorder = true;
private boolean collapseBorder = true;
private boolean drawEmptyCellsBorder = true;
private int horizontalBorderSpacing = 0;
private int verticalBorderSpacing = 0;
private boolean includeHeader = true;
private int innerBorder = INNER_BORDERS_ALL;
/**
* Indicates the alignment of the title see label alignment for details
*
* @see com.sun.lwuit.Label#setAlignment(int)
*/
private int titleAlignment = Label.CENTER;
/**
* Indicates the alignment of the cells see label alignment for details
*
* @see com.sun.lwuit.Label#setAlignment(int)
*/
private int cellAlignment = Label.LEFT;
/**
* This flag allows us to workaround issue 275 without incuring too many updateModel calls
*/
private boolean potentiallyDirtyModel;
/**
* Constructor for usage by GUI builder and automated tools, normally one
* should use the version that accepts the model
*/
public Table() {
this(new DefaultTableModel(new String[]{"Col1", "Col2"}, new String[][]{
{"1", "2"},
{"3", "4"}}));
}
/**
* Create a table with a new model
*
* @param model the model underlying this table
*/
public Table(TableModel model) {
this.model = model;
updateModel();
setUIID("Table");
}
/**
* Create a table with a new model
*
* @param model the model underlying this table
* @param includeHeader Indicates whether the table should render a table header as the first row
*/
public Table(TableModel model, boolean includeHeader) {
setUIID("Table");
this.includeHeader = includeHeader;
this.model = model;
updateModel();
}
/**
* Returns the selected row in the table
*
* @return the offset of the selected row in the table if a selection exists
*/
public int getSelectedRow() {
Form f = getComponentForm();
if(f != null) {
Component c = f.getFocused();
if(c != null) {
return getCellRow(c);
}
}
return -1;
}
/**
* Returns the selected column in the table
*
* @return the offset of the selected column in the table if a selection exists
*/
public int getSelectedColumn() {
Form f = getComponentForm();
if(f != null) {
Component c = f.getFocused();
if(c != null) {
return getCellColumn(c);
}
}
return -1;
}
private void updateModel() {
int selectionRow = -1, selectionColumn = -1;
Form f = getComponentForm();
if(f != null) {
Component c = f.getFocused();
if(c != null) {
selectionRow = getCellRow(c);
selectionColumn = getCellColumn(c);
}
}
removeAll();
int columnCount = model.getColumnCount();
// another row for the table header
if(includeHeader) {
setLayout(new TableLayout(model.getRowCount() + 1, columnCount));
for(int iter = 0 ; iter < columnCount ; iter++) {
String name = model.getColumnName(iter);
Component header = createCellImpl(name, -1, iter, false);
TableLayout.Constraint con = createCellConstraint(name, -1, iter);
addComponent(con, header);
}
} else {
setLayout(new TableLayout(model.getRowCount(), columnCount));
}
for(int r = 0 ; r < model.getRowCount() ; r++) {
for(int c = 0 ; c < columnCount ; c++) {
Object value = model.getValueAt(r, c);
// null should be returned for spanned over values
if(value != null) {
boolean e = model.isCellEditable(r, c);
Component cell = createCellImpl(value, r, c, e);
if(cell != null) {
TableLayout.Constraint con = createCellConstraint(value, r, c);
// returns the current row we iterate about
int currentRow = ((TableLayout)getLayout()).getNextRow();
if(currentRow > model.getRowCount()) {
return;
}
addComponent(con, cell);
if(r == selectionRow && c == selectionColumn) {
cell.requestFocus();
}
}
}
}
}
}
/**
* @inheritDoc
*/
protected void paintGlass(Graphics g) {
if ((drawBorder) && (innerBorder!=INNER_BORDERS_NONE)) {
int xPos = getAbsoluteX();
int yPos = getAbsoluteY();
g.translate(xPos, yPos);
int rows = model.getRowCount();
int cols = model.getColumnCount();
if(includeHeader) {
rows++;
}
g.setColor(getStyle().getFgColor());
TableLayout t = (TableLayout)getLayout();
int actualWidth = Math.max(getWidth(), getScrollDimension().getWidth());
int actualHeight = Math.max(getHeight(), getScrollDimension().getHeight());
if ((collapseBorder) || (innerBorder!=INNER_BORDERS_ALL) || // inner borders cols/rows are supported only in collapsed mode
(t.hasHorizontalSpanning()) || (t.hasVerticalSpanning())) { // TODO - We currently don't support separate borders for tables with spanned cells
if ((innerBorder==INNER_BORDERS_ALL) || (innerBorder==INNER_BORDERS_ROWS)) {
if(t.hasVerticalSpanning()) {
// iterate over the components and draw a line on the side of all
// the components other than the ones that are at the last column.
for(int cellRow = 0 ; cellRow < rows - 1; cellRow++) {
for(int cellColumn = 0 ; cellColumn < cols ; cellColumn++) {
// if this isn't the last row
if(cellRow + t.getCellVerticalSpan(cellRow, cellColumn) - 1 != rows - 1) {
// if this is a spanned through cell we don't want to draw a line here
if(t.isCellSpannedThroughHorizontally(cellRow, cellColumn)) {
continue;
}
int x = t.getColumnPosition(cellColumn);
int y = t.getRowPosition(cellRow);
int rowHeight = t.getRowPosition(cellRow + t.getCellVerticalSpan(cellRow, cellColumn)) - y;
int columnWidth;
if(cellColumn < getModel().getColumnCount() - 1) {
columnWidth = t.getColumnPosition(cellColumn + 1) - x;
} else {
columnWidth = getWidth() - y;
}
if ((innerBorder!=INNER_BORDERS_ROWS) || (shouldDrawInnerBorderAfterRow(cellRow))) {
g.drawLine(x, y + rowHeight, x + columnWidth, y + rowHeight);
}
}
}
}
} else {
// this is much faster since we don't need to check spanning
for(int row = 1 ; row < rows; row++) {
int y = t.getRowPosition(row);
if ((innerBorder!=INNER_BORDERS_ROWS) || (shouldDrawInnerBorderAfterRow(row-1))) {
g.drawLine(0, y, actualWidth, y);
}
//g.drawLine(0+2, y+2, actualWidth-2, y+2);
}
}
}
if ((innerBorder==INNER_BORDERS_ALL) || (innerBorder==INNER_BORDERS_COLS)) {
if(t.hasHorizontalSpanning()) {
// iterate over the components and draw a line on the side of all
// the components other than the ones that are at the last column.
for(int cellRow = 0 ; cellRow < rows ; cellRow++) {
for(int cellColumn = 0 ; cellColumn < cols - 1 ; cellColumn++) {
// if this isn't the last column
if(cellColumn + t.getCellHorizontalSpan(cellRow, cellColumn) - 1 != cols - 1) {
// if this is a spanned through cell we don't want to draw a line here
if(t.isCellSpannedThroughVertically(cellRow, cellColumn)) {
continue;
}
int x = t.getColumnPosition(cellColumn);
int y = t.getRowPosition(cellRow);
int rowHeight;
int columnWidth = t.getColumnPosition(cellColumn + t.getCellHorizontalSpan(cellRow, cellColumn)) - x;
if(cellRow < getModel().getRowCount() - 1) {
rowHeight = t.getRowPosition(cellRow + 1) - y;
} else {
rowHeight = getHeight() - y;
}
g.drawLine(x + columnWidth, y, x + columnWidth, y + rowHeight);
}
}
}
} else {
for(int col = 1 ; col < cols ; col++) {
int x = t.getColumnPosition(col);
g.drawLine(x, 0, x, actualHeight);
//g.drawLine(x+2, 0+2, x+2, actualHeight-2);
}
}
}
} else { // separate border
//if ((!t.hasHorizontalSpanning()) && (!t.hasVerticalSpanning())) {
for(int row = 0 ; row < rows; row++) {
int y = t.getRowPosition(row);
int h;
if (row+1<rows) {
h=t.getRowPosition(row+1)-y;
} else {
h=getY()+actualHeight-y-2;
}
for(int col = 0 ; col < cols ; col++) {
int x = t.getColumnPosition(col);
int w;
if (col+1<cols) {
w=t.getColumnPosition(col+1)-x;
} else {
w=getX()+actualWidth-x-2;
}
Component comp=t.getComponentAt(row, col);
if ((comp.isVisible()) &&
((drawEmptyCellsBorder) ||
((comp.getWidth()-comp.getStyle().getPadding(false, Component.RIGHT) - comp.getStyle().getPadding(false, Component.LEFT)>0) &&
(comp.getHeight()-comp.getStyle().getPadding(false, Component.TOP) - comp.getStyle().getPadding(false, Component.BOTTOM)>0)))) {
int rightMargin=comp.getStyle().getMargin(Component.RIGHT);
int bottomMargin=comp.getStyle().getMargin(Component.BOTTOM);
if (col==0) {
rightMargin*=2; // Since the first cell includes margins from both sides (left/right) so the next cell location is farther away - but we don't want to paint the border up to it
}
if (row==0) {
bottomMargin*=2;
}
g.drawRect(x+comp.getStyle().getMargin(Component.LEFT), y+comp.getStyle().getMargin(Component.TOP), w-2-rightMargin, h-2-bottomMargin);
}
}
}
}
g.translate(-xPos, -yPos);
}
}
private Component createCellImpl(Object value, final int row, final int column, boolean editable) {
Component c = createCell(value, row, column, editable);
c.putClientProperty("row", new Integer(row));
c.putClientProperty("column", new Integer(column));
// we do this here to allow subclasses to return a text area or its subclass
if(c instanceof TextArea) {
((TextArea)c).addActionListener(listener);
}
Style s = c.getSelectedStyle();
//s.setMargin(0, 0, 0, 0);
s.setMargin(verticalBorderSpacing, verticalBorderSpacing, horizontalBorderSpacing, horizontalBorderSpacing);
if ((drawBorder) && (innerBorder!=INNER_BORDERS_NONE)) {
s.setBorder(null);
s = c.getUnselectedStyle();
s.setBorder(null);
} else {
s = c.getUnselectedStyle();
}
s.setBgTransparency(0);
//s.setMargin(0, 0, 0, 0);
s.setMargin(verticalBorderSpacing, verticalBorderSpacing, horizontalBorderSpacing, horizontalBorderSpacing);
return c;
}
/**
* Creates a cell based on the given value
*
* @param value the new value object
* @param row row number, -1 for the header rows
* @param column column number
* @param editable true if the cell is editable
* @return cell component instance
*/
protected Component createCell(Object value, int row, int column, boolean editable) {
if(row == -1) {
Label header = new Label((String)value);
header.setUIID("TableHeader");
header.setAlignment(titleAlignment);
header.setFocusable(true);
return header;
}
if(editable) {
TextField cell = new TextField("" + value, -1);
cell.setLeftAndRightEditingTrigger(false);
cell.setUIID("TableCell");
return cell;
}
Label cell = new Label("" + value);
cell.setUIID("TableCell");
cell.setAlignment(cellAlignment);
cell.setFocusable(true);
return cell;
}
/**
* @inheritDoc
*/
public void initComponent() {
// this can happen if deinitialize is invoked due to a menu command which modifies
// the content of the table while the listener wasn't bound
if(potentiallyDirtyModel) {
updateModel();
potentiallyDirtyModel = false;
}
model.addDataChangeListener(listener);
}
/**
* @inheritDoc
*/
public void deinitialize() {
// we unbind the listener to prevent a memory leak for the use case of keeping
// the model while discarding the component
// Prevent the model listener from being removed when the VKB is shown
if(!Display.getInstance().isVirtualKeyboardShowing()) {
potentiallyDirtyModel = true;
model.removeDataChangeListener(listener);
} else {
potentiallyDirtyModel = false;
}
}
/**
* Replaces the underlying model
*
* @param model the new model
*/
public void setModel(TableModel model) {
this.model = model;
updateModel();
revalidate();
}
/**
* Returns the model instance
*
* @return the model instance
*/
public TableModel getModel() {
return model;
}
/**
* Indicates whether the table border should be drawn
*
* @return the drawBorder
*/
public boolean isDrawBorder() {
return drawBorder;
}
/**
* Indicates whether the table border should be drawn
*
* @param drawBorder the drawBorder to set
*/
public void setDrawBorder(boolean drawBorder) {
this.drawBorder = drawBorder;
repaint();
}
/**
* Sets how to draw the inner border (All of it, only rows/columns, none, groups)
* Note that setting to any mode other than NONE/ALL will result in the border drawing as collapsed whether this is a collpased border or not
*
* @param innerBorder one of the INNER_BORDER_* constants
*/
public void setInnerBorderMode(int innerBorder) {
if ((innerBorder<INNER_BORDERS_NONE) || (innerBorder>INNER_BORDERS_ALL)) {
throw new IllegalArgumentException("Inner border mode must be one of the INNER_BORDER_* constants");
}
this.innerBorder=innerBorder;
repaint();
}
/**
* Returns the current inner border mode
*
* @return the current inner border mode (one of the INNER_BORDER_* constants)
*/
public int getInnerBorderMode() {
return innerBorder;
}
/**
* Returns whether an inner border should be drawn after the specified row.
* This allows customization in subclasses to create for example the effects of segments in atable, i.e. instead of a line after each row - lines after "chunks" of rows.
* Note that this is queried only when the inner border mode is set to INNER_BORDER_ROWS
*
* @param row The row in question
* @return true to draw inner border, false otherwise
*/
protected boolean shouldDrawInnerBorderAfterRow(int row) {
return true;
}
/**
* Indicates whether the borders of the cells should collapse to form a one line border
*
* @param collapseBorder true to collapse (default), false for separate borders
*/
public void setCollapseBorder(boolean collapseBorder) {
if (this.collapseBorder!=collapseBorder) {
this.collapseBorder = collapseBorder;
if ((horizontalBorderSpacing!=0) || (verticalBorderSpacing!=0)) { // Only if one of the spacing was not 0, then we need to update, since otherwise the margin is 0 for both collapse and separate modes
updateMargins();
}
repaint();
}
}
/**
* Indicates whether empty cells should have borders (relevant only for separate borders and not for collapsed)
*
* @param drawEmptyCellsBorder - true to draw (default), false otherwise
*/
public void setDrawEmptyCellsBorder(boolean drawEmptyCellsBorder) {
this.drawEmptyCellsBorder = drawEmptyCellsBorder;
repaint();
}
/**
* Sets the spacing of cells border (relevant only for separate borders and not for collapsed)
*
* @param horizontal - The horizontal spacing
* @param vertical - The vertical spacing
*/
public void setBorderSpacing(int horizontal, int vertical) {
horizontalBorderSpacing=horizontal;
verticalBorderSpacing=vertical;
updateMargins();
}
private void updateMargins() {
TableLayout t = (TableLayout)getLayout();
int hSpace=horizontalBorderSpacing;
int vSpace=verticalBorderSpacing;
if (collapseBorder) { // not relevant for collapse border
hSpace=0;
vSpace=0;
}
if ((!t.hasHorizontalSpanning()) && (!t.hasVerticalSpanning())) {
for(int row = 0 ; row < t.getRows(); row++) {
for(int col = 0 ; col < t.getColumns() ; col++) {
Component cmp=null;
try {
cmp=t.getComponentAt(row, col);
} catch (Exception e) {
// parent of cmp can be null as well - TODO - check why
}
if (cmp!=null) {
int leftMargin=(col==0)?hSpace:0;
int topMargin=(row==0)?vSpace:0;
cmp.getUnselectedStyle().setMargin(topMargin, vSpace, leftMargin, hSpace);
cmp.getSelectedStyle().setMargin(topMargin, vSpace, leftMargin, hSpace);
}
}
}
}
repaint();
}
/**
* Indicates the alignment of the title see label alignment for details
*
* @return the title alignment
* @see com.sun.lwuit.Label#setAlignment(int)
*/
public int getTitleAlignment() {
return titleAlignment;
}
/**
* Indicates the alignment of the title see label alignment for details
*
* @param titleAlignment the title alignment
* @see com.sun.lwuit.Label#setAlignment(int)
*/
public void setTitleAlignment(int titleAlignment) {
this.titleAlignment = titleAlignment;
repaint();
}
/**
* Returns the column in which the given cell is placed
*
* @param cell the component representing the cell placed in the table
* @return the column in which the cell was placed in the table
*/
public int getCellColumn(Component cell) {
Integer i = ((Integer)cell.getClientProperty("column"));
if(i != null) {
return i.intValue();
}
return -1;
}
/**
* Returns the row in which the given cell is placed
*
* @param cell the component representing the cell placed in the table
* @return the row in which the cell was placed in the table
*/
public int getCellRow(Component cell) {
Integer i = ((Integer)cell.getClientProperty("row"));
if(i != null) {
return i.intValue();
}
return -1;
}
/**
* Indicates the alignment of the cells see label alignment for details
*
* @see com.sun.lwuit.Label#setAlignment(int)
* @return the cell alignment
*/
public int getCellAlignment() {
return cellAlignment;
}
/**
* Indicates the alignment of the cells see label alignment for details
*
* @param cellAlignment the table cell alignment
* @see com.sun.lwuit.Label#setAlignment(int)
*/
public void setCellAlignment(int cellAlignment) {
this.cellAlignment = cellAlignment;
repaint();
}
/**
* Indicates whether the table should render a table header as the first row
*
* @return the includeHeader
*/
public boolean isIncludeHeader() {
return includeHeader;
}
/**
* Indicates whether the table should render a table header as the first row
*
* @param includeHeader the includeHeader to set
*/
public void setIncludeHeader(boolean includeHeader) {
this.includeHeader = includeHeader;
updateModel();
}
/**
* Creates the table cell constraint for the given cell, this method can be overriden for
* the purposes of modifying the table constraints.
*
* @param value the value of the cell
* @param row the table row
* @param column the table column
* @return the table constraint
*/
protected TableLayout.Constraint createCellConstraint(Object value, int row, int column) {
if(includeHeader) {
row++;
}
TableLayout t = (TableLayout)getLayout();
return t.createConstraint(row, column);
}
/**
* @inheritDoc
*/
public String[] getPropertyNames() {
return new String[] {"data", "header"};
}
/**
* @inheritDoc
*/
public Class[] getPropertyTypes() {
return new Class[] {String[][].class, String[].class};
}
/**
* @inheritDoc
*/
public Object getPropertyValue(String name) {
if(name.equals("data")) {
return ((DefaultTableModel)model).data;
}
if(name.equals("header")) {
return ((DefaultTableModel)model).columnNames;
}
return null;
}
/**
* @inheritDoc
*/
public String setPropertyValue(String name, Object value) {
if(name.equals("data")) {
setModel(new DefaultTableModel(((DefaultTableModel)model).columnNames, (String[][])value));
return null;
}
if(name.equals("header")) {
setModel(new DefaultTableModel((String[])value, ((DefaultTableModel)model).data));
return null;
}
return super.setPropertyValue(name, value);
}
class Listener implements DataChangedListener, ActionListener {
/**
* @inheritDoc
*/
public final void dataChanged(int row, int column) {
Object value = model.getValueAt(row, column);
boolean e = model.isCellEditable(row, column);
Component cell = createCellImpl(value, row, column, e);
TableLayout t = (TableLayout)getLayout();
TableLayout.Constraint con = createCellConstraint(value, row, column);
if(includeHeader) {
row++;
}
Component c = t.getComponentAt(row, column);
removeComponent(c);
// a repaint sent right before this might result in an artifact for some use cases so
// removing visibility essentially cancels repaints
c.setVisible(false);
addComponent(con, cell);
layoutContainer();
cell.requestFocus();
revalidate();
}
public void actionPerformed(ActionEvent evt) {
TextArea t = (TextArea)evt.getSource();
int row = getCellRow(t);
int column = getCellColumn(t);
getModel().setValueAt(row, column, t.getText());
}
}
}