1 /**
2  * Native list box control.
3  *
4  * `ListBox` wraps the Win32 `"ListBox"` window class: a scrollable, single-column
5  * list of selectable string items. It exposes item management (add / insert /
6  * remove / clear), selection access, per-item user data, and delegate-based
7  * events for selection changes and item activation (double-click).
8  */
9 module deft.controls.listbox;
10 
11 version (Windows):
12 
13 import core.sys.windows.windows;
14 
15 import deft.controls.control;
16 import deft.events;
17 import deft.util.strings;
18 import deft.widget;
19 
20 /// How many items a list box lets the user select at once.
21 enum ListBoxSelection
22 {
23 	/// One item at a time.
24 	single,
25 	/// Several items via click/space toggling.
26 	multiple,
27 	/// A contiguous or ctrl-extended range (Shift/Ctrl+click).
28 	extended,
29 }
30 
31 /// A native Win32 list box: a scrollable column of selectable string items.
32 class ListBox : Control
33 {
34 	/// Fired when the selection changes; carries the new selected index.
35 	Event!(int) onSelectionChanged;
36 
37 	/// Fired when an item is activated (double-clicked); carries its index.
38 	Event!(int) onItemActivated;
39 
40 	private ListBoxSelection selection_;
41 
42 	/// Keeps GC-allocated item data reachable; see `setItemData`.
43 	private void*[] retainedData_;
44 
45 	/**
46 	 * Create a list box as a child of `parent`.
47 	 *
48 	 * The control is created with `LBS_NOTIFY` (so it reports selection and
49 	 * double-click notifications), `LBS_HASSTRINGS`, a vertical scroll bar, a
50 	 * border, and a tab stop for keyboard navigation. `selection` chooses the
51 	 * selection mode: `single` (one item), `multiple` (toggle several with
52 	 * click/space), or `extended` (Shift/Ctrl+click ranges).
53 	 */
54 	this(Widget parent, ListBoxSelection selection = ListBoxSelection.single)
55 	{
56 		// super() must be the first statement, so the style is computed by a
57 		// helper rather than with a switch in the constructor body.
58 		super(parent, "ListBox", win32StyleFor(selection));
59 		selection_ = selection;
60 		subclass();
61 	}
62 
63 	/// Map a `ListBoxSelection` to its Win32 window style bits.
64 	private static DWORD win32StyleFor(ListBoxSelection selection)
65 	{
66 		DWORD style =
67 			LBS_NOTIFY | LBS_HASSTRINGS | WS_VSCROLL | WS_BORDER | WS_TABSTOP;
68 		final switch (selection)
69 		{
70 			case ListBoxSelection.single:
71 				return style;
72 			case ListBoxSelection.multiple:
73 				return style | LBS_MULTIPLESEL;
74 			case ListBoxSelection.extended:
75 				return style | LBS_EXTENDEDSEL;
76 		}
77 	}
78 
79 	/// Append `text` to the end of the list; returns the new item's index.
80 	int addItem(string text)
81 	{
82 		return cast(int) SendMessageW(handle, LB_ADDSTRING, 0,
83 			cast(LPARAM) text.toWStringz);
84 	}
85 
86 	/// Insert `text` at `index`, shifting later items down.
87 	void insertItem(int index, string text)
88 	{
89 		SendMessageW(handle, LB_INSERTSTRING, index, cast(LPARAM) text.toWStringz);
90 	}
91 
92 	/// Remove the item at `index`.
93 	void removeItem(int index)
94 	{
95 		SendMessageW(handle, LB_DELETESTRING, index, 0);
96 	}
97 
98 	/// Remove all items (and release any retained item data; see `setItemData`).
99 	void clear()
100 	{
101 		SendMessageW(handle, LB_RESETCONTENT, 0, 0);
102 		retainedData_ = null;
103 	}
104 
105 	/// Return the selected item's index, or -1 (`LB_ERR`) if none is selected.
106 	int getSelectedIndex()
107 	{
108 		return cast(int) SendMessageW(handle, LB_GETCURSEL, 0, 0);
109 	}
110 
111 	/// Select the item at `index` (pass -1 to clear the selection).
112 	void setSelectedIndex(int index)
113 	{
114 		SendMessageW(handle, LB_SETCURSEL, index, 0);
115 	}
116 
117 	/**
118 	 * Return the indices of every selected item, or `null` if none are selected.
119 	 *
120 	 * Only meaningful for `multiple` and `extended` list boxes; on a `single`
121 	 * list box the underlying messages report no selection.
122 	 */
123 	int[] getSelectedIndices()
124 	{
125 		int count = cast(int) SendMessageW(handle, LB_GETSELCOUNT, 0, 0);
126 		if (count <= 0)
127 			return null;
128 
129 		auto buf = new int[count];
130 		SendMessageW(handle, LB_GETSELITEMS, cast(WPARAM) count,
131 			cast(LPARAM) buf.ptr);
132 		return buf;
133 	}
134 
135 	/**
136 	 * Select or deselect the item at `index`.
137 	 *
138 	 * Only meaningful for `multiple` and `extended` list boxes; use
139 	 * `setSelectedIndex` for `single` list boxes.
140 	 */
141 	void setItemSelected(int index, bool selected)
142 	{
143 		SendMessageW(handle, LB_SETSEL, selected ? TRUE : FALSE,
144 			cast(LPARAM) index);
145 	}
146 
147 	/// Return the number of items in the list.
148 	int getItemCount()
149 	{
150 		return cast(int) SendMessageW(handle, LB_GETCOUNT, 0, 0);
151 	}
152 
153 	/// Return the text of the item at `index`, or `""` if it has none.
154 	string getItemText(int index)
155 	{
156 		int len = cast(int) SendMessageW(handle, LB_GETTEXTLEN, index, 0);
157 		if (len <= 0)
158 			return "";
159 
160 		auto buf = new wchar[len + 1];
161 		int got = cast(int) SendMessageW(handle, LB_GETTEXT, index,
162 			cast(LPARAM) buf.ptr);
163 		return fromWString(buf[0 .. got]);
164 	}
165 
166 	/**
167 	 * Associate an opaque `data` pointer with the item at `index`.
168 	 *
169 	 * The pointer is stored inside the native control, where the D garbage
170 	 * collector cannot see it. To keep GC-allocated `data` from being collected
171 	 * out from under the control, Deft also retains a reference internally for the
172 	 * control's lifetime; the retained references are released by `clear()`.
173 	 */
174 	void setItemData(int index, void* data)
175 	{
176 		if (data !is null)
177 			retainedData_ ~= data;
178 		SendMessageW(handle, LB_SETITEMDATA, index, cast(LPARAM) data);
179 	}
180 
181 	/// Return the opaque pointer previously stored for the item at `index`.
182 	void* getItemData(int index)
183 	{
184 		return cast(void*) SendMessageW(handle, LB_GETITEMDATA, index, 0);
185 	}
186 
187 	/**
188 	 * Route a `WM_COMMAND` notification. Fires `onSelectionChanged` on
189 	 * `LBN_SELCHANGE` and `onItemActivated` on `LBN_DBLCLK`.
190 	 */
191 	override bool processCommand(ushort code)
192 	{
193 		switch (code)
194 		{
195 			case LBN_SELCHANGE:
196 				onSelectionChanged.fire(getSelectedIndex());
197 				return true;
198 			case LBN_DBLCLK:
199 				onItemActivated.fire(getSelectedIndex());
200 				return true;
201 			default:
202 				return false;
203 		}
204 	}
205 
206 	/**
207 	 * Auto-select the first item when a single-select list box gains focus and
208 	 * nothing is selected yet, so a screen reader announces an item on tab-in.
209 	 * Focus messages are never consumed.
210 	 */
211 	override bool processSubclassed(UINT msg, WPARAM wParam, LPARAM lParam,
212 		ref LRESULT result)
213 	{
214 		if (msg == WM_SETFOCUS
215 			&& selection_ == ListBoxSelection.single
216 			&& getSelectedIndex() < 0
217 			&& getItemCount() > 0)
218 			setSelectedIndex(0);
219 
220 		return false;
221 	}
222 
223 	/// A sensible default size for a list box.
224 	override Size getPreferredSize()
225 	{
226 		return Size(200, 120);
227 	}
228 }