gecko-dev/accessible/jsat/Utils.jsm
2016-08-09 15:38:54 -04:00

1115 lines
31 KiB
JavaScript

/* 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();
});
};