diff --git a/.gitmodules b/.gitmodules index aec6aa80953..41835aa2324 100644 --- a/.gitmodules +++ b/.gitmodules @@ -34,9 +34,6 @@ [submodule "include/configobj"] path = include/configobj url = https://github.com/DiffSK/configobj.git -[submodule "include/cldr-emoji-annotation"] - path = include/cldr-emoji-annotation - url = https://github.com/fujiwarat/cldr-emoji-annotation [submodule "include/py2exe"] path = include/py2exe url = https://github.com/nvaccess/py2exe-bin @@ -46,3 +43,6 @@ [submodule "include/w3c-aria-practices"] path = include/w3c-aria-practices url = https://github.com/w3c/aria-practices +[submodule "include/cldr"] + path = include/cldr + url = https://github.com/unicode-org/cldr.git diff --git a/cldrDict_sconscript b/cldrDict_sconscript index 4a227160d69..e6bf1a2b0e3 100644 --- a/cldrDict_sconscript +++ b/cldrDict_sconscript @@ -118,8 +118,8 @@ NVDAToCLDRLocales = { "zh_tw":("zh","zh_Hant"), } -annotationsDir = env.Dir("include/cldr-emoji-annotation/annotations") -annotationsDerivedDir = env.Dir("include/cldr-emoji-annotation/annotationsDerived") +annotationsDir = env.Dir("include/cldr/common/annotations") +annotationsDerivedDir = env.Dir("include/cldr/common/annotationsDerived") for destLocale, sourceLocales in NVDAToCLDRLocales.items(): cldrSources = [] # First add all annotations, then the derived ones. diff --git a/include/cldr b/include/cldr new file mode 160000 index 00000000000..d1c59aeea6a --- /dev/null +++ b/include/cldr @@ -0,0 +1 @@ +Subproject commit d1c59aeea6a26cd1018e164e8c470e4873925cac diff --git a/include/cldr-emoji-annotation b/include/cldr-emoji-annotation deleted file mode 160000 index abab174d47e..00000000000 --- a/include/cldr-emoji-annotation +++ /dev/null @@ -1 +0,0 @@ -Subproject commit abab174d47e2d9f355f21fb0da0732854ed0e905 diff --git a/nvdaHelper/interfaces/nvdaControllerInternal/nvdaControllerInternal.acf b/nvdaHelper/interfaces/nvdaControllerInternal/nvdaControllerInternal.acf index c870200ada3..7749274d6c8 100644 --- a/nvdaHelper/interfaces/nvdaControllerInternal/nvdaControllerInternal.acf +++ b/nvdaHelper/interfaces/nvdaControllerInternal/nvdaControllerInternal.acf @@ -17,7 +17,6 @@ http://www.gnu.org/licenses/old-licenses/gpl-2.0.html ] interface NvdaControllerInternal { [fault_status,comm_status] requestRegistration(); - [fault_status,comm_status] reportLiveRegion(); [fault_status,comm_status] inputLangChangeNotify(); [fault_status,comm_status] typedCharacterNotify(); [fault_status,comm_status] displayModelTextChangeNotify(); @@ -25,4 +24,5 @@ interface NvdaControllerInternal { [fault_status,comm_status] vbufChangeNotify(); [fault_status,comm_status] installAddonPackageFromPath(); [fault_status,comm_status] drawFocusRectNotify(); + [fault_status,comm_status] reportLiveRegion(); } diff --git a/nvdaHelper/interfaces/nvdaControllerInternal/nvdaControllerInternal.idl b/nvdaHelper/interfaces/nvdaControllerInternal/nvdaControllerInternal.idl index d692bbf8f1a..85f26456128 100644 --- a/nvdaHelper/interfaces/nvdaControllerInternal/nvdaControllerInternal.idl +++ b/nvdaHelper/interfaces/nvdaControllerInternal/nvdaControllerInternal.idl @@ -37,13 +37,6 @@ interface NvdaControllerInternal { error_status_t __stdcall requestRegistration([in,string] const wchar_t* uuidString); -/** - * Notifies NVDA that a live region was updated. - * @param text the text to report for the live region. - * @param level The level of live region, I.E. "polite" - */ - error_status_t __stdcall reportLiveRegion([in,string] const wchar_t* text, [in,string] const wchar_t* level); - /** * Notifies NVDA that the keyboard layout has changed for this thread. * @param threadID the thread the layout change occured in @@ -97,4 +90,11 @@ interface NvdaControllerInternal { * Notifies NVDA that a focus rect has been drawn in the given window. */ error_status_t __stdcall drawFocusRectNotify([in] const long hwnd, [in] const long left, [in] const long top, [in] const long right, [in] const long bottom); + +/** + * Notifies NVDA that a live region was updated. + * @param text the text to report for the live region. + * @param level The level of live region, I.E. "polite" + */ + error_status_t __stdcall reportLiveRegion([in,string] const wchar_t* text, [in,string] const wchar_t* level); }; diff --git a/nvdaHelper/local/nvdaControllerInternal.c b/nvdaHelper/local/nvdaControllerInternal.c index acf074b4794..3808746d5fa 100644 --- a/nvdaHelper/local/nvdaControllerInternal.c +++ b/nvdaHelper/local/nvdaControllerInternal.c @@ -19,11 +19,6 @@ error_status_t __stdcall nvdaControllerInternal_requestRegistration(const wchar_ return _nvdaControllerInternal_requestRegistration(uuidString); } -error_status_t(__stdcall *_nvdaControllerInternal_reportLiveRegion)(const wchar_t*, const wchar_t*); -error_status_t __stdcall nvdaControllerInternal_reportLiveRegion(const wchar_t* text, const wchar_t* politeness) { - return _nvdaControllerInternal_reportLiveRegion(text, politeness); -} - error_status_t(__stdcall *_nvdaControllerInternal_inputLangChangeNotify)(const long, const unsigned long, const wchar_t*); error_status_t __stdcall nvdaControllerInternal_inputLangChangeNotify(const long threadID, const unsigned long hkl, const wchar_t* layoutString) { return _nvdaControllerInternal_inputLangChangeNotify(threadID,hkl,layoutString); @@ -79,3 +74,8 @@ error_status_t(__stdcall *_nvdaControllerInternal_drawFocusRectNotify)(const lon error_status_t __stdcall nvdaControllerInternal_drawFocusRectNotify(const long hwnd, const long left, const long top, const long right, const long bottom) { return _nvdaControllerInternal_drawFocusRectNotify(hwnd,left,top,right,bottom); } + +error_status_t(__stdcall *_nvdaControllerInternal_reportLiveRegion)(const wchar_t*, const wchar_t*); +error_status_t __stdcall nvdaControllerInternal_reportLiveRegion(const wchar_t* text, const wchar_t* politeness) { + return _nvdaControllerInternal_reportLiveRegion(text, politeness); +} diff --git a/nvdaHelper/local/nvdaHelperLocal.def b/nvdaHelper/local/nvdaHelperLocal.def index c68a24dd865..8735a7e61e3 100644 --- a/nvdaHelper/local/nvdaHelperLocal.def +++ b/nvdaHelper/local/nvdaHelperLocal.def @@ -33,7 +33,6 @@ EXPORTS VBuf_locateTextFieldNodeAtOffset VBuf_setSelectionOffsets _nvdaControllerInternal_requestRegistration - _nvdaControllerInternal_reportLiveRegion _nvdaControllerInternal_displayModelTextChangeNotify _nvdaControllerInternal_inputLangChangeNotify _nvdaControllerInternal_inputCompositionUpdate @@ -61,3 +60,4 @@ EXPORTS audioDucking_shouldDelay logMessage getOleClipboardText + _nvdaControllerInternal_reportLiveRegion diff --git a/nvdaHelper/remote/nvdaHelperRemote.def b/nvdaHelper/remote/nvdaHelperRemote.def index 4ab99ab0be6..b544bab6ff1 100644 --- a/nvdaHelper/remote/nvdaHelperRemote.def +++ b/nvdaHelper/remote/nvdaHelperRemote.def @@ -12,7 +12,6 @@ EXPORTS logMessage NVDALogCrtReportHook nvdaInProcUtils_winword_expandToLine - nvdaControllerInternal_reportLiveRegion nvdaControllerInternal_logMessage nvdaControllerInternal_vbufChangeNotify nvdaControllerInternal_installAddonPackageFromPath @@ -20,3 +19,4 @@ EXPORTS nvdaController_speakText nvdaController_cancelSpeech nvdaController_brailleMessage + nvdaControllerInternal_reportLiveRegion diff --git a/nvdaHelper/vbufBackends/mshtml/node.cpp b/nvdaHelper/vbufBackends/mshtml/node.cpp index 86708595b57..34e8a6abf61 100755 --- a/nvdaHelper/vbufBackends/mshtml/node.cpp +++ b/nvdaHelper/vbufBackends/mshtml/node.cpp @@ -379,7 +379,7 @@ void MshtmlVBufStorage_controlFieldNode_t::preProcessLiveRegion(const MshtmlVBuf if (this->ariaLiveNode) { this->ariaLivePoliteness = this->ariaLiveNode->ariaLivePoliteness; } else { - this->ariaLivePoliteness = nullptr; + this->ariaLivePoliteness = L""; } } i=attribsMap.find(L"HTMLAttrib::aria-relevant"); @@ -411,11 +411,8 @@ void MshtmlVBufStorage_controlFieldNode_t::preProcessLiveRegion(const MshtmlVBuf } void MshtmlVBufStorage_controlFieldNode_t::reportLiveText(wstring& text, wstring& politeness) { - for(auto c: text) { - if(!iswspace(c)) { - nvdaControllerInternal_reportLiveRegion(text.c_str(), politeness.c_str()); - break; - } + if(!all_of(text.cbegin(), text.cend(), iswspace)) { + nvdaControllerInternal_reportLiveRegion(text.c_str(), politeness.c_str()); } } diff --git a/readme.md b/readme.md index 1c3f8a46926..6c67c6d311d 100644 --- a/readme.md +++ b/readme.md @@ -90,7 +90,7 @@ For reference, the following run time dependencies are included in Git submodule * [ConfigObj](https://github.com/DiffSK/configobj), commit f9a265c * [Six](https://pypi.python.org/pypi/six), version 1.12.0, required by wxPython and ConfigObj * [liblouis](http://www.liblouis.org/), version 3.15.0 -* [Unicode Common Locale Data Repository (CLDR)](http://cldr.unicode.org/) Emoji Annotations, version 37.0 +* [Unicode Common Locale Data Repository (CLDR)](http://cldr.unicode.org/) Emoji Annotations, version 38.0 * NVDA images and sounds * [Adobe Acrobat accessibility interface, version XI](https://download.macromedia.com/pub/developer/acrobat/AcrobatAccess.zip) * Adobe FlashAccessibility interface typelib diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index 136127a302b..ebe78723975 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -85,10 +85,11 @@ def normalizeIA2TextFormatField(formatField): except KeyError: invalid=None if invalid: - invalid=invalid.lower() - if invalid=="spelling": + # aria-invalid can contain multiple values separated by a comma. + invalidList = [x.lower().strip() for x in invalid.split(',')] + if "spelling" in invalidList: formatField["invalid-spelling"]=True - elif invalid=="grammar": + if "grammar" in invalidList: formatField["invalid-grammar"]=True color=formatField.get('color') if color: diff --git a/source/NVDAObjects/UIA/VisualStudio.py b/source/NVDAObjects/UIA/VisualStudio.py index 380c17cf27a..edd3ec03b8b 100644 --- a/source/NVDAObjects/UIA/VisualStudio.py +++ b/source/NVDAObjects/UIA/VisualStudio.py @@ -7,10 +7,11 @@ available in Visual Studio and SQL Server Management Studio. """ -from . import UIA +from . import UIA, ToolTip import speech import braille import api +import time class IntelliSenseItem(UIA): @@ -53,6 +54,34 @@ class IntelliSenseLiveRegion(UIA): } +class CompletionToolTip(ToolTip): + """ A tool tip for which duplicate open events can be fired. + """ + + #: Keeps track of the last ToolTipOpened event (text, time) + _lastToolTipOpenedInfo = (None, None) + #: The duplicate tooltip events will be dropped within this time window + _preventDuplicateToolTipSeconds = 0.2 + + def event_UIA_toolTipOpened(self): + oldText, oldTime = self._lastToolTipOpenedInfo + newText = self.name + newTime = time.time() + self.__class__._lastToolTipOpenedInfo = (newText, newTime) + withinPossibleDupToolTipTimeWindow = ( + oldTime is not None + and (newTime - oldTime) < self._preventDuplicateToolTipSeconds + ) + if newText == oldText and withinPossibleDupToolTipTimeWindow: + # Tool-tip event suspected to be a duplicate, drop the event. + # - Users attempting to rapidly re-announce tool-tips may + # have the announcement erroneously suppressed + # - Users on slower systems (or systems under load) may still + # receive duplicate announcements + return + super().event_UIA_toolTipOpened() + + def findExtraOverlayClasses(obj, clsList): if obj.UIAAutomationId in _INTELLISENSE_LIST_AUTOMATION_IDS: clsList.insert(0, IntelliSenseList) @@ -64,3 +93,5 @@ def findExtraOverlayClasses(obj, clsList): and isinstance(obj.previous.previous, IntelliSenseList) ): clsList.insert(0, IntelliSenseLiveRegion) + elif obj.UIAAutomationId == "completion tooltip": + clsList.insert(0, CompletionToolTip) diff --git a/source/NVDAObjects/UIA/wordDocument.py b/source/NVDAObjects/UIA/wordDocument.py index 60e153067ee..08bf414210e 100644 --- a/source/NVDAObjects/UIA/wordDocument.py +++ b/source/NVDAObjects/UIA/wordDocument.py @@ -272,9 +272,6 @@ def getTextWithFields(self,formatConfig=None): class WordBrowseModeDocument(UIABrowseModeDocument): - def _get_isAlive(self): - return True - def shouldSetFocusToObj(self,obj): # Ignore strange editable text fields surrounding most inner fields (links, table cells etc) if obj.role==controlTypes.ROLE_EDITABLETEXT and obj.UIAElement.cachedAutomationID.startswith('UIA_AutomationId_Word_Content'): diff --git a/source/NVDAObjects/window/winword.py b/source/NVDAObjects/window/winword.py index 5c1040b9e52..bcdcecaff6c 100755 --- a/source/NVDAObjects/window/winword.py +++ b/source/NVDAObjects/window/winword.py @@ -1,5 +1,3 @@ -# -*- coding: UTF-8 -*- -# NVDAObjects/window/winword.py # A part of NonVisual Desktop Access (NVDA) # Copyright (C) 2006-2020 NV Access Limited, Manish Agrawal, Derek Riemer, Babbage B.V. # This file is covered by the GNU General Public License. @@ -613,8 +611,10 @@ def _get_locationText(self): textList.append(_("{distance} from top edge of page").format(distance=distance)) return ", ".join(textList) - def copyToClipboard(self): + def copyToClipboard(self, notify): self._rangeObj.copy() + if notify: + ui.reportTextCopiedToClipboard(self.text) return True def find(self,text,caseSensitive=False,reverse=False): @@ -1372,24 +1372,6 @@ def script_increaseDecreaseFontSize(self,gesture): # Translators: a message when increasing or decreasing font size in Microsoft Word ui.message(_("{size:g} point font").format(size=val)) - def script_toggleChangeTracking(self, gesture): - if not self.WinwordDocumentObject: - # We cannot fetch the Word object model, so we therefore cannot report the status change. - # The object model may be unavailable because this is a pure UIA implementation such as Windows 10 Mail, - # or it's within Windows Defender Application Guard. - # In this case, just let the gesture through and don't report anything. - return gesture.send() - val = self._WaitForValueChangeForAction( - lambda: gesture.send(), - lambda: self.WinwordDocumentObject.TrackRevisions - ) - if val: - # Translators: a message when toggling change tracking in Microsoft word - ui.message(_("Change tracking on")) - else: - # Translators: a message when toggling change tracking in Microsoft word - ui.message(_("Change tracking off")) - @script(gesture="kb:control+shift+8") def script_toggleDisplayNonprintingCharacters(self, gesture): if not self.WinwordWindowObject: @@ -1514,7 +1496,6 @@ def initOverlayClass(self): "kb:control+1":"changeLineSpacing", "kb:control+2":"changeLineSpacing", "kb:control+5":"changeLineSpacing", - "kb:control+shift+e": "toggleChangeTracking", "kb:control+pageUp": "caret_moveByLine", "kb:control+pageDown": "caret_moveByLine", } diff --git a/source/api.py b/source/api.py index a68a521e930..2f819ded16d 100644 --- a/source/api.py +++ b/source/api.py @@ -138,7 +138,7 @@ def setFocusObject(obj): try: treeInterceptorObject=treeInterceptorHandler.update(o) except: - log.exception("Error updating tree interceptor") + log.error("Error updating tree interceptor", exc_info=True) #Always make sure that the focus object's treeInterceptor is forced to either the found treeInterceptor (if its in it) or to None #This is to make sure that the treeInterceptor does not have to be looked up, which can cause problems for winInputHook if obj is o or obj in treeInterceptorObject: diff --git a/source/appModules/calculator.py b/source/appModules/calculator.py new file mode 100644 index 00000000000..9d00eb81a34 --- /dev/null +++ b/source/appModules/calculator.py @@ -0,0 +1,108 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2020 NV Access Limited, Joseph Lee +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. + +"""App module for Windows 10 Calculator""" + +import appModuleHandler +import api +from NVDAObjects.UIA import UIA +import queueHandler +import ui +import scriptHandler + +# #9428: do not announce current values until calculations are done in order to avoid repetitions. +noCalculatorEntryAnnouncements = [ + # Display field with Calculator set to full screen mode. + "CalculatorResults", + # In the middle of a calculation expression entry. + "CalculatorExpression", + # Results display with Calculator set to compact overlay i.e. always on top mode. + "CalculatorAlwaysOnTopResults", + # Calculator expressions with Calculator set to always on top mode. + "ExpressionContainer", + # Date range selector. + "ContentPresenter", + # Briefly shown when closing date calculation calendar. + "Light Dismiss", + # Unit conversion/convert from. + "Value1", + # Unit conversion/converts into. + "Value2", +] + + +class AppModule(appModuleHandler.AppModule): + + _shouldAnnounceResult = False + # Name change says the same thing multiple times for some items. + _resultsCache = "" + + def event_nameChange(self, obj, nextHandler): + if not isinstance(obj, UIA): + return + # No, announce value changes immediately except for calculator results and expressions. + if ( + obj.UIAAutomationId in noCalculatorEntryAnnouncements + or obj.UIAElement.cachedClassName == "LandmarkTarget" + ): + self._shouldAnnounceResult = False + # For the rest: + elif ( + obj.UIAAutomationId not in noCalculatorEntryAnnouncements + and obj.name != self._resultsCache + ): + # For unit conversion, both name change and notification events are fired, + # although UIA notification event presents much better messages. + # For date calculation, live region change event is also fired for difference between dates. + if obj.UIAAutomationId != "DateDiffAllUnitsResultLabel": + ui.message(obj.name) + self._resultsCache = obj.name + if not self._shouldAnnounceResult: + return + self._shouldAnnounceResult = False + nextHandler() + + def event_UIA_notification(self, obj, nextHandler, activityId=None, **kwargs): + try: + shouldAnnounceNotification = ( + obj.previous.UIAAutomationId in + ("numberPad", "UnitConverterRootGrid") + ) + except AttributeError: + resultElement = api.getForegroundObject().children[1].lastChild + # Redesigned in 2019 due to introduction of "always on top" i.e. compact overlay mode. + if resultElement.UIAElement.cachedClassName != "LandmarkTarget": + resultElement = resultElement.parent.children[1] + shouldAnnounceNotification = ( + resultElement + and resultElement.firstChild + and resultElement.firstChild.UIAAutomationId not in noCalculatorEntryAnnouncements + ) + # Display updated activity ID seen when entering calculations should be ignored + # as as it is redundant if speak typed characters is on. + if shouldAnnounceNotification or activityId != "DisplayUpdated": + nextHandler() + + # A list of native commands to handle calculator result announcement. + _calculatorResultGestures = ("kb:enter", "kb:numpadEnter", "kb:escape") + + @scriptHandler.script(gestures=_calculatorResultGestures) + def script_calculatorResult(self, gesture): + # To prevent double focus announcement, check where we are. + focus = api.getFocusObject() + gesture.send() + # In redstone, calculator result keeps firing name change, + # so tell it to do so if and only if enter has been pressed. + self._shouldAnnounceResult = True + # Hack: only announce display text when an actual calculator button (usually equals button) is pressed. + # In redstone, pressing enter does not move focus to equals button. + if isinstance(focus, UIA): + if focus.UIAAutomationId in ("CalculatorResults", "CalculatorAlwaysOnTopResults"): + queueHandler.queueFunction(queueHandler.eventQueue, focus.reportFocus) + else: + resultsScreen = api.getForegroundObject().children[1].lastChild + if isinstance(resultsScreen, UIA) and resultsScreen.UIAElement.cachedClassName == "LandmarkTarget": + # And no, do not allow focus to move. + queueHandler.queueFunction(queueHandler.eventQueue, resultsScreen.firstChild.reportFocus) diff --git a/source/appModules/mmc.py b/source/appModules/mmc.py index e25a24b38c6..e543dcb38c5 100644 --- a/source/appModules/mmc.py +++ b/source/appModules/mmc.py @@ -1,14 +1,14 @@ -#appModules/mmc.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2015-2019 NV Access Limited, David Parduhn, Bill Dengler, Leonard de Ruijter -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2015-2020 NV Access Limited, David Parduhn, Bill Dengler, Leonard de Ruijter, Ćukasz Golonka +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. import api import appModuleHandler import controlTypes import eventHandler from NVDAObjects.IAccessible import IAccessible +from NVDAObjects.behaviors import ToolTip class MMCTable(IAccessible): def _get_focusRedirect(self): @@ -20,10 +20,36 @@ def _get_focusRedirect(self): class MMCTableCell(IAccessible): + """ Cell and rowheader makes no sense for these controls. Mapping them to list items has added benefit + of suppressing selected. """ + role = controlTypes.ROLE_LISTITEM + def event_selection(self): if self.parent.hasFocus and api.getFocusObject() != self: eventHandler.executeEvent("gainFocus", self) + def _get_positionInfo(self): + """ When 'Guess object position info when unavailable' is enabled + these controls report very strange information such as 65537 of 12, especially in braille. + Disable reporting of position info all together. """ + return None + + +class toolTipWithEmptyName(ToolTip): + previousToolTipText = '' + + def _get_name(self): + """ ToolTips appearing when hovering mouse over graphical view have empty name, + but it can be retrieved via display model. """ + return self.displayText + + def event_show(self): + # Stop repeating the same tooltip over and over again. + toolTipText = self.displayText + if toolTipText != toolTipWithEmptyName.previousToolTipText: + toolTipWithEmptyName.previousToolTipText = toolTipText + super().event_show() + class AppModule(appModuleHandler.AppModule): def chooseNVDAObjectOverlayClasses(self, obj, clsList): @@ -33,3 +59,5 @@ def chooseNVDAObjectOverlayClasses(self, obj, clsList): elif obj.role in (controlTypes.ROLE_TABLECELL, controlTypes.ROLE_TABLEROWHEADER): clsList.insert(0, MMCTableCell) + if obj.windowClassName == "tooltips_class32" and obj.name is None: + clsList.insert(0, toolTipWithEmptyName) diff --git a/source/appModules/winword.py b/source/appModules/winword.py new file mode 100644 index 00000000000..088d35c48a4 --- /dev/null +++ b/source/appModules/winword.py @@ -0,0 +1,42 @@ +# 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 + +""" 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 +Microsoft Word only. +""" + +import appModuleHandler +from scriptHandler import script +import ui +from NVDAObjects.IAccessible.winword import WordDocument + + +class AppModule(appModuleHandler.AppModule): + + def chooseNVDAObjectOverlayClasses(self, obj, clsList): + if WordDocument in clsList: + clsList.insert(0, WinwordWordDocument) + + +class WinwordWordDocument(WordDocument): + + @script(gesture="kb:control+shift+e") + def script_toggleChangeTracking(self, gesture): + if not self.WinwordDocumentObject: + # We cannot fetch the Word object model, so we therefore cannot report the status change. + # The object model may be unavailable because it's within Windows Defender Application Guard. + # In this case, just let the gesture through and don't report anything. + return gesture.send() + val = self._WaitForValueChangeForAction( + lambda: gesture.send(), + lambda: self.WinwordDocumentObject.TrackRevisions + ) + if val: + # Translators: a message when toggling change tracking in Microsoft word + ui.message(_("Change tracking on")) + else: + # Translators: a message when toggling change tracking in Microsoft word + ui.message(_("Change tracking off")) diff --git a/source/brailleViewer/brailleViewerGui.py b/source/brailleViewer/brailleViewerGui.py index fce29f9e14e..b781a29f0ce 100644 --- a/source/brailleViewer/brailleViewerGui.py +++ b/source/brailleViewer/brailleViewerGui.py @@ -261,6 +261,8 @@ class BrailleViewerFrame( _brailleOutputLastSet: str _brailleOutput: wx.TextCtrl _shouldShowOnStartupCheckBox: wx.CheckBox + #: True if _mouseOver has been bound to mouse moved events. + _mouseMotionBound: bool = False def __init__(self, numCells, onDestroyed): log.debug(f"Starting braille viewer with {numCells} cells") @@ -402,11 +404,13 @@ def _onShouldHoverRouteToCellCheckBoxChanged(self, evt: wx.CommandEvent): self._updateMouseOverBinding(evt.IsChecked()) def _updateMouseOverBinding(self, shouldReceiveMouseMotion: bool): - if not shouldReceiveMouseMotion: + if not shouldReceiveMouseMotion and self._mouseMotionBound: self._brailleOutput.Unbind(wx.EVT_MOTION, source=None, handler=self._mouseOver) + self._mouseMotionBound = False self._hoverTracker.cancelPendingHover() else: self._brailleOutput.Bind(wx.EVT_MOTION, handler=self._mouseOver) + self._mouseMotionBound = True def _mouseOver(self, unused: wx.MouseEvent): if _shouldDoHover(): diff --git a/source/gui/guiHelper.py b/source/gui/guiHelper.py index e0ccd3bcbc9..b33edad2875 100644 --- a/source/gui/guiHelper.py +++ b/source/gui/guiHelper.py @@ -45,7 +45,7 @@ def __init__(self,parent): from contextlib import contextmanager import wx -from wx.lib import scrolledpanel +from wx.lib import scrolledpanel, newevent from abc import ABCMeta #: border space to be used around all controls in dialogs @@ -159,17 +159,49 @@ class LabeledControlHelper(object): the two controls together. Relies on guiHelper.associateElements(), any limitations in guiHelper.associateElements() also apply here. """ - def __init__(self, parent, labelText, wxCtrlClass, **kwargs): + + # When the control is enabled / disabled this event is raised. + # A handler is automatically added to the control to ensure the label is also enabled/disabled. + EnableChanged, EVT_ENABLE_CHANGED = newevent.NewEvent() + + def __init__(self, parent: wx.Window, labelText: str, wxCtrlClass: wx.Control, **kwargs): """ @param parent: An instance of the parent wx window. EG wx.Dialog @param labelText: The text to associate with a wx control. - @type labelText: string @param wxCtrlClass: The class to associate with the label, eg: wx.TextCtrl - @type wxCtrlClass: class @param kwargs: The keyword arguments used to instantiate the wxCtrlClass """ object.__init__(self) - self._label = wx.StaticText(parent, label=labelText) - self._ctrl = wxCtrlClass(parent, **kwargs) + + class LabelEnableChangedListener(wx.StaticText): + isDestroyed = False + isListening = False + + def _onDestroy(self, evt: wx.WindowDestroyEvent): + self.isDestroyed = True + + def listenForEnableChanged(self, _ctrl: wx.Window): + self.Bind(wx.EVT_WINDOW_DESTROY, self._onDestroy) + _ctrl.Bind(LabeledControlHelper.EVT_ENABLE_CHANGED, self._onEnableChanged) + self.isListening = True + + def _onEnableChanged(self, evt: wx.Event): + if self.isListening and not self.isDestroyed: + self.Enable(evt.isEnabled) + + class WxCtrlWithEnableEvnt(wxCtrlClass): + def Enable(self, enable=True): + evt = LabeledControlHelper.EnableChanged(isEnabled=enable) + wx.PostEvent(self, evt) + super().Enable(enable) + + def Disable(self): + evt = LabeledControlHelper.EnableChanged(isEnabled=False) + wx.PostEvent(self, evt) + super().Disable() + + self._label = LabelEnableChangedListener(parent, label=labelText) + self._ctrl = WxCtrlWithEnableEvnt(parent, **kwargs) + self._label.listenForEnableChanged(self._ctrl) self._sizer = associateElements(self._label, self._ctrl) @property diff --git a/source/speech/manager.py b/source/speech/manager.py index 42fd3622137..b74da7d081f 100644 --- a/source/speech/manager.py +++ b/source/speech/manager.py @@ -333,7 +333,7 @@ def ensureEndUtterance(seq: SpeechSequence): else: lastOutSeq = outSeqs[-1] if outSeqs else None lastCommand = lastOutSeq[-1] if lastOutSeq else None - if not lastCommand or isinstance(lastCommand, (EndUtteranceCommand, ConfigProfileTriggerCommand)): + if lastCommand is None or isinstance(lastCommand, (EndUtteranceCommand, ConfigProfileTriggerCommand)): # It doesn't make sense to start with or repeat EndUtteranceCommands. # We also don't want an EndUtteranceCommand immediately after a ConfigProfileTriggerCommand. return @@ -458,15 +458,29 @@ def _buildNextUtterance(self): # apply any parameters changed before the preemption. params = self._curPriQueue.paramTracker.getChanged() utterance.extend(params) - for seq in self._curPriQueue.pendingSequences: + lastSequenceIndexAddedToUtterance = None + for seqIndex, seq in enumerate(self._curPriQueue.pendingSequences): if isinstance(seq[0], EndUtteranceCommand): # The utterance ends here. break utterance.extend(seq) + lastSequenceIndexAddedToUtterance = seqIndex # if any items are cancelled, cancel the whole utterance. - if utterance and not self._checkForCancellations(utterance): + try: + utteranceValid = len(utterance) == 0 or self._checkForCancellations(utterance) + except IndexError: + log.error( + f"Checking for cancellations failed, cancelling sequence: {utterance}", + exc_info=True + ) + # Avoid infinite recursion by removing the problematic sequences: + del self._curPriQueue.pendingSequences[:lastSequenceIndexAddedToUtterance + 1] + utteranceValid = False + + if utteranceValid: + return utterance + else: return self._buildNextUtterance() - return utterance def _checkForCancellations(self, utterance: SpeechSequence) -> bool: """ @@ -480,8 +494,9 @@ def _checkForCancellations(self, utterance: SpeechSequence) -> bool: return True utteranceIndex = self._getUtteranceIndex(utterance) if utteranceIndex is None: - log.error("no utterance index, cant save cancellable commands") - return False + raise IndexError( + f"no utterance index({utteranceIndex}, cant save cancellable commands" + ) cancellableItems = list( item for item in reversed(utterance) if isinstance(item, _CancellableSpeechCommand) ) @@ -542,7 +557,7 @@ def _getMostRecentlyCancelledUtterance(self) -> Optional[_IndexT]: ) for index in cancelledIndexes: if ( - not latestCancelledUtteranceIndex + latestCancelledUtteranceIndex is None or self._isIndexABeforeIndexB(latestCancelledUtteranceIndex, index) ): latestCancelledUtteranceIndex = index diff --git a/source/ui.py b/source/ui.py index 6bcab338289..74720c9bbeb 100644 --- a/source/ui.py +++ b/source/ui.py @@ -106,10 +106,16 @@ def reportTextCopiedToClipboard(text: Optional[str] = None): # or the clipboard content did not match what was just copied. message(_("Unable to copy")) return + # Depending on the speech synthesizer, large amount of spoken text can freeze NVDA (#11843) + if len(text) < 1024: + spokenText = text + else: + # Translators: Spoken instead of a lengthy text when copied to clipboard. + spokenText = _("%d characters") % len(text) message( # Translators: Announced when a text has been copied to clipboard. # {text} is replaced by the copied text. - text=_("Copied to clipboard: {text}").format(text=text), + text=_("Copied to clipboard: {text}").format(text=spokenText), # Translators: Displayed in braille when a text has been copied to clipboard. # {text} is replaced by the copied text. brailleText=_("Copied: {text}").format(text=text) diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index cd9c82136c0..6e20d72d977 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -225,7 +225,7 @@ def getId(cls) -> str: @classmethod def getDisplayName(cls) -> str: # Translators: Description for NVDA's built-in screen highlighter. - return _("Focus Highlight") + return _("Visual Highlight") def _get_supportedSettings(self) -> SupportedSettingType: return [ diff --git a/tests/lint/flake8.ini b/tests/lint/flake8.ini index db2937e1025..5a404949682 100644 --- a/tests/lint/flake8.ini +++ b/tests/lint/flake8.ini @@ -48,6 +48,4 @@ exclude = # don't bother looking in the following subdirectories / files. filename = *.py, - *.pyw, - *sconscript, - sconstruct + *.pyw diff --git a/tests/system/robot/chromeTests.py b/tests/system/robot/chromeTests.py index 307de0dca4e..d9cd0aa6b74 100644 --- a/tests/system/robot/chromeTests.py +++ b/tests/system/robot/chromeTests.py @@ -238,7 +238,7 @@ def test_ariaTreeGrid_browseMode(): testFile = os.path.join(ARIAExamplesDir, "treegrid", "treegrid-1.html") _chrome.prepareChrome( f""" - + """ ) # Jump to the first heading in the iframe. @@ -288,3 +288,60 @@ def test_ariaTreeGrid_browseMode(): "level 1 Treegrids are awesome Want to learn how to use them? aaron at thegoogle dot rocks expanded", ]) ) + + +def ARIAInvalid_spellingAndGrammar(): + """ + Tests ARIA invalid values of "spelling", "grammar" and "spelling, grammar". + Please note that although IAccessible2 allows multiple values for invalid, + multiple values to aria-invalid is not yet standard. + And even if it were, they would be separated by space, not comma +thus the html for this test would need to change, + but the expected output shouldn't need to. + """ + _chrome.prepareChrome( + r""" +
Big caat meos
+Small a dog woofs
+Fat a ffrog crokes
+ """ + ) + actualSpeech = _chrome.getSpeechAfterKey("downArrow") + _asserts.strings_match( + actualSpeech, + "Big spelling error caat meos" + ) + actualSpeech = _chrome.getSpeechAfterKey("downArrow") + _asserts.strings_match( + actualSpeech, + "Small grammar error a dog woofs" + ) + actualSpeech = _chrome.getSpeechAfterKey("downArrow") + _asserts.strings_match( + actualSpeech, + "Fat spelling error grammar error a ffrog crokes" + ) + + +def test_ariaCheckbox_browseMode(): + """ + Navigate to an unchecked checkbox in reading mode. + """ + testFile = os.path.join(ARIAExamplesDir, "checkbox", "checkbox-1", "checkbox-1.html") + _chrome.prepareChrome( + f""" + + """ + ) + # Jump to the first heading in the iframe. + actualSpeech = _chrome.getSpeechAfterKey("h") + _asserts.strings_match( + actualSpeech, + "frame main landmark Checkbox Example (Two State) heading level 1" + ) + # Navigate to the checkbox. + actualSpeech = _chrome.getSpeechAfterKey("x") + _asserts.strings_match( + actualSpeech, + "Sandwich Condiments grouping list with 4 items Lettuce check box not checked" + ) diff --git a/tests/system/robot/chromeTests.robot b/tests/system/robot/chromeTests.robot index 2cdcc276ec4..8063420da2a 100644 --- a/tests/system/robot/chromeTests.robot +++ b/tests/system/robot/chromeTests.robot @@ -40,3 +40,10 @@ ARIA treegrid [Documentation] Ensure that ARIA treegrids are accessible as a standard table in browse mode. # Excluded due to regular failures. test_ariaTreeGrid_browseMode +ARIA invalid spelling and grammar + [Documentation] Tests ARIA invalid values of "spelling", "grammar" and "spelling, grammar". + ARIAInvalid_spellingAndGrammar +ARIA checkbox + [Documentation] Navigate to an unchecked checkbox in reading mode. + [Tags] aria-at + test_ariaCheckbox_browseMode diff --git a/tests/unit/test_speechManager/__init__.py b/tests/unit/test_speechManager/__init__.py index e0563cfc237..21c3b968d75 100644 --- a/tests/unit/test_speechManager/__init__.py +++ b/tests/unit/test_speechManager/__init__.py @@ -1139,12 +1139,15 @@ def setUp(self): config.conf['featureFlag']['cancelExpiredFocusSpeech'] = 1 # yes -class Test_pr11651(unittest.TestCase): +class RegressionTests(unittest.TestCase): + """Tests to prevent regressions after issues are fixed. + """ def test_redundantSequenceAfterEndUtterance(self): """ Tests that redundant param change and index commands are not emitted as an extra utterance when the preceeding utterance contained param change commands and an EndUtterance command. + See PR #11651 E.g. speaking a character. """ smi = SpeechManagerInteractions(self) @@ -1168,3 +1171,28 @@ def test_redundantSequenceAfterEndUtterance(self): smi.indexReached(1) smi.doneSpeaking() smi.pumpAll() + + def test_nonSpokenCharacter(self): + """Test for fix to GH#11752 - NVDA Freeze with unicode value U+000B + Actually, the speech manager receives an empty string for the character U+000B. NVDA + does not have a mapping for this character. + It is questionable whether we should send anything to the synth when there is no content, however + what constitutes 'content' is currently not easy to define. + """ + smi = SpeechManagerInteractions(self) + + speechSequence = [ + CharacterModeCommand(True), + '', + smi.create_EndUtteranceCommand(expectedToBecomeIndex=1) + ] + with smi.expectation(): + seqIndexes = smi.speak(speechSequence) + smi.expect_synthSpeak(seqIndexes) + + +class RegressionTests_withCancellableSpeechEnabled(RegressionTests): + """Note, while cancellable speech is configurable test with and without it enabled.""" + def setUp(self): + super().setUp() + config.conf['featureFlag']['cancelExpiredFocusSpeech'] = 1 # yes diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 4b864c7ba67..cab268536ad 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -26,6 +26,10 @@ What's New in NVDA - In web browsers and other applications that support browse mode, the Elements List dialog (NVDA+F7) can now be invoked when in focus mode. (#10453) - Updates to ARIA live regions are now suppressed when reporting of dynamic content changes is disabled. (#9077) - NVDA will now report "Copied to clipboard" before the copied text. (#6757) +- Presentation of graphical view table in disk management has been improved. (#10048) +- Labels for controls are now disabled (greyed out) when the control is disabled. (#11809) +- Updated CLDR emoji annotation to version 38. (#11817) +- The inbuilt "Focus Highlight" feature has been renamed "Vision Highlight". (#11700) == Bug Fixes == @@ -39,7 +43,7 @@ What's New in NVDA - When "attempt to cancel expired focus events" is enabled, the title of the tab is now announced again when switching tabs in Firefox. (#11397) - NVDA no longer fails to announce a list item after typing a character in a list when speaking with the SAPI5 Ivona voices. (#11651) - It is again possible to use browse mode when reading emails in Windows 10 Mail 16005.13110 and later. (#11439) -- When using the SAPI5 Ivona voices from harposoftware.com, NvDA is now able to save configuration, switch synthesizers, and no longer will stay silent after restarting. (#11650) +- When using the SAPI5 Ivona voices from harposoftware.com, NVDA is now able to save configuration, switch synthesizers, and no longer will stay silent after restarting. (#11650) - It is now possible to enter number 6 in computer braille from a braille keyboard on HIMS displays. (#11710) - Major performance improvements in Azure Data Studio. (#11533, #11715) - With "Attempt to Cancel speech for expired focus events" enabled the title of the NVDA Find dialog is announced again. (#11632) @@ -52,12 +56,17 @@ What's New in NVDA - Strong and emphasis tags in Internet Explorer can again be suppressed from being reported by returning off Report Emphasis in NVDA's Document Formatting settings. (#11808) - A freeze of several seconds experienced by a small amount of users when arrowing between cells in Excel should no longer occur. (#11818) - In Microsoft Teams builds with version numbers like 1.3.00.28xxx, NVDA no longer fails reading messages in chats or Teams channels due to an incorrectly focused menu. (#11821) +- Text marked both as being a spelling and grammar error at the same time in Google Chrome will be appropriately announced as both a spelling and grammar error by NVDA. (#11787) +- When using Outlook (French locale), the shortcut for 'Reply all' (control+shift+R) works again. (#11196) +- In Visual Studio, IntelliSense tool tips that provide additional details about the currently selected IntelliSense item are now only reported once. (#11611) +- In Windows 10 Calculator, NVDA will not announce progress of calculations if speak typed characters is disabled. (#9428) == Changes for Developers == - System tests can now send keys using spy.emulateKeyPress, which takes a key identifier that conforms to NVDA's own key names, and by default also blocks until the action is executed. (#11581) - NVDA no longer requires the current directory to be the NVDA application directory in order to function. (#6491) - The aria live politeness setting for live regions can now be found on NVDA Objects using the liveRegionPoliteness property. (#11596) +- It is now possible to define separate gestures for Outlook and Word document. (#11196) = 2020.3 = diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 83ad57696d3..17e845334cb 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -307,7 +307,7 @@ For example, if you are typing into an editable text field, the editable text fi The most common way of navigating around Windows with NVDA is to simply move the system focus using standard Windows keyboard commands, such as pressing tab and shift+tab to move forward and back between controls, pressing alt to get to the menu bar and then using the arrows to navigate menus, and using alt+tab to move between running applications. As you do this, NVDA will report information about the object with focus, such as its name, type, value, state, description, keyboard shortcut and positional information. -When [Focus Highlight #VisionFocusHighlight] is enabled, the location of the current system focus is also exposed visually. +When [Visual Highlight #VisionFocusHighlight] is enabled, the location of the current system focus is also exposed visually. There are some key commands that are useful when moving with the System focus: %kc:beginInclude @@ -363,7 +363,7 @@ Similarly, a toolbar contains controls, so you must move inside the toolbar to a The object currently being reviewed is called the navigator object. Once you navigate to an object, you can review its content using the [text review commands #ReviewingText] while in [Object review mode #ObjectReview]. -When [Focus Highlight #VisionFocusHighlight] is enabled, the location of the current navigator object is also exposed visually. +When [Visual Highlight #VisionFocusHighlight] is enabled, the location of the current navigator object is also exposed visually. By default, the navigator object moves along with the System focus, though this behaviour can be toggled on and off. Note: Braille following Object Navigation can be configured via [Braille Tether #BrailleTether]. @@ -506,7 +506,7 @@ Browse mode is also optionally available for Microsoft Word documents. In browse mode, the content of the document is made available in a flat representation that can be navigated with the cursor keys as if it were a normal text document. All of NVDA's [system caret #SystemCaret] key commands will work in this mode; e.g. say all, report formatting, table navigation commands, etc. -When [Focus Highlight #VisionFocusHighlight] is enabled, the location of the virtual browse mode caret is also exposed visually. +When [Visual Highlight #VisionFocusHighlight] is enabled, the location of the virtual browse mode caret is also exposed visually. Information such as whether text is a link, heading, etc. is reported along with the text as you move. Sometimes, you will need to interact directly with controls in these documents. @@ -776,8 +776,8 @@ Additional vision enhancement providers can be provided in [NVDA add-ons #Addons NVDA's vision settings can be changed in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog. -++ Focus Highlight ++[VisionFocusHighlight] -Focus Highlight can help to identify the [system focus #SystemFocus], [navigator object #ObjectNavigation] and [browse mode #BrowseMode] positions. +++ Visual Highlight ++[VisionFocusHighlight] +Visual Highlight can help to identify the [system focus #SystemFocus], [navigator object #ObjectNavigation] and [browse mode #BrowseMode] positions. These positions are highlighted with a coloured rectangle outline. - Solid blue highlights a combined navigator object and system focus location (e.g. because [the navigator object follows the system focus #ReviewCursorFollowFocus]). - Dashed blue highlights just the system focus object. @@ -785,7 +785,7 @@ These positions are highlighted with a coloured rectangle outline. - Solid yellow highlights the virtual caret used in browse mode (where there is no physical caret such as in web browsers). - -When Focus Highlight is enabled in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog, you can [change whether or not to highlight the focus, navigator object or browse mode caret #VisionSettingsFocusHighlight]. +When Visual Highlight is enabled in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog, you can [change whether or not to highlight the focus, navigator object or browse mode caret #VisionSettingsFocusHighlight]. ++ Screen Curtain ++[VisionScreenCurtain] As a blind or vision impaired user, it is often not possible or necessary to see the contents of the screen. @@ -1400,10 +1400,10 @@ The Vision category in the NVDA Settings dialog allows you to enable, disable an Note that the available options in this category could be extended by [NVDA add-ons #AddonsManager]. By default, this settings category contains the following options: -==== Focus Highlight ====[VisionSettingsFocusHighlight] -The check boxes in the Focus Highlight grouping control the behaviour of NVDA's built-in [Focus Highlight #VisionFocusHighlight] facility. +==== Visual Highlight ====[VisionSettingsFocusHighlight] +The check boxes in the Visual Highlight grouping control the behaviour of NVDA's built-in [Visual Highlight #VisionFocusHighlight] facility. -- Enable Highlighting: Toggles Focus Highlight on and off. +- Enable Highlighting: Toggles Visual Highlight on and off. - Highlight system focus: toggles whether the [system focus #SystemFocus] will be highlighted. - Highlight navigator object: toggles whether the [navigator object #ObjectNavigation] will be highlighted. - Highlight browse mode cursor: Toggles whether the [virtual browse mode cursor #BrowseMode] will be highlighted.