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