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