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