zul/src/main/java/org/zkoss/zul/Listbox.java
/* Listbox.java
Purpose:
Description:
History:
Wed Jun 15 17:25:00 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 static org.zkoss.lang.Generics.cast;
import java.lang.reflect.Method;
import java.util.AbstractCollection;
import java.util.AbstractList;
import java.util.AbstractSequentialList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.io.Serializables;
import org.zkoss.json.JSONAware;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Exceptions;
import org.zkoss.lang.Library;
import org.zkoss.lang.Objects;
import org.zkoss.lang.Strings;
import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.au.AuRequests;
import org.zkoss.zk.au.out.AuInvoke;
import org.zkoss.zk.ui.AbstractComponent;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Components;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.CheckEvent;
import org.zkoss.zk.ui.event.CloneableEventListener;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.SelectEvent;
import org.zkoss.zk.ui.event.SerializableEventListener;
import org.zkoss.zk.ui.ext.Blockable;
import org.zkoss.zk.ui.ext.render.Cropper;
import org.zkoss.zk.ui.sys.BooleanPropertyAccess;
import org.zkoss.zk.ui.sys.IntegerPropertyAccess;
import org.zkoss.zk.ui.sys.PropertyAccess;
import org.zkoss.zk.ui.util.Clients;
import org.zkoss.zk.ui.util.ComponentCloneListener;
import org.zkoss.zul.event.DataLoadingEvent;
import org.zkoss.zul.event.GroupsDataEvent;
import org.zkoss.zul.event.GroupsDataListener;
import org.zkoss.zul.event.ListDataEvent;
import org.zkoss.zul.event.ListDataListener;
import org.zkoss.zul.event.PageSizeEvent;
import org.zkoss.zul.event.PagingEvent;
import org.zkoss.zul.event.PagingListener;
import org.zkoss.zul.event.ZulEvents;
import org.zkoss.zul.ext.GroupsSelectableModel;
import org.zkoss.zul.ext.Pageable;
import org.zkoss.zul.ext.Paginal;
import org.zkoss.zul.ext.Selectable;
import org.zkoss.zul.ext.SelectionControl;
import org.zkoss.zul.ext.Sortable;
import org.zkoss.zul.impl.DataLoader;
import org.zkoss.zul.impl.GroupsListModel;
import org.zkoss.zul.impl.ListboxDataLoader;
import org.zkoss.zul.impl.MeshElement;
import org.zkoss.zul.impl.Padding;
import org.zkoss.zul.impl.Utils;
import org.zkoss.zul.impl.XulElement;
/**
* A listbox.
*
* <p>
* Event:
* <ol>
* <li>{@link org.zkoss.zk.ui.event.SelectEvent} is sent when user changes the
* selection.</li>
* <li>onAfterRender is sent when the model's data has been rendered.(since 5.0.4)</li>
* <li>onCheckSelectAll is sent when user click on selectAll checkbox.(since 6.5.5)</li>
* </ol>
*
* <p>
* See <a href="package-summary.html">Specification</a>.
* </p>
*
* <p>
* Besides creating {@link Listitem} programmatically, you could assign a data
* model (a {@link ListModel} or {@link GroupsModel} instance) to a listbox via
* {@link #setModel(ListModel)} or {@link #setModel(GroupsModel)} and then the
* listbox will retrieve data via {@link ListModel#getElementAt} when necessary.
*
* <p>
* Besides assign a list model, you could assign a renderer (a
* {@link ListitemRenderer} instance) to a listbox, such that the listbox will
* use this renderer to render the data returned by
* {@link ListModel#getElementAt}. If not assigned, the default renderer, which
* assumes a label per list item, is used. In other words, the default renderer
* adds a label to a Listitem by calling toString against the object returned by
* {@link ListModel#getElementAt}</p>
*
* <p>To retrieve what are selected in Listbox with a {@link Selectable}
* {@link ListModel}, you shall use {@link Selectable#getSelection} to get what
* is currently selected object in {@link ListModel} rather than using
* {@link Listbox#getSelectedItems}. That is, you shall operate on the item of
* the {@link ListModel} rather than on the {@link Listitem} of the {@link Listbox}
* if you use the {@link Selectable} {@link ListModel}.</p>
*
* <pre><code>
* Set selection = ((Selectable)getModel()).getSelection();
* </code></pre>
*
* <p>[Since 6.0.0] If a model is set, whether the listbox allows
* the multiple selection depends on {@link Selectable#setMultiple}.
* In other words, the application shall not access listbox directly if
* a model is assigned. Rather, the application shall access the model
* directly.
*
* <p>
* There are two ways to handle long content: scrolling and paging. If
* {@link #getMold} is "default", scrolling is used if {@link #setHeight} is
* called and too much content to display. If {@link #getMold} is "paging",
* paging is used if two or more pages are required. To control the number of
* items to display in a page, use {@link #setPageSize}.
*
* <p>
* If paging is used, the page controller is either created automatically or
* assigned explicitly by {@link #setPaginal}. The paging controller specified
* explicitly by {@link #setPaginal} is called the external page controller. It
* is useful if you want to put the paging controller at different location
* (other than as a child component), or you want to use the same controller to
* control multiple listboxes.
*
* <p>
* Default {@link #getZclass}: z-listbox.(since 3.5.0)
*
* <p>
* To have a list box without stripping, you can specify a non-existent style
* class to {@link #setOddRowSclass}.
*
* <h3>Clustering and Serialization</h3>
*
* <p>
* When used in a clustering environment, you have to make
* {@link ListitemRenderer} ({@link #setItemRenderer}) and {@link ListModel} (
* {@link #setModel}) either serializable or re-assign them when
* {@link #sessionDidActivate} is called.
*
* <h3>Render on Demand (rod)</h3>
* [ZK EE]
* [Since 5.0.0]
*
* <p>For huge data, you can turn on Listbox's ROD to request ZK engine to load from
* {@link ListModel} only the required data chunk and create only the required
* {@link Listitem}s in memory and render only the required DOM elements in
* browser. So it saves both the memory and the processing time in both server
* and browser for huge data. If you don't use the {@link ListModel} with the
* Listbox, turn on the ROD will still have ZK engine to render only a chunk of
* DOM elements in browser so it at least saves the memory and processing time
* in browser. Note that ROD works only if the Listbox is configured to has a
* limited "view port" height. That is, either the Listbox is in the "paging"
* mold or you have to {@link #setHeight(String)},{@link #setVflex(String)},
* or {@link #setRows(int)} of the Listbox to make ROD works.</p>
*
* <p>You can turn on/off ROD for all Listboxes in the application or only
* for a specific Listbox. To turn on ROD for all Listboxes in the application,
* you have to specify the Library Property "org.zkoss.zul.listbox.rod" to
* "true" in WEB-INF/zk.xml. If you did not specify the Library Property,
* default is false.</p>
*
* <pre>{@code
* <library-property>
* <name>org.zkoss.zul.listbox.rod</name>
* <value>true</value>
* </library-property>
* }</pre>
*
* <p>To turn on ROD for a specific Listbox, you have to specify the Listbox's
* attribute map with key "org.zkoss.zul.listbox.rod" to true. That is, for
* example, if in a zul file, you shall specify <custom-attributes> of the
* Listbox like this:</p>
*
* <pre>{@code
* <listbox ...>
* <custom-attributes org.zkoss.zul.listbox.rod="true"/>
* </listbox>
* }</pre>
*
* <p>You can mix the Library Property and <custom-attributes> ways together.
* The <custom-attributes> way always takes higher priority. So you
* can turn OFF ROD in general and turn ON only some specific Listbox component.
* Or you can turn ON ROD in general and turn OFF only some specific Listbox
* component.</P>
*
* <p>Since only partial {@link Listitem}s are created and rendered in the
* Listbox if you turn the ROD on, there will be some limitations on accessing
* {@link Listitem}s. For example, if you call
* <pre><code>
* Listitem itemAt100 = (Listitem) getItemAtIndex(100);
* </code></pre>
* <p>The {@link Listitem} in index 100 is not necessary created yet if it is
* not in the current "view port" and you will get "null" instead.</p>
*
* <p>And it is generally a bad idea to "cache" the created {@link Listitem}
* in your application if you turn the ROD on because Listitems might be removed
* later. Basically, you shall operate on the item of the {@link ListModel}
* rather than on the {@link Listitem} of the {@link Listbox} if you use the
* {@link ListModel} and ROD.</p>
*
* <h3>Custom Attributes</h3>
* <dl>
* <dt>org.zkoss.zul.listbox.rightSelect</dt>
* <dd>Specifies whether the selection shall be toggled when user right clicks on
* item, if the checkmark ({@link #isCheckmark}) is enabled.</br>
* Notice that you could specify this attribute in any of its ancestor's attributes.
* It will be inherited.</dd>
* <dt>org.zkoss.zul.listbox.rod</dt>
* <dd>Specifies whether to enable ROD (render-on-demand).</br>
* Notice that you could specify this attribute in any of its ancestor's attributes.
* It will be inherited.</dd>
* <dt>org.zkoss.zul.listbox.autoSort</dt>.(since 5.0.7)
* <dd>Specifies whether to sort the model when the following cases:</br>
* <ol>
* <li>{@link #setModel} is called and {@link Listheader#setSortDirection} is set.</li>
* <li>{@link Listheader#setSortDirection} is called.</li>
* <li>Model receives {@link ListDataEvent} and {@link Listheader#setSortDirection} is set.</li>
* </ol>
* If you want to ignore sort when receiving {@link ListDataEvent},
* you can specifies the value as "ignore.change".</br>
* Notice that you could specify this attribute in any of its ancestor's attributes.
* It will be inherited.</dd>
* </dl>
* <dt>org.zkoss.zul.listbox.groupSelect</dt>
* <dd>Specifies whether Listgroups under this Listbox are selectable. Notice that
* you could specify this attribute in any of its ancestor's attributes. It will
* be inherited. Default value is false.</dd>
*
* <dt>org.zkoss.zul.listbox.preloadSize</dt>.(since 5.0.8)
* <dd>Specifies the number of items to preload when receiving
* the rendering request from the client.
* <p>It is used only if live data ({@link #setModel(ListModel)} and
* not paging ({@link #getPagingChild}).</dd>
*
* <dt>org.zkoss.zul.listbox.initRodSize</dt>.(since 5.0.8)
* <dd>Specifies the number of items rendered when the Listbox first render.
* <p>
* It is used only if live data ({@link #setModel(ListModel)} and not paging
* ({@link #getPagingChild}).</dd>
*
* <dt>org.zkoss.zul.listbox.selectOnHighlight.disabled</dt>.(since 7.0.4)
* <dd>Sets whether to disable select functionality when highlighting text
* content with mouse dragging or not.</dd>
*
* @author tomyeh
* @see ListModel
* @see ListitemRenderer
* @see ListitemRendererExt
*/
public class Listbox extends MeshElement {
private static final long serialVersionUID = 2009111111L;
public static final String LOADING_MODEL = "org.zkoss.zul.loadingModel";
public static final String SYNCING_MODEL = "org.zkoss.zul.syncingModel";
private static final Logger log = LoggerFactory.getLogger(Listbox.class);
private static final String ATTR_ON_INIT_RENDER_POSTED = "org.zkoss.zul.onInitLaterPosted";
private static final String ATTR_ON_PAGING_INIT_RENDERER_POSTED = "org.zkoss.zul.onPagingInitPosted";
private static final int INIT_LIMIT = 50;
private transient DataLoader _dataLoader;
private transient List<Listitem> _items;
private transient List<int[]> _groupsInfo;
private transient List<Listgroup> _groups;
/** A list of selected items. */
private transient Set<Listitem> _selItems;
/** A readonly copy of {@link #_selItems}. */
private transient Set<Listitem> _roSelItems;
private int _maxlength;
private int _rows, _jsel = -1;
private transient Listhead _listhead;
private transient Listfoot _listfoot;
private transient Frozen _frozen;
private transient ListModel<?> _model;
private transient ListitemRenderer<?> _renderer;
private transient ListDataListener _dataListener;
private transient GroupsDataListener _groupsDataListener;
private transient Collection<Component> _heads;
private int _hdcnt;
private String _innerWidth = "100%";
/** The name. */
private String _name;
/** The paging controller, used only if mold = "paging". */
private transient Paginal _pgi;
private transient boolean _isReplacingItem;
private transient int _focusIndex = -1;
/**
* The paging controller, used only if mold = "paging" and user doesn't
* assign a controller via {@link #setPaginal}. If exists, it is the last
* child
*/
private transient Paging _paging;
private EventListener<Event> _pgListener, _pgImpListener, _modelInitListener;
/** The style class of the odd row. */
private String _scOddRow = null;
/** the # of rows to preload. */
private int _preloadsz = 50;
/** maintain the number of the visible item in Paging mold. */
private int _visibleItemCount;
private int _currentTop = 0; // since 5.0.0 scroll position
private int _currentLeft = 0;
private int _topPad; // since 5.0.0 top padding
private String _nonselTags; //since 5.0.5 for non-selectable tags
private int _anchorTop = 0; //since ZK 5.0.11 , 6.0.0 anchor position
private int _anchorLeft = 0;
private boolean _multiple;
private boolean _disabled, _checkmark;
private boolean _renderAll; //since 5.0.0
private transient boolean _rod;
/** whether to ignore ListDataEvent.SELECTION_CHANGED */
private transient boolean _ignoreDataSelectionEvent;
/** the message to display when there are no items */
private String _emptyMessage;
//ZK-3103: if setSelectedIndex is called, should force scroll into view
private boolean _shallSyncSelInView = false;
//ZK-3982: opened group scrolled out of view
private boolean _shallUpdateScrollPos = false;
//ZK-4329: avoid scroll into view when selectAll is performed
private transient boolean _ignoreSelectedItem;
static {
addClientEvent(Listbox.class, Events.ON_RENDER, CE_DUPLICATE_IGNORE | CE_IMPORTANT | CE_NON_DEFERRABLE);
addClientEvent(Listbox.class, Events.ON_INNER_WIDTH, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
//ZK-925 We can't use CE_DUPLICATE_IGNORE in "onSelect" event since we need to sync the status when multiple select in ROD.
addClientEvent(Listbox.class, Events.ON_SELECT, CE_IMPORTANT);
addClientEvent(Listbox.class, Events.ON_FOCUS, CE_DUPLICATE_IGNORE);
addClientEvent(Listbox.class, Events.ON_BLUR, CE_DUPLICATE_IGNORE);
addClientEvent(Listbox.class, Events.ON_SCROLL_POS, CE_DUPLICATE_IGNORE | CE_IMPORTANT); // since 5.0.0
addClientEvent(Listbox.class, Events.ON_TOP_PAD, CE_DUPLICATE_IGNORE); // since
// 5.0.0
addClientEvent(Listbox.class, Events.ON_DATA_LOADING, CE_DUPLICATE_IGNORE | CE_IMPORTANT | CE_NON_DEFERRABLE); // since 5.0.0
addClientEvent(Listbox.class, ZulEvents.ON_PAGE_SIZE, CE_DUPLICATE_IGNORE | CE_IMPORTANT | CE_NON_DEFERRABLE); //since 5.0.2
// since 6.0.0, F60-ZK-715
addClientEvent(Listbox.class, Events.ON_ACROSS_PAGE, CE_DUPLICATE_IGNORE | CE_IMPORTANT | CE_NON_DEFERRABLE);
// since 6.0.0/5.0.11, B50-ZK-798
addClientEvent(Listbox.class, Events.ON_ANCHOR_POS, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
// since 6.5.5 F65-ZK-2014
addClientEvent(Listbox.class, Events.ON_CHECK_SELECT_ALL, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
}
public Listbox() {
init();
}
private void init() {
_items = new AbstractSequentialList<Listitem>() {
public ListIterator<Listitem> listIterator(int index) {
return new ItemIter(index);
}
public Listitem get(int j) {
final Component o = Listbox.this.getChildren().get(j + _hdcnt);
if (o instanceof Listitem)
return (Listitem) o;
throw new IndexOutOfBoundsException("Wrong index: " + j);
}
public int size() {
int sz = getChildren().size() - _hdcnt;
if (_listfoot != null)
--sz;
if (_paging != null)
--sz;
if (_frozen != null)
--sz;
return sz;
}
/**
* override for Listgroup
*
* @since 3.5.1
*/
protected void removeRange(int fromIndex, int toIndex) {
ListIterator<Listitem> it = listIterator(toIndex);
for (int n = toIndex - fromIndex; --n >= 0 && it.hasPrevious();) {
it.previous();
it.remove();
}
}
/**
* Override to remove unnecessary Listitem re-indexing (when ROD is on, clear() is called frequently).
*/
public void clear() {
final boolean oldFlag = setReplacingItem(true);
try {
//Bug ZK-1834: if there are selected items, clear first.
if (getSelectedCount() > 0) {
clearSelection();
// Bug ZK-1842 Listbox scroll bug listheader sort
_anchorLeft = _anchorTop = 0;
}
super.clear();
} finally {
setReplacingItem(oldFlag);
}
}
};
_selItems = new LinkedHashSet<Listitem>(4);
_roSelItems = Collections.unmodifiableSet(_selItems);
_heads = new AbstractCollection<Component>() {
public int size() {
return _hdcnt;
}
public Iterator<Component> iterator() {
return new Iter();
}
};
_groupsInfo = new LinkedList<int[]>();
_groups = new AbstractList<Listgroup>() {
public int size() {
return getGroupCount();
}
public Iterator<Listgroup> iterator() {
return new IterGroups();
}
public Listgroup get(int index) {
return (Listgroup) getItemAtIndex(_groupsInfo.get(index)[0]);
}
};
}
private int getRealIndex(int index) {
final int offset = _model != null && !_renderAll ? getDataLoader().getOffset() : 0;
return index - (offset < 0 ? 0 : offset);
}
@SuppressWarnings("unchecked")
public <T extends Component> List<T> getChildren() {
return (List<T>) new Children();
}
protected class Children extends AbstractComponent.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();
}
}
}
/**
* Initializes _dataListener and register the listener to the model
*/
private void initDataListener() {
if (_dataListener == null)
_dataListener = new ListDataListener() {
public void onChange(ListDataEvent event) {
onListDataChange(event);
}
};
_model.addListDataListener(_dataListener);
// ZK-3088: for updating group status
if (_model instanceof GroupsListModel) {
if (_groupsDataListener == null) {
_groupsDataListener = new GroupsDataListener() {
public void onChange(GroupsDataEvent event) {
onGroupsDataChange(event);
}
};
}
((GroupsListModel) _model).getGroupsModel().addGroupsDataListener(_groupsDataListener);
}
}
/**
* Returns {@link Listhead} belonging to this listbox, or null if no list
* headers at all.
*/
public Listhead getListhead() {
return _listhead;
}
/**
* Returns {@link Listfoot} belonging to this listbox, or null if no list
* footers at all.
*/
public Listfoot getListfoot() {
return _listfoot;
}
/**
* Returns the frozen child.
*
* @since 5.0.0
*/
public Frozen getFrozen() {
return _frozen;
}
/**
* Returns a collection of heads, including {@link #getListhead} and
* auxiliary heads ({@link Auxhead}) (never null).
*
* @since 3.0.0
*/
public Collection<Component> getHeads() {
return _heads;
}
/**
* Returns whether the HTML's select tag is used.
*/
/* package */ boolean inSelectMold() {
return "select".equals(getMold());
}
/**
* Returns whether the check mark shall be displayed in front of each item.
* <p>
* Default: false.
*/
public boolean isCheckmark() {
return _checkmark;
}
/**
* Sets whether the check mark shall be displayed in front of each item.
* <p>
* The check mark is a checkbox if {@link #isMultiple} returns true. It is a
* radio button if {@link #isMultiple} returns false.
*/
public void setCheckmark(boolean checkmark) {
if (_checkmark != checkmark) {
_checkmark = checkmark;
smartUpdate("checkmark", checkmark);
}
}
/**
* Sets the inner width of this component. The inner width is the width of
* the inner table. By default, it is 100%. That is, it is the same as the
* width of this component. However, it is changed when the user is sizing
* the column's width.
*
* <p>
* Application developers rarely call this method, unless they want to
* preserve the widths of sizable columns changed by the user. To preserve
* the widths, the developer have to store the widths of all columns and the
* inner width ({@link #getInnerWidth}), and then restore them when
* re-creating this component.
*
* @param innerWidth
* the inner width. If null, "100%" is assumed.
* @since 3.0.0
*/
public void setInnerWidth(String innerWidth) {
if (innerWidth == null)
innerWidth = "100%";
if (!_innerWidth.equals(innerWidth)) {
_innerWidth = innerWidth;
smartUpdate("innerWidth", innerWidth);
}
}
/**
* Returns the inner width of this component. The inner width is the width
* of the inner table.
* <p>
* Default: "100%"
*
* @see #setInnerWidth
* @since 3.0.0
*/
public String getInnerWidth() {
return _innerWidth;
}
/**
* Returns whether to grow and shrink vertical to fit their given space, so
* called vertical flexibility.
*
* <p>
* Note: this attribute is ignored if {@link #setRows} is specified
*
* <p>
* Default: false.
*/
public boolean isVflex() {
final String vflex = getVflex();
if ("true".equals(vflex) || "min".equals(vflex)) {
return true;
}
if (Strings.isBlank(vflex) || "false".equals(vflex)) {
return false;
}
return Integer.parseInt(vflex) > 0;
}
/**
* Sets whether to grow and shrink vertical to fit their given space, so
* called vertical flexibility.
*
* <p>
* Note: this attribute is ignored if {@link #setRows} is specified
*/
public void setVflex(boolean vflex) {
if (isVflex() != vflex) {
setVflex(String.valueOf(vflex));
}
}
@Override
public void setVflex(String flex) { //ZK-4296: Error indicating incorrect usage when using both vflex and rows
if (_rows != 0)
throw new UiException("Not allowed to set vflex and rows at the same time");
super.setVflex(flex);
}
/**
* Returns whether it is disabled.
* <p>
* Default: false.
*/
public boolean isDisabled() {
return _disabled;
}
/**
* Sets whether it is disabled.
* <p>Note that it is only applied when mold is "select".
*/
public void setDisabled(boolean disabled) {
if (_disabled != disabled) {
_disabled = disabled;
smartUpdate("disabled", _disabled);
}
}
/**
* Returns the rows. Zero means no limitation.
* <p>
* Default: 0.
*/
public int getRows() {
return _rows;
}
/**
* Sets the rows.
* <p>
* Note: Not allowed to set rows and height/vflex at the same time
*/
public void setRows(int rows) throws WrongValueException {
checkBeforeSetRows();
if (rows < 0) {
throw new WrongValueException("Illegal rows: " + rows);
}
if (_rows != rows) {
_rows = rows;
smartUpdate("rows", _rows);
}
}
@Override
public void setHeight(String height) {
if (_rows != 0)
throw new UiException("Not allowed to set height and rows at the same time");
super.setHeight(height);
}
/**
* Returns the seltype.
* <p>
* Default: "single".
*/
public String getSeltype() {
return _multiple ? "multiple" : "single";
}
/**
* Sets the seltype.
* Allowed values:single,multiple
*
*/
public void setSeltype(String seltype) throws WrongValueException {
if ("single".equals(seltype))
setMultiple(false);
else if ("multiple".equals(seltype))
setMultiple(true);
else
throw new WrongValueException("Unknown seltype: " + seltype);
}
/**
* Returns whether multiple selections are allowed.
* <p>
* Default: false.
*/
public boolean isMultiple() {
return _multiple;
}
/**
* Sets whether multiple selections are allowed.
* <p>Notice that, if a model is assigned, it will change the model's
* state (by {@link Selectable#setMultiple}).
*/
@SuppressWarnings("rawtypes")
public void setMultiple(boolean multiple) {
if (_multiple != multiple) {
_multiple = multiple;
if (!_multiple && _selItems.size() > 1) {
final Listitem item = getSelectedItem();
for (Iterator<Listitem> it = _selItems.iterator(); it.hasNext();) {
final Listitem li = it.next();
if (li != item) {
li.setSelectedDirectly(false);
it.remove();
}
}
// No need to update selId because multiple will do the job at
// client
}
if (_model != null)
((Selectable) _model).setMultiple(multiple);
smartUpdate("multiple", _multiple);
}
}
/**
* Returns the maximal length of each item's label.
* <p>
* It is meaningful only for the select mold.
*/
public int getMaxlength() {
return _maxlength;
}
/**
* Sets the maximal length of each item's label.
* <p>
* It is meaningful only for the select mold.
*/
public void setMaxlength(int maxlength) {
if (maxlength < 0)
maxlength = 0;
if (_maxlength != maxlength) {
_maxlength = maxlength;
smartUpdate("maxlength", maxlength);
}
}
/**
* Returns the name of this component.
* <p>
* Default: null.
* <p>
* The name is used only to work with "legacy" Web application that handles
* user's request by servlets. It works only with HTTP/HTML-based browsers.
* It doesn't work with other kind of clients.
* <p>
* Don't use this method if your application is purely based on ZK's
* event-driven model.
*/
public String getName() {
return _name;
}
/**
* Sets the name of this component.
* <p>
* The name is used only to work with "legacy" Web application that handles
* user's request by servlets. It works only with HTTP/HTML-based browsers.
* It doesn't work with other kind of clients.
* <p>
* Don't use this method if your application is purely based on ZK's
* event-driven model.
*
* @param name
* the name of this component.
*/
public void setName(String name) {
if (name != null && name.length() == 0)
name = null;
if (!Objects.equals(_name, name)) {
_name = name;
smartUpdate("name", name);
}
}
/** Sets a list of HTML tag names that shall <i>not</i> cause the list item
* being selected if they are clicked.
* <p>Default: null (it means button, input, textarea and a). If you want
* to select no matter which tag is clicked, please specify an empty string.
* @param tags a list of HTML tag names that will <i>not</i> cause the list item
* being selected if clicked. Specify null to use the default and "" to
* indicate none.
* @since 5.0.5
*/
public void setNonselectableTags(String tags) {
if (!Objects.equals(_nonselTags, tags)) {
_nonselTags = tags;
smartUpdate("nonselectableTags", tags);
}
}
/** Returns a list of HTML tag names that shall <i>not</i> cause the list item
* being selected if they are clicked.
* <p>Refer to {@link #setNonselectableTags} for details.
* @since 5.0.5
*/
public String getNonselectableTags() {
return _nonselTags;
}
/**
* Returns a live list of all {@link Listitem}. By live we mean you can add
* or remove them directly with the List interface. In other words, you
* could add or remove an item by manipulating the returned list directly.
*/
public List<Listitem> getItems() {
return _items;
}
/**
* Returns the number of items.
*/
public int getItemCount() {
return _items.size();
}
/**
* Returns the item at the specified index.
*
* <p>
* Note: if live data is used ({@link #getModel} is not null), the returned
* item might NOT be loaded yet. To ensure it is loaded, you have to invoke
* {@link #renderItem}.
*/
public Listitem getItemAtIndex(int index) {
final int realindex = getRealIndex(index);
return realindex < 0 || realindex >= _items.size() ? null : _items.get(realindex);
}
/**
* Returns the index of the specified item, or -1 if not found.
*/
public int getIndexOfItem(Listitem item) {
return item == null ? -1 : item.getIndex();
}
/**
* Returns the index of the selected item (-1 if no one is selected).
*/
public int getSelectedIndex() {
return _jsel;
}
/* package */boolean isLoadingModel() {
return getAttribute(LOADING_MODEL) != null;
}
/**
* Deselects all of the currently selected items and selects the item with
* the given index.
*/
@SuppressWarnings("rawtypes")
public void setSelectedIndex(int jsel) {
final int isz = _items.size();
final int tsz = _model != null ? _model.getSize() : isz;
if (jsel >= tsz)
throw new UiException("Out of bound: " + jsel + " while size=" + tsz);
if (jsel < -1)
jsel = -1;
if (jsel < 0) { // unselect all
clearSelection();
} else if (jsel != _jsel || (_multiple && _selItems.size() > 1) || !_selItems.contains(getItemAtIndex(_jsel))) {
for (Listitem item : _selItems) {
item.setSelectedDirectly(false);
}
_selItems.clear();
_jsel = jsel;
Listitem item = getItemAtIndex(_jsel);
if (item == null) { // to be selected item is not there
if (inPagingMold()) {
final int offset = _jsel - _jsel % getPageSize();
final int limit = getPageSize();
getDataLoader().syncModel(offset, limit); // force reloading
if (_jsel != jsel) //Bug ZK-1537: _jsel changed after syncModel if model is never synchronized
_jsel = jsel;
//ZK-3103: this will be triggered once with model, scrolling selection into view
_shallSyncSelInView = true;
} else {
smartUpdate("selInView_", _jsel);
}
} else {
if (!item.isDisabled()) { // Bug ZK-1715: not select item if disabled.
item.setSelectedDirectly(true);
_selItems.add(item);
//ZK-3103: should scroll selection into view
//if model is present, this will be erroneously triggered twice
if (_model == null || !_rod)
_shallSyncSelInView = true;
}
}
if (inSelectMold()) {
smartUpdate("selectedIndex", _jsel);
} else if (item != null)
smartUpdate("selectedItem", item);
// Bug 1734950: don't count on index (since it may change)
// On the other hand, it is OK with select-mold since
// it invalidates if items are added or removed
}
if (_jsel >= 0 && inPagingMold()) {
final Listitem item = getItemAtIndex(_jsel);
int size = getDataLoader().getOffset();
for (Iterator it = new VisibleChildrenIterator(true); it.hasNext(); size++)
if (item.equals(it.next()))
break;
final int pg = size / getPageSize();
if (pg != getActivePage())
setActivePage(pg);
}
}
/**
* Deselects all of the currently selected items and selects the given item.
* <p>
* It is the same as {@link #setSelectedItem}.
*
* @param item
* the item to select. If null, all items are deselected.
*/
public void selectItem(Listitem item) {
if (item == null) {
setSelectedIndex(-1);
} else {
if (item.getParent() != this)
throw new UiException("Not a child: " + item);
if (_multiple || !item.isSelected())
setSelectedIndex(item.getIndex());
}
}
/**
* Selects the given item, without deselecting any other items that are
* already selected..
*
* <p>Notice that if you assign a model to a listbox ({@link #setModel}),
* you shall not invoke this method directly. Rather, use {@link Selectable}
* instead.
*/
public void addItemToSelection(Listitem item) {
if (item.getParent() != this)
throw new UiException("Not a child: " + item);
if (!item.isSelected()) {
if (!_multiple) {
selectItem(item);
} else {
if (item.getIndex() < _jsel || _jsel < 0) {
_jsel = item.getIndex();
if (!_ignoreSelectedItem) {
// ZK-866
// update the change of selected index
if (inSelectMold()) {
smartUpdate("selectedIndex", _jsel);
} else if (item != null)
smartUpdate("selectedItem", item);
}
}
if (!item.isDisabled()) { // Bug ZK-1715: not select item if disabled.
item.setSelectedDirectly(true);
_selItems.add(item);
}
// ZK-2113: should be same as normal mold
// if (inSelectMold()) {
// item.smartUpdate("selected", true);
// } else {
// }
smartUpdateSelection();
}
}
}
/**
* Deselects the given item without deselecting other items.
*
* <p>Notice that if you assign a model to a listbox ({@link #setModel}),
* you shall not invoke this method directly. Rather, use {@link Selectable}
* instead.
*/
public void removeItemFromSelection(Listitem item) {
if (item.getParent() != this)
throw new UiException("Not a child: " + item);
if (item.isSelected()) {
if (!_multiple) {
clearSelection();
} else {
item.setSelectedDirectly(false);
_selItems.remove(item);
fixSelectedIndex(0);
// ZK-2113: should be same as normal mold
// if (inSelectMold()) {
// item.smartUpdate("selected", false);
// } else {
// }
smartUpdateSelection();
}
}
}
/**
* Note: we have to update all selection at once, since addItemToSelection
* and removeItemFromSelection might be called interchangeably.
*/
private void smartUpdateSelection() {
final StringBuffer sb = new StringBuffer(80);
for (Listitem item : _selItems) {
if (sb.length() > 0)
sb.append(',');
sb.append(item.getUuid());
}
smartUpdate("chgSel", sb.toString());
}
/**
* If the specified item is selected, it is deselected. If it is not
* selected, it is selected. Other items in the list box that are selected
* are not affected, and retain their selected state.
*/
public void toggleItemSelection(Listitem item) {
if (item.isSelected())
removeItemFromSelection(item);
else
addItemToSelection(item);
}
/**
* Clears the selection.
*/
public void clearSelection() {
if (!_selItems.isEmpty()) {
for (Listitem item : _selItems) {
item.setSelectedDirectly(false);
}
_selItems.clear();
_jsel = -1;
if (inSelectMold())
smartUpdate("selectedIndex", -1);
else
smartUpdate("selectedItem", null);
// Bug 1734950: don't count on index (since it may change)
}
}
/**
* Selects all items.
*/
public void selectAll() {
if (!_multiple)
throw new UiException("Appliable only to the multiple seltype: " + this);
if (_items.size() != _selItems.size()) {
for (Listitem item : _items) {
_selItems.add(item);
item.setSelectedDirectly(true);
}
_jsel = _items.isEmpty() ? -1 : 0;
smartUpdate("selectAll", true);
}
}
/**
* Returns the selected item.
*
* <p>
* Note: if live data is used ({@link #getModel} is not null), the returned
* item might NOT be loaded yet. To ensure it is loaded, you have to invoke
* {@link #renderItem}.
*/
public Listitem getSelectedItem() {
return _jsel >= 0 ? _jsel > 0 && _selItems.size() == 1 // optimize for performance
? _selItems.iterator().next() : getItemAtIndex(_jsel) : null;
}
/**
* Deselects all of the currently selected items and selects the given item.
* <p>
* It is the same as {@link #selectItem}.
*/
public void setSelectedItem(Listitem item) {
selectItem(item);
}
/**
* Selects the given listitems.
*
* @since 3.6.0
*/
public void setSelectedItems(Set<Listitem> listItems) {
if (!isMultiple())
throw new WrongValueException("Listbox must allow multiple selections.");
for (Iterator<Listitem> it = listItems.iterator(); it.hasNext();) {
addItemToSelection(it.next());
}
}
/**
* Returns all selected items.
*
* <p>
* Note: if live data is used ({@link #getModel} is not null), the returned
* item might NOT be loaded yet. To ensure it is loaded, you have to invoke
* {@link #renderItem}.
*/
public Set<Listitem> getSelectedItems() {
return _roSelItems;
}
/**
* Returns the number of items being selected.
*/
public int getSelectedCount() {
return _selItems.size();
}
/**
* Appends an item.
*
* <p>
* Note: if live data is used ({@link #getModel} is not null), the returned
* item might NOT be loaded yet. To ensure it is loaded, you have to invoke
* {@link #renderItem}.
*/
public Listitem appendItem(String label, String value) {
final Listitem item = new Listitem(label, value);
item.applyProperties();
item.setParent(this);
return item;
}
/**
* Removes the child item in the list box at the given index.
*
* <p>
* Note: if live data is used ({@link #getModel} is not null), the returned
* item might NOT be loaded yet. To ensure it is loaded, you have to invoke
* {@link #renderItem}.
*
* @return the removed item.
*/
public Listitem removeItemAt(int index) {
final Listitem item = getItemAtIndex(index);
removeChild(item);
return item;
}
// --Paging--//
/**
* Returns the paging controller, or null if not available. Note: the paging
* controller is used only if {@link #getMold} is "paging".
*
* <p>
* If mold is "paging", this method never returns null, because a child
* paging controller is created automatically (if not specified by
* developers with {@link #setPaginal}).
*
* <p>
* If a paging controller is specified (either by {@link #setPaginal}, or by
* {@link #setMold} with "paging"), the listbox will rely on the paging
* controller to handle long-content instead of scrolling.
*/
public Paginal getPaginal() {
return _pgi;
}
/**
* Specifies the paging controller. Note: the paging controller is used only
* if {@link #getMold} is "paging".
*
* <p>It is OK, though without any effect, to specify a paging controller
* even if mold is not "paging".
*
* @param pgi the paging controller. If null and {@link #getMold} is
* "paging", a paging controller is created automatically as a child
* component (see {@link #getPagingChild}).
*/
public void setPaginal(Paginal pgi) {
if (!Objects.equals(pgi, _pgi)) {
final Paginal old = _pgi;
_pgi = pgi; // assign before detach paging, since removeChild
// assumes it
if (inPagingMold()) {
if (old != null)
removePagingListener(old);
if (_pgi == null) {
if (_paging != null) {
_pgi = _paging;
} else
newInternalPaging();
} else { // _pgi != null
if (_pgi != _paging) {
if (_paging != null)
_paging.detach();
_pgi.setTotalSize(getDataLoader().getTotalSize());
addPagingListener(_pgi);
if (_pgi instanceof Component)
smartUpdate("paginal", _pgi);
}
}
// Bug ZK-1696: model also preserves paging information
if (_model instanceof Pageable) {
Pageable m = (Pageable) _model;
m.setPageSize(_pgi.getPageSize());
m.setActivePage(_pgi.getActivePage());
}
}
}
}
/**
* Creates the internal paging component.
*/
private void newInternalPaging() {
final Paging paging = new InternalPaging();
paging.setDetailed(true);
paging.applyProperties();
//min page size is 1
if (_model instanceof Pageable && ((Pageable) _model).getPageSize() > 0) {
paging.setPageSize(((Pageable) _model).getPageSize());
}
paging.setTotalSize(getDataLoader().getTotalSize());
//min page index is 0
if (_model instanceof Pageable && ((Pageable) _model).getActivePage() >= 0) {
paging.setActivePage(((Pageable) _model).getActivePage());
}
paging.setParent(this);
if (_pgi != null)
addPagingListener(_pgi);
}
@SuppressWarnings("serial")
private class PGListener implements PagingListener {
public void onEvent(Event event) {
//Bug ZK-1622: reset anchor position after changing page
_anchorTop = 0;
_anchorLeft = 0;
if (event instanceof PagingEvent) {
PagingEvent pe = (PagingEvent) event;
int pgsz = pe.getPageable().getPageSize();
int actpg = pe.getActivePage();
if (PageableModel.INTERNAL_EVENT.equals(event.getName())) {
if (pgsz > 0) //min page size is 1
_pgi.setPageSize(pgsz);
if (actpg >= 0) //min page index is 0
_pgi.setActivePage(actpg);
} else if (_model instanceof Pageable) {
// Bug ZK-1696: model also preserves paging information
// additional check to avoid infinite loop
((Pageable) _model).setActivePage(actpg);
}
Events.postEvent(new PagingEvent(event.getName(), Listbox.this, pe.getPageable(), actpg));
}
}
public Object willClone(Component comp) {
return null; // skip to clone
}
}
@SuppressWarnings("serial")
private class PGImpListener implements PagingListener {
public void onEvent(Event event) {
if (_model != null && inPagingMold()) {
final Paginal pgi = getPaginal();
int pgsz = pgi.getPageSize();
int ofs = pgi.getActivePage() * pgsz;
if (event instanceof PagingEvent) {
// Bug ZK-1696: PagingEvent have the newest paging information
pgsz = ((PagingEvent) event).getPageable().getPageSize();
ofs = ((PagingEvent) event).getActivePage() * pgsz;
}
if (_model instanceof Pageable) {
((Pageable) _model).setPageSize(pgsz);
((Pageable) _model).setActivePage(pgi.getActivePage());
}
if (_rod) {
getDataLoader().syncModel(ofs, pgsz);
}
postOnPagingInitRender();
}
invalidate();
}
public Object willClone(Component comp) {
return null; // skip to clone
}
}
/** Adds the event listener for the onPaging event. */
private void addPagingListener(Paginal pgi) {
if (_pgListener == null)
_pgListener = new PGListener();
pgi.addEventListener(ZulEvents.ON_PAGING, _pgListener);
if (_model instanceof PageableModel) {
((PageableModel) _model).addPagingEventListener((PagingListener) _pgListener);
}
if (_pgImpListener == null)
_pgImpListener = new PGImpListener();
pgi.addEventListener("onPagingImpl", _pgImpListener);
}
/** Removes the event listener for the onPaging event. */
private void removePagingListener(Paginal pgi) {
if (_model instanceof PageableModel) {
((PageableModel) _model).removePagingEventListener((PagingListener) _pgListener);
}
pgi.removeEventListener(ZulEvents.ON_PAGING, _pgListener);
pgi.removeEventListener("onPagingImpl", _pgImpListener);
}
/**
* Returns the child paging controller that is created automatically, or
* null if mold is not "paging", or the controller is specified externally
* by {@link #setPaginal}.
*
* @since 3.0.7
*/
public Paging getPagingChild() {
return _paging;
}
protected Paginal pgi() {
if (_pgi == null)
throw new IllegalStateException("Available only the paging mold");
return _pgi;
}
/**
* Sets the active page in which the specified item is. The active page will
* become the page that contains the specified item.
*
* @param item
* the item to show. If the item is null or doesn't belong to
* this listbox, nothing happens.
* @since 3.0.4
* @see #setActivePage(int)
*/
public void setActivePage(Listitem item) {
if (item != null && item.getParent() == this) {
final int pg = item.getIndex() / getPageSize();
if (pg != getActivePage())
setActivePage(pg);
}
}
@Override
public void setActivePage(int pg) throws WrongValueException {
// Bug ZK-1696: model also preserves paging information
if (_model instanceof Pageable) {
((Pageable) _model).setActivePage(pg);
}
super.setActivePage(pg);
}
/**
* Returns whether this listbox is in the paging mold.
*/
/* package */boolean inPagingMold() {
return "paging".equals(getMold());
}
/**
* Returns the number of visible descendant {@link Listitem}.
*
* @since 3.5.1
*/
public int getVisibleItemCount() {
return _visibleItemCount;
}
/* package */void addVisibleItemCount(int count) {
if (count != 0) {
_visibleItemCount += count;
if (inPagingMold()) {
final Paginal pgi = getPaginal();
int totalSize = getDataLoader().getTotalSize();
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(totalSize);
invalidate(); // the set of visible items might change
} else if (((Cropper) getDataLoader()).isCropper()) {
getDataLoader().updateModelInfo();
} else {
smartUpdate("visibleItemCount", _visibleItemCount);
}
}
}
/**
* Returns the style class for the odd rows.
* <p>
* Default: {@link #getZclass()}-odd. (since 3.5.0)
*
* @since 3.0.0
*/
public String getOddRowSclass() {
return _scOddRow == null ? getZclass() + "-odd" : _scOddRow;
}
/**
* Sets the style class for the odd rows. If the style class doesn't exist,
* the striping effect disappears. You can provide different effects by
* providing the proper style classes.
*
* @since 3.0.0
*/
public void setOddRowSclass(String scls) {
if (scls != null && scls.length() == 0)
scls = null;
if (!Objects.equals(_scOddRow, scls)) {
_scOddRow = scls;
smartUpdate("oddRowSclass", scls);
}
}
/**
* Returns the number of listgroup
*
* @since 3.5.0
*/
public int getGroupCount() {
return _groupsInfo.size();
}
/**
* Returns a list of all {@link Listgroup}.
*
* @since 3.5.0
*/
public List<Listgroup> getGroups() {
return _groups;
}
/**
* Returns whether listgroup exists.
*
* @since 3.5.0
*/
public boolean hasGroup() {
return !_groupsInfo.isEmpty();
}
/** Sets true to avoid unnecessary Listitem re-indexing when render template.
* @param b true to skip
* @return original true/false status
* @see Renderer#render
*/
/* package */boolean setReplacingItem(boolean b) {
final boolean old = _isReplacingItem;
if (_model != null) // B60-ZK-898: only apply when model is used.
_isReplacingItem = b;
return old;
}
/* package */void fixItemIndices(int j, int to, boolean infront) {
int realj = getRealIndex(j);
if (realj < 0) {
realj = 0;
}
if (realj < _items.size()) {
final int beginning = j;
for (Iterator<Listitem> it = _items.listIterator(realj); it.hasNext() && (to < 0 || j <= to); ++j) {
Listitem o = it.next();
o.setIndexDirectly(j);
if (_isReplacingItem) //@see Renderer#render
break; //set only the first Listitem, 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 Listgroup) {
int[] g = getLastGroupsInfoAt(j + (infront ? -1 : 1));
if (g != null) {
g[0] = j;
if (g[2] != -1)
g[2] += (infront ? 1 : -1);
}
}
}
}
}
/**
* 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 fixItemIndices(int j, int to) {
int realj = getRealIndex(j);
if (realj < 0)
realj = 0;
if (realj < _items.size()) {
for (Iterator<Listitem> it = _items.listIterator(realj); it.hasNext() && (to < 0 || j <= to); ++j)
it.next().setIndexDirectly(j);
}
}
/* package */Listgroup getListgroupAt(int index) {
if (_groupsInfo.isEmpty())
return null;
final int[] g = getGroupsInfoAt(index);
if (g != null) {
return (Listgroup) getItemAtIndex(g[0]);
}
return null;
}
/**
* Returns the group index which matches with the ListModel index.
*
* @param index
* the list item index
* @return the associated group index of the list item 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;
}
/* package */int[] getGroupsInfoAt(int index) {
return getGroupsInfoAt(index, false);
}
/**
* Returns an int array that it has two length, one is an index of
* listgroup, and the other is the number of items of listgroup(inclusive).
*/
/* package */int[] getGroupsInfoAt(int index, boolean isListgroup) {
for (int[] g : _groupsInfo) {
if (isListgroup) {
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;
}
public void beforeChildAdded(Component newChild, Component refChild) {
if (newChild instanceof Listitem) {
if (newChild instanceof Listgroupfoot) {
if (!hasGroup())
throw new UiException("Listgroupfoot cannot exist alone, you have to add a Listgroup first");
if (refChild == null) {
if (getLastChild() instanceof Listgroupfoot)
throw new UiException("Only one Listgroupfoot is allowed per Listgroup");
}
}
} else if (newChild instanceof Listhead) {
if (_listhead != null && _listhead != newChild)
throw new UiException("Only one listhead is allowed: " + this);
} else if (newChild instanceof Frozen) {
if (_frozen != null && _frozen != newChild)
throw new UiException("Only one frozen child is allowed: " + this);
if (inSelectMold())
log.warn("Mold select ignores frozen");
} else if (newChild instanceof Listfoot) {
if (_listfoot != null && _listfoot != newChild)
throw new UiException("Only one listfoot is allowed: " + this);
if (inSelectMold())
log.warn("Mold select ignores listfoot");
} else if (newChild instanceof Paging) {
if (_paging != null && _paging != newChild)
throw new UiException("Only one paging is allowed: " + this);
if (_pgi != null)
throw new UiException("External paging cannot coexist with child paging");
if (!inPagingMold())
throw new UiException("The child paging is allowed only in the paging mold");
} else if (!(newChild instanceof Auxhead)) {
throw new UiException("Unsupported child for Listbox: " + newChild);
}
super.beforeChildAdded(newChild, refChild);
}
private boolean hasGroupsModel() {
return _model instanceof GroupsListModel;
}
public boolean insertBefore(Component newChild, Component refChild) {
if (newChild instanceof Listitem) {
final boolean isReorder = newChild.getParent() == this;
//bug #3051305: Active Page not update when drag & drop item to the end
if (isReorder) {
checkInvalidateForMoved((Listitem) newChild, true);
}
fixGroupsInfoBeforeInsert(newChild, refChild, isReorder);
// first: listhead or auxhead
// last two: listfoot and paging
if (refChild != null && refChild.getParent() != this)
refChild = null; // Bug 1649625: it becomes the last child
if (refChild != null && (refChild == _listhead || refChild instanceof Auxhead))
refChild = getChildren().size() > _hdcnt ? getChildren().get(_hdcnt) : null;
refChild = fixRefChildBeforeFoot(refChild);
final Listitem newItem = (Listitem) newChild;
final int jfrom = newItem.getParent() == this ? newItem.getIndex() : -1;
if (super.insertBefore(newChild, refChild)) {
// Maintain _items
final int jto = refChild instanceof Listitem ? ((Listitem) refChild).getIndex() : -1,
fixFrom = jfrom < 0 || (jto >= 0 && jfrom > jto) ? jto : jfrom;
// jfrom < 0: use jto
// jto < 0: use jfrom
// otherwise: use min(jfrom, jto)
if (fixFrom < 0)
newItem.setIndexDirectly(_items.size() - 1 + getDataLoader().getOffset());
else
fixItemIndices(fixFrom, jfrom >= 0 && jto >= 0 ? jfrom > jto ? jfrom : jto : -1, !isReorder);
// Maintain selected
final int newIndex = newItem.getIndex();
if (newItem.isSelected()) {
if (_jsel < 0) {
_jsel = newIndex;
_selItems.add(newItem);
smartUpdateSelection();
} else if (_multiple) {
if (_jsel > newIndex) {
_jsel = newIndex;
}
_selItems.add(newItem);
smartUpdateSelection();
} else { // deselect
newItem.setSelectedDirectly(false);
}
} else {
if (jfrom < 0) { // no existent child
if (!isLoadingModel() && _jsel >= newIndex)
++_jsel;
} else if (_jsel >= 0) { // any selected
if (jfrom > _jsel) { // from below
if (jto >= 0 && jto <= _jsel)
++_jsel;
} else { // from above
if (jto < 0 || jto > _jsel)
--_jsel;
}
}
}
fixGroupsInfoAfterInsert(newItem);
//bug #3049167: Totalsize increase when drag & drop in paging Listbox/Grid
if (!isReorder) { //if reorder, not an insert
afterInsert(newChild);
}
return true;
} // insert
} else if (newChild instanceof Listhead) {
final boolean added = _listhead == null;
refChild = fixRefChildForHeader(refChild);
if (super.insertBefore(newChild, refChild)) {
_listhead = (Listhead) newChild;
if (added)
++_hdcnt; // it may be moved, not inserted
return true;
}
} else if (newChild instanceof Auxhead) {
final boolean added = newChild.getParent() != this;
refChild = fixRefChildForHeader(refChild);
if (super.insertBefore(newChild, refChild)) {
if (added)
++_hdcnt; // it may be moved, not inserted
return true;
}
} else if (newChild instanceof Frozen) {
refChild = _paging; // the last two: listfoot and paging
if (super.insertBefore(newChild, refChild)) {
_frozen = (Frozen) newChild;
return true;
}
} else if (newChild instanceof Listfoot) {
// the last two: listfoot and paging
if (_frozen != null)
refChild = _frozen;
else
refChild = _paging;
if (super.insertBefore(newChild, refChild)) {
_listfoot = (Listfoot) newChild;
return true;
}
} else if (newChild instanceof Paging) {
refChild = null; // the last: paging
if (super.insertBefore(newChild, refChild)) {
_pgi = _paging = (Paging) newChild;
return true;
}
} else {
return super.insertBefore(newChild, refChild);
// impossible but to make it more extensible
}
return false;
}
private Component fixRefChildForHeader(Component refChild) {
if (refChild != null && refChild.getParent() != this)
refChild = null;
// try the first listitem
if (refChild == null || (refChild != _listhead && !(refChild instanceof Auxhead)))
refChild = getChildren().size() > _hdcnt ? getChildren().get(_hdcnt) : null;
// try listfoot or paging if no listem
refChild = fixRefChildBeforeFoot(refChild);
return refChild;
}
private Component fixRefChildBeforeFoot(Component refChild) {
if (refChild == null) {
if (_listfoot != null)
refChild = _listfoot;
else if (_frozen != null)
refChild = _frozen;
else
refChild = _paging;
} else if (refChild == _paging) {
if (_listfoot != null)
refChild = _listfoot;
else if (_frozen != null)
refChild = _frozen;
}
return refChild;
}
/**
* If the child is a listgroup, its listgroupfoot will be removed at the
* same time.
*/
public boolean removeChild(Component child) {
if (_paging == child && _pgi == child && inPagingMold())
throw new IllegalStateException(
"The paging component cannot be removed manually. It is removed automatically when changing the mold");
// Feature 1906110: prevent developers from removing it accidently
if (child instanceof Listitem && child.getParent() == this)
beforeRemove(child);
if (!super.removeChild(child))
return false;
if (_listhead == child) {
_listhead = null;
--_hdcnt;
} else if (_listfoot == child) {
_listfoot = null;
} else if (_frozen == child) {
_frozen = null;
} else if (child instanceof Listitem) {
// maintain items
final Listitem item = (Listitem) child;
final int index = item.getIndex();
item.setIndexDirectly(-1); // mark
// Maintain selected
if (item.isSelected()) {
_selItems.remove(item);
if (_jsel == index) {
fixSelectedIndex(index);
}
smartUpdateSelection();
} else {
if (!isLoadingModel() && _jsel >= index) {
--_jsel;
}
}
fixGroupsInfoAfterRemove(child, index);
} else if (_paging == child) {
_paging = null;
if (_pgi == child)
_pgi = null;
} else if (child instanceof Auxhead) {
--_hdcnt;
}
if (((Cropper) getDataLoader()).isCropper()) {
getDataLoader().updateModelInfo();
}
return true;
}
/**
* Callback if a list item has been inserted.
* <p>
* Note: it won't be called if other kind of child is inserted.
* <p>
* When this method is called, the index is correct.
* <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 (_isReplacingItem) //@see Renderer#render
return; //called by #insertBefore(), skip handling GroupInfo
updateVisibleCount((Listitem) comp, false);
checkInvalidateForMoved((Listitem) comp, false);
}
/**
* Callback if a list item will be removed (not removed yet). Note: it won't
* be called if other kind of child is removed.
* <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 (_isReplacingItem) //@see Renderer#render
return; //called by #removeChild(), skip handling GroupInfo
updateVisibleCount((Listitem) comp, true);
checkInvalidateForMoved((Listitem) comp, true);
}
/**
* Update the number of the visible item before it is removed or after it is
* added.
*/
private void updateVisibleCount(Listitem item, boolean isRemove) {
if (item instanceof Listgroup || item.isVisible()) {
final Listgroup g = getListgroupAt(item.getIndex());
// We shall update the number of the visible item in the following
// cases.
// 1) If the item is a type of Listgroupfoot, it is always shown.
// 2) If the item is a type of Listgroup, it is always shown.
// 3) If the item doesn't belong to any group.
// 4) If the group of the item is open.
if (item instanceof Listgroupfoot || item instanceof Listgroup || g == null || g.isOpen())
addVisibleItemCount(isRemove ? -1 : 1);
if (item instanceof Listgroup) {
final Listgroup group = (Listgroup) item;
// 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.
if (item.getPreviousSibling() instanceof Listitem) {
final Listitem preRow = (Listitem) item.getPreviousSibling();
if (preRow == null) {
if (!group.isOpen()) {
addVisibleItemCount(isRemove ? group.getVisibleItemCount() : -group.getVisibleItemCount());
}
} else {
final Listgroup preGroup = preRow instanceof Listgroup ? (Listgroup) preRow
: getListgroupAt(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());
}
}
} else if (!group.isOpen()) {
addVisibleItemCount(isRemove ? group.getVisibleItemCount() : -group.getVisibleItemCount());
}
}
}
if (inPagingMold()) {
int totalSize = getDataLoader().getTotalSize();
if (isRemove && _model instanceof Pageable) {
Pageable p = (Pageable) _model;
int actpg = p.getActivePage();
int maxPageIndex = p.getPageCount() - 1;
if (actpg > maxPageIndex) {
p.setActivePage(maxPageIndex);
}
}
getPaginal().setTotalSize(totalSize);
}
}
/**
* 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(Listitem 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 elem which in act and act has only one elem
if (inPagingMold() && !isInvalidated()) {
final int j = child.getIndex(), pgsz = getPageSize(), n = (getActivePage() + 1) * pgsz;
if (j >= n)
return; // case 2
final int cnt = getItems().size(), n2 = n - pgsz;
if (j >= n2 && cnt <= n && (!bRemove || cnt > n2 + 1))
return; // case 1
invalidate();
}
}
/**
* An iterator used by visible children.
*/
@SuppressWarnings("rawtypes")
private class VisibleChildrenIterator implements Iterator {
private final ListIterator<Listitem> _it = getItems().listIterator();
private int _count = 0;
private boolean _isBeginning = false;
private VisibleChildrenIterator() {
}
private VisibleChildrenIterator(boolean isBeginning) {
_isBeginning = isBeginning;
}
public boolean hasNext() {
if (!inPagingMold())
return _it.hasNext();
if (!_isBeginning && _count >= getPaginal().getPageSize()) {
return false;
}
if (_count == 0 && !_isBeginning) {
final Paginal pgi = getPaginal();
int begin = pgi.getActivePage() * pgi.getPageSize();
for (int i = 0; i < begin && _it.hasNext();) {
getVisibleRow((Listitem) _it.next());
i++;
}
}
return _it.hasNext();
}
private Listitem getVisibleRow(Listitem item) {
if (item instanceof Listgroup) {
final Listgroup g = (Listgroup) item;
if (!g.isOpen()) {
for (int j = 0, len = g.getItemCount(); j < len && _it.hasNext(); j++)
_it.next();
}
}
while (!item.isVisible() && _it.hasNext())
item = (Listitem) _it.next();
return item;
}
public Object next() {
if (!inPagingMold())
return _it.next();
_count++;
final Listitem item = (Listitem) _it.next();
return _it.hasNext() ? getVisibleRow(item) : item;
}
public void remove() {
throw new UnsupportedOperationException();
}
}
/**
* Fix the selected index, _jsel, assuming there are no selected one before
* (and excludes) j-the item.
*/
private void fixSelectedIndex(int j) {
if (!_selItems.isEmpty()) {
int realj = getRealIndex(j);
if (realj < 0)
realj = 0;
if (realj < _items.size()) {
for (Iterator<Listitem> it = _items.listIterator(realj); it.hasNext(); ++j) {
final Listitem item = it.next();
if (item.isSelected()) {
_jsel = j;
return;
}
}
}
}
_jsel = -1;
}
private void fixGroupsInfoBeforeInsert(Component newChild, Component refChild, boolean isReorder) {
if (_isReplacingItem) //@see Renderer#render
return; //called by #insertBefore(), skip handling GroupInfo
if (newChild instanceof Listgroupfoot) {
if (refChild == null) {
if (isReorder) {
final int idx = ((Listgroupfoot) newChild).getIndex();
final int[] ginfo = getGroupsInfoAt(idx);
if (ginfo != null) {
ginfo[1]--;
ginfo[2] = -1;
}
}
final int[] g = _groupsInfo.get(getGroupCount() - 1);
g[2] = getItems().get(getItems().size() - 1).getIndex();
} else if (refChild instanceof Listitem) {
final int idx = ((Listitem) refChild).getIndex();
final int[] g = getGroupsInfoAt(idx);
if (g == null)
throw new UiException("Listgroupfoot cannot exist alone, you have to add a Listgroup first");
if (g[2] != -1)
throw new UiException("Only one Listgroupfoot is allowed per Listgroup");
if (idx != (g[0] + g[1]))
throw new UiException("Listgroupfoot must be placed after the last Row of the Listgroup");
g[2] = idx - 1;
if (isReorder) {
final int nindex = ((Listgroupfoot) newChild).getIndex();
final int[] ginfo = getGroupsInfoAt(nindex);
if (ginfo != null) {
ginfo[1]--;
ginfo[2] = -1;
}
}
} else {
final Component preRefChild = refChild.getPreviousSibling();
if (preRefChild instanceof Listitem) {
final int idx = ((Listitem) preRefChild).getIndex();
//bug 2936019: Execption when Listbox insertBefore() group + groupfoot
final int[] g = getGroupsInfoAt(idx, preRefChild instanceof Listgroup);
if (g == null)
throw new UiException("Listgroupfoot cannot exist alone, you have to add a Listgroup first");
if (g[2] != -1)
throw new UiException("Only one Listgroupfoot is allowed per Listgroup");
if (idx + 1 != (g[0] + g[1]))
throw new UiException("Listgroupfoot must be placed after the last Row of the Listgroup");
g[2] = idx;
if (isReorder) {
final int nindex = ((Listgroupfoot) newChild).getIndex();
final int[] ginfo = getGroupsInfoAt(nindex);
if (ginfo != null) {
ginfo[1]--;
ginfo[2] = -1;
}
}
}
}
}
}
private void fixGroupsInfoAfterInsert(Listitem newItem) {
if (_isReplacingItem) //@see Renderer#render
return; //called by #insertBefore(), skip handling GroupInfo
if (newItem instanceof Listgroup) {
Listgroup lg = (Listgroup) newItem;
if (_groupsInfo.isEmpty())
_groupsInfo.add(new int[] { lg.getIndex(), getItemCount() - lg.getIndex(), -1 });
else {
int idx = 0;
int[] prev = null, next = null;
for (int[] g : _groupsInfo) {
if (g[0] <= lg.getIndex()) {
prev = g;
idx++;
} else {
next = g;
break;
}
}
if (prev != null) {
int index = lg.getIndex(), 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 listgroupfoot
} else if (next != null) {
_groupsInfo.add(idx, new int[] { lg.getIndex(), next[0] - lg.getIndex(), -1 });
}
}
} else if (!_groupsInfo.isEmpty()) {
int index = newItem.getIndex();
final int[] g = getGroupsInfoAt(index);
if (g != null) {
g[1]++;
if (g[2] != -1 && (g[2] >= index || newItem instanceof Listgroupfoot))
g[2] = g[0] + g[1] - 1;
}
}
}
private void fixGroupsInfoAfterRemove(Component child, int index) {
if (!_isReplacingItem) { //@see Renderer#render
//called by #removeChild(), handling GroupInfo if !isReplcingItem
if (child instanceof Listgroup) {
int[] prev = null, remove = null;
for (int[] g : _groupsInfo) {
if (g[0] == index) {
remove = g;
break;
}
prev = g;
}
if (prev != null && remove != null) {
prev[1] += remove[1] - 1;
}
fixItemIndices(index, -1, false);
if (remove != null) {
_groupsInfo.remove(remove);
final int idx = remove[2];
if (idx != -1) {
final int realIndex = getRealIndex(idx) - 1;
if (realIndex >= 0 && realIndex < getItemCount())
removeChild(getChildren().get(realIndex));
}
}
} else if (!_groupsInfo.isEmpty()) {
final int[] g = getGroupsInfoAt(index);
if (g != null) {
g[1]--;
if (g[2] != -1)
g[2]--;
fixItemIndices(index, -1, false);
} else
fixItemIndices(index, -1, false);
if (child instanceof Listgroupfoot) {
final int[] g1 = getGroupsInfoAt(index);
if (g1 != null)
g1[2] = -1;
}
} else
fixItemIndices(index, -1);
}
if (hasGroupsModel() && getItemCount() <= 0) { // remove to empty,
// reset _groupsInfo
_groupsInfo = new LinkedList<int[]>();
}
//bug 3057288
//getDataLoader().updateModelInfo(); //itemsInvalidate after really removed
//return true;
}
// -- ListModel dependent codes --//
/**
* Returns the model associated with this list box, or null if this list box
* is not associated with any list data model.
*
* <p>
* Note: if {@link #setModel(GroupsModel)} was called with a groups model,
* this method returns an instance of {@link ListModel} encapsulating it.
*
* @see #setModel(ListModel)
* @see #setModel(GroupsModel)
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> ListModel<T> getModel() {
return (ListModel) _model;
}
/**
* Returns the list model associated with this list box, or null if this
* list box is associated with a {@link GroupsModel} or not associated with
* any list data model.
*
* @since 3.5.0
* @see #setModel(ListModel)
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> ListModel<T> getListModel() {
return _model instanceof GroupsListModel ? null : (ListModel) _model;
}
/**
* Returns the groups model associated with this list box, or null if this
* list box is associated with a {@link ListModel} or not associated with
* any list data model.
*
* @since 3.5.0
* @see #setModel(GroupsModel)
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public <D, G, F> GroupsModel<D, G, F> getGroupsModel() {
return _model instanceof GroupsListModel ? ((GroupsListModel) _model).getGroupsModel() : null;
}
/**
* Sets the list model associated with this listbox. If a non-null model is
* assigned, no matter whether it is the same as the previous, it will
* always cause re-render.
*
* @param model the list model to associate, or null to dis-associate any
* previous model. If not null, it must implement {@link Selectable}.
* @exception UiException if failed to initialize with the model
* @see #getListModel
* @see #setModel(GroupsModel)
*/
public void setModel(ListModel<?> model) {
//ZK-3514: speed up
if (_model != null && _model != model) {
int threshold = Utils.getIntAttribute(this, "org.zkoss.zul.invalidateThreshold", 10, true);
int diff = Math.abs((model != null ? model.getSize() : 0) - _model.getSize());
if (diff > threshold)
invalidate();
}
if (model != null) {
if (!(model instanceof Selectable))
throw new UiException(model.getClass() + " must implement " + Selectable.class);
if (model instanceof GroupsSelectableModel) {
((GroupsSelectableModel) model).setGroupSelectable(isListgroupSelectable());
}
if (_model != model) {
if (_model != null) {
_model.removeListDataListener(_dataListener);
if (_model instanceof PageableModel && _pgListener != null)
((PageableModel) _model).removePagingEventListener((PagingListener) _pgListener);
GroupsModel groupsModel = getGroupsModel();
if (groupsModel != null) {
((GroupsListModel) _model).cleanInternalListener();
groupsModel.removeGroupsDataListener(_groupsDataListener);
}
/* Bug ZK-1512: should clear listitem anyway
if (_model instanceof GroupsListModel)
getItems().clear();*/
resetDataLoader(); // Bug 3357641
if (!isAutosort()) {
Listhead hds = getListhead();
if (hds != null) {
for (Component listheader : hds.getChildren()) {
((Listheader) listheader).setSortDirection("natural");
}
}
}
}
getItems().clear(); // Bug 1807414, ZK-1512
_visibleItemCount = 0; //Bug ZK-3735: should clear _visibleItemCount before syncModel.
if (!inSelectMold()) {
smartUpdate("model",
model instanceof GroupsListModel || model instanceof GroupsModel ? "group" : true);
}
_model = model;
initDataListener();
setAttribute(Attributes.BEFORE_MODEL_ITEMS_RENDERED, Boolean.TRUE);
//ZK-3173: move the block here to avoid modifying pgi "again" before PagingEvent is handled
if (inPagingMold()) {
if (_model instanceof PageableModel)
((PageableModel) _model).addPagingEventListener((PagingListener) _pgListener);
//B30-2129667, B36-2782751, (ROD) exception when zul applyProperties
//must update paginal totalSize or exception in setActivePage
final Paginal pgi = getPaginal();
Pageable m = _model instanceof Pageable ? (Pageable) _model : null;
//if pageable model contain non-default values, sync from model to pgi
//otherwise, sync from pgi to model
if (m != null) {
if (m.getPageSize() > 0) { //min page size is 1
pgi.setPageSize(m.getPageSize());
} else {
m.setPageSize(pgi.getPageSize());
}
}
pgi.setTotalSize(getDataLoader().getTotalSize());
if (m != null) {
if (m.getActivePage() >= 0) { //min page index is 0
pgi.setActivePage(m.getActivePage());
} else {
m.setActivePage(pgi.getActivePage());
}
}
}
}
final Execution exec = Executions.getCurrent();
final boolean defer = exec == null ? false
: exec.getAttribute("zkoss.Listbox.deferInitModel_" + getUuid()) != null;
final boolean rod = evalRod();
//Always syncModel because it is easier for user to enfore reload
if (!defer || !rod) { //if attached and rod, defer the model sync
getDataLoader().syncModel(-1, -1);
removeAttribute(Attributes.BEFORE_MODEL_ITEMS_RENDERED);
}
if (!doSort(this))
postOnInitRender();
// Since user might setModel and setItemRender separately or
// repeatedly,
// we don't handle it right now until the event processing phase
// such that we won't render the same set of data twice
// --
// For better performance, we shall load the first few row now
// (to save a roundtrip)
} else if (_model != null) {
_model.removeListDataListener(_dataListener);
if (_model instanceof PageableModel && _pgListener != null)
((PageableModel) _model).removePagingEventListener((PagingListener) _pgListener);
_model = null;
getItems().clear();
if (!inSelectMold())
smartUpdate("model", false);
getDataLoader().updateModelInfo();
}
}
/**
* Sets the groups model associated with this list box. If a non-null model
* is assigned, no matter whether it is the same as the previous, it will
* always cause re-render.
*
* <p>
* The groups model is used to represent a list of data with grouping.
*
* @param model
* the groups model to associate, or null to dis-associate any
* previous model.
* @exception UiException
* if failed to initialize with the model
* @since 3.5.0
* @see #setModel(ListModel)
* @see #getGroupsModel()
*/
@SuppressWarnings("rawtypes")
public void setModel(GroupsModel<?, ?, ?> model) {
if (model instanceof AbstractGroupsModel) {
((AbstractGroupsModel) model).setGroupSelectable(isListgroupSelectable());
}
setModel((ListModel) (model != null ? GroupsListModel.toListModel(model) : null));
}
private static boolean doSort(Listbox listbox) {
Listhead hds = listbox.getListhead();
if (!listbox.isAutosort() || hds == null)
return false;
for (Iterator<Component> it = hds.getChildren().iterator(); it.hasNext();) {
final Listheader hd = (Listheader) it.next();
String dir = hd.getSortDirection();
if (!"natural".equals(dir)) {
return hd.doSort("ascending".equals(dir));
}
}
return false;
}
/**
* Returns the renderer to render each item, or null if the default renderer
* is used.
*/
@SuppressWarnings("unchecked")
public <T> ListitemRenderer<T> getItemRenderer() {
return (ListitemRenderer<T>) _renderer;
}
/**
* Sets the renderer which is used to render each item if {@link #getModel}
* is not null.
*
* <p>
* Note: changing a render will not cause the listbox to re-render. If you
* want it to re-render, you could assign the same model again (i.e.,
* setModel(getModel())), or fire an {@link ListDataEvent} event.
*
* @param renderer
* the renderer, or null to use the default.
* @exception UiException
* if failed to initialize with the model
*/
public void setItemRenderer(ListitemRenderer<?> renderer) {
if (_renderer != renderer) {
_renderer = renderer;
if (_model != null) {
if ((renderer instanceof ListitemRendererExt) || (_renderer instanceof ListitemRendererExt)) {
// bug# 2388345, a new renderer that might new own Listitem,
// shall clean all Listitems first
getItems().clear();
getDataLoader().syncModel(-1, -1); // we have to recreate
// all
} else if (getAttribute(ATTR_ON_INIT_RENDER_POSTED) == null) {
getDataLoader().syncModel(-1, -1); // have to recreate all
} else {
//bug# 3039282, we need to resyncModel if not in a defer mode
final Execution exec = Executions.getCurrent();
final boolean defer = exec == null ? false
: exec.getAttribute("zkoss.Listbox.deferInitModel_" + getUuid()) != null;
final boolean rod = evalRod();
if (!defer || !rod)
getDataLoader().syncModel(-1, -1);
}
}
}
}
/**
* Sets the renderer by use of a class name. It creates an instance
* automatically.
*/
@SuppressWarnings("rawtypes")
public void setItemRenderer(String clsnm) throws ClassNotFoundException, NoSuchMethodException,
IllegalAccessException, InstantiationException, java.lang.reflect.InvocationTargetException {
if (clsnm != null)
setItemRenderer((ListitemRenderer) Classes.newInstanceByThread(clsnm));
}
/**
* Handles a private event, onInitRender. It is used only for
* implementation, and you rarely need to invoke it explicitly.
*/
public void onInitRender() {
removeAttribute(ATTR_ON_INIT_RENDER_POSTED);
doInitRenderer();
}
/**
* Handles a private event, onPagingInitRender. It is used only for
* implementation, and you rarely need to invoke it explicitly.
*/
public void onPagingInitRender() {
removeAttribute(ATTR_ON_PAGING_INIT_RENDERER_POSTED);
doInitRenderer();
}
@SuppressWarnings("rawtypes")
private void doInitRenderer() {
if (getPage() == null)
return;
// sync the multiple status from model
if (_model != null)
setMultiple(((Selectable) _model).isMultiple());
final Renderer renderer = new Renderer();
try {
int pgsz, ofs;
if (inPagingMold()) {
pgsz = _pgi.getPageSize();
ofs = _pgi.getActivePage() * pgsz;
} else {
pgsz = inSelectMold() ? getItemCount() : getDataLoader().getLimit();
ofs = inSelectMold() ? 0 : getDataLoader().getOffset();
// we don't know # of visible rows, so a 'smart' guess
// It is OK since client will send back request if not enough
}
final int cnt = getItemCount() + getDataLoader().getOffset();
if (ofs >= cnt) { // not possible; just in case
ofs = cnt - pgsz;
if (ofs < 0)
ofs = 0;
}
int j = 0;
int realOfs = ofs - getDataLoader().getOffset();
if (realOfs < 0)
realOfs = 0;
boolean open = true;
for (Listitem item = getItems().size() <= realOfs ? null : getItems().get(realOfs), nxt; j < pgsz
&& item != null; item = nxt, j++) {
nxt = nextListitem(item); //retrieve first since it might be changed
if (item.isVisible() && (open || item instanceof Listgroupfoot || item instanceof Listgroup)) {
renderer.render(item, j + ofs);
}
if (item instanceof Listgroup)
open = ((Listgroup) item).isOpen();
}
getDataLoader().updateModelInfo();
} catch (Throwable ex) {
renderer.doCatch(ex);
} finally {
renderer.doFinally();
}
Events.postEvent(ZulEvents.ON_AFTER_RENDER, this, null); // notify the listbox when items have been rendered.
removeAttribute(Attributes.BEFORE_MODEL_ITEMS_RENDERED);
}
private static Listitem nextListitem(Listitem item) {
final Component c = item.getNextSibling();
return c instanceof Listitem ? (Listitem) c : null;
//listitem must be placed contineously
}
private void postOnInitRender() {
// 20080724, Henri Chen: optimize to avoid postOnInitRender twice
if (getAttribute(ATTR_ON_INIT_RENDER_POSTED) == null) {
setAttribute(ATTR_ON_INIT_RENDER_POSTED, Boolean.TRUE);
// Bug ZL-1696: manipulate list from api might happen before list
// render, use sendEvent instead of postEvent to render list first
Events.postEvent("onInitRender", this, null);
}
}
private void postOnPagingInitRender() {
if (getAttribute(ATTR_ON_PAGING_INIT_RENDERER_POSTED) == null
&& getAttribute(ATTR_ON_INIT_RENDER_POSTED) == null) { // B50-ZK-345
setAttribute(ATTR_ON_PAGING_INIT_RENDERER_POSTED, Boolean.TRUE);
Events.postEvent("onPagingInitRender", this, null);
}
}
private void onGroupsDataChange(GroupsDataEvent event) {
getDataLoader().doGroupsDataChange(event);
}
/**
* Handles when the list model's content changed.
*/
@SuppressWarnings("rawtypes")
private void onListDataChange(ListDataEvent event) {
int type = event.getType();
// ZK-4549: should ignore before handling sorting
if (getAttribute(Attributes.BEFORE_MODEL_ITEMS_RENDERED) != null
&& (type == ListDataEvent.INTERVAL_ADDED || type == ListDataEvent.INTERVAL_REMOVED))
return;
//sort when add
if ((type == ListDataEvent.INTERVAL_ADDED || type == ListDataEvent.CONTENTS_CHANGED)
&& !isIgnoreSortWhenChanged()) {
if (doSort(this)) {
getDataLoader().updateModelInfo();
} else {
getDataLoader().doListDataChange(event);
postOnInitRender(); // to improve performance
}
} else if (type == ListDataEvent.SELECTION_CHANGED) {
if (!_ignoreDataSelectionEvent) {
if (event.getIndex0() > -1) {
setSelectedIndex(event.getIndex0());
// auto scrollIntoView, if item is not rendered
}
doSelectionChanged();
}
} else if (type == ListDataEvent.MULTIPLE_CHANGED) {
setMultiple(((Selectable) _model).isMultiple());
} else if (type == ListDataEvent.DISABLE_CLIENT_UPDATE) {
_ignoreSelectedItem = true;
} else if (type == ListDataEvent.ENABLE_CLIENT_UPDATE) {
_ignoreSelectedItem = false;
} else {
getDataLoader().doListDataChange(event);
postOnInitRender(); // to improve performance
// TODO: We have to skip the synchronization of the target component
// when the event is fired from it, i.e. No need to sync the sorting
// status here.
if (event.getType() == ListDataEvent.STRUCTURE_CHANGED && _model instanceof Sortable && _listhead != null) { //ZK-1705 added null check for _listhead
Sortable<Object> smodel = cast(_model);
List<Listheader> headers = cast(_listhead.getChildren());
boolean found = false;
for (Listheader col : headers) {
if (found) {
col.setSortDirection("natural");
} else {
Comparator<Object> cmpr = cast(col.getSortAscending());
String dir = smodel.getSortDirection(cmpr);
found = !"natural".equals(dir);
if (!found) {
cmpr = cast(col.getSortDescending());
dir = smodel.getSortDirection(cmpr);
found = !"natural".equals(dir);
}
col.setSortDirection(dir);
}
}
}
if (_model.getSize() == 0) { // Bug ZK-1834: model is empty
resetDataLoader(true);
}
}
}
/** Called when SELECTION_CHANGED is received. */
private void doSelectionChanged() {
final Selectable<Object> smodel = getSelectableModel();
final Set<Object> selObjs = smodel.getSelection();
final int expSelCnt = selObjs.size();
if (expSelCnt == 0) {
clearSelection();
return;
}
//Optimize the single-selection case
if (!_multiple && _jsel >= 0) {
if (smodel.isSelected(_model.getElementAt(_jsel)))
return; //nothing changed
getSelectedItem().setSelected(false);
}
//Unfortunately, we have to scan all list items either ROD or not,
//because the selection is meaningful only if a listitem is loaded
//The performance is not good but it rarely happens (since it is called
//if the selection is modified by app directly)
int selCnt = 0;
for (final Listitem item : _items) {
if (item.isLoaded()) { //selected is meaningful only if loaded
boolean sel = selObjs.contains(_model.getElementAt(item.getIndex()));
if (sel)
++selCnt;
item.setSelected(sel);
}
if (selCnt == expSelCnt && selCnt >= _selItems.size())
break; //done (all selected items are sync)
}
}
@SuppressWarnings("unchecked")
private Selectable<Object> getSelectableModel() {
return (Selectable<Object>) _model;
}
/** Used to render listitem if _model is specified. */
/* package */class Renderer { // use package for easy to call (if override)
@SuppressWarnings("rawtypes")
private final ListitemRenderer _renderer;
private boolean _rendered, _ctrled;
/* package */@SuppressWarnings("rawtypes")
Renderer() {
_renderer = (ListitemRenderer) getDataLoader().getRealRenderer();
}
/* package */@SuppressWarnings({ "unchecked", "rawtypes" })
void render(Listitem item, int index) throws Throwable {
if (item.isLoaded()) {
return; // nothing to do
}
if (!_rendered && (_renderer instanceof RendererCtrl)) {
((RendererCtrl) _renderer).doTry();
_ctrled = true;
}
final Listcell cell = (Listcell) item.getFirstChild();
if (!(_renderer instanceof ListitemRendererExt)
|| (((ListitemRendererExt) _renderer).getControls() & ListitemRendererExt.DETACH_ON_RENDER) != 0) { // detach
// (default)
cell.detach();
}
//bug #3039843: Paging Listbox without rod, ListModel shall not fully loaded
//check if the item is a selected item and add into selected set
final Object value = _model.getElementAt(index);
final SelectionControl ctrl = getSelectableModel().getSelectionControl();
final boolean selectable = ctrl == null ? true : ctrl.isSelectable(value);
//bug #ZK-675: Selection was lost if a render replace the listitem
final boolean selected = ((Selectable) _model).isSelected(value);
final boolean oldFlag = setReplacingItem(true); //skipFixItemIndices when rendering
try {
try {
_renderer.render(item, value, index);
} catch (AbstractMethodError ex) {
final Method m = _renderer.getClass().getMethod("render",
new Class<?>[] { Listitem.class, Object.class });
m.setAccessible(true);
m.invoke(_renderer, new Object[] { item, value });
}
Object v = item.getAttribute(Attributes.MODEL_RENDERAS);
if (v != null) //a new listitem is created to replace the existent one
item = (Listitem) v;
} catch (Throwable ex) {
try {
item.setLabel(Exceptions.getMessage(ex));
} catch (Throwable t) {
log.error("", t);
}
item.setLoaded(true);
throw ex;
} finally {
setReplacingItem(oldFlag);
if (item.getChildren().isEmpty())
cell.setParent(item);
}
//bug #ZK-675: Selection was lost if a render replace the listitem
//Also we have to add it to selection when rendered
if (selected)
addItemToSelection(item);
if (ctrl != null)
item.setSelectable(selectable);
item.setLoaded(true);
_rendered = true;
}
/* package */void doCatch(Throwable ex) {
if (_ctrled) {
try {
((RendererCtrl) _renderer).doCatch(ex);
} catch (Throwable t) {
throw UiException.Aide.wrap(t);
}
} else {
throw UiException.Aide.wrap(ex);
}
}
/* package */void doFinally() {
if (_ctrled)
((RendererCtrl) _renderer).doFinally();
}
}
/**
* Renders the specified {@link Listitem} if not loaded yet, with
* {@link #getItemRenderer}.
*
* <p>
* It does nothing if {@link #getModel} returns null. In other words, it is
* meaningful only if live data model is used.
*
* @see #renderItems
* @see #renderAll
* @return the list item being passed to this method
*/
public Listitem renderItem(Listitem li) {
if (_model != null && li != null && !li.isLoaded()) {
final Renderer renderer = new Renderer();
try {
renderer.render(li, li.getIndex());
} catch (Throwable ex) {
renderer.doCatch(ex);
} finally {
renderer.doFinally();
}
}
return li;
}
/**
* Renders all {@link Listitem} if not loaded yet, with
* {@link #getItemRenderer}.
*
* @see #renderItem
* @see #renderItems
*/
public void renderAll() {
if (_model == null)
return;
_renderAll = true;
getDataLoader().setLoadAll(_renderAll);
final Renderer renderer = new Renderer();
if (!getItems().isEmpty())
try {
Listitem item = getItems().get(0);
int index = item.getIndex();
for (Listitem nxt; item != null; item = nxt) {
nxt = nextListitem(item); //retrieve first since it might be changed
renderer.render(item, index++);
}
} catch (Throwable ex) {
renderer.doCatch(ex);
} finally {
renderer.doFinally();
}
}
/** Renders the given set of list items.
*/
public void renderItems(Set<? extends Listitem> items) {
if (_model == null)
return;
if (items.isEmpty())
return; // nothing to do
final Renderer renderer = new Renderer();
try {
for (final Listitem item : items)
renderer.render(item, item.getIndex());
} catch (Throwable ex) {
renderer.doCatch(ex);
} finally {
renderer.doFinally();
}
}
/** Sets the mold to render this component.
*
* @param mold the mold. If null or empty, "default" is assumed.
* Allowed values: default, select, paging
* @see org.zkoss.zk.ui.metainfo.ComponentDefinition
*/
public void setMold(String mold) {
final String old = getMold();
if (!Objects.equals(old, mold)) {
super.setMold(mold);
// we have to change model before detaching paging,
// since removeChild assumes it
if ("paging".equals(old)) { // change from paging
if (_paging != null) {
removePagingListener(_paging);
_paging.detach();
} else if (_pgi != null) {
removePagingListener(_pgi);
}
if (getModel() != null) {
getDataLoader().syncModel(0, initRodSize()); // change offset back to 0
postOnInitRender();
}
invalidate(); // paging mold -> non-paging mold
} else if (inPagingMold()) { // change to paging
if (_pgi != null)
addPagingListener(_pgi);
else
newInternalPaging();
_topPad = 0;
_currentTop = 0;
_currentLeft = 0;
// enforce a page loading
// B50-ZK-345: speed up onPagingImpl to surpass onInitRender
Events.postEvent(10001, new PagingEvent("onPagingImpl", (Component) _pgi, _pgi.getActivePage()));
invalidate(); // non-paging mold -> paging mold
}
}
}
/**
* Returns the message to display when there are no items
* @return String
* @since 5.0.7
*/
public String getEmptyMessage() {
return _emptyMessage;
}
/**
* Sets the message to display when there are no items
* @param emptyMessage
* @since 5.0.7
*/
public void setEmptyMessage(String emptyMessage) {
if (!Objects.equals(emptyMessage, _emptyMessage)) {
this._emptyMessage = emptyMessage;
smartUpdate("emptyMessage", this._emptyMessage);
}
}
public String getZclass() {
return _zclass == null ? "z-listbox" : _zclass;
}
@SuppressWarnings("serial")
private class ItemIter implements ListIterator<Listitem>, java.io.Serializable {
private ListIterator<Listitem> _it;
private int _j;
private boolean _bNxt;
private ItemIter(int index) {
_j = index;
}
public void add(Listitem o) {
prepare();
_it.add(o);
++_j;
}
public boolean hasNext() {
return _j < _items.size();
}
public boolean hasPrevious() {
return _j > 0;
}
public Listitem next() {
if (!hasNext()) //use _items.size() to control if reach listfoot
throw new NoSuchElementException();
prepare();
final Listitem o = _it.next();
++_j;
_bNxt = true;
return o;
}
public Listitem previous() {
if (!hasPrevious()) //use _j >= 0 to control if reach listhead
throw new NoSuchElementException();
prepare();
final Listitem o = _it.previous();
--_j;
_bNxt = false;
return o;
}
public int nextIndex() {
return _j;
}
public int previousIndex() {
return _j - 1;
}
public void remove() {
if (_it == null)
throw new IllegalStateException();
_it.remove();
if (_bNxt)
--_j;
}
public void set(Listitem o) {
if (_it == null)
throw new IllegalStateException();
_it.set(o);
}
private void prepare() {
if (_it == null)
_it = cast(getChildren().listIterator(_j + _hdcnt));
}
}
private boolean evalRod() {
return Utils.testAttribute(this, "org.zkoss.zul.listbox.rod", false, true)
&& !(_model instanceof GroupsListModel);
//TODO: performance enhancement: support GroupsModel in ROD
}
private void setFocusIndex(int index) {
// F60-ZK-715: notify Listbox widget to set _focusItem
if (index != _focusIndex) {
_focusIndex = index;
smartUpdate("focusIndex", index);
}
}
/* package */DataLoader getDataLoader() {
if (_dataLoader == null) {
_rod = evalRod();
final String loadercls = Library.getProperty("org.zkoss.zul.listbox.DataLoader.class");
try {
_dataLoader = _rod && loadercls != null ? (DataLoader) Classes.forNameByThread(loadercls).newInstance()
: new ListboxDataLoader();
} catch (Exception e) {
throw UiException.Aide.wrap(e);
}
_dataLoader.init(this, 0, initRodSize());
}
return _dataLoader;
}
public void onPageAttached(Page newpage, Page oldpage) {
super.onPageAttached(newpage, oldpage);
if (oldpage == null) { // mark as a new attached Listbox
final Execution exec = Executions.getCurrent();
exec.setAttribute("zkoss.Listbox.deferInitModel_" + getUuid(), Boolean.TRUE);
exec.setAttribute("zkoss.Listbox.attached_" + getUuid(), Boolean.TRUE);
// prepare a right moment to init Listbox (must be as early as possible)
this.addEventListener("onInitModel", _modelInitListener = new ModelInitListener());
Events.postEvent(20000, new Event("onInitModel", this)); //first event to be called
}
GroupsModel groupsModel = getGroupsModel();
if (_model != null || groupsModel != null) {
getDataLoader().syncModel(-1, -1);
postOnInitRender();
}
if (_model != null && _dataListener != null) {
_model.removeListDataListener(_dataListener);
_model.addListDataListener(_dataListener);
}
if (_model instanceof PageableModel && _pgListener != null) {
((PageableModel) _model).removePagingEventListener((PagingListener) _pgListener);
((PageableModel) _model).addPagingEventListener((PagingListener) _pgListener);
}
if (groupsModel != null && _groupsDataListener != null) {
groupsModel.removeGroupsDataListener(_groupsDataListener);
groupsModel.addGroupsDataListener(_groupsDataListener);
}
}
public void onPageDetached(Page page) {
super.onPageDetached(page);
if (_model != null && _dataListener != null)
_model.removeListDataListener(_dataListener);
if (_model instanceof PageableModel && _pgListener != null)
((PageableModel) _model).removePagingEventListener((PagingListener) _pgListener);
GroupsModel groupsModel = getGroupsModel();
if (groupsModel != null && _groupsDataListener != null)
groupsModel.removeGroupsDataListener(_groupsDataListener);
}
private void resetDataLoader() {
resetDataLoader(true);
}
private void resetDataLoader(boolean shallReset) {
if (_dataLoader != null) {
if (shallReset) {
_dataLoader.reset();
smartUpdate("_lastoffset", 0); //reset for bug 3357641
}
_dataLoader = null;
}
if (shallReset) {
// Bug ZK-373
smartUpdate("resetDataLoader", true);
_currentTop = 0;
_currentLeft = 0;
_anchorTop = 0;
_anchorLeft = 0;
_topPad = 0;
}
}
@SuppressWarnings("serial")
private class ModelInitListener implements SerializableEventListener<Event>, CloneableEventListener<Event> {
public void onEvent(Event event) throws Exception {
if (_modelInitListener != null) {
Listbox.this.removeEventListener("onInitModel", _modelInitListener);
_modelInitListener = null;
}
// initialize data loader
// Tricky! might has been initialized when apply
// properties
if (_dataLoader != null) {
final boolean rod = evalRod();
if (_rod != rod || getItems().isEmpty()) {
if (_model != null) { // so has to recreate
// list items
getItems().clear();
resetDataLoader(); // enforce recreate the dataloader
initModel(); //init the model
} else {
resetDataLoader(); // enforce recreate the dataloader
// dataloader
// Bug ZK-1895
//The attribute shall be removed, otherwise DataLoader will not syncModel when setModel
Executions.getCurrent().removeAttribute("zkoss.Listbox.deferInitModel_" + getUuid());
}
}
} else if (_model != null) { //items in model not init yet
initModel(); //init the model
} else {
//The attribute shall be removed, otherwise DataLoader will not syncModel when setModel
Executions.getCurrent().removeAttribute("zkoss.Listbox.deferInitModel_" + getUuid());
}
final DataLoader loader = getDataLoader();
// initialize paginal if any
Paginal pgi = getPaginal();
if (pgi != null)
pgi.setTotalSize(loader.getTotalSize());
}
private void initModel() {
Executions.getCurrent().removeAttribute("zkoss.Listbox.deferInitModel_" + getUuid());
setModel(_model); //init the model
}
public Object willClone(Component comp) {
return null; // skip to clone
}
}
// Cloneable//
@SuppressWarnings("rawtypes")
public Object clone() {
final Listbox clone = (Listbox) super.clone();
clone.init();
// remove cached listeners
clone._pgListener = null;
clone._pgImpListener = null;
//recreate the DataLoader
final int offset = clone.getDataLoader().getOffset();
clone.afterUnmarshal(offset);
// after _pgi ready, and then getLimit() will work
final int limit = clone.getDataLoader().getLimit();
clone.resetDataLoader(false); // no need to reset, it will reset the old reference.
clone.getDataLoader().init(clone, offset, limit);
if (clone._model != null) {
if (clone._model instanceof ComponentCloneListener) {
final ListModel model = (ListModel) ((ComponentCloneListener) clone._model).willClone(clone);
if (model != null)
clone._model = model;
}
// we use the same data model but we have to create a new listener
clone._dataListener = null;
clone.initDataListener();
// As the bug in tree - B30-1892446.zul, the component clone won't
// clone the posted event, so we need to remove the attributes here.
clone.removeAttribute(ATTR_ON_INIT_RENDER_POSTED);
clone.removeAttribute(ATTR_ON_PAGING_INIT_RENDERER_POSTED);
clone.getDataLoader().setLoadAll(_renderAll);
}
clone._groupsInfo.addAll(_groupsInfo);
return clone;
}
private void afterUnmarshal(int index) {
for (Iterator<Component> it = getChildren().iterator(); it.hasNext();) {
final Object child = it.next();
if (child instanceof Listitem) {
final Listitem li = (Listitem) child;
li.setIndexDirectly(index++); // since Listitem.clone() resets
// index
if (li.isSelected()) {
_selItems.add(li);
}
} else if (child instanceof Listhead) {
_listhead = (Listhead) child;
} else if (child instanceof Listfoot) {
_listfoot = (Listfoot) child;
} else if (child instanceof Frozen) {
_frozen = (Frozen) child;
} else if (child instanceof Paging) {
_pgi = _paging = (Paging) child;
addPagingListener(_pgi);
}
}
}
// -- Serializable --//
// NOTE: they must be declared as private
private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
s.defaultWriteObject();
willSerialize(_model);
Serializables.smartWrite(s, _model);
willSerialize(_renderer);
Serializables.smartWrite(s, _renderer);
// keep the scrolling status after serialized
if (_dataLoader != null) {
s.writeInt(_dataLoader.getOffset());
s.writeInt(_dataLoader.getLimit());
} else {
s.writeInt(0);
s.writeInt(100);
}
int size = _groupsInfo.size();
s.writeInt(size);
if (size > 0)
s.writeObject(_groupsInfo);
}
@SuppressWarnings("rawtypes")
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
_model = (ListModel) s.readObject();
didDeserialize(_model);
_renderer = (ListitemRenderer) s.readObject();
didDeserialize(_renderer);
init();
int offset = s.readInt();
afterUnmarshal(offset);
int limit = s.readInt();
resetDataLoader(false); // no need to reset, it will reset the old reference.
getDataLoader().init(this, offset, limit);
if (_model != null) {
initDataListener();
getDataLoader().setLoadAll(_renderAll);
// Map#Entry cannot be serialized, we have to restore them
if (_model instanceof ListModelMap) {
for (Listitem item : getItems())
item.setValue(_model.getElementAt(item.getIndex()));
}
}
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));
}
}
public void sessionWillPassivate(Page page) {
super.sessionWillPassivate(page);
willPassivate(_model);
willPassivate(_renderer);
}
public void sessionDidActivate(Page page) {
super.sessionDidActivate(page);
didActivate(_model);
didActivate(_renderer);
}
// -- ComponentCtrl --//
protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException {
super.renderProperties(renderer);
// ZK-4970: reset invalidateListitems here.
Executions.getCurrent().removeAttribute("zkoss.Listbox.invalidateListitems" + getUuid());
if (_rows > 0)
renderer.render("rows", getRows());
render(renderer, "name", _name);
render(renderer, "emptyMessage", _emptyMessage);
if (inSelectMold()) {
render(renderer, "multiple", isMultiple());
render(renderer, "disabled", isDisabled());
if (_maxlength > 0)
renderer.render("maxlength", _maxlength);
} else {
render(renderer, "oddRowSclass", _scOddRow);
render(renderer, "checkmark", isCheckmark());
render(renderer, "multiple", isMultiple());
if (_model != null) {
render(renderer, "model",
_model instanceof GroupsListModel || _model instanceof GroupsModel ? "group" : true);
if (isMultiple()) {
SelectionControl selectionControl = getSelectableModel().getSelectionControl();
if (selectionControl != null) {
// cannot use render(renderer) here, we have to send a false state
// to client.
renderer.render("$$selectAll", selectionControl.isSelectAll());
}
}
}
if (!"100%".equals(_innerWidth))
render(renderer, "innerWidth", _innerWidth);
if (_currentTop != 0)
renderer.render("_currentTop", _currentTop);
if (_currentLeft != 0)
renderer.render("_currentLeft", _currentLeft);
//ZK-798
if (_anchorTop != 0)
renderer.render("_anchorTop", _anchorTop);
if (_anchorLeft != 0)
renderer.render("_anchorLeft", _anchorLeft);
renderer.render("_topPad", _topPad);
renderer.render("_totalSize", getDataLoader().getTotalSize());
renderer.render("_offset", getDataLoader().getOffset());
if (_rod && !_renderAll) {
if (((Cropper) getDataLoader()).isCropper())//bug #2936064
renderer.render("_listbox$rod", true);
int sz = initRodSize();
if (sz != INIT_LIMIT)
renderer.render("initRodSize", initRodSize());
if (!inPagingMold() && _jsel >= 0)
renderer.render("_selInView", _jsel); // B50-ZK-56
}
if (_nonselTags != null)
renderer.render("nonselectableTags", _nonselTags);
if (isCheckmarkDeselectOther())
renderer.render("checkmarkDeselectOther", true);
if (!isRightSelect())
renderer.render("rightSelect", false);
if (isListgroupSelectable())
renderer.render("groupSelect", true);
if (isSelectOnHighlightDisabled()) // F70-ZK-2433
renderer.render("selectOnHighlightDisabled", true);
}
if (_pgi != null && _pgi instanceof Component) {
renderer.render("paginal", _pgi);
// ZK 8, if no model used in paging mold, we don't support select all in this case
if (_model == null) {
renderer.render("_listbox$noSelectAll", true); // B50-ZK-873, separate the select all condition and isCropper
}
//ZK-3103: only true when setSelectedIndex is called
if (_shallSyncSelInView) {
renderer.render("_listbox$shallSyncSelInView", true);
_shallSyncSelInView = false;
}
}
if (_focusIndex > -1)
renderer.render("focusIndex", _focusIndex); // F60-ZK-715
if (_shallUpdateScrollPos) {
renderer.render("_listbox$shallUpdateScrollPos", true);
_shallUpdateScrollPos = false;
}
}
/** Returns whether to toggle a list item selection on right click
*/
private boolean isRightSelect() {
return Utils.testAttribute(this, "org.zkoss.zul.listbox.rightSelect", true, true);
}
protected boolean isAutohidePaging() {
return Utils.testAttribute(this, "org.zkoss.zul.listbox.autohidePaging", true, true);
}
/** Returns whether to sort all of item when model or sort direction be changed.
* @since 5.0.7
*/
/*package*/ boolean isAutosort() {
String attr = "org.zkoss.zul.listbox.autoSort";
Object val = getAttribute(attr, true);
if (val == null)
val = Library.getProperty(attr);
return val instanceof Boolean ? ((Boolean) val).booleanValue()
: val != null ? "true".equals(val) || "ignore.change".equals(val) : false;
}
protected boolean isSelectOnHighlightDisabled() {
return Utils.testAttribute(this, "org.zkoss.zul.listbox.selectOnHighlight.disabled", false, true);
}
/**
* Returns the number of items to preload when receiving the rendering
* request from the client.
* <p>
* Default: 50. (Since 6.0.1)
* <p>
* It is used only if live data ({@link #setModel(ListModel)} and not paging
* ({@link #getPagingChild}.
*/
private int preloadSize() {
final String size = (String) getAttribute("pre-load-size");
int sz = size != null ? Integer.parseInt(size) : _preloadsz;
if ((sz = Utils.getIntAttribute(this, "org.zkoss.zul.listbox.preloadSize", sz, true)) < 0)
throw new UiException("nonnegative is required: " + sz);
return sz;
}
/**
* Returns the number of items rendered when the Listbox first render.
* <p>
* Default: 50. (Since 6.0.1)
* <p>
* It is used only if live data ({@link #setModel(ListModel)} and not paging
* ({@link #getPagingChild}.
*/
private int initRodSize() {
int sz = Utils.getIntAttribute(this, "org.zkoss.zul.listbox.initRodSize", INIT_LIMIT, true);
if ((sz) < 0)
throw new UiException("nonnegative is required: " + sz);
return sz;
}
/** Returns whether to sort all of item when model or sort direction be changed.
* @since 5.0.7
*/
private boolean isIgnoreSortWhenChanged() {
String attr = "org.zkoss.zul.listbox.autoSort";
Object val = getAttribute(attr, true);
if (val == null)
val = Library.getProperty(attr);
return val == null ? true : "ignore.change".equals(val);
}
/** Returns whether to toggle the selection if clicking on a list item
* with a checkmark.
*/
private boolean isCheckmarkDeselectOther() {
if (_ckDeselectOther == null) //ok to race
_ckDeselectOther = Boolean
.valueOf("true".equals(Library.getProperty("org.zkoss.zul.listbox.checkmarkDeselectOthers")));
return Utils.testAttribute(this, "org.zkoss.zul.listbox.checkmarkDeselectOthers", _ckDeselectOther.booleanValue(), true);
}
private static Boolean _ckDeselectOther;
/**
* Returns whether Listgroup is selectable.
*/
private boolean isListgroupSelectable() {
return Utils.testAttribute(this, "org.zkoss.zul.listbox.groupSelect", false, true);
}
private <T> Set<T> collectUnselectedObjects(Set<T> previousSelection, Set<T> currentSelection) {
Set<T> prevSeldItems = previousSelection != null ? new LinkedHashSet<T>(previousSelection)
: new LinkedHashSet<T>();
if (currentSelection != null && prevSeldItems.size() > 0)
prevSeldItems.removeAll(currentSelection);
return prevSeldItems;
}
private static HashMap<String, PropertyAccess> _properties = new HashMap<String, PropertyAccess>(8);
static {
_properties.put("_currentTop", new IntegerPropertyAccess() {
public void setValue(Component cmp, Integer currentTop) {
((Listbox) cmp)._currentTop = currentTop;
}
public Integer getValue(Component cmp) {
return ((Listbox) cmp)._currentTop;
}
});
_properties.put("_currentLeft", new IntegerPropertyAccess() {
public void setValue(Component cmp, Integer currentLeft) {
((Listbox) cmp)._currentLeft = currentLeft;
}
public Integer getValue(Component cmp) {
return ((Listbox) cmp)._currentLeft;
}
});
_properties.put("_anchorTop", new IntegerPropertyAccess() {
public void setValue(Component cmp, Integer anchorTop) {
((Listbox) cmp)._anchorTop = anchorTop;
}
public Integer getValue(Component cmp) {
return ((Listbox) cmp)._anchorTop;
}
});
_properties.put("_anchorLeft", new IntegerPropertyAccess() {
public void setValue(Component cmp, Integer anchorLeft) {
((Listbox) cmp)._anchorLeft = anchorLeft;
}
public Integer getValue(Component cmp) {
return ((Listbox) cmp)._anchorLeft;
}
});
_properties.put("_topPad", new IntegerPropertyAccess() {
public void setValue(Component cmp, Integer topPad) {
((Listbox) cmp)._topPad = topPad;
}
public Integer getValue(Component cmp) {
return ((Listbox) cmp)._topPad;
}
});
_properties.put("_selInView", new IntegerPropertyAccess() {
public void setValue(Component cmp, Integer jsel) {
((Listbox) cmp)._jsel = jsel;
}
public Integer getValue(Component cmp) {
return ((Listbox) cmp)._jsel;
}
});
_properties.put("_listbox$shallSyncSelInView", new BooleanPropertyAccess() {
public void setValue(Component cmp, Boolean shallSyncSelInView) {
((Listbox) cmp)._shallSyncSelInView = shallSyncSelInView;
}
public Boolean getValue(Component cmp) {
return ((Listbox) cmp)._shallSyncSelInView;
}
});
_properties.put("_listbox$shallUpdateScrollPos", new BooleanPropertyAccess() {
public void setValue(Component cmp, Boolean shallUpdateScrollPos) {
((Listbox) cmp)._shallUpdateScrollPos = shallUpdateScrollPos;
}
public Boolean getValue(Component cmp) {
return ((Listbox) cmp)._shallUpdateScrollPos;
}
});
}
public PropertyAccess getPropertyAccess(String prop) {
PropertyAccess pa = _properties.get(prop);
if (pa != null)
return pa;
return super.getPropertyAccess(prop);
}
/**
* Processes an AU request.
*
* <p>
* Default: in addition to what are handled by {@link XulElement#service},
* it also handles onSelect.
*
* @since 5.0.0
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
final String cmd = request.getCommand();
if (cmd.equals(Events.ON_DATA_LOADING)) {
if (_rod) {
Executions.getCurrent().setAttribute("zkoss.zul.listbox.onDataLoading." + this.getUuid(), Boolean.TRUE); //indicate doing dataloading
}
Events.postEvent(DataLoadingEvent.getDataLoadingEvent(request, preloadSize()));
} else if (inPagingMold() && cmd.equals(ZulEvents.ON_PAGE_SIZE)) { //since 5.0.2
final Map<String, Object> data = request.getData();
final int oldsize = getPageSize();
int size = AuRequests.getInt(data, "size", oldsize);
if (size != oldsize) {
int begin = getActivePage() * oldsize;
int end = begin + oldsize;
end = Math.min(getPaginal().getTotalSize(), end);
int sel = getSelectedIndex();
if (sel < 0 || sel < begin || sel >= end) { //not in selection range
sel = size > oldsize ? (end - 1) : begin;
}
int newpg = sel / size;
setPageSize(size);
setActivePage(newpg);
// Bug: B50-3204965: onChangePageSize is not fired in autopaging scenario
Events.postEvent(new PageSizeEvent(cmd, this, pgi(), size));
}
} else if (cmd.equals(Events.ON_SCROLL_POS)) {
final Map<String, Object> data = request.getData();
_currentTop = AuRequests.getInt(data, "top", 0);
_currentLeft = AuRequests.getInt(data, "left", 0);
} else if (cmd.equals(Events.ON_ANCHOR_POS)) {
final Map<String, Object> data = request.getData();
_anchorTop = AuRequests.getInt(data, "top", 0);
_anchorLeft = AuRequests.getInt(data, "left", 0);
} else if (cmd.equals(Events.ON_TOP_PAD)) {
_topPad = AuRequests.getInt(request.getData(), "topPad", 0);
} else if (cmd.equals(Events.ON_SELECT)) {
if (_rod && Executions.getCurrent()
.getAttribute("zkoss.zul.listbox.onDataLoading." + this.getUuid()) != null) //indicate doing dataloading
return; //skip all onSelect event after the onDataLoading
Desktop desktop = request.getDesktop();
Map data = request.getData();
List<String> sitems = cast((List) data.get("items"));
boolean selectAll = Boolean.parseBoolean(data.get("selectAll") + "");
boolean paging = inPagingMold();
Set<Listitem> prevSeldItems = new LinkedHashSet<Listitem>(_selItems);
Set<Listitem> curSeldItems = AuRequests.convertToItems(desktop, sitems);
Set<Listitem> realPrevSeldItems = new LinkedHashSet<Listitem>(prevSeldItems);
Set<Object> prevSeldObjects = _model != null
? new LinkedHashSet<Object>(getSelectableModel().getSelection()) : new LinkedHashSet<Object>();
// fine tune with B50-ZK-547.
Selectable<Object> smodel = _model != null ? getSelectableModel() : null;
// ZK-2089: prevSeldItems should skip listgroup if listgroup is not selectable
if (!isListgroupSelectable() && prevSeldItems.size() > 0) {
// use toArray() to prevent java.util.ConcurrentModificationException
for (Object item : prevSeldItems.toArray()) {
if (item instanceof Listgroup) {
prevSeldItems.remove(item);
realPrevSeldItems.remove(item);
}
}
}
int from, to;
Paginal pgi = getPaginal();
if (pgi != null) {
int pgsz = pgi.getPageSize();
from = pgi.getActivePage() * pgsz;
to = from + pgsz; // excluded
} else {
from = 0;
to = 0;
}
// remove the selection in other page
if (paging && (!isCheckmarkDeselectOther() || selectAll)) {
// use toArray() to prevent java.util.ConcurrentModificationException
for (Object item : realPrevSeldItems.toArray()) {
int index = ((Listitem) item).getIndex();
if (index >= to || index < from)
realPrevSeldItems.remove(item);
}
}
if (curSeldItems == null)
curSeldItems = new HashSet<Listitem>(); //just in case
SelectionControl ctrl = smodel != null ? smodel.getSelectionControl() : null;
int start = -1;
int end = -1;
if (_rod) { // Bug: ZK-592
Map<String, Object> m = cast((Map) data.get("range"));
if (m != null) {
curSeldItems.addAll(_selItems); // keep other selected items.
start = AuRequests.getInt(m, "start", -1);
end = AuRequests.getInt(m, "end", -1);
for (Iterator<Listitem> it = _items.iterator(); it.hasNext();) {
Listitem item = it.next();
int index = item.getIndex();
if (index >= start && index <= end) {
// the same logic come from JS file (SelectWidget)
// for Bug: 2030986
if (!item.isDisabled() && item.isSelectable())
curSeldItems.add(item);
}
}
}
}
disableClientUpdate(true);
final boolean oldIDSE = _ignoreDataSelectionEvent;
_ignoreDataSelectionEvent = true;
try {
if (AuRequests.getBoolean(request.getData(), "clearFirst")) {
clearSelection();
if (_model != null)
((Selectable) _model).clearSelection();
}
if (!_multiple || (_model == null && !_rod && !paging && curSeldItems.size() <= 1)) {
//If _model, selItems is only a subset (so we can't optimize it)
final Listitem item = curSeldItems.size() > 0 ? curSeldItems.iterator().next() : null;
selectItem(item);
if (_model != null) {
final List<Object> selObjs = new ArrayList<Object>();
if (item != null) {
Object ele = _model.getElementAt(item.getIndex());
if (ctrl == null || ctrl.isSelectable(ele))
selObjs.add(ele);
}
getSelectableModel().setSelection(selObjs);
}
} else {
for (final Listitem item : curSeldItems) {
if (!_selItems.contains(item)) {
addItemToSelection(item);
if (smodel != null) { //still have to add selection if not multiple select
Object ele = _model.getElementAt(item.getIndex());
if (ctrl == null || ctrl.isSelectable(ele))
smodel.addToSelection(ele);
}
}
}
if (smodel != null) {
while (start >= 0 && end >= 0 && start <= end) {
//ZK-2804: add those items not in _items as selected
Object ele = _model.getElementAt(start++);
if (ctrl == null || ctrl.isSelectable(ele))
smodel.addToSelection(ele);
}
}
if (prevSeldItems != null && prevSeldItems.size() > 0) {
int firstItemIndex = _items.get(0).getIndex();
int lastItemIndex = _items.get(_items.size() - 1).getIndex();
final Map<String, Object> rodItemIndexRange = cast((Map) data.get("rodItemIndexRange"));
if (rodItemIndexRange != null) {
final Integer clientFirstItemIndex = (Integer) rodItemIndexRange.get("start");
if (clientFirstItemIndex != null) {
firstItemIndex = clientFirstItemIndex;
}
final Integer clientLastItemIndex = (Integer) rodItemIndexRange.get("end");
if (clientLastItemIndex != null) {
lastItemIndex = clientLastItemIndex;
}
}
for (final Listitem item : prevSeldItems) {
final int index = item.getIndex();
if (firstItemIndex <= index && index <= lastItemIndex // ZK-2658
&& !curSeldItems.contains(item)
&& (!paging || (from <= index && index < to))
) {
removeItemFromSelection(item);
if (smodel != null)
smodel.removeFromSelection(_model.getElementAt(index));
}
}
}
}
} finally {
_ignoreDataSelectionEvent = oldIDSE;
disableClientUpdate(false);
}
Set<Listitem> unselectedItems;
if (_model != null && paging) {
prevSeldItems = null;
unselectedItems = null;
} else {
unselectedItems = collectUnselectedObjects(realPrevSeldItems, curSeldItems);
}
Set<Object> unselectedObjects;
Set<Object> selectedObjects = new LinkedHashSet<Object>();
if (_model == null) {
prevSeldObjects = null;
unselectedObjects = null;
} else {
for (Listitem i : curSeldItems)
selectedObjects.add(_model.getElementAt(i.getIndex()));
unselectedObjects = collectUnselectedObjects(prevSeldObjects, smodel.getSelection());
}
if (ctrl != null) {
if (selectAll)
ctrl.setSelectAll(true);
}
if (sitems == null || sitems.isEmpty() || _model == null)
selectedObjects = null;
SelectEvent evt = new SelectEvent(Events.ON_SELECT, this, curSeldItems, prevSeldItems, unselectedItems,
selectedObjects, prevSeldObjects, unselectedObjects,
desktop.getComponentByUuidIfAny((String) data.get("reference")), null, AuRequests.parseKeys(data));
Events.postEvent(evt);
} else if (cmd.equals(Events.ON_INNER_WIDTH)) {
final String width = AuRequests.getInnerWidth(request);
_innerWidth = width == null ? "100%" : width;
} else if (cmd.equals(Events.ON_RENDER)) {
final Set<Listitem> items = AuRequests.convertToItems(request.getDesktop(), getRequestData(request));
int cnt = items.size();
if (cnt == 0)
return; // nothing to do
// B85-ZK-3572 get size using preloadSize()
int size = preloadSize();
cnt = size - cnt;
if (cnt > 0 && size > 0) { // Feature 1740072: pre-load
if (cnt > size)
cnt = size; // at most 8 more to load
// 1. locate the first item found in items
final List<Listitem> toload = new LinkedList<Listitem>();
Iterator<Listitem> it = _items.iterator();
while (it.hasNext()) {
final Listitem li = it.next();
if (items.contains(li)) // found
break;
if (!li.isLoaded())
toload.add(0, li); // reverse order
}
// 2. add unload items before the found one
if (!toload.isEmpty()) {
int bfcnt = cnt / 3;
for (Iterator<Listitem> e = toload.iterator(); bfcnt > 0 && e.hasNext(); --bfcnt, --cnt) {
items.add(e.next());
}
}
// 3. add unloaded after the found one
while (cnt > 0 && it.hasNext()) {
final Listitem li = it.next();
if (!li.isLoaded() && items.add(li))
--cnt;
}
}
Listbox.this.renderItems(items);
} else if (cmd.equals(Events.ON_ACROSS_PAGE)) { // F60-ZK-715
final Map<String, Object> data = request.getData();
int page = AuRequests.getInt(data, "page", 0);
int offset = AuRequests.getInt(data, "offset", 0);
int shift = AuRequests.getInt(data, "shift", 0);
int pageSize = getPageSize();
int index = page * pageSize + offset;
int from = shift < 0 ? index + shift : index;
int to = shift > 0 ? index + shift : index;
//Update UI
final int tsz = getItemCount();
final int toUI = Math.min(to, tsz - 1); // capped by size
if (!isMultiple() || shift == 0) {
// B65-ZK-1969 and B65-1715
if ((_model == null && index >= tsz) || (_model != null && index >= _model.getSize()))
index = tsz - 1;
setSelectedIndex(index);
setFocusIndex(offset < 0 ? pageSize - 1 : offset);
} else {
Set<Listitem> items = new HashSet<Listitem>();
for (int i = from; i <= toUI; i++)
items.add(_items.get(i));
setSelectedItems(items);
setActivePage(index / pageSize);
setFocusIndex(offset);
}
//Update Model
if (_model != null) {
final boolean oldIDSE = _ignoreDataSelectionEvent;
_ignoreDataSelectionEvent = true;
try {
to = Math.min(to, _model.getSize() - 1); // capped by size
final Selectable<Object> smodel = getSelectableModel();
if (!smodel.isMultiple() || shift == 0) {
if (!smodel.isMultiple())
from = to;
smodel.clearSelection();
}
while (from <= to)
smodel.addToSelection(_model.getElementAt(from++));
} finally {
_ignoreDataSelectionEvent = oldIDSE;
}
}
// B60-ZK-815: simulate onSelect event when going across page
SelectEvent<Listitem, Object> evt = new SelectEvent<Listitem, Object>("onSelect", this, getSelectedItems(),
getItemAtIndex(index), shift != 0 ? SelectEvent.SHIFT_KEY : 0);
Events.postEvent(evt);
// a way to notify Client MVVM
Events.postEvent(new Event(Events.ON_ACROSS_PAGE, this, data));
} else if (cmd.equals(Events.ON_CHECK_SELECT_ALL)) { // F65-ZK-2014
CheckEvent evt = CheckEvent.getCheckEvent(request);
if (_model != null) {
final Selectable<Object> selectableModel = getSelectableModel();
SelectionControl selectionControl = selectableModel.getSelectionControl();
if (selectionControl == null)
throw new IllegalStateException(
"SelectionControl cannot be null, please implement SelectionControl interface for SelectablModel");
selectionControl.setSelectAll(evt.isChecked());
}
Events.postEvent(evt);
} else if (cmd.equals("onUpdateSelectAll")) {
if (_model != null) {
final SelectionControl selectionControl = getSelectableModel().getSelectionControl();
if (selectionControl != null) {
Clients.response(new AuInvoke(this, "$doService", new Object[] { cmd, new JSONAware() {
public String toJSONString() {
return String.valueOf(selectionControl.isSelectAll());
}
} }));
}
}
} else
super.service(request, everError);
}
@SuppressWarnings("unchecked")
private List<String> getRequestData(org.zkoss.zk.au.AuRequest request) {
return (List<String>) request.getData().get("items");
}
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, Padding, Blockable {
// --Padding--//
public int getHeight() {
return _topPad;
}
public void setHeight(int height) {
_topPad = height;
}
// --Cropper--//
public boolean isCropper() {
return ((Cropper) getDataLoader()).isCropper();
}
public Component getCropOwner() {
return Listbox.this;
}
public Set<? extends Component> getAvailableAtClient() {
return ((Cropper) getDataLoader()).getAvailableAtClient();
}
public boolean shallBlock(AuRequest request) {
return isDisabled() || !Components.isRealVisible(Listbox.this);
}
}
/**
* An iterator used by _heads.
*/
private class Iter implements Iterator<Component> {
private final Iterator<Component> _it = getChildren().iterator();
private int _j;
public boolean hasNext() {
return _j < _hdcnt;
}
public Component next() {
final Component o = _it.next();
++_j;
return o;
}
public void remove() {
throw new UnsupportedOperationException();
}
}
/**
* An iterator used by _groups.
*/
private class IterGroups implements Iterator<Listgroup> {
private final Iterator<int[]> _it = _groupsInfo.iterator();
private int _j;
public boolean hasNext() {
return _j < getGroupCount();
}
public Listgroup next() {
final Listgroup o = (Listgroup) getItemAtIndex(_it.next()[0]);
++_j;
return o;
}
public void remove() {
throw new UnsupportedOperationException();
}
}
@Override
public void setPageSize(int pgsz) throws WrongValueException {
// Bug ZK-1696: model also preserves paging information
if (_model instanceof Pageable) {
((Pageable) _model).setPageSize(pgsz);
}
super.setPageSize(pgsz);
}
public void onAfterRender() {
if (inPagingMold() && _model instanceof Pageable) {
Pageable m = (Pageable) _model;
if (m.getPageSize() > 0) { //min page size is 1
_pgi.setPageSize(m.getPageSize());
} else {
m.setPageSize(_pgi.getPageSize());
}
_pgi.setTotalSize(getDataLoader().getTotalSize());
if (m.getActivePage() >= 0) { //min page index is 0
_pgi.setActivePage(m.getActivePage());
} else {
m.setActivePage(_pgi.getActivePage());
}
}
}
/**
* Scroll to the specified item by the given index.
* @param index the index of item
* @since 8.5.2
*/
public void scrollToIndex(int index) {
ListModel<Object> model = getModel();
int itemCount = model != null ? model.getSize() : getItemCount();
if (index < 0 || index > itemCount - 1) {
throw new IndexOutOfBoundsException("Illegal index: " + index);
}
response(new AuInvoke(this, "scrollToIndex", index, (double) index / itemCount));
}
/**
* Sets whether to update the scroll position on init
* <p>Default: false.
* <p>Note: internal use only
* @param shallUpdateScrollPos whether update the scroll position on init
* @since 8.6.0
*/
public void shallUpdateScrollPos(boolean shallUpdateScrollPos) {
_shallUpdateScrollPos = shallUpdateScrollPos;
}
}