From ea94505ea5f3fd249722e6c1b5044f6e20f8847d Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Thu, 26 Sep 2024 05:47:50 +0200 Subject: [PATCH] Speech feedback on more Word formatting shortcuts (#17197) Fixes (at least partially) #10271 Supersedes #10283. Summary of the issue: User wants to have font formatting state announced when native font formatting shortcut are executed under Word and Outlook. Description of user facing changes The results of the following MS Word shortcuts are now reported: control+shift+D: Underline double control+shift+W: Underline words only control+shift+A: All caps ctrl+Shift+K: Small caps ctrl+shift+i, ctrl+shift+b and ctrl+shift+u: alternative gestures for toggle italic, bold and underline. When relevant, these shortcuts are also reported in Outlook. Notes about various capitalization shortcuts In Word, the Shift+F3 and Ctrl+Shift+A shortcuts are slightly different: Shift+F3 only modifies the case of the current selection (or the word under the cursor if there is no selection) Ctrl+Shift+A also modifies the case of selected text (or current word), but in addition, it modifies the case of the characters that will be typed at insertion point of the cursor. Outlook's Ctrl+Shift+A shortcut is equivalent to Word's Shift+F3, whereas Word's Ctrl+Shift+A has no equivalent under Outlook as far as I know. Messages Word's shortcuts Ctrl+Shift+A, Ctrl+Shift+K and Shift+F3 modify all the selection's (or current word's) capitalization. However, Ctrl+Shift+A and Ctrl+Shift+K also impact the next characters that will be typed. Thus for this shortcut "on" and "off" have been used in their announced message in order to denote a state change. On the contrary Shift+F3 announces only the result of a modification of current selection (or word); thus "on" and "off" have not been used in its message. Description of development approach Use object model and polling to check state change, as done for other formatting scripts. --- source/NVDAObjects/window/winword.py | 156 +++++++++++++++++++++------ source/appModules/winword.py | 8 +- user_docs/en/changes.md | 1 + 3 files changed, 133 insertions(+), 32 deletions(-) diff --git a/source/NVDAObjects/window/winword.py b/source/NVDAObjects/window/winword.py index 8e42a339607..672d7886d16 100755 --- a/source/NVDAObjects/window/winword.py +++ b/source/NVDAObjects/window/winword.py @@ -10,6 +10,7 @@ Optional, Dict, Generator, + Self, TYPE_CHECKING, ) @@ -43,6 +44,7 @@ import locationHelper from enum import IntEnum import documentBase +from utils.displayString import DisplayStringIntEnum if TYPE_CHECKING: import inputCore @@ -78,6 +80,69 @@ wdMaximumNumberOfRows = 15 wdStartOfRangeColumnNumber = 16 wdMaximumNumberOfColumns = 18 + + +class WdUnderline(DisplayStringIntEnum): + # Word underline styles + # see https://docs.microsoft.com/en-us/office/vba/api/word.wdunderline + NONE = 0 + SINGLE = 1 + WORDS = 2 + DOUBLE = 3 + DOTTED = 4 + THICK = 6 + DASH = 7 + DOT_DASH = 9 + DOT_DOT_DASH = 10 + WAVY = 11 + DOTTED_HEAVY = 20 + DASH_HEAVY = 23 + DOT_DASH_HEAVY = 25 + DOT_DOT_DASH_HEAVY = 26 + WAVY_HEAVY = 27 + DASH_LONG = 39 + WAVY_DOUBLE = 43 + DASH_LONG_HEAVY = 55 + + @property + def _displayStringLabels(self) -> dict[Self, str]: + return { + WdUnderline.SINGLE: "", # For single underline we just say "underline" + # Translators: an underline style in Microsoft Word as announced in the font window. + WdUnderline.WORDS: _("Words only"), + # Translators: an underline style in Microsoft Word as announced in the font window. + WdUnderline.DOUBLE: _("Double"), + # Translators: an underline style in Microsoft Word as announced in the font window. + WdUnderline.DOTTED: _("Dotted"), + # Translators: an underline style in Microsoft Word as announced in the font window. + WdUnderline.THICK: _("Thick"), + # Translators: an underline style in Microsoft Word as announced in the font window. + WdUnderline.DASH: _("Dash"), + # Translators: an underline style in Microsoft Word as announced in the font window. + WdUnderline.DOT_DASH: _("Dot dash"), + # Translators: an underline style in Microsoft Word as announced in the font window. + WdUnderline.DOT_DOT_DASH: _("Dot dot dash"), + # Translators: an underline style in Microsoft Word as announced in the font window. + WdUnderline.WAVY: _("Wave"), + # Translators: an underline style in Microsoft Word as announced in the font window. + WdUnderline.DOTTED_HEAVY: _("Dotted heavy"), + # Translators: an underline style in Microsoft Word as announced in the font window. + WdUnderline.DASH_HEAVY: _("Dashed heavy"), + # Translators: an underline style in Microsoft Word as announced in the font window. + WdUnderline.DOT_DASH_HEAVY: _("Dot dash heavy"), + # Translators: an underline style in Microsoft Word as announced in the font window. + WdUnderline.DOT_DOT_DASH_HEAVY: _("Dot dot dash heavy"), + # Translators: an underline style in Microsoft Word as announced in the font window. + WdUnderline.WAVY_HEAVY: _("Wave heavy"), + # Translators: an underline style in Microsoft Word as announced in the font window. + WdUnderline.DASH_LONG: _("Dashed long"), + # Translators: an underline style in Microsoft Word as announced in the font window. + WdUnderline.WAVY_DOUBLE: _("Wave double"), + # Translators: an underline style in Microsoft Word as announced in the font window. + WdUnderline.DASH_LONG_HEAVY: _("Dashed long heavy"), + } + + # Horizontal alignment wdAlignParagraphLeft = 0 wdAlignParagraphCenter = 1 @@ -1507,6 +1572,7 @@ def _WaitForValueChangeForAction(self, action, fetcher, timeout=0.15): curTime = time.time() return curVal + @script(gesture="kb:control+b") def script_toggleBold(self, gesture): if not self.WinwordSelectionObject: # We cannot fetch the Word object model, so we therefore cannot report the format change. @@ -1524,6 +1590,7 @@ def script_toggleBold(self, gesture): # Translators: a message when toggling formatting in Microsoft word ui.message(_("Bold off")) + @script(gestures=["kb:control+i", "kb:control+shift+i"]) def script_toggleItalic(self, gesture): if not self.WinwordSelectionObject: # We cannot fetch the Word object model, so we therefore cannot report the format change. @@ -1541,23 +1608,59 @@ def script_toggleItalic(self, gesture): # Translators: a message when toggling formatting in Microsoft word ui.message(_("Italic off")) - def script_toggleUnderline(self, gesture): + @script(gestures=["kb:control+u", "kb:control+shift+u", "kb:control+shift+d"]) + def script_toggleUnderline(self, gesture: "inputCore.InputGesture"): if not self.WinwordSelectionObject: - # We cannot fetch the Word object model, so we therefore cannot report the format change. - # The object model may be unavailable because this is a pure UIA implementation such as Windows 10 Mail, or its within Windows Defender Application Guard. - # Eventually UIA will have its own way of detecting format changes at the cursor. For now, just let the gesture through and don't erport anything. + # The object model may be unavailable because this is a pure UIA implementation such as Windows 10 Mail, + # or its within Windows Defender Application Guard. + # Eventually UIA will have its own way of detecting format changes at the cursor. + # For now, just let the gesture through and don't report anything. return gesture.send() val = self._WaitForValueChangeForAction( lambda: gesture.send(), lambda: self.WinwordSelectionObject.font.underline, ) - if val: - # Translators: a message when toggling formatting in Microsoft word - ui.message(_("Underline on")) + if val != WdUnderline.NONE: + try: + style = WdUnderline(val).displayString + if len(style) > 0: + style += " " + # Translators: a message when toggling formatting in Microsoft word + ui.message(_("Underline {style}on").format(style=style)) + except ValueError: + # In case an unlisted value is returned by Word Object model. + # This may happen if the selection contains multiple underline styles and if the gesture has failed to + # apply the underline style (e.g. too short timeout, gesture mismatch due to localization mismatch + # between Word and NVDA, etc.) + log.debugWarning(f"No underline value for {val}") else: # Translators: a message when toggling formatting in Microsoft word ui.message(_("Underline off")) + @script(gesture="kb:control+shift+k") + def script_toggleCaps(self, gesture: "inputCore.InputGesture"): + if not self.WinwordSelectionObject: + # We cannot fetch the Word object model, so we therefore cannot report the format change. + # The object model may be unavailable because this is a pure UIA implementation such as Windows 10 Mail, + # or its within Windows Defender Application Guard. + # Eventually UIA will have its own way of detecting format changes at the cursor. + # For now, just let the gesture through and don't report anything. + return gesture.send() + val = self._WaitForValueChangeForAction( + lambda: gesture.send(), + lambda: (self.WinwordSelectionObject.font.allcaps, self.WinwordSelectionObject.font.smallcaps), + ) + if val[0]: + # Translators: a message when toggling formatting to 'all capital' in Microsoft word + ui.message(_("All caps on")) + elif val[1]: + # Translators: a message when toggling formatting to 'small capital' in Microsoft word + ui.message(_("Small caps on")) + else: + # Translators: a message when toggling formatting to 'No capital' in Microsoft word + ui.message(_("Caps off")) + + @script(gestures=["kb:control+l", "kb:control+e", "kb:control+r", "kb:control+j"]) def script_toggleAlignment(self, gesture): if not self.WinwordSelectionObject: # We cannot fetch the Word object model, so we therefore cannot report the format change. @@ -1598,6 +1701,7 @@ def script_changeParagraphLeftIndent(self, gesture): msg = self.getLocalizedMeasurementTextForPointSize(margin + val) ui.message(msg) + @script(gestures=["kb:control+=", "kb:control+shift+="]) def script_toggleSuperscriptSubscript(self, gesture): if not self.WinwordSelectionObject: # We cannot fetch the Word object model, so we therefore cannot report the format change. @@ -1621,6 +1725,7 @@ def script_toggleSuperscriptSubscript(self, gesture): # Translators: a message when toggling formatting in Microsoft word ui.message(_("Baseline")) + @script(gesture="kb:alt+shift+downArrow") def script_moveParagraphDown(self, gesture): oldBookmark = self.makeTextInfo(textInfos.POSITION_CARET).bookmark gesture.send() @@ -1636,6 +1741,7 @@ def script_moveParagraphDown(self, gesture): # Translators: a message reported when a paragraph is moved below a blank paragraph ui.message(_("Moved below blank paragraph")) + @script(gesture="kb:alt+shift+upArrow") def script_moveParagraphUp(self, gesture): oldBookmark = self.makeTextInfo(textInfos.POSITION_CARET).bookmark gesture.send() @@ -1652,6 +1758,16 @@ def script_moveParagraphUp(self, gesture): # Translators: a message reported when a paragraph is moved above a blank paragraph ui.message(_("Moved above blank paragraph")) + @script( + gestures=[ + "kb:alt+shift+rightArrow", + "kb:alt+shift+leftArrow", + "kb:control+shift+n", + "kb:control+alt+1", + "kb:control+alt+2", + "kb:control+alt+3", + ], + ) def script_increaseDecreaseOutlineLevel(self, gesture): if not self.WinwordSelectionObject: # We cannot fetch the Word object model, so we therefore cannot report the format change. @@ -1668,6 +1784,7 @@ def script_increaseDecreaseOutlineLevel(self, gesture): _("{styleName} style, outline level {outlineLevel}").format(styleName=style, outlineLevel=val), ) + @script(gestures=["kb:control+[", "kb:control+]", "kb:control+shift+,", "kb:control+shift+."]) def script_increaseDecreaseFontSize(self, gesture): if not self.WinwordSelectionObject: # We cannot fetch the Word object model, so we therefore cannot report the format change. @@ -1779,6 +1896,7 @@ def getLocalizedMeasurementTextForPointSize(self, offset): offset, ).format(offset=offset) + @script(gestures=["kb:control+1", "kb:control+2", "kb:control+5"]) def script_changeLineSpacing(self, gesture): if not self.WinwordSelectionObject: # We cannot fetch the Word object model, so we therefore cannot report the format change. @@ -1822,30 +1940,6 @@ def initOverlayClass(self): self.bindGesture("kb:alt+shift+pageDown", "caret_changeSelection") __gestures = { - "kb:control+[": "increaseDecreaseFontSize", - "kb:control+]": "increaseDecreaseFontSize", - "kb:control+shift+,": "increaseDecreaseFontSize", - "kb:control+shift+.": "increaseDecreaseFontSize", - "kb:control+b": "toggleBold", - "kb:control+i": "toggleItalic", - "kb:control+u": "toggleUnderline", - "kb:control+=": "toggleSuperscriptSubscript", - "kb:control+shift+=": "toggleSuperscriptSubscript", - "kb:control+l": "toggleAlignment", - "kb:control+e": "toggleAlignment", - "kb:control+r": "toggleAlignment", - "kb:control+j": "toggleAlignment", - "kb:alt+shift+downArrow": "moveParagraphDown", - "kb:alt+shift+upArrow": "moveParagraphUp", - "kb:alt+shift+rightArrow": "increaseDecreaseOutlineLevel", - "kb:alt+shift+leftArrow": "increaseDecreaseOutlineLevel", - "kb:control+shift+n": "increaseDecreaseOutlineLevel", - "kb:control+alt+1": "increaseDecreaseOutlineLevel", - "kb:control+alt+2": "increaseDecreaseOutlineLevel", - "kb:control+alt+3": "increaseDecreaseOutlineLevel", - "kb:control+1": "changeLineSpacing", - "kb:control+2": "changeLineSpacing", - "kb:control+5": "changeLineSpacing", "kb:control+pageUp": "caret_moveByLine", "kb:control+pageDown": "caret_moveByLine", } diff --git a/source/appModules/winword.py b/source/appModules/winword.py index 4bab1814629..7e18fe0290b 100644 --- a/source/appModules/winword.py +++ b/source/appModules/winword.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) 2019-2020 NV Access Limited, Cyrille Bougot +# Copyright (C) 2019-2024 NV Access Limited, Cyrille Bougot """App module for Microsoft Word. Word and Outlook share a lot of code and components. This app module gathers the code that is relevant for @@ -40,3 +40,9 @@ def script_toggleChangeTracking(self, gesture): else: # Translators: a message when toggling change tracking in Microsoft word ui.message(_("Change tracking off")) + + __gestures = { + "kb:control+shift+b": "toggleBold", + "kb:control+shift+w": "toggleUnderline", + "kb:control+shift+a": "toggleCaps", + } diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 3f1ee9ec5a1..c3d0e198b62 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -31,6 +31,7 @@ In order to use this feature, the application volume adjuster needs to be enable * It now starts with a more user friendly explanation of its purpose, instead of a warning. (#12351) * The initial window can now be exited with `escape` or `alt+f4`. (#10799) * It will now show a message to the user, including the error, in the rare event of a Windows error while attempting COM re-registrations. +* In Word and Outlook the result of more font formatting shortcuts is now reported. (#10271, @CyrilleB79) ### Bug Fixes