zul/src/main/java/org/zkoss/zul/Rows.java
/* Rows.java
Purpose:
Description:
History:
Tue Oct 25 16:02:39 2005, Created by tomyeh
Copyright (C) 2005 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;
import java.util.AbstractList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.ext.render.Cropper;
import org.zkoss.zul.ext.Pageable;
import org.zkoss.zul.ext.Paginal;
import org.zkoss.zul.impl.DataLoader;
import org.zkoss.zul.impl.GroupsListModel;
import org.zkoss.zul.impl.XulElement;
/**
* Defines the rows of a grid.
* Each child of a rows element should be a {@link Row} element.
* <p>Default {@link #getZclass}: z-rows.(since 3.5.0)
*
* @author tomyeh
*/
public class Rows extends XulElement {
private int _visibleItemCount;
private transient List<int[]> _groupsInfo;
private transient List<Group> _groups;
private transient boolean _isReplacingRow;
public Rows() {
init();
}
private void init() {
_groupsInfo = new LinkedList<int[]>();
_groups = new AbstractList<Group>() {
public int size() {
return getGroupCount();
}
public Iterator<Group> iterator() {
return new IterGroups();
}
public Group get(int index) {
final int realIndex = getRealIndex(_groupsInfo.get(index)[0]);
return (realIndex >= 0 && realIndex < getChildren().size()) ? (Group) getChildren().get(realIndex)
: null;
}
};
}
private int getRealIndex(int index) {
final Grid grid = getGrid();
final int offset = grid != null && grid.getModel() != null ? grid.getDataLoader().getOffset() : 0;
return index - (offset < 0 ? 0 : offset);
}
/** Returns the grid that contains this rows.
* <p>It is the same as {@link #getParent}.
*/
public Grid getGrid() {
return (Grid) getParent();
}
/**
* Returns the number of groups.
* @since 3.5.0
*/
public int getGroupCount() {
return _groupsInfo.size();
}
/**
* Returns a list of all {@link Group}.
* @since 3.5.0
*/
public List<Group> getGroups() {
return _groups;
}
/**
* Returns whether Group exists.
* @since 3.5.0
*/
public boolean hasGroup() {
return !_groupsInfo.isEmpty();
}
//Paging//
/**
* Returns the number of visible descendant {@link Row}.
* @since 3.5.1
*/
public int getVisibleItemCount() {
return _visibleItemCount;
}
/*package*/ void addVisibleItemCount(int count) {
if (count != 0) {
_visibleItemCount += count;
final Grid grid = getGrid();
if (grid != null) {
if (grid.inPagingMold()) {
final Paginal pgi = grid.getPaginal();
int newTotalSize = grid.getDataLoader().getTotalSize();
//ZK-3173: if visible item count reduces, active page might exceed max page count
ListModel<?> model = grid.getModel();
if (count < 0 && model instanceof Pageable) {
Pageable p = (Pageable) model;
int actpg = p.getActivePage();
int maxPageIndex = p.getPageCount() - 1;
if (actpg > maxPageIndex) {
p.setActivePage(maxPageIndex);
}
}
pgi.setTotalSize(newTotalSize);
if (grid.getModel() != null)
grid.invalidate();
else {
invalidate();
grid.getDataLoader().updateModelInfo();
}
} else if (((Cropper) grid.getDataLoader()).isCropper()) {
invalidate();
grid.getDataLoader().updateModelInfo();
} else {
smartUpdate("visibleItemCount", _visibleItemCount);
}
}
}
}
/**
* Set true to avoid unnecessary row re-indexing when render template.
* @param b true to skip
* @return original true/false status
* @see Grid.Renderer#render
*/
/*package*/ boolean setReplacingRow(boolean b) {
final boolean old = _isReplacingRow;
Grid grid = getGrid();
if (grid != null && grid.getModel() != null) // B60-ZK-898: only apply when model is used.
_isReplacingRow = b;
return old;
}
/*package*/ void fixGroupIndex(int j, int to, boolean infront) {
int realj = getRealIndex(j);
if (realj < 0) {
realj = 0;
}
if (realj < getChildren().size()) {
final int beginning = j;
for (Iterator<Component> it = getChildren().listIterator(realj); it.hasNext() && (to < 0 || j <= to); ++j) {
Component o = it.next();
((Row) o).setIndexDirectly(j);
if (_isReplacingRow) //@see Grid.Renderer#render
break; //set only the first Row, skip handling GroupInfo
// if beginning is a group, we don't need to change its groupInfo,
// because
// it is not reliable when infront is true.
if ((!infront || beginning != j) && o instanceof Group) {
int[] g = getLastGroupsInfoAt(j + (infront ? -1 : 1));
if (g != null) {
g[0] = j;
if (g[2] != -1)
g[2] += (infront ? 1 : -1);
}
}
}
}
}
/*package*/ Group getGroup(int index) {
if (_groupsInfo.isEmpty())
return null;
final int[] g = getGroupsInfoAt(index);
if (g != null) {
final int realIndex = getRealIndex(g[0]);
//if realIndex < 0 means g is half loaded, Group head is not in server
if (realIndex >= 0 && realIndex < getChildren().size()) {
Row row = (Row) getChildren().get(realIndex);
return (Group) row;
}
}
return null;
}
/*package*/ int[] getGroupsInfoAt(int index) {
return getGroupsInfoAt(index, false);
}
/**
* Returns an int array that it has two length, one is an index of Group,
* and the other is the number of items of Group(inclusive).
*/
/*package*/ int[] getGroupsInfoAt(int index, boolean isGroup) {
for (Iterator<int[]> it = _groupsInfo.iterator(); it.hasNext();) {
int[] g = it.next();
if (isGroup) {
if (index == g[0])
return g;
} else if (index > g[0] && index <= (g[0] + g[1]))
return g;
}
return null;
}
/**
* Returns the last groups info which matches with the same index.
* Because dynamically maintain the index of the groups will occur the same index
* at the same time in the loop.
*/
/*package*/ int[] getLastGroupsInfoAt(int index) {
int[] rg = null;
for (int[] g : _groupsInfo) {
if (index == g[0])
rg = g;
else if (index < g[0])
break;
}
return rg;
}
/**
* Returns the last groups index which matches with the same index.
* @param index the row index in Rows.
* @return the associated group index of the row index.
*/
/*package*/ int getGroupIndex(int index) {
int j = 0, gindex = -1;
int[] g = null;
for (Iterator<int[]> it = _groupsInfo.iterator(); it.hasNext(); ++j) {
g = it.next();
if (index == g[0])
gindex = j;
else if (index < g[0])
break;
}
return gindex != -1 ? gindex
: g != null && index < (g[0] + g[1]) ? (j - 1)
: g != null && index == (g[0] + g[1]) && g[2] == -1 ? (j - 1) : gindex;
}
//-- Component --//
public void beforeParentChanged(Component parent) {
if (parent != null && !(parent instanceof Grid))
throw new UiException("Unsupported parent for rows: " + parent);
super.beforeParentChanged(parent);
}
public void beforeChildAdded(Component child, Component refChild) {
if (!(child instanceof Row))
throw new UiException("Unsupported child for rows: " + child);
if (child instanceof Groupfoot) {
if (!hasGroup())
throw new UiException("Groupfoot cannot exist alone, you have to add a Group first");
if (refChild == null) {
if (getLastChild() instanceof Groupfoot)
throw new UiException("Only one Groupfoot is allowed per Group");
}
}
super.beforeChildAdded(child, refChild);
}
private boolean hasGroupsModel() {
final Grid grid = getGrid();
return grid != null && grid.getModel() instanceof GroupsListModel;
}
public boolean insertBefore(Component child, Component refChild) {
final Grid grid = getGrid();
final boolean isReorder = child.getParent() == this;
//bug #3051305: Active Page not update when drag & drop item to the end
if (isReorder) {
checkInvalidateForMoved(child, true);
}
Row newItem = (Row) child;
final int jfrom = hasGroup() && newItem.getParent() == this ? newItem.getIndex() : -1;
fixGroupsInfoBeforeInsert(newItem, (Row) refChild, isReorder);
if (super.insertBefore(child, refChild)) {
final int jto = refChild instanceof Row ? ((Row) refChild).getIndex() : -1,
fixFrom = jfrom < 0 || (jto >= 0 && jfrom > jto) ? jto : jfrom;
if (fixFrom < 0) {
newItem.setIndexDirectly(
getChildren().size() - 1 + (grid != null ? grid.getDataLoader().getOffset() : 0));
} else {
fixGroupIndex(fixFrom, jfrom >= 0 && jto >= 0 ? jfrom > jto ? jfrom : jto : -1, !isReorder);
}
fixGroupsInfoAfterInsert(newItem);
//bug #3049167: Totalsize increase when drag & drop in paging Listbox/Grid
if (!isReorder) {
afterInsert(child);
}
return true;
}
return false;
}
/**
* If the child is a group, its groupfoot will be removed at the same time.
*/
public boolean removeChild(Component child) {
if (child.getParent() == this)
beforeRemove(child);
final boolean hasGroup = hasGroup();
int index = ((Row) child).getIndex();
if (super.removeChild(child)) {
((Row) child).setIndexDirectly(-1);
fixGroupsInfoAfterRemove((Row) child, index);
return true;
}
return false;
}
/**
* Fix Childitem._index since j-th item.
*
* @param j
* the start index (inclusion)
* @param to
* the end index (inclusion). If -1, up to the end.
*/
private void fixRowIndices(int j, int to) {
int realj = getRealIndex(j);
if (realj < 0)
realj = 0;
List items = getChildren();
if (realj < items.size()) {
for (Iterator it = items.listIterator(realj); it.hasNext() && (to < 0 || j <= to); ++j)
((Row) it.next()).setIndexDirectly(j);
}
}
/** Callback if a child has been inserted.
* <p>Default: invalidate if it is the paging mold and it affects
* the view of the active page.
* @since 3.0.5
*/
protected void afterInsert(Component comp) {
if (_isReplacingRow) //@see Grid.Renderer#render
return; //called by #insertBefore(), skip handling item count, etc.
updateVisibleCount((Row) comp, false);
checkInvalidateForMoved(comp, false);
}
/** Callback if a child will be removed (not removed yet).
* <p>Default: invalidate if it is the paging mold and it affects
* the view of the active page.
* @since 3.0.5
*/
protected void beforeRemove(Component comp) {
if (_isReplacingRow) //@see Grid.Renderer#render
return; //called by #removeChild(), skip handling item count, etc.
updateVisibleCount((Row) comp, true);
checkInvalidateForMoved(comp, true);
}
private void fixGroupsInfoBeforeInsert(Row newItem, Row refChild, boolean isReorder) {
if (_isReplacingRow) //@see Grid.Renderer#render
return; //called by #insertBefore(), skip handling GroupInfo
if (newItem instanceof Groupfoot) {
if (refChild == null) {
if (isReorder) {
final int idx = newItem.getIndex();
final int[] ginfo = getGroupsInfoAt(idx);
if (ginfo != null) {
ginfo[1]--;
ginfo[2] = -1;
}
}
final int[] g = _groupsInfo.get(getGroupCount() - 1);
g[2] = getChildren().size() - (isReorder ? 2 : 1);
} else {
final int idx = ((Row) refChild).getIndex();
final int[] g = getGroupsInfoAt(idx);
if (g == null)
throw new UiException("Groupfoot cannot exist alone, you have to add a Group first");
if (g[2] != -1)
throw new UiException("Only one Goupfooter is allowed per Group");
if (idx != (g[0] + g[1]))
throw new UiException("Groupfoot must be placed after the last Row of the Group");
g[2] = idx - 1;
if (isReorder) {
final int nindex = newItem.getIndex();
final int[] ginfo = getGroupsInfoAt(nindex);
if (ginfo != null) {
ginfo[1]--;
ginfo[2] = -1;
}
}
}
}
}
private void fixGroupsInfoAfterInsert(Row newItem) {
if (_isReplacingRow) //@see Grid.Renderer#render
return; //called by #insertBefore(), skip handling GroupInfo
if (newItem instanceof Group) {
Group group = (Group) newItem;
int index = group.getIndex();
if (_groupsInfo.isEmpty())
_groupsInfo.add(new int[] { group.getIndex(), getChildren().size() - index, -1 });
else {
int idx = 0;
int[] prev = null, next = null;
for (Iterator<int[]> it = _groupsInfo.iterator(); it.hasNext();) {
int[] g = it.next();
if (g[0] <= index) {
prev = g;
idx++;
} else {
next = g;
break;
}
}
if (prev != null) {
int leng = index - prev[0], size = prev[1] - leng + 1;
prev[1] = leng;
_groupsInfo.add(idx, new int[] { index, size, size > 1 && prev[2] >= index ? prev[2] + 1 : -1 });
if (size > 1 && prev[2] >= index)
prev[2] = -1; // reset groupfoot
} else if (next != null) {
_groupsInfo.add(idx, new int[] { index, next[0] - index, -1 });
}
}
} else if (hasGroup()) {
int index = newItem.getIndex();
final int[] g = getGroupsInfoAt(index);
if (g != null) {
g[1]++;
if (g[2] != -1 && (g[2] >= index || newItem instanceof Groupfoot))
g[2] = g[0] + g[1] - 1;
}
}
}
private void fixGroupsInfoAfterRemove(Row child, int index) {
if (!_isReplacingRow) { //@see Grid.Renderer#render
//called by #removeChild, handling GroupInfo if !isReplcingRow
if (child instanceof Group) {
int[] prev = null, remove = null;
for (Iterator<int[]> it = _groupsInfo.iterator(); it.hasNext();) {
int[] g = it.next();
if (g[0] == index) {
remove = g;
break;
}
prev = g;
}
if (prev != null && remove != null) {
prev[1] += remove[1] - 1;
}
fixGroupIndex(index, -1, false);
if (remove != null) {
_groupsInfo.remove(remove);
final int idx = remove[2];
if (idx != -1) {
final int realIndex = getRealIndex(idx) - 1; //bug #2936064
if (realIndex >= 0 && realIndex < getChildren().size())
removeChild(getChildren().get(realIndex));
}
}
} else if (hasGroup()) {
final int[] g = getGroupsInfoAt(index);
if (g != null) {
g[1]--;
if (g[2] != -1)
g[2]--;
fixGroupIndex(index, -1, false);
} else
fixGroupIndex(index, -1, false);
if (child instanceof Groupfoot) {
final int[] g1 = getGroupsInfoAt(index);
if (g1 != null) { // group info maybe remove cause of grouphead removed in previous op
g1[2] = -1;
}
}
} else {
fixRowIndices(index, -1);
}
}
if (hasGroupsModel() && getChildren().size() <= 0) { //remove to empty, reset _groupsInfo
_groupsInfo = new LinkedList<int[]>();
}
}
/**
* Update the number of the visible item before it is removed or after it is added.
*/
private void updateVisibleCount(Row row, boolean isRemove) {
if (row instanceof Group || row.isVisible()) {
final Group g = getGroup(row.getIndex());
// We shall update the number of the visible item in the following cases.
// 1) If the row is a type of Groupfoot, it is always shown.
// 2) If the row is a type of Group, it is always shown.
// 3) If the row doesn't belong to any group.
// 4) If the group of the row is open.
if (row.isVisible() && (row instanceof Groupfoot || row instanceof Group || g == null || g.isOpen())) // B50-3303770
addVisibleItemCount(isRemove ? -1 : 1);
if (row instanceof Group) {
final Group group = (Group) row;
// If the previous group exists, we shall update the number of
// the visible item from the number of the visible item of the current group.
final Row preRow = (Row) row.getPreviousSibling();
if (preRow == null) {
if (!group.isOpen()) {
addVisibleItemCount(isRemove ? group.getVisibleItemCount() : -group.getVisibleItemCount());
}
} else {
final Group preGroup = preRow instanceof Group ? (Group) preRow : getGroup(preRow.getIndex());
if (preGroup != null) {
if (!preGroup.isOpen() && group.isOpen())
addVisibleItemCount(isRemove ? -group.getVisibleItemCount() : group.getVisibleItemCount());
else if (preGroup.isOpen() && !group.isOpen())
addVisibleItemCount(isRemove ? group.getVisibleItemCount() : -group.getVisibleItemCount());
} else {
if (!group.isOpen())
addVisibleItemCount(isRemove ? group.getVisibleItemCount() : -group.getVisibleItemCount());
}
}
}
}
final Grid grid = getGrid();
if (grid != null && grid.inPagingMold()) {
int newTotalSize = grid.getDataLoader().getTotalSize();
//ZK-3173: if visible item count reduces, active page might exceed max page count
ListModel<?> model = grid.getModel();
if (isRemove && model instanceof Pageable) {
Pageable p = (Pageable) model;
int actpg = p.getActivePage();
int maxPageIndex = p.getPageCount() - 1;
if (actpg > maxPageIndex) {
p.setActivePage(maxPageIndex);
}
}
grid.getPaginal().setTotalSize(newTotalSize);
}
}
/** Checks whether to invalidate, when a child has been added or
* or will be removed.
* @param bRemove if child will be removed
*/
private void checkInvalidateForMoved(Component child, boolean bRemove) {
//No need to invalidate if
//1) act == last and child in act
//2) act != last and child after act
//Except removing last element which in act and act has only one element
final Grid grid = getGrid();
if (grid != null && grid.inPagingMold() && !isInvalidated()) {
final List<Component> children = getChildren();
final int sz = children.size(), pgsz = grid.getPageSize();
int n = sz - (grid.getActivePage() + 1) * pgsz;
if (n <= 0) { //must be last page
n += pgsz; //check in-act (otherwise, check after-act)
if (bRemove && n <= 1) { //last element, in act and remove
invalidate();
return;
}
} else if (n > 50)
n = 50; //check at most 50 items (for better performance)
for (ListIterator<Component> it = children.listIterator(sz); --n >= 0 && it.hasPrevious();)
if (it.previous() == child)
return; //no need to invalidate
invalidate();
}
}
public String getZclass() {
return _zclass == null ? "z-rows" : _zclass;
}
/**
* @deprecated as of release 6.0.0. To control the size of Grid related
* components, please refer to {@link Grid} and {@link Column} instead.
*/
public void setWidth(String width) {
// Don't remove this method, it's to override super.setWidth().
}
/**
* @deprecated as of release 6.0.0. To control the size of Grid related
* components, please refer to {@link Grid} and {@link Column} instead.
*/
public void setHflex(String flex) {
// Don't remove this method, it's to override super.setHflex().
}
//Cloneable//
public Object clone() {
final Rows clone = (Rows) super.clone();
clone.init();
clone._groupsInfo.addAll(_groupsInfo);
Grid grid = getGrid();
final int offset = grid != null ? grid.getDataLoader().getOffset() : 0;
clone.afterUnmarshal(offset);
return clone;
}
private void afterUnmarshal(int index) {
for (Iterator it = getChildren().iterator(); it.hasNext();) {
((Row) it.next()).setIndexDirectly(index++);
}
}
//-- Serializable --//
private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
s.defaultWriteObject();
int size = _groupsInfo.size();
s.writeInt(size);
if (size > 0)
s.writeObject(_groupsInfo);
Grid grid = getGrid();
DataLoader loader = grid != null ? grid.getDataLoader() : null;
if (loader != null) {
s.writeInt(loader.getOffset());
} else
s.writeInt(0);
}
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
init();
int size = s.readInt();
if (size > 0) {
List groupsInfo = (List) s.readObject();
for (int i = 0; i < size; i++)
_groupsInfo.add((int[]) groupsInfo.get(i));
}
int offset = s.readInt();
afterUnmarshal(offset);
}
public <T extends Component> List<T> getChildren() {
return (List<T>) new Children();
}
protected class Children extends XulElement.Children {
protected void removeRange(int fromIndex, int toIndex) {
ListIterator<Component> it = listIterator(toIndex);
for (int n = toIndex - fromIndex; --n >= 0 && it.hasPrevious();) {
it.previous();
it.remove();
}
}
public void clear() {
final boolean oldFlag = setReplacingRow(true);
try {
super.clear();
} finally {
setReplacingRow(oldFlag);
}
}
}
// super
protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException {
super.renderProperties(renderer);
final Grid grid = getGrid();
renderer.render("_offset", grid == null ? 0 : grid.getDataLoader().getOffset()); //go with each cropping
renderer.render("visibleItemCount", _visibleItemCount); //go with each cropping
}
//-- ComponentCtrl --//
public Object getExtraCtrl() {
return new ExtraCtrl();
}
/** A utility class to implement {@link #getExtraCtrl}.
* It is used only by component developers.
*/
protected class ExtraCtrl extends XulElement.ExtraCtrl implements Cropper {
//--Cropper--//
public boolean isCropper() {
final Grid grid = getGrid();
return grid != null && ((Cropper) grid.getDataLoader()).isCropper();
}
public Component getCropOwner() {
return getGrid();
}
public Set<? extends Component> getAvailableAtClient() {
final Grid grid = getGrid();
return grid != null ? ((Cropper) grid.getDataLoader()).getAvailableAtClient() : null;
}
}
/**
* An iterator used by _groups.
*/
private class IterGroups implements Iterator<Group> {
private final Iterator<int[]> _it = _groupsInfo.iterator();
private int _j;
public boolean hasNext() {
return _j < getGroupCount();
}
public Group next() {
++_j;
final int realIndex = getRealIndex((_it.next())[0]);
return (realIndex >= 0 && realIndex < getChildren().size()) ? (Group) getChildren().get(realIndex) : null;
}
public void remove() {
throw new UnsupportedOperationException();
}
}
}