Presentation.jsm 24 KB

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