accessible/src/jsat/ContentControl.jsm

branch
TOR_BUG_9701
changeset 3
141e0f1194b1
equal deleted inserted replaced
-1:000000000000 0:5cec36d78ce1
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 };

mercurial