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