001: /* *************************************************************************
002:
003: Millstone(TM)
004: Open Sourced User Interface Library for
005: Internet Development with Java
006:
007: Millstone is a registered trademark of IT Mill Ltd
008: Copyright (C) 2000-2005 IT Mill Ltd
009:
010: *************************************************************************
011:
012: This library is free software; you can redistribute it and/or
013: modify it under the terms of the GNU Lesser General Public
014: license version 2.1 as published by the Free Software Foundation.
015:
016: This library is distributed in the hope that it will be useful,
017: but WITHOUT ANY WARRANTY; without even the implied warranty of
018: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
019: Lesser General Public License for more details.
020:
021: You should have received a copy of the GNU Lesser General Public
022: License along with this library; if not, write to the Free Software
023: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
024:
025: *************************************************************************
026:
027: For more information, contact:
028:
029: IT Mill Ltd phone: +358 2 4802 7180
030: Ruukinkatu 2-4 fax: +358 2 4802 7181
031: 20540, Turku email: info@itmill.com
032: Finland company www: www.itmill.com
033:
034: Primary source for MillStone information and releases: www.millstone.org
035:
036: ********************************************************************** */
037:
038: package org.millstone.base.ui;
039:
040: import java.lang.reflect.Method;
041: import java.util.Collection;
042: import java.util.HashSet;
043: import java.util.Iterator;
044: import java.util.LinkedHashSet;
045: import java.util.LinkedList;
046: import java.util.Map;
047: import java.util.Set;
048: import java.util.Stack;
049: import java.util.StringTokenizer;
050:
051: import org.millstone.base.data.Container;
052: import org.millstone.base.data.util.ContainerHierarchicalWrapper;
053: import org.millstone.base.event.Action;
054: import org.millstone.base.terminal.KeyMapper;
055: import org.millstone.base.terminal.PaintException;
056: import org.millstone.base.terminal.PaintTarget;
057: import org.millstone.base.terminal.Resource;
058:
059: /** MenuTree component.
060: * MenuTree can be used to select an item (or multiple items)
061: * from a hierarchical set of items.
062: *
063: * @author IT Mill Ltd.
064: * @version 3.1.1
065: * @since 3.0
066: */
067: public class Tree extends Select implements Container.Hierarchical,
068: Action.Container {
069:
070: /* Static members ***************************************************** */
071:
072: private static final Method EXPAND_METHOD;
073: private static final Method COLLAPSE_METHOD;
074:
075: static {
076: try {
077: EXPAND_METHOD = ExpandListener.class.getDeclaredMethod(
078: "nodeExpand", new Class[] { ExpandEvent.class });
079: COLLAPSE_METHOD = CollapseListener.class
080: .getDeclaredMethod("nodeCollapse",
081: new Class[] { CollapseEvent.class });
082: } catch (java.lang.NoSuchMethodException e) {
083: // This should never happen
084: e.printStackTrace();
085: throw new java.lang.RuntimeException(
086: "Internal error, please report");
087: }
088: }
089:
090: /* Private members **************************************************** */
091:
092: /** Set of expanded nodes */
093: private HashSet expanded = new HashSet();
094:
095: /** List of action handlers */
096: private LinkedList actionHandlers = null;
097:
098: /** Action mapper */
099: private KeyMapper actionMapper = null;
100:
101: /** Is the tree selectable */
102: private boolean selectable = true;
103:
104: /* Tree constructors ************************************************** */
105:
106: /** Create new empty tree */
107: public Tree() {
108: }
109:
110: /** Create new empty tree with caption. */
111: public Tree(String caption) {
112: setCaption(caption);
113: }
114:
115: /** Create new tree with caption and connect it to a Container. */
116: public Tree(String caption, Container dataSource) {
117: setCaption(caption);
118: setContainerDataSource(dataSource);
119: }
120:
121: /* Expanding and collapsing ******************************************* */
122:
123: /** Check is an item is expanded
124: * @return true iff the item is expanded
125: */
126: public boolean isExpanded(Object itemId) {
127: return expanded.contains(itemId);
128: }
129:
130: /** Expand an item.
131: *
132: * @return True iff the expand operation succeeded
133: */
134: public boolean expandItem(Object itemId) {
135:
136: // Succeeds if the node is already expanded
137: if (isExpanded(itemId))
138: return true;
139:
140: // Nodes that can not have children are not expandable
141: if (!areChildrenAllowed(itemId))
142: return false;
143:
144: // Expand
145: expanded.add(itemId);
146: requestRepaint();
147: fireExpandEvent(itemId);
148:
149: return true;
150: }
151:
152: /** Expand items recursively
153: *
154: * Expands all the children recursively starting from an item.
155: * Operation succeeds only if all expandable items are expanded.
156: * @return True iff the expand operation succeeded
157: */
158: public boolean expandItemsRecursively(Object startItemId) {
159:
160: boolean result = true;
161:
162: // Initial stack
163: Stack todo = new Stack();
164: todo.add(startItemId);
165:
166: // Expand recursively
167: while (!todo.isEmpty()) {
168: Object id = todo.pop();
169: if (areChildrenAllowed(id) && !expandItem(id)) {
170: result = false;
171: }
172: if (hasChildren(id)) {
173: todo.addAll(getChildren(id));
174: }
175: }
176:
177: return result;
178: }
179:
180: /** Collapse an item.
181: *
182: * @return True iff the collapse operation succeeded
183: */
184: public boolean collapseItem(Object itemId) {
185:
186: // Succeeds if the node is already collapsed
187: if (!isExpanded(itemId))
188: return true;
189:
190: // Collapse
191: expanded.remove(itemId);
192: requestRepaint();
193: fireCollapseEvent(itemId);
194:
195: return true;
196: }
197:
198: /** Collapse items recursively
199: *
200: * Collapse all the children recursively starting from an item.
201: * Operation succeeds only if all expandable items are collapsed.
202: * @return True iff the collapse operation succeeded
203: */
204: public boolean collapseItemsRecursively(Object startItemId) {
205:
206: boolean result = true;
207:
208: // Initial stack
209: Stack todo = new Stack();
210: todo.add(startItemId);
211:
212: // Collapse recursively
213: while (!todo.isEmpty()) {
214: Object id = todo.pop();
215: if (areChildrenAllowed(id) && !collapseItem(id)) {
216: result = false;
217: }
218: if (hasChildren(id)) {
219: todo.addAll(getChildren(id));
220: }
221: }
222:
223: return result;
224: }
225:
226: /** Getter for property selectable.
227: *
228: * <p>The tree is selectable by default.</p>
229: *
230: * @return Value of property selectable.
231: */
232: public boolean isSelectable() {
233: return this .selectable;
234: }
235:
236: /** Setter for property selectable.
237: *
238: * <p>The tree is selectable by default.</p>
239: *
240: * @param selectable New value of property selectable.
241: */
242: public void setSelectable(boolean selectable) {
243: if (this .selectable != selectable) {
244: this .selectable = selectable;
245: requestRepaint();
246: }
247: }
248:
249: /* Component API ****************************************************** */
250:
251: /**
252: * @see org.millstone.base.ui.AbstractComponent#getTag()
253: */
254: public String getTag() {
255: return "tree";
256: }
257:
258: /**
259: * @see org.millstone.base.terminal.VariableOwner#changeVariables(Object source, Map variables)
260: */
261: public void changeVariables(Object source, Map variables) {
262:
263: // Collapse nodes
264: if (variables.containsKey("collapse")) {
265: String[] keys = (String[]) variables.get("collapse");
266: for (int i = 0; i < keys.length; i++) {
267: Object id = itemIdMapper.get(keys[i]);
268: if (id != null)
269: collapseItem(id);
270: }
271: }
272:
273: // Expand nodes
274: if (variables.containsKey("expand")) {
275: String[] keys = (String[]) variables.get("expand");
276: for (int i = 0; i < keys.length; i++) {
277: Object id = itemIdMapper.get(keys[i]);
278: if (id != null)
279: expandItem(id);
280: }
281: }
282:
283: // Selections are handled by the select component
284: super .changeVariables(source, variables);
285:
286: // Actions
287: if (variables.containsKey("action")) {
288:
289: StringTokenizer st = new StringTokenizer((String) variables
290: .get("action"), ",");
291: if (st.countTokens() == 2) {
292: Object itemId = itemIdMapper.get(st.nextToken());
293: Action action = (Action) actionMapper.get(st
294: .nextToken());
295: if (action != null && containsId(itemId)
296: && actionHandlers != null)
297: for (Iterator i = actionHandlers.iterator(); i
298: .hasNext();)
299: ((Action.Handler) i.next()).handleAction(
300: action, this , itemId);
301: }
302: }
303: }
304:
305: /**
306: * @see org.millstone.base.ui.AbstractComponent#paintContent(PaintTarget)
307: */
308: public void paintContent(PaintTarget target) throws PaintException {
309:
310: // Focus control id
311: if (this .getFocusableId() > 0) {
312: target.addAttribute("focusid", this .getFocusableId());
313: }
314:
315: // The tab ordering number
316: if (this .getTabIndex() > 0)
317: target.addAttribute("tabindex", this .getTabIndex());
318:
319: // Paint tree attributes
320: if (isSelectable())
321: target.addAttribute("selectmode",
322: (isMultiSelect() ? "multi" : "single"));
323: else
324: target.addAttribute("selectmode", "none");
325: if (isNewItemsAllowed())
326: target.addAttribute("allownewitem", true);
327:
328: // Initialize variables
329: Set actionSet = new LinkedHashSet();
330: String[] selectedKeys;
331: if (isMultiSelect())
332: selectedKeys = new String[((Set) getValue()).size()];
333: else
334: selectedKeys = new String[(getValue() == null ? 0 : 1)];
335: int keyIndex = 0;
336: LinkedList expandedKeys = new LinkedList();
337:
338: // Iterate trough hierarchical tree using a stack of iterators
339: Stack iteratorStack = new Stack();
340: Collection ids = rootItemIds();
341: if (ids != null)
342: iteratorStack.push(ids.iterator());
343: while (!iteratorStack.isEmpty()) {
344:
345: // Get the iterator for current tree level
346: Iterator i = (Iterator) iteratorStack.peek();
347:
348: // If the level is finished, back to previous tree level
349: if (!i.hasNext()) {
350:
351: // Remove used iterator from the stack
352: iteratorStack.pop();
353:
354: // Close node
355: if (!iteratorStack.isEmpty())
356: target.endTag("node");
357: }
358:
359: // Add the item on current level
360: else {
361: Object itemId = i.next();
362:
363: // Start the item / node
364: boolean isNode = areChildrenAllowed(itemId);
365: if (isNode)
366: target.startTag("node");
367: else
368: target.startTag("leaf");
369:
370: // Add attributes
371: target.addAttribute("caption", getItemCaption(itemId));
372: Resource icon = getItemIcon(itemId);
373: if (icon != null)
374: target.addAttribute("icon", getItemIcon(itemId));
375: String key = itemIdMapper.key(itemId);
376: target.addAttribute("key", key);
377: if (isSelected(itemId)) {
378: target.addAttribute("selected", true);
379: selectedKeys[keyIndex++] = key;
380: }
381: if (areChildrenAllowed(itemId) && isExpanded(itemId)) {
382: target.addAttribute("expanded", true);
383: expandedKeys.add(key);
384: }
385:
386: // Actions
387: if (actionHandlers != null) {
388: target.startTag("al");
389: for (Iterator ahi = actionHandlers.iterator(); ahi
390: .hasNext();) {
391: Action[] aa = ((Action.Handler) ahi.next())
392: .getActions(itemId, this );
393: if (aa != null)
394: for (int ai = 0; ai < aa.length; ai++) {
395: String akey = actionMapper.key(aa[ai]);
396: actionSet.add(aa[ai]);
397: target.addSection("ak", akey);
398: }
399: }
400: target.endTag("al");
401: }
402:
403: // Add children if expanded, or close the tag
404: if (isExpanded(itemId) && hasChildren(itemId)
405: && areChildrenAllowed(itemId)) {
406: iteratorStack.push(getChildren(itemId).iterator());
407: } else {
408: if (isNode)
409: target.endTag("node");
410: else
411: target.endTag("leaf");
412: }
413: }
414: }
415:
416: // Actions
417: if (!actionSet.isEmpty()) {
418: target.startTag("actions");
419: target.addVariable(this , "action", "");
420: for (Iterator i = actionSet.iterator(); i.hasNext();) {
421: Action a = (Action) i.next();
422: target.startTag("action");
423: if (a.getCaption() != null)
424: target.addAttribute("caption", a.getCaption());
425: if (a.getIcon() != null)
426: target.addAttribute("icon", a.getIcon());
427: target.addAttribute("key", actionMapper.key(a));
428: target.endTag("action");
429: }
430: target.endTag("actions");
431: }
432:
433: // Selected
434: target.addVariable(this , "selected", selectedKeys);
435:
436: // Expand and collapse
437: target.addVariable(this , "expand", new String[] {});
438: target.addVariable(this , "collapse", new String[] {});
439:
440: // New items
441: target.addVariable(this , "newitem", new String[] {});
442: }
443:
444: /* Container.Hierarchical API ***************************************** */
445:
446: /**
447: * @see org.millstone.base.data.Container.Hierarchical#areChildrenAllowed(Object)
448: */
449: public boolean areChildrenAllowed(Object itemId) {
450: return ((Container.Hierarchical) items)
451: .areChildrenAllowed(itemId);
452: }
453:
454: /**
455: * @see org.millstone.base.data.Container.Hierarchical#getChildren(Object)
456: */
457: public Collection getChildren(Object itemId) {
458: return ((Container.Hierarchical) items).getChildren(itemId);
459: }
460:
461: /**
462: * @see org.millstone.base.data.Container.Hierarchical#getParent(Object)
463: */
464: public Object getParent(Object itemId) {
465: return ((Container.Hierarchical) items).getParent(itemId);
466: }
467:
468: /**
469: * @see org.millstone.base.data.Container.Hierarchical#hasChildren(Object)
470: */
471: public boolean hasChildren(Object itemId) {
472: return ((Container.Hierarchical) items).hasChildren(itemId);
473: }
474:
475: /**
476: * @see org.millstone.base.data.Container.Hierarchical#isRoot(Object)
477: */
478: public boolean isRoot(Object itemId) {
479: return ((Container.Hierarchical) items).isRoot(itemId);
480: }
481:
482: /**
483: * @see org.millstone.base.data.Container.Hierarchical#rootItemIds()
484: */
485: public Collection rootItemIds() {
486: return ((Container.Hierarchical) items).rootItemIds();
487: }
488:
489: /**
490: * @see org.millstone.base.data.Container.Hierarchical#setChildrenAllowed(Object, boolean)
491: */
492: public boolean setChildrenAllowed(Object itemId,
493: boolean areChildrenAllowed) {
494: boolean success = ((Container.Hierarchical) items)
495: .setChildrenAllowed(itemId, areChildrenAllowed);
496: if (success)
497: fireValueChange();
498: return success;
499: }
500:
501: /**
502: * @see org.millstone.base.data.Container.Hierarchical#setParent(Object, Object)
503: */
504: public boolean setParent(Object itemId, Object newParentId) {
505: boolean success = ((Container.Hierarchical) items).setParent(
506: itemId, newParentId);
507: if (success)
508: requestRepaint();
509: return success;
510: }
511:
512: /* Overriding select behavior******************************************** */
513:
514: /**
515: * @see org.millstone.base.data.Container.Viewer#setContainerDataSource(Container)
516: */
517: public void setContainerDataSource(Container newDataSource) {
518:
519: // Assure that the data source is ordered by making unordered
520: // containers ordered by wrapping them
521: if (Container.Hierarchical.class.isAssignableFrom(newDataSource
522: .getClass()))
523: super .setContainerDataSource(newDataSource);
524: else
525: super
526: .setContainerDataSource(new ContainerHierarchicalWrapper(
527: newDataSource));
528: }
529:
530: /* Expand event and listener ****************************************** */
531:
532: /** Event to fired when a node is expanded.
533: * ExapandEvent is fired when a node is to be expanded.
534: * it can me used to dynamically fill the sub-nodes of the
535: * node.
536: * @author IT Mill Ltd.
537: * @version 3.1.1
538: * @since 3.0
539: */
540: public class ExpandEvent extends Component.Event {
541:
542: /**
543: * Serial generated by eclipse.
544: */
545: private static final long serialVersionUID = 3832624001804481075L;
546: private Object expandedItemId;
547:
548: /** New instance of options change event
549: * @param source Source of the event.
550: */
551: public ExpandEvent(Component source, Object expandedItemId) {
552: super (source);
553: this .expandedItemId = expandedItemId;
554: }
555:
556: /** Node where the event occurred
557: * @return Source of the event.
558: */
559: public Object getItemId() {
560: return this .expandedItemId;
561: }
562: }
563:
564: /** Expand event listener
565: * @author IT Mill Ltd.
566: * @version 3.1.1
567: * @since 3.0
568: */
569: public interface ExpandListener {
570:
571: /** A node has been expanded.
572: * @param event Expand event.
573: */
574: public void nodeExpand(ExpandEvent event);
575: }
576:
577: /** Add expand listener
578: * @param listener Listener to be added.
579: */
580: public void addListener(ExpandListener listener) {
581: addListener(ExpandEvent.class, listener, EXPAND_METHOD);
582: }
583:
584: /** Remove expand listener
585: * @param listener Listener to be removed.
586: */
587: public void removeListener(ExpandListener listener) {
588: removeListener(ExpandEvent.class, listener, EXPAND_METHOD);
589: }
590:
591: /** Emit expand event. */
592: protected void fireExpandEvent(Object itemId) {
593: fireEvent(new ExpandEvent(this , itemId));
594: }
595:
596: /* Collapse event ****************************************** */
597:
598: /** Collapse event
599: * @author IT Mill Ltd.
600: * @version 3.1.1
601: * @since 3.0
602: */
603: public class CollapseEvent extends Component.Event {
604:
605: /**
606: * Serial generated by eclipse.
607: */
608: private static final long serialVersionUID = 3257009834783290160L;
609:
610: private Object collapsedItemId;
611:
612: /** New instance of options change event
613: * @param source Source of the event.
614: */
615: public CollapseEvent(Component source, Object collapsedItemId) {
616: super (source);
617: this .collapsedItemId = collapsedItemId;
618: }
619:
620: /** Node where the event occurred
621: * @return Source of the event.
622: */
623: public Object getItemId() {
624: return collapsedItemId;
625: }
626: }
627:
628: /** Collapse event listener
629: * @author IT Mill Ltd.
630: * @version 3.1.1
631: * @since 3.0
632: */
633: public interface CollapseListener {
634:
635: /** A node has been collapsed.
636: * @param event Collapse event.
637: */
638: public void nodeCollapse(CollapseEvent event);
639: }
640:
641: /** Add collapse listener
642: * @param listener Listener to be added.
643: */
644: public void addListener(CollapseListener listener) {
645: addListener(CollapseEvent.class, listener, COLLAPSE_METHOD);
646: }
647:
648: /** Remove collapse listener
649: * @param listener Listener to be removed.
650: */
651: public void removeListener(CollapseListener listener) {
652: removeListener(CollapseEvent.class, listener, COLLAPSE_METHOD);
653: }
654:
655: /** Emit collapse event. */
656: protected void fireCollapseEvent(Object itemId) {
657: fireEvent(new CollapseEvent(this , itemId));
658: }
659:
660: /* Action container *************************************************** */
661:
662: /** Adds an action handler.
663: * @see org.millstone.base.event.Action.Container#addActionHandler(Action.Handler)
664: */
665: public void addActionHandler(Action.Handler actionHandler) {
666:
667: if (actionHandler != null) {
668:
669: if (actionHandlers == null) {
670: actionHandlers = new LinkedList();
671: actionMapper = new KeyMapper();
672: }
673:
674: if (!actionHandlers.contains(actionHandler)) {
675: actionHandlers.add(actionHandler);
676: requestRepaint();
677: }
678: }
679: }
680:
681: /** Removes an action handler.
682: * @see org.millstone.base.event.Action.Container#removeActionHandler(Action.Handler)
683: */
684: public void removeActionHandler(Action.Handler actionHandler) {
685:
686: if (actionHandlers != null
687: && actionHandlers.contains(actionHandler)) {
688:
689: actionHandlers.remove(actionHandler);
690:
691: if (actionHandlers.isEmpty()) {
692: actionHandlers = null;
693: actionMapper = null;
694: }
695:
696: requestRepaint();
697: }
698: }
699:
700: /**
701: * @see org.millstone.base.ui.Select#getVisibleItemIds()
702: */
703: public Collection getVisibleItemIds() {
704:
705: LinkedList visible = new LinkedList();
706:
707: // Iterate trough hierarchical tree using a stack of iterators
708: Stack iteratorStack = new Stack();
709: Collection ids = rootItemIds();
710: if (ids != null)
711: iteratorStack.push(ids.iterator());
712: while (!iteratorStack.isEmpty()) {
713:
714: // Get the iterator for current tree level
715: Iterator i = (Iterator) iteratorStack.peek();
716:
717: // If the level is finished, back to previous tree level
718: if (!i.hasNext()) {
719:
720: // Remove used iterator from the stack
721: iteratorStack.pop();
722: }
723:
724: // Add the item on current level
725: else {
726: Object itemId = i.next();
727:
728: visible.add(itemId);
729:
730: // Add children if expanded, or close the tag
731: if (isExpanded(itemId) && hasChildren(itemId)) {
732: iteratorStack.push(getChildren(itemId).iterator());
733: }
734: }
735: }
736:
737: return visible;
738: }
739:
740: /** Adding new items is not supported.
741: * @see org.millstone.base.ui.Select#setNewItemsAllowed(boolean)
742: * @throws UnsupportedOperationException if set to true.
743: */
744: public void setNewItemsAllowed(boolean allowNewOptions)
745: throws UnsupportedOperationException {
746: if (allowNewOptions)
747: throw new UnsupportedOperationException();
748: }
749:
750: /** Focusing to this component is not supported.
751: * @see org.millstone.base.ui.AbstractField#focus()
752: * @throws UnsupportedOperationException if invoked.
753: */
754: public void focus() throws UnsupportedOperationException {
755: throw new UnsupportedOperationException();
756: }
757:
758: }
|