1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/accessible/src/jsat/AccessFu.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1075 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +'use strict'; 1.9 + 1.10 +const Cc = Components.classes; 1.11 +const Ci = Components.interfaces; 1.12 +const Cu = Components.utils; 1.13 +const Cr = Components.results; 1.14 + 1.15 +this.EXPORTED_SYMBOLS = ['AccessFu']; 1.16 + 1.17 +Cu.import('resource://gre/modules/Services.jsm'); 1.18 + 1.19 +Cu.import('resource://gre/modules/accessibility/Utils.jsm'); 1.20 + 1.21 +const ACCESSFU_DISABLE = 0; 1.22 +const ACCESSFU_ENABLE = 1; 1.23 +const ACCESSFU_AUTO = 2; 1.24 + 1.25 +const SCREENREADER_SETTING = 'accessibility.screenreader'; 1.26 + 1.27 +this.AccessFu = { 1.28 + /** 1.29 + * Initialize chrome-layer accessibility functionality. 1.30 + * If accessibility is enabled on the platform, then a special accessibility 1.31 + * mode is started. 1.32 + */ 1.33 + attach: function attach(aWindow) { 1.34 + Utils.init(aWindow); 1.35 + 1.36 + try { 1.37 + Services.androidBridge.handleGeckoMessage( 1.38 + { type: 'Accessibility:Ready' }); 1.39 + Services.obs.addObserver(this, 'Accessibility:Settings', false); 1.40 + } catch (x) { 1.41 + // Not on Android 1.42 + if (aWindow.navigator.mozSettings) { 1.43 + let lock = aWindow.navigator.mozSettings.createLock(); 1.44 + let req = lock.get(SCREENREADER_SETTING); 1.45 + req.addEventListener('success', () => { 1.46 + this._systemPref = req.result[SCREENREADER_SETTING]; 1.47 + this._enableOrDisable(); 1.48 + }); 1.49 + aWindow.navigator.mozSettings.addObserver( 1.50 + SCREENREADER_SETTING, this.handleEvent.bind(this)); 1.51 + } 1.52 + } 1.53 + 1.54 + this._activatePref = new PrefCache( 1.55 + 'accessibility.accessfu.activate', this._enableOrDisable.bind(this)); 1.56 + 1.57 + this._enableOrDisable(); 1.58 + }, 1.59 + 1.60 + /** 1.61 + * Shut down chrome-layer accessibility functionality from the outside. 1.62 + */ 1.63 + detach: function detach() { 1.64 + // Avoid disabling twice. 1.65 + if (this._enabled) { 1.66 + this._disable(); 1.67 + } 1.68 + if (Utils.MozBuildApp === 'mobile/android') { 1.69 + Services.obs.removeObserver(this, 'Accessibility:Settings'); 1.70 + } else if (Utils.win.navigator.mozSettings) { 1.71 + Utils.win.navigator.mozSettings.removeObserver( 1.72 + SCREENREADER_SETTING, this.handleEvent.bind(this)); 1.73 + } 1.74 + delete this._activatePref; 1.75 + Utils.uninit(); 1.76 + }, 1.77 + 1.78 + /** 1.79 + * Start AccessFu mode, this primarily means controlling the virtual cursor 1.80 + * with arrow keys. 1.81 + */ 1.82 + _enable: function _enable() { 1.83 + if (this._enabled) 1.84 + return; 1.85 + this._enabled = true; 1.86 + 1.87 + Cu.import('resource://gre/modules/accessibility/Utils.jsm'); 1.88 + Cu.import('resource://gre/modules/accessibility/PointerAdapter.jsm'); 1.89 + Cu.import('resource://gre/modules/accessibility/Presentation.jsm'); 1.90 + 1.91 + Logger.info('Enabled'); 1.92 + 1.93 + for each (let mm in Utils.AllMessageManagers) { 1.94 + this._addMessageListeners(mm); 1.95 + this._loadFrameScript(mm); 1.96 + } 1.97 + 1.98 + // Add stylesheet 1.99 + let stylesheetURL = 'chrome://global/content/accessibility/AccessFu.css'; 1.100 + let stylesheet = Utils.win.document.createProcessingInstruction( 1.101 + 'xml-stylesheet', 'href="' + stylesheetURL + '" type="text/css"'); 1.102 + Utils.win.document.insertBefore(stylesheet, Utils.win.document.firstChild); 1.103 + this.stylesheet = Cu.getWeakReference(stylesheet); 1.104 + 1.105 + 1.106 + // Populate quicknav modes 1.107 + this._quicknavModesPref = 1.108 + new PrefCache( 1.109 + 'accessibility.accessfu.quicknav_modes', 1.110 + (aName, aValue) => { 1.111 + this.Input.quickNavMode.updateModes(aValue); 1.112 + }, true); 1.113 + 1.114 + // Check for output notification 1.115 + this._notifyOutputPref = 1.116 + new PrefCache('accessibility.accessfu.notify_output'); 1.117 + 1.118 + 1.119 + this.Input.start(); 1.120 + Output.start(); 1.121 + PointerAdapter.start(); 1.122 + 1.123 + Services.obs.addObserver(this, 'remote-browser-shown', false); 1.124 + Services.obs.addObserver(this, 'inprocess-browser-shown', false); 1.125 + Services.obs.addObserver(this, 'Accessibility:NextObject', false); 1.126 + Services.obs.addObserver(this, 'Accessibility:PreviousObject', false); 1.127 + Services.obs.addObserver(this, 'Accessibility:Focus', false); 1.128 + Services.obs.addObserver(this, 'Accessibility:ActivateObject', false); 1.129 + Services.obs.addObserver(this, 'Accessibility:LongPress', false); 1.130 + Services.obs.addObserver(this, 'Accessibility:MoveByGranularity', false); 1.131 + Utils.win.addEventListener('TabOpen', this); 1.132 + Utils.win.addEventListener('TabClose', this); 1.133 + Utils.win.addEventListener('TabSelect', this); 1.134 + 1.135 + if (this.readyCallback) { 1.136 + this.readyCallback(); 1.137 + delete this.readyCallback; 1.138 + } 1.139 + 1.140 + if (Utils.MozBuildApp !== 'mobile/android') { 1.141 + this.announce( 1.142 + Utils.stringBundle.GetStringFromName('screenReaderStarted')); 1.143 + } 1.144 + }, 1.145 + 1.146 + /** 1.147 + * Disable AccessFu and return to default interaction mode. 1.148 + */ 1.149 + _disable: function _disable() { 1.150 + if (!this._enabled) 1.151 + return; 1.152 + 1.153 + this._enabled = false; 1.154 + 1.155 + Logger.info('Disabled'); 1.156 + 1.157 + Utils.win.document.removeChild(this.stylesheet.get()); 1.158 + 1.159 + if (Utils.MozBuildApp !== 'mobile/android') { 1.160 + this.announce( 1.161 + Utils.stringBundle.GetStringFromName('screenReaderStopped')); 1.162 + } 1.163 + 1.164 + for each (let mm in Utils.AllMessageManagers) { 1.165 + mm.sendAsyncMessage('AccessFu:Stop'); 1.166 + this._removeMessageListeners(mm); 1.167 + } 1.168 + 1.169 + this.Input.stop(); 1.170 + Output.stop(); 1.171 + PointerAdapter.stop(); 1.172 + 1.173 + Utils.win.removeEventListener('TabOpen', this); 1.174 + Utils.win.removeEventListener('TabClose', this); 1.175 + Utils.win.removeEventListener('TabSelect', this); 1.176 + 1.177 + Services.obs.removeObserver(this, 'remote-browser-shown'); 1.178 + Services.obs.removeObserver(this, 'inprocess-browser-shown'); 1.179 + Services.obs.removeObserver(this, 'Accessibility:NextObject'); 1.180 + Services.obs.removeObserver(this, 'Accessibility:PreviousObject'); 1.181 + Services.obs.removeObserver(this, 'Accessibility:Focus'); 1.182 + Services.obs.removeObserver(this, 'Accessibility:ActivateObject'); 1.183 + Services.obs.removeObserver(this, 'Accessibility:LongPress'); 1.184 + Services.obs.removeObserver(this, 'Accessibility:MoveByGranularity'); 1.185 + 1.186 + delete this._quicknavModesPref; 1.187 + delete this._notifyOutputPref; 1.188 + 1.189 + if (this.doneCallback) { 1.190 + this.doneCallback(); 1.191 + delete this.doneCallback; 1.192 + } 1.193 + }, 1.194 + 1.195 + _enableOrDisable: function _enableOrDisable() { 1.196 + try { 1.197 + if (!this._activatePref) { 1.198 + return; 1.199 + } 1.200 + let activatePref = this._activatePref.value; 1.201 + if (activatePref == ACCESSFU_ENABLE || 1.202 + this._systemPref && activatePref == ACCESSFU_AUTO) 1.203 + this._enable(); 1.204 + else 1.205 + this._disable(); 1.206 + } catch (x) { 1.207 + dump('Error ' + x.message + ' ' + x.fileName + ':' + x.lineNumber); 1.208 + } 1.209 + }, 1.210 + 1.211 + receiveMessage: function receiveMessage(aMessage) { 1.212 + Logger.debug(() => { 1.213 + return ['Recieved', aMessage.name, JSON.stringify(aMessage.json)]; 1.214 + }); 1.215 + 1.216 + switch (aMessage.name) { 1.217 + case 'AccessFu:Ready': 1.218 + let mm = Utils.getMessageManager(aMessage.target); 1.219 + if (this._enabled) { 1.220 + mm.sendAsyncMessage('AccessFu:Start', 1.221 + {method: 'start', buildApp: Utils.MozBuildApp}); 1.222 + } 1.223 + break; 1.224 + case 'AccessFu:Present': 1.225 + this._output(aMessage.json, aMessage.target); 1.226 + break; 1.227 + case 'AccessFu:Input': 1.228 + this.Input.setEditState(aMessage.json); 1.229 + break; 1.230 + case 'AccessFu:ActivateContextMenu': 1.231 + this.Input.activateContextMenu(aMessage.json); 1.232 + break; 1.233 + case 'AccessFu:DoScroll': 1.234 + this.Input.doScroll(aMessage.json); 1.235 + break; 1.236 + } 1.237 + }, 1.238 + 1.239 + _output: function _output(aPresentationData, aBrowser) { 1.240 + for each (let presenter in aPresentationData) { 1.241 + if (!presenter) 1.242 + continue; 1.243 + 1.244 + try { 1.245 + Output[presenter.type](presenter.details, aBrowser); 1.246 + } catch (x) { 1.247 + Logger.logException(x); 1.248 + } 1.249 + } 1.250 + 1.251 + if (this._notifyOutputPref.value) { 1.252 + Services.obs.notifyObservers(null, 'accessfu-output', 1.253 + JSON.stringify(aPresentationData)); 1.254 + } 1.255 + }, 1.256 + 1.257 + _loadFrameScript: function _loadFrameScript(aMessageManager) { 1.258 + if (this._processedMessageManagers.indexOf(aMessageManager) < 0) { 1.259 + aMessageManager.loadFrameScript( 1.260 + 'chrome://global/content/accessibility/content-script.js', true); 1.261 + this._processedMessageManagers.push(aMessageManager); 1.262 + } else if (this._enabled) { 1.263 + // If the content-script is already loaded and AccessFu is enabled, 1.264 + // send an AccessFu:Start message. 1.265 + aMessageManager.sendAsyncMessage('AccessFu:Start', 1.266 + {method: 'start', buildApp: Utils.MozBuildApp}); 1.267 + } 1.268 + }, 1.269 + 1.270 + _addMessageListeners: function _addMessageListeners(aMessageManager) { 1.271 + aMessageManager.addMessageListener('AccessFu:Present', this); 1.272 + aMessageManager.addMessageListener('AccessFu:Input', this); 1.273 + aMessageManager.addMessageListener('AccessFu:Ready', this); 1.274 + aMessageManager.addMessageListener('AccessFu:ActivateContextMenu', this); 1.275 + aMessageManager.addMessageListener('AccessFu:DoScroll', this); 1.276 + }, 1.277 + 1.278 + _removeMessageListeners: function _removeMessageListeners(aMessageManager) { 1.279 + aMessageManager.removeMessageListener('AccessFu:Present', this); 1.280 + aMessageManager.removeMessageListener('AccessFu:Input', this); 1.281 + aMessageManager.removeMessageListener('AccessFu:Ready', this); 1.282 + aMessageManager.removeMessageListener('AccessFu:ActivateContextMenu', this); 1.283 + aMessageManager.removeMessageListener('AccessFu:DoScroll', this); 1.284 + }, 1.285 + 1.286 + _handleMessageManager: function _handleMessageManager(aMessageManager) { 1.287 + if (this._enabled) { 1.288 + this._addMessageListeners(aMessageManager); 1.289 + } 1.290 + this._loadFrameScript(aMessageManager); 1.291 + }, 1.292 + 1.293 + observe: function observe(aSubject, aTopic, aData) { 1.294 + switch (aTopic) { 1.295 + case 'Accessibility:Settings': 1.296 + this._systemPref = JSON.parse(aData).enabled; 1.297 + this._enableOrDisable(); 1.298 + break; 1.299 + case 'Accessibility:NextObject': 1.300 + this.Input.moveCursor('moveNext', 'Simple', 'gesture'); 1.301 + break; 1.302 + case 'Accessibility:PreviousObject': 1.303 + this.Input.moveCursor('movePrevious', 'Simple', 'gesture'); 1.304 + break; 1.305 + case 'Accessibility:ActivateObject': 1.306 + this.Input.activateCurrent(JSON.parse(aData)); 1.307 + break; 1.308 + case 'Accessibility:LongPress': 1.309 + this.Input.sendContextMenuMessage(); 1.310 + break; 1.311 + case 'Accessibility:Focus': 1.312 + this._focused = JSON.parse(aData); 1.313 + if (this._focused) { 1.314 + this.autoMove({ forcePresent: true, noOpIfOnScreen: true }); 1.315 + } 1.316 + break; 1.317 + case 'Accessibility:MoveByGranularity': 1.318 + this.Input.moveByGranularity(JSON.parse(aData)); 1.319 + break; 1.320 + case 'remote-browser-shown': 1.321 + case 'inprocess-browser-shown': 1.322 + { 1.323 + // Ignore notifications that aren't from a BrowserOrApp 1.324 + let frameLoader = aSubject.QueryInterface(Ci.nsIFrameLoader); 1.325 + if (!frameLoader.ownerIsBrowserOrAppFrame) { 1.326 + return; 1.327 + } 1.328 + this._handleMessageManager(frameLoader.messageManager); 1.329 + break; 1.330 + } 1.331 + } 1.332 + }, 1.333 + 1.334 + handleEvent: function handleEvent(aEvent) { 1.335 + switch (aEvent.type) { 1.336 + case 'TabOpen': 1.337 + { 1.338 + let mm = Utils.getMessageManager(aEvent.target); 1.339 + this._handleMessageManager(mm); 1.340 + break; 1.341 + } 1.342 + case 'TabClose': 1.343 + { 1.344 + let mm = Utils.getMessageManager(aEvent.target); 1.345 + let mmIndex = this._processedMessageManagers.indexOf(mm); 1.346 + if (mmIndex > -1) { 1.347 + this._removeMessageListeners(mm); 1.348 + this._processedMessageManagers.splice(mmIndex, 1); 1.349 + } 1.350 + break; 1.351 + } 1.352 + case 'TabSelect': 1.353 + { 1.354 + if (this._focused) { 1.355 + // We delay this for half a second so the awesomebar could close, 1.356 + // and we could use the current coordinates for the content item. 1.357 + // XXX TODO figure out how to avoid magic wait here. 1.358 + this.autoMove({ 1.359 + delay: 500, 1.360 + forcePresent: true, 1.361 + noOpIfOnScreen: true, 1.362 + moveMethod: 'moveFirst' }); 1.363 + } 1.364 + break; 1.365 + } 1.366 + default: 1.367 + { 1.368 + // A settings change, it does not have an event type 1.369 + if (aEvent.settingName == SCREENREADER_SETTING) { 1.370 + this._systemPref = aEvent.settingValue; 1.371 + this._enableOrDisable(); 1.372 + } 1.373 + break; 1.374 + } 1.375 + } 1.376 + }, 1.377 + 1.378 + autoMove: function autoMove(aOptions) { 1.379 + let mm = Utils.getMessageManager(Utils.CurrentBrowser); 1.380 + mm.sendAsyncMessage('AccessFu:AutoMove', aOptions); 1.381 + }, 1.382 + 1.383 + announce: function announce(aAnnouncement) { 1.384 + this._output(Presentation.announce(aAnnouncement), Utils.CurrentBrowser); 1.385 + }, 1.386 + 1.387 + // So we don't enable/disable twice 1.388 + _enabled: false, 1.389 + 1.390 + // Layerview is focused 1.391 + _focused: false, 1.392 + 1.393 + // Keep track of message managers tha already have a 'content-script.js' 1.394 + // injected. 1.395 + _processedMessageManagers: [], 1.396 + 1.397 + /** 1.398 + * Adjusts the given bounds relative to the given browser. Converts from screen 1.399 + * or device pixels to either device or CSS pixels. 1.400 + * @param {Rect} aJsonBounds the bounds to adjust 1.401 + * @param {browser} aBrowser the browser we want the bounds relative to 1.402 + * @param {bool} aToCSSPixels whether to convert to CSS pixels (as opposed to 1.403 + * device pixels) 1.404 + * @param {bool} aFromDevicePixels whether to convert from device pixels (as 1.405 + * opposed to screen pixels) 1.406 + */ 1.407 + adjustContentBounds: function(aJsonBounds, aBrowser, aToCSSPixels, aFromDevicePixels) { 1.408 + let bounds = new Rect(aJsonBounds.left, aJsonBounds.top, 1.409 + aJsonBounds.right - aJsonBounds.left, 1.410 + aJsonBounds.bottom - aJsonBounds.top); 1.411 + let win = Utils.win; 1.412 + let dpr = win.devicePixelRatio; 1.413 + let vp = Utils.getViewport(win); 1.414 + let offset = { left: -win.mozInnerScreenX, top: -win.mozInnerScreenY }; 1.415 + 1.416 + if (!aBrowser.contentWindow) { 1.417 + // OOP browser, add offset of browser. 1.418 + // The offset of the browser element in relation to its parent window. 1.419 + let clientRect = aBrowser.getBoundingClientRect(); 1.420 + let win = aBrowser.ownerDocument.defaultView; 1.421 + offset.left += clientRect.left + win.mozInnerScreenX; 1.422 + offset.top += clientRect.top + win.mozInnerScreenY; 1.423 + } 1.424 + 1.425 + // Here we scale from screen pixels to layout device pixels by dividing by 1.426 + // the resolution (caused by pinch-zooming). The resolution is the viewport 1.427 + // zoom divided by the devicePixelRatio. If there's no viewport, then we're 1.428 + // on a platform without pinch-zooming and we can just ignore this. 1.429 + if (!aFromDevicePixels && vp) { 1.430 + bounds = bounds.scale(vp.zoom / dpr, vp.zoom / dpr); 1.431 + } 1.432 + 1.433 + // Add the offset; the offset is in CSS pixels, so multiply the 1.434 + // devicePixelRatio back in before adding to preserve unit consistency. 1.435 + bounds = bounds.translate(offset.left * dpr, offset.top * dpr); 1.436 + 1.437 + // If we want to get to CSS pixels from device pixels, this needs to be 1.438 + // further divided by the devicePixelRatio due to widget scaling. 1.439 + if (aToCSSPixels) { 1.440 + bounds = bounds.scale(1 / dpr, 1 / dpr); 1.441 + } 1.442 + 1.443 + return bounds.expandToIntegers(); 1.444 + } 1.445 +}; 1.446 + 1.447 +var Output = { 1.448 + brailleState: { 1.449 + startOffset: 0, 1.450 + endOffset: 0, 1.451 + text: '', 1.452 + selectionStart: 0, 1.453 + selectionEnd: 0, 1.454 + 1.455 + init: function init(aOutput) { 1.456 + if (aOutput && 'output' in aOutput) { 1.457 + this.startOffset = aOutput.startOffset; 1.458 + this.endOffset = aOutput.endOffset; 1.459 + // We need to append a space at the end so that the routing key corresponding 1.460 + // to the end of the output (i.e. the space) can be hit to move the caret there. 1.461 + this.text = aOutput.output + ' '; 1.462 + this.selectionStart = typeof aOutput.selectionStart === 'number' ? 1.463 + aOutput.selectionStart : this.selectionStart; 1.464 + this.selectionEnd = typeof aOutput.selectionEnd === 'number' ? 1.465 + aOutput.selectionEnd : this.selectionEnd; 1.466 + 1.467 + return { text: this.text, 1.468 + selectionStart: this.selectionStart, 1.469 + selectionEnd: this.selectionEnd }; 1.470 + } 1.471 + 1.472 + return null; 1.473 + }, 1.474 + 1.475 + adjustText: function adjustText(aText) { 1.476 + let newBraille = []; 1.477 + let braille = {}; 1.478 + 1.479 + let prefix = this.text.substring(0, this.startOffset).trim(); 1.480 + if (prefix) { 1.481 + prefix += ' '; 1.482 + newBraille.push(prefix); 1.483 + } 1.484 + 1.485 + newBraille.push(aText); 1.486 + 1.487 + let suffix = this.text.substring(this.endOffset).trim(); 1.488 + if (suffix) { 1.489 + suffix = ' ' + suffix; 1.490 + newBraille.push(suffix); 1.491 + } 1.492 + 1.493 + this.startOffset = braille.startOffset = prefix.length; 1.494 + this.text = braille.text = newBraille.join('') + ' '; 1.495 + this.endOffset = braille.endOffset = braille.text.length - suffix.length; 1.496 + braille.selectionStart = this.selectionStart; 1.497 + braille.selectionEnd = this.selectionEnd; 1.498 + 1.499 + return braille; 1.500 + }, 1.501 + 1.502 + adjustSelection: function adjustSelection(aSelection) { 1.503 + let braille = {}; 1.504 + 1.505 + braille.startOffset = this.startOffset; 1.506 + braille.endOffset = this.endOffset; 1.507 + braille.text = this.text; 1.508 + this.selectionStart = braille.selectionStart = aSelection.selectionStart + this.startOffset; 1.509 + this.selectionEnd = braille.selectionEnd = aSelection.selectionEnd + this.startOffset; 1.510 + 1.511 + return braille; 1.512 + } 1.513 + }, 1.514 + 1.515 + speechHelper: { 1.516 + EARCONS: ['virtual_cursor_move.ogg', 1.517 + 'virtual_cursor_key.ogg', 1.518 + 'clicked.ogg'], 1.519 + 1.520 + earconBuffers: {}, 1.521 + 1.522 + inited: false, 1.523 + 1.524 + webspeechEnabled: false, 1.525 + 1.526 + deferredOutputs: [], 1.527 + 1.528 + init: function init() { 1.529 + let window = Utils.win; 1.530 + this.webspeechEnabled = !!window.speechSynthesis && 1.531 + !!window.SpeechSynthesisUtterance; 1.532 + 1.533 + let settingsToGet = 2; 1.534 + let settingsCallback = (aName, aSetting) => { 1.535 + if (--settingsToGet > 0) { 1.536 + return; 1.537 + } 1.538 + 1.539 + this.inited = true; 1.540 + 1.541 + for (let actions of this.deferredOutputs) { 1.542 + this.output(actions); 1.543 + } 1.544 + }; 1.545 + 1.546 + this._volumeSetting = new SettingCache( 1.547 + 'accessibility.screenreader-volume', settingsCallback, 1.548 + { defaultValue: 1, callbackNow: true, callbackOnce: true }); 1.549 + this._rateSetting = new SettingCache( 1.550 + 'accessibility.screenreader-rate', settingsCallback, 1.551 + { defaultValue: 0, callbackNow: true, callbackOnce: true }); 1.552 + 1.553 + for (let earcon of this.EARCONS) { 1.554 + let earconName = /(^.*)\..*$/.exec(earcon)[1]; 1.555 + this.earconBuffers[earconName] = new WeakMap(); 1.556 + this.earconBuffers[earconName].set( 1.557 + window, new window.Audio('chrome://global/content/accessibility/' + earcon)); 1.558 + } 1.559 + }, 1.560 + 1.561 + uninit: function uninit() { 1.562 + if (this.inited) { 1.563 + delete this._volumeSetting; 1.564 + delete this._rateSetting; 1.565 + } 1.566 + this.inited = false; 1.567 + }, 1.568 + 1.569 + output: function output(aActions) { 1.570 + if (!this.inited) { 1.571 + this.deferredOutputs.push(aActions); 1.572 + return; 1.573 + } 1.574 + 1.575 + for (let action of aActions) { 1.576 + let window = Utils.win; 1.577 + Logger.debug('tts.' + action.method, '"' + action.data + '"', 1.578 + JSON.stringify(action.options)); 1.579 + 1.580 + if (!action.options.enqueue && this.webspeechEnabled) { 1.581 + window.speechSynthesis.cancel(); 1.582 + } 1.583 + 1.584 + if (action.method === 'speak' && this.webspeechEnabled) { 1.585 + let utterance = new window.SpeechSynthesisUtterance(action.data); 1.586 + let requestedRate = this._rateSetting.value; 1.587 + utterance.volume = this._volumeSetting.value; 1.588 + utterance.rate = requestedRate >= 0 ? 1.589 + requestedRate + 1 : 1 / (Math.abs(requestedRate) + 1); 1.590 + window.speechSynthesis.speak(utterance); 1.591 + } else if (action.method === 'playEarcon') { 1.592 + let audioBufferWeakMap = this.earconBuffers[action.data]; 1.593 + if (audioBufferWeakMap) { 1.594 + let node = audioBufferWeakMap.get(window).cloneNode(false); 1.595 + node.volume = this._volumeSetting.value; 1.596 + node.play(); 1.597 + } 1.598 + } 1.599 + } 1.600 + } 1.601 + }, 1.602 + 1.603 + start: function start() { 1.604 + Cu.import('resource://gre/modules/Geometry.jsm'); 1.605 + this.speechHelper.init(); 1.606 + }, 1.607 + 1.608 + stop: function stop() { 1.609 + if (this.highlightBox) { 1.610 + Utils.win.document.documentElement.removeChild(this.highlightBox.get()); 1.611 + delete this.highlightBox; 1.612 + } 1.613 + 1.614 + if (this.announceBox) { 1.615 + Utils.win.document.documentElement.removeChild(this.announceBox.get()); 1.616 + delete this.announceBox; 1.617 + } 1.618 + 1.619 + this.speechHelper.uninit(); 1.620 + }, 1.621 + 1.622 + Speech: function Speech(aDetails, aBrowser) { 1.623 + this.speechHelper.output(aDetails.actions); 1.624 + }, 1.625 + 1.626 + Visual: function Visual(aDetails, aBrowser) { 1.627 + switch (aDetails.method) { 1.628 + case 'showBounds': 1.629 + { 1.630 + let highlightBox = null; 1.631 + if (!this.highlightBox) { 1.632 + // Add highlight box 1.633 + highlightBox = Utils.win.document. 1.634 + createElementNS('http://www.w3.org/1999/xhtml', 'div'); 1.635 + Utils.win.document.documentElement.appendChild(highlightBox); 1.636 + highlightBox.id = 'virtual-cursor-box'; 1.637 + 1.638 + // Add highlight inset for inner shadow 1.639 + let inset = Utils.win.document. 1.640 + createElementNS('http://www.w3.org/1999/xhtml', 'div'); 1.641 + inset.id = 'virtual-cursor-inset'; 1.642 + 1.643 + highlightBox.appendChild(inset); 1.644 + this.highlightBox = Cu.getWeakReference(highlightBox); 1.645 + } else { 1.646 + highlightBox = this.highlightBox.get(); 1.647 + } 1.648 + 1.649 + let padding = aDetails.padding; 1.650 + let r = AccessFu.adjustContentBounds(aDetails.bounds, aBrowser, true); 1.651 + 1.652 + // First hide it to avoid flickering when changing the style. 1.653 + highlightBox.style.display = 'none'; 1.654 + highlightBox.style.top = (r.top - padding) + 'px'; 1.655 + highlightBox.style.left = (r.left - padding) + 'px'; 1.656 + highlightBox.style.width = (r.width + padding*2) + 'px'; 1.657 + highlightBox.style.height = (r.height + padding*2) + 'px'; 1.658 + highlightBox.style.display = 'block'; 1.659 + 1.660 + break; 1.661 + } 1.662 + case 'hideBounds': 1.663 + { 1.664 + let highlightBox = this.highlightBox ? this.highlightBox.get() : null; 1.665 + if (highlightBox) 1.666 + highlightBox.style.display = 'none'; 1.667 + break; 1.668 + } 1.669 + case 'showAnnouncement': 1.670 + { 1.671 + let announceBox = this.announceBox ? this.announceBox.get() : null; 1.672 + if (!announceBox) { 1.673 + announceBox = Utils.win.document. 1.674 + createElementNS('http://www.w3.org/1999/xhtml', 'div'); 1.675 + announceBox.id = 'announce-box'; 1.676 + Utils.win.document.documentElement.appendChild(announceBox); 1.677 + this.announceBox = Cu.getWeakReference(announceBox); 1.678 + } 1.679 + 1.680 + announceBox.innerHTML = '<div>' + aDetails.text + '</div>'; 1.681 + announceBox.classList.add('showing'); 1.682 + 1.683 + if (this._announceHideTimeout) 1.684 + Utils.win.clearTimeout(this._announceHideTimeout); 1.685 + 1.686 + if (aDetails.duration > 0) 1.687 + this._announceHideTimeout = Utils.win.setTimeout( 1.688 + function () { 1.689 + announceBox.classList.remove('showing'); 1.690 + this._announceHideTimeout = 0; 1.691 + }.bind(this), aDetails.duration); 1.692 + break; 1.693 + } 1.694 + case 'hideAnnouncement': 1.695 + { 1.696 + let announceBox = this.announceBox ? this.announceBox.get() : null; 1.697 + if (announceBox) 1.698 + announceBox.classList.remove('showing'); 1.699 + break; 1.700 + } 1.701 + } 1.702 + }, 1.703 + 1.704 + get androidBridge() { 1.705 + delete this.androidBridge; 1.706 + if (Utils.MozBuildApp === 'mobile/android') { 1.707 + this.androidBridge = Services.androidBridge; 1.708 + } else { 1.709 + this.androidBridge = null; 1.710 + } 1.711 + return this.androidBridge; 1.712 + }, 1.713 + 1.714 + Android: function Android(aDetails, aBrowser) { 1.715 + const ANDROID_VIEW_TEXT_CHANGED = 0x10; 1.716 + const ANDROID_VIEW_TEXT_SELECTION_CHANGED = 0x2000; 1.717 + 1.718 + if (!this.androidBridge) { 1.719 + return; 1.720 + } 1.721 + 1.722 + for each (let androidEvent in aDetails) { 1.723 + androidEvent.type = 'Accessibility:Event'; 1.724 + if (androidEvent.bounds) 1.725 + androidEvent.bounds = AccessFu.adjustContentBounds(androidEvent.bounds, aBrowser); 1.726 + 1.727 + switch(androidEvent.eventType) { 1.728 + case ANDROID_VIEW_TEXT_CHANGED: 1.729 + androidEvent.brailleOutput = this.brailleState.adjustText(androidEvent.text); 1.730 + break; 1.731 + case ANDROID_VIEW_TEXT_SELECTION_CHANGED: 1.732 + androidEvent.brailleOutput = this.brailleState.adjustSelection(androidEvent.brailleOutput); 1.733 + break; 1.734 + default: 1.735 + androidEvent.brailleOutput = this.brailleState.init(androidEvent.brailleOutput); 1.736 + break; 1.737 + } 1.738 + this.androidBridge.handleGeckoMessage(androidEvent); 1.739 + } 1.740 + }, 1.741 + 1.742 + Haptic: function Haptic(aDetails, aBrowser) { 1.743 + Utils.win.navigator.vibrate(aDetails.pattern); 1.744 + }, 1.745 + 1.746 + Braille: function Braille(aDetails, aBrowser) { 1.747 + Logger.debug('Braille output: ' + aDetails.text); 1.748 + } 1.749 +}; 1.750 + 1.751 +var Input = { 1.752 + editState: {}, 1.753 + 1.754 + start: function start() { 1.755 + // XXX: This is too disruptive on desktop for now. 1.756 + // Might need to add special modifiers. 1.757 + if (Utils.MozBuildApp != 'browser') { 1.758 + Utils.win.document.addEventListener('keypress', this, true); 1.759 + } 1.760 + Utils.win.addEventListener('mozAccessFuGesture', this, true); 1.761 + }, 1.762 + 1.763 + stop: function stop() { 1.764 + if (Utils.MozBuildApp != 'browser') { 1.765 + Utils.win.document.removeEventListener('keypress', this, true); 1.766 + } 1.767 + Utils.win.removeEventListener('mozAccessFuGesture', this, true); 1.768 + }, 1.769 + 1.770 + handleEvent: function Input_handleEvent(aEvent) { 1.771 + try { 1.772 + switch (aEvent.type) { 1.773 + case 'keypress': 1.774 + this._handleKeypress(aEvent); 1.775 + break; 1.776 + case 'mozAccessFuGesture': 1.777 + this._handleGesture(aEvent.detail); 1.778 + break; 1.779 + } 1.780 + } catch (x) { 1.781 + Logger.logException(x); 1.782 + } 1.783 + }, 1.784 + 1.785 + _handleGesture: function _handleGesture(aGesture) { 1.786 + let gestureName = aGesture.type + aGesture.touches.length; 1.787 + Logger.debug('Gesture', aGesture.type, 1.788 + '(fingers: ' + aGesture.touches.length + ')'); 1.789 + 1.790 + switch (gestureName) { 1.791 + case 'dwell1': 1.792 + case 'explore1': 1.793 + this.moveToPoint('Simple', aGesture.touches[0].x, 1.794 + aGesture.touches[0].y); 1.795 + break; 1.796 + case 'doubletap1': 1.797 + this.activateCurrent(); 1.798 + break; 1.799 + case 'doubletaphold1': 1.800 + this.sendContextMenuMessage(); 1.801 + break; 1.802 + case 'swiperight1': 1.803 + this.moveCursor('moveNext', 'Simple', 'gestures'); 1.804 + break; 1.805 + case 'swipeleft1': 1.806 + this.moveCursor('movePrevious', 'Simple', 'gesture'); 1.807 + break; 1.808 + case 'swipeup1': 1.809 + this.contextAction('backward'); 1.810 + break; 1.811 + case 'swipedown1': 1.812 + this.contextAction('forward'); 1.813 + break; 1.814 + case 'exploreend1': 1.815 + case 'dwellend1': 1.816 + this.activateCurrent(null, true); 1.817 + break; 1.818 + case 'swiperight2': 1.819 + this.sendScrollMessage(-1, true); 1.820 + break; 1.821 + case 'swipedown2': 1.822 + this.sendScrollMessage(-1); 1.823 + break; 1.824 + case 'swipeleft2': 1.825 + this.sendScrollMessage(1, true); 1.826 + break; 1.827 + case 'swipeup2': 1.828 + this.sendScrollMessage(1); 1.829 + break; 1.830 + case 'explore2': 1.831 + Utils.CurrentBrowser.contentWindow.scrollBy( 1.832 + -aGesture.deltaX, -aGesture.deltaY); 1.833 + break; 1.834 + case 'swiperight3': 1.835 + this.moveCursor('moveNext', this.quickNavMode.current, 'gesture'); 1.836 + break; 1.837 + case 'swipeleft3': 1.838 + this.moveCursor('movePrevious', this.quickNavMode.current, 'gesture'); 1.839 + break; 1.840 + case 'swipedown3': 1.841 + this.quickNavMode.next(); 1.842 + AccessFu.announce('quicknav_' + this.quickNavMode.current); 1.843 + break; 1.844 + case 'swipeup3': 1.845 + this.quickNavMode.previous(); 1.846 + AccessFu.announce('quicknav_' + this.quickNavMode.current); 1.847 + break; 1.848 + } 1.849 + }, 1.850 + 1.851 + _handleKeypress: function _handleKeypress(aEvent) { 1.852 + let target = aEvent.target; 1.853 + 1.854 + // Ignore keys with modifiers so the content could take advantage of them. 1.855 + if (aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey) 1.856 + return; 1.857 + 1.858 + switch (aEvent.keyCode) { 1.859 + case 0: 1.860 + // an alphanumeric key was pressed, handle it separately. 1.861 + // If it was pressed with either alt or ctrl, just pass through. 1.862 + // If it was pressed with meta, pass the key on without the meta. 1.863 + if (this.editState.editing) 1.864 + return; 1.865 + 1.866 + let key = String.fromCharCode(aEvent.charCode); 1.867 + try { 1.868 + let [methodName, rule] = this.keyMap[key]; 1.869 + this.moveCursor(methodName, rule, 'keyboard'); 1.870 + } catch (x) { 1.871 + return; 1.872 + } 1.873 + break; 1.874 + case aEvent.DOM_VK_RIGHT: 1.875 + if (this.editState.editing) { 1.876 + if (!this.editState.atEnd) 1.877 + // Don't move forward if caret is not at end of entry. 1.878 + // XXX: Fix for rtl 1.879 + return; 1.880 + else 1.881 + target.blur(); 1.882 + } 1.883 + this.moveCursor(aEvent.shiftKey ? 'moveLast' : 'moveNext', 'Simple', 'keyboard'); 1.884 + break; 1.885 + case aEvent.DOM_VK_LEFT: 1.886 + if (this.editState.editing) { 1.887 + if (!this.editState.atStart) 1.888 + // Don't move backward if caret is not at start of entry. 1.889 + // XXX: Fix for rtl 1.890 + return; 1.891 + else 1.892 + target.blur(); 1.893 + } 1.894 + this.moveCursor(aEvent.shiftKey ? 'moveFirst' : 'movePrevious', 'Simple', 'keyboard'); 1.895 + break; 1.896 + case aEvent.DOM_VK_UP: 1.897 + if (this.editState.multiline) { 1.898 + if (!this.editState.atStart) 1.899 + // Don't blur content if caret is not at start of text area. 1.900 + return; 1.901 + else 1.902 + target.blur(); 1.903 + } 1.904 + 1.905 + if (Utils.MozBuildApp == 'mobile/android') 1.906 + // Return focus to native Android browser chrome. 1.907 + Services.androidBridge.handleGeckoMessage( 1.908 + { type: 'ToggleChrome:Focus' }); 1.909 + break; 1.910 + case aEvent.DOM_VK_RETURN: 1.911 + if (this.editState.editing) 1.912 + return; 1.913 + this.activateCurrent(); 1.914 + break; 1.915 + default: 1.916 + return; 1.917 + } 1.918 + 1.919 + aEvent.preventDefault(); 1.920 + aEvent.stopPropagation(); 1.921 + }, 1.922 + 1.923 + moveToPoint: function moveToPoint(aRule, aX, aY) { 1.924 + let mm = Utils.getMessageManager(Utils.CurrentBrowser); 1.925 + mm.sendAsyncMessage('AccessFu:MoveToPoint', {rule: aRule, 1.926 + x: aX, y: aY, 1.927 + origin: 'top'}); 1.928 + }, 1.929 + 1.930 + moveCursor: function moveCursor(aAction, aRule, aInputType) { 1.931 + let mm = Utils.getMessageManager(Utils.CurrentBrowser); 1.932 + mm.sendAsyncMessage('AccessFu:MoveCursor', 1.933 + {action: aAction, rule: aRule, 1.934 + origin: 'top', inputType: aInputType}); 1.935 + }, 1.936 + 1.937 + contextAction: function contextAction(aDirection) { 1.938 + // XXX: For now, the only supported context action is adjusting a range. 1.939 + let mm = Utils.getMessageManager(Utils.CurrentBrowser); 1.940 + mm.sendAsyncMessage('AccessFu:AdjustRange', {direction: aDirection}); 1.941 + }, 1.942 + 1.943 + moveByGranularity: function moveByGranularity(aDetails) { 1.944 + const MOVEMENT_GRANULARITY_PARAGRAPH = 8; 1.945 + 1.946 + if (!this.editState.editing) { 1.947 + if (aDetails.granularity === MOVEMENT_GRANULARITY_PARAGRAPH) { 1.948 + this.moveCursor('move' + aDetails.direction, 'Paragraph', 'gesture'); 1.949 + return; 1.950 + } 1.951 + } else { 1.952 + aDetails.atStart = this.editState.atStart; 1.953 + aDetails.atEnd = this.editState.atEnd; 1.954 + } 1.955 + 1.956 + let mm = Utils.getMessageManager(Utils.CurrentBrowser); 1.957 + let type = this.editState.editing ? 'AccessFu:MoveCaret' : 1.958 + 'AccessFu:MoveByGranularity'; 1.959 + mm.sendAsyncMessage(type, aDetails); 1.960 + }, 1.961 + 1.962 + activateCurrent: function activateCurrent(aData, aActivateIfKey = false) { 1.963 + let mm = Utils.getMessageManager(Utils.CurrentBrowser); 1.964 + let offset = aData && typeof aData.keyIndex === 'number' ? 1.965 + aData.keyIndex - Output.brailleState.startOffset : -1; 1.966 + 1.967 + mm.sendAsyncMessage('AccessFu:Activate', 1.968 + {offset: offset, activateIfKey: aActivateIfKey}); 1.969 + }, 1.970 + 1.971 + sendContextMenuMessage: function sendContextMenuMessage() { 1.972 + let mm = Utils.getMessageManager(Utils.CurrentBrowser); 1.973 + mm.sendAsyncMessage('AccessFu:ContextMenu', {}); 1.974 + }, 1.975 + 1.976 + activateContextMenu: function activateContextMenu(aDetails) { 1.977 + if (Utils.MozBuildApp === 'mobile/android') { 1.978 + let p = AccessFu.adjustContentBounds(aDetails.bounds, Utils.CurrentBrowser, 1.979 + true, true).center(); 1.980 + Services.obs.notifyObservers(null, 'Gesture:LongPress', 1.981 + JSON.stringify({x: p.x, y: p.y})); 1.982 + } 1.983 + }, 1.984 + 1.985 + setEditState: function setEditState(aEditState) { 1.986 + this.editState = aEditState; 1.987 + }, 1.988 + 1.989 + // XXX: This is here for backwards compatability with screen reader simulator 1.990 + // it should be removed when the extension is updated on amo. 1.991 + scroll: function scroll(aPage, aHorizontal) { 1.992 + this.sendScrollMessage(aPage, aHorizontal); 1.993 + }, 1.994 + 1.995 + sendScrollMessage: function sendScrollMessage(aPage, aHorizontal) { 1.996 + let mm = Utils.getMessageManager(Utils.CurrentBrowser); 1.997 + mm.sendAsyncMessage('AccessFu:Scroll', {page: aPage, horizontal: aHorizontal, origin: 'top'}); 1.998 + }, 1.999 + 1.1000 + doScroll: function doScroll(aDetails) { 1.1001 + let horizontal = aDetails.horizontal; 1.1002 + let page = aDetails.page; 1.1003 + let p = AccessFu.adjustContentBounds(aDetails.bounds, Utils.CurrentBrowser, 1.1004 + true, true).center(); 1.1005 + let wu = Utils.win.QueryInterface(Ci.nsIInterfaceRequestor). 1.1006 + getInterface(Ci.nsIDOMWindowUtils); 1.1007 + wu.sendWheelEvent(p.x, p.y, 1.1008 + horizontal ? page : 0, horizontal ? 0 : page, 0, 1.1009 + Utils.win.WheelEvent.DOM_DELTA_PAGE, 0, 0, 0, 0); 1.1010 + }, 1.1011 + 1.1012 + get keyMap() { 1.1013 + delete this.keyMap; 1.1014 + this.keyMap = { 1.1015 + a: ['moveNext', 'Anchor'], 1.1016 + A: ['movePrevious', 'Anchor'], 1.1017 + b: ['moveNext', 'Button'], 1.1018 + B: ['movePrevious', 'Button'], 1.1019 + c: ['moveNext', 'Combobox'], 1.1020 + C: ['movePrevious', 'Combobox'], 1.1021 + d: ['moveNext', 'Landmark'], 1.1022 + D: ['movePrevious', 'Landmark'], 1.1023 + e: ['moveNext', 'Entry'], 1.1024 + E: ['movePrevious', 'Entry'], 1.1025 + f: ['moveNext', 'FormElement'], 1.1026 + F: ['movePrevious', 'FormElement'], 1.1027 + g: ['moveNext', 'Graphic'], 1.1028 + G: ['movePrevious', 'Graphic'], 1.1029 + h: ['moveNext', 'Heading'], 1.1030 + H: ['movePrevious', 'Heading'], 1.1031 + i: ['moveNext', 'ListItem'], 1.1032 + I: ['movePrevious', 'ListItem'], 1.1033 + k: ['moveNext', 'Link'], 1.1034 + K: ['movePrevious', 'Link'], 1.1035 + l: ['moveNext', 'List'], 1.1036 + L: ['movePrevious', 'List'], 1.1037 + p: ['moveNext', 'PageTab'], 1.1038 + P: ['movePrevious', 'PageTab'], 1.1039 + r: ['moveNext', 'RadioButton'], 1.1040 + R: ['movePrevious', 'RadioButton'], 1.1041 + s: ['moveNext', 'Separator'], 1.1042 + S: ['movePrevious', 'Separator'], 1.1043 + t: ['moveNext', 'Table'], 1.1044 + T: ['movePrevious', 'Table'], 1.1045 + x: ['moveNext', 'Checkbox'], 1.1046 + X: ['movePrevious', 'Checkbox'] 1.1047 + }; 1.1048 + 1.1049 + return this.keyMap; 1.1050 + }, 1.1051 + 1.1052 + quickNavMode: { 1.1053 + get current() { 1.1054 + return this.modes[this._currentIndex]; 1.1055 + }, 1.1056 + 1.1057 + previous: function quickNavMode_previous() { 1.1058 + if (--this._currentIndex < 0) 1.1059 + this._currentIndex = this.modes.length - 1; 1.1060 + }, 1.1061 + 1.1062 + next: function quickNavMode_next() { 1.1063 + if (++this._currentIndex >= this.modes.length) 1.1064 + this._currentIndex = 0; 1.1065 + }, 1.1066 + 1.1067 + updateModes: function updateModes(aModes) { 1.1068 + if (aModes) { 1.1069 + this.modes = aModes.split(','); 1.1070 + } else { 1.1071 + this.modes = []; 1.1072 + } 1.1073 + }, 1.1074 + 1.1075 + _currentIndex: -1 1.1076 + } 1.1077 +}; 1.1078 +AccessFu.Input = Input;