0001: /*
0002: * $Id$ $Revision$ $Date$
0003: *
0004: * ==============================================================================
0005: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
0006: * use this file except in compliance with the License. You may obtain a copy of
0007: * the License at
0008: *
0009: * http://www.apache.org/licenses/LICENSE-2.0
0010: *
0011: * Unless required by applicable law or agreed to in writing, software
0012: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
0013: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
0014: * License for the specific language governing permissions and limitations under
0015: * the License.
0016: */
0017: package wicket.extensions.markup.html.tree;
0018:
0019: import java.io.Serializable;
0020: import java.util.ArrayList;
0021: import java.util.Collections;
0022: import java.util.Enumeration;
0023: import java.util.HashMap;
0024: import java.util.Iterator;
0025: import java.util.List;
0026: import java.util.Map;
0027:
0028: import javax.swing.event.TreeModelEvent;
0029: import javax.swing.event.TreeModelListener;
0030: import javax.swing.tree.TreeModel;
0031: import javax.swing.tree.TreeNode;
0032:
0033: import wicket.Component;
0034: import wicket.ajax.AjaxRequestTarget;
0035: import wicket.behavior.HeaderContributor;
0036: import wicket.markup.MarkupStream;
0037: import wicket.markup.html.WebMarkupContainer;
0038: import wicket.markup.html.panel.Panel;
0039: import wicket.markup.html.tree.Tree;
0040: import wicket.model.IDetachable;
0041: import wicket.model.IModel;
0042: import wicket.model.Model;
0043: import wicket.util.string.AppendingStringBuffer;
0044:
0045: /**
0046: * This class encapsulates the logic for displaying and (partial) updating the
0047: * tree. Actual presentation is out of scope of this class. User should derive
0048: * they own tree (if needed) from {@link DefaultAbstractTree} or {@link Tree}
0049: * (recommended).
0050: *
0051: * @author Matej Knopp
0052: */
0053: public abstract class AbstractTree extends Panel implements
0054: ITreeStateListener, TreeModelListener {
0055:
0056: /**
0057: * Interface for visiting individual tree items.
0058: */
0059: private static interface IItemCallback {
0060: /**
0061: * Visits the tree item.
0062: *
0063: * @param item
0064: * the item to visit
0065: */
0066: void visitItem(TreeItem item);
0067: }
0068:
0069: /**
0070: * This class represents one row in rendered tree (TreeNode). Only TreeNodes
0071: * that are visible (all their parent are expanded) have TreeItem created
0072: * for them.
0073: */
0074: private final class TreeItem extends WebMarkupContainer {
0075: /**
0076: * whether this tree item should also render it's children to response.
0077: * this is set if we need the whole subtree rendered as one component in
0078: * ajax response, so that we can replace it in one step (replacing
0079: * individual rows is very slow in javascript, therefore we replace the
0080: * whole subtree)
0081: */
0082: private final static int FLAG_RENDER_CHILDREN = FLAG_RESERVED8;
0083:
0084: private static final long serialVersionUID = 1L;
0085:
0086: /**
0087: * tree item children - we need this to traverse items in correct order
0088: * when rendering
0089: */
0090: private List children = null;
0091:
0092: /** tree item level - how deep is this item in tree */
0093: private int level;
0094:
0095: /**
0096: * Construct.
0097: *
0098: * @param id
0099: * The component id
0100: * @param node
0101: * tree node
0102: * @param level
0103: * current level
0104: */
0105: public TreeItem(String id, final TreeNode node, int level) {
0106: super (id, new Model((Serializable) node));
0107:
0108: nodeToItemMap.put(node, this );
0109: this .level = level;
0110: setOutputMarkupId(true);
0111:
0112: // if this isn't a root item in rootless mode
0113: if (level != -1) {
0114: populateTreeItem(this , level);
0115: }
0116: }
0117:
0118: /**
0119: * @return The children
0120: */
0121: public List getChildren() {
0122: return children;
0123: }
0124:
0125: /**
0126: * @return The current level
0127: */
0128: public int getLevel() {
0129: return level;
0130: }
0131:
0132: /**
0133: * @see wicket.Component#getMarkupId()
0134: */
0135: public String getMarkupId() {
0136: // this is overriden to produce id that begins with id of tree
0137: // if the tree has set (shorter) id in markup, we can use it to
0138: // shorten the id of individual TreeItems
0139: return AbstractTree.this .getMarkupId() + "_" + getId();
0140: }
0141:
0142: /**
0143: * @return parent item
0144: */
0145: public TreeItem getParentItem() {
0146: return (TreeItem) nodeToItemMap
0147: .get(((TreeNode) getModelObject()).getParent());
0148: }
0149:
0150: /**
0151: * Sets the children.
0152: *
0153: * @param children
0154: * The children
0155: */
0156: public void setChildren(List children) {
0157: this .children = children;
0158: }
0159:
0160: /**
0161: * Whether to render children.
0162: *
0163: * @return whether to render children
0164: */
0165: protected final boolean isRenderChildren() {
0166: return getFlag(FLAG_RENDER_CHILDREN);
0167: }
0168:
0169: /**
0170: * @see wicket.MarkupContainer#onRender(wicket.markup.MarkupStream)
0171: */
0172: protected void onRender(final MarkupStream markupStream) {
0173: // is this root and tree is in rootless mode?
0174: if (this == rootItem && isRootLess() == true) {
0175: // yes, write empty div with id
0176: // this is necesary for createElement js to work correctly
0177: getResponse().write(
0178: "<div style=\"display:none\" id=\""
0179: + getMarkupId() + "\"></div>");
0180: markupStream.skipComponent();
0181: } else {
0182: // remember current index
0183: final int index = markupStream.getCurrentIndex();
0184:
0185: // render the item
0186: super .onRender(markupStream);
0187:
0188: // should we also render children (ajax response)
0189: if (isRenderChildren()) {
0190: // visit every child
0191: visitItemChildren(this , new IItemCallback() {
0192: public void visitItem(TreeItem item) {
0193: // rewind markupStream
0194: markupStream.setCurrentIndex(index);
0195: // render child
0196: item.onRender(markupStream);
0197: }
0198: });
0199: // children are rendered, clear the flag
0200: setRenderChildren(false);
0201: }
0202: }
0203: }
0204:
0205: protected final void setRenderChildren(boolean value) {
0206: setFlag(FLAG_RENDER_CHILDREN, value);
0207: }
0208:
0209: protected void onDetach() {
0210: super .onDetach();
0211: Object object = getModelObject();
0212: if (object instanceof IDetachable) {
0213: ((IDetachable) object).detach();
0214: }
0215: }
0216: }
0217:
0218: /**
0219: * Components that holds tree items. This is similiar to ListView, but it
0220: * renders tree items in the right order.
0221: */
0222: private class TreeItemContainer extends WebMarkupContainer {
0223: private static final long serialVersionUID = 1L;
0224:
0225: /**
0226: * Construct.
0227: *
0228: * @param id
0229: * The component id
0230: */
0231: public TreeItemContainer(String id) {
0232: super (id);
0233: }
0234:
0235: /**
0236: * @see wicket.MarkupContainer#remove(wicket.Component)
0237: */
0238: public void remove(Component component) {
0239: // when a treeItem is removed, remove reference to it from
0240: // nodeToItemMAp
0241: if (component instanceof TreeItem) {
0242: nodeToItemMap.remove(((TreeItem) component)
0243: .getModelObject());
0244: }
0245: super .remove(component);
0246: }
0247:
0248: /**
0249: * renders the tree items, making sure that items are rendered in the
0250: * order they should be
0251: *
0252: * @param markupStream
0253: */
0254: protected void onRender(final MarkupStream markupStream) {
0255: // Save position in markup stream
0256: final int markupStart = markupStream.getCurrentIndex();
0257:
0258: // have we rendered at least one item?
0259: final class Rendered {
0260: boolean rendered = false;
0261: }
0262: ;
0263: final Rendered rendered = new Rendered();
0264:
0265: // is there a root item? (non-empty tree)
0266: if (rootItem != null) {
0267: IItemCallback callback = new IItemCallback() {
0268: public void visitItem(TreeItem item) {
0269: // rewind markup stream
0270: markupStream.setCurrentIndex(markupStart);
0271:
0272: // render component
0273: item.render(markupStream);
0274:
0275: rendered.rendered = true;
0276: }
0277: };
0278:
0279: // visit item and it's children
0280: visitItemAndChildren(rootItem, callback);
0281: }
0282:
0283: if (rendered.rendered == false) {
0284: // tree is empty, just move the markupStream
0285: markupStream.skipComponent();
0286: }
0287: }
0288: }
0289:
0290: /**
0291: * Returns an iterator that iterates trough the enumeration.
0292: *
0293: * @param enumeration
0294: * The enumeration to iterate through
0295: * @return The iterator
0296: */
0297: private static final Iterator toIterator(
0298: final Enumeration enumeration) {
0299: return new Iterator() {
0300: private Enumeration e = enumeration;
0301:
0302: public boolean hasNext() {
0303: return e.hasMoreElements();
0304: }
0305:
0306: public Object next() {
0307: return e.nextElement();
0308: }
0309:
0310: public void remove() {
0311: throw new UnsupportedOperationException(
0312: "Remove is not supported on enumeration.");
0313: }
0314: };
0315: }
0316:
0317: private boolean attached = false;
0318:
0319: /** comma separated list of ids of elements to be deleted. */
0320: private final AppendingStringBuffer deleteIds = new AppendingStringBuffer();
0321:
0322: /**
0323: * whether the whole tree is dirty (so the whole tree needs to be
0324: * refreshed).
0325: */
0326: private boolean dirtyAll = false;
0327:
0328: /**
0329: * list of dirty items. if children property of these items is null, the
0330: * chilren will be rebuild.
0331: */
0332: private final List dirtyItems = new ArrayList();
0333:
0334: /**
0335: * list of dirty items which need the DOM structure to be created for them
0336: * (added items)
0337: */
0338: private final List dirtyItemsCreateDOM = new ArrayList();
0339:
0340: /** counter for generating unique ids of every tree item. */
0341: private int idCounter = 0;
0342:
0343: /** Component whose children are tree items. */
0344: private TreeItemContainer itemContainer;
0345:
0346: /**
0347: * map that maps TreeNode to TreeItem. TreeItems only exists for TreeNodes,
0348: * that are visibled (their parents are not collapsed).
0349: */
0350: private final Map nodeToItemMap = new HashMap();
0351:
0352: /**
0353: * we need to track previous model. if the model changes, we unregister the
0354: * tree from listeners of old model and register the tree as litener of new
0355: * model.
0356: */
0357: private TreeModel previousModel = null;
0358:
0359: /** root item of the tree. */
0360: private TreeItem rootItem = null;
0361:
0362: /** whether the tree root is shown. */
0363: private boolean rootLess = false;
0364:
0365: /** stores reference to tree state. */
0366: private ITreeState state;
0367:
0368: /**
0369: * Tree constructor
0370: *
0371: * @param id
0372: * The component id
0373: */
0374: public AbstractTree(String id) {
0375: super (id);
0376: init();
0377: }
0378:
0379: /**
0380: * Tree constructor
0381: *
0382: * @param id
0383: * The component id
0384: * @param model
0385: * The tree model
0386: */
0387: public AbstractTree(String id, IModel model) {
0388: super (id, model);
0389: init();
0390: }
0391:
0392: /** called when all nodes are collapsed. */
0393: public final void allNodesCollapsed() {
0394: invalidateAll();
0395: }
0396:
0397: /** called when all nodes are expaned. */
0398: public final void allNodesExpanded() {
0399: invalidateAll();
0400: }
0401:
0402: /**
0403: * Returns the TreeState of this tree.
0404: *
0405: * @return Tree state instance
0406: */
0407: public ITreeState getTreeState() {
0408: if (state == null) {
0409: state = newTreeState();
0410:
0411: // add this object as listener of the state
0412: state.addTreeStateListener(this );
0413: // FIXME: Where should we remove the listener?
0414: }
0415: return state;
0416: }
0417:
0418: /**
0419: * This method is called before the onAttach is called. Code here gets
0420: * executed before the items have been populated.
0421: */
0422: protected void onBeforeAttach() {
0423: }
0424:
0425: /**
0426: * Called at the beginning of the request (not ajax request, unless we are
0427: * rendering the entire component)
0428: */
0429: public void internalAttach() {
0430: if (attached == false) {
0431: onBeforeAttach();
0432:
0433: checkModel();
0434:
0435: // Do we have to rebuld the whole tree?
0436: if (dirtyAll && rootItem != null) {
0437: clearAllItem();
0438: } else {
0439: // rebuild chilren of dirty nodes that need it
0440: rebuildDirty();
0441: }
0442:
0443: // is root item created? (root item is null if the items have not
0444: // been created yet, or the whole tree was dirty and clearAllITem
0445: // has been called
0446: if (rootItem == null) {
0447: TreeNode rootNode = (TreeNode) ((TreeModel) getModelObject())
0448: .getRoot();
0449: if (rootNode != null) {
0450: if (isRootLess()) {
0451: rootItem = newTreeItem(rootNode, -1);
0452: } else {
0453: rootItem = newTreeItem(rootNode, 0);
0454: }
0455: itemContainer.add(rootItem);
0456: buildItemChildren(rootItem);
0457: }
0458: }
0459:
0460: attached = true;
0461: }
0462:
0463: super .internalAttach();
0464: }
0465:
0466: /**
0467: * @see wicket.MarkupContainer#internalDetach()
0468: */
0469: public void internalDetach() {
0470: super .internalDetach();
0471: attached = false;
0472: }
0473:
0474: /**
0475: * Call to refresh the whole tree. This should only be called when the
0476: * roodNode has been replaced or the entiry tree model changed.
0477: */
0478: public final void invalidateAll() {
0479: updated();
0480: this .dirtyAll = true;
0481: }
0482:
0483: /**
0484: * @return whether the tree root is shown
0485: */
0486: public final boolean isRootLess() {
0487: return rootLess;
0488: };
0489:
0490: /**
0491: * @see wicket.extensions.markup.html.tree.ITreeStateListener#nodeCollapsed(javax.swing.tree.TreeNode)
0492: */
0493: public final void nodeCollapsed(TreeNode node) {
0494: if (isNodeVisible(node) == true) {
0495: invalidateNodeWithChildren(node);
0496: }
0497: }
0498:
0499: /**
0500: * @see wicket.extensions.markup.html.tree.ITreeStateListener#nodeExpanded(javax.swing.tree.TreeNode)
0501: */
0502: public final void nodeExpanded(TreeNode node) {
0503: if (isNodeVisible(node) == true) {
0504: invalidateNodeWithChildren(node);
0505: }
0506: }
0507:
0508: /**
0509: * @see wicket.extensions.markup.html.tree.ITreeStateListener#nodeSelected(javax.swing.tree.TreeNode)
0510: */
0511: public final void nodeSelected(TreeNode node) {
0512: if (isNodeVisible(node)) {
0513: invalidateNode(node, true);
0514: }
0515: }
0516:
0517: /**
0518: * @see wicket.extensions.markup.html.tree.ITreeStateListener#nodeUnselected(javax.swing.tree.TreeNode)
0519: */
0520: public final void nodeUnselected(TreeNode node) {
0521: if (isNodeVisible(node)) {
0522: invalidateNode(node, true);
0523: }
0524: }
0525:
0526: /**
0527: * Sets whether the root of the tree should be visible.
0528: *
0529: * @param rootLess
0530: * whether the root should be visible
0531: */
0532: public void setRootLess(boolean rootLess) {
0533: if (this .rootLess != rootLess) {
0534: this .rootLess = rootLess;
0535: invalidateAll();
0536:
0537: // if the tree is in rootless mode, make sure the root node is
0538: // expanded
0539: if (rootLess == true && getModelObject() != null) {
0540: getTreeState().expandNode(
0541: (TreeNode) ((TreeModel) getModelObject())
0542: .getRoot());
0543: }
0544: }
0545: }
0546:
0547: /**
0548: * @see javax.swing.event.TreeModelListener#treeNodesChanged(javax.swing.event.TreeModelEvent)
0549: */
0550: public final void treeNodesChanged(TreeModelEvent e) {
0551: // has root node changed?
0552: if (e.getChildren() == null) {
0553: if (rootItem != null) {
0554: invalidateNode((TreeNode) rootItem.getModelObject(),
0555: true);
0556: }
0557: } else {
0558: // go through all changed nodes
0559: Object[] children = e.getChildren();
0560: if (children != null)
0561: for (int i = 0; i < children.length; i++) {
0562: TreeNode node = (TreeNode) children[i];
0563: if (isNodeVisible(node)) {
0564: // if the nodes is visible invalidate it
0565: invalidateNode(node, true);
0566: }
0567: }
0568: }
0569: };
0570:
0571: /**
0572: * Marks the last but one visible child node of the given item as dirty,
0573: * if give child is the last item of parent.
0574: *
0575: * We need this to refresh the previous visible item in case the
0576: * inserted / deleteditem was last. The reason is that the line
0577: * shape of previous item chages from L to |- .
0578: *
0579: * @param parent
0580: * @param child
0581: */
0582: private void markTheLastButOneChildDirty(TreeItem parent,
0583: TreeItem child) {
0584: if (parent.getChildren().indexOf(child) == parent.getChildren()
0585: .size() - 1) {
0586: // go through the childrend backwards, start at the last but one
0587: // item
0588: for (int i = parent.getChildren().size() - 2; i >= 0; --i) {
0589: TreeItem item = (TreeItem) parent.getChildren().get(i);
0590:
0591: // invalidate the node and it's children, so that they are redrawn
0592: invalidateNodeWithChildren((TreeNode) item
0593: .getModelObject());
0594:
0595: }
0596: }
0597: }
0598:
0599: /**
0600: * @see javax.swing.event.TreeModelListener#treeNodesInserted(javax.swing.event.TreeModelEvent)
0601: */
0602: public final void treeNodesInserted(TreeModelEvent e) {
0603: // get the parent node of inserted nodes
0604: TreeNode parent = (TreeNode) e.getTreePath()
0605: .getLastPathComponent();
0606:
0607: if (isNodeVisible(parent) && isNodeExpanded(parent)) {
0608: TreeItem parentItem = (TreeItem) nodeToItemMap.get(parent);
0609: for (int i = 0; i < e.getChildren().length; ++i) {
0610: TreeNode node = (TreeNode) e.getChildren()[i];
0611: int index = e.getChildIndices()[i];
0612: TreeItem item = newTreeItem(node,
0613: parentItem.getLevel() + 1);
0614: itemContainer.add(item);
0615: parentItem.getChildren().add(index, item);
0616:
0617: markTheLastButOneChildDirty(parentItem, item);
0618:
0619: dirtyItems.add(item);
0620: dirtyItemsCreateDOM.add(item);
0621: }
0622: }
0623: }
0624:
0625: /**
0626: * @see javax.swing.event.TreeModelListener#treeNodesRemoved(javax.swing.event.TreeModelEvent)
0627: */
0628: public final void treeNodesRemoved(TreeModelEvent e) {
0629: // get the parent node of inserted nodes
0630: TreeNode parent = (TreeNode) e.getTreePath()
0631: .getLastPathComponent();
0632: TreeItem parentItem = (TreeItem) nodeToItemMap.get(parent);
0633:
0634: if (isNodeVisible(parent) && isNodeExpanded(parent)) {
0635:
0636: for (int i = 0; i < e.getChildren().length; ++i) {
0637: TreeNode node = (TreeNode) e.getChildren()[i];
0638:
0639: TreeItem item = (TreeItem) nodeToItemMap.get(node);
0640: if (item != null) {
0641: markTheLastButOneChildDirty(parentItem, item);
0642:
0643: parentItem.getChildren().remove(item);
0644:
0645: // go though item children and remove every one of them
0646: visitItemChildren(item, new IItemCallback() {
0647: public void visitItem(TreeItem item) {
0648: removeItem(item);
0649:
0650: // unselect the node
0651: getTreeState().selectNode(
0652: (TreeNode) item.getModelObject(),
0653: false);
0654: }
0655: });
0656:
0657: removeItem(item);
0658: }
0659: }
0660: }
0661: }
0662:
0663: /**
0664: * @see javax.swing.event.TreeModelListener#treeStructureChanged(javax.swing.event.TreeModelEvent)
0665: */
0666: public final void treeStructureChanged(TreeModelEvent e) {
0667: // get the parent node of changed nodes
0668: TreeNode node = (TreeNode) e.getTreePath()
0669: .getLastPathComponent();
0670:
0671: // has the tree root changed?
0672: if (e.getTreePath().getPathCount() == 1
0673: && node.equals(rootItem.getModelObject())) {
0674: invalidateAll();
0675: } else {
0676: invalidateNodeWithChildren(node);
0677: }
0678: }
0679:
0680: /**
0681: * Updates the changed portions of the tree using given AjaxRequestTarget.
0682: * Call this method if you modified the tree model during an ajax request
0683: * target and you want to partially update the component on page. Make sure
0684: * that the tree model has fired the proper listener functions.
0685: *
0686: * @param target
0687: * Ajax request target used to send the update to the page
0688: */
0689: public final void updateTree(final AjaxRequestTarget target) {
0690: if (target == null) {
0691: return;
0692: }
0693:
0694: // check whether the model hasn't changed
0695: checkModel();
0696:
0697: // is the whole tree dirty
0698: if (dirtyAll) {
0699: // render entire tree component
0700: target.addComponent(this );
0701: } else {
0702: // remove DOM elements that need to be removed
0703: if (deleteIds.length() != 0) {
0704: String js = getElementsDeleteJavascript();
0705:
0706: // add the javascript to target
0707: target.prependJavascript(js);
0708: }
0709:
0710: // We have to repeat this as long as there are any dirty items to be created.
0711: // The reason why we can't do this in one pass is that some of the items
0712: // may need to be inserted after items that has not been inserted yet, so we have
0713: // to detect those and wait until the items they depend on are inserted.
0714: while (dirtyItemsCreateDOM.isEmpty() == false) {
0715: for (Iterator i = dirtyItemsCreateDOM.iterator(); i
0716: .hasNext();) {
0717: TreeItem item = (TreeItem) i.next();
0718: TreeItem parent = item.getParentItem();
0719: int index = parent.getChildren().indexOf(item);
0720: TreeItem previous;
0721: // we need item before this (in dom structure)
0722:
0723: if (index == 0) {
0724: previous = parent;
0725: } else {
0726: previous = (TreeItem) parent.getChildren().get(
0727: index - 1);
0728: // get the last item of previous item subtree
0729: while (previous.getChildren() != null
0730: && previous.getChildren().size() > 0) {
0731: previous = (TreeItem) previous
0732: .getChildren().get(
0733: previous.getChildren()
0734: .size() - 1);
0735: }
0736: }
0737: // check if the previous item isn't waiting to be inserted
0738: if (dirtyItemsCreateDOM.contains(previous) == false) {
0739: // it's already in dom, so we can use it as point of insertion
0740: target
0741: .prependJavascript("Wicket.Tree.createElement(\""
0742: + item.getMarkupId()
0743: + "\","
0744: + "\""
0745: + previous.getMarkupId()
0746: + "\")");
0747:
0748: // remove the item so we don't process it again
0749: i.remove();
0750: } else {
0751: // we don't do anything here, inserting this item will have to wait
0752: // until the previous item gets inserted
0753: }
0754: }
0755: }
0756:
0757: // iterate through dirty items
0758: for (Iterator i = dirtyItems.iterator(); i.hasNext();) {
0759: TreeItem item = (TreeItem) i.next();
0760: // does the item need to rebuild children?
0761: if (item.getChildren() == null) {
0762: // rebuld the children
0763: buildItemChildren(item);
0764:
0765: // set flag on item so that it renders itself together with
0766: // it's children
0767: item.setRenderChildren(true);
0768: }
0769:
0770: // add the component to target
0771: target.addComponent(item);
0772: }
0773:
0774: // clear dirty flags
0775: updated();
0776: }
0777: }
0778:
0779: /**
0780: * Returns whether the given node is expanded.
0781: *
0782: * @param node
0783: * The node to inspect
0784: * @return true if the node is expanded, false otherwise
0785: */
0786: protected final boolean isNodeExpanded(TreeNode node) {
0787: // In root less mode the root node is always expanded
0788: if (isRootLess() && rootItem != null
0789: && rootItem.getModelObject().equals(node)) {
0790: return true;
0791: }
0792:
0793: return getTreeState().isNodeExpanded(node);
0794: }
0795:
0796: /**
0797: * Creates the TreeState, which is an object where the current state of tree
0798: * (which nodes are expanded / collapsed, selected, ...) is stored.
0799: *
0800: * @return Tree state instance
0801: */
0802: protected ITreeState newTreeState() {
0803: return new DefaultTreeState();
0804: }
0805:
0806: /**
0807: * Called after the rendering of tree is complete. Here we clear the dirty
0808: * flags.
0809: */
0810: protected void onAfterRender() {
0811: // rendering is complete, clear all dirty flags and items
0812: updated();
0813: }
0814:
0815: /**
0816: * This method is called after creating every TreeItem. This is the place
0817: * for adding components on item (junction links, labels, icons...)
0818: *
0819: * @param item
0820: * newly created tree item. The node can be obtained as
0821: * item.getModelObject()
0822: *
0823: * @param level
0824: * how deep the component is in tree hierarchy (0 for root item)
0825: */
0826: protected abstract void populateTreeItem(WebMarkupContainer item,
0827: int level);
0828:
0829: /**
0830: * Builds the children for given TreeItem. It recursively traverses children
0831: * of it's TreeNode and creates TreeItem for every visible TreeNode.
0832: *
0833: * @param item
0834: * The parent tree item
0835: */
0836: private final void buildItemChildren(TreeItem item) {
0837: List items;
0838:
0839: // if the node is expanded
0840: if (isNodeExpanded((TreeNode) item.getModelObject())) {
0841: // build the items for children of the items' treenode.
0842: items = buildTreeItems(nodeChildren((TreeNode) item
0843: .getModelObject()), item.getLevel() + 1);
0844: } else {
0845: // it's not expanded, just set children to an empty list
0846: items = Collections.EMPTY_LIST;
0847: }
0848:
0849: item.setChildren(items);
0850: }
0851:
0852: /**
0853: * Builds (recursively) TreeItems for the given Iterator of TreeNodes.
0854: *
0855: * @param nodes
0856: * The nodes to build tree items for
0857: * @param level
0858: * The current level
0859: * @return List with new tree items
0860: */
0861: private final List buildTreeItems(Iterator nodes, int level) {
0862: List result = new ArrayList();
0863:
0864: // for each node
0865: while (nodes.hasNext()) {
0866: TreeNode node = (TreeNode) nodes.next();
0867: // create tree item
0868: TreeItem item = newTreeItem(node, level);
0869: itemContainer.add(item);
0870:
0871: // builds it children (recursively)
0872: buildItemChildren(item);
0873:
0874: // add item to result
0875: result.add(item);
0876: }
0877:
0878: return result;
0879: }
0880:
0881: /**
0882: * Checks whether the model has been chaned, and if so unregister and
0883: * register listeners.
0884: */
0885: private final void checkModel() {
0886: // find out whether the model object (the TreeModel) has been changed
0887: TreeModel model = (TreeModel) getModelObject();
0888: if (model != previousModel) {
0889: if (previousModel != null) {
0890: previousModel.removeTreeModelListener(this );
0891: }
0892:
0893: previousModel = model;
0894:
0895: if (model != null) {
0896: model.addTreeModelListener(this );
0897: }
0898: // model has been changed, redraw whole tree
0899: invalidateAll();
0900: }
0901: }
0902:
0903: /**
0904: * Removes all TreeItem components.
0905: */
0906: private final void clearAllItem() {
0907: visitItemAndChildren(rootItem, new IItemCallback() {
0908: public void visitItem(TreeItem item) {
0909: item.remove();
0910: }
0911: });
0912: rootItem = null;
0913: }
0914:
0915: /**
0916: * Returns the javascript used to delete removed elements.
0917: *
0918: * @return The javascript
0919: */
0920: private String getElementsDeleteJavascript() {
0921: // build the javascript call
0922: final AppendingStringBuffer buffer = new AppendingStringBuffer(
0923: 100);
0924:
0925: buffer.append("Wicket.Tree.removeNodes(\"");
0926:
0927: // first parameter is the markup id of tree (will be used as prefix to
0928: // build ids of child items
0929: buffer.append(getMarkupId() + "_\",[");
0930:
0931: // append the ids of elements to be deleted
0932: buffer.append(deleteIds);
0933:
0934: // does the buffer end if ','?
0935: if (buffer.endsWith(",")) {
0936: // it does, trim it
0937: buffer.setLength(buffer.length() - 1);
0938: }
0939:
0940: buffer.append("]);");
0941:
0942: return buffer.toString();
0943: }
0944:
0945: //
0946: // State and Model's callbacks
0947: //
0948:
0949: /**
0950: * returns the short version of item id (just the number part).
0951: *
0952: * @param item
0953: * The tree item
0954: * @return The id
0955: */
0956: private String getShortItemId(TreeItem item) {
0957: // show much of component id can we skip? (to minimize the length of
0958: // javascript being sent)
0959: final int skip = getMarkupId().length() + 1; // the length of id of
0960: // tree and '_'.
0961: return item.getMarkupId().substring(skip);
0962: }
0963:
0964: /**
0965: * Initialize the component.
0966: */
0967: private final void init() {
0968: setVersioned(false);
0969:
0970: // we need id when we are replacing the whole tree
0971: setOutputMarkupId(true);
0972:
0973: // create container for tree items
0974: itemContainer = new TreeItemContainer("i");
0975: add(itemContainer);
0976:
0977: add(HeaderContributor.forJavaScript(AbstractTree.class,
0978: "res/tree.js"));
0979: }
0980:
0981: /**
0982: * Invalidates single node (without children). On the next render, this node
0983: * will be updated. Node will not be rebuilt, unless forceRebuild is true.
0984: *
0985: * @param node
0986: * The node to invalidate
0987: * @param forceRebuild
0988: */
0989: private final void invalidateNode(TreeNode node,
0990: boolean forceRebuild) {
0991: if (dirtyAll == false) {
0992: // get item for this node
0993: TreeItem item = (TreeItem) nodeToItemMap.get(node);
0994:
0995: if (item != null) {
0996: boolean createDOM = false;
0997:
0998: if (forceRebuild) {
0999: // recreate the item
1000: int level = item.getLevel();
1001: List children = item.getChildren();
1002: String id = item.getId();
1003:
1004: // store the parent of old item
1005: TreeItem parent = item.getParentItem();
1006:
1007: // if the old item has a parent, store it's index
1008: int index = parent != null ? parent.getChildren()
1009: .indexOf(item) : -1;
1010:
1011: createDOM = dirtyItemsCreateDOM.contains(item);
1012:
1013: dirtyItems.remove(item);
1014: dirtyItemsCreateDOM.remove(item);
1015:
1016: item.remove();
1017:
1018: item = newTreeItem(node, level, id);
1019: itemContainer.add(item);
1020:
1021: item.setChildren(children);
1022:
1023: // was the item an root item?
1024: if (parent == null) {
1025: rootItem = item;
1026: } else {
1027: parent.getChildren().set(index, item);
1028: }
1029: }
1030:
1031: dirtyItems.add(item);
1032: if (createDOM)
1033: dirtyItemsCreateDOM.add(item);
1034: }
1035: }
1036: }
1037:
1038: /**
1039: * Invalidates node and it's children. On the next render, the node and
1040: * children will be updated. Node children will be rebuilt.
1041: *
1042: * @param node
1043: * The node to invalidate
1044: */
1045: private final void invalidateNodeWithChildren(TreeNode node) {
1046: if (dirtyAll == false) {
1047: // get item for this node
1048: TreeItem item = (TreeItem) nodeToItemMap.get(node);
1049:
1050: // is the item visible?
1051: if (item != null) {
1052: // go though item children and remove every one of them
1053: visitItemChildren(item, new IItemCallback() {
1054: public void visitItem(TreeItem item) {
1055: removeItem(item);
1056: }
1057: });
1058:
1059: // set children to null so that they get rebuild
1060: item.setChildren(null);
1061:
1062: // add item to dirty items
1063: dirtyItems.add(item);
1064: }
1065: }
1066: }
1067:
1068: /**
1069: * Returns whether the given node is visibled, e.g. all it's parents are
1070: * expanded.
1071: *
1072: * @param node
1073: * The node to inspect
1074: * @return true if the node is visible, false otherwise
1075: */
1076: private final boolean isNodeVisible(TreeNode node) {
1077: while (node.getParent() != null) {
1078: if (isNodeExpanded(node.getParent()) == false) {
1079: return false;
1080: }
1081: node = node.getParent();
1082: }
1083: return true;
1084: }
1085:
1086: /**
1087: * Creates a tree item for given node.
1088: *
1089: * @param node
1090: * The tree node
1091: * @param level
1092: * The level
1093: * @return The new tree item
1094: */
1095: private final TreeItem newTreeItem(TreeNode node, int level) {
1096: return new TreeItem("" + idCounter++, node, level);
1097: }
1098:
1099: /**
1100: * Creates a tree item for given node with specified id.
1101: *
1102: * @param node
1103: * The tree node
1104: * @param level
1105: * The level
1106: * @param id
1107: * the component id
1108: * @return The new tree item
1109: */
1110: private final TreeItem newTreeItem(TreeNode node, int level,
1111: String id) {
1112: return new TreeItem(id, node, level);
1113: }
1114:
1115: /**
1116: * Return the representation of node children as Iterator interface.
1117: *
1118: * @param node
1119: * The tree node
1120: * @return iterable presentation of node children
1121: */
1122: private final Iterator nodeChildren(TreeNode node) {
1123: return toIterator(node.children());
1124: }
1125:
1126: /**
1127: * Rebuilds children of every item in dirtyItems that needs it. This method
1128: * is called for non-partial update.
1129: */
1130: private final void rebuildDirty() {
1131: // go through dirty items
1132: for (Iterator i = dirtyItems.iterator(); i.hasNext();) {
1133: TreeItem item = (TreeItem) i.next();
1134: // item chilren need to be rebuilt
1135: if (item.getChildren() == null) {
1136: buildItemChildren(item);
1137: }
1138: }
1139: }
1140:
1141: /**
1142: * Removes the item, appends it's id to deleteIds. This is called when a
1143: * items parent is being deleted or rebuilt.
1144: *
1145: * @param item
1146: * The item to remove
1147: */
1148: private void removeItem(TreeItem item) {
1149: // even if the item is dirty it's no longer necessary to update id
1150: dirtyItems.remove(item);
1151:
1152: // if the item was about to be created
1153: if (dirtyItemsCreateDOM.contains(item)) {
1154: // we needed to create DOM element, we no longer do
1155: dirtyItemsCreateDOM.remove(item);
1156: } else {
1157: // add items id (it's short version) to ids of DOM elements that will be
1158: // removed
1159: deleteIds.append(getShortItemId(item));
1160: deleteIds.append(",");
1161: }
1162:
1163: // remove the id
1164: // note that this doesn't update item's parent's children list
1165: item.remove();
1166: }
1167:
1168: /**
1169: * Calls after the tree has been rendered. Clears all dirty flags.
1170: */
1171: private final void updated() {
1172: this .dirtyAll = false;
1173: this .dirtyItems.clear();
1174: this .dirtyItemsCreateDOM.clear();
1175: deleteIds.clear(); // FIXME: Recreate it to save some space?
1176: }
1177:
1178: /**
1179: * Call the callback#visitItem method for the given item and all it's
1180: * chilren.
1181: *
1182: * @param item
1183: * The tree item
1184: * @param callback
1185: * item call back
1186: */
1187: private final void visitItemAndChildren(TreeItem item,
1188: IItemCallback callback) {
1189: callback.visitItem(item);
1190: visitItemChildren(item, callback);
1191: }
1192:
1193: /**
1194: * Call the callback#visitItem method for every child of given item.
1195: *
1196: * @param item
1197: * The tree item
1198: * @param callback
1199: * The callback
1200: */
1201: private final void visitItemChildren(TreeItem item,
1202: IItemCallback callback) {
1203: if (item.getChildren() != null) {
1204: for (Iterator i = item.getChildren().iterator(); i
1205: .hasNext();) {
1206: TreeItem child = (TreeItem) i.next();
1207: visitItemAndChildren(child, callback);
1208: }
1209: }
1210: }
1211:
1212: /**
1213: * Returns the component associated with given node, or null, if node is not
1214: * visible. This is useful in situations when you want to touch the node
1215: * element in html.
1216: *
1217: * @param node
1218: * Tree node
1219: * @return Component associated with given node, or null if node is not
1220: * visible.
1221: */
1222: public Component getNodeComponent(TreeNode node) {
1223: return (Component) nodeToItemMap.get(node);
1224: }
1225: }
|