/*
* Beryl - A web platform based on XML, XSLT and Java
* This file is part of the Beryl XML GUI
*
* Copyright (C) 2004 Wenzel Jakob <wazlaf@tigris.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* This program 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-3107 USA
*/
package org.beryl.gui.widgets;
import java.awt.Component;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import javax.swing.AbstractCellEditor;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableModel;
import org.beryl.gui.GUIEvent;
import org.beryl.gui.GUIEventListener;
import org.beryl.gui.GUIException;
import org.beryl.gui.Widget;
import org.beryl.gui.WidgetInfo;
import org.beryl.gui.model.MapChangeEvent;
import org.beryl.gui.model.MapDataModel;
import org.beryl.gui.model.ModelChangeEvent;
import org.beryl.gui.model.ModelChangeListener;
import org.beryl.gui.model.TableChangeEvent;
import org.beryl.gui.model.TableDataModel;
import org.beryl.gui.model.TableRow;
import org.beryl.gui.table.TableSorter;
public class Table extends Widget {
protected static WidgetInfo tableInfo = null;
private JTable table = null;
private JScrollPane scrollPane = null;
private TableSorter sorter = null;
private TableModel messageModel = null;
private TableDataModel tableDataModel = null;
private boolean sendEvents = true;
private boolean processEvents = true;
private ArrayList columnKeys = null;
private ArrayList columnLabels = null;
private ArrayList columnSizes = null;
private Class[] columnClasses = null;
private String indexKey = null;
private String valueKey = null;
private String message = null;
private JTableHeader tableHeader = null;
static {
tableInfo = new WidgetInfo(Table.class, widgetInfo);
tableInfo.addProperty("indexkey", "string", "");
tableInfo.addProperty("valuekey", "string", "");
tableInfo.addProperty("verticalScrollBar", "bool", Boolean.FALSE);
tableInfo.addProperty("horizontalScrollBar", "bool", Boolean.FALSE);
tableInfo.addProperty("selectionMode", "enum", "multiple_interval");
tableInfo.addEvent("rightclick");
tableInfo.addEvent("doubleclick");
};
/**
* Custom table model to display information messages
*/
private class MessageModel extends AbstractTableModel {
public int getColumnCount() {
return 1;
}
public int getRowCount() {
return 1;
}
public String getColumnName(int column) {
return "";
}
public Object getValueAt(int rowIndex, int columnIndex) {
return message;
}
};
/**
* The custom cell editor lets you add your own widgets as renderer
* components of a <tt>Table</tt>.
*/
private class CustomCellRenderer implements TableCellRenderer {
private Widget widget = null;
public CustomCellRenderer(Widget widget) {
this.widget = widget;
}
public Component getTableCellRendererComponent(
JTable table,
Object value,
boolean isSelected,
boolean hasFocus,
int row,
int column) {
try {
if (table.isRowSelected(row))
widget.setProperty("background", table.getSelectionBackground());
else
widget.setProperty("background", table.getBackground());
} catch (GUIException e) {
throw new RuntimeException(e);
}
return widget.getRealWidget();
}
}
/**
* The custom cell editor lets you add your own widgets as editor
* components of a <tt>Table</tt>.
* The Table's data model must contain a key called "value" from which
* the edited value will be extracted
*/
private class CustomCellEditor extends AbstractCellEditor implements TableCellEditor {
private Widget widget = null;
private CustomCellEditor(Widget widget) {
this.widget = widget;
}
public Component getTableCellEditorComponent(
JTable table,
Object value,
boolean isSelected,
int row,
int column) {
try {
widget.setProperty("background", table.getSelectionBackground());
} catch (GUIException e) {
throw new RuntimeException(e);
}
return widget.getRealWidget();
}
public Object getCellEditorValue() {
return widget.getDataModel().getValue("value");
}
};
private class TableDataModelAdapter implements TableModel, ModelChangeListener {
private TableDataModel model = null;
private ArrayList listeners = null;
public TableDataModelAdapter(TableDataModel model) {
this.model = model;
model.addModelChangeListener(this);
listeners = new ArrayList();
}
public int getColumnCount() {
return columnKeys.size();
}
public int getRowCount() {
return model.getRowCount();
}
public Object getValueAt(int row, int column) {
return model.getValue(row, (String) columnKeys.get(column));
}
public void setValueAt(Object value, int row, int column) {
try {
model.setValue(row, (String) columnKeys.get(column), value);
} catch (GUIException e) {
throw new RuntimeException(e);
}
}
public boolean isCellEditable(int row, int column) {
return model.isEditable(row, (String) columnKeys.get(column));
}
public Class getColumnClass(int column) {
if (columnClasses != null)
return columnClasses[column];
return Object.class;
}
public String getColumnName(int column) {
return (String) columnLabels.get(column);
}
public void addTableModelListener(TableModelListener listener) {
listeners.add(listener);
}
public void removeTableModelListener(TableModelListener listener) {
listeners.remove(listener);
}
public void modelChanged(ModelChangeEvent e) {
if (message == null) {
TableChangeEvent tce = (TableChangeEvent) e;
TableModelEvent event =
new TableModelEvent(
this,
tce.getFirstIndex(),
tce.getLastIndex(),
columnKeys.indexOf(tce.getKey()),
tce.getType());
try {
sendEvents = false;
processEvents = false;
for (int i = 0; i < listeners.size(); i++) {
((TableModelListener) listeners.get(i)).tableChanged(event);
}
synchronizeDataModel();
} catch (GUIException ex) {
throw new RuntimeException(ex);
} finally {
sendEvents = true;
processEvents = true;
}
table.repaint();
}
}
}
public Table(Widget parent, String name) throws GUIException {
super(parent, name);
table = new JTable() {
/**
* Swing bugfix: Allow different renderers/editor for each column,
* removes the limitation of 1 renderer/editor per column
*/
public TableCellRenderer getCellRenderer(int row, int column) {
if (message == null) {
int sortedRow = sorter.getSortedRowForRow(row);
try {
TableRow tableRow = tableDataModel.getTableRow(sortedRow);
String key = (String) columnKeys.get(column);
if (tableRow.hasCustomRenderer(key)) {
Widget renderer =
tableRow.getRenderer(
Table.this,
getValueAt(row, column),
isCellSelected(row, column),
hasFocus(),
tableRow,
key);
if (renderer != null) {
return new CustomCellRenderer(renderer);
}
}
} catch (GUIException e) {
/* There should be no exception here */
throw new RuntimeException(e);
}
if (tableDataModel != null) {
Object object = tableDataModel.getValue(sortedRow, (String) columnKeys.get(column));
if (object != null) {
return getDefaultRenderer(object.getClass());
}
}
}
/* Fall back to the JTable internal processing */
return super.getCellRenderer(row, column);
}
public TableCellEditor getCellEditor(int row, int column) {
if (message == null) {
int sortedRow = sorter.getSortedRowForRow(row);
try {
TableRow tableRow = tableDataModel.getTableRow(sortedRow);
String key = (String) columnKeys.get(column);
Widget editor =
tableRow.getEditor(
Table.this,
getValueAt(row, column),
tableDataModel.getTableRow(sortedRow),
key);
if (editor != null) {
return new CustomCellEditor(editor);
}
} catch (GUIException e) {
/* There should be no exception here */
throw new RuntimeException(e);
}
if (tableDataModel != null) {
Object object = tableDataModel.getValue(sortedRow, (String) columnKeys.get(column));
if (object != null) {
return getDefaultEditor(object.getClass());
}
}
}
/* Fall back to the JTable internal processing */
return super.getCellEditor(row, column);
}
};
tableHeader = table.getTableHeader();
sorter = new TableSorter();
sorter.addMouseListenerToHeaderInTable(table);
messageModel = new MessageModel();
scrollPane = new JScrollPane(table);
columnKeys = new ArrayList();
columnLabels = new ArrayList();
columnSizes = new ArrayList();
table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
try {
if (sendEvents && !e.getValueIsAdjusting()) {
synchronizeDataModel();
}
} catch (GUIException ex) {
throw new RuntimeException(ex);
}
}
});
table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
table.setRowSelectionAllowed(true);
table.setColumnSelectionAllowed(false);
}
/**
* This lets you hide the current table and display a message instead.
* A call with a null message makes the table visible again. Use this
* method to indicate for example that there were "No Results" for
* a database query
*/
public void setMessage(String message) {
this.message = message;
if (message == null) {
table.setTableHeader(tableHeader);
table.setModel(sorter);
} else if (message != null) {
table.setTableHeader(null);
table.setModel(messageModel);
}
}
public void setColumnClasses(Class columnClasses[]) {
this.columnClasses = columnClasses;
}
private void synchronizeDataModel() throws GUIException {
MapDataModel model = getDataModel();
if (model == null)
return;
if (indexKey != null) {
int indices[] = table.getSelectedRows();
for (int i = 0; i < indices.length; i++) {
indices[i] = sorter.getRowForSortedRow(indices[i]);
}
model.setValue(Table.this, indexKey, indices);
}
if (valueKey != null) {
int indices[] = table.getSelectedRows();
TableRow rows[] = new TableRow[indices.length];
for (int i = 0; i < indices.length; i++) {
rows[i] = tableDataModel.getTableRow(sorter.getRowForSortedRow(indices[i]));
}
model.setValue(Table.this, valueKey, rows);
}
}
public void setTableDataModel(TableDataModel tableDataModel) throws GUIException {
ModelChangeEvent event = new ModelChangeEvent(this, tableDataModel);
this.tableDataModel = tableDataModel;
try {
sendEvents = false;
table.editingStopped(null); /* This is necessary to ensure the changes won't be discarded */
TableDataModelAdapter adapter = new TableDataModelAdapter(tableDataModel);
sorter.setModel(adapter);
table.setModel(sorter);
} finally {
sendEvents = true;
}
/* Reload data model information */
modelChanged(event);
for (int i = 0; i < columnSizes.size(); i++) {
Integer size = (Integer) columnSizes.get(i);
if (size != null) {
table.getColumnModel().getColumn(i).setPreferredWidth(size.intValue());
}
}
}
public TableDataModel getTableDataModel() {
return this.tableDataModel;
}
public void setProperty(String name, Object value) throws GUIException {
if (name.startsWith("column.")) {
String key = name.substring(7, name.length());
if (columnKeys.contains(key)) {
columnLabels.set(columnKeys.indexOf(key), (String) value);
} else {
columnKeys.add(key);
columnLabels.add((String) value);
columnSizes.add(null);
}
} else if (name.startsWith("columnsize.")) {
int column = columnKeys.indexOf(name.substring(11, name.length()));
if (column != -1) {
columnSizes.set(column, value);
} else {
throw new GUIException("Invalid column name while trying to set column size");
}
} else if ("verticalScrollBar".equals(name)) {
scrollPane.setVerticalScrollBarPolicy(
((Boolean) value).booleanValue()
? JScrollPane.VERTICAL_SCROLLBAR_ALWAYS
: JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
} else if ("horizontalScrollBar".equals(name)) {
scrollPane.setHorizontalScrollBarPolicy(
((Boolean) value).booleanValue()
? JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS
: JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
} else if ("indexkey".equals(name)) {
indexKey = (String) value;
} else if ("valuekey".equals(name)) {
valueKey = (String) value;
} else {
super.setProperty(name, value);
}
}
private void setSelectionValues(TableRow[] values) {
ListSelectionModel model = table.getSelectionModel();
if (values == null) {
table.clearSelection();
} else {
table.clearSelection();
for (int i = 0; i < values.length; i++) {
int index = sorter.getSortedRowForRow(tableDataModel.indexOf(values[i]));
table.addRowSelectionInterval(index, index);
}
}
}
private void setSelectionIndices(int[] indices) {
ListSelectionModel model = table.getSelectionModel();
if (indices == null) {
table.clearSelection();
} else {
table.clearSelection();
for (int i = 0; i < indices.length; i++) {
int index = sorter.getSortedRowForRow(indices[i]);
table.addRowSelectionInterval(index, index);
}
}
}
public void reload() throws GUIException {
MapDataModel model = getDataModel();
if (model != null) {
try {
processEvents = false;
int[] indices = indexKey == null ? null : (int[]) model.getValue(indexKey);
TableRow values[] = valueKey == null ? null : (TableRow[]) model.getValue(valueKey);
if (indices != null) {
setSelectionIndices(indices);
} else if (values != null) {
setSelectionValues(values);
}
if (((values != null && indices == null) || (values == null && indices == null)) && indexKey != null) {
int indices2[] = table.getSelectedRows();
for (int i = 0; i < indices2.length; i++) {
indices2[i] = sorter.getRowForSortedRow(indices[i]);
}
model.setValue(Table.this, indexKey, indices);
}
if (((indices != null && values == null) || (values == null && indices == null)) && valueKey != null) {
int indices2[] = table.getSelectedRows();
TableRow rows[] = new TableRow[indices2.length];
for (int i = 0; i < indices2.length; i++)
rows[i] = tableDataModel.getTableRow(sorter.getRowForSortedRow(indices2[i]));
model.setValue(Table.this, valueKey, rows);
}
} finally {
processEvents = true;
}
}
}
public void modelChanged(ModelChangeEvent e) throws GUIException {
if (processEvents) {
try {
sendEvents = false;
if (e.getSource() == this) {
try {
reload();
} catch (IllegalArgumentException ex) {
/* Ignore, table data model is not yet set */
} catch (ArrayIndexOutOfBoundsException ex) {
/* Ignore, table data model is not yet set */
}
} else if (e instanceof MapChangeEvent) {
MapChangeEvent event = (MapChangeEvent) e;
if (event.getKey() == null) {
reload();
} else if (event.getKey().equals(indexKey)) {
setSelectionIndices((int[]) ((MapChangeEvent) e).getNewValue());
try {
processEvents = false;
if (valueKey != null) {
int indices[] = table.getSelectedRows();
TableRow rows[] = new TableRow[indices.length];
for (int i = 0; i < indices.length; i++)
rows[i] = tableDataModel.getTableRow(sorter.getRowForSortedRow(indices[i]));
((MapDataModel) event.getModel()).setValue(Table.this, valueKey, rows);
}
} finally {
processEvents = true;
}
} else if (event.getKey().equals(valueKey)) {
setSelectionValues((TableRow[]) ((MapChangeEvent) e).getNewValue());
try {
processEvents = false;
if (indexKey != null) {
int indices[] = table.getSelectedRows();
for (int i = 0; i < indices.length; i++) {
indices[i] = sorter.getRowForSortedRow(indices[i]);
}
((MapDataModel) event.getModel()).setValue(Table.this, indexKey, indices);
}
} finally {
processEvents = true;
}
}
}
} finally {
sendEvents = true;
}
}
}
public void addListener(String event, final String name, final GUIEventListener listener) throws GUIException {
if ("rightclick".equals(event)) {
table.addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent me) {
if (me.isPopupTrigger() && message == null) {
listener.eventOccured(new GUIEvent(Table.this, name, me));
}
}
public void mouseReleased(MouseEvent me) {
mousePressed(me);
}
});
} else if (event.equals("doubleclick")) {
table.addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent me) {
if (me.getClickCount() == 2 && message == null) {
listener.eventOccured(new GUIEvent(Table.this, name, me));
}
}
});
} else {
super.addListener(event, name, listener);
}
}
public Component getWidget() {
return scrollPane;
}
public Component getRealWidget() {
return table;
}
public WidgetInfo getWidgetInfo() {
return tableInfo;
}
}