summaryrefslogtreecommitdiff
path: root/prev/wintable/accessibility.h
diff options
context:
space:
mode:
Diffstat (limited to 'prev/wintable/accessibility.h')
-rw-r--r--prev/wintable/accessibility.h1081
1 files changed, 1081 insertions, 0 deletions
diff --git a/prev/wintable/accessibility.h b/prev/wintable/accessibility.h
new file mode 100644
index 0000000..ff7364b
--- /dev/null
+++ b/prev/wintable/accessibility.h
@@ -0,0 +1,1081 @@
+// 24 december 2014
+
+// implement MSAA-conformant accessibility
+// we need to use MSAA because UI Automation is too new for us
+// unfortunately, MSAA's documentation is... very poor. very ambiguous, very inconsistent (just run through this file's commit history and watch the TODO progression to see)
+// resources:
+// http://msdn.microsoft.com/en-us/library/ms971338.aspx
+// http://msdn.microsoft.com/en-us/library/windows/desktop/cc307844.aspx
+// http://msdn.microsoft.com/en-us/library/windows/desktop/cc307847.aspx
+// http://blogs.msdn.com/b/saraford/archive/2004/08/20/which-controls-support-which-msaa-properties-and-how-these-controls-implement-msaa-properties.aspx
+// http://msdn.microsoft.com/en-us/library/ms971325
+// http://msdn.microsoft.com/en-us/library/windows/desktop/dd318017%28v=vs.85%29.aspx
+// http://msdn.microsoft.com/en-us/library/windows/desktop/dd373624%28v=vs.85%29.aspx
+
+// notes:
+// - TODO figure out what to do about header
+// - a row extends as far right as the right edge of the last cell in the row; anything to the right of that is treated as table space (just like with mouse selection)
+// - this has the added effect that hit-testing can only ever return either the table or a cell, never a row
+// - cells have no children; checkbox cells are themselves the accessible object
+// - TODO if we ever add combobox columns, this will need to change somehow
+// - only Table and Cell can have focus; only Row can have selection
+// - TODO allow selecting a cell?
+
+// TODOs:
+// - make sure E_POINTER and RPC_E_DISCONNECTED are correct returns for IAccessible
+// - return last error on newTableAcc() in all accessible functions
+// - figure out what should be names and what should be values
+// - figure out what to do about that header row
+// - http://acccheck.codeplex.com/
+
+// uncomment this to debug table linked list management
+//#define TABLE_DEBUG_LINKEDLIST
+
+// TODO get rid of this
+typedef struct tableAccWhat tableAccWhat;
+
+struct tableAccWhat {
+ LONG role;
+ intptr_t row;
+ intptr_t column;
+};
+
+struct tableAcc {
+ const IAccessibleVtbl *vtbl;
+ ULONG refcount;
+ struct table *t;
+ IAccessible *std;
+ tableAccWhat what;
+
+ // the list of currently active accessibility objects is a doubly linked list
+ struct tableAcc *prev;
+ struct tableAcc *next;
+};
+
+#ifdef TABLE_DEBUG_LINKEDLIST
+void list(struct table *t)
+{
+ struct tableAcc *ta;
+
+ printf("\n");
+ if (t->firstAcc == NULL) {
+ printf("\tempty\n");
+ return;
+ }
+ printf("\t-> ");
+ for (ta = t->firstAcc; ta != NULL; ta = ta->next)
+ printf("%p ", ta);
+ printf("\n\t<- ");
+ for (ta = t->firstAcc; ta->next != NULL; ta = ta->next)
+ ;
+ for (; ta != NULL; ta = ta->prev)
+ printf("%p ", ta);
+ printf("\n");
+}
+#endif
+
+// called after each allocation
+static struct tableAcc *newTableAcc(struct table *t, LONG role, intptr_t row, intptr_t column);
+
+// common validation for accessibility functions that take varChild
+// also normalizes what as if varChild == CHILDID_SELF
+static HRESULT normalizeWhat(struct tableAcc *ta, VARIANT varChild, tableAccWhat *what)
+{
+ LONG cid;
+
+ if (varChild.vt != VT_I4)
+ return E_INVALIDARG;
+ cid = varChild.lVal;
+ if (cid == CHILDID_SELF)
+ return S_OK;
+ cid--;
+ if (cid < 0)
+ return E_INVALIDARG;
+ switch (what->role) {
+ case ROLE_SYSTEM_TABLE:
+ // TODO +1?
+ if (cid >= ta->t->count)
+ return E_INVALIDARG;
+ what->role = ROLE_SYSTEM_ROW;
+ what->row = (intptr_t) cid;
+ what->column = -1;
+ break;
+ case ROLE_SYSTEM_ROW:
+ case ROLE_SYSTEM_CELL:
+ // TODO
+ // TODO also figure out what to do if the current row/cell become invalid (rows being removed, etc.)
+ break;
+ }
+ return S_OK;
+}
+
+#define TA ((struct tableAcc *) this)
+
+static HRESULT STDMETHODCALLTYPE tableAccQueryInterface(IAccessible *this, REFIID riid, void **ppvObject)
+{
+ if (ppvObject == NULL)
+ return E_POINTER;
+ if (IsEqualIID(riid, &IID_IUnknown) ||
+ IsEqualIID(riid, &IID_IDispatch) ||
+ IsEqualIID(riid, &IID_IAccessible)) {
+ IAccessible_AddRef(this);
+ *ppvObject = (void *) this;
+ return S_OK;
+ }
+ *ppvObject = NULL;
+ return E_NOINTERFACE;
+}
+
+// TODO use InterlockedIncrement()/InterlockedDecrement() for these?
+
+static ULONG STDMETHODCALLTYPE tableAccAddRef(IAccessible *this)
+{
+ TA->refcount++;
+ // TODO correct?
+ return TA->refcount;
+}
+
+static ULONG STDMETHODCALLTYPE tableAccRelease(IAccessible *this)
+{
+ TA->refcount--;
+ if (TA->refcount == 0) {
+ struct tableAcc *prev, *next;
+
+#ifdef TABLE_DEBUG_LINKEDLIST
+if (TA->t != NULL) { printf("before delete:"); list(TA->t); }
+#endif
+ if (TA->t != NULL && TA->t->firstAcc == TA)
+ TA->t->firstAcc = TA->next;
+ prev = TA->prev;
+ next = TA->next;
+ if (prev != NULL)
+ prev->next = next;
+ if (next != NULL)
+ next->prev = prev;
+#ifdef TABLE_DEBUG_LINKEDLIST
+if (TA->t != NULL) { printf("after delete:"); list(TA->t); }
+#endif
+ if (TA->std != NULL)
+ IAccessible_Release(TA->std);
+ tableFree(TA, "error freeing Table accessibility object");
+ return 0;
+ }
+ return TA->refcount;
+}
+
+// IDispatch
+// TODO make sure relegating these to the standard accessibility object is harmless
+
+static HRESULT STDMETHODCALLTYPE tableAccGetTypeInfoCount(IAccessible *this, UINT *pctinfo)
+{
+ if (TA->t == NULL || TA->std == NULL) {
+ // TODO set values on error
+ return RPC_E_DISCONNECTED;
+ }
+ return IAccessible_GetTypeInfoCount(TA->std, pctinfo);
+}
+
+static HRESULT STDMETHODCALLTYPE tableAccGetTypeInfo(IAccessible *this, UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo)
+{
+ if (TA->t == NULL || TA->std == NULL) {
+ // TODO set values on error
+ return RPC_E_DISCONNECTED;
+ }
+ return IAccessible_GetTypeInfo(TA->std, iTInfo, lcid, ppTInfo);
+}
+
+static HRESULT STDMETHODCALLTYPE tableAccGetIDsOfNames(IAccessible *this, REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId)
+{
+ if (TA->t == NULL || TA->std == NULL) {
+ // TODO set values on error
+ return RPC_E_DISCONNECTED;
+ }
+ return IAccessible_GetIDsOfNames(TA->std, riid, rgszNames, cNames, lcid, rgDispId);
+}
+
+static HRESULT STDMETHODCALLTYPE tableAccInvoke(IAccessible *this, DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
+{
+ if (TA->t == NULL || TA->std == NULL) {
+ // TODO set values on error
+ return RPC_E_DISCONNECTED;
+ }
+ return IAccessible_Invoke(TA->std, dispIdMember, riid, lcid, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr);
+}
+
+// IAccessible
+
+static HRESULT STDMETHODCALLTYPE tableAccget_accParent(IAccessible *this, IDispatch **ppdispParent)
+{
+ if (ppdispParent == NULL)
+ return E_POINTER;
+ // TODO set ppdispParent to zero?
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ // TODO check if row/column is still valid
+ switch (TA->what.role) {
+ case ROLE_SYSTEM_TABLE:
+ // defer to standard accessible object
+ // TODO [EDGE CASE/POOR DOCUMENTATION?] https://msdn.microsoft.com/en-us/library/ms971325 says "Returns the IDispatch interface of the Table object."; isn't that just returning self?
+ return IAccessible_get_accParent(TA->std, ppdispParent);
+ case ROLE_SYSTEM_ROW:
+ *ppdispParent = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_TABLE, -1, -1);
+ return S_OK;
+ case ROLE_SYSTEM_CELL:
+ *ppdispParent = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_ROW, TA->what.row, -1);
+ return S_OK;
+ }
+ // TODO actually do this right
+ // TODO un-GetLastError() this
+ panic("impossible blah blah blah TODO write this");
+ return E_FAIL;
+}
+
+static HRESULT STDMETHODCALLTYPE tableAccget_accChildCount(IAccessible *this, long *pcountChildren)
+{
+ if (pcountChildren == NULL)
+ return E_POINTER;
+ // TODO set pcountChildren to zero?
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ switch (TA->what.role) {
+ case ROLE_SYSTEM_TABLE:
+ // TODO header row
+ *pcountChildren = (long) (TA->t->count);
+ return S_OK;
+ case ROLE_SYSTEM_ROW:
+ *pcountChildren = (long) (TA->t->nColumns);
+ return S_OK;
+ case ROLE_SYSTEM_CELL:
+ *pcountChildren = 0;
+ return S_OK;
+ }
+ // TODO actually do this right
+ // TODO un-GetLastError() this
+ panic("impossible blah blah blah TODO write this");
+ return E_FAIL;
+}
+
+// TODO [EDGE CASE/NOT DOCUMENTED/CHECK SAMPLE] what happens if CHILDID_SELF is passed?
+// TODO [EDGE CASE/NOT DOCUMENTED/CHECK SAMPLE] what SHOULD happen if an out of bounds ID is passed?
+static HRESULT STDMETHODCALLTYPE tableAccget_accChild(IAccessible *this, VARIANT varChild, IDispatch **ppdispChild)
+{
+ LONG cid;
+
+ if (ppdispChild == NULL)
+ return E_POINTER;
+ *ppdispChild = NULL;
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ if (varChild.vt != VT_I4)
+ return E_INVALIDARG;
+ cid = varChild.lVal;
+ if (cid < 0)
+ // TODO really?
+ return E_INVALIDARG;
+ if (cid == CHILDID_SELF)
+ return E_FAIL; // TODO
+ cid--;
+ switch (TA->what.role) {
+ case ROLE_SYSTEM_TABLE:
+ // TODO table header
+ if (TA->t->count == 0)
+ return S_FALSE;
+ if (cid > TA->t->count - 1)
+ // TODO really?
+ return E_INVALIDARG;
+ *ppdispChild = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_ROW, cid, -1);
+ return S_OK;
+ case ROLE_SYSTEM_ROW:
+ // TODO verify that row is still valid
+ if (TA->t->nColumns == 0)
+ return S_FALSE;
+ if (cid > TA->t->nColumns - 1)
+ // TODO really?
+ return E_INVALIDARG;
+ *ppdispChild = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_CELL, TA->what.row, cid);
+ case ROLE_SYSTEM_CELL:
+ // TODO verify that row/column are still valid?
+ return S_FALSE;
+ }
+ // TODO actually do this right
+ // TODO un-GetLastError() this
+ panic("impossible blah blah blah TODO write this");
+ return E_FAIL;
+}
+
+static HRESULT STDMETHODCALLTYPE tableAccget_accName(IAccessible *this, VARIANT varChild, BSTR *pszName)
+{
+ HRESULT hr;
+ tableAccWhat what;
+
+ if (pszName == NULL)
+ return E_POINTER;
+ // TODO double-check that this must be set to zero
+ *pszName = NULL;
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ what = TA->what;
+ hr = normalizeWhat(TA, varChild, &what);
+ if (hr != S_OK)
+ return hr;
+ switch (what.role) {
+ case ROLE_SYSTEM_TABLE:
+ // defer to standard accessible object
+ return IAccessible_get_accName(TA->std, varChild, pszName);
+ case ROLE_SYSTEM_ROW:
+ // TODO
+ return S_FALSE;
+ case ROLE_SYSTEM_CELL:
+ // TODO
+ return S_FALSE;
+ }
+ // TODO actually do this right
+ // TODO un-GetLastError() this
+ panic("impossible blah blah blah TODO write this");
+ return E_FAIL;
+}
+
+// this differs quite much from what is described at https://msdn.microsoft.com/en-us/library/ms971325
+static HRESULT STDMETHODCALLTYPE tableAccget_accValue(IAccessible *this, VARIANT varChild, BSTR *pszValue)
+{
+ HRESULT hr;
+ tableAccWhat what;
+ WCHAR *text;
+
+ if (pszValue == NULL)
+ return E_POINTER;
+ // TODO set pszValue to zero?
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ what = TA->what;
+ hr = normalizeWhat(TA, varChild, &what);
+ if (hr != S_OK)
+ return hr;
+ switch (what.role) {
+ case ROLE_SYSTEM_TABLE:
+ // TODO really?
+ return IAccessible_get_accValue(TA->std, varChild, pszValue);
+ case ROLE_SYSTEM_ROW:
+ // TODO
+ return DISP_E_MEMBERNOTFOUND;
+ case ROLE_SYSTEM_CELL:
+ switch (TA->t->columnTypes[what.column]) {
+ case tableColumnText:
+ text = getCellText(TA->t, what.row, what.column);
+ // TODO check for error
+ *pszValue = SysAllocString(text);
+ returnCellData(TA->t, what.row, what.column, text);
+ return S_OK;
+ case tableColumnImage:
+ // TODO
+ return DISP_E_MEMBERNOTFOUND;
+ case tableColumnCheckbox:
+ // TODO!!!!!!
+ return DISP_E_MEMBERNOTFOUND;
+ }
+ }
+ // TODO actually do this right
+ // TODO un-GetLastError() this
+ panic("impossible blah blah blah TODO write this");
+ return E_FAIL;
+}
+
+static HRESULT STDMETHODCALLTYPE tableAccget_accDescription(IAccessible *this, VARIANT varChild, BSTR *pszDescription)
+{
+ HRESULT hr;
+ tableAccWhat what;
+
+ if (pszDescription == NULL)
+ return E_POINTER;
+ *pszDescription = NULL;
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ what = TA->what;
+ hr = normalizeWhat(TA, varChild, &what);
+ if (hr != S_OK)
+ return hr;
+ // don't support descriptions anyway; do return the above errors just to be safe
+ return DISP_E_MEMBERNOTFOUND;
+}
+
+static HRESULT STDMETHODCALLTYPE tableAccget_accRole(IAccessible *this, VARIANT varChild, VARIANT *pvarRole)
+{
+ HRESULT hr;
+ tableAccWhat what;
+
+ if (pvarRole == NULL)
+ return E_POINTER;
+ pvarRole->vt = VT_EMPTY;
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ what = TA->what;
+ hr = normalizeWhat(TA, varChild, &what);
+ if (hr != S_OK)
+ return hr;
+ pvarRole->vt = VT_I4;
+ pvarRole->lVal = what.role;
+ return S_OK;
+}
+
+// TODO reason about STATE_SYSTEM_INVISIBLE and STATE_SYSTEM_OFFSCREEN
+static HRESULT STDMETHODCALLTYPE tableAccget_accState(IAccessible *this, VARIANT varChild, VARIANT *pvarState)
+{
+ HRESULT hr;
+ tableAccWhat what;
+ LONG state;
+
+ if (pvarState == NULL)
+ return E_POINTER;
+ pvarState->vt = VT_EMPTY;
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ what = TA->what;
+ hr = normalizeWhat(TA, varChild, &what);
+ if (hr != S_OK)
+ return hr;
+
+ state = 0;
+ switch (what.role) {
+ case ROLE_SYSTEM_TABLE:
+ hr = IAccessible_get_accState(TA->std, varChild, pvarState);
+ if (hr != S_OK)
+ return hr;
+ // TODO make sure pvarState->vt == VT_I4 (what to return otherwise?)
+ state |= pvarState->lVal;
+ break;
+ case ROLE_SYSTEM_ROW:
+ state |= STATE_SYSTEM_SELECTABLE;
+ if (TA->t->selectedRow == what.row)
+ state |= STATE_SYSTEM_SELECTED;
+ break;
+ case ROLE_SYSTEM_CELL:
+ if (TA->t->columnTypes[what.column] == tableColumnCheckbox) {
+ // TODO is there no STATE_SYSTEM_CHECKABLE?
+ if (isCheckboxChecked(TA->t, what.row, what.column))
+ state |= STATE_SYSTEM_CHECKED;
+ }
+ state |= STATE_SYSTEM_FOCUSABLE;
+ if (TA->t->selectedRow == what.row && TA->t->selectedColumn == what.column)
+ state |= STATE_SYSTEM_FOCUSED;
+ if (TA->t->columnTypes[what.column] != tableColumnCheckbox)
+ state |= STATE_SYSTEM_READONLY;
+ break;
+ }
+ pvarState->vt = VT_I4;
+ pvarState->lVal = state;
+ return S_OK;
+}
+
+static HRESULT STDMETHODCALLTYPE tableAccget_accHelp(IAccessible *this, VARIANT varChild, BSTR *pszHelp)
+{
+ HRESULT hr;
+ tableAccWhat what;
+
+ if (pszHelp == NULL)
+ return E_POINTER;
+ *pszHelp = NULL;
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ what = TA->what;
+ hr = normalizeWhat(TA, varChild, &what);
+ if (hr != S_OK)
+ return hr;
+ // don't support help anyway; do return the above errors just to be safe
+ return DISP_E_MEMBERNOTFOUND;
+}
+
+// TODO Inspect.exe seems to ignore the DISP_E_MEMBERNOTFOUND and just tells us the help topic is the empty string; make sure this works right
+static HRESULT STDMETHODCALLTYPE tableAccget_accHelpTopic(IAccessible *this, BSTR *pszHelpFile, VARIANT varChild, long *pidTopic)
+{
+ HRESULT hr;
+ tableAccWhat what;
+
+ if (pszHelpFile == NULL || pidTopic == NULL)
+ return E_POINTER;
+ // TODO set pszHelpFile and pidTopic to zero?
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ what = TA->what;
+ hr = normalizeWhat(TA, varChild, &what);
+ if (hr != S_OK)
+ return hr;
+ // don't support Windows Help (the super-old .hlp help files) topics anyway; do return the above errors just to be safe
+ // TODO [EDGE CASE??] or should we defer back to the standard accessible object? get_accHelp() was explicitly documented as not being supported by the standard/common controls, but this one isn't...
+ return DISP_E_MEMBERNOTFOUND;
+}
+
+static HRESULT STDMETHODCALLTYPE tableAccget_accKeyboardShortcut(IAccessible *this, VARIANT varChild, BSTR *pszKeyboardShortcut)
+{
+ HRESULT hr;
+ tableAccWhat what;
+
+ if (pszKeyboardShortcut == NULL)
+ return E_POINTER;
+ // TODO set pszKeyboardShortcut to zero?
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ what = TA->what;
+ hr = normalizeWhat(TA, varChild, &what);
+ if (hr != S_OK)
+ return hr;
+ // defer to the standard accessible object for the table itself in case a program assigns an access key somehow (adjacent label?); MSDN says to, anyway
+ if (what.role == ROLE_SYSTEM_TABLE)
+ return IAccessible_get_accKeyboardShortcut(TA->std, varChild, pszKeyboardShortcut);
+ if (what.role == ROLE_SYSTEM_CELL)
+ ; // TODO implement this for checkbox cells?
+ return DISP_E_MEMBERNOTFOUND;
+}
+
+// TODO TEST THIS
+// TODO [EDGE CASE??] no parents?
+static HRESULT STDMETHODCALLTYPE tableAccget_accFocus(IAccessible *this, VARIANT *pvarChild)
+{
+ HRESULT hr;
+
+ if (pvarChild == NULL)
+ return E_POINTER;
+ // TODO set pvarChild to empty?
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ // TODO verify that TA is still pointing to a valid row/column
+
+ // first see if the control has the focus
+ // this is why a standard accessible object is needed on all accessible objects
+ hr = IAccessible_get_accFocus(TA->std, pvarChild);
+ // check the pvarChild type instead of hr
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/dd318479%28v=vs.85%29.aspx does this
+ // TODO [EDGE CASE] figure out why
+ if (pvarChild->vt != VT_I4)
+ return hr;
+
+ switch (TA->what.role) {
+ case ROLE_SYSTEM_TABLE:
+ if (TA->t->selectedRow != -1 && TA->t->selectedColumn != -1)
+ goto selectedCell;
+ goto self;
+ case ROLE_SYSTEM_ROW:
+ if (TA->t->selectedRow != TA->what.row)
+ goto nothing;
+ goto selectedCell;
+ case ROLE_SYSTEM_CELL:
+ if (TA->t->selectedRow != TA->what.row)
+ goto nothing;
+ if (TA->t->selectedColumn != TA->what.column)
+ goto nothing;
+ goto self;
+ }
+ // TODO actually do this right
+ // TODO un-GetLastError() this
+ panic("impossible blah blah blah TODO write this");
+ return E_FAIL;
+
+nothing:
+ pvarChild->vt = VT_EMPTY;
+ // TODO really this one?
+ return S_FALSE;
+
+self:
+ pvarChild->vt = VT_I4;
+ pvarChild->lVal = CHILDID_SELF;
+ return S_OK;
+
+selectedCell:
+ pvarChild->vt = VT_I4;
+ pvarChild->pdispVal = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_CELL, TA->what.row, TA->what.column);
+ return S_OK;
+}
+
+// note: https://msdn.microsoft.com/en-us/library/ms971325 is geared toward cell-based selection
+// we have row-based selection, so only Tables implement this method, and they return a row
+static HRESULT STDMETHODCALLTYPE tableAccget_accSelection(IAccessible *this, VARIANT *pvarChildren)
+{
+ if (pvarChildren == NULL)
+ return E_POINTER;
+ // TOOD set pvarChildren to VT_EMPTY?
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ if (TA->what.role != ROLE_SYSTEM_TABLE)
+ // TODO [EDGE CASE] implement this for row anyway? how?
+ return DISP_E_MEMBERNOTFOUND;
+ if (TA->t->selectedRow == -1) {
+ pvarChildren->vt = VT_EMPTY;
+ return S_OK;
+ }
+ pvarChildren->vt = VT_DISPATCH;
+ pvarChildren->pdispVal = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_ROW, TA->t->selectedRow, -1);
+ return S_OK;
+}
+
+static HRESULT STDMETHODCALLTYPE tableAccget_accDefaultAction(IAccessible *this, VARIANT varChild, BSTR *pszDefaultAction)
+{
+ HRESULT hr;
+ tableAccWhat what;
+
+ if (pszDefaultAction == NULL)
+ return E_POINTER;
+ *pszDefaultAction = NULL;
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ what = TA->what;
+ hr = normalizeWhat(TA, varChild, &what);
+ if (hr != S_OK)
+ return hr;
+ if (what.role == ROLE_SYSTEM_CELL)
+ ; // TODO implement this for checkbox cells?
+ return DISP_E_MEMBERNOTFOUND;
+}
+
+// TODO should this method result in an event?
+// TODO [EDGE CASE] how do we deselect? in the table or in the row? wouldn't this go against multiple selection?
+// TODO require cell rows to be selected before focusing?
+static HRESULT STDMETHODCALLTYPE tableAccaccSelect(IAccessible *this, long flagsSelect, VARIANT varChild)
+{
+ HRESULT hr;
+ tableAccWhat what;
+
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ what = TA->what;
+ hr = normalizeWhat(TA, varChild, &what);
+ if (hr != S_OK)
+ return hr;
+ if (what.role == ROLE_SYSTEM_TABLE) // defer to the standard accessible object
+ return IAccessible_accSelect(TA->std, flagsSelect, varChild);
+ // reject flags that are only applicable to multiple selection
+ if ((flagsSelect & (SELFLAG_EXTENDSELECTION | SELFLAG_ADDSELECTION | SELFLAG_REMOVESELECTION)) != 0)
+ return E_INVALIDARG;
+ // and do nothing if a no-op
+ if (flagsSelect == SELFLAG_NONE)
+ return S_FALSE;
+ // TODO cast ~ expressions to the correct type
+ switch (what.role) {
+ case ROLE_SYSTEM_ROW:
+ // reject any other flag
+ if ((flagsSelect & (~SELFLAG_TAKESELECTION)) != 0)
+ return E_INVALIDARG;
+ if ((flagsSelect & SELFLAG_TAKESELECTION) != 0) {
+ if (TA->t->nColumns == 0) // can't select
+ return S_FALSE;
+ // if no column selected, select first (congruent to behavior of certain keyboard events)
+ // TODO handle GetLastError()
+ if (TA->t->selectedColumn == -1)
+ doselect(TA->t, what.row, TA->t->selectedColumn);
+ else
+ doselect(TA->t, what.row, 0);
+ return S_OK;
+ }
+ return S_FALSE;
+ case ROLE_SYSTEM_CELL:
+ // reject any other flag
+ if ((flagsSelect & (~SELFLAG_TAKEFOCUS)) != 0)
+ return E_INVALIDARG;
+ if ((flagsSelect & SELFLAG_TAKEFOCUS) != 0) {
+ doselect(TA->t, what.row, what.column);
+ return S_OK;
+ }
+ return S_FALSE;
+ }
+ // TODO actually do this right
+ // TODO un-GetLastError() this
+ panic("impossible blah blah blah TODO write this");
+ return E_FAIL;
+}
+
+static HRESULT STDMETHODCALLTYPE tableAccaccLocation(IAccessible *this, long *pxLeft, long *pyTop, long *pcxWidth, long *pcyHeight, VARIANT varChild)
+{
+ HRESULT hr;
+ tableAccWhat what;
+ RECT r;
+ POINT pt;
+ struct rowcol rc;
+
+ if (pxLeft == NULL || pyTop == NULL || pcxWidth == NULL || pcyHeight == NULL)
+ return E_POINTER;
+ // TODO set the out parameters to zero?
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ what = TA->what;
+ hr = normalizeWhat(TA, varChild, &what);
+ if (hr != S_OK)
+ return hr;
+ switch (what.role) {
+ case ROLE_SYSTEM_TABLE:
+ return IAccessible_accLocation(TA->std, pxLeft, pyTop, pcxWidth, pcyHeight, varChild);
+ case ROLE_SYSTEM_ROW:
+ // TODO actually write this
+ return E_FAIL;
+ case ROLE_SYSTEM_CELL:
+ rc.row = what.row;
+ rc.column = what.column;
+ if (!rowColumnToClientRect(TA->t, rc, &r)) {
+ // TODO [EDGE CASE] what do we do here?
+ // TODO we have to return something indicating that the object is off-screen
+ }
+ // TODO [EDGE CASE] intersect with client rect?
+ break;
+ }
+ pt.x = r.left;
+ pt.y = r.top;
+ if (ClientToScreen(TA->t->hwnd, &pt) == 0)
+ return HRESULT_FROM_WIN32(GetLastError());
+ *pxLeft = pt.x;
+ *pyTop = pt.y;
+ *pcxWidth = r.right - r.left;
+ *pcyHeight = r.bottom - r.top;
+ return S_OK;
+}
+
+static HRESULT STDMETHODCALLTYPE tableAccaccNavigate(IAccessible *this, long navDir, VARIANT varStart, VARIANT *pvarEndUpAt)
+{
+ HRESULT hr;
+ tableAccWhat what;
+ intptr_t row = -1;
+ intptr_t column = -1;
+
+ if (pvarEndUpAt == NULL)
+ return E_POINTER;
+ // TODO set pvarEndUpAt to an invalid value?
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ what = TA->what;
+ hr = normalizeWhat(TA, varStart, &what);
+ if (hr != S_OK)
+ return hr;
+ switch (what.role) {
+ case ROLE_SYSTEM_TABLE:
+ switch (navDir) {
+ case NAVDIR_FIRSTCHILD:
+ // TODO header row
+ if (TA->t->count == 0)
+ goto nowhere;
+ row = 0;
+ goto specificRow;
+ case NAVDIR_LASTCHILD:
+ // TODO header row
+ if (TA->t->count == 0)
+ goto nowhere;
+ row = TA->t->count - 1;
+ goto specificRow;
+ }
+ // otherwise, defer to the standard accessible object
+ return IAccessible_accNavigate(TA->std, navDir, varStart, pvarEndUpAt);
+ case ROLE_SYSTEM_ROW:
+ row = what.row;
+ switch (navDir) {
+ case NAVDIR_UP:
+ case NAVDIR_PREVIOUS:
+ if (row == 0) // can't go up
+ goto nowhere;
+ row--;
+ // row should still be valid because normalizeWhat() returns an error if the current row is no longer valid, and if that's valid, the row above it should also be valid
+ goto specificRow;
+ case NAVDIR_DOWN:
+ case NAVDIR_NEXT:
+ if (row == TA->t->count - 1) // can't go down
+ goto nowhere;
+ row++;
+ // row should still be valid by the above conjecture
+ goto specificRow;
+ case NAVDIR_LEFT:
+ case NAVDIR_RIGHT:
+ goto nowhere;
+// TODO this doesn't actually exist yet https://msdn.microsoft.com/en-us/library/ms971325 talks about it
+// case NAVDIR_PARENT:
+// goto tableItself;
+ case NAVDIR_FIRSTCHILD:
+ if (TA->t->nColumns == 0)
+ goto nowhere;
+ column = 0;
+ goto specificCell;
+ case NAVDIR_LASTCHILD:
+ if (TA->t->nColumns == 0)
+ goto nowhere;
+ column = TA->t->nColumns - 1;
+ goto specificCell;
+ }
+ // TODO differentiate between unsupported navigation directions and invalid navigation directions
+ goto nowhere;
+ case ROLE_SYSTEM_CELL:
+ row = what.row;
+ column = what.column;
+ switch (navDir) {
+ case NAVDIR_UP:
+ if (row == 0) // can't go up
+ goto nowhere;
+ row--;
+ goto specificCell;
+ case NAVDIR_DOWN:
+ if (row == TA->t->count - 1) // can't go down
+ goto nowhere;
+ row++;
+ goto specificCell;
+ case NAVDIR_LEFT:
+ case NAVDIR_PREVIOUS:
+ if (column == 0) // can't go left
+ goto nowhere;
+ column--;
+ goto specificCell;
+ case NAVDIR_RIGHT:
+ case NAVDIR_NEXT:
+ if (column == TA->t->nColumns - 1) // can't go right
+ goto nowhere;
+ column++;
+ goto specificCell;
+// TODO this doesn't actually exist yet https://msdn.microsoft.com/en-us/library/ms971325 talks about it
+// case NAVDIR_PARENT:
+// goto specificRow;
+ case NAVDIR_FIRSTCHILD:
+ case NAVDIR_LASTCHILD:
+ goto nowhere;
+ }
+ // TODO differentiate between unsupported navigation directions and invalid navigation directions
+ goto nowhere;
+ }
+ // TODO actually do this right
+ // TODO un-GetLastError() this
+ panic("impossible blah blah blah TODO write this");
+ return E_FAIL;
+
+nowhere:
+ pvarEndUpAt->vt = VT_EMPTY;
+ return S_FALSE;
+
+tableItself:
+ pvarEndUpAt->vt = VT_DISPATCH;
+ pvarEndUpAt->pdispVal = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_TABLE, -1, -1);
+ return S_OK;
+
+specificRow:
+ pvarEndUpAt->vt = VT_DISPATCH;
+ pvarEndUpAt->pdispVal = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_ROW, row, -1);
+ return S_OK;
+
+specificCell:
+ pvarEndUpAt->vt = VT_DISPATCH;
+ pvarEndUpAt->pdispVal = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_CELL, row, column);
+ return S_OK;
+}
+
+// TODO [EDGE CASE??] should this ever return parents?
+static HRESULT STDMETHODCALLTYPE tableAccaccHitTest(IAccessible *this, long xLeft, long yTop, VARIANT *pvarChild)
+{
+ POINT pt;
+ struct rowcol rc;
+ RECT r;
+
+ if (pvarChild == NULL)
+ return E_POINTER;
+ // TODO set pvarChild to an invalid value?
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+
+ pt.x = xLeft;
+ pt.y = yTop;
+ if (ScreenToClient(TA->t->hwnd, &pt) == 0)
+ return HRESULT_FROM_WIN32(GetLastError());
+
+ // first see if the point is even IN the table
+ if (GetClientRect(TA->t->hwnd, &r) == 0)
+ return HRESULT_FROM_WIN32(GetLastError());
+ r.top += TA->t->headerHeight;
+ if (PtInRect(&r, pt) == 0)
+ goto outside;
+
+ // now see if we're in a cell or in the table
+ // TODO also handle GetLastError() here
+ rc = clientCoordToRowColumn(TA->t, pt);
+ switch (TA->what.role) {
+ case ROLE_SYSTEM_TABLE:
+ // either the table or the cell
+ if (rc.row == -1 || rc.column == -1)
+ goto self;
+ goto specificCell;
+ case ROLE_SYSTEM_ROW:
+ // a specific cell, but only if in the same row
+ // TODO actually do we really need these spurious rc.column ==/!= -1 checks?
+ if (rc.row == TA->what.row) {
+ if (rc.column == -1)
+ // TODO de-GetLastError() this
+ panic("impossible situation TODO write this");
+ goto specificCell;
+ }
+ goto outside;
+ case ROLE_SYSTEM_CELL:
+ if (rc.row == TA->what.row && rc.column == TA->what.column)
+ goto self;
+ goto outside;
+ }
+ // TODO actually do this right
+ // TODO un-GetLastError() this
+ panic("impossible blah blah blah TODO write this");
+ return E_FAIL;
+
+outside:
+ pvarChild->vt = VT_EMPTY;
+ return S_FALSE;
+
+self:
+ pvarChild->vt = VT_I4;
+ pvarChild->lVal = CHILDID_SELF;
+ return S_OK;
+
+specificCell:
+ pvarChild->vt = VT_DISPATCH;
+ // TODO GetLastError() here too
+ pvarChild->pdispVal = (IDispatch *) newTableAcc(TA->t, ROLE_SYSTEM_CELL, rc.row, rc.column);
+ return S_OK;
+}
+
+static HRESULT STDMETHODCALLTYPE tableAccaccDoDefaultAction(IAccessible *this, VARIANT varChild)
+{
+ HRESULT hr;
+ tableAccWhat what;
+
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ what = TA->what;
+ hr = normalizeWhat(TA, varChild, &what);
+ if (hr != S_OK)
+ return hr;
+ if (what.role == ROLE_SYSTEM_CELL)
+ ; // TODO implement this for checkbox cells?
+ return DISP_E_MEMBERNOTFOUND;
+}
+
+// inconsistencies, inconsistencies
+// https://msdn.microsoft.com/en-us/library/windows/desktop/dd318491%28v=vs.85%29.aspx says to just return E_NOTIMPL and not even bother with an implementation; in fact it doesn't even *have* the documentation anymore
+// http://blogs.msdn.com/b/saraford/archive/2004/08/20/which-controls-support-which-msaa-properties-and-how-these-controls-implement-msaa-properties.aspx says never to return E_NOTIMPL from an IAccessible method (but it also discounts RPC_E_DISCONNECTED (not explicitly), so I'm guessing this is a much older document)
+// let's just do what our put_accValue() does and do full validation, then just return DISP_E_MEMBERNOTFOUND
+// I really hope UI Automation isn't so ambiguous and inconsistent... too bad I'm still choosing to support Windows XP while its market share (compared to *every other OS ever*) is still as large as it is
+static HRESULT STDMETHODCALLTYPE tableAccput_accName(IAccessible *this, VARIANT varChild, BSTR szName)
+{
+ HRESULT hr;
+ tableAccWhat what;
+
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ what = TA->what;
+ hr = normalizeWhat(TA, varChild, &what);
+ if (hr != S_OK)
+ return hr;
+ // don't support setting values anyway; do return the above errors just to be safe
+ return DISP_E_MEMBERNOTFOUND;
+}
+
+static HRESULT STDMETHODCALLTYPE tableAccput_accValue(IAccessible *this, VARIANT varChild, BSTR szValue)
+{
+ HRESULT hr;
+ tableAccWhat what;
+
+ if (TA->t == NULL || TA->std == NULL)
+ return RPC_E_DISCONNECTED;
+ what = TA->what;
+ hr = normalizeWhat(TA, varChild, &what);
+ if (hr != S_OK)
+ return hr;
+ // don't support setting values anyway; do return the above errors just to be safe
+ // TODO defer ROW_SYSTEM_TABLE to the standard accessible object?
+ // TODO implement for checkboxes?
+ return DISP_E_MEMBERNOTFOUND;
+}
+
+static const IAccessibleVtbl tableAccVtbl = {
+ .QueryInterface = tableAccQueryInterface,
+ .AddRef = tableAccAddRef,
+ .Release = tableAccRelease,
+ .GetTypeInfoCount = tableAccGetTypeInfoCount,
+ .GetTypeInfo = tableAccGetTypeInfo,
+ .GetIDsOfNames = tableAccGetIDsOfNames,
+ .Invoke = tableAccInvoke,
+ .get_accParent = tableAccget_accParent,
+ .get_accChildCount = tableAccget_accChildCount,
+ .get_accChild = tableAccget_accChild,
+ .get_accName = tableAccget_accName,
+ .get_accValue = tableAccget_accValue,
+ .get_accDescription = tableAccget_accDescription,
+ .get_accRole = tableAccget_accRole,
+ .get_accState = tableAccget_accState,
+ .get_accHelp = tableAccget_accHelp,
+ .get_accHelpTopic = tableAccget_accHelpTopic,
+ .get_accKeyboardShortcut = tableAccget_accKeyboardShortcut,
+ .get_accFocus = tableAccget_accFocus,
+ .get_accSelection = tableAccget_accSelection,
+ .get_accDefaultAction = tableAccget_accDefaultAction,
+ .accSelect = tableAccaccSelect,
+ .accLocation = tableAccaccLocation,
+ .accNavigate = tableAccaccNavigate,
+ .accHitTest = tableAccaccHitTest,
+ .accDoDefaultAction = tableAccaccDoDefaultAction,
+ .put_accName = tableAccput_accName,
+ .put_accValue = tableAccput_accValue,
+};
+
+static struct tableAcc *newTableAcc(struct table *t, LONG role, intptr_t row, intptr_t column)
+{
+ struct tableAcc *ta;
+ HRESULT hr;
+ IAccessible *std;
+
+ ta = (struct tableAcc *) tableAlloc(sizeof (struct tableAcc), "error creating Table accessibility object");
+ ta->vtbl = &tableAccVtbl;
+ ta->refcount = 1;
+ ta->t = t;
+ // TODO adjust last argument
+ hr = CreateStdAccessibleObject(t->hwnd, OBJID_CLIENT, &IID_IAccessible, (void **) (&std));
+ if (hr != S_OK)
+ // TODO panichresult
+ panic("error creating standard accessible object for Table");
+ ta->std = std;
+ ta->what.role = role;
+ ta->what.row = row;
+ ta->what.column = column;
+
+#ifdef TABLE_DEBUG_LINKEDLIST
+printf("before add:"); list(t);
+#endif
+ ta->next = t->firstAcc;
+ if (t->firstAcc != NULL)
+ t->firstAcc->prev = ta;
+ t->firstAcc = ta;
+#ifdef TABLE_DEBUG_LINKEDLIST
+printf("after add:"); list(t);
+#endif
+
+ return ta;
+}
+
+static void invalidateTableAccs(struct table *t)
+{
+ struct tableAcc *ta;
+
+ for (ta = t->firstAcc; ta != NULL; ta = ta->next) {
+ ta->t = NULL;
+ IAccessible_Release(ta->std);
+ ta->std = NULL;
+ }
+ t->firstAcc = NULL;
+ NotifyWinEvent(EVENT_OBJECT_DESTROY, t->hwnd, OBJID_CLIENT, CHILDID_SELF);
+}
+
+HANDLER(accessibilityHandler)
+{
+ struct tableAcc *ta;
+
+ if (uMsg != WM_GETOBJECT)
+ return FALSE;
+ // OBJID_CLIENT evaluates to an expression of type LONG
+ // the documentation for WM_GETOBJECT says to cast "it" to a DWORD before comparing
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/dd373624%28v=vs.85%29.aspx casts them both to DWORDs; let's do that
+ // its two siblings only cast lParam, resulting in an erroneous DWORD to LONG comparison
+ // The Old New Thing book does not cast anything
+ // Microsoft's MSAA sample casts lParam to LONG instead!
+ // (As you can probably tell, the biggest problem with MSAA is that its documentation is ambiguous and/or self-contradictory...)
+ if (((DWORD) lParam) != ((DWORD) OBJID_CLIENT))
+ return FALSE;
+ ta = newTableAcc(t, ROLE_SYSTEM_TABLE, -1, -1);
+ *lResult = LresultFromObject(&IID_IAccessible, wParam, (LPUNKNOWN) (ta));
+ // TODO check *lResult
+ // TODO adjust pointer
+ IAccessible_Release((IAccessible *) ta);
+ return TRUE;
+}