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 }