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