summaryrefslogtreecommitdiffstats
path: root/accessible/jsat/AccessFu.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'accessible/jsat/AccessFu.jsm')
-rw-r--r--accessible/jsat/AccessFu.jsm1000
1 files changed, 1000 insertions, 0 deletions
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;