Skip to content

Commit 4f11b39

Browse files
Poedit: ensure scripts work no matter how many plurals a language has (nvaccess#16589)
Fixes nvaccess#16318 Summary of the issue: Recently the poedit appModule was rewritten to support poedit 3.4. However, scripts for reading translation nodes, comments and old source text did not function for languages that had no plurals (Chinese) or 2 or more plurals (Polish). It did however function for languages with exactly one plural (such as French). As the controlIDs for windows in poedit are not static, but do stay relative to each other, the appModule originally used offsets from the main dataView control. This worked okay for the translation warning, but did not work for controls in the sidebar in all cases, as it seems that there are extra controlIDs consumed for some hidden windows between the main splitter and the sidebar, depending on how many plurals a language has. E.g. In French, the controlID offset for the Sidebar (relative to the Dataview control) is 29. In Chinese it is 27, and in Plish it is 33. I think it might be roughly two offsets per plural. Description of user facing changes Poedit scripts such as Report translation notes (control+shift+a), Report comments (control+shift+c) and Report old source text (control+shift+o) now function no matter how many plurals a language has. Description of development approach Control ID offsets for controls in the sidebar (such as translation notes, old source text, and comments) are now relative to the sidebar itself, rather than the Dataview control. The sidebar window is located by first finding the main splitter window (using a controlID relative to the dataview control) and then finding the next visible sibling window from there. To aide in refactoring, support for controlID offsets in the Pro version are now handled by simply minusing 5 from the given controlID offset, as all the offsets in the pro version differed by 5. Testing strategy:
1 parent 8caf17c commit 4f11b39

File tree

2 files changed

+74
-41
lines changed

2 files changed

+74
-41
lines changed

source/appModules/poedit.py

Lines changed: 73 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from enum import IntEnum
1010

11+
import ctypes
1112
import api
1213
import appModuleHandler
1314
import controlTypes
@@ -19,6 +20,8 @@
1920
from NVDAObjects import NVDAObject
2021
from NVDAObjects.window import Window
2122
from scriptHandler import getLastScriptRepeatCount, script
23+
from logHandler import log
24+
2225

2326
LEFT_TO_RIGHT_EMBEDDING = "\u202a"
2427
"""Character often found in translator comments."""
@@ -27,24 +30,33 @@
2730
SCRCAT_POEDIT = _("Poedit")
2831

2932

30-
class _WindowControlIdOffset(IntEnum):
33+
class _WindowControlIdOffsetFromDataView(IntEnum):
3134
"""Window control ID's are not static, however, the order of ids stays the same.
3235
Therefore, using a wxDataView control in the translations list as a reference,
3336
we can safely calculate control ids accross releases or instances.
3437
This class contains window control id offsets relative to the wxDataView window.
3538
"""
3639

3740
PRO_IDENTIFIER = -10 # This is a button in the free version
38-
OLD_SOURCE_TEXT_PRO = 60
39-
OLD_SOURCE_TEXT = 65
40-
TRANSLATOR_NOTES_PRO = 63
41-
TRANSLATOR_NOTES = 68 # 63 in Pro
42-
COMMENT_PRO = 66
43-
COMMENT = 71
41+
MAIN_SPLITTER_IDENTIFIER = -2 # The splitter that holds the translation list
4442
TRANSLATION_WARNING = 17
4543
NEEDS_WORK_SWITCH = 21
4644

4745

46+
class _WindowControlIdOffsetFromSidebar(IntEnum):
47+
"""Window control ID's are not static, however, the order of ids stays the same.
48+
Therefore, using the Sidebar window as a reference,
49+
we can safely calculate control ids accross releases or instances.
50+
This class contains window control id offsets relative to the Sidebar window.
51+
Note that this Sidebar window itself is found relative to the dataview's ancestor splitter control.
52+
"""
53+
54+
PRO_OFFSET = -5
55+
OLD_SOURCE_TEXT = 36
56+
TRANSLATOR_NOTES = 39
57+
COMMENT = 42
58+
59+
4860
def _findDescendantObject(
4961
parentWindowHandle: int,
5062
controlId: int | None = None,
@@ -79,42 +91,58 @@ def _get__dataViewControlId(self) -> int | None:
7991
return None
8092
return dataView.windowControlID
8193

94+
_sidebarControlId: int | None
95+
"""Type definition for auto prop '_get__sidebarControlId'"""
96+
97+
def _get__sidebarControlId(self) -> int | None:
98+
dataViewControlId = self._dataViewControlId
99+
splitterControlID = dataViewControlId + _WindowControlIdOffsetFromDataView.MAIN_SPLITTER_IDENTIFIER
100+
fg = api.getForegroundObject()
101+
splitterHwnd = windowUtils.findDescendantWindow(fg.windowHandle, controlID=splitterControlID)
102+
sidebarHwnd = winUser.getWindow(splitterHwnd, winUser.GW_HWNDNEXT)
103+
while sidebarHwnd and not ctypes.windll.user32.IsWindowVisible(sidebarHwnd):
104+
sidebarHwnd = winUser.getWindow(sidebarHwnd, winUser.GW_HWNDNEXT)
105+
if not sidebarHwnd:
106+
return None
107+
return winUser.getControlID(sidebarHwnd)
108+
82109
_isPro: bool
83110
"""Type definition for auto prop '_get__isPro'"""
84111

85112
def _get__isPro(self) -> bool:
86113
"""Returns whether this instance of Poedit is a pro version."""
87-
obj = self._getNVDAObjectForWindowControlIdOffset(_WindowControlIdOffset.PRO_IDENTIFIER)
114+
obj = self._getNVDAObjectForWindowControlIdOffsetFromDataView(
115+
_WindowControlIdOffsetFromDataView.PRO_IDENTIFIER
116+
)
88117
return obj is None
89118

90-
def _correctWindowControllIdOfset(
91-
self,
92-
windowControlIdOffset: _WindowControlIdOffset
93-
) -> _WindowControlIdOffset:
94-
"""Corrects a _WindowControlIdOffset when a pro version of Poedit is active."""
95-
if self._isPro:
96-
match windowControlIdOffset:
97-
case _WindowControlIdOffset.OLD_SOURCE_TEXT:
98-
return _WindowControlIdOffset.OLD_SOURCE_TEXT_PRO
99-
case _WindowControlIdOffset.TRANSLATOR_NOTES:
100-
return _WindowControlIdOffset.TRANSLATOR_NOTES_PRO
101-
case _WindowControlIdOffset.COMMENT:
102-
return _WindowControlIdOffset.COMMENT_PRO
103-
return windowControlIdOffset
104-
105-
def _getNVDAObjectForWindowControlIdOffset(
119+
def _getNVDAObjectForWindowControlIdOffsetFromDataView(
106120
self,
107-
windowControlIdOffset: _WindowControlIdOffset
121+
windowControlIdOffset: _WindowControlIdOffsetFromDataView
108122
) -> Window | None:
109123
fg = api.getForegroundObject()
110124
return _findDescendantObject(fg.windowHandle, self._dataViewControlId + windowControlIdOffset)
111125

126+
def _getNVDAObjectForWindowControlIdOffsetFromSidebar(
127+
self,
128+
windowControlIdOffset: _WindowControlIdOffsetFromSidebar
129+
) -> Window | None:
130+
fg = api.getForegroundObject()
131+
sidebarControlId = self._sidebarControlId
132+
if sidebarControlId is None:
133+
log.error("Sidebar can not be found")
134+
return None
135+
extraOffset = 0
136+
if self._isPro:
137+
extraOffset = _WindowControlIdOffsetFromSidebar.PRO_OFFSET
138+
return _findDescendantObject(fg.windowHandle, sidebarControlId + extraOffset + windowControlIdOffset)
139+
112140
_translatorNotesObj: Window | None
113141
"""Type definition for auto prop '_get__translatorNotesObj'"""
114142

115143
def _get__translatorNotesObj(self) -> Window | None:
116-
return self._getNVDAObjectForWindowControlIdOffset(
117-
self._correctWindowControllIdOfset(_WindowControlIdOffset.TRANSLATOR_NOTES)
144+
return self._getNVDAObjectForWindowControlIdOffsetFromSidebar(
145+
_WindowControlIdOffsetFromSidebar.TRANSLATOR_NOTES
118146
)
119147

120148
def _reportControlScriptHelper(self, obj: Window, description: str):
@@ -164,8 +192,8 @@ def script_reportAutoCommentsWindow(self, gesture):
164192
"""Type definition for auto prop '_get__commentObj'"""
165193

166194
def _get__commentObj(self) -> Window | None:
167-
return self._getNVDAObjectForWindowControlIdOffset(
168-
self._correctWindowControllIdOfset(_WindowControlIdOffset.COMMENT)
195+
return self._getNVDAObjectForWindowControlIdOffsetFromSidebar(
196+
_WindowControlIdOffsetFromSidebar.COMMENT
169197
)
170198

171199
@script(
@@ -191,8 +219,8 @@ def script_reportCommentsWindow(self, gesture):
191219
"""Type definition for auto prop '_get__oldSourceTextObj'"""
192220

193221
def _get__oldSourceTextObj(self) -> Window | None:
194-
return self._getNVDAObjectForWindowControlIdOffset(
195-
self._correctWindowControllIdOfset(_WindowControlIdOffset.OLD_SOURCE_TEXT)
222+
return self._getNVDAObjectForWindowControlIdOffsetFromSidebar(
223+
_WindowControlIdOffsetFromSidebar.OLD_SOURCE_TEXT
196224
)
197225

198226
@script(
@@ -217,7 +245,9 @@ def script_reportOldSourceText(self, gesture):
217245
"""Type definition for auto prop '_get__translationWarningObj'"""
218246

219247
def _get__translationWarningObj(self) -> Window | None:
220-
return self._getNVDAObjectForWindowControlIdOffset(_WindowControlIdOffset.TRANSLATION_WARNING)
248+
return self._getNVDAObjectForWindowControlIdOffsetFromDataView(
249+
_WindowControlIdOffsetFromDataView.TRANSLATION_WARNING
250+
)
221251

222252
@script(
223253
description=pgettext(
@@ -241,7 +271,9 @@ def script_reportTranslationWarning(self, gesture):
241271
"""Type definition for auto prop '_get__needsWorkObj'"""
242272

243273
def _get__needsWorkObj(self) -> Window | None:
244-
obj = self._getNVDAObjectForWindowControlIdOffset(_WindowControlIdOffset.NEEDS_WORK_SWITCH)
274+
obj = self._getNVDAObjectForWindowControlIdOffsetFromDataView(
275+
_WindowControlIdOffsetFromDataView.NEEDS_WORK_SWITCH
276+
)
245277
if obj and obj.role == controlTypes.Role.CHECKBOX:
246278
return obj
247279
return None
@@ -270,19 +302,19 @@ def _get_name(self) -> str:
270302

271303

272304
class PoeditListItem(NVDAObject):
273-
_warningControlToReport: _WindowControlIdOffset | None
305+
_warningControlToReport: _WindowControlIdOffsetFromDataView | None
274306
appModule: AppModule
275307

276-
def _get__warningControlToReport(self) -> _WindowControlIdOffset | None:
308+
def _get__warningControlToReport(self) -> int | None:
277309
obj = self.appModule._needsWorkObj
278310
if obj and controlTypes.State.CHECKED in obj.states:
279-
return _WindowControlIdOffset.NEEDS_WORK_SWITCH
311+
return _WindowControlIdOffsetFromDataView.NEEDS_WORK_SWITCH
280312
obj = self.appModule._oldSourceTextObj
281313
if obj and not obj.hasIrrelevantLocation:
282-
return _WindowControlIdOffset.OLD_SOURCE_TEXT
314+
return _WindowControlIdOffsetFromSidebar.OLD_SOURCE_TEXT
283315
obj = self.appModule._translationWarningObj
284316
if obj and obj.parent and obj.parent.parent and not obj.parent.parent.hasIrrelevantLocation:
285-
return _WindowControlIdOffset.TRANSLATION_WARNING
317+
return _WindowControlIdOffsetFromDataView.TRANSLATION_WARNING
286318
return None
287319

288320
def _get_name(self):
@@ -301,9 +333,9 @@ def reportFocus(self):
301333
tones.beep(440, 50)
302334
return
303335
match self._warningControlToReport:
304-
case _WindowControlIdOffset.OLD_SOURCE_TEXT:
336+
case _WindowControlIdOffsetFromSidebar.OLD_SOURCE_TEXT:
305337
tones.beep(495, 50)
306-
case _WindowControlIdOffset.TRANSLATION_WARNING:
338+
case _WindowControlIdOffsetFromDataView.TRANSLATION_WARNING:
307339
tones.beep(550, 50)
308-
case _WindowControlIdOffset.NEEDS_WORK_SWITCH:
340+
case _WindowControlIdOffsetFromDataView.NEEDS_WORK_SWITCH:
309341
tones.beep(660, 50)

user_docs/en/changes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ There are many minor bug fixes for applications, such as Thunderbird, Adobe Read
9090
* In Firefox and Chromium-based browsers, NVDA will correctly enter focus mode when pressing enter when positioned within a presentational list (ul / ol) inside editable content. (#16325)
9191
* Column state change is now correctly reported when selecting columns to display in Thunderbird message list. (#16323)
9292
* The command line switch `-h`/`--help` works properly again. (#16522, @XLTechie)
93+
* NVDA's support for the Poedit translation software version 3.4 or higher correctly functions when translating languages with 1 or more than 2 plural forms (e.g. Chinese, Polish). (#16318)
9394

9495
### Changes for Developers
9596

0 commit comments

Comments
 (0)