a11y uia: Bridge QAccessibleSelectionInterface

Bridge QAccessibleSelectionInterface that was introduced
in commit 9d16d5e224 to
UIA's ISelectionProvider and ISelectionProvider2 interfaces
by extending the existing WindowsUiaSelectionProvider
to make use of the QAccessibleSelectionInterface.

Also make use of that interface to implement handling
for the ISelectionProviderItem interface methods when
an object has a parent that implements the
QAccessibleSelectionInterface.

Sample use from NVDA's Python console [1] with this
commit in place:

1) start NVDA
2) run the interview example (examples\widgets\itemviews\interview\interview.exe)
3) select "Item 1:0", "Item 2:0" and "Item 3:0" by left-clicking
   on "Item 1:0" in the left view, then shift+clicking on "Item 3:0".
4) press Numpad_insert+control+z to start the NVDA Python console and
   capture snapshot variables
5) query and use the interfaces using NVDA's Python console:

    >>> import UIAHandler
    >>> selectionpattern2 = focus.parent.UIAElement.GetCurrentPattern(10034).QueryInterface(UIAHandler.IUIAutomationSelectionPattern2)
    >>> selectionpattern2.CurrentFirstSelectedItem.CurrentName
    'Item 1:0'
    >>> selectionpattern2.CurrentLastSelectedItem.CurrentName
    'Item 3:0'
    >>> selectionpattern2.CurrentItemCount
    3
    >>> selectionitempattern = focus.UIAElement.GetCurrentPattern(10010).QueryInterface(UIAHandler.IUIAutomationSelectionItemPattern)
    >>> selectionitempattern.CurrentIsSelected
    1
    >>> selectionitempattern.RemoveFromSelection()
    0
    >>> selectionitempattern.CurrentIsSelected
    0
    >>> selectionpattern2.CurrentItemCount
    2
    >>> selectionitempattern.Select()
    0
    >>> selectionitempattern.CurrentIsSelected
    1

(Note that calling selectionitempattern.AddToSelection
in that example would also unselect all other currently
selected entries, because the underlying implementation
has a selection mode of QAbstractItemView::SingleSelection
set in QAccessibleTree::selectRow, which gets called
from QAccessibleTable::select via QAccessibleTableCell::selectCell.)

[1] https://www.nvaccess.org/files/nvda/documentation/developerGuide.html#PythonConsole

Change-Id: I7003bae5bbcfd5c685620bf710781165ed70f106
Reviewed-by: Jan Arve Sæther <jan-arve.saether@qt.io>
This commit is contained in:
Michael Weghorn 2023-07-14 12:06:13 +01:00
parent 3bace699bf
commit 4f9c66131d
3 changed files with 110 additions and 46 deletions

View File

