From fa619c2d5b3c85799afffd03c0a380dd681a2cb0 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Mon, 20 Jan 2025 14:28:39 +1000 Subject: [PATCH] MS Word documents: report when headings are collapsed in both speech and braille. (#17499) Summary of the issue: In Microsoft Word, it is possible to collapse a heading, so that all the paragraphs directly following the heading are hidden. As of MS Word 16.0.18226, a new UIA text range custom attribute was added, to convey the expand collapse state of headings. NVDA should report when a heading is collapsed based on this value. Description of user facing changes In Microsoft Word, If a heading is collapsed, NVDA will now report this in speech and braille. Description of development approach * When fetching format information for a text range in MS Word via UIA, if the range supports the expand collapse custom attribute, expose the 'collapsed' key on its format field, setting it to true for collapsed, and false for expanded. * speech.getFormatfieldSpeech: speak "collapsed" when moving by line or paragraph over text with 'collapsed' set to true in its format field, or if it changes to true when moving by word / character. * In Braille, Report a plus (+) sign at the start of text that is collapsed according to its format field. Testing strategy: Tested with MS Word 16.0.18331. * Open a new document in Microsoft word. * Type an initial line of text. * Type a second line of text and format it as a heading (press alt+shift+rightArrow) * Type another line of text directly below it formatted as a normal paragraph. * Arrow up to the heading, noting that the heading is spoken / braille dnormally (I.e. heading level 2 test). No expand / collapse info is reported. * Collapse the heading by choosing collapse in the context menu. * Arrow up and back down to read the heading. Note that NvDA now reports 'collapsed' in speech and a plus (+) sign in braille. Known issues with pull request: None known. This implementation only reports when a heading is collapsed, and not explicitly when expanded. However, as expanded headings will far outweigh collapsed, I think it would be way too noisy to report expanded, when that is going to be the norm. --- nvdaHelper/remote/WinWord/Constants.h | 3 ++- nvdaHelper/remote/winword.cpp | 6 +++++- source/NVDAObjects/UIA/wordDocument.py | 18 +++++++++++++++++- source/NVDAObjects/window/winword.py | 6 +++++- source/braille.py | 5 ++++- source/speech/speech.py | 15 ++++++++++++++- user_docs/en/changes.md | 1 + 7 files changed, 48 insertions(+), 6 deletions(-) diff --git a/nvdaHelper/remote/WinWord/Constants.h b/nvdaHelper/remote/WinWord/Constants.h index d59bbc6489b..a82cd80c2d2 100644 --- a/nvdaHelper/remote/WinWord/Constants.h +++ b/nvdaHelper/remote/WinWord/Constants.h @@ -1,7 +1,7 @@ /* This file is a part of the NVDA project. URL: http://www.nvda-project.org/ -Copyright 2016-2023 NVDA contributors. +Copyright 2016-2025 NVDA contributors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 2.0, as published by the Free Software Foundation. @@ -79,6 +79,7 @@ constexpr int wdDISPID_PAGESETUP_PAGEWIDTH = 105; constexpr int wdDISPID_PAGESETUP_SECTIONSTART = 114; constexpr int wdDISPID_PAGESETUP_TEXTCOLUMNS = 119; constexpr int wdDISPID_PARAGRAPH_OUTLINELEVEL = 202; +constexpr int wdDISPID_PARAGRAPH_COLLAPSED_STATE = 1203; constexpr int wdDISPID_PARAGRAPH_RANGE = 0; constexpr int wdDISPID_PARAGRAPH_STYLE = 100; constexpr int wdDISPID_PARAGRAPHFORMAT_ALIGNMENT = 101; diff --git a/nvdaHelper/remote/winword.cpp b/nvdaHelper/remote/winword.cpp index bd05584a0ee..ee1b6e2cd89 100644 --- a/nvdaHelper/remote/winword.cpp +++ b/nvdaHelper/remote/winword.cpp @@ -1,7 +1,7 @@ /* This file is a part of the NVDA project. URL: http://www.nvda-project.org/ -Copyright 2006-2023 NVDA contributors. +Copyright 2006-2025 NVDA contributors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 2.0, as published by the Free Software Foundation. @@ -304,6 +304,10 @@ int generateHeadingXML(IDispatch* pDispatchParagraph, IDispatch* pDispatchParagr int headingLevel=getHeadingLevelFromParagraph(pDispatchParagraph); if(!headingLevel) return 0; XMLStream<=startOffset) { diff --git a/source/NVDAObjects/UIA/wordDocument.py b/source/NVDAObjects/UIA/wordDocument.py index 41ec251d524..cc6b8363b95 100644 --- a/source/NVDAObjects/UIA/wordDocument.py +++ b/source/NVDAObjects/UIA/wordDocument.py @@ -1,7 +1,7 @@ # This file is covered by the GNU General Public License. # A part of NonVisual Desktop Access (NVDA) # See the file COPYING for more details. -# Copyright (C) 2016-2023 NV Access Limited, Joseph Lee, Jakub Lukowicz, Cyrille Bougot +# Copyright (C) 2016-2025 NV Access Limited, Joseph Lee, Jakub Lukowicz, Cyrille Bougot from typing import ( Optional, @@ -51,6 +51,13 @@ class UIACustomAttributeID(enum.IntEnum): COLUMN_NUMBER = 2 SECTION_NUMBER = 3 BOOKMARK_NAME = 4 + COLUMNS_IN_SECTION = 5 + EXPAND_COLLAPSE_STATE = 6 + + +class EXPAND_COLLAPSE_STATE(enum.IntEnum): + COLLAPSED = 0 + EXPANDED = 1 #: the non-printable unicode character that represents the end of cell or end of row mark in Microsoft Word @@ -483,6 +490,15 @@ def _getFormatFieldAtRange(self, textRange, formatConfig, ignoreMixedValues=Fals ) if isinstance(textColumnNumber, int): formatField.field["text-column-number"] = textColumnNumber + expandCollapseState = UIARemote.msWord_getCustomAttributeValue( + docElement, + textRange, + UIACustomAttributeID.EXPAND_COLLAPSE_STATE, + ) + if expandCollapseState == EXPAND_COLLAPSE_STATE.COLLAPSED: + formatField.field["collapsed"] = True + elif expandCollapseState == EXPAND_COLLAPSE_STATE.EXPANDED: + formatField.field["collapsed"] = False return formatField def _getIndentValueDisplayString(self, val: float) -> str: diff --git a/source/NVDAObjects/window/winword.py b/source/NVDAObjects/window/winword.py index 10dd2ebc8bc..3115ea91484 100755 --- a/source/NVDAObjects/window/winword.py +++ b/source/NVDAObjects/window/winword.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2024 NV Access Limited, Manish Agrawal, Derek Riemer, Babbage B.V., Cyrille Bougot +# Copyright (C) 2006-2025 NV Access Limited, Manish Agrawal, Derek Riemer, Babbage B.V., Cyrille Bougot # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -1113,6 +1113,10 @@ def _normalizeControlField(self, field): field["name"] = name field["alwaysReportName"] = True field["role"] = controlTypes.Role.FRAME + if field.pop("collapsedState", None) == "true": + if "states" not in field: + field["states"] = set() + field["states"].add(controlTypes.State.COLLAPSED) newField = LazyControlField_RowAndColumnHeaderText(self) newField.update(field) return newField diff --git a/source/braille.py b/source/braille.py index 54a70dac138..00eb8a817cc 100644 --- a/source/braille.py +++ b/source/braille.py @@ -1,7 +1,7 @@ # 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) 2008-2024 NV Access Limited, Joseph Lee, Babbage B.V., Davy Kager, Bram Duvigneau, +# Copyright (C) 2008-2025 NV Access Limited, Joseph Lee, Babbage B.V., Davy Kager, Bram Duvigneau, # Leonard de Ruijter, Burman's Computer and Education Ltd., Julien Cochuyt from enum import StrEnum @@ -1160,6 +1160,9 @@ def getFormatFieldBraille(field, fieldCache, isAtStart, formatConfig): # Translators: Displayed in braille for a heading with a level. # %s is replaced with the level. textList.append(_("h%s") % headingLevel) + collapsed = field.get("collapsed") + if collapsed: + textList.append(positiveStateLabels[controlTypes.State.COLLAPSED]) if formatConfig["reportLinks"]: link = field.get("link") oldLink = fieldCache.get("link") diff --git a/source/speech/speech.py b/source/speech/speech.py index 49992c0b03f..4045a1154d1 100644 --- a/source/speech/speech.py +++ b/source/speech/speech.py @@ -1,7 +1,7 @@ # 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-2024 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Babbage B.V., Bill Dengler, +# Copyright (C) 2006-2025 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Babbage B.V., Bill Dengler, # Julien Cochuyt, Derek Riemer, Cyrille Bougot, Leonard de Ruijter, Łukasz Golonka """High-level functions to speak information.""" @@ -2620,6 +2620,19 @@ def getFormatFieldSpeech( # noqa: C901 # Translators: Speaks the heading level (example output: heading level 2). text = _("heading level %d") % headingLevel textList.append(text) + collapsed = attrs.get("collapsed") + oldCollapsed = attrsCache.get("collapsed") if attrsCache is not None else None + # collapsed state should be spoken when beginning to speak lines or paragraphs + # Ensuring a similar experience to if it was a state on a controlField + if collapsed and ( + initialFormat + and ( + reason in [OutputReason.FOCUS, OutputReason.QUICKNAV] + or unit in (textInfos.UNIT_LINE, textInfos.UNIT_PARAGRAPH) + ) + or collapsed != oldCollapsed + ): + textList.append(State.COLLAPSED.displayString) if formatConfig["reportStyle"]: style = attrs.get("style") oldStyle = attrsCache.get("style") if attrsCache is not None else None diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 133fe64d973..1d701a88119 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -27,6 +27,7 @@ To use this feature, "allow NVDA to control the volume of other applications" mu * Automatic language switching is now supported when using Microsoft Speech API version 5 (SAPI5) and Microsoft Speech Platform voices. (#17146, @gexgd0419) * NVDA can now be configured to speak the current line or paragraph when navigating with braille navigation keys. (#17053, @nvdaes) * In Word, the selection update is now reported when using Word commands to extend or reduce the selection (`f8` or `shift+f8`). (#3293, @CyrilleB79) +* In Microsoft Word 16.0.18226 and higher or when using Word object model, NVDA will now report if a heading is collapsed in both speech and braille. (#17499) ### Changes