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 */
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
32 * The interface for all presenter classes. A presenter could be, for example,
33 * a speech output module, or a visual cursor indicator.
35 function Presenter() {}
37 Presenter.prototype = {
39 * The type of presenter. Used for matching it with the appropriate output method.
44 * The virtual cursor's position changed.
45 * @param {PivotContext} aContext the context object for the new pivot
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
51 pivotChanged: function pivotChanged(aContext, aReason, aIsFromUserInput) {}, // jshint ignore:line
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.
58 actionInvoked: function actionInvoked(aObject, aActionName) {}, // jshint ignore:line
61 * Text has changed, either by the user or by the system. TODO.
63 textChanged: function textChanged(aIsInserted, aStartOffset, aLength, aText, // jshint ignore:line
64 aModifiedText) {}, // jshint ignore:line
67 * Text selection has changed. TODO.
69 textSelectionChanged: function textSelectionChanged(
70 aText, aStart, aEnd, aOldStart, aOldEnd, aIsFromUserInput) {}, // jshint ignore:line
73 * Selection has changed. TODO.
74 * @param {nsIAccessible} aObject the object that has been selected.
76 selectionChanged: function selectionChanged(aObject) {}, // jshint ignore:line
80 * @param {nsIAccessible} aAccessible the object whose value has changed.
82 valueChanged: function valueChanged(aAccessible) {}, // jshint ignore:line
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'.
91 tabStateChanged: function tabStateChanged(aDocObj, aPageState) {}, // jshint ignore:line
94 * The current tab has changed.
95 * @param {PivotContext} aDocContext context object for tab's
97 * @param {PivotContext} aVCContext context object for tab's current
98 * virtual cursor position.
100 tabSelected: function tabSelected(aDocContext, aVCContext) {}, // jshint ignore:line
103 * The viewport has changed, either a scroll, pan, zoom, or
104 * landscape/portrait toggle.
105 * @param {Window} aWindow window of viewport that changed.
107 viewportChanged: function viewportChanged(aWindow) {}, // jshint ignore:line
110 * We have entered or left text editing mode.
112 editingModeChanged: function editingModeChanged(aIsEditing) {}, // jshint ignore:line
115 * Announce something. Typically an app state change.
117 announce: function announce(aAnnouncement) {}, // jshint ignore:line
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.
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.
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.
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) {
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);
165 eventType: 'viewport-change',
167 padding: this.BORDER_PADDING
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.
182 this._displayedAccessibles.set(aContext.accessible.document.window,
183 { accessible: aContext.accessibleForBounds,
184 startOffset: aContext.startOffset,
185 endOffset: aContext.endOffset });
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,
199 eventType: 'vc-change',
201 padding: this.BORDER_PADDING
205 Logger.logException(e, 'Failed to get bounds');
210 VisualPresenter.prototype.tabSelected =
211 function VisualPresenter_tabSelected(aDocContext, aVCContext) {
212 return this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE);
215 VisualPresenter.prototype.tabStateChanged =
216 function VisualPresenter_tabStateChanged(aDocObj, aPageState) {
217 if (aPageState == 'newdoc') {
218 return {type: this.type, details: {eventType: 'tabstate-change'}};
225 * Android presenter. Fires Android a11y events.
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 =
249 AndroidPresenter.prototype.pivotChanged =
250 function AndroidPresenter_pivotChanged(aContext, aReason) {
251 if (!aContext.accessible) {
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
266 androidEvents.push({eventType: this.ANDROID_VIEW_HOVER_EXIT, text: []});
269 let brailleOutput = {};
270 if (Utils.AndroidSdkVersion >= 16) {
271 if (!this._braillePresenter) {
272 this._braillePresenter = new BraillePresenter();
274 brailleOutput = this._braillePresenter.pivotChanged(aContext, aReason).
278 if (aReason === Ci.nsIAccessiblePivot.REASON_TEXT) {
279 if (Utils.AndroidSdkVersion >= 16) {
280 let adjustedText = aContext.textAndAdjustedOffsets;
283 eventType: this.ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
284 text: [adjustedText.text],
285 fromIndex: adjustedText.startOffset,
286 toIndex: adjustedText.endOffset
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(
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});
305 details: androidEvents
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)) {
321 eventType: this.ANDROID_VIEW_CLICKED,
322 text: Utils.localize(UtteranceGenerator.genForAction(aObject,
324 checked: state.contains(States.CHECKED)
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);
335 AndroidPresenter.prototype.tabStateChanged =
336 function AndroidPresenter_tabStateChanged(aDocObj, aPageState) {
337 return this.announce(
338 UtteranceGenerator.genForTabStateChange(aDocObj, aPageState));
341 AndroidPresenter.prototype.textChanged = function AndroidPresenter_textChanged(
342 aIsInserted, aStart, aLength, aText, aModifiedText) {
344 eventType: this.ANDROID_VIEW_TEXT_CHANGED,
352 eventDetails.addedCount = aLength;
353 eventDetails.beforeText =
354 aText.substring(0, aStart) + aText.substring(aStart + aLength);
356 eventDetails.removedCount = aLength;
357 eventDetails.beforeText =
358 aText.substring(0, aStart) + aModifiedText + aText.substring(aStart);
361 return {type: this.type, details: [eventDetails]};
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();
373 let brailleOutput = this._braillePresenter.textSelectionChanged(
374 aText, aStart, aEnd, aOldStart, aOldEnd, aIsFromUserInput).details;
377 eventType: this.ANDROID_VIEW_TEXT_SELECTION_CHANGED,
381 itemCount: aText.length,
382 brailleOutput: brailleOutput
386 if (Utils.AndroidSdkVersion >= 16 && aIsFromUserInput) {
387 let [from, to] = aOldStart < aStart ?
388 [aOldStart, aStart] : [aStart, aOldStart];
390 eventType: this.ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
399 details: androidEvents
403 AndroidPresenter.prototype.viewportChanged =
404 function AndroidPresenter_viewportChanged(aWindow) {
405 if (Utils.AndroidSdkVersion < 14) {
412 eventType: this.ANDROID_VIEW_SCROLLED,
414 scrollX: aWindow.scrollX,
415 scrollY: aWindow.scrollY,
416 maxScrollX: aWindow.scrollMaxX,
417 maxScrollY: aWindow.scrollMaxY
422 AndroidPresenter.prototype.editingModeChanged =
423 function AndroidPresenter_editingModeChanged(aIsEditing) {
424 return this.announce(UtteranceGenerator.genForEditingMode(aIsEditing));
427 AndroidPresenter.prototype.announce =
428 function AndroidPresenter_announce(aAnnouncement) {
429 let localizedAnnouncement = Utils.localize(aAnnouncement).join(' ');
433 eventType: (Utils.AndroidSdkVersion >= 16) ?
434 this.ANDROID_ANNOUNCEMENT : this.ANDROID_VIEW_TEXT_CHANGED,
435 text: [localizedAnnouncement],
436 addedCount: localizedAnnouncement.length,
443 AndroidPresenter.prototype.liveRegion =
444 function AndroidPresenter_liveRegion(aContext, aIsPolite,
445 aIsHide, aModifiedText) {
446 return this.announce(
447 UtteranceGenerator.genForLiveRegion(aContext, aIsHide, aModifiedText));
451 * A B2G presenter for Gaia.
453 function B2GPresenter() {}
455 B2GPresenter.prototype = Object.create(Presenter.prototype);
457 B2GPresenter.prototype.type = 'B2G';
460 * A pattern used for haptic feedback.
463 B2GPresenter.prototype.PIVOT_CHANGE_HAPTIC_PATTERN = [40];
466 * Pivot move reasons.
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) {
481 eventType: 'vc-change',
482 data: UtteranceGenerator.genForContext(aContext),
484 pattern: this.PIVOT_CHANGE_HAPTIC_PATTERN,
485 isKey: aContext.accessible.role === Roles.KEY,
486 reason: this.pivotChangedReasons[aReason],
487 isUserInput: aIsUserInput
493 B2GPresenter.prototype.valueChanged =
494 function B2GPresenter_valueChanged(aAccessible) {
498 eventType: 'value-change',
499 data: aAccessible.value
504 B2GPresenter.prototype.actionInvoked =
505 function B2GPresenter_actionInvoked(aObject, aActionName) {
510 data: UtteranceGenerator.genForAction(aObject, aActionName)
515 B2GPresenter.prototype.liveRegion = function B2GPresenter_liveRegion(aContext,
516 aIsPolite, aIsHide, aModifiedText) {
520 eventType: 'liveregion-change',
521 data: UtteranceGenerator.genForLiveRegion(aContext, aIsHide,
523 options: {enqueue: aIsPolite}
528 B2GPresenter.prototype.announce =
529 function B2GPresenter_announce(aAnnouncement) {
533 eventType: 'announcement',
540 * A braille presenter
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) {
557 output: Utils.localize(BrailleGenerator.genForContext(aContext)).join(
565 BraillePresenter.prototype.textSelectionChanged =
566 function BraillePresenter_textSelectionChanged(aText, aStart, aEnd) {
570 selectionStart: aStart,
576 this.Presentation = { // jshint ignore:line
578 delete this.presenters;
580 'mobile/android': [VisualPresenter, AndroidPresenter],
581 'b2g': [VisualPresenter, B2GPresenter],
582 'browser': [VisualPresenter, B2GPresenter, AndroidPresenter]
584 this.presenters = [new P() for (P of presenterMap[Utils.MozBuildApp])]; // jshint ignore:line
585 return this.presenters;
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
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
601 textChanged: function Presentation_textChanged(aIsInserted, aStartOffset,
604 return [p.textChanged(aIsInserted, aStartOffset, aLength, aText, // jshint ignore:line
605 aModifiedText) for each (p in this.presenters)]; // jshint ignore:line
608 textSelectionChanged: function textSelectionChanged(aText, aStart, aEnd,
611 return [p.textSelectionChanged(aText, aStart, aEnd, aOldStart, aOldEnd, // jshint ignore:line
612 aIsFromUserInput) for each (p in this.presenters)]; // jshint ignore:line
615 valueChanged: function valueChanged(aAccessible) {
616 return [ p.valueChanged(aAccessible) for (p of this.presenters) ]; // jshint ignore:line
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
624 viewportChanged: function Presentation_viewportChanged(aWindow) {
625 return [p.viewportChanged(aWindow) for each (p in this.presenters)]; // jshint ignore:line
628 editingModeChanged: function Presentation_editingModeChanged(aIsEditing) {
629 return [p.editingModeChanged(aIsEditing) for each (p in this.presenters)]; // jshint ignore:line
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
639 liveRegion: function Presentation_liveRegion(aAccessible, aIsPolite, aIsHide,
642 if (!aModifiedText) {
643 context = new PivotContext(aAccessible, null, -1, -1, true,
644 aIsHide ? true : false);
646 return [p.liveRegion(context, aIsPolite, aIsHide, aModifiedText) // jshint ignore:line
647 for (p of this.presenters)]; // jshint ignore:line