Skip to content
5 changes: 3 additions & 2 deletions source/IAccessibleHandler/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2006-2007 NVDA Contributors <http://www.nvda-project.org/>
# Copyright (C) 2006-2021 NV Access Limited, Łukasz Golonka, Leonard de Ruijter
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

Expand All @@ -23,6 +23,7 @@
import oleacc
import JABHandler
import UIAHandler
import textUtils

from comInterfaces import Accessibility as IA

Expand Down Expand Up @@ -1073,7 +1074,7 @@ def getRecursiveTextFromIAccessibleTextObject(obj, startOffset=0, endOffset=-1):
return text
textList = []
for i, t in enumerate(text):
if ord(t) == 0xFFFC:
if ord(t) == ord(textUtils.OBJ_REPLACEMENT_CHAR):
try:
index = hypertextObject.hyperlinkIndex(i + startOffset)
childTextObject = hypertextObject.hyperlink(index).QueryInterface(IA.IAccessible)
Expand Down
21 changes: 13 additions & 8 deletions source/NVDAObjects/IAccessible/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#A part of NonVisual Desktop Access (NVDA)
#Copyright (C) 2006-2018 NV Access Limited, Babbage B.V.
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2006-2021 NV Access Limited, Babbage B.V.
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
import typing

from comtypes.automation import IEnumVARIANT, VARIANT
from comtypes import COMError, IServiceProvider, GUID
Expand Down Expand Up @@ -176,11 +177,11 @@ def expand(self,unit):
if not text:
return
try:
self._startOffset=text.rindex(u'\ufffc',0,oldStart-self._startOffset)
self._startOffset = text.rindex(textUtils.OBJ_REPLACEMENT_CHAR, 0, oldStart - self._startOffset)
except ValueError:
pass
try:
self._endOffset=text.index(u'\ufffc',oldEnd-self._startOffset)
self._endOffset = text.index(textUtils.OBJ_REPLACEMENT_CHAR, oldEnd - self._startOffset)
except ValueError:
pass

Expand Down Expand Up @@ -328,7 +329,11 @@ def _getParagraphOffsets(self,offset):
def _lineNumFromOffset(self,offset):
return -1

