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