|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 let Ci = Components.interfaces; |
|
6 let Cu = Components.utils; |
|
7 |
|
8 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
9 XPCOMUtils.defineLazyModuleGetter(this, 'Services', |
|
10 'resource://gre/modules/Services.jsm'); |
|
11 XPCOMUtils.defineLazyModuleGetter(this, 'Utils', |
|
12 'resource://gre/modules/accessibility/Utils.jsm'); |
|
13 XPCOMUtils.defineLazyModuleGetter(this, 'Logger', |
|
14 'resource://gre/modules/accessibility/Utils.jsm'); |
|
15 XPCOMUtils.defineLazyModuleGetter(this, 'Roles', |
|
16 'resource://gre/modules/accessibility/Constants.jsm'); |
|
17 XPCOMUtils.defineLazyModuleGetter(this, 'TraversalRules', |
|
18 'resource://gre/modules/accessibility/TraversalRules.jsm'); |
|
19 XPCOMUtils.defineLazyModuleGetter(this, 'Presentation', |
|
20 'resource://gre/modules/accessibility/Presentation.jsm'); |
|
21 |
|
22 this.EXPORTED_SYMBOLS = ['ContentControl']; |
|
23 |
|
24 const MOVEMENT_GRANULARITY_CHARACTER = 1; |
|
25 const MOVEMENT_GRANULARITY_WORD = 2; |
|
26 const MOVEMENT_GRANULARITY_PARAGRAPH = 8; |
|
27 |
|
28 this.ContentControl = function ContentControl(aContentScope) { |
|
29 this._contentScope = Cu.getWeakReference(aContentScope); |
|
30 this._vcCache = new WeakMap(); |
|
31 this._childMessageSenders = new WeakMap(); |
|
32 }; |
|
33 |
|
34 this.ContentControl.prototype = { |
|
35 messagesOfInterest: ['AccessFu:MoveCursor', |
|
36 'AccessFu:ClearCursor', |
|
37 'AccessFu:MoveToPoint', |
|
38 'AccessFu:AutoMove', |
|
39 'AccessFu:Activate', |
|
40 'AccessFu:MoveCaret', |
|
41 'AccessFu:MoveByGranularity'], |
|
42 |
|
43 start: function cc_start() { |
|
44 let cs = this._contentScope.get(); |
|
45 for (let message of this.messagesOfInterest) { |
|
46 cs.addMessageListener(message, this); |
|
47 } |
|
48 }, |
|
49 |
|
50 stop: function cc_stop() { |
|
51 let cs = this._contentScope.get(); |
|
52 for (let message of this.messagesOfInterest) { |
|
53 cs.removeMessageListener(message, this); |
|
54 } |
|
55 }, |
|
56 |
|
57 get document() { |
|
58 return this._contentScope.get().content.document; |
|
59 }, |
|
60 |
|
61 get window() { |
|
62 return this._contentScope.get().content; |
|
63 }, |
|
64 |
|
65 get vc() { |
|
66 return Utils.getVirtualCursor(this.document); |
|
67 }, |
|
68 |
|
69 receiveMessage: function cc_receiveMessage(aMessage) { |
|
70 Logger.debug(() => { |
|
71 return ['ContentControl.receiveMessage', |
|
72 aMessage.name, |
|
73 JSON.stringify(aMessage.json)]; |
|
74 }); |
|
75 |
|
76 try { |
|
77 let func = this['handle' + aMessage.name.slice(9)]; // 'AccessFu:'.length |
|
78 if (func) { |
|
79 func.bind(this)(aMessage); |
|
80 } else { |
|
81 Logger.warning('ContentControl: Unhandled message:', aMessage.name); |
|
82 } |
|
83 } catch (x) { |
|
84 Logger.logException( |
|
85 x, 'Error handling message: ' + JSON.stringify(aMessage.json)); |
|
86 } |
|
87 }, |
|
88 |
|
89 handleMoveCursor: function cc_handleMoveCursor(aMessage) { |
|
90 let origin = aMessage.json.origin; |
|
91 let action = aMessage.json.action; |
|
92 let vc = this.vc; |
|
93 |
|
94 if (origin != 'child' && this.sendToChild(vc, aMessage)) { |
|
95 // Forwarded succesfully to child cursor. |
|
96 return; |
|
97 } |
|
98 |
|
99 let moved = vc[action](TraversalRules[aMessage.json.rule]); |
|
100 |
|
101 if (moved) { |
|
102 if (origin === 'child') { |
|
103 // We just stepped out of a child, clear child cursor. |
|
104 Utils.getMessageManager(aMessage.target).sendAsyncMessage( |
|
105 'AccessFu:ClearCursor', {}); |
|
106 } else { |
|
107 // We potentially landed on a new child cursor. If so, we want to |
|
108 // either be on the first or last item in the child doc. |
|
109 let childAction = action; |
|
110 if (action === 'moveNext') { |
|
111 childAction = 'moveFirst'; |
|
112 } else if (action === 'movePrevious') { |
|
113 childAction = 'moveLast'; |
|
114 } |
|
115 |
|
116 // Attempt to forward move to a potential child cursor in our |
|
117 // new position. |
|
118 this.sendToChild(vc, aMessage, { action: childAction}); |
|
119 } |
|
120 } else if (!this._childMessageSenders.has(aMessage.target)) { |
|
121 // We failed to move, and the message is not from a child, so forward |
|
122 // to parent. |
|
123 this.sendToParent(aMessage); |
|
124 } |
|
125 }, |
|
126 |
|
127 handleMoveToPoint: function cc_handleMoveToPoint(aMessage) { |
|
128 let [x, y] = [aMessage.json.x, aMessage.json.y]; |
|
129 let rule = TraversalRules[aMessage.json.rule]; |
|
130 let vc = this.vc; |
|
131 let win = this.window; |
|
132 |
|
133 let dpr = win.devicePixelRatio; |
|
134 this.vc.moveToPoint(rule, x * dpr, y * dpr, true); |
|
135 |
|
136 let delta = Utils.isContentProcess ? |
|
137 { x: x - win.mozInnerScreenX, y: y - win.mozInnerScreenY } : {}; |
|
138 this.sendToChild(vc, aMessage, delta); |
|
139 }, |
|
140 |
|
141 handleClearCursor: function cc_handleClearCursor(aMessage) { |
|
142 let forwarded = this.sendToChild(this.vc, aMessage); |
|
143 this.vc.position = null; |
|
144 if (!forwarded) { |
|
145 this._contentScope.get().sendAsyncMessage('AccessFu:CursorCleared'); |
|
146 } |
|
147 }, |
|
148 |
|
149 handleAutoMove: function cc_handleAutoMove(aMessage) { |
|
150 this.autoMove(null, aMessage.json); |
|
151 }, |
|
152 |
|
153 handleActivate: function cc_handleActivate(aMessage) { |
|
154 let activateAccessible = (aAccessible) => { |
|
155 Logger.debug(() => { |
|
156 return ['activateAccessible', Logger.accessibleToString(aAccessible)]; |
|
157 }); |
|
158 try { |
|
159 if (aMessage.json.activateIfKey && |
|
160 aAccessible.role != Roles.KEY) { |
|
161 // Only activate keys, don't do anything on other objects. |
|
162 return; |
|
163 } |
|
164 } catch (e) { |
|
165 // accessible is invalid. Silently fail. |
|
166 return; |
|
167 } |
|
168 |
|
169 if (aAccessible.actionCount > 0) { |
|
170 aAccessible.doAction(0); |
|
171 } else { |
|
172 let control = Utils.getEmbeddedControl(aAccessible); |
|
173 if (control && control.actionCount > 0) { |
|
174 control.doAction(0); |
|
175 } |
|
176 |
|
177 // XXX Some mobile widget sets do not expose actions properly |
|
178 // (via ARIA roles, etc.), so we need to generate a click. |
|
179 // Could possibly be made simpler in the future. Maybe core |
|
180 // engine could expose nsCoreUtiles::DispatchMouseEvent()? |
|
181 let docAcc = Utils.AccRetrieval.getAccessibleFor(this.document); |
|
182 let docX = {}, docY = {}, docW = {}, docH = {}; |
|
183 docAcc.getBounds(docX, docY, docW, docH); |
|
184 |
|
185 let objX = {}, objY = {}, objW = {}, objH = {}; |
|
186 aAccessible.getBounds(objX, objY, objW, objH); |
|
187 |
|
188 let x = Math.round((objX.value - docX.value) + objW.value / 2); |
|
189 let y = Math.round((objY.value - docY.value) + objH.value / 2); |
|
190 |
|
191 let node = aAccessible.DOMNode || aAccessible.parent.DOMNode; |
|
192 |
|
193 for (let eventType of ['mousedown', 'mouseup']) { |
|
194 let evt = this.document.createEvent('MouseEvents'); |
|
195 evt.initMouseEvent(eventType, true, true, this.window, |
|
196 x, y, 0, 0, 0, false, false, false, false, 0, null); |
|
197 node.dispatchEvent(evt); |
|
198 } |
|
199 } |
|
200 |
|
201 if (aAccessible.role !== Roles.KEY) { |
|
202 // Keys will typically have a sound of their own. |
|
203 this._contentScope.get().sendAsyncMessage('AccessFu:Present', |
|
204 Presentation.actionInvoked(aAccessible, 'click')); |
|
205 } |
|
206 }; |
|
207 |
|
208 let focusedAcc = Utils.AccRetrieval.getAccessibleFor( |
|
209 this.document.activeElement); |
|
210 if (focusedAcc && focusedAcc.role === Roles.ENTRY) { |
|
211 let accText = focusedAcc.QueryInterface(Ci.nsIAccessibleText); |
|
212 let oldOffset = accText.caretOffset; |
|
213 let newOffset = aMessage.json.offset; |
|
214 let text = accText.getText(0, accText.characterCount); |
|
215 |
|
216 if (newOffset >= 0 && newOffset <= accText.characterCount) { |
|
217 accText.caretOffset = newOffset; |
|
218 } |
|
219 |
|
220 this.presentCaretChange(text, oldOffset, accText.caretOffset); |
|
221 return; |
|
222 } |
|
223 |
|
224 let vc = this.vc; |
|
225 if (!this.sendToChild(vc, aMessage)) { |
|
226 activateAccessible(vc.position); |
|
227 } |
|
228 }, |
|
229 |
|
230 handleMoveByGranularity: function cc_handleMoveByGranularity(aMessage) { |
|
231 // XXX: Add sendToChild. Right now this is only used in Android, so no need. |
|
232 let direction = aMessage.json.direction; |
|
233 let granularity; |
|
234 |
|
235 switch(aMessage.json.granularity) { |
|
236 case MOVEMENT_GRANULARITY_CHARACTER: |
|
237 granularity = Ci.nsIAccessiblePivot.CHAR_BOUNDARY; |
|
238 break; |
|
239 case MOVEMENT_GRANULARITY_WORD: |
|
240 granularity = Ci.nsIAccessiblePivot.WORD_BOUNDARY; |
|
241 break; |
|
242 default: |
|
243 return; |
|
244 } |
|
245 |
|
246 if (direction === 'Previous') { |
|
247 this.vc.movePreviousByText(granularity); |
|
248 } else if (direction === 'Next') { |
|
249 this.vc.moveNextByText(granularity); |
|
250 } |
|
251 }, |
|
252 |
|
253 presentCaretChange: function cc_presentCaretChange( |
|
254 aText, aOldOffset, aNewOffset) { |
|
255 if (aOldOffset !== aNewOffset) { |
|
256 let msg = Presentation.textSelectionChanged(aText, aNewOffset, aNewOffset, |
|
257 aOldOffset, aOldOffset, true); |
|
258 this._contentScope.get().sendAsyncMessage('AccessFu:Present', msg); |
|
259 } |
|
260 }, |
|
261 |
|
262 handleMoveCaret: function cc_handleMoveCaret(aMessage) { |
|
263 let direction = aMessage.json.direction; |
|
264 let granularity = aMessage.json.granularity; |
|
265 let accessible = this.vc.position; |
|
266 let accText = accessible.QueryInterface(Ci.nsIAccessibleText); |
|
267 let oldOffset = accText.caretOffset; |
|
268 let text = accText.getText(0, accText.characterCount); |
|
269 |
|
270 let start = {}, end = {}; |
|
271 if (direction === 'Previous' && !aMessage.json.atStart) { |
|
272 switch (granularity) { |
|
273 case MOVEMENT_GRANULARITY_CHARACTER: |
|
274 accText.caretOffset--; |
|
275 break; |
|
276 case MOVEMENT_GRANULARITY_WORD: |
|
277 accText.getTextBeforeOffset(accText.caretOffset, |
|
278 Ci.nsIAccessibleText.BOUNDARY_WORD_START, start, end); |
|
279 accText.caretOffset = end.value === accText.caretOffset ? |
|
280 start.value : end.value; |
|
281 break; |
|
282 case MOVEMENT_GRANULARITY_PARAGRAPH: |
|
283 let startOfParagraph = text.lastIndexOf('\n', accText.caretOffset - 1); |
|
284 accText.caretOffset = startOfParagraph !== -1 ? startOfParagraph : 0; |
|
285 break; |
|
286 } |
|
287 } else if (direction === 'Next' && !aMessage.json.atEnd) { |
|
288 switch (granularity) { |
|
289 case MOVEMENT_GRANULARITY_CHARACTER: |
|
290 accText.caretOffset++; |
|
291 break; |
|
292 case MOVEMENT_GRANULARITY_WORD: |
|
293 accText.getTextAtOffset(accText.caretOffset, |
|
294 Ci.nsIAccessibleText.BOUNDARY_WORD_END, start, end); |
|
295 accText.caretOffset = end.value; |
|
296 break; |
|
297 case MOVEMENT_GRANULARITY_PARAGRAPH: |
|
298 accText.caretOffset = text.indexOf('\n', accText.caretOffset + 1); |
|
299 break; |
|
300 } |
|
301 } |
|
302 |
|
303 this.presentCaretChange(text, oldOffset, accText.caretOffset); |
|
304 }, |
|
305 |
|
306 getChildCursor: function cc_getChildCursor(aAccessible) { |
|
307 let acc = aAccessible || this.vc.position; |
|
308 if (Utils.isAliveAndVisible(acc) && acc.role === Roles.INTERNAL_FRAME) { |
|
309 let domNode = acc.DOMNode; |
|
310 let mm = this._childMessageSenders.get(domNode, null); |
|
311 if (!mm) { |
|
312 mm = Utils.getMessageManager(domNode); |
|
313 mm.addWeakMessageListener('AccessFu:MoveCursor', this); |
|
314 this._childMessageSenders.set(domNode, mm); |
|
315 } |
|
316 |
|
317 return mm; |
|
318 } |
|
319 |
|
320 return null; |
|
321 }, |
|
322 |
|
323 sendToChild: function cc_sendToChild(aVirtualCursor, aMessage, aReplacer) { |
|
324 let mm = this.getChildCursor(aVirtualCursor.position); |
|
325 if (!mm) { |
|
326 return false; |
|
327 } |
|
328 |
|
329 // XXX: This is a silly way to make a deep copy |
|
330 let newJSON = JSON.parse(JSON.stringify(aMessage.json)); |
|
331 newJSON.origin = 'parent'; |
|
332 for (let attr in aReplacer) { |
|
333 newJSON[attr] = aReplacer[attr]; |
|
334 } |
|
335 |
|
336 mm.sendAsyncMessage(aMessage.name, newJSON); |
|
337 return true; |
|
338 }, |
|
339 |
|
340 sendToParent: function cc_sendToParent(aMessage) { |
|
341 // XXX: This is a silly way to make a deep copy |
|
342 let newJSON = JSON.parse(JSON.stringify(aMessage.json)); |
|
343 newJSON.origin = 'child'; |
|
344 aMessage.target.sendAsyncMessage(aMessage.name, newJSON); |
|
345 }, |
|
346 |
|
347 /** |
|
348 * Move cursor and/or present its location. |
|
349 * aOptions could have any of these fields: |
|
350 * - delay: in ms, before actual move is performed. Another autoMove call |
|
351 * would cancel it. Useful if we want to wait for a possible trailing |
|
352 * focus move. Default 0. |
|
353 * - noOpIfOnScreen: if accessible is alive and visible, don't do anything. |
|
354 * - forcePresent: present cursor location, whether we move or don't. |
|
355 * - moveToFocused: if there is a focused accessible move to that. This takes |
|
356 * precedence over given anchor. |
|
357 * - moveMethod: pivot move method to use, default is 'moveNext', |
|
358 */ |
|
359 autoMove: function cc_autoMove(aAnchor, aOptions = {}) { |
|
360 let win = this.window; |
|
361 win.clearTimeout(this._autoMove); |
|
362 |
|
363 let moveFunc = () => { |
|
364 let vc = this.vc; |
|
365 let acc = aAnchor; |
|
366 let rule = aOptions.onScreenOnly ? |
|
367 TraversalRules.SimpleOnScreen : TraversalRules.Simple; |
|
368 let forcePresentFunc = () => { |
|
369 if (aOptions.forcePresent) { |
|
370 this._contentScope.get().sendAsyncMessage( |
|
371 'AccessFu:Present', Presentation.pivotChanged( |
|
372 vc.position, null, Ci.nsIAccessiblePivot.REASON_NONE, |
|
373 vc.startOffset, vc.endOffset)); |
|
374 } |
|
375 }; |
|
376 |
|
377 if (aOptions.noOpIfOnScreen && |
|
378 Utils.isAliveAndVisible(vc.position, true)) { |
|
379 forcePresentFunc(); |
|
380 return; |
|
381 } |
|
382 |
|
383 if (aOptions.moveToFocused) { |
|
384 acc = Utils.AccRetrieval.getAccessibleFor( |
|
385 this.document.activeElement) || acc; |
|
386 } |
|
387 |
|
388 let moved = false; |
|
389 let moveMethod = aOptions.moveMethod || 'moveNext'; // default is moveNext |
|
390 let moveFirstOrLast = moveMethod in ['moveFirst', 'moveLast']; |
|
391 if (!moveFirstOrLast || acc) { |
|
392 // We either need next/previous or there is an anchor we need to use. |
|
393 moved = vc[moveFirstOrLast ? 'moveNext' : moveMethod](rule, acc, true); |
|
394 } |
|
395 if (moveFirstOrLast && !moved) { |
|
396 // We move to first/last after no anchor move happened or succeeded. |
|
397 moved = vc[moveMethod](rule); |
|
398 } |
|
399 |
|
400 let sentToChild = this.sendToChild(vc, { |
|
401 name: 'AccessFu:AutoMove', |
|
402 json: aOptions |
|
403 }); |
|
404 |
|
405 if (!moved && !sentToChild) { |
|
406 forcePresentFunc(); |
|
407 } |
|
408 }; |
|
409 |
|
410 if (aOptions.delay) { |
|
411 this._autoMove = win.setTimeout(moveFunc, aOptions.delay); |
|
412 } else { |
|
413 moveFunc(); |
|
414 } |
|
415 }, |
|
416 |
|
417 QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, |
|
418 Ci.nsIMessageListener |
|
419 ]) |
|
420 }; |