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 }