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