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 }