summaryrefslogtreecommitdiffstats
path: root/accessible/jsat
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /accessible/jsat
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'accessible/jsat')
-rw-r--r--accessible/jsat/AccessFu.css59
-rw-r--r--accessible/jsat/AccessFu.jsm1000
-rw-r--r--accessible/jsat/Constants.jsm59
-rw-r--r--accessible/jsat/ContentControl.jsm528
-rw-r--r--accessible/jsat/EventManager.jsm723
-rw-r--r--accessible/jsat/Gestures.jsm956
-rw-r--r--accessible/jsat/OutputGenerator.jsm1003
-rw-r--r--accessible/jsat/PointerAdapter.jsm174
-rw-r--r--accessible/jsat/Presentation.jsm769
-rw-r--r--accessible/jsat/Traversal.jsm419
-rw-r--r--accessible/jsat/Utils.jsm1114
-rw-r--r--accessible/jsat/content-script.js151
-rw-r--r--accessible/jsat/jar.mn10
-rw-r--r--accessible/jsat/moz.build20
-rw-r--r--accessible/jsat/sounds/clicked.oggbin0 -> 6618 bytes
-rw-r--r--accessible/jsat/sounds/virtual_cursor_key.oggbin0 -> 4224 bytes
-rw-r--r--accessible/jsat/sounds/virtual_cursor_move.oggbin0 -> 5636 bytes
17 files changed, 6985 insertions, 0 deletions
diff --git a/accessible/jsat/AccessFu.css b/accessible/jsat/AccessFu.css
new file mode 100644
index 000000000..d3930ff09
--- /dev/null
+++ b/accessible/jsat/AccessFu.css
@@ -0,0 +1,59 @@
+/* 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/. */
+
+#virtual-cursor-box {
+ position: fixed;
+ border: 1px solid orange;
+ pointer-events: none;
+ display: none;
+ border-radius: 2px;
+ box-shadow: 1px 1px 1px #444;
+ display: none;
+ z-index: 10;
+}
+
+#virtual-cursor-box.show {
+ display: block;
+}
+
+#virtual-cursor-box > div {
+ border-radius: 1px;
+ box-shadow: inset 1px 1px 1px #444;
+ display: block;
+ box-sizing: border-box;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+}
+
+#announce-box {
+ position: fixed;
+ width: 7.5em;
+ height: 5em;
+ top: calc(100% - 50% - 2.5em);
+ left: calc(100% - 50% - 3.75em);
+ pointer-events: none;
+ display: table;
+ font-size: 28pt;
+ font-weight: 700;
+ color: orange;
+ background-color: black;
+ border-radius: 0.25em;
+}
+
+#announce-box:not(.showing) {
+ opacity: 0.0;
+ -moz-transition: opacity 0.4s linear;
+}
+
+#announce-box.showing {
+ opacity: 1.0;
+ -moz-transition: opacity 0.2s linear;
+}
+
+#announce-box * {
+ text-align: center;
+ display: table-cell;
+ vertical-align: middle;
+}
diff --git a/accessible/jsat/AccessFu.jsm b/accessible/jsat/AccessFu.jsm
new file mode 100644
index 000000000..c6b16b38f
--- /dev/null
+++ b/accessible/jsat/AccessFu.jsm
@@ -0,0 +1,1000 @@
+/* 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 AccessFu, Components, Utils, PrefCache, Logger, Services,
+ PointerAdapter, dump, Presentation, Rect */
+/* exported AccessFu */
+
+'use strict';
+
+const {utils: Cu, interfaces: Ci} = Components;
+
+this.EXPORTED_SYMBOLS = ['AccessFu']; // jshint ignore:line
+
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/accessibility/Utils.jsm');
+
+const ACCESSFU_DISABLE = 0; // jshint ignore:line
+const ACCESSFU_ENABLE = 1;
+const ACCESSFU_AUTO = 2;
+
+const SCREENREADER_SETTING = 'accessibility.screenreader';
+const QUICKNAV_MODES_PREF = 'accessibility.accessfu.quicknav_modes';
+const QUICKNAV_INDEX_PREF = 'accessibility.accessfu.quicknav_index';
+
+this.AccessFu = { // jshint ignore:line
+ /**
+ * Initialize chrome-layer accessibility functionality.
+ * If accessibility is enabled on the platform, then a special accessibility
+ * mode is started.
+ */
+ attach: function attach(aWindow) {
+ Utils.init(aWindow);
+
+ try {
+ Services.androidBridge.handleGeckoMessage(
+ { type: 'Accessibility:Ready' });
+ Services.obs.addObserver(this, 'Accessibility:Settings', false);
+ } catch (x) {
+ // Not on Android
+ if (aWindow.navigator.mozSettings) {
+ let lock = aWindow.navigator.mozSettings.createLock();
+ let req = lock.get(SCREENREADER_SETTING);
+ req.addEventListener('success', () => {
+ this._systemPref = req.result[SCREENREADER_SETTING];
+ this._enableOrDisable();
+ });
+ aWindow.navigator.mozSettings.addObserver(
+ SCREENREADER_SETTING, this.handleEvent);
+ }
+ }
+
+ this._activatePref = new PrefCache(
+ 'accessibility.accessfu.activate', this._enableOrDisable.bind(this));
+
+ this._enableOrDisable();
+ },
+
+ /**
+ * Shut down chrome-layer accessibility functionality from the outside.
+ */
+ detach: function detach() {
+ // Avoid disabling twice.
+ if (this._enabled) {
+ this._disable();
+ }
+ if (Utils.MozBuildApp === 'mobile/android') {
+ Services.obs.removeObserver(this, 'Accessibility:Settings');
+ } else if (Utils.win.navigator.mozSettings) {
+ Utils.win.navigator.mozSettings.removeObserver(
+ SCREENREADER_SETTING, this.handleEvent);
+ }
+ delete this._activatePref;
+ Utils.uninit();
+ },
+
+ /**
+ * A lazy getter for event handler that binds the scope to AccessFu object.
+ */
+ get handleEvent() {
+ delete this.handleEvent;
+ this.handleEvent = this._handleEvent.bind(this);
+ return this.handleEvent;
+ },
+
+ /**
+ * Start AccessFu mode, this primarily means controlling the virtual cursor
+ * with arrow keys.
+ */
+ _enable: function _enable() {
+ if (this._enabled) {
+ return;
+ }
+ this._enabled = true;
+
+ Cu.import('resource://gre/modules/accessibility/Utils.jsm');
+ Cu.import('resource://gre/modules/accessibility/PointerAdapter.jsm');
+ Cu.import('resource://gre/modules/accessibility/Presentation.jsm');
+
+ for (let mm of Utils.AllMessageManagers) {
+ this._addMessageListeners(mm);
+ this._loadFrameScript(mm);
+ }
+
+ // Add stylesheet
+ let stylesheetURL = 'chrome://global/content/accessibility/AccessFu.css';
+ let stylesheet = Utils.win.document.createProcessingInstruction(
+ 'xml-stylesheet', 'href="' + stylesheetURL + '" type="text/css"');
+ Utils.win.document.insertBefore(stylesheet, Utils.win.document.firstChild);
+ this.stylesheet = Cu.getWeakReference(stylesheet);
+
+
+ // Populate quicknav modes
+ this._quicknavModesPref =
+ new PrefCache(QUICKNAV_MODES_PREF, (aName, aValue, aFirstRun) => {
+ this.Input.quickNavMode.updateModes(aValue);
+ if (!aFirstRun) {
+ // If the modes change, reset the current mode index to 0.
+ Services.prefs.setIntPref(QUICKNAV_INDEX_PREF, 0);
+ }
+ }, true);
+
+ this._quicknavCurrentModePref =
+ new PrefCache(QUICKNAV_INDEX_PREF, (aName, aValue) => {
+ this.Input.quickNavMode.updateCurrentMode(Number(aValue));
+ }, true);
+
+ // Check for output notification
+ this._notifyOutputPref =
+ new PrefCache('accessibility.accessfu.notify_output');
+
+
+ this.Input.start();
+ Output.start();
+ PointerAdapter.start();
+
+ Services.obs.addObserver(this, 'remote-browser-shown', false);
+ Services.obs.addObserver(this, 'inprocess-browser-shown', false);
+ Services.obs.addObserver(this, 'Accessibility:NextObject', false);
+ Services.obs.addObserver(this, 'Accessibility:PreviousObject', false);
+ Services.obs.addObserver(this, 'Accessibility:Focus', false);
+ Services.obs.addObserver(this, 'Accessibility:ActivateObject', false);
+ Services.obs.addObserver(this, 'Accessibility:LongPress', false);
+ Services.obs.addObserver(this, 'Accessibility:ScrollForward', false);
+ Services.obs.addObserver(this, 'Accessibility:ScrollBackward', false);
+ Services.obs.addObserver(this, 'Accessibility:MoveByGranularity', false);
+ Utils.win.addEventListener('TabOpen', this);
+ Utils.win.addEventListener('TabClose', this);
+ Utils.win.addEventListener('TabSelect', this);
+
+ if (this.readyCallback) {
+ this.readyCallback();
+ delete this.readyCallback;
+ }
+
+ Logger.info('AccessFu:Enabled');
+ },
+
+ /**
+ * Disable AccessFu and return to default interaction mode.
+ */
+ _disable: function _disable() {
+ if (!this._enabled) {
+ return;
+ }
+
+ this._enabled = false;
+
+ Utils.win.document.removeChild(this.stylesheet.get());
+
+ for (let mm of Utils.AllMessageManagers) {
+ mm.sendAsyncMessage('AccessFu:Stop');
+ this._removeMessageListeners(mm);
+ }
+
+ this.Input.stop();
+ Output.stop();
+ PointerAdapter.stop();
+
+ Utils.win.removeEventListener('TabOpen', this);
+ Utils.win.removeEventListener('TabClose', this);
+ Utils.win.removeEventListener('TabSelect', this);
+
+ Services.obs.removeObserver(this, 'remote-browser-shown');
+ Services.obs.removeObserver(this, 'inprocess-browser-shown');
+ Services.obs.removeObserver(this, 'Accessibility:NextObject');
+ Services.obs.removeObserver(this, 'Accessibility:PreviousObject');
+ Services.obs.removeObserver(this, 'Accessibility:Focus');
+ Services.obs.removeObserver(this, 'Accessibility:ActivateObject');
+ Services.obs.removeObserver(this, 'Accessibility:LongPress');
+ Services.obs.removeObserver(this, 'Accessibility:ScrollForward');
+ Services.obs.removeObserver(this, 'Accessibility:ScrollBackward');
+ Services.obs.removeObserver(this, 'Accessibility:MoveByGranularity');
+
+ delete this._quicknavModesPref;
+ delete this._notifyOutputPref;
+
+ if (this.doneCallback) {
+ this.doneCallback();
+ delete this.doneCallback;
+ }
+
+ Logger.info('AccessFu:Disabled');
+ },
+
+ _enableOrDisable: function _enableOrDisable() {
+ try {
+ if (!this._activatePref) {
+ return;
+ }
+ let activatePref = this._activatePref.value;
+ if (activatePref == ACCESSFU_ENABLE ||
+ this._systemPref && activatePref == ACCESSFU_AUTO) {
+ this._enable();
+ } else {
+ this._disable();
+ }
+ } catch (x) {
+ dump('Error ' + x.message + ' ' + x.fileName + ':' + x.lineNumber);
+ }
+ },
+
+ receiveMessage: function receiveMessage(aMessage) {
+ Logger.debug(() => {
+ return ['Recieved', aMessage.name, JSON.stringify(aMessage.json)];
+ });
+
+ switch (aMessage.name) {
+ case 'AccessFu:Ready':
+ let mm = Utils.getMessageManager(aMessage.target);
+ if (this._enabled) {
+ mm.sendAsyncMessage('AccessFu:Start',
+ {method: 'start', buildApp: Utils.MozBuildApp});
+ }
+ break;
+ case 'AccessFu:Present':
+ this._output(aMessage.json, aMessage.target);
+ break;
+ case 'AccessFu:Input':
+ this.Input.setEditState(aMessage.json);
+ break;
+ case 'AccessFu:DoScroll':
+ this.Input.doScroll(aMessage.json);
+ break;
+ }
+ },
+
+ _output: function _output(aPresentationData, aBrowser) {
+ if (!Utils.isAliveAndVisible(
+ Utils.AccService.getAccessibleFor(aBrowser))) {
+ return;
+ }
+ for (let presenter of aPresentationData) {
+ if (!presenter) {
+ continue;
+ }
+
+ try {
+ Output[presenter.type](presenter.details, aBrowser);
+ } catch (x) {
+ Logger.logException(x);
+ }
+ }
+
+ if (this._notifyOutputPref.value) {
+ Services.obs.notifyObservers(null, 'accessibility-output',
+ JSON.stringify(aPresentationData));
+ }
+ },
+
+ _loadFrameScript: function _loadFrameScript(aMessageManager) {
+ if (this._processedMessageManagers.indexOf(aMessageManager) < 0) {
+ aMessageManager.loadFrameScript(
+ 'chrome://global/content/accessibility/content-script.js', true);
+ this._processedMessageManagers.push(aMessageManager);
+ } else if (this._enabled) {
+ // If the content-script is already loaded and AccessFu is enabled,
+ // send an AccessFu:Start message.
+ aMessageManager.sendAsyncMessage('AccessFu:Start',
+ {method: 'start', buildApp: Utils.MozBuildApp});
+ }
+ },
+
+ _addMessageListeners: function _addMessageListeners(aMessageManager) {
+ aMessageManager.addMessageListener('AccessFu:Present', this);
+ aMessageManager.addMessageListener('AccessFu:Input', this);
+ aMessageManager.addMessageListener('AccessFu:Ready', this);
+ aMessageManager.addMessageListener('AccessFu:DoScroll', this);
+ },
+
+ _removeMessageListeners: function _removeMessageListeners(aMessageManager) {
+ aMessageManager.removeMessageListener('AccessFu:Present', this);
+ aMessageManager.removeMessageListener('AccessFu:Input', this);
+ aMessageManager.removeMessageListener('AccessFu:Ready', this);
+ aMessageManager.removeMessageListener('AccessFu:DoScroll', this);
+ },
+
+ _handleMessageManager: function _handleMessageManager(aMessageManager) {
+ if (this._enabled) {
+ this._addMessageListeners(aMessageManager);
+ }
+ this._loadFrameScript(aMessageManager);
+ },
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case 'Accessibility:Settings':
+ this._systemPref = JSON.parse(aData).enabled;
+ this._enableOrDisable();
+ break;
+ case 'Accessibility:NextObject':
+ case 'Accessibility:PreviousObject':
+ {
+ let rule = aData ?
+ aData.substr(0, 1).toUpperCase() + aData.substr(1).toLowerCase() :
+ 'Simple';
+ let method = aTopic.replace(/Accessibility:(\w+)Object/, 'move$1');
+ this.Input.moveCursor(method, rule, 'gesture');
+ break;
+ }
+ case 'Accessibility:ActivateObject':
+ this.Input.activateCurrent(JSON.parse(aData));
+ break;
+ case 'Accessibility:LongPress':
+ this.Input.sendContextMenuMessage();
+ break;
+ case 'Accessibility:ScrollForward':
+ this.Input.androidScroll('forward');
+ break;
+ case 'Accessibility:ScrollBackward':
+ this.Input.androidScroll('backward');
+ break;
+ case 'Accessibility:Focus':
+ this._focused = JSON.parse(aData);
+ if (this._focused) {
+ this.autoMove({ forcePresent: true, noOpIfOnScreen: true });
+ }
+ break;
+ case 'Accessibility:MoveByGranularity':
+ this.Input.moveByGranularity(JSON.parse(aData));
+ break;
+ case 'remote-browser-shown':
+ case 'inprocess-browser-shown':
+ {
+ // Ignore notifications that aren't from a BrowserOrApp
+ let frameLoader = aSubject.QueryInterface(Ci.nsIFrameLoader);
+ if (!frameLoader.ownerIsMozBrowserOrAppFrame) {
+ return;
+ }
+ this._handleMessageManager(frameLoader.messageManager);
+ break;
+ }
+ }
+ },
+
+ _handleEvent: function _handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case 'TabOpen':
+ {
+ let mm = Utils.getMessageManager(aEvent.target);
+ this._handleMessageManager(mm);
+ break;
+ }
+ case 'TabClose':
+ {
+ let mm = Utils.getMessageManager(aEvent.target);
+ let mmIndex = this._processedMessageManagers.indexOf(mm);
+ if (mmIndex > -1) {
+ this._removeMessageListeners(mm);
+ this._processedMessageManagers.splice(mmIndex, 1);
+ }
+ break;
+ }
+ case 'TabSelect':
+ {
+ if (this._focused) {
+ // We delay this for half a second so the awesomebar could close,
+ // and we could use the current coordinates for the content item.
+ // XXX TODO figure out how to avoid magic wait here.
+ this.autoMove({
+ delay: 500,
+ forcePresent: true,
+ noOpIfOnScreen: true,
+ moveMethod: 'moveFirst' });
+ }
+ break;
+ }
+ default:
+ {
+ // A settings change, it does not have an event type
+ if (aEvent.settingName == SCREENREADER_SETTING) {
+ this._systemPref = aEvent.settingValue;
+ this._enableOrDisable();
+ }
+ break;
+ }
+ }
+ },
+
+ autoMove: function autoMove(aOptions) {
+ let mm = Utils.getMessageManager(Utils.CurrentBrowser);
+ mm.sendAsyncMessage('AccessFu:AutoMove', aOptions);
+ },
+
+ announce: function announce(aAnnouncement) {
+ this._output(Presentation.announce(aAnnouncement), Utils.CurrentBrowser);
+ },
+
+ // So we don't enable/disable twice
+ _enabled: false,
+
+ // Layerview is focused
+ _focused: false,
+
+ // Keep track of message managers tha already have a 'content-script.js'
+ // injected.
+ _processedMessageManagers: [],
+
+ /**
+ * Adjusts the given bounds relative to the given browser.
+ * @param {Rect} aJsonBounds the bounds to adjust
+ * @param {browser} aBrowser the browser we want the bounds relative to
+ * @param {bool} aToCSSPixels whether to convert to CSS pixels (as opposed to
+ * device pixels)
+ */
+ adjustContentBounds:
+ function(aJsonBounds, aBrowser, aToCSSPixels) {
+ let bounds = new Rect(aJsonBounds.left, aJsonBounds.top,
+ aJsonBounds.right - aJsonBounds.left,
+ aJsonBounds.bottom - aJsonBounds.top);
+ let win = Utils.win;
+ let dpr = win.devicePixelRatio;
+ let offset = { left: -win.mozInnerScreenX, top: -win.mozInnerScreenY };
+
+ // Add the offset; the offset is in CSS pixels, so multiply the
+ // devicePixelRatio back in before adding to preserve unit consistency.
+ bounds = bounds.translate(offset.left * dpr, offset.top * dpr);
+
+ // If we want to get to CSS pixels from device pixels, this needs to be
+ // further divided by the devicePixelRatio due to widget scaling.
+ if (aToCSSPixels) {
+ bounds = bounds.scale(1 / dpr, 1 / dpr);
+ }
+
+ return bounds.expandToIntegers();
+ }
+};
+
+var Output = {
+ brailleState: {
+ startOffset: 0,
+ endOffset: 0,
+ text: '',
+ selectionStart: 0,
+ selectionEnd: 0,
+
+ init: function init(aOutput) {
+ if (aOutput && 'output' in aOutput) {
+ this.startOffset = aOutput.startOffset;
+ this.endOffset = aOutput.endOffset;
+ // We need to append a space at the end so that the routing key
+ // corresponding to the end of the output (i.e. the space) can be hit to
+ // move the caret there.
+ this.text = aOutput.output + ' ';
+ this.selectionStart = typeof aOutput.selectionStart === 'number' ?
+ aOutput.selectionStart : this.selectionStart;
+ this.selectionEnd = typeof aOutput.selectionEnd === 'number' ?
+ aOutput.selectionEnd : this.selectionEnd;
+
+ return { text: this.text,
+ selectionStart: this.selectionStart,
+ selectionEnd: this.selectionEnd };
+ }
+
+ return null;
+ },
+
+ adjustText: function adjustText(aText) {
+ let newBraille = [];
+ let braille = {};
+
+ let prefix = this.text.substring(0, this.startOffset).trim();
+ if (prefix) {
+ prefix += ' ';
+ newBraille.push(prefix);
+ }
+
+ newBraille.push(aText);
+
+ let suffix = this.text.substring(this.endOffset).trim();
+ if (suffix) {
+ suffix = ' ' + suffix;
+ newBraille.push(suffix);
+ }
+
+ this.startOffset = braille.startOffset = prefix.length;
+ this.text = braille.text = newBraille.join('') + ' ';
+ this.endOffset = braille.endOffset = braille.text.length - suffix.length;
+ braille.selectionStart = this.selectionStart;
+ braille.selectionEnd = this.selectionEnd;
+
+ return braille;
+ },
+
+ adjustSelection: function adjustSelection(aSelection) {
+ let braille = {};
+
+ braille.startOffset = this.startOffset;
+ braille.endOffset = this.endOffset;
+ braille.text = this.text;
+ this.selectionStart = braille.selectionStart =
+ aSelection.selectionStart + this.startOffset;
+ this.selectionEnd = braille.selectionEnd =
+ aSelection.selectionEnd + this.startOffset;
+
+ return braille;
+ }
+ },
+
+ start: function start() {
+ Cu.import('resource://gre/modules/Geometry.jsm');
+ },
+
+ stop: function stop() {
+ if (this.highlightBox) {
+ let highlightBox = this.highlightBox.get();
+ if (highlightBox) {
+ highlightBox.remove();
+ }
+ delete this.highlightBox;
+ }
+ },
+
+ B2G: function B2G(aDetails) {
+ Utils.dispatchChromeEvent('accessibility-output', aDetails);
+ },
+
+ Visual: function Visual(aDetail, aBrowser) {
+ switch (aDetail.eventType) {
+ case 'viewport-change':
+ case 'vc-change':
+ {
+ let highlightBox = null;
+ if (!this.highlightBox) {
+ let doc = Utils.win.document;
+ // Add highlight box
+ highlightBox = Utils.win.document.
+ createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ let parent = doc.body || doc.documentElement;
+ parent.appendChild(highlightBox);
+ highlightBox.id = 'virtual-cursor-box';
+
+ // Add highlight inset for inner shadow
+ highlightBox.appendChild(
+ doc.createElementNS('http://www.w3.org/1999/xhtml', 'div'));
+
+ this.highlightBox = Cu.getWeakReference(highlightBox);
+ } else {
+ highlightBox = this.highlightBox.get();
+ }
+
+ let padding = aDetail.padding;
+ let r = AccessFu.adjustContentBounds(aDetail.bounds, aBrowser, true);
+
+ // First hide it to avoid flickering when changing the style.
+ highlightBox.classList.remove('show');
+ highlightBox.style.top = (r.top - padding) + 'px';
+ highlightBox.style.left = (r.left - padding) + 'px';
+ highlightBox.style.width = (r.width + padding*2) + 'px';
+ highlightBox.style.height = (r.height + padding*2) + 'px';
+ highlightBox.classList.add('show');
+
+ break;
+ }
+ case 'tabstate-change':
+ {
+ let highlightBox = this.highlightBox ? this.highlightBox.get() : null;
+ if (highlightBox) {
+ highlightBox.classList.remove('show');
+ }
+ break;
+ }
+ }
+ },
+
+ get androidBridge() {
+ delete this.androidBridge;
+ if (Utils.MozBuildApp === 'mobile/android') {
+ this.androidBridge = Services.androidBridge;
+ } else {
+ this.androidBridge = null;
+ }
+ return this.androidBridge;
+ },
+
+ Android: function Android(aDetails, aBrowser) {
+ const ANDROID_VIEW_TEXT_CHANGED = 0x10;
+ const ANDROID_VIEW_TEXT_SELECTION_CHANGED = 0x2000;
+
+ if (!this.androidBridge) {
+ return;
+ }
+
+ for (let androidEvent of aDetails) {
+ androidEvent.type = 'Accessibility:Event';
+ if (androidEvent.bounds) {
+ androidEvent.bounds = AccessFu.adjustContentBounds(
+ androidEvent.bounds, aBrowser);
+ }
+
+ switch(androidEvent.eventType) {
+ case ANDROID_VIEW_TEXT_CHANGED:
+ androidEvent.brailleOutput = this.brailleState.adjustText(
+ androidEvent.text);
+ break;
+ case ANDROID_VIEW_TEXT_SELECTION_CHANGED:
+ androidEvent.brailleOutput = this.brailleState.adjustSelection(
+ androidEvent.brailleOutput);
+ break;
+ default:
+ androidEvent.brailleOutput = this.brailleState.init(
+ androidEvent.brailleOutput);
+ break;
+ }
+ this.androidBridge.handleGeckoMessage(androidEvent);
+ }
+ },
+
+ Braille: function Braille(aDetails) {
+ Logger.debug('Braille output: ' + aDetails.output);
+ }
+};
+
+var Input = {
+ editState: {},
+
+ start: function start() {
+ // XXX: This is too disruptive on desktop for now.
+ // Might need to add special modifiers.
+ if (Utils.MozBuildApp != 'browser') {
+ Utils.win.document.addEventListener('keypress', this, true);
+ }
+ Utils.win.addEventListener('mozAccessFuGesture', this, true);
+ },
+
+ stop: function stop() {
+ if (Utils.MozBuildApp != 'browser') {
+ Utils.win.document.removeEventListener('keypress', this, true);
+ }
+ Utils.win.removeEventListener('mozAccessFuGesture', this, true);
+ },
+
+ handleEvent: function Input_handleEvent(aEvent) {
+ try {
+ switch (aEvent.type) {
+ case 'keypress':
+ this._handleKeypress(aEvent);
+ break;
+ case 'mozAccessFuGesture':
+ this._handleGesture(aEvent.detail);
+ break;
+ }
+ } catch (x) {
+ Logger.logException(x);
+ }
+ },
+
+ _handleGesture: function _handleGesture(aGesture) {
+ let gestureName = aGesture.type + aGesture.touches.length;
+ Logger.debug('Gesture', aGesture.type,
+ '(fingers: ' + aGesture.touches.length + ')');
+
+ switch (gestureName) {
+ case 'dwell1':
+ case 'explore1':
+ this.moveToPoint('Simple', aGesture.touches[0].x,
+ aGesture.touches[0].y);
+ break;
+ case 'doubletap1':
+ this.activateCurrent();
+ break;
+ case 'doubletaphold1':
+ Utils.dispatchChromeEvent('accessibility-control', 'quicknav-menu');
+ break;
+ case 'swiperight1':
+ this.moveCursor('moveNext', 'Simple', 'gestures');
+ break;
+ case 'swipeleft1':
+ this.moveCursor('movePrevious', 'Simple', 'gesture');
+ break;
+ case 'swipeup1':
+ this.moveCursor(
+ 'movePrevious', this.quickNavMode.current, 'gesture', true);
+ break;
+ case 'swipedown1':
+ this.moveCursor('moveNext', this.quickNavMode.current, 'gesture', true);
+ break;
+ case 'exploreend1':
+ case 'dwellend1':
+ this.activateCurrent(null, true);
+ break;
+ case 'swiperight2':
+ if (aGesture.edge) {
+ Utils.dispatchChromeEvent('accessibility-control',
+ 'edge-swipe-right');
+ break;
+ }
+ this.sendScrollMessage(-1, true);
+ break;
+ case 'swipedown2':
+ if (aGesture.edge) {
+ Utils.dispatchChromeEvent('accessibility-control', 'edge-swipe-down');
+ break;
+ }
+ this.sendScrollMessage(-1);
+ break;
+ case 'swipeleft2':
+ if (aGesture.edge) {
+ Utils.dispatchChromeEvent('accessibility-control', 'edge-swipe-left');
+ break;
+ }
+ this.sendScrollMessage(1, true);
+ break;
+ case 'swipeup2':
+ if (aGesture.edge) {
+ Utils.dispatchChromeEvent('accessibility-control', 'edge-swipe-up');
+ break;
+ }
+ this.sendScrollMessage(1);
+ break;
+ case 'explore2':
+ Utils.CurrentBrowser.contentWindow.scrollBy(
+ -aGesture.deltaX, -aGesture.deltaY);
+ break;
+ case 'swiperight3':
+ this.moveCursor('moveNext', this.quickNavMode.current, 'gesture');
+ break;
+ case 'swipeleft3':
+ this.moveCursor('movePrevious', this.quickNavMode.current, 'gesture');
+ break;
+ case 'swipedown3':
+ this.quickNavMode.next();
+ AccessFu.announce('quicknav_' + this.quickNavMode.current);
+ break;
+ case 'swipeup3':
+ this.quickNavMode.previous();
+ AccessFu.announce('quicknav_' + this.quickNavMode.current);
+ break;
+ case 'tripletap3':
+ Utils.dispatchChromeEvent('accessibility-control', 'toggle-shade');
+ break;
+ case 'tap2':
+ Utils.dispatchChromeEvent('accessibility-control', 'toggle-pause');
+ break;
+ }
+ },
+
+ _handleKeypress: function _handleKeypress(aEvent) {
+ let target = aEvent.target;
+
+ // Ignore keys with modifiers so the content could take advantage of them.
+ if (aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey) {
+ return;
+ }
+
+ switch (aEvent.keyCode) {
+ case 0:
+ // an alphanumeric key was pressed, handle it separately.
+ // If it was pressed with either alt or ctrl, just pass through.
+ // If it was pressed with meta, pass the key on without the meta.
+ if (this.editState.editing) {
+ return;
+ }
+
+ let key = String.fromCharCode(aEvent.charCode);
+ try {
+ let [methodName, rule] = this.keyMap[key];
+ this.moveCursor(methodName, rule, 'keyboard');
+ } catch (x) {
+ return;
+ }
+ break;
+ case aEvent.DOM_VK_RIGHT:
+ if (this.editState.editing) {
+ if (!this.editState.atEnd) {
+ // Don't move forward if caret is not at end of entry.
+ // XXX: Fix for rtl
+ return;
+ } else {
+ target.blur();
+ }
+ }
+ this.moveCursor(aEvent.shiftKey ?
+ 'moveLast' : 'moveNext', 'Simple', 'keyboard');
+ break;
+ case aEvent.DOM_VK_LEFT:
+ if (this.editState.editing) {
+ if (!this.editState.atStart) {
+ // Don't move backward if caret is not at start of entry.
+ // XXX: Fix for rtl
+ return;
+ } else {
+ target.blur();
+ }
+ }
+ this.moveCursor(aEvent.shiftKey ?
+ 'moveFirst' : 'movePrevious', 'Simple', 'keyboard');
+ break;
+ case aEvent.DOM_VK_UP:
+ if (this.editState.multiline) {
+ if (!this.editState.atStart) {
+ // Don't blur content if caret is not at start of text area.
+ return;
+ } else {
+ target.blur();
+ }
+ }
+
+ if (Utils.MozBuildApp == 'mobile/android') {
+ // Return focus to native Android browser chrome.
+ Services.androidBridge.handleGeckoMessage(
+ { type: 'ToggleChrome:Focus' });
+ }
+ break;
+ case aEvent.DOM_VK_RETURN:
+ if (this.editState.editing) {
+ return;
+ }
+ this.activateCurrent();
+ break;
+ default:
+ return;
+ }
+
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ },
+
+ moveToPoint: function moveToPoint(aRule, aX, aY) {
+ // XXX: Bug 1013408 - There is no alignment between the chrome window's
+ // viewport size and the content viewport size in Android. This makes
+ // sending mouse events beyond its bounds impossible.
+ if (Utils.MozBuildApp === 'mobile/android') {
+ let mm = Utils.getMessageManager(Utils.CurrentBrowser);
+ mm.sendAsyncMessage('AccessFu:MoveToPoint',
+ {rule: aRule, x: aX, y: aY, origin: 'top'});
+ } else {
+ let win = Utils.win;
+ Utils.winUtils.sendMouseEvent('mousemove',
+ aX - win.mozInnerScreenX, aY - win.mozInnerScreenY, 0, 0, 0);
+ }
+ },
+
+ moveCursor: function moveCursor(aAction, aRule, aInputType, aAdjustRange) {
+ let mm = Utils.getMessageManager(Utils.CurrentBrowser);
+ mm.sendAsyncMessage('AccessFu:MoveCursor',
+ { action: aAction, rule: aRule,
+ origin: 'top', inputType: aInputType,
+ adjustRange: aAdjustRange });
+ },
+
+ androidScroll: function androidScroll(aDirection) {
+ let mm = Utils.getMessageManager(Utils.CurrentBrowser);
+ mm.sendAsyncMessage('AccessFu:AndroidScroll',
+ { direction: aDirection, origin: 'top' });
+ },
+
+ moveByGranularity: function moveByGranularity(aDetails) {
+ const GRANULARITY_PARAGRAPH = 8;
+ const GRANULARITY_LINE = 4;
+
+ if (!this.editState.editing) {
+ if (aDetails.granularity & (GRANULARITY_PARAGRAPH | GRANULARITY_LINE)) {
+ this.moveCursor('move' + aDetails.direction, 'Simple', 'gesture');
+ return;
+ }
+ } else {
+ aDetails.atStart = this.editState.atStart;
+ aDetails.atEnd = this.editState.atEnd;
+ }
+
+ let mm = Utils.getMessageManager(Utils.CurrentBrowser);
+ let type = this.editState.editing ? 'AccessFu:MoveCaret' :
+ 'AccessFu:MoveByGranularity';
+ mm.sendAsyncMessage(type, aDetails);
+ },
+
+ activateCurrent: function activateCurrent(aData, aActivateIfKey = false) {
+ let mm = Utils.getMessageManager(Utils.CurrentBrowser);
+ let offset = aData && typeof aData.keyIndex === 'number' ?
+ aData.keyIndex - Output.brailleState.startOffset : -1;
+
+ mm.sendAsyncMessage('AccessFu:Activate',
+ {offset: offset, activateIfKey: aActivateIfKey});
+ },
+
+ sendContextMenuMessage: function sendContextMenuMessage() {
+ let mm = Utils.getMessageManager(Utils.CurrentBrowser);
+ mm.sendAsyncMessage('AccessFu:ContextMenu', {});
+ },
+
+ setEditState: function setEditState(aEditState) {
+ Logger.debug(() => { return ['setEditState', JSON.stringify(aEditState)] });
+ this.editState = aEditState;
+ },
+
+ // XXX: This is here for backwards compatability with screen reader simulator
+ // it should be removed when the extension is updated on amo.
+ scroll: function scroll(aPage, aHorizontal) {
+ this.sendScrollMessage(aPage, aHorizontal);
+ },
+
+ sendScrollMessage: function sendScrollMessage(aPage, aHorizontal) {
+ let mm = Utils.getMessageManager(Utils.CurrentBrowser);
+ mm.sendAsyncMessage('AccessFu:Scroll',
+ {page: aPage, horizontal: aHorizontal, origin: 'top'});
+ },
+
+ doScroll: function doScroll(aDetails) {
+ let horizontal = aDetails.horizontal;
+ let page = aDetails.page;
+ let p = AccessFu.adjustContentBounds(
+ aDetails.bounds, Utils.CurrentBrowser, true).center();
+ Utils.winUtils.sendWheelEvent(p.x, p.y,
+ horizontal ? page : 0, horizontal ? 0 : page, 0,
+ Utils.win.WheelEvent.DOM_DELTA_PAGE, 0, 0, 0, 0);
+ },
+
+ get keyMap() {
+ delete this.keyMap;
+ this.keyMap = {
+ a: ['moveNext', 'Anchor'],
+ A: ['movePrevious', 'Anchor'],
+ b: ['moveNext', 'Button'],
+ B: ['movePrevious', 'Button'],
+ c: ['moveNext', 'Combobox'],
+ C: ['movePrevious', 'Combobox'],
+ d: ['moveNext', 'Landmark'],
+ D: ['movePrevious', 'Landmark'],
+ e: ['moveNext', 'Entry'],
+ E: ['movePrevious', 'Entry'],
+ f: ['moveNext', 'FormElement'],
+ F: ['movePrevious', 'FormElement'],
+ g: ['moveNext', 'Graphic'],
+ G: ['movePrevious', 'Graphic'],
+ h: ['moveNext', 'Heading'],
+ H: ['movePrevious', 'Heading'],
+ i: ['moveNext', 'ListItem'],
+ I: ['movePrevious', 'ListItem'],
+ k: ['moveNext', 'Link'],
+ K: ['movePrevious', 'Link'],
+ l: ['moveNext', 'List'],
+ L: ['movePrevious', 'List'],
+ p: ['moveNext', 'PageTab'],
+ P: ['movePrevious', 'PageTab'],
+ r: ['moveNext', 'RadioButton'],
+ R: ['movePrevious', 'RadioButton'],
+ s: ['moveNext', 'Separator'],
+ S: ['movePrevious', 'Separator'],
+ t: ['moveNext', 'Table'],
+ T: ['movePrevious', 'Table'],
+ x: ['moveNext', 'Checkbox'],
+ X: ['movePrevious', 'Checkbox']
+ };
+
+ return this.keyMap;
+ },
+
+ quickNavMode: {
+ get current() {
+ return this.modes[this._currentIndex];
+ },
+
+ previous: function quickNavMode_previous() {
+ Services.prefs.setIntPref(QUICKNAV_INDEX_PREF,
+ this._currentIndex > 0 ?
+ this._currentIndex - 1 : this.modes.length - 1);
+ },
+
+ next: function quickNavMode_next() {
+ Services.prefs.setIntPref(QUICKNAV_INDEX_PREF,
+ this._currentIndex + 1 >= this.modes.length ?
+ 0 : this._currentIndex + 1);
+ },
+
+ updateModes: function updateModes(aModes) {
+ if (aModes) {
+ this.modes = aModes.split(',');
+ } else {
+ this.modes = [];
+ }
+ },
+
+ updateCurrentMode: function updateCurrentMode(aModeIndex) {
+ Logger.debug('Quicknav mode:', this.modes[aModeIndex]);
+ this._currentIndex = aModeIndex;
+ }
+ }
+};
+AccessFu.Input = Input;
diff --git a/accessible/jsat/Constants.jsm b/accessible/jsat/Constants.jsm
new file mode 100644
index 000000000..152604431
--- /dev/null
+++ b/accessible/jsat/Constants.jsm
@@ -0,0 +1,59 @@
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+this.EXPORTED_SYMBOLS = ['Roles', 'Events', 'Relations',
+ 'Filters', 'States', 'Prefilters'];
+
+function ConstantsMap (aObject, aPrefix, aMap = {}, aModifier = null) {
+ let offset = aPrefix.length;
+ for (var name in aObject) {
+ if (name.indexOf(aPrefix) === 0) {
+ aMap[name.slice(offset)] = aModifier ?
+ aModifier(aObject[name]) : aObject[name];
+ }
+ }
+
+ return aMap;
+}
+
+XPCOMUtils.defineLazyGetter(
+ this, 'Roles',
+ function() {
+ return ConstantsMap(Ci.nsIAccessibleRole, 'ROLE_');
+ });
+
+XPCOMUtils.defineLazyGetter(
+ this, 'Events',
+ function() {
+ return ConstantsMap(Ci.nsIAccessibleEvent, 'EVENT_');
+ });
+
+XPCOMUtils.defineLazyGetter(
+ this, 'Relations',
+ function() {
+ return ConstantsMap(Ci.nsIAccessibleRelation, 'RELATION_');
+ });
+
+XPCOMUtils.defineLazyGetter(
+ this, 'Prefilters',
+ function() {
+ return ConstantsMap(Ci.nsIAccessibleTraversalRule, 'PREFILTER_');
+ });
+
+XPCOMUtils.defineLazyGetter(
+ this, 'Filters',
+ function() {
+ return ConstantsMap(Ci.nsIAccessibleTraversalRule, 'FILTER_');
+ });
+
+XPCOMUtils.defineLazyGetter(
+ this, 'States',
+ function() {
+ let statesMap = ConstantsMap(Ci.nsIAccessibleStates, 'STATE_', {},
+ (val) => { return { base: val, extended: 0 }; });
+ ConstantsMap(Ci.nsIAccessibleStates, 'EXT_STATE_', statesMap,
+ (val) => { return { base: 0, extended: val }; });
+ return statesMap;
+ });
diff --git a/accessible/jsat/ContentControl.jsm b/accessible/jsat/ContentControl.jsm
new file mode 100644
index 000000000..f5fd471ba
--- /dev/null
+++ b/accessible/jsat/ContentControl.jsm
@@ -0,0 +1,528 @@
+/* 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/. */
+
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, 'Services',
+ 'resource://gre/modules/Services.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
+ 'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
+ 'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Roles',
+ 'resource://gre/modules/accessibility/Constants.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'TraversalRules',
+ 'resource://gre/modules/accessibility/Traversal.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'TraversalHelper',
+ 'resource://gre/modules/accessibility/Traversal.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Presentation',
+ 'resource://gre/modules/accessibility/Presentation.jsm');
+
+this.EXPORTED_SYMBOLS = ['ContentControl'];
+
+const MOVEMENT_GRANULARITY_CHARACTER = 1;
+const MOVEMENT_GRANULARITY_WORD = 2;
+const MOVEMENT_GRANULARITY_PARAGRAPH = 8;
+
+this.ContentControl = function ContentControl(aContentScope) {
+ this._contentScope = Cu.getWeakReference(aContentScope);
+ this._childMessageSenders = new WeakMap();
+};
+
+this.ContentControl.prototype = {
+ messagesOfInterest: ['AccessFu:MoveCursor',
+ 'AccessFu:ClearCursor',
+ 'AccessFu:MoveToPoint',
+ 'AccessFu:AutoMove',
+ 'AccessFu:Activate',
+ 'AccessFu:MoveCaret',
+ 'AccessFu:MoveByGranularity',
+ 'AccessFu:AndroidScroll'],
+
+ start: function cc_start() {
+ let cs = this._contentScope.get();
+ for (let message of this.messagesOfInterest) {
+ cs.addMessageListener(message, this);
+ }
+ cs.addEventListener('mousemove', this);
+ },
+
+ stop: function cc_stop() {
+ let cs = this._contentScope.get();
+ for (let message of this.messagesOfInterest) {
+ cs.removeMessageListener(message, this);
+ }
+ cs.removeEventListener('mousemove', this);
+ },
+
+ get document() {
+ return this._contentScope.get().content.document;
+ },
+
+ get window() {
+ return this._contentScope.get().content;
+ },
+
+ get vc() {
+ return Utils.getVirtualCursor(this.document);
+ },
+
+ receiveMessage: function cc_receiveMessage(aMessage) {
+ Logger.debug(() => {
+ return ['ContentControl.receiveMessage',
+ aMessage.name,
+ JSON.stringify(aMessage.json)];
+ });
+
+ // If we get an explicit message, we should immediately cancel any autoMove
+ this.cancelAutoMove();
+
+ try {
+ let func = this['handle' + aMessage.name.slice(9)]; // 'AccessFu:'.length
+ if (func) {
+ func.bind(this)(aMessage);
+ } else {
+ Logger.warning('ContentControl: Unhandled message:', aMessage.name);
+ }
+ } catch (x) {
+ Logger.logException(
+ x, 'Error handling message: ' + JSON.stringify(aMessage.json));
+ }
+ },
+
+ handleAndroidScroll: function cc_handleAndroidScroll(aMessage) {
+ let vc = this.vc;
+ let position = vc.position;
+
+ if (aMessage.json.origin != 'child' && this.sendToChild(vc, aMessage)) {
+ // Forwarded succesfully to child cursor.
+ return;
+ }
+
+ // Counter-intuitive, but scrolling backward (ie. up), actually should
+ // increase range values.
+ if (this.adjustRange(position, aMessage.json.direction === 'backward')) {
+ return;
+ }
+
+ this._contentScope.get().sendAsyncMessage('AccessFu:DoScroll',
+ { bounds: Utils.getBounds(position, true),
+ page: aMessage.json.direction === 'forward' ? 1 : -1,
+ horizontal: false });
+ },
+
+ handleMoveCursor: function cc_handleMoveCursor(aMessage) {
+ let origin = aMessage.json.origin;
+ let action = aMessage.json.action;
+ let adjustRange = aMessage.json.adjustRange;
+ let vc = this.vc;
+
+ if (origin != 'child' && this.sendToChild(vc, aMessage)) {
+ // Forwarded succesfully to child cursor.
+ return;
+ }
+
+ if (adjustRange && this.adjustRange(vc.position, action === 'moveNext')) {
+ return;
+ }
+
+ let moved = TraversalHelper.move(vc, action, aMessage.json.rule);
+
+ if (moved) {
+ if (origin === 'child') {
+ // We just stepped out of a child, clear child cursor.
+ Utils.getMessageManager(aMessage.target).sendAsyncMessage(
+ 'AccessFu:ClearCursor', {});
+ } else {
+ // We potentially landed on a new child cursor. If so, we want to
+ // either be on the first or last item in the child doc.
+ let childAction = action;
+ if (action === 'moveNext') {
+ childAction = 'moveFirst';
+ } else if (action === 'movePrevious') {
+ childAction = 'moveLast';
+ }
+
+ // Attempt to forward move to a potential child cursor in our
+ // new position.
+ this.sendToChild(vc, aMessage, { action: childAction }, true);
+ }
+ } else if (!this._childMessageSenders.has(aMessage.target) &&
+ origin !== 'top') {
+ // We failed to move, and the message is not from a parent, so forward
+ // to it.
+ this.sendToParent(aMessage);
+ } else {
+ this._contentScope.get().sendAsyncMessage('AccessFu:Present',
+ Presentation.noMove(action));
+ }
+ },
+
+ handleEvent: function cc_handleEvent(aEvent) {
+ if (aEvent.type === 'mousemove') {
+ this.handleMoveToPoint(
+ { json: { x: aEvent.screenX, y: aEvent.screenY, rule: 'Simple' } });
+ }
+ if (!Utils.getMessageManager(aEvent.target)) {
+ aEvent.preventDefault();
+ } else {
+ aEvent.target.focus();
+ }
+ },
+
+ handleMoveToPoint: function cc_handleMoveToPoint(aMessage) {
+ let [x, y] = [aMessage.json.x, aMessage.json.y];
+ let rule = TraversalRules[aMessage.json.rule];
+
+ let dpr = this.window.devicePixelRatio;
+ this.vc.moveToPoint(rule, x * dpr, y * dpr, true);
+ },
+
+ handleClearCursor: function cc_handleClearCursor(aMessage) {
+ let forwarded = this.sendToChild(this.vc, aMessage);
+ this.vc.position = null;
+ if (!forwarded) {
+ this._contentScope.get().sendAsyncMessage('AccessFu:CursorCleared');
+ }
+ this.document.activeElement.blur();
+ },
+
+ handleAutoMove: function cc_handleAutoMove(aMessage) {
+ this.autoMove(null, aMessage.json);
+ },
+
+ handleActivate: function cc_handleActivate(aMessage) {
+ let activateAccessible = (aAccessible) => {
+ Logger.debug(() => {
+ return ['activateAccessible', Logger.accessibleToString(aAccessible)];
+ });
+ try {
+ if (aMessage.json.activateIfKey &&
+ !Utils.isActivatableOnFingerUp(aAccessible)) {
+ // Only activate keys, don't do anything on other objects.
+ return;
+ }
+ } catch (e) {
+ // accessible is invalid. Silently fail.
+ return;
+ }
+
+ if (aAccessible.actionCount > 0) {
+ aAccessible.doAction(0);
+ } else {
+ let control = Utils.getEmbeddedControl(aAccessible);
+ if (control && control.actionCount > 0) {
+ control.doAction(0);
+ }
+
+ // XXX Some mobile widget sets do not expose actions properly
+ // (via ARIA roles, etc.), so we need to generate a click.
+ // Could possibly be made simpler in the future. Maybe core
+ // engine could expose nsCoreUtiles::DispatchMouseEvent()?
+ let docAcc = Utils.AccService.getAccessibleFor(this.document);
+ let docX = {}, docY = {}, docW = {}, docH = {};
+ docAcc.getBounds(docX, docY, docW, docH);
+
+ let objX = {}, objY = {}, objW = {}, objH = {};
+ aAccessible.getBounds(objX, objY, objW, objH);
+
+ let x = Math.round((objX.value - docX.value) + objW.value / 2);
+ let y = Math.round((objY.value - docY.value) + objH.value / 2);
+
+ let node = aAccessible.DOMNode || aAccessible.parent.DOMNode;
+
+ for (let eventType of ['mousedown', 'mouseup']) {
+ let evt = this.document.createEvent('MouseEvents');
+ evt.initMouseEvent(eventType, true, true, this.window,
+ x, y, 0, 0, 0, false, false, false, false, 0, null);
+ node.dispatchEvent(evt);
+ }
+ }
+
+ if (!Utils.isActivatableOnFingerUp(aAccessible)) {
+ // Keys will typically have a sound of their own.
+ this._contentScope.get().sendAsyncMessage('AccessFu:Present',
+ Presentation.actionInvoked(aAccessible, 'click'));
+ }
+ };
+
+ let focusedAcc = Utils.AccService.getAccessibleFor(
+ this.document.activeElement);
+ if (focusedAcc && this.vc.position === focusedAcc
+ && focusedAcc.role === Roles.ENTRY) {
+ let accText = focusedAcc.QueryInterface(Ci.nsIAccessibleText);
+ let oldOffset = accText.caretOffset;
+ let newOffset = aMessage.json.offset;
+ let text = accText.getText(0, accText.characterCount);
+
+ if (newOffset >= 0 && newOffset <= accText.characterCount) {
+ accText.caretOffset = newOffset;
+ }
+
+ this.presentCaretChange(text, oldOffset, accText.caretOffset);
+ return;
+ }
+
+ // recursively find a descendant that is activatable.
+ let getActivatableDescendant = (aAccessible) => {
+ if (aAccessible.actionCount > 0) {
+ return aAccessible;
+ }
+
+ for (let acc = aAccessible.firstChild; acc; acc = acc.nextSibling) {
+ let activatable = getActivatableDescendant(acc);
+ if (activatable) {
+ return activatable;
+ }
+ }
+
+ return null;
+ };
+
+ let vc = this.vc;
+ if (!this.sendToChild(vc, aMessage, null, true)) {
+ let position = vc.position;
+ activateAccessible(getActivatableDescendant(position) || position);
+ }
+ },
+
+ adjustRange: function cc_adjustRange(aAccessible, aStepUp) {
+ let acc = Utils.getEmbeddedControl(aAccessible) || aAccessible;
+ try {
+ acc.QueryInterface(Ci.nsIAccessibleValue);
+ } catch (x) {
+ // This is not an adjustable, return false.
+ return false;
+ }
+
+ let elem = acc.DOMNode;
+ if (!elem) {
+ return false;
+ }
+
+ if (elem.tagName === 'INPUT' && elem.type === 'range') {
+ elem[aStepUp ? 'stepDown' : 'stepUp']();
+ let evt = this.document.createEvent('UIEvent');
+ evt.initEvent('change', true, true);
+ elem.dispatchEvent(evt);
+ } else {
+ let evt = this.document.createEvent('KeyboardEvent');
+ let keycode = aStepUp ? evt.DOM_VK_DOWN : evt.DOM_VK_UP;
+ evt.initKeyEvent(
+ "keypress", false, true, null, false, false, false, false, keycode, 0);
+ elem.dispatchEvent(evt);
+ }
+
+ return true;
+ },
+
+ handleMoveByGranularity: function cc_handleMoveByGranularity(aMessage) {
+ // XXX: Add sendToChild. Right now this is only used in Android, so no need.
+ let direction = aMessage.json.direction;
+ let granularity;
+
+ switch(aMessage.json.granularity) {
+ case MOVEMENT_GRANULARITY_CHARACTER:
+ granularity = Ci.nsIAccessiblePivot.CHAR_BOUNDARY;
+ break;
+ case MOVEMENT_GRANULARITY_WORD:
+ granularity = Ci.nsIAccessiblePivot.WORD_BOUNDARY;
+ break;
+ default:
+ return;
+ }
+
+ if (direction === 'Previous') {
+ this.vc.movePreviousByText(granularity);
+ } else if (direction === 'Next') {
+ this.vc.moveNextByText(granularity);
+ }
+ },
+
+ presentCaretChange: function cc_presentCaretChange(
+ aText, aOldOffset, aNewOffset) {
+ if (aOldOffset !== aNewOffset) {
+ let msg = Presentation.textSelectionChanged(aText, aNewOffset, aNewOffset,
+ aOldOffset, aOldOffset, true);
+ this._contentScope.get().sendAsyncMessage('AccessFu:Present', msg);
+ }
+ },
+
+ handleMoveCaret: function cc_handleMoveCaret(aMessage) {
+ let direction = aMessage.json.direction;
+ let granularity = aMessage.json.granularity;
+ let accessible = this.vc.position;
+ let accText = accessible.QueryInterface(Ci.nsIAccessibleText);
+ let oldOffset = accText.caretOffset;
+ let text = accText.getText(0, accText.characterCount);
+
+ let start = {}, end = {};
+ if (direction === 'Previous' && !aMessage.json.atStart) {
+ switch (granularity) {
+ case MOVEMENT_GRANULARITY_CHARACTER:
+ accText.caretOffset--;
+ break;
+ case MOVEMENT_GRANULARITY_WORD:
+ accText.getTextBeforeOffset(accText.caretOffset,
+ Ci.nsIAccessibleText.BOUNDARY_WORD_START, start, end);
+ accText.caretOffset = end.value === accText.caretOffset ?
+ start.value : end.value;
+ break;
+ case MOVEMENT_GRANULARITY_PARAGRAPH:
+ let startOfParagraph = text.lastIndexOf('\n', accText.caretOffset - 1);
+ accText.caretOffset = startOfParagraph !== -1 ? startOfParagraph : 0;
+ break;
+ }
+ } else if (direction === 'Next' && !aMessage.json.atEnd) {
+ switch (granularity) {
+ case MOVEMENT_GRANULARITY_CHARACTER:
+ accText.caretOffset++;
+ break;
+ case MOVEMENT_GRANULARITY_WORD:
+ accText.getTextAtOffset(accText.caretOffset,
+ Ci.nsIAccessibleText.BOUNDARY_WORD_END, start, end);
+ accText.caretOffset = end.value;
+ break;
+ case MOVEMENT_GRANULARITY_PARAGRAPH:
+ accText.caretOffset = text.indexOf('\n', accText.caretOffset + 1);
+ break;
+ }
+ }
+
+ this.presentCaretChange(text, oldOffset, accText.caretOffset);
+ },
+
+ getChildCursor: function cc_getChildCursor(aAccessible) {
+ let acc = aAccessible || this.vc.position;
+ if (Utils.isAliveAndVisible(acc) && acc.role === Roles.INTERNAL_FRAME) {
+ let domNode = acc.DOMNode;
+ let mm = this._childMessageSenders.get(domNode, null);
+ if (!mm) {
+ mm = Utils.getMessageManager(domNode);
+ mm.addWeakMessageListener('AccessFu:MoveCursor', this);
+ this._childMessageSenders.set(domNode, mm);
+ }
+
+ return mm;
+ }
+
+ return null;
+ },
+
+ sendToChild: function cc_sendToChild(aVirtualCursor, aMessage, aReplacer,
+ aFocus) {
+ let position = aVirtualCursor.position;
+ let mm = this.getChildCursor(position);
+ if (!mm) {
+ return false;
+ }
+
+ if (aFocus) {
+ position.takeFocus();
+ }
+
+ // XXX: This is a silly way to make a deep copy
+ let newJSON = JSON.parse(JSON.stringify(aMessage.json));
+ newJSON.origin = 'parent';
+ for (let attr in aReplacer) {
+ newJSON[attr] = aReplacer[attr];
+ }
+
+ mm.sendAsyncMessage(aMessage.name, newJSON);
+ return true;
+ },
+
+ sendToParent: function cc_sendToParent(aMessage) {
+ // XXX: This is a silly way to make a deep copy
+ let newJSON = JSON.parse(JSON.stringify(aMessage.json));
+ newJSON.origin = 'child';
+ aMessage.target.sendAsyncMessage(aMessage.name, newJSON);
+ },
+
+ /**
+ * Move cursor and/or present its location.
+ * aOptions could have any of these fields:
+ * - delay: in ms, before actual move is performed. Another autoMove call
+ * would cancel it. Useful if we want to wait for a possible trailing
+ * focus move. Default 0.
+ * - noOpIfOnScreen: if accessible is alive and visible, don't do anything.
+ * - forcePresent: present cursor location, whether we move or don't.
+ * - moveToFocused: if there is a focused accessible move to that. This takes
+ * precedence over given anchor.
+ * - moveMethod: pivot move method to use, default is 'moveNext',
+ */
+ autoMove: function cc_autoMove(aAnchor, aOptions = {}) {
+ this.cancelAutoMove();
+
+ let moveFunc = () => {
+ let vc = this.vc;
+ let acc = aAnchor;
+ let rule = aOptions.onScreenOnly ?
+ TraversalRules.SimpleOnScreen : TraversalRules.Simple;
+ let forcePresentFunc = () => {
+ if (aOptions.forcePresent) {
+ this._contentScope.get().sendAsyncMessage(
+ 'AccessFu:Present', Presentation.pivotChanged(
+ vc.position, null, Ci.nsIAccessiblePivot.REASON_NONE,
+ vc.startOffset, vc.endOffset, false));
+ }
+ };
+
+ if (aOptions.noOpIfOnScreen &&
+ Utils.isAliveAndVisible(vc.position, true)) {
+ forcePresentFunc();
+ return;
+ }
+
+ if (aOptions.moveToFocused) {
+ acc = Utils.AccService.getAccessibleFor(
+ this.document.activeElement) || acc;
+ }
+
+ let moved = false;
+ let moveMethod = aOptions.moveMethod || 'moveNext'; // default is moveNext
+ let moveFirstOrLast = moveMethod in ['moveFirst', 'moveLast'];
+ if (!moveFirstOrLast || acc) {
+ // We either need next/previous or there is an anchor we need to use.
+ moved = vc[moveFirstOrLast ? 'moveNext' : moveMethod](rule, acc, true,
+ false);
+ }
+ if (moveFirstOrLast && !moved) {
+ // We move to first/last after no anchor move happened or succeeded.
+ moved = vc[moveMethod](rule, false);
+ }
+
+ let sentToChild = this.sendToChild(vc, {
+ name: 'AccessFu:AutoMove',
+ json: {
+ moveMethod: aOptions.moveMethod,
+ moveToFocused: aOptions.moveToFocused,
+ noOpIfOnScreen: true,
+ forcePresent: true
+ }
+ }, null, true);
+
+ if (!moved && !sentToChild) {
+ forcePresentFunc();
+ }
+ };
+
+ if (aOptions.delay) {
+ this._autoMove = this.window.setTimeout(moveFunc, aOptions.delay);
+ } else {
+ moveFunc();
+ }
+ },
+
+ cancelAutoMove: function cc_cancelAutoMove() {
+ this.window.clearTimeout(this._autoMove);
+ this._autoMove = 0;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference,
+ Ci.nsIMessageListener
+ ])
+};
diff --git a/accessible/jsat/EventManager.jsm b/accessible/jsat/EventManager.jsm
new file mode 100644
index 000000000..4d635eb68
--- /dev/null
+++ b/accessible/jsat/EventManager.jsm
@@ -0,0 +1,723 @@
+/* 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/. */
+
+'use strict';
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const TEXT_NODE = 3;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Services',
+ 'resource://gre/modules/Services.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
+ 'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
+ 'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Presentation',
+ 'resource://gre/modules/accessibility/Presentation.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Roles',
+ 'resource://gre/modules/accessibility/Constants.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Events',
+ 'resource://gre/modules/accessibility/Constants.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'States',
+ 'resource://gre/modules/accessibility/Constants.jsm');
+
+this.EXPORTED_SYMBOLS = ['EventManager'];
+
+this.EventManager = function EventManager(aContentScope, aContentControl) {
+ this.contentScope = aContentScope;
+ this.contentControl = aContentControl;
+ this.addEventListener = this.contentScope.addEventListener.bind(
+ this.contentScope);
+ this.removeEventListener = this.contentScope.removeEventListener.bind(
+ this.contentScope);
+ this.sendMsgFunc = this.contentScope.sendAsyncMessage.bind(
+ this.contentScope);
+ this.webProgress = this.contentScope.docShell.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebProgress);
+};
+
+this.EventManager.prototype = {
+ editState: { editing: false },
+
+ start: function start() {
+ try {
+ if (!this._started) {
+ Logger.debug('EventManager.start');
+
+ this._started = true;
+
+ AccessibilityEventObserver.addListener(this);
+
+ this.webProgress.addProgressListener(this,
+ (Ci.nsIWebProgress.NOTIFY_STATE_ALL |
+ Ci.nsIWebProgress.NOTIFY_LOCATION));
+ this.addEventListener('wheel', this, true);
+ this.addEventListener('scroll', this, true);
+ this.addEventListener('resize', this, true);
+ this._preDialogPosition = new WeakMap();
+ }
+ this.present(Presentation.tabStateChanged(null, 'newtab'));
+
+ } catch (x) {
+ Logger.logException(x, 'Failed to start EventManager');
+ }
+ },
+
+ // XXX: Stop is not called when the tab is closed (|TabClose| event is too
+ // late). It is only called when the AccessFu is disabled explicitly.
+ stop: function stop() {
+ if (!this._started) {
+ return;
+ }
+ Logger.debug('EventManager.stop');
+ AccessibilityEventObserver.removeListener(this);
+ try {
+ this._preDialogPosition = new WeakMap();
+ this.webProgress.removeProgressListener(this);
+ this.removeEventListener('wheel', this, true);
+ this.removeEventListener('scroll', this, true);
+ this.removeEventListener('resize', this, true);
+ } catch (x) {
+ // contentScope is dead.
+ } finally {
+ this._started = false;
+ }
+ },
+
+ handleEvent: function handleEvent(aEvent) {
+ Logger.debug(() => {
+ return ['DOMEvent', aEvent.type];
+ });
+
+ try {
+ switch (aEvent.type) {
+ case 'wheel':
+ {
+ let attempts = 0;
+ let delta = aEvent.deltaX || aEvent.deltaY;
+ this.contentControl.autoMove(
+ null,
+ { moveMethod: delta > 0 ? 'moveNext' : 'movePrevious',
+ onScreenOnly: true, noOpIfOnScreen: true, delay: 500 });
+ break;
+ }
+ case 'scroll':
+ case 'resize':
+ {
+ // the target could be an element, document or window
+ let window = null;
+ if (aEvent.target instanceof Ci.nsIDOMWindow)
+ window = aEvent.target;
+ else if (aEvent.target instanceof Ci.nsIDOMDocument)
+ window = aEvent.target.defaultView;
+ else if (aEvent.target instanceof Ci.nsIDOMElement)
+ window = aEvent.target.ownerDocument.defaultView;
+ this.present(Presentation.viewportChanged(window));
+ break;
+ }
+ }
+ } catch (x) {
+ Logger.logException(x, 'Error handling DOM event');
+ }
+ },
+
+ handleAccEvent: function handleAccEvent(aEvent) {
+ Logger.debug(() => {
+ return ['A11yEvent', Logger.eventToString(aEvent),
+ Logger.accessibleToString(aEvent.accessible)];
+ });
+
+ // Don't bother with non-content events in firefox.
+ if (Utils.MozBuildApp == 'browser' &&
+ aEvent.eventType != Events.VIRTUALCURSOR_CHANGED &&
+ // XXX Bug 442005 results in DocAccessible::getDocType returning
+ // NS_ERROR_FAILURE. Checking for aEvent.accessibleDocument.docType ==
+ // 'window' does not currently work.
+ (aEvent.accessibleDocument.DOMDocument.doctype &&
+ aEvent.accessibleDocument.DOMDocument.doctype.name === 'window')) {
+ return;
+ }
+
+ switch (aEvent.eventType) {
+ case Events.VIRTUALCURSOR_CHANGED:
+ {
+ let pivot = aEvent.accessible.
+ QueryInterface(Ci.nsIAccessibleDocument).virtualCursor;
+ let position = pivot.position;
+ if (position && position.role == Roles.INTERNAL_FRAME)
+ break;
+ let event = aEvent.
+ QueryInterface(Ci.nsIAccessibleVirtualCursorChangeEvent);
+ let reason = event.reason;
+ let oldAccessible = event.oldAccessible;
+
+ if (this.editState.editing &&
+ !Utils.getState(position).contains(States.FOCUSED)) {
+ aEvent.accessibleDocument.takeFocus();
+ }
+ this.present(
+ Presentation.pivotChanged(position, oldAccessible, reason,
+ pivot.startOffset, pivot.endOffset,
+ aEvent.isFromUserInput));
+
+ break;
+ }
+ case Events.STATE_CHANGE:
+ {
+ let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent);
+ let state = Utils.getState(event);
+ if (state.contains(States.CHECKED)) {
+ if (aEvent.accessible.role === Roles.SWITCH) {
+ this.present(
+ Presentation.
+ actionInvoked(aEvent.accessible,
+ event.isEnabled ? 'on' : 'off'));
+ } else {
+ this.present(
+ Presentation.
+ actionInvoked(aEvent.accessible,
+ event.isEnabled ? 'check' : 'uncheck'));
+ }
+ } else if (state.contains(States.SELECTED)) {
+ this.present(
+ Presentation.
+ actionInvoked(aEvent.accessible,
+ event.isEnabled ? 'select' : 'unselect'));
+ }
+ break;
+ }
+ case Events.NAME_CHANGE:
+ {
+ let acc = aEvent.accessible;
+ if (acc === this.contentControl.vc.position) {
+ this.present(Presentation.nameChanged(acc));
+ } else {
+ let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
+ ['text', 'all']);
+ if (liveRegion) {
+ this.present(Presentation.nameChanged(acc, isPolite));
+ }
+ }
+ break;
+ }
+ case Events.SCROLLING_START:
+ {
+ this.contentControl.autoMove(aEvent.accessible);
+ break;
+ }
+ case Events.TEXT_CARET_MOVED:
+ {
+ let acc = aEvent.accessible.QueryInterface(Ci.nsIAccessibleText);
+ let caretOffset = aEvent.
+ QueryInterface(Ci.nsIAccessibleCaretMoveEvent).caretOffset;
+
+ // We could get a caret move in an accessible that is not focused,
+ // it doesn't mean we are not on any editable accessible. just not
+ // on this one..
+ let state = Utils.getState(acc);
+ if (state.contains(States.FOCUSED)) {
+ this._setEditingMode(aEvent, caretOffset);
+ if (state.contains(States.EDITABLE)) {
+ this.present(Presentation.textSelectionChanged(acc.getText(0, -1),
+ caretOffset, caretOffset, 0, 0, aEvent.isFromUserInput));
+ }
+ }
+ break;
+ }
+ case Events.OBJECT_ATTRIBUTE_CHANGED:
+ {
+ let evt = aEvent.QueryInterface(
+ Ci.nsIAccessibleObjectAttributeChangedEvent);
+ if (evt.changedAttribute.toString() !== 'aria-hidden') {
+ // Only handle aria-hidden attribute change.
+ break;
+ }
+ let hidden = Utils.isHidden(aEvent.accessible);
+ this[hidden ? '_handleHide' : '_handleShow'](evt);
+ if (this.inTest) {
+ this.sendMsgFunc("AccessFu:AriaHidden", { hidden: hidden });
+ }
+ break;
+ }
+ case Events.SHOW:
+ {
+ this._handleShow(aEvent);
+ break;
+ }
+ case Events.HIDE:
+ {
+ let evt = aEvent.QueryInterface(Ci.nsIAccessibleHideEvent);
+ this._handleHide(evt);
+ break;
+ }
+ case Events.TEXT_INSERTED:
+ case Events.TEXT_REMOVED:
+ {
+ let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
+ ['text', 'all']);
+ if (aEvent.isFromUserInput || liveRegion) {
+ // Handle all text mutations coming from the user or if they happen
+ // on a live region.
+ this._handleText(aEvent, liveRegion, isPolite);
+ }
+ break;
+ }
+ case Events.FOCUS:
+ {
+ // Put vc where the focus is at
+ let acc = aEvent.accessible;
+ let doc = aEvent.accessibleDocument;
+ this._setEditingMode(aEvent);
+ if ([Roles.CHROME_WINDOW,
+ Roles.DOCUMENT,
+ Roles.APPLICATION].indexOf(acc.role) < 0) {
+ this.contentControl.autoMove(acc);
+ }
+
+ if (this.inTest) {
+ this.sendMsgFunc("AccessFu:Focused");
+ }
+ break;
+ }
+ case Events.DOCUMENT_LOAD_COMPLETE:
+ {
+ let position = this.contentControl.vc.position;
+ // Check if position is in the subtree of the DOCUMENT_LOAD_COMPLETE
+ // event's dialog accesible or accessible document
+ let subtreeRoot = aEvent.accessible.role === Roles.DIALOG ?
+ aEvent.accessible : aEvent.accessibleDocument;
+ if (aEvent.accessible === aEvent.accessibleDocument ||
+ (position && Utils.isInSubtree(position, subtreeRoot))) {
+ // Do not automove into the document if the virtual cursor is already
+ // positioned inside it.
+ break;
+ }
+ this._preDialogPosition.set(aEvent.accessible.DOMNode, position);
+ this.contentControl.autoMove(aEvent.accessible, { delay: 500 });
+ break;
+ }
+ case Events.VALUE_CHANGE:
+ case Events.TEXT_VALUE_CHANGE:
+ {
+ let position = this.contentControl.vc.position;
+ let target = aEvent.accessible;
+ if (position === target ||
+ Utils.getEmbeddedControl(position) === target) {
+ this.present(Presentation.valueChanged(target));
+ } else {
+ let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
+ ['text', 'all']);
+ if (liveRegion) {
+ this.present(Presentation.valueChanged(target, isPolite));
+ }
+ }
+ }
+ }
+ },
+
+ _setEditingMode: function _setEditingMode(aEvent, aCaretOffset) {
+ let acc = aEvent.accessible;
+ let accText, characterCount;
+ let caretOffset = aCaretOffset;
+
+ try {
+ accText = acc.QueryInterface(Ci.nsIAccessibleText);
+ } catch (e) {
+ // No text interface on this accessible.
+ }
+
+ if (accText) {
+ characterCount = accText.characterCount;
+ if (caretOffset === undefined) {
+ caretOffset = accText.caretOffset;
+ }
+ }
+
+ // Update editing state, both for presenter and other things
+ let state = Utils.getState(acc);
+
+ let editState = {
+ editing: state.contains(States.EDITABLE) &&
+ state.contains(States.FOCUSED),
+ multiline: state.contains(States.MULTI_LINE),
+ atStart: caretOffset === 0,
+ atEnd: caretOffset === characterCount
+ };
+
+ // Not interesting
+ if (!editState.editing && editState.editing === this.editState.editing) {
+ return;
+ }
+
+ if (editState.editing !== this.editState.editing) {
+ this.present(Presentation.editingModeChanged(editState.editing));
+ }
+
+ if (editState.editing !== this.editState.editing ||
+ editState.multiline !== this.editState.multiline ||
+ editState.atEnd !== this.editState.atEnd ||
+ editState.atStart !== this.editState.atStart) {
+ this.sendMsgFunc("AccessFu:Input", editState);
+ }
+
+ this.editState = editState;
+ },
+
+ _handleShow: function _handleShow(aEvent) {
+ let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
+ ['additions', 'all']);
+ // Only handle show if it is a relevant live region.
+ if (!liveRegion) {
+ return;
+ }
+ // Show for text is handled by the EVENT_TEXT_INSERTED handler.
+ if (aEvent.accessible.role === Roles.TEXT_LEAF) {
+ return;
+ }
+ this._dequeueLiveEvent(Events.HIDE, liveRegion);
+ this.present(Presentation.liveRegion(liveRegion, isPolite, false));
+ },
+
+ _handleHide: function _handleHide(aEvent) {
+ let {liveRegion, isPolite} = this._handleLiveRegion(
+ aEvent, ['removals', 'all']);
+ let acc = aEvent.accessible;
+ if (liveRegion) {
+ // Hide for text is handled by the EVENT_TEXT_REMOVED handler.
+ if (acc.role === Roles.TEXT_LEAF) {
+ return;
+ }
+ this._queueLiveEvent(Events.HIDE, liveRegion, isPolite);
+ } else {
+ let vc = Utils.getVirtualCursor(this.contentScope.content.document);
+ if (vc.position &&
+ (Utils.getState(vc.position).contains(States.DEFUNCT) ||
+ Utils.isInSubtree(vc.position, acc))) {
+ let position = this._preDialogPosition.get(aEvent.accessible.DOMNode) ||
+ aEvent.targetPrevSibling || aEvent.targetParent;
+ if (!position) {
+ try {
+ position = acc.previousSibling;
+ } catch (x) {
+ // Accessible is unattached from the accessible tree.
+ position = acc.parent;
+ }
+ }
+ this.contentControl.autoMove(position,
+ { moveToFocused: true, delay: 500 });
+ }
+ }
+ },
+
+ _handleText: function _handleText(aEvent, aLiveRegion, aIsPolite) {
+ let event = aEvent.QueryInterface(Ci.nsIAccessibleTextChangeEvent);
+ let isInserted = event.isInserted;
+ let txtIface = aEvent.accessible.QueryInterface(Ci.nsIAccessibleText);
+
+ let text = '';
+ try {
+ text = txtIface.getText(0, Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT);
+ } catch (x) {
+ // XXX we might have gotten an exception with of a
+ // zero-length text. If we did, ignore it (bug #749810).
+ if (txtIface.characterCount) {
+ throw x;
+ }
+ }
+ // If there are embedded objects in the text, ignore them.
+ // Assuming changes to the descendants would already be handled by the
+ // show/hide event.
+ let modifiedText = event.modifiedText.replace(/\uFFFC/g, '');
+ if (modifiedText != event.modifiedText && !modifiedText.trim()) {
+ return;
+ }
+
+ if (aLiveRegion) {
+ if (aEvent.eventType === Events.TEXT_REMOVED) {
+ this._queueLiveEvent(Events.TEXT_REMOVED, aLiveRegion, aIsPolite,
+ modifiedText);
+ } else {
+ this._dequeueLiveEvent(Events.TEXT_REMOVED, aLiveRegion);
+ this.present(Presentation.liveRegion(aLiveRegion, aIsPolite, false,
+ modifiedText));
+ }
+ } else {
+ this.present(Presentation.textChanged(aEvent.accessible, isInserted,
+ event.start, event.length, text, modifiedText));
+ }
+ },
+
+ _handleLiveRegion: function _handleLiveRegion(aEvent, aRelevant) {
+ if (aEvent.isFromUserInput) {
+ return {};
+ }
+ let parseLiveAttrs = function parseLiveAttrs(aAccessible) {
+ let attrs = Utils.getAttributes(aAccessible);
+ if (attrs['container-live']) {
+ return {
+ live: attrs['container-live'],
+ relevant: attrs['container-relevant'] || 'additions text',
+ busy: attrs['container-busy'],
+ atomic: attrs['container-atomic'],
+ memberOf: attrs['member-of']
+ };
+ }
+ return null;
+ };
+ // XXX live attributes are not set for hidden accessibles yet. Need to
+ // climb up the tree to check for them.
+ let getLiveAttributes = function getLiveAttributes(aEvent) {
+ let liveAttrs = parseLiveAttrs(aEvent.accessible);
+ if (liveAttrs) {
+ return liveAttrs;
+ }
+ let parent = aEvent.targetParent;
+ while (parent) {
+ liveAttrs = parseLiveAttrs(parent);
+ if (liveAttrs) {
+ return liveAttrs;
+ }
+ parent = parent.parent
+ }
+ return {};
+ };
+ let {live, relevant, busy, atomic, memberOf} = getLiveAttributes(aEvent);
+ // If container-live is not present or is set to |off| ignore the event.
+ if (!live || live === 'off') {
+ return {};
+ }
+ // XXX: support busy and atomic.
+
+ // Determine if the type of the mutation is relevant. Default is additions
+ // and text.
+ let isRelevant = Utils.matchAttributeValue(relevant, aRelevant);
+ if (!isRelevant) {
+ return {};
+ }
+ return {
+ liveRegion: aEvent.accessible,
+ isPolite: live === 'polite'
+ };
+ },
+
+ _dequeueLiveEvent: function _dequeueLiveEvent(aEventType, aLiveRegion) {
+ let domNode = aLiveRegion.DOMNode;
+ if (this._liveEventQueue && this._liveEventQueue.has(domNode)) {
+ let queue = this._liveEventQueue.get(domNode);
+ let nextEvent = queue[0];
+ if (nextEvent.eventType === aEventType) {
+ Utils.win.clearTimeout(nextEvent.timeoutID);
+ queue.shift();
+ if (queue.length === 0) {
+ this._liveEventQueue.delete(domNode)
+ }
+ }
+ }
+ },
+
+ _queueLiveEvent: function _queueLiveEvent(aEventType, aLiveRegion, aIsPolite, aModifiedText) {
+ if (!this._liveEventQueue) {
+ this._liveEventQueue = new WeakMap();
+ }
+ let eventHandler = {
+ eventType: aEventType,
+ timeoutID: Utils.win.setTimeout(this.present.bind(this),
+ 20, // Wait for a possible EVENT_SHOW or EVENT_TEXT_INSERTED event.
+ Presentation.liveRegion(aLiveRegion, aIsPolite, true, aModifiedText))
+ };
+
+ let domNode = aLiveRegion.DOMNode;
+ if (this._liveEventQueue.has(domNode)) {
+ this._liveEventQueue.get(domNode).push(eventHandler);
+ } else {
+ this._liveEventQueue.set(domNode, [eventHandler]);
+ }
+ },
+
+ present: function present(aPresentationData) {
+ this.sendMsgFunc("AccessFu:Present", aPresentationData);
+ },
+
+ onStateChange: function onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ let tabstate = '';
+
+ let loadingState = Ci.nsIWebProgressListener.STATE_TRANSFERRING |
+ Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
+ let loadedState = Ci.nsIWebProgressListener.STATE_STOP |
+ Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+
+ if ((aStateFlags & loadingState) == loadingState) {
+ tabstate = 'loading';
+ } else if ((aStateFlags & loadedState) == loadedState &&
+ !aWebProgress.isLoadingDocument) {
+ tabstate = 'loaded';
+ }
+
+ if (tabstate) {
+ let docAcc = Utils.AccService.getAccessibleFor(aWebProgress.DOMWindow.document);
+ this.present(Presentation.tabStateChanged(docAcc, tabstate));
+ }
+ },
+
+ onProgressChange: function onProgressChange() {},
+
+ onLocationChange: function onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ let docAcc = Utils.AccService.getAccessibleFor(aWebProgress.DOMWindow.document);
+ this.present(Presentation.tabStateChanged(docAcc, 'newdoc'));
+ },
+
+ onStatusChange: function onStatusChange() {},
+
+ onSecurityChange: function onSecurityChange() {},
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference,
+ Ci.nsISupports,
+ Ci.nsIObserver])
+};
+
+const AccessibilityEventObserver = {
+
+ /**
+ * A WeakMap containing [content, EventManager] pairs.
+ */
+ eventManagers: new WeakMap(),
+
+ /**
+ * A total number of registered eventManagers.
+ */
+ listenerCount: 0,
+
+ /**
+ * An indicator of an active 'accessible-event' observer.
+ */
+ started: false,
+
+ /**
+ * Start an AccessibilityEventObserver.
+ */
+ start: function start() {
+ if (this.started || this.listenerCount === 0) {
+ return;
+ }
+ Services.obs.addObserver(this, 'accessible-event', false);
+ this.started = true;
+ },
+
+ /**
+ * Stop an AccessibilityEventObserver.
+ */
+ stop: function stop() {
+ if (!this.started) {
+ return;
+ }
+ Services.obs.removeObserver(this, 'accessible-event');
+ // Clean up all registered event managers.
+ this.eventManagers = new WeakMap();
+ this.listenerCount = 0;
+ this.started = false;
+ },
+
+ /**
+ * Register an EventManager and start listening to the
+ * 'accessible-event' messages.
+ *
+ * @param aEventManager EventManager
+ * An EventManager object that was loaded into the specific content.
+ */
+ addListener: function addListener(aEventManager) {
+ let content = aEventManager.contentScope.content;
+ if (!this.eventManagers.has(content)) {
+ this.listenerCount++;
+ }
+ this.eventManagers.set(content, aEventManager);
+ // Since at least one EventManager was registered, start listening.
+ Logger.debug('AccessibilityEventObserver.addListener. Total:',
+ this.listenerCount);
+ this.start();
+ },
+
+ /**
+ * Unregister an EventManager and, optionally, stop listening to the
+ * 'accessible-event' messages.
+ *
+ * @param aEventManager EventManager
+ * An EventManager object that was stopped in the specific content.
+ */
+ removeListener: function removeListener(aEventManager) {
+ let content = aEventManager.contentScope.content;
+ if (!this.eventManagers.delete(content)) {
+ return;
+ }
+ this.listenerCount--;
+ Logger.debug('AccessibilityEventObserver.removeListener. Total:',
+ this.listenerCount);
+ if (this.listenerCount === 0) {
+ // If there are no EventManagers registered at the moment, stop listening
+ // to the 'accessible-event' messages.
+ this.stop();
+ }
+ },
+
+ /**
+ * Lookup an EventManager for a specific content. If the EventManager is not
+ * found, walk up the hierarchy of parent windows.
+ * @param content Window
+ * A content Window used to lookup the corresponding EventManager.
+ */
+ getListener: function getListener(content) {
+ let eventManager = this.eventManagers.get(content);
+ if (eventManager) {
+ return eventManager;
+ }
+ let parent = content.parent;
+ if (parent === content) {
+ // There is no parent or the parent is of a different type.
+ return null;
+ }
+ return this.getListener(parent);
+ },
+
+ /**
+ * Handle the 'accessible-event' message.
+ */
+ observe: function observe(aSubject, aTopic, aData) {
+ if (aTopic !== 'accessible-event') {
+ return;
+ }
+ let event = aSubject.QueryInterface(Ci.nsIAccessibleEvent);
+ if (!event.accessibleDocument) {
+ Logger.warning(
+ 'AccessibilityEventObserver.observe: no accessible document:',
+ Logger.eventToString(event), "accessible:",
+ Logger.accessibleToString(event.accessible));
+ return;
+ }
+ let content = event.accessibleDocument.window;
+ // Match the content window to its EventManager.
+ let eventManager = this.getListener(content);
+ if (!eventManager || !eventManager._started) {
+ if (Utils.MozBuildApp === 'browser' &&
+ !(content instanceof Ci.nsIDOMChromeWindow)) {
+ Logger.warning(
+ 'AccessibilityEventObserver.observe: ignored event:',
+ Logger.eventToString(event), "accessible:",
+ Logger.accessibleToString(event.accessible), "document:",
+ Logger.accessibleToString(event.accessibleDocument));
+ }
+ return;
+ }
+ try {
+ eventManager.handleAccEvent(event);
+ } catch (x) {
+ Logger.logException(x, 'Error handing accessible event');
+ } finally {
+ return;
+ }
+ }
+};
diff --git a/accessible/jsat/Gestures.jsm b/accessible/jsat/Gestures.jsm
new file mode 100644
index 000000000..cc431614c
--- /dev/null
+++ b/accessible/jsat/Gestures.jsm
@@ -0,0 +1,956 @@
+/* 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, GestureSettings, XPCOMUtils, Utils, Promise, Logger */
+/* exported GestureSettings, GestureTracker */
+
+/******************************************************************************
+ All gestures have the following pathways when being resolved(v)/rejected(x):
+ Tap -> DoubleTap (x)
+ -> Dwell (x)
+ -> Swipe (x)
+
+ DoubleTap -> TripleTap (x)
+ -> TapHold (x)
+
+ TripleTap -> DoubleTapHold (x)
+
+ Dwell -> DwellEnd (v)
+
+ Swipe -> Explore (x)
+
+ TapHold -> TapHoldEnd (v)
+
+ DoubleTapHold -> DoubleTapHoldEnd (v)
+
+ DwellEnd -> Explore (x)
+
+ TapHoldEnd -> Explore (x)
+
+ DoubleTapHoldEnd -> Explore (x)
+
+ ExploreEnd -> Explore (x)
+
+ Explore -> ExploreEnd (v)
+******************************************************************************/
+
+'use strict';
+
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = ['GestureSettings', 'GestureTracker']; // jshint ignore:line
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+XPCOMUtils.defineLazyModuleGetter(this, 'Utils', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Logger', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'setTimeout', // jshint ignore:line
+ 'resource://gre/modules/Timer.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'clearTimeout', // jshint ignore:line
+ 'resource://gre/modules/Timer.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Promise', // jshint ignore:line
+ 'resource://gre/modules/Promise.jsm');
+
+// Default maximum duration of swipe
+const SWIPE_MAX_DURATION = 200;
+// Default maximum amount of time allowed for a gesture to be considered a
+// multitouch
+const MAX_MULTITOUCH = 125;
+// Default maximum consecutive pointer event timeout
+const MAX_CONSECUTIVE_GESTURE_DELAY = 200;
+// Default delay before tap turns into dwell
+const DWELL_THRESHOLD = 250;
+// Minimal swipe distance in inches
+const SWIPE_MIN_DISTANCE = 0.4;
+// Maximum distance the pointer could move during a tap in inches
+const TAP_MAX_RADIUS = 0.2;
+// Directness coefficient. It is based on the maximum 15 degree angle between
+// consequent pointer move lines.
+const DIRECTNESS_COEFF = 1.44;
+// Amount in inches from the edges of the screen for it to be an edge swipe
+const EDGE = 0.1;
+// Multiply timeouts by this constant, x2 works great too for slower users.
+const TIMEOUT_MULTIPLIER = 1;
+// A single pointer down/up sequence periodically precedes the tripple swipe
+// gesture on Android. This delay acounts for that.
+const IS_ANDROID = Utils.MozBuildApp === 'mobile/android' &&
+ Utils.AndroidSdkVersion >= 14;
+
+/**
+ * A point object containing distance travelled data.
+ * @param {Object} aPoint A point object that looks like: {
+ * x: x coordinate in pixels,
+ * y: y coordinate in pixels
+ * }
+ */
+function Point(aPoint) {
+ this.startX = this.x = aPoint.x;
+ this.startY = this.y = aPoint.y;
+ this.distanceTraveled = 0;
+ this.totalDistanceTraveled = 0;
+}
+
+Point.prototype = {
+ /**
+ * Update the current point coordiates.
+ * @param {Object} aPoint A new point coordinates.
+ */
+ update: function Point_update(aPoint) {
+ let lastX = this.x;
+ let lastY = this.y;
+ this.x = aPoint.x;
+ this.y = aPoint.y;
+ this.distanceTraveled = this.getDistanceToCoord(lastX, lastY);
+ this.totalDistanceTraveled += this.distanceTraveled;
+ },
+
+ reset: function Point_reset() {
+ this.distanceTraveled = 0;
+ this.totalDistanceTraveled = 0;
+ },
+
+ /**
+ * Get distance between the current point coordinates and the given ones.
+ * @param {Number} aX A pixel value for the x coordinate.
+ * @param {Number} aY A pixel value for the y coordinate.
+ * @return {Number} A distance between point's current and the given
+ * coordinates.
+ */
+ getDistanceToCoord: function Point_getDistanceToCoord(aX, aY) {
+ return Math.hypot(this.x - aX, this.y - aY);
+ },
+
+ /**
+ * Get the direct distance travelled by the point so far.
+ */
+ get directDistanceTraveled() {
+ return this.getDistanceToCoord(this.startX, this.startY);
+ }
+};
+
+/**
+ * An externally accessible collection of settings used in gesture resolition.
+ * @type {Object}
+ */
+this.GestureSettings = { // jshint ignore:line
+ /**
+ * Maximum duration of swipe
+ * @type {Number}
+ */
+ swipeMaxDuration: SWIPE_MAX_DURATION * TIMEOUT_MULTIPLIER,
+
+ /**
+ * Maximum amount of time allowed for a gesture to be considered a multitouch.
+ * @type {Number}
+ */
+ maxMultitouch: MAX_MULTITOUCH * TIMEOUT_MULTIPLIER,
+
+ /**
+ * Maximum consecutive pointer event timeout.
+ * @type {Number}
+ */
+ maxConsecutiveGestureDelay:
+ MAX_CONSECUTIVE_GESTURE_DELAY * TIMEOUT_MULTIPLIER,
+
+ /**
+ * A maximum time we wait for a next pointer down event to consider a sequence
+ * a multi-action gesture.
+ * @type {Number}
+ */
+ maxGestureResolveTimeout:
+ MAX_CONSECUTIVE_GESTURE_DELAY * TIMEOUT_MULTIPLIER,
+
+ /**
+ * Delay before tap turns into dwell
+ * @type {Number}
+ */
+ dwellThreshold: DWELL_THRESHOLD * TIMEOUT_MULTIPLIER,
+
+ /**
+ * Minimum distance that needs to be travelled for the pointer move to be
+ * fired.
+ * @type {Number}
+ */
+ travelThreshold: 0.025
+};
+
+/**
+ * An interface that handles the pointer events and calculates the appropriate
+ * gestures.
+ * @type {Object}
+ */
+this.GestureTracker = { // jshint ignore:line
+ /**
+ * Reset GestureTracker to its initial state.
+ * @return {[type]} [description]
+ */
+ reset: function GestureTracker_reset() {
+ if (this.current) {
+ this.current.clearTimer();
+ }
+ delete this.current;
+ },
+
+ /**
+ * Create a new gesture object and attach resolution handler to it as well as
+ * handle the incoming pointer event.
+ * @param {Object} aDetail A new pointer event detail.
+ * @param {Number} aTimeStamp A new pointer event timeStamp.
+ * @param {Function} aGesture A gesture constructor (default: Tap).
+ */
+ _init: function GestureTracker__init(aDetail, aTimeStamp, aGesture) {
+ // Only create a new gesture on |pointerdown| event.
+ if (aDetail.type !== 'pointerdown') {
+ return;
+ }
+ let GestureConstructor = aGesture || (IS_ANDROID ? DoubleTap : Tap);
+ this._create(GestureConstructor);
+ this._update(aDetail, aTimeStamp);
+ },
+
+ /**
+ * Handle the incoming pointer event with the existing gesture object(if
+ * present) or with the newly created one.
+ * @param {Object} aDetail A new pointer event detail.
+ * @param {Number} aTimeStamp A new pointer event timeStamp.
+ */
+ handle: function GestureTracker_handle(aDetail, aTimeStamp) {
+ Logger.gesture(() => {
+ return ['Pointer event', Utils.dpi, 'at:', aTimeStamp, JSON.stringify(aDetail)];
+ });
+ this[this.current ? '_update' : '_init'](aDetail, aTimeStamp);
+ },
+
+ /**
+ * Create a new gesture object and attach resolution handler to it.
+ * @param {Function} aGesture A gesture constructor.
+ * @param {Number} aTimeStamp An original pointer event timeStamp.
+ * @param {Array} aPoints All changed points associated with the new pointer
+ * event.
+ * @param {?String} aLastEvent Last pointer event type.
+ */
+ _create: function GestureTracker__create(aGesture, aTimeStamp, aPoints, aLastEvent) {
+ this.current = new aGesture(aTimeStamp, aPoints, aLastEvent); /* A constructor name should start with an uppercase letter. */ // jshint ignore:line
+ this.current.then(this._onFulfill.bind(this));
+ },
+
+ /**
+ * Handle the incoming pointer event with the existing gesture object.
+ * @param {Object} aDetail A new pointer event detail.
+ * @param {Number} aTimeStamp A new pointer event timeStamp.
+ */
+ _update: function GestureTracker_update(aDetail, aTimeStamp) {
+ this.current[aDetail.type](aDetail.points, aTimeStamp);
+ },
+
+ /**
+ * A resolution handler function for the current gesture promise.
+ * @param {Object} aResult A resolution payload with the relevant gesture id
+ * and an optional new gesture contructor.
+ */
+ _onFulfill: function GestureTracker__onFulfill(aResult) {
+ let {id, gestureType} = aResult;
+ let current = this.current;
+ // Do nothing if there's no existing gesture or there's already a newer
+ // gesture.
+ if (!current || current.id !== id) {
+ return;
+ }
+ // Only create a gesture if we got a constructor.
+ if (gestureType) {
+ this._create(gestureType, current.startTime, current.points,
+ current.lastEvent);
+ } else {
+ this.current.clearTimer();
+ delete this.current;
+ }
+ }
+};
+
+/**
+ * Compile a mozAccessFuGesture detail structure.
+ * @param {String} aType A gesture type.
+ * @param {Object} aPoints Gesture's points.
+ * @param {String} xKey A default key for the x coordinate. Default is
+ * 'startX'.
+ * @param {String} yKey A default key for the y coordinate. Default is
+ * 'startY'.
+ * @return {Object} a mozAccessFuGesture detail structure.
+ */
+function compileDetail(aType, aPoints, keyMap = {x: 'startX', y: 'startY'}) {
+ let touches = [];
+ let maxDeltaX = 0;
+ let maxDeltaY = 0;
+ for (let identifier in aPoints) {
+ let point = aPoints[identifier];
+ let touch = {};
+ for (let key in keyMap) {
+ touch[key] = point[keyMap[key]];
+ }
+ touches.push(touch);
+ let deltaX = point.x - point.startX;
+ let deltaY = point.y - point.startY;
+ // Determine the maximum x and y travel intervals.
+ if (Math.abs(maxDeltaX) < Math.abs(deltaX)) {
+ maxDeltaX = deltaX;
+ }
+ if (Math.abs(maxDeltaY) < Math.abs(deltaY)) {
+ maxDeltaY = deltaY;
+ }
+ // Since the gesture is resolving, reset the points' distance information
+ // since they are passed to the next potential gesture.
+ point.reset();
+ }
+ return {
+ type: aType,
+ touches: touches,
+ deltaX: maxDeltaX,
+ deltaY: maxDeltaY
+ };
+}
+
+/**
+ * A general gesture object.
+ * @param {Number} aTimeStamp An original pointer event's timeStamp that started
+ * the gesture resolution sequence.
+ * @param {Object} aPoints An existing set of points (from previous events).
+ * Default is an empty object.
+ * @param {?String} aLastEvent Last pointer event type.
+ */
+function Gesture(aTimeStamp, aPoints = {}, aLastEvent = undefined) {
+ this.startTime = Date.now();
+ Logger.gesture('Creating', this.id, 'gesture.');
+ this.points = aPoints;
+ this.lastEvent = aLastEvent;
+ this._deferred = Promise.defer();
+ // Call this._handleResolve or this._handleReject when the promise is
+ // fulfilled with either resolve or reject.
+ this.promise = this._deferred.promise.then(this._handleResolve.bind(this),
+ this._handleReject.bind(this));
+ this.startTimer(aTimeStamp);
+}
+
+Gesture.prototype = {
+ /**
+ * Get the gesture timeout delay.
+ * @return {Number}
+ */
+ _getDelay: function Gesture__getDelay() {
+ // If nothing happens withing the
+ // GestureSettings.maxConsecutiveGestureDelay, we should not wait for any
+ // more pointer events and consider them the part of the same gesture -
+ // reject this gesture promise.
+ return GestureSettings.maxConsecutiveGestureDelay;
+ },
+
+ /**
+ * Clear the existing timer.
+ */
+ clearTimer: function Gesture_clearTimer() {
+ Logger.gesture('clearTimeout', this.type);
+ clearTimeout(this._timer);
+ delete this._timer;
+ },
+
+ /**
+ * Start the timer for gesture timeout.
+ * @param {Number} aTimeStamp An original pointer event's timeStamp that
+ * started the gesture resolution sequence.
+ */
+ startTimer: function Gesture_startTimer(aTimeStamp) {
+ Logger.gesture('startTimer', this.type);
+ this.clearTimer();
+ let delay = this._getDelay(aTimeStamp);
+ let handler = () => {
+ Logger.gesture('timer handler');
+ this.clearTimer();
+ if (!this._inProgress) {
+ this._deferred.reject();
+ } else if (this._rejectToOnWait) {
+ this._deferred.reject(this._rejectToOnWait);
+ }
+ };
+ if (delay <= 0) {
+ handler();
+ } else {
+ this._timer = setTimeout(handler, delay);
+ }
+ },
+
+ /**
+ * Add a gesture promise resolution callback.
+ * @param {Function} aCallback
+ */
+ then: function Gesture_then(aCallback) {
+ this.promise.then(aCallback);
+ },
+
+ /**
+ * Update gesture's points. Test the points set with the optional gesture test
+ * function.
+ * @param {Array} aPoints An array with the changed points from the new
+ * pointer event.
+ * @param {String} aType Pointer event type.
+ * @param {Boolean} aCanCreate A flag that enables including the new points.
+ * Default is false.
+ * @param {Boolean} aNeedComplete A flag that indicates that the gesture is
+ * completing. Default is false.
+ * @return {Boolean} Indicates whether the gesture can be complete (it is
+ * set to true iff the aNeedComplete is true and there was a change to at
+ * least one point that belongs to the gesture).
+ */
+ _update: function Gesture__update(aPoints, aType, aCanCreate = false, aNeedComplete = false) {
+ let complete;
+ let lastEvent;
+ for (let point of aPoints) {
+ let identifier = point.identifier;
+ let gesturePoint = this.points[identifier];
+ if (gesturePoint) {
+ if (aType === 'pointerdown' && aCanCreate) {
+ // scratch the previous pointer with that id.
+ this.points[identifier] = new Point(point);
+ } else {
+ gesturePoint.update(point);
+ }
+ if (aNeedComplete) {
+ // Since the gesture is completing and at least one of the gesture
+ // points is updated, set the return value to true.
+ complete = true;
+ }
+ lastEvent = lastEvent || aType;
+ } else if (aCanCreate) {
+ // Only create a new point if aCanCreate is true.
+ this.points[identifier] =
+ new Point(point);
+ lastEvent = lastEvent || aType;
+ }
+ }
+ this.lastEvent = lastEvent || this.lastEvent;
+ // If test function is defined test the points.
+ if (this.test) {
+ this.test(complete);
+ }
+ return complete;
+ },
+
+ /**
+ * Emit a mozAccessFuGesture (when the gesture is resolved).
+ * @param {Object} aDetail a compiled mozAccessFuGesture detail structure.
+ */
+ _emit: function Gesture__emit(aDetail) {
+ let evt = new Utils.win.CustomEvent('mozAccessFuGesture', {
+ bubbles: true,
+ cancelable: true,
+ detail: aDetail
+ });
+ Utils.win.dispatchEvent(evt);
+ },
+
+ /**
+ * Handle the pointer down event.
+ * @param {Array} aPoints A new pointer down points.
+ * @param {Number} aTimeStamp A new pointer down timeStamp.
+ */
+ pointerdown: function Gesture_pointerdown(aPoints, aTimeStamp) {
+ this._inProgress = true;
+ this._update(aPoints, 'pointerdown',
+ aTimeStamp - this.startTime < GestureSettings.maxMultitouch);
+ },
+
+ /**
+ * Handle the pointer move event.
+ * @param {Array} aPoints A new pointer move points.
+ */
+ pointermove: function Gesture_pointermove(aPoints) {
+ this._update(aPoints, 'pointermove');
+ },
+
+ /**
+ * Handle the pointer up event.
+ * @param {Array} aPoints A new pointer up points.
+ */
+ pointerup: function Gesture_pointerup(aPoints) {
+ let complete = this._update(aPoints, 'pointerup', false, true);
+ if (complete) {
+ this._deferred.resolve();
+ }
+ },
+
+ /**
+ * A subsequent gesture constructor to resolve the current one to. E.g.
+ * tap->doubletap, dwell->dwellend, etc.
+ * @type {Function}
+ */
+ resolveTo: null,
+
+ /**
+ * A unique id for the gesture. Composed of the type + timeStamp.
+ */
+ get id() {
+ delete this._id;
+ this._id = this.type + this.startTime;
+ return this._id;
+ },
+
+ /**
+ * A gesture promise resolve callback. Compile and emit the gesture.
+ * @return {Object} Returns a structure to the gesture handler that looks like
+ * this: {
+ * id: current gesture id,
+ * gestureType: an optional subsequent gesture constructor.
+ * }
+ */
+ _handleResolve: function Gesture__handleResolve() {
+ if (this.isComplete) {
+ return;
+ }
+ Logger.gesture('Resolving', this.id, 'gesture.');
+ this.isComplete = true;
+ this.clearTimer();
+ let detail = this.compile();
+ if (detail) {
+ this._emit(detail);
+ }
+ return {
+ id: this.id,
+ gestureType: this.resolveTo
+ };
+ },
+
+ /**
+ * A gesture promise reject callback.
+ * @return {Object} Returns a structure to the gesture handler that looks like
+ * this: {
+ * id: current gesture id,
+ * gestureType: an optional subsequent gesture constructor.
+ * }
+ */
+ _handleReject: function Gesture__handleReject(aRejectTo) {
+ if (this.isComplete) {
+ return;
+ }
+ Logger.gesture('Rejecting', this.id, 'gesture.');
+ this.isComplete = true;
+ this.clearTimer();
+ return {
+ id: this.id,
+ gestureType: aRejectTo
+ };
+ },
+
+ /**
+ * A default compilation function used to build the mozAccessFuGesture event
+ * detail. The detail always includes the type and the touches associated
+ * with the gesture.
+ * @return {Object} Gesture event detail.
+ */
+ compile: function Gesture_compile() {
+ return compileDetail(this.type, this.points);
+ }
+};
+
+/**
+ * A mixin for an explore related object.
+ */
+function ExploreGesture() {
+ this.compile = () => {
+ // Unlike most of other gestures explore based gestures compile using the
+ // current point position and not the start one.
+ return compileDetail(this.type, this.points, {x: 'x', y: 'y'});
+ };
+}
+
+/**
+ * Check the in progress gesture for completion.
+ */
+function checkProgressGesture(aGesture) {
+ aGesture._inProgress = true;
+ if (aGesture.lastEvent === 'pointerup') {
+ if (aGesture.test) {
+ aGesture.test(true);
+ }
+ aGesture._deferred.resolve();
+ }
+}
+
+/**
+ * A common travel gesture. When the travel gesture is created, all subsequent
+ * pointer events' points are tested for their total distance traveled. If that
+ * distance exceeds the _threshold distance, the gesture will be rejected to a
+ * _travelTo gesture.
+ * @param {Number} aTimeStamp An original pointer event's timeStamp that started
+ * the gesture resolution sequence.
+ * @param {Object} aPoints An existing set of points (from previous events).
+ * @param {?String} aLastEvent Last pointer event type.
+ * @param {Function} aTravelTo A contructor for the gesture to reject to when
+ * travelling (default: Explore).
+ * @param {Number} aThreshold Travel threshold (default:
+ * GestureSettings.travelThreshold).
+ */
+function TravelGesture(aTimeStamp, aPoints, aLastEvent, aTravelTo = Explore, aThreshold = GestureSettings.travelThreshold) {
+ Gesture.call(this, aTimeStamp, aPoints, aLastEvent);
+ this._travelTo = aTravelTo;
+ this._threshold = aThreshold;
+}
+
+TravelGesture.prototype = Object.create(Gesture.prototype);
+
+/**
+ * Test the gesture points for travel. The gesture will be rejected to
+ * this._travelTo gesture iff at least one point crosses this._threshold.
+ */
+TravelGesture.prototype.test = function TravelGesture_test() {
+ if (!this._travelTo) {
+ return;
+ }
+ for (let identifier in this.points) {
+ let point = this.points[identifier];
+ if (point.totalDistanceTraveled / Utils.dpi > this._threshold) {
+ this._deferred.reject(this._travelTo);
+ return;
+ }
+ }
+};
+
+/**
+ * DwellEnd gesture.
+ * @param {Number} aTimeStamp An original pointer event's timeStamp that started
+ * the gesture resolution sequence.
+ * @param {Object} aPoints An existing set of points (from previous events).
+ * @param {?String} aLastEvent Last pointer event type.
+ */
+function DwellEnd(aTimeStamp, aPoints, aLastEvent) {
+ this._inProgress = true;
+ // If the pointer travels, reject to Explore.
+ TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent);
+ checkProgressGesture(this);
+}
+
+DwellEnd.prototype = Object.create(TravelGesture.prototype);
+DwellEnd.prototype.type = 'dwellend';
+
+/**
+ * TapHoldEnd gesture. This gesture can be represented as the following diagram:
+ * pointerdown-pointerup-pointerdown-*wait*-pointerup.
+ * @param {Number} aTimeStamp An original pointer event's timeStamp that started
+ * the gesture resolution sequence.
+ * @param {Object} aPoints An existing set of points (from previous events).
+ * @param {?String} aLastEvent Last pointer event type.
+ */
+function TapHoldEnd(aTimeStamp, aPoints, aLastEvent) {
+ this._inProgress = true;
+ // If the pointer travels, reject to Explore.
+ TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent);
+ checkProgressGesture(this);
+}
+
+TapHoldEnd.prototype = Object.create(TravelGesture.prototype);
+TapHoldEnd.prototype.type = 'tapholdend';
+
+/**
+ * DoubleTapHoldEnd gesture. This gesture can be represented as the following
+ * diagram:
+ * pointerdown-pointerup-pointerdown-pointerup-pointerdown-*wait*-pointerup.
+ * @param {Number} aTimeStamp An original pointer event's timeStamp that started
+ * the gesture resolution sequence.
+ * @param {Object} aPoints An existing set of points (from previous events).
+ * @param {?String} aLastEvent Last pointer event type.
+ */
+function DoubleTapHoldEnd(aTimeStamp, aPoints, aLastEvent) {
+ this._inProgress = true;
+ // If the pointer travels, reject to Explore.
+ TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent);
+ checkProgressGesture(this);
+}
+
+DoubleTapHoldEnd.prototype = Object.create(TravelGesture.prototype);
+DoubleTapHoldEnd.prototype.type = 'doubletapholdend';
+
+/**
+ * A common tap gesture object.
+ * @param {Number} aTimeStamp An original pointer event's timeStamp that started
+ * the gesture resolution sequence.
+ * @param {Object} aPoints An existing set of points (from previous events).
+ * @param {?String} aLastEvent Last pointer event type.
+ * @param {Function} aRejectToOnWait A constructor for the next gesture to
+ * reject to in case no pointermove or pointerup happens within the
+ * GestureSettings.dwellThreshold.
+ * @param {Function} aTravelTo An optional constuctor for the next gesture to
+ * reject to in case the the TravelGesture test fails.
+ * @param {Function} aRejectToOnPointerDown A constructor for the gesture to
+ * reject to if a finger comes down immediately after the tap.
+ */
+function TapGesture(aTimeStamp, aPoints, aLastEvent, aRejectToOnWait, aTravelTo, aRejectToOnPointerDown) {
+ this._rejectToOnWait = aRejectToOnWait;
+ this._rejectToOnPointerDown = aRejectToOnPointerDown;
+ // If the pointer travels, reject to aTravelTo.
+ TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent, aTravelTo,
+ TAP_MAX_RADIUS);
+}
+
+TapGesture.prototype = Object.create(TravelGesture.prototype);
+TapGesture.prototype._getDelay = function TapGesture__getDelay() {
+ // If, for TapGesture, no pointermove or pointerup happens within the
+ // GestureSettings.dwellThreshold, reject.
+ // Note: the original pointer event's timeStamp is irrelevant here.
+ return GestureSettings.dwellThreshold;
+};
+
+TapGesture.prototype.pointerup = function TapGesture_pointerup(aPoints) {
+ if (this._rejectToOnPointerDown) {
+ let complete = this._update(aPoints, 'pointerup', false, true);
+ if (complete) {
+ this.clearTimer();
+ if (GestureSettings.maxGestureResolveTimeout) {
+ this._pointerUpTimer = setTimeout(() => {
+ clearTimeout(this._pointerUpTimer);
+ delete this._pointerUpTimer;
+ this._deferred.resolve();
+ }, GestureSettings.maxGestureResolveTimeout);
+ } else {
+ this._deferred.resolve();
+ }
+ }
+ } else {
+ TravelGesture.prototype.pointerup.call(this, aPoints);
+ }
+};
+
+TapGesture.prototype.pointerdown = function TapGesture_pointerdown(aPoints, aTimeStamp) {
+ if (this._pointerUpTimer) {
+ clearTimeout(this._pointerUpTimer);
+ delete this._pointerUpTimer;
+ this._deferred.reject(this._rejectToOnPointerDown);
+ } else {
+ TravelGesture.prototype.pointerdown.call(this, aPoints, aTimeStamp);
+ }
+};
+
+
+/**
+ * Tap gesture.
+ * @param {Number} aTimeStamp An original pointer event's timeStamp that started
+ * the gesture resolution sequence.
+ * @param {Object} aPoints An existing set of points (from previous events).
+ * @param {?String} aLastEvent Last pointer event type.
+ */
+function Tap(aTimeStamp, aPoints, aLastEvent) {
+ // If the pointer travels, reject to Swipe.
+ TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, Dwell, Swipe, DoubleTap);
+}
+
+Tap.prototype = Object.create(TapGesture.prototype);
+Tap.prototype.type = 'tap';
+
+
+/**
+ * Double Tap gesture.
+ * @param {Number} aTimeStamp An original pointer event's timeStamp that started
+ * the gesture resolution sequence.
+ * @param {Object} aPoints An existing set of points (from previous events).
+ * @param {?String} aLastEvent Last pointer event type.
+ */
+function DoubleTap(aTimeStamp, aPoints, aLastEvent) {
+ this._inProgress = true;
+ TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, TapHold, null, TripleTap);
+}
+
+DoubleTap.prototype = Object.create(TapGesture.prototype);
+DoubleTap.prototype.type = 'doubletap';
+
+/**
+ * Triple Tap gesture.
+ * @param {Number} aTimeStamp An original pointer event's timeStamp that started
+ * the gesture resolution sequence.
+ * @param {Object} aPoints An existing set of points (from previous events).
+ * @param {?String} aLastEvent Last pointer event type.
+ */
+function TripleTap(aTimeStamp, aPoints, aLastEvent) {
+ this._inProgress = true;
+ TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, DoubleTapHold, null, null);
+}
+
+TripleTap.prototype = Object.create(TapGesture.prototype);
+TripleTap.prototype.type = 'tripletap';
+
+/**
+ * Common base object for gestures that are created as resolved.
+ * @param {Number} aTimeStamp An original pointer event's timeStamp that started
+ * the gesture resolution sequence.
+ * @param {Object} aPoints An existing set of points (from previous events).
+ * @param {?String} aLastEvent Last pointer event type.
+ */
+function ResolvedGesture(aTimeStamp, aPoints, aLastEvent) {
+ Gesture.call(this, aTimeStamp, aPoints, aLastEvent);
+ // Resolve the guesture right away.
+ this._deferred.resolve();
+}
+
+ResolvedGesture.prototype = Object.create(Gesture.prototype);
+
+/**
+ * Dwell gesture
+ * @param {Number} aTimeStamp An original pointer event's timeStamp that started
+ * the gesture resolution sequence.
+ * @param {Object} aPoints An existing set of points (from previous events).
+ * @param {?String} aLastEvent Last pointer event type.
+ */
+function Dwell(aTimeStamp, aPoints, aLastEvent) {
+ ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent);
+}
+
+Dwell.prototype = Object.create(ResolvedGesture.prototype);
+Dwell.prototype.type = 'dwell';
+Dwell.prototype.resolveTo = DwellEnd;
+
+/**
+ * TapHold gesture
+ * @param {Number} aTimeStamp An original pointer event's timeStamp that started
+ * the gesture resolution sequence.
+ * @param {Object} aPoints An existing set of points (from previous events).
+ * @param {?String} aLastEvent Last pointer event type.
+ */
+function TapHold(aTimeStamp, aPoints, aLastEvent) {
+ ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent);
+}
+
+TapHold.prototype = Object.create(ResolvedGesture.prototype);
+TapHold.prototype.type = 'taphold';
+TapHold.prototype.resolveTo = TapHoldEnd;
+
+/**
+ * DoubleTapHold gesture
+ * @param {Number} aTimeStamp An original pointer event's timeStamp that started
+ * the gesture resolution sequence.
+ * @param {Object} aPoints An existing set of points (from previous events).
+ * @param {?String} aLastEvent Last pointer event type.
+ */
+function DoubleTapHold(aTimeStamp, aPoints, aLastEvent) {
+ ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent);
+}
+
+DoubleTapHold.prototype = Object.create(ResolvedGesture.prototype);
+DoubleTapHold.prototype.type = 'doubletaphold';
+DoubleTapHold.prototype.resolveTo = DoubleTapHoldEnd;
+
+/**
+ * Explore gesture
+ * @param {Number} aTimeStamp An original pointer event's timeStamp that started
+ * the gesture resolution sequence.
+ * @param {Object} aPoints An existing set of points (from previous events).
+ * @param {?String} aLastEvent Last pointer event type.
+ */
+function Explore(aTimeStamp, aPoints, aLastEvent) {
+ ExploreGesture.call(this);
+ ResolvedGesture.call(this, aTimeStamp, aPoints, aLastEvent);
+}
+
+Explore.prototype = Object.create(ResolvedGesture.prototype);
+Explore.prototype.type = 'explore';
+Explore.prototype.resolveTo = ExploreEnd;
+
+/**
+ * ExploreEnd gesture.
+ * @param {Number} aTimeStamp An original pointer event's timeStamp that started
+ * the gesture resolution sequence.
+ * @param {Object} aPoints An existing set of points (from previous events).
+ * @param {?String} aLastEvent Last pointer event type.
+ */
+function ExploreEnd(aTimeStamp, aPoints, aLastEvent) {
+ this._inProgress = true;
+ ExploreGesture.call(this);
+ // If the pointer travels, reject to Explore.
+ TravelGesture.call(this, aTimeStamp, aPoints, aLastEvent);
+ checkProgressGesture(this);
+}
+
+ExploreEnd.prototype = Object.create(TravelGesture.prototype);
+ExploreEnd.prototype.type = 'exploreend';
+
+/**
+ * Swipe gesture.
+ * @param {Number} aTimeStamp An original pointer event's timeStamp that started
+ * the gesture resolution sequence.
+ * @param {Object} aPoints An existing set of points (from previous events).
+ * @param {?String} aLastEvent Last pointer event type.
+ */
+function Swipe(aTimeStamp, aPoints, aLastEvent) {
+ this._inProgress = true;
+ this._rejectToOnWait = Explore;
+ Gesture.call(this, aTimeStamp, aPoints, aLastEvent);
+ checkProgressGesture(this);
+}
+
+Swipe.prototype = Object.create(Gesture.prototype);
+Swipe.prototype.type = 'swipe';
+Swipe.prototype._getDelay = function Swipe__getDelay(aTimeStamp) {
+ // Swipe should be completed within the GestureSettings.swipeMaxDuration from
+ // the initial pointer down event.
+ return GestureSettings.swipeMaxDuration - this.startTime + aTimeStamp;
+};
+
+/**
+ * Determine wither the gesture was Swipe or Explore.
+ * @param {Booler} aComplete A flag that indicates whether the gesture is and
+ * will be complete after the test.
+ */
+Swipe.prototype.test = function Swipe_test(aComplete) {
+ if (!aComplete) {
+ // No need to test if the gesture is not completing or can't be complete.
+ return;
+ }
+ let reject = true;
+ // If at least one point travelled for more than SWIPE_MIN_DISTANCE and it was
+ // direct enough, consider it a Swipe.
+ for (let identifier in this.points) {
+ let point = this.points[identifier];
+ let directDistance = point.directDistanceTraveled;
+ if (directDistance / Utils.dpi >= SWIPE_MIN_DISTANCE ||
+ directDistance * DIRECTNESS_COEFF >= point.totalDistanceTraveled) {
+ reject = false;
+ }
+ }
+ if (reject) {
+ this._deferred.reject(Explore);
+ }
+};
+
+/**
+ * Compile a swipe related mozAccessFuGesture event detail.
+ * @return {Object} A mozAccessFuGesture detail object.
+ */
+Swipe.prototype.compile = function Swipe_compile() {
+ let type = this.type;
+ let detail = compileDetail(type, this.points,
+ {x1: 'startX', y1: 'startY', x2: 'x', y2: 'y'});
+ let deltaX = detail.deltaX;
+ let deltaY = detail.deltaY;
+ let edge = EDGE * Utils.dpi;
+ if (Math.abs(deltaX) > Math.abs(deltaY)) {
+ // Horizontal swipe.
+ let startPoints = detail.touches.map(touch => touch.x1);
+ if (deltaX > 0) {
+ detail.type = type + 'right';
+ detail.edge = Math.min.apply(null, startPoints) <= edge;
+ } else {
+ detail.type = type + 'left';
+ detail.edge =
+ Utils.win.screen.width - Math.max.apply(null, startPoints) <= edge;
+ }
+ } else {
+ // Vertical swipe.
+ let startPoints = detail.touches.map(touch => touch.y1);
+ if (deltaY > 0) {
+ detail.type = type + 'down';
+ detail.edge = Math.min.apply(null, startPoints) <= edge;
+ } else {
+ detail.type = type + 'up';
+ detail.edge =
+ Utils.win.screen.height - Math.max.apply(null, startPoints) <= edge;
+ }
+ }
+ return detail;
+};
diff --git a/accessible/jsat/OutputGenerator.jsm b/accessible/jsat/OutputGenerator.jsm
new file mode 100644
index 000000000..36b43a569
--- /dev/null
+++ b/accessible/jsat/OutputGenerator.jsm
@@ -0,0 +1,1003 @@
+/* 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, Utils, PrefCache, States, Roles, Logger */
+/* exported UtteranceGenerator, BrailleGenerator */
+
+'use strict';
+
+const {utils: Cu, interfaces: Ci} = Components;
+
+const INCLUDE_DESC = 0x01;
+const INCLUDE_NAME = 0x02;
+const INCLUDE_VALUE = 0x04;
+const NAME_FROM_SUBTREE_RULE = 0x10;
+const IGNORE_EXPLICIT_NAME = 0x20;
+
+const OUTPUT_DESC_FIRST = 0;
+const OUTPUT_DESC_LAST = 1;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Utils', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'PrefCache', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Logger', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Roles', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Constants.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'States', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Constants.jsm');
+
+this.EXPORTED_SYMBOLS = ['UtteranceGenerator', 'BrailleGenerator']; // jshint ignore:line
+
+var OutputGenerator = {
+
+ defaultOutputOrder: OUTPUT_DESC_LAST,
+
+ /**
+ * Generates output for a PivotContext.
+ * @param {PivotContext} aContext object that generates and caches
+ * context information for a given accessible and its relationship with
+ * another accessible.
+ * @return {Object} An array of speech data. Depending on the utterance order,
+ * the data describes the context for an accessible object either
+ * starting from the accessible's ancestry or accessible's subtree.
+ */
+ genForContext: function genForContext(aContext) {
+ let output = [];
+ let self = this;
+ let addOutput = function addOutput(aAccessible) {
+ output.push.apply(output, self.genForObject(aAccessible, aContext));
+ };
+ let ignoreSubtree = function ignoreSubtree(aAccessible) {
+ let roleString = Utils.AccService.getStringRole(aAccessible.role);
+ let nameRule = self.roleRuleMap[roleString] || 0;
+ // Ignore subtree if the name is explicit and the role's name rule is the
+ // NAME_FROM_SUBTREE_RULE.
+ return (((nameRule & INCLUDE_VALUE) && aAccessible.value) ||
+ ((nameRule & NAME_FROM_SUBTREE_RULE) &&
+ (Utils.getAttributes(aAccessible)['explicit-name'] === 'true' &&
+ !(nameRule & IGNORE_EXPLICIT_NAME))));
+ };
+
+ let contextStart = this._getContextStart(aContext);
+
+ if (this.outputOrder === OUTPUT_DESC_FIRST) {
+ contextStart.forEach(addOutput);
+ addOutput(aContext.accessible);
+ for (let node of aContext.subtreeGenerator(true, ignoreSubtree)) {
+ addOutput(node);
+ }
+ } else {
+ for (let node of aContext.subtreeGenerator(false, ignoreSubtree)) {
+ addOutput(node);
+ }
+ addOutput(aContext.accessible);
+
+ // If there are any documents in new ancestry, find a first one and place
+ // it in the beginning of the utterance.
+ let doc, docIndex = contextStart.findIndex(
+ ancestor => ancestor.role === Roles.DOCUMENT);
+
+ if (docIndex > -1) {
+ doc = contextStart.splice(docIndex, 1)[0];
+ }
+
+ contextStart.reverse().forEach(addOutput);
+ if (doc) {
+ output.unshift.apply(output, self.genForObject(doc, aContext));
+ }
+ }
+
+ return output;
+ },
+
+
+ /**
+ * Generates output for an object.
+ * @param {nsIAccessible} aAccessible accessible object to generate output
+ * for.
+ * @param {PivotContext} aContext object that generates and caches
+ * context information for a given accessible and its relationship with
+ * another accessible.
+ * @return {Array} A 2 element array of speech data. The first element
+ * describes the object and its state. The second element is the object's
+ * name. Whether the object's description or it's role is included is
+ * determined by {@link roleRuleMap}.
+ */
+ genForObject: function genForObject(aAccessible, aContext) {
+ let roleString = Utils.AccService.getStringRole(aAccessible.role);
+ let func = this.objectOutputFunctions[
+ OutputGenerator._getOutputName(roleString)] ||
+ this.objectOutputFunctions.defaultFunc;
+
+ let flags = this.roleRuleMap[roleString] || 0;
+
+ if (aAccessible.childCount === 0) {
+ flags |= INCLUDE_NAME;
+ }
+
+ return func.apply(this, [aAccessible, roleString,
+ Utils.getState(aAccessible), flags, aContext]);
+ },
+
+ /**
+ * Generates output for an action performed.
+ * @param {nsIAccessible} aAccessible accessible object that the action was
+ * invoked in.
+ * @param {string} aActionName the name of the action, one of the keys in
+ * {@link gActionMap}.
+ * @return {Array} A one element array with action data.
+ */
+ genForAction: function genForAction(aObject, aActionName) {}, // jshint ignore:line
+
+ /**
+ * Generates output for an announcement.
+ * @param {string} aAnnouncement unlocalized announcement.
+ * @return {Array} An announcement speech data to be localized.
+ */
+ genForAnnouncement: function genForAnnouncement(aAnnouncement) {}, // jshint ignore:line
+
+ /**
+ * Generates output for a tab state change.
+ * @param {nsIAccessible} aAccessible accessible object of the tab's attached
+ * document.
+ * @param {string} aTabState the tab state name, see
+ * {@link Presenter.tabStateChanged}.
+ * @return {Array} The tab state utterace.
+ */
+ genForTabStateChange: function genForTabStateChange(aObject, aTabState) {}, // jshint ignore:line
+
+ /**
+ * Generates output for announcing entering and leaving editing mode.
+ * @param {aIsEditing} boolean true if we are in editing mode
+ * @return {Array} The mode utterance
+ */
+ genForEditingMode: function genForEditingMode(aIsEditing) {}, // jshint ignore:line
+
+ _getContextStart: function getContextStart(aContext) {}, // jshint ignore:line
+
+ /**
+ * Adds an accessible name and description to the output if available.
+ * @param {Array} aOutput Output array.
+ * @param {nsIAccessible} aAccessible current accessible object.
+ * @param {Number} aFlags output flags.
+ */
+ _addName: function _addName(aOutput, aAccessible, aFlags) {
+ let name;
+ if ((Utils.getAttributes(aAccessible)['explicit-name'] === 'true' &&
+ !(aFlags & IGNORE_EXPLICIT_NAME)) || (aFlags & INCLUDE_NAME)) {
+ name = aAccessible.name;
+ }
+
+ let description = aAccessible.description;
+ if (description) {
+ // Compare against the calculated name unconditionally, regardless of name rule,
+ // so we can make sure we don't speak duplicated descriptions
+ let tmpName = name || aAccessible.name;
+ if (tmpName && (description !== tmpName)) {
+ name = name || '';
+ name = this.outputOrder === OUTPUT_DESC_FIRST ?
+ description + ' - ' + name :
+ name + ' - ' + description;
+ }
+ }
+
+ if (!name || !name.trim()) {
+ return;
+ }
+ aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'push' : 'unshift'](name);
+ },
+
+ /**
+ * Adds a landmark role to the output if available.
+ * @param {Array} aOutput Output array.
+ * @param {nsIAccessible} aAccessible current accessible object.
+ */
+ _addLandmark: function _addLandmark(aOutput, aAccessible) {
+ let landmarkName = Utils.getLandmarkName(aAccessible);
+ if (!landmarkName) {
+ return;
+ }
+ aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'unshift' : 'push']({
+ string: landmarkName
+ });
+ },
+
+ /**
+ * Adds math roles to the output, for a MathML accessible.
+ * @param {Array} aOutput Output array.
+ * @param {nsIAccessible} aAccessible current accessible object.
+ * @param {String} aRoleStr aAccessible's role string.
+ */
+ _addMathRoles: function _addMathRoles(aOutput, aAccessible, aRoleStr) {
+ // First, determine the actual role to use (e.g. mathmlfraction).
+ let roleStr = aRoleStr;
+ switch(aAccessible.role) {
+ case Roles.MATHML_CELL:
+ case Roles.MATHML_ENCLOSED:
+ case Roles.MATHML_LABELED_ROW:
+ case Roles.MATHML_ROOT:
+ case Roles.MATHML_SQUARE_ROOT:
+ case Roles.MATHML_TABLE:
+ case Roles.MATHML_TABLE_ROW:
+ // Use the default role string.
+ break;
+ case Roles.MATHML_MULTISCRIPTS:
+ case Roles.MATHML_OVER:
+ case Roles.MATHML_SUB:
+ case Roles.MATHML_SUB_SUP:
+ case Roles.MATHML_SUP:
+ case Roles.MATHML_UNDER:
+ case Roles.MATHML_UNDER_OVER:
+ // For scripted accessibles, use the string 'mathmlscripted'.
+ roleStr = 'mathmlscripted';
+ break;
+ case Roles.MATHML_FRACTION:
+ // From a semantic point of view, the only important point is to
+ // distinguish between fractions that have a bar and those that do not.
+ // Per the MathML 3 spec, the latter happens iff the linethickness
+ // attribute is of the form [zero-float][optional-unit]. In that case,
+ // we use the string 'mathmlfractionwithoutbar'.
+ let linethickness = Utils.getAttributes(aAccessible).linethickness;
+ if (linethickness) {
+ let numberMatch = linethickness.match(/^(?:\d|\.)+/);
+ if (numberMatch && !parseFloat(numberMatch[0])) {
+ roleStr += 'withoutbar';
+ }
+ }
+ break;
+ default:
+ // Otherwise, do not output the actual role.
+ roleStr = null;
+ break;
+ }
+
+ // Get the math role based on the position in the parent accessible
+ // (e.g. numerator for the first child of a mathmlfraction).
+ let mathRole = Utils.getMathRole(aAccessible);
+ if (mathRole) {
+ aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'push' : 'unshift']
+ ({string: this._getOutputName(mathRole)});
+ }
+ if (roleStr) {
+ aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'push' : 'unshift']
+ ({string: this._getOutputName(roleStr)});
+ }
+ },
+
+ /**
+ * Adds MathML menclose notations to the output.
+ * @param {Array} aOutput Output array.
+ * @param {nsIAccessible} aAccessible current accessible object.
+ */
+ _addMencloseNotations: function _addMencloseNotations(aOutput, aAccessible) {
+ let notations = Utils.getAttributes(aAccessible).notation || 'longdiv';
+ aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'push' : 'unshift'].apply(
+ aOutput, notations.split(' ').map(notation => {
+ return { string: this._getOutputName('notation-' + notation) };
+ }));
+ },
+
+ /**
+ * Adds an entry type attribute to the description if available.
+ * @param {Array} aOutput Output array.
+ * @param {nsIAccessible} aAccessible current accessible object.
+ * @param {String} aRoleStr aAccessible's role string.
+ */
+ _addType: function _addType(aOutput, aAccessible, aRoleStr) {
+ if (aRoleStr !== 'entry') {
+ return;
+ }
+
+ let typeName = Utils.getAttributes(aAccessible)['text-input-type'];
+ // Ignore the the input type="text" case.
+ if (!typeName || typeName === 'text') {
+ return;
+ }
+ aOutput.push({string: 'textInputType_' + typeName});
+ },
+
+ _addState: function _addState(aOutput, aState, aRoleStr) {}, // jshint ignore:line
+
+ _addRole: function _addRole(aOutput, aAccessible, aRoleStr) {}, // jshint ignore:line
+
+ get outputOrder() {
+ if (!this._utteranceOrder) {
+ this._utteranceOrder = new PrefCache('accessibility.accessfu.utterance');
+ }
+ return typeof this._utteranceOrder.value === 'number' ?
+ this._utteranceOrder.value : this.defaultOutputOrder;
+ },
+
+ _getOutputName: function _getOutputName(aName) {
+ return aName.replace(/\s/g, '');
+ },
+
+ roleRuleMap: {
+ 'menubar': INCLUDE_DESC,
+ 'scrollbar': INCLUDE_DESC,
+ 'grip': INCLUDE_DESC,
+ 'alert': INCLUDE_DESC | INCLUDE_NAME,
+ 'menupopup': INCLUDE_DESC,
+ 'menuitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'tooltip': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'columnheader': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'rowheader': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'column': NAME_FROM_SUBTREE_RULE,
+ 'row': NAME_FROM_SUBTREE_RULE,
+ 'cell': INCLUDE_DESC | INCLUDE_NAME,
+ 'application': INCLUDE_NAME,
+ 'document': INCLUDE_NAME,
+ 'grouping': INCLUDE_DESC | INCLUDE_NAME,
+ 'toolbar': INCLUDE_DESC,
+ 'table': INCLUDE_DESC | INCLUDE_NAME,
+ 'link': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'helpballoon': NAME_FROM_SUBTREE_RULE,
+ 'list': INCLUDE_DESC | INCLUDE_NAME,
+ 'listitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'outline': INCLUDE_DESC,
+ 'outlineitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'pagetab': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'graphic': INCLUDE_DESC,
+ 'switch': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'pushbutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'checkbutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'radiobutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'buttondropdown': NAME_FROM_SUBTREE_RULE,
+ 'combobox': INCLUDE_DESC | INCLUDE_VALUE,
+ 'droplist': INCLUDE_DESC,
+ 'progressbar': INCLUDE_DESC | INCLUDE_VALUE,
+ 'slider': INCLUDE_DESC | INCLUDE_VALUE,
+ 'spinbutton': INCLUDE_DESC | INCLUDE_VALUE,
+ 'diagram': INCLUDE_DESC,
+ 'animation': INCLUDE_DESC,
+ 'equation': INCLUDE_DESC,
+ 'buttonmenu': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'buttondropdowngrid': NAME_FROM_SUBTREE_RULE,
+ 'pagetablist': INCLUDE_DESC,
+ 'canvas': INCLUDE_DESC,
+ 'check menu item': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'label': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'password text': INCLUDE_DESC,
+ 'popup menu': INCLUDE_DESC,
+ 'radio menu item': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'table column header': NAME_FROM_SUBTREE_RULE,
+ 'table row header': NAME_FROM_SUBTREE_RULE,
+ 'tear off menu item': NAME_FROM_SUBTREE_RULE,
+ 'toggle button': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'parent menuitem': NAME_FROM_SUBTREE_RULE,
+ 'header': INCLUDE_DESC,
+ 'footer': INCLUDE_DESC,
+ 'entry': INCLUDE_DESC | INCLUDE_NAME | INCLUDE_VALUE,
+ 'caption': INCLUDE_DESC,
+ 'document frame': INCLUDE_DESC,
+ 'heading': INCLUDE_DESC,
+ 'calendar': INCLUDE_DESC | INCLUDE_NAME,
+ 'combobox option': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'listbox option': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
+ 'listbox rich option': NAME_FROM_SUBTREE_RULE,
+ 'gridcell': NAME_FROM_SUBTREE_RULE,
+ 'check rich option': NAME_FROM_SUBTREE_RULE,
+ 'term': NAME_FROM_SUBTREE_RULE,
+ 'definition': NAME_FROM_SUBTREE_RULE,
+ 'key': NAME_FROM_SUBTREE_RULE,
+ 'image map': INCLUDE_DESC,
+ 'option': INCLUDE_DESC,
+ 'listbox': INCLUDE_DESC,
+ 'definitionlist': INCLUDE_DESC | INCLUDE_NAME,
+ 'dialog': INCLUDE_DESC | INCLUDE_NAME,
+ 'chrome window': IGNORE_EXPLICIT_NAME,
+ 'app root': IGNORE_EXPLICIT_NAME,
+ 'statusbar': NAME_FROM_SUBTREE_RULE,
+ 'mathml table': INCLUDE_DESC | INCLUDE_NAME,
+ 'mathml labeled row': NAME_FROM_SUBTREE_RULE,
+ 'mathml table row': NAME_FROM_SUBTREE_RULE,
+ 'mathml cell': INCLUDE_DESC | INCLUDE_NAME,
+ 'mathml fraction': INCLUDE_DESC,
+ 'mathml square root': INCLUDE_DESC,
+ 'mathml root': INCLUDE_DESC,
+ 'mathml enclosed': INCLUDE_DESC,
+ 'mathml sub': INCLUDE_DESC,
+ 'mathml sup': INCLUDE_DESC,
+ 'mathml sub sup': INCLUDE_DESC,
+ 'mathml under': INCLUDE_DESC,
+ 'mathml over': INCLUDE_DESC,
+ 'mathml under over': INCLUDE_DESC,
+ 'mathml multiscripts': INCLUDE_DESC,
+ 'mathml identifier': INCLUDE_DESC,
+ 'mathml number': INCLUDE_DESC,
+ 'mathml operator': INCLUDE_DESC,
+ 'mathml text': INCLUDE_DESC,
+ 'mathml string literal': INCLUDE_DESC,
+ 'mathml row': INCLUDE_DESC,
+ 'mathml style': INCLUDE_DESC,
+ 'mathml error': INCLUDE_DESC },
+
+ mathmlRolesSet: new Set([
+ Roles.MATHML_MATH,
+ Roles.MATHML_IDENTIFIER,
+ Roles.MATHML_NUMBER,
+ Roles.MATHML_OPERATOR,
+ Roles.MATHML_TEXT,
+ Roles.MATHML_STRING_LITERAL,
+ Roles.MATHML_GLYPH,
+ Roles.MATHML_ROW,
+ Roles.MATHML_FRACTION,
+ Roles.MATHML_SQUARE_ROOT,
+ Roles.MATHML_ROOT,
+ Roles.MATHML_FENCED,
+ Roles.MATHML_ENCLOSED,
+ Roles.MATHML_STYLE,
+ Roles.MATHML_SUB,
+ Roles.MATHML_SUP,
+ Roles.MATHML_SUB_SUP,
+ Roles.MATHML_UNDER,
+ Roles.MATHML_OVER,
+ Roles.MATHML_UNDER_OVER,
+ Roles.MATHML_MULTISCRIPTS,
+ Roles.MATHML_TABLE,
+ Roles.LABELED_ROW,
+ Roles.MATHML_TABLE_ROW,
+ Roles.MATHML_CELL,
+ Roles.MATHML_ACTION,
+ Roles.MATHML_ERROR,
+ Roles.MATHML_STACK,
+ Roles.MATHML_LONG_DIVISION,
+ Roles.MATHML_STACK_GROUP,
+ Roles.MATHML_STACK_ROW,
+ Roles.MATHML_STACK_CARRIES,
+ Roles.MATHML_STACK_CARRY,
+ Roles.MATHML_STACK_LINE
+ ]),
+
+ objectOutputFunctions: {
+ _generateBaseOutput:
+ function _generateBaseOutput(aAccessible, aRoleStr, aState, aFlags) {
+ let output = [];
+
+ if (aFlags & INCLUDE_DESC) {
+ this._addState(output, aState, aRoleStr);
+ this._addType(output, aAccessible, aRoleStr);
+ this._addRole(output, aAccessible, aRoleStr);
+ }
+
+ if (aFlags & INCLUDE_VALUE && aAccessible.value.trim()) {
+ output[this.outputOrder === OUTPUT_DESC_FIRST ? 'push' : 'unshift'](
+ aAccessible.value);
+ }
+
+ this._addName(output, aAccessible, aFlags);
+ this._addLandmark(output, aAccessible);
+
+ return output;
+ },
+
+ label: function label(aAccessible, aRoleStr, aState, aFlags, aContext) {
+ if (aContext.isNestedControl ||
+ aContext.accessible == Utils.getEmbeddedControl(aAccessible)) {
+ // If we are on a nested control, or a nesting label,
+ // we don't need the context.
+ return [];
+ }
+
+ return this.objectOutputFunctions.defaultFunc.apply(this, arguments);
+ },
+
+ entry: function entry(aAccessible, aRoleStr, aState, aFlags) {
+ let rolestr = aState.contains(States.MULTI_LINE) ? 'textarea' : 'entry';
+ return this.objectOutputFunctions.defaultFunc.apply(
+ this, [aAccessible, rolestr, aState, aFlags]);
+ },
+
+ pagetab: function pagetab(aAccessible, aRoleStr, aState, aFlags) {
+ let itemno = {};
+ let itemof = {};
+ aAccessible.groupPosition({}, itemof, itemno);
+ let output = [];
+ this._addState(output, aState);
+ this._addRole(output, aAccessible, aRoleStr);
+ output.push({
+ string: 'objItemOfN',
+ args: [itemno.value, itemof.value]
+ });
+
+ this._addName(output, aAccessible, aFlags);
+ this._addLandmark(output, aAccessible);
+
+ return output;
+ },
+
+ table: function table(aAccessible, aRoleStr, aState, aFlags) {
+ let output = [];
+ let table;
+ try {
+ table = aAccessible.QueryInterface(Ci.nsIAccessibleTable);
+ } catch (x) {
+ Logger.logException(x);
+ return output;
+ } finally {
+ // Check if it's a layout table, and bail out if true.
+ // We don't want to speak any table information for layout tables.
+ if (table.isProbablyForLayout()) {
+ return output;
+ }
+ this._addRole(output, aAccessible, aRoleStr);
+ output.push.call(output, {
+ string: this._getOutputName('tblColumnInfo'),
+ count: table.columnCount
+ }, {
+ string: this._getOutputName('tblRowInfo'),
+ count: table.rowCount
+ });
+ this._addName(output, aAccessible, aFlags);
+ this._addLandmark(output, aAccessible);
+ return output;
+ }
+ },
+
+ gridcell: function gridcell(aAccessible, aRoleStr, aState, aFlags) {
+ let output = [];
+ this._addState(output, aState);
+ this._addName(output, aAccessible, aFlags);
+ this._addLandmark(output, aAccessible);
+ return output;
+ },
+
+ // Use the table output functions for MathML tabular elements.
+ mathmltable: function mathmltable() {
+ return this.objectOutputFunctions.table.apply(this, arguments);
+ },
+
+ mathmlcell: function mathmlcell() {
+ return this.objectOutputFunctions.cell.apply(this, arguments);
+ },
+
+ mathmlenclosed: function mathmlenclosed(aAccessible, aRoleStr, aState,
+ aFlags, aContext) {
+ let output = this.objectOutputFunctions.defaultFunc.
+ apply(this, [aAccessible, aRoleStr, aState, aFlags, aContext]);
+ this._addMencloseNotations(output, aAccessible);
+ return output;
+ }
+ }
+};
+
+/**
+ * Generates speech utterances from objects, actions and state changes.
+ * An utterance is an array of speech data.
+ *
+ * It should not be assumed that flattening an utterance array would create a
+ * gramatically correct sentence. For example, {@link genForObject} might
+ * return: ['graphic', 'Welcome to my home page'].
+ * Each string element in an utterance should be gramatically correct in itself.
+ * Another example from {@link genForObject}: ['list item 2 of 5', 'Alabama'].
+ *
+ * An utterance is ordered from the least to the most important. Speaking the
+ * last string usually makes sense, but speaking the first often won't.
+ * For example {@link genForAction} might return ['button', 'clicked'] for a
+ * clicked event. Speaking only 'clicked' makes sense. Speaking 'button' does
+ * not.
+ */
+this.UtteranceGenerator = { // jshint ignore:line
+ __proto__: OutputGenerator, // jshint ignore:line
+
+ gActionMap: {
+ jump: 'jumpAction',
+ press: 'pressAction',
+ check: 'checkAction',
+ uncheck: 'uncheckAction',
+ on: 'onAction',
+ off: 'offAction',
+ select: 'selectAction',
+ unselect: 'unselectAction',
+ open: 'openAction',
+ close: 'closeAction',
+ switch: 'switchAction',
+ click: 'clickAction',
+ collapse: 'collapseAction',
+ expand: 'expandAction',
+ activate: 'activateAction',
+ cycle: 'cycleAction'
+ },
+
+ //TODO: May become more verbose in the future.
+ genForAction: function genForAction(aObject, aActionName) {
+ return [{string: this.gActionMap[aActionName]}];
+ },
+
+ genForLiveRegion:
+ function genForLiveRegion(aContext, aIsHide, aModifiedText) {
+ let utterance = [];
+ if (aIsHide) {
+ utterance.push({string: 'hidden'});
+ }
+ return utterance.concat(aModifiedText || this.genForContext(aContext));
+ },
+
+ genForAnnouncement: function genForAnnouncement(aAnnouncement) {
+ return [{
+ string: aAnnouncement
+ }];
+ },
+
+ genForTabStateChange: function genForTabStateChange(aObject, aTabState) {
+ switch (aTabState) {
+ case 'newtab':
+ return [{string: 'tabNew'}];
+ case 'loading':
+ return [{string: 'tabLoading'}];
+ case 'loaded':
+ return [aObject.name, {string: 'tabLoaded'}];
+ case 'loadstopped':
+ return [{string: 'tabLoadStopped'}];
+ case 'reload':
+ return [{string: 'tabReload'}];
+ default:
+ return [];
+ }
+ },
+
+ genForEditingMode: function genForEditingMode(aIsEditing) {
+ return [{string: aIsEditing ? 'editingMode' : 'navigationMode'}];
+ },
+
+ objectOutputFunctions: {
+
+ __proto__: OutputGenerator.objectOutputFunctions, // jshint ignore:line
+
+ defaultFunc: function defaultFunc() {
+ return this.objectOutputFunctions._generateBaseOutput.apply(
+ this, arguments);
+ },
+
+ heading: function heading(aAccessible, aRoleStr, aState, aFlags) {
+ let level = {};
+ aAccessible.groupPosition(level, {}, {});
+ let utterance = [{string: 'headingLevel', args: [level.value]}];
+
+ this._addName(utterance, aAccessible, aFlags);
+ this._addLandmark(utterance, aAccessible);
+
+ return utterance;
+ },
+
+ listitem: function listitem(aAccessible, aRoleStr, aState, aFlags) {
+ let itemno = {};
+ let itemof = {};
+ aAccessible.groupPosition({}, itemof, itemno);
+ let utterance = [];
+ if (itemno.value == 1) {
+ // Start of list
+ utterance.push({string: 'listStart'});
+ }
+ else if (itemno.value == itemof.value) {
+ // last item
+ utterance.push({string: 'listEnd'});
+ }
+
+ this._addName(utterance, aAccessible, aFlags);
+ this._addLandmark(utterance, aAccessible);
+
+ return utterance;
+ },
+
+ list: function list(aAccessible, aRoleStr, aState, aFlags) {
+ return this._getListUtterance
+ (aAccessible, aRoleStr, aFlags, aAccessible.childCount);
+ },
+
+ definitionlist:
+ function definitionlist(aAccessible, aRoleStr, aState, aFlags) {
+ return this._getListUtterance
+ (aAccessible, aRoleStr, aFlags, aAccessible.childCount / 2);
+ },
+
+ application: function application(aAccessible, aRoleStr, aState, aFlags) {
+ // Don't utter location of applications, it gets tiring.
+ if (aAccessible.name != aAccessible.DOMNode.location) {
+ return this.objectOutputFunctions.defaultFunc.apply(this,
+ [aAccessible, aRoleStr, aState, aFlags]);
+ }
+
+ return [];
+ },
+
+ cell: function cell(aAccessible, aRoleStr, aState, aFlags, aContext) {
+ let utterance = [];
+ let cell = aContext.getCellInfo(aAccessible);
+ if (cell) {
+ let addCellChanged =
+ function addCellChanged(aUtterance, aChanged, aString, aIndex) {
+ if (aChanged) {
+ aUtterance.push({string: aString, args: [aIndex + 1]});
+ }
+ };
+ let addExtent = function addExtent(aUtterance, aExtent, aString) {
+ if (aExtent > 1) {
+ aUtterance.push({string: aString, args: [aExtent]});
+ }
+ };
+ let addHeaders = function addHeaders(aUtterance, aHeaders) {
+ if (aHeaders.length > 0) {
+ aUtterance.push.apply(aUtterance, aHeaders);
+ }
+ };
+
+ addCellChanged(utterance, cell.columnChanged, 'columnInfo',
+ cell.columnIndex);
+ addCellChanged(utterance, cell.rowChanged, 'rowInfo', cell.rowIndex);
+
+ addExtent(utterance, cell.columnExtent, 'spansColumns');
+ addExtent(utterance, cell.rowExtent, 'spansRows');
+
+ addHeaders(utterance, cell.columnHeaders);
+ addHeaders(utterance, cell.rowHeaders);
+ }
+
+ this._addName(utterance, aAccessible, aFlags);
+ this._addLandmark(utterance, aAccessible);
+
+ return utterance;
+ },
+
+ columnheader: function columnheader() {
+ return this.objectOutputFunctions.cell.apply(this, arguments);
+ },
+
+ rowheader: function rowheader() {
+ return this.objectOutputFunctions.cell.apply(this, arguments);
+ },
+
+ statictext: function statictext(aAccessible) {
+ if (Utils.isListItemDecorator(aAccessible, true)) {
+ return [];
+ }
+
+ return this.objectOutputFunctions.defaultFunc.apply(this, arguments);
+ }
+ },
+
+ _getContextStart: function _getContextStart(aContext) {
+ return aContext.newAncestry;
+ },
+
+ _addRole: function _addRole(aOutput, aAccessible, aRoleStr) {
+ if (this.mathmlRolesSet.has(aAccessible.role)) {
+ this._addMathRoles(aOutput, aAccessible, aRoleStr);
+ } else {
+ aOutput.push({string: this._getOutputName(aRoleStr)});
+ }
+ },
+
+ _addState: function _addState(aOutput, aState, aRoleStr) {
+
+ if (aState.contains(States.UNAVAILABLE)) {
+ aOutput.push({string: 'stateUnavailable'});
+ }
+
+ if (aState.contains(States.READONLY)) {
+ aOutput.push({string: 'stateReadonly'});
+ }
+
+ // Don't utter this in Jelly Bean, we let TalkBack do it for us there.
+ // This is because we expose the checked information on the node itself.
+ // XXX: this means the checked state is always appended to the end,
+ // regardless of the utterance ordering preference.
+ if ((Utils.AndroidSdkVersion < 16 || Utils.MozBuildApp === 'browser') &&
+ aState.contains(States.CHECKABLE)) {
+ let checked = aState.contains(States.CHECKED);
+ let statetr;
+ if (aRoleStr === 'switch') {
+ statetr = checked ? 'stateOn' : 'stateOff';
+ } else {
+ statetr = checked ? 'stateChecked' : 'stateNotChecked';
+ }
+ aOutput.push({string: statetr});
+ }
+
+ if (aState.contains(States.PRESSED)) {
+ aOutput.push({string: 'statePressed'});
+ }
+
+ if (aState.contains(States.EXPANDABLE)) {
+ let statetr = aState.contains(States.EXPANDED) ?
+ 'stateExpanded' : 'stateCollapsed';
+ aOutput.push({string: statetr});
+ }
+
+ if (aState.contains(States.REQUIRED)) {
+ aOutput.push({string: 'stateRequired'});
+ }
+
+ if (aState.contains(States.TRAVERSED)) {
+ aOutput.push({string: 'stateTraversed'});
+ }
+
+ if (aState.contains(States.HASPOPUP)) {
+ aOutput.push({string: 'stateHasPopup'});
+ }
+
+ if (aState.contains(States.SELECTED)) {
+ aOutput.push({string: 'stateSelected'});
+ }
+ },
+
+ _getListUtterance:
+ function _getListUtterance(aAccessible, aRoleStr, aFlags, aItemCount) {
+ let utterance = [];
+ this._addRole(utterance, aAccessible, aRoleStr);
+ utterance.push({
+ string: this._getOutputName('listItemsCount'),
+ count: aItemCount
+ });
+
+ this._addName(utterance, aAccessible, aFlags);
+ this._addLandmark(utterance, aAccessible);
+
+ return utterance;
+ }
+};
+
+this.BrailleGenerator = { // jshint ignore:line
+ __proto__: OutputGenerator, // jshint ignore:line
+
+ genForContext: function genForContext(aContext) {
+ let output = OutputGenerator.genForContext.apply(this, arguments);
+
+ let acc = aContext.accessible;
+
+ // add the static text indicating a list item; do this for both listitems or
+ // direct first children of listitems, because these are both common
+ // browsing scenarios
+ let addListitemIndicator = function addListitemIndicator(indicator = '*') {
+ output.unshift(indicator);
+ };
+
+ if (acc.indexInParent === 1 &&
+ acc.parent.role == Roles.LISTITEM &&
+ acc.previousSibling.role == Roles.STATICTEXT) {
+ if (acc.parent.parent && acc.parent.parent.DOMNode &&
+ acc.parent.parent.DOMNode.nodeName == 'UL') {
+ addListitemIndicator();
+ } else {
+ addListitemIndicator(acc.previousSibling.name.trim());
+ }
+ } else if (acc.role == Roles.LISTITEM && acc.firstChild &&
+ acc.firstChild.role == Roles.STATICTEXT) {
+ if (acc.parent.DOMNode.nodeName == 'UL') {
+ addListitemIndicator();
+ } else {
+ addListitemIndicator(acc.firstChild.name.trim());
+ }
+ }
+
+ return output;
+ },
+
+ objectOutputFunctions: {
+
+ __proto__: OutputGenerator.objectOutputFunctions, // jshint ignore:line
+
+ defaultFunc: function defaultFunc() {
+ return this.objectOutputFunctions._generateBaseOutput.apply(
+ this, arguments);
+ },
+
+ listitem: function listitem(aAccessible, aRoleStr, aState, aFlags) {
+ let braille = [];
+
+ this._addName(braille, aAccessible, aFlags);
+ this._addLandmark(braille, aAccessible);
+
+ return braille;
+ },
+
+ cell: function cell(aAccessible, aRoleStr, aState, aFlags, aContext) {
+ let braille = [];
+ let cell = aContext.getCellInfo(aAccessible);
+ if (cell) {
+ let addHeaders = function addHeaders(aBraille, aHeaders) {
+ if (aHeaders.length > 0) {
+ aBraille.push.apply(aBraille, aHeaders);
+ }
+ };
+
+ braille.push({
+ string: this._getOutputName('cellInfo'),
+ args: [cell.columnIndex + 1, cell.rowIndex + 1]
+ });
+
+ addHeaders(braille, cell.columnHeaders);
+ addHeaders(braille, cell.rowHeaders);
+ }
+
+ this._addName(braille, aAccessible, aFlags);
+ this._addLandmark(braille, aAccessible);
+ return braille;
+ },
+
+ columnheader: function columnheader() {
+ return this.objectOutputFunctions.cell.apply(this, arguments);
+ },
+
+ rowheader: function rowheader() {
+ return this.objectOutputFunctions.cell.apply(this, arguments);
+ },
+
+ statictext: function statictext(aAccessible) {
+ // Since we customize the list bullet's output, we add the static
+ // text from the first node in each listitem, so skip it here.
+ if (Utils.isListItemDecorator(aAccessible)) {
+ return [];
+ }
+
+ return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
+ },
+
+ _useStateNotRole:
+ function _useStateNotRole(aAccessible, aRoleStr, aState, aFlags) {
+ let braille = [];
+ this._addState(braille, aState, aRoleStr);
+ this._addName(braille, aAccessible, aFlags);
+ this._addLandmark(braille, aAccessible);
+
+ return braille;
+ },
+
+ switch: function braille_generator_object_output_functions_switch() {
+ return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
+ },
+
+ checkbutton: function checkbutton() {
+ return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
+ },
+
+ radiobutton: function radiobutton() {
+ return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
+ },
+
+ togglebutton: function togglebutton() {
+ return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
+ }
+ },
+
+ _getContextStart: function _getContextStart(aContext) {
+ if (aContext.accessible.parent.role == Roles.LINK) {
+ return [aContext.accessible.parent];
+ }
+
+ return [];
+ },
+
+ _getOutputName: function _getOutputName(aName) {
+ return OutputGenerator._getOutputName(aName) + 'Abbr';
+ },
+
+ _addRole: function _addRole(aBraille, aAccessible, aRoleStr) {
+ if (this.mathmlRolesSet.has(aAccessible.role)) {
+ this._addMathRoles(aBraille, aAccessible, aRoleStr);
+ } else {
+ aBraille.push({string: this._getOutputName(aRoleStr)});
+ }
+ },
+
+ _addState: function _addState(aBraille, aState, aRoleStr) {
+ if (aState.contains(States.CHECKABLE)) {
+ aBraille.push({
+ string: aState.contains(States.CHECKED) ?
+ this._getOutputName('stateChecked') :
+ this._getOutputName('stateUnchecked')
+ });
+ }
+ if (aRoleStr === 'toggle button') {
+ aBraille.push({
+ string: aState.contains(States.PRESSED) ?
+ this._getOutputName('statePressed') :
+ this._getOutputName('stateUnpressed')
+ });
+ }
+ }
+};
diff --git a/accessible/jsat/PointerAdapter.jsm b/accessible/jsat/PointerAdapter.jsm
new file mode 100644
index 000000000..ff54976b7
--- /dev/null
+++ b/accessible/jsat/PointerAdapter.jsm
@@ -0,0 +1,174 @@
+/* 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, Utils, Logger, GestureSettings,
+ GestureTracker */
+/* exported PointerRelay, PointerAdapter */
+
+'use strict';
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = ['PointerRelay', 'PointerAdapter']; // jshint ignore:line
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+XPCOMUtils.defineLazyModuleGetter(this, 'Utils', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Logger', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'GestureSettings', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Gestures.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'GestureTracker', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Gestures.jsm');
+
+// The virtual touch ID generated by a mouse event.
+const MOUSE_ID = 'mouse';
+// Synthesized touch ID.
+const SYNTH_ID = -1;
+
+var PointerRelay = { // jshint ignore:line
+ /**
+ * A mapping of events we should be intercepting. Entries with a value of
+ * |true| are used for compiling high-level gesture events. Entries with a
+ * value of |false| are cancelled and do not propogate to content.
+ */
+ get _eventsOfInterest() {
+ delete this._eventsOfInterest;
+
+ switch (Utils.widgetToolkit) {
+ case 'android':
+ this._eventsOfInterest = {
+ 'touchstart' : true,
+ 'touchmove' : true,
+ 'touchend' : true };
+ break;
+
+ case 'gonk':
+ this._eventsOfInterest = {
+ 'touchstart' : true,
+ 'touchmove' : true,
+ 'touchend' : true,
+ 'mousedown' : false,
+ 'mousemove' : false,
+ 'mouseup': false,
+ 'click': false };
+ break;
+
+ default:
+ // Desktop.
+ this._eventsOfInterest = {
+ 'mousemove' : true,
+ 'mousedown' : true,
+ 'mouseup': true,
+ 'click': false
+ };
+ if ('ontouchstart' in Utils.win) {
+ for (let eventType of ['touchstart', 'touchmove', 'touchend']) {
+ this._eventsOfInterest[eventType] = true;
+ }
+ }
+ break;
+ }
+
+ return this._eventsOfInterest;
+ },
+
+ _eventMap: {
+ 'touchstart' : 'pointerdown',
+ 'mousedown' : 'pointerdown',
+ 'touchmove' : 'pointermove',
+ 'mousemove' : 'pointermove',
+ 'touchend' : 'pointerup',
+ 'mouseup': 'pointerup'
+ },
+
+ start: function PointerRelay_start(aOnPointerEvent) {
+ Logger.debug('PointerRelay.start');
+ this.onPointerEvent = aOnPointerEvent;
+ for (let eventType in this._eventsOfInterest) {
+ Utils.win.addEventListener(eventType, this, true, true);
+ }
+ },
+
+ stop: function PointerRelay_stop() {
+ Logger.debug('PointerRelay.stop');
+ delete this.lastPointerMove;
+ delete this.onPointerEvent;
+ for (let eventType in this._eventsOfInterest) {
+ Utils.win.removeEventListener(eventType, this, true, true);
+ }
+ },
+
+ handleEvent: function PointerRelay_handleEvent(aEvent) {
+ // Don't bother with chrome mouse events.
+ if (Utils.MozBuildApp === 'browser' &&
+ aEvent.view.top instanceof Ci.nsIDOMChromeWindow) {
+ return;
+ }
+ if (aEvent.mozInputSource === Ci.nsIDOMMouseEvent.MOZ_SOURCE_UNKNOWN ||
+ aEvent.isSynthesized) {
+ // Ignore events that are scripted or clicks from the a11y API.
+ return;
+ }
+
+ let changedTouches = aEvent.changedTouches || [{
+ identifier: MOUSE_ID,
+ screenX: aEvent.screenX,
+ screenY: aEvent.screenY,
+ target: aEvent.target
+ }];
+
+ if (Utils.widgetToolkit === 'android' &&
+ changedTouches.length === 1 && changedTouches[0].identifier === 1) {
+ return;
+ }
+
+ if (changedTouches.length === 1 &&
+ changedTouches[0].identifier === SYNTH_ID) {
+ return;
+ }
+
+ aEvent.preventDefault();
+ aEvent.stopImmediatePropagation();
+
+ let type = aEvent.type;
+ if (!this._eventsOfInterest[type]) {
+ return;
+ }
+ let pointerType = this._eventMap[type];
+ this.onPointerEvent({
+ type: pointerType,
+ points: Array.prototype.map.call(changedTouches,
+ function mapTouch(aTouch) {
+ return {
+ identifier: aTouch.identifier,
+ x: aTouch.screenX,
+ y: aTouch.screenY
+ };
+ }
+ )
+ });
+ }
+};
+
+this.PointerAdapter = { // jshint ignore:line
+ start: function PointerAdapter_start() {
+ Logger.debug('PointerAdapter.start');
+ GestureTracker.reset();
+ PointerRelay.start(this.handleEvent);
+ },
+
+ stop: function PointerAdapter_stop() {
+ Logger.debug('PointerAdapter.stop');
+ PointerRelay.stop();
+ GestureTracker.reset();
+ },
+
+ handleEvent: function PointerAdapter_handleEvent(aDetail) {
+ let timeStamp = Date.now();
+ GestureTracker.handle(aDetail, timeStamp);
+ }
+};
diff --git a/accessible/jsat/Presentation.jsm b/accessible/jsat/Presentation.jsm
new file mode 100644
index 000000000..6912d0ea5
--- /dev/null
+++ b/accessible/jsat/Presentation.jsm
@@ -0,0 +1,769 @@
+/* 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, Utils, Logger, BraillePresenter, Presentation,
+ UtteranceGenerator, BrailleGenerator, States, Roles, PivotContext */
+/* exported Presentation */
+
+'use strict';
+
+const {utils: Cu, interfaces: Ci} = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Logger', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'PivotContext', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'UtteranceGenerator', // jshint ignore:line
+ 'resource://gre/modules/accessibility/OutputGenerator.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'BrailleGenerator', // jshint ignore:line
+ 'resource://gre/modules/accessibility/OutputGenerator.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Roles', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Constants.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'States', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Constants.jsm');
+
+this.EXPORTED_SYMBOLS = ['Presentation']; // jshint ignore:line
+
+/**
+ * The interface for all presenter classes. A presenter could be, for example,
+ * a speech output module, or a visual cursor indicator.
+ */
+function Presenter() {}
+
+Presenter.prototype = {
+ /**
+ * The type of presenter. Used for matching it with the appropriate output method.
+ */
+ type: 'Base',
+
+ /**
+ * The virtual cursor's position changed.
+ * @param {PivotContext} aContext the context object for the new pivot
+ * position.
+ * @param {int} aReason the reason for the pivot change.
+ * See nsIAccessiblePivot.
+ * @param {bool} aIsFromUserInput the pivot change was invoked by the user
+ */
+ pivotChanged: function pivotChanged(aContext, aReason, aIsFromUserInput) {}, // jshint ignore:line
+
+ /**
+ * An object's action has been invoked.
+ * @param {nsIAccessible} aObject the object that has been invoked.
+ * @param {string} aActionName the name of the action.
+ */
+ actionInvoked: function actionInvoked(aObject, aActionName) {}, // jshint ignore:line
+
+ /**
+ * Text has changed, either by the user or by the system. TODO.
+ */
+ textChanged: function textChanged(aAccessible, aIsInserted, aStartOffset, // jshint ignore:line
+ aLength, aText, aModifiedText) {}, // jshint ignore:line
+
+ /**
+ * Text selection has changed. TODO.
+ */
+ textSelectionChanged: function textSelectionChanged(
+ aText, aStart, aEnd, aOldStart, aOldEnd, aIsFromUserInput) {}, // jshint ignore:line
+
+ /**
+ * Selection has changed. TODO.
+ * @param {nsIAccessible} aObject the object that has been selected.
+ */
+ selectionChanged: function selectionChanged(aObject) {}, // jshint ignore:line
+
+ /**
+ * Name has changed.
+ * @param {nsIAccessible} aAccessible the object whose value has changed.
+ */
+ nameChanged: function nameChanged(aAccessible) {}, // jshint ignore: line
+
+ /**
+ * Value has changed.
+ * @param {nsIAccessible} aAccessible the object whose value has changed.
+ */
+ valueChanged: function valueChanged(aAccessible) {}, // jshint ignore:line
+
+ /**
+ * The tab, or the tab's document state has changed.
+ * @param {nsIAccessible} aDocObj the tab document accessible that has had its
+ * state changed, or null if the tab has no associated document yet.
+ * @param {string} aPageState the state name for the tab, valid states are:
+ * 'newtab', 'loading', 'newdoc', 'loaded', 'stopped', and 'reload'.
+ */
+ tabStateChanged: function tabStateChanged(aDocObj, aPageState) {}, // jshint ignore:line
+
+ /**
+ * The current tab has changed.
+ * @param {PivotContext} aDocContext context object for tab's
+ * document.
+ * @param {PivotContext} aVCContext context object for tab's current
+ * virtual cursor position.
+ */
+ tabSelected: function tabSelected(aDocContext, aVCContext) {}, // jshint ignore:line
+
+ /**
+ * The viewport has changed, either a scroll, pan, zoom, or
+ * landscape/portrait toggle.
+ * @param {Window} aWindow window of viewport that changed.
+ * @param {PivotContext} aCurrentContext context of last pivot change.
+ */
+ viewportChanged: function viewportChanged(aWindow, aCurrentContext) {}, // jshint ignore:line
+
+ /**
+ * We have entered or left text editing mode.
+ */
+ editingModeChanged: function editingModeChanged(aIsEditing) {}, // jshint ignore:line
+
+ /**
+ * Announce something. Typically an app state change.
+ */
+ announce: function announce(aAnnouncement) {}, // jshint ignore:line
+
+
+ /**
+ * User tried to move cursor forward or backward with no success.
+ * @param {string} aMoveMethod move method that was used (eg. 'moveNext').
+ */
+ noMove: function noMove(aMoveMethod) {},
+
+ /**
+ * Announce a live region.
+ * @param {PivotContext} aContext context object for an accessible.
+ * @param {boolean} aIsPolite A politeness level for a live region.
+ * @param {boolean} aIsHide An indicator of hide/remove event.
+ * @param {string} aModifiedText Optional modified text.
+ */
+ liveRegion: function liveRegionShown(aContext, aIsPolite, aIsHide, // jshint ignore:line
+ aModifiedText) {} // jshint ignore:line
+};
+
+/**
+ * Visual presenter. Draws a box around the virtual cursor's position.
+ */
+function VisualPresenter() {}
+
+VisualPresenter.prototype = Object.create(Presenter.prototype);
+
+VisualPresenter.prototype.type = 'Visual';
+
+/**
+ * The padding in pixels between the object and the highlight border.
+ */
+VisualPresenter.prototype.BORDER_PADDING = 2;
+
+VisualPresenter.prototype.viewportChanged =
+ function VisualPresenter_viewportChanged(aWindow, aCurrentContext) {
+ if (!aCurrentContext) {
+ return null;
+ }
+
+ let currentAcc = aCurrentContext.accessibleForBounds;
+ let start = aCurrentContext.startOffset;
+ let end = aCurrentContext.endOffset;
+ if (Utils.isAliveAndVisible(currentAcc)) {
+ let bounds = (start === -1 && end === -1) ? Utils.getBounds(currentAcc) :
+ Utils.getTextBounds(currentAcc, start, end);
+
+ return {
+ type: this.type,
+ details: {
+ eventType: 'viewport-change',
+ bounds: bounds,
+ padding: this.BORDER_PADDING
+ }
+ };
+ }
+
+ return null;
+ };
+
+VisualPresenter.prototype.pivotChanged =
+ function VisualPresenter_pivotChanged(aContext) {
+ if (!aContext.accessible) {
+ // XXX: Don't hide because another vc may be using the highlight.
+ return null;
+ }
+
+ try {
+ aContext.accessibleForBounds.scrollTo(
+ Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);
+
+ let bounds = (aContext.startOffset === -1 && aContext.endOffset === -1) ?
+ aContext.bounds : Utils.getTextBounds(aContext.accessibleForBounds,
+ aContext.startOffset,
+ aContext.endOffset);
+
+ return {
+ type: this.type,
+ details: {
+ eventType: 'vc-change',
+ bounds: bounds,
+ padding: this.BORDER_PADDING
+ }
+ };
+ } catch (e) {
+ Logger.logException(e, 'Failed to get bounds');
+ return null;
+ }
+ };
+
+VisualPresenter.prototype.tabSelected =
+ function VisualPresenter_tabSelected(aDocContext, aVCContext) {
+ return this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE);
+ };
+
+VisualPresenter.prototype.tabStateChanged =
+ function VisualPresenter_tabStateChanged(aDocObj, aPageState) {
+ if (aPageState == 'newdoc') {
+ return {type: this.type, details: {eventType: 'tabstate-change'}};
+ }
+
+ return null;
+ };
+
+/**
+ * Android presenter. Fires Android a11y events.
+ */
+function AndroidPresenter() {}
+
+AndroidPresenter.prototype = Object.create(Presenter.prototype);
+
+AndroidPresenter.prototype.type = 'Android';
+
+// Android AccessibilityEvent type constants.
+AndroidPresenter.prototype.ANDROID_VIEW_CLICKED = 0x01;
+AndroidPresenter.prototype.ANDROID_VIEW_LONG_CLICKED = 0x02;
+AndroidPresenter.prototype.ANDROID_VIEW_SELECTED = 0x04;
+AndroidPresenter.prototype.ANDROID_VIEW_FOCUSED = 0x08;
+AndroidPresenter.prototype.ANDROID_VIEW_TEXT_CHANGED = 0x10;
+AndroidPresenter.prototype.ANDROID_WINDOW_STATE_CHANGED = 0x20;
+AndroidPresenter.prototype.ANDROID_VIEW_HOVER_ENTER = 0x80;
+AndroidPresenter.prototype.ANDROID_VIEW_HOVER_EXIT = 0x100;
+AndroidPresenter.prototype.ANDROID_VIEW_SCROLLED = 0x1000;
+AndroidPresenter.prototype.ANDROID_VIEW_TEXT_SELECTION_CHANGED = 0x2000;
+AndroidPresenter.prototype.ANDROID_ANNOUNCEMENT = 0x4000;
+AndroidPresenter.prototype.ANDROID_VIEW_ACCESSIBILITY_FOCUSED = 0x8000;
+AndroidPresenter.prototype.ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY =
+ 0x20000;
+
+AndroidPresenter.prototype.pivotChanged =
+ function AndroidPresenter_pivotChanged(aContext, aReason) {
+ if (!aContext.accessible) {
+ return null;
+ }
+
+ let androidEvents = [];
+
+ let isExploreByTouch = (aReason == Ci.nsIAccessiblePivot.REASON_POINT &&
+ Utils.AndroidSdkVersion >= 14);
+ let focusEventType = (Utils.AndroidSdkVersion >= 16) ?
+ this.ANDROID_VIEW_ACCESSIBILITY_FOCUSED :
+ this.ANDROID_VIEW_FOCUSED;
+
+ if (isExploreByTouch) {
+ // This isn't really used by TalkBack so this is a half-hearted attempt
+ // for now.
+ androidEvents.push({eventType: this.ANDROID_VIEW_HOVER_EXIT, text: []});
+ }
+
+ let brailleOutput = {};
+ if (Utils.AndroidSdkVersion >= 16) {
+ if (!this._braillePresenter) {
+ this._braillePresenter = new BraillePresenter();
+ }
+ brailleOutput = this._braillePresenter.pivotChanged(aContext, aReason).
+ details;
+ }
+
+ if (aReason === Ci.nsIAccessiblePivot.REASON_TEXT) {
+ if (Utils.AndroidSdkVersion >= 16) {
+ let adjustedText = aContext.textAndAdjustedOffsets;
+
+ androidEvents.push({
+ eventType: this.ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
+ text: [adjustedText.text],
+ fromIndex: adjustedText.startOffset,
+ toIndex: adjustedText.endOffset
+ });
+ }
+ } else {
+ let state = Utils.getState(aContext.accessible);
+ androidEvents.push({eventType: (isExploreByTouch) ?
+ this.ANDROID_VIEW_HOVER_ENTER : focusEventType,
+ text: Utils.localize(UtteranceGenerator.genForContext(
+ aContext)),
+ bounds: aContext.bounds,
+ clickable: aContext.accessible.actionCount > 0,
+ checkable: state.contains(States.CHECKABLE),
+ checked: state.contains(States.CHECKED),
+ brailleOutput: brailleOutput});
+ }
+
+
+ return {
+ type: this.type,
+ details: androidEvents
+ };
+ };
+
+AndroidPresenter.prototype.actionInvoked =
+ function AndroidPresenter_actionInvoked(aObject, aActionName) {
+ let state = Utils.getState(aObject);
+
+ // Checkable objects use TalkBack's text derived from the event state,
+ // so we don't populate the text here.
+ let text = '';
+ if (!state.contains(States.CHECKABLE)) {
+ text = Utils.localize(UtteranceGenerator.genForAction(aObject,
+ aActionName));
+ }
+
+ return {
+ type: this.type,
+ details: [{
+ eventType: this.ANDROID_VIEW_CLICKED,
+ text: text,
+ checked: state.contains(States.CHECKED)
+ }]
+ };
+ };
+
+AndroidPresenter.prototype.tabSelected =
+ function AndroidPresenter_tabSelected(aDocContext, aVCContext) {
+ // Send a pivot change message with the full context utterance for this doc.
+ return this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE);
+ };
+
+AndroidPresenter.prototype.tabStateChanged =
+ function AndroidPresenter_tabStateChanged(aDocObj, aPageState) {
+ return this.announce(
+ UtteranceGenerator.genForTabStateChange(aDocObj, aPageState));
+ };
+
+AndroidPresenter.prototype.textChanged = function AndroidPresenter_textChanged(
+ aAccessible, aIsInserted, aStart, aLength, aText, aModifiedText) {
+ let eventDetails = {
+ eventType: this.ANDROID_VIEW_TEXT_CHANGED,
+ text: [aText],
+ fromIndex: aStart,
+ removedCount: 0,
+ addedCount: 0
+ };
+
+ if (aIsInserted) {
+ eventDetails.addedCount = aLength;
+ eventDetails.beforeText =
+ aText.substring(0, aStart) + aText.substring(aStart + aLength);
+ } else {
+ eventDetails.removedCount = aLength;
+ eventDetails.beforeText =
+ aText.substring(0, aStart) + aModifiedText + aText.substring(aStart);
+ }
+
+ return {type: this.type, details: [eventDetails]};
+ };
+
+AndroidPresenter.prototype.textSelectionChanged =
+ function AndroidPresenter_textSelectionChanged(aText, aStart, aEnd, aOldStart,
+ aOldEnd, aIsFromUserInput) {
+ let androidEvents = [];
+
+ if (Utils.AndroidSdkVersion >= 14 && !aIsFromUserInput) {
+ if (!this._braillePresenter) {
+ this._braillePresenter = new BraillePresenter();
+ }
+ let brailleOutput = this._braillePresenter.textSelectionChanged(
+ aText, aStart, aEnd, aOldStart, aOldEnd, aIsFromUserInput).details;
+
+ androidEvents.push({
+ eventType: this.ANDROID_VIEW_TEXT_SELECTION_CHANGED,
+ text: [aText],
+ fromIndex: aStart,
+ toIndex: aEnd,
+ itemCount: aText.length,
+ brailleOutput: brailleOutput
+ });
+ }
+
+ if (Utils.AndroidSdkVersion >= 16 && aIsFromUserInput) {
+ let [from, to] = aOldStart < aStart ?
+ [aOldStart, aStart] : [aStart, aOldStart];
+ androidEvents.push({
+ eventType: this.ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
+ text: [aText],
+ fromIndex: from,
+ toIndex: to
+ });
+ }
+
+ return {
+ type: this.type,
+ details: androidEvents
+ };
+ };
+
+AndroidPresenter.prototype.viewportChanged =
+ function AndroidPresenter_viewportChanged(aWindow, aCurrentContext) {
+ if (Utils.AndroidSdkVersion < 14) {
+ return null;
+ }
+
+ let events = [{
+ eventType: this.ANDROID_VIEW_SCROLLED,
+ text: [],
+ scrollX: aWindow.scrollX,
+ scrollY: aWindow.scrollY,
+ maxScrollX: aWindow.scrollMaxX,
+ maxScrollY: aWindow.scrollMaxY
+ }];
+
+ if (Utils.AndroidSdkVersion >= 16 && aCurrentContext) {
+ let currentAcc = aCurrentContext.accessibleForBounds;
+ if (Utils.isAliveAndVisible(currentAcc)) {
+ events.push({
+ eventType: this.ANDROID_VIEW_ACCESSIBILITY_FOCUSED,
+ bounds: Utils.getBounds(currentAcc)
+ });
+ }
+ }
+
+ return {
+ type: this.type,
+ details: events
+ };
+ };
+
+AndroidPresenter.prototype.editingModeChanged =
+ function AndroidPresenter_editingModeChanged(aIsEditing) {
+ return this.announce(UtteranceGenerator.genForEditingMode(aIsEditing));
+ };
+
+AndroidPresenter.prototype.announce =
+ function AndroidPresenter_announce(aAnnouncement) {
+ let localizedAnnouncement = Utils.localize(aAnnouncement).join(' ');
+ return {
+ type: this.type,
+ details: [{
+ eventType: (Utils.AndroidSdkVersion >= 16) ?
+ this.ANDROID_ANNOUNCEMENT : this.ANDROID_VIEW_TEXT_CHANGED,
+ text: [localizedAnnouncement],
+ addedCount: localizedAnnouncement.length,
+ removedCount: 0,
+ fromIndex: 0
+ }]
+ };
+ };
+
+AndroidPresenter.prototype.liveRegion =
+ function AndroidPresenter_liveRegion(aContext, aIsPolite,
+ aIsHide, aModifiedText) {
+ return this.announce(
+ UtteranceGenerator.genForLiveRegion(aContext, aIsHide, aModifiedText));
+ };
+
+AndroidPresenter.prototype.noMove =
+ function AndroidPresenter_noMove(aMoveMethod) {
+ return {
+ type: this.type,
+ details: [
+ { eventType: this.ANDROID_VIEW_ACCESSIBILITY_FOCUSED,
+ exitView: aMoveMethod,
+ text: ['']
+ }]
+ };
+ };
+
+/**
+ * A B2G presenter for Gaia.
+ */
+function B2GPresenter() {}
+
+B2GPresenter.prototype = Object.create(Presenter.prototype);
+
+B2GPresenter.prototype.type = 'B2G';
+
+B2GPresenter.prototype.keyboardEchoSetting =
+ new PrefCache('accessibility.accessfu.keyboard_echo');
+B2GPresenter.prototype.NO_ECHO = 0;
+B2GPresenter.prototype.CHARACTER_ECHO = 1;
+B2GPresenter.prototype.WORD_ECHO = 2;
+B2GPresenter.prototype.CHARACTER_AND_WORD_ECHO = 3;
+
+/**
+ * A pattern used for haptic feedback.
+ * @type {Array}
+ */
+B2GPresenter.prototype.PIVOT_CHANGE_HAPTIC_PATTERN = [40];
+
+/**
+ * Pivot move reasons.
+ * @type {Array}
+ */
+B2GPresenter.prototype.pivotChangedReasons = ['none', 'next', 'prev', 'first',
+ 'last', 'text', 'point'];
+
+B2GPresenter.prototype.pivotChanged =
+ function B2GPresenter_pivotChanged(aContext, aReason, aIsUserInput) {
+ if (!aContext.accessible) {
+ return null;
+ }
+
+ return {
+ type: this.type,
+ details: {
+ eventType: 'vc-change',
+ data: UtteranceGenerator.genForContext(aContext),
+ options: {
+ pattern: this.PIVOT_CHANGE_HAPTIC_PATTERN,
+ isKey: Utils.isActivatableOnFingerUp(aContext.accessible),
+ reason: this.pivotChangedReasons[aReason],
+ isUserInput: aIsUserInput,
+ hints: aContext.interactionHints
+ }
+ }
+ };
+ };
+
+B2GPresenter.prototype.nameChanged =
+ function B2GPresenter_nameChanged(aAccessible, aIsPolite = true) {
+ return {
+ type: this.type,
+ details: {
+ eventType: 'name-change',
+ data: aAccessible.name,
+ options: {enqueue: aIsPolite}
+ }
+ };
+ };
+
+B2GPresenter.prototype.valueChanged =
+ function B2GPresenter_valueChanged(aAccessible, aIsPolite = true) {
+
+ // the editable value changes are handled in the text changed presenter
+ if (Utils.getState(aAccessible).contains(States.EDITABLE)) {
+ return null;
+ }
+
+ return {
+ type: this.type,
+ details: {
+ eventType: 'value-change',
+ data: aAccessible.value,
+ options: {enqueue: aIsPolite}
+ }
+ };
+ };
+
+B2GPresenter.prototype.textChanged = function B2GPresenter_textChanged(
+ aAccessible, aIsInserted, aStart, aLength, aText, aModifiedText) {
+ let echoSetting = this.keyboardEchoSetting.value;
+ let text = '';
+
+ if (echoSetting == this.CHARACTER_ECHO ||
+ echoSetting == this.CHARACTER_AND_WORD_ECHO) {
+ text = aModifiedText;
+ }
+
+ // add word if word boundary is added
+ if ((echoSetting == this.WORD_ECHO ||
+ echoSetting == this.CHARACTER_AND_WORD_ECHO) &&
+ aIsInserted && aLength === 1) {
+ let accText = aAccessible.QueryInterface(Ci.nsIAccessibleText);
+ let startBefore = {}, endBefore = {};
+ let startAfter = {}, endAfter = {};
+ accText.getTextBeforeOffset(aStart,
+ Ci.nsIAccessibleText.BOUNDARY_WORD_END, startBefore, endBefore);
+ let maybeWord = accText.getTextBeforeOffset(aStart + 1,
+ Ci.nsIAccessibleText.BOUNDARY_WORD_END, startAfter, endAfter);
+ if (endBefore.value !== endAfter.value) {
+ text += maybeWord;
+ }
+ }
+
+ return {
+ type: this.type,
+ details: {
+ eventType: 'text-change',
+ data: text
+ }
+ };
+
+ };
+
+B2GPresenter.prototype.actionInvoked =
+ function B2GPresenter_actionInvoked(aObject, aActionName) {
+ return {
+ type: this.type,
+ details: {
+ eventType: 'action',
+ data: UtteranceGenerator.genForAction(aObject, aActionName)
+ }
+ };
+ };
+
+B2GPresenter.prototype.liveRegion = function B2GPresenter_liveRegion(aContext,
+ aIsPolite, aIsHide, aModifiedText) {
+ return {
+ type: this.type,
+ details: {
+ eventType: 'liveregion-change',
+ data: UtteranceGenerator.genForLiveRegion(aContext, aIsHide,
+ aModifiedText),
+ options: {enqueue: aIsPolite}
+ }
+ };
+ };
+
+B2GPresenter.prototype.announce =
+ function B2GPresenter_announce(aAnnouncement) {
+ return {
+ type: this.type,
+ details: {
+ eventType: 'announcement',
+ data: aAnnouncement
+ }
+ };
+ };
+
+B2GPresenter.prototype.noMove =
+ function B2GPresenter_noMove(aMoveMethod) {
+ return {
+ type: this.type,
+ details: {
+ eventType: 'no-move',
+ data: aMoveMethod
+ }
+ };
+ };
+
+/**
+ * A braille presenter
+ */
+function BraillePresenter() {}
+
+BraillePresenter.prototype = Object.create(Presenter.prototype);
+
+BraillePresenter.prototype.type = 'Braille';
+
+BraillePresenter.prototype.pivotChanged =
+ function BraillePresenter_pivotChanged(aContext) {
+ if (!aContext.accessible) {
+ return null;
+ }
+
+ return {
+ type: this.type,
+ details: {
+ output: Utils.localize(BrailleGenerator.genForContext(aContext)).join(
+ ' '),
+ selectionStart: 0,
+ selectionEnd: 0
+ }
+ };
+ };
+
+BraillePresenter.prototype.textSelectionChanged =
+ function BraillePresenter_textSelectionChanged(aText, aStart, aEnd) {
+ return {
+ type: this.type,
+ details: {
+ selectionStart: aStart,
+ selectionEnd: aEnd
+ }
+ };
+ };
+
+this.Presentation = { // jshint ignore:line
+ get presenters() {
+ delete this.presenters;
+ let presenterMap = {
+ 'mobile/android': [VisualPresenter, AndroidPresenter],
+ 'b2g': [VisualPresenter, B2GPresenter],
+ 'browser': [VisualPresenter, B2GPresenter, AndroidPresenter]
+ };
+ this.presenters = presenterMap[Utils.MozBuildApp].map(P => new P());
+ return this.presenters;
+ },
+
+ get displayedAccessibles() {
+ delete this.displayedAccessibles;
+ this.displayedAccessibles = new WeakMap();
+ return this.displayedAccessibles;
+ },
+
+ pivotChanged: function Presentation_pivotChanged(
+ aPosition, aOldPosition, aReason, aStartOffset, aEndOffset, aIsUserInput) {
+ let context = new PivotContext(
+ aPosition, aOldPosition, aStartOffset, aEndOffset);
+ if (context.accessible) {
+ this.displayedAccessibles.set(context.accessible.document.window, context);
+ }
+
+ return this.presenters.map(p => p.pivotChanged(context, aReason, aIsUserInput));
+ },
+
+ actionInvoked: function Presentation_actionInvoked(aObject, aActionName) {
+ return this.presenters.map(p => p.actionInvoked(aObject, aActionName));
+ },
+
+ textChanged: function Presentation_textChanged(aAccessible, aIsInserted,
+ aStartOffset, aLength, aText,
+ aModifiedText) {
+ return this.presenters.map(p => p.textChanged(aAccessible, aIsInserted,
+ aStartOffset, aLength,
+ aText, aModifiedText));
+ },
+
+ textSelectionChanged: function textSelectionChanged(aText, aStart, aEnd,
+ aOldStart, aOldEnd,
+ aIsFromUserInput) {
+ return this.presenters.map(p => p.textSelectionChanged(aText, aStart, aEnd,
+ aOldStart, aOldEnd,
+ aIsFromUserInput));
+ },
+
+ nameChanged: function nameChanged(aAccessible) {
+ return this.presenters.map(p => p.nameChanged(aAccessible));
+ },
+
+ valueChanged: function valueChanged(aAccessible) {
+ return this.presenters.map(p => p.valueChanged(aAccessible));
+ },
+
+ tabStateChanged: function Presentation_tabStateChanged(aDocObj, aPageState) {
+ return this.presenters.map(p => p.tabStateChanged(aDocObj, aPageState));
+ },
+
+ viewportChanged: function Presentation_viewportChanged(aWindow) {
+ let context = this.displayedAccessibles.get(aWindow);
+ return this.presenters.map(p => p.viewportChanged(aWindow, context));
+ },
+
+ editingModeChanged: function Presentation_editingModeChanged(aIsEditing) {
+ return this.presenters.map(p => p.editingModeChanged(aIsEditing));
+ },
+
+ announce: function Presentation_announce(aAnnouncement) {
+ // XXX: Typically each presenter uses the UtteranceGenerator,
+ // but there really isn't a point here.
+ return this.presenters.map(p => p.announce(UtteranceGenerator.genForAnnouncement(aAnnouncement)));
+ },
+
+ noMove: function Presentation_noMove(aMoveMethod) {
+ return this.presenters.map(p => p.noMove(aMoveMethod));
+ },
+
+ liveRegion: function Presentation_liveRegion(aAccessible, aIsPolite, aIsHide,
+ aModifiedText) {
+ let context;
+ if (!aModifiedText) {
+ context = new PivotContext(aAccessible, null, -1, -1, true,
+ aIsHide ? true : false);
+ }
+ return this.presenters.map(p => p.liveRegion(context, aIsPolite, aIsHide,
+ aModifiedText));
+ }
+};
diff --git a/accessible/jsat/Traversal.jsm b/accessible/jsat/Traversal.jsm
new file mode 100644
index 000000000..5b3bbdf89
--- /dev/null
+++ b/accessible/jsat/Traversal.jsm
@@ -0,0 +1,419 @@
+/* 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 PrefCache, Roles, Prefilters, States, Filters, Utils,
+ TraversalRules, Components, XPCOMUtils */
+/* exported TraversalRules, TraversalHelper */
+
+'use strict';
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = ['TraversalRules', 'TraversalHelper']; // jshint ignore:line
+
+Cu.import('resource://gre/modules/accessibility/Utils.jsm');
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Roles', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Constants.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Filters', // 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, 'Prefilters', // jshint ignore:line
+ 'resource://gre/modules/accessibility/Constants.jsm');
+
+var gSkipEmptyImages = new PrefCache('accessibility.accessfu.skip_empty_images');
+
+function BaseTraversalRule(aRoles, aMatchFunc, aPreFilter, aContainerRule) {
+ this._explicitMatchRoles = new Set(aRoles);
+ this._matchRoles = aRoles;
+ if (aRoles.length) {
+ if (aRoles.indexOf(Roles.LABEL) < 0) {
+ this._matchRoles.push(Roles.LABEL);
+ }
+ if (aRoles.indexOf(Roles.INTERNAL_FRAME) < 0) {
+ // Used for traversing in to child OOP frames.
+ this._matchRoles.push(Roles.INTERNAL_FRAME);
+ }
+ }
+ this._matchFunc = aMatchFunc || function() { return Filters.MATCH; };
+ this.preFilter = aPreFilter || gSimplePreFilter;
+ this.containerRule = aContainerRule;
+}
+
+BaseTraversalRule.prototype = {
+ getMatchRoles: function BaseTraversalRule_getmatchRoles(aRoles) {
+ aRoles.value = this._matchRoles;
+ return aRoles.value.length;
+ },
+
+ match: function BaseTraversalRule_match(aAccessible)
+ {
+ let role = aAccessible.role;
+ if (role == Roles.INTERNAL_FRAME) {
+ return (Utils.getMessageManager(aAccessible.DOMNode)) ?
+ Filters.MATCH | Filters.IGNORE_SUBTREE : Filters.IGNORE;
+ }
+
+ let matchResult =
+ (this._explicitMatchRoles.has(role) || !this._explicitMatchRoles.size) ?
+ this._matchFunc(aAccessible) : Filters.IGNORE;
+
+ // If we are on a label that nests a checkbox/radio we should land on it.
+ // It is a bigger touch target, and it reduces clutter.
+ if (role == Roles.LABEL && !(matchResult & Filters.IGNORE_SUBTREE)) {
+ let control = Utils.getEmbeddedControl(aAccessible);
+ if (control && this._explicitMatchRoles.has(control.role)) {
+ matchResult = this._matchFunc(control) | Filters.IGNORE_SUBTREE;
+ }
+ }
+
+ return matchResult;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessibleTraversalRule])
+};
+
+var gSimpleTraversalRoles =
+ [Roles.MENUITEM,
+ Roles.LINK,
+ Roles.PAGETAB,
+ Roles.GRAPHIC,
+ Roles.STATICTEXT,
+ Roles.TEXT_LEAF,
+ Roles.PUSHBUTTON,
+ Roles.CHECKBUTTON,
+ Roles.RADIOBUTTON,
+ Roles.COMBOBOX,
+ Roles.PROGRESSBAR,
+ Roles.BUTTONDROPDOWN,
+ Roles.BUTTONMENU,
+ Roles.CHECK_MENU_ITEM,
+ Roles.PASSWORD_TEXT,
+ Roles.RADIO_MENU_ITEM,
+ Roles.TOGGLE_BUTTON,
+ Roles.ENTRY,
+ Roles.KEY,
+ Roles.HEADER,
+ Roles.HEADING,
+ Roles.SLIDER,
+ Roles.SPINBUTTON,
+ Roles.OPTION,
+ Roles.LISTITEM,
+ Roles.GRID_CELL,
+ Roles.COLUMNHEADER,
+ Roles.ROWHEADER,
+ Roles.STATUSBAR,
+ Roles.SWITCH,
+ Roles.MATHML_MATH];
+
+var gSimpleMatchFunc = function gSimpleMatchFunc(aAccessible) {
+ // An object is simple, if it either has a single child lineage,
+ // or has a flat subtree.
+ function isSingleLineage(acc) {
+ for (let child = acc; child; child = child.firstChild) {
+ if (Utils.visibleChildCount(child) > 1) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ function isFlatSubtree(acc) {
+ for (let child = acc.firstChild; child; child = child.nextSibling) {
+ // text leafs inherit the actionCount of any ancestor that has a click
+ // listener.
+ if ([Roles.TEXT_LEAF, Roles.STATICTEXT].indexOf(child.role) >= 0) {
+ continue;
+ }
+ if (Utils.visibleChildCount(child) > 0 || child.actionCount > 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ switch (aAccessible.role) {
+ case Roles.COMBOBOX:
+ // We don't want to ignore the subtree because this is often
+ // where the list box hangs out.
+ return Filters.MATCH;
+ case Roles.TEXT_LEAF:
+ {
+ // Nameless text leaves are boring, skip them.
+ let name = aAccessible.name;
+ return (name && name.trim()) ? Filters.MATCH : Filters.IGNORE;
+ }
+ case Roles.STATICTEXT:
+ // Ignore prefix static text in list items. They are typically bullets or numbers.
+ return Utils.isListItemDecorator(aAccessible) ?
+ Filters.IGNORE : Filters.MATCH;
+ case Roles.GRAPHIC:
+ return TraversalRules._shouldSkipImage(aAccessible);
+ case Roles.HEADER:
+ case Roles.HEADING:
+ case Roles.COLUMNHEADER:
+ case Roles.ROWHEADER:
+ case Roles.STATUSBAR:
+ if ((aAccessible.childCount > 0 || aAccessible.name) &&
+ (isSingleLineage(aAccessible) || isFlatSubtree(aAccessible))) {
+ return Filters.MATCH | Filters.IGNORE_SUBTREE;
+ }
+ return Filters.IGNORE;
+ case Roles.GRID_CELL:
+ return isSingleLineage(aAccessible) || isFlatSubtree(aAccessible) ?
+ Filters.MATCH | Filters.IGNORE_SUBTREE : Filters.IGNORE;
+ case Roles.LISTITEM:
+ {
+ let item = aAccessible.childCount === 2 &&
+ aAccessible.firstChild.role === Roles.STATICTEXT ?
+ aAccessible.lastChild : aAccessible;
+ return isSingleLineage(item) || isFlatSubtree(item) ?
+ Filters.MATCH | Filters.IGNORE_SUBTREE : Filters.IGNORE;
+ }
+ default:
+ // Ignore the subtree, if there is one. So that we don't land on
+ // the same content that was already presented by its parent.
+ return Filters.MATCH |
+ Filters.IGNORE_SUBTREE;
+ }
+};
+
+var gSimplePreFilter = Prefilters.DEFUNCT |
+ Prefilters.INVISIBLE |
+ Prefilters.ARIA_HIDDEN |
+ Prefilters.TRANSPARENT;
+
+this.TraversalRules = { // jshint ignore:line
+ Simple: new BaseTraversalRule(gSimpleTraversalRoles, gSimpleMatchFunc),
+
+ SimpleOnScreen: new BaseTraversalRule(
+ gSimpleTraversalRoles, gSimpleMatchFunc,
+ Prefilters.DEFUNCT | Prefilters.INVISIBLE | Prefilters.ARIA_HIDDEN |
+ Prefilters.TRANSPARENT | Prefilters.OFFSCREEN),
+
+ Anchor: new BaseTraversalRule(
+ [Roles.LINK],
+ function Anchor_match(aAccessible)
+ {
+ // We want to ignore links, only focus named anchors.
+ if (Utils.getState(aAccessible).contains(States.LINKED)) {
+ return Filters.IGNORE;
+ } else {
+ return Filters.MATCH;
+ }
+ }),
+
+ Button: new BaseTraversalRule(
+ [Roles.PUSHBUTTON,
+ Roles.SPINBUTTON,
+ Roles.TOGGLE_BUTTON,
+ Roles.BUTTONDROPDOWN,
+ Roles.BUTTONDROPDOWNGRID]),
+
+ Combobox: new BaseTraversalRule(
+ [Roles.COMBOBOX,
+ Roles.LISTBOX]),
+
+ Landmark: new BaseTraversalRule(
+ [],
+ function Landmark_match(aAccessible) {
+ return Utils.getLandmarkName(aAccessible) ? Filters.MATCH :
+ Filters.IGNORE;
+ }, null, true),
+
+ /* A rule for Android's section navigation, lands on landmarks, regions, and
+ on headings to aid navigation of traditionally structured documents */
+ Section: new BaseTraversalRule(
+ [],
+ function Section_match(aAccessible) {
+ if (aAccessible.role === Roles.HEADING) {
+ return Filters.MATCH;
+ }
+
+ let matchedRole = Utils.matchRoles(aAccessible, [
+ 'banner',
+ 'complementary',
+ 'contentinfo',
+ 'main',
+ 'navigation',
+ 'search',
+ 'region'
+ ]);
+
+ return matchedRole ? Filters.MATCH : Filters.IGNORE;
+ }, null, true),
+
+ Entry: new BaseTraversalRule(
+ [Roles.ENTRY,
+ Roles.PASSWORD_TEXT]),
+
+ FormElement: new BaseTraversalRule(
+ [Roles.PUSHBUTTON,
+ Roles.SPINBUTTON,
+ Roles.TOGGLE_BUTTON,
+ Roles.BUTTONDROPDOWN,
+ Roles.BUTTONDROPDOWNGRID,
+ Roles.COMBOBOX,
+ Roles.LISTBOX,
+ Roles.ENTRY,
+ Roles.PASSWORD_TEXT,
+ Roles.PAGETAB,
+ Roles.RADIOBUTTON,
+ Roles.RADIO_MENU_ITEM,
+ Roles.SLIDER,
+ Roles.CHECKBUTTON,
+ Roles.CHECK_MENU_ITEM,
+ Roles.SWITCH]),
+
+ Graphic: new BaseTraversalRule(
+ [Roles.GRAPHIC],
+ function Graphic_match(aAccessible) {
+ return TraversalRules._shouldSkipImage(aAccessible);
+ }),
+
+ Heading: new BaseTraversalRule(
+ [Roles.HEADING],
+ function Heading_match(aAccessible) {
+ return aAccessible.childCount > 0 ? Filters.MATCH : Filters.IGNORE;
+ }),
+
+ ListItem: new BaseTraversalRule(
+ [Roles.LISTITEM,
+ Roles.TERM]),
+
+ Link: new BaseTraversalRule(
+ [Roles.LINK],
+ function Link_match(aAccessible)
+ {
+ // We want to ignore anchors, only focus real links.
+ if (Utils.getState(aAccessible).contains(States.LINKED)) {
+ return Filters.MATCH;
+ } else {
+ return Filters.IGNORE;
+ }
+ }),
+
+ /* For TalkBack's "Control" granularity. Form conrols and links */
+ Control: new BaseTraversalRule(
+ [Roles.PUSHBUTTON,
+ Roles.SPINBUTTON,
+ Roles.TOGGLE_BUTTON,
+ Roles.BUTTONDROPDOWN,
+ Roles.BUTTONDROPDOWNGRID,
+ Roles.COMBOBOX,
+ Roles.LISTBOX,
+ Roles.ENTRY,
+ Roles.PASSWORD_TEXT,
+ Roles.PAGETAB,
+ Roles.RADIOBUTTON,
+ Roles.RADIO_MENU_ITEM,
+ Roles.SLIDER,
+ Roles.CHECKBUTTON,
+ Roles.CHECK_MENU_ITEM,
+ Roles.SWITCH,
+ Roles.LINK,
+ Roles.MENUITEM],
+ function Control_match(aAccessible)
+ {
+ // We want to ignore anchors, only focus real links.
+ if (aAccessible.role == Roles.LINK &&
+ !Utils.getState(aAccessible).contains(States.LINKED)) {
+ return Filters.IGNORE;
+ }
+ return Filters.MATCH;
+ }),
+
+ List: new BaseTraversalRule(
+ [Roles.LIST,
+ Roles.DEFINITION_LIST],
+ null, null, true),
+
+ PageTab: new BaseTraversalRule(
+ [Roles.PAGETAB]),
+
+ Paragraph: new BaseTraversalRule(
+ [Roles.PARAGRAPH,
+ Roles.SECTION],
+ function Paragraph_match(aAccessible) {
+ for (let child = aAccessible.firstChild; child; child = child.nextSibling) {
+ if (child.role === Roles.TEXT_LEAF) {
+ return Filters.MATCH | Filters.IGNORE_SUBTREE;
+ }
+ }
+
+ return Filters.IGNORE;
+ }),
+
+ RadioButton: new BaseTraversalRule(
+ [Roles.RADIOBUTTON,
+ Roles.RADIO_MENU_ITEM]),
+
+ Separator: new BaseTraversalRule(
+ [Roles.SEPARATOR]),
+
+ Table: new BaseTraversalRule(
+ [Roles.TABLE]),
+
+ Checkbox: new BaseTraversalRule(
+ [Roles.CHECKBUTTON,
+ Roles.CHECK_MENU_ITEM,
+ Roles.SWITCH /* A type of checkbox that represents on/off values */]),
+
+ _shouldSkipImage: function _shouldSkipImage(aAccessible) {
+ if (gSkipEmptyImages.value && aAccessible.name === '') {
+ return Filters.IGNORE;
+ }
+ return Filters.MATCH;
+ }
+};
+
+this.TraversalHelper = {
+ _helperPivotCache: null,
+
+ get helperPivotCache() {
+ delete this.helperPivotCache;
+ this.helperPivotCache = new WeakMap();
+ return this.helperPivotCache;
+ },
+
+ getHelperPivot: function TraversalHelper_getHelperPivot(aRoot) {
+ let pivot = this.helperPivotCache.get(aRoot.DOMNode);
+ if (!pivot) {
+ pivot = Utils.AccService.createAccessiblePivot(aRoot);
+ this.helperPivotCache.set(aRoot.DOMNode, pivot);
+ }
+
+ return pivot;
+ },
+
+ move: function TraversalHelper_move(aVirtualCursor, aMethod, aRule) {
+ let rule = TraversalRules[aRule];
+
+ if (rule.containerRule) {
+ let moved = false;
+ let helperPivot = this.getHelperPivot(aVirtualCursor.root);
+ helperPivot.position = aVirtualCursor.position;
+
+ // We continue to step through containers until there is one with an
+ // atomic child (via 'Simple') on which we could land.
+ while (!moved) {
+ if (helperPivot[aMethod](rule)) {
+ aVirtualCursor.modalRoot = helperPivot.position;
+ moved = aVirtualCursor.moveFirst(TraversalRules.Simple);
+ aVirtualCursor.modalRoot = null;
+ } else {
+ // If we failed to step to another container, break and return false.
+ break;
+ }
+ }
+
+ return moved;
+ } else {
+ return aVirtualCursor[aMethod](rule);
+ }
+ }
+
+};
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();
+ });
+};
diff --git a/accessible/jsat/content-script.js b/accessible/jsat/content-script.js
new file mode 100644
index 000000000..6ef0dadc3
--- /dev/null
+++ b/accessible/jsat/content-script.js
@@ -0,0 +1,151 @@
+/* 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/. */
+
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
+ 'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Presentation',
+ 'resource://gre/modules/accessibility/Presentation.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
+ 'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'EventManager',
+ 'resource://gre/modules/accessibility/EventManager.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'ContentControl',
+ 'resource://gre/modules/accessibility/ContentControl.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Roles',
+ 'resource://gre/modules/accessibility/Constants.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'States',
+ 'resource://gre/modules/accessibility/Constants.jsm');
+
+Logger.info('content-script.js', content.document.location);
+
+var eventManager = null;
+var contentControl = null;
+
+function forwardToParent(aMessage) {
+ // XXX: This is a silly way to make a deep copy
+ let newJSON = JSON.parse(JSON.stringify(aMessage.json));
+ newJSON.origin = 'child';
+ sendAsyncMessage(aMessage.name, newJSON);
+}
+
+function forwardToChild(aMessage, aListener, aVCPosition) {
+ let acc = aVCPosition || Utils.getVirtualCursor(content.document).position;
+
+ if (!Utils.isAliveAndVisible(acc) || acc.role != Roles.INTERNAL_FRAME) {
+ return false;
+ }
+
+ Logger.debug(() => {
+ return ['forwardToChild', Logger.accessibleToString(acc),
+ aMessage.name, JSON.stringify(aMessage.json, null, ' ')];
+ });
+
+ let mm = Utils.getMessageManager(acc.DOMNode);
+
+ if (aListener) {
+ mm.addMessageListener(aMessage.name, aListener);
+ }
+
+ // XXX: This is a silly way to make a deep copy
+ let newJSON = JSON.parse(JSON.stringify(aMessage.json));
+ newJSON.origin = 'parent';
+ if (Utils.isContentProcess) {
+ // XXX: OOP content's screen offset is 0,
+ // so we remove the real screen offset here.
+ newJSON.x -= content.mozInnerScreenX;
+ newJSON.y -= content.mozInnerScreenY;
+ }
+ mm.sendAsyncMessage(aMessage.name, newJSON);
+ return true;
+}
+
+function activateContextMenu(aMessage) {
+ let position = Utils.getVirtualCursor(content.document).position;
+ if (!forwardToChild(aMessage, activateContextMenu, position)) {
+ let center = Utils.getBounds(position, true).center();
+
+ let evt = content.document.createEvent('HTMLEvents');
+ evt.initEvent('contextmenu', true, true);
+ evt.clientX = center.x;
+ evt.clientY = center.y;
+ position.DOMNode.dispatchEvent(evt);
+ }
+}
+
+function presentCaretChange(aText, aOldOffset, aNewOffset) {
+ if (aOldOffset !== aNewOffset) {
+ let msg = Presentation.textSelectionChanged(aText, aNewOffset, aNewOffset,
+ aOldOffset, aOldOffset, true);
+ sendAsyncMessage('AccessFu:Present', msg);
+ }
+}
+
+function scroll(aMessage) {
+ let position = Utils.getVirtualCursor(content.document).position;
+ if (!forwardToChild(aMessage, scroll, position)) {
+ sendAsyncMessage('AccessFu:DoScroll',
+ { bounds: Utils.getBounds(position, true),
+ page: aMessage.json.page,
+ horizontal: aMessage.json.horizontal });
+ }
+}
+
+addMessageListener(
+ 'AccessFu:Start',
+ function(m) {
+ if (m.json.logLevel) {
+ Logger.logLevel = Logger[m.json.logLevel];
+ }
+
+ Logger.debug('AccessFu:Start');
+ if (m.json.buildApp)
+ Utils.MozBuildApp = m.json.buildApp;
+
+ addMessageListener('AccessFu:ContextMenu', activateContextMenu);
+ addMessageListener('AccessFu:Scroll', scroll);
+
+ if (!contentControl) {
+ contentControl = new ContentControl(this);
+ }
+ contentControl.start();
+
+ if (!eventManager) {
+ eventManager = new EventManager(this, contentControl);
+ }
+ eventManager.inTest = m.json.inTest;
+ eventManager.start();
+
+ function contentStarted() {
+ let accDoc = Utils.AccService.getAccessibleFor(content.document);
+ if (accDoc && !Utils.getState(accDoc).contains(States.BUSY)) {
+ sendAsyncMessage('AccessFu:ContentStarted');
+ } else {
+ content.setTimeout(contentStarted, 0);
+ }
+ }
+
+ if (m.json.inTest) {
+ // During a test we want to wait for the document to finish loading for
+ // consistency.
+ contentStarted();
+ }
+ });
+
+addMessageListener(
+ 'AccessFu:Stop',
+ function(m) {
+ Logger.debug('AccessFu:Stop');
+
+ removeMessageListener('AccessFu:ContextMenu', activateContextMenu);
+ removeMessageListener('AccessFu:Scroll', scroll);
+
+ eventManager.stop();
+ contentControl.stop();
+ });
+
+sendAsyncMessage('AccessFu:Ready');
diff --git a/accessible/jsat/jar.mn b/accessible/jsat/jar.mn
new file mode 100644
index 000000000..970fb9a9b
--- /dev/null
+++ b/accessible/jsat/jar.mn
@@ -0,0 +1,10 @@
+# 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/.
+
+toolkit.jar:
+ content/global/accessibility/AccessFu.css (AccessFu.css)
+ content/global/accessibility/content-script.js (content-script.js)
+ content/global/accessibility/virtual_cursor_move.ogg (sounds/virtual_cursor_move.ogg)
+ content/global/accessibility/virtual_cursor_key.ogg (sounds/virtual_cursor_key.ogg)
+ content/global/accessibility/clicked.ogg (sounds/clicked.ogg)
diff --git a/accessible/jsat/moz.build b/accessible/jsat/moz.build
new file mode 100644
index 000000000..b9051f532
--- /dev/null
+++ b/accessible/jsat/moz.build
@@ -0,0 +1,20 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_JS_MODULES.accessibility += [
+ 'AccessFu.jsm',
+ 'Constants.jsm',
+ 'ContentControl.jsm',
+ 'EventManager.jsm',
+ 'Gestures.jsm',
+ 'OutputGenerator.jsm',
+ 'PointerAdapter.jsm',
+ 'Presentation.jsm',
+ 'Traversal.jsm',
+ 'Utils.jsm'
+]
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/accessible/jsat/sounds/clicked.ogg b/accessible/jsat/sounds/clicked.ogg
new file mode 100644
index 000000000..68388018e
--- /dev/null
+++ b/accessible/jsat/sounds/clicked.ogg
Binary files differ
diff --git a/accessible/jsat/sounds/virtual_cursor_key.ogg b/accessible/jsat/sounds/virtual_cursor_key.ogg
new file mode 100644
index 000000000..b29b55b44
--- /dev/null
+++ b/accessible/jsat/sounds/virtual_cursor_key.ogg
Binary files differ
diff --git a/accessible/jsat/sounds/virtual_cursor_move.ogg b/accessible/jsat/sounds/virtual_cursor_move.ogg
new file mode 100644
index 000000000..da9793460
--- /dev/null
+++ b/accessible/jsat/sounds/virtual_cursor_move.ogg
Binary files differ