Skip to content

Commit 9b26ce5

Browse files
authored
Merge 3babce3 into 64b6074
2 parents 64b6074 + 3babce3 commit 9b26ce5

2 files changed

Lines changed: 238 additions & 154 deletions

File tree

source/appModules/poedit.py

Lines changed: 180 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,210 @@
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-2013 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
1011
import api
1112
import appModuleHandler
1213
import controlTypes
13-
import displayModel
14-
import textInfos
14+
from scriptHandler import script, getLastScriptRepeatCount
1515
import tones
1616
import ui
17-
from NVDAObjects.IAccessible import sysListView32
17+
from NVDAObjects import NVDAObject
1818
import windowUtils
1919
import NVDAObjects.IAccessible
2020
import winUser
2121

2222

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}
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 the first sub window of the main foreground as a reference,
31+
we can safely calculate control ids accross releases.
32+
This class contains window control id offsets relative to the first sub window of the main foreground window.
33+
"""
34+
OLD_SOURCE_TEXT = 77
35+
TRANSLATOR_NOTES = 80
36+
COMMENT = 83
37+
TRANSLATION_WARNING = 28
38+
39+
40+
def getObjectWithControlId(parentWindowHandle: int, controlId: int) -> Optional[NVDAObject]:
41+
"""
42+
Finds a window with the given controlId, starting from the window belonging to the given parentWindowHandle.
2743
"""
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
44+
try:
45+
obj = NVDAObjects.IAccessible.getNVDAObjectFromEvent(
46+
windowUtils.findDescendantWindow(parentWindowHandle, controlID=controlId),
47+
winUser.OBJID_CLIENT, 0
48+
)
49+
except LookupError:
50+
obj = None
51+
return obj
4252

4353

4454
class AppModule(appModuleHandler.AppModule):
4555

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
56+
def _getNVDAObjectForWindowControlIdOffset(self, windowControlIdOffset: WindowControlIdOffset):
57+
fg = api.getForegroundObject()
58+
return getObjectWithControlId(
59+
fg.windowHandle,
60+
fg.firstChild.windowControlID + windowControlIdOffset
61+
)
62+
63+
def _reportControlScriptHelper(self, windowControlIdOffset: WindowControlIdOffset, description: str):
64+
obj = self._getNVDAObjectForWindowControlIdOffset(windowControlIdOffset)
5565
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."))
66+
if not obj.hasIrrelevantLocation and not obj.parent.parent.hasIrrelevantLocation:
67+
message = obj.name.replace(LEFT_TO_RIGHT_EMBEDDING, '')
68+
repeats = getLastScriptRepeatCount()
69+
if repeats == 0:
70+
ui.message(message)
71+
else:
72+
ui.browseableMessage(message, description.title())
73+
else:
74+
ui.message(
75+
# Translators: this message is reported when there is nothing
76+
# to be presented to the user in Poedit.
77+
# {description} is replaced by the description of the window to be reported, e.g. translator notes
78+
pgettext("poedit", "No {description}").format(description=description)
79+
)
6280
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:
7581
# 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-
}
82+
# a requested window in Poedit.
83+
# {description} is replaced by the description of the window to be reported, e.g. translator notes
84+
ui.message(pgettext("poedit", "Could not find {description} window.").format(description=description))
85+
86+
@script(
87+
description=pgettext(
88+
"poedit",
89+
# Translators: The description of an NVDA command for Poedit.
90+
"Reports any notes for translators. If pressed twice, presents the notes in browse mode"
91+
),
92+
gesture="kb:control+shift+a",
93+
)
94+
def script_reportTranslatorNotes(self, gesture):
95+
self._reportControlScriptHelper(
96+
WindowControlIdOffset.TRANSLATOR_NOTES,
97+
# Translators: The description of the "Translator notes" window in poedit.
98+
# This text is reported when the given window contains no item to report or could not be found.
99+
pgettext("poedit", "notes for translators")
100+
)
101+
102+
@script(
103+
description=pgettext(
104+
"poedit",
105+
# Translators: The description of an NVDA command for Poedit.
106+
"Reports any comment in the comments window. If pressed twice, presents the comment in browse mode"
107+
),
108+
gesture="kb:control+shift+c",
109+
)
110+
def script_reportComment(self, gesture):
111+
self._reportControlScriptHelper(
112+
WindowControlIdOffset.COMMENT,
113+
# Translators: The description of the "comment" window in poedit.
114+
# This text is reported when the given window contains no item to report or could not be found.
115+
pgettext("poedit", "comment")
116+
)
117+
118+
@script(
119+
description=pgettext(
120+
"poedit",
121+
# Translators: The description of an NVDA command for Poedit.
122+
"Reports the old source text, if any. If pressed twice, presents the text in browse mode"
123+
),
124+
gesture="kb:control+shift+o",
125+
)
126+
def script_reportOldSourceText(self, gesture):
127+
self._reportControlScriptHelper(
128+
WindowControlIdOffset.OLD_SOURCE_TEXT,
129+
# Translators: The description of the "old source text" window in poedit.
130+
# This text is reported when the given window contains no item to report or could not be found.
131+
pgettext("poedit", "old source text")
132+
)
133+
134+
@script(
135+
description=pgettext(
136+
"poedit",
137+
# Translators: The description of an NVDA command for Poedit.
138+
"Reports a translation warning, if any. If pressed twice, presents the warning in browse mode"
139+
),
140+
gesture="kb:control+shift+w",
141+
)
142+
def script_reportTranslationWarning(self, gesture):
143+
self._reportControlScriptHelper(
144+
WindowControlIdOffset.TRANSLATION_WARNING,
145+
# Translators: The description of the "translation warning" window in poedit.
146+
# This text is reported when the given window contains no item to report or could not be found.
147+
pgettext("poedit", "translation warning")
148+
)
93149

94150
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
151+
if obj.role == controlTypes.Role.LISTITEM and obj.windowClassName == "wxWindowNR":
152+
clsList.insert(0, PoeditListItem)
153+
elif (
154+
obj.role in (controlTypes.Role.EDITABLETEXT, controlTypes.Role.DOCUMENT)
155+
and obj.windowClassName == "RICHEDIT50W"
156+
):
157+
clsList.insert(0, PoeditRichEdit)
158+
108159

109-
class PoeditListItem(sysListView32.ListItem):
160+
class PoeditRichEdit(NVDAObject):
110161

111-
def _get_isBold(self):
112-
info=displayModel.DisplayModelTextInfo(self,position=textInfos.POSITION_FIRST)
113-
info.expand(textInfos.UNIT_CHARACTER)
114-
fields=info.getTextWithFields()
162+
def _get_name(self,) -> str:
163+
# These rich edit controls are incorrectly labeled.
164+
# Oleacc doesn't return any name, and UIA defaults to RichEdit Control.
165+
# The label object is positioned just above the field on the screen.
166+
l, t, w, h = self.location
115167
try:
116-
return fields[0].field['bold']
117-
except:
118-
return False
168+
self.name = NVDAObjects.NVDAObject.objectFromPoint(l + 10, t - 10).name
169+
except AttributeError:
170+
return super().name
171+
return self.name
172+
173+
174+
class PoeditListItem(NVDAObject):
175+
176+
_warningControlToReport: Optional[WindowControlIdOffset]
177+
178+
def _get__warningControlToReport(self) -> Optional[WindowControlIdOffset]:
179+
obj = self.appModule._getNVDAObjectForWindowControlIdOffset(WindowControlIdOffset.OLD_SOURCE_TEXT)
180+
if obj and not obj.hasIrrelevantLocation:
181+
return WindowControlIdOffset.OLD_SOURCE_TEXT
182+
obj = self.appModule._getNVDAObjectForWindowControlIdOffset(WindowControlIdOffset.TRANSLATION_WARNING)
183+
if (
184+
obj
185+
and obj.parent and obj.parent.parent
186+
and not obj.parent.parent.hasIrrelevantLocation
187+
):
188+
return WindowControlIdOffset.TRANSLATION_WARNING
189+
return None
119190

120191
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:
192+
name = super().name
193+
if self._warningControlToReport or not self.description:
194+
# This translation has a warning.
195+
# Prepend an asterix (*) to the name
196+
name = f"* {name}"
197+
self.name = name
198+
return self.name
199+
200+
def reportFocus(self):
201+
super().reportFocus()
202+
if not self.description:
203+
# This item is untranslated
204+
tones.beep(440, 50)
205+
return
206+
warning = self._warningControlToReport
207+
if warning is WindowControlIdOffset.OLD_SOURCE_TEXT:
130208
tones.beep(550, 50)
209+
elif warning is WindowControlIdOffset.TRANSLATION_WARNING:
210+
tones.beep(660, 50)

0 commit comments

Comments
 (0)