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
2 changes: 1 addition & 1 deletion source/NVDAObjects/IAccessible/MSHTML.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ def _get_treeInterceptorClass(self):
return virtualBuffers.MSHTML.MSHTML
return super(MSHTML,self).treeInterceptorClass

def getValueForAriaCurrent(self):
def _get_isCurrent(self):
return self.HTMLAttributes["aria-current"]

def _get_HTMLAttributes(self):
Expand Down
2 changes: 1 addition & 1 deletion source/NVDAObjects/IAccessible/ia2Web.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def _get_positionInfo(self):
info['level']=level
return info

def getValueForAriaCurrent(self):
def _get_isCurrent(self):
current = self.IA2Attributes.get("current", False)
return current

Expand Down
12 changes: 9 additions & 3 deletions source/NVDAObjects/UIA/edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,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.getValueForAriaCurrent()
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 @@ -389,9 +389,15 @@ def _get_description(self):
pass
return super(EdgeNode,self).description

def getValueForAriaCurrent(self):
# 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 = re.match("current=(\w+);", ariaProperties)
match = self.RE_ARIA_CURRENT_PROP_VALUE.match(ariaProperties)
log.debug("aria props = %s" % ariaProperties)
if match:
valueOfAriaCurrent = match.group(1)
Expand Down
5 changes: 3 additions & 2 deletions source/NVDAObjects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -798,8 +798,9 @@ def _get_statusBar(self):
"""
return None

def getValueForAriaCurrent(self):
"""Gets the value for aria-current. Normally returns False. If this object is current
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
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
36 changes: 17 additions & 19 deletions source/speech.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ def speakObjectProperties(obj,reason=controlTypes.REASON_QUERY,index=None,**allo
newPropertyValues["_tableID"]=obj.tableID
except NotImplementedError:
pass
newPropertyValues['current']=obj.getValueForAriaCurrent()
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 @@ -1003,19 +1003,12 @@ def getSpeechTextForProperties(reason=controlTypes.REASON_QUERY,**propertyValues
# 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 is not None and ariaCurrent != False:
if ariaCurrent=="page":
textList.append(_("current page"))
elif ariaCurrent=="step":
textList.append(_("current step"))
elif ariaCurrent=="location":
textList.append(_("current location"))
elif ariaCurrent=="date":
textList.append(_("current date"))
elif ariaCurrent=="time":
textList.append(_("current time"))
else:
textList.append(_("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])
indexInGroup=propertyValues.get('positionInfo_indexInGroup',0)
similarItemsInGroup=propertyValues.get('positionInfo_similarItemsInGroup',0)
if 0<indexInGroup<=similarItemsInGroup:
Expand Down Expand Up @@ -1118,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 @@ -1139,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
6 changes: 0 additions & 6 deletions source/virtualBuffers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,6 @@ def _getParagraphOffsets(self,offset):
return lineStart.value,lineEnd.value

def _normalizeControlField(self,attrs):

ariaCurrent = attrs.get("IAccessible2::attribute_current")
if ariaCurrent != None:
attrs['current']= ariaCurrent
del attrs["IAccessible2::attribute_current"]

tableLayout=attrs.get('table-layout')
if tableLayout:
attrs['table-layout']=tableLayout=="1"
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