1 // Written in the D programming language.
2 
3 /**
4 
5 This module contains tree widgets implementation
6 
7 
8 TreeWidgetBase - abstract tree widget
9 
10 TreeWidget - Tree widget with items which can have icons and labels
11 
12 
13 Synopsis:
14 
15 ----
16 import dlangui.widgets.tree;
17 
18 // tree view example
19 TreeWidget tree = new TreeWidget("TREE1");
20 tree.layoutWidth(WRAP_CONTENT).layoutHeight(FILL_PARENT);
21 TreeItem tree1 = tree.items.newChild("group1", "Group 1"d, "document-open");
22 tree1.newChild("g1_1", "Group 1 item 1"d);
23 tree1.newChild("g1_2", "Group 1 item 2"d);
24 tree1.newChild("g1_3", "Group 1 item 3"d);
25 TreeItem tree2 = tree.items.newChild("group2", "Group 2"d, "document-save");
26 tree2.newChild("g2_1", "Group 2 item 1"d, "edit-copy");
27 tree2.newChild("g2_2", "Group 2 item 2"d, "edit-cut");
28 tree2.newChild("g2_3", "Group 2 item 3"d, "edit-paste");
29 tree2.newChild("g2_4", "Group 2 item 4"d);
30 TreeItem tree3 = tree.items.newChild("group3", "Group 3"d);
31 tree3.newChild("g3_1", "Group 3 item 1"d);
32 tree3.newChild("g3_2", "Group 3 item 2"d);
33 TreeItem tree32 = tree3.newChild("g3_3", "Group 3 item 3"d);
34 tree3.newChild("g3_4", "Group 3 item 4"d);
35 tree32.newChild("group3_2_1", "Group 3 item 2 subitem 1"d);
36 tree32.newChild("group3_2_2", "Group 3 item 2 subitem 2"d);
37 tree32.newChild("group3_2_3", "Group 3 item 2 subitem 3"d);
38 tree32.newChild("group3_2_4", "Group 3 item 2 subitem 4"d);
39 tree32.newChild("group3_2_5", "Group 3 item 2 subitem 5"d);
40 tree3.newChild("g3_5", "Group 3 item 5"d);
41 tree3.newChild("g3_6", "Group 3 item 6"d);
42 
43 LinearLayout treeLayout = new HorizontalLayout("TREE");
44 LinearLayout treeControlledPanel = new VerticalLayout();
45 treeLayout.layoutWidth = FILL_PARENT;
46 treeControlledPanel.layoutWidth = FILL_PARENT;
47 treeControlledPanel.layoutHeight = FILL_PARENT;
48 TextWidget treeItemLabel = new TextWidget("TREE_ITEM_DESC");
49 treeItemLabel.layoutWidth = FILL_PARENT;
50 treeItemLabel.layoutHeight = FILL_PARENT;
51 treeItemLabel.alignment = Align.Center;
52 treeItemLabel.text = "Sample text"d;
53 treeControlledPanel.addChild(treeItemLabel);
54 treeLayout.addChild(tree);
55 treeLayout.addChild(new ResizerWidget());
56 treeLayout.addChild(treeControlledPanel);
57 
58 tree.selectionListener = delegate(TreeItems source, TreeItem selectedItem, bool activated) {
59     dstring label = "Selected item: "d ~ toUTF32(selectedItem.id) ~ (activated ? " selected + activated"d : " selected"d);
60     treeItemLabel.text = label;
61 };
62 
63 tree.items.selectItem(tree.items.child(0));
64 ----
65 
66 Copyright: Vadim Lopatin, 2014
67 License:   Boost License 1.0
68 Authors:   Vadim Lopatin, coolreader.org@gmail.com
69 */
70 module dlangui.widgets.tree;
71 
72 import dlangui.widgets.widget;
73 import dlangui.widgets.controls;
74 import dlangui.widgets.scroll;
75 import dlangui.widgets.menu;
76 import dlangui.widgets.popup;
77 import dlangui.widgets.layouts;
78 import std.conv;
79 import std.algorithm;
80 
81 // tree widget item data container
82 class TreeItem {
83     protected TreeItem _parent;
84     protected string _id;
85     protected string _iconRes;
86     protected int _level;
87     protected UIString _text;
88     protected ObjectList!TreeItem _children;
89     protected bool _expanded;
90 
91     this(string id) {
92         _id = id;
93         _expanded = true;
94     }
95     this(string id, dstring label, string iconRes = null) {
96         _id = id;
97         _expanded = true;
98         _iconRes = iconRes;
99         _text.value = label;
100     }
101     this(string id, UIString label, string iconRes = null) {
102         _id = id;
103         _expanded = true;
104         _iconRes = iconRes;
105         _text = label;
106     }
107     this(string id, string labelRes, string iconRes = null) {
108         _id = id;
109         _expanded = true;
110         _iconRes = iconRes;
111         _text.id = labelRes;
112     }
113     /// create and add new child item
114     TreeItem newChild(string id, dstring label, string iconRes = null) {
115         TreeItem res = new TreeItem(id, label, iconRes);
116         addChild(res);
117         return res;
118     }
119 
120     /// returns true if item supports collapse
121     @property bool canCollapse() {
122         if (auto r = root) {
123             return r.canCollapse(this);
124         }
125         return true;
126     }
127 
128     /// returns topmost item
129     @property TreeItems root() {
130         TreeItem p = this;
131         while (p._parent)
132             p = p._parent;
133         return cast(TreeItems)p;
134     }
135 
136     /// returns true if this item is root item
137     @property bool isRoot() {
138         return false;
139     }
140     
141     void clear() {
142         _children.clear();
143     }
144 
145     @property TreeItem parent() { return _parent; }
146     @property protected TreeItem parent(TreeItem p) { _parent = p; return this; }
147     @property string id() { return _id; }
148     @property TreeItem id(string id) { _id = id; return this; }
149     @property string iconRes() { return _iconRes; }
150     @property TreeItem iconRes(string res) { _iconRes = res; return this; }
151     @property int level() { return _level; }
152     @property protected TreeItem level(int level) { 
153         _level = level;
154         for (int i = 0; i < childCount; i++)
155             child(i).level = _level + 1;
156         return this; 
157     }
158     @property bool expanded() { return _expanded; }
159     @property protected TreeItem expanded(bool expanded) { _expanded = expanded; return this; }
160     /** Returns true if this item and all parents are expanded. */
161     bool isFullyExpanded() {
162         if (!_expanded)
163             return false;
164         if (!_parent)
165             return true;
166         return _parent.isFullyExpanded();
167     }
168     /** Returns true if all parents are expanded. */
169     bool isVisible() {
170         if (_parent)
171             return _parent.isFullyExpanded();
172         return false;
173     }
174     void expand() {
175         _expanded = true;
176         if (_parent)
177             _parent.expand();
178     }
179     void collapse() {
180         _expanded = false;
181     }
182 
183     @property TreeItem selectedItem() {
184         return root.selectedItem();
185     }
186 
187     @property TreeItem defaultItem() {
188         return root.defaultItem();
189     }
190 
191     @property bool isSelected() {
192         return (selectedItem is this);
193     }
194 
195     @property bool isDefault() {
196         return (defaultItem is this);
197     }
198 
199     /// get widget text
200     @property dstring text() { return _text; }
201     /// set text to show
202     @property TreeItem text(dstring s) {
203         _text = s; 
204         return this;
205     }
206     /// set text to show
207     @property TreeItem text(UIString s) { 
208         _text = s;
209         return this;
210     }
211     /// set text resource ID to show
212     @property TreeItem textResource(string s) {
213         _text = s; 
214         return this;
215     }
216 
217     bool compareId(string id) {
218         return _id !is null && _id.equal(id);
219     }
220 
221     @property TreeItem topParent() {
222         if (!_parent)
223             return this;
224         return _parent.topParent;
225     }
226 
227 
228     protected int _intParam;
229     @property int intParam() {
230         return _intParam;
231     }
232     @property TreeItem intParam(int value) {
233         _intParam = value;
234         return this;
235     }
236 
237     protected Object _objectParam;
238     @property Object objectParam() {
239         return _objectParam;
240     }
241 
242     @property TreeItem objectParam(Object value) {
243         _objectParam = value;
244         return this;
245     }
246 
247 
248     /// returns true if item has at least one child
249     @property bool hasChildren() { return childCount > 0; }
250 
251     /// returns number of children of this widget
252     @property int childCount() { return _children.count; }
253     /// returns child by index
254     TreeItem child(int index) { return _children.get(index); }
255     /// adds child, returns added item
256     TreeItem addChild(TreeItem item) { 
257         return _children.add(item).parent(this).level(_level + 1);
258     }
259     /// removes child, returns removed item
260     TreeItem removeChild(int index) { 
261         TreeItem res = _children.remove(index);
262         if (res !is null)
263             res.parent = null;
264         return res;
265     }
266     /// removes child by ID, returns removed item
267     TreeItem removeChild(string ID) {
268         TreeItem res = null;
269         int index = _children.indexOf(ID);
270         if (index < 0)
271             return null;
272         res = _children.remove(index); 
273         if (res !is null)
274             res.parent = null;
275         return res;
276     }
277     /// returns index of widget in child list, -1 if passed widget is not a child of this widget
278     int childIndex(TreeItem item) { return _children.indexOf(item); }
279     /// notify listeners
280     protected void onUpdate(TreeItem item) {
281         root.onUpdate(item);
282     }
283     protected void toggleExpand(TreeItem item) {
284         root.toggleExpand(item);
285     }
286     protected void selectItem(TreeItem item) {
287         root.selectItem(item);
288     }
289     protected void activateItem(TreeItem item) {
290         root.activateItem(item);
291     }
292 
293     protected TreeItem nextVisible(TreeItem item, ref bool found) {
294         if (this is item)
295             found = true;
296         else if (found && isVisible)
297             return this;
298         for (int i = 0; i < childCount; i++) {
299             TreeItem res = child(i).nextVisible(item, found);
300             if (res)
301                 return res;
302         }
303         return null;
304     }
305 
306     protected TreeItem prevVisible(TreeItem item, ref TreeItem prevFoundVisible) {
307         if (this is item)
308             return prevFoundVisible;
309         else if (isVisible)
310             prevFoundVisible = this;
311         for (int i = 0; i < childCount; i++) {
312             TreeItem res = child(i).prevVisible(item, prevFoundVisible);
313             if (res)
314                 return res;
315         }
316         return null;
317     }
318 
319     /// returns item by id, null if not found
320     TreeItem findItemById(string id) {
321         if (_id.equal(id))
322             return this;
323         for (int i = 0; i < childCount; i++) {
324             TreeItem res = child(i).findItemById(id);
325             if (res)
326                 return res;
327         }
328         return null;
329     }
330 }
331 
332 interface OnTreeContentChangeListener {
333     void onTreeContentChange(TreeItems source);
334 }
335 
336 interface OnTreeStateChangeListener {
337     void onTreeStateChange(TreeItems source);
338 }
339 
340 interface OnTreeSelectionChangeListener {
341     void onTreeItemSelected(TreeItems source, TreeItem selectedItem, bool activated);
342 }
343 
344 class TreeItems : TreeItem {
345     // signal handler OnTreeContentChangeListener
346     Listener!OnTreeContentChangeListener contentListener;
347     Listener!OnTreeStateChangeListener stateListener;
348     Listener!OnTreeSelectionChangeListener selectionListener;
349 
350     protected bool _noCollapseForSingleTopLevelItem;
351     @property bool noCollapseForSingleTopLevelItem() { return _noCollapseForSingleTopLevelItem; }
352     @property TreeItems noCollapseForSingleTopLevelItem(bool flg) { _noCollapseForSingleTopLevelItem = flg; return this; }
353 
354     protected TreeItem _selectedItem;
355     protected TreeItem _defaultItem;
356 
357     this() {
358         super("tree");
359     }
360 
361     /// returns true if this item is root item
362     override @property bool isRoot() {
363         return true;
364     }
365     
366     /// notify listeners
367     override protected void onUpdate(TreeItem item) {
368         if (contentListener.assigned)
369             contentListener(this);
370     }
371 
372     bool canCollapse(TreeItem item) {
373         if (!_noCollapseForSingleTopLevelItem)
374             return true;
375         if (!hasChildren)
376             return false;
377         if (_children.count == 1 && _children[0] is item)
378             return false;
379         return true;
380     }
381 
382     bool canCollapseTopLevel() {
383         if (!_noCollapseForSingleTopLevelItem)
384             return true;
385         if (!hasChildren)
386             return false;
387         if (_children.count == 1)
388             return false;
389         return true;
390     }
391 
392     override void toggleExpand(TreeItem item) {
393         if (item.expanded) {
394             if (item.canCollapse())
395                 item.collapse();
396         } else
397             item.expand();
398         if (stateListener.assigned)
399             stateListener(this);
400     }
401 
402     override void selectItem(TreeItem item) {
403         if (_selectedItem is item)
404             return;
405         _selectedItem = item;
406         if (stateListener.assigned)
407             stateListener(this);
408         if (selectionListener.assigned)
409             selectionListener(this, _selectedItem, false);
410     }
411 
412     void setDefaultItem(TreeItem item) {
413         _defaultItem = item;
414         if (stateListener.assigned)
415             stateListener(this);
416     }
417 
418     override void activateItem(TreeItem item) {
419         if (!(_selectedItem is item)) {
420             _selectedItem = item;
421             if (stateListener.assigned)
422                 stateListener(this);
423         }
424         if (selectionListener.assigned)
425             selectionListener(this, _selectedItem, true);
426     }
427 
428     @property override TreeItem selectedItem() {
429         return _selectedItem;
430     }
431 
432     @property override TreeItem defaultItem() {
433         return _defaultItem;
434     }
435 
436     void selectNext() {
437         if (!hasChildren)
438             return;
439         if (!_selectedItem)
440             selectItem(child(0));
441         bool found = false;
442         TreeItem next = nextVisible(_selectedItem, found);
443         if (next)
444             selectItem(next);
445     }
446 
447     void selectPrevious() {
448         if (!hasChildren)
449             return;
450         TreeItem found = null;
451         TreeItem prev = prevVisible(_selectedItem, found);
452         if (prev)
453             selectItem(prev);
454     }
455 }
456 
457 /// grid control action codes
458 enum TreeActions : int {
459     /// no action
460     None = 0,
461     /// move selection up
462     Up = 2000,
463     /// move selection down
464     Down,
465     /// move selection left
466     Left,
467     /// move selection right
468     Right,
469 
470     /// scroll up, w/o changing selection
471     ScrollUp,
472     /// scroll down, w/o changing selection
473     ScrollDown,
474     /// scroll left, w/o changing selection
475     ScrollLeft,
476     /// scroll right, w/o changing selection
477     ScrollRight,
478 
479     /// scroll top w/o changing selection
480     ScrollTop,
481     /// scroll bottom, w/o changing selection
482     ScrollBottom,
483 
484     /// scroll up, w/o changing selection
485     ScrollPageUp,
486     /// scroll down, w/o changing selection
487     ScrollPageDown,
488     /// scroll left, w/o changing selection
489     ScrollPageLeft,
490     /// scroll right, w/o changing selection
491     ScrollPageRight,
492 
493     /// move cursor one page up
494     PageUp,
495     /// move cursor one page up with selection
496     SelectPageUp,
497     /// move cursor one page down
498     PageDown,
499     /// move cursor one page down with selection
500     SelectPageDown,
501     /// move cursor to the beginning of page
502     PageBegin, 
503     /// move cursor to the beginning of page with selection
504     SelectPageBegin, 
505     /// move cursor to the end of page
506     PageEnd,   
507     /// move cursor to the end of page with selection
508     SelectPageEnd,   
509     /// move cursor to the beginning of line
510     LineBegin,
511     /// move cursor to the beginning of line with selection
512     SelectLineBegin,
513     /// move cursor to the end of line
514     LineEnd,
515     /// move cursor to the end of line with selection
516     SelectLineEnd,
517     /// move cursor to the beginning of document
518     DocumentBegin,
519     /// move cursor to the beginning of document with selection
520     SelectDocumentBegin,
521     /// move cursor to the end of document
522     DocumentEnd,
523     /// move cursor to the end of document with selection
524     SelectDocumentEnd,
525 }
526 
527 
528 const int DOUBLE_CLICK_TIME_MS = 250;
529 
530 interface OnTreePopupMenuListener {
531     MenuItem onTreeItemPopupMenu(TreeItems source, TreeItem selectedItem);
532 }
533 
534 /// Item widget for displaying in trees
535 class TreeItemWidget : HorizontalLayout {
536     TreeItem _item;
537     TextWidget _tab;
538     ImageWidget _expander;
539     ImageWidget _icon;
540     TextWidget _label;
541     HorizontalLayout _body;
542     long lastClickTime;
543 
544     Listener!OnTreePopupMenuListener popupMenuListener;
545 
546     @property TreeItem item() { return _item; }
547 
548 
549     this(TreeItem item) {
550         super(item.id);
551         styleId = STYLE_TREE_ITEM;
552 
553         clickable = true;
554         focusable = true;
555         trackHover = true;
556 
557         _item = item;
558         _tab = new TextWidget("tab");
559         //dchar[] tabText;
560         //dchar[] singleTab = [' ', ' ', ' ', ' '];
561         //for (int i = 1; i < _item.level; i++)
562         //    tabText ~= singleTab;
563         //_tab.text = cast(dstring)tabText;
564         int level = _item.level - 1;
565         if (!_item.root.canCollapseTopLevel())
566             level--;
567         if (level < 0)
568             level = 0;
569         int w = level * style.font.size * 2;
570         _tab.minWidth = w;
571         _tab.maxWidth = w;
572         if (_item.canCollapse()) {
573             _expander = new ImageWidget("expander", _item.hasChildren && _item.expanded ? "arrow_right_down_black" : "arrow_right_hollow");
574             _expander.styleId = STYLE_TREE_ITEM_EXPAND_ICON;
575             _expander.clickable = true;
576             _expander.trackHover = true;
577             _expander.visibility = _item.hasChildren ? Visibility.Visible : Visibility.Invisible;
578             //_expander.setState(State.Parent);
579 
580             _expander.click = delegate(Widget source) {
581                 _item.selectItem(_item);
582                 _item.toggleExpand(_item);
583                 return true;
584             };
585         }
586         click = delegate(Widget source) {
587             long ts = currentTimeMillis();
588             _item.selectItem(_item);
589             if (ts - lastClickTime < DOUBLE_CLICK_TIME_MS) {
590                 if (_item.hasChildren) {
591                     _item.toggleExpand(_item);
592                 } else {
593                     _item.activateItem(_item);
594                 }
595             }
596             lastClickTime = ts;
597             return true;
598         };
599         _body = new HorizontalLayout("item_body");
600         _body.styleId = STYLE_TREE_ITEM_BODY;
601         _body.setState(State.Parent);
602         if (_item.iconRes.length > 0) {
603             _icon = new ImageWidget("icon", _item.iconRes);
604             _icon.styleId = STYLE_TREE_ITEM_ICON;
605             _icon.setState(State.Parent);
606             _body.addChild(_icon);
607         }
608         _label = new TextWidget("label", _item.text);
609         _label.styleId = STYLE_TREE_ITEM_LABEL;
610         _label.setState(State.Parent);
611         _label.padding(Rect(BACKEND_GUI ? 5 : 0, 0, 0, 0));
612         _body.addChild(_label);
613         // append children
614         addChild(_tab);
615         if (_expander)
616             addChild(_expander);
617         addChild(_body);
618     }
619 
620     override bool onKeyEvent(KeyEvent event) {
621         if (keyEvent.assigned && keyEvent(this, event))
622             return true; // processed by external handler
623         if (!focused || !visible)
624             return false;
625         if (event.action != KeyAction.KeyDown)
626             return false;
627         int action = 0;
628         switch (event.keyCode) with(KeyCode) {
629             case SPACE:
630             case RETURN:
631                 if (_item.hasChildren)
632                     _item.toggleExpand(_item);
633                 else
634                     _item.activateItem(_item);
635                 return true;
636             default:
637                 break;
638         }
639         return false;
640     }
641 
642     /// process mouse event; return true if event is processed by widget.
643     override bool onMouseEvent(MouseEvent event) {
644         if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Right) {
645             if (popupMenuListener.assigned) {
646                 MenuItem menu = popupMenuListener(_item.root, _item);
647                 if (menu) {
648                     PopupMenu popupMenu = new PopupMenu(menu);
649                     PopupWidget popup = window.showPopup(popupMenu, this, PopupAlign.Point | PopupAlign.Right, event.x, event.y);
650                     popup.flags = PopupFlags.CloseOnClickOutside;
651                     return true;
652                 }
653             }
654         }
655         return super.onMouseEvent(event);
656     }
657 
658     void updateWidget() {
659         if (_expander) {
660             _expander.drawable = _item.expanded ? "arrow_right_down_black" : "arrow_right_hollow";
661         }
662         if (_item.isVisible)
663             visibility = Visibility.Visible;
664         else
665             visibility = Visibility.Gone;
666         if (_item.isSelected)
667             setState(State.Selected);
668         else
669             resetState(State.Selected);
670         if (_item.isDefault)
671             setState(State.Default);
672         else
673             resetState(State.Default);
674     }
675 }
676 
677 
678 
679 /// Abstract tree widget
680 class TreeWidgetBase :  ScrollWidget, OnTreeContentChangeListener, OnTreeStateChangeListener, OnTreeSelectionChangeListener, OnKeyHandler {
681 
682     protected TreeItems _tree;
683 
684     @property ref TreeItems items() { return _tree; }
685 
686     Signal!OnTreeSelectionChangeListener selectionChange;
687     /// allows to provide individual popup menu for items
688     Listener!OnTreePopupMenuListener popupMenu;
689 
690     protected bool _needUpdateWidgets;
691     protected bool _needUpdateWidgetStates;
692 
693     protected bool _noCollapseForSingleTopLevelItem;
694     @property bool noCollapseForSingleTopLevelItem() {
695         return _noCollapseForSingleTopLevelItem;
696     }
697     @property TreeWidgetBase noCollapseForSingleTopLevelItem(bool flg) { 
698         _noCollapseForSingleTopLevelItem = flg;
699         if (_tree)
700             _tree.noCollapseForSingleTopLevelItem = flg;
701         return this;
702     }
703 
704     protected MenuItem onTreeItemPopupMenu(TreeItems source, TreeItem selectedItem) {
705         if (popupMenu)
706             return popupMenu(source, selectedItem);
707         return null;
708     }
709 
710     /// empty parameter list constructor - for usage by factory
711     this() {
712         this(null);
713     }
714     /// create with ID parameter
715     this(string ID, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) {
716         super(ID, hscrollbarMode, vscrollbarMode);
717         contentWidget = new VerticalLayout("TREE_CONTENT");
718         _tree = new TreeItems();
719         _tree.contentListener = this;
720         _tree.stateListener = this;
721         _tree.selectionListener = this;
722         _needUpdateWidgets = true;
723         _needUpdateWidgetStates = true;
724         acceleratorMap.add( [
725             new Action(TreeActions.Up, KeyCode.UP, 0),
726             new Action(TreeActions.Down, KeyCode.DOWN, 0),
727             new Action(TreeActions.ScrollLeft, KeyCode.LEFT, 0),
728             new Action(TreeActions.ScrollRight, KeyCode.RIGHT, 0),
729             //new Action(TreeActions.LineBegin, KeyCode.HOME, 0),
730             //new Action(TreeActions.LineEnd, KeyCode.END, 0),
731             new Action(TreeActions.PageUp, KeyCode.PAGEUP, 0),
732             new Action(TreeActions.PageDown, KeyCode.PAGEDOWN, 0),
733             //new Action(TreeActions.PageBegin, KeyCode.PAGEUP, KeyFlag.Control),
734             //new Action(TreeActions.PageEnd, KeyCode.PAGEDOWN, KeyFlag.Control),
735             new Action(TreeActions.ScrollTop, KeyCode.HOME, KeyFlag.Control),
736             new Action(TreeActions.ScrollBottom, KeyCode.END, KeyFlag.Control),
737             new Action(TreeActions.ScrollPageUp, KeyCode.PAGEUP, KeyFlag.Control),
738             new Action(TreeActions.ScrollPageDown, KeyCode.PAGEDOWN, KeyFlag.Control),
739             new Action(TreeActions.ScrollUp, KeyCode.UP, KeyFlag.Control),
740             new Action(TreeActions.ScrollDown, KeyCode.DOWN, KeyFlag.Control),
741             new Action(TreeActions.ScrollLeft, KeyCode.LEFT, KeyFlag.Control),
742             new Action(TreeActions.ScrollRight, KeyCode.RIGHT, KeyFlag.Control),
743         ]);
744     }
745 
746     ~this() {
747         if (_tree) {
748             destroy(_tree);
749             _tree = null;
750         }
751     }
752 
753     /** Override to use custom tree item widgets. */
754     protected Widget createItemWidget(TreeItem item) {
755         TreeItemWidget res = new TreeItemWidget(item);
756         res.keyEvent = this;
757         res.popupMenuListener = &onTreeItemPopupMenu;
758         return res;
759     }
760 
761     /// returns item by id, null if not found
762     TreeItem findItemById(string id) {
763         return _tree.findItemById(id);
764     }
765 
766     override bool onKey(Widget source, KeyEvent event) {
767         if (event.action == KeyAction.KeyDown) {
768             Action action = findKeyAction(event.keyCode, event.flags); // & (KeyFlag.Shift | KeyFlag.Alt | KeyFlag.Control)
769             if (action !is null) {
770                 return handleAction(action);
771             }
772         }
773         return false;
774     }
775 
776     protected void addWidgets(TreeItem item) {
777         if (item.level > 0)
778             _contentWidget.addChild(createItemWidget(item));
779         for (int i = 0; i < item.childCount; i++)
780             addWidgets(item.child(i));
781     }
782 
783     protected void updateWidgets() {
784         _contentWidget.removeAllChildren();
785         addWidgets(_tree);
786         _needUpdateWidgets = false;
787     }
788 
789     void clearAllItems() {
790         items.clear();
791         updateWidgets();
792         requestLayout();
793     }
794 
795     protected void updateWidgetStates() {
796         for (int i = 0; i < _contentWidget.childCount; i++) {
797             TreeItemWidget child = cast(TreeItemWidget)_contentWidget.child(i);
798             if (child)
799                 child.updateWidget();
800         }
801         _needUpdateWidgetStates = false;
802     }
803 
804     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
805     override void layout(Rect rc) {
806         if (visibility == Visibility.Gone) {
807             return;
808         }
809         if (_needUpdateWidgets)
810             updateWidgets();
811         if (_needUpdateWidgetStates)
812             updateWidgetStates();
813         super.layout(rc);
814     }
815 
816     /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
817     override void measure(int parentWidth, int parentHeight) { 
818         if (visibility == Visibility.Gone) {
819             return;
820         }
821         if (_needUpdateWidgets)
822             updateWidgets();
823         if (_needUpdateWidgetStates)
824             updateWidgetStates();
825         super.measure(parentWidth, parentHeight);
826     }
827 
828     /// listener
829     override void onTreeContentChange(TreeItems source) {
830         _needUpdateWidgets = true;
831         requestLayout();
832     }
833 
834     override void onTreeStateChange(TreeItems source) {
835         _needUpdateWidgetStates = true;
836         requestLayout();
837     }
838 
839     TreeItemWidget findItemWidget(TreeItem item) {
840         for (int i = 0; i < _contentWidget.childCount; i++) {
841             TreeItemWidget child = cast(TreeItemWidget) _contentWidget.child(i);
842             if (child && child.item is item)
843                 return child;
844         }
845         return null;
846     }
847 
848     override void onTreeItemSelected(TreeItems source, TreeItem selectedItem, bool activated) {
849         TreeItemWidget selected = findItemWidget(selectedItem);
850         if (selected && selected.visibility == Visibility.Visible) {
851             selected.setFocus();
852             makeWidgetVisible(selected, false, true);
853         }
854         if (selectionChange.assigned)
855             selectionChange(source, selectedItem, activated);
856     }
857 
858     void makeItemVisible(TreeItem item) {
859         TreeItemWidget widget = findItemWidget(item);
860         if (widget && widget.visibility == Visibility.Visible) {
861             makeWidgetVisible(widget, false, true);
862         }
863     }
864 
865     void clearSelection() {
866         _tree.selectItem(null);
867     }
868 
869     void selectItem(TreeItem item, bool makeVisible = true) {
870         if (!item) {
871             clearSelection();
872             return;
873         }
874         _tree.selectItem(item);
875         if (makeVisible)
876             makeItemVisible(item);
877     }
878 
879     void selectItem(string itemId, bool makeVisible = true) {
880         TreeItem item = findItemById(itemId);
881         selectItem(item, makeVisible);
882     }
883 
884     override protected bool handleAction(const Action a) {
885         Log.d("tree.handleAction ", a.id);
886         switch (a.id) with(TreeActions)
887         {
888             case ScrollLeft:
889                 if (_hscrollbar)
890                     _hscrollbar.sendScrollEvent(ScrollAction.LineUp);
891                 break;
892             case ScrollRight:
893                 if (_hscrollbar)
894                     _hscrollbar.sendScrollEvent(ScrollAction.LineDown);
895                 break;
896             case ScrollUp:
897                 if (_vscrollbar)
898                     _vscrollbar.sendScrollEvent(ScrollAction.LineUp);
899                 break;
900             case ScrollPageUp:
901                 if (_vscrollbar)
902                     _vscrollbar.sendScrollEvent(ScrollAction.PageUp);
903                 break;
904             case ScrollDown:
905                 if (_vscrollbar)
906                     _vscrollbar.sendScrollEvent(ScrollAction.LineDown);
907                 break;
908             case ScrollPageDown:
909                 if (_vscrollbar)
910                     _vscrollbar.sendScrollEvent(ScrollAction.PageDown);
911                 break;
912             case Up:
913                 _tree.selectPrevious();
914                 break;
915             case Down:
916                 _tree.selectNext();
917                 break;
918             case PageUp:
919                 // TODO: implement page up
920                 _tree.selectPrevious();
921                 break;
922             case PageDown:
923                 // TODO: implement page down
924                 _tree.selectPrevious();
925                 break;
926             default:
927                 return super.handleAction(a);
928         }
929         return true;
930     }
931 }
932 
933 /// Tree widget with items which can have icons and labels
934 class TreeWidget :  TreeWidgetBase {
935     /// empty parameter list constructor - for usage by factory
936     this() {
937         this(null);
938     }
939     /// create with ID parameter
940     this(string ID) {
941         super(ID);
942     }
943 }