Skip to content

Commit 566b981

Browse files
authored
Merge 32e19f5 into a380b6a
2 parents a380b6a + 32e19f5 commit 566b981

2 files changed

Lines changed: 208 additions & 103 deletions

File tree

source/appModules/poedit.py

Lines changed: 202 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,231 @@
1-
#appModules/poedit.py
2-
#A part of NonVisual Desktop Access (NVDA)
3-
#Copyright (C) 2012-2013 Mesar Hameed, NV Access Limited
4-
#This file is covered by the GNU General Public License.
5-
#See the file COPYING for more details.
1+
# A part of NonVisual Desktop Access (NVDA)
2+
# This file is covered by the GNU General Public License.
3+
# See the file COPYING for more details.
4+
# Copyright (C) 2012-2023 Mesar Hameed, NV Access Limited, Leonard de Ruijter
65

76
"""App module for Poedit.
87
"""
98

9+
from enum import IntEnum
10+
from typing import Optional
11+
1012
import api
1113
import appModuleHandler
1214
import controlTypes
13-
import displayModel
14-
import textInfos
15+
import NVDAObjects.IAccessible
1516
import tones
1617
import ui
17-
from NVDAObjects.IAccessible import sysListView32
1818
import windowUtils
19-
import NVDAObjects.IAccessible
2019
import winUser
20+
from NVDAObjects import NVDAObject
21+
from scriptHandler import getLastScriptRepeatCount, script
22+
23+
LEFT_TO_RIGHT_EMBEDDING = "\u202a"
24+
"""Character often found in translator comments."""
25+
26+
27+
class _WindowControlIdOffset(IntEnum):
28+
"""Window control ID's in poedit tend to be stable within one release, then change in a new release.
29+
However, the order of ids stays the same.
30+
Therefore, using a wxDataView control in the translations list as a reference,
31+
we can safely calculate control ids accross releases.
32+
This class contains window control id offsets relative to the wxDataView window.
33+
"""
34+
35+
OLD_SOURCE_TEXT = 65
36+
TRANSLATOR_NOTES = 68
37+
COMMENT = 71
38+
TRANSLATION_WARNING = 16
39+
NEEDS_WORK_SWITCH = 21
2140

2241

