From de4e8814f39ab6ae9fbc5330b007494ef6888cd3 Mon Sep 17 00:00:00 2001 From: Michael Weghorn Date: Mon, 23 Oct 2023 03:11:29 +0300 Subject: [PATCH] soffice: Support text attrs according to IA2 spec (#15649) 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. --- source/appModules/soffice.py | 109 ++++++++++++++++++++++++++++------- user_docs/en/changes.t2t | 6 +- 2 files changed, 93 insertions(+), 22 deletions(-) diff --git a/source/appModules/soffice.py b/source/appModules/soffice.py index 79a9ec783a2..c75df0e83a8 100755 --- a/source/appModules/soffice.py +++ b/source/appModules/soffice.py @@ -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)) @@ -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( @@ -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. @@ -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) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 034daab3b99..d179729f0a2 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -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)