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 }