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