Skip to content

Commit

Permalink
soffice: Support text attrs according to IA2 spec (#15649)
Browse files Browse the repository at this point in the history
Fixes #15648

Summary of the issue:
So far, LibreOffice was using custom attribute and value
names for reporting text attributes. Spelling errors were
not reported via any attribute. NVDA was using the presence
of a specific underline as heuristic to detect and report
spelling errors.
This works for some cases, but e.g. does not cause misspelled
words on a line being annonced as such when reading a
line in LibreOffice Writer (issue #15648).

Description of user facing changes
Announcement of text attributes also works with
LibreOffice version 24.2 and above.
When announcing a line in LibreOffice Writer, misspelled
words are announced as such with LibreOffice version 24.2
and above.

Description of development approach
Switch LibreOffice from using custom text attribute names
and values to using attributes according to the IAccessible2
text attributes specification
( https://wiki.linuxfoundation.org/accessibility/iaccessible2/textattributes )
instead and implement reporting of the "invalid:spelling;"
attribute for misspelled words:

https://gerrit.libreoffice.org/c/core/+/157804
https://gerrit.libreoffice.org/c/core/+/157845
https://gerrit.libreoffice.org/c/core/+/157867
https://gerrit.libreoffice.org/c/core/+/157939
https://gerrit.libreoffice.org/c/core/+/158088
https://gerrit.libreoffice.org/c/core/+/158089
https://gerrit.libreoffice.org/c/core/+/158090

These changes are contained in LibreOffice >= 24.2.

Adapt NVDA to evaluate those text attributes by
using the already existing implementation from
the IA2TextTextInfo base class in
SymphonyTextInfo._getFormatFieldAndOffsets.

For backwards-compatibility with LibreOffice
versions <= 7.6, keep support for the legacy
attributes and move the handling for that into
a new helper method
SymphonyTextInfo_getFormatFieldFromLegacyAttributesString.

For the case where the legacy attributes are used,
the text attribute string starts with "Version:1;"
(s. the LibreOffice code dropped in
https://gerrit.libreoffice.org/c/core/+/158090 ),
so use that as a criterion what code path to take.

Extract another helper method and address some of the
pre-existing lint issues, but silence
the C901 one for the method that was extracted to handle
the legacy attributes
("'SymphonyTextInfo._getFormatFieldFromLegacyAttributesString' is
too complex (27)").
It's at least already less complex than the single
one was before.
  • Loading branch information
michaelweghorn committed Oct 23, 2023
1 parent 3e62667 commit de4e881
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 22 deletions.
109 changes: 89 additions & 20 deletions source/appModules/soffice.py
Expand Up @@ -31,20 +31,28 @@


class SymphonyTextInfo(IA2TextTextInfo):

def _getFormatFieldAndOffsets(self,offset,formatConfig,calculateOffsets=True):
obj = self.obj
try:
startOffset,endOffset,attribsString=obj.IAccessibleTextObject.attributes(offset)
except COMError:
log.debugWarning("could not get attributes",exc_info=True)
return textInfos.FormatField(),(self._startOffset,self._endOffset)
# C901 '_getFormatFieldFromLegacyAttributesString' is too complex
# Note: when working on _getFormatFieldFromLegacyAttributesString, look for opportunities to simplify
# and move logic out into smaller helper functions.
# This is legacy code, kept for compatibility reasons.
def _getFormatFieldFromLegacyAttributesString( # noqa: C901
self,
attribsString: str,
offset: int
) -> textInfos.FormatField:

"""Get format field with information retrieved from a text
attributes string containing LibreOffice's legacy custom text
attributes (used by LibreOffice <= 7.6), instead of attributes
according to the IAccessible2 text attributes specification
(used by LibreOffice >= 24.2).
:param attribsString: Legacy text attributes string.
:param offset: Character offset for which to retrieve the
attributes.
:return: Format field containing the text attribute information.
"""
formatField=textInfos.FormatField()
if not attribsString and offset>0:
try:
attribsString=obj.IAccessibleTextObject.attributes(offset-1)[2]
except COMError:
pass
if attribsString:
formatField.update(splitIA2Attribs(attribsString))

Expand Down Expand Up @@ -101,6 +109,73 @@ def _getFormatFieldAndOffsets(self,offset,formatConfig,calculateOffsets=True):
if backgroundColor:
formatField['background-color']=colors.RGB.fromString(backgroundColor)

if offset == 0:
# Only include the list item prefix on the first line of the paragraph.
numbering = formatField.get("Numbering")
if numbering:
formatField["line-prefix"] = numbering.get("NumberingPrefix") or numbering.get("BulletChar")

return formatField

def _getFormatFieldAndOffsetsFromAttributes(
self,
offset: int,
formatConfig: Optional[dict],
calculateOffsets: bool
) -> tuple[textInfos.FormatField, tuple[int, int]]:
"""Get format field and offset information from either
attributes according to the IAccessible2 specification
(for LibreOffice >= 24.2) or from legacy custom
text attributes (used by LibreOffice <= 7.6 and Apache OpenOffice).
:param offset: Character offset for which to retrieve the
attributes.
:param formatConfig: Format configuration.
:param calculateOffsets: Whether to calculate offsets.
:return: Format field containing the text attribute information
and start and end offset of the attribute run.
"""
obj = self.obj
try:
startOffset, endOffset, attribsString = obj.IAccessibleTextObject.attributes(offset)
except COMError:
log.debugWarning("could not get attributes", exc_info=True)
return textInfos.FormatField(), (self._startOffset, self._endOffset)

if not attribsString and offset > 0:
try:
attribsString = obj.IAccessibleTextObject.attributes(offset - 1)[2]
except COMError:
pass

# LibreOffice >= 24.2 uses IAccessible2 text attributes, earlier versions use
# custom attributes, with the attributes string starting with "Version:1;"
if attribsString and attribsString.startswith('Version:1;'):
formatField = self._getFormatFieldFromLegacyAttributesString(
attribsString,
offset
)
else:
formatField, (startOffset, endOffset) = super()._getFormatFieldAndOffsets(
offset,
formatConfig,
calculateOffsets
)

return formatField, (startOffset, endOffset)

def _getFormatFieldAndOffsets(
self,
offset: int,
formatConfig: Optional[dict],
calculateOffsets: bool = True
) -> tuple[textInfos.FormatField, tuple[int, int]]:
formatField, (startOffset, endOffset) = self._getFormatFieldAndOffsetsFromAttributes(
offset,
formatConfig,
calculateOffsets
)
obj = self.obj

# optimisation: Assume a hyperlink occupies a full attribute run.
try:
if obj.IAccessibleTextObject.QueryInterface(
Expand All @@ -110,12 +185,6 @@ def _getFormatFieldAndOffsets(self,offset,formatConfig,calculateOffsets=True):
except COMError:
pass

if offset == 0:
# Only include the list item prefix on the first line of the paragraph.
numbering = formatField.get("Numbering")
if numbering:
formatField["line-prefix"] = numbering.get("NumberingPrefix") or numbering.get("BulletChar")

if obj.hasFocus:
# Symphony exposes some information for the caret position as attributes on the document object.
# optimisation: Use the tree interceptor to get the document.
Expand All @@ -134,7 +203,7 @@ def _getFormatFieldAndOffsets(self,offset,formatConfig,calculateOffsets=True):
except KeyError:
pass

return formatField,(startOffset,endOffset)
return formatField, (startOffset, endOffset)

def _getLineOffsets(self, offset):
start, end = super(SymphonyTextInfo, self)._getLineOffsets(offset)
Expand Down
6 changes: 4 additions & 2 deletions user_docs/en/changes.t2t
Expand Up @@ -30,8 +30,10 @@ This option now announces additional relevant information about an object when t
- Reporting of object shortcut keys has been improved. (#10807, @CyrilleB79)
- The SAPI4 synthesizer now properly supports volume, rate and pitch changes embedded in speech. (#15271, @LeonarddeR)
- Multi line state is now correctly reported in applications using Java Access Bridge. (#14609)
- In LibreOffice, words deleted using the ``control+backspace`` keyboard shortcut are now also properly announced when the deleted word is followed by whitespace (like spaces and tabs). (#15436)
- In LibreOffice, announcement of the status bar using the ``NVDA+end`` keyboard shortcut now also works for dialogs in LibreOffice version 24.2 and newer. (#15591)
- In LibreOffice, words deleted using the ``control+backspace`` keyboard shortcut are now also properly announced when the deleted word is followed by whitespace (like spaces and tabs). (#15436, @michaelweghorn)
- In LibreOffice, announcement of the status bar using the ``NVDA+end`` keyboard shortcut now also works for dialogs in LibreOffice version 24.2 and newer. (#15591, @michaelweghorn)
- In LibreOffice, text attributes according to the [IAccessible2 text attributes specification https://wiki.linuxfoundation.org/accessibility/iaccessible2/textattributes] are supported, which is required to support announcement of text attributes in LibreOffice versions 24.2 and above.
This makes the announcement of spelling errors work when announcing a line in Writer. (#15648, @michaelweghorn)
- In Microsoft Excel with UIA disabled, braille is updated, and the active cell content is spoken, when ``control+y``, ``control+z`` or ``alt+backspace`` is pressed. (15547)
- In Microsoft Word with UIA disabled braille is updated when ``control+v``, ``control+x``, ``control+y``, ``control+z``, ``alt+backspace``, ``backspace`` or ``control+backspace`` is pressed.
It is also updated with UIA enabled, when typing text and braille is tethered to review and review follows caret. (#3276)
Expand Down

0 comments on commit de4e881

Please sign in to comment.