/******************************************************************************
* Product: Posterita Ajax UI *
* Copyright (C) 2007 Posterita Ltd. All Rights Reserved. *
* This program is free software; you can redistribute it and/or modify it *
* under the terms version 2 of the GNU General Public License as published *
* by the Free Software Foundation. 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 General Public License for more details. *
* You should have received a copy of the GNU General Public License along *
* with this program; if not, write to the Free Software Foundation, Inc., *
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. *
* For the text or an alternative of this public license, you may reach us *
* Posterita Ltd., 3, Draper Avenue, Quatre Bornes, Mauritius *
* or via info@posterita.org or http://www.posterita.org/ *
*****************************************************************************/
package org.adempiere.webui.component;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.adempiere.webui.apps.AEnv;
import org.adempiere.webui.event.TableValueChangeEvent;
import org.adempiere.webui.event.TableValueChangeListener;
import org.compiere.minigrid.IDColumn;
import org.compiere.util.DisplayType;
import org.compiere.util.Env;
import org.compiere.util.MSort;
import org.compiere.util.Util;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zul.Decimalbox;
import org.zkoss.zul.ListModel;
import org.zkoss.zul.Listbox;
import org.zkoss.zul.Listcell;
import org.zkoss.zul.Listitem;
import org.zkoss.zul.ListitemRenderer;
import org.zkoss.zul.ListitemRendererExt;
/**
* Renderer for {@link org.adempiere.webui.component.ListItems}
* for the {@link org.adempiere.webui.component.Listbox}.
*
* @author Andrew Kimball
*
*/
public class WListItemRenderer implements ListitemRenderer, EventListener, ListitemRendererExt
{
/** Array of listeners for changes in the table components. */
protected ArrayList<TableValueChangeListener> m_listeners =
new ArrayList<TableValueChangeListener>();
/** A list containing the indices of the currently selected ListItems. */
private Set<ListItem> m_selectedItems = new HashSet<ListItem>();
/** Array of table details. */
private ArrayList<WTableColumn> m_tableColumns = new ArrayList<WTableColumn>();
/** Array of {@link ListHeader}s for the list head. */
private ArrayList<ListHeader> m_headers = new ArrayList<ListHeader>();
private Listbox listBox;
private EventListener cellListener;
/**
* Default constructor.
*
*/
public WListItemRenderer()
{
super();
}
/**
* Constructor specifying the column headers.
*
* @param columnNames vector of column titles.
*/
public WListItemRenderer(List< ? extends String> columnNames)
{
super();
WTableColumn tableColumn;
for (String columnName : columnNames)
{
tableColumn = new WTableColumn();
tableColumn.setHeaderValue(Util.cleanAmp(columnName));
m_tableColumns.add(tableColumn);
}
}
/**
* Get the column details of the specified <code>column</code>.
*
* @param columnIndex The index of the column for which details are to be retrieved.
* @return The details of the column at the specified index.
*/
private WTableColumn getColumn(int columnIndex)
{
try
{
return m_tableColumns.get(columnIndex);
}
catch (IndexOutOfBoundsException exception)
{
throw new IllegalArgumentException("There is no WTableColumn at column "
+ columnIndex);
}
}
/* (non-Javadoc)
* @see org.zkoss.zul.ListitemRenderer#render(org.zkoss.zul.Listitem, java.lang.Object)
*/
public void render(Listitem item, Object data) throws Exception
{
render((ListItem)item, data);
}
/**
* Renders the <code>data</code> to the specified <code>Listitem</code>.
*
* @param item the listitem to render the result.
* Note: when this method is called, the listitem has no child
* at all.
* @param data that is returned from {@link ListModel#getElementAt}
* @throws Exception
* @see {@link #render(Listitem, Object)}
*/
private void render(ListItem item, Object data)
{
Listcell listcell = null;
int colIndex = 0;
int rowIndex = item.getIndex();
WListbox table = null;
if (item.getListbox() instanceof WListbox)
{
table = (WListbox)item.getListbox();
}
if (!(data instanceof List))
{
throw new IllegalArgumentException("A model element was not a list");
}
if (listBox == null || listBox != item.getListbox())
{
listBox = item.getListbox();
}
if (cellListener == null)
{
cellListener = new CellListener();
}
for (Object field : (List<?>)data)
{
listcell = getCellComponent(table, field, rowIndex, colIndex);
listcell.setParent(item);
listcell.addEventListener(Events.ON_DOUBLE_CLICK, cellListener);
colIndex++;
}
return;
}
/**
* Generate the cell for the given <code>field</code>.
*
* @param table The table into which the cell will be placed.
* @param field The data field for which the cell is to be created.
* @param rowIndex The row in which the cell is to be placed.
* @param columnIndex The column in which the cell is to be placed.
* @return The list cell component.
*/
private Listcell getCellComponent(WListbox table, Object field,
int rowIndex, int columnIndex)
{
ListCell listcell = new ListCell();
boolean isCellEditable = table != null ? table.isCellEditable(rowIndex, columnIndex) : false;
// TODO put this in factory method for generating cell renderers, which
// are assigned to Table Columns
if (field != null)
{
if (field instanceof Boolean)
{
listcell.setValue(Boolean.valueOf(field.toString()));
if (table != null && columnIndex == 0)
table.setCheckmark(false);
Checkbox checkbox = new Checkbox();
checkbox.setChecked(Boolean.valueOf(field.toString()));
if (isCellEditable)
{
checkbox.setEnabled(true);
checkbox.addEventListener(Events.ON_CHECK, this);
}
else
{
checkbox.setEnabled(false);
}
listcell.appendChild(checkbox);
ZkCssHelper.appendStyle(listcell, "text-align:center");
}
else if (field instanceof Number)
{
DecimalFormat format = field instanceof BigDecimal
? DisplayType.getNumberFormat(DisplayType.Amount, AEnv.getLanguage(Env.getCtx()))
: DisplayType.getNumberFormat(DisplayType.Integer, AEnv.getLanguage(Env.getCtx()));
// set cell value to allow sorting
listcell.setValue(field.toString());
if (isCellEditable)
{
NumberBox numberbox = new NumberBox(false);
numberbox.setFormat(format);
numberbox.setValue(field);
numberbox.setWidth("100px");
numberbox.setEnabled(true);
numberbox.setStyle("text-align:right; "
+ listcell.getStyle());
numberbox.addEventListener(Events.ON_CHANGE, this);
listcell.appendChild(numberbox);
}
else
{
listcell.setLabel(format.format(((Number)field).doubleValue()));
ZkCssHelper.appendStyle(listcell, "text-align:right");
}
}
else if (field instanceof Timestamp)
{
SimpleDateFormat dateFormat = DisplayType.getDateFormat(DisplayType.Date, AEnv.getLanguage(Env.getCtx()));
listcell.setValue(dateFormat.format((Timestamp)field));
if (isCellEditable)
{
Datebox datebox = new Datebox();
datebox.setFormat(dateFormat.toPattern());
datebox.setValue(new Date(((Timestamp)field).getTime()));
datebox.addEventListener(Events.ON_CHANGE, this);
listcell.appendChild(datebox);
}
else
{
listcell.setLabel(dateFormat.format((Timestamp)field));
}
}
else if (field instanceof String)
{
listcell.setValue(field.toString());
if (isCellEditable)
{
Textbox textbox = new Textbox();
textbox.setValue(field.toString());
textbox.addEventListener(Events.ON_CHANGE, this);
listcell.appendChild(textbox);
}
else
{
listcell.setLabel(field.toString());
}
}
// if ID column make it invisible
else if (field instanceof IDColumn)
{
listcell.setValue(((IDColumn) field).getRecord_ID());
if (!table.isCheckmark()) {
table.setCheckmark(true);
table.removeEventListener(Events.ON_SELECT, this);
table.addEventListener(Events.ON_SELECT, this);
}
}
else
{
listcell.setLabel(field.toString());
listcell.setValue(field.toString());
}
}
else
{
listcell.setLabel("");
listcell.setValue("");
}
return listcell;
}
/**
* Update Table Column.
*
* @param index The index of the column to update
* @param header The header text for the column
*/
public void updateColumn(int index, String header)
{
WTableColumn tableColumn;
tableColumn = getColumn(index);
tableColumn.setHeaderValue(Util.cleanAmp(header));
return;
} // updateColumn
/**
* Add Table Column.
* after adding a column, you need to set the column classes again
* (DefaultTableModel fires TableStructureChanged, which calls
* JTable.tableChanged .. createDefaultColumnsFromModel
* @param header The header text for the column
*/
public void addColumn(String header)
{
WTableColumn tableColumn;
tableColumn = new WTableColumn();
tableColumn.setHeaderValue(Util.cleanAmp(header));
m_tableColumns.add(tableColumn);
return;
} // addColumn
/**
* Get the number of columns.
* @return the number of columns
*/
public int getNoColumns()
{
return m_tableColumns.size();
}
/**
* This is unused.
* The readonly proprty of a column should be set in
* the parent table.
*
* @param colIndex
* @param readOnly
* @deprecated
*/
public void setRO(int colIndex, Boolean readOnly)
{
return;
}
/**
* Create a ListHeader using the given <code>headerValue</code> to
* generate the header text.
* The <code>toString</code> method of the <code>headerValue</code>
* is used to set the header text.
*
* @param headerValue The object to use for generating the header text.
* @param headerIndex The column index of the header
* @param classType
* @return The generated ListHeader
* @see #renderListHead(ListHead)
*/
private Component getListHeaderComponent(Object headerValue, int headerIndex, Class<?> classType)
{
ListHeader header = null;
String headerText = headerValue.toString();
if (m_headers.size() <= headerIndex || m_headers.get(headerIndex) == null)
{
if (classType != null && classType.isAssignableFrom(IDColumn.class))
{
header = new ListHeader("");
header.setWidth("20px");
}
else
{
Comparator<Object> ascComparator = getColumnComparator(true, headerIndex);
Comparator<Object> dscComparator = getColumnComparator(false, headerIndex);
header = new ListHeader(headerText);
header.setSort("auto");
header.setSortAscending(ascComparator);
header.setSortDescending(dscComparator);
int width = headerText.trim().length() * 9;
if (width > 300)
width = 300;
else if (classType != null)
{
if (classType.equals(String.class))
{
if (width > 0 && width < 180)
width = 180;
}
else if (classType.equals(IDColumn.class))
{
header.setSort("none");
if (width == 0)
width = 30;
}
else if (width > 0 && width < 100 && (classType == null || !classType.isAssignableFrom(Boolean.class)))
width = 100;
}
else if (width > 0 && width < 100)
width = 100;
header.setWidth(width + "px");
}
m_headers.add(header);
}
else
{
header = m_headers.get(headerIndex);
if (!header.getLabel().equals(headerText))
{
header.setLabel(headerText);
}
}
return header;
}
/**
* set custom list header
* @param index
* @param header
*/
public void setListHeader(int index, ListHeader header) {
int size = m_headers.size();
if (size <= index) {
while (size <= index) {
if (size == index)
m_headers.add(header);
else
m_headers.add(null);
size++;
}
} else
m_headers.set(index, header);
}
/**
* Obtain the comparator for a given column.
*
* @param ascending whether the comparator will sort ascending
* @param columnIndex the index of the column for which the comparator is required
* @return comparator for the given column for the given direction
*/
protected Comparator<Object> getColumnComparator(boolean ascending, final int columnIndex)
{
return new ColumnComparator(ascending, columnIndex);
}
public static class ColumnComparator implements Comparator<Object>
{
private int columnIndex;
private MSort sort;
public ColumnComparator(boolean ascending, int columnIndex)
{
this.columnIndex = columnIndex;
sort = new MSort(0, null);
sort.setSortAsc(ascending);
}
public int compare(Object o1, Object o2)
{
Object item1 = ((List<?>)o1).get(columnIndex);
Object item2 = ((List<?>)o2).get(columnIndex);
return sort.compare(item1, item2);
}
public int getColumnIndex()
{
return columnIndex;
}
}
/**
* Render the ListHead for the table with headers for the table columns.
*
* @param head The ListHead component to render.
* @see #addColumn(String)
* @see #WListItemRenderer(List)
*/
public void renderListHead(ListHead head)
{
Component header;
WTableColumn column;
for (int columnIndex = 0; columnIndex < m_tableColumns.size(); columnIndex++)
{
column = m_tableColumns.get(columnIndex);
header = getListHeaderComponent(column.getHeaderValue(), columnIndex, column.getColumnClass());
head.appendChild(header);
}
head.setSizable(true);
return;
}
/* (non-Javadoc)
* @see org.zkoss.zk.ui.event.EventListener#onEvent(org.zkoss.zk.ui.event.Event)
*/
public void onEvent(Event event) throws Exception
{
int col = -1;
int row = -1;
Object value = null;
TableValueChangeEvent vcEvent = null;
WTableColumn tableColumn;
Component source = event.getTarget();
if (isWithinListCell(source))
{
row = getRowPosition(source);
col = getColumnPosition(source);
tableColumn = m_tableColumns.get(col);
if (source instanceof Checkbox)
{
value = Boolean.valueOf(((Checkbox)source).isChecked());
}
else if (source instanceof Decimalbox)
{
value = ((Decimalbox)source).getValue();
}
else if (source instanceof Datebox)
{
value = ((Datebox)source).getValue();
}
else if (source instanceof Textbox)
{
value = ((Textbox)source).getValue();
}
if(value != null)
{
vcEvent = new TableValueChangeEvent(source,
tableColumn.getHeaderValue().toString(),
row, col,
value, value);
fireTableValueChange(vcEvent);
}
}
else if (event.getTarget() instanceof WListbox && Events.ON_SELECT.equals(event.getName()))
{
WListbox table = (WListbox) event.getTarget();
if (table.isCheckmark()) {
int cnt = table.getRowCount();
if (cnt == 0 || !(table.getValueAt(0, 0) instanceof IDColumn))
return;
//update IDColumn
tableColumn = m_tableColumns.get(0);
for (int i = 0; i < cnt; i++) {
IDColumn idcolumn = (IDColumn) table.getValueAt(i, 0);
Listitem item = table.getItemAtIndex(i);
value = item.isSelected();
Boolean old = idcolumn.isSelected();
if (!old.equals(value)) {
vcEvent = new TableValueChangeEvent(source,
tableColumn.getHeaderValue().toString(),
i, 0,
old, value);
fireTableValueChange(vcEvent);
}
}
}
}
return;
}
private boolean isWithinListCell(Component source) {
if (source instanceof Listcell)
return true;
Component c = source.getParent();
while(c != null) {
if (c instanceof Listcell)
return true;
c = c.getParent();
}
return false;
}
/**
* Get the row index of the given <code>source</code> component.
*
* @param source The component for which the row index is to be found.
* @return The row index of the given component.
*/
protected int getRowPosition(Component source)
{
Listcell cell;
ListItem item;
int row = -1;
cell = findListcell(source);
item = (ListItem)cell.getParent();
row = item.getIndex();
return row;
}
private Listcell findListcell(Component source) {
if (source instanceof Listcell)
return (Listcell) source;
Component c = source.getParent();
while(c != null) {
if (c instanceof Listcell)
return (Listcell) c;
c = c.getParent();
}
return null;
}
/**
* Get the column index of the given <code>source</code> component.
*
* @param source The component for which the column index is to be found.
* @return The column index of the given component.
*/
protected int getColumnPosition(Component source)
{
Listcell cell;
int col = -1;
cell = findListcell(source);
col = cell.getColumnIndex();
return col;
}
/**
* Reset the renderer.
* This should be called if the table using this renderer is cleared.
*/
public void clearColumns()
{
m_tableColumns.clear();
}
/**
* Clear the renderer.
* This should be called if the table using this renderer is cleared.
*/
public void clearSelection()
{
m_selectedItems.clear();
}
/**
* Add a listener for changes in the table's component values.
*
* @param listener The listener to add.
*/
public void addTableValueChangeListener(TableValueChangeListener listener)
{
if (listener == null)
{
return;
}
m_listeners.add(listener);
}
public void removeTableValueChangeListener(TableValueChangeListener listener)
{
if (listener == null)
{
return;
}
m_listeners.remove(listener);
}
/**
* Fire the given table value change <code>event</code>.
*
* @param event The event to pass to the listeners
*/
private void fireTableValueChange(TableValueChangeEvent event)
{
for (TableValueChangeListener listener : m_listeners)
{
listener.tableValueChange(event);
}
}
/* (non-Javadoc)
* @see org.zkoss.zul.ListitemRendererExt#getControls()
*/
public int getControls()
{
return DETACH_ON_RENDER;
}
/* (non-Javadoc)
* @see org.zkoss.zul.ListitemRendererExt#newListcell(org.zkoss.zul.Listitem)
*/
public Listcell newListcell(Listitem item)
{
ListCell cell = new ListCell();
cell.applyProperties();
return cell;
}
/* (non-Javadoc)
* @see org.zkoss.zul.ListitemRendererExt#newListitem(org.zkoss.zul.Listbox)
*/
public Listitem newListitem(Listbox listbox)
{
ListItem item = new ListItem();
item.applyProperties();
return item;
}
/**
* @param index
* @param header
*/
public void setColumnHeader(int index, String header)
{
if (index >= 0 && index < m_tableColumns.size())
{
m_tableColumns.get(index).setHeaderValue(Util.cleanAmp(header));
}
}
public void setColumnClass(int index, Class<?> classType) {
if (index >= 0 && index < m_tableColumns.size())
{
m_tableColumns.get(index).setColumnClass(classType);
}
}
class CellListener implements EventListener {
public CellListener() {
}
public void onEvent(Event event) throws Exception {
if (listBox != null && Events.ON_DOUBLE_CLICK.equals(event.getName())) {
Event evt = new Event(Events.ON_DOUBLE_CLICK, listBox);
Events.sendEvent(listBox, evt);
}
}
}
}