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