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 }