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         foreach(c; _children) {
143             c.parent = null;
144             if(c is root.selectedItem)
145                 root.selectItem(null);
146         }
147         _children.clear();
148         root.onUpdate(this);
149     }
150 
151     @property TreeItem parent() { return _parent; }
152     @property protected TreeItem parent(TreeItem p) { _parent = p; return this; }
153     @property string id() { return _id; }
154     @property TreeItem id(string id) { _id = id; return this; }
155     @property string iconRes() { return _iconRes; }
156     @property TreeItem iconRes(string res) { _iconRes = res; return this; }
157     @property int level() { return _level; }
158     @property protected TreeItem level(int level) {
159         _level = level;
160         for (int i = 0; i < childCount; i++)
161             child(i).level = _level + 1;
162         return this;
163     }
164     @property bool expanded() { return _expanded; }
165     @property protected TreeItem expanded(bool expanded) { _expanded = expanded; return this; }
166     /** Returns true if this item and all parents are expanded. */
167     bool isFullyExpanded() {
168         if (!_expanded)
169             return false;
170         if (!_parent)
171             return true;
172         return _parent.isFullyExpanded();
173     }
174     /** Returns true if all parents are expanded. */
175     bool isVisible() {
176         if (_parent)
177             return _parent.isFullyExpanded();
178         return false;
179     }
180     void expand() {
181         _expanded = true;
182         if (_parent)
183             _parent.expand();
184     }
185     void collapse() {
186         _expanded = false;
187     }
188     /// expand this node and all children
189     void expandAll() {
190         foreach(c; _children) {
191             if (!c._expanded && c.canCollapse) //?
192                 c.expandAll();
193         }
194         if (!expanded)
195             toggleExpand(this);
196     }
197     /// expand this node and all children
198     void collapseAll() {
199         foreach(c; _children) {
200             if (c._expanded && c.canCollapse)
201                 c.collapseAll();
202         }
203         if (expanded)
204             toggleExpand(this);
205     }
206 
207     @property TreeItem selectedItem() {
208         return root.selectedItem();
209     }
210 
211     @property TreeItem defaultItem() {
212         return root.defaultItem();
213     }
214 
215     @property bool isSelected() {
216         return (selectedItem is this);
217     }
218 
219     @property bool isDefault() {
220         return (defaultItem is this);
221     }
222 
223     /// get widget text
224     @property dstring text() { return _text; }
225     /// set text to show
226     @property TreeItem text(dstring s) {
227         _text = s;
228         return this;
229     }
230     /// set text to show
231     @property TreeItem text(UIString s) {
232         _text = s;
233         return this;
234     }
235     /// set text resource ID to show
236     @property TreeItem textResource(string s) {
237         _text = s;
238         return this;
239     }
240 
241     bool compareId(string id) {
242         return _id !is null && _id.equal(id);
243     }
244 
245     @property TreeItem topParent() {
246         if (!_parent)
247             return this;
248         return _parent.topParent;
249     }
250 
251 
252     protected int _intParam;
253     @property int intParam() {
254         return _intParam;
255     }
256     @property TreeItem intParam(int value) {
257         _intParam = value;
258         return this;
259     }
260 
261     protected Object _objectParam;
262     @property Object objectParam() {
263         return _objectParam;
264     }
265 
266     @property TreeItem objectParam(Object value) {
267         _objectParam = value;
268         return this;
269     }
270 
271 
272     /// returns true if item has at least one child
273     @property bool hasChildren() { return childCount > 0; }
274 
275     /// returns number of children of this widget
276     @property int childCount() { return _children.count; }
277     /// returns child by index
278     TreeItem child(int index) { return _children.get(index); }
279     /// adds child, returns added item
280     TreeItem addChild(TreeItem item, int index = -1) {
281         TreeItem res = _children.insert(item, index).parent(this).level(_level + 1);
282         root.onUpdate(res);
283         return res;
284     }
285     /// removes child, returns removed item
286     TreeItem removeChild(int index) {
287         if (index < 0 || index >= _children.count)
288             return null;
289         TreeItem res = _children.remove(index);
290         TreeItem newSelection = null;
291         if (res !is null) {
292             res.parent = null;
293             if (root && root.selectedItem is res) {
294                 if (index < _children.count)
295                     newSelection = _children[index];
296                 else if (index > 0)
297                     newSelection = _children[index - 1];
298                 else
299                     newSelection = this;
300             }
301         }
302         root.selectItem(newSelection);
303         root.onUpdate(this);
304         return res;
305     }
306     /// removes child by reference, returns removed item
307     TreeItem removeChild(TreeItem child) {
308         TreeItem res = null;
309         int index = _children.indexOf(child);
310         return removeChild(index);
311     }
312     /// removes child by ID, returns removed item
313     TreeItem removeChild(string ID) {
314         TreeItem res = null;
315         int index = _children.indexOf(ID);
316         return removeChild(index);
317     }
318     /// returns index of widget in child list, -1 if passed widget is not a child of this widget
319     int childIndex(TreeItem item) { return _children.indexOf(item); }
320     /// notify listeners
321     protected void onUpdate(TreeItem item) {
322         if (root)
323             root.onUpdate(item);
324     }
325     protected void toggleExpand(TreeItem item) {
326         root.toggleExpand(item);
327     }
328     protected void selectItem(TreeItem item) {
329         root.selectItem(item);
330     }
331     protected void activateItem(TreeItem item) {
332         root.activateItem(item);
333     }
334 
335     protected TreeItem nextVisible(TreeItem item, ref bool found) {
336         if (this is item)
337             found = true;
338         else if (found && isVisible)
339             return this;
340         for (int i = 0; i < childCount; i++) {
341             TreeItem res = child(i).nextVisible(item, found);
342             if (res)
343                 return res;
344         }
345         return null;
346     }
347 
348     protected TreeItem prevVisible(TreeItem item, ref TreeItem prevFoundVisible) {
349         if (this is item)
350             return prevFoundVisible;
351         else if (isVisible)
352             prevFoundVisible = this;
353         for (int i = 0; i < childCount; i++) {
354             TreeItem res = child(i).prevVisible(item, prevFoundVisible);
355             if (res)
356                 return res;
357         }
358         return null;
359     }
360 
361     /// returns item by id, null if not found
362     TreeItem findItemById(string id) {
363         if (_id.equal(id))
364             return this;
365         for (int i = 0; i < childCount; i++) {
366             TreeItem res = child(i).findItemById(id);
367             if (res)
368                 return res;
369         }
370         return null;
371     }
372 }
373 
374 interface OnTreeContentChangeListener {
375     void onTreeContentChange(TreeItems source);
376 }
377 
378 interface OnTreeStateChangeListener {
379     void onTreeStateChange(TreeItems source);
380 }
381 
382 interface OnTreeExpandedStateListener {
383     void onTreeExpandedStateChange(TreeItems source, TreeItem item);
384 }
385 
386 interface OnTreeSelectionChangeListener {
387     void onTreeItemSelected(TreeItems source, TreeItem selectedItem, bool activated);
388 }
389 
390 class TreeItems : TreeItem {
391     // signal handler OnTreeContentChangeListener
392     Listener!OnTreeContentChangeListener contentListener;
393     Listener!OnTreeStateChangeListener stateListener;
394     Listener!OnTreeSelectionChangeListener selectionListener;
395     Listener!OnTreeExpandedStateListener expandListener;
396 
397     protected bool _noCollapseForSingleTopLevelItem;
398     @property bool noCollapseForSingleTopLevelItem() { return _noCollapseForSingleTopLevelItem; }
399     @property TreeItems noCollapseForSingleTopLevelItem(bool flg) { _noCollapseForSingleTopLevelItem = flg; return this; }
400 
401     protected TreeItem _selectedItem;
402     protected TreeItem _defaultItem;
403 
404     this() {
405         super("tree");
406     }
407 
408     /// returns true if this item is root item
409     override @property bool isRoot() {
410         return true;
411     }
412 
413     /// notify listeners
414     override protected void onUpdate(TreeItem item) {
415         if (contentListener.assigned)
416             contentListener(this);
417     }
418 
419     bool canCollapse(TreeItem item) {
420         if (!_noCollapseForSingleTopLevelItem)
421             return true;
422         if (!hasChildren)
423             return false;
424         if (_children.count == 1 && _children[0] is item)
425             return false;
426         return true;
427     }
428 
429     bool canCollapseTopLevel() {
430         if (!_noCollapseForSingleTopLevelItem)
431             return true;
432         if (!hasChildren)
433             return false;
434         if (_children.count == 1)
435             return false;
436         return true;
437     }
438 
439     override void toggleExpand(TreeItem item) {
440         bool expandChanged = false;
441         if (item.expanded) {
442             if (item.canCollapse()) {
443                 item.collapse();
444                 expandChanged = true;
445             }
446         } else {
447             item.expand();
448             expandChanged = true;
449         }
450         if (stateListener.assigned)
451             stateListener(this);
452         if (expandChanged && expandListener.assigned)
453             expandListener(this, item);
454     }
455 
456     override void selectItem(TreeItem item) {
457         if (_selectedItem is item)
458             return;
459         _selectedItem = item;
460         if (stateListener.assigned)
461             stateListener(this);
462         if (selectionListener.assigned)
463             selectionListener(this, _selectedItem, false);
464     }
465 
466     void setDefaultItem(TreeItem item) {
467         _defaultItem = item;
468         if (stateListener.assigned)
469             stateListener(this);
470     }
471 
472     override void activateItem(TreeItem item) {
473         if (!(_selectedItem is item)) {
474             _selectedItem = item;
475             if (stateListener.assigned)
476                 stateListener(this);
477         }
478         if (selectionListener.assigned)
479             selectionListener(this, _selectedItem, true);
480     }
481 
482     @property override TreeItem selectedItem() {
483         return _selectedItem;
484     }
485 
486     @property override TreeItem defaultItem() {
487         return _defaultItem;
488     }
489 
490     void selectNext() {
491         if (!hasChildren)
492             return;
493         if (!_selectedItem)
494             selectItem(child(0));
495         bool found = false;
496         TreeItem next = nextVisible(_selectedItem, found);
497         if (next)
498             selectItem(next);
499     }
500 
501     void selectPrevious() {
502         if (!hasChildren)
503             return;
504         TreeItem found = null;
505         TreeItem prev = prevVisible(_selectedItem, found);
506         if (prev)
507             selectItem(prev);
508     }
509 }
510 
511 /// grid control action codes
512 enum TreeActions : int {
513     /// no action
514     None = 0,
515     /// move selection up
516     Up = 2000,
517     /// move selection down
518     Down,
519     /// move selection left
520     Left,
521     /// move selection right
522     Right,
523 
524     /// scroll up, w/o changing selection
525     ScrollUp,
526     /// scroll down, w/o changing selection
527     ScrollDown,
528     /// scroll left, w/o changing selection
529     ScrollLeft,
530     /// scroll right, w/o changing selection
531     ScrollRight,
532 
533     /// scroll top w/o changing selection
534     ScrollTop,
535     /// scroll bottom, w/o changing selection
536     ScrollBottom,
537 
538     /// scroll up, w/o changing selection
539     ScrollPageUp,
540     /// scroll down, w/o changing selection
541     ScrollPageDown,
542     /// scroll left, w/o changing selection
543     ScrollPageLeft,
544     /// scroll right, w/o changing selection
545     ScrollPageRight,
546 
547     /// move cursor one page up
548     PageUp,
549     /// move cursor one page up with selection
550     SelectPageUp,
551     /// move cursor one page down
552     PageDown,
553     /// move cursor one page down with selection
554     SelectPageDown,
555     /// move cursor to the beginning of page
556     PageBegin,
557     /// move cursor to the beginning of page with selection
558     SelectPageBegin,
559     /// move cursor to the end of page
560     PageEnd,
561     /// move cursor to the end of page with selection
562     SelectPageEnd,
563     /// move cursor to the beginning of line
564     LineBegin,
565     /// move cursor to the beginning of line with selection
566     SelectLineBegin,
567     /// move cursor to the end of line
568     LineEnd,
569     /// move cursor to the end of line with selection
570     SelectLineEnd,
571     /// move cursor to the beginning of document
572     DocumentBegin,
573     /// move cursor to the beginning of document with selection
574     SelectDocumentBegin,
575     /// move cursor to the end of document
576     DocumentEnd,
577     /// move cursor to the end of document with selection
578     SelectDocumentEnd,
579 }
580 
581 
582 const int DOUBLE_CLICK_TIME_MS = 250;
583 
584 interface OnTreePopupMenuListener {
585     MenuItem onTreeItemPopupMenu(TreeItems source, TreeItem selectedItem);
586 }
587 
588 /// Item widget for displaying in trees
589 class TreeItemWidget : HorizontalLayout {
590     TreeItem _item;
591     TextWidget _tab;
592     ImageWidget _expander;
593     ImageWidget _icon;
594     TextWidget _label;
595     HorizontalLayout _body;
596     long lastClickTime;
597 
598     Listener!OnTreePopupMenuListener popupMenuListener;
599 
600     @property TreeItem item() { return _item; }
601 
602 
603     this(TreeItem item) {
604         super(item.id);
605         styleId = STYLE_TREE_ITEM;
606 
607         clickable = true;
608         focusable = true;
609         trackHover = true;
610 
611         _item = item;
612         _tab = new TextWidget("tab");
613         //dchar[] tabText;
614         //dchar[] singleTab = [' ', ' ', ' ', ' '];
615         //for (int i = 1; i < _item.level; i++)
616         //    tabText ~= singleTab;
617         //_tab.text = cast(dstring)tabText;
618         int level = _item.level - 1;
619         if (!_item.root.canCollapseTopLevel())
620             level--;
621         if (level < 0)
622             level = 0;
623         int w = level * style.font.size * 3 / 4;
624         _tab.minWidth = w;
625         _tab.maxWidth = w;
626         if (_item.canCollapse()) {
627             _expander = new ImageWidget("expander", _item.hasChildren && _item.expanded ? "arrow_right_down_black" : "arrow_right_hollow");
628             _expander.styleId = STYLE_TREE_ITEM_EXPAND_ICON;
629             _expander.clickable = true;
630             _expander.trackHover = true;
631             _expander.visibility = _item.hasChildren ? Visibility.Visible : Visibility.Invisible;
632             //_expander.setState(State.Parent);
633 
634             _expander.click = delegate(Widget source) {
635                 _item.selectItem(_item);
636                 _item.toggleExpand(_item);
637                 return true;
638             };
639         }
640         click = delegate(Widget source) {
641             long ts = currentTimeMillis();
642             _item.selectItem(_item);
643             if (ts - lastClickTime < DOUBLE_CLICK_TIME_MS) {
644                 if (_item.hasChildren) {
645                     _item.toggleExpand(_item);
646                 } else {
647                     _item.activateItem(_item);
648                 }
649             }
650             lastClickTime = ts;
651             return true;
652         };
653         _body = new HorizontalLayout("item_body");
654         _body.styleId = STYLE_TREE_ITEM_BODY;
655         _body.setState(State.Parent);
656         if (_item.iconRes.length > 0) {
657             _icon = new ImageWidget("icon", _item.iconRes);
658             _icon.styleId = STYLE_TREE_ITEM_ICON;
659             _icon.setState(State.Parent);
660             _icon.padding(Rect(0, 0, BACKEND_GUI ? 5 : 0, 0));
661             _body.addChild(_icon);
662         }
663         _label = new TextWidget("label", _item.text);
664         _label.styleId = STYLE_TREE_ITEM_LABEL;
665         _label.setState(State.Parent);
666         _body.addChild(_label);
667         // append children
668         addChild(_tab);
669         if (_expander)
670             addChild(_expander);
671         addChild(_body);
672     }
673 
674     override bool onKeyEvent(KeyEvent event) {
675         if (keyEvent.assigned && keyEvent(this, event))
676             return true; // processed by external handler
677         if (!focused || !visible)
678             return false;
679         if (event.action != KeyAction.KeyDown)
680             return false;
681         int action = 0;
682         switch (event.keyCode) with(KeyCode) {
683             case SPACE:
684             case RETURN:
685                 if (_item.hasChildren)
686                     _item.toggleExpand(_item);
687                 else
688                     _item.activateItem(_item);
689                 return true;
690             default:
691                 break;
692         }
693         return false;
694     }
695 
696     /// process mouse event; return true if event is processed by widget.
697     override bool onMouseEvent(MouseEvent event) {
698         if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Right) {
699             if (popupMenuListener.assigned) {
700                 MenuItem menu = popupMenuListener(_item.root, _item);
701                 if (menu) {
702                     PopupMenu popupMenu = new PopupMenu(menu);
703                     PopupWidget popup = window.showPopup(popupMenu, this, PopupAlign.Point | PopupAlign.Right, event.x, event.y);
704                     popup.flags = PopupFlags.CloseOnClickOutside;
705                     return true;
706                 }
707             }
708         }
709         return super.onMouseEvent(event);
710     }
711 
712     void updateWidget() {
713         if (_expander) {
714             _expander.drawable = _item.expanded ? "arrow_right_down_black" : "arrow_right_hollow";
715         }
716         if (_item.isVisible)
717             visibility = Visibility.Visible;
718         else
719             visibility = Visibility.Gone;
720         if (_item.isSelected)
721             setState(State.Selected);
722         else
723             resetState(State.Selected);
724         if (_item.isDefault)
725             setState(State.Default);
726         else
727             resetState(State.Default);
728     }
729 }
730 
731 
732 
733 /// Abstract tree widget
734 class TreeWidgetBase :  ScrollWidget, OnTreeContentChangeListener, OnTreeStateChangeListener, OnTreeSelectionChangeListener, OnTreeExpandedStateListener, OnKeyHandler {
735 
736     protected TreeItems _tree;
737 
738     @property ref TreeItems items() { return _tree; }
739 
740     Signal!OnTreeSelectionChangeListener selectionChange;
741     Signal!OnTreeExpandedStateListener expandedChange;
742     /// allows to provide individual popup menu for items
743     Listener!OnTreePopupMenuListener popupMenu;
744 
745     protected bool _needUpdateWidgets;
746     protected bool _needUpdateWidgetStates;
747 
748     protected bool _noCollapseForSingleTopLevelItem;
749     @property bool noCollapseForSingleTopLevelItem() {
750         return _noCollapseForSingleTopLevelItem;
751     }
752     @property TreeWidgetBase noCollapseForSingleTopLevelItem(bool flg) {
753         _noCollapseForSingleTopLevelItem = flg;
754         if (_tree)
755             _tree.noCollapseForSingleTopLevelItem = flg;
756         return this;
757     }
758 
759     protected MenuItem onTreeItemPopupMenu(TreeItems source, TreeItem selectedItem) {
760         if (popupMenu)
761             return popupMenu(source, selectedItem);
762         return null;
763     }
764 
765     /// empty parameter list constructor - for usage by factory
766     this() {
767         this(null);
768     }
769     /// create with ID parameter
770     this(string ID, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) {
771         super(ID, hscrollbarMode, vscrollbarMode);
772         contentWidget = new VerticalLayout("TREE_CONTENT");
773         _tree = new TreeItems();
774         _tree.contentListener = this;
775         _tree.stateListener = this;
776         _tree.selectionListener = this;
777         _tree.expandListener = this;
778 
779         _needUpdateWidgets = true;
780         _needUpdateWidgetStates = true;
781         acceleratorMap.add( [
782             new Action(TreeActions.Up, KeyCode.UP, 0),
783             new Action(TreeActions.Down, KeyCode.DOWN, 0),
784             new Action(TreeActions.ScrollLeft, KeyCode.LEFT, 0),
785             new Action(TreeActions.ScrollRight, KeyCode.RIGHT, 0),
786             //new Action(TreeActions.LineBegin, KeyCode.HOME, 0),
787             //new Action(TreeActions.LineEnd, KeyCode.END, 0),
788             new Action(TreeActions.PageUp, KeyCode.PAGEUP, 0),
789             new Action(TreeActions.PageDown, KeyCode.PAGEDOWN, 0),
790             //new Action(TreeActions.PageBegin, KeyCode.PAGEUP, KeyFlag.Control),
791             //new Action(TreeActions.PageEnd, KeyCode.PAGEDOWN, KeyFlag.Control),
792             new Action(TreeActions.ScrollTop, KeyCode.HOME, KeyFlag.Control),
793             new Action(TreeActions.ScrollBottom, KeyCode.END, KeyFlag.Control),
794             new Action(TreeActions.ScrollPageUp, KeyCode.PAGEUP, KeyFlag.Control),
795             new Action(TreeActions.ScrollPageDown, KeyCode.PAGEDOWN, KeyFlag.Control),
796             new Action(TreeActions.ScrollUp, KeyCode.UP, KeyFlag.Control),
797             new Action(TreeActions.ScrollDown, KeyCode.DOWN, KeyFlag.Control),
798             new Action(TreeActions.ScrollLeft, KeyCode.LEFT, KeyFlag.Control),
799             new Action(TreeActions.ScrollRight, KeyCode.RIGHT, KeyFlag.Control),
800         ]);
801     }
802 
803     ~this() {
804         if (_tree) {
805             destroy(_tree);
806             _tree = null;
807         }
808     }
809 
810     /** Override to use custom tree item widgets. */
811     protected Widget createItemWidget(TreeItem item) {
812         TreeItemWidget res = new TreeItemWidget(item);
813         res.keyEvent = this;
814         res.popupMenuListener = &onTreeItemPopupMenu;
815         return res;
816     }
817 
818     /// returns item by id, null if not found
819     TreeItem findItemById(string id) {
820         return _tree.findItemById(id);
821     }
822 
823     override bool onKey(Widget source, KeyEvent event) {
824         if (event.action == KeyAction.KeyDown) {
825             Action action = findKeyAction(event.keyCode, event.flags); // & (KeyFlag.Shift | KeyFlag.Alt | KeyFlag.Control)
826             if (action !is null) {
827                 return handleAction(action);
828             }
829         }
830         return false;
831     }
832 
833     protected void addWidgets(TreeItem item) {
834         if (item.level > 0)
835             _contentWidget.addChild(createItemWidget(item));
836         for (int i = 0; i < item.childCount; i++)
837             addWidgets(item.child(i));
838     }
839 
840     protected void updateWidgets() {
841         _contentWidget.removeAllChildren();
842         addWidgets(_tree);
843         _needUpdateWidgets = false;
844     }
845 
846     void clearAllItems() {
847         items.clear();
848         updateWidgets();
849         requestLayout();
850     }
851 
852     protected void updateWidgetStates() {
853         for (int i = 0; i < _contentWidget.childCount; i++) {
854             TreeItemWidget child = cast(TreeItemWidget)_contentWidget.child(i);
855             if (child)
856                 child.updateWidget();
857         }
858         _needUpdateWidgetStates = false;
859     }
860 
861     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
862     override void layout(Rect rc) {
863         if (visibility == Visibility.Gone) {
864             return;
865         }
866         if (_needUpdateWidgets)
867             updateWidgets();
868         if (_needUpdateWidgetStates)
869             updateWidgetStates();
870         super.layout(rc);
871     }
872 
873     override Point minimumVisibleContentSize() {
874         return Point(100.pointsToPixels, 100.pointsToPixels);
875     }
876 
877     /// calculate full content size in pixels
878     override Point fullContentSize() {
879         if (_needUpdateWidgets)
880             updateWidgets();
881         if (_needUpdateWidgetStates)
882             updateWidgetStates();
883         return super.fullContentSize();
884         //_contentWidget.measure(SIZE_UNSPECIFIED, SIZE_UNSPECIFIED);
885         //return Point(_contentWidget.measuredWidth,_contentWidget.measuredHeight);
886     }
887 
888     /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
889     override void measure(int parentWidth, int parentHeight) {
890         if (visibility == Visibility.Gone) {
891             return;
892         }
893         if (_needUpdateWidgets)
894             updateWidgets();
895         if (_needUpdateWidgetStates)
896             updateWidgetStates();
897         super.measure(parentWidth, parentHeight);
898     }
899 
900     /// listener
901     override void onTreeContentChange(TreeItems source) {
902         _needUpdateWidgets = true;
903         requestLayout();
904         //updateScrollBars();
905     }
906 
907     override void onTreeStateChange(TreeItems source) {
908         _needUpdateWidgetStates = true;
909         requestLayout();
910         //updateScrollBars();
911     }
912 
913     override void onTreeExpandedStateChange(TreeItems source, TreeItem item) {
914         if (expandedChange.assigned)
915             expandedChange(source, item);
916         layout(pos);
917         //requestLayout();
918         //updateScrollBars();
919     }
920 
921     TreeItemWidget findItemWidget(TreeItem item) {
922         for (int i = 0; i < _contentWidget.childCount; i++) {
923             TreeItemWidget child = cast(TreeItemWidget) _contentWidget.child(i);
924             if (child && child.item is item)
925                 return child;
926         }
927         return null;
928     }
929 
930     override void onTreeItemSelected(TreeItems source, TreeItem selectedItem, bool activated) {
931         TreeItemWidget selected = findItemWidget(selectedItem);
932         if (selected && selected.visibility == Visibility.Visible) {
933             selected.setFocus();
934             makeWidgetVisible(selected, false, true);
935         }
936         if (selectionChange.assigned)
937             selectionChange(source, selectedItem, activated);
938     }
939 
940     void makeItemVisible(TreeItem item) {
941         TreeItemWidget widget = findItemWidget(item);
942         if (widget && widget.visibility == Visibility.Visible) {
943             makeWidgetVisible(widget, false, true);
944         }
945     }
946 
947     void clearSelection() {
948         _tree.selectItem(null);
949     }
950 
951     void selectItem(TreeItem item, bool makeVisible = true) {
952         if (!item) {
953             clearSelection();
954             return;
955         }
956         _tree.selectItem(item);
957         if (makeVisible)
958             makeItemVisible(item);
959     }
960 
961     void selectItem(string itemId, bool makeVisible = true) {
962         TreeItem item = findItemById(itemId);
963         selectItem(item, makeVisible);
964     }
965 
966     override protected bool handleAction(const Action a) {
967         Log.d("tree.handleAction ", a.id);
968         switch (a.id) with(TreeActions)
969         {
970             case ScrollLeft:
971                 if (_hscrollbar)
972                     _hscrollbar.sendScrollEvent(ScrollAction.LineUp);
973                 break;
974             case ScrollRight:
975                 if (_hscrollbar)
976                     _hscrollbar.sendScrollEvent(ScrollAction.LineDown);
977                 break;
978             case ScrollUp:
979                 if (_vscrollbar)
980                     _vscrollbar.sendScrollEvent(ScrollAction.LineUp);
981                 break;
982             case ScrollPageUp:
983                 if (_vscrollbar)
984                     _vscrollbar.sendScrollEvent(ScrollAction.PageUp);
985                 break;
986             case ScrollDown:
987                 if (_vscrollbar)
988                     _vscrollbar.sendScrollEvent(ScrollAction.LineDown);
989                 break;
990             case ScrollPageDown:
991                 if (_vscrollbar)
992                     _vscrollbar.sendScrollEvent(ScrollAction.PageDown);
993                 break;
994             case Up:
995                 _tree.selectPrevious();
996                 break;
997             case Down:
998                 _tree.selectNext();
999                 break;
1000             case PageUp:
1001                 // TODO: implement page up
1002                 _tree.selectPrevious();
1003                 break;
1004             case PageDown:
1005                 // TODO: implement page down
1006                 _tree.selectPrevious();
1007                 break;
1008             default:
1009                 return super.handleAction(a);
1010         }
1011         return true;
1012     }
1013 }
1014 
1015 /// Tree widget with items which can have icons and labels
1016 class TreeWidget :  TreeWidgetBase {
1017     /// empty parameter list constructor - for usage by factory
1018     this() {
1019         this(null);
1020     }
1021     /// create with ID parameter
1022     this(string ID, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) {
1023         super(ID, hscrollbarMode, vscrollbarMode);
1024     }
1025 }