Skip to content

Commit cb098a6

Browse files
authored
Merge ec76697 into 6e521b1
2 parents 6e521b1 + ec76697 commit cb098a6

3 files changed

Lines changed: 274 additions & 105 deletions

File tree

source/appModules/poedit.py

Lines changed: 265 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,292 @@
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, Rui Fontes
65

7-
"""App module for Poedit.
6+
"""App module for Poedit 3.4+.
87
"""
98

9+
from enum import IntEnum
10+
1011
import api
1112
import appModuleHandler
1213
import controlTypes
13-
import displayModel
14-
import textInfos
14+
import NVDAObjects.IAccessible
1515
import tones
1616
import ui
17-
from NVDAObjects.IAccessible import sysListView32
1817
import windowUtils
19-
import NVDAObjects.IAccessible
2018
import winUser
19+
from NVDAObjects import NVDAObject
20+
from NVDAObjects.window import Window
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 are not static, however, the order of ids stays the same.
29+
Therefore, using a wxDataView control in the translations list as a reference,
30+
we can safely calculate control ids accross releases or instances.
31+
This class contains window control id offsets relative to the wxDataView window.
32+
"""
33+
34+
PRO_IDENTIFIER = -10 # This is a button in the free version
35+
OLD_SOURCE_TEXT_PRO = 60
36+
OLD_SOURCE_TEXT = 65
37+
TRANSLATOR_NOTES_PRO = 63
38+
TRANSLATOR_NOTES = 68 # 63 in Pro
39+
COMMENT_PRO = 66
40+
COMMENT = 71
41+
TRANSLATION_WARNING = 17
42+
NEEDS_WORK_SWITCH = 21
2143

2244

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}
45+
def _findDescendantObject(
46+
parentWindowHandle: int,
47+
controlId: int | None = None,
48+
className: str | None = None,
49+
) -> Window | None:
2750
"""
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
51+
Finds a window with the given controlId or class name,
52+
starting from the window belonging to the given parentWindowHandle,
53+
and returns the object belonging to it.
54+
"""
55+
try:
56+
obj = NVDAObjects.IAccessible.getNVDAObjectFromEvent(
57+
windowUtils.findDescendantWindow(parentWindowHandle, controlID=controlId, className=className),
58+
winUser.OBJID_CLIENT,
59+
0,
60+
)
61+
except LookupError:
62+
obj = None
63+
return obj
4264

4365

