1 /**
2  * Native checked list box (`SysListView32` with checkboxes).
3  *
4  * `CheckListBox` wraps the Win32 list-view common control in list mode with the
5  * checkbox extended style: a single-column list where every item carries its own
6  * checkbox that can be toggled independently of the selection. It exposes item
7  * management, per-item checked state, single selection and delegate-based events
8  * for check toggles and selection changes. Because it is a real native control,
9  * it brings MSAA accessibility for free.
10  *
11  * Win32 stores the per-item checkbox in the "state image" bits of the item state
12  * (mask `LVIS_STATEIMAGEMASK`): state-image index 1 means unchecked and index 2
13  * means checked.
14  */
15 module deft.controls.checklistbox;
16 
17 version (Windows):
18 
19 import core.sys.windows.windows;
20 import core.sys.windows.commctrl;
21 
22 import deft.controls.control;
23 import deft.events;
24 import deft.util.strings;
25 import deft.widget;
26 
27 /// A native list of items, each with its own checkbox.
28 class CheckListBox : Control
29 {
30 	/// Fired when an item's checkbox is toggled; argument is the item index.
31 	Event!(int) onItemChecked;
32 	/// Fired when the selected item changes; argument is the new selected index.
33 	Event!(int) onSelectionChanged;
34 
35 	/**
36 	 * Create a checked list box as a child of `parent`.
37 	 *
38 	 * The control is single-select, always shows the selection, takes part in tab
39 	 * navigation and has a border. Checkboxes are enabled via the
40 	 * `LVS_EX_CHECKBOXES` extended style.
41 	 */
42 	this(Widget parent)
43 	{
44 		super(parent, "SysListView32",
45 			LVS_LIST | LVS_SINGLESEL | LVS_SHOWSELALWAYS | WS_TABSTOP | WS_BORDER);
46 
47 		SendMessageW(handle, LVM_SETEXTENDEDLISTVIEWSTYLE, 0, LVS_EX_CHECKBOXES);
48 
49 		// Subclass so WM_SETFOCUS can move focus onto the first item when nothing
50 		// is selected yet — keyboard users land on a real, navigable item.
51 		subclass();
52 	}
53 
54 	/// Append an item with the given `text`. Returns the new item's index.
55 	int addItem(string text)
56 	{
57 		LVITEMW item;
58 		item.mask = LVIF_TEXT;
59 		item.iItem = getItemCount();
60 		item.pszText = cast(LPWSTR) text.toWStringz;
61 		return cast(int) SendMessageW(handle, LVM_INSERTITEMW, 0, cast(LPARAM)&item);
62 	}
63 
64 	/// Return the number of items.
65 	int getItemCount()
66 	{
67 		return cast(int) SendMessageW(handle, LVM_GETITEMCOUNT, 0, 0);
68 	}
69 
70 	/// Remove all items.
71 	void clear()
72 	{
73 		SendMessageW(handle, LVM_DELETEALLITEMS, 0, 0);
74 	}
75 
76 	/// Return the text of the item at `index`.
77 	string getItemText(int index)
78 	{
79 		// LVM_GETITEMTEXTW returns the number of characters copied; a result that
80 		// fills the buffer (cap-1) may have been truncated, so grow and retry.
81 		for (int cap = 256;; cap *= 2)
82 		{
83 			auto buf = new wchar[cap];
84 			LVITEMW item;
85 			item.iSubItem = 0;
86 			item.pszText = buf.ptr;
87 			item.cchTextMax = cap;
88 			int got = cast(int) SendMessageW(handle, LVM_GETITEMTEXTW, index,
89 				cast(LPARAM)&item);
90 			if (got < cap - 1 || cap >= 1 << 16)
91 				return fromWString(buf[0 .. got]);
92 		}
93 	}
94 
95 	/// Return whether the item at `index` is checked.
96 	bool isChecked(int index)
97 	{
98 		auto st = cast(uint) SendMessageW(handle, LVM_GETITEMSTATE, index,
99 			LVIS_STATEIMAGEMASK);
100 		return ((st >> 12) == 2);
101 	}
102 
103 	/// Set the checked state of the item at `index`.
104 	void setChecked(int index, bool checked)
105 	{
106 		LVITEMW item;
107 		item.stateMask = LVIS_STATEIMAGEMASK;
108 		item.state = (checked ? 2 : 1) << 12;
109 		SendMessageW(handle, LVM_SETITEMSTATE, index, cast(LPARAM)&item);
110 	}
111 
112 	/// Return the index of the selected item, or -1 if nothing is selected.
113 	int getSelectedIndex()
114 	{
115 		return cast(int) SendMessageW(handle, LVM_GETNEXTITEM, cast(WPARAM)-1, LVNI_SELECTED);
116 	}
117 
118 	/// Select (and focus) the item at `index`.
119 	void setSelectedIndex(int index)
120 	{
121 		LVITEMW item;
122 		item.state = LVIS_SELECTED | LVIS_FOCUSED;
123 		item.stateMask = LVIS_SELECTED | LVIS_FOCUSED;
124 		SendMessageW(handle, LVM_SETITEMSTATE, index, cast(LPARAM)&item);
125 	}
126 
127 	/// Route list-view selection and check-toggle notifications to their events.
128 	override bool processNotify(NMHDR* header)
129 	{
130 		if (header.code == LVN_ITEMCHANGED)
131 		{
132 			auto nm = cast(NMLISTVIEW*) header;
133 			if (nm.uChanged & LVIF_STATE)
134 			{
135 				// Selection: fire only on a 0 -> selected transition.
136 				if ((nm.uNewState & LVIS_SELECTED) && !(nm.uOldState & LVIS_SELECTED))
137 					onSelectionChanged.fire(nm.iItem);
138 
139 				// Check toggle: compare the old and new state-image indices. Guard
140 				// oldImg != 0 so the initial 0 -> state transition on insert (when
141 				// the checkbox first appears) does not fire a spurious event.
142 				int oldImg = (nm.uOldState & LVIS_STATEIMAGEMASK) >> 12;
143 				int newImg = (nm.uNewState & LVIS_STATEIMAGEMASK) >> 12;
144 				if (oldImg != 0 && newImg != 0 && oldImg != newImg)
145 					onItemChecked.fire(nm.iItem);
146 			}
147 			return true;
148 		}
149 		return false;
150 	}
151 
152 	/**
153 	 * Move focus onto the first item when the control gains focus with nothing
154 	 * selected, so a keyboard user starts on a navigable item. Always returns
155 	 * false so default processing still runs.
156 	 */
157 	override bool processSubclassed(UINT msg, WPARAM wParam, LPARAM lParam,
158 		ref LRESULT result)
159 	{
160 		if (msg == WM_SETFOCUS && getSelectedIndex() < 0 && getItemCount() > 0)
161 			setSelectedIndex(0);
162 		return false;
163 	}
164 
165 	/// A sensible default size for a checked list.
166 	override Size getPreferredSize()
167 	{
168 		return Size(200, 160);
169 	}
170 }