23-
def fetchObject(obj, path):
24-
"""Fetch the child object described by path.
25-
@returns: requested object if found, or None
26-
@rtype: L{NVDAObjects.NVDAObject}
42+
def _findDescendantObject(
43+
parentWindowHandle: int,
44+
controlId: Optional[int] = None,
45+
className: Optional[str] = None
46+
) -> Optional[NVDAObject]:
47+
"""
48+
Finds a window with the given controlId or class name,
49+
starting from the window belonging to the given parentWindowHandle,
50+
and returns the object belonging to it.
2751
"""
28-
path.reverse()
29-
p = obj
30-
while len(path) and p.firstChild:
31-
p = p.firstChild
32-
steps = path.pop()
33-
i=0
34-
while i<steps and p.next:
35-
p = p.next
36-
i += 1
37-
# the path requests us to look for further siblings, but none found.
38-
if i<steps: return None
39-
# the path requests us to look for further children, but none found.
40-
if len(path): return None
41-
return p
52+
try:
53+
obj = NVDAObjects.IAccessible.getNVDAObjectFromEvent(
54+
windowUtils.findDescendantWindow(
55+
parentWindowHandle,
56+
controlID=controlId,
57+
className=className
58+
),
59+
winUser.OBJID_CLIENT,
60+
0
61+
)
62+
except LookupError:
63+
obj = None
64+
return obj
4265

4366

4467
class AppModule(appModuleHandler.AppModule):
68+
def __init__(self, processID, appName=None):
69+
super().__init__(processID, appName)
70+
self._dataViewControlId: int = 0
4571

46-
def script_reportAutoCommentsWindow(self,gesture):
47-
obj = fetchObject(api.getForegroundObject(), [2, 0, 1, 0, 1, 0, 1])
48-
if obj and obj.windowControlID != 101:
49-
try:
50-
obj = obj.next.firstChild
51-
except AttributeError:
52-
obj = None
53-
elif obj:
54-
obj = obj.firstChild
72+
def _getNVDAObjectForWindowControlIdOffset(self, windowControlIdOffset: _WindowControlIdOffset):
73+
fg = api.getForegroundObject()
74+
if not self._dataViewControlId:
75+
obj = _findDescendantObject(fg.windowHandle, className="wxDataView")
76+
self._dataViewControlId = obj.windowControlID
77+
return _findDescendantObject(fg.windowHandle, self._dataViewControlId + windowControlIdOffset)
78+
79+
def _reportControlScriptHelper(self, windowControlIdOffset: _WindowControlIdOffset, description: str):
80+
obj = self._getNVDAObjectForWindowControlIdOffset(windowControlIdOffset)
5581
if obj:
56-
try:
57-
ui.message(obj.name + " " + obj.value)
58-
except:
59-
# Translators: this message is reported when there are no
60-
# notes for translators to be presented to the user in Poedit.
61-
ui.message(_("No notes for translators."))
82+
if not obj.hasIrrelevantLocation and not obj.parent.parent.hasIrrelevantLocation:
83+
message = obj.name.replace(LEFT_TO_RIGHT_EMBEDDING, "")
84+
repeats = getLastScriptRepeatCount()
85+
if repeats == 0:
86+
ui.message(message)
87+
else:
88+
ui.browseableMessage(message, description.title())
89+
else:
90+
ui.message(
91+
# Translators: this message is reported when there is nothing
92+
# to be presented to the user in Poedit.
93+
# {description} is replaced by the description of the window to be reported,
94+
# e.g. translator notes
95+
pgettext("poedit", "No {description}").format(description=description)
96+
)
6297
else:
63-
# Translators: this message is reported when NVDA is unable to find
64-
# the 'Notes for translators' window in poedit.
65-
ui.message(_("Could not find Notes for translators window."))
66-
# Translators: The description of an NVDA command for Poedit.
67-
script_reportAutoCommentsWindow.__doc__ = _("Reports any notes for translators")
68-
69-
def script_reportCommentsWindow(self,gesture):
70-
try:
71-
obj = NVDAObjects.IAccessible.getNVDAObjectFromEvent(
72-
windowUtils.findDescendantWindow(api.getForegroundObject().windowHandle, visible=True, controlID=104),
73-
winUser.OBJID_CLIENT, 0)
74-
except LookupError:
7598
# Translators: this message is reported when NVDA is unable to find
76-
# the 'comments' window in poedit.
77-
ui.message(_("Could not find comment window."))
78-
return None
79-
try:
80-
ui.message(obj.name + " " + obj.value)
81-
except:
82-
# Translators: this message is reported when there are no
83-
# comments to be presented to the user in the translator
84-
# comments window in poedit.
85-
ui.message(_("No comment."))
86-
# Translators: The description of an NVDA command for Poedit.
87-
script_reportCommentsWindow.__doc__ = _("Reports any comments in the comments window")
88-
89-
__gestures = {
90-
"kb:control+shift+c": "reportCommentsWindow",
91-
"kb:control+shift+a": "reportAutoCommentsWindow",
92-
}
99+
# a requested window in Poedit.
100+
# {description} is replaced by the description of the window to be reported, e.g. translator notes
101+
ui.message(
102+
pgettext("poedit", "Could not find {description} window.").format(description=description)
103+
)
104+
105+
@script(
106+
description=pgettext(
107+
"poedit",
108+
# Translators: The description of an NVDA command for Poedit.
109+
"Reports any notes for translators. If pressed twice, presents the notes in browse mode",
110+
),
111+
gesture="kb:control+shift+a",
112+
)
113+
def script_reportAutoCommentsWindow(self, gesture):
114+
self._reportControlScriptHelper(
115+
_WindowControlIdOffset.TRANSLATOR_NOTES,
116+
# Translators: The description of the "Translator notes" window in poedit.
117+
# This text is reported when the given window contains no item to report or could not be found.
118+
pgettext("poedit", "notes for translators"),
119+
)
120+
121+
@script(
122+
description=pgettext(
123+
"poedit",
124+
# Translators: The description of an NVDA command for Poedit.
125+
"Reports any comment in the comments window. "
126+
"If pressed twice, presents the comment in browse mode",
127+
),
128+
gesture="kb:control+shift+c",
129+
)
130+
def script_reportCommentsWindow(self, gesture):
131+
self._reportControlScriptHelper(
132+
_WindowControlIdOffset.COMMENT,
133+
# Translators: The description of the "comment" window in poedit.
134+
# This text is reported when the given window contains no item to report or could not be found.
135+
pgettext("poedit", "comment"),
136+
)
137+
138+
@script(
139+
description=pgettext(
140+
"poedit",
141+
# Translators: The description of an NVDA command for Poedit.
142+
"Reports the old source text, if any. If pressed twice, presents the text in browse mode",
143+
),
144+
gesture="kb:control+shift+o",
145+
)
146+
def script_reportOldSourceText(self, gesture):
147+
self._reportControlScriptHelper(
148+
_WindowControlIdOffset.OLD_SOURCE_TEXT,
149+
# Translators: The description of the "old source text" window in poedit.
150+
# This text is reported when the given window contains no item to report or could not be found.
151+
pgettext("poedit", "old source text"),
152+
)
153+
154+
@script(
155+
description=pgettext(
156+
"poedit",
157+
# Translators: The description of an NVDA command for Poedit.
158+
"Reports a translation warning, if any. If pressed twice, presents the warning in browse mode",
159+
),
160+
gesture="kb:control+shift+w",
161+
)
162+
def script_reportTranslationWarning(self, gesture):
163+
self._reportControlScriptHelper(
164+
_WindowControlIdOffset.TRANSLATION_WARNING,
165+
# Translators: The description of the "translation warning" window in poedit.
166+
# This text is reported when the given window contains no item to report or could not be found.
167+
pgettext("poedit", "translation warning"),
168+
)
93169

94170
def chooseNVDAObjectOverlayClasses(self, obj, clsList):
95-
if "SysListView32" in obj.windowClassName and obj.role==controlTypes.Role.LISTITEM:
96-
clsList.insert(0,PoeditListItem)
97-
98-
def event_NVDAObject_init(self, obj):
99-
if obj.role == controlTypes.Role.EDITABLETEXT and controlTypes.State.MULTILINE in obj.states and obj.isInForeground:
100-
# Oleacc often gets the name wrong.
101-
# The label object is positioned just above the field on the screen.
102-
l, t, w, h = obj.location
103-
try:
104-
obj.name = NVDAObjects.NVDAObject.objectFromPoint(l + 10, t - 10).name
105-
except AttributeError:
106-
pass
107-
return
171+
if obj.role == controlTypes.Role.LISTITEM and obj.windowClassName == "wxWindowNR":
172+
clsList.insert(0, PoeditListItem)
173+
elif (
174+
obj.role in (controlTypes.Role.EDITABLETEXT, controlTypes.Role.DOCUMENT)
175+
and obj.windowClassName == "RICHEDIT50W"
176+
):
177+
clsList.insert(0, PoeditRichEdit)
108178

109-
class PoeditListItem(sysListView32.ListItem):
110179

111-
def _get_isBold(self):
112-
info=displayModel.DisplayModelTextInfo(self,position=textInfos.POSITION_FIRST)
113-
info.expand(textInfos.UNIT_CHARACTER)
114-
fields=info.getTextWithFields()
180+
class PoeditRichEdit(NVDAObject):
181+
def _get_name(self) -> str:
182+
# These rich edit controls are incorrectly labeled.
183+
# Oleacc doesn't return any name, and UIA defaults to RichEdit Control.
184+
# The label object is positioned just above the field on the screen.
185+
l, t, w, h = self.location
115186
try:
116-
return fields[0].field['bold']
117-
except:
118-
return False
187+
self.name = NVDAObjects.NVDAObject.objectFromPoint(l + 10, t - 10).name
188+
except AttributeError:
189+
return super().name
190+
return self.name
191+
192+
193+
class PoeditListItem(NVDAObject):
194+
_warningControlToReport: Optional[_WindowControlIdOffset]
195+
196+
def _get__warningControlToReport(self) -> Optional[_WindowControlIdOffset]:
197+
obj = self.appModule._getNVDAObjectForWindowControlIdOffset(_WindowControlIdOffset.NEEDS_WORK_SWITCH)
198+
if obj and controlTypes.State.CHECKED in obj.states:
199+
return _WindowControlIdOffset.NEEDS_WORK_SWITCH
200+
obj = self.appModule._getNVDAObjectForWindowControlIdOffset(_WindowControlIdOffset.OLD_SOURCE_TEXT)
201+
if obj and not obj.hasIrrelevantLocation:
202+
return _WindowControlIdOffset.OLD_SOURCE_TEXT
203+
obj = self.appModule._getNVDAObjectForWindowControlIdOffset(
204+
_WindowControlIdOffset.TRANSLATION_WARNING
205+
)
206+
if obj and obj.parent and obj.parent.parent and not obj.parent.parent.hasIrrelevantLocation:
207+
return _WindowControlIdOffset.TRANSLATION_WARNING
208+
return None
119209

120210
def _get_name(self):
121-
# If this item is untranslated or fuzzy, then it will be bold.
122-
# Other info on the web says that the background color of
123-
# the item changes, but this doesn't seem to be true while testing.
124-
name = super(PoeditListItem,self).name
125-
return "* " + name if self.isBold else name
126-
127-
def event_gainFocus(self):
128-
super(sysListView32.ListItem, self).event_gainFocus()
129-
if self.isBold:
211+
name = super().name
212+
if self._warningControlToReport or not self.description:
213+
# This translation has a warning.
214+
# Prepend an asterix (*) to the name
215+
name = f"* {name}"
216+
self.name = name
217+
return self.name
218+
219+
def reportFocus(self):
220+
super().reportFocus()
221+
if not self.description:
222+
# This item is untranslated
223+
tones.beep(440, 50)
224+
return
225+
warning = self._warningControlToReport
226+
if warning is _WindowControlIdOffset.OLD_SOURCE_TEXT:
227+
tones.beep(495, 50)
228+
elif warning is _WindowControlIdOffset.TRANSLATION_WARNING:
130229
tones.beep(550, 50)
230+
elif warning is _WindowControlIdOffset.NEEDS_WORK_SWITCH:
231+
tones.beep(660, 50)

user_docs/en/userGuide.t2t

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,10 +1258,14 @@ Note: The above shortcuts work only with the default formatting string for fooba
12581258
%kc:endInclude
12591259

12601260
++ Poedit ++[Poedit]
1261+
NVDA offers enhanced support for Poedit 3.4 or newer.
1262+
12611263
%kc:beginInclude
12621264
|| Name | Key | Description |
1263-
| Report Comments Window | control+shift+c | Reports any comments in the comments window. |
1264-
| Report notes for translators | control+shift+a | Reports any notes for translators. |
1265+
| Report notes for translators | ``control+shift+a`` | Reports any notes for translators. If pressed twice, presents the notes in browse mode |
1266+
| Report Comment | ``control+shift+c`` | Reports any comment in the comments window. If pressed twice, presents the comment in browse mode |
1267+
| Report Old Source Text | ``control+shift+o`` | Reports the old source text, if any. If pressed twice, presents the text in browse mode |
1268+
| Report Translation Warning | ``control+shift+w`` | Reports a translation warning, if any. If pressed twice, presents the warning in browse mode |
12651269
%kc:endInclude
12661270

12671271
++ Kindle for PC ++[Kindle]

0 commit comments

Comments
 (0)