accessible/src/jsat/Presentation.jsm

branch
TOR_BUG_9701
changeset 3
141e0f1194b1
equal deleted inserted replaced
-1:000000000000 0:d890ebd8cd84
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 'use strict';
6
7 const Cc = Components.classes;
8 const Ci = Components.interfaces;
9 const Cu = Components.utils;
10 const Cr = Components.results;
11
12 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
13 XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
14 'resource://gre/modules/accessibility/Utils.jsm');
15 XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
16 'resource://gre/modules/accessibility/Utils.jsm');
17 XPCOMUtils.defineLazyModuleGetter(this, 'PivotContext',
18 'resource://gre/modules/accessibility/Utils.jsm');
19 XPCOMUtils.defineLazyModuleGetter(this, 'UtteranceGenerator',
20 'resource://gre/modules/accessibility/OutputGenerator.jsm');
21 XPCOMUtils.defineLazyModuleGetter(this, 'BrailleGenerator',
22 'resource://gre/modules/accessibility/OutputGenerator.jsm');
23 XPCOMUtils.defineLazyModuleGetter(this, 'Roles',
24 'resource://gre/modules/accessibility/Constants.jsm');
25 XPCOMUtils.defineLazyModuleGetter(this, 'States',
26 'resource://gre/modules/accessibility/Constants.jsm');
27
28 this.EXPORTED_SYMBOLS = ['Presentation'];
29
30 /**
31 * The interface for all presenter classes. A presenter could be, for example,
32 * a speech output module, or a visual cursor indicator.
33 */
34 function Presenter() {}
35
36 Presenter.prototype = {
37 /**
38 * The type of presenter. Used for matching it with the appropriate output method.
39 */
40 type: 'Base',
41
42 /**
43 * The virtual cursor's position changed.
44 * @param {PivotContext} aContext the context object for the new pivot
45 * position.
46 * @param {int} aReason the reason for the pivot change.
47 * See nsIAccessiblePivot.
48 */
49 pivotChanged: function pivotChanged(aContext, aReason) {},
50
51 /**
52 * An object's action has been invoked.
53 * @param {nsIAccessible} aObject the object that has been invoked.
54 * @param {string} aActionName the name of the action.
55 */
56 actionInvoked: function actionInvoked(aObject, aActionName) {},
57
58 /**
59 * Text has changed, either by the user or by the system. TODO.
60 */
61 textChanged: function textChanged(aIsInserted, aStartOffset,
62 aLength, aText,
63 aModifiedText) {},
64
65 /**
66 * Text selection has changed. TODO.
67 */
68 textSelectionChanged: function textSelectionChanged(aText, aStart, aEnd, aOldStart, aOldEnd, aIsFromUser) {},
69
70 /**
71 * Selection has changed. TODO.
72 * @param {nsIAccessible} aObject the object that has been selected.
73 */
74 selectionChanged: function selectionChanged(aObject) {},
75
76 /**
77 * Value has changed.
78 * @param {nsIAccessible} aAccessible the object whose value has changed.
79 */
80 valueChanged: function valueChanged(aAccessible) {},
81
82 /**
83 * The tab, or the tab's document state has changed.
84 * @param {nsIAccessible} aDocObj the tab document accessible that has had its
85 * state changed, or null if the tab has no associated document yet.
86 * @param {string} aPageState the state name for the tab, valid states are:
87 * 'newtab', 'loading', 'newdoc', 'loaded', 'stopped', and 'reload'.
88 */
89 tabStateChanged: function tabStateChanged(aDocObj, aPageState) {},
90
91 /**
92 * The current tab has changed.
93 * @param {PivotContext} aDocContext context object for tab's
94 * document.
95 * @param {PivotContext} aVCContext context object for tab's current
96 * virtual cursor position.
97 */
98 tabSelected: function tabSelected(aDocContext, aVCContext) {},
99
100 /**
101 * The viewport has changed, either a scroll, pan, zoom, or
102 * landscape/portrait toggle.
103 * @param {Window} aWindow window of viewport that changed.
104 */
105 viewportChanged: function viewportChanged(aWindow) {},
106
107 /**
108 * We have entered or left text editing mode.
109 */
110 editingModeChanged: function editingModeChanged(aIsEditing) {},
111
112 /**
113 * Announce something. Typically an app state change.
114 */
115 announce: function announce(aAnnouncement) {},
116
117
118
119 /**
120 * Announce a live region.
121 * @param {PivotContext} aContext context object for an accessible.
122 * @param {boolean} aIsPolite A politeness level for a live region.
123 * @param {boolean} aIsHide An indicator of hide/remove event.
124 * @param {string} aModifiedText Optional modified text.
125 */
126 liveRegion: function liveRegionShown(aContext, aIsPolite, aIsHide,
127 aModifiedText) {}
128 };
129
130 /**
131 * Visual presenter. Draws a box around the virtual cursor's position.
132 */
133
134 this.VisualPresenter = function VisualPresenter() {
135 this._displayedAccessibles = new WeakMap();
136 };
137
138 VisualPresenter.prototype = {
139 __proto__: Presenter.prototype,
140
141 type: 'Visual',
142
143 /**
144 * The padding in pixels between the object and the highlight border.
145 */
146 BORDER_PADDING: 2,
147
148 viewportChanged: function VisualPresenter_viewportChanged(aWindow) {
149 let currentDisplay = this._displayedAccessibles.get(aWindow);
150 if (!currentDisplay) {
151 return null;
152 }
153
154 let currentAcc = currentDisplay.accessible;
155 let start = currentDisplay.startOffset;
156 let end = currentDisplay.endOffset;
157 if (Utils.isAliveAndVisible(currentAcc)) {
158 let bounds = (start === -1 && end === -1) ? Utils.getBounds(currentAcc) :
159 Utils.getTextBounds(currentAcc, start, end);
160
161 return {
162 type: this.type,
163 details: {
164 method: 'showBounds',
165 bounds: bounds,
166 padding: this.BORDER_PADDING
167 }
168 };
169 }
170
171 return null;
172 },
173
174 pivotChanged: function VisualPresenter_pivotChanged(aContext, aReason) {
175 if (!aContext.accessible) {
176 // XXX: Don't hide because another vc may be using the highlight.
177 return null;
178 }
179
180 this._displayedAccessibles.set(aContext.accessible.document.window,
181 { accessible: aContext.accessibleForBounds,
182 startOffset: aContext.startOffset,
183 endOffset: aContext.endOffset });
184
185 try {
186 aContext.accessibleForBounds.scrollTo(
187 Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);
188
189 let bounds = (aContext.startOffset === -1 && aContext.endOffset === -1) ?
190 aContext.bounds : Utils.getTextBounds(aContext.accessibleForBounds,
191 aContext.startOffset,
192 aContext.endOffset);
193
194 return {
195 type: this.type,
196 details: {
197 method: 'showBounds',
198 bounds: bounds,
199 padding: this.BORDER_PADDING
200 }
201 };
202 } catch (e) {
203 Logger.logException(e, 'Failed to get bounds');
204 return null;
205 }
206 },
207
208 tabSelected: function VisualPresenter_tabSelected(aDocContext, aVCContext) {
209 return this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE);
210 },
211
212 tabStateChanged: function VisualPresenter_tabStateChanged(aDocObj,
213 aPageState) {
214 if (aPageState == 'newdoc')
215 return {type: this.type, details: {method: 'hideBounds'}};
216
217 return null;
218 },
219
220 announce: function VisualPresenter_announce(aAnnouncement) {
221 return {
222 type: this.type,
223 details: {
224 method: 'showAnnouncement',
225 text: aAnnouncement,
226 duration: 1000
227 }
228 };
229 }
230 };
231
232 /**
233 * Android presenter. Fires Android a11y events.
234 */
235
236 this.AndroidPresenter = function AndroidPresenter() {};
237
238 AndroidPresenter.prototype = {
239 __proto__: Presenter.prototype,
240
241 type: 'Android',
242
243 // Android AccessibilityEvent type constants.
244 ANDROID_VIEW_CLICKED: 0x01,
245 ANDROID_VIEW_LONG_CLICKED: 0x02,
246 ANDROID_VIEW_SELECTED: 0x04,
247 ANDROID_VIEW_FOCUSED: 0x08,
248 ANDROID_VIEW_TEXT_CHANGED: 0x10,
249 ANDROID_WINDOW_STATE_CHANGED: 0x20,
250 ANDROID_VIEW_HOVER_ENTER: 0x80,
251 ANDROID_VIEW_HOVER_EXIT: 0x100,
252 ANDROID_VIEW_SCROLLED: 0x1000,
253 ANDROID_VIEW_TEXT_SELECTION_CHANGED: 0x2000,
254 ANDROID_ANNOUNCEMENT: 0x4000,
255 ANDROID_VIEW_ACCESSIBILITY_FOCUSED: 0x8000,
256 ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY: 0x20000,
257
258 pivotChanged: function AndroidPresenter_pivotChanged(aContext, aReason) {
259 if (!aContext.accessible)
260 return null;
261
262 let androidEvents = [];
263
264 let isExploreByTouch = (aReason == Ci.nsIAccessiblePivot.REASON_POINT &&
265 Utils.AndroidSdkVersion >= 14);
266 let focusEventType = (Utils.AndroidSdkVersion >= 16) ?
267 this.ANDROID_VIEW_ACCESSIBILITY_FOCUSED :
268 this.ANDROID_VIEW_FOCUSED;
269
270 if (isExploreByTouch) {
271 // This isn't really used by TalkBack so this is a half-hearted attempt
272 // for now.
273 androidEvents.push({eventType: this.ANDROID_VIEW_HOVER_EXIT, text: []});
274 }
275
276 let brailleOutput = {};
277 if (Utils.AndroidSdkVersion >= 16) {
278 if (!this._braillePresenter) {
279 this._braillePresenter = new BraillePresenter();
280 }
281 brailleOutput = this._braillePresenter.pivotChanged(aContext, aReason).
282 details;
283 }
284
285 if (aReason === Ci.nsIAccessiblePivot.REASON_TEXT) {
286 if (Utils.AndroidSdkVersion >= 16) {
287 let adjustedText = aContext.textAndAdjustedOffsets;
288
289 androidEvents.push({
290 eventType: this.ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
291 text: [adjustedText.text],
292 fromIndex: adjustedText.startOffset,
293 toIndex: adjustedText.endOffset
294 });
295 }
296 } else {
297 let state = Utils.getState(aContext.accessible);
298 androidEvents.push({eventType: (isExploreByTouch) ?
299 this.ANDROID_VIEW_HOVER_ENTER : focusEventType,
300 text: UtteranceGenerator.genForContext(aContext).output,
301 bounds: aContext.bounds,
302 clickable: aContext.accessible.actionCount > 0,
303 checkable: state.contains(States.CHECKABLE),
304 checked: state.contains(States.CHECKED),
305 brailleOutput: brailleOutput});
306 }
307
308
309 return {
310 type: this.type,
311 details: androidEvents
312 };
313 },
314
315 actionInvoked: function AndroidPresenter_actionInvoked(aObject, aActionName) {
316 let state = Utils.getState(aObject);
317
318 // Checkable objects will have a state changed event we will use instead.
319 if (state.contains(States.CHECKABLE))
320 return null;
321
322 return {
323 type: this.type,
324 details: [{
325 eventType: this.ANDROID_VIEW_CLICKED,
326 text: UtteranceGenerator.genForAction(aObject, aActionName),
327 checked: state.contains(States.CHECKED)
328 }]
329 };
330 },
331
332 tabSelected: function AndroidPresenter_tabSelected(aDocContext, aVCContext) {
333 // Send a pivot change message with the full context utterance for this doc.
334 return this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE);
335 },
336
337 tabStateChanged: function AndroidPresenter_tabStateChanged(aDocObj,
338 aPageState) {
339 return this.announce(
340 UtteranceGenerator.genForTabStateChange(aDocObj, aPageState).join(' '));
341 },
342
343 textChanged: function AndroidPresenter_textChanged(aIsInserted, aStart,
344 aLength, aText,
345 aModifiedText) {
346 let eventDetails = {
347 eventType: this.ANDROID_VIEW_TEXT_CHANGED,
348 text: [aText],
349 fromIndex: aStart,
350 removedCount: 0,
351 addedCount: 0
352 };
353
354 if (aIsInserted) {
355 eventDetails.addedCount = aLength;
356 eventDetails.beforeText =
357 aText.substring(0, aStart) + aText.substring(aStart + aLength);
358 } else {
359 eventDetails.removedCount = aLength;
360 eventDetails.beforeText =
361 aText.substring(0, aStart) + aModifiedText + aText.substring(aStart);
362 }
363
364 return {type: this.type, details: [eventDetails]};
365 },
366
367 textSelectionChanged: function AndroidPresenter_textSelectionChanged(aText, aStart,
368 aEnd, aOldStart,
369 aOldEnd, aIsFromUser) {
370 let androidEvents = [];
371
372 if (Utils.AndroidSdkVersion >= 14 && !aIsFromUser) {
373 if (!this._braillePresenter) {
374 this._braillePresenter = new BraillePresenter();
375 }
376 let brailleOutput = this._braillePresenter.textSelectionChanged(aText, aStart, aEnd,
377 aOldStart, aOldEnd,
378 aIsFromUser).details;
379
380 androidEvents.push({
381 eventType: this.ANDROID_VIEW_TEXT_SELECTION_CHANGED,
382 text: [aText],
383 fromIndex: aStart,
384 toIndex: aEnd,
385 itemCount: aText.length,
386 brailleOutput: brailleOutput
387 });
388 }
389
390 if (Utils.AndroidSdkVersion >= 16 && aIsFromUser) {
391 let [from, to] = aOldStart < aStart ? [aOldStart, aStart] : [aStart, aOldStart];
392 androidEvents.push({
393 eventType: this.ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
394 text: [aText],
395 fromIndex: from,
396 toIndex: to
397 });
398 }
399
400 return {
401 type: this.type,
402 details: androidEvents
403 };
404 },
405
406 viewportChanged: function AndroidPresenter_viewportChanged(aWindow) {
407 if (Utils.AndroidSdkVersion < 14)
408 return null;
409
410 return {
411 type: this.type,
412 details: [{
413 eventType: this.ANDROID_VIEW_SCROLLED,
414 text: [],
415 scrollX: aWindow.scrollX,
416 scrollY: aWindow.scrollY,
417 maxScrollX: aWindow.scrollMaxX,
418 maxScrollY: aWindow.scrollMaxY
419 }]
420 };
421 },
422
423 editingModeChanged: function AndroidPresenter_editingModeChanged(aIsEditing) {
424 return this.announce(
425 UtteranceGenerator.genForEditingMode(aIsEditing).join(' '));
426 },
427
428 announce: function AndroidPresenter_announce(aAnnouncement) {
429 return {
430 type: this.type,
431 details: [{
432 eventType: (Utils.AndroidSdkVersion >= 16) ?
433 this.ANDROID_ANNOUNCEMENT : this.ANDROID_VIEW_TEXT_CHANGED,
434 text: [aAnnouncement],
435 addedCount: aAnnouncement.length,
436 removedCount: 0,
437 fromIndex: 0
438 }]
439 };
440 },
441
442 liveRegion: function AndroidPresenter_liveRegion(aContext, aIsPolite,
443 aIsHide, aModifiedText) {
444 return this.announce(
445 UtteranceGenerator.genForLiveRegion(aContext, aIsHide,
446 aModifiedText).join(' '));
447 }
448 };
449
450 /**
451 * A speech presenter for direct TTS output
452 */
453
454 this.SpeechPresenter = function SpeechPresenter() {};
455
456 SpeechPresenter.prototype = {
457 __proto__: Presenter.prototype,
458
459 type: 'Speech',
460
461 pivotChanged: function SpeechPresenter_pivotChanged(aContext, aReason) {
462 if (!aContext.accessible)
463 return null;
464
465 return {
466 type: this.type,
467 details: {
468 actions: [
469 {method: 'playEarcon',
470 data: aContext.accessible.role === Roles.KEY ?
471 'virtual_cursor_key' : 'virtual_cursor_move',
472 options: {}},
473 {method: 'speak',
474 data: UtteranceGenerator.genForContext(aContext).output.join(' '),
475 options: {enqueue: true}}
476 ]
477 }
478 };
479 },
480
481 valueChanged: function SpeechPresenter_valueChanged(aAccessible) {
482 return {
483 type: this.type,
484 details: {
485 actions: [
486 { method: 'speak',
487 data: aAccessible.value,
488 options: { enqueue: false } }
489 ]
490 }
491 }
492 },
493
494 actionInvoked: function SpeechPresenter_actionInvoked(aObject, aActionName) {
495 let actions = [];
496 if (aActionName === 'click') {
497 actions.push({method: 'playEarcon',
498 data: 'clicked',
499 options: {}});
500 } else {
501 actions.push({method: 'speak',
502 data: UtteranceGenerator.genForAction(aObject, aActionName).join(' '),
503 options: {enqueue: false}});
504 }
505 return { type: this.type, details: { actions: actions } };
506 },
507
508 liveRegion: function SpeechPresenter_liveRegion(aContext, aIsPolite, aIsHide,
509 aModifiedText) {
510 return {
511 type: this.type,
512 details: {
513 actions: [{
514 method: 'speak',
515 data: UtteranceGenerator.genForLiveRegion(aContext, aIsHide,
516 aModifiedText).join(' '),
517 options: {enqueue: aIsPolite}
518 }]
519 }
520 };
521 },
522
523 announce: function SpeechPresenter_announce(aAnnouncement) {
524 return {
525 type: this.type,
526 details: {
527 actions: [{
528 method: 'speak', data: aAnnouncement, options: { enqueue: false }
529 }]
530 }
531 };
532 }
533 };
534
535 /**
536 * A haptic presenter
537 */
538
539 this.HapticPresenter = function HapticPresenter() {};
540
541 HapticPresenter.prototype = {
542 __proto__: Presenter.prototype,
543
544 type: 'Haptic',
545
546 PIVOT_CHANGE_PATTERN: [40],
547
548 pivotChanged: function HapticPresenter_pivotChanged(aContext, aReason) {
549 return { type: this.type, details: { pattern: this.PIVOT_CHANGE_PATTERN } };
550 }
551 };
552
553 /**
554 * A braille presenter
555 */
556
557 this.BraillePresenter = function BraillePresenter() {};
558
559 BraillePresenter.prototype = {
560 __proto__: Presenter.prototype,
561
562 type: 'Braille',
563
564 pivotChanged: function BraillePresenter_pivotChanged(aContext, aReason) {
565 if (!aContext.accessible) {
566 return null;
567 }
568
569 let brailleOutput = BrailleGenerator.genForContext(aContext);
570 brailleOutput.output = brailleOutput.output.join(' ');
571 brailleOutput.selectionStart = 0;
572 brailleOutput.selectionEnd = 0;
573
574 return { type: this.type, details: brailleOutput };
575 },
576
577 textSelectionChanged: function BraillePresenter_textSelectionChanged(aText, aStart,
578 aEnd, aOldStart,
579 aOldEnd, aIsFromUser) {
580 return { type: this.type,
581 details: { selectionStart: aStart,
582 selectionEnd: aEnd } };
583 },
584
585
586 };
587
588 this.Presentation = {
589 get presenters() {
590 delete this.presenters;
591 let presenterMap = {
592 'mobile/android': [VisualPresenter, AndroidPresenter],
593 'b2g': [VisualPresenter, SpeechPresenter, HapticPresenter],
594 'browser': [VisualPresenter, SpeechPresenter, HapticPresenter,
595 AndroidPresenter]
596 };
597 this.presenters = [new P() for (P of presenterMap[Utils.MozBuildApp])];
598 return this.presenters;
599 },
600
601 pivotChanged: function Presentation_pivotChanged(aPosition, aOldPosition, aReason,
602 aStartOffset, aEndOffset) {
603 let context = new PivotContext(aPosition, aOldPosition, aStartOffset, aEndOffset);
604 return [p.pivotChanged(context, aReason)
605 for each (p in this.presenters)];
606 },
607
608 actionInvoked: function Presentation_actionInvoked(aObject, aActionName) {
609 return [p.actionInvoked(aObject, aActionName)
610 for each (p in this.presenters)];
611 },
612
613 textChanged: function Presentation_textChanged(aIsInserted, aStartOffset,
614 aLength, aText,
615 aModifiedText) {
616 return [p.textChanged(aIsInserted, aStartOffset, aLength,
617 aText, aModifiedText)
618 for each (p in this.presenters)];
619 },
620
621 textSelectionChanged: function textSelectionChanged(aText, aStart, aEnd,
622 aOldStart, aOldEnd,
623 aIsFromUser) {
624 return [p.textSelectionChanged(aText, aStart, aEnd,
625 aOldStart, aOldEnd, aIsFromUser)
626 for each (p in this.presenters)];
627 },
628
629 valueChanged: function valueChanged(aAccessible) {
630 return [ p.valueChanged(aAccessible) for (p of this.presenters) ];
631 },
632
633 tabStateChanged: function Presentation_tabStateChanged(aDocObj, aPageState) {
634 return [p.tabStateChanged(aDocObj, aPageState)
635 for each (p in this.presenters)];
636 },
637
638 viewportChanged: function Presentation_viewportChanged(aWindow) {
639 return [p.viewportChanged(aWindow)
640 for each (p in this.presenters)];
641 },
642
643 editingModeChanged: function Presentation_editingModeChanged(aIsEditing) {
644 return [p.editingModeChanged(aIsEditing)
645 for each (p in this.presenters)];
646 },
647
648 announce: function Presentation_announce(aAnnouncement) {
649 // XXX: Typically each presenter uses the UtteranceGenerator,
650 // but there really isn't a point here.
651 return [p.announce(UtteranceGenerator.genForAnnouncement(aAnnouncement)[0])
652 for each (p in this.presenters)];
653 },
654
655 liveRegion: function Presentation_liveRegion(aAccessible, aIsPolite, aIsHide,
656 aModifiedText) {
657 let context;
658 if (!aModifiedText) {
659 context = new PivotContext(aAccessible, null, -1, -1, true,
660 aIsHide ? true : false);
661 }
662 return [p.liveRegion(context, aIsPolite, aIsHide, aModifiedText) for (
663 p of this.presenters)];
664 }
665 };

mercurial