gecko-dev/accessible/jsat/AccessFu.jsm
Ehsan Akhgari cb369370b3 Bug 1310845 - Remove support for mozapp iframes; r=fabrice,jryans,baku,mcmanus
This patch removes support for mozapp iframes, leaving support for
mozbrowser iframes intact.  Some of the code has been rewritten in order
to phrase things in terms of mozbrowser only, as opposed to mozbrowser
or app.  In some places, code that was only useful with apps has been
completely removed, so that the APIs consumed can also be removed.  In
some places where the notion of appId was bleeding out of this API, now
we use NO_APP_ID.  Other notions of appId which were restricted to this
API have been removed.
2016-11-15 18:31:46 -05:00

1001 lines
32 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global 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 Browser
let frameLoader = aSubject.QueryInterface(Ci.nsIFrameLoader);
if (!frameLoader.ownerIsMozBrowserFrame) {
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;