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