@ -305,15 +305,17 @@ HRESULT QWindowsUiaMainProvider::GetPatternProvider(PATTERNID idPattern, IUnknow
break;
case UIA_SelectionPatternId:
case UIA_SelectionPattern2Id:
// Lists of items.
if (accessible->role() == QAccessible::List
// Selections via QAccessibleSelectionInterface or lists of items.
if (accessible->selectionInterface()
|| accessible->role() == QAccessible::List
|| accessible->role() == QAccessible::PageTabList) {
*pRetVal = new QWindowsUiaSelectionProvider(id());
}
break;
case UIA_SelectionItemPatternId:
// Items within a list and radio buttons.
if ((accessible->role() == QAccessible::RadioButton)
// Parent supports selection interface or items within a list and radio buttons.
if ((accessible->parent() && accessible->parent()->selectionInterface())
|| (accessible->role() == QAccessible::RadioButton)
|| (accessible->role() == QAccessible::ListItem)
|| (accessible->role() == QAccessible::PageTab)) {
*pRetVal = new QWindowsUiaSelectionItemProvider(id());

View File

@ -36,6 +36,14 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionItemProvider::Select()
if (!accessible)
return UIA_E_ELEMENTNOTAVAILABLE;
if (QAccessibleInterface *parent = accessible->parent()) {
if (QAccessibleSelectionInterface *selectionInterface = parent->selectionInterface()) {
selectionInterface->clear();
bool ok = selectionInterface->select(accessible);
return ok ? S_OK : S_FALSE;
}
}
QAccessibleActionInterface *actionInterface = accessible->actionInterface();
if (!actionInterface)
return UIA_E_ELEMENTNOTAVAILABLE;
@ -73,6 +81,13 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionItemProvider::AddToSelection()
if (!accessible)
return UIA_E_ELEMENTNOTAVAILABLE;
if (QAccessibleInterface *parent = accessible->parent()) {
if (QAccessibleSelectionInterface *selectionInterface = parent->selectionInterface()) {
bool ok = selectionInterface->select(accessible);
return ok ? S_OK : S_FALSE;
}
}
QAccessibleActionInterface *actionInterface = accessible->actionInterface();
if (!actionInterface)
return UIA_E_ELEMENTNOTAVAILABLE;
@ -98,6 +113,13 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionItemProvider::RemoveFromSelection(
if (!accessible)
return UIA_E_ELEMENTNOTAVAILABLE;
if (QAccessibleInterface *parent = accessible->parent()) {
if (QAccessibleSelectionInterface *selectionInterface = parent->selectionInterface()) {
bool ok = selectionInterface->unselect(accessible);
return ok ? S_OK : S_FALSE;
}
}
QAccessibleActionInterface *actionInterface = accessible->actionInterface();
if (!actionInterface)
return UIA_E_ELEMENTNOTAVAILABLE;
@ -124,6 +146,14 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionItemProvider::get_IsSelected(BOOL
if (!accessible)
return UIA_E_ELEMENTNOTAVAILABLE;
if (QAccessibleInterface *parent = accessible->parent()) {
if (QAccessibleSelectionInterface *selectionInterface = parent->selectionInterface()) {
bool selected = selectionInterface->isSelected(accessible);
*pRetVal = selected ? TRUE : FALSE;
return S_OK;
}
}
if (accessible->role() == QAccessible::RadioButton)
*pRetVal = accessible->state().checked;
else if (accessible->role() == QAccessible::PageTab)
@ -146,12 +176,18 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionItemProvider::get_SelectionContain
if (!accessible)
return UIA_E_ELEMENTNOTAVAILABLE;
QAccessibleInterface *parent = accessible->parent();
if (parent && parent->selectionInterface()) {
*pRetVal = QWindowsUiaMainProvider::providerForAccessible(parent);
return S_OK;
}
QAccessibleActionInterface *actionInterface = accessible->actionInterface();
if (!actionInterface)
return UIA_E_ELEMENTNOTAVAILABLE;
// Radio buttons do not require a container.
if (QAccessibleInterface *parent = accessible->parent()) {
if (parent) {
if ((accessible->role() == QAccessible::ListItem && parent->role() == QAccessible::List)
|| (accessible->role() == QAccessible::PageTab && parent->role() == QAccessible::PageTabList)) {
*pRetVal = QWindowsUiaMainProvider::providerForAccessible(parent);

View File

@ -41,19 +41,23 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionProvider::GetSelection(SAFEARRAY *
if (!accessible)
return UIA_E_ELEMENTNOTAVAILABLE;
// First put selected items in a list, then build a safe array with the right size.
// First get/create list of selected items, then build a safe array with the right size.
QList<QAccessibleInterface *> selectedList;
const int childCount = accessible->childCount();
selectedList.reserve(childCount);
for (int i = 0; i < childCount; ++i) {
if (QAccessibleInterface *child = accessible->child(i)) {
if (accessible->role() == QAccessible::PageTabList) {
if (child->role() == QAccessible::PageTab && child->state().focused) {
selectedList.append(child);
}
} else {
if (child->state().selected) {
selectedList.append(child);
if (QAccessibleSelectionInterface *selectionInterface = accessible->selectionInterface()) {
selectedList = selectionInterface->selectedItems();
} else {
const int childCount = accessible->childCount();
selectedList.reserve(childCount);
for (int i = 0; i < childCount; ++i) {
if (QAccessibleInterface *child = accessible->child(i)) {
if (accessible->role() == QAccessible::PageTabList) {
if (child->role() == QAccessible::PageTab && child->state().focused) {
selectedList.append(child);
}
} else {
if (child->state().selected) {
selectedList.append(child);
}
}
}
}
@ -104,11 +108,15 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionProvider::get_IsSelectionRequired(
// Initially returns false if none are selected. After the first selection, it may be required.
bool anySelected = false;
for (int i = 0; i < accessible->childCount(); ++i) {
if (QAccessibleInterface *child = accessible->child(i)) {
if (child->state().selected) {
anySelected = true;
break;
if (QAccessibleSelectionInterface *selectionInterface = accessible->selectionInterface()) {
anySelected = selectionInterface->selectedItem(0) != nullptr;
} else {
for (int i = 0; i < accessible->childCount(); ++i) {
if (QAccessibleInterface *child = accessible->child(i)) {
if (child->state().selected) {
anySelected = true;
break;
}
}
}
}
@ -131,17 +139,23 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionProvider::get_FirstSelectedItem(__
return UIA_E_ELEMENTNOTAVAILABLE;
QAccessibleInterface *firstSelectedChild = nullptr;
int i = 0;
while (!firstSelectedChild && i < accessible->childCount()) {
if (QAccessibleInterface *child = accessible->child(i)) {
if (accessible->role() == QAccessible::PageTabList) {
if (child->role() == QAccessible::PageTab && child->state().focused)
if (QAccessibleSelectionInterface *selectionInterface = accessible->selectionInterface()) {
firstSelectedChild = selectionInterface->selectedItem(0);
if (!firstSelectedChild)
return UIA_E_ELEMENTNOTAVAILABLE;
} else {
int i = 0;
while (!firstSelectedChild && i < accessible->childCount()) {
if (QAccessibleInterface *child = accessible->child(i)) {
if (accessible->role() == QAccessible::PageTabList) {
if (child->role() == QAccessible::PageTab && child->state().focused)
firstSelectedChild = child;
} else if (child->state().selected) {
firstSelectedChild = child;
} else if (child->state().selected) {
firstSelectedChild = child;
}
}
i++;
}
i++;
}
if (!firstSelectedChild)
@ -169,17 +183,24 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionProvider::get_LastSelectedItem(__R
return UIA_E_ELEMENTNOTAVAILABLE;
QAccessibleInterface *lastSelectedChild = nullptr;
int i = accessible->childCount() - 1;
while (!lastSelectedChild && i >= 0) {
if (QAccessibleInterface *child = accessible->child(i)) {
if (accessible->role() == QAccessible::PageTabList) {
if (child->role() == QAccessible::PageTab && child->state().focused)
if (QAccessibleSelectionInterface *selectionInterface = accessible->selectionInterface()) {
const int selectedItemCount = selectionInterface->selectedItemCount();
if (selectedItemCount <= 0)
return UIA_E_ELEMENTNOTAVAILABLE;
lastSelectedChild = selectionInterface->selectedItem(selectedItemCount - 1);
} else {
int i = accessible->childCount() - 1;
while (!lastSelectedChild && i >= 0) {
if (QAccessibleInterface *child = accessible->child(i)) {
if (accessible->role() == QAccessible::PageTabList) {
if (child->role() == QAccessible::PageTab && child->state().focused)
lastSelectedChild = child;
} else if (child->state().selected) {
lastSelectedChild = child;
} else if (child->state().selected) {
lastSelectedChild = child;
}
}
i--;
}
i--;
}
if (!lastSelectedChild)
@ -212,19 +233,24 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionProvider::get_ItemCount(__RPC__out
if (!accessible)
return UIA_E_ELEMENTNOTAVAILABLE;
int selectedCount = 0;
for (int i = 0; i < accessible->childCount(); i++) {
if (QAccessibleInterface *child = accessible->child(i)) {
if (accessible->role() == QAccessible::PageTabList) {
if (child->role() == QAccessible::PageTab && child->state().focused)
if (QAccessibleSelectionInterface *selectionInterface = accessible->selectionInterface())
*pRetVal = selectionInterface->selectedItemCount();
else {
int selectedCount = 0;
for (int i = 0; i < accessible->childCount(); i++) {
if (QAccessibleInterface *child = accessible->child(i)) {
if (accessible->role() == QAccessible::PageTabList) {
if (child->role() == QAccessible::PageTab && child->state().focused)
selectedCount++;
} else if (child->state().selected) {
selectedCount++;
} else if (child->state().selected) {
selectedCount++;
}
}
}
*pRetVal = selectedCount;
}
*pRetVal = selectedCount;
return S_OK;
}