/* 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.bind(this)); } } 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.bind(this)); } delete this._activatePref; Utils.uninit(); }, /** * 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.AccRetrieval.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.ownerIsBrowserOrAppFrame) { 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;