Utils.jsm 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113
  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, Services, PluralForm, Logger, Rect, Utils,
  5. States, Relations, Roles, dump, Events, PivotContext, PrefCache */
  6. /* exported Utils, Logger, PivotContext, PrefCache, SettingCache */
  7. 'use strict';
  8. const {classes: Cc, utils: Cu, interfaces: Ci} = Components;
  9. Cu.import('resource://gre/modules/XPCOMUtils.jsm');
  10. XPCOMUtils.defineLazyModuleGetter(this, 'Services', // jshint ignore:line
  11. 'resource://gre/modules/Services.jsm');
  12. XPCOMUtils.defineLazyModuleGetter(this, 'Rect', // jshint ignore:line
  13. 'resource://gre/modules/Geometry.jsm');
  14. XPCOMUtils.defineLazyModuleGetter(this, 'Roles', // jshint ignore:line
  15. 'resource://gre/modules/accessibility/Constants.jsm');
  16. XPCOMUtils.defineLazyModuleGetter(this, 'Events', // jshint ignore:line
  17. 'resource://gre/modules/accessibility/Constants.jsm');
  18. XPCOMUtils.defineLazyModuleGetter(this, 'Relations', // jshint ignore:line
  19. 'resource://gre/modules/accessibility/Constants.jsm');
  20. XPCOMUtils.defineLazyModuleGetter(this, 'States', // jshint ignore:line
  21. 'resource://gre/modules/accessibility/Constants.jsm');
  22. XPCOMUtils.defineLazyModuleGetter(this, 'PluralForm', // jshint ignore:line
  23. 'resource://gre/modules/PluralForm.jsm');
  24. this.EXPORTED_SYMBOLS = ['Utils', 'Logger', 'PivotContext', 'PrefCache', // jshint ignore:line
  25. 'SettingCache'];
  26. this.Utils = { // jshint ignore:line
  27. _buildAppMap: {
  28. '{3c2e2abc-06d4-11e1-ac3b-374f68613e61}': 'b2g',
  29. '{d1bfe7d9-c01e-4237-998b-7b5f960a4314}': 'graphene',
  30. '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}': 'browser'
  31. },
  32. init: function Utils_init(aWindow) {
  33. if (this._win) {
  34. // XXX: only supports attaching to one window now.
  35. throw new Error('Only one top-level window could used with AccessFu');
  36. }
  37. this._win = Cu.getWeakReference(aWindow);
  38. },
  39. uninit: function Utils_uninit() {
  40. if (!this._win) {
  41. return;
  42. }
  43. delete this._win;
  44. },
  45. get win() {
  46. if (!this._win) {
  47. return null;
  48. }
  49. return this._win.get();
  50. },
  51. get winUtils() {
  52. let win = this.win;
  53. if (!win) {
  54. return null;
  55. }
  56. return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(
  57. Ci.nsIDOMWindowUtils);
  58. },
  59. get AccService() {
  60. if (!this._AccService) {
  61. this._AccService = Cc['@mozilla.org/accessibilityService;1'].
  62. getService(Ci.nsIAccessibilityService);
  63. }
  64. return this._AccService;
  65. },
  66. set MozBuildApp(value) {
  67. this._buildApp = value;
  68. },
  69. get MozBuildApp() {
  70. if (!this._buildApp) {
  71. this._buildApp = this._buildAppMap[Services.appinfo.ID];
  72. }
  73. return this._buildApp;
  74. },
  75. get OS() {
  76. if (!this._OS) {
  77. this._OS = Services.appinfo.OS;
  78. }
  79. return this._OS;
  80. },
  81. get widgetToolkit() {
  82. if (!this._widgetToolkit) {
  83. this._widgetToolkit = Services.appinfo.widgetToolkit;
  84. }
  85. return this._widgetToolkit;
  86. },
  87. get ScriptName() {
  88. if (!this._ScriptName) {
  89. this._ScriptName =
  90. (Services.appinfo.processType == 2) ? 'AccessFuContent' : 'AccessFu';
  91. }
  92. return this._ScriptName;
  93. },
  94. get AndroidSdkVersion() {
  95. if (!this._AndroidSdkVersion) {
  96. if (Services.appinfo.OS == 'Android') {
  97. this._AndroidSdkVersion = Services.sysinfo.getPropertyAsInt32(
  98. 'version');
  99. } else {
  100. // Most useful in desktop debugging.
  101. this._AndroidSdkVersion = 16;
  102. }
  103. }
  104. return this._AndroidSdkVersion;
  105. },
  106. set AndroidSdkVersion(value) {
  107. // When we want to mimic another version.
  108. this._AndroidSdkVersion = value;
  109. },
  110. get BrowserApp() {
  111. if (!this.win) {
  112. return null;
  113. }
  114. switch (this.MozBuildApp) {
  115. case 'mobile/android':
  116. return this.win.BrowserApp;
  117. case 'browser':
  118. return this.win.gBrowser;
  119. case 'b2g':
  120. return this.win.shell;
  121. default:
  122. return null;
  123. }
  124. },
  125. get CurrentBrowser() {
  126. if (!this.BrowserApp) {
  127. return null;
  128. }
  129. if (this.MozBuildApp == 'b2g') {
  130. return this.BrowserApp.contentBrowser;
  131. }
  132. return this.BrowserApp.selectedBrowser;
  133. },
  134. get CurrentContentDoc() {
  135. let browser = this.CurrentBrowser;
  136. return browser ? browser.contentDocument : null;
  137. },
  138. get AllMessageManagers() {
  139. let messageManagers = new Set();
  140. function collectLeafMessageManagers(mm) {
  141. for (let i = 0; i < mm.childCount; i++) {
  142. let childMM = mm.getChildAt(i);
  143. if ('sendAsyncMessage' in childMM) {
  144. messageManagers.add(childMM);
  145. } else {
  146. collectLeafMessageManagers(childMM);
  147. }
  148. }
  149. }
  150. collectLeafMessageManagers(this.win.messageManager);
  151. let document = this.CurrentContentDoc;
  152. if (document) {
  153. if (document.location.host === 'b2g') {
  154. // The document is a b2g app chrome (ie. Mulet).
  155. let contentBrowser = this.win.content.shell.contentBrowser;
  156. messageManagers.add(this.getMessageManager(contentBrowser));
  157. document = contentBrowser.contentDocument;
  158. }
  159. let remoteframes = document.querySelectorAll('iframe');
  160. for (let i = 0; i < remoteframes.length; ++i) {
  161. let mm = this.getMessageManager(remoteframes[i]);
  162. if (mm) {
  163. messageManagers.add(mm);
  164. }
  165. }
  166. }
  167. return messageManagers;
  168. },
  169. get isContentProcess() {
  170. delete this.isContentProcess;
  171. this.isContentProcess =
  172. Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
  173. return this.isContentProcess;
  174. },
  175. localize: function localize(aOutput) {
  176. let outputArray = Array.isArray(aOutput) ? aOutput : [aOutput];
  177. let localized =
  178. outputArray.map(details => this.stringBundle.get(details));
  179. // Clean up the white space.
  180. return localized.filter(word => word).map(word => word.trim()).
  181. filter(trimmed => trimmed);
  182. },
  183. get stringBundle() {
  184. delete this.stringBundle;
  185. let bundle = Services.strings.createBundle(
  186. 'chrome://global/locale/AccessFu.properties');
  187. this.stringBundle = {
  188. get: function stringBundle_get(aDetails = {}) {
  189. if (!aDetails || typeof aDetails === 'string') {
  190. return aDetails;
  191. }
  192. let str = '';
  193. let string = aDetails.string;
  194. if (!string) {
  195. return str;
  196. }
  197. try {
  198. let args = aDetails.args;
  199. let count = aDetails.count;
  200. if (args) {
  201. str = bundle.formatStringFromName(string, args, args.length);
  202. } else {
  203. str = bundle.GetStringFromName(string);
  204. }
  205. if (count) {
  206. str = PluralForm.get(count, str);
  207. str = str.replace('#1', count);
  208. }
  209. } catch (e) {
  210. Logger.debug('Failed to get a string from a bundle for', string);
  211. } finally {
  212. return str;
  213. }
  214. }
  215. };
  216. return this.stringBundle;
  217. },
  218. getMessageManager: function getMessageManager(aBrowser) {
  219. try {
  220. return aBrowser.QueryInterface(Ci.nsIFrameLoaderOwner).
  221. frameLoader.messageManager;
  222. } catch (x) {
  223. return null;
  224. }
  225. },
  226. getState: function getState(aAccessibleOrEvent) {
  227. if (aAccessibleOrEvent instanceof Ci.nsIAccessibleStateChangeEvent) {
  228. return new State(
  229. aAccessibleOrEvent.isExtraState ? 0 : aAccessibleOrEvent.state,
  230. aAccessibleOrEvent.isExtraState ? aAccessibleOrEvent.state : 0);
  231. } else {
  232. let state = {};
  233. let extState = {};
  234. aAccessibleOrEvent.getState(state, extState);
  235. return new State(state.value, extState.value);
  236. }
  237. },
  238. getAttributes: function getAttributes(aAccessible) {
  239. let attributes = {};
  240. if (aAccessible && aAccessible.attributes) {
  241. let attributesEnum = aAccessible.attributes.enumerate();
  242. // Populate |attributes| object with |aAccessible|'s attribute key-value
  243. // pairs.
  244. while (attributesEnum.hasMoreElements()) {
  245. let attribute = attributesEnum.getNext().QueryInterface(
  246. Ci.nsIPropertyElement);
  247. attributes[attribute.key] = attribute.value;
  248. }
  249. }
  250. return attributes;
  251. },
  252. getVirtualCursor: function getVirtualCursor(aDocument) {
  253. let doc = (aDocument instanceof Ci.nsIAccessible) ? aDocument :
  254. this.AccService.getAccessibleFor(aDocument);
  255. return doc.QueryInterface(Ci.nsIAccessibleDocument).virtualCursor;
  256. },
  257. getContentResolution: function _getContentResolution(aAccessible) {
  258. let res = { value: 1 };
  259. aAccessible.document.window.QueryInterface(
  260. Ci.nsIInterfaceRequestor).getInterface(
  261. Ci.nsIDOMWindowUtils).getResolution(res);
  262. return res.value;
  263. },
  264. getBounds: function getBounds(aAccessible, aPreserveContentScale) {
  265. let objX = {}, objY = {}, objW = {}, objH = {};
  266. aAccessible.getBounds(objX, objY, objW, objH);
  267. let scale = aPreserveContentScale ? 1 :
  268. this.getContentResolution(aAccessible);
  269. return new Rect(objX.value, objY.value, objW.value, objH.value).scale(
  270. scale, scale);
  271. },
  272. getTextBounds: function getTextBounds(aAccessible, aStart, aEnd,
  273. aPreserveContentScale) {
  274. let accText = aAccessible.QueryInterface(Ci.nsIAccessibleText);
  275. let objX = {}, objY = {}, objW = {}, objH = {};
  276. accText.getRangeExtents(aStart, aEnd, objX, objY, objW, objH,
  277. Ci.nsIAccessibleCoordinateType.COORDTYPE_SCREEN_RELATIVE);
  278. let scale = aPreserveContentScale ? 1 :
  279. this.getContentResolution(aAccessible);
  280. return new Rect(objX.value, objY.value, objW.value, objH.value).scale(
  281. scale, scale);
  282. },
  283. /**
  284. * Get current display DPI.
  285. */
  286. get dpi() {
  287. delete this.dpi;
  288. this.dpi = this.winUtils.displayDPI;
  289. return this.dpi;
  290. },
  291. isInSubtree: function isInSubtree(aAccessible, aSubTreeRoot) {
  292. let acc = aAccessible;
  293. // If aSubTreeRoot is an accessible document, we will only walk up the
  294. // ancestry of documents and skip everything else.
  295. if (aSubTreeRoot instanceof Ci.nsIAccessibleDocument) {
  296. while (acc) {
  297. let parentDoc = acc instanceof Ci.nsIAccessibleDocument ?
  298. acc.parentDocument : acc.document;
  299. if (parentDoc === aSubTreeRoot) {
  300. return true;
  301. }
  302. acc = parentDoc;
  303. }
  304. return false;
  305. }
  306. while (acc) {
  307. if (acc == aSubTreeRoot) {
  308. return true;
  309. }
  310. try {
  311. acc = acc.parent;
  312. } catch (x) {
  313. Logger.debug('Failed to get parent:', x);
  314. acc = null;
  315. }
  316. }
  317. return false;
  318. },
  319. isHidden: function isHidden(aAccessible) {
  320. // Need to account for aria-hidden, so can't just check for INVISIBLE
  321. // state.
  322. let hidden = Utils.getAttributes(aAccessible).hidden;
  323. return hidden && hidden === 'true';
  324. },
  325. visibleChildCount: function visibleChildCount(aAccessible) {
  326. let count = 0;
  327. for (let child = aAccessible.firstChild; child; child = child.nextSibling) {
  328. if (!this.isHidden(child)) {
  329. ++count;
  330. }
  331. }
  332. return count;
  333. },
  334. inHiddenSubtree: function inHiddenSubtree(aAccessible) {
  335. for (let acc=aAccessible; acc; acc=acc.parent) {
  336. if (this.isHidden(acc)) {
  337. return true;
  338. }
  339. }
  340. return false;
  341. },
  342. isAliveAndVisible: function isAliveAndVisible(aAccessible, aIsOnScreen) {
  343. if (!aAccessible) {
  344. return false;
  345. }
  346. try {
  347. let state = this.getState(aAccessible);
  348. if (state.contains(States.DEFUNCT) || state.contains(States.INVISIBLE) ||
  349. (aIsOnScreen && state.contains(States.OFFSCREEN)) ||
  350. Utils.inHiddenSubtree(aAccessible)) {
  351. return false;
  352. }
  353. } catch (x) {
  354. return false;
  355. }
  356. return true;
  357. },
  358. matchAttributeValue: function matchAttributeValue(aAttributeValue, values) {
  359. let attrSet = new Set(aAttributeValue.split(' '));
  360. for (let value of values) {
  361. if (attrSet.has(value)) {
  362. return value;
  363. }
  364. }
  365. },
  366. getLandmarkName: function getLandmarkName(aAccessible) {
  367. return this.matchRoles(aAccessible, [
  368. 'banner',
  369. 'complementary',
  370. 'contentinfo',
  371. 'main',
  372. 'navigation',
  373. 'search'
  374. ]);
  375. },
  376. getMathRole: function getMathRole(aAccessible) {
  377. return this.matchRoles(aAccessible, [
  378. 'base',
  379. 'close-fence',
  380. 'denominator',
  381. 'numerator',
  382. 'open-fence',
  383. 'overscript',
  384. 'presubscript',
  385. 'presuperscript',
  386. 'root-index',
  387. 'subscript',
  388. 'superscript',
  389. 'underscript'
  390. ]);
  391. },
  392. matchRoles: function matchRoles(aAccessible, aRoles) {
  393. let roles = this.getAttributes(aAccessible)['xml-roles'];
  394. if (!roles) {
  395. return;
  396. }
  397. // Looking up a role that would match any in the provided roles.
  398. return this.matchAttributeValue(roles, aRoles);
  399. },
  400. getEmbeddedControl: function getEmbeddedControl(aLabel) {
  401. if (aLabel) {
  402. let relation = aLabel.getRelationByType(Relations.LABEL_FOR);
  403. for (let i = 0; i < relation.targetsCount; i++) {
  404. let target = relation.getTarget(i);
  405. if (target.parent === aLabel) {
  406. return target;
  407. }
  408. }
  409. }
  410. return null;
  411. },
  412. isListItemDecorator: function isListItemDecorator(aStaticText,
  413. aExcludeOrdered) {
  414. let parent = aStaticText.parent;
  415. if (aExcludeOrdered && parent.parent.DOMNode.nodeName === 'OL') {
  416. return false;
  417. }
  418. return parent.role === Roles.LISTITEM && parent.childCount > 1 &&
  419. aStaticText.indexInParent === 0;
  420. },
  421. dispatchChromeEvent: function dispatchChromeEvent(aType, aDetails) {
  422. let details = {
  423. type: aType,
  424. details: JSON.stringify(
  425. typeof aDetails === 'string' ? { eventType : aDetails } : aDetails)
  426. };
  427. let window = this.win;
  428. let shell = window.shell || window.content.shell;
  429. if (shell) {
  430. // On B2G device.
  431. shell.sendChromeEvent(details);
  432. } else {
  433. // Dispatch custom event to have support for desktop and screen reader
  434. // emulator add-on.
  435. window.dispatchEvent(new window.CustomEvent(aType, {
  436. bubbles: true,
  437. cancelable: true,
  438. detail: details
  439. }));
  440. }
  441. },
  442. isActivatableOnFingerUp: function isActivatableOnFingerUp(aAccessible) {
  443. if (aAccessible.role === Roles.KEY) {
  444. return true;
  445. }
  446. let quick_activate = this.getAttributes(aAccessible)['moz-quick-activate'];
  447. return quick_activate && JSON.parse(quick_activate);
  448. }
  449. };
  450. /**
  451. * State object used internally to process accessible's states.
  452. * @param {Number} aBase Base state.
  453. * @param {Number} aExtended Extended state.
  454. */
  455. function State(aBase, aExtended) {
  456. this.base = aBase;
  457. this.extended = aExtended;
  458. }
  459. State.prototype = {
  460. contains: function State_contains(other) {
  461. return !!(this.base & other.base || this.extended & other.extended);
  462. },
  463. toString: function State_toString() {
  464. let stateStrings = Utils.AccService.
  465. getStringStates(this.base, this.extended);
  466. let statesArray = new Array(stateStrings.length);
  467. for (let i = 0; i < statesArray.length; i++) {
  468. statesArray[i] = stateStrings.item(i);
  469. }
  470. return '[' + statesArray.join(', ') + ']';
  471. }
  472. };
  473. this.Logger = { // jshint ignore:line
  474. GESTURE: -1,
  475. DEBUG: 0,
  476. INFO: 1,
  477. WARNING: 2,
  478. ERROR: 3,
  479. _LEVEL_NAMES: ['GESTURE', 'DEBUG', 'INFO', 'WARNING', 'ERROR'],
  480. logLevel: 1, // INFO;
  481. test: false,
  482. log: function log(aLogLevel) {
  483. if (aLogLevel < this.logLevel) {
  484. return;
  485. }
  486. let args = Array.prototype.slice.call(arguments, 1);
  487. let message = (typeof(args[0]) === 'function' ? args[0]() : args).join(' ');
  488. message = '[' + Utils.ScriptName + '] ' + this._LEVEL_NAMES[aLogLevel + 1] +
  489. ' ' + message + '\n';
  490. dump(message);
  491. // Note: used for testing purposes. If |this.test| is true, also log to
  492. // the console service.
  493. if (this.test) {
  494. try {
  495. Services.console.logStringMessage(message);
  496. } catch (ex) {
  497. // There was an exception logging to the console service.
  498. }
  499. }
  500. },
  501. info: function info() {
  502. this.log.apply(
  503. this, [this.INFO].concat(Array.prototype.slice.call(arguments)));
  504. },
  505. gesture: function gesture() {
  506. this.log.apply(
  507. this, [this.GESTURE].concat(Array.prototype.slice.call(arguments)));
  508. },
  509. debug: function debug() {
  510. this.log.apply(
  511. this, [this.DEBUG].concat(Array.prototype.slice.call(arguments)));
  512. },
  513. warning: function warning() {
  514. this.log.apply(
  515. this, [this.WARNING].concat(Array.prototype.slice.call(arguments)));
  516. },
  517. error: function error() {
  518. this.log.apply(
  519. this, [this.ERROR].concat(Array.prototype.slice.call(arguments)));
  520. },
  521. logException: function logException(
  522. aException, aErrorMessage = 'An exception has occured') {
  523. try {
  524. let stackMessage = '';
  525. if (aException.stack) {
  526. stackMessage = ' ' + aException.stack.replace(/\n/g, '\n ');
  527. } else if (aException.location) {
  528. let frame = aException.location;
  529. let stackLines = [];
  530. while (frame && frame.lineNumber) {
  531. stackLines.push(
  532. ' ' + frame.name + '@' + frame.filename + ':' + frame.lineNumber);
  533. frame = frame.caller;
  534. }
  535. stackMessage = stackLines.join('\n');
  536. } else {
  537. stackMessage =
  538. '(' + aException.fileName + ':' + aException.lineNumber + ')';
  539. }
  540. this.error(aErrorMessage + ':\n ' +
  541. aException.message + '\n' +
  542. stackMessage);
  543. } catch (x) {
  544. this.error(x);
  545. }
  546. },
  547. accessibleToString: function accessibleToString(aAccessible) {
  548. if (!aAccessible) {
  549. return '[ null ]';
  550. }
  551. try {
  552. return'[ ' + Utils.AccService.getStringRole(aAccessible.role) +
  553. ' | ' + aAccessible.name + ' ]';
  554. } catch (x) {
  555. return '[ defunct ]';
  556. }
  557. },
  558. eventToString: function eventToString(aEvent) {
  559. let str = Utils.AccService.getStringEventType(aEvent.eventType);
  560. if (aEvent.eventType == Events.STATE_CHANGE) {
  561. let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent);
  562. let stateStrings = event.isExtraState ?
  563. Utils.AccService.getStringStates(0, event.state) :
  564. Utils.AccService.getStringStates(event.state, 0);
  565. str += ' (' + stateStrings.item(0) + ')';
  566. }
  567. if (aEvent.eventType == Events.VIRTUALCURSOR_CHANGED) {
  568. let event = aEvent.QueryInterface(
  569. Ci.nsIAccessibleVirtualCursorChangeEvent);
  570. let pivot = aEvent.accessible.QueryInterface(
  571. Ci.nsIAccessibleDocument).virtualCursor;
  572. str += ' (' + this.accessibleToString(event.oldAccessible) + ' -> ' +
  573. this.accessibleToString(pivot.position) + ')';
  574. }
  575. return str;
  576. },
  577. statesToString: function statesToString(aAccessible) {
  578. return Utils.getState(aAccessible).toString();
  579. },
  580. dumpTree: function dumpTree(aLogLevel, aRootAccessible) {
  581. if (aLogLevel < this.logLevel) {
  582. return;
  583. }
  584. this._dumpTreeInternal(aLogLevel, aRootAccessible, 0);
  585. },
  586. _dumpTreeInternal:
  587. function _dumpTreeInternal(aLogLevel, aAccessible, aIndent) {
  588. let indentStr = '';
  589. for (let i = 0; i < aIndent; i++) {
  590. indentStr += ' ';
  591. }
  592. this.log(aLogLevel, indentStr,
  593. this.accessibleToString(aAccessible),
  594. '(' + this.statesToString(aAccessible) + ')');
  595. for (let i = 0; i < aAccessible.childCount; i++) {
  596. this._dumpTreeInternal(aLogLevel, aAccessible.getChildAt(i),
  597. aIndent + 1);
  598. }
  599. }
  600. };
  601. /**
  602. * PivotContext: An object that generates and caches context information
  603. * for a given accessible and its relationship with another accessible.
  604. *
  605. * If the given accessible is a label for a nested control, then this
  606. * context will represent the nested control instead of the label.
  607. * With the exception of bounds calculation, which will use the containing
  608. * label. In this case the |accessible| field would be the embedded control,
  609. * and the |accessibleForBounds| field would be the label.
  610. */
  611. this.PivotContext = function PivotContext(aAccessible, aOldAccessible, // jshint ignore:line
  612. aStartOffset, aEndOffset, aIgnoreAncestry = false,
  613. aIncludeInvisible = false) {
  614. this._accessible = aAccessible;
  615. this._nestedControl = Utils.getEmbeddedControl(aAccessible);
  616. this._oldAccessible =
  617. this._isDefunct(aOldAccessible) ? null : aOldAccessible;
  618. this.startOffset = aStartOffset;
  619. this.endOffset = aEndOffset;
  620. this._ignoreAncestry = aIgnoreAncestry;
  621. this._includeInvisible = aIncludeInvisible;
  622. };
  623. PivotContext.prototype = {
  624. get accessible() {
  625. // If the current pivot accessible has a nested control,
  626. // make this context use it publicly.
  627. return this._nestedControl || this._accessible;
  628. },
  629. get oldAccessible() {
  630. return this._oldAccessible;
  631. },
  632. get isNestedControl() {
  633. return !!this._nestedControl;
  634. },
  635. get accessibleForBounds() {
  636. return this._accessible;
  637. },
  638. get textAndAdjustedOffsets() {
  639. if (this.startOffset === -1 && this.endOffset === -1) {
  640. return null;
  641. }
  642. if (!this._textAndAdjustedOffsets) {
  643. let result = {startOffset: this.startOffset,
  644. endOffset: this.endOffset,
  645. text: this._accessible.QueryInterface(Ci.nsIAccessibleText).
  646. getText(0,
  647. Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT)};
  648. let hypertextAcc = this._accessible.QueryInterface(
  649. Ci.nsIAccessibleHyperText);
  650. // Iterate through the links in backwards order so text replacements don't
  651. // affect the offsets of links yet to be processed.
  652. for (let i = hypertextAcc.linkCount - 1; i >= 0; i--) {
  653. let link = hypertextAcc.getLinkAt(i);
  654. let linkText = '';
  655. if (link instanceof Ci.nsIAccessibleText) {
  656. linkText = link.QueryInterface(Ci.nsIAccessibleText).
  657. getText(0,
  658. Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT);
  659. }
  660. let start = link.startIndex;
  661. let end = link.endIndex;
  662. for (let offset of ['startOffset', 'endOffset']) {
  663. if (this[offset] >= end) {
  664. result[offset] += linkText.length - (end - start);
  665. }
  666. }
  667. result.text = result.text.substring(0, start) + linkText +
  668. result.text.substring(end);
  669. }
  670. this._textAndAdjustedOffsets = result;
  671. }
  672. return this._textAndAdjustedOffsets;
  673. },
  674. /**
  675. * Get a list of |aAccessible|'s ancestry up to the root.
  676. * @param {nsIAccessible} aAccessible.
  677. * @return {Array} Ancestry list.
  678. */
  679. _getAncestry: function _getAncestry(aAccessible) {
  680. let ancestry = [];
  681. let parent = aAccessible;
  682. try {
  683. while (parent && (parent = parent.parent)) {
  684. ancestry.push(parent);
  685. }
  686. } catch (x) {
  687. // A defunct accessible will raise an exception geting parent.
  688. Logger.debug('Failed to get parent:', x);
  689. }
  690. return ancestry.reverse();
  691. },
  692. /**
  693. * A list of the old accessible's ancestry.
  694. */
  695. get oldAncestry() {
  696. if (!this._oldAncestry) {
  697. if (!this._oldAccessible || this._ignoreAncestry) {
  698. this._oldAncestry = [];
  699. } else {
  700. this._oldAncestry = this._getAncestry(this._oldAccessible);
  701. this._oldAncestry.push(this._oldAccessible);
  702. }
  703. }
  704. return this._oldAncestry;
  705. },
  706. /**
  707. * A list of the current accessible's ancestry.
  708. */
  709. get currentAncestry() {
  710. if (!this._currentAncestry) {
  711. this._currentAncestry = this._ignoreAncestry ? [] :
  712. this._getAncestry(this.accessible);
  713. }
  714. return this._currentAncestry;
  715. },
  716. /*
  717. * This is a list of the accessible's ancestry up to the common ancestor
  718. * of the accessible and the old accessible. It is useful for giving the
  719. * user context as to where they are in the heirarchy.
  720. */
  721. get newAncestry() {
  722. if (!this._newAncestry) {
  723. this._newAncestry = this._ignoreAncestry ? [] :
  724. this.currentAncestry.filter(
  725. (currentAncestor, i) => currentAncestor !== this.oldAncestry[i]);
  726. }
  727. return this._newAncestry;
  728. },
  729. /*
  730. * Traverse the accessible's subtree in pre or post order.
  731. * It only includes the accessible's visible chidren.
  732. * Note: needSubtree is a function argument that can be used to determine
  733. * whether aAccessible's subtree is required.
  734. */
  735. _traverse: function* _traverse(aAccessible, aPreorder, aStop) {
  736. if (aStop && aStop(aAccessible)) {
  737. return;
  738. }
  739. let child = aAccessible.firstChild;
  740. while (child) {
  741. let include;
  742. if (this._includeInvisible) {
  743. include = true;
  744. } else {
  745. include = !Utils.isHidden(child);
  746. }
  747. if (include) {
  748. if (aPreorder) {
  749. yield child;
  750. for (let node of this._traverse(child, aPreorder, aStop)) {
  751. yield node;
  752. }
  753. } else {
  754. for (let node of this._traverse(child, aPreorder, aStop)) {
  755. yield node;
  756. }
  757. yield child;
  758. }
  759. }
  760. child = child.nextSibling;
  761. }
  762. },
  763. /**
  764. * Get interaction hints for the context ancestry.
  765. * @return {Array} Array of interaction hints.
  766. */
  767. get interactionHints() {
  768. let hints = [];
  769. this.newAncestry.concat(this.accessible).reverse().forEach(aAccessible => {
  770. let hint = Utils.getAttributes(aAccessible)['moz-hint'];
  771. if (hint) {
  772. hints.push(hint);
  773. } else if (aAccessible.actionCount > 0) {
  774. hints.push({
  775. string: Utils.AccService.getStringRole(
  776. aAccessible.role).replace(/\s/g, '') + '-hint'
  777. });
  778. }
  779. });
  780. return hints;
  781. },
  782. /*
  783. * A subtree generator function, used to generate a flattened
  784. * list of the accessible's subtree in pre or post order.
  785. * It only includes the accessible's visible chidren.
  786. * @param {boolean} aPreorder A flag for traversal order. If true, traverse
  787. * in preorder; if false, traverse in postorder.
  788. * @param {function} aStop An optional function, indicating whether subtree
  789. * traversal should stop.
  790. */
  791. subtreeGenerator: function subtreeGenerator(aPreorder, aStop) {
  792. return this._traverse(this.accessible, aPreorder, aStop);
  793. },
  794. getCellInfo: function getCellInfo(aAccessible) {
  795. if (!this._cells) {
  796. this._cells = new WeakMap();
  797. }
  798. let domNode = aAccessible.DOMNode;
  799. if (this._cells.has(domNode)) {
  800. return this._cells.get(domNode);
  801. }
  802. let cellInfo = {};
  803. let getAccessibleCell = function getAccessibleCell(aAccessible) {
  804. if (!aAccessible) {
  805. return null;
  806. }
  807. if ([
  808. Roles.CELL,
  809. Roles.COLUMNHEADER,
  810. Roles.ROWHEADER,
  811. Roles.MATHML_CELL
  812. ].indexOf(aAccessible.role) < 0) {
  813. return null;
  814. }
  815. try {
  816. return aAccessible.QueryInterface(Ci.nsIAccessibleTableCell);
  817. } catch (x) {
  818. Logger.logException(x);
  819. return null;
  820. }
  821. };
  822. let getHeaders = function* getHeaders(aHeaderCells) {
  823. let enumerator = aHeaderCells.enumerate();
  824. while (enumerator.hasMoreElements()) {
  825. yield enumerator.getNext().QueryInterface(Ci.nsIAccessible).name;
  826. }
  827. };
  828. cellInfo.current = getAccessibleCell(aAccessible);
  829. if (!cellInfo.current) {
  830. Logger.warning(aAccessible,
  831. 'does not support nsIAccessibleTableCell interface.');
  832. this._cells.set(domNode, null);
  833. return null;
  834. }
  835. let table = cellInfo.current.table;
  836. if (table.isProbablyForLayout()) {
  837. this._cells.set(domNode, null);
  838. return null;
  839. }
  840. cellInfo.previous = null;
  841. let oldAncestry = this.oldAncestry.reverse();
  842. let ancestor = oldAncestry.shift();
  843. while (!cellInfo.previous && ancestor) {
  844. let cell = getAccessibleCell(ancestor);
  845. if (cell && cell.table === table) {
  846. cellInfo.previous = cell;
  847. }
  848. ancestor = oldAncestry.shift();
  849. }
  850. if (cellInfo.previous) {
  851. cellInfo.rowChanged = cellInfo.current.rowIndex !==
  852. cellInfo.previous.rowIndex;
  853. cellInfo.columnChanged = cellInfo.current.columnIndex !==
  854. cellInfo.previous.columnIndex;
  855. } else {
  856. cellInfo.rowChanged = true;
  857. cellInfo.columnChanged = true;
  858. }
  859. cellInfo.rowExtent = cellInfo.current.rowExtent;
  860. cellInfo.columnExtent = cellInfo.current.columnExtent;
  861. cellInfo.columnIndex = cellInfo.current.columnIndex;
  862. cellInfo.rowIndex = cellInfo.current.rowIndex;
  863. cellInfo.columnHeaders = [];
  864. if (cellInfo.columnChanged && cellInfo.current.role !==
  865. Roles.COLUMNHEADER) {
  866. cellInfo.columnHeaders = [...getHeaders(cellInfo.current.columnHeaderCells)];
  867. }
  868. cellInfo.rowHeaders = [];
  869. if (cellInfo.rowChanged &&
  870. (cellInfo.current.role === Roles.CELL ||
  871. cellInfo.current.role === Roles.MATHML_CELL)) {
  872. cellInfo.rowHeaders = [...getHeaders(cellInfo.current.rowHeaderCells)];
  873. }
  874. this._cells.set(domNode, cellInfo);
  875. return cellInfo;
  876. },
  877. get bounds() {
  878. if (!this._bounds) {
  879. this._bounds = Utils.getBounds(this.accessibleForBounds);
  880. }
  881. return this._bounds.clone();
  882. },
  883. _isDefunct: function _isDefunct(aAccessible) {
  884. try {
  885. return Utils.getState(aAccessible).contains(States.DEFUNCT);
  886. } catch (x) {
  887. return true;
  888. }
  889. }
  890. };
  891. this.PrefCache = function PrefCache(aName, aCallback, aRunCallbackNow) { // jshint ignore:line
  892. this.name = aName;
  893. this.callback = aCallback;
  894. let branch = Services.prefs;
  895. this.value = this._getValue(branch);
  896. if (this.callback && aRunCallbackNow) {
  897. try {
  898. this.callback(this.name, this.value, true);
  899. } catch (x) {
  900. Logger.logException(x);
  901. }
  902. }
  903. branch.addObserver(aName, this, true);
  904. };
  905. PrefCache.prototype = {
  906. _getValue: function _getValue(aBranch) {
  907. try {
  908. if (!this.type) {
  909. this.type = aBranch.getPrefType(this.name);
  910. }
  911. switch (this.type) {
  912. case Ci.nsIPrefBranch.PREF_STRING:
  913. return aBranch.getCharPref(this.name);
  914. case Ci.nsIPrefBranch.PREF_INT:
  915. return aBranch.getIntPref(this.name);
  916. case Ci.nsIPrefBranch.PREF_BOOL:
  917. return aBranch.getBoolPref(this.name);
  918. default:
  919. return null;
  920. }
  921. } catch (x) {
  922. // Pref does not exist.
  923. return null;
  924. }
  925. },
  926. observe: function observe(aSubject) {
  927. this.value = this._getValue(aSubject.QueryInterface(Ci.nsIPrefBranch));
  928. Logger.info('pref changed', this.name, this.value);
  929. if (this.callback) {
  930. try {
  931. this.callback(this.name, this.value, false);
  932. } catch (x) {
  933. Logger.logException(x);
  934. }
  935. }
  936. },
  937. QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
  938. Ci.nsISupportsWeakReference])
  939. };
  940. this.SettingCache = function SettingCache(aName, aCallback, aOptions = {}) { // jshint ignore:line
  941. this.value = aOptions.defaultValue;
  942. let runCallback = () => {
  943. if (aCallback) {
  944. aCallback(aName, this.value);
  945. if (aOptions.callbackOnce) {
  946. runCallback = () => {};
  947. }
  948. }
  949. };
  950. let settings = Utils.win.navigator.mozSettings;
  951. if (!settings) {
  952. if (aOptions.callbackNow) {
  953. runCallback();
  954. }
  955. return;
  956. }
  957. let lock = settings.createLock();
  958. let req = lock.get(aName);
  959. req.addEventListener('success', () => {
  960. this.value = req.result[aName] === undefined ?
  961. aOptions.defaultValue : req.result[aName];
  962. if (aOptions.callbackNow) {
  963. runCallback();
  964. }
  965. });
  966. settings.addObserver(aName,
  967. (evt) => {
  968. this.value = evt.settingValue;
  969. runCallback();
  970. });
  971. };