summaryrefslogtreecommitdiffstats
path: root/accessible/jsat/Utils.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'accessible/jsat/Utils.jsm')
-rw-r--r--accessible/jsat/Utils.jsm1114
1 files changed, 1114 insertions, 0 deletions
diff --git a/accessible/jsat/Utils.jsm b/accessible/jsat/Utils.jsm
new file mode 100644
index 000000000..4e478cab0
--- /dev/null
+++ b/accessible/jsat/Utils.jsm
@@ -0,0 +1,1114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* global Components, XPCOMUtils, Services, PluralForm, Logger, Rect, Utils,
+ States, Relations, Roles, dump, Events, PivotContext, PrefCache */
+/* exported Utils, Logger, PivotContext, PrefCache, SettingCache */
+
+'use strict';
+
+const {classes: Cc, utils: Cu, interfaces: Ci} = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Services', // jshint ignore:line
+ 'resource://gre/modules/Services.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Rect', // jshint ignore:line
+ 'resource://gre/modules/Geometry.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Roles', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Constants.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Events', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Constants.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Relations', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Constants.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'States', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Constants.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'PluralForm', // jshint ignore:line
+ 'resource://gre/modules/PluralForm.jsm');
+
+this.EXPORTED_SYMBOLS = ['Utils', 'Logger', 'PivotContext', 'PrefCache', // jshint ignore:line
+ 'SettingCache'];
+
+this.Utils = { // jshint ignore:line
+ _buildAppMap: {
+ '{3c2e2abc-06d4-11e1-ac3b-374f68613e61}': 'b2g',
+ '{d1bfe7d9-c01e-4237-998b-7b5f960a4314}': 'graphene',
+ '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}': 'browser',
+ '{aa3c5121-dab2-40e2-81ca-7ea25febc110}': 'mobile/android',
+ '{a23983c0-fd0e-11dc-95ff-0800200c9a66}': 'mobile/xul'
+ },
+
+ init: function Utils_init(aWindow) {
+ if (this._win) {
+ // XXX: only supports attaching to one window now.
+ throw new Error('Only one top-level window could used with AccessFu');
+ }
+ this._win = Cu.getWeakReference(aWindow);
+ },
+
+ uninit: function Utils_uninit() {
+ if (!this._win) {
+ return;
+ }
+ delete this._win;
+ },
+
+ get win() {
+ if (!this._win) {
+ return null;
+ }
+ return this._win.get();
+ },
+
+ get winUtils() {
+ let win = this.win;
+ if (!win) {
+ return null;
+ }
+ return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(
+ Ci.nsIDOMWindowUtils);
+ },
+
+ get AccService() {
+ if (!this._AccService) {
+ this._AccService = Cc['@mozilla.org/accessibilityService;1'].
+ getService(Ci.nsIAccessibilityService);
+ }
+
+ return this._AccService;
+ },
+
+ set MozBuildApp(value) {
+ this._buildApp = value;
+ },
+
+ get MozBuildApp() {
+ if (!this._buildApp) {
+ this._buildApp = this._buildAppMap[Services.appinfo.ID];
+ }
+ return this._buildApp;
+ },
+
+ get OS() {
+ if (!this._OS) {
+ this._OS = Services.appinfo.OS;
+ }
+ return this._OS;
+ },
+
+ get widgetToolkit() {
+ if (!this._widgetToolkit) {
+ this._widgetToolkit = Services.appinfo.widgetToolkit;
+ }
+ return this._widgetToolkit;
+ },
+
+ get ScriptName() {
+ if (!this._ScriptName) {
+ this._ScriptName =
+ (Services.appinfo.processType == 2) ? 'AccessFuContent' : 'AccessFu';
+ }
+ return this._ScriptName;
+ },
+
+ get AndroidSdkVersion() {
+ if (!this._AndroidSdkVersion) {
+ if (Services.appinfo.OS == 'Android') {
+ this._AndroidSdkVersion = Services.sysinfo.getPropertyAsInt32(
+ 'version');
+ } else {
+ // Most useful in desktop debugging.
+ this._AndroidSdkVersion = 16;
+ }
+ }
+ return this._AndroidSdkVersion;
+ },
+
+ set AndroidSdkVersion(value) {
+ // When we want to mimic another version.
+ this._AndroidSdkVersion = value;
+ },
+
+ get BrowserApp() {
+ if (!this.win) {
+ return null;
+ }
+ switch (this.MozBuildApp) {
+ case 'mobile/android':
+ return this.win.BrowserApp;
+ case 'browser':
+ return this.win.gBrowser;
+ case 'b2g':
+ return this.win.shell;
+ default:
+ return null;
+ }
+ },
+
+ get CurrentBrowser() {
+ if (!this.BrowserApp) {
+ return null;
+ }
+ if (this.MozBuildApp == 'b2g') {
+ return this.BrowserApp.contentBrowser;
+ }
+ return this.BrowserApp.selectedBrowser;
+ },
+
+ get CurrentContentDoc() {
+ let browser = this.CurrentBrowser;
+ return browser ? browser.contentDocument : null;
+ },
+
+ get AllMessageManagers() {
+ let messageManagers = new Set();
+
+ function collectLeafMessageManagers(mm) {
+ for (let i = 0; i < mm.childCount; i++) {
+ let childMM = mm.getChildAt(i);
+
+ if ('sendAsyncMessage' in childMM) {
+ messageManagers.add(childMM);
+ } else {
+ collectLeafMessageManagers(childMM);
+ }
+ }
+ }
+
+ collectLeafMessageManagers(this.win.messageManager);
+
+ let document = this.CurrentContentDoc;
+
+ if (document) {
+ if (document.location.host === 'b2g') {
+ // The document is a b2g app chrome (ie. Mulet).
+ let contentBrowser = this.win.content.shell.contentBrowser;
+ messageManagers.add(this.getMessageManager(contentBrowser));
+ document = contentBrowser.contentDocument;
+ }
+
+ let remoteframes = document.querySelectorAll('iframe');
+
+ for (let i = 0; i < remoteframes.length; ++i) {
+ let mm = this.getMessageManager(remoteframes[i]);
+ if (mm) {
+ messageManagers.add(mm);
+ }
+ }
+
+ }
+
+ return messageManagers;
+ },
+
+ get isContentProcess() {
+ delete this.isContentProcess;
+ this.isContentProcess =
+ Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
+ return this.isContentProcess;
+ },
+
+ localize: function localize(aOutput) {
+ let outputArray = Array.isArray(aOutput) ? aOutput : [aOutput];
+ let localized =
+ outputArray.map(details => this.stringBundle.get(details));
+ // Clean up the white space.
+ return localized.filter(word => word).map(word => word.trim()).
+ filter(trimmed => trimmed);
+ },
+
+ get stringBundle() {
+ delete this.stringBundle;
+ let bundle = Services.strings.createBundle(
+ 'chrome://global/locale/AccessFu.properties');
+ this.stringBundle = {
+ get: function stringBundle_get(aDetails = {}) {
+ if (!aDetails || typeof aDetails === 'string') {
+ return aDetails;
+ }
+ let str = '';
+ let string = aDetails.string;
+ if (!string) {
+ return str;
+ }
+ try {
+ let args = aDetails.args;
+ let count = aDetails.count;
+ if (args) {
+ str = bundle.formatStringFromName(string, args, args.length);
+ } else {
+ str = bundle.GetStringFromName(string);
+ }
+ if (count) {
+ str = PluralForm.get(count, str);
+ str = str.replace('#1', count);
+ }
+ } catch (e) {
+ Logger.debug('Failed to get a string from a bundle for', string);
+ } finally {
+ return str;
+ }
+ }
+ };
+ return this.stringBundle;
+ },
+
+ getMessageManager: function getMessageManager(aBrowser) {
+ try {
+ return aBrowser.QueryInterface(Ci.nsIFrameLoaderOwner).
+ frameLoader.messageManager;
+ } catch (x) {
+ return null;
+ }
+ },
+
+ getState: function getState(aAccessibleOrEvent) {
+ if (aAccessibleOrEvent instanceof Ci.nsIAccessibleStateChangeEvent) {
+ return new State(
+ aAccessibleOrEvent.isExtraState ? 0 : aAccessibleOrEvent.state,
+ aAccessibleOrEvent.isExtraState ? aAccessibleOrEvent.state : 0);
+ } else {
+ let state = {};
+ let extState = {};
+ aAccessibleOrEvent.getState(state, extState);
+ return new State(state.value, extState.value);
+ }
+ },
+
+ getAttributes: function getAttributes(aAccessible) {
+ let attributes = {};
+
+ if (aAccessible && aAccessible.attributes) {
+ let attributesEnum = aAccessible.attributes.enumerate();
+
+ // Populate |attributes| object with |aAccessible|'s attribute key-value
+ // pairs.
+ while (attributesEnum.hasMoreElements()) {
+ let attribute = attributesEnum.getNext().QueryInterface(
+ Ci.nsIPropertyElement);
+ attributes[attribute.key] = attribute.value;
+ }
+ }
+
+ return attributes;
+ },
+
+ getVirtualCursor: function getVirtualCursor(aDocument) {
+ let doc = (aDocument instanceof Ci.nsIAccessible) ? aDocument :
+ this.AccService.getAccessibleFor(aDocument);
+
+ return doc.QueryInterface(Ci.nsIAccessibleDocument).virtualCursor;
+ },
+
+ getContentResolution: function _getContentResolution(aAccessible) {
+ let res = { value: 1 };
+ aAccessible.document.window.QueryInterface(
+ Ci.nsIInterfaceRequestor).getInterface(
+ Ci.nsIDOMWindowUtils).getResolution(res);
+ return res.value;
+ },
+
+ getBounds: function getBounds(aAccessible, aPreserveContentScale) {
+ let objX = {}, objY = {}, objW = {}, objH = {};
+ aAccessible.getBounds(objX, objY, objW, objH);
+
+ let scale = aPreserveContentScale ? 1 :
+ this.getContentResolution(aAccessible);
+
+ return new Rect(objX.value, objY.value, objW.value, objH.value).scale(
+ scale, scale);
+ },
+
+ getTextBounds: function getTextBounds(aAccessible, aStart, aEnd,
+ aPreserveContentScale) {
+ let accText = aAccessible.QueryInterface(Ci.nsIAccessibleText);
+ let objX = {}, objY = {}, objW = {}, objH = {};
+ accText.getRangeExtents(aStart, aEnd, objX, objY, objW, objH,
+ Ci.nsIAccessibleCoordinateType.COORDTYPE_SCREEN_RELATIVE);
+
+ let scale = aPreserveContentScale ? 1 :
+ this.getContentResolution(aAccessible);
+
+ return new Rect(objX.value, objY.value, objW.value, objH.value).scale(
+ scale, scale);
+ },
+
+ /**
+ * Get current display DPI.
+ */
+ get dpi() {
+ delete this.dpi;
+ this.dpi = this.winUtils.displayDPI;
+ return this.dpi;
+ },
+
+ isInSubtree: function isInSubtree(aAccessible, aSubTreeRoot) {
+ let acc = aAccessible;
+
+ // If aSubTreeRoot is an accessible document, we will only walk up the
+ // ancestry of documents and skip everything else.
+ if (aSubTreeRoot instanceof Ci.nsIAccessibleDocument) {
+ while (acc) {
+ let parentDoc = acc instanceof Ci.nsIAccessibleDocument ?
+ acc.parentDocument : acc.document;
+ if (parentDoc === aSubTreeRoot) {
+ return true;
+ }
+ acc = parentDoc;
+ }
+ return false;
+ }
+
+ while (acc) {
+ if (acc == aSubTreeRoot) {
+ return true;
+ }
+
+ try {
+ acc = acc.parent;
+ } catch (x) {
+ Logger.debug('Failed to get parent:', x);
+ acc = null;
+ }
+ }
+
+ return false;
+ },
+
+ isHidden: function isHidden(aAccessible) {
+ // Need to account for aria-hidden, so can't just check for INVISIBLE
+ // state.
+ let hidden = Utils.getAttributes(aAccessible).hidden;
+ return hidden && hidden === 'true';
+ },
+
+ visibleChildCount: function visibleChildCount(aAccessible) {
+ let count = 0;
+ for (let child = aAccessible.firstChild; child; child = child.nextSibling) {
+ if (!this.isHidden(child)) {
+ ++count;
+ }
+ }
+ return count;
+ },
+
+ inHiddenSubtree: function inHiddenSubtree(aAccessible) {
+ for (let acc=aAccessible; acc; acc=acc.parent) {
+ if (this.isHidden(acc)) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ isAliveAndVisible: function isAliveAndVisible(aAccessible, aIsOnScreen) {
+ if (!aAccessible) {
+ return false;
+ }
+
+ try {
+ let state = this.getState(aAccessible);
+ if (state.contains(States.DEFUNCT) || state.contains(States.INVISIBLE) ||
+ (aIsOnScreen && state.contains(States.OFFSCREEN)) ||
+ Utils.inHiddenSubtree(aAccessible)) {
+ return false;
+ }
+ } catch (x) {
+ return false;
+ }
+
+ return true;
+ },
+
+ matchAttributeValue: function matchAttributeValue(aAttributeValue, values) {
+ let attrSet = new Set(aAttributeValue.split(' '));
+ for (let value of values) {
+ if (attrSet.has(value)) {
+ return value;
+ }
+ }
+ },
+
+ getLandmarkName: function getLandmarkName(aAccessible) {
+ return this.matchRoles(aAccessible, [
+ 'banner',
+ 'complementary',
+ 'contentinfo',
+ 'main',
+ 'navigation',
+ 'search'
+ ]);
+ },
+
+ getMathRole: function getMathRole(aAccessible) {
+ return this.matchRoles(aAccessible, [
+ 'base',
+ 'close-fence',
+ 'denominator',
+ 'numerator',
+ 'open-fence',
+ 'overscript',
+ 'presubscript',
+ 'presuperscript',
+ 'root-index',
+ 'subscript',
+ 'superscript',
+ 'underscript'
+ ]);
+ },
+
+ matchRoles: function matchRoles(aAccessible, aRoles) {
+ let roles = this.getAttributes(aAccessible)['xml-roles'];
+ if (!roles) {
+ return;
+ }
+
+ // Looking up a role that would match any in the provided roles.
+ return this.matchAttributeValue(roles, aRoles);
+ },
+
+ getEmbeddedControl: function getEmbeddedControl(aLabel) {
+ if (aLabel) {
+ let relation = aLabel.getRelationByType(Relations.LABEL_FOR);
+ for (let i = 0; i < relation.targetsCount; i++) {
+ let target = relation.getTarget(i);
+ if (target.parent === aLabel) {
+ return target;
+ }
+ }
+ }
+
+ return null;
+ },
+
+ isListItemDecorator: function isListItemDecorator(aStaticText,
+ aExcludeOrdered) {
+ let parent = aStaticText.parent;
+ if (aExcludeOrdered && parent.parent.DOMNode.nodeName === 'OL') {
+ return false;
+ }
+
+ return parent.role === Roles.LISTITEM && parent.childCount > 1 &&
+ aStaticText.indexInParent === 0;
+ },
+
+ dispatchChromeEvent: function dispatchChromeEvent(aType, aDetails) {
+ let details = {
+ type: aType,
+ details: JSON.stringify(
+ typeof aDetails === 'string' ? { eventType : aDetails } : aDetails)
+ };
+ let window = this.win;
+ let shell = window.shell || window.content.shell;
+ if (shell) {
+ // On B2G device.
+ shell.sendChromeEvent(details);
+ } else {
+ // Dispatch custom event to have support for desktop and screen reader
+ // emulator add-on.
+ window.dispatchEvent(new window.CustomEvent(aType, {
+ bubbles: true,
+ cancelable: true,
+ detail: details
+ }));
+ }
+
+ },
+
+ isActivatableOnFingerUp: function isActivatableOnFingerUp(aAccessible) {
+ if (aAccessible.role === Roles.KEY) {
+ return true;
+ }
+ let quick_activate = this.getAttributes(aAccessible)['moz-quick-activate'];
+ return quick_activate && JSON.parse(quick_activate);
+ }
+};
+
+/**
+ * State object used internally to process accessible's states.
+ * @param {Number} aBase Base state.
+ * @param {Number} aExtended Extended state.
+ */
+function State(aBase, aExtended) {
+ this.base = aBase;
+ this.extended = aExtended;
+}
+
+State.prototype = {
+ contains: function State_contains(other) {
+ return !!(this.base & other.base || this.extended & other.extended);
+ },
+ toString: function State_toString() {
+ let stateStrings = Utils.AccService.
+ getStringStates(this.base, this.extended);
+ let statesArray = new Array(stateStrings.length);
+ for (let i = 0; i < statesArray.length; i++) {
+ statesArray[i] = stateStrings.item(i);
+ }
+ return '[' + statesArray.join(', ') + ']';
+ }
+};
+
+this.Logger = { // jshint ignore:line
+ GESTURE: -1,
+ DEBUG: 0,
+ INFO: 1,
+ WARNING: 2,
+ ERROR: 3,
+ _LEVEL_NAMES: ['GESTURE', 'DEBUG', 'INFO', 'WARNING', 'ERROR'],
+
+ logLevel: 1, // INFO;
+
+ test: false,
+
+ log: function log(aLogLevel) {
+ if (aLogLevel < this.logLevel) {
+ return;
+ }
+
+ let args = Array.prototype.slice.call(arguments, 1);
+ let message = (typeof(args[0]) === 'function' ? args[0]() : args).join(' ');
+ message = '[' + Utils.ScriptName + '] ' + this._LEVEL_NAMES[aLogLevel + 1] +
+ ' ' + message + '\n';
+ dump(message);
+ // Note: used for testing purposes. If |this.test| is true, also log to
+ // the console service.
+ if (this.test) {
+ try {
+ Services.console.logStringMessage(message);
+ } catch (ex) {
+ // There was an exception logging to the console service.
+ }
+ }
+ },
+
+ info: function info() {
+ this.log.apply(
+ this, [this.INFO].concat(Array.prototype.slice.call(arguments)));
+ },
+
+ gesture: function gesture() {
+ this.log.apply(
+ this, [this.GESTURE].concat(Array.prototype.slice.call(arguments)));
+ },
+
+ debug: function debug() {
+ this.log.apply(
+ this, [this.DEBUG].concat(Array.prototype.slice.call(arguments)));
+ },
+
+ warning: function warning() {
+ this.log.apply(
+ this, [this.WARNING].concat(Array.prototype.slice.call(arguments)));
+ },
+
+ error: function error() {
+ this.log.apply(
+ this, [this.ERROR].concat(Array.prototype.slice.call(arguments)));
+ },
+
+ logException: function logException(
+ aException, aErrorMessage = 'An exception has occured') {
+ try {
+ let stackMessage = '';
+ if (aException.stack) {
+ stackMessage = ' ' + aException.stack.replace(/\n/g, '\n ');
+ } else if (aException.location) {
+ let frame = aException.location;
+ let stackLines = [];
+ while (frame && frame.lineNumber) {
+ stackLines.push(
+ ' ' + frame.name + '@' + frame.filename + ':' + frame.lineNumber);
+ frame = frame.caller;
+ }
+ stackMessage = stackLines.join('\n');
+ } else {
+ stackMessage =
+ '(' + aException.fileName + ':' + aException.lineNumber + ')';
+ }
+ this.error(aErrorMessage + ':\n ' +
+ aException.message + '\n' +
+ stackMessage);
+ } catch (x) {
+ this.error(x);
+ }
+ },
+
+ accessibleToString: function accessibleToString(aAccessible) {
+ if (!aAccessible) {
+ return '[ null ]';
+ }
+
+ try {
+ return'[ ' + Utils.AccService.getStringRole(aAccessible.role) +
+ ' | ' + aAccessible.name + ' ]';
+ } catch (x) {
+ return '[ defunct ]';
+ }
+ },
+
+ eventToString: function eventToString(aEvent) {
+ let str = Utils.AccService.getStringEventType(aEvent.eventType);
+ if (aEvent.eventType == Events.STATE_CHANGE) {
+ let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent);
+ let stateStrings = event.isExtraState ?
+ Utils.AccService.getStringStates(0, event.state) :
+ Utils.AccService.getStringStates(event.state, 0);
+ str += ' (' + stateStrings.item(0) + ')';
+ }
+
+ if (aEvent.eventType == Events.VIRTUALCURSOR_CHANGED) {
+ let event = aEvent.QueryInterface(
+ Ci.nsIAccessibleVirtualCursorChangeEvent);
+ let pivot = aEvent.accessible.QueryInterface(
+ Ci.nsIAccessibleDocument).virtualCursor;
+ str += ' (' + this.accessibleToString(event.oldAccessible) + ' -> ' +
+ this.accessibleToString(pivot.position) + ')';
+ }
+
+ return str;
+ },
+
+ statesToString: function statesToString(aAccessible) {
+ return Utils.getState(aAccessible).toString();
+ },
+
+ dumpTree: function dumpTree(aLogLevel, aRootAccessible) {
+ if (aLogLevel < this.logLevel) {
+ return;
+ }
+
+ this._dumpTreeInternal(aLogLevel, aRootAccessible, 0);
+ },
+
+ _dumpTreeInternal:
+ function _dumpTreeInternal(aLogLevel, aAccessible, aIndent) {
+ let indentStr = '';
+ for (let i = 0; i < aIndent; i++) {
+ indentStr += ' ';
+ }
+ this.log(aLogLevel, indentStr,
+ this.accessibleToString(aAccessible),
+ '(' + this.statesToString(aAccessible) + ')');
+ for (let i = 0; i < aAccessible.childCount; i++) {
+ this._dumpTreeInternal(aLogLevel, aAccessible.getChildAt(i),
+ aIndent + 1);
+ }
+ }
+};
+
+/**
+ * PivotContext: An object that generates and caches context information
+ * for a given accessible and its relationship with another accessible.
+ *
+ * If the given accessible is a label for a nested control, then this
+ * context will represent the nested control instead of the label.
+ * With the exception of bounds calculation, which will use the containing
+ * label. In this case the |accessible| field would be the embedded control,
+ * and the |accessibleForBounds| field would be the label.
+ */
+this.PivotContext = function PivotContext(aAccessible, aOldAccessible, // jshint ignore:line
+ aStartOffset, aEndOffset, aIgnoreAncestry = false,
+ aIncludeInvisible = false) {
+ this._accessible = aAccessible;
+ this._nestedControl = Utils.getEmbeddedControl(aAccessible);
+ this._oldAccessible =
+ this._isDefunct(aOldAccessible) ? null : aOldAccessible;
+ this.startOffset = aStartOffset;
+ this.endOffset = aEndOffset;
+ this._ignoreAncestry = aIgnoreAncestry;
+ this._includeInvisible = aIncludeInvisible;
+};
+
+PivotContext.prototype = {
+ get accessible() {
+ // If the current pivot accessible has a nested control,
+ // make this context use it publicly.
+ return this._nestedControl || this._accessible;
+ },
+
+ get oldAccessible() {
+ return this._oldAccessible;
+ },
+
+ get isNestedControl() {
+ return !!this._nestedControl;
+ },
+
+ get accessibleForBounds() {
+ return this._accessible;
+ },
+
+ get textAndAdjustedOffsets() {
+ if (this.startOffset === -1 && this.endOffset === -1) {
+ return null;
+ }
+
+ if (!this._textAndAdjustedOffsets) {
+ let result = {startOffset: this.startOffset,
+ endOffset: this.endOffset,
+ text: this._accessible.QueryInterface(Ci.nsIAccessibleText).
+ getText(0,
+ Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT)};
+ let hypertextAcc = this._accessible.QueryInterface(
+ Ci.nsIAccessibleHyperText);
+
+ // Iterate through the links in backwards order so text replacements don't
+ // affect the offsets of links yet to be processed.
+ for (let i = hypertextAcc.linkCount - 1; i >= 0; i--) {
+ let link = hypertextAcc.getLinkAt(i);
+ let linkText = '';
+ if (link instanceof Ci.nsIAccessibleText) {
+ linkText = link.QueryInterface(Ci.nsIAccessibleText).
+ getText(0,
+ Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT);
+ }
+
+ let start = link.startIndex;
+ let end = link.endIndex;
+ for (let offset of ['startOffset', 'endOffset']) {
+ if (this[offset] >= end) {
+ result[offset] += linkText.length - (end - start);
+ }
+ }
+ result.text = result.text.substring(0, start) + linkText +
+ result.text.substring(end);
+ }
+
+ this._textAndAdjustedOffsets = result;
+ }
+
+ return this._textAndAdjustedOffsets;
+ },
+
+ /**
+ * Get a list of |aAccessible|'s ancestry up to the root.
+ * @param {nsIAccessible} aAccessible.
+ * @return {Array} Ancestry list.
+ */
+ _getAncestry: function _getAncestry(aAccessible) {
+ let ancestry = [];
+ let parent = aAccessible;
+ try {
+ while (parent && (parent = parent.parent)) {
+ ancestry.push(parent);
+ }
+ } catch (x) {
+ // A defunct accessible will raise an exception geting parent.
+ Logger.debug('Failed to get parent:', x);
+ }
+ return ancestry.reverse();
+ },
+
+ /**
+ * A list of the old accessible's ancestry.
+ */
+ get oldAncestry() {
+ if (!this._oldAncestry) {
+ if (!this._oldAccessible || this._ignoreAncestry) {
+ this._oldAncestry = [];
+ } else {
+ this._oldAncestry = this._getAncestry(this._oldAccessible);
+ this._oldAncestry.push(this._oldAccessible);
+ }
+ }
+ return this._oldAncestry;
+ },
+
+ /**
+ * A list of the current accessible's ancestry.
+ */
+ get currentAncestry() {
+ if (!this._currentAncestry) {
+ this._currentAncestry = this._ignoreAncestry ? [] :
+ this._getAncestry(this.accessible);
+ }
+ return this._currentAncestry;
+ },
+
+ /*
+ * This is a list of the accessible's ancestry up to the common ancestor
+ * of the accessible and the old accessible. It is useful for giving the
+ * user context as to where they are in the heirarchy.
+ */
+ get newAncestry() {
+ if (!this._newAncestry) {
+ this._newAncestry = this._ignoreAncestry ? [] :
+ this.currentAncestry.filter(
+ (currentAncestor, i) => currentAncestor !== this.oldAncestry[i]);
+ }
+ return this._newAncestry;
+ },
+
+ /*
+ * Traverse the accessible's subtree in pre or post order.
+ * It only includes the accessible's visible chidren.
+ * Note: needSubtree is a function argument that can be used to determine
+ * whether aAccessible's subtree is required.
+ */
+ _traverse: function* _traverse(aAccessible, aPreorder, aStop) {
+ if (aStop && aStop(aAccessible)) {
+ return;
+ }
+ let child = aAccessible.firstChild;
+ while (child) {
+ let include;
+ if (this._includeInvisible) {
+ include = true;
+ } else {
+ include = !Utils.isHidden(child);
+ }
+ if (include) {
+ if (aPreorder) {
+ yield child;
+ for (let node of this._traverse(child, aPreorder, aStop)) {
+ yield node;
+ }
+ } else {
+ for (let node of this._traverse(child, aPreorder, aStop)) {
+ yield node;
+ }
+ yield child;
+ }
+ }
+ child = child.nextSibling;
+ }
+ },
+
+ /**
+ * Get interaction hints for the context ancestry.
+ * @return {Array} Array of interaction hints.
+ */
+ get interactionHints() {
+ let hints = [];
+ this.newAncestry.concat(this.accessible).reverse().forEach(aAccessible => {
+ let hint = Utils.getAttributes(aAccessible)['moz-hint'];
+ if (hint) {
+ hints.push(hint);
+ } else if (aAccessible.actionCount > 0) {
+ hints.push({
+ string: Utils.AccService.getStringRole(
+ aAccessible.role).replace(/\s/g, '') + '-hint'
+ });
+ }
+ });
+ return hints;
+ },
+
+ /*
+ * A subtree generator function, used to generate a flattened
+ * list of the accessible's subtree in pre or post order.
+ * It only includes the accessible's visible chidren.
+ * @param {boolean} aPreorder A flag for traversal order. If true, traverse
+ * in preorder; if false, traverse in postorder.
+ * @param {function} aStop An optional function, indicating whether subtree
+ * traversal should stop.
+ */
+ subtreeGenerator: function subtreeGenerator(aPreorder, aStop) {
+ return this._traverse(this.accessible, aPreorder, aStop);
+ },
+
+ getCellInfo: function getCellInfo(aAccessible) {
+ if (!this._cells) {
+ this._cells = new WeakMap();
+ }
+
+ let domNode = aAccessible.DOMNode;
+ if (this._cells.has(domNode)) {
+ return this._cells.get(domNode);
+ }
+
+ let cellInfo = {};
+ let getAccessibleCell = function getAccessibleCell(aAccessible) {
+ if (!aAccessible) {
+ return null;
+ }
+ if ([
+ Roles.CELL,
+ Roles.COLUMNHEADER,
+ Roles.ROWHEADER,
+ Roles.MATHML_CELL
+ ].indexOf(aAccessible.role) < 0) {
+ return null;
+ }
+ try {
+ return aAccessible.QueryInterface(Ci.nsIAccessibleTableCell);
+ } catch (x) {
+ Logger.logException(x);
+ return null;
+ }
+ };
+ let getHeaders = function* getHeaders(aHeaderCells) {
+ let enumerator = aHeaderCells.enumerate();
+ while (enumerator.hasMoreElements()) {
+ yield enumerator.getNext().QueryInterface(Ci.nsIAccessible).name;
+ }
+ };
+
+ cellInfo.current = getAccessibleCell(aAccessible);
+
+ if (!cellInfo.current) {
+ Logger.warning(aAccessible,
+ 'does not support nsIAccessibleTableCell interface.');
+ this._cells.set(domNode, null);
+ return null;
+ }
+
+ let table = cellInfo.current.table;
+ if (table.isProbablyForLayout()) {
+ this._cells.set(domNode, null);
+ return null;
+ }
+
+ cellInfo.previous = null;
+ let oldAncestry = this.oldAncestry.reverse();
+ let ancestor = oldAncestry.shift();
+ while (!cellInfo.previous && ancestor) {
+ let cell = getAccessibleCell(ancestor);
+ if (cell && cell.table === table) {
+ cellInfo.previous = cell;
+ }
+ ancestor = oldAncestry.shift();
+ }
+
+ if (cellInfo.previous) {
+ cellInfo.rowChanged = cellInfo.current.rowIndex !==
+ cellInfo.previous.rowIndex;
+ cellInfo.columnChanged = cellInfo.current.columnIndex !==
+ cellInfo.previous.columnIndex;
+ } else {
+ cellInfo.rowChanged = true;
+ cellInfo.columnChanged = true;
+ }
+
+ cellInfo.rowExtent = cellInfo.current.rowExtent;
+ cellInfo.columnExtent = cellInfo.current.columnExtent;
+ cellInfo.columnIndex = cellInfo.current.columnIndex;
+ cellInfo.rowIndex = cellInfo.current.rowIndex;
+
+ cellInfo.columnHeaders = [];
+ if (cellInfo.columnChanged && cellInfo.current.role !==
+ Roles.COLUMNHEADER) {
+ cellInfo.columnHeaders = [...getHeaders(cellInfo.current.columnHeaderCells)];
+ }
+ cellInfo.rowHeaders = [];
+ if (cellInfo.rowChanged &&
+ (cellInfo.current.role === Roles.CELL ||
+ cellInfo.current.role === Roles.MATHML_CELL)) {
+ cellInfo.rowHeaders = [...getHeaders(cellInfo.current.rowHeaderCells)];
+ }
+
+ this._cells.set(domNode, cellInfo);
+ return cellInfo;
+ },
+
+ get bounds() {
+ if (!this._bounds) {
+ this._bounds = Utils.getBounds(this.accessibleForBounds);
+ }
+
+ return this._bounds.clone();
+ },
+
+ _isDefunct: function _isDefunct(aAccessible) {
+ try {
+ return Utils.getState(aAccessible).contains(States.DEFUNCT);
+ } catch (x) {
+ return true;
+ }
+ }
+};
+
+this.PrefCache = function PrefCache(aName, aCallback, aRunCallbackNow) { // jshint ignore:line
+ this.name = aName;
+ this.callback = aCallback;
+
+ let branch = Services.prefs;
+ this.value = this._getValue(branch);
+
+ if (this.callback && aRunCallbackNow) {
+ try {
+ this.callback(this.name, this.value, true);
+ } catch (x) {
+ Logger.logException(x);
+ }
+ }
+
+ branch.addObserver(aName, this, true);
+};
+
+PrefCache.prototype = {
+ _getValue: function _getValue(aBranch) {
+ try {
+ if (!this.type) {
+ this.type = aBranch.getPrefType(this.name);
+ }
+ switch (this.type) {
+ case Ci.nsIPrefBranch.PREF_STRING:
+ return aBranch.getCharPref(this.name);
+ case Ci.nsIPrefBranch.PREF_INT:
+ return aBranch.getIntPref(this.name);
+ case Ci.nsIPrefBranch.PREF_BOOL:
+ return aBranch.getBoolPref(this.name);
+ default:
+ return null;
+ }
+ } catch (x) {
+ // Pref does not exist.
+ return null;
+ }
+ },
+
+ observe: function observe(aSubject) {
+ this.value = this._getValue(aSubject.QueryInterface(Ci.nsIPrefBranch));
+ Logger.info('pref changed', this.name, this.value);
+ if (this.callback) {
+ try {
+ this.callback(this.name, this.value, false);
+ } catch (x) {
+ Logger.logException(x);
+ }
+ }
+ },
+
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference])
+};
+
+this.SettingCache = function SettingCache(aName, aCallback, aOptions = {}) { // jshint ignore:line
+ this.value = aOptions.defaultValue;
+ let runCallback = () => {
+ if (aCallback) {
+ aCallback(aName, this.value);
+ if (aOptions.callbackOnce) {
+ runCallback = () => {};
+ }
+ }
+ };
+
+ let settings = Utils.win.navigator.mozSettings;
+ if (!settings) {
+ if (aOptions.callbackNow) {
+ runCallback();
+ }
+ return;
+ }
+
+
+ let lock = settings.createLock();
+ let req = lock.get(aName);
+
+ req.addEventListener('success', () => {
+ this.value = req.result[aName] === undefined ?
+ aOptions.defaultValue : req.result[aName];
+ if (aOptions.callbackNow) {
+ runCallback();
+ }
+ });
+
+ settings.addObserver(aName,
+ (evt) => {
+ this.value = evt.settingValue;
+ runCallback();
+ });
+};