diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index bed54661a28..22a729f572a 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -3,7 +3,9 @@ #This file is covered by the GNU General Public License. #See the file COPYING for more details. +from comtypes.automation import IEnumVARIANT, VARIANT from comtypes import COMError, IServiceProvider, GUID +from comtypes.hresult import S_OK, S_FALSE import ctypes import os import re @@ -1214,6 +1216,47 @@ def _get_rowHeaderText(self): def _get_columnHeaderText(self): return self._tableHeaderTextHelper("column") + def _get_selectionContainer(self): + if self.table: + return self.table + return super(IAccessible,self).selectionContainer + + def _getSelectedItemsCount_accSelection(self,maxCount): + sel=self.IAccessibleObject.accSelection + if not sel: + raise NotImplementedError + enumObj=sel.QueryInterface(IEnumVARIANT) + if not enumObj: + raise NotImplementedError + # Call the rawmethod for IEnumVARIANT::Next as COMTypes' overloaded version does not allow limiting the amount of items returned + numItemsFetched=ctypes.c_ulong() + itemsBuf=(VARIANT*(maxCount+1))() + res=enumObj._IEnumVARIANT__com_Next(maxCount,itemsBuf,ctypes.byref(numItemsFetched)) + # IEnumVARIANT returns S_FALSE if the buffer is too small, although it still writes as many as it can. + # For our purposes, we can treat both S_OK and S_FALSE as success. + if res!=S_OK and res!=S_FALSE: + raise COMError(res,None,None) + return numItemsFetched.value if numItemsFetched.value<=maxCount else sys.maxint + + def getSelectedItemsCount(self,maxCount): + # To fetch the number of selected items, we first try MSAA's accSelection, but if that fails in any way, we fall back to using IAccessibleTable2's nSelectedCells, if we are on an IAccessible2 table. + # Currently Chrome does not implement accSelection, thus for Google Sheets we must use nSelectedCells when on a table. + try: + return self._getSelectedItemsCount_accSelection(maxCount) + except (COMError,NotImplementedError) as e: + log.debug("Cannot fetch selected items count using accSelection, %s"%e) + pass + if self.IAccessibleTable2Object: + try: + return self.IAccessibleTable2Object.nSelectedCells + except COMError as e: + log.debug("Error calling IAccessibleTable2::nSelectedCells, %s"%e) + pass + else: + log.debug("No means of getting a selection count from this IAccessible") + return super(IAccessible,self).getSelectedItemsCount(maxCount) + + def _get_table(self): if not isinstance(self.IAccessibleObject,IAccessibleHandler.IAccessible2): return None diff --git a/source/NVDAObjects/__init__.py b/source/NVDAObjects/__init__.py index 3ea34e1dcdc..5de0078a768 100644 --- a/source/NVDAObjects/__init__.py +++ b/source/NVDAObjects/__init__.py @@ -1181,3 +1181,15 @@ def _get__hasNavigableText(self): return True else: return False + + def _get_selectionContainer(self): + """ An ancestor NVDAObject which manages the selection for this object and other descendants.""" + return None + + def getSelectedItemsCount(self,maxCount=2): + """ + Fetches the number of descendants currently selected. + For performance, this method will only count up to the given maxCount number, and if there is one more above that, then sys.maxint is returned stating that many items are selected. + """ + return 0 + diff --git a/source/controlTypes.py b/source/controlTypes.py index d9d138e1f44..68f9293262d 100644 --- a/source/controlTypes.py +++ b/source/controlTypes.py @@ -727,7 +727,18 @@ def processNegativeStates(role, states, reason, negativeStates=None): # but only if it is either focused or this is something other than a change event. # The condition stops "not selected" from being spoken in some broken controls # when the state change for the previous focus is issued before the focus change. - if role in (ROLE_LISTITEM, ROLE_TREEVIEWITEM, ROLE_TABLEROW) and STATE_SELECTABLE in states and (reason != REASON_CHANGE or STATE_FOCUSED in states): + if ( + STATE_SELECTABLE in states + and (reason != REASON_CHANGE or STATE_FOCUSED in states) + and role in ( + ROLE_LISTITEM, + ROLE_TREEVIEWITEM, + ROLE_TABLEROW, + ROLE_TABLECELL, + ROLE_TABLECOLUMNHEADER, + ROLE_TABLEROWHEADER + ) + ): speakNegatives.add(STATE_SELECTED) # Restrict "not checked" in a similar way to "not selected". if (role in (ROLE_CHECKBOX, ROLE_RADIOBUTTON, ROLE_CHECKMENUITEM) or STATE_CHECKABLE in states) and (STATE_HALFCHECKED not in states) and (reason != REASON_CHANGE or STATE_FOCUSED in states): diff --git a/source/speech.py b/source/speech.py index 571ca79bb4b..d3aaf7e151a 100755 --- a/source/speech.py +++ b/source/speech.py @@ -313,6 +313,19 @@ def speakObjectProperties(obj,reason=controlTypes.REASON_QUERY,index=None,**allo newPropertyValues['current']=obj.isCurrent if allowedProperties.get('placeholder', False): newPropertyValues['placeholder']=obj.placeholder + # When speaking an object due to a focus change, the 'selected' state should not be reported if only one item is selected. + # This is because that one item will be the focused object, and saying selected is redundant. + # Rather, 'unselected' will be spoken for an unselected object if 1 or more items are selected. + states=newPropertyValues.get('states') + if states is not None and reason==controlTypes.REASON_FOCUS: + if ( + controlTypes.STATE_SELECTABLE in states + and controlTypes.STATE_SELECTED in states + and obj.selectionContainer + and obj.selectionContainer.getSelectedItemsCount(2)==1 + ): + states.discard(controlTypes.STATE_SELECTED) + states.discard(controlTypes.STATE_SELECTABLE) #Get the speech text for the properties we want to speak, and then speak it text=getSpeechTextForProperties(reason,**newPropertyValues) if text: diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 34f3583bc52..2ef9333628a 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -10,6 +10,7 @@ What's New in NVDA - Replied / Forwarded status is now reported on mail items in the Microsoft Outlook message list. (#6911) - NVDA is now able to read descriptions for emoji as well as other characters that are part of the Unicode Common Locale Data Repository. (#6523) - In Microsoft Word, the cursor's distance from the top and left edges of the page can be reported by pressing NVDA+numpadDelete. (#1939) +- In Google Sheets with braille mode enabled, NVDA no longer announces 'selected' on every cell when moving focus between cells. (#8879) == Changes ==