/*
* Ext GWT 2.2.4 - Ext for GWT
* Copyright(c) 2007-2010, Ext JS, LLC.
* licensing@extjs.com
*
* http://extjs.com/license
*/
package com.extjs.gxt.ui.client.widget.treegrid;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.extjs.gxt.ui.client.GXT;
import com.extjs.gxt.ui.client.Style.SortDir;
import com.extjs.gxt.ui.client.core.El;
import com.extjs.gxt.ui.client.core.FastMap;
import com.extjs.gxt.ui.client.core.XDOM;
import com.extjs.gxt.ui.client.data.ModelData;
import com.extjs.gxt.ui.client.data.ModelIconProvider;
import com.extjs.gxt.ui.client.data.TreeLoader;
import com.extjs.gxt.ui.client.event.ComponentEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.GridEvent;
import com.extjs.gxt.ui.client.event.TreeGridEvent;
import com.extjs.gxt.ui.client.store.ListStore;
import com.extjs.gxt.ui.client.store.Record;
import com.extjs.gxt.ui.client.store.Store;
import com.extjs.gxt.ui.client.store.StoreEvent;
import com.extjs.gxt.ui.client.store.StoreListener;
import com.extjs.gxt.ui.client.store.TreeStore;
import com.extjs.gxt.ui.client.store.TreeStoreEvent;
import com.extjs.gxt.ui.client.widget.grid.ColumnModel;
import com.extjs.gxt.ui.client.widget.grid.Grid;
import com.extjs.gxt.ui.client.widget.grid.GridView;
import com.extjs.gxt.ui.client.widget.treepanel.TreeStyle;
import com.extjs.gxt.ui.client.widget.treepanel.TreePanel.Joint;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.AbstractImagePrototype;
import com.google.gwt.user.client.ui.Accessibility;
/**
* A hierarchical tree grid bound to a <code>TreeStore</code>.
*
* <p />
* A <code>TreeGridCellRenderer</code> can be assigned to the
* <code>ColumnConfig</code> in which the tree will be displayed.
*
* <p />
* With state enabled, TreePanel will save and restore the expand state of the
* nodes in the tree. A <code>ModelKeyProvider</code> must specified with the
* <code>TreeStore</code> this tree is bound to. Save and restore works with
* both local, and asynchronous loading of children.
*
* @param <M> the model type
*/
@SuppressWarnings("deprecation")
public class TreeGrid<M extends ModelData> extends Grid<M> {
public class TreeNode {
protected String id;
protected Element joint, icon;
protected M m;
private boolean childrenRendered;
private boolean expand;
private boolean expandDeep;
private boolean expanded;
private boolean leaf = true;
private boolean loaded;
private boolean loading;
public TreeNode(String id, M m) {
this.id = id;
this.m = m;
}
public void clearElements() {
joint = null;
icon = null;
}
public int getItemCount() {
return treeStore.getChildCount(m);
}
public M getModel() {
return m;
}
public TreeNode getParent() {
M p = treeStore.getParent(m);
return findNode(p);
}
public int indexOf(TreeNode child) {
M c = child.getModel();
return store.indexOf(c);
}
public boolean isExpanded() {
return expanded;
}
public boolean isLeaf() {
return !hasChildren(m);
}
public void setExpanded(boolean expand) {
TreeGrid.this.setExpanded(m, expand);
}
public void setLeaf(boolean leaf) {
this.leaf = leaf;
TreeGrid.this.refresh(m);
}
}
protected Map<M, String> cache;
protected boolean filtering;
protected TreeLoader<M> loader;
protected Map<String, TreeNode> nodes = new FastMap<TreeNode>();
protected TreeGridView treeGridView;
protected TreeStore<M> treeStore;
private boolean autoLoad, autoExpand;
private boolean caching = true;
private boolean columnLines;
private boolean expandOnFilter = true;
private ModelIconProvider<M> iconProvider;
private ListStore<M> listStore = new ListStore<M>() {
@Override
public boolean equals(M model1, M model2) {
return treeStore.equals(model1, model2);
}
@Override
public Record getRecord(M model) {
return treeStore.getRecord(model);
}
@Override
public boolean hasRecord(M model) {
return treeStore.hasRecord(model);
}
};
private StoreListener<M> storeListener = new StoreListener<M>() {
@Override
public void storeAdd(StoreEvent<M> se) {
onAdd((TreeStoreEvent<M>) se);
}
@Override
public void storeClear(StoreEvent<M> se) {
onDataChanged((TreeStoreEvent<M>) se);
}
@Override
public void storeDataChanged(StoreEvent<M> se) {
onDataChanged((TreeStoreEvent<M>) se);
}
@Override
public void storeFilter(StoreEvent<M> se) {
onFilter((TreeStoreEvent<M>) se);
}
@Override
public void storeRemove(StoreEvent<M> se) {
onRemove((TreeStoreEvent<M>) se);
}
@Override
public void storeUpdate(StoreEvent<M> se) {
onUpdate((TreeStoreEvent<M>) se);
}
};
private TreeStyle style = new TreeStyle();
private Boolean useKeyProvider = null;
@SuppressWarnings({"unchecked", "rawtypes"})
public TreeGrid(TreeStore store, ColumnModel cm) {
this.store = listStore;
this.cm = cm;
focusable = true;
baseStyle = "x-grid-panel";
this.treeStore = store;
this.loader = treeStore.getLoader();
addStyleName("x-treegrid");
disabledStyle = null;
treeStore.addStoreListener(storeListener);
setView(new TreeGridView());
disableTextSelection(true);
setSelectionModel(new TreeGridSelectionModel<M>());
}
/**
* Collapses all nodes.
*/
public void collapseAll() {
for (M child : treeStore.getRootItems()) {
setExpanded(child, false, true);
}
}
/**
* Expands all nodes.
*/
public void expandAll() {
for (M child : treeStore.getRootItems()) {
setExpanded(child, true, true);
}
}
/**
* Returns the tree node for the given target.
*
* @param target the target element
* @return the tree node or null if no match
*/
public TreeNode findNode(Element target) {
Element row = (Element) getView().findRow(target);
if (row != null) {
El item = fly(row).selectNode(".x-tree3-node");
if (item != null) {
String id = item.getId();
TreeNode node = nodes.get(id);
return node;
}
}
return null;
}
/**
* Returns the model icon provider.
*
* @return the icon provider
*/
public ModelIconProvider<M> getIconProvider() {
return iconProvider;
}
/**
* Returns the tree style.
*
* @return the tree style
*/
public TreeStyle getStyle() {
return style;
}
/**
* Returns the tree's tree store.
*
* @return the tree store
*/
public TreeStore<M> getTreeStore() {
return treeStore;
}
/**
* Returns the tree's view.
*
* @return the view
*/
public TreeGridView getTreeView() {
return treeGridView;
}
/**
* Returns true if auto expand is enabled.
*
* @return the auto expand state
*/
public boolean isAutoExpand() {
return autoExpand;
}
/**
* Returns true if auto load is enabled.
*
* @return the auto load state
*/
public boolean isAutoLoad() {
return autoLoad;
}
/**
* Returns true when a loader is queried for it's children each time a node is
* expanded. Only applies when using a loader with the tree store.
*
* @return true if caching
*/
public boolean isCaching() {
return caching;
}
/**
* Returns true if column lines are enabled.
*
* @return true if column lines are enabled
*/
public boolean isColumnLines() {
return columnLines;
}
/**
* Returns true if the model is expanded.
*
* @param model the model
* @return true if expanded
*/
public boolean isExpanded(M model) {
TreeNode node = findNode(model);
return node.isExpanded();
}
/**
* Returns the if expand all and collapse all is enabled on filter changes.
*
* @return the expand all collapse all state
*/
public boolean isExpandOnFilter() {
return expandOnFilter;
}
/**
* Returns true if the model is a leaf node. The leaf state allows a tree item
* to specify if it has children before the children have been realized.
*
* @param model the model
* @return the leaf state
*/
public boolean isLeaf(M model) {
TreeNode node = findNode(model);
return node != null && node.isLeaf();
}
@Override
public void reconfigure(ListStore<M> store, ColumnModel cm) {
throw new UnsupportedOperationException("Please call the other reconfigure method");
}
public void reconfigure(TreeStore<M> store, ColumnModel cm) {
if (isLoadMask() && rendered) {
mask(GXT.MESSAGES.loadMask_msg());
}
if (rendered) {
this.store.removeAll();
if (cache != null) {
cache.clear();
}
nodes.clear();
treeGridView.initData(this.store, cm);
treeGridView.treeStore = store;
}
if (treeStore != null) {
treeStore.removeStoreListener(storeListener);
}
loader = null;
treeStore = store;
if (treeStore != null) {
loader = treeStore.getLoader();
treeStore.addStoreListener(storeListener);
}
this.cm = cm;
// rebind the sm
setSelectionModel(sm);
if (isViewReady()) {
view.refresh(true);
doInitialLoad();
}
if (isLoadMask() && rendered) {
unmask();
}
fireEvent(Events.Reconfigure);
}
/**
* If set to true, all non leaf nodes will be expanded automatically (defaults
* to false).
*
* @param autoExpand the auto expand state to set.
*/
public void setAutoExpand(boolean autoExpand) {
this.autoExpand = autoExpand;
}
/**
* Sets whether all children should automatically be loaded recursively
* (defaults to false). Useful when the tree must be fully populated when
* initially rendered.
*
* @param autoLoad true to auto load
*/
public void setAutoLoad(boolean autoLoad) {
this.autoLoad = autoLoad;
}
/**
* Sets whether the children should be cached after first being retrieved from
* the store (defaults to true). When <code>false</code>, a load request will
* be made each time a node is expanded.
*
* @param caching the caching state
*/
public void setCaching(boolean caching) {
this.caching = caching;
}
/**
* True to enable column separation lines (defaults to false).
*
* @param columnLines true to enable column separation lines
*/
public void setColumnLines(boolean columnLines) {
this.columnLines = columnLines;
}
/**
* Sets the item's expand state.
*
* @param model the model
* @param expand true to expand
*/
public void setExpanded(M model, boolean expand) {
setExpanded(model, expand, false);
}
/**
* Sets the item's expand state.
*
* @param model the model
* @param expand true to expand
* @param deep true to expand all children recursively
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public void setExpanded(M model, boolean expand, boolean deep) {
TreeNode node = findNode(model);
if (node != null) {
if (expand) {
// make parents visible
List<M> list = new ArrayList<M>();
M p = model;
while ((p = treeStore.getParent(p)) != null) {
if (!findNode(p).isExpanded()) {
list.add(p);
}
}
for (int i = list.size() - 1; i >= 0; i--) {
M item = list.get(i);
setExpanded(item, expand, false);
}
}
TreeGridEvent<M> tge = new TreeGridEvent<M>(this);
tge.setModel(model);
if (expand) {
if (!node.isLeaf()) {
// if we are loading, ignore it
if (node.loading) {
return;
}
// if we have a loader and node is not loaded make
// load request and exit method
if (!node.expanded && loader != null && (!node.loaded || !caching) && !filtering) {
treeStore.removeAll(model);
node.expand = true;
node.expandDeep = deep;
node.loading = true;
treeGridView.onLoading(node);
loader.loadChildren(model);
return;
}
if (!node.expanded && fireEvent(Events.BeforeExpand, tge)) {
node.expanded = true;
if (!node.childrenRendered) {
renderChildren(model, false);
node.childrenRendered = true;
}
// expand
treeGridView.expand(node);
if (isStateful() && treeStore.getKeyProvider() != null) {
Map<String, Object> state = getState();
List<String> expanded = (List) state.get("expanded");
if (expanded == null) {
expanded = new ArrayList<String>();
state.put("expanded", expanded);
}
String id = treeStore.getKeyProvider().getKey(model);
if (!expanded.contains(id)) {
expanded.add(id);
saveState();
}
}
fireEvent(Events.Expand, tge);
}
if (deep) {
setExpandChildren(model, true);
} else {
statefulExpand(treeStore.getChildren(model));
}
}
} else {
if (node.expanded && fireEvent(Events.BeforeCollapse, tge)) {
node.expanded = false;
// collapse
treeGridView.collapse(node);
if (isStateful() && treeStore.getKeyProvider() != null) {
Map<String, Object> state = getState();
List<String> expanded = (List) state.get("expanded");
String id = treeStore.getKeyProvider().getKey(model);
if (expanded != null && expanded.contains(id)) {
expanded.remove(id);
saveState();
}
}
fireEvent(Events.Collapse, tge);
}
if (deep) {
setExpandChildren(model, false);
}
}
}
}
/**
* Sets whether the tree should expand all and collapse all when filters are
* applied (defaults to true).
*
* @param expandOnFilter true to expand and collapse on filter changes
*/
public void setExpandOnFilter(boolean expandOnFilter) {
this.expandOnFilter = expandOnFilter;
}
/**
* Sets the tree's model icon provider which provides the icon style for each
* model.
*
* @param iconProvider the icon provider
*/
public void setIconProvider(ModelIconProvider<M> iconProvider) {
this.iconProvider = iconProvider;
}
/**
* Sets the item's leaf state. The leaf state allows control of the expand
* icon before the children have been realized.
*
* @param model the model
* @param leaf the leaf state
*/
public void setLeaf(M model, boolean leaf) {
TreeNode t = findNode(model);
if (t != null) {
t.setLeaf(leaf);
}
}
@Override
public void setView(GridView view) {
assert view instanceof TreeGridView : "The view for a TreeGrid has to be an instance of TreeGridView";
super.setView(view);
treeGridView = (TreeGridView) view;
}
/**
* Toggles the model's expand state.
*
* @param model the model
*/
public void toggle(M model) {
TreeNode node = findNode(model);
if (node != null) {
setExpanded(model, !node.expanded);
}
}
protected Joint calcualteJoint(M model) {
if (model == null) {
return Joint.NONE;
}
TreeNode node = findNode(model);
Joint joint = Joint.NONE;
if (node == null) {
return joint;
}
if (!node.isLeaf()) {
boolean children = true;
if (node.isExpanded()) {
joint = children ? Joint.EXPANDED : Joint.NONE;
} else {
joint = children ? Joint.COLLAPSED : Joint.NONE;
}
}
return joint;
}
protected AbstractImagePrototype calculateIconStyle(M model) {
AbstractImagePrototype style = null;
if (iconProvider != null) {
AbstractImagePrototype iconStyle = iconProvider.getIcon((M) model);
if (iconStyle != null) {
return iconStyle;
}
}
TreeNode node = findNode(model);
TreeStyle ts = getStyle();
if (!node.isLeaf()) {
if (isExpanded(model)) {
style = ts.getNodeOpenIcon();
} else {
style = ts.getNodeCloseIcon();
}
} else {
style = ts.getLeafIcon();
}
return style;
}
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
protected ComponentEvent createComponentEvent(Event event) {
return new TreeGridEvent(this, event);
}
@Override
protected void doApplyStoreState(Map<String, Object> state) {
String sortField = (String) state.get("sortField");
if (treeStore.getLoader() == null && sortField != null) {
String sortDir = (String) state.get("sortDir");
SortDir dir = SortDir.findDir(sortDir);
treeStore.sort(sortField, dir);
}
}
protected int findLastOpenChildIndex(M model) {
TreeNode mark = findNode(model);
M lc = model;
while (mark != null && mark.expanded) {
M m = treeStore.getLastChild(mark.m);
if (m != null) {
lc = m;
mark = findNode(lc);
} else {
break;
}
}
return store.indexOf(lc);
}
protected TreeNode findNode(M m) {
if (m == null || useKeyProvider == null) return null;
return nodes.get(useKeyProvider ? generateModelId(m) : cache.get(m));
}
protected String generateModelId(M m) {
return getId() + "_"
+ (treeStore.getKeyProvider() != null ? treeStore.getKeyProvider().getKey(m) : XDOM.getUniqueId());
}
protected boolean hasChildren(M m) {
TreeNode node = findNode(m);
if (loader != null && !node.loaded) {
return loader.hasChildren(node.getModel());
}
if (!node.leaf || treeStore.hasChildren(node.getModel())) {
return true;
}
return false;
}
protected void onAdd(TreeStoreEvent<M> se) {
if (viewReady) {
M p = se.getParent();
if (p == null) {
for (M child : se.getChildren()) {
register(child);
}
if (se.getIndex() > 0) {
M prev = treeStore.getChild(se.getIndex() - 1);
int index = findLastOpenChildIndex(prev);
store.insert(se.getChildren(), index + 1);
} else {
store.insert(se.getChildren(), se.getIndex());
}
} else {
TreeNode node = findNode(p);
if (node != null) {
for (M child : se.getChildren()) {
register(child);
}
if (!node.expanded) {
refresh(p);
return;
}
int index = se.getIndex();
if (index == 0) {
int pindex = store.indexOf(p);
store.insert(se.getChildren(), pindex + 1);
} else {
index = store.indexOf(treeStore.getChild(p, index - 1));
TreeNode mark = findNode(store.getAt(index));
index = findLastOpenChildIndex(mark.m);
store.insert(se.getChildren(), index + 1);
}
refresh(p);
}
}
}
}
@Override
protected void onAfterRenderView() {
super.onAfterRenderView();
doInitialLoad();
}
@Override
protected void onClick(GridEvent<M> e) {
M m = e.getModel();
if (m != null) {
TreeNode node = findNode(m);
if (node != null) {
Element jointEl = treeGridView.getJointElement(node);
if (jointEl != null && e.within(jointEl)) {
toggle(m);
} else {
super.onClick(e);
}
}
}
}
protected void onDataChanged(TreeStoreEvent<M> se) {
if (!isRendered() || !viewReady) {
return;
}
M p = se.getParent();
if (p == null) {
store.removeAll();
if (cache != null) {
cache.clear();
}
nodes.clear();
renderChildren(null, autoLoad);
statefulExpand(treeStore.getRootItems());
} else {
TreeNode n = findNode(p);
n.loaded = true;
n.loading = false;
renderChildren(p, autoLoad);
if (n.expand && !n.isLeaf()) {
n.expand = false;
boolean deep = n.expandDeep;
n.expandDeep = false;
boolean c = caching;
caching = true;
setExpanded(p, true, deep);
caching = c;
} else {
refresh(p);
}
}
}
@Override
protected void onDoubleClick(GridEvent<M> e) {
super.onDoubleClick(e);
toggle(e.getModel());
}
protected void onFilter(TreeStoreEvent<M> se) {
onDataChanged(se);
if (expandOnFilter && treeStore.isFiltered()) {
expandAll();
}
}
protected void onRemove(TreeStoreEvent<M> se) {
if (viewReady) {
unregister(se.getChild());
store.remove(se.getChild());
for (M child : se.getChildren()) {
unregister(child);
store.remove(child);
}
TreeNode p = findNode(se.getParent());
if (p != null && p.expanded && p.getItemCount() == 0) {
setExpanded(p.m, false);
} else if (p != null && p.getItemCount() == 0) {
refresh(se.getParent());
}
}
}
@Override
protected void onRender(Element target, int index) {
super.onRender(target, index);
el().setTabIndex(0);
el().setElementAttribute("hideFocus", "true");
if (columnLines) {
addStyleName("x-grid-with-col-lines");
}
Accessibility.setRole(getElement(), "treegrid");
sinkEvents(Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS | Event.KEYEVENTS);
}
protected void onUpdate(TreeStoreEvent<M> se) {
store.update(se.getModel());
store.fireEvent(Store.Update, se);
}
protected void refresh(M model) {
TreeNode node = findNode(model);
if (rendered && viewReady && node != null) {
AbstractImagePrototype style = calculateIconStyle(model);
treeGridView.onIconStyleChange(node, style);
Joint j = calcualteJoint(model);
treeGridView.onJointChange(node, j);
}
}
protected String register(M m) {
if (useKeyProvider == null) {
if (treeStore.getKeyProvider() == null) {
useKeyProvider = false;
} else {
useKeyProvider = true;
}
}
if (!useKeyProvider) {
if (cache == null) {
cache = new HashMap<M, String>();
}
String id = cache.get(m);
if (id == null) {
id = generateModelId(m);
cache.put(m, id);
nodes.put(id, new TreeNode(id, m));
}
return id;
}
String id = generateModelId(m);
if (!nodes.containsKey(id)) {
nodes.put(id, new TreeNode(id, m));
}
return id;
}
protected void renderChildren(M parent, boolean auto) {
List<M> children = parent == null ? treeStore.getRootItems() : treeStore.getChildren(parent);
for (M child : children) {
register(child);
}
if (parent == null) {
store.add(children);
}
for (M child : children) {
if (autoExpand) {
final M c = child;
DeferredCommand.addCommand(new Command() {
public void execute() {
setExpanded(c, true);
}
});
} else if (loader != null) {
if (autoLoad) {
if (store.isFiltered() || (!auto)) {
renderChildren(child, auto);
} else {
loader.loadChildren(child);
}
}
}
}
}
protected void unregister(M m) {
TreeNode node = null;
if (m != null && useKeyProvider != null && (node = findNode(m)) != null) {
node.clearElements();
nodes.remove(useKeyProvider ? generateModelId(m) : cache.remove(m));
TreeGridEvent<M> e = new TreeGridEvent<M>(this);
e.setModel(m);
e.setTreeNode(node);
fireEvent(Events.Unregister, e);
}
}
private void doInitialLoad() {
if (treeStore.getRootItems().size() == 0 && loader != null) {
loader.load();
} else {
renderChildren(null, false);
if (autoExpand) {
expandAll();
} else {
statefulExpand(treeStore.getRootItems());
}
}
}
private void setExpandChildren(M m, boolean expand) {
for (M child : treeStore.getChildren(m)) {
setExpanded(child, expand, true);
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
private void statefulExpand(List<M> children) {
if (isStateful() && treeStore.getKeyProvider() != null) {
List<String> expanded = (List) getState().get("expanded");
if (expanded != null && expanded.size() > 0) {
for (M child : children) {
String id = treeStore.getKeyProvider().getKey(child);
if (expanded.contains(id)) {
setExpanded(child, true);
}
}
}
}
}
}