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