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