You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by sv...@apache.org on 2012/02/05 23:02:55 UTC
[2/9] Merge remote-tracking branch 'origin/master'
http://git-wip-us.apache.org/repos/asf/wicket/blob/31726809/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/tree/AbstractTree.java
----------------------------------------------------------------------
diff --cc wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/tree/AbstractTree.java
index cf75695,0000000..60dfaf2
mode 100644,000000..100644
--- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/tree/AbstractTree.java
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/tree/AbstractTree.java
@@@ -1,1767 -1,0 +1,1767 @@@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.extensions.markup.html.tree;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.swing.event.TreeModelEvent;
+import javax.swing.event.TreeModelListener;
+import javax.swing.tree.TreeModel;
+import javax.swing.tree.TreeNode;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.WicketRuntimeException;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.behavior.Behavior;
+import org.apache.wicket.markup.ComponentTag;
+import org.apache.wicket.markup.IMarkupFragment;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.head.JavaScriptHeaderItem;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.internal.HtmlHeaderContainer;
+import org.apache.wicket.markup.html.list.AbstractItem;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.model.IDetachable;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.request.Response;
+import org.apache.wicket.request.resource.JavaScriptResourceReference;
+import org.apache.wicket.request.resource.ResourceReference;
+import org.apache.wicket.util.lang.Args;
+import org.apache.wicket.util.string.AppendingStringBuffer;
+import org.apache.wicket.util.visit.IVisit;
+import org.apache.wicket.util.visit.IVisitor;
+
+
+/**
+ * This class encapsulates the logic for displaying and (partial) updating the tree. Actual
+ * presentation is out of scope of this class. User should derive they own tree (if needed) from
+ * {@link BaseTree} (recommended).
+ *
+ * @author Matej Knopp
+ */
+@Deprecated
+public abstract class AbstractTree extends Panel
+ implements
+ ITreeStateListener,
+ TreeModelListener,
+ AjaxRequestTarget.ITargetRespondListener
+{
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Interface for visiting individual tree items.
+ */
+ private static interface IItemCallback
+ {
+ /**
+ * Visits the tree item.
+ *
+ * @param item
+ * the item to visit
+ */
+ void visitItem(TreeItem item);
+ }
+
+ /**
+ * This class represents one row in rendered tree (TreeNode). Only TreeNodes that are visible
+ * (all their parent are expanded) have TreeItem created for them.
+ */
+ private final class TreeItem extends AbstractItem
+ {
+ /**
+ * whether this tree item should also render it's children to response. this is set if we
+ * need the whole subtree rendered as one component in ajax response, so that we can replace
+ * it in one step (replacing individual rows is very slow in javascript, therefore we
+ * replace the whole subtree)
+ */
+ private final static int FLAG_RENDER_CHILDREN = FLAG_RESERVED8;
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * tree item children - we need this to traverse items in correct order when rendering
+ */
+ private List<TreeItem> children = null;
+
+ /** tree item level - how deep is this item in tree */
+ private final int level;
+
+ private final TreeItem parent;
+
+ /**
+ * Construct.
+ *
+ * @param id
+ * The component id
+ * @param node
+ * tree node
+ * @param level
+ * current level
+ * @param parent
+ */
+ public TreeItem(TreeItem parent, String id, final Object node, int level)
+ {
+ super(id, new Model<Serializable>((Serializable)node));
+
+ this.parent = parent;
+
+ nodeToItemMap.put(node, this);
+ this.level = level;
+ setOutputMarkupId(true);
+
+ // if this isn't a root item in rootless mode
+ if (level != -1)
+ {
+ populateTreeItem(this, level);
+ }
+ }
+
+ public TreeItem getParentItem()
+ {
+ return parent;
+ }
+
+ /**
+ * @return The children
+ */
+ public List<TreeItem> getChildren()
+ {
+ return children;
+ }
+
+ /**
+ * @return The current level
+ */
+ public int getLevel()
+ {
+ return level;
+ }
+
+ /**
+ * @see org.apache.wicket.Component#getMarkupId()
+ */
+ @Override
+ public String getMarkupId()
+ {
+ // this is overridden to produce id that begins with id of tree
+ // if the tree has set (shorter) id in markup, we can use it to
+ // shorten the id of individual TreeItems
+ return AbstractTree.this.getMarkupId() + "_" + getId();
+ }
+
+ /**
+ * Sets the children.
+ *
+ * @param children
+ * The children
+ */
+ public void setChildren(List<TreeItem> children)
+ {
+ this.children = children;
+ }
+
+ /**
+ * Whether to render children.
+ *
+ * @return whether to render children
+ */
+ protected final boolean isRenderChildren()
+ {
+ return getFlag(FLAG_RENDER_CHILDREN);
+ }
+
+ /**
+ * Whether the TreeItem has any child TreeItems
+ *
+ * @return true if there are one or more child TreeItems; false otherwise
+ */
+ public boolean hasChildTreeItems()
+ {
+ return children != null && !children.isEmpty();
+ }
+
+ /**
+ * @see org.apache.wicket.MarkupContainer#onRender()
+ */
+ @Override
+ protected void onRender()
+ {
+ // is this root and tree is in rootless mode?
+ if (this == rootItem && isRootLess() == true)
+ {
+ // yes, write empty div with id
+ // this is necessary for createElement js to work correctly
+ String tagName = ((ComponentTag)getMarkup().get(0)).getName();
+ Response response = getResponse();
+ response.write("<" + tagName + " style=\"display:none\" id=\"" + getMarkupId() +
+ "\">");
+ if ("table".equals(tagName))
+ {
+ response.write("<tbody><tr><td></td></tr></tbody>");
+ }
+ response.write("</" + tagName + ">");
+ }
+ else
+ {
+ // render the item
+ super.onRender();
+
+ // should we also render children (ajax response)
+ if (isRenderChildren())
+ {
+ // visit every child
+ visitItemChildren(this, new IItemCallback()
+ {
+ @Override
+ public void visitItem(TreeItem item)
+ {
+ // render child
+ item.onRender();
+
+ // go through the behaviors and invoke IBehavior.afterRender
+ List<? extends Behavior> behaviors = item.getBehaviors();
+ for (Behavior behavior : behaviors)
+ {
+ behavior.afterRender(item);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ *
+ * @return model object
+ */
+ public Object getModelObject()
+ {
+ return getDefaultModelObject();
+ }
+
+ @Override
+ public void renderHead(final HtmlHeaderContainer container)
+ {
+ super.renderHead(container);
+
+ if (isRenderChildren())
+ {
+ // visit every child
+ visitItemChildren(this, new IItemCallback()
+ {
+ @Override
+ public void visitItem(TreeItem item)
+ {
+ // write header contributions from the children of item
+ item.visitChildren(new IVisitor<Component, Void>()
+ {
+ @Override
+ public void component(final Component component,
+ final IVisit<Void> visit)
+ {
+ if (component.isVisible())
+ {
+ component.renderHead(container);
+ }
+ else
+ {
+ visit.dontGoDeeper();
+ }
+ }
+ });
+ }
+ });
+ }
+ }
+
+ protected final void setRenderChildren(boolean value)
+ {
+ setFlag(FLAG_RENDER_CHILDREN, value);
+ }
+
+ @Override
+ protected void onDetach()
+ {
+ super.onDetach();
+ Object object = getModelObject();
+ if (object instanceof IDetachable)
+ {
+ ((IDetachable)object).detach();
+ }
+
+ if (isRenderChildren())
+ {
+ // visit every child
+ visitItemChildren(this, new IItemCallback()
+ {
+ @Override
+ public void visitItem(TreeItem item)
+ {
+ item.detach();
+ }
+ });
+ }
+
+ // children are rendered, clear the flag
+ setRenderChildren(false);
+ }
+
+ @Override
+ protected void onBeforeRender()
+ {
+ onBeforeRenderInternal();
+ super.onBeforeRender();
+
+ if (isRenderChildren())
+ {
+ // visit every child
+ visitItemChildren(this, new IItemCallback()
+ {
+ @Override
+ public void visitItem(TreeItem item)
+ {
+ item.prepareForRender();
+ }
+ });
+ }
+ }
+
+ @Override
+ protected void onAfterRender()
+ {
+ super.onAfterRender();
+ if (isRenderChildren())
+ {
+ // visit every child
+ visitItemChildren(this, new IItemCallback()
+ {
+ @Override
+ public void visitItem(TreeItem item)
+ {
+ item.afterRender();
+ }
+ });
+ }
+ }
+
+ private boolean hasParentWithChildrenMarkedToRecreation()
+ {
+ return getParentItem() != null &&
+ (getParentItem().getChildren() == null || getParentItem().hasParentWithChildrenMarkedToRecreation());
+ }
+ }
+
+ /**
+ * Components that holds tree items. This is similar to ListView, but it renders tree items in
+ * the right order.
+ */
+ private class TreeItemContainer extends WebMarkupContainer
+ {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Construct.
+ *
+ * @param id
+ * The component id
+ */
+ public TreeItemContainer(String id)
+ {
+ super(id);
+ }
+
+ /**
+ * @see org.apache.wicket.MarkupContainer#remove(org.apache.wicket.Component)
+ */
+ @Override
+ public TreeItemContainer remove(Component component)
+ {
+ // when a treeItem is removed, remove reference to it from
+ // nodeToItemMAp
+ if (component instanceof TreeItem)
+ {
+ nodeToItemMap.remove(((TreeItem)component).getModelObject());
+ }
+ super.remove(component);
+ return this;
+ }
+
+ /**
+ * @see org.apache.wicket.MarkupContainer#onRender()
+ */
+ @Override
+ protected void onRender()
+ {
+ // is there a root item? (non-empty tree)
+ if (rootItem != null)
+ {
+ IItemCallback callback = new IItemCallback()
+ {
+ @Override
+ public void visitItem(TreeItem item)
+ {
+ // render component
+ item.render();
+ }
+ };
+
+ // visit item and it's children
+ visitItemAndChildren(rootItem, callback);
+ }
+ }
+
+ @Override
+ public IMarkupFragment getMarkup(final Component child)
+ {
+ // The childs markup is always equal to the parents markup.
+ return getMarkup();
+ }
+ }
+
+ private boolean attached = false;
+
+ /** comma separated list of ids of elements to be deleted. */
+ private final AppendingStringBuffer deleteIds = new AppendingStringBuffer();
+
+ /**
+ * whether the whole tree is dirty (so the whole tree needs to be refreshed).
+ */
+ private boolean dirtyAll = false;
+
+ /**
+ * list of dirty items. if children property of these items is null, the children will be
+ * rebuild.
+ */
+ private final Set<TreeItem> dirtyItems = new HashSet<TreeItem>();
+
+ /**
+ * list of dirty items which need the DOM structure to be created for them (added items)
+ */
+ private final Set<TreeItem> dirtyItemsCreateDOM = new HashSet<TreeItem>();
+
+ /** counter for generating unique ids of every tree item. */
+ private int idCounter = 0;
+
+ /** Component whose children are tree items. */
+ private TreeItemContainer itemContainer;
+
+ /**
+ * map that maps TreeNode to TreeItem. TreeItems only exists for TreeNodes, that are visible
+ * (their parents are not collapsed).
+ */
+ // TODO this field is not serializable but nested inside an serializable component
+ private final Map<Object, TreeItem> nodeToItemMap = new HashMap<Object, TreeItem>();
+
+ /**
+ * we need to track previous model. if the model changes, we unregister the tree from listeners
+ * of old model and register the tree as listener of new model.
+ */
+
+ // TODO this field is not serializable but nested inside an serializable component
+ private TreeModel previousModel = null;
+
+ /** root item of the tree. */
+ private TreeItem rootItem = null;
+
+ /** whether the tree root is shown. */
+ private boolean rootLess = false;
+
+ /** stores reference to tree state. */
+ private ITreeState state;
+
+ /**
+ * Tree constructor
+ *
+ * @param id
+ * The component id
+ */
+ public AbstractTree(String id)
+ {
+ super(id);
+ init();
+ }
+
+ /**
+ * Tree constructor
+ *
+ * @param id
+ * The component id
+ * @param model
+ * The tree model
+ */
+ public AbstractTree(String id, IModel<? extends TreeModel> model)
+ {
+ super(id, model);
+ init();
+ }
+
+ /** called when all nodes are collapsed. */
+ @Override
+ public final void allNodesCollapsed()
+ {
+ invalidateAll();
+ }
+
+ /** called when all nodes are expanded. */
+ @Override
+ public final void allNodesExpanded()
+ {
+ invalidateAll();
+ }
+
+ /**
+ *
+ * @return model
+ */
+ @SuppressWarnings("unchecked")
+ public IModel<? extends TreeModel> getModel()
+ {
+ return (IModel<? extends TreeModel>)getDefaultModel();
+ }
+
+ /**
+ * @return treemodel
+ */
+ public TreeModel getModelObject()
+ {
+ return (TreeModel)getDefaultModelObject();
+ }
+
+ /**
+ *
+ * @param model
+ * @return this
+ */
+ public MarkupContainer setModel(IModel<? extends TreeModel> model)
+ {
+ setDefaultModel(model);
+ return this;
+ }
+
+ /**
+ *
+ * @param model
+ * @return this
+ */
+ public MarkupContainer setModelObject(TreeModel model)
+ {
+ setDefaultModelObject(model);
+ return this;
+ }
+
+ /**
+ * Returns the TreeState of this tree.
+ *
+ * @return Tree state instance
+ */
+ public ITreeState getTreeState()
+ {
+ if (state == null)
+ {
+ state = newTreeState();
+
+ // add this object as listener of the state
+ state.addTreeStateListener(this);
+ // FIXME: Where should we remove the listener?
+ }
+ return state;
+ }
+
+ /**
+ * This method is called before the onAttach is called. Code here gets executed before the items
+ * have been populated.
+ */
+ protected void onBeforeAttach()
+ {
+ }
+
+ // This is necessary because MarkupContainer.onBeforeRender involves calling
+ // beforeRender on children, which results in stack overflow when called from TreeItem
+ private void onBeforeRenderInternal()
+ {
+ if (attached == false)
+ {
+ onBeforeAttach();
+
+ checkModel();
+
+ // Do we have to rebuild the whole tree?
+ if (dirtyAll && rootItem != null)
+ {
+ clearAllItem();
+ }
+ else
+ {
+ // rebuild children of dirty nodes that need it
+ rebuildDirty();
+ }
+
+ // is root item created? (root item is null if the items have not
+ // been created yet, or the whole tree was dirty and clearAllITem
+ // has been called
+ if (rootItem == null)
+ {
+ Object rootNode = getModelObject().getRoot();
+ if (rootNode != null)
+ {
+ if (isRootLess())
+ {
+ rootItem = newTreeItem(null, rootNode, -1);
+ }
+ else
+ {
+ rootItem = newTreeItem(null, rootNode, 0);
+ }
+ itemContainer.add(rootItem);
+ buildItemChildren(rootItem);
+ }
+ }
+
+ attached = true;
+ }
+ }
+
+ /**
+ * Called at the beginning of the request (not ajax request, unless we are rendering the entire
+ * component)
+ */
+ @Override
+ public void onBeforeRender()
+ {
+ onBeforeRenderInternal();
+ super.onBeforeRender();
+ }
+
+ /**
+ * @see org.apache.wicket.MarkupContainer#onDetach()
+ */
+ @Override
+ public void onDetach()
+ {
+ attached = false;
+ super.onDetach();
+ if (getTreeState() instanceof IDetachable)
+ {
+ ((IDetachable)getTreeState()).detach();
+ }
+ }
+
+ /**
+ * Call to refresh the whole tree. This should only be called when the roodNode has been
+ * replaced or the entiry tree model changed.
+ */
+ public final void invalidateAll()
+ {
+ updated();
+ dirtyAll = true;
+ }
+
+ /**
+ * @return whether the tree root is shown
+ */
+ public final boolean isRootLess()
+ {
+ return rootLess;
+ }
+
+ /**
+ * @see org.apache.wicket.extensions.markup.html.tree.ITreeStateListener#nodeCollapsed(Object)
+ */
+ @Override
+ public final void nodeCollapsed(Object node)
+ {
+ if (isNodeVisible(node) == true)
+ {
+ invalidateNodeWithChildren(node);
+ }
+ }
+
+ /**
+ * @see org.apache.wicket.extensions.markup.html.tree.ITreeStateListener#nodeExpanded(Object)
+ */
+ @Override
+ public final void nodeExpanded(Object node)
+ {
+ if (isNodeVisible(node) == true)
+ {
+ invalidateNodeWithChildren(node);
+ }
+ }
+
+ /**
+ * @see org.apache.wicket.extensions.markup.html.tree.ITreeStateListener#nodeSelected(Object)
+ */
+ @Override
+ public final void nodeSelected(Object node)
+ {
+ if (isNodeVisible(node))
+ {
+ invalidateNode(node, isForceRebuildOnSelectionChange());
+ }
+ }
+
+ /**
+ * @see org.apache.wicket.extensions.markup.html.tree.ITreeStateListener#nodeUnselected(Object)
+ */
+ @Override
+ public final void nodeUnselected(Object node)
+ {
+ if (isNodeVisible(node))
+ {
+ invalidateNode(node, isForceRebuildOnSelectionChange());
+ }
+ }
+
+ /**
+ * Determines whether the TreeNode needs to be rebuilt if it is selected or deselected
+ *
+ * @return true if the node should be rebuilt after (de)selection, false otherwise
+ */
+ protected boolean isForceRebuildOnSelectionChange()
+ {
+ return true;
+ }
+
+ /**
+ * Sets whether the root of the tree should be visible.
+ *
+ * @param rootLess
+ * whether the root should be visible
+ */
+ public void setRootLess(boolean rootLess)
+ {
+ if (this.rootLess != rootLess)
+ {
+ this.rootLess = rootLess;
+ invalidateAll();
+
+ // if the tree is in rootless mode, make sure the root node is
+ // expanded
+ if (rootLess == true && getModelObject() != null)
+ {
+ getTreeState().expandNode(getModelObject().getRoot());
+ }
+ }
+ }
+
+ /**
+ * @see javax.swing.event.TreeModelListener#treeNodesChanged(javax.swing.event.TreeModelEvent)
+ */
+ @Override
+ public final void treeNodesChanged(TreeModelEvent e)
+ {
+ if (dirtyAll)
+ {
+ return;
+ }
+ // has root node changed?
+ if (e.getChildren() == null)
+ {
+ if (rootItem != null)
+ {
+ invalidateNode(rootItem.getModelObject(), true);
+ }
+ }
+ else
+ {
+ // go through all changed nodes
+ Object[] children = e.getChildren();
+ if (children != null)
+ {
+ for (Object node : children)
+ {
+ if (isNodeVisible(node))
+ {
+ // if the nodes is visible invalidate it
+ invalidateNode(node, true);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Marks the last but one visible child node of the given item as dirty, if give child is the
+ * last item of parent.
+ *
+ * We need this to refresh the previous visible item in case the inserted / deleted item was
+ * last. The reason is that the line shape of previous item changes from L to |- .
+ *
+ * @param parent
+ * @param child
+ */
+ private void markTheLastButOneChildDirty(TreeItem parent, TreeItem child)
+ {
+ if (parent.getChildren().indexOf(child) == parent.getChildren().size() - 1)
+ {
+ // go through the children backwards, start at the last but one
+ // item
+ for (int i = parent.getChildren().size() - 2; i >= 0; --i)
+ {
+ TreeItem item = parent.getChildren().get(i);
+
+ // invalidate the node and it's children, so that they are
+ // redrawn
+ invalidateNodeWithChildren(item.getModelObject());
+
+ }
+ }
+ }
+
+ /**
+ * @see javax.swing.event.TreeModelListener#treeNodesInserted(javax.swing.event.TreeModelEvent)
+ */
+ @Override
+ public final void treeNodesInserted(TreeModelEvent e)
+ {
+ if (dirtyAll)
+ {
+ return;
+ }
+
+ // get the parent node of inserted nodes
+ Object parentNode = e.getTreePath().getLastPathComponent();
+ TreeItem parentItem = nodeToItemMap.get(parentNode);
+
+
+ if (parentItem != null && isNodeVisible(parentNode))
+ {
+ List<?> eventChildren = Arrays.asList(e.getChildren());
+
+ // parentNode was a leaf before this insertion event only if every one of
+ // its current children is in the event's list of children
+ boolean wasLeaf = true;
+ int nodeChildCount = getChildCount(parentNode);
+ for (int i = 0; wasLeaf && i < nodeChildCount; i++)
+ {
+ wasLeaf = eventChildren.contains(getChildAt(parentNode, i));
+ }
+
+ boolean addingToHiddedRoot = parentItem.getParentItem() == null && isRootLess();
+ // if parent was a presented leaf
+ if (wasLeaf && !addingToHiddedRoot)
+ {
+ // parentNode now has children for the first time, so we may need to invalidate
+ // grandparent so that parentNode's junctionLink gets rebuilt with a plus/minus link
+ Object grandparentNode = getParentNode(parentNode);
+ boolean addingToHiddedRootSon = grandparentNode != null &&
+ getParentNode(grandparentNode) == null && isRootLess();
+ // if visible, invalidate the grandparent
+ if (grandparentNode != null && !addingToHiddedRootSon)
+ {
+ invalidateNodeWithChildren(grandparentNode);
+ }
+ else
+ {
+ // if not, simply invalidating the parent node
+ // OBS.: forcing rebuild since unlike the grandparent, the old
+ // leaf parent needs to rebuild with plus/minus link
+ invalidateNode(parentNode, true);
+ }
+ getTreeState().expandNode(parentNode);
+ }
+ else
+ {
+ if (isNodeExpanded(parentNode))
+ {
+ List<TreeItem> itemChildren = parentItem.getChildren();
+ int childLevel = parentItem.getLevel() + 1;
+ final int[] childIndices = e.getChildIndices();
+ for (int i = 0; i < eventChildren.size(); ++i)
+ {
+ TreeItem item = newTreeItem(parentItem, eventChildren.get(i), childLevel);
+ itemContainer.add(item);
+
+ if (itemChildren != null)
+ {
+ itemChildren.add(childIndices[i], item);
+ markTheLastButOneChildDirty(parentItem, item);
+ }
+
+ if (!dirtyItems.contains(item))
+ {
+ dirtyItems.add(item);
+ }
+
+ if (!dirtyItemsCreateDOM.contains(item) &&
+ !item.hasParentWithChildrenMarkedToRecreation())
+ {
+ dirtyItemsCreateDOM.add(item);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @see javax.swing.event.TreeModelListener#treeNodesRemoved(javax.swing.event.TreeModelEvent)
+ */
+ @Override
+ public final void treeNodesRemoved(TreeModelEvent removalEvent)
+ {
+ if (dirtyAll)
+ {
+ return;
+ }
+
+ // get the parent node of deleted nodes
+ Object parentNode = removalEvent.getTreePath().getLastPathComponent();
+ TreeItem parentItem = nodeToItemMap.get(parentNode);
+
+ // unselect all removed items
+ List<Object> selection = new ArrayList<Object>(getTreeState().getSelectedNodes());
+ List<Object> removed = Arrays.asList(removalEvent.getChildren());
+ for (Object selectedNode : selection)
+ {
+ Object cursor = selectedNode;
+ while (cursor != null)
+ {
+ if (removed.contains(cursor))
+ {
+ getTreeState().selectNode(selectedNode, false);
+ }
+ if (cursor instanceof TreeNode)
+ {
+ cursor = ((TreeNode)cursor).getParent();
+ }
+ else
+ {
+ cursor = null;
+ }
+ }
+ }
+
+ if (parentItem != null && isNodeVisible(parentNode))
+ {
+ if (isNodeExpanded(parentNode))
+ {
+ // deleted nodes were visible; we need to delete their TreeItems
+ for (Object deletedNode : removalEvent.getChildren())
+ {
+ TreeItem itemToDelete = nodeToItemMap.get(deletedNode);
+ if (itemToDelete != null)
+ {
+ markTheLastButOneChildDirty(parentItem, itemToDelete);
+
+ // remove all the deleted item's children
+ visitItemChildren(itemToDelete, new IItemCallback()
+ {
+ @Override
+ public void visitItem(TreeItem item)
+ {
+ removeItem(item);
+ }
+ });
+
+ parentItem.getChildren().remove(itemToDelete);
+ removeItem(itemToDelete);
+ }
+ }
+ }
+
+ if (!parentItem.hasChildTreeItems())
+ {
+ // rebuild parent's icon to show it no longer has children
+ invalidateNode(parentNode, true);
+ }
+ }
+ }
+
+ /**
+ * @see javax.swing.event.TreeModelListener#treeStructureChanged(javax.swing.event.TreeModelEvent)
+ */
+ @Override
+ public final void treeStructureChanged(TreeModelEvent e)
+ {
+ if (dirtyAll)
+ {
+ return;
+ }
+
+ // get the parent node of changed nodes
+ Object node = e.getTreePath() != null ? e.getTreePath().getLastPathComponent() : null;
+
+ // has the tree root changed?
+ if (node == null || e.getTreePath().getPathCount() == 1)
+ {
+ invalidateAll();
+ }
+ else
+ {
+ invalidateNodeWithChildren(node);
+ }
+ }
+
+ /**
+ * Allows to intercept adding dirty components to AjaxRequestTarget.
+ *
+ * @param target
+ * @param component
+ */
+ protected void addComponent(AjaxRequestTarget target, Component component)
+ {
+ target.add(component);
+ }
+
+ @Override
+ public void onTargetRespond(AjaxRequestTarget target)
+ {
+ // check whether the model hasn't changed
+ checkModel();
+
+ // is the whole tree dirty
+ if (dirtyAll)
+ {
+ // render entire tree component
+ addComponent(target, this);
+ }
+ else
+ {
+ // remove DOM elements that need to be removed
+ if (deleteIds.length() != 0)
+ {
+ String js = getElementsDeleteJavaScript();
+
+ // add the javascript to target
+ target.prependJavaScript(js);
+ }
+
+ // We have to repeat this as long as there are any dirty items to be
+ // created.
+ // The reason why we can't do this in one pass is that some of the
+ // items
+ // may need to be inserted after items that has not been inserted
+ // yet, so we have
+ // to detect those and wait until the items they depend on are
+ // inserted.
+ while (dirtyItemsCreateDOM.isEmpty() == false)
+ {
+ for (Iterator<TreeItem> i = dirtyItemsCreateDOM.iterator(); i.hasNext();)
+ {
+ TreeItem item = i.next();
+ TreeItem parent = item.getParentItem();
+ int index = parent.getChildren().indexOf(item);
+ TreeItem previous;
+ // we need item before this (in dom structure)
+
+ if (index == 0)
+ {
+ previous = parent;
+ }
+ else
+ {
+ previous = parent.getChildren().get(index - 1);
+ // get the last item of previous item subtree
+ while (previous.getChildren() != null && previous.getChildren().size() > 0)
+ {
+ previous = previous.getChildren()
+ .get(previous.getChildren().size() - 1);
+ }
+ }
+ // check if the previous item isn't waiting to be inserted
+ if (dirtyItemsCreateDOM.contains(previous) == false)
+ {
+ // it's already in dom, so we can use it as point of
+ // insertion
+ target.prependJavaScript("Wicket.Tree.createElement(\"" +
+ item.getMarkupId() + "\"," + "\"" + previous.getMarkupId() + "\")");
+
+ // remove the item so we don't process it again
+ i.remove();
+ }
+ else
+ {
+ // we don't do anything here, inserting this item will
+ // have to wait
+ // until the previous item gets inserted
+ }
+ }
+ }
+
+ // iterate through dirty items
+ for (TreeItem item : dirtyItems)
+ {
+ // does the item need to rebuild children?
+ if (item.getChildren() == null)
+ {
+ // rebuild the children
+ buildItemChildren(item);
+
+ // set flag on item so that it renders itself together with
+ // it's children
+ item.setRenderChildren(true);
+ }
+
+ // add the component to target
+ addComponent(target, item);
+ }
+
+ // clear dirty flags
+ updated();
+ }
+ }
+
+ /**
+ * Convenience method that updates changed portions on tree. You can call this method during
- * Ajax response, where calling {@link #updateTree(AjaxRequestTarget)} would be appropriate, but
++ * Ajax response, where calling {@link #updateTree(org.apache.wicket.ajax.AjaxRequestTarget)} would be appropriate, but
+ * you don't have the AjaxRequestTarget instance. However, it is also safe to call this method
+ * outside Ajax response.
+ */
+ public final void updateTree()
+ {
- AjaxRequestTarget handler = AjaxRequestTarget.get();
- if (handler == null)
++ AjaxRequestTarget target = getRequestCycle().find(AjaxRequestTarget.class);
++ if (target == null)
+ {
+ throw new WicketRuntimeException(
+ "No AjaxRequestTarget available to execute updateTree(ART target)");
+ }
+
- updateTree(handler);
++ updateTree(target);
+ }
+
+ /**
+ * Updates the changed portions of the tree using given AjaxRequestTarget. Call this method if
+ * you modified the tree model during an ajax request target and you want to partially update
+ * the component on page. Make sure that the tree model has fired the proper listener functions.
+ * <p>
+ * <b>You can only call this method once in a request.</b>
+ *
+ * @param target
+ * Ajax request target used to send the update to the page
+ */
+ public final void updateTree(final AjaxRequestTarget target)
+ {
+ Args.notNull(target, "target");
+ target.registerRespondListener(this);
+ }
+
+ /**
+ * Returns whether the given node is expanded.
+ *
+ * @param node
+ * The node to inspect
+ * @return true if the node is expanded, false otherwise
+ */
+ protected final boolean isNodeExpanded(Object node)
+ {
+ // In root less mode the root node is always expanded
+ if (isRootLess() && rootItem != null && rootItem.getModelObject().equals(node))
+ {
+ return true;
+ }
+
+ return getTreeState().isNodeExpanded(node);
+ }
+
+ /**
+ * Creates the TreeState, which is an object where the current state of tree (which nodes are
+ * expanded / collapsed, selected, ...) is stored.
+ *
+ * @return Tree state instance
+ */
+ protected ITreeState newTreeState()
+ {
+ return new DefaultTreeState();
+ }
+
+ /**
+ * Called after the rendering of tree is complete. Here we clear the dirty flags.
+ */
+ @Override
+ protected void onAfterRender()
+ {
+ super.onAfterRender();
+ // rendering is complete, clear all dirty flags and items
+ updated();
+ }
+
+ /**
+ * This method is called after creating every TreeItem. This is the place for adding components
+ * on item (junction links, labels, icons...)
+ *
+ * @param item
+ * newly created tree item. The node can be obtained as item.getModelObject()
+ *
+ * @param level
+ * how deep the component is in tree hierarchy (0 for root item)
+ */
+ protected abstract void populateTreeItem(WebMarkupContainer item, int level);
+
+ /**
+ * Builds the children for given TreeItem. It recursively traverses children of it's TreeNode
+ * and creates TreeItem for every visible TreeNode.
+ *
+ * @param item
+ * The parent tree item
+ */
+ private void buildItemChildren(TreeItem item)
+ {
+ List<TreeItem> items;
+
+ // if the node is expanded
+ if (isNodeExpanded(item.getModelObject()))
+ {
+ // build the items for children of the items' treenode.
+ items = buildTreeItems(item, nodeChildren(item.getModelObject()), item.getLevel() + 1);
+ }
+ else
+ {
+ // it's not expanded, just set children to an empty list
+ items = new ArrayList<TreeItem>(0);
+ }
+
+ item.setChildren(items);
+ }
+
+ /**
+ * Builds (recursively) TreeItems for the given Iterator of TreeNodes.
+ *
+ * @param parent
+ * @param nodes
+ * The nodes to build tree items for
+ * @param level
+ * The current level
+ * @return List with new tree items
+ */
+ private List<TreeItem> buildTreeItems(TreeItem parent, Iterator<Object> nodes, int level)
+ {
+ List<TreeItem> result = new ArrayList<TreeItem>();
+
+ // for each node
+ while (nodes.hasNext())
+ {
+ Object node = nodes.next();
+ // create tree item
+ TreeItem item = newTreeItem(parent, node, level);
+ itemContainer.add(item);
+
+ // builds it children (recursively)
+ buildItemChildren(item);
+
+ // add item to result
+ result.add(item);
+ }
+
+ return result;
+ }
+
+ /**
+ * Checks whether the model has been changed, and if so unregister and register listeners.
+ */
+ private void checkModel()
+ {
+ // find out whether the model object (the TreeModel) has been changed
+ TreeModel model = getModelObject();
+ if (model != previousModel)
+ {
+ if (previousModel != null)
+ {
+ previousModel.removeTreeModelListener(this);
+ }
+
+ previousModel = model;
+
+ if (model != null)
+ {
+ model.addTreeModelListener(this);
+ }
+ // model has been changed, redraw whole tree
+ invalidateAll();
+ }
+ }
+
+ /**
+ * Removes all TreeItem components.
+ */
+ private void clearAllItem()
+ {
+ visitItemAndChildren(rootItem, new IItemCallback()
+ {
+ @Override
+ public void visitItem(TreeItem item)
+ {
+ item.remove();
+ }
+ });
+ rootItem = null;
+ }
+
+ /**
+ * Returns the javascript used to delete removed elements.
+ *
+ * @return The javascript
+ */
+ private String getElementsDeleteJavaScript()
+ {
+ // build the javascript call
+ final AppendingStringBuffer buffer = new AppendingStringBuffer(100);
+
+ buffer.append("Wicket.Tree.removeNodes(\"");
+
+ // first parameter is the markup id of tree (will be used as prefix to
+ // build ids of child items
+ buffer.append(getMarkupId() + "_\",[");
+
+ // append the ids of elements to be deleted
+ buffer.append(deleteIds);
+
+ // does the buffer end if ','?
+ if (buffer.endsWith(","))
+ {
+ // it does, trim it
+ buffer.setLength(buffer.length() - 1);
+ }
+
+ buffer.append("]);");
+
+ return buffer.toString();
+ }
+
+ //
+ // State and Model callbacks
+ //
+
+ /**
+ * returns the short version of item id (just the number part).
+ *
+ * @param item
+ * The tree item
+ * @return The id
+ */
+ private String getShortItemId(TreeItem item)
+ {
+ // show much of component id can we skip? (to minimize the length of
+ // javascript being sent)
+ final int skip = getMarkupId().length() + 1; // the length of id of
+ // tree and '_'.
+ return item.getMarkupId().substring(skip);
+ }
+
+ private final static ResourceReference JAVASCRIPT = new JavaScriptResourceReference(
+ AbstractTree.class, "res/tree.js");
+
+ /**
+ * Initialize the component.
+ */
+ private void init()
+ {
+ setVersioned(false);
+
+ // we need id when we are replacing the whole tree
+ setOutputMarkupId(true);
+
+ // create container for tree items
+ itemContainer = new TreeItemContainer("i");
+ add(itemContainer);
+
+ checkModel();
+ }
+
+ /**
+ * INTERNAL
+ *
+ * @param node
+ */
+ public final void markNodeDirty(Object node)
+ {
+ invalidateNode(node, false);
+ }
+
+ /**
+ * INTERNAL
+ *
+ * @param node
+ */
+ public final void markNodeChildrenDirty(Object node)
+ {
+ TreeItem item = nodeToItemMap.get(node);
+ if (item != null)
+ {
+ visitItemChildren(item, new IItemCallback()
+ {
+ @Override
+ public void visitItem(TreeItem item)
+ {
+ invalidateNode(item.getModelObject(), false);
+ }
+ });
+ }
+ }
+
+ /**
+ * Invalidates single node (without children). On the next render, this node will be updated.
+ * Node will not be rebuilt, unless forceRebuild is true.
+ *
+ * @param node
+ * The node to invalidate
+ * @param forceRebuild
+ */
+ private void invalidateNode(Object node, boolean forceRebuild)
+ {
+ if (dirtyAll == false)
+ {
+ // get item for this node
+ TreeItem item = nodeToItemMap.get(node);
+
+ if (item != null)
+ {
+ boolean createDOM = false;
+
+ if (forceRebuild)
+ {
+ // recreate the item
+ int level = item.getLevel();
+ List<TreeItem> children = item.getChildren();
+ String id = item.getId();
+
+ // store the parent of old item
+ TreeItem parent = item.getParentItem();
+
+ // if the old item has a parent, store it's index
+ int index = parent != null ? parent.getChildren().indexOf(item) : -1;
+
+ createDOM = dirtyItemsCreateDOM.contains(item);
+
+ dirtyItems.remove(item);
+ dirtyItemsCreateDOM.remove(item);
+
+ item.remove();
+
+ item = newTreeItem(parent, node, level, id);
+ itemContainer.add(item);
+
+ item.setChildren(children);
+
+ // was the item an root item?
+ if (parent == null)
+ {
+ rootItem = item;
+ }
+ else
+ {
+ parent.getChildren().set(index, item);
+ }
+ }
+
+ if (!dirtyItems.contains(item))
+ {
+ dirtyItems.add(item);
+ }
+
+ if (createDOM && !dirtyItemsCreateDOM.contains(item))
+ {
+ dirtyItemsCreateDOM.add(item);
+ }
+ }
+ }
+ }
+
+ /**
+ * Invalidates node and it's children. On the next render, the node and children will be
+ * updated. Node children will be rebuilt.
+ *
+ * @param node
+ * The node to invalidate
+ */
+ private void invalidateNodeWithChildren(Object node)
+ {
+ if (dirtyAll == false)
+ {
+ // get item for this node
+ TreeItem item = nodeToItemMap.get(node);
+
+ // is the item visible?
+ if (item != null)
+ {
+ // go though item children and remove every one of them
+ visitItemChildren(item, new IItemCallback()
+ {
+ @Override
+ public void visitItem(TreeItem item)
+ {
+ removeItem(item);
+ }
+ });
+
+ // set children to null so that they get rebuild
+ item.setChildren(null);
+
+ if (!dirtyItems.contains(item))
+ {
+ // add item to dirty items
+ dirtyItems.add(item);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns whether the given node is visible, e.g. all it's parents are expanded.
+ *
+ * @param node
+ * The node to inspect
+ * @return true if the node is visible, false otherwise
+ */
+ private boolean isNodeVisible(Object node)
+ {
+ if (node == null)
+ {
+ return false;
+ }
+ Object parent = getParentNode(node);
+ while (parent != null)
+ {
+ if (isNodeExpanded(parent) == false)
+ {
+ return false;
+ }
+ parent = getParentNode(parent);
+ }
+ return true;
+ }
+
+ /**
+ * Returns parent node of given node.
+ *
+ * @param node
+ * @return parent node
+ */
+ public Object getParentNode(Object node)
+ {
+ TreeItem item = nodeToItemMap.get(node);
+ if (item == null)
+ {
+ return null;
+ }
+ else
+ {
+ TreeItem parent = item.getParentItem();
+ return parent == null ? null : parent.getModelObject();
+ }
+ }
+
+ /**
+ * Creates a tree item for given node.
+ *
+ * @param parent
+ * @param node
+ * The tree node
+ * @param level
+ * The level *
+ * @return The new tree item
+ */
+ private TreeItem newTreeItem(TreeItem parent, Object node, int level)
+ {
+ return new TreeItem(parent, "" + idCounter++, node, level);
+ }
+
+ /**
+ * Creates a tree item for given node with specified id.
+ *
+ * @param parent
+ * @param node
+ * The tree node
+ * @param level
+ * The level
+ * @param id
+ * the component id
+ * @return The new tree item
+ */
+ private TreeItem newTreeItem(TreeItem parent, Object node, int level, String id)
+ {
+ return new TreeItem(parent, id, node, level);
+ }
+
+ /**
+ * Return the representation of node children as Iterator interface.
+ *
+ * @param node
+ * The tree node
+ * @return iterable presentation of node children
+ */
+ public final Iterator<Object> nodeChildren(Object node)
+ {
+ TreeModel model = getTreeModel();
+ int count = model.getChildCount(node);
+ List<Object> nodes = new ArrayList<Object>(count);
+ for (int i = 0; i < count; ++i)
+ {
+ nodes.add(model.getChild(node, i));
+ }
+ return nodes.iterator();
+ }
+
+ /**
+ * @param parent
+ * @param index
+ * @return child
+ */
+ public final Object getChildAt(Object parent, int index)
+ {
+ return getTreeModel().getChild(parent, index);
+ }
+
+ /**
+ *
+ * @param node
+ * @return boolean
+ */
+ public final boolean isLeaf(Object node)
+ {
+ return getTreeModel().isLeaf(node);
+ }
+
+ /**
+ * @param parent
+ * @return child count
+ */
+ public final int getChildCount(Object parent)
+ {
+ return getTreeModel().getChildCount(parent);
+ }
+
+ private TreeModel getTreeModel()
+ {
+ return getModelObject();
+ }
+
+ /**
+ * Rebuilds children of every item in dirtyItems that needs it. This method is called for
+ * non-partial update.
+ */
+ private void rebuildDirty()
+ {
+ // go through dirty items
+ for (TreeItem item : dirtyItems)
+ {
+ // item children need to be rebuilt
+ if (item.getChildren() == null)
+ {
+ buildItemChildren(item);
+ }
+ }
+ }
+
+ /**
+ * Removes the item, appends it's id to deleteIds. This is called when a items parent is being
+ * deleted or rebuilt.
+ *
+ * @param item
+ * The item to remove
+ */
+ private void removeItem(TreeItem item)
+ {
+ // even if the item is dirty it's no longer necessary to update id
+ dirtyItems.remove(item);
+
+ // if the item was about to be created
+ if (dirtyItemsCreateDOM.contains(item))
+ {
+ // we needed to create DOM element, we no longer do
+ dirtyItemsCreateDOM.remove(item);
+ }
+ else
+ {
+ // add items id (it's short version) to ids of DOM elements that
+ // will be
+ // removed
+ deleteIds.append(getShortItemId(item));
+ deleteIds.append(",");
+ }
+
+ if (item.getParent() != null)
+ {
+ // remove the id
+ // note that this doesn't update item's parent's children list
+ item.remove();
+ }
+ }
+
+ /**
+ * Calls after the tree has been rendered. Clears all dirty flags.
+ */
+ private void updated()
+ {
+ dirtyAll = false;
+ dirtyItems.clear();
+ dirtyItemsCreateDOM.clear();
+ deleteIds.clear(); // FIXME: Recreate it to save some space?
+ }
+
+ /**
+ * Call the callback#visitItem method for the given item and all it's children.
+ *
+ * @param item
+ * The tree item
+ * @param callback
+ * item call back
+ */
+ private void visitItemAndChildren(TreeItem item, IItemCallback callback)
+ {
+ callback.visitItem(item);
+ visitItemChildren(item, callback);
+ }
+
+ /**
+ * Call the callback#visitItem method for every child of given item.
+ *
+ * @param item
+ * The tree item
+ * @param callback
+ * The callback
+ */
+ private void visitItemChildren(TreeItem item, IItemCallback callback)
+ {
+ if (item.getChildren() != null)
+ {
+ for (TreeItem child : item.getChildren())
+ {
+ visitItemAndChildren(child, callback);
+ }
+ }
+ }
+
+ /**
+ * Returns the component associated with given node, or null, if node is not visible. This is
+ * useful in situations when you want to touch the node element in html.
+ *
+ * @param node
+ * Tree node
+ * @return Component associated with given node, or null if node is not visible.
+ */
+ public Component getNodeComponent(Object node)
+ {
+ return nodeToItemMap.get(node);
+ }
+
+ @Override
+ public void renderHead(IHeaderResponse response)
+ {
+ response.render(JavaScriptHeaderItem.forReference(JAVASCRIPT));
+ }
+}
http://git-wip-us.apache.org/repos/asf/wicket/blob/31726809/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/tree/BaseTree.java
----------------------------------------------------------------------
diff --cc wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/tree/BaseTree.java
index f96514a,0000000..ba73c56
mode 100644,000000..100644
--- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/tree/BaseTree.java
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/tree/BaseTree.java
@@@ -1,478 -1,0 +1,478 @@@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.extensions.markup.html.tree;
+
+import javax.swing.tree.TreeModel;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.IClusterable;
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.markup.html.AjaxFallbackLink;
+import org.apache.wicket.ajax.markup.html.AjaxLink;
+import org.apache.wicket.ajax.markup.html.IAjaxLink;
+import org.apache.wicket.behavior.Behavior;
+import org.apache.wicket.markup.ComponentTag;
+import org.apache.wicket.markup.head.CssHeaderItem;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.request.Response;
+import org.apache.wicket.request.resource.PackageResourceReference;
+import org.apache.wicket.request.resource.ResourceReference;
+import org.apache.wicket.util.string.Strings;
+
+/**
+ * An abstract Tree component that should serve as a base for custom Tree Components.
+ *
+ * It has one abstract method - {@link #newNodeComponent(String, IModel)} that needs to be
+ * overridden.
+ *
+ * @author Matej Knopp
+ */
+@Deprecated
+public abstract class BaseTree extends AbstractTree
+{
+ /**
+ * Construct.
+ *
+ * @param id
+ */
+ public BaseTree(String id)
+ {
+ this(id, null);
+ }
+
+ /**
+ * Construct.
+ *
+ * @param id
+ * @param model
+ */
+ public BaseTree(String id, IModel<? extends TreeModel> model)
+ {
+ super(id, model);
+ }
+
+ // default stylesheet resource
+ private static final ResourceReference CSS = new PackageResourceReference(BaseTree.class,
+ "res/base-tree.css");
+
+ /**
+ * Returns the stylesheet reference
+ *
+ * @return stylesheet reference
+ */
+ protected ResourceReference getCSS()
+ {
+ return CSS;
+ }
+
+ private static final long serialVersionUID = 1L;
+
+ private static final String JUNCTION_LINK_ID = "junctionLink";
+ private static final String NODE_COMPONENT_ID = "nodeComponent";
+
+ /**
+ * @see org.apache.wicket.extensions.markup.html.tree.AbstractTree#populateTreeItem(org.apache.wicket.markup.html.WebMarkupContainer,
+ * int)
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void populateTreeItem(WebMarkupContainer item, int level)
+ {
+ // add junction link
+ Object node = item.getDefaultModelObject();
+ Component junctionLink = newJunctionLink(item, JUNCTION_LINK_ID, node);
+ junctionLink.add(new JunctionBorder(node, level));
+ item.add(junctionLink);
+
+ // add node component
+ Component nodeComponent = newNodeComponent(NODE_COMPONENT_ID,
+ (IModel<Object>)item.getDefaultModel());
+ item.add(nodeComponent);
+
+ // add behavior that conditionally adds the "selected" CSS class name
+ item.add(new Behavior()
+ {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onComponentTag(Component component, ComponentTag tag)
+ {
+ Object node = component.getDefaultModelObject();
+ String klass = getItemClass(node);
+ if (!Strings.isEmpty(klass))
+ {
+ CharSequence oldClass = tag.getAttribute("class");
+ if (Strings.isEmpty(oldClass))
+ {
+ tag.put("class", klass);
+ }
+ else
+ {
+ tag.put("class", oldClass + " " + klass);
+ }
+ }
+ }
+ });
+ }
+
+ protected String getItemClass(Object node)
+ {
+ if (getTreeState().isNodeSelected(node))
+ {
+ return getSelectedClass();
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the class name that will be added to row's CSS class for selected rows
+ *
+ * @return CSS class name
+ */
+ protected String getSelectedClass()
+ {
+ return "selected";
+ }
+
+ /**
+ * Creates a new component for the given TreeNode.
+ *
+ * @param id
+ * component ID
+ * @param model
+ * model that returns the node
+ * @return component for node
+ */
+ protected abstract Component newNodeComponent(String id, IModel<Object> model);
+
+ /**
+ * Returns whether the provided node is last child of it's parent.
+ *
+ * @param node
+ * The node
+ * @return whether the provided node is the last child
+ */
+ private boolean isNodeLast(Object node)
+ {
+ Object parent = getParentNode(node);
+ if (parent == null)
+ {
+ return true;
+ }
+ else
+ {
+ return getChildAt(parent, getChildCount(parent) - 1).equals(node);
+ }
+ }
+
+ /**
+ * Class that wraps a link (or span) with a junction table cells.
+ *
+ * @author Matej Knopp
+ */
+ private class JunctionBorder extends Behavior
+ {
+ private static final long serialVersionUID = 1L;
+
+ // TODO this field is not serializable but nested inside an serializable component
+ private final Object node;
+ private final int level;
+
+ /**
+ * Construct.
+ *
+ * @param node
+ * @param level
+ */
+ public JunctionBorder(Object node, int level)
+ {
+ this.node = node;
+ this.level = level;
+ }
+
+ /**
- * @see org.apache.wicket.behavior.AbstractBehavior#onRendered(org.apache.wicket.Component)
++ * @see org.apache.wicket.behavior.Behavior#afterRender(org.apache.wicket.Component)
+ */
+ @Override
+ public void afterRender(final Component component)
+ {
+ component.getResponse().write("</td>");
+ }
+
+ /**
+ * @see org.apache.wicket.behavior.Behavior#beforeRender(org.apache.wicket.Component)
+ */
+ @Override
+ public void beforeRender(final Component component)
+ {
+ Response response = component.getResponse();
+ Object parent = getParentNode(node);
+
+ CharSequence classes[] = new CharSequence[level];
+ for (int i = 0; i < level; ++i)
+ {
+ if (parent == null || isNodeLast(parent))
+ {
+ classes[i] = "spacer";
+ }
+ else
+ {
+ classes[i] = "line";
+ }
+
+ parent = getParentNode(parent);
+ }
+
+ for (int i = level - 1; i >= 0; --i)
+ {
+ response.write("<td class=\"" + classes[i] + "\"><span></span></td>");
+ }
+
+ if (isNodeLast(node))
+ {
+ response.write("<td class=\"half-line\">");
+ }
+ else
+ {
+ response.write("<td class=\"line\">");
+ }
+ }
+ }
+
+ /**
+ * Creates the junction link for given node. Also (optionally) creates the junction image. If
+ * the node is a leaf (it has no children), the created junction link is non-functional.
+ *
+ * @param parent
+ * parent component of the link
+ * @param id
+ * wicket:id of the component
+ * @param node
+ * tree node for which the link should be created.
+ * @return The link component
+ */
+ protected Component newJunctionLink(MarkupContainer parent, final String id, final Object node)
+ {
+ final MarkupContainer junctionLink;
+
+ if (isLeaf(node) == false)
+ {
+ junctionLink = newLink(id, new ILinkCallback()
+ {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onClick(AjaxRequestTarget target)
+ {
+ if (isNodeExpanded(node))
+ {
+ getTreeState().collapseNode(node);
+ }
+ else
+ {
+ getTreeState().expandNode(node);
+ }
+ onJunctionLinkClicked(target, node);
+
+ if (target != null)
+ {
+ updateTree(target);
+ }
+ }
+ });
+ junctionLink.add(new Behavior()
+ {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onComponentTag(Component component, ComponentTag tag)
+ {
+ if (isNodeExpanded(node))
+ {
+ tag.put("class", "junction-open");
+ }
+ else
+ {
+ tag.put("class", "junction-closed");
+ }
+ }
+ });
+ }
+ else
+ {
+ junctionLink = new WebMarkupContainer(id)
+ {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * @see org.apache.wicket.Component#onComponentTag(org.apache.wicket.markup.ComponentTag)
+ */
+ @Override
+ protected void onComponentTag(ComponentTag tag)
+ {
+ super.onComponentTag(tag);
+ tag.setName("span");
+ tag.put("class", "junction-corner");
+ }
+ };
+
+ }
+
+ return junctionLink;
+ }
+
+ /**
+ * Callback function called after user clicked on an junction link. The node has already been
+ * expanded/collapsed (depending on previous status).
+ *
+ * @param target
+ * Request target - may be null on non-ajax call
+ *
+ * @param node
+ * Node for which this callback is relevant
+ */
+ protected void onJunctionLinkClicked(AjaxRequestTarget target, Object node)
+ {
+ }
+
+ /**
+ * Helper class for calling an action from a link.
+ *
+ * @author Matej Knopp
+ */
+ public interface ILinkCallback extends IAjaxLink, IClusterable
+ {
+ }
+
+ /**
+ * Creates a link of type specified by current linkType. When the links is clicked it calls the
+ * specified callback.
+ *
+ * @param id
+ * The component id
+ * @param callback
+ * The link call back. {@code null} is passed for its onClick(AjaxRequestTarget) for
+ * {@link LinkType#REGULAR} and eventually for {@link LinkType#AJAX_FALLBACK}.
+ * @return The link component
+ */
+ public MarkupContainer newLink(String id, final ILinkCallback callback)
+ {
+ if (getLinkType() == LinkType.REGULAR)
+ {
+ return new Link<Void>(id)
+ {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * @see org.apache.wicket.markup.html.link.Link#onClick()
+ */
+ @Override
+ public void onClick()
+ {
+ callback.onClick(null);
+ }
+ };
+ }
+ else if (getLinkType() == LinkType.AJAX)
+ {
+ return new AjaxLink<Void>(id)
+ {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * @see org.apache.wicket.ajax.markup.html.AjaxLink#onClick(org.apache.wicket.ajax.AjaxRequestTarget)
+ */
+ @Override
+ public void onClick(AjaxRequestTarget target)
+ {
+ callback.onClick(target);
+ }
+ };
+ }
+ else
+ {
+ return new AjaxFallbackLink<Void>(id)
+ {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * @see org.apache.wicket.ajax.markup.html.AjaxFallbackLink#onClick(org.apache.wicket.ajax.AjaxRequestTarget)
+ */
+ @Override
+ public void onClick(AjaxRequestTarget target)
+ {
+ callback.onClick(target);
+ }
+ };
+ }
+ }
+
+ /**
+ * Returns the current type of links on tree items.
+ *
+ * @return The link type
+ */
+ public LinkType getLinkType()
+ {
+ return linkType;
+ }
+
+ /**
+ * Sets the type of links on tree items. After the link type is changed, the whole tree must be
+ * rebuilt (call invalidateAll).
+ *
+ * @param linkType
+ * type of links
+ */
+ public void setLinkType(LinkType linkType)
+ {
+ if (this.linkType != linkType)
+ {
+ this.linkType = linkType;
+ }
+ }
+
+ /**
+ * @see org.apache.wicket.extensions.markup.html.tree.AbstractTree#isForceRebuildOnSelectionChange()
+ */
+ @Override
+ protected boolean isForceRebuildOnSelectionChange()
+ {
+ return false;
+ }
+
+ @Override
+ public void renderHead(IHeaderResponse response)
+ {
+ super.renderHead(response);
+ ResourceReference css = getCSS();
+ if (css != null)
+ {
+ response.render(CssHeaderItem.forReference(css));
+ }
+
+ }
+
+ private LinkType linkType = LinkType.AJAX;
+}
http://git-wip-us.apache.org/repos/asf/wicket/blob/31726809/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/tree/DefaultAbstractTree.java
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/wicket/blob/31726809/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/repeater/util/ProviderSubsetTest.java
----------------------------------------------------------------------
diff --cc wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/repeater/util/ProviderSubsetTest.java
index 0000000,0000000..31e90a0
new file mode 100644
--- /dev/null
+++ b/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/repeater/util/ProviderSubsetTest.java
@@@ -1,0 -1,0 +1,171 @@@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements. See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License. You may obtain a copy of the License at
++ *
++ * http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++package org.apache.wicket.extensions.markup.html.repeater.util;
++
++import java.util.ArrayList;
++import java.util.Arrays;
++import java.util.Iterator;
++import java.util.List;
++
++import org.apache.wicket.extensions.markup.html.repeater.tree.ITreeProvider;
++import org.apache.wicket.extensions.markup.html.repeater.util.ProviderSubset;
++import org.apache.wicket.model.IModel;
++import org.junit.Assert;
++import org.junit.Test;
++
++/**
++ * Test for {@link ProviderSubset}.
++ *
++ * @author svenmeier
++ */
++public class ProviderSubsetTest extends Assert
++{
++ private ITreeProvider<String> provider = new EmptyProvider();
++
++ /**
++ * All models requested from the provider.
++ */
++ private List<StringModel> models = new ArrayList<StringModel>();
++
++ /**
++ * Test set methods.
++ */
++ @Test
++ public void setMethods()
++ {
++ ProviderSubset<String> subset = new ProviderSubset<String>(provider);
++
++ subset.add("A");
++ subset.addAll(Arrays.asList("AA", "AAA"));
++
++ assertEquals(3, subset.size());
++
++ Iterator<String> iterator = subset.iterator();
++ assertTrue(iterator.hasNext());
++ iterator.next();
++ assertTrue(iterator.hasNext());
++ iterator.next();
++ assertTrue(iterator.hasNext());
++ iterator.next();
++ assertFalse(iterator.hasNext());
++ try
++ {
++ iterator.next();
++ fail();
++ }
++ catch (Exception expected)
++ {
++ }
++
++ assertTrue(subset.contains("A"));
++ assertTrue(subset.contains("AA"));
++ assertTrue(subset.contains("AAA"));
++
++ subset.createModel().detach();
++
++ for (StringModel model : models)
++ {
++ assertTrue(model.isDetached());
++ }
++
++ assertTrue(subset.contains("A"));
++ assertTrue(subset.contains("AA"));
++ assertTrue(subset.contains("AAA"));
++ }
++
++ private class StringModel implements IModel<String>
++ {
++
++ private static final long serialVersionUID = 1L;
++
++ private String string;
++
++ private boolean detached;
++
++ public StringModel(String string)
++ {
++ this.string = string;
++ models.add(this);
++ }
++
++ public String getObject()
++ {
++ detached = false;
++ return string;
++ }
++
++ public void setObject(String string)
++ {
++ detached = false;
++ this.string = string;
++ }
++
++ public void detach()
++ {
++ detached = true;
++ }
++
++ public boolean isDetached()
++ {
++ return detached;
++ }
++
++ @Override
++ public boolean equals(Object obj)
++ {
++ return string == ((StringModel)obj).string;
++ }
++
++ @Override
++ public int hashCode()
++ {
++ return string.hashCode();
++ }
++ }
++
++ private class EmptyProvider implements ITreeProvider<String>
++ {
++
++ private static final long serialVersionUID = 1L;
++
++ private List<String> EMPTY = new ArrayList<String>();
++
++ public Iterator<String> getRoots()
++ {
++ return EMPTY.iterator();
++ }
++
++ public boolean hasChildren(String object)
++ {
++ return false;
++ }
++
++ public Iterator<String> getChildren(String string)
++ {
++ throw new UnsupportedOperationException();
++ }
++
++ public IModel<String> model(String string)
++ {
++ return new StringModel(string);
++ }
++
++ public void detach()
++ {
++ }
++ }
++}
http://git-wip-us.apache.org/repos/asf/wicket/blob/31726809/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/repeater/util/TreeModelProviderTest.java
----------------------------------------------------------------------
diff --cc wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/repeater/util/TreeModelProviderTest.java
index 0000000,0000000..840481b
new file mode 100644
--- /dev/null
+++ b/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/repeater/util/TreeModelProviderTest.java
@@@ -1,0 -1,0 +1,156 @@@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements. See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License. You may obtain a copy of the License at
++ *
++ * http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++package org.apache.wicket.extensions.markup.html.repeater.util;
++
++import java.util.Iterator;
++
++import javax.swing.tree.DefaultMutableTreeNode;
++import javax.swing.tree.DefaultTreeModel;
++import javax.swing.tree.MutableTreeNode;
++
++import org.apache.wicket.extensions.markup.html.repeater.util.TreeModelProvider;
++import org.apache.wicket.model.IModel;
++import org.apache.wicket.model.Model;
++import org.junit.Assert;
++import org.junit.Test;
++
++/**
++ * Test for {@link TreeModelProvider}.
++ *
++ * @author svenmeier
++ */
++public class TreeModelProviderTest extends Assert
++{
++ private DefaultMutableTreeNode root;
++
++ private DefaultTreeModel treeModel;
++
++ /**
++ * Construct.
++ */
++ public TreeModelProviderTest()
++ {
++ root = new DefaultMutableTreeNode("JTree");
++ DefaultMutableTreeNode parent;
++
++ parent = new DefaultMutableTreeNode("colors");
++ root.add(parent);
++ parent.add(new DefaultMutableTreeNode("blue"));
++ parent.add(new DefaultMutableTreeNode("violet"));
++ parent.add(new DefaultMutableTreeNode("red"));
++ parent.add(new DefaultMutableTreeNode("yellow"));
++
++ parent = new DefaultMutableTreeNode("sports");
++ root.add(parent);
++ parent.add(new DefaultMutableTreeNode("basketball"));
++ parent.add(new DefaultMutableTreeNode("soccer"));
++ parent.add(new DefaultMutableTreeNode("football"));
++ parent.add(new DefaultMutableTreeNode("hockey"));
++
++ parent = new DefaultMutableTreeNode("food");
++ root.add(parent);
++ parent.add(new DefaultMutableTreeNode("hot dogs"));
++ parent.add(new DefaultMutableTreeNode("pizza"));
++ parent.add(new DefaultMutableTreeNode("ravioli"));
++ parent.add(new DefaultMutableTreeNode("bananas"));
++
++ treeModel = new DefaultTreeModel(root);
++ }
++
++ /**
++ * Test roots and children.
++ */
++ @Test
++ public void rootsAndChildren()
++ {
++ TreeModelProvider<DefaultMutableTreeNode> provider = new TreeModelProvider<DefaultMutableTreeNode>(
++ treeModel)
++ {
++ private static final long serialVersionUID = 1L;
++
++ @Override
++ public IModel<DefaultMutableTreeNode> model(DefaultMutableTreeNode object)
++ {
++ return Model.of(object);
++ }
++ };
++
++ Iterator<DefaultMutableTreeNode> roots = provider.getRoots();
++ assertTrue(roots.hasNext());
++ DefaultMutableTreeNode root = roots.next();
++ assertEquals("JTree", root.getUserObject());
++ assertFalse(roots.hasNext());
++
++ Iterator<DefaultMutableTreeNode> children = provider.getChildren(root);
++ assertTrue(children.hasNext());
++ assertEquals("colors", children.next().getUserObject());
++ assertTrue(children.hasNext());
++ assertEquals("sports", children.next().getUserObject());
++ assertTrue(children.hasNext());
++ assertEquals("food", children.next().getUserObject());
++ assertFalse(roots.hasNext());
++
++ treeModel.nodeChanged(root);
++ }
++
++ /**
++ * Test updating.
++ */
++ @Test
++ public void update()
++ {
++ TreeModelProvider<DefaultMutableTreeNode> provider = new TreeModelProvider<DefaultMutableTreeNode>(
++ treeModel)
++ {
++ private static final long serialVersionUID = 1L;
++
++ @Override
++ public IModel<DefaultMutableTreeNode> model(DefaultMutableTreeNode object)
++ {
++ return Model.of(object);
++ }
++ };
++
++ assertFalse(provider.completeUpdate);
++ assertEquals(null, provider.nodeUpdates);
++ assertEquals(null, provider.branchUpdates);
++
++ treeModel.removeNodeFromParent((MutableTreeNode)root.getChildAt(0).getChildAt(0));
++
++ assertFalse(provider.completeUpdate);
++ assertEquals(null, provider.nodeUpdates);
++ assertEquals(1, provider.branchUpdates.size());
++
++ treeModel.nodeChanged(root.getChildAt(1));
++
++ assertFalse(provider.completeUpdate);
++ assertEquals(1, provider.nodeUpdates.size());
++ assertEquals(1, provider.branchUpdates.size());
++
++ treeModel.nodeStructureChanged(root.getChildAt(2));
++
++ assertFalse(provider.completeUpdate);
++ assertEquals(1, provider.nodeUpdates.size());
++ assertEquals(2, provider.branchUpdates.size());
++
++ treeModel.setRoot(new DefaultMutableTreeNode("bam!"));
++
++ assertTrue(provider.completeUpdate);
++ assertEquals(1, provider.nodeUpdates.size());
++ assertEquals(2, provider.branchUpdates.size());
++ }
++}