def _iterTextWithEmbeddedObjects(self, withFields, formatConfig=None):
def _iterTextWithEmbeddedObjects(
self,
withFields,
formatConfig=None
) -> typing.Generator[typing.Union[textInfos.FieldCommand, str, int], None, None]:
"""Iterate through the text, splitting at embedded object characters.
Where an embedded object character occurs, its offset is provided.
@param withFields: Whether to output control/format fields.
Expand All @@ -352,7 +357,7 @@ def _iterTextWithEmbeddedObjects(self, withFields, formatConfig=None):
while chunkStart < itemLen:
# Find the next embedded object character.
try:
chunkEnd = item.index(u"\uFFFC", chunkStart)
chunkEnd = item.index(textUtils.OBJ_REPLACEMENT_CHAR, chunkStart)
except ValueError:
# This is the last chunk of text.
yield item[chunkStart:]
Expand Down
18 changes: 18 additions & 0 deletions source/NVDAObjects/IAccessible/chromium.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,28 @@
from NVDAObjects.IAccessible import IAccessible
from virtualBuffers.gecko_ia2 import Gecko_ia2 as GeckoVBuf, Gecko_ia2_TextInfo as GeckoVBufTextInfo
from . import ia2Web
from logHandler import log


class ChromeVBufTextInfo(GeckoVBufTextInfo):

def _calculateDescriptionFrom(self, attrs) -> controlTypes.DescriptionFrom:
"""Overridable calculation of DescriptionFrom
@param attrs: source attributes for the TextInfo
@return: the origin for accDescription.
@note: Chrome provides 'IAccessible2::attribute_description-from' which declares the origin used for
accDescription. Chrome also provides `IAccessible2::attribute_description` to maintain compatibility
with FireFox.
"""
ia2attrDescriptionFrom = attrs.get("IAccessible2::attribute_description-from")
try:
return controlTypes.DescriptionFrom(ia2attrDescriptionFrom)
except ValueError:
if ia2attrDescriptionFrom:
log.debugWarning(f"Unknown 'description-from' IA2Attribute value: {ia2attrDescriptionFrom}")
# fallback to Firefox approach
return super()._calculateDescriptionFrom(attrs)

def _normalizeControlField(self, attrs):
attrs = super()._normalizeControlField(attrs)
if attrs['role'] == controlTypes.Role.TOGGLEBUTTON and controlTypes.STATE_CHECKABLE in attrs['states']:
Expand Down
77 changes: 46 additions & 31 deletions source/NVDAObjects/IAccessible/ia2TextMozilla.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
#NVDAObjects/IAccessible/ia2TextMozilla.py
#A part of NonVisual Desktop Access (NVDA)
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.
#Copyright (C) 2015-2019 NV Access Limited, Mozilla Corporation
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2015-2021 NV Access Limited, Mozilla Corporation

"""Support for the IAccessible2 rich text model first implemented by Mozilla.
This is now used by other applications as well.
"""
import typing

from comtypes import COMError
import winUser
import textInfos
from textInfos import offsets
import controlTypes
from comInterfaces import IAccessible2Lib as IA2
import api
Expand All @@ -20,28 +21,35 @@
import locationHelper
from logHandler import log

class FakeEmbeddingTextInfo(textInfos.offsets.OffsetsTextInfo):

class FakeEmbeddingTextInfo(offsets.OffsetsTextInfo):
encoding = None

def _getStoryLength(self):
return self.obj.childCount

def _iterTextWithEmbeddedObjects(self, withFields, formatConfig=None):
return range(self._startOffset, self._endOffset)
def _iterTextWithEmbeddedObjects(
self,
withFields,
formatConfig=None
) -> typing.Generator[int, None, None]:
yield from range(self._startOffset, self._endOffset)

def _getUnitOffsets(self,unit,offset):
if unit in (textInfos.UNIT_WORD,textInfos.UNIT_LINE):
unit=textInfos.UNIT_CHARACTER
return super(FakeEmbeddingTextInfo,self)._getUnitOffsets(unit,offset)

def _getRawTextInfo(obj):

def _getRawTextInfo(obj) -> type(offsets.OffsetsTextInfo):
if not hasattr(obj, "IAccessibleTextObject") and obj.role in (controlTypes.Role.TABLE, controlTypes.Role.TABLEROW):
return FakeEmbeddingTextInfo
elif obj.TextInfo is NVDAObjectTextInfo:
return NVDAObjectTextInfo
return IA2TextTextInfo

def _getEmbedded(obj, offset):

def _getEmbedded(obj, offset) -> typing.Optional[IAccessible]:
if not hasattr(obj, "IAccessibleTextObject"):
return obj.getChild(offset)
# Mozilla uses IAccessibleHypertext to facilitate quick retrieval of embedded objects.
Expand All @@ -57,8 +65,8 @@ def _getEmbedded(obj, offset):

class MozillaCompoundTextInfo(CompoundTextInfo):

def _getControlFieldForObject(self, obj, ignoreEditableText=True):
controlField = super()._getControlFieldForObject(obj, ignoreEditableText=ignoreEditableText)
def _getControlFieldForObject(self, obj):
controlField = super()._getControlFieldForObject(obj)
if controlField is None:
return None
# Set the uniqueID of the controlField if we can get one
Expand Down Expand Up @@ -179,7 +187,7 @@ def _getSelectionBase(self):
return ti, obj
raise RuntimeError("No selection or caret")

def _makeRawTextInfo(self, obj, position):
def _makeRawTextInfo(self, obj, position) -> offsets.OffsetsTextInfo:
return _getRawTextInfo(obj)(obj, position)

def _getEmbedding(self, obj):
Expand Down Expand Up @@ -244,7 +252,7 @@ def _findContentDescendant(self, obj, position):
ti._startOffset=ti._endOffset=descendantOffset.value
return ti,obj

def _iterRecursiveText(self, ti, controlStack, formatConfig):
def _iterRecursiveText(self, ti: offsets.OffsetsTextInfo, controlStack, formatConfig):
if ti.obj == self._endObj:
end = True
ti.setEndPoint(self._end, "endToEnd")
Expand All @@ -257,9 +265,15 @@ def _iterRecursiveText(self, ti, controlStack, formatConfig):
elif isinstance(item, str):
yield item
elif isinstance(item, int): # Embedded object.
embedded = _getEmbedded(ti.obj, item)
embedded: typing.Optional[IAccessible] = _getEmbedded(ti.obj, item)
notText = _getRawTextInfo(embedded) is NVDAObjectTextInfo
if controlStack is not None:
if (
controlStack is not None
and not (
self._isObjectEditableText(embedded)
or self._isNamedlinkDestination(embedded)
)
):
controlField = self._getControlFieldForObject(embedded)
controlStack.append(controlField)
if controlField:
Expand All @@ -272,8 +286,9 @@ def _iterRecursiveText(self, ti, controlStack, formatConfig):
# Note #11291:
# Using a space character (EG " ") causes 'space' to be announced after objects like graphics.
# If this is replaced with an empty string, routing to cell becomes innaccurate.
# Using the OBJECT REPLACEMENT CHARACTER (EG "\uFFFC") results in '"0xFFFC' being displayed on
# the braille device.
# Using the textUtils.OBJ_REPLACEMENT_CHAR which is the
# "OBJECT REPLACEMENT CHARACTER" (EG "\uFFFC")
# results in '"0xFFFC' being displayed on the braille device.
yield " "
else:
for subItem in self._iterRecursiveText(self._makeRawTextInfo(embedded, textInfos.POSITION_ALL), controlStack, formatConfig):
Expand Down Expand Up @@ -304,16 +319,19 @@ def _getText(self, withFields, formatConfig=None):
ti = self._start
cannotBeStart = False
while obj and obj != rootObj:
field = self._getControlFieldForObject(obj)
if field:
if not (
self._isObjectEditableText(obj)
or self._isNamedlinkDestination(obj)
):
field = self._getControlFieldForObject(obj)
if ti._startOffset == 0:
if not cannotBeStart:
field["_startOfNode"] = True
else:
# We're not at the start of this object, which also means we're not at the start of any ancestors.
cannotBeStart = True
fields.insert(0, textInfos.FieldCommand("controlStart", field))
controlStack.insert(0, field)
controlStack.insert(0, field)
ti = self._getEmbedding(obj)
if not ti:
log.debugWarning(
Expand All @@ -332,7 +350,7 @@ def _getText(self, withFields, formatConfig=None):
# (We already handled collapsed above.)
return fields
obj = self._startObj
while fields[-1] is not None:
while fields[-1] is not None and controlStack:
# The end hasn't yet been reached, which means it isn't a descendant of obj.
# Therefore, continue from where obj was embedded.
if withFields:
Expand All @@ -345,10 +363,9 @@ def _getText(self, withFields, formatConfig=None):
# range is invalid, so just return nothing.
log.debugWarning("Tried to walk up past the root. Objects probably dead.")
return []
if field:
# This object had a control field.
field["_endOfNode"] = True
fields.append(textInfos.FieldCommand("controlEnd", None))
# This object had a control field.
field["_endOfNode"] = True
fields.append(textInfos.FieldCommand("controlEnd", None))
ti = self._getEmbedding(obj)
if not ti:
log.debugWarning(
Expand All @@ -368,13 +385,11 @@ def _getText(self, withFields, formatConfig=None):
# Determine whether the range covers the end of any ancestors of endObj.
obj = self._endObj
ti = self._end
while obj and obj != rootObj:
while obj and obj != rootObj and controlStack:
field = controlStack.pop()
if field:
fields.append(textInfos.FieldCommand("controlEnd", None))
fields.append(textInfos.FieldCommand("controlEnd", None))
if ti.compareEndPoints(self._makeRawTextInfo(obj, textInfos.POSITION_ALL), "endToEnd") == 0:
if field:
field["_endOfNode"] = True
field["_endOfNode"] = True
else:
# We're not at the end of this object, which also means we're not at the end of any ancestors.
break
Expand Down
20 changes: 14 additions & 6 deletions source/NVDAObjects/IAccessible/ia2Web.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
#NVDAObjects/IAccessible/ia2Web.py
#A part of NonVisual Desktop Access (NVDA)
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.
#Copyright (C) 2006-2017 NV Access Limited
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2006-2021 NV Access Limited

"""Base classes with common support for browsers exposing IAccessible2.
"""

import typing
from ctypes import c_short
from comtypes import COMError, BSTR
import oleacc
Expand Down Expand Up @@ -34,6 +33,15 @@ def _get_positionInfo(self):
info['level']=level
return info

def _get_descriptionFrom(self) -> controlTypes.DescriptionFrom:
ia2attrDescriptionFrom: typing.Optional[str] = self.IA2Attributes.get("description-from")
try:
return controlTypes.DescriptionFrom(ia2attrDescriptionFrom)
except ValueError:
if ia2attrDescriptionFrom:
log.debugWarning(f"Unknown 'description-from' IA2Attribute value: {ia2attrDescriptionFrom}")
return controlTypes.DescriptionFrom.UNKNOWN

def _get_isCurrent(self) -> controlTypes.IsCurrent:
ia2attrCurrent: str = self.IA2Attributes.get("current", "false")
try:
Expand Down
34 changes: 29 additions & 5 deletions source/NVDAObjects/IAccessible/mozilla.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# -*- coding: UTF-8 -*-
#NVDAObjects/IAccessible/mozilla.py
#A part of NonVisual Desktop Access (NVDA)
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.
#Copyright (C) 2006-2017 NV Access Limited, Peter Vágner
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2006-2021 NV Access Limited, Peter Vágner

import IAccessibleHandler
from comInterfaces import IAccessible2Lib as IA2
Expand All @@ -23,6 +22,31 @@ def _get_states(self):
states.add(controlTypes.STATE_CHECKABLE)
return states

def _get_descriptionFrom(self) -> controlTypes.DescriptionFrom:
"""Firefox does not yet support 'description-from' attribute (which informs
NVDA of the source of accDescription after the name/description computation
is complete. However, a primary use-case can be supported via the IA2attribute
'description' which is exposed by Firefox and tells us the value of the "aria-description"
attribute. If the value of accDescription matches, we can infer that the source
of accDescription is 'aria-description'. Note: At the time of development not all
HTML elements result in the IA2attribute 'description' being exposed. EG on a 'span'
element it is not exposed.
Comment thread
feerrenrut marked this conversation as resolved.
Resolved
"""
log.debug("Getting mozilla descriptionFrom")
ariaDesc = self.IA2Attributes.get("description", "")
log.debug(f"description IA2Attribute is: {ariaDesc}")
if (
ariaDesc == "" # aria-description is missing or empty
# Ensure that aria-description is actually the value used.
# I.E. accDescription is sourced from the aria-description attribute as a result of the
# name/description computation.
# If the values don't match, some other source must have been used.
or self.description != ariaDesc
):
return controlTypes.DescriptionFrom.UNKNOWN
else:
return controlTypes.DescriptionFrom.ARIA_DESCRIPTION

def _get_presentationType(self):
presType=super(Mozilla,self).presentationType
if presType==self.presType_content:
Expand Down
8 changes: 7 additions & 1 deletion source/NVDAObjects/JAB/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2006-2021 NV Access Limited, Leonard de Ruijter, Joseph Lee, Renaud Paquay, pvagner
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

import ctypes
import re
import eventHandler
import keyLabels
import JABHandler
import controlTypes
import textUtils
from ..window import Window
from ..behaviors import EditableTextWithoutAutoSelectDetection, Dialog
import textInfos.offsets
Expand Down Expand Up @@ -174,7 +180,7 @@ def getEmbeddedObject(self, offset=0):
# We need to count the embedded objects to determine which child to use.
# This could possibly be optimised by caching.
text = self._getTextRange(0, offset + 1)
childIndex = text.count(u"\uFFFC") - 1
childIndex = text.count(textUtils.OBJ_REPLACEMENT_CHAR) - 1
jabContext=self.obj.jabContext.getAccessibleChildFromContext(childIndex)
if jabContext:
return JAB(jabContext=jabContext)
Expand Down
Loading