zul/src/main/java/org/zkoss/zul/Treeitem.java
/* Treeitem.java
Purpose:
Description:
History:
Wed Jul 6 18:56:15 2005, Created by tomyeh
Copyright (C) 2005 Potix Corporation. All Rights Reserved.
{{IS_RIGHT
This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zul;
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WebApps;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.OpenEvent;
import org.zkoss.zk.ui.ext.Scope;
import org.zkoss.zk.ui.sys.BooleanPropertyAccess;
import org.zkoss.zk.ui.sys.ComponentCtrl;
import org.zkoss.zk.ui.sys.PropertyAccess;
import org.zkoss.zul.event.PagingEvent;
import org.zkoss.zul.ext.Paginal;
import org.zkoss.zul.ext.TreeOpenableModel;
import org.zkoss.zul.impl.XulElement;
/**
* A treeitem.
*
* <p>Event:
* <ol>
* <li>onOpen is sent when a tree item is opened or closed by user.</li>
* <li>onDoubleClick is sent when user double-clicks the treeitem.</li>
* <li>onRightClick is sent when user right-clicks the treeitem.</li>
* </ol>
*
* @author tomyeh
*/
public class Treeitem extends XulElement implements org.zkoss.zk.ui.ext.Disable {
private transient Treerow _treerow;
private transient Treechildren _treechildren;
private Object _value;
private boolean _open = true;
private boolean _selected;
private boolean _disabled;
private boolean _selectable = true;
/** whether the content of this item is loaded; used if
* the tree owning this item is using a tree model.
*/
private boolean _loaded;
private boolean _rendered;
static {
addClientEvent(Treeitem.class, Events.ON_OPEN, CE_IMPORTANT);
}
public Treeitem() {
}
public Treeitem(String label) {
setLabel(label);
}
public Treeitem(String label, Object value) {
setLabel(label);
setValue(value);
}
/**
* Returns whether it is selectable.
* <p>Default: true.</p>
* @since 8.0.0
*/
public boolean isSelectable() {
return _selectable;
}
/** Sets whether it is selectable.
*
* <p>If the tree is in a checkmark mode, the selectable state will affect
* the checkable icon to display or not.</p>
* <p>Default: true.</p>
* @param selectable
*/
public void setSelectable(boolean selectable) {
if (_selectable != selectable) {
_selectable = selectable;
// non-checkable cannot be selected
if (!_selectable)
setSelected(false);
smartUpdate("selectable", selectable);
}
}
/**
* Unload the tree item
* <p>To load the tree item, with
* {@link Tree#renderItem(Treeitem)}, {@link Tree#renderItem(Treeitem, Object)}, or {@link Tree#renderItems(java.util.Set)}
*
* @since 3.0.4
*/
public void unload() {
if (isLoaded()) {
//Clean its children
if (getTreechildren() != null)
getTreechildren().getChildren().clear();
//Set the load status to unloaded
setLoaded(false);
//Change the "+/-" sign icon
setOpen(false);
}
}
/**
* Sets whether it is disabled.
* @since 3.0.1
*/
public void setDisabled(boolean disabled) {
if (_disabled != disabled) {
_disabled = disabled;
smartUpdate("disabled", disabled);
}
}
/** Returns whether it is disabled.
* <p>Default: false.
* @since 3.0.1
*/
public boolean isDisabled() {
return _disabled;
}
/**
* Returns true whether this tree item is rendered. Unlike {@link #isLoaded()}
* which is used to check whether all children of this tree item are loaded.
* <p>Default: false
* @since 7.0.0
*/
public boolean isRendered() {
if (_rendered) return true;
Tree tree = getTree();
if (tree != null) {
return tree.getModel() == null;
}
return false;
}
// Component developer use only. (since 7.0.0)
/*package*/ void setRendered(boolean rendered) {
if (_rendered != rendered) {
_rendered = rendered;
smartUpdate("_loaded", _rendered);
}
}
/**
* Return true whether all children of this tree item, if any, is loaded
* @return true whether all children of this tree item is loaded
* @since 3.0.0
*/
public boolean isLoaded() {
return _loaded;
}
/**
* Sets whether all children of this tree item, if any, is loaded.
* @since 3.0.0
*/
/*package*/ void setLoaded(boolean loaded) {
if (_loaded != loaded) {
_loaded = loaded;
smartUpdate("_loadedChildren", _loaded);
}
}
/**
* Please use {@link Treecell} or {@link Tree} instead.
* @deprecated as of release 7.0.3.
*/
public void setStubonly(String stubonly) {
// Don't remove this method, it's to override super.setStubonly().
super.setStubonly(stubonly);
}
/**
* Please use {@link Treecell} or {@link Tree} instead.
* @deprecated as of release 7.0.3.
*/
public void setStubonly(boolean stubonly) {
// Don't remove this method, it's to override super.setStubonly().
super.setStubonly(stubonly);
}
/**
* return the index of this item
* @return the index of this item
* @since 5.0.9
*/
public int getIndex() {
List list = this.getParent().getChildren();
return list.indexOf(this);
}
/** Returns the treerow that this tree item owns (might null).
* Each tree items has exactly one tree row.
*/
public Treerow getTreerow() {
return _treerow;
}
/** Returns the treechildren that this tree item owns, or null if
* doesn't have any child.
*/
public Treechildren getTreechildren() {
return _treechildren;
}
/** Returns whether the element is to act as a container
* which can have child elements.
*/
public boolean isContainer() {
return _treechildren != null;
}
/** Returns whether this element contains no child elements.
*/
public boolean isEmpty() {
return _treechildren == null || _treechildren.getChildren().isEmpty();
}
/** Returns the value. It could be anything you want.
* <p>Default: null.
* <p>Note: the value is not sent to the browser, so it is OK to be
* anything.
*/
@SuppressWarnings("unchecked")
public <T> T getValue() {
return (T) _value;
}
/** Sets the value.
* @param value the value.
* Note: the value is not sent to the browser, so it is OK to be
* anything.
*/
public <T> void setValue(T value) {
_value = value;
}
/** Returns whether this container is open.
* <p>Default: true.
*/
public boolean isOpen() {
return _open;
}
/** Sets whether this container is open.
*/
public void setOpen(boolean open) {
if (_open != open) {
_open = open;
//Note: _treerow might not be ready yet because it might be
//initialized before creating child components (for ZK pages)
smartUpdate("open", _open);
//If the item is open, its tree has model and not rendered, render the item
if (_treechildren != null)
addVisibleItemCount((_open ? 1 : -1) * _treechildren.getVisibleItemCount());
Tree tree = getTree();
if (tree != null && tree.getModel() != null) {
if (_open && !isLoaded())
tree.renderItem(this);
}
}
}
/** Returns whether this item is selected.
*/
public boolean isSelected() {
return _selected;
}
/** Returns whether this item is selected.
*/
public void setSelected(boolean selected) {
if (_selected != selected) {
final Tree tree = getTree();
if (tree != null) {
//Note: we don't update it here but let its parent does the job
tree.toggleItemSelection(this);
} else {
_selected = selected;
}
}
}
/** Updates _selected directly and invalidate getTreerow if necessary.
*/
/*package*/ void setSelectedDirectly(boolean selected) {
_selected = selected;
}
/** Returns the level this cell is. The root is level 0.
*/
public int getLevel() {
int level = 0;
for (Component item = this;; ++level) {
final Component tch = item.getParent();
if (tch == null)
break;
item = tch.getParent();
if (item == null || item instanceof Tree)
break;
}
return level;
}
/** Returns the label of the {@link Treecell} it contains, or null
* if no such cell.
*/
public String getLabel() {
return _treerow != null ? _treerow.getLabel() : null;
}
/** Sets the label of the {@link Treecell} it contains.
*
* <p>If treerow and treecell are not created, we automatically create it.
*
* <p>Notice that this method will create a treerow and treecell automatically
* if they don't exist. Thus, you cannot attach a treerow to it again if
* set an image or a label.
*/
public void setLabel(String label) {
autoTreerow().setLabel(label);
}
private Treerow autoTreerow() {
if (_treerow == null) {
final Treerow row = new Treerow();
row.applyProperties();
row.setParent(this);
}
return _treerow;
}
/** Returns the image of the {@link Treecell} it contains.
*/
public String getImage() {
return _treerow != null ? _treerow.getImage() : null;
}
/** Sets the image of the {@link Treecell} it contains.
*
* <p>If treerow and treecell are not created, we automatically create it.
*
* <p>Notice that this method will create a treerow and treecell automatically
* if they don't exist. Thus, you cannot attach a treerow to it again if
* set an image or a label.
*/
public void setImage(String image) {
autoTreerow().setImage(image);
}
/** Returns the parent tree item,
* or null if this item is already the top level of the tree.
* The parent tree item is actually the grandparent if any.
*
* @since 3.0.0
*/
public Treeitem getParentItem() {
final Component p = getParent();
final Component gp = p != null ? p.getParent() : null;
return gp instanceof Treeitem ? (Treeitem) gp : null;
}
/** Returns the tree owning this item.
*/
public Tree getTree() {
for (Component p = this; (p = p.getParent()) != null;)
if (p instanceof Tree)
return (Tree) p;
return null;
}
/*package*/ boolean isRealVisible() {
if (!isVisible())
return false;
Component comp = getParent();
return comp == null
|| ((comp instanceof Treechildren) ? ((Treechildren) comp).isRealVisible() : comp.isVisible());
}
//-- super --//
public boolean setVisible(boolean visible) {
if (isVisible() != visible) {
smartUpdate("visible", visible);
int count = isOpen() && _treechildren != null ? _treechildren.getVisibleItemCount() + 1 : 1;
if (visible) {
boolean result = super.setVisible(visible);
addVisibleItemCount(count);
return result;
} else {
addVisibleItemCount(-count);
return super.setVisible(visible);
}
}
return visible;
}
/**
* Returns the number of visible descendant {@link Treechildren}.
* Descendants include direct children, grand children and so on.
*
* @since 3.6.1
*/
public int getVisibleItemCount() {
return isVisible() ? 1 + (_open && _treechildren != null ? _treechildren.getVisibleItemCount() : 0) : 0;
}
/**
* adds the number of the visible item to the count of its parent.
* @param count
* @since 3.0.7
*/
void addVisibleItemCount(int count) {
Treechildren tc = (Treechildren) getParent();
if (tc != null && super.isVisible())
tc.addVisibleItemCount(count);
}
/**
* @deprecated as of release 6.0.0. To control the size of Tree related
* components, please refer to {@link Tree} and {@link Treecol} instead.
*/
public void setWidth(String width) {
// Don't remove this method, it's to override super.setWidth().
}
/**
* @deprecated as of release 6.0.0. To control the size of Tree related
* components, please refer to {@link Tree} and {@link Treecol} instead.
*/
public void setHflex(String flex) {
// Don't remove this method, it's to override super.setHflex().
}
//-- Component --//
public void beforeParentChanged(Component parent) {
if (parent != null && !(parent instanceof Treechildren))
throw new UiException("Wrong parent: " + parent);
super.beforeParentChanged(parent);
}
public void setParent(Component parent) {
final Component oldp = getParent();
if (oldp == parent)
return; //nothing changed
final Tree oldtree = oldp != null ? getTree() : null;
super.setParent(parent);
//maintain the selected status
if (oldtree != null)
oldtree.onTreeitemRemoved(this);
if (parent != null) {
final Tree tree = getTree();
if (tree != null)
tree.onTreeitemAdded(this);
}
}
public void beforeChildAdded(Component child, Component refChild) {
if (child instanceof Treerow) {
if (_treerow != null && _treerow != child)
throw new UiException("Only one treerow is allowed: " + this);
} else if (child instanceof Treechildren) {
if (_treechildren != null && _treechildren != child)
throw new UiException("Only one treechildren is allowed: " + this);
} else {
throw new UiException("Unsupported child for tree item: " + child);
}
super.beforeChildAdded(child, refChild);
}
public boolean insertBefore(Component child, Component refChild) {
if (child instanceof Treerow) {
if (super.insertBefore(child, refChild)) {
_treerow = (Treerow) child;
return true;
}
} else if (child instanceof Treechildren) {
if (super.insertBefore(child, refChild)) {
_treechildren = (Treechildren) child;
return true;
}
} else {
return super.insertBefore(child, refChild);
//impossible but more extensible
}
return false;
}
public void onChildAdded(Component child) {
super.onChildAdded(child);
if (_treechildren == child)
addVisibleItemCount(_treechildren.getVisibleItemCount());
}
public void onChildRemoved(Component child) {
if (child instanceof Treerow) {
_treerow = null;
} else if (child instanceof Treechildren) {
if (isOpen())
addVisibleItemCount(-_treechildren.getVisibleItemCount());
_treechildren = null;
}
super.onChildRemoved(child);
}
// Returns whether the treeitem should be visited.
private static boolean shallVisitTree(Tree tree, Component child) {
final Treeitem item = (Treeitem) child;
int count = item.isOpen() && item.getTreechildren() != null ? item.getTreechildren().getVisibleItemCount() : 0;
Integer visited = (Integer) tree.getAttribute(Attributes.VISITED_ITEM_COUNT);
final Paginal pgi = tree.getPaginal();
final int ofs = pgi.getActivePage() * pgi.getPageSize();
int visit = visited != null ? visited.intValue() + 1 : 1;
boolean shoulbBeVisited = ofs < visit + count;
if (visited == null)
visited = new Integer(shoulbBeVisited ? 1 : count + 1);
else
visited = new Integer(visited.intValue() + (shoulbBeVisited ? 1 : count + 1));
Integer total = (Integer) tree.getAttribute(Attributes.VISITED_ITEM_TOTAL);
if (total == null)
total = new Integer(count + 1);
else
total = new Integer(total.intValue() + count + 1);
tree.setAttribute(Attributes.VISITED_ITEM_COUNT, visited);
tree.setAttribute(Attributes.VISITED_ITEM_TOTAL, total);
return shoulbBeVisited;
}
// Returns whether the specified should be rendered.
private static boolean shallRenderTree(Tree tree) {
Integer visited = (Integer) tree.getAttribute(Attributes.VISITED_ITEM_COUNT);
final Paginal pgi = tree.getPaginal();
final int ofs = pgi.getActivePage() * pgi.getPageSize();
if (ofs < visited.intValue()) {
// count the rendered item
Integer renderedCount = (Integer) tree.getAttribute(Attributes.RENDERED_ITEM_COUNT);
if (renderedCount == null)
renderedCount = new Integer(1);
else
renderedCount = new Integer(renderedCount.intValue() + 1);
tree.setAttribute(Attributes.RENDERED_ITEM_COUNT, renderedCount);
return true;
}
return false;
}
protected void redrawChildren(Writer out) throws IOException {
Tree tree = getTree();
if (!tree.inPagingMold()) {
super.redrawChildren(out);
} else if (isRealVisible() && shallVisitTree(tree, this)) {
if (shallRenderTree(tree)) {
ComponentCtrl child = getTreerow();
if (child != null)
child.redraw(out);
}
boolean close = !isOpen();
ComponentCtrl child = getTreechildren();
if (child != null) {
if (close)
((Component) child).setAttribute(Attributes.SHALL_RENDER_ITEM, Boolean.TRUE);
child.redraw(out);
if (close)
((Component) child).removeAttribute(Attributes.SHALL_RENDER_ITEM);
}
}
}
//Cloneable//
public Object clone() {
final Treeitem clone = (Treeitem) super.clone();
int cnt = 0;
if (clone._treerow != null)
++cnt;
if (clone._treechildren != null)
++cnt;
if (cnt > 0)
clone.afterUnmarshal(cnt);
return clone;
}
private void afterUnmarshal(int cnt) {
for (Iterator it = getChildren().iterator(); it.hasNext();) {
final Object child = it.next();
if (child instanceof Treerow) {
_treerow = (Treerow) child;
if (--cnt == 0)
break;
} else if (child instanceof Treechildren) {
_treechildren = (Treechildren) child;
if (--cnt == 0)
break;
}
}
}
//-- Serializable --//
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
afterUnmarshal(-1);
}
//-- ComponentCtrl --//
protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException {
super.renderProperties(renderer);
render(renderer, "selected", isSelected());
render(renderer, "disabled", isDisabled());
if (!isOpen())
renderer.render("open", false);
if (!isSelectable())
renderer.render("checkable", false);
render(renderer, "_loadedChildren", isLoaded());
render(renderer, "_loaded", isRendered());
if (_value instanceof String)
render(renderer, "value", _value);
}
//--ComponentCtrl--//
private static HashMap<String, PropertyAccess> _properties = new HashMap<String, PropertyAccess>(2);
static {
_properties.put("_loadedChildren", new BooleanPropertyAccess() {
public void setValue(Component cmp, Boolean loadedChildren) {
((Treeitem) cmp).setLoaded(loadedChildren != null && loadedChildren);
}
public Boolean getValue(Component cmp) {
return ((Treeitem) cmp).isLoaded();
}
});
_properties.put("_loaded", new BooleanPropertyAccess() {
public void setValue(Component cmp, Boolean rendered) {
((Treeitem) cmp).setRendered(rendered != null && rendered);
}
public Boolean getValue(Component cmp) {
return ((Treeitem) cmp).isRendered();
}
});
}
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(Event, Scope)},
* it also handles onOpen.
* @since 5.0.0
*/
public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
final String cmd = request.getCommand();
if (cmd.equals(Events.ON_OPEN)) {
OpenEvent evt = OpenEvent.getOpenEvent(request);
final boolean open = evt.isOpen();
final Tree tree = getTree();
boolean hasOpenableModel = false;
if (tree != null && tree.getModel() != null) {
if (open && !isLoaded()) {
tree.renderItem(Treeitem.this);
// better client side performance with invalidate
if (_treechildren != null && _treechildren.getChildren().size() >= 5)
invalidate();
}
@SuppressWarnings("rawtypes")
TreeModel model = tree.getModel();
if (model instanceof TreeOpenableModel) {
if (open)
hasOpenableModel = ((TreeOpenableModel) model).addOpenPath(tree.getTreeitemPath(tree, this));
else
hasOpenableModel = ((TreeOpenableModel) model).removeOpenPath(tree.getTreeitemPath(tree, this));
}
}
if (!hasOpenableModel && _treechildren != null && super.isVisible()) {
if (open)
addVisibleItemCount(_treechildren.getVisibleItemCount());
else {
addVisibleItemCount(-_treechildren.getVisibleItemCount());
}
}
// Bug #3170417 the status should update after update the visibleItemCount
_open = open;
if (tree != null && tree.inPagingMold()) {
//bug ZK-2375 clear the closed children and fire a paging event to make sure
//everything on the current active page will be rendered correctly
@SuppressWarnings("rawtypes")
TreeModel model = tree.getModel();
if (!open && model != null && WebApps.getFeature("ee")) {
int activePage = tree.getActivePage();
getChildren().clear();
setRendered(false);
setLoaded(false);
Events.postEvent(new PagingEvent("onPagingImpl", (Paging) tree.getPaginal(), activePage));
}
// Bug #2838782
tree.focus();
}
Events.postEvent(evt);
} else
super.service(request, everError);
}
}