1 /**
2  * Native combo box control.
3  *
4  * `ComboBox` wraps the Win32 `"ComboBox"` window class. It supports three
5  * interaction styles (see `ComboBoxStyle`): a non-editable drop-down list, an
6  * editable drop-down, and an always-open simple list with an editable field.
7  * It exposes item management (add / insert / remove / clear), selection access,
8  * per-item user data, and delegate-based events for selection and text changes.
9  */
10 module deft.controls.combobox;
11 
12 version (Windows):
13 
14 import core.sys.windows.windows;
15 
16 import deft.controls.control;
17 import deft.events;
18 import deft.util.strings;
19 import deft.widget;
20 
21 /// The interaction style of a combo box.
22 enum ComboBoxStyle
23 {
24 	/// A non-editable dropdown: the user can only pick an existing item.
25 	dropDownList,
26 	/// An editable dropdown: the user can pick an item or type a new value.
27 	dropDown,
28 	/// An always-open list with an editable field above it.
29 	simple,
30 }
31 
32 /// A native Win32 combo box of selectable string items.
33 class ComboBox : Control
34 {
35 	/// Fired when the selection changes; carries the new selected index.
36 	Event!(int) onSelectionChanged;
37 
38 	/// Fired when the editable text changes; carries the new text.
39 	Event!(string) onTextChanged;
40 
41 	private ComboBoxStyle style_;
42 	private bool editable_;
43 
44 	/// Keeps GC-allocated item data reachable; see `setItemData`.
45 	private void*[] retainedData_;
46 
47 	/**
48 	 * Create a combo box as a child of `parent`.
49 	 *
50 	 * The `style` selects the interaction model: `dropDownList` (a non-editable
51 	 * drop-down list, the default), `dropDown` (an editable drop-down), or
52 	 * `simple` (an always-open list with an editable field). All styles get a
53 	 * vertical scroll bar for the list and a tab stop for keyboard navigation.
54 	 */
55 	this(Widget parent, ComboBoxStyle style = ComboBoxStyle.dropDownList)
56 	{
57 		// super() must be the first statement, so the style is computed by a
58 		// helper rather than with a switch in the constructor body.
59 		super(parent, "ComboBox", win32StyleFor(style));
60 		style_ = style;
61 		editable_ = style == ComboBoxStyle.dropDown || style == ComboBoxStyle.simple;
62 		subclass();
63 	}
64 
65 	/// Map a `ComboBoxStyle` to its Win32 window style bits.
66 	private static DWORD win32StyleFor(ComboBoxStyle style)
67 	{
68 		DWORD win32Style = WS_VSCROLL | WS_TABSTOP;
69 		final switch (style)
70 		{
71 			case ComboBoxStyle.dropDownList:
72 				return win32Style | CBS_DROPDOWNLIST;
73 			case ComboBoxStyle.dropDown:
74 				return win32Style | CBS_DROPDOWN;
75 			case ComboBoxStyle.simple:
76 				return win32Style | CBS_SIMPLE;
77 		}
78 	}
79 
80 	/// Append `text` to the end of the list; returns the new item's index.
81 	int addItem(string text)
82 	{
83 		return cast(int) SendMessageW(handle, CB_ADDSTRING, 0,
84 			cast(LPARAM) text.toWStringz);
85 	}
86 
87 	/// Insert `text` at `index`, shifting later items down.
88 	void insertItem(int index, string text)
89 	{
90 		SendMessageW(handle, CB_INSERTSTRING, index, cast(LPARAM) text.toWStringz);
91 	}
92 
93 	/// Remove the item at `index`.
94 	void removeItem(int index)
95 	{
96 		SendMessageW(handle, CB_DELETESTRING, index, 0);
97 	}
98 
99 	/// Remove all items (and release any retained item data; see `setItemData`).
100 	void clear()
101 	{
102 		SendMessageW(handle, CB_RESETCONTENT, 0, 0);
103 		retainedData_ = null;
104 	}
105 
106 	/// Return the selected item's index, or -1 (`CB_ERR`) if none is selected.
107 	int getSelectedIndex()
108 	{
109 		return cast(int) SendMessageW(handle, CB_GETCURSEL, 0, 0);
110 	}
111 
112 	/// Select the item at `index` (pass -1 to clear the selection).
113 	void setSelectedIndex(int index)
114 	{
115 		SendMessageW(handle, CB_SETCURSEL, index, 0);
116 	}
117 
118 	/// Return the number of items in the list.
119 	int getItemCount()
120 	{
121 		return cast(int) SendMessageW(handle, CB_GETCOUNT, 0, 0);
122 	}
123 
124 	/// Return the text of the item at `index`, or `""` if it has none.
125 	string getItemText(int index)
126 	{
127 		int len = cast(int) SendMessageW(handle, CB_GETLBTEXTLEN, index, 0);
128 		if (len <= 0)
129 			return "";
130 
131 		auto buf = new wchar[len + 1];
132 		int got = cast(int) SendMessageW(handle, CB_GETLBTEXT, index,
133 			cast(LPARAM) buf.ptr);
134 		return fromWString(buf[0 .. got]);
135 	}
136 
137 	/**
138 	 * Associate an opaque `data` pointer with the item at `index`.
139 	 *
140 	 * The pointer is stored inside the native control, where the D garbage
141 	 * collector cannot see it. To keep GC-allocated `data` from being collected
142 	 * out from under the control, Deft also retains a reference internally for the
143 	 * control's lifetime; the retained references are released by `clear()`.
144 	 */
145 	void setItemData(int index, void* data)
146 	{
147 		if (data !is null)
148 			retainedData_ ~= data;
149 		SendMessageW(handle, CB_SETITEMDATA, index, cast(LPARAM) data);
150 	}
151 
152 	/// Return the opaque pointer previously stored for the item at `index`.
153 	void* getItemData(int index)
154 	{
155 		return cast(void*) SendMessageW(handle, CB_GETITEMDATA, index, 0);
156 	}
157 
158 	/**
159 	 * Route a `WM_COMMAND` notification. Fires `onSelectionChanged` on
160 	 * `CBN_SELCHANGE` and `onTextChanged` on `CBN_EDITCHANGE` (the latter is
161 	 * only meaningful for the editable styles).
162 	 */
163 	override bool processCommand(ushort code)
164 	{
165 		switch (code)
166 		{
167 			case CBN_SELCHANGE:
168 				onSelectionChanged.fire(getSelectedIndex());
169 				return true;
170 			case CBN_EDITCHANGE:
171 				onTextChanged.fire(getText());
172 				return true;
173 			default:
174 				return false;
175 		}
176 	}
177 
178 	/**
179 	 * Auto-select the first item when a non-editable drop-down list receives
180 	 * focus, so a screen reader announces an item as the user tabs in. Never
181 	 * consumes the focus message.
182 	 */
183 	override bool processSubclassed(UINT msg, WPARAM wParam, LPARAM lParam,
184 		ref LRESULT result)
185 	{
186 		if (msg == WM_SETFOCUS
187 			&& style_ == ComboBoxStyle.dropDownList
188 			&& getSelectedIndex() < 0
189 			&& getItemCount() > 0)
190 			setSelectedIndex(0);
191 
192 		return false;
193 	}
194 
195 	/// A sensible default size for a combo box.
196 	override Size getPreferredSize()
197 	{
198 		if (style_ == ComboBoxStyle.simple)
199 			return Size(200, 120);
200 
201 		return Size(200, 26);
202 	}
203 }