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