1 // Written in the D programming language.
2 
3 /**
4 DLANGUI library.
5 
6 This module contains declaration of tabbed view controls.
7 
8 
9 
10 Synopsis:
11 
12 ----
13 import dlangui.widgets.tabs;
14 
15 ----
16 
17 Copyright: Vadim Lopatin, 2014
18 License:   $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
19 Authors:   $(WEB coolreader.org, Vadim Lopatin)
20 */
21 module dlangui.widgets.tabs;
22 
23 import dlangui.core.signals;
24 import dlangui.widgets.layouts;
25 import dlangui.widgets.controls;
26 
27 interface TabHandler {
28     void onTabChanged(string newActiveTabId, string previousTabId);
29 }
30 
31 class TabItem {
32     private string _iconRes;
33     private string _id;
34     private UIString _label;
35     private long _lastAccessTs;
36     this(string id, string labelRes, string iconRes = null) {
37         _id = id;
38         _label = labelRes;
39         _iconRes = iconRes;
40     }
41     this(string id, dstring labelRes, string iconRes = null) {
42         _id = id;
43         _label = labelRes;
44         _iconRes = iconRes;
45         _lastAccessTs = std.datetime.Clock.currStdTime;
46     }
47     @property string iconId() const { return _iconRes; }
48     @property string id() const { return _id; }
49     @property ref UIString text() { return _label; }
50     @property TabItem iconId(string id) { _iconRes = id; return this; }
51     @property TabItem id(string  id) { _id = id; return this; }
52     @property long lastAccessTs() { return _lastAccessTs; }
53     @property void lastAccessTs(long ts) { _lastAccessTs = ts; }
54     void updateAccessTs() { _lastAccessTs = std.datetime.Clock.currStdTime; }
55 }
56 
57 class TabItemWidget : HorizontalLayout {
58     private ImageWidget _icon;
59     private TextWidget _label;
60     private ImageButton _closeButton;
61     private TabItem _item;
62     private bool _enableCloseButton;
63     @property TabItem tabItem() { return _item; }
64     @property TabControl tabControl() { return cast(TabControl)parent; }
65     this(TabItem item, bool enableCloseButton = true) {
66         styleId = "TAB_UP_BUTTON";
67         _enableCloseButton = enableCloseButton;
68         _icon = new ImageWidget();
69         _label = new TextWidget();
70         _label.styleId = "TAB_UP_BUTTON_TEXT";
71         _label.state = State.Parent;
72         _closeButton = new ImageButton("CLOSE");
73         _closeButton.styleId = "BUTTON_TRANSPARENT";
74         _closeButton.drawableId = "close";
75 		_closeButton.trackHover = true;
76         _closeButton.onClickListener = &onClick;
77         if (_enableCloseButton) {
78             _closeButton.visibility = Visibility.Gone;
79         } else {
80             _closeButton.visibility = Visibility.Visible;
81         }
82         addChild(_icon);
83         addChild(_label);
84         addChild(_closeButton);
85         setItem(item);
86 		clickable = true;
87         trackHover = true;
88     }
89     protected bool onClick(Widget source) {
90         if (source.compareId("CLOSE")) {
91             Log.d("tab close button pressed");
92         }
93         return true;
94     }
95     protected void setItem(TabItem item) {
96         _item = item;
97         if (item.iconId !is null) {
98             _icon.visibility = Visibility.Visible;
99             _icon.drawableId = item.iconId;
100         } else {
101             _icon.visibility = Visibility.Gone;
102         }
103         _label.text = item.text;
104         id = item.id;
105     }
106 }
107 
108 /// tab item list helper class
109 class TabItemList {
110     private TabItem[] _list;
111     private int _len;
112 
113     this() {
114     }
115 
116     /// get item by index
117     TabItem get(int index) {
118         if (index < 0 || index >= _len)
119             return null;
120         return _list[index];
121     }
122     /// get item by id
123     TabItem get(string id) {
124         int idx = indexById(id);
125         if (idx < 0)
126             return null;
127         return _list[idx];
128     }
129     @property int length() const { return _len; }
130     /// append new item
131     TabItemList add(TabItem item) {
132         return insert(item, -1);
133     }
134     /// insert new item to specified position
135     TabItemList insert(TabItem item, int index) {
136         if (index > _len || index < 0)
137             index = _len;
138         if (_list.length <= _len)
139             _list.length = _len + 4;
140         for (int i = _len; i > index; i--)
141             _list[i] = _list[i - 1];
142         _list[index] = item;
143         _len++;
144         return this;
145     }
146     /// remove item by index
147     TabItem remove(int index) {
148         TabItem res = _list[index];
149         for (int i = index; i < _len - 1; i++)
150             _list[i] = _list[i + 1];
151         _len--;
152         return res;
153     }
154     /// find tab index by id
155     int indexById(string id) {
156         import std.algorithm;
157         for (int i = 0; i < _len; i++) {
158             if (_list[i].id.equal(id))
159                 return i;
160         }
161         return -1;
162     }
163 }
164 
165 class TabControl : WidgetGroup {
166     protected TabItemList _items;
167     protected ImageButton _moreButton;
168     protected bool _enableCloseButton;
169     protected TabItemWidget[] _sortedItems;
170 
171     protected void delegate(string newActiveTabId, string previousTabId) _onTabChanged;
172     @property void delegate(string newActiveTabId, string previousTabId) onTabChangedListener() { return _onTabChanged; }
173     @property TabControl onTabChangedListener(void delegate(string newActiveTabId, string previousTabId) listener) { _onTabChanged = listener; return this; }
174 
175     this(string ID) {
176         super(ID);
177         _items = new TabItemList();
178         _moreButton = new ImageButton("MORE", "tab_more");
179         _moreButton.styleId = "BUTTON_TRANSPARENT";
180         _moreButton.onClickListener = &onClick;
181         _moreButton.margins(Rect(3,3,3,6));
182         _enableCloseButton = true;
183         styleId = "TAB_UP";
184         addChild(_moreButton); // first child is always MORE button, the rest corresponds to tab list
185     }
186     /// returns tab count
187     @property int tabCount() const {
188         return _items.length;
189     }
190     /// returns tab item by id (null if index out of range)
191     TabItem tab(int index) {
192         return _items.get(index);
193     }
194     /// returns tab item by id (null if not found)
195     TabItem tab(string id) {
196         return _items.get(id);
197     }
198     /// get tab index by tab id (-1 if not found)
199     int tabIndex(string id) {
200         return _items.indexById(id);
201     }
202     protected void updateTabs() {
203         // TODO:
204     }
205     static bool accessTimeComparator(TabItemWidget a, TabItemWidget b) {
206         return (a.tabItem.lastAccessTs > b.tabItem.lastAccessTs);
207     }
208     protected TabItemWidget[] sortedItems() {
209         _sortedItems.length = _items.length;
210         for (int i = 0; i < _items.length; i++)
211             _sortedItems[i] = cast(TabItemWidget)_children.get(i + 1);
212         std.algorithm.sort!(accessTimeComparator)(_sortedItems);
213         return _sortedItems;
214     }
215     /// remove tab
216     TabControl removeTab(string id) {
217         int index = _items.indexById(id);
218         if (index >= 0) {
219             _children.remove(index + 1);
220             _items.remove(index);
221             requestLayout();
222         }
223         return this;
224     }
225     /// add new tab
226     TabControl addTab(TabItem item, int index = -1, bool enableCloseButton = false) {
227         _items.insert(item, index);
228         TabItemWidget widget = new TabItemWidget(item, enableCloseButton);
229         widget.parent = this;
230         widget.onClickListener = &onClick;
231         _children.insert(widget, index);
232         updateTabs();
233         requestLayout();
234         return this;
235     }
236     /// add new tab by id and label string
237     TabControl addTab(string id, dstring label, string iconId = null, bool enableCloseButton = false) {
238         TabItem item = new TabItem(id, label, iconId);
239         return addTab(item, -1, enableCloseButton);
240     }
241     /// add new tab by id and label string resource id
242     TabControl addTab(string id, string labelResourceId, string iconId = null, bool enableCloseButton = false) {
243         TabItem item = new TabItem(id, labelResourceId, iconId);
244         return addTab(item, -1, enableCloseButton);
245     }
246     protected bool onClick(Widget source) {
247         if (source.compareId("MORE")) {
248             Log.d("tab MORE button pressed");
249             return true;
250         }
251         string id = source.id;
252         int index = tabIndex(id);
253         if (index >= 0) {
254             selectTab(index);
255         }
256         return true;
257     }
258     /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
259     override void measure(int parentWidth, int parentHeight) { 
260         //Log.d("tabControl.measure enter");
261         Rect m = margins;
262         Rect p = padding;
263         // calc size constraints for children
264         int pwidth = parentWidth;
265         int pheight = parentHeight;
266         if (parentWidth != SIZE_UNSPECIFIED)
267             pwidth -= m.left + m.right + p.left + p.right;
268         if (parentHeight != SIZE_UNSPECIFIED)
269             pheight -= m.top + m.bottom + p.top + p.bottom;
270         // measure children
271         Point sz;
272         _moreButton.measure(pwidth, pheight);
273         sz.x = _moreButton.measuredWidth;
274         sz.y = _moreButton.measuredHeight;
275         pwidth -= sz.x;
276         for (int i = 1; i < _children.count; i++) {
277             Widget tab = _children.get(i);
278             tab.visibility = Visibility.Visible;
279             tab.measure(pwidth, pheight);
280             if (sz.y < tab.measuredHeight)
281                 sz.y = tab.measuredHeight;
282             if (sz.x + tab.measuredWidth > pwidth)
283                 break;
284             sz.x += tab.measuredWidth;
285         }
286         measuredContent(parentWidth, parentHeight, sz.x, sz.y);
287         //Log.d("tabControl.measure exit");
288     }
289     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
290     override void layout(Rect rc) {
291         //Log.d("tabControl.layout enter");
292         _needLayout = false;
293         if (visibility == Visibility.Gone) {
294             return;
295         }
296         _pos = rc;
297         applyMargins(rc);
298         applyPadding(rc);
299         // more button
300         Rect moreRc = rc;
301         moreRc.left = rc.right - _moreButton.measuredWidth;
302         _moreButton.layout(moreRc);
303         rc.right -= _moreButton.measuredWidth;
304         // tabs
305         int maxw = rc.width;
306         // measure and update visibility
307         TabItemWidget[] sorted = sortedItems();
308         int w = 0;
309         for (int i = 0; i < sorted.length; i++) {
310             TabItemWidget widget = sorted[i];
311             widget.visibility = Visibility.Visible;
312             widget.measure(rc.width, rc.height);
313             if (w + widget.measuredWidth < maxw) {
314                 w += widget.measuredWidth;
315             } else {
316                 widget.visibility = Visibility.Gone;
317             }
318         }
319         // layout visible items
320         for (int i = 1; i < _children.count; i++) {
321             TabItemWidget widget = cast(TabItemWidget)_children.get(i);
322             if (widget.visibility != Visibility.Visible)
323                 continue;
324             w = widget.measuredWidth;
325             rc.right = rc.left + w;
326             widget.layout(rc);
327             rc.left += w;
328         }
329         //Log.d("tabControl.layout exit");
330     }
331     /// Draw widget at its position to buffer
332     override void onDraw(DrawBuf buf) {
333         //Log.d("tabControl.onDraw enter");
334         if (visibility != Visibility.Visible)
335             return;
336         super.onDraw(buf);
337         Rect rc = _pos;
338         applyMargins(rc);
339         applyPadding(rc);
340         auto saver = ClipRectSaver(buf, rc);
341 		for (int i = 0; i < _children.count; i++) {
342 			Widget item = _children.get(i);
343 			if (item.visibility != Visibility.Visible)
344 				continue;
345 			item.onDraw(buf);
346 		}
347         //Log.d("tabControl.onDraw exit");
348     }
349 
350     protected string _selectedTabId;
351 
352     void selectTab(int index) {
353         if (_children.get(index + 1).compareId(_selectedTabId))
354             return; // already selected
355         string previousSelectedTab = _selectedTabId;
356 		for (int i = 1; i < _children.count; i++) {
357             if (index == i - 1) {
358                 _children.get(i).state = State.Selected;
359                 _selectedTabId = _children.get(i).id;
360             } else {
361                 _children.get(i).state = State.Normal;
362             }
363         }
364         if (_onTabChanged !is null)
365             _onTabChanged(_selectedTabId, previousSelectedTab);
366     }
367 }
368 
369 /// container for widgets controlled by TabControl
370 class TabHost : FrameLayout, TabHandler {
371     this(string ID, TabControl tabControl = null) {
372         super(ID);
373         _tabControl = tabControl;
374         if (_tabControl !is null)
375             _tabControl.onTabChangedListener = &onTabChanged;
376         styleId = "TAB_HOST";
377     }
378     protected TabControl _tabControl;
379     /// get currently set control widget
380     @property TabControl tabControl() { return _tabControl; }
381     /// set new control widget
382     @property TabHost tabControl(TabControl newWidget) { 
383         _tabControl = newWidget; 
384         if (_tabControl !is null)
385             _tabControl.onTabChangedListener = &onTabChanged;
386         return this;
387     }
388 
389     protected void delegate(string newActiveTabId, string previousTabId) _onTabChanged;
390     @property void delegate(string newActiveTabId, string previousTabId) onTabChangedListener() { return _onTabChanged; }
391     @property TabHost onTabChangedListener(void delegate(string newActiveTabId, string previousTabId) listener) { _onTabChanged = listener; return this; }
392 
393     protected override void onTabChanged(string newActiveTabId, string previousTabId) {
394         if (newActiveTabId !is null) {
395             showChild(newActiveTabId, Visibility.Invisible, true);
396         }
397         if (_onTabChanged !is null)
398             _onTabChanged(newActiveTabId, previousTabId);
399     }
400 
401     /// remove tab
402     TabHost removeTab(string id) {
403         assert(_tabControl !is null, "No TabControl set for TabHost");
404         Widget child = removeChild(id);
405         if (child !is null) {
406             destroy(child);
407         }
408         _tabControl.removeTab(id);
409         requestLayout();
410         return this;
411     }
412     /// add new tab by id and label string
413     TabHost addTab(Widget widget, dstring label, string iconId = null, bool enableCloseButton = false) {
414         assert(_tabControl !is null, "No TabControl set for TabHost");
415         assert(widget.id !is null, "ID for tab host page is mandatory");
416         assert(_children.indexOf(id) == -1, "duplicate ID for tab host page");
417         _tabControl.addTab(widget.id, label, iconId, enableCloseButton);
418         addChild(widget);
419         return this;
420     }
421     /// add new tab by id and label string resource id
422     TabHost addTab(Widget widget, string labelResourceId, string iconId = null, bool enableCloseButton = false) {
423         assert(_tabControl !is null, "No TabControl set for TabHost");
424         assert(widget.id !is null, "ID for tab host page is mandatory");
425         assert(_children.indexOf(id) == -1, "duplicate ID for tab host page");
426         _tabControl.addTab(widget.id, labelResourceId, iconId, enableCloseButton);
427         addChild(widget);
428         return this;
429     }
430     /// select tab
431     void selectTab(string ID) {
432         int index = _tabControl.tabIndex(ID);
433         if (index != -1) {
434             _tabControl.selectTab(index);
435         }
436     }
437 //    /// request relayout of widget and its children
438 //    override void requestLayout() {
439 //		Log.d("TabHost.requestLayout called");
440 //		super.requestLayout();
441 //        //_needLayout = true;
442 //    }
443 //    /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
444 //    override void layout(Rect rc) {
445 //		Log.d("TabHost.layout() called");
446 //		super.layout(rc);
447 //		Log.d("after layout(): needLayout = ", needLayout);
448 //	}
449 
450 }
451 
452 /// compound widget - contains from TabControl widget (tabs header) and TabHost (content pages)
453 class TabWidget : VerticalLayout, TabHandler {
454     protected TabControl _tabControl;
455     protected TabHost _tabHost;
456     this(string ID) {
457         super(ID);
458         _tabControl = new TabControl("TAB_CONTROL");
459         _tabHost = new TabHost("TAB_HOST", _tabControl);
460         styleId = "TAB_WIDGET";
461         addChild(_tabControl);
462         addChild(_tabHost);
463     }
464 
465     protected void delegate(string newActiveTabId, string previousTabId) _onTabChanged;
466     @property void delegate(string newActiveTabId, string previousTabId) onTabChangedListener() { return _onTabChanged; }
467     @property TabWidget onTabChangedListener(void delegate(string newActiveTabId, string previousTabId) listener) { _onTabChanged = listener; return this; }
468 
469     protected override void onTabChanged(string newActiveTabId, string previousTabId) {
470         // forward to listener
471         if (_onTabChanged !is null)
472             _onTabChanged(newActiveTabId, previousTabId);
473     }
474 
475     /// add new tab by id and label string resource id
476     TabWidget addTab(Widget widget, string labelResourceId, string iconId = null, bool enableCloseButton = false) {
477         _tabHost.addTab(widget, labelResourceId, iconId, enableCloseButton);
478         return this;
479     }
480     /// add new tab by id and label (raw value)
481     TabWidget addTab(Widget widget, dstring label, string iconId = null, bool enableCloseButton = false) {
482         _tabHost.addTab(widget, label, iconId, enableCloseButton);
483         return this;
484     }
485     /// remove tab by id
486     TabWidget removeTab(string id) {
487         _tabHost.removeTab(id);
488         requestLayout();
489         return this;
490     }
491     /// select tab
492     void selectTab(string ID) {
493         _tabHost.selectTab(ID);
494     }
495 //    /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
496 //    override void layout(Rect rc) {
497 //		Log.d("TabWidget.layout() called");
498 //		super.layout(rc);
499 //		Log.d("after layout(): tabhost.needLayout = ", _tabHost.needLayout);
500 //	}
501 }