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