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