/* GroupsListModelImpl.java
Purpose:
Description:
History:
Fri Aug 29 23:22:57 2008, Created by tomyeh
Copyright (C) 2008 Potix Corporation. All Rights Reserved.
{{IS_RIGHT
This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zul.impl;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.zkoss.lang.Objects;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.util.ComponentCloneListener;
import org.zkoss.zul.AbstractListModel;
import org.zkoss.zul.GroupsModel;
import org.zkoss.zul.Rows;
import org.zkoss.zul.event.GroupsDataEvent;
import org.zkoss.zul.event.GroupsDataListener;
import org.zkoss.zul.event.ListDataEvent;
import org.zkoss.zul.ext.GroupingInfo;
import org.zkoss.zul.ext.GroupsSelectableModel;
import org.zkoss.zul.ext.GroupsSortableModel;
import org.zkoss.zul.ext.Selectable;
import org.zkoss.zul.ext.SelectionControl;
/**
* Encapsulates {@link org.zkoss.zul.GroupsModel} as an instance of {@link org.zkoss.zul.ListModel}
* such that it is easier to handle by {@link org.zkoss.zul.Listbox} and
* {@link org.zkoss.zul.Group}.
*
* @author tomyeh
* @since 3.5.0
*/
public class GroupsListModel<D, G, F> extends AbstractListModel<Object> implements GroupsSelectableModel<Object> {
protected GroupsModel<D, G, F> _model;
private transient int _size;
/** An array of the group offset.
* The group offset is the offset from 0 that a group shall appear
* at the client.
* <p>For example, _gpofs[2] is the offset of group 2 (the third group)
* at the client.
*/
private transient int[] _gpofs;
/** Whether a group has a foot. */
private transient boolean[] _gpfts;
/** Whether a group is closed. */
private transient boolean[] _gpopens;
private transient GroupsDataListener _listener;
/** groupInfo list used in {@link Rows} */
private transient List<int[]> _gpinfo;
/** Returns the list model ({@link org.zkoss.zul.ListModel}) representing the given
* groups model.
* @since 6.0.0
*/
public static <D, G, F> GroupsListModel<D, G, F> toListModel(GroupsModel<D, G, F> model) {
if (model instanceof GroupsSortableModel)
return new GroupsListModelExt<D, G, F>(model);
return new GroupsListModel<D, G, F>(model);
}
protected GroupsListModel(GroupsModel<D, G, F> model) {
_model = model;
init();
}
/*package*/ void init() {
final int groupCount = _model.getGroupCount();
_gpofs = new int[groupCount];
_gpfts = new boolean[groupCount];
_gpopens = new boolean[groupCount];
_size = 0;
for (int j = 0; j < groupCount; ++j) {
_gpofs[j] = _size;
_gpopens[j] = _model.isGroupOpened(j);
_size += 1 + (_gpopens[j] ? _model.getChildCount(j) : 0); //closed group deemed as zero child in ListModel
_gpfts[j] = _model.hasGroupfoot(j);
if (_gpfts[j])
++_size;
}
if (_listener == null) {
_listener = new DataListener();
_model.addGroupsDataListener(_listener);
}
}
public List<int[]> getGroupsInfos() {
_gpinfo = new LinkedList<int[]>();
for (int j = 0; j < _gpofs.length; ++j) {
final int offset1 = _gpofs[j];
final int offset2 = getNextOffset(j);
_gpinfo.add(new int[] { offset1, offset2 - offset1, hasGroupfoot(j) ? offset2 - 1 : -1 });
}
return _gpinfo;
}
/**
* Returns the offset from 0 that a group in this ListModel.
* <p>For example, _gpofs[2] is the offset of group 2 (the third group)
* in this ListModel.
* @param groupIndex the group index
* @return the offset from 0 that a group in this ListModel.
*/
public int getGroupOffset(int groupIndex) {
return _gpofs[groupIndex];
}
public GroupsModel<D, G, F> getGroupsModel() {
return _model;
}
/** Returns the number of groups in the data model.
*/
/*package*/ int getGroupCount() {
return _gpofs.length;
}
/** Returns the number of items belong the specified group.
*/
/*package*/ int getChildCount(int groupIndex) {
int v = getNextOffset(groupIndex) - _gpofs[groupIndex] - 1;
return _gpfts[groupIndex] ? v - 1 : v;
}
/**
* Returns whether the Group has a Groupfoot or not.
*/
/*package*/ boolean hasGroupfoot(int groupIndex) {
return _gpfts[groupIndex];
}
/** Returns the offset of the next group.
*/
private int getNextOffset(int groupIndex) {
return groupIndex >= (_gpofs.length - 1) ? _size : _gpofs[groupIndex + 1];
}
/**
* Returns the group info of given index
*/
public GroupingInfo getDataInfo(int index) {
if (index < 0 || index >= _size)
throw new IndexOutOfBoundsException("Not in 0.." + _size + ": " + index);
int gi = Arrays.binarySearch(_gpofs, index);
if (gi >= 0)
return new GroupDataInfo(GroupDataInfo.GROUP, gi, 0, _gpopens[gi]);
gi = -gi - 2; //0 ~ _gpofs.length - 2
int ofs = index - _gpofs[gi] - 1;
if (_gpfts[gi] && ofs >= getNextOffset(gi) - _gpofs[gi] - 2) //child count
return new GroupDataInfo(GroupDataInfo.GROUPFOOT, gi, 0, _gpopens[gi]);
return new GroupDataInfo(GroupDataInfo.ELEMENT, gi, ofs, _gpopens[gi]);
}
//Object//
public boolean equals(Object o) {
if (this == o)
return true;
return _model.equals(o instanceof GroupsListModel ? ((GroupsListModel) o)._model : o);
}
public int hashCode() {
return _model.hashCode();
}
public String toString() {
return Objects.toString(_model);
}
//For Backward Compatibility//
/** @deprecated As of release 6.0.0, replaced with {@link #addToSelection}.
*/
public void addSelection(Object obj) {
addToSelection(obj);
}
/** @deprecated As of release 6.0.0, replaced with {@link #removeFromSelection}.
*/
public void removeSelection(Object obj) {
removeFromSelection(obj);
}
//ListModel
//ListModel assume each item in the ListModel is visible; thus items inside closed
//Group is deemed not in the ListModel
public Object getElementAt(int index) {
final GroupingInfo info = getDataInfo(index);
if (info.getType() == GroupDataInfo.GROUP)
return _model.getGroup(info.getGroupIndex());
if (info.getType() == GroupDataInfo.GROUPFOOT)
return _model.getGroupfoot(info.getGroupIndex());
return _model.getChild(info.getGroupIndex(), info.getOffset());
}
//ListModel assume each item in the ListModel is visible; thus items inside closed
//Group is not count into size
public int getSize() {
return _size;
}
//Selectable
@SuppressWarnings("unchecked")
public Set<Object> getSelection() {
if (_model instanceof Selectable)
return ((Selectable) _model).getSelection();
return super.getSelection();
}
/** {@inheritDoc} */
@SuppressWarnings("unchecked")
public void setSelection(Collection<?> selection) {
if (_model instanceof Selectable)
((Selectable) _model).setSelection(selection);
else
super.setSelection(selection);
}
/** {@inheritDoc} */
public boolean isSelected(Object obj) {
if (_model instanceof Selectable)
return ((Selectable) _model).isSelected(obj);
return super.isSelected(obj);
}
/** {@inheritDoc} */
public boolean isSelectionEmpty() {
if (_model instanceof Selectable)
return ((Selectable) _model).isSelectionEmpty();
return super.isSelectionEmpty();
}
/** {@inheritDoc} */
@SuppressWarnings("unchecked")
public boolean addToSelection(Object obj) {
if (_model instanceof Selectable)
return ((Selectable) _model).addToSelection(obj);
else
return super.addToSelection(obj);
}
/** {@inheritDoc} */
public boolean removeFromSelection(Object obj) {
if (_model instanceof Selectable)
return ((Selectable) _model).removeFromSelection(obj);
return super.removeFromSelection(obj);
}
/** {@inheritDoc} */
public void clearSelection() {
if (_model instanceof Selectable)
((Selectable) _model).clearSelection();
else
super.clearSelection();
}
/** {@inheritDoc} */
public boolean isMultiple() {
if (_model instanceof Selectable)
return ((Selectable) _model).isMultiple();
return super.isMultiple();
}
/** {@inheritDoc} */
public void setMultiple(boolean multiple) {
if (_model instanceof Selectable)
((Selectable) _model).setMultiple(multiple);
else
super.setMultiple(multiple);
}
public SelectionControl getSelectionControl() {
if (_model instanceof GroupsSelectableModel) {
return ((GroupsSelectableModel) _model).getSelectionControl();
} else {
return super.getSelectionControl();
}
}
public void setSelectionControl(SelectionControl ctrl) {
if (_model instanceof GroupsSelectableModel) {
((GroupsSelectableModel) _model).setSelectionControl(ctrl);
} else {
super.setSelectionControl(ctrl);
}
}
//Serializable//
private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
_model.removeGroupsDataListener(_listener); //avoid being serialized
try {
s.defaultWriteObject();
} finally {
_model.addGroupsDataListener(_listener);
}
}
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
init();
}
@SuppressWarnings("unchecked")
public Object clone() {
GroupsListModel clone = (GroupsListModel) super.clone();
clone._listener = null;
return clone;
}
public void setGroupSelectable(boolean groupSelectable) {
if (_model instanceof GroupsSelectableModel) {
((GroupsSelectableModel) _model).setGroupSelectable(groupSelectable);
}
}
public boolean isGroupSelectable() {
if (_model instanceof GroupsSelectableModel) {
return ((GroupsSelectableModel) _model).isGroupSelectable();
}
return false;
}
private class DataListener implements GroupsDataListener {
public void onChange(GroupsDataEvent event) {
int type = event.getType(), j0 = event.getIndex0(), j1 = event.getIndex1();
switch (type) {
case GroupsDataEvent.CONTENTS_CHANGED:
case GroupsDataEvent.INTERVAL_ADDED:
case GroupsDataEvent.INTERVAL_REMOVED:
final int gi = event.getGroupIndex();
if (gi < 0 || gi >= _gpofs.length)
throw new IndexOutOfBoundsException("Group index not in 0.." + getGroupCount() + ", " + gi);
int ofs = _gpofs[gi] + 1;
j0 = j0 >= 0 ? j0 + ofs : ofs;
if (j1 >= 0) {
j1 = j1 + ofs;
} else {
j1 = getNextOffset(gi) - 1;
if (_gpfts[gi])
--j1; //exclude groupfoot
}
init(); //re-initialize the model information
break;
case GroupsDataEvent.GROUPS_CHANGED:
case GroupsDataEvent.GROUPS_ADDED:
case GroupsDataEvent.GROUPS_REMOVED:
type -= GroupsDataEvent.GROUPS_CHANGED;
if (j0 >= 0) {
if (j0 >= _gpofs.length)
throw new IndexOutOfBoundsException("Group index not in 0.." + getGroupCount() + ", " + j0);
j0 = _gpofs[j0];
}
if (j1 >= 0) {
if (j1 >= _gpofs.length)
throw new IndexOutOfBoundsException("Group index not in 0.." + getGroupCount() + ", " + j1);
j1 = getNextOffset(j1) - 1; //include groupfoot
}
init(); //re-initialize the model information
break;
case GroupsDataEvent.SELECTION_CHANGED:
type = ListDataEvent.SELECTION_CHANGED;
break;
case GroupsDataEvent.MULTIPLE_CHANGED:
type = ListDataEvent.MULTIPLE_CHANGED;
break;
case GroupsDataEvent.GROUPS_OPENED: //ZK-2812: onOpen event listener didn't trigger when using GroupsModel
int index = event.getGroupIndex();
if (event.getModel().getChildCount(index) <= 0)
return;
init(); //re-initialize the model information
boolean open = _gpopens[index];
if (open)
type = ListDataEvent.INTERVAL_ADDED;
else
type = ListDataEvent.INTERVAL_REMOVED;
j0 = _gpofs[index] + 1;
j1 = 0;
break;
case GroupsDataEvent.DISABLE_CLIENT_UPDATE:
type = ListDataEvent.DISABLE_CLIENT_UPDATE;
break;
case GroupsDataEvent.ENABLE_CLIENT_UPDATE:
type = ListDataEvent.ENABLE_CLIENT_UPDATE;
break;
default:
init(); //re-initialize the model information
break;
}
fireEvent(type, j0, j1);
}
}
/** The group information returned by {@link GroupsListModel#getDataInfo}.
*/
public static class GroupDataInfo implements GroupingInfo {
/** Indicates the data is a group (a.k.a., the head of the group). */
public static final byte GROUP = 0;
/** Indicates the data is a group foot. */
public static final byte GROUPFOOT = 1;
/** Indicates the data is an element of a group. */
public static final byte ELEMENT = 2;
/** The index of the group. */
private int _groupIndex;
/** The offset of an element in a group.
* It is meaningful only if its type is {@link #ELEMENT}.
*/
private int _offset;
/** The type of the data.
* It is one of {@link #GROUP}, {@link #GROUPFOOT} and {@link #ELEMENT}.
*/
private byte _type;
/** Whether the group is closed.
* It is meaningful only if its type is {@link #GROUP}.
*/
private boolean _open;
private GroupDataInfo(byte type, int groupIndex, int offset, boolean open) {
_type = type;
_groupIndex = groupIndex;
_offset = offset;
_open = open;
}
public int getType() {
return _type;
}
public int getGroupIndex() {
return _groupIndex;
}
public int getOffset() {
return _offset;
}
public boolean isOpen() {
return _open;
}
}
}
/*package*/ class GroupsListModelExt<D, G, F> extends GroupsListModel<D, G, F>
implements GroupsSortableModel<D>, ComponentCloneListener, Cloneable {
/*package*/ GroupsListModelExt(GroupsModel<D, G, F> model) {
super(model);
}
/**
* Groups and sorts the data by the specified column and comparator.
* It only called when {@link org.zkoss.zul.Listbox} or {@link org.zkoss.zul.Grid} has the sort function.
* @param colIndex the index of the column
*/
@SuppressWarnings("unchecked")
public void group(Comparator<D> cmpr, boolean ascending, int colIndex) {
if (!(_model instanceof GroupsSortableModel))
throw new UiException(GroupsSortableModel.class + " must be implemented in " + _model.getClass());
((GroupsSortableModel) _model).group(cmpr, ascending, colIndex);
}
/** Sorts the data by the specified column and comparator.
*/
@SuppressWarnings("unchecked")
public void sort(Comparator<D> cmpr, boolean ascending, int colIndex) {
if (!(_model instanceof GroupsSortableModel))
throw new UiException(GroupsSortableModel.class + " must be implemented in " + _model.getClass());
((GroupsSortableModel) _model).sort(cmpr, ascending, colIndex);
}
@SuppressWarnings("unchecked")
public Object willClone(Component comp) {
if (_model instanceof ComponentCloneListener) {
GroupsListModelExt clone = (GroupsListModelExt) clone();
GroupsModel m = (GroupsModel) ((ComponentCloneListener) _model).willClone(comp);
if (m != null)
clone._model = m;
clone.init(); // reset grouping info
return clone;
}
return null; // no need to clone
}
}