Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I6358 support aria current #6860

Merged
merged 9 commits into from
Mar 14, 2017
1 change: 1 addition & 0 deletions nvdaHelper/vbufBackends/mshtml/mshtml.cpp
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 @@ -387,6 +389,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
Original file line number Diff line number Diff line change
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"
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. This should be a property rather than a method. Aside from consistency with other NVDAObject properties (name, description, etc.), this means this will be cached during a core tick which improves performance. In NVDA, you can do this by simply naming the method _get_foo, which will create a property named foo.
  2. While this is from the ARIA spec, we try to keep global concepts pretty abstract/platform agnostic. This one is tricky to name because just having a property called current looks kinda weird. We feel the name isCurrent is probably best. While this is perhaps a tiny bit misleading (the "is" prefix normally suggests it's a boolean), it's the best we could come up with... and it is boolean-ish anyway in the sense that it's either current or not, with the type just being a nicety for the user.
  3. The docstring should probably say that this indicates whether this object is the current element in a set of related elements... or something like that; something similar to the summary sentence in the ARIA spec. It can still say it's mapped from aria-current, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah ok. I'm not 100% comfortable with the name, but I can't think of anything better.

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
Original file line number Diff line number Diff line change
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)
ariaCurrent = propertyValues.get('current', False)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Please rename to current.

if ariaCurrent:
try:
textList.append(controlTypes.isCurrentLabels[ariaCurrent])
except KeyError:
log.debugWarning("Aria-current value not handled: %s"%ariaCurrent)
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)
ariaCurrent=field.get('current', None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename.


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 ariaCurrent:
text.append(getBrailleTextForProperties(current=ariaCurrent))
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": ariaCurrent,
}
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":ariaCurrent}
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
Original file line number Diff line number Diff line change
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change the comment prefix to #::

#: l1
#: l2

This means it'll be treated as the documentation of the variable by epydoc (the code doc tool).

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
Original file line number Diff line number Diff line change
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's possible that someone might set aria-current on a table cell; e.g. in a calendar. So, it also needs to be reported for the table cell case above this.

if content and not speakContentFirst:
out.append(content)
return CHUNK_SEPARATOR.join(out)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding speaking for list items, etc., this can be achieved by handling current similar to the way we handle clickable below. The conditions for current should be the same as clickable except we do want to report it even if extraDetail is True (whereas we don't for clickable). The tricky thing here is that ideally, we still want to report current if we're reporting clickable (and vice versa). So, I guess the clickable and current block will have to be merged.

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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