Skip to content

Commit

Permalink
Support aria current (PR #6860)
Browse files Browse the repository at this point in the history
Support for aria-current in focus mode for chrome

Fixes #6358
  • Loading branch information
feerrenrut committed Mar 14, 2017
1 parent ea6bdf7 commit 2fde87e
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 23 deletions.
1 change: 1 addition & 0 deletions nvdaHelper/vbufBackends/mshtml/mshtml.cpp
Expand Up @@ -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();
}

Expand Down
3 changes: 3 additions & 0 deletions source/NVDAObjects/IAccessible/MSHTML.py
Expand Up @@ -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)

Expand Down
4 changes: 4 additions & 0 deletions source/NVDAObjects/IAccessible/ia2Web.py
Expand Up @@ -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

Expand Down
20 changes: 19 additions & 1 deletion source/NVDAObjects/UIA/edge.py
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions source/NVDAObjects/__init__.py
Expand Up @@ -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.
"""
Expand Down
47 changes: 31 additions & 16 deletions source/braille.py
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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")
Expand All @@ -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:
Expand All @@ -736,15 +751,15 @@ 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
mathPres.ensureInit()
if mathPres.brailleProvider:
try:
if text:
text += " "
text += TEXT_SEPARATOR
text += mathPres.brailleProvider.getBrailleForMathMl(
info.getMathMl(field))
except (NotImplementedError, LookupError):
Expand Down Expand Up @@ -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):

Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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])
Expand Down
17 changes: 17 additions & 0 deletions source/controlTypes.py
Expand Up @@ -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.
Expand Down
27 changes: 21 additions & 6 deletions source/speech.py
Expand Up @@ -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:
Expand Down Expand Up @@ -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<indexInGroup<=similarItemsInGroup:
Expand Down Expand Up @@ -1035,6 +1043,7 @@ def getControlFieldSpeech(attrs,ancestorAttrs,fieldType,formatConfig=None,extraD
role=attrs.get('role',controlTypes.ROLE_UNKNOWN)
states=attrs.get('states',set())
keyboardShortcut=attrs.get('keyboardShortcut', "")
ariaCurrent=attrs.get('current', None)
value=attrs.get('value',"")
if reason==controlTypes.REASON_FOCUS or attrs.get('alwaysReportDescription',False):
description=attrs.get('description',"")
Expand All @@ -1050,6 +1059,7 @@ def getControlFieldSpeech(attrs,ancestorAttrs,fieldType,formatConfig=None,extraD
roleText=getSpeechTextForProperties(reason=reason,role=role)
stateText=getSpeechTextForProperties(reason=reason,states=states,_role=role)
keyboardShortcutText=getSpeechTextForProperties(reason=reason,keyboardShortcut=keyboardShortcut) if config.conf["presentation"]["reportKeyboardShortcuts"] else ""
ariaCurrentText=getSpeechTextForProperties(reason=reason,current=ariaCurrent)
nameText=getSpeechTextForProperties(reason=reason,name=name)
valueText=getSpeechTextForProperties(reason=reason,value=value)
descriptionText=(getSpeechTextForProperties(reason=reason,description=description)
Expand Down Expand Up @@ -1101,7 +1111,8 @@ def getControlFieldSpeech(attrs,ancestorAttrs,fieldType,formatConfig=None,extraD
getProps['rowHeaderText'] = attrs.get("table-rowheadertext")
getProps['columnHeaderText'] = attrs.get("table-columnheadertext")
return (getSpeechTextForProperties(_tableID=tableID, **getProps)
+ (" %s" % stateText if stateText else ""))
+ (" %s" % stateText if stateText else "")
+ (" %s" % ariaCurrentText if ariaCurrent else ""))

# General cases
elif (
Expand All @@ -1112,7 +1123,7 @@ def getControlFieldSpeech(attrs,ancestorAttrs,fieldType,formatConfig=None,extraD
content = attrs.get("content")
if content and speakContentFirst:
out.append(content)
out.extend(x for x in (nameText,(stateText if speakStatesFirst else roleText),(roleText if speakStatesFirst else stateText),valueText,descriptionText,levelText,keyboardShortcutText) if x)
out.extend(x for x in (nameText,(stateText if speakStatesFirst else roleText),(roleText if speakStatesFirst else stateText),ariaCurrentText,valueText,descriptionText,levelText,keyboardShortcutText) if x)
if content and not speakContentFirst:
out.append(content)
return CHUNK_SEPARATOR.join(out)
Expand All @@ -1122,10 +1133,14 @@ def getControlFieldSpeech(attrs,ancestorAttrs,fieldType,formatConfig=None,extraD
return _("out of %s")%roleText

# Special cases
elif not extraDetail and not speakEntry and fieldType in ("start_addedToControlFieldStack","start_relative") and controlTypes.STATE_CLICKABLE in states:
# Clickable.
return getSpeechTextForProperties(states=set([controlTypes.STATE_CLICKABLE]))

elif not speakEntry and fieldType in ("start_addedToControlFieldStack","start_relative"):
out = []
if not extraDetail and controlTypes.STATE_CLICKABLE in states:
# Clickable.
out.append(getSpeechTextForProperties(states=set([controlTypes.STATE_CLICKABLE])))
if ariaCurrent:
out.append(ariaCurrentText)
return CHUNK_SEPARATOR.join(out)
else:
return ""

Expand Down
3 changes: 3 additions & 0 deletions source/virtualBuffers/MSHTML.py
Expand Up @@ -50,6 +50,9 @@ def _normalizeFormatField(self, attrs):

def _normalizeControlField(self,attrs):
level=None
ariaCurrent = attrs.get('HTMLAttrib::aria-current', None)
if ariaCurrent is not None:
attrs['current']=ariaCurrent
accRole=attrs.get('IAccessible::role',0)
accRole=int(accRole) if isinstance(accRole,basestring) and accRole.isdigit() else accRole
nodeName=attrs.get('IHTMLDOMNode::nodeName',"")
Expand Down
3 changes: 3 additions & 0 deletions source/virtualBuffers/gecko_ia2.py
Expand Up @@ -23,6 +23,9 @@
class Gecko_ia2_TextInfo(VirtualBufferTextInfo):

def _normalizeControlField(self,attrs):
ariaCurrent = attrs.get("IAccessible2::attribute_current")
if ariaCurrent != None:
attrs['current']= ariaCurrent
accRole=attrs['IAccessible::role']
accRole=int(accRole) if accRole.isdigit() else accRole
role=IAccessibleHandler.IAccessibleRolesToNVDARoles.get(accRole,controlTypes.ROLE_UNKNOWN)
Expand Down

0 comments on commit 2fde87e

Please sign in to comment.