From 4f9c66131d6bd4c61ef59d749ed2ec0ce402c409 Mon Sep 17 00:00:00 2001 From: Michael Weghorn Date: Fri, 14 Jul 2023 12:06:13 +0100 Subject: [PATCH] a11y uia: Bridge QAccessibleSelectionInterface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bridge QAccessibleSelectionInterface that was introduced in commit 9d16d5e2245c26e5746fd7609300b84a2a983457 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 --- .../uiautomation/qwindowsuiamainprovider.cpp | 10 +- .../qwindowsuiaselectionitemprovider.cpp | 38 +++++- .../qwindowsuiaselectionprovider.cpp | 108 +++++++++++------- 3 files changed, 110 insertions(+), 46 deletions(-) diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp index 93b9d705c0..6ee4c9082a 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp @@ -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()); diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionitemprovider.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionitemprovider.cpp index 7e829b24a7..95bc2f7570 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionitemprovider.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionitemprovider.cpp @@ -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); diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionprovider.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionprovider.cpp index b5bf6c12e8..37148c655a 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionprovider.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionprovider.cpp @@ -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 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; }