1 // Written by Christopher E. Miller 2 // See the included license.txt for copyright and license details. 3 4 module dfl.tabcontrol; 5 6 import core.sys.windows.windows; 7 import core.sys.windows.commctrl; 8 9 import dfl.application; 10 import dfl.base; 11 import dfl.collections; 12 import dfl.control; 13 import dfl.drawing; 14 import dfl.event; 15 import dfl.exception; 16 import dfl.internal.dlib; 17 import dfl.panel; 18 19 private extern (Windows) void _initTabcontrol(); 20 21 class TabPage : Panel { 22 23 this(Dstring tabText) { 24 this(); 25 26 this.text = tabText; 27 } 28 29 /+ 30 31 this(Object v) { // package 32 this(getObjectString(v)); 33 } 34 +/ 35 36 this() { 37 Application.ppin(cast(void*) this); 38 39 ctrlStyle |= ControlStyles.CONTAINER_CONTROL; 40 41 wstyle &= ~WS_VISIBLE; 42 cbits &= ~CBits.VISIBLE; 43 } 44 45 override Dstring toString() { 46 return text; 47 } 48 49 alias opEquals = Control.opEquals; 50 51 override Dequ opEquals(Object o) { 52 return text == getObjectString(o); 53 } 54 55 Dequ opEquals(Dstring val) { 56 return text == val; 57 } 58 59 alias opCmp = Control.opCmp; 60 61 override int opCmp(Object o) { 62 return stringICmp(text, getObjectString(o)); 63 } 64 65 int opCmp(Dstring val) { 66 return stringICmp(text, val); 67 } 68 69 // imageIndex 70 71 override @property void text(Dstring newText) { 72 // Note: this probably causes toStringz() to be called twice, 73 // allocating 2 of the same string. 74 75 super.text = newText; 76 77 if (created) { 78 TabControl tc; 79 tc = cast(TabControl) parent; 80 if (tc) { 81 tc.updateTabText(this, newText); 82 } 83 } 84 } 85 86 alias text = Panel.text; // Overload with Panel.text. 87 88 /+ 89 final @property void toolTipText(Dstring ttt) { 90 // TODO: ... 91 } 92 93 94 final @property Dstring toolTipText() { 95 // TODO: ... 96 return null; 97 } 98 +/ 99 100 /+ package +/ /+ protected +/ override int _rtype() { // package 101 return 4; 102 } 103 104 protected override void setBoundsCore(int x, int y, int width, int height, 105 BoundsSpecified specified) { 106 assert(0); // Cannot set bounds of TabPage; it is done automatically. 107 } 108 109 package final @property void realBounds(Rect r) { 110 // DMD 0.124: if I don't put this here, super.setBoundsCore ends up calling setBoundsCore instead of super.setBoundsCore. 111 void delegate(int, int, int, int, BoundsSpecified) _foo = &setBoundsCore; 112 113 super.setBoundsCore(r.x, r.y, r.width, r.height, BoundsSpecified.ALL); 114 } 115 116 protected override void setVisibleCore(bool byes) { 117 assert(0); // Cannot set visibility of TabPage; it is done automatically. 118 } 119 120 package final @property void realVisible(bool byes) { 121 // DMD 0.124: if I don't put this here, super.setVisibleCore ends up calling setVisibleCore instead of super.setVisibleCore. 122 void delegate(bool byes) _foo = &setVisibleCore; 123 124 super.setVisibleCore(byes); 125 } 126 } 127 128 package union TcItem { 129 TC_ITEMW tciw; 130 TC_ITEMA tcia; 131 struct { 132 UINT mask; 133 UINT lpReserved1; 134 UINT lpReserved2; 135 private void* pszText; 136 int cchTextMax; 137 int iImage; 138 LPARAM lParam; 139 } 140 } 141 142 class TabPageCollection { 143 protected this(TabControl owner) 144 in { 145 assert(owner.tchildren is null); 146 } 147 body { 148 tc = owner; 149 } 150 151 private: 152 153 TabControl tc; 154 TabPage[] _pages = null; 155 156 void doPages() 157 in { 158 assert(created); 159 } 160 body { 161 Rect area; 162 area = tc.displayRectangle; 163 164 Message m; 165 m.hWnd = tc.handle; 166 167 // Note: duplicate code. 168 //TC_ITEMA tci; 169 TcItem tci; 170 if (dfl.internal.utf.useUnicode) { 171 m.msg = TCM_INSERTITEMW; // <-- 172 foreach (int i, TabPage page; _pages) { 173 // TODO: TCIF_RTLREADING flag based on rightToLeft property. 174 tci.mask = TCIF_TEXT | TCIF_PARAM; 175 tci.tciw.pszText = cast(typeof(tci.tciw.pszText)) dfl.internal.utf.toUnicodez( 176 page.text); // <-- 177 static assert(tci.lParam.sizeof >= (void*).sizeof); 178 tci.lParam = cast(LPARAM) cast(void*) page; 179 180 m.wParam = i; 181 m.lParam = cast(LPARAM)&tci.tciw; 182 tc.prevWndProc(m); 183 assert(cast(int) m.result != -1); 184 } 185 } else { 186 m.msg = TCM_INSERTITEMA; // <-- 187 foreach (int i, TabPage page; _pages) { 188 // TODO: TCIF_RTLREADING flag based on rightToLeft property. 189 tci.mask = TCIF_TEXT | TCIF_PARAM; 190 tci.tcia.pszText = cast(typeof(tci.tcia.pszText)) dfl.internal.utf.toAnsiz(page.text); // <-- 191 static assert(tci.lParam.sizeof >= (void*).sizeof); 192 tci.lParam = cast(LPARAM) cast(void*) page; 193 194 m.wParam = i; 195 m.lParam = cast(LPARAM)&tci.tcia; 196 tc.prevWndProc(m); 197 assert(cast(int) m.result != -1); 198 } 199 } 200 } 201 202 package final @property bool created() { 203 return tc && tc.created(); 204 } 205 206 void _added(size_t idx, TabPage val) { 207 if (val.parent) { 208 TabControl tc; 209 tc = cast(TabControl) val.parent; 210 if (tc && tc.tabPages.indexOf(val) != -1) { 211 throw new DflException("TabPage already has a parent"); 212 } 213 } 214 215 //val.realVisible = false; 216 assert(val.visible == false); 217 assert(!(tc is null)); 218 val.parent = tc; 219 220 if (created) { 221 Message m; 222 //TC_ITEMA tci; 223 TcItem tci; 224 // TODO: TCIF_RTLREADING flag based on rightToLeft property. 225 tci.mask = TCIF_TEXT | TCIF_PARAM; 226 static assert(tci.lParam.sizeof >= (void*).sizeof); 227 tci.lParam = cast(LPARAM) cast(void*) val; 228 if (dfl.internal.utf.useUnicode) { 229 tci.tciw.pszText = cast(typeof(tci.tciw.pszText)) dfl.internal.utf.toUnicodez(val.text); 230 m = Message(tc.handle, TCM_INSERTITEMW, idx, cast(LPARAM)&tci.tciw); 231 } else { 232 tci.tcia.pszText = cast(typeof(tci.tcia.pszText)) dfl.internal.utf.toAnsiz(val.text); 233 m = Message(tc.handle, TCM_INSERTITEMA, idx, cast(LPARAM)&tci.tcia); 234 } 235 tc.prevWndProc(m); 236 assert(cast(int) m.result != -1); 237 238 if (tc.selectedTab is val) { 239 //val.realVisible = true; 240 tc.tabToFront(val); 241 } 242 } 243 } 244 245 void _removed(size_t idx, TabPage val) { 246 if (size_t.max == idx) { // Clear all. 247 if (created) { 248 Message m; 249 m = Message(tc.handle, TCM_DELETEALLITEMS, 0, 0); 250 tc.prevWndProc(m); 251 } 252 } else { 253 //val.parent = null; // Can't do that. 254 255 if (created) { 256 Message m; 257 m = Message(tc.handle, TCM_DELETEITEM, idx, 0); 258 tc.prevWndProc(m); 259 260 // Hide this one. 261 val.realVisible = false; 262 263 // Show next visible. 264 val = tc.selectedTab; 265 if (val) { 266 tc.tabToFront(val); 267 } 268 } 269 } 270 } 271 272 public: 273 274 mixin ListWrapArray!(TabPage, _pages, _blankListCallback!(TabPage), _added, 275 _blankListCallback!(TabPage), _removed, true, false, false, true); // CLEAR_EACH 276 } 277 278 enum TabAlignment : ubyte { 279 TOP, 280 BOTTOM, 281 LEFT, 282 RIGHT, 283 } 284 285 enum TabAppearance : ubyte { 286 NORMAL, 287 BUTTONS, 288 FLAT_BUTTONS, 289 } 290 291 enum TabDrawMode : ubyte { 292 NORMAL, 293 OWNER_DRAW_FIXED, 294 } 295 296 class TabControlBase : ControlSuperClass { 297 this() { 298 _initTabcontrol(); 299 300 wstyle |= WS_TABSTOP; 301 ctrlStyle |= ControlStyles.SELECTABLE | ControlStyles.CONTAINER_CONTROL; 302 wclassStyle = tabcontrolClassStyle; 303 } 304 305 final @property void drawMode(TabDrawMode dm) { 306 switch (dm) { 307 case TabDrawMode.OWNER_DRAW_FIXED: 308 _style(wstyle | TCS_OWNERDRAWFIXED); 309 break; 310 311 case TabDrawMode.NORMAL: 312 _style(wstyle & ~TCS_OWNERDRAWFIXED); 313 break; 314 315 default: 316 assert(0); 317 } 318 319 _crecreate(); 320 } 321 322 final @property TabDrawMode drawMode() { 323 if (wstyle & TCS_OWNERDRAWFIXED) { 324 return TabDrawMode.OWNER_DRAW_FIXED; 325 } 326 return TabDrawMode.NORMAL; 327 } 328 329 override @property Rect displayRectangle() { 330 if (!created) { 331 return super.displayRectangle(); // Hack? 332 } else { 333 RECT drr; 334 Message m; 335 drr.left = 0; 336 drr.top = 0; 337 drr.right = clientSize.width; 338 drr.bottom = clientSize.height; 339 m = Message(hwnd, TCM_ADJUSTRECT, FALSE, cast(LPARAM)&drr); 340 prevWndProc(m); 341 return Rect(&drr); 342 } 343 } 344 345 protected override @property Size defaultSize() { 346 return Size(200, 200); // ? 347 } 348 349 final Rect getTabRect(int i) { 350 Rect result; 351 352 if (created) { 353 RECT rt; 354 Message m; 355 m = Message(hwnd, TCM_GETITEMRECT, cast(WPARAM) i, cast(LPARAM)&rt); 356 prevWndProc(m); 357 if (!m.result) { 358 goto rtfail; 359 } 360 result = Rect(&rt); 361 } else { 362 rtfail: with (result) { 363 x = 0; 364 y = 0; 365 width = 0; 366 height = 0; 367 } 368 } 369 370 return result; 371 } 372 373 // drawItem event. 374 //EventHandler selectedIndexChanged; 375 Event!(TabControlBase, EventArgs) selectedIndexChanged; 376 //CancelEventHandler selectedIndexChanging; 377 Event!(TabControlBase, CancelEventArgs) selectedIndexChanging; 378 379 protected override void createParams(ref CreateParams cp) { 380 super.createParams(cp); 381 382 cp.className = TABCONTROL_CLASSNAME; 383 } 384 385 protected void onSelectedIndexChanged(EventArgs ea) { 386 selectedIndexChanged(this, ea); 387 } 388 389 protected void onSelectedIndexChanging(CancelEventArgs ea) { 390 selectedIndexChanging(this, ea); 391 } 392 393 protected override void prevWndProc(ref Message msg) { 394 //msg.result = CallWindowProcA(tabcontrolPrevWndProc, msg.hWnd, msg.msg, msg.wParam, msg.lParam); 395 msg.result = dfl.internal.utf.callWindowProc(tabcontrolPrevWndProc, 396 msg.hWnd, msg.msg, msg.wParam, msg.lParam); 397 } 398 399 protected override void wndProc(ref Message m) { 400 // TODO: support the tab control messages. 401 402 switch (m.msg) { 403 /+ 404 case WM_SETFOCUS: 405 _exStyle(_exStyle() | WS_EX_CONTROLPARENT); 406 break; 407 408 case WM_KILLFOCUS: 409 _exStyle(_exStyle() & ~WS_EX_CONTROLPARENT); 410 break; 411 +/ 412 413 case TCM_DELETEALLITEMS: 414 m.result = FALSE; 415 return; 416 417 case TCM_DELETEITEM: 418 m.result = FALSE; 419 return; 420 421 case TCM_INSERTITEMA: 422 case TCM_INSERTITEMW: 423 m.result = -1; 424 return; 425 426 //case TCM_REMOVEIMAGE: 427 // return; 428 429 //case TCM_SETIMAGELIST: 430 // m.result = cast(LRESULT)null; 431 // return; 432 433 case TCM_SETITEMA: 434 case TCM_SETITEMW: 435 m.result = FALSE; 436 return; 437 438 case TCM_SETITEMEXTRA: 439 m.result = FALSE; 440 return; 441 442 case TCM_SETITEMSIZE: 443 m.result = 0; 444 return; 445 446 case TCM_SETPADDING: 447 return; 448 449 case TCM_SETTOOLTIPS: 450 return; 451 452 default: 453 } 454 455 super.wndProc(m); 456 } 457 458 protected override void onReflectedMessage(ref Message m) { 459 super.onReflectedMessage(m); 460 461 TabPage page; 462 NMHDR* nmh; 463 nmh = cast(NMHDR*) m.lParam; 464 465 switch (nmh.code) { 466 case TCN_SELCHANGE: 467 onSelectedIndexChanged(EventArgs.empty); 468 break; 469 470 case TCN_SELCHANGING: { 471 scope CancelEventArgs ea = new CancelEventArgs; 472 onSelectedIndexChanging(ea); 473 if (ea.cancel) { 474 m.result = TRUE; // Prevent change. 475 return; 476 } 477 } 478 m.result = FALSE; // Allow change. 479 return; 480 481 default: 482 } 483 } 484 } 485 486 class TabControl : TabControlBase { 487 this() { 488 tchildren = new TabPageCollection(this); 489 _pad = Point(6, 3); 490 } 491 492 final @property void alignment(TabAlignment talign) { 493 switch (talign) { 494 case TabAlignment.TOP: 495 _style(wstyle & ~(TCS_VERTICAL | TCS_RIGHT | TCS_BOTTOM)); 496 break; 497 498 case TabAlignment.BOTTOM: 499 _style((wstyle & ~(TCS_VERTICAL | TCS_RIGHT)) | TCS_BOTTOM); 500 break; 501 502 case TabAlignment.LEFT: 503 _style((wstyle & ~(TCS_BOTTOM | TCS_RIGHT)) | TCS_VERTICAL); 504 break; 505 506 case TabAlignment.RIGHT: 507 _style((wstyle & ~TCS_BOTTOM) | TCS_VERTICAL | TCS_RIGHT); 508 break; 509 510 default: 511 assert(0); 512 } 513 514 // Display rectangle changed. 515 516 if (created && visible) { 517 invalidate(true); // Update children too ? 518 519 TabPage page; 520 page = selectedTab; 521 if (page) { 522 page.realBounds = displayRectangle; 523 } 524 } 525 } 526 527 final @property TabAlignment alignment() { 528 // Note: TCS_RIGHT and TCS_BOTTOM are the same flag. 529 530 if (wstyle & TCS_VERTICAL) { 531 if (wstyle & TCS_RIGHT) { 532 return TabAlignment.RIGHT; 533 } 534 return TabAlignment.LEFT; 535 } else { 536 if (wstyle & TCS_BOTTOM) { 537 return TabAlignment.BOTTOM; 538 } 539 return TabAlignment.TOP; 540 } 541 } 542 543 final @property void appearance(TabAppearance tappear) { 544 switch (tappear) { 545 case TabAppearance.NORMAL: 546 _style(wstyle & ~(TCS_BUTTONS | TCS_FLATBUTTONS)); 547 break; 548 549 case TabAppearance.BUTTONS: 550 _style((wstyle & ~TCS_FLATBUTTONS) | TCS_BUTTONS); 551 break; 552 553 case TabAppearance.FLAT_BUTTONS: 554 _style(wstyle | TCS_BUTTONS | TCS_FLATBUTTONS); 555 break; 556 557 default: 558 assert(0); 559 } 560 561 if (created && visible) { 562 invalidate(false); 563 564 TabPage page; 565 page = selectedTab; 566 if (page) { 567 page.realBounds = displayRectangle; 568 } 569 } 570 } 571 572 final @property TabAppearance appearance() { 573 if (wstyle & TCS_FLATBUTTONS) { 574 return TabAppearance.FLAT_BUTTONS; 575 } 576 if (wstyle & TCS_BUTTONS) { 577 return TabAppearance.BUTTONS; 578 } 579 return TabAppearance.NORMAL; 580 } 581 582 final @property void padding(Point pad) { 583 if (created) { 584 SendMessageA(hwnd, TCM_SETPADDING, 0, MAKELPARAM(pad.x, pad.y)); 585 586 TabPage page; 587 page = selectedTab; 588 if (page) { 589 page.realBounds = displayRectangle; 590 } 591 } 592 593 _pad = pad; 594 } 595 596 final @property Point padding() { 597 return _pad; 598 } 599 600 final @property TabPageCollection tabPages() { 601 return tchildren; 602 } 603 604 final @property void multiline(bool byes) { 605 if (byes) { 606 _style(_style() | TCS_MULTILINE); 607 } else { 608 _style(_style() & ~TCS_MULTILINE); 609 } 610 611 TabPage page; 612 page = selectedTab; 613 if (page) { 614 page.realBounds = displayRectangle; 615 } 616 } 617 618 final @property bool multiline() { 619 return (_style() & TCS_MULTILINE) != 0; 620 } 621 622 final @property int rowCount() { 623 if (!created || !multiline) { 624 return 0; 625 } 626 Message m; 627 m = Message(hwnd, TCM_GETROWCOUNT, 0, 0); 628 prevWndProc(m); 629 return cast(int) m.result; 630 } 631 632 final @property int tabCount() { 633 return tchildren._pages.length; 634 } 635 636 final @property void selectedIndex(int i) { 637 if (!created || !tchildren._pages.length) { 638 return; 639 } 640 641 TabPage curpage; 642 curpage = selectedTab; 643 if (curpage is tchildren._pages[i]) { 644 return; // Already selected. 645 } 646 curpage.realVisible = false; 647 648 SendMessageA(hwnd, TCM_SETCURSEL, cast(WPARAM) i, 0); 649 tabToFront(tchildren._pages[i]); 650 } 651 652 // Returns -1 if there are no tabs selected. 653 final @property int selectedIndex() { 654 if (!created || !tchildren._pages.length) { 655 return -1; 656 } 657 Message m; 658 m = Message(hwnd, TCM_GETCURSEL, 0, 0); 659 prevWndProc(m); 660 return cast(int) m.result; 661 } 662 663 final @property void selectedTab(TabPage page) { 664 int i; 665 i = tabPages.indexOf(page); 666 if (-1 != i) { 667 selectedIndex = i; 668 } 669 } 670 671 final @property TabPage selectedTab() { 672 int i; 673 i = selectedIndex; 674 if (-1 == i) { 675 return null; 676 } 677 return tchildren._pages[i]; 678 } 679 680 /+ 681 682 final @property void showToolTips(bool byes) { 683 if(byes) { 684 _style(_style() | TCS_TOOLTIPS); 685 } else { 686 _style(_style() & ~TCS_TOOLTIPS); 687 } 688 } 689 690 691 final @property bool showToolTips() { 692 return (_style() & TCS_TOOLTIPS) != 0; 693 } 694 +/ 695 696 protected override void onHandleCreated(EventArgs ea) { 697 super.onHandleCreated(ea); 698 699 SendMessageA(hwnd, TCM_SETPADDING, 0, MAKELPARAM(_pad.x, _pad.y)); 700 701 tchildren.doPages(); 702 703 // Bring selected tab to front. 704 if (tchildren._pages.length) { 705 int i; 706 i = selectedIndex; 707 if (-1 != i) { 708 tabToFront(tchildren._pages[i]); 709 } 710 } 711 } 712 713 protected override void onLayout(LayoutEventArgs ea) { 714 if (tchildren._pages.length) { 715 int i; 716 i = selectedIndex; 717 if (-1 != i) { 718 tchildren._pages[i].realBounds = displayRectangle; 719 //assert(tchildren._pages[i].bounds == displayRectangle); 720 } 721 } 722 723 //super.onLayout(ea); // Tab control shouldn't even have other controls on it. 724 super.onLayout(ea); // Should call it for consistency. Ideally it just checks handlers.length == 0 and does nothing. 725 } 726 727 /+ 728 protected override void wndProc(ref Message m) { 729 // TODO: support the tab control messages. 730 731 switch(m.msg) { 732 /+ // Now handled in onLayout(). 733 case WM_WINDOWPOSCHANGED: { 734 WINDOWPOS* wp; 735 wp = cast(WINDOWPOS*)m.lParam; 736 737 if(!(wp.flags & SWP_NOSIZE) || (wp.flags & SWP_FRAMECHANGED)) { 738 if(tchildren._pages.length) { 739 int i; 740 i = selectedIndex; 741 if(-1 != i) { 742 tchildren._pages[i].realBounds = displayRectangle; 743 //assert(tchildren._pages[i].bounds == displayRectangle); 744 } 745 } 746 } 747 } 748 break; 749 +/ 750 751 default: 752 } 753 754 super.wndProc(m); 755 } 756 +/ 757 758 protected override void onReflectedMessage(ref Message m) { 759 TabPage page; 760 NMHDR* nmh; 761 nmh = cast(NMHDR*) m.lParam; 762 763 switch (nmh.code) { 764 case TCN_SELCHANGE: 765 page = selectedTab; 766 if (page) { 767 tabToFront(page); 768 } 769 super.onReflectedMessage(m); 770 break; 771 772 case TCN_SELCHANGING: 773 super.onReflectedMessage(m); 774 if (!m.result) { // Allowed. 775 page = selectedTab; 776 if (page) { 777 page.realVisible = false; 778 } 779 } 780 return; 781 782 default: 783 super.onReflectedMessage(m); 784 } 785 } 786 787 /+ 788 /+ package +/ /+ protected +/ override int _rtype() { // package 789 return 0x20; 790 } 791 +/ 792 793 private: 794 Point _pad; 795 TabPageCollection tchildren; 796 797 void tabToFront(TabPage page) { 798 page.realBounds = displayRectangle; 799 //page.realVisible = true; 800 SetWindowPos(page.handle, HWND_TOP, 0, 0, 0, 0, /+ SWP_NOACTIVATE | +/ SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); 801 assert(page.visible == true); 802 803 /+ 804 // Make sure the previous tab isn't still focused. 805 // Will "steal" focus if done programatically. 806 SetFocus(handle); 807 //SetFocus(page.handle); 808 +/ 809 } 810 811 void updateTabText(TabPage page, Dstring newText) 812 in { 813 assert(created); 814 } 815 body { 816 int i; 817 i = tabPages.indexOf(page); 818 assert(-1 != i); 819 820 //TC_ITEMA tci; 821 TcItem tci; 822 tci.mask = TCIF_TEXT; 823 Message m; 824 if (dfl.internal.utf.useUnicode) { 825 tci.tciw.pszText = cast(typeof(tci.tciw.pszText)) dfl.internal.utf.toUnicodez(newText); 826 m = Message(hwnd, TCM_SETITEMW, cast(WPARAM) i, cast(LPARAM)&tci.tciw); 827 } else { 828 tci.tcia.pszText = cast(typeof(tci.tcia.pszText)) dfl.internal.utf.toAnsiz(newText); 829 m = Message(hwnd, TCM_SETITEMA, cast(WPARAM) i, cast(LPARAM)&tci.tcia); 830 } 831 prevWndProc(m); 832 833 // Updating a tab's text could cause tab rows to be adjusted, 834 // so update the selected tab's area. 835 page = selectedTab; 836 if (page) { 837 page.realBounds = displayRectangle; 838 } 839 } 840 }