1 /**
2  * Accessibility — custom accessible names for controls.
3  *
4  * Standard Win32 common controls (ListView, TreeView, Button, …) already expose
5  * MSAA accessibility to screen readers such as JAWS and NVDA — items, roles and
6  * navigation all work with no extra code. The one thing missing for controls
7  * that lack a visible text label (for example a bare TreeView used as a category
8  * panel) is a human-readable *name*.
9  *
10  * `setAccessibleName` supplies one. Rather than implementing a full `IAccessible`
11  * proxy and intercepting `WM_GETOBJECT`, it uses MSAA Direct Annotation
12  * (`IAccPropServices::SetHwndPropStr`) to override just the name property on the
13  * control's default accessible object. oleacc then serves that name through the
14  * standard accessibility path automatically. This is the same mechanism WinForms
15  * uses to implement `Control.AccessibleName`.
16  *
17  * COM must be initialized on the calling (UI) thread first — `Application.initialize`
18  * does this.
19  */
20 module deft.accessibility;
21 
22 version (Windows):
23 
24 import core.sys.windows.windows;
25 import core.sys.windows.oaidl : VARIANT;
26 import core.sys.windows.objbase : CoCreateInstance;
27 import core.sys.windows.unknwn : IUnknown;
28 import core.sys.windows.wtypes : CLSCTX;
29 
30 import deft.util.strings;
31 import deft.widget : Widget;
32 
33 /// MSAA annotated property identifier — a GUID, passed by value.
34 private alias MSAAPROPID = GUID;
35 
36 /// `OBJID_CLIENT` from WinUser — the client area object id.
37 private enum DWORD objIdClient = 0xFFFF_FFFC;
38 
39 /// `CHILDID_SELF` — the object itself rather than a child element.
40 private enum DWORD childIdSelf = 0;
41 
42 // CLSID_AccPropServices {b5f8350b-0548-48b1-a6ee-88bd00b4a5e7}
43 private static immutable GUID clsidAccPropServices =
44 	GUID(0xb5f8350b, 0x0548, 0x48b1,
45 		[0xa6, 0xee, 0x88, 0xbd, 0x00, 0xb4, 0xa5, 0xe7]);
46 
47 // IID_IAccPropServices {6e26e776-04f0-495d-80e4-3330352e3169}
48 private static immutable GUID iidAccPropServices =
49 	GUID(0x6e26e776, 0x04f0, 0x495d,
50 		[0x80, 0xe4, 0x33, 0x30, 0x35, 0x2e, 0x31, 0x69]);
51 
52 // PROPID_ACC_NAME {608d3df8-8128-4aa7-a428-f55e49267291}
53 private static immutable MSAAPROPID propIdAccName =
54 	GUID(0x608d3df8, 0x8128, 0x4aa7,
55 		[0xa4, 0x28, 0xf5, 0x5e, 0x49, 0x26, 0x72, 0x91]);
56 
57 /**
58  * Minimal binding for `IAccPropServices`.
59  *
60  * Only `SetHwndPropStr` is called, but every method must be declared in IDL
61  * order so the COM vtable layout is correct. `IAccPropServer` arguments are
62  * declared as `IUnknown` (a pointer-compatible placeholder) since they are
63  * never used here.
64  */
65 private interface IAccPropServices : IUnknown
66 {
67 extern (Windows):
68 	HRESULT SetPropValue(const(BYTE)* pIDString, DWORD dwIDStringLen,
69 		MSAAPROPID idProp, VARIANT var);
70 	HRESULT SetPropServer(const(BYTE)* pIDString, DWORD dwIDStringLen,
71 		const(MSAAPROPID)* paProps, int cProps, IUnknown pServer, int annoScope);
72 	HRESULT ClearProps(const(BYTE)* pIDString, DWORD dwIDStringLen,
73 		const(MSAAPROPID)* paProps, int cProps);
74 	HRESULT SetHwndProp(HWND hwnd, DWORD idObject, DWORD idChild,
75 		MSAAPROPID idProp, VARIANT var);
76 	HRESULT SetHwndPropStr(HWND hwnd, DWORD idObject, DWORD idChild,
77 		MSAAPROPID idProp, const(wchar)* str);
78 	HRESULT SetHwndPropServer(HWND hwnd, DWORD idObject, DWORD idChild,
79 		const(MSAAPROPID)* paProps, int cProps, IUnknown pServer, int annoScope);
80 	HRESULT ClearHwndProps(HWND hwnd, DWORD idObject, DWORD idChild,
81 		const(MSAAPROPID)* paProps, int cProps);
82 	HRESULT ComposeHwndIdentityString(HWND hwnd, DWORD idObject, DWORD idChild,
83 		BYTE** ppIDString, DWORD* pdwIDStringLen);
84 	HRESULT DecomposeHwndIdentityString(const(BYTE)* pIDString,
85 		DWORD dwIDStringLen, HWND* phwnd, DWORD* pidObject, DWORD* pidChild);
86 }
87 
88 /**
89  * Set the accessible name a screen reader announces for a control.
90  *
91  * Has no effect on a null widget or one without a native handle, and fails
92  * silently if the annotation service is unavailable (the control then keeps its
93  * default accessibility, which is correct for most controls).
94  */
95 void setAccessibleName(Widget widget, string name)
96 {
97 	if (widget is null || widget.handle is null)
98 		return;
99 
100 	IAccPropServices services;
101 	HRESULT hr = CoCreateInstance(
102 		&clsidAccPropServices,
103 		null,
104 		CLSCTX.CLSCTX_INPROC_SERVER,
105 		&iidAccPropServices,
106 		cast(void**)&services);
107 
108 	if (hr < 0 || services is null)
109 		return;
110 	scope (exit)
111 		services.Release();
112 
113 	services.SetHwndPropStr(
114 		widget.handle,
115 		objIdClient,
116 		childIdSelf,
117 		propIdAccName,
118 		name.toWStringz);
119 }