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