diff --git a/source/appModules/poedit.py b/source/appModules/poedit.py index 5c72bf778c3..fb0579a9fc1 100644 --- a/source/appModules/poedit.py +++ b/source/appModules/poedit.py @@ -1,130 +1,298 @@ -#appModules/poedit.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2012-2013 Mesar Hameed, NV Access Limited -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# 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) 2012-2023 Mesar Hameed, NV Access Limited, Leonard de Ruijter, Rui Fontes -"""App module for Poedit. +"""App module for Poedit 3.4+. """ +from enum import IntEnum + import api import appModuleHandler import controlTypes -import displayModel -import textInfos +import NVDAObjects.IAccessible import tones import ui -from NVDAObjects.IAccessible import sysListView32 import windowUtils -import NVDAObjects.IAccessible import winUser +from NVDAObjects import NVDAObject +from NVDAObjects.window import Window +from scriptHandler import getLastScriptRepeatCount, script + +LEFT_TO_RIGHT_EMBEDDING = "\u202a" +"""Character often found in translator comments.""" + + +class _WindowControlIdOffset(IntEnum): + """Window control ID's are not static, however, the order of ids stays the same. + Therefore, using a wxDataView control in the translations list as a reference, + we can safely calculate control ids accross releases or instances. + This class contains window control id offsets relative to the wxDataView window. + """ + + PRO_IDENTIFIER = -10 # This is a button in the free version + OLD_SOURCE_TEXT_PRO = 60 + OLD_SOURCE_TEXT = 65 + TRANSLATOR_NOTES_PRO = 63 + TRANSLATOR_NOTES = 68 # 63 in Pro + COMMENT_PRO = 66 + COMMENT = 71 + TRANSLATION_WARNING = 17 + NEEDS_WORK_SWITCH = 21 -def fetchObject(obj, path): - """Fetch the child object described by path. - @returns: requested object if found, or None - @rtype: L{NVDAObjects.NVDAObject} +def _findDescendantObject( + parentWindowHandle: int, + controlId: int | None = None, + className: str | None = None, +) -> Window | None: """ - path.reverse() - p = obj - while len(path) and p.firstChild: - p = p.firstChild - steps = path.pop() - i=0 - while i int | None: + fg = api.getForegroundObject() + dataView = _findDescendantObject(fg.windowHandle, className="wxDataView") + if not dataView: + return None + return dataView.windowControlID + + _isPro: bool + """Type definition for auto prop '_get__isPro'""" + + def _get__isPro(self) -> bool: + """Returns whether this instance of Poedit is a pro version.""" + obj = self._getNVDAObjectForWindowControlIdOffset(_WindowControlIdOffset.PRO_IDENTIFIER) + return obj is None + + def _correctWindowControllIdOfset( + self, + windowControlIdOffset: _WindowControlIdOffset + ) -> _WindowControlIdOffset: + """Corrects a _WindowControlIdOffset when a pro version of Poedit is active.""" + if self._isPro: + match windowControlIdOffset: + case _WindowControlIdOffset.OLD_SOURCE_TEXT: + return _WindowControlIdOffset.OLD_SOURCE_TEXT_PRO + case _WindowControlIdOffset.TRANSLATOR_NOTES: + return _WindowControlIdOffset.TRANSLATOR_NOTES_PRO + case _WindowControlIdOffset.COMMENT: + return _WindowControlIdOffset.COMMENT_PRO + return windowControlIdOffset + + def _getNVDAObjectForWindowControlIdOffset( + self, + windowControlIdOffset: _WindowControlIdOffset + ) -> Window | None: + fg = api.getForegroundObject() + return _findDescendantObject(fg.windowHandle, self._dataViewControlId + windowControlIdOffset) + + _translatorNotesObj: Window | None + """Type definition for auto prop '_get__translatorNotesObj'""" + + def _get__translatorNotesObj(self) -> Window | None: + return self._getNVDAObjectForWindowControlIdOffset( + self._correctWindowControllIdOfset(_WindowControlIdOffset.TRANSLATOR_NOTES) + ) + + def _reportControlScriptHelper(self, obj: Window, description: str): if obj: - try: - ui.message(obj.name + " " + obj.value) - except: - # Translators: this message is reported when there are no - # notes for translators to be presented to the user in Poedit. - ui.message(_("No notes for translators.")) + if not obj.hasIrrelevantLocation and not obj.parent.parent.hasIrrelevantLocation: + message = obj.name.replace(LEFT_TO_RIGHT_EMBEDDING, "") + repeats = getLastScriptRepeatCount() + if repeats == 0: + ui.message(message) + else: + ui.browseableMessage(message, description.title()) + else: + ui.message( + # Translators: this message is reported when there is nothing + # to be presented to the user in Poedit. + # {description} is replaced by the description of the window to be reported, + # e.g. translator notes + pgettext("poedit", "No {description}").format(description=description) + ) else: - # Translators: this message is reported when NVDA is unable to find - # the 'Notes for translators' window in poedit. - ui.message(_("Could not find Notes for translators window.")) - # Translators: The description of an NVDA command for Poedit. - script_reportAutoCommentsWindow.__doc__ = _("Reports any notes for translators") + ui.message( + # Translators: this message is reported when NVDA is unable to find + # a requested window in Poedit. + # {description} is replaced by the description of the window to be reported, e.g. translator notes + pgettext("poedit", "Could not find {description} window.").format(description=description) + ) - def script_reportCommentsWindow(self,gesture): - try: - obj = NVDAObjects.IAccessible.getNVDAObjectFromEvent( - windowUtils.findDescendantWindow(api.getForegroundObject().windowHandle, visible=True, controlID=104), - winUser.OBJID_CLIENT, 0) - except LookupError: - # Translators: this message is reported when NVDA is unable to find - # the 'comments' window in poedit. - ui.message(_("Could not find comment window.")) - return None - try: - ui.message(obj.name + " " + obj.value) - except: - # Translators: this message is reported when there are no - # comments to be presented to the user in the translator - # comments window in poedit. - ui.message(_("No comment.")) - # Translators: The description of an NVDA command for Poedit. - script_reportCommentsWindow.__doc__ = _("Reports any comments in the comments window") - - __gestures = { - "kb:control+shift+c": "reportCommentsWindow", - "kb:control+shift+a": "reportAutoCommentsWindow", - } + @script( + description=pgettext( + "poedit", + # Translators: The description of an NVDA command for Poedit. + "Reports any notes for translators. If pressed twice, presents the notes in browse mode", + ), + gesture="kb:control+shift+a", + ) + def script_reportAutoCommentsWindow(self, gesture): + self._reportControlScriptHelper( + self._translatorNotesObj, + # Translators: The description of the "Translator notes" window in poedit. + # This text is reported when the given window contains no item to report or could not be found. + pgettext("poedit", "notes for translators"), + ) + + _commentObj: Window | None + """Type definition for auto prop '_get__commentObj'""" + + def _get__commentObj(self) -> Window | None: + return self._getNVDAObjectForWindowControlIdOffset( + self._correctWindowControllIdOfset(_WindowControlIdOffset.COMMENT) + ) + + @script( + description=pgettext( + "poedit", + # Translators: The description of an NVDA command for Poedit. + "Reports any comment in the comments window. " + "If pressed twice, presents the comment in browse mode", + ), + gesture="kb:control+shift+c", + ) + def script_reportCommentsWindow(self, gesture): + self._reportControlScriptHelper( + self._commentObj, + # Translators: The description of the "comment" window in poedit. + # This text is reported when the given window contains no item to report or could not be found. + pgettext("poedit", "comment"), + ) + + _oldSourceTextObj: Window | None + """Type definition for auto prop '_get__oldSourceTextObj'""" + + def _get__oldSourceTextObj(self) -> Window | None: + return self._getNVDAObjectForWindowControlIdOffset( + self._correctWindowControllIdOfset(_WindowControlIdOffset.OLD_SOURCE_TEXT) + ) + + @script( + description=pgettext( + "poedit", + # Translators: The description of an NVDA command for Poedit. + "Reports the old source text, if any. If pressed twice, presents the text in browse mode", + ), + gesture="kb:control+shift+o", + ) + def script_reportOldSourceText(self, gesture): + self._reportControlScriptHelper( + self._oldSourceTextObj, + # Translators: The description of the "old source text" window in poedit. + # This text is reported when the given window contains no item to report or could not be found. + pgettext("poedit", "old source text"), + ) + + _translationWarningObj: Window | None + """Type definition for auto prop '_get__translationWarningObj'""" + + def _get__translationWarningObj(self) -> Window | None: + return self._getNVDAObjectForWindowControlIdOffset(_WindowControlIdOffset.TRANSLATION_WARNING) + + @script( + description=pgettext( + "poedit", + # Translators: The description of an NVDA command for Poedit. + "Reports a translation warning, if any. If pressed twice, presents the warning in browse mode", + ), + gesture="kb:control+shift+w", + ) + def script_reportTranslationWarning(self, gesture): + self._reportControlScriptHelper( + self._translationWarningObj, + # Translators: The description of the "translation warning" window in poedit. + # This text is reported when the given window contains no item to report or could not be found. + pgettext("poedit", "translation warning"), + ) + + _needsWorkObj: Window | None + """Type definition for auto prop '_get__needsWorkObj'""" + + def _get__needsWorkObj(self) -> Window | None: + obj = self._getNVDAObjectForWindowControlIdOffset(_WindowControlIdOffset.NEEDS_WORK_SWITCH) + if obj and obj.role == controlTypes.Role.CHECKBOX: + return obj + return None def chooseNVDAObjectOverlayClasses(self, obj, clsList): - if "SysListView32" in obj.windowClassName and obj.role==controlTypes.Role.LISTITEM: - clsList.insert(0,PoeditListItem) - - def event_NVDAObject_init(self, obj): - if obj.role == controlTypes.Role.EDITABLETEXT and controlTypes.State.MULTILINE in obj.states and obj.isInForeground: - # Oleacc often gets the name wrong. - # The label object is positioned just above the field on the screen. - l, t, w, h = obj.location - try: - obj.name = NVDAObjects.NVDAObject.objectFromPoint(l + 10, t - 10).name - except AttributeError: - pass - return + if obj.role == controlTypes.Role.LISTITEM and obj.windowClassName == "wxWindowNR": + clsList.insert(0, PoeditListItem) + elif ( + obj.role in (controlTypes.Role.EDITABLETEXT, controlTypes.Role.DOCUMENT) + and obj.windowClassName == "RICHEDIT50W" + ): + clsList.insert(0, PoeditRichEdit) -class PoeditListItem(sysListView32.ListItem): - def _get_isBold(self): - info=displayModel.DisplayModelTextInfo(self,position=textInfos.POSITION_FIRST) - info.expand(textInfos.UNIT_CHARACTER) - fields=info.getTextWithFields() +class PoeditRichEdit(NVDAObject): + def _get_name(self) -> str: + # These rich edit controls are incorrectly labeled. + # Oleacc doesn't return any name, and UIA defaults to RichEdit Control. + # The label object is positioned just above the field on the screen. + l, t, w, h = self.location try: - return fields[0].field['bold'] - except: - return False + self.name = NVDAObjects.NVDAObject.objectFromPoint(l + 10, t - 10).name + except AttributeError: + return super().name + return self.name + + +class PoeditListItem(NVDAObject): + _warningControlToReport: _WindowControlIdOffset | None + appModule: AppModule + + def _get__warningControlToReport(self) -> _WindowControlIdOffset | None: + obj = self.appModule._needsWorkObj + if obj and controlTypes.State.CHECKED in obj.states: + return _WindowControlIdOffset.NEEDS_WORK_SWITCH + obj = self.appModule._oldSourceTextObj + if obj and not obj.hasIrrelevantLocation: + return _WindowControlIdOffset.OLD_SOURCE_TEXT + obj = self.appModule._translationWarningObj + if obj and obj.parent and obj.parent.parent and not obj.parent.parent.hasIrrelevantLocation: + return _WindowControlIdOffset.TRANSLATION_WARNING + return None def _get_name(self): - # If this item is untranslated or fuzzy, then it will be bold. - # Other info on the web says that the background color of - # the item changes, but this doesn't seem to be true while testing. - name = super(PoeditListItem,self).name - return "* " + name if self.isBold else name - - def event_gainFocus(self): - super(sysListView32.ListItem, self).event_gainFocus() - if self.isBold: - tones.beep(550, 50) + name = super().name + if self._warningControlToReport or not self.description: + # This translation has a warning. + # Prepend an asterix (*) to the name + name = f"* {name}" + self.name = name + return self.name + + def reportFocus(self): + super().reportFocus() + if not self.description: + # This item is untranslated + tones.beep(440, 50) + return + match self._warningControlToReport: + case _WindowControlIdOffset.OLD_SOURCE_TEXT: + tones.beep(495, 50) + case _WindowControlIdOffset.TRANSLATION_WARNING: + tones.beep(550, 50) + case _WindowControlIdOffset.NEEDS_WORK_SWITCH: + tones.beep(660, 50) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index a7ebfcfddf8..304aee5da84 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -24,6 +24,8 @@ This option now announces additional relevant information about an object when t - When requesting formatting information on Excel cells, borders and background will only be reported if there is such formatting. (#15560, @CyrilleB79) - The command to toggle the screen curtain now has a default gesture: ``NVDA+control+escape``. (#10560, @CyrilleB79) - NVDA will again no longer report unlabelled groupings such as in recent versions of Microsoft Office 365 menus. (#15638) +- NVDA's support for [Poedit https://poedit.net] is overhauled for Poedit version 3 and above. +Users of Poedit 1 are encouraged to update to Poedit 3 if they want to rely on enhanced accessibility in Poedit, such as shortcuts to read translator notes and comments. (#15313, #7303, @LeonarddeR) - Braille viewer and speech viewer are now disabled in secure mode. (#15680) - @@ -91,6 +93,7 @@ That method receives a ``DriverRegistrar`` object on which the ``addUsbDevices`` - ``speechDictHandler.speechDictVars`` has been removed. Use ``NVDAState.WritePaths.speechDictsDir`` instead of ``speechDictHandler.speechDictVars.speechDictsPath``. (#15614, @lukaszgo1) - ``languageHandler.makeNpgettext`` and ``languageHandler.makePgettext`` have been removed. ``npgettext`` and ``pgettext`` are supported natively now. (#15546) +- The app module for [Poedit https://poedit.net] has been changed significantly. The ``fetchObject`` function has been removed. (#15313, #7303, @LeonarddeR) % Insert new list items here as the alias appModule table should be kept at the bottom of this list - The following app modules are removed. Code which imports from one of them, should instead import from the replacement module. (#15618, @lukaszgo1) diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index ae39749729f..fede61a6bb5 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -1265,10 +1265,14 @@ Note: The above shortcuts work only with the default formatting string for fooba %kc:endInclude ++ Poedit ++[Poedit] +NVDA offers enhanced support for Poedit 3.4 or newer. + %kc:beginInclude || Name | Key | Description | -| Report Comments Window | control+shift+c | Reports any comments in the comments window. | -| Report notes for translators | control+shift+a | Reports any notes for translators. | +| Report notes for translators | ``control+shift+a`` | Reports any notes for translators. If pressed twice, presents the notes in browse mode | +| Report Comment | ``control+shift+c`` | Reports any comment in the comments window. If pressed twice, presents the comment in browse mode | +| Report Old Source Text | ``control+shift+o`` | Reports the old source text, if any. If pressed twice, presents the text in browse mode | +| Report Translation Warning | ``control+shift+w`` | Reports a translation warning, if any. If pressed twice, presents the warning in browse mode | %kc:endInclude ++ Kindle for PC ++[Kindle]