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