1 // Written by Christopher E. Miller
2 // See the included license.txt for copyright and license details.
3 
4 module dfl.button;
5 
6 import core.sys.windows.windows;
7 
8 import dfl.exception;
9 import dfl.base;
10 import dfl.control;
11 import dfl.application;
12 import dfl.event;
13 import dfl.drawing;
14 import dfl.internal.dlib;
15 
16 private extern (Windows) void _initButton();
17 
18 abstract class ButtonBase : ControlSuperClass {
19 
20    @property void textAlign(ContentAlignment calign) {
21       LONG wl = _bstyle() & ~(BS_BOTTOM | BS_CENTER | BS_TOP | BS_RIGHT | BS_LEFT | BS_VCENTER);
22 
23       final switch (calign) {
24          case ContentAlignment.TOP_LEFT:
25             wl |= BS_TOP | BS_LEFT;
26             break;
27 
28          case ContentAlignment.BOTTOM_CENTER:
29             wl |= BS_BOTTOM | BS_CENTER;
30             break;
31 
32          case ContentAlignment.BOTTOM_LEFT:
33             wl |= BS_BOTTOM | BS_LEFT;
34             break;
35 
36          case ContentAlignment.BOTTOM_RIGHT:
37             wl |= BS_BOTTOM | BS_RIGHT;
38             break;
39 
40          case ContentAlignment.MIDDLE_CENTER:
41             wl |= BS_CENTER | BS_VCENTER;
42             break;
43 
44          case ContentAlignment.MIDDLE_LEFT:
45             wl |= BS_VCENTER | BS_LEFT;
46             break;
47 
48          case ContentAlignment.MIDDLE_RIGHT:
49             wl |= BS_VCENTER | BS_RIGHT;
50             break;
51 
52          case ContentAlignment.TOP_CENTER:
53             wl |= BS_TOP | BS_CENTER;
54             break;
55 
56          case ContentAlignment.TOP_RIGHT:
57             wl |= BS_TOP | BS_RIGHT;
58             break;
59       }
60 
61       _bstyle(wl);
62 
63       _crecreate();
64    }
65 
66    @property ContentAlignment textAlign() {
67       LONG wl = _bstyle();
68 
69       if (wl & BS_VCENTER) { // Middle.
70          if (wl & BS_CENTER) {
71             return ContentAlignment.MIDDLE_CENTER;
72          }
73          if (wl & BS_RIGHT) {
74             return ContentAlignment.MIDDLE_RIGHT;
75          }
76          return ContentAlignment.MIDDLE_LEFT;
77       } else if (wl & BS_BOTTOM) { // Bottom.
78          if (wl & BS_CENTER) {
79             return ContentAlignment.BOTTOM_CENTER;
80          }
81          if (wl & BS_RIGHT) {
82             return ContentAlignment.BOTTOM_RIGHT;
83          }
84          return ContentAlignment.BOTTOM_LEFT;
85       } else { // Top.
86          if (wl & BS_CENTER) {
87             return ContentAlignment.TOP_CENTER;
88          }
89          if (wl & BS_RIGHT) {
90             return ContentAlignment.TOP_RIGHT;
91          }
92          return ContentAlignment.TOP_LEFT;
93       }
94    }
95 
96    // Border stuff...
97 
98    /+
99       override void createHandle() {
100          if(isHandleCreated) {
101             return;
102          }
103 
104          createClassHandle(BUTTON_CLASSNAME);
105 
106          onHandleCreated(EventArgs.empty);
107       }
108    +/
109 
110       protected override void createParams(ref CreateParams cp) {
111          super.createParams(cp);
112 
113          cp.className = BUTTON_CLASSNAME;
114          if (isdef) {
115             cp.menu = cast(HMENU) IDOK;
116             if (!(cp.style & WS_DISABLED)) {
117                cp.style |= BS_DEFPUSHBUTTON;
118             }
119          } else if (cp.style & WS_DISABLED) {
120             cp.style &= ~BS_DEFPUSHBUTTON;
121          }
122       }
123 
124    protected override void prevWndProc(ref Message msg) {
125       //msg.result = CallWindowProcA(buttonPrevWndProc, msg.hWnd, msg.msg, msg.wParam, msg.lParam);
126       msg.result = dfl.internal.utf.callWindowProc(buttonPrevWndProc, msg.hWnd,
127             msg.msg, msg.wParam, msg.lParam);
128    }
129 
130    protected override void onReflectedMessage(ref Message m) {
131       super.onReflectedMessage(m);
132 
133       switch (m.msg) {
134          case WM_COMMAND:
135             assert(cast(HWND) m.lParam == handle);
136 
137             switch (HIWORD(m.wParam)) {
138                case BN_CLICKED:
139                   onClick(EventArgs.empty);
140                   break;
141 
142                default:
143             }
144             break;
145 
146          default:
147       }
148    }
149 
150    protected override void wndProc(ref Message msg) {
151       switch (msg.msg) {
152          case WM_LBUTTONDOWN:
153             onMouseDown(new MouseEventArgs(MouseButtons.LEFT, 0,
154                      cast(short) LOWORD(msg.lParam), cast(short) HIWORD(msg.lParam), 0));
155             break;
156 
157          case WM_LBUTTONUP:
158             onMouseUp(new MouseEventArgs(MouseButtons.LEFT, 1,
159                      cast(short) LOWORD(msg.lParam), cast(short) HIWORD(msg.lParam), 0));
160             break;
161 
162          default:
163             super.wndProc(msg);
164             return;
165       }
166       prevWndProc(msg);
167    }
168 
169    /+
170       protected override void onHandleCreated(EventArgs ea) {
171          super.onHandleCreated(ea);
172 
173          /+
174             // Done in createParams() now.
175             if(isdef) {
176                SetWindowLongA(handle, GWL_ID, IDOK);
177             }
178          +/
179       }
180    +/
181 
182       this() {
183          _initButton();
184 
185          wstyle |= WS_TABSTOP /+ | BS_NOTIFY +/ ;
186          ctrlStyle |= ControlStyles.SELECTABLE;
187          wclassStyle = buttonClassStyle;
188       }
189 
190    protected:
191 
192    final @property void isDefault(bool byes) {
193       isdef = byes;
194    }
195 
196    final @property bool isDefault() {
197       //return (_bstyle() & BS_DEFPUSHBUTTON) == BS_DEFPUSHBUTTON;
198       //return GetDlgCtrlID(m.hWnd) == IDOK;
199       return isdef;
200    }
201 
202    protected override bool processMnemonic(dchar charCode) {
203       if (canSelect) {
204          if (isMnemonic(charCode, text)) {
205             select();
206             //Application.doEvents(); // ?
207             //performClick();
208             onClick(EventArgs.empty);
209             return true;
210          }
211       }
212       return false;
213    }
214 
215    override @property Size defaultSize() {
216       return Size(75, 23);
217    }
218 
219    private:
220    bool isdef = false;
221 
222 package:
223 final:
224    // Automatically redraws button styles, unlike _style().
225    // Don't use with regular window styles ?
226    void _bstyle(LONG newStyle) {
227       if (isHandleCreated) //SendMessageA(handle, BM_SETSTYLE, LOWORD(newStyle), MAKELPARAM(TRUE, 0));
228       {
229          SendMessageA(handle, BM_SETSTYLE, newStyle, MAKELPARAM(TRUE, 0));
230       }
231 
232       wstyle = newStyle;
233       //_style(newStyle);
234    }
235 
236    LONG _bstyle() {
237       return _style();
238    }
239 }
240 
241 class Button : ButtonBase, IButtonControl {
242    this() {
243    }
244 
245    @property DialogResult dialogResult() {
246       return dresult;
247    }
248 
249    @property void dialogResult(DialogResult dr) {
250       dresult = dr;
251    }
252 
253    // True if default button.
254    void notifyDefault(bool byes) {
255       isDefault = byes;
256 
257       if (byes) {
258          if (enabled) { // Only show thick border if enabled.
259             _bstyle(_bstyle() | BS_DEFPUSHBUTTON);
260          }
261       } else {
262          _bstyle(_bstyle() & ~BS_DEFPUSHBUTTON);
263       }
264    }
265 
266    void performClick() {
267       if (!enabled || !visible || !isHandleCreated) { // ?
268          return; // ?
269       }
270 
271       // This is actually not so good because it sets focus to the control.
272       //SendMessageA(handle, BM_CLICK, 0, 0); // So that wndProc() gets it.
273 
274       onClick(EventArgs.empty);
275    }
276 
277    protected override void onClick(EventArgs ea) {
278       super.onClick(ea);
279 
280       if (!(Application._compat & DflCompat.FORM_DIALOGRESULT_096)) {
281          if (DialogResult.NONE != this.dialogResult) {
282             auto xx = cast(IDialogResult) topLevelControl;
283             if (xx) {
284                xx.dialogResult = this.dialogResult;
285             }
286          }
287       }
288    }
289 
290    protected override void wndProc(ref Message m) {
291       switch (m.msg) {
292          case WM_ENABLE: {
293                             // Fixing the thick border of a default button when enabling and disabling it.
294 
295                             // To-do: check if correct implementation.
296 
297                             DWORD bst;
298                             bst = _bstyle();
299                             if (bst & BS_DEFPUSHBUTTON) {
300                                //_bstyle(bst); // Force the border to be updated. Only works when enabling.
301                                if (!m.wParam) {
302                                   _bstyle(bst & ~BS_DEFPUSHBUTTON);
303                                }
304                             } else if (m.wParam) {
305                                //if(GetDlgCtrlID(m.hWnd) == IDOK)
306                                if (isdef) {
307                                   _bstyle(bst | BS_DEFPUSHBUTTON);
308                                }
309                             }
310                          }
311                          break;
312 
313          default:
314       }
315 
316       super.wndProc(m);
317    }
318 
319    override @property void text(Dstring txt) {
320       if (txt.length) {
321          assert(!this.image, "Button image with text not supported");
322       }
323 
324       super.text = txt;
325    }
326 
327    alias text = Control.text; // Overload.
328 
329    final @property Image image() {
330       return _img;
331    }
332 
333    final @property void image(Image img)
334       in {
335          if (img) {
336             assert(!this.text.length, "Button image with text not supported");
337          }
338       }
339    body {
340       /+
341          if(_picbm) {
342             _picbm.dispose();
343             _picbm = null;
344          }
345       +/
346 
347          _img = null; // In case of exception.
348       LONG imgst = 0;
349       if (img) {
350          /+
351             if(cast(Bitmap)img) {
352                imgst = BS_BITMAP;
353             } else if(cast(Icon)img) {
354                imgst = BS_ICON;
355             } else {
356                if(cast(Picture)img) {
357                   _picbm = (cast(Picture)img).toBitmap();
358                   imgst = BS_BITMAP;
359                   goto not_unsupported;
360                }
361 
362                throw new DflException("Unsupported image format");
363 not_unsupported: ;
364             }
365          +/
366             switch (img._imgtype(null)) {
367                case 1:
368                   imgst = BS_BITMAP;
369                   break;
370 
371                case 2:
372                   imgst = BS_ICON;
373                   break;
374 
375                default:
376                   throw new DflException("Unsupported image format");
377 not_unsupported:
378                   ;
379             }
380       }
381 
382       _img = img;
383       _style((_style() & ~(BS_BITMAP | BS_ICON)) | imgst); // Redrawn manually in setImg().
384       if (img) {
385          if (isHandleCreated) {
386             setImg(imgst);
387          }
388       }
389       //_bstyle((_bstyle() & ~(BS_BITMAP | BS_ICON)) | imgst);
390    }
391 
392    private void setImg(LONG bsImageStyle)
393       in {
394          assert(isHandleCreated);
395       }
396    body {
397       WPARAM wparam = 0;
398       LPARAM lparam = 0;
399 
400       /+
401          if(bsImageStyle & BS_BITMAP) {
402             wparam = IMAGE_BITMAP;
403             lparam = cast(LPARAM)(_picbm ? _picbm.handle : (cast(Bitmap)_img).handle);
404          } else if(bsImageStyle & BS_ICON) {
405             wparam = IMAGE_ICON;
406             lparam = cast(LPARAM)((cast(Icon)(_img)).handle);
407          } else
408          {
409             return;
410          }
411       +/
412          if (!_img) {
413             return;
414          }
415       HGDIOBJ hgo;
416       switch (_img._imgtype(&hgo)) {
417          case 1:
418             wparam = IMAGE_BITMAP;
419             break;
420 
421          case 2:
422             wparam = IMAGE_ICON;
423             break;
424 
425          default:
426             return;
427       }
428       lparam = cast(LPARAM) hgo;
429 
430       //assert(lparam);
431       SendMessageA(handle, BM_SETIMAGE, wparam, lparam);
432       invalidate();
433    }
434 
435    protected override void onHandleCreated(EventArgs ea) {
436       super.onHandleCreated(ea);
437 
438       setImg(_bstyle());
439    }
440 
441    protected override void onHandleDestroyed(EventArgs ea) {
442       super.onHandleDestroyed(ea);
443 
444       /+
445          if(_picbm) {
446             _picbm.dispose();
447             _picbm = null;
448          }
449       +/
450    }
451 
452    private:
453    DialogResult dresult = DialogResult.NONE;
454    Image _img = null;
455    //Bitmap _picbm = null; // If -_img- is a Picture, need to keep a separate Bitmap.
456 }
457 
458 class CheckBox : ButtonBase {
459 
460    final @property void appearance(Appearance ap) {
461       final switch (ap) {
462          case Appearance.NORMAL:
463             _bstyle(_bstyle() & ~BS_PUSHLIKE);
464             break;
465 
466          case Appearance.BUTTON:
467             _bstyle(_bstyle() | BS_PUSHLIKE);
468             break;
469       }
470 
471       _crecreate();
472    }
473 
474    final @property Appearance appearance() {
475       if (_bstyle() & BS_PUSHLIKE) {
476          return Appearance.BUTTON;
477       }
478       return Appearance.NORMAL;
479    }
480 
481    final @property void autoCheck(bool byes) {
482       if (byes) {
483          _bstyle((_bstyle() & ~BS_CHECKBOX) | BS_AUTOCHECKBOX);
484       } else {
485          _bstyle((_bstyle() & ~BS_AUTOCHECKBOX) | BS_CHECKBOX);
486       }
487       // Enabling/disabling the window before creation messes
488       // up the autocheck style flag, so handle it manually.
489       _autocheck = byes;
490    }
491 
492    final @property bool autoCheck() {
493       /+
494          return (_bstyle() & BS_AUTOCHECKBOX) == BS_AUTOCHECKBOX;
495       +/
496          return _autocheck;
497    }
498 
499    this() {
500       wstyle |= BS_AUTOCHECKBOX | BS_LEFT | BS_VCENTER; // Auto check and MIDDLE_LEFT by default.
501    }
502 
503    /+
504       protected override void onClick(EventArgs ea) {
505          _updateState();
506 
507          super.onClick(ea);
508       }
509    +/
510 
511       final @property void checked(bool byes) {
512          if (byes) {
513             _check = CheckState.CHECKED;
514          } else {
515             _check = CheckState.UNCHECKED;
516          }
517 
518          if (isHandleCreated) {
519             SendMessageA(handle, BM_SETCHECK, cast(WPARAM) _check, 0);
520          }
521       }
522 
523    // Returns true for indeterminate too.
524    final @property bool checked() {
525       if (isHandleCreated) {
526          _updateState();
527       }
528       return _check != CheckState.UNCHECKED;
529    }
530 
531    final @property void checkState(CheckState st) {
532       _check = st;
533 
534       if (isHandleCreated) {
535          SendMessageA(handle, BM_SETCHECK, cast(WPARAM) st, 0);
536       }
537    }
538 
539    final @property CheckState checkState() {
540       if (isHandleCreated) {
541          _updateState();
542       }
543       return _check;
544    }
545 
546    protected override void onHandleCreated(EventArgs ea) {
547       super.onHandleCreated(ea);
548 
549       if (_autocheck) {
550          _bstyle((_bstyle() & ~BS_CHECKBOX) | BS_AUTOCHECKBOX);
551       } else {
552          _bstyle((_bstyle() & ~BS_AUTOCHECKBOX) | BS_CHECKBOX);
553       }
554 
555       SendMessageA(handle, BM_SETCHECK, cast(WPARAM) _check, 0);
556    }
557 
558    private:
559    CheckState _check = CheckState.UNCHECKED; // Not always accurate.
560    bool _autocheck = true;
561 
562    void _updateState() {
563       _check = cast(CheckState) SendMessageA(handle, BM_GETCHECK, 0, 0);
564    }
565 }
566 
567 class RadioButton : ButtonBase {
568 
569    final @property void appearance(Appearance ap) {
570       final switch (ap) {
571          case Appearance.NORMAL:
572             _bstyle(_bstyle() & ~BS_PUSHLIKE);
573             break;
574 
575          case Appearance.BUTTON:
576             _bstyle(_bstyle() | BS_PUSHLIKE);
577             break;
578       }
579 
580       _crecreate();
581    }
582 
583    final @property Appearance appearance() {
584       if (_bstyle() & BS_PUSHLIKE) {
585          return Appearance.BUTTON;
586       }
587       return Appearance.NORMAL;
588    }
589 
590    final @property void autoCheck(bool byes) {
591       /+
592          if(byes) {
593             _bstyle((_bstyle() & ~BS_RADIOBUTTON) | BS_AUTORADIOBUTTON);
594          } else {
595             _bstyle((_bstyle() & ~BS_AUTORADIOBUTTON) | BS_RADIOBUTTON);
596          }
597       // Enabling/disabling the window before creation messes
598       // up the autocheck style flag, so handle it manually.
599       +/
600          _autocheck = byes;
601    }
602 
603    final @property bool autoCheck() {
604       /+ // Also commented out when using BS_AUTORADIOBUTTON.
605          return (_bstyle() & BS_AUTOCHECKBOX) == BS_AUTOCHECKBOX;
606       +/
607          return _autocheck;
608    }
609 
610    this() {
611       wstyle &= ~WS_TABSTOP;
612       //wstyle |= BS_AUTORADIOBUTTON | BS_LEFT | BS_VCENTER; // MIDDLE_LEFT by default.
613       wstyle |= BS_RADIOBUTTON | BS_LEFT | BS_VCENTER; // MIDDLE_LEFT by default.
614    }
615 
616    protected override void onClick(EventArgs ea) {
617       if (autoCheck) {
618          if (parent) { // Sanity.
619             foreach (Control ctrl; parent.controls) {
620                if (ctrl is this) {
621                   continue;
622                }
623                if ((ctrl._rtype() & (1 | 8)) == (1 | 8)) { // Radio button + auto check.
624                   (cast(RadioButton) ctrl).checked = false;
625                }
626             }
627          }
628          checked = true;
629       }
630 
631       super.onClick(ea);
632    }
633 
634    /+
635       protected override void onClick(EventArgs ea) {
636          _updateState();
637 
638          super.onClick(ea);
639       }
640    +/
641 
642       final @property void checked(bool byes) {
643          if (byes) {
644             _check = CheckState.CHECKED;
645          } else {
646             _check = CheckState.UNCHECKED;
647          }
648 
649          if (isHandleCreated) {
650             SendMessageA(handle, BM_SETCHECK, cast(WPARAM) _check, 0);
651          }
652       }
653 
654    // Returns true for indeterminate too.
655    final @property bool checked() {
656       if (isHandleCreated) {
657          _updateState();
658       }
659       return _check != CheckState.UNCHECKED;
660    }
661 
662    final @property void checkState(CheckState st) {
663       _check = st;
664 
665       if (isHandleCreated) {
666          SendMessageA(handle, BM_SETCHECK, cast(WPARAM) st, 0);
667       }
668    }
669 
670    final @property CheckState checkState() {
671       if (isHandleCreated) {
672          _updateState();
673       }
674       return _check;
675    }
676 
677    void performClick() {
678       //onClick(EventArgs.empty);
679       SendMessageA(handle, BM_CLICK, 0, 0); // So that wndProc() gets it.
680    }
681 
682    protected override void onHandleCreated(EventArgs ea) {
683       super.onHandleCreated(ea);
684 
685       /+
686          if(_autocheck) {
687             _bstyle((_bstyle() & ~BS_RADIOBUTTON) | BS_AUTORADIOBUTTON);
688          } else {
689             _bstyle((_bstyle() & ~BS_AUTORADIOBUTTON) | BS_RADIOBUTTON);
690          }
691       +/
692 
693          SendMessageA(handle, BM_SETCHECK, cast(WPARAM) _check, 0);
694    }
695 
696    /+
697       protected override void onReflectedMessage(ref Message m) {
698          super.onReflectedMessage(m);
699 
700          switch(m.msg) {
701             /+
702                // Without this, with XP styles, the background just ends up transparent; not the requested color.
703                // This erases the text when XP styles aren't enabled.
704             case WM_CTLCOLORSTATIC:
705             case WM_CTLCOLORBTN: {
706                                     //if(hasVisualStyle)
707                                     {
708                                        RECT rect;
709                                        rect.right = width;
710                                        rect.bottom = height;
711                                        FillRect(cast(HDC)m.wParam, &rect, hbrBg);
712                                     }
713                                  }
714                                  break;
715                                  +/
716 
717             default:
718          }
719       }
720    +/
721 
722       /+ package +/ /+ protected +/ override int _rtype() { // package
723          if (autoCheck) {
724             return 1 | 8; // Radio button + auto check.
725          }
726          return 1; // Radio button.
727       }
728 
729    private:
730    CheckState _check = CheckState.UNCHECKED; // Not always accurate.
731    bool _autocheck = true;
732 
733    void _updateState() {
734       _check = cast(CheckState) SendMessageA(handle, BM_GETCHECK, 0, 0);
735    }
736 }