diff --git a/source/NVDAObjects/IAccessible/MSHTML.py b/source/NVDAObjects/IAccessible/MSHTML.py index 80956fd7a0f..16ed0988041 100644 --- a/source/NVDAObjects/IAccessible/MSHTML.py +++ b/source/NVDAObjects/IAccessible/MSHTML.py @@ -545,9 +545,11 @@ def _get_treeInterceptorClass(self): def _get_isCurrent(self): isCurrent = self.HTMLAttributes["aria-current"] - if isCurrent == "false": - isCurrent = None - return isCurrent + try: + return controlTypes.IsCurrent(isCurrent) + except ValueError: + log.debugWarning(f"Unknown aria-current value: {isCurrent}") + return controlTypes.IsCurrent.NO def _get_HTMLAttributes(self): return HTMLAttribCache(self.HTMLNode) diff --git a/source/NVDAObjects/IAccessible/ia2Web.py b/source/NVDAObjects/IAccessible/ia2Web.py index d475130c6d0..03560d315b6 100644 --- a/source/NVDAObjects/IAccessible/ia2Web.py +++ b/source/NVDAObjects/IAccessible/ia2Web.py @@ -34,11 +34,13 @@ def _get_positionInfo(self): info['level']=level return info - def _get_isCurrent(self): - current = self.IA2Attributes.get("current", None) - if current == "false": - current = None - return current + def _get_isCurrent(self) -> controlTypes.IsCurrent: + ia2attrCurrent: str = self.IA2Attributes.get("current", "false") + try: + return controlTypes.IsCurrent(ia2attrCurrent) + except ValueError: + log.debugWarning(f"Unknown 'current' IA2Attribute value: {ia2attrCurrent}") + return controlTypes.IsCurrent.NO def _get_placeholder(self): placeholder = self.IA2Attributes.get('placeholder', None) diff --git a/source/NVDAObjects/UIA/edge.py b/source/NVDAObjects/UIA/edge.py index beb661d9808..6efa1655700 100644 --- a/source/NVDAObjects/UIA/edge.py +++ b/source/NVDAObjects/UIA/edge.py @@ -156,7 +156,7 @@ def _getControlFieldForObject(self,obj,isEmbedded=False,startOfNode=False,endOfN if obj.role==controlTypes.ROLE_COMBOBOX and obj.UIATextPattern: field['states'].add(controlTypes.STATE_EDITABLE) # report if the field is 'current' - field['current']=obj.isCurrent + field['current'] = obj.isCurrent if obj.placeholder and obj._isTextEmpty: field['placeholder']=obj.placeholder # For certain controls, if ARIA overrides the label, then force the field's content (value) to the label @@ -470,15 +470,18 @@ def _get_ariaProperties(self): # "false" is ignored by the regEx and will not produce a match RE_ARIA_CURRENT_PROP_VALUE = re.compile("current=(?!false)(\w+);") - def _get_isCurrent(self): + def _get_isCurrent(self) -> controlTypes.IsCurrent: ariaProperties=self._getUIACacheablePropertyValue(UIAHandler.UIA_AriaPropertiesPropertyId) match = self.RE_ARIA_CURRENT_PROP_VALUE.search(ariaProperties) - log.debug("aria props = %s" % ariaProperties) if match: valueOfAriaCurrent = match.group(1) - log.debug("aria current value = %s" % valueOfAriaCurrent) - return valueOfAriaCurrent - return None + try: + return controlTypes.IsCurrent(valueOfAriaCurrent) + except ValueError: + log.debugWarning( + f"Unknown aria-current value: {valueOfAriaCurrent}, ariaProperties: {ariaProperties}" + ) + return controlTypes.IsCurrent.NO def _get_roleText(self): roleText = self.ariaProperties.get('roledescription', None) diff --git a/source/NVDAObjects/__init__.py b/source/NVDAObjects/__init__.py index 97067b92011..c671755e972 100644 --- a/source/NVDAObjects/__init__.py +++ b/source/NVDAObjects/__init__.py @@ -975,12 +975,13 @@ def _get_statusBar(self): """ return None - def _get_isCurrent(self): + isCurrent: controlTypes.IsCurrent #: type info for auto property _get_isCurrent + + def _get_isCurrent(self) -> controlTypes.IsCurrent: """Gets the value that indicates whether this object is the current element in a set of related - elements. This maps to aria-current. Normally returns None. If this object is current - it will return one of the following values: "true", "page", "step", "location", "date", "time" + elements. This maps to aria-current. """ - return None + return controlTypes.IsCurrent.NO def _get_shouldAcceptShowHideCaretEvent(self): """Some objects/applications send show/hide caret events when we don't expect it, such as when the cursor is blinking. diff --git a/source/braille.py b/source/braille.py index 082aea7d9f4..166f5e75a9c 100644 --- a/source/braille.py +++ b/source/braille.py @@ -579,13 +579,9 @@ def getPropertiesBraille(**propertyValues) -> str: # noqa: C901 # %s is replaced with the column number. columnStr = _("c{columnNumber}").format(columnNumber=columnNumber) textList.append(columnStr) - current = propertyValues.get('current', False) - if current: - try: - textList.append(controlTypes.isCurrentLabels[current]) - except KeyError: - log.debugWarning("Aria-current value not handled: %s"%current) - textList.append(controlTypes.isCurrentLabels[True]) + isCurrent = propertyValues.get('current', controlTypes.IsCurrent.NO) + if isCurrent != controlTypes.IsCurrent.NO: + textList.append(isCurrent.displayString) placeholder = propertyValues.get('placeholder', None) if placeholder: textList.append(placeholder) @@ -670,7 +666,7 @@ def getControlFieldBraille(info, field, ancestors, reportStart, formatConfig): states = field.get("states", set()) value=field.get('value',None) - current=field.get('current', None) + current = field.get('current', controlTypes.IsCurrent.NO) placeholder=field.get('placeholder', None) roleText = field.get('roleTextBraille', field.get('roleText')) landmark = field.get("landmark") diff --git a/source/controlTypes.py b/source/controlTypes.py index 639a0e3d0a3..b29b4da5b68 100644 --- a/source/controlTypes.py +++ b/source/controlTypes.py @@ -6,6 +6,8 @@ from typing import Dict, Union, Set, Any, Optional, List from enum import Enum, auto +from logHandler import log + ROLE_UNKNOWN=0 ROLE_WINDOW=1 ROLE_TITLEBAR=2 @@ -649,21 +651,49 @@ class OutputReason(Enum): QUICKNAV = auto() -#: Text to use for 'current' values. These describe if an item is the current item -#: within a particular kind of selection. -isCurrentLabels: Dict[Union[bool, str], str] = { +class IsCurrent(Enum): + """Values to use within NVDA to denote 'current' values. + These describe if an item is the current item within a particular kind of selection. + EG aria-current + """ + NO = "false" + YES = "true" + PAGE = "page" + STEP = "step" + LOCATION = "location" + DATE = "date" + TIME = "time" + + @property + def displayString(self): + """ + @return: The translated UI display string that should be used for this value of the IsCurrent enum + """ + try: + return _isCurrentLabels[self] + except KeyError: + log.debugWarning(f"No translation mapping for: {self}") + # there is a value for 'current' but NVDA hasn't learned about it yet, + # at least describe in the general sense that this item is 'current' + return _isCurrentLabels[IsCurrent.YES] + + +#: Text to use for 'current' values. These describe if an item is the current item +#: within a particular kind of selection. EG aria-current +_isCurrentLabels: Dict[Enum, str] = { + IsCurrent.NO: "", # There is nothing extra to say for items that are not current. # Translators: Presented when an item is marked as current in a collection of items - True:_("current"), + IsCurrent.YES: _("current"), # Translators: Presented when a page item is marked as current in a collection of page items - "page":_("current page"), + IsCurrent.PAGE: _("current page"), # Translators: Presented when a step item is marked as current in a collection of step items - "step":_("current step"), + IsCurrent.STEP: _("current step"), # Translators: Presented when a location item is marked as current in a collection of location items - "location":_("current location"), + IsCurrent.LOCATION: _("current location"), # Translators: Presented when a date item is marked as current in a collection of date items - "date":_("current date"), + IsCurrent.DATE: _("current date"), # Translators: Presented when a time item is marked as current in a collection of time items - "time":_("current time"), + IsCurrent.TIME: _("current time"), } diff --git a/source/speech/__init__.py b/source/speech/__init__.py index 9bb1064eeba..41d92821c6b 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -362,7 +362,7 @@ def getObjectPropertiesSpeech( # noqa: C901 positionInfo=obj.positionInfo elif value and name == "current": # getPropertiesSpeech names this "current", but the NVDAObject property is - # named "isCurrent". + # named "isCurrent", it's type should always be controltypes.IsCurrent newPropertyValues['current'] = obj.isCurrent elif value: # Certain properties such as row and column numbers have presentational versions, which should be used for speech if they are available. @@ -1611,15 +1611,12 @@ def getPropertiesSpeech( # noqa: C901 if rowCount or columnCount: # The caller is entering a table, so ensure that it is treated as a new table, even if the previous table was the same. oldTableID = None - ariaCurrent = propertyValues.get('current', False) - if ariaCurrent: - try: - ariaCurrentLabel = controlTypes.isCurrentLabels[ariaCurrent] - textList.append(ariaCurrentLabel) - except KeyError: - log.debugWarning("Aria-current value not handled: %s"%ariaCurrent) - ariaCurrentLabel = controlTypes.isCurrentLabels[True] - textList.append(ariaCurrentLabel) + + # speak isCurrent property EG aria-current + isCurrent = propertyValues.get('current', controlTypes.IsCurrent.NO) + if isCurrent != controlTypes.IsCurrent.NO: + textList.append(isCurrent.displayString) + placeholder: Optional[str] = propertyValues.get('placeholder', None) if placeholder: textList.append(placeholder) @@ -1682,7 +1679,7 @@ def getControlFieldSpeech( # noqa: C901 name = "" states=attrs.get('states',set()) keyboardShortcut=attrs.get('keyboardShortcut', "") - ariaCurrent=attrs.get('current', None) + isCurrent = attrs.get('current', controlTypes.IsCurrent.NO) placeholderValue=attrs.get('placeholder', None) value=attrs.get('value',"") if reason == OutputReason.FOCUS or attrs.get('alwaysReportDescription', False): @@ -1712,7 +1709,7 @@ def getControlFieldSpeech( # noqa: C901 keyboardShortcutSequence = getPropertiesSpeech( reason=reason, keyboardShortcut=keyboardShortcut ) - ariaCurrentSequence = getPropertiesSpeech(reason=reason, current=ariaCurrent) + isCurrentSequence = getPropertiesSpeech(reason=reason, current=isCurrent) placeholderSequence = getPropertiesSpeech(reason=reason, placeholder=placeholderValue) nameSequence = getPropertiesSpeech(reason=reason, name=name) valueSequence = getPropertiesSpeech(reason=reason, value=value) @@ -1829,7 +1826,7 @@ def getControlFieldSpeech( # noqa: C901 getProps['columnHeaderText'] = attrs.get("table-columnheadertext") tableCellSequence = getPropertiesSpeech(_tableID=tableID, **getProps) tableCellSequence.extend(stateTextSequence) - tableCellSequence.extend(ariaCurrentSequence) + tableCellSequence.extend(isCurrentSequence) types.logBadSequenceTypes(tableCellSequence) return tableCellSequence @@ -1878,7 +1875,7 @@ def getControlFieldSpeech( # noqa: C901 out.extend(stateTextSequence if speakStatesFirst else roleTextSequence) out.extend(roleTextSequence if speakStatesFirst else stateTextSequence) out.append(containerContainsText) - out.extend(ariaCurrentSequence) + out.extend(isCurrentSequence) out.extend(valueSequence) out.extend(descriptionSequence) out.extend(levelSequence) @@ -1913,8 +1910,8 @@ def getControlFieldSpeech( # noqa: C901 # Special cases elif not speakEntry and fieldType in ("start_addedToControlFieldStack","start_relative"): out = [] - if ariaCurrent: - out.extend(ariaCurrentSequence) + if isCurrent != controlTypes.IsCurrent.NO: + out.extend(isCurrentSequence) # Speak expanded / collapsed / level for treeview items (in ARIA treegrids) if role == controlTypes.ROLE_TREEVIEWITEM: if controlTypes.STATE_EXPANDED in states: diff --git a/source/virtualBuffers/MSHTML.py b/source/virtualBuffers/MSHTML.py index 204a92f9705..797c7477de2 100644 --- a/source/virtualBuffers/MSHTML.py +++ b/source/virtualBuffers/MSHTML.py @@ -48,9 +48,17 @@ def _normalizeFormatField(self, attrs): def _normalizeControlField(self,attrs): level=None - ariaCurrent = attrs.get('HTMLAttrib::aria-current', None) - if ariaCurrent not in (None, "false"): - attrs['current']=ariaCurrent + + ariaCurrentValue = attrs.get('HTMLAttrib::aria-current', 'false') + try: + ariaCurrent = controlTypes.IsCurrent(ariaCurrentValue) + except ValueError: + log.debugWarning(f"Unknown aria-current value: {ariaCurrentValue}") + ariaCurrent = controlTypes.IsCurrent.NO + + if ariaCurrent != controlTypes.IsCurrent.NO: + attrs['current'] = ariaCurrent + placeholder = self._getPlaceholderAttribute(attrs, 'HTMLAttrib::aria-placeholder') if placeholder: attrs['placeholder']=placeholder diff --git a/source/virtualBuffers/gecko_ia2.py b/source/virtualBuffers/gecko_ia2.py index c2ac7be94c3..910cdb94845 100755 --- a/source/virtualBuffers/gecko_ia2.py +++ b/source/virtualBuffers/gecko_ia2.py @@ -52,9 +52,14 @@ def _normalizeControlField(self,attrs): if attrVal is not None: attrs[attr]=int(attrVal) - current = attrs.get("IAccessible2::attribute_current") - if current not in (None, 'false'): - attrs['current']= current + valForCurrent = attrs.get("IAccessible2::attribute_current", "false") + try: + isCurrent = controlTypes.IsCurrent(valForCurrent) + except ValueError: + log.debugWarning(f"Unknown isCurrent value: {valForCurrent}") + isCurrent = controlTypes.IsCurrent.NO + if isCurrent != controlTypes.IsCurrent.NO: + attrs['current'] = isCurrent placeholder = self._getPlaceholderAttribute(attrs, "IAccessible2::attribute_placeholder") if placeholder is not None: attrs['placeholder']= placeholder diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 5eafac90abb..2f53c3aaa56 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -37,6 +37,15 @@ What's New in NVDA - getConfigDirs - use globalVars.appArgs.configPath instead - Module level REASON_* constants are removed from controlTypes - please use controlTypes.OutputReason instead. (#11969) - REASON_QUICKNAV has been removed from browseMode - use controlTypes.OutputReason.QUICKNAV instead. (#11969) +- 'NVDAObject' (and derivatives) property 'isCurrent' now strictly returns Enum class 'controlTypes.IsCurrent'. (#11782) + - 'isCurrent' is no longer Optional, and thus will not return None. + - When an object is not current 'controlTypes.IsCurrent.NO' is returned. +- The 'controlTypes.isCurrentLabels' mapping has been removed. (#11782) + - Instead use the 'displayString' property on a 'controlTypes.IsCurrent' enum value. EG 'controlTypes.IsCurrent.YES.displayString' +- 'NVDAObject' (and derivatives) property 'isCurrent' now strictly returns 'controlTypes.IsCurrent'. (#11782) + - 'isCurrent' is no longer Optional, and thus will not return None, when an object is not current 'controlTypes.IsCurrent.NO' is returned. +- The 'controlTypes.isCurrentLabels' has been removed, instead use the 'displayString' property on a 'controlTypes.IsCurrent' enum value. (#11782) + - EG 'controlTypes.IsCurrent.YES.displayString' = 2020.4 =