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 && event.button == MouseButton.Left) {
593             if (source.compareId("MORE")) {
594                 Log.d("tab MORE button pressed");
595                 if (handleMorePopupMenu())
596                     return true;
597                 if (moreButtonClick.assigned) {
598                     moreButtonClick(this);
599                 }
600                 return true;
601             }
602             string id = source.id;
603             int index = tabIndex(id);
604             if (index >= 0) {
605                 selectTab(index, true);
606             }
607         }
608         return true;
609     }
610 
611     /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
612     override void measure(int parentWidth, int parentHeight) {
613         //Log.d("tabControl.measure enter");
614         Rect m = margins;
615         Rect p = padding;
616         // calc size constraints for children
617         int pwidth = parentWidth;
618         int pheight = parentHeight;
619         if (parentWidth != SIZE_UNSPECIFIED)
620             pwidth -= m.left + m.right + p.left + p.right;
621         if (parentHeight != SIZE_UNSPECIFIED)
622             pheight -= m.top + m.bottom + p.top + p.bottom;
623         // measure children
624         Point sz;
625         if (_moreButton.visibility == Visibility.Visible) {
626             _moreButton.measure(pwidth, pheight);
627             sz.x = _moreButton.measuredWidth;
628             sz.y = _moreButton.measuredHeight;
629         }
630         pwidth -= sz.x;
631         for (int i = 1; i < _children.count; i++) {
632             Widget tab = _children.get(i);
633             tab.visibility = Visibility.Visible;
634             tab.measure(pwidth, pheight);
635             if (sz.y < tab.measuredHeight)
636                 sz.y = tab.measuredHeight;
637             if (sz.x + tab.measuredWidth > pwidth)
638                 break;
639             sz.x += tab.measuredWidth - _buttonOverlap;
640         }
641         measuredContent(parentWidth, parentHeight, sz.x, sz.y);
642         //Log.d("tabControl.measure exit");
643     }
644 
645     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
646     override void layout(Rect rc) {
647         //Log.d("tabControl.layout enter");
648         _needLayout = false;
649         if (visibility == Visibility.Gone) {
650             return;
651         }
652         _pos = rc;
653         applyMargins(rc);
654         applyPadding(rc);
655         // more button
656         Rect moreRc = rc;
657         if (_moreButton.visibility == Visibility.Visible) {
658             moreRc.left = rc.right - _moreButton.measuredWidth;
659             _moreButton.layout(moreRc);
660             rc.right -= _moreButton.measuredWidth;
661         }
662         // tabs
663         int maxw = rc.width;
664         // measure and update visibility
665         TabItemWidget[] sorted = sortedItems();
666         int w = 0;
667         for (int i = 0; i < sorted.length; i++) {
668             TabItemWidget widget = sorted[i];
669             widget.visibility = Visibility.Visible;
670             widget.measure(rc.width, rc.height);
671             if (w + widget.measuredWidth < maxw) {
672                 w += widget.measuredWidth - _buttonOverlap;
673             } else {
674                 widget.visibility = Visibility.Gone;
675             }
676         }
677         // layout visible items
678         for (int i = 1; i < _children.count; i++) {
679             TabItemWidget widget = cast(TabItemWidget)_children.get(i);
680             if (widget.visibility != Visibility.Visible)
681                 continue;
682             w = widget.measuredWidth;
683             rc.right = rc.left + w;
684             widget.layout(rc);
685             rc.left += w - _buttonOverlap;
686         }
687         //Log.d("tabControl.layout exit");
688     }
689 
690     /// Draw widget at its position to buffer
691     override void onDraw(DrawBuf buf) {
692         if (visibility != Visibility.Visible)
693             return;
694         //debug Log.d("TabControl.onDraw enter");
695         super.Widget.onDraw(buf);
696         Rect rc = _pos;
697         applyMargins(rc);
698         applyPadding(rc);
699         auto saver = ClipRectSaver(buf, rc);
700         // draw all items except selected
701         for (int i = _children.count - 1; i >= 0; i--) {
702             Widget item = _children.get(i);
703             if (item.visibility != Visibility.Visible)
704                 continue;
705             if (item.id.equal(_selectedTabId)) // skip selected
706                 continue;
707             item.onDraw(buf);
708         }
709         // draw selected item
710         for (int i = 0; i < _children.count; i++) {
711             Widget item = _children.get(i);
712             if (item.visibility != Visibility.Visible)
713                 continue;
714             if (!item.id.equal(_selectedTabId)) // skip all except selected
715                 continue;
716             item.onDraw(buf);
717         }
718         //debug Log.d("TabControl.onDraw exit");
719     }
720 
721     protected string _selectedTabId;
722 
723     @property string selectedTabId() const {
724         return _selectedTabId;
725     }
726 
727     void updateAccessTs() {
728         int index = _items.indexById(_selectedTabId);
729         if (index >= 0)
730             _items[index].updateAccessTs();
731     }
732 
733     void selectTab(int index, bool updateAccess) {
734         if (index < 0 || index + 1 >= _children.count) {
735             Log.e("Tried to access tab out of bounds (index = %d, count = %d)",index,_children.count-1);
736             return;
737         }
738         if (_children.get(index + 1).compareId(_selectedTabId))
739             return; // already selected
740         string previousSelectedTab = _selectedTabId;
741         for (int i = 1; i < _children.count; i++) {
742             if (index == i - 1) {
743                 _children.get(i).state = State.Selected;
744                 _selectedTabId = _children.get(i).id;
745                 if (updateAccess)
746                     updateAccessTs();
747             } else {
748                 _children.get(i).state = State.Normal;
749             }
750         }
751         if (tabChanged.assigned)
752             tabChanged(_selectedTabId, previousSelectedTab);
753     }
754 }
755 
756 /// container for widgets controlled by TabControl
757 class TabHost : FrameLayout, TabHandler {
758     /// empty parameter list constructor - for usage by factory
759     this() {
760         this(null);
761     }
762     /// create with ID parameter
763     this(string ID, TabControl tabControl = null) {
764         super(ID);
765         _tabControl = tabControl;
766         if (_tabControl !is null)
767             _tabControl.tabChanged = &onTabChanged;
768         styleId = STYLE_TAB_HOST;
769     }
770     protected TabControl _tabControl;
771     /// get currently set control widget
772     @property TabControl tabControl() { return _tabControl; }
773     /// set new control widget
774     @property TabHost tabControl(TabControl newWidget) {
775         _tabControl = newWidget;
776         if (_tabControl !is null)
777             _tabControl.tabChanged = &onTabChanged;
778         return this;
779     }
780 
781     protected Visibility _hiddenTabsVisibility = Visibility.Invisible;
782     @property Visibility hiddenTabsVisibility() { return _hiddenTabsVisibility; }
783     @property void hiddenTabsVisibility(Visibility v) { _hiddenTabsVisibility = v; }
784 
785     /// signal of tab change (e.g. by clicking on tab header)
786     Signal!TabHandler tabChanged;
787 
788     protected override void onTabChanged(string newActiveTabId, string previousTabId) {
789         if (newActiveTabId !is null) {
790             showChild(newActiveTabId, _hiddenTabsVisibility, true);
791         }
792         if (tabChanged.assigned)
793             tabChanged(newActiveTabId, previousTabId);
794     }
795 
796     /// get tab content widget by id
797     Widget tabBody(string id) {
798         for (int i = 0; i < _children.count; i++) {
799             if (_children[i].compareId(id))
800                 return _children[i];
801         }
802         return null;
803     }
804 
805     /// remove tab
806     TabHost removeTab(string id) {
807         assert(_tabControl !is null, "No TabControl set for TabHost");
808         Widget child = removeChild(id);
809         if (child !is null) {
810             destroy(child);
811         }
812         _tabControl.removeTab(id);
813         requestLayout();
814         return this;
815     }
816 
817     /// add new tab by id and label string
818     TabHost addTab(Widget widget, dstring label, string iconId = null, bool enableCloseButton = false, dstring tooltipText = null) {
819         assert(_tabControl !is null, "No TabControl set for TabHost");
820         assert(widget.id !is null, "ID for tab host page is mandatory");
821         assert(_children.indexOf(id) == -1, "duplicate ID for tab host page");
822         _tabControl.addTab(widget.id, label, iconId, enableCloseButton, tooltipText);
823         tabInitialization(widget);
824         //widget.focusGroup = true; // doesn't allow move focus outside of tab content
825         addChild(widget);
826         return this;
827     }
828 
829     /// add new tab by id and label string resource id
830     TabHost addTab(Widget widget, string labelResourceId, string iconId = null, bool enableCloseButton = false, dstring tooltipText = null) {
831         assert(_tabControl !is null, "No TabControl set for TabHost");
832         assert(widget.id !is null, "ID for tab host page is mandatory");
833         assert(_children.indexOf(id) == -1, "duplicate ID for tab host page");
834         _tabControl.addTab(widget.id, labelResourceId, iconId, enableCloseButton, tooltipText);
835         tabInitialization(widget);
836         addChild(widget);
837         return this;
838     }
839 
840     /// add new tab by id and label UIString
841     TabHost addTab(Widget widget, UIString label, string iconId = null, bool enableCloseButton = false, dstring tooltipText = null) {
842         assert(_tabControl !is null, "No TabControl set for TabHost");
843         assert(widget.id !is null, "ID for tab host page is mandatory");
844         assert(_children.indexOf(id) == -1, "duplicate ID for tab host page");
845         _tabControl.addTab(widget.id, label, iconId, enableCloseButton, tooltipText);
846         tabInitialization(widget);
847         addChild(widget);
848         return this;
849     }
850 
851     // handles initial tab selection & hides subsequently added tabs so
852     // they don't appear in the same frame
853     private void tabInitialization(Widget widget) {
854         if(_tabControl.selectedTabId is null) {
855             selectTab(_tabControl.tab(0).id,false);
856         } else {
857             widget.visibility = Visibility.Invisible;
858         }
859     }
860 
861     /// select tab
862     void selectTab(string ID, bool updateAccess) {
863         int index = _tabControl.tabIndex(ID);
864         if (index != -1) {
865             _tabControl.selectTab(index, updateAccess);
866         }
867     }
868 //    /// request relayout of widget and its children
869 //    override void requestLayout() {
870 //        Log.d("TabHost.requestLayout called");
871 //        super.requestLayout();
872 //        //_needLayout = true;
873 //    }
874 //    /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
875 //    override void layout(Rect rc) {
876 //        Log.d("TabHost.layout() called");
877 //        super.layout(rc);
878 //        Log.d("after layout(): needLayout = ", needLayout);
879 //    }
880 
881 }
882 
883 
884 
885 /// compound widget - contains from TabControl widget (tabs header) and TabHost (content pages)
886 class TabWidget : VerticalLayout, TabHandler, TabCloseHandler {
887     protected TabControl _tabControl;
888     protected TabHost _tabHost;
889     /// empty parameter list constructor - for usage by factory
890     this() {
891         this(null);
892     }
893     /// create with ID parameter
894     this(string ID, Align tabAlignment = Align.Top) {
895         super(ID);
896         _tabControl = new TabControl("TAB_CONTROL", tabAlignment);
897         _tabHost = new TabHost("TAB_HOST", _tabControl);
898         _tabControl.tabChanged.connect(this);
899         _tabControl.tabClose.connect(this);
900         styleId = STYLE_TAB_WIDGET;
901         if (tabAlignment == Align.Top) {
902             addChild(_tabControl);
903             addChild(_tabHost);
904         } else {
905             addChild(_tabHost);
906             addChild(_tabControl);
907         }
908         focusGroup = true;
909     }
910 
911     TabControl tabControl() { return _tabControl; }
912     TabHost tabHost() { return _tabHost; }
913 
914     /// signal of tab change (e.g. by clicking on tab header)
915     Signal!TabHandler tabChanged;
916     /// signal on tab close button
917     Signal!TabCloseHandler tabClose;
918 
919     protected override void onTabClose(string tabId) {
920         if (tabClose.assigned)
921             tabClose(tabId);
922     }
923 
924     protected override void onTabChanged(string newActiveTabId, string previousTabId) {
925         // forward to listener
926         if (tabChanged.assigned)
927             tabChanged(newActiveTabId, previousTabId);
928     }
929 
930     /// add new tab by id and label string resource id
931     TabWidget addTab(Widget widget, string labelResourceId, string iconId = null, bool enableCloseButton = false, dstring tooltipText = null) {
932         _tabHost.addTab(widget, labelResourceId, iconId, enableCloseButton, tooltipText);
933         return this;
934     }
935 
936     /// add new tab by id and label (raw value)
937     TabWidget addTab(Widget widget, dstring label, string iconId = null, bool enableCloseButton = false, dstring tooltipText = null) {
938         _tabHost.addTab(widget, label, iconId, enableCloseButton, tooltipText);
939         return this;
940     }
941 
942     /// remove tab by id
943     TabWidget removeTab(string id) {
944         _tabHost.removeTab(id);
945         requestLayout();
946         return this;
947     }
948 
949     /// change name of tab
950     void renameTab(string ID, dstring name) {
951         _tabControl.renameTab(ID, name);
952     }
953 
954     /// change name of tab
955     void renameTab(int index, dstring name) {
956         _tabControl.renameTab(index, name);
957     }
958 
959     /// change name of tab
960     void renameTab(int index, string id, dstring name) {
961         _tabControl.renameTab(index, id, name);
962     }
963 
964     @property Visibility hiddenTabsVisibility() { return _tabHost.hiddenTabsVisibility; }
965     @property void hiddenTabsVisibility(Visibility v) { _tabHost.hiddenTabsVisibility = v; }
966 
967     /// select tab
968     void selectTab(string ID, bool updateAccess = true) {
969         _tabHost.selectTab(ID, updateAccess);
970     }
971 
972     /// select tab
973     void selectTab(int index, bool updateAccess = true) {
974         _tabControl.selectTab(index, updateAccess);
975     }
976 
977     /// get tab content widget by id
978     Widget tabBody(string id) {
979         return _tabHost.tabBody(id);
980     }
981 
982     /// get tab content widget by id
983     Widget tabBody(int index) {
984         string id = _tabControl.tab(index).id;
985         return _tabHost.tabBody(id);
986     }
987 
988     /// returns tab item by id (null if index out of range)
989     TabItem tab(int index) {
990         return _tabControl.tab(index);
991     }
992     /// returns tab item by id (null if not found)
993     TabItem tab(string id) {
994         return _tabControl.tab(id);
995     }
996     /// returns tab count
997     @property int tabCount() const {
998         return _tabControl.tabCount;
999     }
1000     /// get tab index by tab id (-1 if not found)
1001     int tabIndex(string id) {
1002         return _tabControl.tabIndex(id);
1003     }
1004 
1005     /// change style ids
1006     void setStyles(string tabWidgetStyle, string tabStyle, string tabButtonStyle, string tabButtonTextStyle, string tabHostStyle = null) {
1007         styleId = tabWidgetStyle;
1008         _tabControl.setStyles(tabStyle, tabButtonStyle, tabButtonTextStyle);
1009         _tabHost.styleId = tabHostStyle;
1010     }
1011 
1012     /// override to handle specific actions
1013     override bool handleAction(const Action a) {
1014         if (a.id == StandardAction.TabSelectItem) {
1015             selectTab(cast(int)a.longParam);
1016             return true;
1017         }
1018         return super.handleAction(a);
1019     }
1020 
1021     private bool _tabNavigationInProgress;
1022 
1023     /// process key event, return true if event is processed.
1024     override bool onKeyEvent(KeyEvent event) {
1025         if (_tabNavigationInProgress) {
1026             if (event.action == KeyAction.KeyDown || event.action == KeyAction.KeyUp) {
1027                 if (!(event.flags & KeyFlag.Control)) {
1028                     _tabNavigationInProgress = false;
1029                     _tabControl.updateAccessTs();
1030                 }
1031             }
1032         }
1033         if (event.action == KeyAction.KeyDown) {
1034             if (event.keyCode == KeyCode.TAB && (event.flags & KeyFlag.Control)) {
1035                 // support Ctrl+Tab and Ctrl+Shift+Tab for navigation
1036                 _tabNavigationInProgress = true;
1037                 int direction = (event.flags & KeyFlag.Shift) ? - 1 : 1;
1038                 int index = _tabControl.getNextItemIndex(direction);
1039                 if (index >= 0)
1040                     selectTab(index, false);
1041                 return true;
1042             }
1043         }
1044         return super.onKeyEvent(event);
1045     }
1046 
1047     @property const(TabItem) selectedTab() const {
1048         return _tabControl.tab(selectedTabId);
1049     }
1050 
1051     @property TabItem selectedTab() {
1052         return _tabControl.tab(selectedTabId);
1053     }
1054 
1055     @property string selectedTabId() const {
1056         return _tabControl._selectedTabId;
1057     }
1058 
1059     /// focus selected tab body
1060     void focusSelectedTab() {
1061         if (!visible)
1062             return;
1063         Widget w = selectedTabBody;
1064         if (w)
1065             w.setFocus();
1066     }
1067 
1068     /// get tab content widget by id
1069     @property Widget selectedTabBody() {
1070         return _tabHost.tabBody(_tabControl._selectedTabId);
1071     }
1072 
1073 }