1 // Written by Christopher E. Miller 2 // See the included license.txt for copyright and license details. 3 4 5 /// 6 module dfl.menu; 7 8 private import dfl.internal.dlib; 9 10 private import dfl.internal.winapi, dfl.control, dfl.base, dfl.event; 11 private import dfl.internal.utf, dfl.drawing, dfl.application, dfl.collections; 12 13 14 version(DFL_NO_MENUS) 15 { 16 } 17 else 18 { 19 /// 20 class ContextMenu: Menu // docmain 21 { 22 /// 23 final void show(Control control, Point pos) 24 { 25 SetForegroundWindow(control.handle); 26 TrackPopupMenu(hmenu, TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_RIGHTBUTTON, 27 pos.x, pos.y, 0, control.handle, null); 28 } 29 30 31 //EventHandler popup; 32 Event!(ContextMenu, EventArgs) popup; /// 33 34 35 // Used internally. 36 this(HMENU hmenu, bool owned = true) 37 { 38 super(hmenu, owned); 39 40 _init(); 41 } 42 43 44 this() 45 { 46 super(CreatePopupMenu()); 47 48 _init(); 49 } 50 51 52 ~this() 53 { 54 Application.removeMenu(this); 55 56 debug(APP_PRINT) 57 cprintf("~ContextMenu\n"); 58 } 59 60 61 protected override void onReflectedMessage(ref Message m) 62 { 63 super.onReflectedMessage(m); 64 65 switch(m.msg) 66 { 67 case WM_INITMENU: 68 assert(cast(HMENU)m.wParam == handle); 69 70 //onPopup(EventArgs.empty); 71 popup(this, EventArgs.empty); 72 break; 73 74 default: 75 } 76 } 77 78 79 private: 80 void _init() 81 { 82 Application.addContextMenu(this); 83 } 84 } 85 86 87 /// 88 class MenuItem: Menu // docmain 89 { 90 /// 91 final @property void text(Dstring txt) // setter 92 { 93 if(!menuItems.length && txt == SEPARATOR_TEXT) 94 { 95 _type(_type() | MFT_SEPARATOR); 96 } 97 else 98 { 99 if(mparent) 100 { 101 MENUITEMINFOA mii; 102 103 if(fType & MFT_SEPARATOR) 104 fType = ~MFT_SEPARATOR; 105 mii.cbSize = mii.sizeof; 106 mii.fMask = MIIM_TYPE | MIIM_STATE; // Not setting the state can cause implicit disabled/gray if the text was empty. 107 mii.fType = fType; 108 mii.fState = fState; 109 //mii.dwTypeData = stringToStringz(txt); 110 111 mparent._setInfo(mid, false, &mii, txt); 112 } 113 } 114 115 mtext = txt; 116 } 117 118 /// ditto 119 final @property Dstring text() // getter 120 { 121 // if(mparent) fetch text ? 122 return mtext; 123 } 124 125 126 /// 127 final @property void parent(Menu m) // setter 128 { 129 m.menuItems.add(this); 130 } 131 132 /// ditto 133 final @property Menu parent() // getter 134 { 135 return mparent; 136 } 137 138 139 package final void _setParent(Menu newParent) 140 { 141 assert(!mparent); 142 mparent = newParent; 143 144 if(cast(size_t)mindex > mparent.menuItems.length) 145 mindex = mparent.menuItems.length; 146 147 _setParent(); 148 } 149 150 151 private void _setParent() 152 { 153 MENUITEMINFOA mii; 154 MenuItem miparent; 155 156 mii.cbSize = mii.sizeof; 157 mii.fMask = MIIM_TYPE | MIIM_STATE | MIIM_ID | MIIM_SUBMENU; 158 mii.fType = fType; 159 mii.fState = fState; 160 mii.wID = mid; 161 mii.hSubMenu = handle; 162 //if(!(fType & MFT_SEPARATOR)) 163 // mii.dwTypeData = stringToStringz(mtext); 164 miparent = cast(MenuItem)mparent; 165 if(miparent && !miparent.hmenu) 166 { 167 miparent.hmenu = CreatePopupMenu(); 168 169 if(miparent.parent() && miparent.parent.hmenu) 170 { 171 MENUITEMINFOA miiPopup; 172 173 miiPopup.cbSize = miiPopup.sizeof; 174 miiPopup.fMask = MIIM_SUBMENU; 175 miiPopup.hSubMenu = miparent.hmenu; 176 miparent.parent._setInfo(miparent._menuID, false, &miiPopup); 177 } 178 } 179 mparent._insert(mindex, true, &mii, (fType & MFT_SEPARATOR) ? null : mtext); 180 } 181 182 183 package final void _unsetParent() 184 { 185 assert(mparent); 186 assert(mparent.menuItems.length > 0); 187 assert(mparent.hmenu); 188 189 // Last child menu item, make the parent non-popup now. 190 if(mparent.menuItems.length == 1) 191 { 192 MenuItem miparent; 193 194 miparent = cast(MenuItem)mparent; 195 if(miparent && miparent.hmenu) 196 { 197 MENUITEMINFOA miiPopup; 198 199 miiPopup.cbSize = miiPopup.sizeof; 200 miiPopup.fMask = MIIM_SUBMENU; 201 miiPopup.hSubMenu = null; 202 miparent.parent._setInfo(miparent._menuID, false, &miiPopup); 203 204 miparent.hmenu = null; 205 } 206 } 207 208 mparent = null; 209 210 if(!Menu._compat092) 211 { 212 mindex = -1; 213 } 214 } 215 216 217 /// 218 final @property void barBreak(bool byes) // setter 219 { 220 if(byes) 221 _type(_type() | MFT_MENUBARBREAK); 222 else 223 _type(_type() & ~MFT_MENUBARBREAK); 224 } 225 226 /// ditto 227 final @property bool barBreak() // getter 228 { 229 return (_type() & MFT_MENUBARBREAK) != 0; 230 } 231 232 233 // Can't be break(). 234 235 /// 236 final @property void breakItem(bool byes) // setter 237 { 238 if(byes) 239 _type(_type() | MFT_MENUBREAK); 240 else 241 _type(_type() & ~MFT_MENUBREAK); 242 } 243 244 /// ditto 245 final @property bool breakItem() // getter 246 { 247 return (_type() & MFT_MENUBREAK) != 0; 248 } 249 250 251 /// 252 final @property void checked(bool byes) // setter 253 { 254 if(byes) 255 _state(_state() | MFS_CHECKED); 256 else 257 _state(_state() & ~MFS_CHECKED); 258 } 259 260 /// ditto 261 final @property bool checked() // getter 262 { 263 return (_state() & MFS_CHECKED) != 0; 264 } 265 266 267 /// 268 final @property void defaultItem(bool byes) // setter 269 { 270 if(byes) 271 _state(_state() | MFS_DEFAULT); 272 else 273 _state(_state() & ~MFS_DEFAULT); 274 } 275 276 /// ditto 277 final @property bool defaultItem() // getter 278 { 279 return (_state() & MFS_DEFAULT) != 0; 280 } 281 282 283 /// 284 final @property void enabled(bool byes) // setter 285 { 286 if(byes) 287 _state(_state() & ~MFS_GRAYED); 288 else 289 _state(_state() | MFS_GRAYED); 290 } 291 292 /// ditto 293 final @property bool enabled() // getter 294 { 295 return (_state() & MFS_GRAYED) == 0; 296 } 297 298 299 /// 300 final @property void index(int idx) // setter 301 {// Note: probably fails when the parent exists because mparent is still set and menuItems.insert asserts it's null. 302 if(mparent) 303 { 304 if(cast(uint)idx > mparent.menuItems.length) 305 throw new DflException("Invalid menu index"); 306 307 //RemoveMenu(mparent.handle, mid, MF_BYCOMMAND); 308 mparent._remove(mid, MF_BYCOMMAND); 309 mparent.menuItems._delitem(mindex); 310 311 /+ 312 mindex = idx; 313 _setParent(); 314 mparent.menuItems._additem(this); 315 +/ 316 mparent.menuItems.insert(idx, this); 317 } 318 319 if(Menu._compat092) 320 { 321 mindex = idx; 322 } 323 } 324 325 /// ditto 326 final @property int index() // getter 327 { 328 return mindex; 329 } 330 331 332 override @property bool isParent() // getter 333 { 334 return handle != null; // ? 335 } 336 337 338 deprecated final @property void mergeOrder(int ord) // setter 339 { 340 //mergeord = ord; 341 } 342 343 deprecated final @property int mergeOrder() // getter 344 { 345 //return mergeord; 346 return 0; 347 } 348 349 350 // TODO: mergeType(). 351 352 353 /// 354 // Returns a NUL char if none. 355 final @property char mnemonic() // getter 356 { 357 bool singleAmp = false; 358 359 foreach(char ch; mtext) 360 { 361 if(singleAmp) 362 { 363 if(ch == '&') 364 singleAmp = false; 365 else 366 return ch; 367 } 368 else 369 { 370 if(ch == '&') 371 singleAmp = true; 372 } 373 } 374 375 return 0; 376 } 377 378 379 /+ 380 // TODO: implement owner drawn menus. 381 382 final @property void ownerDraw(bool byes) // setter 383 { 384 385 } 386 387 final @property bool ownerDraw() // getter 388 { 389 390 } 391 +/ 392 393 394 /// 395 final @property void radioCheck(bool byes) // setter 396 { 397 auto par = parent; 398 auto pidx = index; 399 if(par) 400 par.menuItems._removing(pidx, this); 401 402 if(byes) 403 //_type(_type() | MFT_RADIOCHECK); 404 fType |= MFT_RADIOCHECK; 405 else 406 //_type(_type() & ~MFT_RADIOCHECK); 407 fType &= ~MFT_RADIOCHECK; 408 409 if(par) 410 par.menuItems._added(pidx, this); 411 } 412 413 /// ditto 414 final @property bool radioCheck() // getter 415 { 416 return (_type() & MFT_RADIOCHECK) != 0; 417 } 418 419 420 // TODO: shortcut(), showShortcut(). 421 422 423 /+ 424 // TODO: need to fake this ? 425 426 final @property void visible(bool byes) // setter 427 { 428 // ? 429 mvisible = byes; 430 } 431 432 final @property bool visible() // getter 433 { 434 return mvisible; 435 } 436 +/ 437 438 439 /// 440 final void performClick() 441 { 442 onClick(EventArgs.empty); 443 } 444 445 446 /// 447 final void performSelect() 448 { 449 onSelect(EventArgs.empty); 450 } 451 452 453 // Used internally. 454 this(HMENU hmenu, bool owned = true) // package 455 { 456 super(hmenu, owned); 457 _init(); 458 } 459 460 461 /// 462 this(MenuItem[] items) 463 { 464 if(items.length) 465 { 466 HMENU hm = CreatePopupMenu(); 467 super(hm); 468 } 469 else 470 { 471 super(); 472 } 473 _init(); 474 475 menuItems.addRange(items); 476 } 477 478 /// ditto 479 this(Dstring text) 480 { 481 _init(); 482 483 this.text = text; 484 } 485 486 /// ditto 487 this(Dstring text, MenuItem[] items) 488 { 489 if(items.length) 490 { 491 HMENU hm = CreatePopupMenu(); 492 super(hm); 493 } 494 else 495 { 496 super(); 497 } 498 _init(); 499 500 this.text = text; 501 502 menuItems.addRange(items); 503 } 504 505 /// ditto 506 this() 507 { 508 _init(); 509 } 510 511 512 ~this() 513 { 514 Application.removeMenu(this); 515 516 debug(APP_PRINT) 517 cprintf("~MenuItem\n"); 518 } 519 520 521 override Dstring toString() 522 { 523 return text; 524 } 525 526 527 override Dequ opEquals(Object o) 528 { 529 return text == getObjectString(o); 530 } 531 532 533 Dequ opEquals(Dstring val) 534 { 535 return text == val; 536 } 537 538 539 override int opCmp(Object o) 540 { 541 return stringICmp(text, getObjectString(o)); 542 } 543 544 545 int opCmp(Dstring val) 546 { 547 return stringICmp(text, val); 548 } 549 550 551 protected override void onReflectedMessage(ref Message m) 552 { 553 super.onReflectedMessage(m); 554 555 switch(m.msg) 556 { 557 case WM_COMMAND: 558 assert(LOWORD(m.wParam) == mid); 559 560 onClick(EventArgs.empty); 561 break; 562 563 case WM_MENUSELECT: 564 onSelect(EventArgs.empty); 565 break; 566 567 case WM_INITMENUPOPUP: 568 assert(!HIWORD(m.lParam)); 569 //assert(cast(HMENU)msg.wParam == mparent.handle); 570 assert(cast(HMENU)m.wParam == handle); 571 //assert(GetMenuItemID(mparent.handle, LOWORD(msg.lParam)) == mid); 572 573 onPopup(EventArgs.empty); 574 break; 575 576 default: 577 } 578 } 579 580 581 //EventHandler click; 582 Event!(MenuItem, EventArgs) click; /// 583 //EventHandler popup; 584 Event!(MenuItem, EventArgs) popup; /// 585 //EventHandler select; 586 Event!(MenuItem, EventArgs) select; /// 587 588 589 protected: 590 591 /// 592 final @property int menuID() // getter 593 { 594 return mid; 595 } 596 597 598 package final @property int _menuID() 599 { 600 return mid; 601 } 602 603 604 /// 605 void onClick(EventArgs ea) 606 { 607 click(this, ea); 608 } 609 610 611 /// 612 void onPopup(EventArgs ea) 613 { 614 popup(this, ea); 615 } 616 617 618 /// 619 void onSelect(EventArgs ea) 620 { 621 select(this, ea); 622 } 623 624 625 private: 626 627 int mid; // Menu ID. 628 Dstring mtext; 629 Menu mparent; 630 UINT fType = 0; // MFT_* 631 UINT fState = 0; 632 int mindex = -1; //0; 633 //int mergeord = 0; 634 635 enum SEPARATOR_TEXT = "-"; 636 637 static assert(!MFS_UNCHECKED); 638 static assert(!MFT_STRING); 639 640 641 void _init() 642 { 643 if(Menu._compat092) 644 { 645 mindex = 0; 646 } 647 648 mid = Application.addMenuItem(this); 649 } 650 651 652 @property void _type(UINT newType) // setter 653 { 654 if(mparent) 655 { 656 MENUITEMINFOA mii; 657 658 mii.cbSize = mii.sizeof; 659 mii.fMask = MIIM_TYPE; 660 mii.fType = newType; 661 662 mparent._setInfo(mid, false, &mii); 663 } 664 665 fType = newType; 666 } 667 668 669 @property UINT _type() // getter 670 { 671 // if(mparent) fetch value ? 672 return fType; 673 } 674 675 676 @property void _state(UINT newState) // setter 677 { 678 if(mparent) 679 { 680 MENUITEMINFOA mii; 681 682 mii.cbSize = mii.sizeof; 683 mii.fMask = MIIM_STATE; 684 mii.fState = newState; 685 686 mparent._setInfo(mid, false, &mii); 687 } 688 689 fState = newState; 690 } 691 692 693 @property UINT _state() // getter 694 { 695 // if(mparent) fetch value ? No: Windows seems to add disabled/gray when the text is empty. 696 return fState; 697 } 698 } 699 700 701 /// 702 abstract class Menu: DObject // docmain 703 { 704 // Retain DFL 0.9.2 compatibility. 705 deprecated static void setDFL092() 706 { 707 version(SET_DFL_092) 708 { 709 pragma(msg, "DFL: DFL 0.9.2 compatibility set at compile time"); 710 } 711 else 712 { 713 //_compat092 = true; 714 Application.setCompat(DflCompat.MENU_092); 715 } 716 } 717 718 version(SET_DFL_092) 719 private enum _compat092 = true; 720 else version(DFL_NO_COMPAT) 721 private enum _compat092 = false; 722 else 723 private static @property bool _compat092() // getter 724 { return 0 != (Application._compat & DflCompat.MENU_092); } 725 726 727 /// 728 static class MenuItemCollection 729 { 730 protected this(Menu owner) 731 { 732 _owner = owner; 733 } 734 735 736 package final void _additem(MenuItem mi) 737 { 738 // Fix indices after this point. 739 int idx; 740 idx = mi.index + 1; // Note, not orig idx. 741 if(idx < items.length) 742 { 743 foreach(MenuItem onmi; items[idx .. items.length]) 744 { 745 onmi.mindex++; 746 } 747 } 748 } 749 750 751 // Note: clear() doesn't call this. Update: does now. 752 package final void _delitem(int idx) 753 { 754 // Fix indices after this point. 755 if(idx < items.length) 756 { 757 foreach(MenuItem onmi; items[idx .. items.length]) 758 { 759 onmi.mindex--; 760 } 761 } 762 } 763 764 765 /+ 766 void insert(int index, MenuItem mi) 767 { 768 mi.mindex = index; 769 mi._setParent(_owner); 770 _additem(mi); 771 } 772 +/ 773 774 775 void add(MenuItem mi) 776 { 777 if(!Menu._compat092) 778 { 779 mi.mindex = length; 780 } 781 782 /+ 783 mi._setParent(_owner); 784 _additem(mi); 785 +/ 786 insert(mi.mindex, mi); 787 } 788 789 void add(Dstring value) 790 { 791 return add(new MenuItem(value)); 792 } 793 794 795 void addRange(MenuItem[] items) 796 { 797 if(!Menu._compat092) 798 return _wraparray.addRange(items); 799 800 foreach(MenuItem it; items) 801 { 802 insert(length, it); 803 } 804 } 805 806 void addRange(Dstring[] items) 807 { 808 if(!Menu._compat092) 809 return _wraparray.addRange(items); 810 811 foreach(Dstring it; items) 812 { 813 insert(length, it); 814 } 815 } 816 817 818 // TODO: finish. 819 820 821 package: 822 823 Menu _owner; 824 MenuItem[] items; // Kept populated so the menu can be moved around. 825 826 827 void _added(size_t idx, MenuItem val) 828 { 829 val.mindex = idx; 830 val._setParent(_owner); 831 _additem(val); 832 } 833 834 835 void _removing(size_t idx, MenuItem val) 836 { 837 if(size_t.max == idx) // Clear all. 838 { 839 } 840 else 841 { 842 val._unsetParent(); 843 //RemoveMenu(_owner.handle, val._menuID, MF_BYCOMMAND); 844 //_owner._remove(val._menuID, MF_BYCOMMAND); 845 _owner._remove(idx, MF_BYPOSITION); 846 _delitem(idx); 847 } 848 } 849 850 851 public: 852 853 mixin ListWrapArray!(MenuItem, items, 854 _blankListCallback!(MenuItem), _added, 855 _removing, _blankListCallback!(MenuItem), 856 true, false, false, 857 true) _wraparray; // CLEAR_EACH 858 } 859 860 861 // Extra. 862 deprecated final void opCatAssign(MenuItem mi) 863 { 864 menuItems.insert(menuItems.length, mi); 865 } 866 867 868 private void _init() 869 { 870 items = new MenuItemCollection(this); 871 } 872 873 874 // Menu item that isn't popup (yet). 875 protected this() 876 { 877 _init(); 878 } 879 880 881 // Used internally. 882 this(HMENU hmenu, bool owned = true) // package 883 { 884 this.hmenu = hmenu; 885 this.owned = owned; 886 887 _init(); 888 } 889 890 891 // Used internally. 892 this(HMENU hmenu, MenuItem[] items) // package 893 { 894 this.owned = true; 895 this.hmenu = hmenu; 896 897 _init(); 898 899 menuItems.addRange(items); 900 } 901 902 903 // Don't call directly. 904 @disable this(MenuItem[] items); 905 /+{ 906 /+ 907 this.owned = true; 908 909 _init(); 910 911 menuItems.addRange(items); 912 +/ 913 914 assert(0); 915 }+/ 916 917 918 ~this() 919 { 920 if(owned) 921 DestroyMenu(hmenu); 922 } 923 924 925 /// 926 final @property void tag(Object o) // setter 927 { 928 ttag = o; 929 } 930 931 /// ditto 932 final @property Object tag() // getter 933 { 934 return ttag; 935 } 936 937 938 /// 939 final @property HMENU handle() // getter 940 { 941 return hmenu; 942 } 943 944 945 /// 946 final @property MenuItemCollection menuItems() // getter 947 { 948 return items; 949 } 950 951 952 /// 953 @property bool isParent() // getter 954 { 955 return false; 956 } 957 958 959 /// 960 protected void onReflectedMessage(ref Message m) 961 { 962 } 963 964 965 package final void _reflectMenu(ref Message m) 966 { 967 onReflectedMessage(m); 968 } 969 970 971 /+ package +/ protected void _setInfo(UINT uItem, BOOL fByPosition, LPMENUITEMINFOA lpmii, Dstring typeData = null) // package 972 { 973 if(typeData.length) 974 { 975 if(dfl.internal.utf.useUnicode) 976 { 977 static assert(MENUITEMINFOW.sizeof == MENUITEMINFOA.sizeof); 978 lpmii.dwTypeData = cast(typeof(lpmii.dwTypeData))dfl.internal.utf.toUnicodez(typeData); 979 _setMenuItemInfoW(hmenu, uItem, fByPosition, cast(MENUITEMINFOW*)lpmii); 980 } 981 else 982 { 983 lpmii.dwTypeData = cast(typeof(lpmii.dwTypeData))dfl.internal.utf.unsafeAnsiz(typeData); 984 SetMenuItemInfoA(hmenu, uItem, fByPosition, lpmii); 985 } 986 } 987 else 988 { 989 SetMenuItemInfoA(hmenu, uItem, fByPosition, lpmii); 990 } 991 } 992 993 994 /+ package +/ protected void _insert(UINT uItem, BOOL fByPosition, LPMENUITEMINFOA lpmii, Dstring typeData = null) // package 995 { 996 if(typeData.length) 997 { 998 if(dfl.internal.utf.useUnicode) 999 { 1000 static assert(MENUITEMINFOW.sizeof == MENUITEMINFOA.sizeof); 1001 lpmii.dwTypeData = cast(typeof(lpmii.dwTypeData))dfl.internal.utf.toUnicodez(typeData); 1002 _insertMenuItemW(hmenu, uItem, fByPosition, cast(MENUITEMINFOW*)lpmii); 1003 } 1004 else 1005 { 1006 lpmii.dwTypeData = cast(typeof(lpmii.dwTypeData))dfl.internal.utf.unsafeAnsiz(typeData); 1007 InsertMenuItemA(hmenu, uItem, fByPosition, lpmii); 1008 } 1009 } 1010 else 1011 { 1012 InsertMenuItemA(hmenu, uItem, fByPosition, lpmii); 1013 } 1014 } 1015 1016 1017 /+ package +/ protected void _remove(UINT uPosition, UINT uFlags) // package 1018 { 1019 RemoveMenu(hmenu, uPosition, uFlags); 1020 } 1021 1022 1023 package HMENU hmenu; 1024 1025 private: 1026 bool owned = true; 1027 MenuItemCollection items; 1028 Object ttag; 1029 } 1030 1031 1032 /// 1033 class MainMenu: Menu // docmain 1034 { 1035 // Used internally. 1036 this(HMENU hmenu, bool owned = true) 1037 { 1038 super(hmenu, owned); 1039 } 1040 1041 1042 /// 1043 this() 1044 { 1045 super(CreateMenu()); 1046 } 1047 1048 /// ditto 1049 this(MenuItem[] items) 1050 { 1051 super(CreateMenu(), items); 1052 } 1053 1054 1055 /+ package +/ protected override void _setInfo(UINT uItem, BOOL fByPosition, LPMENUITEMINFOA lpmii, Dstring typeData = null) // package 1056 { 1057 Menu._setInfo(uItem, fByPosition, lpmii, typeData); 1058 1059 if(hwnd) 1060 DrawMenuBar(hwnd); 1061 } 1062 1063 1064 /+ package +/ protected override void _insert(UINT uItem, BOOL fByPosition, LPMENUITEMINFOA lpmii, Dstring typeData = null) // package 1065 { 1066 Menu._insert(uItem, fByPosition, lpmii, typeData); 1067 1068 if(hwnd) 1069 DrawMenuBar(hwnd); 1070 } 1071 1072 1073 /+ package +/ protected override void _remove(UINT uPosition, UINT uFlags) // package 1074 { 1075 Menu._remove(uPosition, uFlags); 1076 1077 if(hwnd) 1078 DrawMenuBar(hwnd); 1079 } 1080 1081 1082 private: 1083 1084 HWND hwnd = HWND.init; 1085 1086 1087 package final void _setHwnd(HWND hwnd) 1088 { 1089 this.hwnd = hwnd; 1090 } 1091 } 1092 } 1093