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 = '