001: /*
002: * $Id: Tree.java 460577 2006-05-09 16:39:14Z ehillenius $
003: * $Revision: 460577 $ $Date: 2006-05-09 18:39:14 +0200 (Tue, 09 May 2006) $
004: *
005: * ==============================================================================
006: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
007: * use this file except in compliance with the License. You may obtain a copy of
008: * the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
014: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
015: * License for the specific language governing permissions and limitations under
016: * the License.
017: */
018: package wicket.markup.html.tree;
019:
020: import java.util.ArrayList;
021: import java.util.Enumeration;
022: import java.util.List;
023:
024: import javax.swing.event.TreeModelEvent;
025: import javax.swing.event.TreeModelListener;
026: import javax.swing.tree.DefaultMutableTreeNode;
027: import javax.swing.tree.TreeModel;
028: import javax.swing.tree.TreePath;
029:
030: import wicket.AttributeModifier;
031: import wicket.Component;
032: import wicket.ResourceReference;
033: import wicket.WicketRuntimeException;
034: import wicket.behavior.HeaderContributor;
035: import wicket.markup.html.PackageResourceReference;
036: import wicket.markup.html.basic.Label;
037: import wicket.markup.html.image.Image;
038: import wicket.markup.html.link.Link;
039: import wicket.markup.html.list.ListItem;
040: import wicket.markup.html.list.ListView;
041: import wicket.markup.html.list.Loop;
042: import wicket.markup.html.panel.Panel;
043: import wicket.model.AbstractReadOnlyDetachableModel;
044: import wicket.model.IModel;
045:
046: /**
047: * An tree that renders as a flat (not-nested) list, using spacers for
048: * indentation and nodes at the end of one row.
049: * <p>
050: * The visible tree rows are put in one flat list. For each row, a list is
051: * constructed with fillers, that can be used to create indentation. After the
052: * fillers, the actual node content is put.
053: * </p>
054: * <p>
055: * </p>
056: *
057: * @author Eelco Hillenius
058: */
059: public class Tree extends AbstractTree implements TreeModelListener {
060: /**
061: * The default node panel. If you provide your own panel by overriding
062: * Tree.newNodePanel, but only want to override the markup, not the
063: * components that are added, you <i>may</i> extend this class. If you want
064: * to use other components than the default, provide a panel or fragment
065: * instead (and that's probably what you want as the look and feel of what
066: * this panel renders may be adjusted by overriding
067: * {@link Tree#createJunctionLink(DefaultMutableTreeNode)} and
068: * {@link Tree#createNodeLink(DefaultMutableTreeNode)}.
069: */
070: public static class DefaultNodePanel extends Panel {
071: private static final long serialVersionUID = 1L;
072:
073: /**
074: * Construct.
075: *
076: * @param panelId
077: * The component id
078: * @param tree
079: * The containing tree component
080: * @param node
081: * The tree node for this panel
082: */
083: public DefaultNodePanel(String panelId, Tree tree,
084: DefaultMutableTreeNode node) {
085: super (panelId);
086: // create a link for expanding and collapsing the node
087: Link expandCollapsLink = tree.createJunctionLink(node);
088: add(expandCollapsLink);
089: // create a link for selecting a node
090: Link selectLink = tree.createNodeLink(node);
091: add(selectLink);
092: }
093: }
094:
095: /**
096: * Renders spacer items.
097: */
098: private static final class SpacerList extends Loop {
099: private static final long serialVersionUID = 1L;
100:
101: /**
102: * Construct.
103: *
104: * @param id
105: * component id
106: * @param size
107: * size of loop
108: */
109: public SpacerList(String id, int size) {
110: super (id, size);
111: }
112:
113: /**
114: * @see wicket.markup.html.list.Loop#populateItem(LoopItem)
115: */
116: protected void populateItem(final Loop.LoopItem loopItem) {
117: // nothing needed; we just render the tags and use CSS to indent
118: }
119: }
120:
121: /**
122: * List view for tree paths.
123: */
124: private final class TreePathsListView extends ListView {
125: private static final long serialVersionUID = 1L;
126:
127: /**
128: * Construct.
129: *
130: * @param name
131: * name of the component
132: */
133: public TreePathsListView(String name) {
134: super (name, treePathsModel);
135: }
136:
137: /**
138: * @see wicket.markup.html.list.ListView#getReuseItems()
139: */
140: public boolean getReuseItems() {
141: return Tree.this .getOptimizeItemRemoval();
142: }
143:
144: /**
145: * @see wicket.markup.html.list.ListView#newItem(int)
146: */
147: protected ListItem newItem(final int index) {
148: IModel listItemModel = getListItemModel(getModel(), index);
149:
150: // create a list item that is smart enough to determine whether
151: // it should be displayed or not
152: return new ListItem(index, listItemModel) {
153: private static final long serialVersionUID = 1L;
154:
155: public boolean isVisible() {
156: TreeState treeState = getTreeState();
157: DefaultMutableTreeNode node = (DefaultMutableTreeNode) getModelObject();
158: final TreePath path = new TreePath(node.getPath());
159: final int row = treeState.getRowForPath(path);
160:
161: // if the row is -1, it is not visible, otherwise it is
162: return (row != -1);
163: }
164: };
165: }
166:
167: /**
168: * @see wicket.markup.html.list.ListView#populateItem(wicket.markup.html.list.ListItem)
169: */
170: protected void populateItem(ListItem listItem) {
171: // get the model object which is a tree node
172: DefaultMutableTreeNode node = (DefaultMutableTreeNode) listItem
173: .getModelObject();
174:
175: // add spacers
176: int level = node.getLevel();
177: listItem.add(new SpacerList("spacers", level));
178:
179: // add node panel
180: Component nodePanel = newNodePanel("node", node);
181: if (nodePanel == null) {
182: throw new WicketRuntimeException(
183: "node panel must be not-null");
184: }
185: if (!"node".equals(nodePanel.getId())) {
186: throw new WicketRuntimeException(
187: "panel must have id 'node' assigned");
188: }
189:
190: listItem.add(nodePanel);
191:
192: // add attr modifier for highlighting the selection
193: listItem.add(new AttributeModifier("class", true,
194: new SelectedPathReplacementModel(Tree.this , node)));
195: }
196: }
197:
198: /**
199: * Model for the paths of the tree.
200: */
201: private final class TreePathsModel extends
202: AbstractReadOnlyDetachableModel {
203: private static final long serialVersionUID = 1L;
204:
205: /** whether this model is dirty. */
206: boolean dirty = true;
207:
208: /** tree paths. */
209: private List paths = new ArrayList();
210:
211: /**
212: * @see wicket.model.AbstractDetachableModel#getNestedModel()
213: */
214: public IModel getNestedModel() {
215: // TODO General: Check calls to this method; original: return paths;
216: return null;
217: }
218:
219: /**
220: * @see wicket.model.AbstractDetachableModel#onAttach()
221: */
222: protected void onAttach() {
223: if (dirty) {
224: paths.clear();
225: TreeModel model = getTreeState().getModel();
226: DefaultMutableTreeNode rootNode = (DefaultMutableTreeNode) model
227: .getRoot();
228: Enumeration e = rootNode.preorderEnumeration();
229: while (e.hasMoreElements()) {
230: DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) e
231: .nextElement();
232: // TreePath path = new TreePath(treeNode.getPath());
233: paths.add(treeNode);
234: }
235: dirty = false;
236: }
237: }
238:
239: /**
240: * @see wicket.model.AbstractDetachableModel#onDetach()
241: */
242: protected void onDetach() {
243: }
244:
245: /**
246: * @see wicket.model.AbstractDetachableModel#onGetObject(wicket.Component)
247: */
248: protected Object onGetObject(Component component) {
249: return paths;
250: }
251:
252: /**
253: * Inserts the given node in the path list with the given index.
254: *
255: * @param index
256: * the index where the node should be inserted in
257: * @param node
258: * node to insert
259: */
260: void add(int index, DefaultMutableTreeNode node) {
261: paths.add(index, node);
262: }
263:
264: /**
265: * Gives the index of the given node withing this tree.
266: *
267: * @param node
268: * node to look for
269: * @return the index of the given node withing this tree
270: */
271: int indexOf(DefaultMutableTreeNode node) {
272: return paths.indexOf(node);
273: }
274:
275: /**
276: * Removes the given node from the path list.
277: *
278: * @param node
279: * the node to remove
280: */
281: void remove(DefaultMutableTreeNode node) {
282: paths.remove(node);
283: }
284: }
285:
286: /** Name of the junction image component; value = 'junctionImage'. */
287: public static final String JUNCTION_IMAGE_NAME = "junctionImage";
288:
289: /** Name of the node image component; value = 'nodeImage'. */
290: public static final String NODE_IMAGE_NAME = "nodeImage";
291:
292: /** Blank image. */
293: private static final ResourceReference BLANK = new PackageResourceReference(
294: Tree.class, "blank.gif");
295:
296: /**
297: * Reference to the css file.
298: */
299: private static final PackageResourceReference CSS = new PackageResourceReference(
300: Tree.class, "tree.css");
301:
302: /** Minus sign image. */
303: private static final ResourceReference MINUS = new PackageResourceReference(
304: Tree.class, "minus.gif");
305:
306: /** Plus sign image. */
307: private static final ResourceReference PLUS = new PackageResourceReference(
308: Tree.class, "plus.gif");
309:
310: private static final long serialVersionUID = 1L;
311:
312: /**
313: * If true, re-rendering the tree is more efficient if the tree model
314: * doesn't get changed. However, if this is true, you need to push changes
315: * to this tree. This can easility be done by registering this tree as the
316: * listener for tree model events (TreeModelListener), but you should <b>be
317: * carefull</b> not to create a memory leak by doing this (e.g. when you
318: * store the tree model in your session, the tree you registered cannot be
319: * GC-ed). TRUE by default.
320: */
321: private boolean reuseItems = true;
322:
323: /** List view for tree paths. */
324: private TreePathsListView treePathsListView;
325:
326: /** Model for the paths of the tree. */
327: private TreePathsModel treePathsModel;
328:
329: /**
330: * Constructor.
331: *
332: * @param id
333: * The id of this container
334: * @param model
335: * the underlying tree model
336: */
337: public Tree(final String id, final TreeModel model) {
338: super (id, model);
339: this .treePathsModel = new TreePathsModel();
340: add(treePathsListView = createTreePathsListView());
341:
342: PackageResourceReference css = getCss();
343: add(HeaderContributor.forCss(css.getScope(), css.getName()));
344: }
345:
346: /**
347: * Construct using the given tree state that holds the model to be used as
348: * the tree model.
349: *
350: * @param id
351: * The id of this container
352: * @param treeState
353: * treeState that holds the underlying tree model
354: */
355: public Tree(String id, TreeState treeState) {
356: super (id, treeState);
357: this .treePathsModel = new TreePathsModel();
358: add(treePathsListView = createTreePathsListView());
359:
360: PackageResourceReference css = getCss();
361: add(HeaderContributor.forCss(css.getScope(), css.getName()));
362: }
363:
364: /**
365: * Gets whether item removal should be optimized. If true, re-rendering the
366: * tree is more efficient if the tree model doesn't get changed. However, if
367: * this is true, you need to push changes to this tree. This can easility be
368: * done by registering this tree as the listener for tree model events
369: * (TreeModelListener), but you should <b>be carefull</b> not to create a
370: * memory leak by doing this (e.g. when you store the tree model in your
371: * session, the tree you registered cannot be GC-ed). TRUE by default.
372: *
373: * @return whether item removal should be optimized
374: * @deprecated Will be replaced by {@link #getReuseItems()}
375: */
376: // TODO Post 1.2: Remove
377: public boolean getOptimizeItemRemoval() {
378: return getReuseItems();
379: }
380:
381: /**
382: * Gets whether items should be reused. If true, re-rendering the tree is
383: * more efficient if the tree model doesn't get changed. However, if this is
384: * true, you need to push changes to this tree. This can easility be done by
385: * registering this tree as the listener for tree model events
386: * (TreeModelListener), but you should <b>be carefull</b> not to create a
387: * memory leak by doing this (e.g. when you store the tree model in your
388: * session, the tree you registered cannot be GC-ed). TRUE by default.
389: *
390: * @return whether items should be reused
391: */
392: public boolean getReuseItems() {
393: return reuseItems;
394: }
395:
396: /**
397: * Sets whether items should be reused. If true, re-rendering the tree is
398: * more efficient if the tree model doesn't get changed. However, if this is
399: * true, you need to push changes to this tree. This can easility be done by
400: * registering this tree as the listener for tree model events
401: * (TreeModelListener), but you should <b>be carefull</b> not to create a
402: * memory leak by doing this (e.g. when you store the tree model in your
403: * session, the tree you registered cannot be GC-ed). TRUE by default.
404: *
405: * @param optimizeItemRemoval
406: * whether the child items should be reused
407: * @deprecated Will be replaced by {@link #setReuseItems(boolean)}
408: */
409: // TODO Post 1.2: Remove
410: public void setOptimizeItemRemoval(boolean optimizeItemRemoval) {
411: setReuseItems(optimizeItemRemoval);
412: }
413:
414: /**
415: * Sets whether item removal should be optimized. If true, re-rendering the
416: * tree is more efficient if the tree model doesn't get changed. However, if
417: * this is true, you need to push changes to this tree. This can easility be
418: * done by registering this tree as the listener for tree model events
419: * (TreeModelListener), but you should <b>be carefull</b> not to create a
420: * memory leak by doing this (e.g. when you store the tree model in your
421: * session, the tree you registered cannot be GC-ed). TRUE by default.
422: *
423: * @param reuseItems
424: * whether the child items should be reused
425: * @return This
426: */
427: public Tree setReuseItems(boolean reuseItems) {
428: this .reuseItems = reuseItems;
429: return this ;
430: }
431:
432: /**
433: * Sets the current tree model.
434: *
435: * @param treeModel
436: * the tree model to set as the current one
437: */
438: public void setTreeModel(final TreeModel treeModel) {
439: super .setTreeModel(treeModel);
440: this .treePathsModel = new TreePathsModel();
441: treePathsListView = createTreePathsListView();
442: replace(treePathsListView);
443: }
444:
445: /**
446: * Sets the current tree state to the given tree state.
447: *
448: * @param treeState
449: * the tree state to set as the current one
450: */
451: public void setTreeState(final TreeState treeState) {
452: super .setTreeState(treeState);
453: this .treePathsModel = new TreePathsModel();
454: treePathsListView = createTreePathsListView();
455: replace(treePathsListView);
456: }
457:
458: /**
459: * @see javax.swing.event.TreeModelListener#treeNodesChanged(javax.swing.event.TreeModelEvent)
460: */
461: public void treeNodesChanged(TreeModelEvent e) {
462: // nothing to do here
463: }
464:
465: /**
466: * @see javax.swing.event.TreeModelListener#treeNodesInserted(javax.swing.event.TreeModelEvent)
467: */
468: public void treeNodesInserted(TreeModelEvent e) {
469: modelChanging();
470: Object[] newNodes = e.getChildren();
471: int len = newNodes.length;
472: for (int i = 0; i < len; i++) {
473: DefaultMutableTreeNode newNode = (DefaultMutableTreeNode) newNodes[i];
474: DefaultMutableTreeNode previousNode = newNode
475: .getPreviousSibling();
476: int insertRow;
477: if (previousNode == null) {
478: previousNode = (DefaultMutableTreeNode) newNode
479: .getParent();
480: }
481: if (previousNode != null) {
482: insertRow = treePathsModel.indexOf(previousNode) + 1;
483: if (insertRow == -1) {
484: throw new IllegalStateException("node "
485: + previousNode
486: + " not found in backing list");
487: }
488: } else {
489: insertRow = 0;
490: }
491: treePathsModel.add(insertRow, newNode);
492: }
493: modelChanged();
494: }
495:
496: /**
497: * @see javax.swing.event.TreeModelListener#treeNodesRemoved(javax.swing.event.TreeModelEvent)
498: */
499: public void treeNodesRemoved(TreeModelEvent e) {
500: modelChanging();
501: Object[] deletedNodes = e.getChildren();
502: int len = deletedNodes.length;
503: for (int i = 0; i < len; i++) {
504: DefaultMutableTreeNode deletedNode = (DefaultMutableTreeNode) deletedNodes[i];
505: treePathsModel.remove(deletedNode);
506: }
507: modelChanged();
508: }
509:
510: /**
511: * @see javax.swing.event.TreeModelListener#treeStructureChanged(javax.swing.event.TreeModelEvent)
512: */
513: public void treeStructureChanged(TreeModelEvent e) {
514: treePathsModel.dirty = true;
515: modelChanged();
516: }
517:
518: /**
519: * Creates a junction link.
520: *
521: * @param node
522: * the node
523: * @return link for expanding/ collapsing the tree
524: */
525: protected Link createJunctionLink(final DefaultMutableTreeNode node) {
526: final Link junctionLink = new Link("junctionLink") {
527: private static final long serialVersionUID = 1L;
528:
529: public void onClick() {
530: junctionLinkClicked(node);
531: }
532: };
533: junctionLink.add(getJunctionImage(node));
534: return junctionLink;
535: }
536:
537: /**
538: * Creates a node link.
539: *
540: * @param node
541: * the model of the node
542: * @return link for selection
543: */
544: protected Link createNodeLink(final DefaultMutableTreeNode node) {
545: final Link nodeLink = new Link("nodeLink") {
546: private static final long serialVersionUID = 1L;
547:
548: public void onClick() {
549: nodeLinkClicked(node);
550: }
551: };
552: nodeLink.add(getNodeImage(node));
553: nodeLink.add(new Label("label", getNodeLabel(node)));
554: return nodeLink;
555: }
556:
557: /**
558: * Creates the tree paths list view.
559: *
560: * @return the tree paths list view
561: */
562: protected final TreePathsListView createTreePathsListView() {
563: final TreePathsListView treePaths = new TreePathsListView(
564: "tree");
565: return treePaths;
566: }
567:
568: /**
569: * Returns whether the path and the selected path are equal. This method is
570: * used by the {@link AttributeModifier}that is used for setting the CSS
571: * class for the selected row.
572: *
573: * @param path
574: * the path
575: * @param selectedPath
576: * the selected path
577: * @return true if the path and the selected are equal, false otherwise
578: */
579: protected boolean equals(final TreePath path,
580: final TreePath selectedPath) {
581: Object pathNode = path.getLastPathComponent();
582: Object selectedPathNode = selectedPath.getLastPathComponent();
583: return (pathNode != null && selectedPathNode != null && pathNode
584: .equals(selectedPathNode));
585: }
586:
587: /**
588: * Gets the stylesheet.
589: *
590: * @return the stylesheet
591: */
592: protected PackageResourceReference getCss() {
593: return CSS;
594: }
595:
596: /**
597: * Get image for a junction; used by method createExpandCollapseLink. If you
598: * use the packaged panel (Tree.html), you must name the component using
599: * JUNCTION_IMAGE_NAME.
600: *
601: * @param node
602: * the tree node
603: * @return the image for the junction
604: */
605: protected Image getJunctionImage(final DefaultMutableTreeNode node) {
606: if (!node.isLeaf()) {
607: // we want the image to be dynamically, yet resolving to a static
608: // image.
609: return new Image(JUNCTION_IMAGE_NAME) {
610: private static final long serialVersionUID = 1L;
611:
612: protected ResourceReference getImageResourceReference() {
613: if (isExpanded(node)) {
614: return MINUS;
615: } else {
616: return PLUS;
617: }
618: }
619: };
620: } else {
621: return new Image(JUNCTION_IMAGE_NAME, BLANK);
622: }
623: }
624:
625: /**
626: * Get image for a node; used by method createNodeLink. If you use the
627: * packaged panel (Tree.html), you must name the component using
628: * NODE_IMAGE_NAME.
629: *
630: * @param node
631: * the tree node
632: * @return the image for the node
633: */
634: protected Image getNodeImage(final DefaultMutableTreeNode node) {
635: return new Image(NODE_IMAGE_NAME, BLANK);
636: }
637:
638: /**
639: * Gets the label of the node that is used for the node link. Defaults to
640: * treeNodeModel.getUserObject().toString(); override to provide a custom
641: * label
642: *
643: * @param node
644: * the tree node
645: * @return the label of the node that is used for the node link
646: */
647: protected String getNodeLabel(final DefaultMutableTreeNode node) {
648: return String.valueOf(node.getUserObject());
649: }
650:
651: /**
652: * @see wicket.Component#internalOnAttach()
653: */
654: protected void internalOnAttach() {
655: // if we don't optimize, rebuild the paths on every request
656: if (!getOptimizeItemRemoval()) {
657: treePathsModel.dirty = true;
658: }
659: }
660:
661: /**
662: * Handler that is called when a junction link is clicked; this
663: * implementation sets the expanded state to one that corresponds with the
664: * node selection.
665: *
666: * @param node
667: * the tree node
668: */
669: protected void junctionLinkClicked(final DefaultMutableTreeNode node) {
670: setExpandedState(node);
671: }
672:
673: /**
674: * Create a new panel for a tree node. This method can be overriden to
675: * provide a custom panel. This way, you can effectively nest anything you
676: * want in the tree, like input fields, images, etc.
677: * <p>
678: * <strong> you must use the provide panelId as the id of your custom panel
679: * </strong><br>
680: * for example, do:
681: *
682: * <pre>
683: * return new MyNodePanel(panelId, node);
684: * </pre>
685: *
686: * </p>
687: * <p>
688: * You can choose to either let your own panel extend from DefaultNodePanel
689: * when you just want to provide different markup but want to reuse the
690: * default components on this panel, or extend from NodePanel directly, and
691: * provide any component structure you like.
692: * </p>
693: *
694: * @param panelId
695: * the id that the panel MUST use
696: * @param node
697: * the tree node for the panel
698: * @return a new Panel
699: */
700: protected Component newNodePanel(String panelId,
701: DefaultMutableTreeNode node) {
702: return new DefaultNodePanel(panelId, this , node);
703: }
704:
705: /**
706: * Handler that is called when a node link is clicked; this implementation
707: * sets the expanded state just as a click on a junction would do. Override
708: * this for custom behavior.
709: *
710: * @param node
711: * the tree node model
712: */
713: protected void nodeLinkClicked(final DefaultMutableTreeNode node) {
714: setSelected(node);
715: }
716: }
|