/* 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 Goupfooter 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) { } /** * @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) { } //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(); } } }