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 }