1 // Written in the D programming language.
2 
3 /**
4 This module contains declaration of tabbed view controls.
5 
6 TabItemWidget - single tab header in tab control
7 TabWidget
8 TabHost
9 TabControl
10 
11 
12 Synopsis:
13 
14 ----
15 import dlangui.widgets.tabs;
16 
17 ----
18 
19 Copyright: Vadim Lopatin, 2014
20 License:   Boost License 1.0
21 Authors:   Vadim Lopatin, coolreader.org@gmail.com
22 */
23 module dlangui.widgets.tabs;
24 
25 import dlangui.core.signals;
26 import dlangui.widgets.layouts;
27 import dlangui.widgets.controls;
28 
29 import std.algorithm;
30 
31 /// current tab is changed handler
32 interface TabHandler {
33     void onTabChanged(string newActiveTabId, string previousTabId);
34 }
35 
36 /// tab close button pressed handler
37 interface TabCloseHandler {
38     void onTabClose(string tabId);
39 }
40 
41 
42 /// tab item metadata
43 class TabItem {
44     private static __gshared long _lastAccessCounter;
45     private string _iconRes;
46     private string _id;
47     private UIString _label;
48     private long _lastAccessTs;
49     this(string id, string labelRes, string iconRes = null) {
50         _id = id;
51         _label.id = labelRes;
52         _iconRes = iconRes;
53     }
54     this(string id, dstring labelRes, string iconRes = null) {
55         _id = id;
56         _label.value = labelRes;
57         _iconRes = iconRes;
58         _lastAccessTs = _lastAccessCounter++;
59     }
60     @property string iconId() const { return _iconRes; }
61     @property string id() const { return _id; }
62     @property ref UIString text() { return _label; }
63     @property TabItem iconId(string id) { _iconRes = id; return this; }
64     @property TabItem id(string  id) { _id = id; return this; }
65     @property long lastAccessTs() { return _lastAccessTs; }
66     @property void lastAccessTs(long ts) { _lastAccessTs = ts; }
67     void updateAccessTs() {
68         _lastAccessTs = _lastAccessCounter++; //std.datetime.Clock.currStdTime;
69     }
70 
71     protected Object _objectParam;
72     @property Object objectParam() {
73         return _objectParam;
74     }
75     @property TabItem objectParam(Object value) {
76         _objectParam = value;
77         return this;
78     }
79 
80     protected int _intParam;
81     @property int intParam() {
82         return _intParam;
83     }
84     @property TabItem intParam(int value) {
85         _intParam = value;
86         return this;
87     }
88 }
89 
90 /// tab item widget - to show tab header
91 class TabItemWidget : HorizontalLayout {
92     private ImageWidget _icon;
93     private TextWidget _label;
94     private ImageButton _closeButton;
95     private TabItem _item;
96     private bool _enableCloseButton;
97     Signal!TabCloseHandler tabClose;
98     @property TabItem tabItem() { return _item; }
99     @property TabControl tabControl() { return cast(TabControl)parent; }
100     this(TabItem item, bool enableCloseButton = true) {
101         styleId = STYLE_TAB_UP_BUTTON;
102         _enableCloseButton = enableCloseButton;
103         _icon = new ImageWidget();
104         _label = new TextWidget();
105         _label.styleId = STYLE_TAB_UP_BUTTON_TEXT;
106         _label.state = State.Parent;
107         _closeButton = new ImageButton("CLOSE");
108         _closeButton.styleId = STYLE_BUTTON_TRANSPARENT;
109         _closeButton.drawableId = "close";
110         _closeButton.trackHover = true;
111         _closeButton.click = &onClick;
112         if (!_enableCloseButton) {
113             _closeButton.visibility = Visibility.Gone;
114         } else {
115             _closeButton.visibility = Visibility.Visible;
116         }
117         addChild(_icon);
118         addChild(_label);
119         addChild(_closeButton);
120         setItem(item);
121         clickable = true;
122         trackHover = true;
123         _label.trackHover = true;
124     }
125     void setStyles(string tabButtonStyle, string tabButtonTextStyle) {
126         styleId = tabButtonStyle;
127         _label.styleId = tabButtonTextStyle;
128     }
129     override void onDraw(DrawBuf buf) {
130         //debug Log.d("TabWidget.onDraw ", id);
131         super.onDraw(buf);
132     }
133     protected bool onClick(Widget source) {
134         if (source.compareId("CLOSE")) {
135             Log.d("tab close button pressed");
136             if (tabClose.assigned)
137                 tabClose(_item.id);
138         }
139         return true;
140     }
141     @property TabItem item() {
142         return _item;
143     }
144     @property void setItem(TabItem item) {
145         _item = item;
146         if (item.iconId !is null) {
147             _icon.visibility = Visibility.Visible;
148             _icon.drawableId = item.iconId;
149         } else {
150             _icon.visibility = Visibility.Gone;
151         }
152         _label.text = item.text;
153         id = item.id;
154     }
155 }
156 
157 /// tab item list helper class
158 class TabItemList {
159     private TabItem[] _list;
160     private int _len;
161 
162     this() {
163     }
164 
165     /// get item by index
166     TabItem get(int index) {
167         if (index < 0 || index >= _len)
168             return null;
169         return _list[index];
170     }
171     /// get item by index
172     const (TabItem) get(int index) const {
173         if (index < 0 || index >= _len)
174             return null;
175         return _list[index];
176     }
177     /// get item by index
178     TabItem opIndex(int index) {
179         return get(index);
180     }
181     /// get item by index
182     const (TabItem) opIndex(int index) const {
183         return get(index);
184     }
185     /// get item by id
186     TabItem get(string id) {
187         int idx = indexById(id);
188         if (idx < 0)
189             return null;
190         return _list[idx];
191     }
192     /// get item by id
193     const (TabItem) get(string id) const {
194         int idx = indexById(id);
195         if (idx < 0)
196             return null;
197         return _list[idx];
198     }
199     /// get item by id
200     TabItem opIndex(string id) {
201         return get(id);
202     }
203     @property int length() const { return _len; }
204     /// append new item
205     TabItemList add(TabItem item) {
206         return insert(item, -1);
207     }
208     /// insert new item to specified position
209     TabItemList insert(TabItem item, int index) {
210         if (index > _len || index < 0)
211             index = _len;
212         if (_list.length <= _len)
213             _list.length = _len + 4;
214         for (int i = _len; i > index; i--)
215             _list[i] = _list[i - 1];
216         _list[index] = item;
217         _len++;
218         return this;
219     }
220     /// remove item by index
221     TabItem remove(int index) {
222         TabItem res = _list[index];
223         for (int i = index; i < _len - 1; i++)
224             _list[i] = _list[i + 1];
225         _len--;
226         return res;
227     }
228     /// find tab index by id
229     int indexById(string id) const {
230         for (int i = 0; i < _len; i++) {
231             if (_list[i].id.equal(id))
232                 return i;
233         }
234         return -1;
235     }
236 }
237 
238 /// tab header - tab labels, with optional More button
239 class TabControl : WidgetGroupDefaultDrawing {
240     protected TabItemList _items;
241     protected ImageButton _moreButton;
242     protected bool _enableCloseButton;
243     protected TabItemWidget[] _sortedItems;
244     protected int _buttonOverlap;
245 
246     protected string _tabStyle;
247     protected string _tabButtonStyle;
248     protected string _tabButtonTextStyle;
249 
250     /// signal of tab change (e.g. by clicking on tab header)
251     Signal!TabHandler tabChanged;
252 
253     /// signal on tab close button
254     Signal!TabCloseHandler tabClose;
255 
256     protected Align _tabAlignment;
257     @property Align tabAlignment() { return _tabAlignment; }
258     @property void tabAlignment(Align a) { _tabAlignment = a; }
259 
260     /// empty parameter list constructor - for usage by factory
261     this() {
262         this(null);
263     }
264     /// create with ID parameter
265     this(string ID, Align tabAlignment = Align.Top) {
266         super(ID);
267         _tabAlignment = tabAlignment;
268         setStyles(STYLE_TAB_UP, STYLE_TAB_UP_BUTTON, STYLE_TAB_UP_BUTTON_TEXT);
269         _items = new TabItemList();
270         _moreButton = new ImageButton("MORE", "tab_more");
271         _moreButton.styleId = STYLE_BUTTON_TRANSPARENT;
272         _moreButton.mouseEvent = &onMouse;
273         _moreButton.margins(Rect(0,0,0,0));
274         _enableCloseButton = true;
275         styleId = _tabStyle;
276         addChild(_moreButton); // first child is always MORE button, the rest corresponds to tab list
277     }
278     void setStyles(string tabStyle, string tabButtonStyle, string tabButtonTextStyle) {
279         _tabStyle = tabStyle;
280         _tabButtonStyle = tabButtonStyle;
281         _tabButtonTextStyle = tabButtonTextStyle;
282         styleId = _tabStyle;
283         for (int i = 1; i < _children.count; i++) {
284             TabItemWidget w = cast(TabItemWidget)_children[i];
285             if (w) {
286                 w.setStyles(_tabButtonStyle, _tabButtonTextStyle);
287             }
288         }
289         _buttonOverlap = currentTheme.get(tabButtonStyle).customLength("overlap", 0);
290     }
291 
292     /// when true, shows close buttons in tabs
293     @property bool enableCloseButton() { return _enableCloseButton; }
294     /// ditto
295     @property void enableCloseButton(bool enabled) {
296         _enableCloseButton = enabled;
297     }
298 
299     /// returns tab count
300     @property int tabCount() const {
301         return _items.length;
302     }
303     /// returns tab item by id (null if index out of range)
304     TabItem tab(int index) {
305         return _items.get(index);
306     }
307     /// returns tab item by id (null if not found)
308     TabItem tab(string id) {
309         return _items.get(id);
310     }
311     /// returns tab item by id (null if not found)
312     const(TabItem) tab(string id) const {
313         return _items.get(id);
314     }
315     /// get tab index by tab id (-1 if not found)
316     int tabIndex(string id) {
317         return _items.indexById(id);
318     }
319     protected void updateTabs() {
320         // TODO:
321     }
322     static bool accessTimeComparator(TabItemWidget a, TabItemWidget b) {
323         return (a.tabItem.lastAccessTs > b.tabItem.lastAccessTs);
324     }
325 
326     protected TabItemWidget[] sortedItems() {
327         _sortedItems.length = _items.length;
328         for (int i = 0; i < _items.length; i++)
329             _sortedItems[i] = cast(TabItemWidget)_children.get(i + 1);
330         std.algorithm.sort!(accessTimeComparator)(_sortedItems);
331         return _sortedItems;
332     }
333 
334     /// find next or previous tab index, based on access time
335     int getNextItemIndex(int direction) {
336         if (_items.length == 0)
337             return -1;
338         if (_items.length == 1)
339             return 0;
340         TabItemWidget[] items = sortedItems();
341         for (int i = 0; i < items.length; i++) {
342             if (items[i].id == _selectedTabId) {
343                 int next = i + direction;
344                 if (next < 0)
345                     next = cast(int)(items.length - 1);
346                 if (next >= items.length)
347                     next = 0;
348                 return _items.indexById(items[next].id);
349             }
350         }
351         return -1;
352     }
353 
354     /// remove tab
355     TabControl removeTab(string id) {
356         string nextId;
357         if (id.equal(_selectedTabId)) {
358             // current tab is being closed: remember next tab id
359             int nextIndex = getNextItemIndex(1);
360             if (nextIndex < 0)
361                 nextIndex = getNextItemIndex(-1);
362             if (nextIndex >= 0)
363                 nextId = _items[nextIndex].id;
364         }
365         int index = _items.indexById(id);
366         if (index >= 0) {
367             Widget w = _children.remove(index + 1);
368             if (w)
369                 destroy(w);
370             _items.remove(index);
371             if (id.equal(_selectedTabId))
372                 _selectedTabId = null;
373             requestLayout();
374         }
375         if (nextId) {
376             index = _items.indexById(nextId);
377             if (index >= 0) {
378                 selectTab(index, true);
379             }
380         }
381         return this;
382     }
383 
384     /// change name of tab
385     void renameTab(string ID, dstring name) {
386         int index = _items.indexById(id);
387         if (index >= 0) {
388             renameTab(index, name);
389         }
390     }
391 
392     /// change name of tab
393     void renameTab(int index, dstring name) {
394         _items[index].text = name;
395         for (int i = 0; i < _children.count; i++) {
396             TabItemWidget widget = cast (TabItemWidget)_children[i];
397             if (widget && widget.item is _items[index]) {
398                 widget.setItem(_items[index]);
399                 requestLayout();
400                 break;
401             }
402         }
403     }
404 
405     protected void onTabClose(string tabId) {
406         if (tabClose.assigned)
407             tabClose(tabId);
408     }
409 
410     /// add new tab
411     TabControl addTab(TabItem item, int index = -1, bool enableCloseButton = false) {
412         _items.insert(item, index);
413         TabItemWidget widget = new TabItemWidget(item, enableCloseButton);
414         widget.parent = this;
415         widget.mouseEvent = &onMouse;
416         widget.setStyles(_tabButtonStyle, _tabButtonTextStyle);
417         widget.tabClose = &onTabClose;
418         _children.insert(widget, index);
419         updateTabs();
420         requestLayout();
421         return this;
422     }
423     /// add new tab by id and label string
424     TabControl addTab(string id, dstring label, string iconId = null, bool enableCloseButton = false) {
425         TabItem item = new TabItem(id, label, iconId);
426         return addTab(item, -1, enableCloseButton);
427     }
428     /// add new tab by id and label string resource id
429     TabControl addTab(string id, string labelResourceId, string iconId = null, bool enableCloseButton = false) {
430         TabItem item = new TabItem(id, labelResourceId, iconId);
431         return addTab(item, -1, enableCloseButton);
432     }
433     protected bool onMouse(Widget source, MouseEvent event) {
434         if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
435             if (source.compareId("MORE")) {
436                 Log.d("tab MORE button pressed");
437                 return true;
438             }
439             string id = source.id;
440             int index = tabIndex(id);
441             if (index >= 0) {
442                 selectTab(index, true);
443             }
444         }
445         return true;
446     }
447     /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
448     override void measure(int parentWidth, int parentHeight) {
449         //Log.d("tabControl.measure enter");
450         Rect m = margins;
451         Rect p = padding;
452         // calc size constraints for children
453         int pwidth = parentWidth;
454         int pheight = parentHeight;
455         if (parentWidth != SIZE_UNSPECIFIED)
456             pwidth -= m.left + m.right + p.left + p.right;
457         if (parentHeight != SIZE_UNSPECIFIED)
458             pheight -= m.top + m.bottom + p.top + p.bottom;
459         // measure children
460         Point sz;
461         if (_moreButton.visibility == Visibility.Visible) {
462             _moreButton.measure(pwidth, pheight);
463             sz.x = _moreButton.measuredWidth;
464             sz.y = _moreButton.measuredHeight;
465         }
466         pwidth -= sz.x;
467         for (int i = 1; i < _children.count; i++) {
468             Widget tab = _children.get(i);
469             tab.visibility = Visibility.Visible;
470             tab.measure(pwidth, pheight);
471             if (sz.y < tab.measuredHeight)
472                 sz.y = tab.measuredHeight;
473             if (sz.x + tab.measuredWidth > pwidth)
474                 break;
475             sz.x += tab.measuredWidth - _buttonOverlap;
476         }
477         measuredContent(parentWidth, parentHeight, sz.x, sz.y);
478         //Log.d("tabControl.measure exit");
479     }
480     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
481     override void layout(Rect rc) {
482         //Log.d("tabControl.layout enter");
483         _needLayout = false;
484         if (visibility == Visibility.Gone) {
485             return;
486         }
487         _pos = rc;
488         applyMargins(rc);
489         applyPadding(rc);
490         // more button
491         Rect moreRc = rc;
492         if (_moreButton.visibility == Visibility.Visible) {
493             moreRc.left = rc.right - _moreButton.measuredWidth;
494             _moreButton.layout(moreRc);
495             rc.right -= _moreButton.measuredWidth;
496         }
497         // tabs
498         int maxw = rc.width;
499         // measure and update visibility
500         TabItemWidget[] sorted = sortedItems();
501         int w = 0;
502         for (int i = 0; i < sorted.length; i++) {
503             TabItemWidget widget = sorted[i];
504             widget.visibility = Visibility.Visible;
505             widget.measure(rc.width, rc.height);
506             if (w + widget.measuredWidth < maxw) {
507                 w += widget.measuredWidth - _buttonOverlap;
508             } else {
509                 widget.visibility = Visibility.Gone;
510             }
511         }
512         // layout visible items
513         for (int i = 1; i < _children.count; i++) {
514             TabItemWidget widget = cast(TabItemWidget)_children.get(i);
515             if (widget.visibility != Visibility.Visible)
516                 continue;
517             w = widget.measuredWidth;
518             rc.right = rc.left + w;
519             widget.layout(rc);
520             rc.left += w - _buttonOverlap;
521         }
522         //Log.d("tabControl.layout exit");
523     }
524 
525     /// Draw widget at its position to buffer
526     override void onDraw(DrawBuf buf) {
527         if (visibility != Visibility.Visible)
528             return;
529         //debug Log.d("TabControl.onDraw enter");
530         super.Widget.onDraw(buf);
531         Rect rc = _pos;
532         applyMargins(rc);
533         applyPadding(rc);
534         auto saver = ClipRectSaver(buf, rc);
535         // draw all items except selected
536         for (int i = _children.count - 1; i >= 0; i--) {
537             Widget item = _children.get(i);
538             if (item.visibility != Visibility.Visible)
539                 continue;
540             if (item.id.equal(_selectedTabId)) // skip selected
541                 continue;
542             item.onDraw(buf);
543         }
544         // draw selected item
545         for (int i = 0; i < _children.count; i++) {
546             Widget item = _children.get(i);
547             if (item.visibility != Visibility.Visible)
548                 continue;
549             if (!item.id.equal(_selectedTabId)) // skip all except selected
550                 continue;
551             item.onDraw(buf);
552         }
553         //debug Log.d("TabControl.onDraw exit");
554     }
555 
556     protected string _selectedTabId;
557 
558     @property string selectedTabId() const {
559         return _selectedTabId;
560     }
561 
562     void updateAccessTs() {
563         int index = _items.indexById(_selectedTabId);
564         if (index >= 0)
565             _items[index].updateAccessTs();
566     }
567 
568     void selectTab(int index, bool updateAccess) {
569         if (index < 0 || index + 1 >= _children.count) {
570             Log.e("Tried to access tab out of bounds (index = %d, count = %d)",index,_children.count-1);
571             return;
572         }
573         if (_children.get(index + 1).compareId(_selectedTabId))
574             return; // already selected
575         string previousSelectedTab = _selectedTabId;
576         for (int i = 1; i < _children.count; i++) {
577             if (index == i - 1) {
578                 _children.get(i).state = State.Selected;
579                 _selectedTabId = _children.get(i).id;
580                 if (updateAccess)
581                     updateAccessTs();
582             } else {
583                 _children.get(i).state = State.Normal;
584             }
585         }
586         if (tabChanged.assigned)
587             tabChanged(_selectedTabId, previousSelectedTab);
588     }
589 
590 }
591 
592 /// container for widgets controlled by TabControl
593 class TabHost : FrameLayout, TabHandler {
594     /// empty parameter list constructor - for usage by factory
595     this() {
596         this(null);
597     }
598     /// create with ID parameter
599     this(string ID, TabControl tabControl = null) {
600         super(ID);
601         _tabControl = tabControl;
602         if (_tabControl !is null)
603             _tabControl.tabChanged = &onTabChanged;
604         styleId = STYLE_TAB_HOST;
605     }
606     protected TabControl _tabControl;
607     /// get currently set control widget
608     @property TabControl tabControl() { return _tabControl; }
609     /// set new control widget
610     @property TabHost tabControl(TabControl newWidget) { 
611         _tabControl = newWidget; 
612         if (_tabControl !is null)
613             _tabControl.tabChanged = &onTabChanged;
614         return this;
615     }
616 
617     protected Visibility _hiddenTabsVisibility = Visibility.Invisible;
618     @property Visibility hiddenTabsVisibility() { return _hiddenTabsVisibility; }
619     @property void hiddenTabsVisibility(Visibility v) { _hiddenTabsVisibility = v; }
620 
621     /// signal of tab change (e.g. by clicking on tab header)
622     Signal!TabHandler tabChanged;
623 
624     protected override void onTabChanged(string newActiveTabId, string previousTabId) {
625         if (newActiveTabId !is null) {
626             showChild(newActiveTabId, _hiddenTabsVisibility, true);
627         }
628         if (tabChanged.assigned)
629             tabChanged(newActiveTabId, previousTabId);
630     }
631 
632     /// get tab content widget by id
633     Widget tabBody(string id) {
634         for (int i = 0; i < _children.count; i++) {
635             if (_children[i].compareId(id))
636                 return _children[i];
637         }
638         return null;
639     }
640 
641     /// remove tab
642     TabHost removeTab(string id) {
643         assert(_tabControl !is null, "No TabControl set for TabHost");
644         Widget child = removeChild(id);
645         if (child !is null) {
646             destroy(child);
647         }
648         _tabControl.removeTab(id);
649         requestLayout();
650         return this;
651     }
652     /// add new tab by id and label string
653     TabHost addTab(Widget widget, dstring label, string iconId = null, bool enableCloseButton = false) {
654         assert(_tabControl !is null, "No TabControl set for TabHost");
655         assert(widget.id !is null, "ID for tab host page is mandatory");
656         assert(_children.indexOf(id) == -1, "duplicate ID for tab host page");
657         _tabControl.addTab(widget.id, label, iconId, enableCloseButton);
658         tabInitialization(widget);
659         //widget.focusGroup = true; // doesn't allow move focus outside of tab content
660         addChild(widget);
661         return this;
662     }
663     /// add new tab by id and label string resource id
664     TabHost addTab(Widget widget, string labelResourceId, string iconId = null, bool enableCloseButton = false) {
665         assert(_tabControl !is null, "No TabControl set for TabHost");
666         assert(widget.id !is null, "ID for tab host page is mandatory");
667         assert(_children.indexOf(id) == -1, "duplicate ID for tab host page");
668         _tabControl.addTab(widget.id, labelResourceId, iconId, enableCloseButton);
669         tabInitialization(widget);
670         addChild(widget);
671         return this;
672     }
673 
674     // handles initial tab selection & hides subsequently added tabs so
675     // they don't appear in the same frame
676     private void tabInitialization(Widget widget) {
677         if(_tabControl.selectedTabId is null) {
678             selectTab(_tabControl.tab(0).id,false);
679         } else {
680             widget.visibility = Visibility.Invisible;
681         }
682     }
683     
684     /// select tab
685     void selectTab(string ID, bool updateAccess) {
686         int index = _tabControl.tabIndex(ID);
687         if (index != -1) {
688             _tabControl.selectTab(index, updateAccess);
689         }
690     }
691 //    /// request relayout of widget and its children
692 //    override void requestLayout() {
693 //        Log.d("TabHost.requestLayout called");
694 //        super.requestLayout();
695 //        //_needLayout = true;
696 //    }
697 //    /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
698 //    override void layout(Rect rc) {
699 //        Log.d("TabHost.layout() called");
700 //        super.layout(rc);
701 //        Log.d("after layout(): needLayout = ", needLayout);
702 //    }
703 
704 }
705 
706 
707 
708 /// compound widget - contains from TabControl widget (tabs header) and TabHost (content pages)
709 class TabWidget : VerticalLayout, TabHandler, TabCloseHandler {
710     protected TabControl _tabControl;
711     protected TabHost _tabHost;
712     /// empty parameter list constructor - for usage by factory
713     this() {
714         this(null);
715     }
716     /// create with ID parameter
717     this(string ID, Align tabAlignment = Align.Top) {
718         super(ID);
719         _tabControl = new TabControl("TAB_CONTROL", tabAlignment);
720         _tabHost = new TabHost("TAB_HOST", _tabControl);
721         _tabControl.tabChanged.connect(this);
722         _tabControl.tabClose.connect(this);
723         styleId = STYLE_TAB_WIDGET;
724         if (tabAlignment == Align.Top) {
725             addChild(_tabControl);
726             addChild(_tabHost);
727         } else {
728             addChild(_tabHost);
729             addChild(_tabControl);
730         }
731         focusGroup = true;
732     }
733 
734     TabControl tabControl() { return _tabControl; }
735     TabHost tabHost() { return _tabHost; }
736 
737     /// signal of tab change (e.g. by clicking on tab header)
738     Signal!TabHandler tabChanged;
739     /// signal on tab close button
740     Signal!TabCloseHandler tabClose;
741 
742     protected override void onTabClose(string tabId) {
743         if (tabClose.assigned)
744             tabClose(tabId);
745     }
746 
747     protected override void onTabChanged(string newActiveTabId, string previousTabId) {
748         // forward to listener
749         if (tabChanged.assigned)
750             tabChanged(newActiveTabId, previousTabId);
751     }
752 
753     /// add new tab by id and label string resource id
754     TabWidget addTab(Widget widget, string labelResourceId, string iconId = null, bool enableCloseButton = false) {
755         _tabHost.addTab(widget, labelResourceId, iconId, enableCloseButton);
756         return this;
757     }
758 
759     /// add new tab by id and label (raw value)
760     TabWidget addTab(Widget widget, dstring label, string iconId = null, bool enableCloseButton = false) {
761         _tabHost.addTab(widget, label, iconId, enableCloseButton);
762         return this;
763     }
764 
765     /// remove tab by id
766     TabWidget removeTab(string id) {
767         _tabHost.removeTab(id);
768         requestLayout();
769         return this;
770     }
771 
772     /// change name of tab
773     void renameTab(string ID, dstring name) {
774         _tabControl.renameTab(ID, name);
775     }
776 
777     /// change name of tab
778     void renameTab(int index, dstring name) {
779         _tabControl.renameTab(index, name);
780     }
781 
782     @property Visibility hiddenTabsVisibility() { return _tabHost.hiddenTabsVisibility; }
783     @property void hiddenTabsVisibility(Visibility v) { _tabHost.hiddenTabsVisibility = v; }
784 
785     /// select tab
786     void selectTab(string ID, bool updateAccess = true) {
787         _tabHost.selectTab(ID, updateAccess);
788     }
789 
790     /// select tab
791     void selectTab(int index, bool updateAccess = true) {
792         _tabControl.selectTab(index, updateAccess);
793     }
794 
795     /// get tab content widget by id
796     Widget tabBody(string id) {
797         return _tabHost.tabBody(id);
798     }
799 
800     /// get tab content widget by id
801     Widget tabBody(int index) {
802         string id = _tabControl.tab(index).id;
803         return _tabHost.tabBody(id);
804     }
805 
806     /// returns tab item by id (null if index out of range)
807     TabItem tab(int index) {
808         return _tabControl.tab(index);
809     }
810     /// returns tab item by id (null if not found)
811     TabItem tab(string id) {
812         return _tabControl.tab(id);
813     }
814     /// returns tab count
815     @property int tabCount() const {
816         return _tabControl.tabCount;
817     }
818     /// get tab index by tab id (-1 if not found)
819     int tabIndex(string id) {
820         return _tabControl.tabIndex(id);
821     }
822 
823     /// change style ids
824     void setStyles(string tabWidgetStyle, string tabStyle, string tabButtonStyle, string tabButtonTextStyle, string tabHostStyle = null) {
825         styleId = tabWidgetStyle;
826         _tabControl.setStyles(tabStyle, tabButtonStyle, tabButtonTextStyle);
827         _tabHost.styleId = tabHostStyle;
828     }
829 
830     private bool _tabNavigationInProgress;
831 
832     /// process key event, return true if event is processed.
833     override bool onKeyEvent(KeyEvent event) {
834         if (_tabNavigationInProgress) {
835             if (event.action == KeyAction.KeyDown || event.action == KeyAction.KeyUp) {
836                 if (!(event.flags & KeyFlag.Control)) {
837                     _tabNavigationInProgress = false;
838                     _tabControl.updateAccessTs();
839                 }
840             }
841         }
842         if (event.action == KeyAction.KeyDown) {
843             if (event.keyCode == KeyCode.TAB && (event.flags & KeyFlag.Control)) {
844                 // support Ctrl+Tab and Ctrl+Shift+Tab for navigation
845                 _tabNavigationInProgress = true;
846                 int direction = (event.flags & KeyFlag.Shift) ? - 1 : 1;
847                 int index = _tabControl.getNextItemIndex(direction);
848                 if (index >= 0)
849                     selectTab(index, false);
850                 return true;
851             }
852         }
853         return super.onKeyEvent(event);
854     }
855 
856     @property const(TabItem) selectedTab() const {
857         return _tabControl.tab(selectedTabId);
858     }
859 
860     @property TabItem selectedTab() {
861         return _tabControl.tab(selectedTabId);
862     }
863 
864     @property string selectedTabId() const {
865         return _tabControl._selectedTabId;
866     }
867 
868     /// get tab content widget by id
869     Widget selectedTabBody() {
870         return _tabHost.tabBody(_tabControl._selectedTabId);
871     }
872 
873 }