diff --git a/nvdaHelper/vbufBackends/mshtml/mshtml.cpp b/nvdaHelper/vbufBackends/mshtml/mshtml.cpp index 8644a784c2f..6a4bfc1d84d 100755 --- a/nvdaHelper/vbufBackends/mshtml/mshtml.cpp +++ b/nvdaHelper/vbufBackends/mshtml/mshtml.cpp @@ -489,6 +489,7 @@ inline void getAttributesFromHTMLDOMNode(IHTMLDOMNode* pHTMLDOMNode,wstring& nod macro_addHTMLAttributeToMap(L"aria-relevant",false,pHTMLAttributeCollection2,attribsMap,tempVar,tempAttribNode); macro_addHTMLAttributeToMap(L"aria-busy",false,pHTMLAttributeCollection2,attribsMap,tempVar,tempAttribNode); macro_addHTMLAttributeToMap(L"aria-atomic",false,pHTMLAttributeCollection2,attribsMap,tempVar,tempAttribNode); + macro_addHTMLAttributeToMap(L"aria-current",false,pHTMLAttributeCollection2,attribsMap,tempVar,tempAttribNode); pHTMLAttributeCollection2->Release(); } diff --git a/source/NVDAObjects/IAccessible/MSHTML.py b/source/NVDAObjects/IAccessible/MSHTML.py index c00d04d42ec..b2ae40ff1f3 100644 --- a/source/NVDAObjects/IAccessible/MSHTML.py +++ b/source/NVDAObjects/IAccessible/MSHTML.py @@ -511,6 +511,9 @@ def _get_treeInterceptorClass(self): return virtualBuffers.MSHTML.MSHTML return super(MSHTML,self).treeInterceptorClass + def _get_isCurrent(self): + return self.HTMLAttributes["aria-current"] + def _get_HTMLAttributes(self): return HTMLAttribCache(self.HTMLNode) diff --git a/source/NVDAObjects/IAccessible/ia2Web.py b/source/NVDAObjects/IAccessible/ia2Web.py index 94a49f0654e..a0215ffd603 100644 --- a/source/NVDAObjects/IAccessible/ia2Web.py +++ b/source/NVDAObjects/IAccessible/ia2Web.py @@ -26,6 +26,10 @@ def _get_positionInfo(self): info['level']=level return info + def _get_isCurrent(self): + current = self.IA2Attributes.get("current", False) + return current + class Document(Ia2Web): value = None diff --git a/source/NVDAObjects/UIA/edge.py b/source/NVDAObjects/UIA/edge.py index c344ef27e4b..0921449c60e 100644 --- a/source/NVDAObjects/UIA/edge.py +++ b/source/NVDAObjects/UIA/edge.py @@ -11,11 +11,11 @@ import config import controlTypes import cursorManager +import re import aria import textInfos import UIAHandler from UIABrowseMode import UIABrowseModeDocument, UIABrowseModeDocumentTextInfo -import aria from UIAUtils import * from . import UIA, UIATextInfo @@ -147,6 +147,8 @@ def _getControlFieldForObject(self,obj,isEmbedded=False,startOfNode=False,endOfN # Combo boxes with a text pattern are editable 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 # For certain controls, if ARIA overrides the label, then force the field's content (value) to the label # Later processing in Edge's getTextWithFields will remove descendant content from fields with a content attribute. ariaProperties=obj.UIAElement.currentAriaProperties @@ -391,6 +393,22 @@ def _get_description(self): pass return super(EdgeNode,self).description + # RegEx to get the value for the aria-current property. This will be looking for a the value of 'current' + # in a list of strings like "something=true;current=date;". We want to capture one group, after the '=' + # character and before the ';' character. + # This could be one of: True, "page", "step", "location", "date", "time" + RE_ARIA_CURRENT_PROP_VALUE = re.compile("current=(\w+);") + + def _get_isCurrent(self): + ariaProperties=self.UIAElement.currentAriaProperties + match = self.RE_ARIA_CURRENT_PROP_VALUE.match(ariaProperties) + log.debug("aria props = %s" % ariaProperties) + if match: + valueOfAriaCurrent = match.group(1) + log.debug("aria current value = %s" % valueOfAriaCurrent) + return valueOfAriaCurrent + return False + class EdgeList(EdgeNode): # non-focusable lists are readonly lists (ensures correct NVDA presentation category) diff --git a/source/NVDAObjects/__init__.py b/source/NVDAObjects/__init__.py index 85fd8aec044..2d485232b0a 100644 --- a/source/NVDAObjects/__init__.py +++ b/source/NVDAObjects/__init__.py @@ -798,6 +798,13 @@ def _get_statusBar(self): """ return None + def _get_isCurrent(self): + """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 False. If this object is current + it will return one of the following values: True, "page", "step", "location", "date", "time" + """ + return False + def reportFocus(self): """Announces this object in a way suitable such that it gained focus. """ diff --git a/source/braille.py b/source/braille.py index f572752f22a..84c840808ed 100644 --- a/source/braille.py +++ b/source/braille.py @@ -388,6 +388,9 @@ ) SELECTION_SHAPE = 0xC0 #: Dots 7 and 8 +# used to separate chunks of text when programmatically joined +TEXT_SEPARATOR = " " + def NVDAObjectHasUsefulText(obj): import displayModel role = obj.role @@ -630,9 +633,16 @@ def getBrailleTextForProperties(**propertyValues): # Translators: Displayed in braille for a table cell column number. # %s is replaced with the column number. textList.append(_("c%s") % columnNumber) + 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]) if includeTableCellCoords and cellCoordsText: textList.append(cellCoordsText) - return " ".join([x for x in textList if x]) + return TEXT_SEPARATOR.join([x for x in textList if x]) class NVDAObjectRegion(Region): """A region to provide a braille representation of an NVDAObject. @@ -655,7 +665,7 @@ def update(self): obj = self.obj presConfig = config.conf["presentation"] role = obj.role - text = getBrailleTextForProperties(name=obj.name, role=role, + text = getBrailleTextForProperties(name=obj.name, role=role, current=obj.isCurrent, value=obj.value if not NVDAObjectHasUsefulText(obj) else None , states=obj.states, description=obj.description if presConfig["reportObjectDescriptions"] else None, @@ -668,7 +678,7 @@ def update(self): mathPres.ensureInit() if mathPres.brailleProvider: try: - text += " " + mathPres.brailleProvider.getBrailleForMathMl( + text += TEXT_SEPARATOR + mathPres.brailleProvider.getBrailleForMathMl( obj.mathMl) except (NotImplementedError, LookupError): pass @@ -698,12 +708,16 @@ def getControlFieldBraille(info, field, ancestors, reportStart, formatConfig): role = field.get("role", controlTypes.ROLE_UNKNOWN) states = field.get("states", set()) value=field.get('value',None) + current=field.get('current', None) if presCat == field.PRESCAT_LAYOUT: + text = [] # The only item we report for these fields is clickable, if present. if controlTypes.STATE_CLICKABLE in states: - return getBrailleTextForProperties(states={controlTypes.STATE_CLICKABLE}) - return None + text.append(getBrailleTextForProperties(states={controlTypes.STATE_CLICKABLE})) + if current: + text.append(getBrailleTextForProperties(current=current)) + return TEXT_SEPARATOR.join(text) if len(text) != 0 else None elif role in (controlTypes.ROLE_TABLECELL, controlTypes.ROLE_TABLECOLUMNHEADER, controlTypes.ROLE_TABLEROWHEADER) and field.get("table-id"): # Table cell. @@ -713,7 +727,8 @@ def getControlFieldBraille(info, field, ancestors, reportStart, formatConfig): "states": states, "rowNumber": field.get("table-rownumber"), "columnNumber": field.get("table-columnnumber"), - "includeTableCellCoords": reportTableCellCoords + "includeTableCellCoords": reportTableCellCoords, + "current": current, } if reportTableHeaders: props["columnHeaderText"] = field.get("table-columnheadertext") @@ -724,7 +739,7 @@ def getControlFieldBraille(info, field, ancestors, reportStart, formatConfig): # Don't report the role for math here. # However, we still need to pass it (hence "_role"). "_role" if role == controlTypes.ROLE_MATH else "role": role, - "states": states,"value":value} + "states": states,"value":value, "current":current} if config.conf["presentation"]["reportKeyboardShortcuts"]: kbShortcut = field.get("keyboardShortcut") if kbShortcut: @@ -736,7 +751,7 @@ def getControlFieldBraille(info, field, ancestors, reportStart, formatConfig): content = field.get("content") if content: if text: - text += " " + text += TEXT_SEPARATOR text += content elif role == controlTypes.ROLE_MATH: import mathPres @@ -744,7 +759,7 @@ def getControlFieldBraille(info, field, ancestors, reportStart, formatConfig): if mathPres.brailleProvider: try: if text: - text += " " + text += TEXT_SEPARATOR text += mathPres.brailleProvider.getBrailleForMathMl( info.getMathMl(field)) except (NotImplementedError, LookupError): @@ -772,7 +787,7 @@ def getFormatFieldBraille(field, isAtStart, formatConfig): # Translators: Displayed in braille for a heading with a level. # %s is replaced with the level. textList.append(_("h%s")%headingLevel) - return " ".join([x for x in textList if x]) + return TEXT_SEPARATOR.join([x for x in textList if x]) class TextInfoRegion(Region): @@ -836,7 +851,7 @@ def _getTypeformFromFormatField(self, field): def _addFieldText(self, text, contentPos): if self.rawText: # Separate this field text from the rest of the text. - text = " " + text + text = TEXT_SEPARATOR + text self.rawText += text textLen = len(text) self.rawTextTypeforms.extend((louis.plain_text,) * textLen) @@ -854,7 +869,7 @@ def _addTextWithFields(self, info, formatConfig, isSelection=False): if self._endsWithField: # The last item added was a field, # so add a space before the content. - self.rawText += " " + self.rawText += TEXT_SEPARATOR self.rawTextTypeforms.append(louis.plain_text) self._rawToContentPos.append(self._currentContentPos) if isSelection and self.selectionStart is None: @@ -982,7 +997,7 @@ def update(self): # There is no text left after stripping line ending characters, # or the last item added can be navigated with a cursor. # Add a space in case the cursor is at the end of the reading unit. - self.rawText += " " + self.rawText += TEXT_SEPARATOR rawTextLen += 1 self.rawTextTypeforms.append(louis.plain_text) self._rawToContentPos.append(self._currentContentPos) @@ -1373,7 +1388,7 @@ def getFocusContextRegions(obj, oldFocusRegions=None): for index, parent in enumerate(ancestors[newAncestorsStart:ancestorsEnd], newAncestorsStart): if not parent.isPresentableFocusAncestor: continue - region = NVDAObjectRegion(parent, appendText=" ") + region = NVDAObjectRegion(parent, appendText=TEXT_SEPARATOR) region._focusAncestorIndex = index region.update() yield region @@ -1404,7 +1419,7 @@ def getFocusRegions(obj, review=False): region2 = None if isinstance(obj, TreeInterceptor): obj = obj.rootNVDAObject - region = NVDAObjectRegion(obj, appendText=" " if region2 else "") + region = NVDAObjectRegion(obj, appendText=TEXT_SEPARATOR if region2 else "") region.update() yield region if region2: @@ -1423,7 +1438,7 @@ def formatCellsForLog(cells): # optimisation: This gets called a lot, so needs to be as efficient as possible. # List comprehensions without function calls are faster than loops. # For str.join, list comprehensions are faster than generator comprehensions. - return " ".join([ + return TEXT_SEPARATOR.join([ "".join([str(dot + 1) for dot in xrange(8) if cell & (1 << dot)]) if cell else "-" for cell in cells]) diff --git a/source/controlTypes.py b/source/controlTypes.py index 4a64cdd86b9..f6f3bf8e67a 100644 --- a/source/controlTypes.py +++ b/source/controlTypes.py @@ -613,6 +613,23 @@ REASON_ONLYCACHE="onlyCache" #} +#: Text to use for 'current' values. These describe if an item is the current item +#: within a particular kind of selection. +isCurrentLabels = { + # Translators: Presented when an item is marked as current in a collection of items + True:_("current"), + # Translators: Presented when a page item is marked as current in a collection of page items + "page":_("current page"), + # Translators: Presented when a step item is marked as current in a collection of step items + "step":_("current step"), + # Translators: Presented when a location item is marked as current in a collection of location items + "location":_("current location"), + # Translators: Presented when a date item is marked as current in a collection of date items + "date":_("current date"), + # Translators: Presented when a time item is marked as current in a collection of time items + "time":_("current time"), +} + def processPositiveStates(role, states, reason, positiveStates): positiveStates = positiveStates.copy() # The user never cares about certain states. diff --git a/source/speech.py b/source/speech.py index ea653e6ef36..3330e9345ab 100755 --- a/source/speech.py +++ b/source/speech.py @@ -307,6 +307,7 @@ def speakObjectProperties(obj,reason=controlTypes.REASON_QUERY,index=None,**allo newPropertyValues["_tableID"]=obj.tableID except NotImplementedError: pass + newPropertyValues['current']=obj.isCurrent #Get the speech text for the properties we want to speak, and then speak it text=getSpeechTextForProperties(reason,**newPropertyValues) if text: @@ -1001,6 +1002,13 @@ def getSpeechTextForProperties(reason=controlTypes.REASON_QUERY,**propertyValues 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: + textList.append(controlTypes.isCurrentLabels[ariaCurrent]) + except KeyError: + log.debugWarning("Aria-current value not handled: %s"%ariaCurrent) + textList.append(controlTypes.isCurrentLabels[True]) indexInGroup=propertyValues.get('positionInfo_indexInGroup',0) similarItemsInGroup=propertyValues.get('positionInfo_similarItemsInGroup',0) if 0