4466
class AppModule(appModuleHandler.AppModule):
67+
cachePropertiesByDefault = True
68+
69+
_dataViewControlId: int | None
70+
"""Type definition for auto prop '_get__dataViewControlId'"""
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 _get__dataViewControlId(self) -> int | None:
73+
fg = api.getForegroundObject()
74+
dataView = _findDescendantObject(fg.windowHandle, className="wxDataView")
75+
if not dataView:
76+
return None
77+
return dataView.windowControlID
78+
79+
_isPro: bool
80+
"""Type definition for auto prop '_get__isPro'"""
81+
82+
def _get__isPro(self) -> bool:
83+
"""Returns whether this instance of Poedit is a pro version."""
84+
obj = self._getNVDAObjectForWindowControlIdOffset(_WindowControlIdOffset.PRO_IDENTIFIER)
85+
return obj is None
86+
87+
def _correctWindowControllIdOfset(self, windowControlIdOffset: _WindowControlIdOffset) -> _WindowControlIdOffset:
88+
"""Corrects a _WindowControlIdOffset when a pro version of Poedit is active."""
89+
if self._isPro:
90+
match windowControlIdOffset:
91+
case _WindowControlIdOffset.OLD_SOURCE_TEXT:
92+
return _WindowControlIdOffset.OLD_SOURCE_TEXT_PRO
93+
case _WindowControlIdOffset.TRANSLATOR_NOTES:
94+
return _WindowControlIdOffset.TRANSLATOR_NOTES_PRO
95+
case _WindowControlIdOffset.COMMENT:
96+
return _WindowControlIdOffset.COMMENT_PRO
97+
return windowControlIdOffset
98+
99+
def _getNVDAObjectForWindowControlIdOffset(self, windowControlIdOffset: _WindowControlIdOffset) -> Window | None:
100+
fg = api.getForegroundObject()
101+
return _findDescendantObject(fg.windowHandle, self._dataViewControlId + windowControlIdOffset)
102+
103+
_translatorNotesObj: Window | None
104+
"""Type definition for auto prop '_get__translatorNotesObj'"""
105+
106+
def _get__translatorNotesObj(self) -> Window | None:
107+
return self._getNVDAObjectForWindowControlIdOffset(
108+
self._correctWindowControllIdOfset(_WindowControlIdOffset.TRANSLATOR_NOTES)
109+
)
110+
111+
def _reportControlScriptHelper(self, obj: Window, description: str):
55112
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."))
113+
if not obj.hasIrrelevantLocation and not obj.parent.parent.hasIrrelevantLocation:
114+
message = obj.name.replace(LEFT_TO_RIGHT_EMBEDDING, "")
115+
repeats = getLastScriptRepeatCount()
116+
if repeats == 0:
117+
ui.message(message)
118+
else:
119+
ui.browseableMessage(message, description.title())
120+
else:
121+
ui.message(
122+
# Translators: this message is reported when there is nothing
123+
# to be presented to the user in Poedit.
124+
# {description} is replaced by the description of the window to be reported,
125+
# e.g. translator notes
126+
pgettext("poedit", "No {description}").format(description=description)
127+
)
62128
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")
129+
ui.message(
130+
# Translators: this message is reported when NVDA is unable to find
131+
# a requested window in Poedit.
132+
# {description} is replaced by the description of the window to be reported, e.g. translator notes
133+
pgettext("poedit", "Could not find {description} window.").format(description=description)
134+
)
68135

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:
75-
# 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-
}
136+
@script(
137+
description=pgettext(
138+
"poedit",
139+
# Translators: The description of an NVDA command for Poedit.
140+
"Reports any notes for translators. If pressed twice, presents the notes in browse mode",
141+
),
142+
gesture="kb:control+shift+a",
143+
)
144+
def script_reportAutoCommentsWindow(self, gesture):
145+
self._reportControlScriptHelper(
146+
self._translatorNotesObj,
147+
# Translators: The description of the "Translator notes" window in poedit.
148+
# This text is reported when the given window contains no item to report or could not be found.
149+
pgettext("poedit", "notes for translators"),
150+
)
151+
152+
_commentObj: Window | None
153+
"""Type definition for auto prop '_get__commentObj'"""
154+
155+
def _get__commentObj(self) -> Window | None:
156+
return self._getNVDAObjectForWindowControlIdOffset(
157+
self._correctWindowControllIdOfset(_WindowControlIdOffset.COMMENT)
158+
)
159+
160+
@script(
161+
description=pgettext(
162+
"poedit",
163+
# Translators: The description of an NVDA command for Poedit.
164+
"Reports any comment in the comments window. "
165+
"If pressed twice, presents the comment in browse mode",
166+
),
167+
gesture="kb:control+shift+c",
168+
)
169+
def script_reportCommentsWindow(self, gesture):
170+
self._reportControlScriptHelper(
171+
self._commentObj,
172+
# Translators: The description of the "comment" window in poedit.
173+
# This text is reported when the given window contains no item to report or could not be found.
174+
pgettext("poedit", "comment"),
175+
)
176+
177+
_oldSourceTextObj: Window | None
178+
"""Type definition for auto prop '_get__oldSourceTextObj'"""
179+
180+
def _get__oldSourceTextObj(self) -> Window | None:
181+
return self._getNVDAObjectForWindowControlIdOffset(
182+
self._correctWindowControllIdOfset(_WindowControlIdOffset.OLD_SOURCE_TEXT)
183+
)
184+
185+
@script(
186+
description=pgettext(
187+
"poedit",
188+
# Translators: The description of an NVDA command for Poedit.
189+
"Reports the old source text, if any. If pressed twice, presents the text in browse mode",
190+
),
191+
gesture="kb:control+shift+o",
192+
)
193+
def script_reportOldSourceText(self, gesture):
194+
self._reportControlScriptHelper(
195+
self._oldSourceTextObj,
196+
# Translators: The description of the "old source text" window in poedit.
197+
# This text is reported when the given window contains no item to report or could not be found.
198+
pgettext("poedit", "old source text"),
199+
)
200+
201+
_translationWarningObj: Window | None
202+
"""Type definition for auto prop '_get__translationWarningObj'"""
203+
204+
def _get__translationWarningObj(self) -> Window | None:
205+
return self._getNVDAObjectForWindowControlIdOffset(_WindowControlIdOffset.TRANSLATION_WARNING)
206+
207+
@script(
208+
description=pgettext(
209+
"poedit",
210+
# Translators: The description of an NVDA command for Poedit.
211+
"Reports a translation warning, if any. If pressed twice, presents the warning in browse mode",
212+
),
213+
gesture="kb:control+shift+w",
214+
)
215+
def script_reportTranslationWarning(self, gesture):
216+
self._reportControlScriptHelper(
217+
self._translationWarningObj,
218+
# Translators: The description of the "translation warning" window in poedit.
219+
# This text is reported when the given window contains no item to report or could not be found.
220+
pgettext("poedit", "translation warning"),
221+
)
222+
223+
_needsWorkObj: Window | None
224+
"""Type definition for auto prop '_get__needsWorkObj'"""
225+
226+
def _get__needsWorkObj(self) -> Window | None:
227+
obj = self._getNVDAObjectForWindowControlIdOffset(_WindowControlIdOffset.NEEDS_WORK_SWITCH)
228+
if obj and obj.role == controlTypes.Role.CHECKBOX:
229+
return obj
230+
return None
93231

