zul/src/main/java/org/zkoss/zul/AbstractTreeModel.java
/* AbstractTreeModel.java
Purpose:
Description:
History:
Aug 10 2007, Created by Jeff Liu
Copyright (C) 2005 Potix Corporation. All Rights Reserved.
{{IS_RIGHT
This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zul;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.io.Serializables;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Library;
import org.zkoss.lang.Objects;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zul.event.PagingEvent;
import org.zkoss.zul.event.PagingListener;
import org.zkoss.zul.event.TreeDataEvent;
import org.zkoss.zul.event.TreeDataListener;
import org.zkoss.zul.ext.Openable;
import org.zkoss.zul.ext.Selectable;
import org.zkoss.zul.ext.SelectionControl;
import org.zkoss.zul.ext.TreeOpenableModel;
import org.zkoss.zul.ext.TreeSelectableModel;
/**
* A skeletal implementation for {@link TreeModel}.
*
* <p>{@link AbstractTreeModel} implements both {@link TreeSelectableModel}
* and {@link TreeOpenableModel}. In other words, it stores the selection
* and open states, such that {@link Tree} and other UI can interact with.
*
* <p>In additions, {@link AbstractTreeModel} also implements
* {@link Selectable} and {@link Openable} to simplify the access
* (and provides backward compatibility to ZK 5 and earlier).
* However, these two interfaces are optional and designed for application.
* {@link Tree} and all ZK core don't access it at all.
*
* <p>For introduction, please refer to <a href=
* "http://books.zkoss.org/wiki/ZK_Developer's_Reference/MVC/Model/Tree_Model"
* >ZK Developer's Reference: Tree Model</a>.
*
* @author jumperchen
* @author tomyeh
* @since 3.0.0
*/
public abstract class AbstractTreeModel<E> implements TreeModel<E>, TreeSelectableModel, TreeOpenableModel,
Selectable<E>, Openable<E>, java.io.Serializable, PageableModel {
private static final Logger log = LoggerFactory.getLogger(AbstractTreeModel.class);
/**
* The root object to be return by method {@link #getRoot()}.
*/
private E _root;
private transient List<TreeDataListener> _listeners = new LinkedList<TreeDataListener>();
private transient List<PagingListener> _pagingListeners = new ArrayList<PagingListener>();
/** The selection. */
protected Set<Path> _selection = new LinkedHashSet<Path>();
/** The open information. */
protected Set<Path> _opens = new LinkedHashSet<Path>();
private boolean _multiple;
private SelectionControl<E> _ctrl;
public void setSelectionControl(SelectionControl ctrl) {
_ctrl = ctrl;
}
public SelectionControl getSelectionControl() {
return _ctrl;
}
/**
* Creates a {@link AbstractTreeModel}.
*
* @param root
* root of tree
*/
public AbstractTreeModel(E root) {
_root = root;
//ZK-2611: only for TreeDataEvent.INTERVAL_REMOVED and INTERVAL_ADDED:
addTreeDataListener(new TreeDataListener() {
public void onChange(TreeDataEvent event) {
updatePath(event);
invalidatePageCount();
}
});
_ctrl = createDefaultSelectionControl();
}
public List<TreeDataListener> getTreeDataListeners() {
return _listeners;
}
private SelectionControl<E> createDefaultSelectionControl() {
String clznm = Library.getProperty("org.zkoss.zul.AbstractTreeModel.DefaultSelectionControl.class");
if (clznm != null) {
try {
return (SelectionControl<E>) Classes.newInstanceByThread(clznm, new Class[] { AbstractTreeModel.class },
new Object[] { this });
} catch (Exception e) {
UiException.Aide.wrap(e, "Can't initialize SelectionControl");
}
}
return new DefaultSelectionControl<>(this);
}
private void updatePath(TreeDataEvent event) {
final int type = event.getType();
final int[] affectedPath = event.getAffectedPath();
if (affectedPath == null || affectedPath.length < 1)
return;
switch (type) {
case TreeDataEvent.INTERVAL_REMOVED:
internalDataChange(_opens, affectedPath, true);
internalDataChange(_selection, affectedPath, true);
break;
case TreeDataEvent.INTERVAL_ADDED:
internalDataChange(_opens, affectedPath, false);
internalDataChange(_selection, affectedPath, false);
break;
}
}
private void internalDataChange(Set<Path> openOrSelect, int[] affectedPath, boolean isRemoved) {
List<Path> list = new LinkedList<Path>(openOrSelect);
int update = isRemoved ? -1 : 1;
for (Iterator<Path> it = list.iterator(); it.hasNext();) {
Path p = it.next();
// p.verifyPrefix should be execute anyway
if (p.verifyPrefix(affectedPath, update) && isRemoved) {
it.remove();
}
}
openOrSelect.clear();
openOrSelect.addAll(list);
}
/**
* Return the root of the tree model.
*/
public E getRoot() {
return _root;
}
/** Sets the root of the tree model.
*/
/*package*/ void setRootDirectly(E root) {
_root = root;
}
/**
* Fires a {@link TreeDataEvent} for all registered listener
*
* <p>
* Note: you can invoke this method only in an event listener.
* @since 6.0.0
*/
public void fireEvent(int evtType, int[] path, int indexFrom, int indexTo) {
final TreeDataEvent evt = new TreeDataEvent(this, evtType, path, indexFrom, indexTo);
for (TreeDataListener l : _listeners)
l.onChange(evt);
}
/**
* Has the same functionality with {@link #fireEvent(int, int[], int, int)},
* while this is used for node removal only
*
* @since 7.0.5
*/
public void fireEvent(int evtType, int[] path, int indexFrom, int indexTo, int[] affectedPath) {
final TreeDataEvent evt = new TreeDataEvent(this, evtType, path, indexFrom, indexTo, affectedPath);
for (TreeDataListener l : _listeners)
l.onChange(evt);
}
/**
* Has the same functionality with {@link #fireEvent(int, int[], int, int)},
* while this is used for node removal only
*
* @since 10.0.0
*/
public void fireEvent(int evtType, int[] path, int indexFrom, int indexTo, int[][] affectedPaths) {
final TreeDataEvent evt = new TreeDataEvent(this, evtType, path, indexFrom, indexTo, affectedPaths);
for (TreeDataListener l : _listeners)
l.onChange(evt);
}
/**
* Fires a {@link TreeDataEvent} for all registered listener when selection
* status has changed.
*
* @since 6.0.0
*/
protected void fireSelectionChanged(int[] path) {
final TreeDataEvent evt = new TreeDataEvent(this, TreeDataEvent.SELECTION_CHANGED, path, 0, 1);
for (TreeDataListener l : _listeners)
l.onChange(evt);
}
/**
* Fires a {@link TreeDataEvent} for all registered listener when open
* status has changed.
*
* @since 6.0.0
*/
protected void fireOpenChanged(int[] path) {
final TreeDataEvent evt = new TreeDataEvent(this, TreeDataEvent.OPEN_CHANGED, path, 0, 1);
for (TreeDataListener l : _listeners)
l.onChange(evt);
}
/**
* Returns the index of child in parent. If either parent or child is null,
* returns -1. If either parent or child don't belong to this tree model,
* returns -1.
* <p>
* The default implementation iterates through all children of
* <code>parent</code> by invoking, and check if <code>child</code> is part
* of them. You could override it if you have a better algorithm.
* {@link #getChild}
*
* @param parent
* a node in the tree, obtained from this data source
* @param child
* the node we are interested in
* @return the index of the child in the parent, or -1 if either child or
* parent are null or don't belong to this tree model
* @since 5.0.6
*/
public int getIndexOfChild(E parent, E child) {
final int cnt = getChildCountOptimized(parent);
for (int j = 0; j < cnt; ++j)
if (Objects.equals(child, getChild(parent, j)))
return j;
return -1;
}
public E getChild(int[] path) {
E node = getRoot();
for (int childCount = 0, i = 0; i < path.length && node != null; i++) {
if (path[i] < 0 || path[i] > (childCount = getChildCountOptimized(node)))
return null;
node = getChild(node, path[i]);
}
return node;
}
//Retrieves the child count. Note: it won't call getChildCount if isLeaf
private int getChildCountOptimized(E parent) {
return isLeaf(parent) ? 0 : getChildCount(parent);
}
/**
* Returns the path from the specified child.
* This implementation looks for the child by traversing every possible
* child (deep-first).
* It is suggested to override this method for better performance,
* if there is a better algorithm.
* @since 6.0.0
*/
public int[] getPath(E child) {
final List<Integer> path = new ArrayList<Integer>();
dfSearch(path, getRoot(), child);
final int[] ipath = new int[path.size()];
for (int j = 0; j < ipath.length; j++)
ipath[j] = path.get(j);
return ipath;
}
private boolean dfSearch(List<Integer> path, E node, E target) {
if (node.equals(target))
return true;
for (int i = 0, size = getChildCountOptimized(node); i < size; i++)
if (dfSearch(path, getChild(node, i), target)) {
path.add(0, new Integer(i));
return true;
}
return false;
}
// -- TreeModel --//
public void addTreeDataListener(TreeDataListener l) {
_listeners.add(l);
}
public void removeTreeDataListener(TreeDataListener l) {
_listeners.remove(l);
}
//TreeSelectableModel//
public void setMultiple(boolean multiple) {
if (_multiple != multiple) {
_multiple = multiple;
fireEvent(TreeDataEvent.MULTIPLE_CHANGED, null, -1, -1);
if (!multiple && _selection.size() > 1) {
final List<Path> sels = new LinkedList<Path>();
sels.addAll(_selection);
final Path sel = sels.remove(0);
_selection.clear();
_selection.add(sel);
for (final Path path : sels)
fireSelectionChanged(path.path);
}
}
}
public boolean isMultiple() {
return _multiple;
}
// TreeSelectableModel
public boolean addSelectionPath(int[] path) {
if (path != null && path.length > 0) {
final int[][] paths = new int[1][path.length];
paths[0] = path;
return addSelectionPaths(paths);
}
return false;
}
public boolean addSelectionPaths(int[][] paths) {
boolean added = false;
final int len = paths != null ? paths.length : 0;
final boolean multiple = isMultiple();
for (int j = 0; j < len; ++j)
if (paths[j] != null) {
final Path path = new Path(paths[j]);
if (multiple) {
if (_selection.add(path)) {
added = true;
fireSelectionChanged(path.path);
}
} else {
if (!_selection.contains(path)) {
added = true;
clearSelection();
_selection.add(path);
fireSelectionChanged(path.path);
}
break; //done
}
}
return added;
}
public boolean removeSelectionPath(int[] path) {
if (path != null && path.length > 0) {
final int[][] paths = new int[1][path.length];
paths[0] = path;
return removeSelectionPaths(paths);
}
return false;
}
public boolean removeSelectionPaths(int[][] paths) {
boolean found = false;
final int len = paths != null ? paths.length : 0;
for (int j = 0; j < len && !_selection.isEmpty(); ++j) {
final Path path = new Path(paths[j]);
if (_selection.remove(path)) {
found = true;
fireSelectionChanged(path.path);
}
if (!isMultiple())
break;
}
return found;
}
public boolean isPathSelected(int[] path) {
return path != null && _selection.contains(new Path(path));
}
public int[] getSelectionPath() {
return _selection.isEmpty() ? null : _selection.iterator().next().path;
}
public int[][] getSelectionPaths() {
if (_selection.isEmpty())
return null;
final int[][] paths = new int[_selection.size()][];
int j = 0;
for (final Path path : _selection)
paths[j++] = path.path;
return paths;
}
public int getSelectionCount() {
return _selection.size();
}
public boolean isSelectionEmpty() {
return _selection.isEmpty();
}
public void clearSelection() {
if (!_selection.isEmpty()) {
final int[][] paths = getSelectionPaths();
if (paths != null) {
_selection.clear();
for (int j = 0; j < paths.length; ++j)
fireSelectionChanged(paths[j]);
}
}
}
// TreeOpenableModel //
public boolean addOpenPath(int[] path) {
if (path != null && path.length > 0) {
final int[][] paths = new int[1][path.length];
paths[0] = path;
return addOpenPaths(paths);
}
return false;
}
public boolean addOpenPaths(int[][] paths) {
boolean added = false;
final int len = paths != null ? paths.length : 0;
for (int j = 0; j < len; ++j) {
if (paths[j] != null) {
final Path path = new Path(paths[j]);
if (_opens.add(path)) {
added = true;
fireOpenChanged(path.path);
}
}
}
return added;
}
public boolean removeOpenPath(int[] path) {
if (path != null && path.length > 0) {
final int[][] paths = new int[1][path.length];
paths[0] = path;
return removeOpenPaths(paths);
}
return false;
}
public boolean removeOpenPaths(int[][] paths) {
boolean found = false;
final int len = paths != null ? paths.length : 0;
for (int j = 0; j < len && !_opens.isEmpty(); ++j) {
final Path path = new Path(paths[j]);
if (_opens.remove(path)) {
found = true;
fireOpenChanged(path.path);
}
}
return found;
}
public boolean isPathOpened(int[] path) {
return path != null && _opens.contains(new Path(path));
}
public int[] getOpenPath() {
return _opens.isEmpty() ? null : _opens.iterator().next().path;
}
public int[][] getOpenPaths() {
if (_opens.isEmpty())
return null;
final int[][] paths = new int[_opens.size()][];
int j = 0;
for (final Path path : _opens)
paths[j++] = path.path;
return paths;
}
public int getOpenCount() {
return _opens.size();
}
public boolean isOpenEmpty() {
return _opens.isEmpty();
}
public void clearOpen() {
if (!_opens.isEmpty()) {
final int[][] paths = getOpenPaths();
if (paths != null) {
_opens.clear();
for (int j = 0; j < paths.length; ++j)
fireOpenChanged(paths[j]);
}
}
}
/** A utility that the deriving class can call to save the states
* before sorting the model.
* <p>Default: saves the selection and open states.
* <p>For example, {@link DefaultTreeModel#sort} invokes it to preserve
* the selection to the same objects (rather than the same paths).
* @since 6.0.0
*/
protected Object beforeSort() {
final States<E> states = new States<E>();
for (final Path path : _selection)
states.selection.add(getChild(path.path));
for (final Path path : _opens)
states.opens.add(getChild(path.path));
return states;
}
/** A utility that the deriving class can call to restore the states
* saved by {@link #beforeSort}
* @since 6.0.0
*/
protected void afterSort(Object ctx) {
if (ctx instanceof States) {
@SuppressWarnings("unchecked")
final States<E> states = (States) ctx;
_selection.clear();
for (final E node : states.selection)
_selection.add(new Path(getPath(node)));
_opens.clear();
for (final E node : states.opens)
_opens.add(new Path(getPath(node)));
}
}
//Selectable//
public Set<E> getSelection() {
final Set<E> selected = new LinkedHashSet<E>();
int[][] paths = getSelectionPaths();
if (paths != null)
for (int i = 0; i < paths.length; i++)
selected.add(getChild(paths[i]));
return selected;
}
public void setSelection(Collection<? extends E> selection) {
if (isSelectionChanged(selection)) {
clearSelection();
for (final E node : selection)
addToSelection(node);
}
}
private boolean isSelectionChanged(Collection<? extends E> selection) {
if (_selection.size() != selection.size())
return true;
for (final E e : selection)
if (!_selection.contains(e))
return true;
return false;
}
@SuppressWarnings("unchecked")
public boolean isSelected(Object child) {
final int[] path = getPath((E) child);
if (path != null && path.length > 0)
return isPathSelected(path);
return false;
}
public boolean addToSelection(E child) {
final int[] path = getPath(child);
if (path != null && path.length > 0)
return addSelectionPath(path);
return false;
}
@SuppressWarnings("unchecked")
public boolean removeFromSelection(Object child) {
final int[] path = getPath((E) child);
if (path != null && path.length > 0)
return removeSelectionPath(path);
return false;
}
//Openable//
public Set<E> getOpenObjects() {
final Set<E> opened = new LinkedHashSet<E>();
int[][] paths = getOpenPaths();
if (paths != null)
for (int i = 0; i < paths.length; i++)
opened.add(getChild(paths[i]));
return opened;
}
public void setOpenObjects(Collection<? extends E> opened) {
clearOpen();
for (final E node : opened)
addOpenObject(node);
}
@SuppressWarnings("unchecked")
public boolean isObjectOpened(Object child) {
final int[] path = getPath((E) child);
if (path != null && path.length > 0)
return isPathOpened(path);
return false;
}
public boolean addOpenObject(E child) {
final int[] path = getPath(child);
if (path != null && path.length > 0)
return addOpenPath(path);
return false;
}
@SuppressWarnings("unchecked")
public boolean removeOpenObject(Object child) {
final int[] path = getPath((E) child);
if (path != null && path.length > 0)
return removeOpenPath(path);
return false;
}
// Serializable//
private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
s.defaultWriteObject();
Serializables.smartWrite(s, _listeners);
Serializables.smartWrite(s, _pagingListeners);
}
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
_listeners = new LinkedList<TreeDataListener>();
_pagingListeners = new ArrayList<PagingListener>();
Serializables.smartRead(s, _listeners);
Serializables.smartRead(s, _pagingListeners);
}
@SuppressWarnings("unchecked")
public Object clone() {
final AbstractTreeModel clone;
try {
clone = (AbstractTreeModel) super.clone();
} catch (CloneNotSupportedException e) {
throw new InternalError();
}
clone._listeners = new LinkedList<TreeDataListener>();
clone._pagingListeners = new ArrayList<PagingListener>();
clone._opens = new LinkedHashSet<Path>(_opens);
clone._selection = new LinkedHashSet<Path>(_selection);
return clone;
}
/** Represents a tree path.
* @since 6.0.0
*/
protected static class Path implements java.io.Serializable, Comparable {
public final int[] path;
protected Path(int[] path) {
this.path = path;
}
protected Path(Path p) {
int length = p.path.length;
this.path = new int[length];
for (int i = 0; i < length; i++) {
this.path[i] = p.path[i];
}
}
public int hashCode() {
return Objects.hashCode(path);
}
public boolean equals(Object o) {
return o instanceof Path && Objects.equals(path, ((Path) o).path);
}
public int compareTo(Object o) {
if (!(o instanceof Path))
throw new WrongValueException(o + " is not Path object");
int length = path.length;
Path toCompared = (Path) o;
int[] toPath = toCompared.path;
int toLength = toCompared.path.length;
int smaller = (length < toLength ? length : toLength);
for (int i = 0; i < smaller; i++) {
if (path[i] != toPath[i])
return path[i] - toPath[i];
}
return length - toLength;
}
private boolean verifyPrefix(int[] path) {
return verifyPrefix(path, 0);
}
/**
* Test if this object's path match the given path when removal or addition,
* and there are three situations:
* 1. if this.path is fully matched, then it should be removed or updated
* 2. if this.path match the given path except the last digit, they are
* sibling, and we have to update the value by "update"
* value of update:
* 0 -> not update
* 1 -> add one if behind prefix path (when model added)
* -1 -> remove one if behind prefix path (when model removed
* 3. if it failed matching before last digit, they are not sibling,
* and just ignore this.path
*
* Note: Because it will update this.path, be careful to use this function.
* @param path the path to be matched
* @param update
* @return boolean
* @since 7.0.5
*/
private boolean verifyPrefix(int[] path, int update) {
if (path.length > this.path.length)
return false;
int i = 0;
for (; i < path.length; i++) {
if (path[i] != this.path[i])
break;
}
if (i == path.length) { //fully match prefix, should be removed
if (update == 1) //only for add case
this.path[i - 1] += update;
return true;
} else if (i == path.length - 1) { //different on last digit, sibling
if (this.path[i] > path[i]) //this.path is later
this.path[i] += update;
return false;
} else {
return false;
}
}
public String toString() {
String result = "[";
for (int i = 0; i < path.length; i++) {
result += path[i] + ", ";
}
result = result.substring(0, result.length() - 2) + "]";
return result;
}
}
private static class States<E> {
private final Set<E> selection = new LinkedHashSet<E>();
private final Set<E> opens = new LinkedHashSet<E>();
}
// Pageable //
private int _pageSize = -1;
private int _activePage = -1;
private int _pageCount = -1; // pending calculation
// Pageable //
public int getPageSize() {
return _pageSize;
}
public void setPageSize(int size) throws WrongValueException {
if (size < 0) {
throw new WrongValueException("expecting positive non zero value, got: " + size);
}
if (_pageSize == size) return; //no change
int oldPageSize = _pageSize;
_pageSize = size;
invalidatePageCount(); //need to invalidate cached value first since we're calling getPageCount() below
//ZK-3173: increased page size might causes active page to exceed page count
//need to correct the active page value before sending out paging event
if (size > oldPageSize) {
if (_activePage > 0) {
int maxPageIndex = getPageCount() - 1;
if (_activePage > maxPageIndex) {
_activePage = maxPageIndex;
}
}
}
for (PagingListener p : _pagingListeners) {
try {
p.onEvent(new PagingEvent(PageableModel.INTERNAL_EVENT, null, this, _activePage));
} catch (Exception e) {
log.warn("Failed to publish internal paging event", e);
}
}
}
/**
* {@inheritDoc}
* <br><br>
* Note: the entire tree will be traversed once to count the number of tree nodes,
* although the result will be cached until the model has changed (open/add/remove/etc.),
* it is still a VERY EXPENSIVE operation, please @Override to provide your
* own implementation for better performance
*
* @return number of total size, or 0 if the model is empty
* @since 10.0.0
*/
public int getTotalSize() {
return getChildNodeCount(_root);
}
/**
* {@inheritDoc}
* <br><br>
* Note: the entire tree will be traversed once to count the number of tree nodes,
* although the result will be cached until the model has changed (open/add/remove/etc.),
* it is still a VERY EXPENSIVE operation, please @Override to provide your
* own implementation for better performance
*
* @return number of pages, or 1 if the model is empty
* @since 8.0.0
*/
public int getPageCount() {
if (_pageCount < 1) { // dirty/invalid value, re-calculation required
if (_root != null) {
int count = getTotalSize();
// no child or _pageSize is still default value -1, return one page anyway
if (count <= 0 || _pageSize < 0) {
_pageCount = 1;
} else {
int pageCount = count / _pageSize;
if (count % _pageSize == 0) {
_pageCount = pageCount;
} else {
_pageCount = pageCount + 1;
}
}
} else {
_pageCount = 1; // will always have at least one page
}
}
return _pageCount;
}
// count the number of child nodes, will traverse into any opened child
// nodes, depth-first style
private int getChildNodeCount(E node) {
int c = getChildCount(node);
int count = c;
for (int i = 0; i < c; i++) {
E child = getChild(node, i);
if (isPathOpened(getPath(child))) {
count += getChildNodeCount(child);
}
}
return count;
}
//ZK-1696: set to a invalid value to trigger re-calculation when getPageCount() is called
private void invalidatePageCount() {
_pageCount = -1;
}
public int getActivePage() {
return _activePage;
}
public void setActivePage(int pg) throws WrongValueException {
if (pg < 0) {
throw new WrongValueException("expecting positive non zero value, got: " + pg);
}
if (pg > 0) {
int pc = _pageCount == -1 ? getPageCount() : _pageCount;
if (pg >= pc) {
pg = pc - 1; //set to last valid page
}
}
if (_activePage == pg) return; //no change
_activePage = pg;
for (PagingListener p : _pagingListeners) {
try {
p.onEvent(new PagingEvent(PageableModel.INTERNAL_EVENT, null, this, pg));
} catch (Exception e) {
log.warn("Failed to publish internal paging event", e);
}
}
}
private List<E> getAllNodes() {
E root = getRoot();
List all = new LinkedList();
if (root != null) {
getChildNodes(all, root);
}
return all;
}
private void getChildNodes(List<E> all, E parent) {
for (int i = 0, j = getChildCount(parent); i < j; i++) {
E child = getChild(parent, i);
if (child != null) {
all.add(child);
getChildNodes(all, child);
}
}
}
/**
* A default selection control implementation for {@link AbstractTreeModel},
* by default it assumes all elements are selectable.
* <p>Note: the implementation is not used for a huge data model, if in this case,
* please implement your own one to speed up.</p>
* @since 8.0.0
*/
public static class DefaultSelectionControl<E> implements SelectionControl<E> {
private AbstractTreeModel<E> model;
public DefaultSelectionControl(AbstractTreeModel<E> model) {
this.model = model;
}
public boolean isSelectable(E e) {
return true;
}
public void setSelectAll(boolean selectAll) {
if (selectAll) {
List<E> all = new LinkedList<>();
List<E> allNodes = model.getAllNodes();
for (E o : allNodes) {
if (isSelectable(o)) // check whether it can be selectable or not
all.add(o);
}
// avoid scroll into view at client side.
model.fireEvent(TreeDataEvent.DISABLE_CLIENT_UPDATE, null, -1, -1);
try {
((Selectable<E>) model).setSelection(all);
} finally {
model.fireEvent(TreeDataEvent.ENABLE_CLIENT_UPDATE, null, -1, -1);
}
} else {
((Selectable) model).clearSelection();
}
}
public boolean isSelectAll() {
List<E> allNodes = model.getAllNodes();
for (E o : allNodes) {
if (isSelectable(o) && !((Selectable) model).isSelected(o))
return false;
}
return true;
}
}
public void addPagingEventListener(PagingListener listener) {
if (listener == null)
throw new NullPointerException();
_pagingListeners.add(listener);
}
public void removePagingEventListener(PagingListener listener) {
_pagingListeners.remove(listener);
}
}