1 /**
2  * Base class for native common controls.
3  *
4  * `Control` creates a child window of a given Win32 class (`"Button"`,
5  * `"SysListView32"`, …) and provides the operations common to all controls:
6  * text get/set, font assignment, a preferred size, parent message routing
7  * (`WM_COMMAND` / `WM_NOTIFY`), and opt-in subclassing for controls that need
8  * to intercept their own messages.
9  */
10 module deft.controls.control;
11 
12 version (Windows):
13 
14 import core.sys.windows.windows;
15 import core.sys.windows.commctrl : DefSubclassProc, SetWindowSubclass, SUBCLASSPROC;
16 
17 import deft.util.strings;
18 import deft.widget;
19 import deft.platform.win32.init : hInstance;
20 import deft.platform.win32.wndproc : lookupWidget;
21 
22 private __gshared int g_nextControlId = 1000;
23 
24 private int nextControlId()
25 {
26 	return g_nextControlId++;
27 }
28 
29 /// The system default GUI font, used for all controls. Cached stock object.
30 HFONT defaultFont()
31 {
32 	return cast(HFONT) GetStockObject(DEFAULT_GUI_FONT);
33 }
34 
35 /// Base class for all Win32 common-control wrappers.
36 class Control : Widget
37 {
38 	private int controlId_;
39 	private bool subclassed_;
40 
41 	/// The control's command identifier (the `hMenu` child id at creation).
42 	int controlId() const @safe pure nothrow @nogc
43 	{
44 		return controlId_;
45 	}
46 
47 	/**
48 	 * Create the control as a child window.
49 	 *
50 	 * `className` is a Win32 window class name (for example `"Button"`).
51 	 * `WS_CHILD | WS_VISIBLE` are added to the supplied `style`. The control is
52 	 * registered with its parent and given the system default GUI font.
53 	 */
54 	this(Widget parent, string className, DWORD style, DWORD exStyle = 0)
55 	{
56 		this.parent_ = parent;
57 		controlId_ = nextControlId();
58 
59 		HWND parentHandle = parent !is null ? parent.handle : null;
60 
61 		// WS_GROUP makes each control start its own keyboard navigation group, so
62 		// arrow keys stay within a control rather than bleeding to the next one.
63 		// Radio buttons that continue a group clear it again (see RadioButton).
64 		handle_ = CreateWindowExW(
65 			exStyle,
66 			className.toWStringz,
67 			""w.ptr,
68 			WS_CHILD | WS_VISIBLE | WS_GROUP | style,
69 			0, 0, 0, 0,
70 			parentHandle,
71 			cast(HMENU) cast(size_t) controlId_,
72 			hInstance(),
73 			null);
74 
75 		registerHandle();
76 
77 		if (parent !is null)
78 			parent.addChild(this);
79 
80 		setFont(defaultFont());
81 	}
82 
83 	/// Set the control's text.
84 	void setText(string text)
85 	{
86 		if (handle)
87 			SetWindowTextW(handle, text.toWStringz);
88 	}
89 
90 	/// Get the control's text.
91 	string getText()
92 	{
93 		if (!handle)
94 			return "";
95 
96 		int len = GetWindowTextLengthW(handle);
97 		if (len <= 0)
98 			return "";
99 
100 		auto buf = new wchar[len + 1];
101 		int got = GetWindowTextW(handle, buf.ptr, cast(int) buf.length);
102 		return fromWString(buf[0 .. got]);
103 	}
104 
105 	/// Assign a font to the control and request a repaint.
106 	void setFont(HFONT font)
107 	{
108 		if (handle)
109 			SendMessageW(handle, WM_SETFONT, cast(WPARAM) font, cast(LPARAM) TRUE);
110 	}
111 
112 	/// A reasonable default preferred size; override per control type.
113 	override Size getPreferredSize()
114 	{
115 		return Size(80, 24);
116 	}
117 
118 	/**
119 	 * Handle a `WM_COMMAND` notification routed from the parent.
120 	 *
121 	 * `notificationCode` is the high word of the command's `wParam`. Return
122 	 * `true` if the notification was handled. The default does nothing.
123 	 */
124 	bool processCommand(ushort notificationCode)
125 	{
126 		return false;
127 	}
128 
129 	/**
130 	 * Handle a `WM_NOTIFY` notification routed from the parent. Return `true`
131 	 * if it was handled. The default does nothing.
132 	 */
133 	bool processNotify(NMHDR* header)
134 	{
135 		return false;
136 	}
137 
138 	/**
139 	 * Install a subclass window procedure so the control can intercept its own
140 	 * messages (for example, swallowing the Enter key in a text field).
141 	 * Idempotent. Subclasses override `processSubclassed` to do the work.
142 	 */
143 	void subclass()
144 	{
145 		if (handle && !subclassed_)
146 		{
147 			SetWindowSubclass(handle, &controlSubclassProc, 1,
148 				cast(DWORD_PTR) cast(void*) this);
149 			subclassed_ = true;
150 		}
151 	}
152 
153 	/**
154 	 * Intercept a message while subclassed. Set `result` and return `true` to
155 	 * consume the message; return `false` to let default processing continue.
156 	 */
157 	bool processSubclassed(UINT msg, WPARAM wParam, LPARAM lParam, ref LRESULT result)
158 	{
159 		return false;
160 	}
161 }
162 
163 /**
164  * Route a parent's `WM_COMMAND` to the originating control. Returns `true` if a
165  * control handled it.
166  */
167 bool routeCommand(WPARAM wParam, LPARAM lParam)
168 {
169 	auto controlHwnd = cast(HWND) lParam;
170 	if (controlHwnd is null)
171 		return false; // menu or accelerator command, not a control
172 
173 	if (auto widget = lookupWidget(controlHwnd))
174 		if (auto control = cast(Control) widget)
175 			return control.processCommand(HIWORD(cast(DWORD) wParam));
176 
177 	return false;
178 }
179 
180 /**
181  * Route a parent's `WM_NOTIFY` to the originating control. Returns `true` if a
182  * control handled it.
183  */
184 bool routeNotify(LPARAM lParam)
185 {
186 	auto header = cast(NMHDR*) lParam;
187 	if (header is null)
188 		return false;
189 
190 	if (auto widget = lookupWidget(header.hwndFrom))
191 		if (auto control = cast(Control) widget)
192 			return control.processNotify(header);
193 
194 	return false;
195 }
196 
197 /// The shared subclass procedure; dispatches to `Control.processSubclassed`.
198 private extern (Windows) LRESULT controlSubclassProc(HWND hwnd, UINT msg,
199 	WPARAM wParam, LPARAM lParam, UINT_PTR idSubclass, DWORD_PTR refData) nothrow
200 {
201 	try
202 	{
203 		auto control = cast(Control) cast(void*) refData;
204 		if (control !is null)
205 		{
206 			LRESULT result;
207 			if (control.processSubclassed(msg, wParam, lParam, result))
208 				return result;
209 		}
210 	}
211 	catch (Throwable)
212 	{
213 		// Never propagate a D throwable through the Win32 dispatcher.
214 	}
215 
216 	return DefSubclassProc(hwnd, msg, wParam, lParam);
217 }