michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: 'use strict'; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: const Cr = Components.results; michael@0: michael@0: this.EXPORTED_SYMBOLS = ['AccessFu']; michael@0: michael@0: Cu.import('resource://gre/modules/Services.jsm'); michael@0: michael@0: Cu.import('resource://gre/modules/accessibility/Utils.jsm'); michael@0: michael@0: const ACCESSFU_DISABLE = 0; michael@0: const ACCESSFU_ENABLE = 1; michael@0: const ACCESSFU_AUTO = 2; michael@0: michael@0: const SCREENREADER_SETTING = 'accessibility.screenreader'; michael@0: michael@0: this.AccessFu = { michael@0: /** michael@0: * Initialize chrome-layer accessibility functionality. michael@0: * If accessibility is enabled on the platform, then a special accessibility michael@0: * mode is started. michael@0: */ michael@0: attach: function attach(aWindow) { michael@0: Utils.init(aWindow); michael@0: michael@0: try { michael@0: Services.androidBridge.handleGeckoMessage( michael@0: { type: 'Accessibility:Ready' }); michael@0: Services.obs.addObserver(this, 'Accessibility:Settings', false); michael@0: } catch (x) { michael@0: // Not on Android michael@0: if (aWindow.navigator.mozSettings) { michael@0: let lock = aWindow.navigator.mozSettings.createLock(); michael@0: let req = lock.get(SCREENREADER_SETTING); michael@0: req.addEventListener('success', () => { michael@0: this._systemPref = req.result[SCREENREADER_SETTING]; michael@0: this._enableOrDisable(); michael@0: }); michael@0: aWindow.navigator.mozSettings.addObserver( michael@0: SCREENREADER_SETTING, this.handleEvent.bind(this)); michael@0: } michael@0: } michael@0: michael@0: this._activatePref = new PrefCache( michael@0: 'accessibility.accessfu.activate', this._enableOrDisable.bind(this)); michael@0: michael@0: this._enableOrDisable(); michael@0: }, michael@0: michael@0: /** michael@0: * Shut down chrome-layer accessibility functionality from the outside. michael@0: */ michael@0: detach: function detach() { michael@0: // Avoid disabling twice. michael@0: if (this._enabled) { michael@0: this._disable(); michael@0: } michael@0: if (Utils.MozBuildApp === 'mobile/android') { michael@0: Services.obs.removeObserver(this, 'Accessibility:Settings'); michael@0: } else if (Utils.win.navigator.mozSettings) { michael@0: Utils.win.navigator.mozSettings.removeObserver( michael@0: SCREENREADER_SETTING, this.handleEvent.bind(this)); michael@0: } michael@0: delete this._activatePref; michael@0: Utils.uninit(); michael@0: }, michael@0: michael@0: /** michael@0: * Start AccessFu mode, this primarily means controlling the virtual cursor michael@0: * with arrow keys. michael@0: */ michael@0: _enable: function _enable() { michael@0: if (this._enabled) michael@0: return; michael@0: this._enabled = true; michael@0: michael@0: Cu.import('resource://gre/modules/accessibility/Utils.jsm'); michael@0: Cu.import('resource://gre/modules/accessibility/PointerAdapter.jsm'); michael@0: Cu.import('resource://gre/modules/accessibility/Presentation.jsm'); michael@0: michael@0: Logger.info('Enabled'); michael@0: michael@0: for each (let mm in Utils.AllMessageManagers) { michael@0: this._addMessageListeners(mm); michael@0: this._loadFrameScript(mm); michael@0: } michael@0: michael@0: // Add stylesheet michael@0: let stylesheetURL = 'chrome://global/content/accessibility/AccessFu.css'; michael@0: let stylesheet = Utils.win.document.createProcessingInstruction( michael@0: 'xml-stylesheet', 'href="' + stylesheetURL + '" type="text/css"'); michael@0: Utils.win.document.insertBefore(stylesheet, Utils.win.document.firstChild); michael@0: this.stylesheet = Cu.getWeakReference(stylesheet); michael@0: michael@0: michael@0: // Populate quicknav modes michael@0: this._quicknavModesPref = michael@0: new PrefCache( michael@0: 'accessibility.accessfu.quicknav_modes', michael@0: (aName, aValue) => { michael@0: this.Input.quickNavMode.updateModes(aValue); michael@0: }, true); michael@0: michael@0: // Check for output notification michael@0: this._notifyOutputPref = michael@0: new PrefCache('accessibility.accessfu.notify_output'); michael@0: michael@0: michael@0: this.Input.start(); michael@0: Output.start(); michael@0: PointerAdapter.start(); michael@0: michael@0: Services.obs.addObserver(this, 'remote-browser-shown', false); michael@0: Services.obs.addObserver(this, 'inprocess-browser-shown', false); michael@0: Services.obs.addObserver(this, 'Accessibility:NextObject', false); michael@0: Services.obs.addObserver(this, 'Accessibility:PreviousObject', false); michael@0: Services.obs.addObserver(this, 'Accessibility:Focus', false); michael@0: Services.obs.addObserver(this, 'Accessibility:ActivateObject', false); michael@0: Services.obs.addObserver(this, 'Accessibility:LongPress', false); michael@0: Services.obs.addObserver(this, 'Accessibility:MoveByGranularity', false); michael@0: Utils.win.addEventListener('TabOpen', this); michael@0: Utils.win.addEventListener('TabClose', this); michael@0: Utils.win.addEventListener('TabSelect', this); michael@0: michael@0: if (this.readyCallback) { michael@0: this.readyCallback(); michael@0: delete this.readyCallback; michael@0: } michael@0: michael@0: if (Utils.MozBuildApp !== 'mobile/android') { michael@0: this.announce( michael@0: Utils.stringBundle.GetStringFromName('screenReaderStarted')); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Disable AccessFu and return to default interaction mode. michael@0: */ michael@0: _disable: function _disable() { michael@0: if (!this._enabled) michael@0: return; michael@0: michael@0: this._enabled = false; michael@0: michael@0: Logger.info('Disabled'); michael@0: michael@0: Utils.win.document.removeChild(this.stylesheet.get()); michael@0: michael@0: if (Utils.MozBuildApp !== 'mobile/android') { michael@0: this.announce( michael@0: Utils.stringBundle.GetStringFromName('screenReaderStopped')); michael@0: } michael@0: michael@0: for each (let mm in Utils.AllMessageManagers) { michael@0: mm.sendAsyncMessage('AccessFu:Stop'); michael@0: this._removeMessageListeners(mm); michael@0: } michael@0: michael@0: this.Input.stop(); michael@0: Output.stop(); michael@0: PointerAdapter.stop(); michael@0: michael@0: Utils.win.removeEventListener('TabOpen', this); michael@0: Utils.win.removeEventListener('TabClose', this); michael@0: Utils.win.removeEventListener('TabSelect', this); michael@0: michael@0: Services.obs.removeObserver(this, 'remote-browser-shown'); michael@0: Services.obs.removeObserver(this, 'inprocess-browser-shown'); michael@0: Services.obs.removeObserver(this, 'Accessibility:NextObject'); michael@0: Services.obs.removeObserver(this, 'Accessibility:PreviousObject'); michael@0: Services.obs.removeObserver(this, 'Accessibility:Focus'); michael@0: Services.obs.removeObserver(this, 'Accessibility:ActivateObject'); michael@0: Services.obs.removeObserver(this, 'Accessibility:LongPress'); michael@0: Services.obs.removeObserver(this, 'Accessibility:MoveByGranularity'); michael@0: michael@0: delete this._quicknavModesPref; michael@0: delete this._notifyOutputPref; michael@0: michael@0: if (this.doneCallback) { michael@0: this.doneCallback(); michael@0: delete this.doneCallback; michael@0: } michael@0: }, michael@0: michael@0: _enableOrDisable: function _enableOrDisable() { michael@0: try { michael@0: if (!this._activatePref) { michael@0: return; michael@0: } michael@0: let activatePref = this._activatePref.value; michael@0: if (activatePref == ACCESSFU_ENABLE || michael@0: this._systemPref && activatePref == ACCESSFU_AUTO) michael@0: this._enable(); michael@0: else michael@0: this._disable(); michael@0: } catch (x) { michael@0: dump('Error ' + x.message + ' ' + x.fileName + ':' + x.lineNumber); michael@0: } michael@0: }, michael@0: michael@0: receiveMessage: function receiveMessage(aMessage) { michael@0: Logger.debug(() => { michael@0: return ['Recieved', aMessage.name, JSON.stringify(aMessage.json)]; michael@0: }); michael@0: michael@0: switch (aMessage.name) { michael@0: case 'AccessFu:Ready': michael@0: let mm = Utils.getMessageManager(aMessage.target); michael@0: if (this._enabled) { michael@0: mm.sendAsyncMessage('AccessFu:Start', michael@0: {method: 'start', buildApp: Utils.MozBuildApp}); michael@0: } michael@0: break; michael@0: case 'AccessFu:Present': michael@0: this._output(aMessage.json, aMessage.target); michael@0: break; michael@0: case 'AccessFu:Input': michael@0: this.Input.setEditState(aMessage.json); michael@0: break; michael@0: case 'AccessFu:ActivateContextMenu': michael@0: this.Input.activateContextMenu(aMessage.json); michael@0: break; michael@0: case 'AccessFu:DoScroll': michael@0: this.Input.doScroll(aMessage.json); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: _output: function _output(aPresentationData, aBrowser) { michael@0: for each (let presenter in aPresentationData) { michael@0: if (!presenter) michael@0: continue; michael@0: michael@0: try { michael@0: Output[presenter.type](presenter.details, aBrowser); michael@0: } catch (x) { michael@0: Logger.logException(x); michael@0: } michael@0: } michael@0: michael@0: if (this._notifyOutputPref.value) { michael@0: Services.obs.notifyObservers(null, 'accessfu-output', michael@0: JSON.stringify(aPresentationData)); michael@0: } michael@0: }, michael@0: michael@0: _loadFrameScript: function _loadFrameScript(aMessageManager) { michael@0: if (this._processedMessageManagers.indexOf(aMessageManager) < 0) { michael@0: aMessageManager.loadFrameScript( michael@0: 'chrome://global/content/accessibility/content-script.js', true); michael@0: this._processedMessageManagers.push(aMessageManager); michael@0: } else if (this._enabled) { michael@0: // If the content-script is already loaded and AccessFu is enabled, michael@0: // send an AccessFu:Start message. michael@0: aMessageManager.sendAsyncMessage('AccessFu:Start', michael@0: {method: 'start', buildApp: Utils.MozBuildApp}); michael@0: } michael@0: }, michael@0: michael@0: _addMessageListeners: function _addMessageListeners(aMessageManager) { michael@0: aMessageManager.addMessageListener('AccessFu:Present', this); michael@0: aMessageManager.addMessageListener('AccessFu:Input', this); michael@0: aMessageManager.addMessageListener('AccessFu:Ready', this); michael@0: aMessageManager.addMessageListener('AccessFu:ActivateContextMenu', this); michael@0: aMessageManager.addMessageListener('AccessFu:DoScroll', this); michael@0: }, michael@0: michael@0: _removeMessageListeners: function _removeMessageListeners(aMessageManager) { michael@0: aMessageManager.removeMessageListener('AccessFu:Present', this); michael@0: aMessageManager.removeMessageListener('AccessFu:Input', this); michael@0: aMessageManager.removeMessageListener('AccessFu:Ready', this); michael@0: aMessageManager.removeMessageListener('AccessFu:ActivateContextMenu', this); michael@0: aMessageManager.removeMessageListener('AccessFu:DoScroll', this); michael@0: }, michael@0: michael@0: _handleMessageManager: function _handleMessageManager(aMessageManager) { michael@0: if (this._enabled) { michael@0: this._addMessageListeners(aMessageManager); michael@0: } michael@0: this._loadFrameScript(aMessageManager); michael@0: }, michael@0: michael@0: observe: function observe(aSubject, aTopic, aData) { michael@0: switch (aTopic) { michael@0: case 'Accessibility:Settings': michael@0: this._systemPref = JSON.parse(aData).enabled; michael@0: this._enableOrDisable(); michael@0: break; michael@0: case 'Accessibility:NextObject': michael@0: this.Input.moveCursor('moveNext', 'Simple', 'gesture'); michael@0: break; michael@0: case 'Accessibility:PreviousObject': michael@0: this.Input.moveCursor('movePrevious', 'Simple', 'gesture'); michael@0: break; michael@0: case 'Accessibility:ActivateObject': michael@0: this.Input.activateCurrent(JSON.parse(aData)); michael@0: break; michael@0: case 'Accessibility:LongPress': michael@0: this.Input.sendContextMenuMessage(); michael@0: break; michael@0: case 'Accessibility:Focus': michael@0: this._focused = JSON.parse(aData); michael@0: if (this._focused) { michael@0: this.autoMove({ forcePresent: true, noOpIfOnScreen: true }); michael@0: } michael@0: break; michael@0: case 'Accessibility:MoveByGranularity': michael@0: this.Input.moveByGranularity(JSON.parse(aData)); michael@0: break; michael@0: case 'remote-browser-shown': michael@0: case 'inprocess-browser-shown': michael@0: { michael@0: // Ignore notifications that aren't from a BrowserOrApp michael@0: let frameLoader = aSubject.QueryInterface(Ci.nsIFrameLoader); michael@0: if (!frameLoader.ownerIsBrowserOrAppFrame) { michael@0: return; michael@0: } michael@0: this._handleMessageManager(frameLoader.messageManager); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: handleEvent: function handleEvent(aEvent) { michael@0: switch (aEvent.type) { michael@0: case 'TabOpen': michael@0: { michael@0: let mm = Utils.getMessageManager(aEvent.target); michael@0: this._handleMessageManager(mm); michael@0: break; michael@0: } michael@0: case 'TabClose': michael@0: { michael@0: let mm = Utils.getMessageManager(aEvent.target); michael@0: let mmIndex = this._processedMessageManagers.indexOf(mm); michael@0: if (mmIndex > -1) { michael@0: this._removeMessageListeners(mm); michael@0: this._processedMessageManagers.splice(mmIndex, 1); michael@0: } michael@0: break; michael@0: } michael@0: case 'TabSelect': michael@0: { michael@0: if (this._focused) { michael@0: // We delay this for half a second so the awesomebar could close, michael@0: // and we could use the current coordinates for the content item. michael@0: // XXX TODO figure out how to avoid magic wait here. michael@0: this.autoMove({ michael@0: delay: 500, michael@0: forcePresent: true, michael@0: noOpIfOnScreen: true, michael@0: moveMethod: 'moveFirst' }); michael@0: } michael@0: break; michael@0: } michael@0: default: michael@0: { michael@0: // A settings change, it does not have an event type michael@0: if (aEvent.settingName == SCREENREADER_SETTING) { michael@0: this._systemPref = aEvent.settingValue; michael@0: this._enableOrDisable(); michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: autoMove: function autoMove(aOptions) { michael@0: let mm = Utils.getMessageManager(Utils.CurrentBrowser); michael@0: mm.sendAsyncMessage('AccessFu:AutoMove', aOptions); michael@0: }, michael@0: michael@0: announce: function announce(aAnnouncement) { michael@0: this._output(Presentation.announce(aAnnouncement), Utils.CurrentBrowser); michael@0: }, michael@0: michael@0: // So we don't enable/disable twice michael@0: _enabled: false, michael@0: michael@0: // Layerview is focused michael@0: _focused: false, michael@0: michael@0: // Keep track of message managers tha already have a 'content-script.js' michael@0: // injected. michael@0: _processedMessageManagers: [], michael@0: michael@0: /** michael@0: * Adjusts the given bounds relative to the given browser. Converts from screen michael@0: * or device pixels to either device or CSS pixels. michael@0: * @param {Rect} aJsonBounds the bounds to adjust michael@0: * @param {browser} aBrowser the browser we want the bounds relative to michael@0: * @param {bool} aToCSSPixels whether to convert to CSS pixels (as opposed to michael@0: * device pixels) michael@0: * @param {bool} aFromDevicePixels whether to convert from device pixels (as michael@0: * opposed to screen pixels) michael@0: */ michael@0: adjustContentBounds: function(aJsonBounds, aBrowser, aToCSSPixels, aFromDevicePixels) { michael@0: let bounds = new Rect(aJsonBounds.left, aJsonBounds.top, michael@0: aJsonBounds.right - aJsonBounds.left, michael@0: aJsonBounds.bottom - aJsonBounds.top); michael@0: let win = Utils.win; michael@0: let dpr = win.devicePixelRatio; michael@0: let vp = Utils.getViewport(win); michael@0: let offset = { left: -win.mozInnerScreenX, top: -win.mozInnerScreenY }; michael@0: michael@0: if (!aBrowser.contentWindow) { michael@0: // OOP browser, add offset of browser. michael@0: // The offset of the browser element in relation to its parent window. michael@0: let clientRect = aBrowser.getBoundingClientRect(); michael@0: let win = aBrowser.ownerDocument.defaultView; michael@0: offset.left += clientRect.left + win.mozInnerScreenX; michael@0: offset.top += clientRect.top + win.mozInnerScreenY; michael@0: } michael@0: michael@0: // Here we scale from screen pixels to layout device pixels by dividing by michael@0: // the resolution (caused by pinch-zooming). The resolution is the viewport michael@0: // zoom divided by the devicePixelRatio. If there's no viewport, then we're michael@0: // on a platform without pinch-zooming and we can just ignore this. michael@0: if (!aFromDevicePixels && vp) { michael@0: bounds = bounds.scale(vp.zoom / dpr, vp.zoom / dpr); michael@0: } michael@0: michael@0: // Add the offset; the offset is in CSS pixels, so multiply the michael@0: // devicePixelRatio back in before adding to preserve unit consistency. michael@0: bounds = bounds.translate(offset.left * dpr, offset.top * dpr); michael@0: michael@0: // If we want to get to CSS pixels from device pixels, this needs to be michael@0: // further divided by the devicePixelRatio due to widget scaling. michael@0: if (aToCSSPixels) { michael@0: bounds = bounds.scale(1 / dpr, 1 / dpr); michael@0: } michael@0: michael@0: return bounds.expandToIntegers(); michael@0: } michael@0: }; michael@0: michael@0: var Output = { michael@0: brailleState: { michael@0: startOffset: 0, michael@0: endOffset: 0, michael@0: text: '', michael@0: selectionStart: 0, michael@0: selectionEnd: 0, michael@0: michael@0: init: function init(aOutput) { michael@0: if (aOutput && 'output' in aOutput) { michael@0: this.startOffset = aOutput.startOffset; michael@0: this.endOffset = aOutput.endOffset; michael@0: // We need to append a space at the end so that the routing key corresponding michael@0: // to the end of the output (i.e. the space) can be hit to move the caret there. michael@0: this.text = aOutput.output + ' '; michael@0: this.selectionStart = typeof aOutput.selectionStart === 'number' ? michael@0: aOutput.selectionStart : this.selectionStart; michael@0: this.selectionEnd = typeof aOutput.selectionEnd === 'number' ? michael@0: aOutput.selectionEnd : this.selectionEnd; michael@0: michael@0: return { text: this.text, michael@0: selectionStart: this.selectionStart, michael@0: selectionEnd: this.selectionEnd }; michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: adjustText: function adjustText(aText) { michael@0: let newBraille = []; michael@0: let braille = {}; michael@0: michael@0: let prefix = this.text.substring(0, this.startOffset).trim(); michael@0: if (prefix) { michael@0: prefix += ' '; michael@0: newBraille.push(prefix); michael@0: } michael@0: michael@0: newBraille.push(aText); michael@0: michael@0: let suffix = this.text.substring(this.endOffset).trim(); michael@0: if (suffix) { michael@0: suffix = ' ' + suffix; michael@0: newBraille.push(suffix); michael@0: } michael@0: michael@0: this.startOffset = braille.startOffset = prefix.length; michael@0: this.text = braille.text = newBraille.join('') + ' '; michael@0: this.endOffset = braille.endOffset = braille.text.length - suffix.length; michael@0: braille.selectionStart = this.selectionStart; michael@0: braille.selectionEnd = this.selectionEnd; michael@0: michael@0: return braille; michael@0: }, michael@0: michael@0: adjustSelection: function adjustSelection(aSelection) { michael@0: let braille = {}; michael@0: michael@0: braille.startOffset = this.startOffset; michael@0: braille.endOffset = this.endOffset; michael@0: braille.text = this.text; michael@0: this.selectionStart = braille.selectionStart = aSelection.selectionStart + this.startOffset; michael@0: this.selectionEnd = braille.selectionEnd = aSelection.selectionEnd + this.startOffset; michael@0: michael@0: return braille; michael@0: } michael@0: }, michael@0: michael@0: speechHelper: { michael@0: EARCONS: ['virtual_cursor_move.ogg', michael@0: 'virtual_cursor_key.ogg', michael@0: 'clicked.ogg'], michael@0: michael@0: earconBuffers: {}, michael@0: michael@0: inited: false, michael@0: michael@0: webspeechEnabled: false, michael@0: michael@0: deferredOutputs: [], michael@0: michael@0: init: function init() { michael@0: let window = Utils.win; michael@0: this.webspeechEnabled = !!window.speechSynthesis && michael@0: !!window.SpeechSynthesisUtterance; michael@0: michael@0: let settingsToGet = 2; michael@0: let settingsCallback = (aName, aSetting) => { michael@0: if (--settingsToGet > 0) { michael@0: return; michael@0: } michael@0: michael@0: this.inited = true; michael@0: michael@0: for (let actions of this.deferredOutputs) { michael@0: this.output(actions); michael@0: } michael@0: }; michael@0: michael@0: this._volumeSetting = new SettingCache( michael@0: 'accessibility.screenreader-volume', settingsCallback, michael@0: { defaultValue: 1, callbackNow: true, callbackOnce: true }); michael@0: this._rateSetting = new SettingCache( michael@0: 'accessibility.screenreader-rate', settingsCallback, michael@0: { defaultValue: 0, callbackNow: true, callbackOnce: true }); michael@0: michael@0: for (let earcon of this.EARCONS) { michael@0: let earconName = /(^.*)\..*$/.exec(earcon)[1]; michael@0: this.earconBuffers[earconName] = new WeakMap(); michael@0: this.earconBuffers[earconName].set( michael@0: window, new window.Audio('chrome://global/content/accessibility/' + earcon)); michael@0: } michael@0: }, michael@0: michael@0: uninit: function uninit() { michael@0: if (this.inited) { michael@0: delete this._volumeSetting; michael@0: delete this._rateSetting; michael@0: } michael@0: this.inited = false; michael@0: }, michael@0: michael@0: output: function output(aActions) { michael@0: if (!this.inited) { michael@0: this.deferredOutputs.push(aActions); michael@0: return; michael@0: } michael@0: michael@0: for (let action of aActions) { michael@0: let window = Utils.win; michael@0: Logger.debug('tts.' + action.method, '"' + action.data + '"', michael@0: JSON.stringify(action.options)); michael@0: michael@0: if (!action.options.enqueue && this.webspeechEnabled) { michael@0: window.speechSynthesis.cancel(); michael@0: } michael@0: michael@0: if (action.method === 'speak' && this.webspeechEnabled) { michael@0: let utterance = new window.SpeechSynthesisUtterance(action.data); michael@0: let requestedRate = this._rateSetting.value; michael@0: utterance.volume = this._volumeSetting.value; michael@0: utterance.rate = requestedRate >= 0 ? michael@0: requestedRate + 1 : 1 / (Math.abs(requestedRate) + 1); michael@0: window.speechSynthesis.speak(utterance); michael@0: } else if (action.method === 'playEarcon') { michael@0: let audioBufferWeakMap = this.earconBuffers[action.data]; michael@0: if (audioBufferWeakMap) { michael@0: let node = audioBufferWeakMap.get(window).cloneNode(false); michael@0: node.volume = this._volumeSetting.value; michael@0: node.play(); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: start: function start() { michael@0: Cu.import('resource://gre/modules/Geometry.jsm'); michael@0: this.speechHelper.init(); michael@0: }, michael@0: michael@0: stop: function stop() { michael@0: if (this.highlightBox) { michael@0: Utils.win.document.documentElement.removeChild(this.highlightBox.get()); michael@0: delete this.highlightBox; michael@0: } michael@0: michael@0: if (this.announceBox) { michael@0: Utils.win.document.documentElement.removeChild(this.announceBox.get()); michael@0: delete this.announceBox; michael@0: } michael@0: michael@0: this.speechHelper.uninit(); michael@0: }, michael@0: michael@0: Speech: function Speech(aDetails, aBrowser) { michael@0: this.speechHelper.output(aDetails.actions); michael@0: }, michael@0: michael@0: Visual: function Visual(aDetails, aBrowser) { michael@0: switch (aDetails.method) { michael@0: case 'showBounds': michael@0: { michael@0: let highlightBox = null; michael@0: if (!this.highlightBox) { michael@0: // Add highlight box michael@0: highlightBox = Utils.win.document. michael@0: createElementNS('http://www.w3.org/1999/xhtml', 'div'); michael@0: Utils.win.document.documentElement.appendChild(highlightBox); michael@0: highlightBox.id = 'virtual-cursor-box'; michael@0: michael@0: // Add highlight inset for inner shadow michael@0: let inset = Utils.win.document. michael@0: createElementNS('http://www.w3.org/1999/xhtml', 'div'); michael@0: inset.id = 'virtual-cursor-inset'; michael@0: michael@0: highlightBox.appendChild(inset); michael@0: this.highlightBox = Cu.getWeakReference(highlightBox); michael@0: } else { michael@0: highlightBox = this.highlightBox.get(); michael@0: } michael@0: michael@0: let padding = aDetails.padding; michael@0: let r = AccessFu.adjustContentBounds(aDetails.bounds, aBrowser, true); michael@0: michael@0: // First hide it to avoid flickering when changing the style. michael@0: highlightBox.style.display = 'none'; michael@0: highlightBox.style.top = (r.top - padding) + 'px'; michael@0: highlightBox.style.left = (r.left - padding) + 'px'; michael@0: highlightBox.style.width = (r.width + padding*2) + 'px'; michael@0: highlightBox.style.height = (r.height + padding*2) + 'px'; michael@0: highlightBox.style.display = 'block'; michael@0: michael@0: break; michael@0: } michael@0: case 'hideBounds': michael@0: { michael@0: let highlightBox = this.highlightBox ? this.highlightBox.get() : null; michael@0: if (highlightBox) michael@0: highlightBox.style.display = 'none'; michael@0: break; michael@0: } michael@0: case 'showAnnouncement': michael@0: { michael@0: let announceBox = this.announceBox ? this.announceBox.get() : null; michael@0: if (!announceBox) { michael@0: announceBox = Utils.win.document. michael@0: createElementNS('http://www.w3.org/1999/xhtml', 'div'); michael@0: announceBox.id = 'announce-box'; michael@0: Utils.win.document.documentElement.appendChild(announceBox); michael@0: this.announceBox = Cu.getWeakReference(announceBox); michael@0: } michael@0: michael@0: announceBox.innerHTML = '
' + aDetails.text + '
'; michael@0: announceBox.classList.add('showing'); michael@0: michael@0: if (this._announceHideTimeout) michael@0: Utils.win.clearTimeout(this._announceHideTimeout); michael@0: michael@0: if (aDetails.duration > 0) michael@0: this._announceHideTimeout = Utils.win.setTimeout( michael@0: function () { michael@0: announceBox.classList.remove('showing'); michael@0: this._announceHideTimeout = 0; michael@0: }.bind(this), aDetails.duration); michael@0: break; michael@0: } michael@0: case 'hideAnnouncement': michael@0: { michael@0: let announceBox = this.announceBox ? this.announceBox.get() : null; michael@0: if (announceBox) michael@0: announceBox.classList.remove('showing'); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: get androidBridge() { michael@0: delete this.androidBridge; michael@0: if (Utils.MozBuildApp === 'mobile/android') { michael@0: this.androidBridge = Services.androidBridge; michael@0: } else { michael@0: this.androidBridge = null; michael@0: } michael@0: return this.androidBridge; michael@0: }, michael@0: michael@0: Android: function Android(aDetails, aBrowser) { michael@0: const ANDROID_VIEW_TEXT_CHANGED = 0x10; michael@0: const ANDROID_VIEW_TEXT_SELECTION_CHANGED = 0x2000; michael@0: michael@0: if (!this.androidBridge) { michael@0: return; michael@0: } michael@0: michael@0: for each (let androidEvent in aDetails) { michael@0: androidEvent.type = 'Accessibility:Event'; michael@0: if (androidEvent.bounds) michael@0: androidEvent.bounds = AccessFu.adjustContentBounds(androidEvent.bounds, aBrowser); michael@0: michael@0: switch(androidEvent.eventType) { michael@0: case ANDROID_VIEW_TEXT_CHANGED: michael@0: androidEvent.brailleOutput = this.brailleState.adjustText(androidEvent.text); michael@0: break; michael@0: case ANDROID_VIEW_TEXT_SELECTION_CHANGED: michael@0: androidEvent.brailleOutput = this.brailleState.adjustSelection(androidEvent.brailleOutput); michael@0: break; michael@0: default: michael@0: androidEvent.brailleOutput = this.brailleState.init(androidEvent.brailleOutput); michael@0: break; michael@0: } michael@0: this.androidBridge.handleGeckoMessage(androidEvent); michael@0: } michael@0: }, michael@0: michael@0: Haptic: function Haptic(aDetails, aBrowser) { michael@0: Utils.win.navigator.vibrate(aDetails.pattern); michael@0: }, michael@0: michael@0: Braille: function Braille(aDetails, aBrowser) { michael@0: Logger.debug('Braille output: ' + aDetails.text); michael@0: } michael@0: }; michael@0: michael@0: var Input = { michael@0: editState: {}, michael@0: michael@0: start: function start() { michael@0: // XXX: This is too disruptive on desktop for now. michael@0: // Might need to add special modifiers. michael@0: if (Utils.MozBuildApp != 'browser') { michael@0: Utils.win.document.addEventListener('keypress', this, true); michael@0: } michael@0: Utils.win.addEventListener('mozAccessFuGesture', this, true); michael@0: }, michael@0: michael@0: stop: function stop() { michael@0: if (Utils.MozBuildApp != 'browser') { michael@0: Utils.win.document.removeEventListener('keypress', this, true); michael@0: } michael@0: Utils.win.removeEventListener('mozAccessFuGesture', this, true); michael@0: }, michael@0: michael@0: handleEvent: function Input_handleEvent(aEvent) { michael@0: try { michael@0: switch (aEvent.type) { michael@0: case 'keypress': michael@0: this._handleKeypress(aEvent); michael@0: break; michael@0: case 'mozAccessFuGesture': michael@0: this._handleGesture(aEvent.detail); michael@0: break; michael@0: } michael@0: } catch (x) { michael@0: Logger.logException(x); michael@0: } michael@0: }, michael@0: michael@0: _handleGesture: function _handleGesture(aGesture) { michael@0: let gestureName = aGesture.type + aGesture.touches.length; michael@0: Logger.debug('Gesture', aGesture.type, michael@0: '(fingers: ' + aGesture.touches.length + ')'); michael@0: michael@0: switch (gestureName) { michael@0: case 'dwell1': michael@0: case 'explore1': michael@0: this.moveToPoint('Simple', aGesture.touches[0].x, michael@0: aGesture.touches[0].y); michael@0: break; michael@0: case 'doubletap1': michael@0: this.activateCurrent(); michael@0: break; michael@0: case 'doubletaphold1': michael@0: this.sendContextMenuMessage(); michael@0: break; michael@0: case 'swiperight1': michael@0: this.moveCursor('moveNext', 'Simple', 'gestures'); michael@0: break; michael@0: case 'swipeleft1': michael@0: this.moveCursor('movePrevious', 'Simple', 'gesture'); michael@0: break; michael@0: case 'swipeup1': michael@0: this.contextAction('backward'); michael@0: break; michael@0: case 'swipedown1': michael@0: this.contextAction('forward'); michael@0: break; michael@0: case 'exploreend1': michael@0: case 'dwellend1': michael@0: this.activateCurrent(null, true); michael@0: break; michael@0: case 'swiperight2': michael@0: this.sendScrollMessage(-1, true); michael@0: break; michael@0: case 'swipedown2': michael@0: this.sendScrollMessage(-1); michael@0: break; michael@0: case 'swipeleft2': michael@0: this.sendScrollMessage(1, true); michael@0: break; michael@0: case 'swipeup2': michael@0: this.sendScrollMessage(1); michael@0: break; michael@0: case 'explore2': michael@0: Utils.CurrentBrowser.contentWindow.scrollBy( michael@0: -aGesture.deltaX, -aGesture.deltaY); michael@0: break; michael@0: case 'swiperight3': michael@0: this.moveCursor('moveNext', this.quickNavMode.current, 'gesture'); michael@0: break; michael@0: case 'swipeleft3': michael@0: this.moveCursor('movePrevious', this.quickNavMode.current, 'gesture'); michael@0: break; michael@0: case 'swipedown3': michael@0: this.quickNavMode.next(); michael@0: AccessFu.announce('quicknav_' + this.quickNavMode.current); michael@0: break; michael@0: case 'swipeup3': michael@0: this.quickNavMode.previous(); michael@0: AccessFu.announce('quicknav_' + this.quickNavMode.current); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: _handleKeypress: function _handleKeypress(aEvent) { michael@0: let target = aEvent.target; michael@0: michael@0: // Ignore keys with modifiers so the content could take advantage of them. michael@0: if (aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey) michael@0: return; michael@0: michael@0: switch (aEvent.keyCode) { michael@0: case 0: michael@0: // an alphanumeric key was pressed, handle it separately. michael@0: // If it was pressed with either alt or ctrl, just pass through. michael@0: // If it was pressed with meta, pass the key on without the meta. michael@0: if (this.editState.editing) michael@0: return; michael@0: michael@0: let key = String.fromCharCode(aEvent.charCode); michael@0: try { michael@0: let [methodName, rule] = this.keyMap[key]; michael@0: this.moveCursor(methodName, rule, 'keyboard'); michael@0: } catch (x) { michael@0: return; michael@0: } michael@0: break; michael@0: case aEvent.DOM_VK_RIGHT: michael@0: if (this.editState.editing) { michael@0: if (!this.editState.atEnd) michael@0: // Don't move forward if caret is not at end of entry. michael@0: // XXX: Fix for rtl michael@0: return; michael@0: else michael@0: target.blur(); michael@0: } michael@0: this.moveCursor(aEvent.shiftKey ? 'moveLast' : 'moveNext', 'Simple', 'keyboard'); michael@0: break; michael@0: case aEvent.DOM_VK_LEFT: michael@0: if (this.editState.editing) { michael@0: if (!this.editState.atStart) michael@0: // Don't move backward if caret is not at start of entry. michael@0: // XXX: Fix for rtl michael@0: return; michael@0: else michael@0: target.blur(); michael@0: } michael@0: this.moveCursor(aEvent.shiftKey ? 'moveFirst' : 'movePrevious', 'Simple', 'keyboard'); michael@0: break; michael@0: case aEvent.DOM_VK_UP: michael@0: if (this.editState.multiline) { michael@0: if (!this.editState.atStart) michael@0: // Don't blur content if caret is not at start of text area. michael@0: return; michael@0: else michael@0: target.blur(); michael@0: } michael@0: michael@0: if (Utils.MozBuildApp == 'mobile/android') michael@0: // Return focus to native Android browser chrome. michael@0: Services.androidBridge.handleGeckoMessage( michael@0: { type: 'ToggleChrome:Focus' }); michael@0: break; michael@0: case aEvent.DOM_VK_RETURN: michael@0: if (this.editState.editing) michael@0: return; michael@0: this.activateCurrent(); michael@0: break; michael@0: default: michael@0: return; michael@0: } michael@0: michael@0: aEvent.preventDefault(); michael@0: aEvent.stopPropagation(); michael@0: }, michael@0: michael@0: moveToPoint: function moveToPoint(aRule, aX, aY) { michael@0: let mm = Utils.getMessageManager(Utils.CurrentBrowser); michael@0: mm.sendAsyncMessage('AccessFu:MoveToPoint', {rule: aRule, michael@0: x: aX, y: aY, michael@0: origin: 'top'}); michael@0: }, michael@0: michael@0: moveCursor: function moveCursor(aAction, aRule, aInputType) { michael@0: let mm = Utils.getMessageManager(Utils.CurrentBrowser); michael@0: mm.sendAsyncMessage('AccessFu:MoveCursor', michael@0: {action: aAction, rule: aRule, michael@0: origin: 'top', inputType: aInputType}); michael@0: }, michael@0: michael@0: contextAction: function contextAction(aDirection) { michael@0: // XXX: For now, the only supported context action is adjusting a range. michael@0: let mm = Utils.getMessageManager(Utils.CurrentBrowser); michael@0: mm.sendAsyncMessage('AccessFu:AdjustRange', {direction: aDirection}); michael@0: }, michael@0: michael@0: moveByGranularity: function moveByGranularity(aDetails) { michael@0: const MOVEMENT_GRANULARITY_PARAGRAPH = 8; michael@0: michael@0: if (!this.editState.editing) { michael@0: if (aDetails.granularity === MOVEMENT_GRANULARITY_PARAGRAPH) { michael@0: this.moveCursor('move' + aDetails.direction, 'Paragraph', 'gesture'); michael@0: return; michael@0: } michael@0: } else { michael@0: aDetails.atStart = this.editState.atStart; michael@0: aDetails.atEnd = this.editState.atEnd; michael@0: } michael@0: michael@0: let mm = Utils.getMessageManager(Utils.CurrentBrowser); michael@0: let type = this.editState.editing ? 'AccessFu:MoveCaret' : michael@0: 'AccessFu:MoveByGranularity'; michael@0: mm.sendAsyncMessage(type, aDetails); michael@0: }, michael@0: michael@0: activateCurrent: function activateCurrent(aData, aActivateIfKey = false) { michael@0: let mm = Utils.getMessageManager(Utils.CurrentBrowser); michael@0: let offset = aData && typeof aData.keyIndex === 'number' ? michael@0: aData.keyIndex - Output.brailleState.startOffset : -1; michael@0: michael@0: mm.sendAsyncMessage('AccessFu:Activate', michael@0: {offset: offset, activateIfKey: aActivateIfKey}); michael@0: }, michael@0: michael@0: sendContextMenuMessage: function sendContextMenuMessage() { michael@0: let mm = Utils.getMessageManager(Utils.CurrentBrowser); michael@0: mm.sendAsyncMessage('AccessFu:ContextMenu', {}); michael@0: }, michael@0: michael@0: activateContextMenu: function activateContextMenu(aDetails) { michael@0: if (Utils.MozBuildApp === 'mobile/android') { michael@0: let p = AccessFu.adjustContentBounds(aDetails.bounds, Utils.CurrentBrowser, michael@0: true, true).center(); michael@0: Services.obs.notifyObservers(null, 'Gesture:LongPress', michael@0: JSON.stringify({x: p.x, y: p.y})); michael@0: } michael@0: }, michael@0: michael@0: setEditState: function setEditState(aEditState) { michael@0: this.editState = aEditState; michael@0: }, michael@0: michael@0: // XXX: This is here for backwards compatability with screen reader simulator michael@0: // it should be removed when the extension is updated on amo. michael@0: scroll: function scroll(aPage, aHorizontal) { michael@0: this.sendScrollMessage(aPage, aHorizontal); michael@0: }, michael@0: michael@0: sendScrollMessage: function sendScrollMessage(aPage, aHorizontal) { michael@0: let mm = Utils.getMessageManager(Utils.CurrentBrowser); michael@0: mm.sendAsyncMessage('AccessFu:Scroll', {page: aPage, horizontal: aHorizontal, origin: 'top'}); michael@0: }, michael@0: michael@0: doScroll: function doScroll(aDetails) { michael@0: let horizontal = aDetails.horizontal; michael@0: let page = aDetails.page; michael@0: let p = AccessFu.adjustContentBounds(aDetails.bounds, Utils.CurrentBrowser, michael@0: true, true).center(); michael@0: let wu = Utils.win.QueryInterface(Ci.nsIInterfaceRequestor). michael@0: getInterface(Ci.nsIDOMWindowUtils); michael@0: wu.sendWheelEvent(p.x, p.y, michael@0: horizontal ? page : 0, horizontal ? 0 : page, 0, michael@0: Utils.win.WheelEvent.DOM_DELTA_PAGE, 0, 0, 0, 0); michael@0: }, michael@0: michael@0: get keyMap() { michael@0: delete this.keyMap; michael@0: this.keyMap = { michael@0: a: ['moveNext', 'Anchor'], michael@0: A: ['movePrevious', 'Anchor'], michael@0: b: ['moveNext', 'Button'], michael@0: B: ['movePrevious', 'Button'], michael@0: c: ['moveNext', 'Combobox'], michael@0: C: ['movePrevious', 'Combobox'], michael@0: d: ['moveNext', 'Landmark'], michael@0: D: ['movePrevious', 'Landmark'], michael@0: e: ['moveNext', 'Entry'], michael@0: E: ['movePrevious', 'Entry'], michael@0: f: ['moveNext', 'FormElement'], michael@0: F: ['movePrevious', 'FormElement'], michael@0: g: ['moveNext', 'Graphic'], michael@0: G: ['movePrevious', 'Graphic'], michael@0: h: ['moveNext', 'Heading'], michael@0: H: ['movePrevious', 'Heading'], michael@0: i: ['moveNext', 'ListItem'], michael@0: I: ['movePrevious', 'ListItem'], michael@0: k: ['moveNext', 'Link'], michael@0: K: ['movePrevious', 'Link'], michael@0: l: ['moveNext', 'List'], michael@0: L: ['movePrevious', 'List'], michael@0: p: ['moveNext', 'PageTab'], michael@0: P: ['movePrevious', 'PageTab'], michael@0: r: ['moveNext', 'RadioButton'], michael@0: R: ['movePrevious', 'RadioButton'], michael@0: s: ['moveNext', 'Separator'], michael@0: S: ['movePrevious', 'Separator'], michael@0: t: ['moveNext', 'Table'], michael@0: T: ['movePrevious', 'Table'], michael@0: x: ['moveNext', 'Checkbox'], michael@0: X: ['movePrevious', 'Checkbox'] michael@0: }; michael@0: michael@0: return this.keyMap; michael@0: }, michael@0: michael@0: quickNavMode: { michael@0: get current() { michael@0: return this.modes[this._currentIndex]; michael@0: }, michael@0: michael@0: previous: function quickNavMode_previous() { michael@0: if (--this._currentIndex < 0) michael@0: this._currentIndex = this.modes.length - 1; michael@0: }, michael@0: michael@0: next: function quickNavMode_next() { michael@0: if (++this._currentIndex >= this.modes.length) michael@0: this._currentIndex = 0; michael@0: }, michael@0: michael@0: updateModes: function updateModes(aModes) { michael@0: if (aModes) { michael@0: this.modes = aModes.split(','); michael@0: } else { michael@0: this.modes = []; michael@0: } michael@0: }, michael@0: michael@0: _currentIndex: -1 michael@0: } michael@0: }; michael@0: AccessFu.Input = Input;