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 }