zul/src/main/java/org/zkoss/zul/Tree.java
/* Tree.java
Purpose:
Description:
History:
Wed Jul 6 18:51:33 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.io.IOException;
import java.io.Writer;
import java.lang.reflect.Method;
import java.util.AbstractCollection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
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.Set;
import java.util.Vector;
import java.util.stream.Collectors;
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.xel.VariableResolver;
import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.au.AuRequests;
import org.zkoss.zk.au.out.AuInvoke;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WebApps;
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.sys.IntegerPropertyAccess;
import org.zkoss.zk.ui.sys.PropertyAccess;
import org.zkoss.zk.ui.sys.ShadowElementsCtrl;
import org.zkoss.zk.ui.util.Clients;
import org.zkoss.zk.ui.util.ComponentCloneListener;
import org.zkoss.zk.ui.util.ForEachStatus;
import org.zkoss.zk.ui.util.Template;
import org.zkoss.zul.event.PageSizeEvent;
import org.zkoss.zul.event.PagingEvent;
import org.zkoss.zul.event.PagingListener;
import org.zkoss.zul.event.RenderEvent;
import org.zkoss.zul.event.TreeDataEvent;
import org.zkoss.zul.event.TreeDataListener;
import org.zkoss.zul.event.ZulEvents;
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.ext.TreeOpenableModel;
import org.zkoss.zul.ext.TreeSelectableModel;
import org.zkoss.zul.ext.TristateModel;
import org.zkoss.zul.impl.MeshElement;
import org.zkoss.zul.impl.Utils;
import org.zkoss.zul.impl.XulElement;
/**
* A container which can be used to hold a tabular
* or hierarchical set of rows of elements.
* <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>
* </ol>
*
* <p>Default {@link #getZclass}: z-tree. (since 3.5.0)
*
* <p>Custom Attributes:
* <dl>
* <dt>org.zkoss.zul.tree.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.tree.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 Treecol#setSortDirection} is set.</li>
* <li>{@link Treecol#setSortDirection} is called.</li>
* <li>Model receives {@link TreeDataEvent} and {@link Treecol#setSortDirection} is set.</li>
* </ol>
* If you want to ignore sort when receiving {@link TreeDataEvent},
* 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>
*
* <br/>
* [Since 6.0.0]
* <p>To retrieve what are selected in Tree with a {@link TreeSelectableModel},
* you shall use {@link TreeSelectableModel#isPathSelected(int[])}
* to check whether the current path is selected in {@link TreeSelectableModel}
* rather than using {@link Tree#getSelectedItems()}. That is, you shall operate on
* the item of the {@link TreeSelectableModel} rather than on the {@link Treeitem}
* of the {@link Tree} if you use the {@link TreeSelectableModel} and {@link TreeModel}.</p>
*
* <pre>{@code
* TreeSelectableModel selModel = ((TreeSelectableModel)getModel());
* int[][] paths = selModel.getSelectionPaths();
* List<E> selected = new ArrayList<E>();
* AbstractTreeModel model = (AbstractTreeModel) selModel;
* for (int i = 0; i < paths.length; i++) {
* selected.add(model.getChild(paths[i]));
* }
* }</pre>
*
* <br/>
* [Since 6.0.0]
* <p> If the TreeModel in Tree implements a {@link TreeSelectableModel}, the
* multiple selections status is applied from the method of
* {@link TreeSelectableModel#isMultiple()}
* </p>
* <pre><code>
* DefaultTreeModel selModel = new DefaultTreeModel(treeNode);
* selModel.setMultiple(true);
* tree.setModel(selModel);
* </code></pre>
*
* <br/>
* [Since 6.0.0]
* <p>To retrieve what are opened nodes in Tree with a {@link TreeOpenableModel},
* you shall use {@link TreeOpenableModel#isPathOpened(int[])}
* to check whether the current path is opened in {@link TreeOpenableModel}
* rather than using {@link Treeitem#isOpen()}. That is, you shall operate on
* the item of the {@link TreeOpenableModel} rather than on the {@link Treeitem}
* of the {@link Tree} if you use the {@link TreeOpenableModel} and {@link TreeModel}.</p>
*
* <pre>{@code
* TreeOpenableModel openModel = ((TreeOpenableModel)getModel());
* int[][] paths = openModel.getOpenPaths();
* List<E> opened = new ArrayList<E>();
* AbstractTreeModel model = (AbstractTreeModel) openModel;
* for (int i = 0; i < paths.length; i++) {
* opened.add(model.getChild(paths[i]));
* }
* }</pre>
*
* <dt>org.zkoss.zul.tree.selectOnHighlight.disabled</dt>.(since 7.0.4)
* <dd>Sets whether to disable select functionality when highlighting text
* content with mouse dragging or not.</dd>
*
* <br/>
* [Since 7.0.0] (EE version only)
*
* <dt>org.zkoss.zul.tree.initRodSize</dt>.
* <dd>Specifies the number of items rendered when the Tree first render.
*
* <dt>org.zkoss.zul.tree.maxRodPageSize</dt>.
* <dd>Specifies how many pages (of treeitems) to keep rendered in memory
* (on the server side) when navigating the tree using pagination. (Paging mold only)
*
* <dt>org.zkoss.zul.tree.preloadSize</dt>.
* <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(TreeModel)} and
* not paging ({@link #getPagingChild}).</dd>
*
*
* @author tomyeh
*/
@SuppressWarnings("serial")
public class Tree extends MeshElement {
private static final Logger log = LoggerFactory.getLogger(Tree.class);
private static final String ATTR_ON_INIT_RENDER_POSTED = "org.zkoss.zul.Tree.onInitLaterPosted";
public static final int DEFAULT_THROTTLE_MILLIS = 300;
private transient Treecols _treecols;
private transient Treefoot _treefoot;
private transient Frozen _frozen;
private transient Treechildren _treechildren;
/** A list of selected items. */
private transient Set<Treeitem> _selItems;
/** The first selected item. */
private transient Treeitem _sel;
private transient Collection<Component> _heads;
private int _rows = 0;
/** The name. */
private String _name;
private boolean _multiple, _checkmark;
private boolean _vflex;
private String _innerWidth = "100%";
private transient TreeModel<Object> _model;
private transient TreeitemRenderer<?> _renderer;
private transient TreeDataListener _dataListener;
private transient Paginal _pgi;
private String _nonselTags; //since 5.0.5 for non-selectable tags
/** 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;
private int _currentTop = 0; // since 5.0.8 scroll position
private int _currentLeft = 0;
private int _anchorTop = 0; //since 5.0.11/6.0.0 anchor position
private int _anchorLeft = 0;
private static final int INIT_LIMIT = -1; // since 7.0.0
private int _preloadsz = 50; // since 7.0.0
private transient LinkedList<Integer> _rodPagingIndex; // since 7.0.0
static {
addClientEvent(Tree.class, Events.ON_RENDER, CE_DUPLICATE_IGNORE | CE_IMPORTANT | CE_NON_DEFERRABLE);
addClientEvent(Tree.class, Events.ON_INNER_WIDTH, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
addClientEvent(Tree.class, Events.ON_SELECT, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
addClientEvent(Tree.class, Events.ON_FOCUS, CE_DUPLICATE_IGNORE);
addClientEvent(Tree.class, Events.ON_BLUR, CE_DUPLICATE_IGNORE);
addClientEvent(Tree.class, ZulEvents.ON_PAGE_SIZE, CE_DUPLICATE_IGNORE | CE_IMPORTANT | CE_NON_DEFERRABLE); //since 5.0.2
addClientEvent(Tree.class, Events.ON_SCROLL_POS, CE_DUPLICATE_IGNORE | CE_IMPORTANT); //since 5.0.4
addClientEvent(Tree.class, Events.ON_ANCHOR_POS, CE_DUPLICATE_IGNORE | CE_IMPORTANT); //since 5.0.11 / 6.0.0
addClientEvent(Tree.class, Events.ON_CHECK_SELECT_ALL, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
}
public Tree() {
init();
}
private void init() {
_selItems = new LinkedHashSet<Treeitem>(4);
_heads = new AbstractCollection<Component>() {
public int size() {
int sz = getChildren().size();
if (_treechildren != null)
--sz;
if (_treefoot != null)
--sz;
if (_paging != null)
--sz;
if (_frozen != null)
--sz;
return sz;
}
public Iterator<Component> iterator() {
return new Iter();
}
};
}
public void onPageAttached(Page newpage, Page oldpage) {
super.onPageAttached(newpage, oldpage);
if (oldpage == null) {
//prepare a right moment to init Tree(must be as early as possible)
this.addEventListener("onInitModel", _modelInitListener = new ModelInitListener());
Events.postEvent(20000, new Event("onInitModel", this)); //first event to be called
}
if (_model != null) {
postOnInitRender();
if (_dataListener != null) {
_model.removeTreeDataListener(_dataListener);
_model.addTreeDataListener(_dataListener);
}
}
if (_model instanceof PageableModel && _pgListener != null) {
((PageableModel) _model).removePagingEventListener((PagingListener) _pgListener);
((PageableModel) _model).addPagingEventListener((PagingListener) _pgListener);
}
}
public void onPageDetached(Page page) {
super.onPageDetached(page);
if (_model != null && _dataListener != null)
_model.removeTreeDataListener(_dataListener);
if (_model instanceof PageableModel && _pgListener != null)
((PageableModel) _model).removePagingEventListener((PagingListener) _pgListener);
}
private class ModelInitListener implements SerializableEventListener<Event>, CloneableEventListener<Event> {
public void onEvent(Event event) throws Exception {
if (_modelInitListener != null) {
Tree.this.removeEventListener("onInitModel", _modelInitListener);
_modelInitListener = null;
}
if (_model != null) { //rows not created yet
//ZK-1007 Left the job to onInitRenderer if exist
if (getAttribute(ATTR_ON_INIT_RENDER_POSTED) == null) {
if (_treechildren == null) {
renderTree();
} else {
setModel(_model);
}
}
}
}
public Object willClone(Component comp) {
return null; // skip to clone
}
}
void addVisibleItemCount(int count) {
if (inPagingMold()) {
Paginal pgi = getPaginal();
int totalSize = pgi.getTotalSize() + count;
//ZK-3173: if visible item count reduces, active page might exceed max page count
if (count < 0 && _model instanceof Pageable) {
Pageable p = (Pageable) _model;
int actpg = p.getActivePage();
if (actpg > 0) {
int maxPageIndex = p.getPageCount() - 1;
if (actpg > maxPageIndex) {
p.setActivePage(maxPageIndex);
}
}
}
pgi.setTotalSize(totalSize);
invalidate(); //the set of visible items might change
}
}
/**
* Returns a map of current visible item.
* @since 3.0.7
*/
Map<Treeitem, Boolean> getVisibleItems() {
Map<Treeitem, Boolean> map = new HashMap<Treeitem, Boolean>();
final Paginal pgi = getPaginal();
final int pgsz = pgi.getPageSize();
final int ofs = pgi.getActivePage() * pgsz;
// data[pageSize, beginPageIndex, visitedCount, visitedTotal, RenderedCount]
int[] data = new int[] { pgsz, ofs, 0, 0, 0 };
getVisibleItemsDFS(getChildren(), map, data);
return map;
}
/**
* Prepare the map of the visible items recursively in deep-first order.
*/
private <T extends Component> boolean getVisibleItemsDFS(List<T> list, Map<Treeitem, Boolean> map, int[] data) {
for (T cmp : list) {
if (cmp instanceof Treeitem) {
if (data[4] >= data[0])
return false; // full
final Treeitem item = (Treeitem) cmp;
if (item.isRealVisible()) {
int count = item.isOpen() && item.getTreechildren() != null
? item.getTreechildren().getVisibleItemCount() : 0;
boolean shoulbBeVisited = data[1] < data[2] + 1 + count;
data[2] += (shoulbBeVisited ? 1 : count + 1);
data[3] += count + 1;
if (shoulbBeVisited) {
if (data[1] < data[2]) {
// count the rendered item
data[4]++;
map.put(item, Boolean.TRUE);
}
if (item.isOpen()) {
if (!getVisibleItemsDFS(item.getChildren(), map, data)) {
return false;
} else {
// the children may be visible.
map.put(item, Boolean.TRUE);
}
}
}
}
} else if (cmp instanceof Treechildren) {
if (!getVisibleItemsDFS(cmp.getChildren(), map, data))
return false;
}
}
return true;
}
/** Sets the mold to render this component.
*
* @param mold the mold. If null or empty, "default" is assumed.
* Allowed values: default, paging
* @see org.zkoss.zk.ui.metainfo.ComponentDefinition
*/
//-- super --//
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);
}
invalidate(); //paging mold -> non-paging mold
} else if (inPagingMold()) { //change to paging
if (_pgi != null)
addPagingListener(_pgi);
else
newInternalPaging();
setSizedByContent(false);
resetPosition(true); //non-paging mold -> paging mold
if (_model instanceof Pageable) {
Pageable m = (Pageable) _model;
if (m.getPageSize() > 0) { //make sure value is valid, min page size is 1
_pgi.setPageSize(m.getPageSize());
} else {
m.setPageSize(_pgi.getPageSize());
}
if (m.getActivePage() >= 0) { //min page index is 0
_pgi.setActivePage(m.getActivePage());
} else {
m.setActivePage(_pgi.getActivePage());
}
}
}
}
}
//--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 tree will rely on the paging controller to handle long-content
* instead of scrolling.
* @since 3.0.7
*/
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}).
* @since 3.0.7
*/
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(getItemCount());
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(getVisibleItemCount());
//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);
}
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(pe.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(), Tree.this, pe.getPageable(), actpg));
}
}
public Object willClone(Component comp) {
return null; // skip to clone
}
}
private class PGImpListener implements PagingListener {
public void onEvent(Event event) {
if (inPagingMold() && event instanceof PagingEvent) {
PagingEvent pe = (PagingEvent) event;
if (_model instanceof Pageable) {
((Pageable) _model).setPageSize(pe.getPageable().getPageSize());
((Pageable) _model).setActivePage(pe.getPageable().getActivePage());
}
if (WebApps.getFeature("ee") && getModel() != null) {
if (_rodPagingIndex == null)
_rodPagingIndex = new LinkedList<Integer>();
int ap = pe.getActivePage();
int size = pe.getPageable().getPageSize();
int mps = maxRodPageSize();
// if mps is less than 0, we don't store the index.
if (mps >= 0 && !_rodPagingIndex.contains(ap)) {
_rodPagingIndex.add(ap);
}
if (mps >= 1 && mps < _rodPagingIndex.size()) {
LinkedList<Integer> sortedIndex = new LinkedList<Integer>();
mps = _rodPagingIndex.size() - mps;
while (mps-- > 0) {
sortedIndex.add(_rodPagingIndex.removeFirst());
}
Collections.sort(sortedIndex);
int i = 0;
int start = sortedIndex.removeFirst() * size;
int end = start + size;
for (Treeitem ti : new ArrayList<Treeitem>(Tree.this.getItems())) {
if (i < start) {
i++;
continue;
}
if (i >= end) {
if (sortedIndex.isEmpty()) {
break;
} else {
start = sortedIndex.removeFirst() * size;
end = start + size;
}
}
if (!ti.isOpen() && ti.getDesktop() != null) {
ti.getChildren().clear();
ti.setRendered(false);
ti.setLoaded(false);
}
i++;
}
}
int start = ap * size;
int end = start + size;
int i = 0;
final Renderer renderer = new Renderer();
try {
for (Treeitem ti : new ArrayList<Treeitem>(Tree.this.getItems())) {
if (i < start) {
i++;
continue;
}
if (i >= end) {
break;
}
if (!ti.isRendered()) {
ti.getChildren().clear();
Treechildren parent = (Treechildren) ti.getParent();
int[] treeitemPath = Tree.this.getTreeitemPath(Tree.this, ti);
Object childNode = _model.getChild(treeitemPath);
renderChildren0(renderer, parent, ti, childNode, treeitemPath[treeitemPath.length - 1]);
}
i++;
}
} catch (Throwable ex) {
renderer.doCatch(ex);
} finally {
renderer.doFinally();
}
}
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 (_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;
}
/** Returns the page size, a.k.a., the number items per page.
* @exception IllegalStateException if {@link #getPaginal} returns null,
* i.e., mold is not "paging" and no external controller is specified.
* @since 2.4.1
*/
public int getPageSize() {
return inPagingMold() ? pgi().getPageSize() : 0;
}
/** Sets the page size, a.k.a., the number items per page.
* <p>Note: mold is "paging" and no external controller is specified.
* @since 2.4.1
*/
public void setPageSize(int pgsz) throws WrongValueException {
if (pgsz < 0 || !inPagingMold())
return;
pgi().setPageSize(pgsz);
// Bug ZK-1696: model need to preserve paging information, too
if (_model instanceof Pageable) {
((Pageable) _model).setPageSize(pgsz);
}
}
protected Paginal pgi() {
if (_pgi == null)
throw new IllegalStateException("Available only the paging mold");
return _pgi;
}
/** Returns whether this tree is in the paging mold.
* @since 3.0.7
*/
/*package*/ boolean inPagingMold() {
return "paging".equals(getMold());
}
private int getVisibleItemCount() {
return _treechildren != null ? _treechildren.getVisibleItemCount() : 0;
}
/** Returns the treecols that this tree owns (might null).
*/
public Treecols getTreecols() {
return _treecols;
}
/** Returns the treefoot that this tree owns (might null).
*/
public Treefoot getTreefoot() {
return _treefoot;
}
/**
* Returns the frozen child.
* @since 7.0.0
*/
public Frozen getFrozen() {
return _frozen;
}
/** Returns the treechildren that this tree owns (might null).
*/
public Treechildren getTreechildren() {
return _treechildren;
}
/** Returns a collection of heads, including {@link #getTreecols}
* and auxiliary heads ({@link Auxhead}) (never null).
*
* @since 3.0.0
*/
public Collection<Component> getHeads() {
return _heads;
}
/** 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 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 tree 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 tree 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 tree item
* being selected if they are clicked.
* <p>Refer to {@link #setNonselectableTags} for details.
* @since 5.0.5
*/
public String getNonselectableTags() {
return _nonselTags;
}
/** 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);
}
}
/** 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() {
return _vflex;
}
/** 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 (_vflex != vflex) {
_vflex = vflex;
smartUpdate("vflex", _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);
}
/**
* 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 the seltype.
* <p>Default: "single".
*/
public String getSeltype() {
return _multiple ? "multiple" : "single";
}
/** Sets the seltype.
* "single","multiple" is supported.
*/
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 TreeSelectableModel#setMultiple}).
*/
public void setMultiple(boolean multiple) {
if (_multiple != multiple) {
_multiple = multiple;
if (!_multiple && _selItems.size() > 1) {
final Treeitem item = getSelectedItem();
for (Iterator<Treeitem> it = _selItems.iterator(); it.hasNext();) {
final Treeitem ti = it.next();
if (ti != item) {
ti.setSelectedDirectly(false);
it.remove();
}
}
//No need to update selId because z.multiple will do the job
}
if (_model != null)
((TreeSelectableModel) _model).setMultiple(multiple);
smartUpdate("multiple", _multiple);
}
}
/** 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, invisible, or doesn't belong
* to the same tree, nothing happens.
* @since 3.0.4
*/
public void setActivePage(Treeitem item) {
if (item.isRealVisible() && item.getTree() == this && isVisible()) {
int index = getVisibleIndexOfItem(item);
if (index != -1) {
final Paginal pgi = getPaginal();
int pg = index / pgi.getPageSize();
if (pg != getActivePage())
setActivePage(pg);
}
}
}
@Override
public void setActivePage(int pg) throws WrongValueException {
// Bug ZK-1696: model need to preserve paging information, too
if (_model instanceof Pageable) {
((Pageable) _model).setActivePage(pg);
}
super.setActivePage(pg);
}
/**
* Returns the index of the specified item in which it should be shown on the
* paging mold recursively in breadth-first order.
* @return -1 if the item is invisible.
* @since 3.0.7
*/
private int getVisibleIndexOfItem(Treeitem item) {
int count = getVisibleIndexOfItem0(item, false);
if (count <= 0)
return -1;
return --count;
}
/**
* Returns the count the specified item in which it should be shown on the
* paging mold recursively in breadth-first order.
* @since 3.0.7
*/
private int getVisibleIndexOfItem0(Treeitem item, boolean inclusive) {
// ZK-2539: use vector instead of recursive calls to avoid stack overflow
// when number of tree items is huge.
Vector<Treeitem> items = new Vector<Treeitem>();
int count = 0;
items.add(item);
while (!items.isEmpty()) {
item = items.remove(0);
if (item == null) {
continue;
} else if (item.isRealVisible()) {
count++;
Treechildren chdrn = item.getTreechildren();
if (inclusive && item.isOpen() && chdrn != null) {
count += chdrn.getVisibleItemCount();
}
}
Component prev = item.getPreviousSibling();
if (prev != null && prev instanceof Treeitem) {
items.add(0, (Treeitem) prev);
inclusive = true;
} else { // go up a level when there's no more prev sibling
Component cmp = item.getParent().getParent();
if (cmp instanceof Treeitem) {
Treeitem parent = (Treeitem) cmp;
if (parent.isRealVisible()) {
parent.setOpen(true);
items.add(0, parent);
inclusive = false;
} else {
count--;
}
}
}
}
return count;
}
// used by Treechildren
public void smartUpdate(String attr, Object value) {
super.smartUpdate(attr, value);
}
/** Returns a readonly list of all descending {@link Treeitem}
* (children's children and so on).
*
* <p>Note: the performance of the size method of returned collection
* is no good.
*/
public Collection<Treeitem> getItems() {
if (_treechildren != null)
return _treechildren.getItems();
return Collections.emptyList();
}
/** Returns the number of child {@link Treeitem}.
* The same as {@link #getItems}.size().
* <p>Note: the performance of this method is no good.
*/
public int getItemCount() {
return _treechildren != null ? _treechildren.getItemCount() : 0;
}
/** 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(Treeitem item) {
if (item == null) {
clearSelection();
} else {
if (item.getTree() != this)
throw new UiException("Not a child: " + item);
if (_sel != item || (_multiple && _selItems.size() > 1)) {
for (Treeitem ti : _selItems)
ti.setSelectedDirectly(false);
_selItems.clear();
_sel = item;
item.setSelectedDirectly(true);
_selItems.add(item);
smartUpdate("selectedItem", item);
}
if (inPagingMold())
setActivePage(item);
}
}
/** Selects the given item, without deselecting any other items
* that are already selected..
*/
public void addItemToSelection(Treeitem item) {
if (item.getTree() != this)
throw new UiException("Not a child: " + item);
if (!item.isSelected()) {
if (!_multiple) {
selectItem(item);
} else {
item.setSelectedDirectly(true);
_selItems.add(item);
if (_sel == null)
_sel = _selItems.iterator().next();
smartUpdateSelection();
}
}
}
/** Deselects the given item without deselecting other items.
*/
public void removeItemFromSelection(Treeitem item) {
if (item.getTree() != this)
throw new UiException("Not a child: " + item);
if (item.isSelected()) {
if (!_multiple) {
clearSelection();
} else {
item.setSelectedDirectly(false);
_selItems.remove(item);
if (_sel == item) //bug fix:3131173
_sel = _selItems.size() > 0 ? _selItems.iterator().next() : null;
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 (Treeitem 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 tree
* that are selected are not affected, and retain their selected state.
*/
public void toggleItemSelection(Treeitem item) {
if (item.isSelected())
removeItemFromSelection(item);
else
addItemToSelection(item);
}
/** Clears the selection.
*/
public void clearSelection() {
if (!_selItems.isEmpty()) {
for (Treeitem item : _selItems)
item.setSelectedDirectly(false);
_selItems.clear();
_sel = null;
smartUpdate("selectedItem", null);
}
}
/** Selects all items.
*/
public void selectAll() {
if (!_multiple)
throw new UiException("Appliable only to the multiple seltype: " + this);
//we don't invoke getItemCount first because it is slow!
boolean first = true;
for (Treeitem item : getItems()) {
if (!item.isSelected()) {
_selItems.add(item);
item.setSelectedDirectly(true);
}
if (first) {
_sel = item;
first = false;
}
}
smartUpdate("selectAll", true);
}
/** Returns the selected item.
*/
public Treeitem getSelectedItem() {
return _sel;
}
/** Deselects all of the currently selected items and selects
* the given item.
* <p>It is the same as {@link #selectItem}.
*/
public void setSelectedItem(Treeitem item) {
selectItem(item);
}
/** Returns all selected items.
*/
public Set<Treeitem> getSelectedItems() {
return Collections.unmodifiableSet(_selItems);
}
/** Returns the number of items being selected.
*/
public int getSelectedCount() {
return _selItems.size();
}
/** Clears all child tree items ({@link Treeitem}.
* <p>Note: after clear, {@link #getTreechildren} won't be null, but
* it has no child
*/
public void clear() {
if (_treechildren != null)
_treechildren.getChildren().clear();
}
//-- Component --//
public String getZclass() {
return _zclass == null ? "z-tree" : _zclass;
}
public void beforeChildAdded(Component newChild, Component refChild) {
if (newChild instanceof Treecols) {
if (_treecols != null && _treecols != newChild)
throw new UiException("Only one treecols is allowed: " + this);
} else if (newChild instanceof Treefoot) {
if (_treefoot != null && _treefoot != newChild)
throw new UiException("Only one treefoot is allowed: " + this);
} else if (newChild instanceof Frozen) {
if (_frozen != null && _frozen != newChild)
throw new UiException("Only one frozen child is allowed: " + this);
} else if (newChild instanceof Treechildren) {
if (_treechildren != null && _treechildren != newChild)
throw new UiException("Only one treechildren is allowed: " + this);
} 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 newChild: " + newChild);
}
super.beforeChildAdded(newChild, refChild);
}
public boolean insertBefore(Component newChild, Component refChild) {
if (newChild instanceof Treecols) {
if (super.insertBefore(newChild, refChild)) {
_treecols = (Treecols) newChild;
return true;
}
} else if (newChild instanceof Treefoot) {
refChild = _paging; //the last two: listfoot and paging
if (super.insertBefore(newChild, refChild)) {
_treefoot = (Treefoot) newChild;
return true;
}
} else if (newChild instanceof Frozen) {
if (super.insertBefore(newChild, refChild)) {
_frozen = (Frozen) newChild;
return true;
}
} else if (newChild instanceof Treechildren) {
if (super.insertBefore(newChild, refChild)) {
_treechildren = (Treechildren) newChild;
fixSelectedSet();
return true;
}
} else if (newChild instanceof Paging) {
refChild = null; //the last: paging
if (super.insertBefore(newChild, refChild)) {
_pgi = _paging = (Paging) newChild;
return true;
}
} else { //Auxhead
return super.insertBefore(newChild, refChild);
}
return false;
}
/** Called by {@link Treeitem} when is added to a tree. */
/*package*/ void onTreeitemAdded(Treeitem item) {
fixNewChild(item);
onTreechildrenAdded(item.getTreechildren());
}
/** Called by {@link Treeitem} when is removed from a tree. */
/*package*/ void onTreeitemRemoved(Treeitem item) {
boolean fixSel = false;
if (item.isSelected()) {
_selItems.remove(item);
fixSel = _sel == item;
if (fixSel && !_multiple)
_sel = null;
}
onTreechildrenRemoved(item.getTreechildren());
if (fixSel)
fixSelected();
}
/** Called by {@link Treechildren} when is added to a tree. */
/*package*/ void onTreechildrenAdded(Treechildren tchs) {
if (tchs == null || tchs.getParent() == this)
return; //already being processed by insertBefore
//main the selected status
for (Treeitem item : tchs.getItems())
fixNewChild(item);
}
/** Fixes the status of new added child. */
private void fixNewChild(Treeitem item) {
if (item.isSelected()) {
if (_sel != null && !_multiple) {
item.setSelectedDirectly(false);
item.invalidate();
} else {
if (_sel == null)
_sel = item;
_selItems.add(item);
}
}
}
/** Called by {@link Treechildren} when is removed from a tree. */
/*package*/ void onTreechildrenRemoved(Treechildren tchs) {
if (tchs == null || tchs.getParent() == this)
return; //already being processed by onChildRemoved
//main the selected status
boolean fixSel = false;
for (Treeitem item : tchs.getItems()) {
if (item.isSelected()) {
_selItems.remove(item);
if (_sel == item) {
if (!_multiple) {
_sel = null;
return; //done
}
fixSel = true;
}
}
}
if (fixSel)
fixSelected();
}
public void onChildAdded(Component child) {
super.onChildAdded(child);
if (child instanceof Treechildren)
addVisibleItemCount(((Treechildren) child).getVisibleItemCount());
}
public void onChildRemoved(Component child) {
if (child instanceof Treecols) {
_treecols = null;
} else if (child instanceof Treefoot) {
_treefoot = null;
} else if (child instanceof Treechildren) {
_treechildren = null;
_selItems.clear();
_sel = null;
addVisibleItemCount(-((Treechildren) child).getVisibleItemCount());
} else if (_paging == child) {
_paging = null;
if (_pgi == child)
_pgi = null;
}
super.onChildRemoved(child);
}
/** Fixes all info about the selected status. */
private void fixSelectedSet() {
_sel = null;
_selItems.clear();
for (Treeitem item : getItems()) {
if (item.isSelected()) {
if (_sel == null) {
_sel = item;
} else if (!_multiple) {
item.setSelectedDirectly(false);
continue;
}
_selItems.add(item);
}
}
}
/** Make _sel to be the first selected item. */
private boolean fixSelected() {
Treeitem sel = null;
switch (_selItems.size()) {
case 1:
sel = _selItems.iterator().next();
case 0:
break;
default:
for (Treeitem item : getItems()) {
if (item.isSelected()) {
sel = item;
break;
}
}
}
if (sel != _sel) {
_sel = sel;
return true;
}
return false;
}
//Cloneable//
@SuppressWarnings("unchecked")
public Object clone() {
int cntSel = _selItems.size();
final Tree clone = (Tree) super.clone();
clone.init();
// remove cached listeners
clone._pgListener = null;
clone._pgImpListener = null;
int cnt = 0;
if (_treecols != null)
++cnt;
if (_treefoot != null)
++cnt;
if (_frozen != null)
++cnt;
if (_treechildren != null)
++cnt;
if (_paging != null)
++cnt;
if (cnt > 0 || cntSel > 0)
clone.afterUnmarshal(cnt, cntSel);
if (clone._model != null) {
if (clone._model instanceof ComponentCloneListener) {
final TreeModel<Object> model = (TreeModel<Object>) ((ComponentCloneListener) clone._model)
.willClone(clone);
if (model != null)
clone._model = model;
}
clone._dataListener = null;
clone.initDataListener();
// As the bug in 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);
}
return clone;
}
/** @param cnt # of children that need special handling (used for optimization).
* -1 means process all of them
* @param cntSel # of selected items
*/
private void afterUnmarshal(int cnt, int cntSel) {
if (cnt != 0) {
for (Component child : getChildren()) {
if (child instanceof Treecols) {
_treecols = (Treecols) child;
if (--cnt == 0)
break;
} else if (child instanceof Frozen) {
_frozen = (Frozen) child;
if (--cnt == 0)
break;
} else if (child instanceof Treefoot) {
_treefoot = (Treefoot) child;
if (--cnt == 0)
break;
} else if (child instanceof Treechildren) {
_treechildren = (Treechildren) child;
if (--cnt == 0)
break;
} else if (child instanceof Paging) {
_pgi = _paging = (Paging) child;
addPagingListener(_pgi);
if (--cnt == 0)
break;
}
}
}
_sel = null;
_selItems.clear();
if (cntSel != 0) {
for (Treeitem ti : getItems()) {
if (ti.isSelected()) {
if (_sel == null)
_sel = ti;
_selItems.add(ti);
if (--cntSel == 0)
break;
}
}
}
}
//-- Serializable --//
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);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
_model = (TreeModel) s.readObject();
didDeserialize(_model);
_renderer = (TreeitemRenderer) s.readObject();
didDeserialize(_renderer);
init();
afterUnmarshal(-1, -1);
if (_model != null)
initDataListener();
}
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 --//
/**
* Handles when the tree model's content changed
*/
private void onTreeDataChange(TreeDataEvent event) {
final int type = event.getType();
final int[] path = event.getPath();
final Component target = path != null ? getChildByPath(path) : null;
switch (type) {
case TreeDataEvent.STRUCTURE_CHANGED:
renderTree();
if (_model instanceof Sortable) {
final Sortable<Object> smodel = cast(_model);
final List<Treecol> cols = cast(_treecols.getChildren());
boolean found = false;
for (final Treecol col : cols) {
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);
}
}
}
return;
case TreeDataEvent.SELECTION_CHANGED:
if (target instanceof Treeitem) {
((Treeitem) target).setSelected(((TreeSelectableModel) _model).isPathSelected(path));
if (_model instanceof TristateModel)
smartUpdateTristate();
}
return;
case TreeDataEvent.TRISTATE_CHANGED:
if (_model instanceof TristateModel)
smartUpdateTristate();
return;
case TreeDataEvent.OPEN_CHANGED:
if (_model instanceof TreeOpenableModel) {
if (target instanceof Treeitem)
((Treeitem) target).setOpen(((TreeOpenableModel) _model).isPathOpened(path));
}
return;
case TreeDataEvent.MULTIPLE_CHANGED:
setMultiple(((TreeSelectableModel) _model).isMultiple());
return;
}
/*
* Loop through indexes array
* if INTERVAL_REMOVED, from end to beginning
*
* 2008/02/12 --- issue: [ 1884112 ]
* When getChildByNode returns null, do nothing
*/
if (target != null) {
Object node = _model.getChild(path);
int indexFrom = event.getIndexFrom();
int indexTo = event.getIndexTo();
if ((type == TreeDataEvent.INTERVAL_ADDED || type == TreeDataEvent.CONTENTS_CHANGED)
&& !isIgnoreSortWhenChanged()) {
doSort(this);
}
switch (type) {
case TreeDataEvent.INTERVAL_ADDED:
for (int i = indexFrom; i <= indexTo; i++)
onTreeDataInsert(target, node, i);
// Fix ZK-5468: the content of the subsequence item might be changed
for (int i = indexTo + 1, endSize = _model.getChildCount(node);
i < endSize; i++) {
onTreeDataContentChange(target, node, i);
}
break;
case TreeDataEvent.INTERVAL_REMOVED:
for (int i = indexTo; i >= indexFrom; i--)
onTreeDataRemoved(target, node, i);
// Fix ZK-5468: the content of the subsequence item might be changed
// no need to plus one for "indexTo" here for removal
for (int i = indexTo, endSize = _model.getChildCount(node);
i < endSize; i++) {
onTreeDataContentChange(target, node, i);
}
break;
case TreeDataEvent.CONTENTS_CHANGED:
for (int i = indexFrom; i <= indexTo; i++)
onTreeDataContentChange(target, node, i);
break;
}
}
}
private void updateHeadercmTristate(Set<?> partialSelections) {
if (getTreecols() != null) {
boolean treecolShouldBePartial = false;
int numberOfChild = 0,
selectedChild = 0;
for (Component cur = getTreechildren().getFirstChild(); cur != null; cur = cur.getNextSibling()) {
if (cur instanceof Treeitem) {
numberOfChild++;
Treeitem curItem = (Treeitem) cur;
if (partialSelections.contains(curItem.getValue())) {
// if 1 child is partial,
// then treecol should be partial, break directly
treecolShouldBePartial = true;
break;
} else {
if (curItem.isSelected())
selectedChild++;
else if (selectedChild != 0) {
// if current is empty, and at least 1 selected found,
// then treecol should be partial, break directly
treecolShouldBePartial = true;
break;
}
}
}
}
treecolShouldBePartial = treecolShouldBePartial || (selectedChild != numberOfChild && selectedChild != 0);
smartUpdate("headercmIcon", treecolShouldBePartial
? TristateModel.State.PARTIAL
: selectedChild == numberOfChild ? TristateModel.State.SELECTED
: TristateModel.State.UNSELECTED);
}
}
private void smartUpdateTristate() {
Set<?> partialSelections = ((TristateModel<?>) _model).getPartials();
// force to enable smartUpdate here.
boolean original = disableClientUpdate(false);
try {
smartUpdateSelection();
smartUpdate("chgPartial", partialSelections.stream().map(this::getChildByNode)
.map(Component::getUuid).collect(Collectors.joining(",")));
updateHeadercmTristate(partialSelections);
} finally {
disableClientUpdate(original);
}
}
/** @param parent either a Tree or Treeitem instance. */
private static Treechildren treechildrenOf(Component parent) {
Treechildren tc = (parent instanceof Tree) ? ((Tree) parent).getTreechildren()
: ((Treeitem) parent).getTreechildren();
if (tc == null) {
tc = new Treechildren();
tc.setParent(parent);
}
return tc;
}
/*
* Handle Treedata insertion
*/
private void onTreeDataInsert(Component parent, Object node, int index) {
/*
* Find the sibling to insertBefore; if there is no sibling or new item
* is inserted at end.
*/
Treeitem newTi = newUnloadedItem();
Treechildren tc = treechildrenOf(parent);
//B50-ZK-721
if (!(parent instanceof Treeitem) || ((Treeitem) parent).isLoaded()) {
List<? extends Component> siblings = tc.getChildren();
// if there is no sibling or new item is inserted at end.
tc.insertBefore(newTi,
// Note: we don't use index >= size(); reason: it detects bug
siblings.isEmpty() || index == siblings.size() ? null : (Treeitem) siblings.get(index));
renderChangedItem(newTi, _model.getChild(node, index));
}
}
/*
* Handle event that child is removed
*/
private void onTreeDataRemoved(Component parent, Object node, int index) {
final Treechildren tc = treechildrenOf(parent);
final List<? extends Component> items = tc.getChildren();
if (items.size() > index) {
((Treeitem) items.get(index)).detach();
} else if (!(parent instanceof Treeitem) || ((Treeitem) parent).isLoaded()) {
tc.detach();
}
}
/*
* Handle event that child's content is changed
*/
private void onTreeDataContentChange(Component parent, Object node, int index) {
List<? extends Component> items = treechildrenOf(parent).getChildren();
/*
* 2008/02/01 --- issue: [ 1884112 ] When Updating TreeModel, throws a IndexOutOfBoundsException
* When I update a children node data of the TreeModel , and fire a
* CONTENTS_CHANGED event, it will throw a IndexOutOfBoundsException , If a
* node doesn't open yet or not load yet.
*
* if parent is loaded, change content.
* else do nothing
*/
if (!items.isEmpty())
renderChangedItem((Treeitem) items.get(index), _model.getChild(node, index));
}
/**
* Return the Tree or Treeitem component by a given associated node in model,
* or null if the treeitem is not instantiated (i.e., rendered) yet.
* It returns this tree if the given node is the root node
* (i.e., {@link TreeModel#getRoot}).
* @since 3.0.0
* @exception IllegalStateException if no model is assigned ({@link #setModel}).
* @see #renderItemByNode
*/
protected Component getChildByNode(Object node) {
if (_model == null)
throw new IllegalStateException("model required");
final Object root = _model.getRoot();
if (Objects.equals(root, node))
return this;
return getChildByPath(_model.getPath(node));
}
/**
* Return the Tree or Treeitem component by a path, or null if corresponding
* Treeitem is not instantiated (i.e., rendered) yet. It returns this tree
* if the given node is the root node. (i.e., {@link TreeModel#getRoot}).
* @since 6.0.0
*/
protected Component getChildByPath(int[] path) {
if (path.length == 0)
return this; // return Tree
Treeitem item = getChildTreeitem(getTreechildren(), path[0]);
for (int j = 1; j < path.length && item != null; j++)
item = getChildTreeitem(item.getTreechildren(), path[j]);
return item;
}
private static Treeitem getChildTreeitem(Treechildren tc, int i) {
if (tc == null)
return null;
List<? extends Component> cs = tc.getChildren();
return i < 0 || i >= cs.size() ? null : (Treeitem) cs.get(i);
}
/*
* Initial Tree data listener
*/
private void initDataListener() {
if (_dataListener == null)
_dataListener = new TreeDataListener() {
public void onChange(TreeDataEvent event) {
onTreeDataChange(event);
}
};
_model.addTreeDataListener(_dataListener);
}
/** Sets the tree model associated with this tree.
*
* <p>Note: changing a render will not cause the tree 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 TreeDataEvent} event.
*
* @param model the tree model to associate, or null to dissociate
* any previous model.
* @exception UiException if failed to initialize with the model
* @since 3.0.0
*/
public void setModel(TreeModel<?> model) {
if (model != null) {
if (!(model instanceof TreeSelectableModel))
throw new UiException(model.getClass() + " must implement " + TreeSelectableModel.class);
if (_model != model) {
if (_model != null) {
_model.removeTreeDataListener(_dataListener);
if (_model instanceof PageableModel && _pgListener != null)
((PageableModel) _model).removePagingEventListener((PagingListener) _pgListener);
if (!isAutosort()) {
Treecols cols = getTreecols();
if (cols != null) {
for (Component treecol : cols.getChildren()) {
((Treecol) treecol).setSortDirection("natural");
}
}
}
} else {
if (_treechildren != null)
_treechildren.detach();
//don't call getItems().clear(), since it readonly
//bug# 3095453: tree can't expand if model is set in button onClick
smartUpdate("model", true);
}
setModelDirectly(model);
initDataListener();
resetPosition(true); //ZK-2712: set different model, reset scroll and anchor position
if (inPagingMold()) {
if (_model instanceof Pageable) {
Pageable m = (Pageable) _model;
if (m.getPageSize() <= 0) { //check for invalid value, min page size is 1
m.setPageSize(_pgi.getPageSize());
}
if (m.getActivePage() < 0) { //check for invalid value, min page index is 0
m.setActivePage(_pgi.getActivePage());
}
}
}
if (_model instanceof TristateModel)
smartUpdate("tristate", true);
}
doSort(this);
postOnInitRender();
} else if (_model != null) {
_model.removeTreeDataListener(_dataListener);
if (_model instanceof PageableModel && _pgListener != null)
((PageableModel) _model).removePagingEventListener((PagingListener) _pgListener);
if (_model instanceof TristateModel)
smartUpdate("tristate", false);
_model = null;
if (_treechildren != null)
_treechildren.detach();
//don't call getItems().clear(), since it readonly
//bug# 3095453: tree can't expand if model is set in button onClick
smartUpdate("model", false);
resetPosition(false);
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private final void setModelDirectly(TreeModel model) {
_model = model;
}
/** Handles a private event, onInitRender. It is used only for
* implementation, and you rarely need to invoke it explicitly.
* @since 6.0.0
*/
public void onInitRender() {
removeAttribute(ATTR_ON_INIT_RENDER_POSTED);
renderTree();
}
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);
Events.postEvent("onInitRender", this, null);
}
}
//--TreeModel dependent codes--//
/** Returns the list model associated with this tree, or null
* if this tree is not associated with any tree data model.
* @return the list model associated with this tree
* @since 3.0.0
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> TreeModel<T> getModel() {
return (TreeModel) _model;
}
private static boolean doSort(Tree tree) {
Treecols cols = tree.getTreecols();
if (!tree.isAutosort() || cols == null)
return false;
for (Component c : cols.getChildren()) {
final Treecol hd = (Treecol) c;
String dir = hd.getSortDirection();
if (!"natural".equals(dir)) {
hd.doSort("ascending".equals(dir));
return true;
}
}
return false;
}
/** 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 tree 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 TreeDataEvent} event.
*
* @param renderer the renderer, or null to use the default.
* @exception UiException if failed to initialize with the model
* @since 5.0.6
*/
public void setItemRenderer(TreeitemRenderer<?> renderer) {
if (_renderer != renderer) {
_renderer = renderer;
if (_model != null)
postOnInitRender();
}
}
/**
* Sets the renderer by use of a class name. It creates an instance
* automatically.
* @since 6.5.2
*/
@SuppressWarnings("rawtypes")
public void setItemRenderer(String clsnm) throws ClassNotFoundException, NoSuchMethodException,
IllegalAccessException, InstantiationException, java.lang.reflect.InvocationTargetException {
if (clsnm != null)
setItemRenderer((TreeitemRenderer) Classes.newInstanceByThread(clsnm));
}
/** Returns the renderer to render each item, or null if the default
* renderer is used.
* @return the renderer to render each item, or null if the default
* @since 5.0.6
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> TreeitemRenderer<T> getItemRenderer() {
return (TreeitemRenderer) _renderer;
}
/*
* Render the root of Tree
* Notice: _model.getRoot() is mapped to Tree, not first Treeitem
*/
private void renderTree() {
if (_treechildren == null) {
Treechildren children = new Treechildren();
children.setParent(this);
} else {
_treechildren.getChildren().clear();
}
if (_model instanceof TreeSelectableModel)
this.setMultiple(((TreeSelectableModel) _model).isMultiple());
if (_model instanceof PageableModel && _pgListener != null) {
((PageableModel) _model).removePagingEventListener((PagingListener) _pgListener);
((PageableModel) _model).addPagingEventListener((PagingListener) _pgListener);
}
final Renderer renderer = new Renderer();
try {
if (_model != null)
renderChildren(renderer, _treechildren, _model.getRoot());
} catch (Throwable ex) {
renderer.doCatch(ex);
} finally {
renderer.doFinally();
}
Events.postEvent(ZulEvents.ON_AFTER_RENDER, this, null); // notify the tree when items have been rendered.
}
private int[] getPath0(Treechildren parent, int index) {
List<Integer> path = new LinkedList<Integer>();
path.add(index);
Component p = parent;
while (true) {
p = p.getParent();
if (p instanceof Treeitem) {
Component treechildren = p.getParent();
if (treechildren != null) {
path.add(0, treechildren.getChildren().indexOf(p));
p = treechildren;
}
} else
break;
}
final int[] ipath = new int[path.size()];
for (int j = 0; j < ipath.length; j++)
ipath[j] = path.get(j);
return ipath;
}
private void renderChildren0(Renderer renderer, Treechildren parent, Treeitem ti, Object childNode, int i)
throws Throwable {
renderer.render(ti, childNode, i);
Object v = ti.getAttribute(Attributes.MODEL_RENDERAS);
if (v != null) { //a new item is created to replace the existent one
(ti = (Treeitem) v).setOpen(false);
}
ti.setRendered(true);
// B60-ZK-767: handle selected/open state here, as it might be replaced
int[] path = null;
boolean isLeaf = childNode != null && _model.isLeaf(childNode);
if (_model instanceof TreeSelectableModel) {
TreeSelectableModel model = (TreeSelectableModel) _model;
if (!model.isSelectionEmpty() && getSelectedCount() != model.getSelectionCount()
&& model.isPathSelected(path = getPath0(parent, i)))
addItemToSelection(ti);
}
if (_model instanceof Selectable) {
Selectable smodel = (Selectable) _model;
SelectionControl control = smodel.getSelectionControl();
if (control != null)
ti.setSelectable(control.isSelectable(childNode));
}
if (_model instanceof TreeOpenableModel) {
TreeOpenableModel model = (TreeOpenableModel) _model;
if (!model.isOpenEmpty()) {
if (!isLeaf) {
if (path == null)
//B70-ZK-2547: use the right way to get path
path = getTreeitemPath(this, ti);
ti.setOpen(model.isPathOpened(path));
}
}
}
if (!isLeaf && ti.getTreechildren() == null) {
Treechildren tc = new Treechildren();
tc.setParent(ti);
//ZK-3022: should render child also if current node is opened
if (ti.isOpen())
this.renderChildren(renderer, tc, childNode);
}
}
/*
* Renders the direct children for the specified parent
*/
private void renderChildren(Renderer renderer, Treechildren parent, Object node) throws Throwable {
final int initSize = initRodSize();
for (int i = 0, j = _model.getChildCount(node); i < j; i++) {
Treeitem ti = newUnloadedItem();
ti.setParent(parent);
// Bug ZK-1696: must render all opened node to have correct page count
TreeOpenableModel model = (TreeOpenableModel) _model;
// render nodes when no ROD or within ROD range or opened node
if (initSize < 0 || i < initSize || model.isPathOpened(toChildPath(node, i))) {
Object childNode = _model.getChild(node, i);
renderChildren0(renderer, parent, ti, childNode, i);
} else { //render empty row
ti.appendChild(new Treerow());
ti.getTreerow().appendChild(new Treecell());
}
}
}
private int[] toChildPath(Object parentNode, int childIndex) {
int[] parentPath = _model.getPath(parentNode);
int arrLength = parentPath.length;
int[] path = new int[arrLength + 1];
System.arraycopy(parentPath, 0, path, 0, arrLength);
path[arrLength] = childIndex;
return path;
}
/**
* Returns the number of rows to preload when receiving the rendering
* request from the client.
* <p>
* Default: 50. (since 7.0.0)
* <p>
* It is used only if live data ({@link #setModel(TreeModel)} 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.tree.preloadSize", sz, true)) < 0)
throw new UiException("nonnegative is required: " + sz);
return sz;
}
/**
* Returns Specifies how many pages (of treeitems) to keep rendered in memory
* (on the server side) when navigating the tree using pagination.
* <p>
* Default: 1. (Since 7.0.0)
* <p>
* It is used only if live data ({@link #setModel(TreeModel)} and in paging mold
* ({@link #getPagingChild}.
*/
private int maxRodPageSize() {
if (WebApps.getFeature("ee")) {
return Utils.getIntAttribute(this, "org.zkoss.zul.tree.maxRodPageSize", INIT_LIMIT, true);
}
return -1;
}
/**
* Returns the number of items rendered when the Tree first render.
* <p>
* Default: 50. (Since 7.0.0)
* <p>
* It is used only if live data ({@link #setModel(TreeModel)} and not paging
* ({@link #getPagingChild}.
*/
private int initRodSize() {
if (WebApps.getFeature("ee")) {
// ZK-2165: should return page size in paging mold
if (inPagingMold())
return getPageSize();
else
return Utils.getIntAttribute(this, "org.zkoss.zul.tree.initRodSize", INIT_LIMIT, true);
}
return -1;
}
/**
* Returns the millisecond of scrolling throttling in Tree render on-demand (ROD).
* <p>Default: 300. (Since 9.6.0)
*/
private int getThrottleMillis() {
if (WebApps.getFeature("ee")) {
return Utils.getIntAttribute(this, "org.zkoss.zul.tree.throttleMillis", DEFAULT_THROTTLE_MILLIS, true);
}
return DEFAULT_THROTTLE_MILLIS;
}
private Treeitem newUnloadedItem() {
Treeitem ti = new Treeitem();
ti.setOpen(false);
return ti;
}
/** Returns the renderer used to render items.
*/
@SuppressWarnings("rawtypes")
private TreeitemRenderer getRealRenderer() {
return _renderer != null ? _renderer : _defRend;
}
@SuppressWarnings("rawtypes")
private static final TreeitemRenderer _defRend = new TreeitemRenderer() {
public void render(Treeitem ti, final Object node, final int index) {
Tree tree = ti.getTree();
final Template tm = tree.getTemplate("model");
if (tm == null) {
Treecell tc = new Treecell(Objects.toString(node));
Treerow tr = null;
ti.setValue(node);
if (ti.getTreerow() == null) {
tr = new Treerow();
tr.setParent(ti);
} else {
tr = ti.getTreerow();
tr.getChildren().clear();
}
tc.setParent(tr);
} else {
final Component[] items = ShadowElementsCtrl
.filterOutShadows(tm.create(ti.getParent(), ti, new VariableResolver() {
public Object resolveVariable(String name) {
if ("each".equals(name)) {
return node;
} else if ("forEachStatus".equals(name)) {
return new ForEachStatus() {
public ForEachStatus getPrevious() {
return null;
}
public Object getEach() {
return getCurrent();
}
public int getIndex() {
return index;
}
public Integer getBegin() {
return 0;
}
public Integer getEnd() {
throw new UnsupportedOperationException("end not available");
}
public Object getCurrent() {
return node;
}
public boolean isFirst() {
return getCount() == 1;
}
public boolean isLast() {
return getIndex() + 1 == getEnd();
}
public Integer getStep() {
return null;
}
public int getCount() {
return getIndex() + 1;
}
};
} else {
return null;
}
}
}, null));
if (items.length != 1)
throw new UiException("The model template must have exactly one item, not " + items.length);
final Treeitem nti = (Treeitem) items[0];
if (nti.getValue() == null) //template might set it
nti.setValue(node);
ti.setAttribute(Attributes.MODEL_RENDERAS, nti);
//indicate a new item is created to replace the existent one
ti.detach();
}
}
};
/** Used to render treeitem if _model is specified. */
private class Renderer implements java.io.Serializable {
@SuppressWarnings("rawtypes")
private final TreeitemRenderer _renderer;
private boolean _rendered, _ctrled;
private Renderer() {
_renderer = getRealRenderer();
}
// B65-ZK-1608: Tree node become leaf node after update
@SuppressWarnings("unchecked")
private void renderChangedItem(Treeitem item, Object node, int index) throws Throwable {
if (!_rendered && (_renderer instanceof RendererCtrl)) {
((RendererCtrl) _renderer).doTry();
_ctrled = true;
}
try {
try {
if (item.getTreerow() != null) // just in case
item.getTreerow().detach();
Treechildren tc = item.getTreechildren();
_renderer.render(item, node, index);
Object newTreeitem = item.getAttribute(Attributes.MODEL_RENDERAS);
if (newTreeitem instanceof Treeitem) {
Treeitem newItem = ((Treeitem) newTreeitem);
// B65-ZK-1765 : Add new Tree node cause Null Pointer Exception (treechildren is null, in case of a leaf node)
if (tc != null) {
newItem.appendChild(tc);
}
if (_model instanceof TreeOpenableModel) {
TreeOpenableModel model = (TreeOpenableModel) _model;
// reset open status - B65-ZK-1639
newItem.setOpen(!(model.isOpenEmpty()
|| !model.isPathOpened(getPath0((Treechildren) newItem.getParent(), index))));
if (!item.isLoaded() && newItem.isOpen())
Tree.this.renderChildren(this, tc, node);
newItem.setLoaded(item.isLoaded());
// B65-ZK-1639.zul
newItem.setRendered(item.isRendered());
}
} else // B70-ZK-2460 : If template is null , item must have been rendered
item.setRendered(true);
} catch (AbstractMethodError ex) {
final Method m = _renderer.getClass().getMethod("render",
new Class<?>[] { Treeitem.class, Object.class });
m.setAccessible(true);
m.invoke(_renderer, new Object[] { item, node });
}
} catch (Throwable ex) {
try {
item.setLabel(Exceptions.getMessage(ex));
} catch (Throwable t) {
log.error("", t);
}
throw ex;
}
_rendered = true;
}
@SuppressWarnings("unchecked")
private void render(Treeitem item, Object node, int index) throws Throwable {
if (!_rendered && (_renderer instanceof RendererCtrl)) {
((RendererCtrl) _renderer).doTry();
_ctrled = true;
}
try {
try {
_renderer.render(item, node, index);
} catch (AbstractMethodError ex) {
final Method m = _renderer.getClass().getMethod("render",
new Class<?>[] { Treeitem.class, Object.class });
m.setAccessible(true);
m.invoke(_renderer, new Object[] { item, node });
}
} catch (Throwable ex) {
try {
item.setLabel(Exceptions.getMessage(ex));
} catch (Throwable t) {
log.error("", t);
}
throw ex;
}
_rendered = true;
}
private 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);
}
}
private void doFinally() {
if (_ctrled)
((RendererCtrl) _renderer).doFinally();
}
}
/** Renders the specified {@link Treeitem}, if not loaded yet,
* with {@link #getItemRenderer}.
*
* <p>It does nothing if {@link #getModel} returns null.
* <p>To unload treeitem, use {@link Treeitem#unload()}.
* @see #renderItems
* @since 3.0.0
*/
public void renderItem(Treeitem item) {
if (_model != null) {
final Renderer renderer = new Renderer();
try {
renderItem0(renderer, item);
} catch (Throwable ex) {
renderer.doCatch(ex);
} finally {
renderer.doFinally();
}
}
}
/** Renders the specified {@link Treeitem}, if not loaded yet,
* with {@link #getItemRenderer}.
*
* <p>It does nothing if {@link #getModel} returns null.
*
*<p>Note: Since the corresponding node is given,
* This method has better performance than
* renderItem(Treeitem item) due to not searching for its
* corresponding node.
* <p>To unload treeitem, use {@link Treeitem#unload()}.
* @see #renderItems
* @since 3.0.0
*/
public void renderItem(Treeitem item, Object node) {
if (_model != null) {
final Renderer renderer = new Renderer();
try {
renderItem0(renderer, item, node);
} catch (Throwable ex) {
renderer.doCatch(ex);
} finally {
renderer.doFinally();
}
}
}
/** Note: it doesn't call render doCatch/doFinally */
private void renderItem0(Renderer renderer, Treeitem item) throws Throwable {
renderItem0(renderer, item, getAssociatedNode(item, this));
}
/** Note: it doesn't call render doCatch/doFinally */
private void renderItem0(Renderer renderer, Treeitem item, Object node) throws Throwable {
if (item.isLoaded()) //all direct children are loaded
return;
/*
* After modified the node in tree model, if node is leaf,
* its treechildren is needed to be dropped.
*/
Treechildren tc = item.getTreechildren();
if (_model.isLeaf(node)) {
if (tc != null)
tc.detach(); //just in case
//no children to render
//Note item already rendered, so no need:
//renderer.render(item, node);
} else {
if (tc != null)
tc.getChildren().clear(); //just in case
else {
tc = new Treechildren();
tc.setParent(item);
}
renderChildren(renderer, tc, node);
}
Object v = item.getAttribute(Attributes.MODEL_RENDERAS);
if (v != null) //a new item is created to replace the existent one
(item = (Treeitem) v).setOpen(false);
item.setLoaded(true);
}
private void renderChangedItem(Treeitem item, Object node) {
/*
* After modified the node in tree model, if node is leaf,
* its treechildren is needed to be dropped.
*/
if (_model != null) {
Treechildren tc = item.getTreechildren();
if (_model.isLeaf(node)) {
if (tc != null)
tc.detach(); //just in case
} else {
if (tc == null) {
tc = new Treechildren();
tc.setParent(item);
}
}
final Renderer renderer = new Renderer();
try {
renderer.renderChangedItem(item, node, item.getIndex()); //re-render
} catch (Throwable ex) {
renderer.doCatch(ex);
} finally {
renderer.doFinally();
}
}
}
/** Renders the specified {@link Treeitem} if not loaded yet,
* with {@link #getItemRenderer}.
*
* <p>It does nothing if {@link #getModel} returns null.
* <p>To unload treeitem, with {@link Treeitem#unload()}.
* @see #renderItem
* @since 3.0.0
*/
public void renderItems(Set<? extends Treeitem> items) {
if (_model == null)
return;
if (items.isEmpty())
return; //nothing to do
final Renderer renderer = new Renderer();
try {
for (final Treeitem item : items) {
renderItem0(renderer, item);
}
} catch (Throwable ex) {
renderer.doCatch(ex);
} finally {
renderer.doFinally();
}
}
/**
* Return a node which is an associated Treeitem ti in a Tree tree
* @since 3.0.0
*/
protected Object getAssociatedNode(Treeitem ti, Tree t) {
return _model.getChild(getTreeitemPath(t, ti));
}
/**
* return the path which is from ZK Component root to ZK Component lastNode
*/
/*package*/ int[] getTreeitemPath(Component root, Component lastNode) {
List<Integer> l = new ArrayList<Integer>();
Component curNode = lastNode;
while (!root.equals(curNode)) {
if (curNode instanceof Treeitem) {
l.add(0, new Integer(((Treeitem) curNode).getIndex()));
}
curNode = curNode.getParent();
}
final Integer[] objs = l.toArray(new Integer[l.size()]);
final int[] path = new int[objs.length];
for (int i = 0; i < objs.length; i++)
path[i] = objs[i].intValue();
return path;
}
/** Load the treeitems by the given node.
* This method must be used with a tree model, and the node is
* one of the value returned by {@link TreeModel#getChild}.
* <p>Notice that this method has to search the model one-by-one.
* The performance might not be good, so use {@link #renderItemByPath}
* if possible.
* @exception IllegalStateException if no model is assigned ({@link #setModel}).
* @return the treeitem that is associated with the give node, or null
* no treeitem is associated (including the give node is the root).
* @since 5.0.6
* @since #getChildByNode
*/
public Treeitem renderItemByNode(Object node) {
return renderItemByPath(_model.getPath(node));
}
/**
* Load the treeitems by giving a path of the treeitems top open.
* <br>Note: By using this method, all treeitems in path will be rendered
* and opened ({@link Treeitem#setOpen}). If you want to visit the rendered
* item in paging mold, please invoke {@link #setActivePage(Treeitem)}.
* @param path - an index path. The first element is the index at the first level
* of the tree structure.
* @return the treeitem from tree by given path
* @since 3.0.0
*/
public Treeitem renderItemByPath(int[] path) {
if (path == null || path.length == 0)
return null;
// Start from root-Tree
Treeitem ti = null;
List<? extends Component> children = this.getTreechildren().getChildren();
/*
* Go through each stop in path and render corresponding treeitem
*/
for (int i = 0; i < path.length; i++) {
if (path[i] < 0 || path[i] >= children.size())
return null;
ti = (Treeitem) children.get(path[i]);
if (i < path.length - 1) {
//re-fixed for 2375: should add treeitem to openpath if using TreeOpenableModel
TreeModel model = this.getModel();
if (model instanceof TreeOpenableModel)
((TreeOpenableModel) model).addOpenPath(Arrays.copyOf(path, i + 1));
ti.setOpen(true);
}
if (ti.getTreechildren() != null) {
children = ti.getTreechildren().getChildren();
} else {
if (i != path.length - 1) {
return null;
}
}
}
return ti;
}
protected void redrawChildren(Writer out) throws IOException {
super.redrawChildren(out);
if (inPagingMold()) {
removeAttribute(Attributes.RENDERED_ITEM_COUNT);
removeAttribute(Attributes.VISITED_ITEM_COUNT);
removeAttribute(Attributes.VISITED_ITEM_TOTAL);
}
}
// AREA JEFF ADDED END
protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException {
super.renderProperties(renderer);
render(renderer, "name", _name);
if (_rows > 0)
renderer.render("rows", getRows());
render(renderer, "multiple", isMultiple());
render(renderer, "checkmark", isCheckmark());
render(renderer, "vflex", isVflex());
if (_model != null) {
render(renderer, "model", true);
if (_model instanceof TristateModel) {
render(renderer, "tristate", true);
Set<?> partialSelections = ((TristateModel<?>) _model).getPartials();
render(renderer, "chgPartial", partialSelections.stream().map(this::getChildByNode)
.map(Component::getUuid).collect(Collectors.joining(",")));
}
if (_model instanceof Selectable && isMultiple()) {
Selectable smodel = (Selectable) _model;
SelectionControl control = smodel.getSelectionControl();
if (control != null) {
renderer.render("$$selectAll", control.isSelectAll());
}
}
}
if (_nonselTags != null)
renderer.render("nonselectableTags", _nonselTags);
if (isCheckmarkDeselectOther())
renderer.render("checkmarkDeselectOther", true);
if (!isRightSelect())
renderer.render("rightSelect", false);
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("_tree$noSelectAll", true);
}
}
if (_currentTop != 0)
renderer.render("_currentTop", _currentTop);
if (_currentLeft != 0)
renderer.render("_currentLeft", _currentLeft);
if (_anchorTop != 0)
renderer.render("_anchorTop", _anchorTop);
if (_anchorLeft != 0)
renderer.render("_anchorLeft", _anchorLeft);
int preloadSz = preloadSize();
if (preloadSz != _preloadsz)
renderer.render("preloadSize", preloadSz);
// ZK-3835: because of ZK-3198, -1 will disable client ROD too
if (initRodSize() == -1)
renderer.render("z$rod0", false);
int throttleMillis = getThrottleMillis();
if (throttleMillis != DEFAULT_THROTTLE_MILLIS)
render(renderer, "throttleMillis", throttleMillis);
}
/** Returns whether to toggle a list item selection on right click
*/
private boolean isRightSelect() {
return Utils.testAttribute(this, "org.zkoss.zul.tree.rightSelect", true, true);
}
protected boolean isAutohidePaging() {
return Utils.testAttribute(this, "org.zkoss.zul.tree.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.tree.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;
}
/** Returns whether to ignore sort all items when model or sort direction be changed.
* <p>Default: {@code true}</p>
* @since 5.0.7
*/
private boolean isIgnoreSortWhenChanged() {
String attr = "org.zkoss.zul.tree.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.tree.checkmarkDeselectOthers")));
return Utils.testAttribute(this, "org.zkoss.zul.tree.checkmarkDeselectOthers", _ckDeselectOther.booleanValue(), true);
}
protected boolean isSelectOnHighlightDisabled() {
return Utils.testAttribute(this, "org.zkoss.zul.tree.selectOnHighlight.disabled", false, true);
}
private static Boolean _ckDeselectOther;
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;
}
/**
* Resets scroll and anchor position.
* @param shouldInvalidate should invalidate or not
* @since 6.5.8
*/
private void resetPosition(boolean shouldInvalidate) {
_currentTop = 0;
_currentLeft = 0;
_anchorTop = 0;
_anchorLeft = 0;
if (shouldInvalidate)
invalidate();
}
private boolean isAllRendered() {
for (Treeitem item : getItems()) {
if (!item.isRendered() || !item.isLoaded())
return false;
}
return true;
}
private static HashMap<String, PropertyAccess> _properties = new HashMap<String, PropertyAccess>(4);
static {
_properties.put("_currentTop", new IntegerPropertyAccess() {
public void setValue(Component cmp, Integer currentTop) {
((Tree) cmp)._currentTop = currentTop;
}
public Integer getValue(Component cmp) {
return ((Tree) cmp)._currentTop;
}
});
_properties.put("_currentLeft", new IntegerPropertyAccess() {
public void setValue(Component cmp, Integer currentLeft) {
((Tree) cmp)._currentLeft = currentLeft;
}
public Integer getValue(Component cmp) {
return ((Tree) cmp)._currentLeft;
}
});
_properties.put("_anchorTop", new IntegerPropertyAccess() {
public void setValue(Component cmp, Integer anchorTop) {
((Tree) cmp)._anchorTop = anchorTop;
}
public Integer getValue(Component cmp) {
return ((Tree) cmp)._anchorTop;
}
});
_properties.put("_anchorLeft", new IntegerPropertyAccess() {
public void setValue(Component cmp, Integer anchorLeft) {
((Tree) cmp)._anchorLeft = anchorLeft;
}
public Integer getValue(Component cmp) {
return ((Tree) cmp)._anchorLeft;
}
});
}
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(AuRequest, boolean)},
* 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();
boolean isSelModel = _model instanceof Selectable;
if (cmd.equals(Events.ON_SELECT)) {
Desktop desktop = request.getDesktop();
Map data = request.getData();
List<String> sitems = cast((List) request.getData().get("items"));
boolean selectAll = Boolean.parseBoolean(data.get("selectAll") + "");
boolean paging = inPagingMold();
// B50-ZK-547: SelectEvent.getSelectItems() does not return multiple selected TreeItems.
Set<Treeitem> prevSeldItems = new LinkedHashSet<Treeitem>(_selItems);
Set<Treeitem> curSeldItems = AuRequests.convertToItems(desktop, sitems);
Set<Treeitem> realPrevSeldItems = new LinkedHashSet<Treeitem>(prevSeldItems);
Set<Object> prevSeldObjects = _model != null
? new LinkedHashSet<Object>(((Selectable) _model).getSelection()) : new LinkedHashSet<Object>();
// fine tune with B50-ZK-547.
Selectable<Object> smodel = _model != null ? (Selectable) _model : null;
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() || (isCheckmarkDeselectOther() && selectAll))) {
// use toArray() to prevent java.util.ConcurrentModificationException
for (Object item : realPrevSeldItems.toArray()) {
int index = ((Treeitem) item).getIndex();
if (index >= to || index < from)
realPrevSeldItems.remove(item);
}
}
disableClientUpdate(true);
try {
if (AuRequests.getBoolean(request.getData(), "clearFirst")) {
clearSelection();
if (_model instanceof TreeSelectableModel)
((TreeSelectableModel) _model).clearSelection();
}
if (!_multiple || (!paging && isAllRendered() && (curSeldItems == null || curSeldItems.size() <= 1))) {
final Treeitem item = curSeldItems != null && curSeldItems.size() > 0
? curSeldItems.iterator().next() : null;
selectItem(item);
if (_model instanceof TreeSelectableModel) {
TreeSelectableModel tsm = (TreeSelectableModel) _model;
tsm.clearSelection();
if (item != null)
tsm.addSelectionPath(getTreeitemPath(this, item));
}
} else {
for (Treeitem item : curSeldItems)
if (!_selItems.contains(item)) {
addItemToSelection(item);
if (_model instanceof TreeSelectableModel)
((TreeSelectableModel) _model).addSelectionPath(getTreeitemPath(this, item));
}
for (Treeitem item : prevSeldItems)
if (!curSeldItems.contains(item)) {
final int index = getVisibleIndexOfItem(item);
if (!paging || (index >= from && index < to)) {
removeItemFromSelection(item);
if (_model instanceof TreeSelectableModel)
((TreeSelectableModel) _model).removeSelectionPath(getTreeitemPath(this, item));
}
}
}
} finally {
disableClientUpdate(false);
}
Set<Treeitem> 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 {
selectedObjects = smodel.getSelection();
unselectedObjects = collectUnselectedObjects(prevSeldObjects, smodel.getSelection());
}
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 (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);
Treeitem item = getSelectedItem();
int sel = getVisibleIndexOfItem(item);
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_INNER_WIDTH)) {
final String width = AuRequests.getInnerWidth(request);
_innerWidth = width == null ? "100%" : width;
} 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_CHECK_SELECT_ALL) && isSelModel) {
CheckEvent evt = CheckEvent.getCheckEvent(request);
final Selectable<Object> selectableModel = (Selectable<Object>) _model;
SelectionControl control = selectableModel.getSelectionControl();
if (control == null)
throw new IllegalStateException(
"SelectionControl cannot be null, please implement SelectionControl interface for SelectablModel");
control.setSelectAll(evt.isChecked());
Events.postEvent(evt);
} else if (cmd.equals("onUpdateSelectAll") && isSelModel) {
final Selectable<Object> selectableModel = (Selectable<Object>) _model;
SelectionControl control = selectableModel.getSelectionControl();
if (control != null) {
Clients.response(new AuInvoke(this, "$doService", new Object[] { cmd, new JSONAware() {
public String toJSONString() {
return String.valueOf(control.isSelectAll());
}
} }));
}
} else if (cmd.equals(Events.ON_RENDER)) {
final RenderEvent<Treeitem> event = RenderEvent.getRenderEvent(request);
final Set<Treeitem> items = event.getItems();
int cnt = items.size();
if (cnt == 0)
return; //nothing to do
int preloadsz = preloadSize();
final Renderer renderer = new Renderer();
try {
Treeitem maxItem = null;
int maxIndex = -1;
//ZK-3022: process from back to front to avoid "render non existent child" problem
List<Treeitem> listItems = new ArrayList<Treeitem>(items);
for (int j = listItems.size() - 1; j >= 0; j--) {
Treeitem ti = listItems.get(j);
if (ti.isRendered())
continue;
int i = ti.getIndex();
if (maxItem == null) {
maxItem = ti;
maxIndex = i;
}
if (i > maxIndex) {
maxItem = ti;
maxIndex = i;
}
ti.getChildren().clear();
Treechildren parent = (Treechildren) ti.getParent();
Object childNode = getAssociatedNode(ti, this);
renderChildren0(renderer, parent, ti, childNode, i);
}
if (preloadsz > 0) {
while (maxItem != null && preloadsz-- > 0) {
maxItem = (Treeitem) maxItem.getNextSibling();
if (maxItem != null) {
if (maxItem.isRendered())
continue;
maxItem.getChildren().clear();
Treechildren parent = (Treechildren) maxItem.getParent();
Object childNode = getAssociatedNode(maxItem, this);
renderChildren0(renderer, parent, maxItem, childNode, maxItem.getIndex());
}
}
}
} catch (Throwable ex) {
renderer.doCatch(ex);
} finally {
renderer.doFinally();
}
} else
super.service(request, everError);
}
/** An iterator used by _heads.
*/
private class Iter implements Iterator<Component> {
private final ListIterator<? extends Component> _it = getChildren().listIterator();
public boolean hasNext() {
while (_it.hasNext()) {
Component o = _it.next();
if (o instanceof Treecols || o instanceof Auxhead) {
_it.previous();
return true;
}
}
return false;
}
public Component next() {
for (;;) {
Component o = _it.next();
if (o instanceof Treecols || o instanceof Auxhead)
return o;
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
@Override
public int getActivePage() {
if (hasAttribute(ATTR_ON_INIT_RENDER_POSTED) && _model instanceof Pageable) {
if (((Pageable) _model).getActivePage() >= 0) { //min page index is 0
return ((Pageable) _model).getActivePage();
}
}
return super.getActivePage();
}
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(getVisibleItemCount());
if (m.getActivePage() >= 0) { //min page index is 0
_pgi.setActivePage(m.getActivePage());
} else {
m.setActivePage(_pgi.getActivePage());
}
}
}
}