001: /*
002: * $Id$ $Revision$ $Date$
003: *
004: * ==============================================================================
005: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
006: * use this file except in compliance with the License. You may obtain a copy of
007: * the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014: * License for the specific language governing permissions and limitations under
015: * the License.
016: */
017: package wicket.extensions.markup.html.tree;
018:
019: import java.io.Serializable;
020:
021: import javax.swing.tree.TreeModel;
022: import javax.swing.tree.TreeNode;
023:
024: import wicket.Component;
025: import wicket.MarkupContainer;
026: import wicket.RequestCycle;
027: import wicket.ResourceReference;
028: import wicket.Response;
029: import wicket.ajax.AjaxRequestTarget;
030: import wicket.ajax.markup.html.AjaxFallbackLink;
031: import wicket.ajax.markup.html.AjaxLink;
032: import wicket.behavior.HeaderContributor;
033: import wicket.markup.ComponentTag;
034: import wicket.markup.MarkupStream;
035: import wicket.markup.html.PackageResourceReference;
036: import wicket.markup.html.WebMarkupContainer;
037: import wicket.markup.html.link.Link;
038: import wicket.model.IModel;
039: import wicket.model.Model;
040: import wicket.util.lang.EnumeratedType;
041:
042: /**
043: * Tree class that contains convenient functions related to presentation of the
044: * tree, which includes junction link, tree item selection link, spacers (with
045: * lines) and default tree item and folder icons.
046: * <p>
047: * The class itself adds no component to tree items. If you use this class
048: * directly, you have to implement populateTreeItem() on your own. If you want
049: * to use an existing (complete) tree class, use {@link Tree}
050: * <p>
051: * This class allows you to choose between 3 types of links.
052: * {@link DefaultAbstractTree#setLinkType(wicket.extensions.markup.html.tree.DefaultAbstractTree.LinkType)}
053: *
054: * @author Matej Knopp
055: */
056: public abstract class DefaultAbstractTree extends AbstractTree {
057: /**
058: * The type of junction links and node selection links.
059: * <dl>
060: * <dt>Regular link</dt>
061: * <dd>Non-ajax link, always refreshes the whole page. Works with
062: * javascript disabled.</dd>
063: * <dt>Ajax link</dt>
064: * <dd>Links that supports partial updates. Doesn't work with javascript
065: * disabled</dd>
066: * <dt>Ajax fallback link</dt>
067: * <dd>Link that supports partial updates. With javascript disabled acts
068: * like regular link. The drawback is that generated url (thus the entire
069: * html) is larger then using the other two</dd>
070: * </dl>
071: */
072: public static final class LinkType extends EnumeratedType {
073:
074: /** partial updates with no fallback. */
075: public static final LinkType AJAX = new LinkType("AJAX");
076:
077: /**
078: * partial updates that falls back to a regular link in case the client
079: * does not support javascript.
080: */
081: public static final LinkType AJAX_FALLBACK = new LinkType(
082: "AJAX_FALLBACK");
083:
084: /**
085: * non-ajax version that always re-renders the whole page.
086: */
087: public static final LinkType REGULAR = new LinkType("REGULAR");
088:
089: private static final long serialVersionUID = 1L;
090:
091: /**
092: * Construct.
093: *
094: * @param name
095: */
096: public LinkType(String name) {
097: super (name);
098: }
099: }
100:
101: /**
102: * Helper class for calling an action from a link.
103: *
104: * @author Matej Knopp
105: */
106: protected interface ILinkCallback extends Serializable {
107: /**
108: * Called when the click is executed.
109: *
110: * @param target
111: * The ajax request target
112: */
113: void onClick(AjaxRequestTarget target);
114: }
115:
116: /**
117: * Reference to the css file.
118: */
119: private static final PackageResourceReference CSS = new PackageResourceReference(
120: DefaultAbstractTree.class, "res/tree.css");
121:
122: /** Reference to the icon of closed tree folder */
123: private static final PackageResourceReference FOLDER_CLOSED = new PackageResourceReference(
124: DefaultAbstractTree.class, "res/folder-closed.gif");
125:
126: /** Reference to the icon of open tree folder */
127: private static final PackageResourceReference FOLDER_OPEN = new PackageResourceReference(
128: DefaultAbstractTree.class, "res/folder-open.gif");
129:
130: /** Reference to the icon of tree item (not a folder) */
131: private static final PackageResourceReference ITEM = new PackageResourceReference(
132: DefaultAbstractTree.class, "res/item.gif");
133:
134: /** The link type, default is {@link LinkType#AJAX ajax}. */
135: private LinkType linkType = LinkType.AJAX;
136:
137: /**
138: * Tree contructor.
139: *
140: * @param id
141: * The component id
142: */
143: public DefaultAbstractTree(String id) {
144: super (id);
145: init();
146: }
147:
148: /**
149: * Tree constructor.
150: *
151: * @param id
152: * The component id
153: * @param model
154: * The tree model
155: */
156: public DefaultAbstractTree(String id, IModel model) {
157: super (id, model);
158: init();
159: };
160:
161: /**
162: * Tree constructor.
163: *
164: * @param id
165: * The component id
166: * @param model
167: * The tree model
168: */
169: public DefaultAbstractTree(String id, TreeModel model) {
170: super (id, new Model((Serializable) model));
171: init();
172: }
173:
174: /**
175: * Returns the current type of links on tree items.
176: *
177: * @return The link type
178: */
179: public LinkType getLinkType() {
180: return linkType;
181: }
182:
183: /**
184: * Sets the type of links on tree items. After the link type is changed, the
185: * whole tree is rebuild and re-rendered.
186: *
187: * @param linkType
188: * type of links
189: */
190: public void setLinkType(LinkType linkType) {
191: if (this .linkType != linkType) {
192: this .linkType = linkType;
193: invalidateAll();
194: }
195: }
196:
197: /**
198: * Returns the resource reference of default stylesheet.
199: *
200: * @return The package resource reference
201: */
202: protected PackageResourceReference getCSS() {
203: return CSS;
204: }
205:
206: /**
207: * Returns the resource reference of default closed tree folder.
208: *
209: * @return The package resource reference
210: */
211: protected ResourceReference getFolderClosed() {
212: return FOLDER_CLOSED;
213: }
214:
215: /**
216: * Returns the resource reference of default open tree folder.
217: *
218: * @return The package resource reference
219: */
220: protected ResourceReference getFolderOpen() {
221: return FOLDER_OPEN;
222: };
223:
224: /**
225: * Returns the resource reference of default tree item (not folder).
226: *
227: * @return The package resource reference
228: */
229: protected ResourceReference getItem() {
230: return ITEM;
231: }
232:
233: /**
234: * Returns the resource reference for icon of specified tree node.
235: *
236: * @param node
237: * The node
238: * @return The package resource reference
239: */
240: protected ResourceReference getNodeIcon(TreeNode node) {
241: if (node.isLeaf() == true) {
242: return getItem();
243: } else {
244: if (isNodeExpanded(node)) {
245: return getFolderOpen();
246: } else {
247: return getFolderClosed();
248: }
249: }
250: }
251:
252: /**
253: * Creates the indentation element. This element should be placed as first
254: * element in the tree item markup to ensure proper indentaion of the tree
255: * item. This implementation also takes care of lines that connect nodes.
256: *
257: * @param parent
258: * The component parent
259: * @param id
260: * The component id
261: * @param node
262: * The tree node for which to create the identation element
263: * @param level
264: * The current level
265: * @return The indentation component
266: */
267: protected Component newIndentation(MarkupContainer parent,
268: String id, final TreeNode node, final int level) {
269: WebMarkupContainer result = new WebMarkupContainer(id) {
270: private static final long serialVersionUID = 1L;
271:
272: /**
273: * @see wicket.MarkupContainer#onComponentTagBody(wicket.markup.MarkupStream,
274: * wicket.markup.ComponentTag)
275: */
276: protected void onComponentTagBody(
277: MarkupStream markupStream, ComponentTag openTag) {
278: Response response = RequestCycle.get().getResponse();
279: TreeNode parent = node.getParent();
280:
281: CharSequence urls[] = new CharSequence[level];
282: for (int i = 0; i < level; ++i) {
283: if (isNodeLast(parent)) {
284: urls[i] = "indent-blank";
285: } else {
286: urls[i] = "indent-line";
287: }
288:
289: parent = parent.getParent();
290: }
291:
292: for (int i = level - 1; i >= 0; --i) {
293: response.write("<span class=\"" + urls[i]
294: + "\"></span>");
295: }
296: }
297: };
298: result.setRenderBodyOnly(true);
299: return result;
300: }
301:
302: /**
303: * Creates an image placed on junction link. This image actually consists of
304: * two spans with different css classes. These classes are specified
305: * according to the stylesheet to make the junction image look well together
306: * with lines connecting nodes.
307: *
308: * @param parent
309: * The component parent
310: * @param id
311: * The component id
312: * @param node
313: * The tree node
314: * @return The component that resprents a junction
315: */
316: protected MarkupContainer newJunctionImage(MarkupContainer parent,
317: final String id, final TreeNode node) {
318: return (MarkupContainer) new WebMarkupContainer(id) {
319: private static final long serialVersionUID = 1L;
320:
321: /**
322: * @see wicket.Component#onComponentTag(wicket.markup.ComponentTag)
323: */
324: protected void onComponentTag(ComponentTag tag) {
325: super .onComponentTag(tag);
326:
327: final String cssClassInner;
328: if (node.isLeaf() == false) {
329: cssClassInner = isNodeExpanded(node) ? "minus"
330: : "plus";
331: } else {
332: cssClassInner = "corner";
333: }
334:
335: final String cssClassOuter = isNodeLast(node) ? "junction-last"
336: : "junction";
337:
338: Response response = RequestCycle.get().getResponse();
339: response.write("<span class=\"" + cssClassOuter
340: + "\"><span class=\"" + cssClassInner
341: + "\"></span></span>");
342: }
343: }.setRenderBodyOnly(true);
344: }
345:
346: /**
347: * Creates the junction link for given node. Also (optionally) creates the
348: * junction image. If the node is a leaf (it has no children), the created
349: * junction link is non-functional.
350: *
351: * @param parent
352: * parent component of the link
353: *
354: * @param id
355: * wicket:id of the component
356: *
357: * @param imageId
358: * wicket:id of the image. this can be null, in that case image
359: * is not created. image is supposed to be placed on the link
360: * (link is parent of image)
361: *
362: * @param node
363: * tree node for which the link should be created.
364: * @return The link component
365: */
366: protected Component newJunctionLink(MarkupContainer parent,
367: final String id, final String imageId, final TreeNode node) {
368: final MarkupContainer junctionLink;
369:
370: if (node.isLeaf() == false) {
371: junctionLink = newLink(parent, id, new ILinkCallback() {
372: private static final long serialVersionUID = 1L;
373:
374: public void onClick(AjaxRequestTarget target) {
375: if (isNodeExpanded(node)) {
376: getTreeState().collapseNode(node);
377: } else {
378: getTreeState().expandNode(node);
379: }
380: onJunctionLinkClicked(target, node);
381: updateTree(target);
382: }
383: });
384: } else {
385: junctionLink = new WebMarkupContainer(id) {
386: private static final long serialVersionUID = 1L;
387:
388: /**
389: * @see wicket.Component#onComponentTag(wicket.markup.ComponentTag)
390: */
391: protected void onComponentTag(ComponentTag tag) {
392: super .onComponentTag(tag);
393: tag.put("onclick", "return false");
394: }
395: };
396:
397: }
398:
399: if (imageId != null) {
400: junctionLink.add(newJunctionImage(junctionLink, imageId,
401: node));
402: }
403:
404: return junctionLink;
405: }
406:
407: /**
408: * Creates a link of type specified by current linkType. When the links is
409: * clicked it calls the specified callback.
410: *
411: * @param parent
412: * The parent component
413: * @param id
414: * The component id
415: * @param callback
416: * The link call back
417: * @return The link component
418: */
419: protected MarkupContainer newLink(MarkupContainer parent,
420: String id, final ILinkCallback callback) {
421: if (getLinkType() == LinkType.REGULAR) {
422: return new Link(id) {
423: private static final long serialVersionUID = 1L;
424:
425: /**
426: * @see wicket.markup.html.link.Link#onClick()
427: */
428: public void onClick() {
429: callback.onClick(null);
430: }
431: };
432: } else if (getLinkType() == LinkType.AJAX) {
433: return new AjaxLink(id) {
434: private static final long serialVersionUID = 1L;
435:
436: /**
437: * @see wicket.ajax.markup.html.AjaxLink#onClick(wicket.ajax.AjaxRequestTarget)
438: */
439: public void onClick(AjaxRequestTarget target) {
440: callback.onClick(target);
441: }
442: };
443: } else {
444: return new AjaxFallbackLink(id) {
445: private static final long serialVersionUID = 1L;
446:
447: /**
448: * @see wicket.ajax.markup.html.AjaxFallbackLink#onClick(wicket.ajax.AjaxRequestTarget)
449: */
450: public void onClick(AjaxRequestTarget target) {
451: callback.onClick(target);
452: }
453: };
454: }
455: }
456:
457: /**
458: * Creates the icon for current node. By default uses image reference
459: * specified by {@link DefaultAbstractTree#getNodeIcon(TreeNode)}.
460: *
461: * @param parent
462: * The parent component
463: * @param id
464: * The component id
465: * @param node
466: * The tree node
467: * @return The web component that represents the icon of the current node
468: */
469: protected Component newNodeIcon(MarkupContainer parent, String id,
470: final TreeNode node) {
471: return new WebMarkupContainer(id) {
472: private static final long serialVersionUID = 1L;
473:
474: protected void onComponentTag(ComponentTag tag) {
475: super .onComponentTag(tag);
476: tag.put("style", "background-image: url('"
477: + RequestCycle.get().urlFor(getNodeIcon(node))
478: + "')");
479: }
480: };
481:
482: }
483:
484: /**
485: * Creates a link that can be used to select / unselect the specified node.
486: *
487: * @param parent
488: * The parent component
489: * @param id
490: * The component id
491: * @param node
492: * The parent node
493: * @return The component that represents the link
494: */
495: protected MarkupContainer newNodeLink(MarkupContainer parent,
496: String id, final TreeNode node) {
497: return newLink(parent, id, new ILinkCallback() {
498: private static final long serialVersionUID = 1L;
499:
500: public void onClick(AjaxRequestTarget target) {
501: getTreeState().selectNode(node,
502: !getTreeState().isNodeSelected(node));
503: onNodeLinkClicked(target, node);
504: updateTree(target);
505: }
506: });
507: }
508:
509: /**
510: * Callback function called after user clicked on an junction link. The node
511: * has already been expanded/collapsed (depending on previous status).
512: *
513: * @param target
514: * Request target - may be null on non-ajax call
515: *
516: * @param node
517: * Node for which this callback is relevant
518: */
519: protected void onJunctionLinkClicked(AjaxRequestTarget target,
520: TreeNode node) {
521: }
522:
523: /**
524: * This callback method is called after user has selected / deselected the
525: * given node.
526: *
527: * @param target
528: * Request target - may be null on non-ajax call
529: *
530: * @param node
531: * Node for which this this callback is fired.
532: */
533: protected void onNodeLinkClicked(AjaxRequestTarget target,
534: TreeNode node) {
535: }
536:
537: /**
538: * Performs the tree initialization. Adds header contribution for the
539: * stylesheet.
540: */
541: private void init() {
542: PackageResourceReference css = getCSS();
543: if (css != null) {
544: add(HeaderContributor.forCss(css.getScope(), css.getName()));
545: }
546: }
547:
548: /**
549: * Returns whether the provided node is last child of it's parent.
550: *
551: * @param node
552: * The node
553: * @return whether the provided node is the last child
554: */
555: private boolean isNodeLast(TreeNode node) {
556: TreeNode parent = node.getParent();
557: if (parent == null) {
558: return true;
559: } else {
560: return parent.getChildAt(parent.getChildCount() - 1)
561: .equals(node);
562: }
563: }
564: }
|