94232
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
233+
if obj.role == controlTypes.Role.LISTITEM and obj.windowClassName == "wxWindowNR":
234+
clsList.insert(0, PoeditListItem)
235+
elif (
236+
obj.role in (controlTypes.Role.EDITABLETEXT, controlTypes.Role.DOCUMENT)
237+
and obj.windowClassName == "RICHEDIT50W"
238+
):
239+
clsList.insert(0, PoeditRichEdit)
108240

109-
class PoeditListItem(sysListView32.ListItem):
110241

111-
def _get_isBold(self):
112-
info=displayModel.DisplayModelTextInfo(self,position=textInfos.POSITION_FIRST)
113-
info.expand(textInfos.UNIT_CHARACTER)
114-
fields=info.getTextWithFields()
242+
class PoeditRichEdit(NVDAObject):
243+
def _get_name(self) -> str:
244+
# These rich edit controls are incorrectly labeled.
245+
# Oleacc doesn't return any name, and UIA defaults to RichEdit Control.
246+
# The label object is positioned just above the field on the screen.
247+
l, t, w, h = self.location
115248
try:
116-
return fields[0].field['bold']
117-
except:
118-
return False
249+
self.name = NVDAObjects.NVDAObject.objectFromPoint(l + 10, t - 10).name
250+
except AttributeError:
251+
return super().name
252+
return self.name
253+
254+
255+
class PoeditListItem(NVDAObject):
256+
_warningControlToReport: _WindowControlIdOffset | None
257+
appModule: AppModule
258+
259+
def _get__warningControlToReport(self) -> _WindowControlIdOffset | None:
260+
obj = self.appModule._needsWorkObj
261+
if obj and controlTypes.State.CHECKED in obj.states:
262+
return _WindowControlIdOffset.NEEDS_WORK_SWITCH
263+
obj = self.appModule._oldSourceTextObj
264+
if obj and not obj.hasIrrelevantLocation:
265+
return _WindowControlIdOffset.OLD_SOURCE_TEXT
266+
obj = self.appModule._translationWarningObj
267+
if obj and obj.parent and obj.parent.parent and not obj.parent.parent.hasIrrelevantLocation:
268+
return _WindowControlIdOffset.TRANSLATION_WARNING
269+
return None
119270

120271
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:
130-
tones.beep(550, 50)
272+
name = super().name
273+
if self._warningControlToReport or not self.description:
274+
# This translation has a warning.
275+
# Prepend an asterix (*) to the name
276+
name = f"* {name}"
277+
self.name = name
278+
return self.name
279+
280+
def reportFocus(self):
281+
super().reportFocus()
282+
if not self.description:
283+
# This item is untranslated
284+
tones.beep(440, 50)
285+
return
286+
match self._warningControlToReport:
287+
case _WindowControlIdOffset.OLD_SOURCE_TEXT:
288+
tones.beep(495, 50)
289+
case _WindowControlIdOffset.TRANSLATION_WARNING:
290+
tones.beep(550, 50)
291+
case _WindowControlIdOffset.NEEDS_WORK_SWITCH:
292+
tones.beep(660, 50)

0 commit comments

Comments
 (0)