forked from nvaccess/nvda
/
skype.py
264 lines (228 loc) · 9.47 KB
/
skype.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# -*- coding: UTF-8 -*-
#appModules/skype.py
#A part of NonVisual Desktop Access (NVDA)
#Copyright (C) 2007-2015 Peter Vágner, NV Access Limited
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.
import re
from comtypes import COMError
import wx
import appModuleHandler
import controlTypes
import winUser
import NVDAObjects.IAccessible
import oleacc
import ui
import windowUtils
import displayModel
import queueHandler
import config
import NVDAObjects.behaviors
import api
from logHandler import log
# Translators: The name of the NVDA command category for Skype specific commands.
SCRCAT_SKYPE = _("Skype")
TYPING_INDICATOR_MATCH = {
("TTypingIndicatorPanel", controlTypes.ROLE_STATICTEXT),
("TWidgetControl", controlTypes.ROLE_LISTITEM), # Skype <= 7.2
}
class Conversation(NVDAObjects.IAccessible.IAccessible):
scriptCategory = SCRCAT_SKYPE
def initOverlayClass(self):
for n in xrange(0, 10):
self.bindGesture("kb:NVDA+control+%d" % n, "reviewRecentMessage")
def _isEqual(self, other):
# Sometimes, we get this object as an unproxied IAccessible,
# which means the location is different, so IAccessible._isEqual return False.
# This can cause us to get a gainFocus and a focusEntered on two different instances.
# We don't care about the location here.
return self.windowHandle == other.windowHandle
def _gainedFocus(self):
# The user has entered this Skype conversation.
if self.appModule.conversation:
# Another conversation was previously focused. Clean it up.
self.appModule.conversation.lostFocus()
self.appModule.conversation = self
try:
self.outputList = NVDAObjects.IAccessible.getNVDAObjectFromEvent(
windowUtils.findDescendantWindow(self.windowHandle, className="TChatContentControl"),
winUser.OBJID_CLIENT, 0).lastChild
except LookupError:
log.debugWarning("Couldn't find output list")
self.outputList = None
else:
self.outputList.startMonitoring()
for wClass, role in TYPING_INDICATOR_MATCH:
try:
self.typingIndicator = NVDAObjects.IAccessible.getNVDAObjectFromEvent(
windowUtils.findDescendantWindow(self.windowHandle, className=wClass),
winUser.OBJID_CLIENT, 1)
except LookupError:
continue
self.typingIndicator.startMonitoring()
break
else:
log.debugWarning("Couldn't find typing indicator")
self.typingIndicator = None
def event_focusEntered(self):
self._gainedFocus()
super(Conversation, self).event_focusEntered()
def event_gainFocus(self):
# A conversation might have its own top level window,
# but foreground changes often trigger gainFocus instead of focusEntered.
self._gainedFocus()
super(Conversation, self).event_gainFocus()
def lostFocus(self):
self.appModule.conversation = None
if self.outputList:
self.outputList.stopMonitoring()
self.outputList = None
if self.typingIndicator:
self.typingIndicator.stopMonitoring()
self.typingIndicator = None
def script_reviewRecentMessage(self, gesture):
try:
index = int(gesture.mainKeyName[-1])
except (AttributeError, ValueError):
return
if index == 0:
index = 10
self.outputList.reviewRecentMessage(index)
# Describes the NVDA command to review messages in Skype.
script_reviewRecentMessage.__doc__ = _("Reports and moves the review cursor to a recent message")
script_reviewRecentMessage.canPropagate = True
class ChatOutputList(NVDAObjects.IAccessible.IAccessible):
def startMonitoring(self):
self.oldMessageCount = None
self.update(initial=True)
displayModel.requestTextChangeNotifications(self, True)
def stopMonitoring(self):
displayModel.requestTextChangeNotifications(self, False)
RE_MESSAGE = re.compile(r"^From (?P<from>.*), (?P<body>.*), sent on (?P<time>.*?)(?: Edited by .* at .*?)?(?: Not delivered|New)?$")
def reportMessage(self, text):
# Messages are ridiculously verbose.
# Strip the time and other metadata if possible.
m = self.RE_MESSAGE.match(text)
if m:
text = "%s, %s" % (m.group("from"), m.group("body"))
ui.message(text)
def _getMessageCount(self):
ia = self.IAccessibleObject
for c in xrange(self.childCount, -1, -1):
try:
if ia.accRole(c) != oleacc.ROLE_SYSTEM_LISTITEM or ia.accState(c) & oleacc.STATE_SYSTEM_UNAVAILABLE:
# Not a message.
continue
except COMError:
# The child probably disappeared after we fetched childCount.
continue
return c
return 0
def update(self, initial=False):
newCount = self._getMessageCount()
if (not initial and config.conf["presentation"]["reportDynamicContentChanges"]
#4644: Don't report a flood of messages.
and newCount - self.oldMessageCount < 5):
ia = self.IAccessibleObject
for c in xrange(self.oldMessageCount + 1, newCount + 1):
text = ia.accName(c)
if not text:
continue
self.reportMessage(text)
self.oldMessageCount = newCount
def event_textChange(self):
# This event is called from another thread, but this needs to run in the main thread.
queueHandler.queueFunction(queueHandler.eventQueue, self.update)
def reviewRecentMessage(self, index):
count = self._getMessageCount()
if index > count:
# Translators: This is presented to inform the user that no instant message has been received.
ui.message(_("No message yet"))
return
message = self.getChild(count - index)
# Reviewing a message should not auto tether
api.setNavigatorObject(message, isFocus=True)
self.reportMessage(message.name)
class Notification(NVDAObjects.behaviors.Notification):
role = controlTypes.ROLE_ALERT
_lastWindow = None
_lastChildCount = None
def _get_name(self):
startIndex = 0
if self.event_objectID is not None:
# This is for an event.
if self.windowHandle == self._lastWindow:
# Another notification is being added to an already visible window.
# Just report the added notification.
startIndex = self._lastChildCount
return " ".join(child.name for child in self.children[startIndex:])
def event_alert(self):
if self.name:
# There is new content.
super(Notification, self).event_alert()
Notification._lastWindow = self.windowHandle
Notification._lastChildCount = self.childCount
# #5405: Some notifications (e.g. if you click once on the System Tray icon) only fire a show event.
# These are ready as soon as the event is fired.
event_show = event_alert
# #5405: Most notifications fire show, but aren't ready at this point.
# They then fire reorder when they're ready.
# #4841: They also fire reorder if another notification is later added to the same window.
event_reorder = event_alert
class TypingIndicator(NVDAObjects.IAccessible.IAccessible):
def initOverlayClass(self):
self._oldName = None
def startMonitoring(self):
displayModel.requestTextChangeNotifications(self, True)
def stopMonitoring(self):
displayModel.requestTextChangeNotifications(self, False)
def _maybeReport(self):
name = self.name
if name == self._oldName:
# There was no real change; just a redraw.
return
self._oldName = name
if name:
ui.message(name)
else:
# Translators: Indicates that a contact stopped typing.
ui.message(_("Typing stopped"))
def event_textChange(self):
# This event is called from another thread, but this needs to run in the main thread.
queueHandler.queueFunction(queueHandler.eventQueue, self._maybeReport)
class AppModule(appModuleHandler.AppModule):
def __init__(self, *args, **kwargs):
super(AppModule, self).__init__(*args, **kwargs)
self.conversation = None
def event_NVDAObject_init(self,obj):
if isinstance(obj, NVDAObjects.IAccessible.IAccessible) and obj.event_objectID is None and controlTypes.STATE_FOCUSED in obj.states and obj.role not in (controlTypes.ROLE_POPUPMENU,controlTypes.ROLE_MENUITEM,controlTypes.ROLE_MENUBAR):
# The window handle reported by Skype accessibles is sometimes incorrect.
# This object is focused, so we can override with the focus window.
obj.windowHandle=winUser.getGUIThreadInfo(None).hwndFocus
obj.windowClassName=winUser.getClassName(obj.windowHandle)
if obj.value and obj.windowClassName in ("TMainUserList", "TConversationList", "TInboxList", "TActiveConversationList", "TConversationsControl"):
# The name and value both include the user's name, so kill the value to avoid doubling up.
# The value includes the Skype name,
# but we care more about the additional info (e.g. new event count) included in the name.
obj.value=None
elif isinstance(obj, NVDAObjects.IAccessible.IAccessible) and obj.IAccessibleRole == oleacc.ROLE_SYSTEM_PANE and not obj.name:
# Prevent extraneous reporting of pane when tabbing through a conversation form.
obj.shouldAllowIAccessibleFocusEvent = False
def chooseNVDAObjectOverlayClasses(self, obj, clsList):
wClass = obj.windowClassName
role = obj.role
if isinstance(obj, NVDAObjects.IAccessible.IAccessible) and obj.windowClassName == "TConversationForm" and obj.IAccessibleRole == oleacc.ROLE_SYSTEM_CLIENT:
clsList.insert(0, Conversation)
elif wClass == "TChatContentControl" and role == controlTypes.ROLE_LIST:
clsList.insert(0, ChatOutputList)
elif isinstance(obj, NVDAObjects.IAccessible.IAccessible) and wClass == "TTrayAlert" and obj.IAccessibleChildID == 0:
clsList.insert(0, Notification)
elif (wClass, role) in TYPING_INDICATOR_MATCH:
clsList.insert(0, TypingIndicator)
def event_gainFocus(self, obj, nextHandler):
if self.conversation and not winUser.isDescendantWindow(self.conversation.windowHandle, obj.windowHandle):
self.conversation.lostFocus()
nextHandler()
def event_appModule_loseFocus(self):
if self.conversation:
self.conversation.lostFocus()