/**
* Sencha GXT 3.1.0-beta - Sencha for GWT
* Copyright(c) 2007-2014, Sencha, Inc.
* licensing@sencha.com
*
* http://www.sencha.com/products/gxt/license/
*/
package com.sencha.gxt.widget.core.client.treegrid;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.google.gwt.cell.client.Cell;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.client.Event;
import com.sencha.gxt.core.client.ValueProvider;
import com.sencha.gxt.core.client.dom.XDOM;
import com.sencha.gxt.core.client.dom.XElement;
import com.sencha.gxt.core.shared.FastMap;
import com.sencha.gxt.data.shared.IconProvider;
import com.sencha.gxt.data.shared.ListStore;
import com.sencha.gxt.data.shared.ModelKeyProvider;
import com.sencha.gxt.data.shared.PropertyAccess;
import com.sencha.gxt.data.shared.TreeStore;
import com.sencha.gxt.data.shared.event.StoreAddEvent;
import com.sencha.gxt.data.shared.event.StoreClearEvent;
import com.sencha.gxt.data.shared.event.StoreDataChangeEvent;
import com.sencha.gxt.data.shared.event.StoreFilterEvent;
import com.sencha.gxt.data.shared.event.StoreHandlers;
import com.sencha.gxt.data.shared.event.StoreRecordChangeEvent;
import com.sencha.gxt.data.shared.event.StoreRemoveEvent;
import com.sencha.gxt.data.shared.event.StoreSortEvent;
import com.sencha.gxt.data.shared.event.StoreUpdateEvent;
import com.sencha.gxt.data.shared.event.TreeStoreRemoveEvent;
import com.sencha.gxt.data.shared.loader.TreeLoader;
import com.sencha.gxt.messages.client.DefaultMessages;
import com.sencha.gxt.widget.core.client.event.BeforeCollapseItemEvent;
import com.sencha.gxt.widget.core.client.event.BeforeCollapseItemEvent.BeforeCollapseItemHandler;
import com.sencha.gxt.widget.core.client.event.BeforeCollapseItemEvent.HasBeforeCollapseItemHandlers;
import com.sencha.gxt.widget.core.client.event.BeforeExpandItemEvent;
import com.sencha.gxt.widget.core.client.event.BeforeExpandItemEvent.BeforeExpandItemHandler;
import com.sencha.gxt.widget.core.client.event.BeforeExpandItemEvent.HasBeforeExpandItemHandlers;
import com.sencha.gxt.widget.core.client.event.CollapseItemEvent;
import com.sencha.gxt.widget.core.client.event.CollapseItemEvent.CollapseItemHandler;
import com.sencha.gxt.widget.core.client.event.CollapseItemEvent.HasCollapseItemHandlers;
import com.sencha.gxt.widget.core.client.event.ExpandItemEvent;
import com.sencha.gxt.widget.core.client.event.ExpandItemEvent.ExpandItemHandler;
import com.sencha.gxt.widget.core.client.event.ExpandItemEvent.HasExpandItemHandlers;
import com.sencha.gxt.widget.core.client.grid.ColumnConfig;
import com.sencha.gxt.widget.core.client.grid.ColumnModel;
import com.sencha.gxt.widget.core.client.grid.Grid;
import com.sencha.gxt.widget.core.client.grid.GridView;
import com.sencha.gxt.widget.core.client.grid.GridView.GridAppearance;
import com.sencha.gxt.widget.core.client.tree.Tree.Joint;
import com.sencha.gxt.widget.core.client.tree.Tree.TreeAppearance;
import com.sencha.gxt.widget.core.client.tree.Tree.TreeNode;
import com.sencha.gxt.widget.core.client.tree.TreeStyle;
/**
* A {@link TreeGrid} provides support for displaying and editing hierarchical data where each item may contain multiple
* properties. The tree grid gets its data from a {@link TreeStore} and its column definitions from a
* {@link ColumnModel}. Each model in the store is rendered as an item in the tree. The fields in the model provide the
* data for each column associated with the item. Any updates to the store are automatically pushed to the tree grid.
* This includes inserting, removing, sorting and filter.
* <p/>
* In GXT version 3, {@link ModelKeyProvider}s and {@link ValueProvider}s provide the interface between your data model
* and the list store and {@link ColumnConfig} classes. This enables a tree grid to work with data of any object type.
* <p/>
* You can provide your own implementation of these interfaces, or you can use a Sencha supplied generator to create
* them for you automatically. A generator runs at compile time to create a Java class that is compiled to JavaScript.
* The Sencha supplied generator can create classes for interfaces that extend the {@link PropertyAccess} interface. The
* generator transparently creates the class at compile time and the {@link GWT#create(Class)} method returns an
* instance of that class at run time. The generated class is managed by GWT and GXT and you generally do not need to
* worry about what the class is called, where it is located, or other similar details.
* <p/>
* Each tree grid has a {@link GridView}. The grid view provides many options for customizing the grid's appearance
* (e.g. striping, mouse-over tracking, empty text). To set these options, get the current grid view using
* {@link TreeGrid#getView()} and then set the desired option on the grid view.
* <p/>
* To customize the appearance of a column in a tree grid, provide a cell implementation using
* {@link ColumnConfig#setCell(Cell)}.
* <p/>
* Tree grids support several ways to manage column widths:
* <ol>
* <li>The most basic approach is to simply give pixel widths to each column. Columns widths will match the specified
* values.</li>
* <li>A column can be identified as an auto-expand column. As the width of the tree grid changes, or columns are
* resized, the specified column's width is adjusted so that the column fills the available width with no horizontal
* scrolling. See @link {@link GridView#setAutoExpandColumn(ColumnConfig)}.</li>
* <li>The tree grid can resize columns based on relative weights, determined by the pixel width assigned to each
* column. As the width of the tree grid or columns change, the weight is used to allocate the available space. Use
* {@link GridView#setAutoFill(boolean)} or {@link GridView#setForceFit(boolean)} to enable this feature:</li>
* <ul>
* <li>With auto fill, the calculations are run when the tree grid is created (or reconfigured). After the tree grid is
* rendered, the column widths will not be adjusted when the available width changes.</li>
* <li>With force fit the width calculations are run every time there are changes to the available width or column
* sizes.</li>
* </ul>
* <li>To prevent a column from participating in auto fill or force fit, use {@link ColumnConfig#setFixed(boolean)}.</li>
* </ol>
* </p>
* The following code snippet illustrates the creation of a simple tree grid with local data for test purposes. For more
* practical examples that show how to load data from remote sources, see the Async TreeGrid example in the online
* Explorer demo.</p>
*
* <pre>{@code
// Generate the key provider and value provider for the Data class
DataProperties dp = GWT.create(DataProperties.class);
// Create the configurations for each column in the tree grid
List<ColumnConfig<Data, ?>> ccs = new LinkedList<ColumnConfig<Data, ?>>();
ccs.add(new ColumnConfig<Data, String>(dp.name(), 200, "Name"));
ccs.add(new ColumnConfig<Data, String>(dp.value(), 200, "Value"));
ColumnModel<Data> cm = new ColumnModel<Test.Data>(ccs);
// Create the store that the contains the data to display in the tree grid
TreeStore<Data> s = new TreeStore<Test.Data>(dp.key());
Data r1 = new Data("Parent 1", "value1");
s.add(r1);
s.add(r1, new Data("Child 1.1", "value2"));
s.add(r1, new Data("Child 1.2", "value3"));
Data r2 = new Data("Parent 2", "value4");
s.add(r2);
s.add(r2, new Data("Child 2.1", "value5"));
s.add(r2, new Data("Child 2.2", "value6"));
// Create the tree grid using the store, column model and column config for the tree column
TreeGrid<Data> tg = new TreeGrid<Data>(s, cm, ccs.get(0));
// Add the tree to a container
RootPanel.get().add(tg);
* }</pre>
* <p/>
* To use the Sencha supplied generator to create model key providers and value providers, extend the
* <code>PropertyAccess</code> interface, parameterized with the type of data you want to access (as shown below) and
* invoke the <code>GWT.create</code> method on its <code>class</code> member (as shown in the code snippet above). This
* creates an instance of the class that can be used to initialize the tree and tree store. In the following code
* snippet we define a new interface called <code>DataProperties</code> that extends the <code>PropertyAccess</code>
* interface and is parameterized with <code>Data</code>, a Plain Old Java Object (POJO).
* <p/>
*
* <pre>
public interface DataProperties extends PropertyAccess<Data> {
@Path("name")
ModelKeyProvider<Data> key();
ValueProvider<Data, String> name();
ValueProvider<Data, String> value();
}
public class Data {
private String name;
private String value;
public Data(String name, String value) {
super();
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
public void setName(String name) {
this.name = name;
}
public void setValue(String value) {
this.value = value;
}
}
* </pre>
* <p/>
* To enable drag and drop for a tree grid, add the following:
* <p/>
*
* <pre>
new TreeGridDragSource<Data>(tg);
TreeGridDropTarget<Data> dt = new TreeGridDropTarget<Data>(tg);
dt.setFeedback(Feedback.BOTH);
* </pre>
* <p/>
* To add reordering support to the drag and drop, include:
*
* <pre>
dt.setAllowSelfAsSource(true);
* </pre>
* <p/>
*
* @param <M> the model type
*/
public class TreeGrid<M> extends Grid<M> implements HasBeforeCollapseItemHandlers<M>, HasCollapseItemHandlers<M>,
HasBeforeExpandItemHandlers<M>, HasExpandItemHandlers<M> {
public static class TreeGridNode<M> extends TreeNode<M> {
protected TreeGridNode(String modelId, M m, String domId) {
super(modelId, m, domId);
}
@Override
public void clearElements() {
super.clearElements();
setContainerElement(null);
setElContainer(null);
element = null;
}
}
protected boolean filtering;
protected TreeLoader<M> loader;
protected Map<String, TreeNode<M>> nodes = new FastMap<TreeNode<M>>();
protected Map<String, TreeNode<M>> nodesByDomId = new FastMap<TreeNode<M>>();
protected StoreHandlers<M> storeHandler = new StoreHandlers<M>() {
@Override
public void onAdd(StoreAddEvent<M> event) {
TreeGrid.this.onAdd(event);
}
@Override
public void onClear(StoreClearEvent<M> event) {
TreeGrid.this.onClear(event);
}
@Override
public void onDataChange(StoreDataChangeEvent<M> event) {
TreeGrid.this.onDataChange(event.getParent());
}
@Override
public void onFilter(StoreFilterEvent<M> event) {
TreeGrid.this.onFilter(event);
}
@Override
public void onRecordChange(StoreRecordChangeEvent<M> event) {
TreeGrid.this.onRecordChange(event);
}
@Override
public void onRemove(StoreRemoveEvent<M> event) {
TreeGrid.this.onRemove((TreeStoreRemoveEvent<M>) event);
}
@Override
public void onSort(StoreSortEvent<M> event) {
TreeGrid.this.onSort(event);
}
@Override
public void onUpdate(StoreUpdateEvent<M> event) {
TreeGrid.this.onUpdate(event);
}
};
protected HandlerRegistration storeHandlerRegistration;
protected TreeGridView<M> treeGridView;
protected TreeStore<M> treeStore;
private final GridAppearance appearance;
private boolean autoLoad, autoExpand;
private boolean caching = true;
private boolean expandOnFilter = true;
private IconProvider<M> iconProvider;
private TreeStyle style = new TreeStyle();
private TreeAppearance treeAppearance;
private ColumnConfig<M, ?> treeColumn;
private boolean expandOnDoubleClick = true;
/**
* Creates a new tree grid.
*
* @param store the tree store
* @param cm the column model
* @param treeColumn the tree column
*/
public TreeGrid(TreeStore<M> store, ColumnModel<M> cm, ColumnConfig<M, ?> treeColumn) {
this(store, cm, treeColumn, GWT.<GridAppearance> create(GridAppearance.class));
}
/**
* Creates a new tree grid.
*
* @param store the tree store
* @param cm the column model
* @param treeColumn the tree column
* @param appearance the grid appearance
*/
public TreeGrid(TreeStore<M> store, ColumnModel<M> cm, ColumnConfig<M, ?> treeColumn, GridAppearance appearance) {
this(store, cm, treeColumn, appearance, GWT.<TreeAppearance> create(TreeAppearance.class));
}
/**
* Creates a new tree grid.
*
* @param store the tree store
* @param cm the column model
* @param treeColumn the tree column
* @param appearance the grid appearance
* @param treeAppearance the tree appearance
*/
public TreeGrid(TreeStore<M> store, ColumnModel<M> cm, ColumnConfig<M, ?> treeColumn, GridAppearance appearance,
TreeAppearance treeAppearance) {
this.appearance = appearance;
this.treeAppearance = treeAppearance;
disabledStyle = null;
SafeHtmlBuilder builder = new SafeHtmlBuilder();
this.appearance.render(builder);
setElement((Element) XDOM.create(builder.toSafeHtml()));
getElement().makePositionable();
// Do not remove, this is being used in Grid.css
addStyleName("x-treegrid");
getElement().setTabIndex(0);
getElement().setAttribute("hideFocus", "true");
this.cm = cm;
setTreeColumn(treeColumn);
this.treeStore = store;
this.store = createListStore();
setSelectionModel(new TreeGridSelectionModel<M>());
disabledStyle = null;
storeHandlerRegistration = treeStore.addStoreHandlers(storeHandler);
setView(new TreeGridView<M>(appearance));
setAllowTextSelection(false);
sinkCellEvents();
}
@Override
public HandlerRegistration addBeforeCollapseHandler(BeforeCollapseItemHandler<M> handler) {
return addHandler(handler, BeforeCollapseItemEvent.getType());
}
@Override
public HandlerRegistration addBeforeExpandHandler(BeforeExpandItemHandler<M> handler) {
return addHandler(handler, BeforeExpandItemEvent.getType());
}
@Override
public HandlerRegistration addCollapseHandler(CollapseItemHandler<M> handler) {
return addHandler(handler, CollapseItemEvent.getType());
}
@Override
public HandlerRegistration addExpandHandler(ExpandItemHandler<M> handler) {
return addHandler(handler, ExpandItemEvent.getType());
}
/**
* 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<M> findNode(Element target) {
Element row = (Element) getView().findRow(target);
if (row != null) {
XElement item = XElement.as(row).selectNode(treeAppearance.itemSelector());
if (item != null) {
String id = item.getId();
TreeNode<M> node = nodesByDomId.get(id);
return node;
}
}
return null;
}
/**
* Returns the grid appearance.
*
* @return the grid appearance
*/
public GridAppearance getAppearance() {
return appearance;
}
/**
* Returns the model icon provider.
*
* @return the icon provider
*/
public IconProvider<M> getIconProvider() {
return iconProvider;
}
/**
* Returns the tree style.
*
* @return the tree style
*/
public TreeStyle getStyle() {
return style;
}
/**
* Returns the tree appearance.
*
* @return the tree appearance
*/
public TreeAppearance getTreeAppearance() {
return treeAppearance;
}
/**
* Returns the column that represents the tree nodes.
*
* @return the tree column
*/
public ColumnConfig<M, ?> getTreeColumn() {
return treeColumn;
}
/**
* Returns the tree loader.
*
* @return the tree loader or null if not specified
*/
public TreeLoader<M> getTreeLoader() {
return loader;
}
/**
* 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<M> 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 the model is expanded.
*
* @param model the model
* @return true if expanded
*/
public boolean isExpanded(M model) {
TreeNode<M> node = findNode(model);
return node != null && node.isExpanded();
}
/**
* Returns the expand on double click state.
*
* @return the expand on double click state
*/
public boolean isExpandOnDoubleClick() {
return expandOnDoubleClick;
}
/**
* 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) {
return !hasChildren(model);
}
@Override
public void reconfigure(ListStore<M> store, ColumnModel<M> cm) {
throw new UnsupportedOperationException("Please call the other reconfigure method");
}
public void reconfigure(TreeStore<M> store, ColumnModel<M> cm, ColumnConfig<M, ?> treeColumn) {
if (isLoadMask()) {
mask(DefaultMessages.getMessages().loadMask_msg());
}
this.store.clear();
nodes.clear();
nodesByDomId.clear();
this.store = createListStore();
if (storeHandlerRegistration != null) {
storeHandlerRegistration.removeHandler();
}
treeStore = store;
if (treeStore != null) {
storeHandlerRegistration = treeStore.addStoreHandlers(storeHandler);
}
treeGridView.initData(this.store, cm);
this.cm = cm;
setTreeColumn(treeColumn);
// rebind the sm
setSelectionModel(sm);
if (isViewReady()) {
view.refresh(true);
doInitialLoad();
}
if (isLoadMask()) {
unmask();
}
}
/**
* Refreshes the data for the given model.
*
* @param model the model to be refreshed
*/
public void refresh(M model) {
TreeNode<M> node = findNode(model);
if (viewReady && node != null) {
treeGridView.onIconStyleChange(node, calculateIconStyle(model));
treeGridView.onJointChange(node, calculateJoint(model));
}
}
/**
* 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;
}
/**
* 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
*/
public void setExpanded(M model, boolean expand, boolean deep) {
if (expand) {
// make parents visible
List<M> list = new ArrayList<M>();
M p = model;
while ((p = treeStore.getParent(p)) != null) {
TreeNode<M> n = findNode(p);
if (n == null || !n.isExpanded()) {
list.add(p);
}
}
for (int i = list.size() - 1; i >= 0; i--) {
M item = list.get(i);
setExpanded(item, expand, false);
}
}
TreeNode<M> node = findNode(model);
if (node == null) {
assert !expand;
return;
}
if (expand) {
if (!isLeaf(model)) {
// if we are loading, ignore it
if (node.isLoading()) {
return;
}
// if we have a loader and node is not loaded make
// load request and exit method
if (!node.isExpanded() && loader != null && (!node.isLoaded() || !caching) && !filtering) {
treeStore.removeChildren(model);
node.setExpand(true);
node.setExpandDeep(deep);
node.setLoading(true);
treeGridView.onLoading(node);
loader.loadChildren(model);
return;
}
if (!node.isExpanded() && fireCancellableEvent(new BeforeExpandItemEvent<M>(model))) {
node.setExpanded(true);
if (!node.isChildrenRendered()) {
renderChildren(model, false);
node.setChildrenRendered(true);
}
// expand
treeGridView.expand(node);
fireEvent(new ExpandItemEvent<M>(model));
}
if (deep) {
setExpandChildren(model, true);
}
}
} else {
if (node.isExpanded() && fireCancellableEvent(new BeforeCollapseItemEvent<M>(model))) {
node.setExpanded(false);
// collapse
treeGridView.collapse(node);
fireEvent(new CollapseItemEvent<M>(model));
}
if (deep) {
setExpandChildren(model, false);
}
}
}
/**
* Determines if the nodes should be expanded and collapsed on double clicks (defaults to {@code true}). Set to false
* when using two clicks to edit inline editing.
*
* @param expandOnDoubleClick true to expand and collapse on double clicks
*/
public void setExpandOnDoubleClick(boolean expandOnDoubleClick) {
this.expandOnDoubleClick = expandOnDoubleClick;
}
/**
* 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(IconProvider<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<M> t = findNode(model);
if (t != null) {
t.setLeaf(leaf);
}
}
/**
* Sets the tree loader.
*
* @param treeLoader the tree loader
*/
public void setTreeLoader(TreeLoader<M> treeLoader) {
this.loader = treeLoader;
}
@Override
public void setView(GridView<M> view) {
assert view instanceof TreeGridView : "The view for a TreeGrid has to be an instance of TreeGridView";
super.setView(view);
treeGridView = (TreeGridView<M>) view;
}
/**
* Toggles the model's expand state.
*
* @param model the model
*/
public void toggle(M model) {
TreeNode<M> node = findNode(model);
if (node != null) {
setExpanded(model, !node.isExpanded());
}
}
protected ImageResource calculateIconStyle(M model) {
ImageResource style = null;
if (iconProvider != null) {
ImageResource iconStyle = iconProvider.getIcon(model);
if (iconStyle != null) {
return iconStyle;
}
}
TreeStyle ts = getStyle();
if (!isLeaf(model)) {
if (isExpanded(model)) {
style = ts.getNodeOpenIcon() != null ? ts.getNodeOpenIcon() : treeAppearance.openNodeIcon();
} else {
style = ts.getNodeCloseIcon() != null ? ts.getNodeCloseIcon() : treeAppearance.closeNodeIcon();
}
} else {
style = ts.getLeafIcon();
}
return style;
}
protected Joint calculateJoint(M model) {
if (model == null) {
return Joint.NONE;
}
TreeNode<M> node = findNode(model);
Joint joint = Joint.NONE;
if (node == null) {
return joint;
}
if (!isLeaf(model)) {
boolean children = true;
if (node.isExpanded()) {
joint = children ? Joint.EXPANDED : Joint.NONE;
} else {
joint = children ? Joint.COLLAPSED : Joint.NONE;
}
}
return joint;
}
protected ListStore<M> createListStore() {
return new ListStore<M>(treeStore.getKeyProvider()) {
@Override
public Record getRecord(M model) {
return treeStore.getRecord(model);
}
@Override
public boolean hasRecord(M model) {
return treeStore.hasRecord(model);
}
};
}
/**
* Returns the index in the liststore of the last child in the subtree starting at the given model. The given model
* (or its children) do not need to already be in the list store, but all other items do. This makes this method
* useful for finding where a model and its subtree belong.
*
* @param model the model used to find the last open child index.
* @return returns the last open child index.
*/
protected int findLastOpenChildIndex(M model) {
// make sure this node is visible
assert treeStore.indexOf(model) != -1;
// Find the next visible node parallel to or above this one
M outer = model;
M nextSibling = treeStore.getNextSibling(outer);
while (nextSibling == null) {
outer = treeStore.getParent(outer);
// check if we are at the root
if (outer == null) {
break;
}
nextSibling = treeStore.getNextSibling(outer);
}
if (outer == null) {
// hit the root, return the last element
return store.size() - 1;
}
// otherwise, we've got the next element - get _its_ index in the liststore, and return that minus one
return store.indexOf(nextSibling) - 1;
}
protected TreeNode<M> findNode(M m) {
if (m == null) return null;
return nodes.get(generateModelId(m));
}
protected String generateModelId(M m) {
return treeStore.getKeyProvider().getKey(m);
}
protected boolean hasChildren(M model) {
TreeNode<M> node = findNode(model);
if (loader != null && node != null && !node.isLoaded()) {
return loader.hasChildren(node.getModel());
}
if (node != null && (!node.isLeaf() || treeStore.hasChildren(node.getModel()))) {
return true;
}
return false;
}
protected void onAdd(StoreAddEvent<M> se) {
if (viewReady) {
for (M child : se.getItems()) {
register(child);
}
M p = treeStore.getParent(se.getItems().get(0));
final int index;
if (p == null) {
// adding to root
if (se.getIndex() == 0) {
// no need to adjust index, since we're adding to the top
index = se.getIndex();
} else {
// adjust index to add to the flattened tree
index = findLastOpenChildIndex(se.getItems().get(se.getItems().size() - 1)) + 1;
}
} else {
// adding to an existing node
TreeNode<M> node = findNode(p);
if (node == null) {
// node's parent collapsed
return;
}
if (!node.isExpanded()) {
refresh(p);
return;
}
if (se.getIndex() == 0) {
// inserting at the top of this node, so adjust based on the parent's index
index = store.indexOf(p) + 1;
} else {
// adjust the index to add to the local flattened tree
index = findLastOpenChildIndex(se.getItems().get(se.getItems().size() - 1)) + 1;
}
refresh(p);
}
store.addAll(index, se.getItems());
}
}
@Override
protected void onAfterRenderView() {
super.onAfterRenderView();
doInitialLoad();
}
protected void onClear(StoreClearEvent<M> event) {
onDataChange(null);
}
@Override
protected void onClick(Event event) {
EventTarget eventTarget = event.getEventTarget();
if (Element.is(eventTarget)) {
M m = store.get(getView().findRowIndex(Element.as(eventTarget)));
if (m != null) {
TreeNode<M> node = findNode(m);
if (node != null) {
Element jointEl = treeGridView.getJointElement(node);
if (jointEl != null && jointEl.isOrHasChild((Element.as(eventTarget)))) {
toggle(m);
} else {
super.onClick(event);
}
}
}
} else {
super.onClick(event);
}
}
protected void onDataChange(M parent) {
if (!viewReady) {
return;
}
if (parent == null) {
store.clear();
nodes.clear();
nodesByDomId.clear();
renderChildren(null, autoLoad);
} else {
TreeNode<M> n = findNode(parent);
if (n != null) {
treeGridView.collapse(n);
n.setExpanded(false);
n.setLoaded(true);
n.setLoading(false);
renderChildren(parent, autoLoad);
if (n.isExpand() && !isLeaf(parent)) {
n.setExpand(false);
boolean deep = n.isExpandDeep();
n.setExpandDeep(false);
boolean c = caching;
caching = true;
setExpanded(parent, true, deep);
caching = c;
} else {
refresh(parent);
}
}
}
}
@Override
protected void onDoubleClick(Event e) {
super.onDoubleClick(e);
if (expandOnDoubleClick) {
if (Element.is(e.getEventTarget())) {
int i = getView().findRowIndex(Element.as(e.getEventTarget()));
M m = store.get(i);
if (m != null) {
toggle(m);
}
}
}
}
protected void onFilter(StoreFilterEvent<M> se) {
onDataChange(null);
if (expandOnFilter && treeStore.isFiltered()) {
expandAll();
}
}
protected void onRecordChange(StoreRecordChangeEvent<M> event) {
store.update(event.getRecord().getModel());
}
protected void onRemove(TreeStoreRemoveEvent<M> event) {
if (viewReady) {
unregister(event.getItem());
store.remove(event.getItem());
for (M child : event.getChildren()) {
unregister(child);
store.remove(child);
}
TreeNode<M> p = findNode(event.getParent());
if (p != null && p.isExpanded() && treeStore.getChildCount(p.getModel()) == 0) {
setExpanded(p.getModel(), false);
} else if (p != null && treeStore.getChildCount(p.getModel()) == 0) {
refresh(event.getParent());
}
}
}
protected void onSort(StoreSortEvent<M> event) {
onDataChange(null);
}
protected void onUpdate(StoreUpdateEvent<M> se) {
for (M m : se.getItems()) {
// looking up by key instead of by index to allow for different model instance, but no equals() equality
if (store.findModelWithKey(store.getKeyProvider().getKey(m)) != null) {
store.update(m);
}
}
}
protected String register(M m) {
String id = generateModelId(m);
if (!nodes.containsKey(id)) {
String domId = XDOM.getUniqueId();
TreeGridNode<M> node = new TreeGridNode<M>(id, m, domId);
nodes.put(id, node);
nodesByDomId.put(domId, node);
}
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 && children.size() > 0) {
store.addAll(children);
}
for (M child : children) {
if (autoExpand) {
final M c = child;
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
setExpanded(c, true);
}
});
} else if (loader != null && autoLoad) {
if (store.isFiltered() || (!auto)) {
renderChildren(child, auto);
} else {
loader.loadChildren(child);
}
}
}
}
protected void setTreeColumn(ColumnConfig<M, ?> treeColumn) {
assert (treeColumn != null && cm.indexOf(treeColumn) != -1) : "treeColumn not found in ColumnModel";
this.treeColumn = treeColumn;
}
protected void unregister(M m) {
TreeNode<M> node = findNode(m);
if (node != null) {
node.clearElements();
TreeNode<M> removed = nodes.remove(generateModelId(m));
assert removed != null;
nodesByDomId.remove(removed.getDomId());
}
}
private void doInitialLoad() {
if (treeStore.getRootItems().size() == 0 && loader != null) {
loader.load();
} else {
renderChildren(null, false);
if (autoExpand) {
expandAll();
}
}
}
private void setExpandChildren(M m, boolean expand) {
for (M child : treeStore.getChildren(m)) {
setExpanded(child, expand, true);
}
}
}