diff options
Diffstat (limited to 'accessible/jsat/AccessFu.jsm')
-rw-r--r-- | accessible/jsat/AccessFu.jsm | 1000 |
1 files changed, 1000 insertions, 0 deletions
diff --git a/accessible/jsat/AccessFu.jsm b/accessible/jsat/AccessFu.jsm new file mode 100644 index 000000000..c6b16b38f --- /dev/null +++ b/accessible/jsat/AccessFu.jsm @@ -0,0 +1,1000 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* global AccessFu, Components, Utils, PrefCache, Logger, Services, + PointerAdapter, dump, Presentation, Rect */ +/* exported AccessFu */ + +'use strict'; + +const {utils: Cu, interfaces: Ci} = Components; + +this.EXPORTED_SYMBOLS = ['AccessFu']; // jshint ignore:line + +Cu.import('resource://gre/modules/Services.jsm'); +Cu.import('resource://gre/modules/accessibility/Utils.jsm'); + +const ACCESSFU_DISABLE = 0; // jshint ignore:line +const ACCESSFU_ENABLE = 1; +const ACCESSFU_AUTO = 2; + +const SCREENREADER_SETTING = 'accessibility.screenreader'; +const QUICKNAV_MODES_PREF = 'accessibility.accessfu.quicknav_modes'; +const QUICKNAV_INDEX_PREF = 'accessibility.accessfu.quicknav_index'; + +this.AccessFu = { // jshint ignore:line + /** + * Initialize chrome-layer accessibility functionality. + * If accessibility is enabled on the platform, then a special accessibility + * mode is started. + */ + attach: function attach(aWindow) { + Utils.init(aWindow); + + try { + Services.androidBridge.handleGeckoMessage( + { type: 'Accessibility:Ready' }); + Services.obs.addObserver(this, 'Accessibility:Settings', false); + } catch (x) { + // Not on Android + if (aWindow.navigator.mozSettings) { + let lock = aWindow.navigator.mozSettings.createLock(); + let req = lock.get(SCREENREADER_SETTING); + req.addEventListener('success', () => { + this._systemPref = req.result[SCREENREADER_SETTING]; + this._enableOrDisable(); + }); + aWindow.navigator.mozSettings.addObserver( + SCREENREADER_SETTING, this.handleEvent); + } + } + + this._activatePref = new PrefCache( + 'accessibility.accessfu.activate', this._enableOrDisable.bind(this)); + + this._enableOrDisable(); + }, + + /** + * Shut down chrome-layer accessibility functionality from the outside. + */ + detach: function detach() { + // Avoid disabling twice. + if (this._enabled) { + this._disable(); + } + if (Utils.MozBuildApp === 'mobile/android') { + Services.obs.removeObserver(this, 'Accessibility:Settings'); + } else if (Utils.win.navigator.mozSettings) { + Utils.win.navigator.mozSettings.removeObserver( + SCREENREADER_SETTING, this.handleEvent); + } + delete this._activatePref; + Utils.uninit(); + }, + + /** + * A lazy getter for event handler that binds the scope to AccessFu object. + */ + get handleEvent() { + delete this.handleEvent; + this.handleEvent = this._handleEvent.bind(this); + return this.handleEvent; + }, + + /** + * Start AccessFu mode, this primarily means controlling the virtual cursor + * with arrow keys. + */ + _enable: function _enable() { + if (this._enabled) { + return; + } + this._enabled = true; + + Cu.import('resource://gre/modules/accessibility/Utils.jsm'); + Cu.import('resource://gre/modules/accessibility/PointerAdapter.jsm'); + Cu.import('resource://gre/modules/accessibility/Presentation.jsm'); + + for (let mm of Utils.AllMessageManagers) { + this._addMessageListeners(mm); + this._loadFrameScript(mm); + } + + // Add stylesheet + let stylesheetURL = 'chrome://global/content/accessibility/AccessFu.css'; + let stylesheet = Utils.win.document.createProcessingInstruction( + 'xml-stylesheet', 'href="' + stylesheetURL + '" type="text/css"'); + Utils.win.document.insertBefore(stylesheet, Utils.win.document.firstChild); + this.stylesheet = Cu.getWeakReference(stylesheet); + + + // Populate quicknav modes + this._quicknavModesPref = + new PrefCache(QUICKNAV_MODES_PREF, (aName, aValue, aFirstRun) => { + this.Input.quickNavMode.updateModes(aValue); + if (!aFirstRun) { + // If the modes change, reset the current mode index to 0. + Services.prefs.setIntPref(QUICKNAV_INDEX_PREF, 0); + } + }, true); + + this._quicknavCurrentModePref = + new PrefCache(QUICKNAV_INDEX_PREF, (aName, aValue) => { + this.Input.quickNavMode.updateCurrentMode(Number(aValue)); + }, true); + + // Check for output notification + this._notifyOutputPref = + new PrefCache('accessibility.accessfu.notify_output'); + + + this.Input.start(); + Output.start(); + PointerAdapter.start(); + + Services.obs.addObserver(this, 'remote-browser-shown', false); + Services.obs.addObserver(this, 'inprocess-browser-shown', false); + Services.obs.addObserver(this, 'Accessibility:NextObject', false); + Services.obs.addObserver(this, 'Accessibility:PreviousObject', false); + Services.obs.addObserver(this, 'Accessibility:Focus', false); + Services.obs.addObserver(this, 'Accessibility:ActivateObject', false); + Services.obs.addObserver(this, 'Accessibility:LongPress', false); + Services.obs.addObserver(this, 'Accessibility:ScrollForward', false); + Services.obs.addObserver(this, 'Accessibility:ScrollBackward', false); + Services.obs.addObserver(this, 'Accessibility:MoveByGranularity', false); + Utils.win.addEventListener('TabOpen', this); + Utils.win.addEventListener('TabClose', this); + Utils.win.addEventListener('TabSelect', this); + + if (this.readyCallback) { + this.readyCallback(); + delete this.readyCallback; + } + + Logger.info('AccessFu:Enabled'); + }, + + /** + * Disable AccessFu and return to default interaction mode. + */ + _disable: function _disable() { + if (!this._enabled) { + return; + } + + this._enabled = false; + + Utils.win.document.removeChild(this.stylesheet.get()); + + for (let mm of Utils.AllMessageManagers) { + mm.sendAsyncMessage('AccessFu:Stop'); + this._removeMessageListeners(mm); + } + + this.Input.stop(); + Output.stop(); + PointerAdapter.stop(); + + Utils.win.removeEventListener('TabOpen', this); + Utils.win.removeEventListener('TabClose', this); + Utils.win.removeEventListener('TabSelect', this); + + Services.obs.removeObserver(this, 'remote-browser-shown'); + Services.obs.removeObserver(this, 'inprocess-browser-shown'); + Services.obs.removeObserver(this, 'Accessibility:NextObject'); + Services.obs.removeObserver(this, 'Accessibility:PreviousObject'); + Services.obs.removeObserver(this, 'Accessibility:Focus'); + Services.obs.removeObserver(this, 'Accessibility:ActivateObject'); + Services.obs.removeObserver(this, 'Accessibility:LongPress'); + Services.obs.removeObserver(this, 'Accessibility:ScrollForward'); + Services.obs.removeObserver(this, 'Accessibility:ScrollBackward'); + Services.obs.removeObserver(this, 'Accessibility:MoveByGranularity'); + + delete this._quicknavModesPref; + delete this._notifyOutputPref; + + if (this.doneCallback) { + this.doneCallback(); + delete this.doneCallback; + } + + Logger.info('AccessFu:Disabled'); + }, + + _enableOrDisable: function _enableOrDisable() { + try { + if (!this._activatePref) { + return; + } + let activatePref = this._activatePref.value; + if (activatePref == ACCESSFU_ENABLE || + this._systemPref && activatePref == ACCESSFU_AUTO) { + this._enable(); + } else { + this._disable(); + } + } catch (x) { + dump('Error ' + x.message + ' ' + x.fileName + ':' + x.lineNumber); + } + }, + + receiveMessage: function receiveMessage(aMessage) { + Logger.debug(() => { + return ['Recieved', aMessage.name, JSON.stringify(aMessage.json)]; + }); + + switch (aMessage.name) { + case 'AccessFu:Ready': + let mm = Utils.getMessageManager(aMessage.target); + if (this._enabled) { + mm.sendAsyncMessage('AccessFu:Start', + {method: 'start', buildApp: Utils.MozBuildApp}); + } + break; + case 'AccessFu:Present': + this._output(aMessage.json, aMessage.target); + break; + case 'AccessFu:Input': + this.Input.setEditState(aMessage.json); + break; + case 'AccessFu:DoScroll': + this.Input.doScroll(aMessage.json); + break; + } + }, + + _output: function _output(aPresentationData, aBrowser) { + if (!Utils.isAliveAndVisible( + Utils.AccService.getAccessibleFor(aBrowser))) { + return; + } + for (let presenter of aPresentationData) { + if (!presenter) { + continue; + } + + try { + Output[presenter.type](presenter.details, aBrowser); + } catch (x) { + Logger.logException(x); + } + } + + if (this._notifyOutputPref.value) { + Services.obs.notifyObservers(null, 'accessibility-output', + JSON.stringify(aPresentationData)); + } + }, + + _loadFrameScript: function _loadFrameScript(aMessageManager) { + if (this._processedMessageManagers.indexOf(aMessageManager) < 0) { + aMessageManager.loadFrameScript( + 'chrome://global/content/accessibility/content-script.js', true); + this._processedMessageManagers.push(aMessageManager); + } else if (this._enabled) { + // If the content-script is already loaded and AccessFu is enabled, + // send an AccessFu:Start message. + aMessageManager.sendAsyncMessage('AccessFu:Start', + {method: 'start', buildApp: Utils.MozBuildApp}); + } + }, + + _addMessageListeners: function _addMessageListeners(aMessageManager) { + aMessageManager.addMessageListener('AccessFu:Present', this); + aMessageManager.addMessageListener('AccessFu:Input', this); + aMessageManager.addMessageListener('AccessFu:Ready', this); + aMessageManager.addMessageListener('AccessFu:DoScroll', this); + }, + + _removeMessageListeners: function _removeMessageListeners(aMessageManager) { + aMessageManager.removeMessageListener('AccessFu:Present', this); + aMessageManager.removeMessageListener('AccessFu:Input', this); + aMessageManager.removeMessageListener('AccessFu:Ready', this); + aMessageManager.removeMessageListener('AccessFu:DoScroll', this); + }, + + _handleMessageManager: function _handleMessageManager(aMessageManager) { + if (this._enabled) { + this._addMessageListeners(aMessageManager); + } + this._loadFrameScript(aMessageManager); + }, + + observe: function observe(aSubject, aTopic, aData) { + switch (aTopic) { + case 'Accessibility:Settings': + this._systemPref = JSON.parse(aData).enabled; + this._enableOrDisable(); + break; + case 'Accessibility:NextObject': + case 'Accessibility:PreviousObject': + { + let rule = aData ? + aData.substr(0, 1).toUpperCase() + aData.substr(1).toLowerCase() : + 'Simple'; + let method = aTopic.replace(/Accessibility:(\w+)Object/, 'move$1'); + this.Input.moveCursor(method, rule, 'gesture'); + break; + } + case 'Accessibility:ActivateObject': + this.Input.activateCurrent(JSON.parse(aData)); + break; + case 'Accessibility:LongPress': + this.Input.sendContextMenuMessage(); + break; + case 'Accessibility:ScrollForward': + this.Input.androidScroll('forward'); + break; + case 'Accessibility:ScrollBackward': + this.Input.androidScroll('backward'); + break; + case 'Accessibility:Focus': + this._focused = JSON.parse(aData); + if (this._focused) { + this.autoMove({ forcePresent: true, noOpIfOnScreen: true }); + } + break; + case 'Accessibility:MoveByGranularity': + this.Input.moveByGranularity(JSON.parse(aData)); + break; + case 'remote-browser-shown': + case 'inprocess-browser-shown': + { + // Ignore notifications that aren't from a BrowserOrApp + let frameLoader = aSubject.QueryInterface(Ci.nsIFrameLoader); + if (!frameLoader.ownerIsMozBrowserOrAppFrame) { + return; + } + this._handleMessageManager(frameLoader.messageManager); + break; + } + } + }, + + _handleEvent: function _handleEvent(aEvent) { + switch (aEvent.type) { + case 'TabOpen': + { + let mm = Utils.getMessageManager(aEvent.target); + this._handleMessageManager(mm); + break; + } + case 'TabClose': + { + let mm = Utils.getMessageManager(aEvent.target); + let mmIndex = this._processedMessageManagers.indexOf(mm); + if (mmIndex > -1) { + this._removeMessageListeners(mm); + this._processedMessageManagers.splice(mmIndex, 1); + } + break; + } + case 'TabSelect': + { + if (this._focused) { + // We delay this for half a second so the awesomebar could close, + // and we could use the current coordinates for the content item. + // XXX TODO figure out how to avoid magic wait here. + this.autoMove({ + delay: 500, + forcePresent: true, + noOpIfOnScreen: true, + moveMethod: 'moveFirst' }); + } + break; + } + default: + { + // A settings change, it does not have an event type + if (aEvent.settingName == SCREENREADER_SETTING) { + this._systemPref = aEvent.settingValue; + this._enableOrDisable(); + } + break; + } + } + }, + + autoMove: function autoMove(aOptions) { + let mm = Utils.getMessageManager(Utils.CurrentBrowser); + mm.sendAsyncMessage('AccessFu:AutoMove', aOptions); + }, + + announce: function announce(aAnnouncement) { + this._output(Presentation.announce(aAnnouncement), Utils.CurrentBrowser); + }, + + // So we don't enable/disable twice + _enabled: false, + + // Layerview is focused + _focused: false, + + // Keep track of message managers tha already have a 'content-script.js' + // injected. + _processedMessageManagers: [], + + /** + * Adjusts the given bounds relative to the given browser. + * @param {Rect} aJsonBounds the bounds to adjust + * @param {browser} aBrowser the browser we want the bounds relative to + * @param {bool} aToCSSPixels whether to convert to CSS pixels (as opposed to + * device pixels) + */ + adjustContentBounds: + function(aJsonBounds, aBrowser, aToCSSPixels) { + let bounds = new Rect(aJsonBounds.left, aJsonBounds.top, + aJsonBounds.right - aJsonBounds.left, + aJsonBounds.bottom - aJsonBounds.top); + let win = Utils.win; + let dpr = win.devicePixelRatio; + let offset = { left: -win.mozInnerScreenX, top: -win.mozInnerScreenY }; + + // Add the offset; the offset is in CSS pixels, so multiply the + // devicePixelRatio back in before adding to preserve unit consistency. + bounds = bounds.translate(offset.left * dpr, offset.top * dpr); + + // If we want to get to CSS pixels from device pixels, this needs to be + // further divided by the devicePixelRatio due to widget scaling. + if (aToCSSPixels) { + bounds = bounds.scale(1 / dpr, 1 / dpr); + } + + return bounds.expandToIntegers(); + } +}; + +var Output = { + brailleState: { + startOffset: 0, + endOffset: 0, + text: '', + selectionStart: 0, + selectionEnd: 0, + + init: function init(aOutput) { + if (aOutput && 'output' in aOutput) { + this.startOffset = aOutput.startOffset; + this.endOffset = aOutput.endOffset; + // We need to append a space at the end so that the routing key + // corresponding to the end of the output (i.e. the space) can be hit to + // move the caret there. + this.text = aOutput.output + ' '; + this.selectionStart = typeof aOutput.selectionStart === 'number' ? + aOutput.selectionStart : this.selectionStart; + this.selectionEnd = typeof aOutput.selectionEnd === 'number' ? + aOutput.selectionEnd : this.selectionEnd; + + return { text: this.text, + selectionStart: this.selectionStart, + selectionEnd: this.selectionEnd }; + } + + return null; + }, + + adjustText: function adjustText(aText) { + let newBraille = []; + let braille = {}; + + let prefix = this.text.substring(0, this.startOffset).trim(); + if (prefix) { + prefix += ' '; + newBraille.push(prefix); + } + + newBraille.push(aText); + + let suffix = this.text.substring(this.endOffset).trim(); + if (suffix) { + suffix = ' ' + suffix; + newBraille.push(suffix); + } + + this.startOffset = braille.startOffset = prefix.length; + this.text = braille.text = newBraille.join('') + ' '; + this.endOffset = braille.endOffset = braille.text.length - suffix.length; + braille.selectionStart = this.selectionStart; + braille.selectionEnd = this.selectionEnd; + + return braille; + }, + + adjustSelection: function adjustSelection(aSelection) { + let braille = {}; + + braille.startOffset = this.startOffset; + braille.endOffset = this.endOffset; + braille.text = this.text; + this.selectionStart = braille.selectionStart = + aSelection.selectionStart + this.startOffset; + this.selectionEnd = braille.selectionEnd = + aSelection.selectionEnd + this.startOffset; + + return braille; + } + }, + + start: function start() { + Cu.import('resource://gre/modules/Geometry.jsm'); + }, + + stop: function stop() { + if (this.highlightBox) { + let highlightBox = this.highlightBox.get(); + if (highlightBox) { + highlightBox.remove(); + } + delete this.highlightBox; + } + }, + + B2G: function B2G(aDetails) { + Utils.dispatchChromeEvent('accessibility-output', aDetails); + }, + + Visual: function Visual(aDetail, aBrowser) { + switch (aDetail.eventType) { + case 'viewport-change': + case 'vc-change': + { + let highlightBox = null; + if (!this.highlightBox) { + let doc = Utils.win.document; + // Add highlight box + highlightBox = Utils.win.document. + createElementNS('http://www.w3.org/1999/xhtml', 'div'); + let parent = doc.body || doc.documentElement; + parent.appendChild(highlightBox); + highlightBox.id = 'virtual-cursor-box'; + + // Add highlight inset for inner shadow + highlightBox.appendChild( + doc.createElementNS('http://www.w3.org/1999/xhtml', 'div')); + + this.highlightBox = Cu.getWeakReference(highlightBox); + } else { + highlightBox = this.highlightBox.get(); + } + + let padding = aDetail.padding; + let r = AccessFu.adjustContentBounds(aDetail.bounds, aBrowser, true); + + // First hide it to avoid flickering when changing the style. + highlightBox.classList.remove('show'); + highlightBox.style.top = (r.top - padding) + 'px'; + highlightBox.style.left = (r.left - padding) + 'px'; + highlightBox.style.width = (r.width + padding*2) + 'px'; + highlightBox.style.height = (r.height + padding*2) + 'px'; + highlightBox.classList.add('show'); + + break; + } + case 'tabstate-change': + { + let highlightBox = this.highlightBox ? this.highlightBox.get() : null; + if (highlightBox) { + highlightBox.classList.remove('show'); + } + break; + } + } + }, + + get androidBridge() { + delete this.androidBridge; + if (Utils.MozBuildApp === 'mobile/android') { + this.androidBridge = Services.androidBridge; + } else { + this.androidBridge = null; + } + return this.androidBridge; + }, + + Android: function Android(aDetails, aBrowser) { + const ANDROID_VIEW_TEXT_CHANGED = 0x10; + const ANDROID_VIEW_TEXT_SELECTION_CHANGED = 0x2000; + + if (!this.androidBridge) { + return; + } + + for (let androidEvent of aDetails) { + androidEvent.type = 'Accessibility:Event'; + if (androidEvent.bounds) { + androidEvent.bounds = AccessFu.adjustContentBounds( + androidEvent.bounds, aBrowser); + } + + switch(androidEvent.eventType) { + case ANDROID_VIEW_TEXT_CHANGED: + androidEvent.brailleOutput = this.brailleState.adjustText( + androidEvent.text); + break; + case ANDROID_VIEW_TEXT_SELECTION_CHANGED: + androidEvent.brailleOutput = this.brailleState.adjustSelection( + androidEvent.brailleOutput); + break; + default: + androidEvent.brailleOutput = this.brailleState.init( + androidEvent.brailleOutput); + break; + } + this.androidBridge.handleGeckoMessage(androidEvent); + } + }, + + Braille: function Braille(aDetails) { + Logger.debug('Braille output: ' + aDetails.output); + } +}; + +var Input = { + editState: {}, + + start: function start() { + // XXX: This is too disruptive on desktop for now. + // Might need to add special modifiers. + if (Utils.MozBuildApp != 'browser') { + Utils.win.document.addEventListener('keypress', this, true); + } + Utils.win.addEventListener('mozAccessFuGesture', this, true); + }, + + stop: function stop() { + if (Utils.MozBuildApp != 'browser') { + Utils.win.document.removeEventListener('keypress', this, true); + } + Utils.win.removeEventListener('mozAccessFuGesture', this, true); + }, + + handleEvent: function Input_handleEvent(aEvent) { + try { + switch (aEvent.type) { + case 'keypress': + this._handleKeypress(aEvent); + break; + case 'mozAccessFuGesture': + this._handleGesture(aEvent.detail); + break; + } + } catch (x) { + Logger.logException(x); + } + }, + + _handleGesture: function _handleGesture(aGesture) { + let gestureName = aGesture.type + aGesture.touches.length; + Logger.debug('Gesture', aGesture.type, + '(fingers: ' + aGesture.touches.length + ')'); + + switch (gestureName) { + case 'dwell1': + case 'explore1': + this.moveToPoint('Simple', aGesture.touches[0].x, + aGesture.touches[0].y); + break; + case 'doubletap1': + this.activateCurrent(); + break; + case 'doubletaphold1': + Utils.dispatchChromeEvent('accessibility-control', 'quicknav-menu'); + break; + case 'swiperight1': + this.moveCursor('moveNext', 'Simple', 'gestures'); + break; + case 'swipeleft1': + this.moveCursor('movePrevious', 'Simple', 'gesture'); + break; + case 'swipeup1': + this.moveCursor( + 'movePrevious', this.quickNavMode.current, 'gesture', true); + break; + case 'swipedown1': + this.moveCursor('moveNext', this.quickNavMode.current, 'gesture', true); + break; + case 'exploreend1': + case 'dwellend1': + this.activateCurrent(null, true); + break; + case 'swiperight2': + if (aGesture.edge) { + Utils.dispatchChromeEvent('accessibility-control', + 'edge-swipe-right'); + break; + } + this.sendScrollMessage(-1, true); + break; + case 'swipedown2': + if (aGesture.edge) { + Utils.dispatchChromeEvent('accessibility-control', 'edge-swipe-down'); + break; + } + this.sendScrollMessage(-1); + break; + case 'swipeleft2': + if (aGesture.edge) { + Utils.dispatchChromeEvent('accessibility-control', 'edge-swipe-left'); + break; + } + this.sendScrollMessage(1, true); + break; + case 'swipeup2': + if (aGesture.edge) { + Utils.dispatchChromeEvent('accessibility-control', 'edge-swipe-up'); + break; + } + this.sendScrollMessage(1); + break; + case 'explore2': + Utils.CurrentBrowser.contentWindow.scrollBy( + -aGesture.deltaX, -aGesture.deltaY); + break; + case 'swiperight3': + this.moveCursor('moveNext', this.quickNavMode.current, 'gesture'); + break; + case 'swipeleft3': + this.moveCursor('movePrevious', this.quickNavMode.current, 'gesture'); + break; + case 'swipedown3': + this.quickNavMode.next(); + AccessFu.announce('quicknav_' + this.quickNavMode.current); + break; + case 'swipeup3': + this.quickNavMode.previous(); + AccessFu.announce('quicknav_' + this.quickNavMode.current); + break; + case 'tripletap3': + Utils.dispatchChromeEvent('accessibility-control', 'toggle-shade'); + break; + case 'tap2': + Utils.dispatchChromeEvent('accessibility-control', 'toggle-pause'); + break; + } + }, + + _handleKeypress: function _handleKeypress(aEvent) { + let target = aEvent.target; + + // Ignore keys with modifiers so the content could take advantage of them. + if (aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey) { + return; + } + + switch (aEvent.keyCode) { + case 0: + // an alphanumeric key was pressed, handle it separately. + // If it was pressed with either alt or ctrl, just pass through. + // If it was pressed with meta, pass the key on without the meta. + if (this.editState.editing) { + return; + } + + let key = String.fromCharCode(aEvent.charCode); + try { + let [methodName, rule] = this.keyMap[key]; + this.moveCursor(methodName, rule, 'keyboard'); + } catch (x) { + return; + } + break; + case aEvent.DOM_VK_RIGHT: + if (this.editState.editing) { + if (!this.editState.atEnd) { + // Don't move forward if caret is not at end of entry. + // XXX: Fix for rtl + return; + } else { + target.blur(); + } + } + this.moveCursor(aEvent.shiftKey ? + 'moveLast' : 'moveNext', 'Simple', 'keyboard'); + break; + case aEvent.DOM_VK_LEFT: + if (this.editState.editing) { + if (!this.editState.atStart) { + // Don't move backward if caret is not at start of entry. + // XXX: Fix for rtl + return; + } else { + target.blur(); + } + } + this.moveCursor(aEvent.shiftKey ? + 'moveFirst' : 'movePrevious', 'Simple', 'keyboard'); + break; + case aEvent.DOM_VK_UP: + if (this.editState.multiline) { + if (!this.editState.atStart) { + // Don't blur content if caret is not at start of text area. + return; + } else { + target.blur(); + } + } + + if (Utils.MozBuildApp == 'mobile/android') { + // Return focus to native Android browser chrome. + Services.androidBridge.handleGeckoMessage( + { type: 'ToggleChrome:Focus' }); + } + break; + case aEvent.DOM_VK_RETURN: + if (this.editState.editing) { + return; + } + this.activateCurrent(); + break; + default: + return; + } + + aEvent.preventDefault(); + aEvent.stopPropagation(); + }, + + moveToPoint: function moveToPoint(aRule, aX, aY) { + // XXX: Bug 1013408 - There is no alignment between the chrome window's + // viewport size and the content viewport size in Android. This makes + // sending mouse events beyond its bounds impossible. + if (Utils.MozBuildApp === 'mobile/android') { + let mm = Utils.getMessageManager(Utils.CurrentBrowser); + mm.sendAsyncMessage('AccessFu:MoveToPoint', + {rule: aRule, x: aX, y: aY, origin: 'top'}); + } else { + let win = Utils.win; + Utils.winUtils.sendMouseEvent('mousemove', + aX - win.mozInnerScreenX, aY - win.mozInnerScreenY, 0, 0, 0); + } + }, + + moveCursor: function moveCursor(aAction, aRule, aInputType, aAdjustRange) { + let mm = Utils.getMessageManager(Utils.CurrentBrowser); + mm.sendAsyncMessage('AccessFu:MoveCursor', + { action: aAction, rule: aRule, + origin: 'top', inputType: aInputType, + adjustRange: aAdjustRange }); + }, + + androidScroll: function androidScroll(aDirection) { + let mm = Utils.getMessageManager(Utils.CurrentBrowser); + mm.sendAsyncMessage('AccessFu:AndroidScroll', + { direction: aDirection, origin: 'top' }); + }, + + moveByGranularity: function moveByGranularity(aDetails) { + const GRANULARITY_PARAGRAPH = 8; + const GRANULARITY_LINE = 4; + + if (!this.editState.editing) { + if (aDetails.granularity & (GRANULARITY_PARAGRAPH | GRANULARITY_LINE)) { + this.moveCursor('move' + aDetails.direction, 'Simple', 'gesture'); + return; + } + } else { + aDetails.atStart = this.editState.atStart; + aDetails.atEnd = this.editState.atEnd; + } + + let mm = Utils.getMessageManager(Utils.CurrentBrowser); + let type = this.editState.editing ? 'AccessFu:MoveCaret' : + 'AccessFu:MoveByGranularity'; + mm.sendAsyncMessage(type, aDetails); + }, + + activateCurrent: function activateCurrent(aData, aActivateIfKey = false) { + let mm = Utils.getMessageManager(Utils.CurrentBrowser); + let offset = aData && typeof aData.keyIndex === 'number' ? + aData.keyIndex - Output.brailleState.startOffset : -1; + + mm.sendAsyncMessage('AccessFu:Activate', + {offset: offset, activateIfKey: aActivateIfKey}); + }, + + sendContextMenuMessage: function sendContextMenuMessage() { + let mm = Utils.getMessageManager(Utils.CurrentBrowser); + mm.sendAsyncMessage('AccessFu:ContextMenu', {}); + }, + + setEditState: function setEditState(aEditState) { + Logger.debug(() => { return ['setEditState', JSON.stringify(aEditState)] }); + this.editState = aEditState; + }, + + // XXX: This is here for backwards compatability with screen reader simulator + // it should be removed when the extension is updated on amo. + scroll: function scroll(aPage, aHorizontal) { + this.sendScrollMessage(aPage, aHorizontal); + }, + + sendScrollMessage: function sendScrollMessage(aPage, aHorizontal) { + let mm = Utils.getMessageManager(Utils.CurrentBrowser); + mm.sendAsyncMessage('AccessFu:Scroll', + {page: aPage, horizontal: aHorizontal, origin: 'top'}); + }, + + doScroll: function doScroll(aDetails) { + let horizontal = aDetails.horizontal; + let page = aDetails.page; + let p = AccessFu.adjustContentBounds( + aDetails.bounds, Utils.CurrentBrowser, true).center(); + Utils.winUtils.sendWheelEvent(p.x, p.y, + horizontal ? page : 0, horizontal ? 0 : page, 0, + Utils.win.WheelEvent.DOM_DELTA_PAGE, 0, 0, 0, 0); + }, + + get keyMap() { + delete this.keyMap; + this.keyMap = { + a: ['moveNext', 'Anchor'], + A: ['movePrevious', 'Anchor'], + b: ['moveNext', 'Button'], + B: ['movePrevious', 'Button'], + c: ['moveNext', 'Combobox'], + C: ['movePrevious', 'Combobox'], + d: ['moveNext', 'Landmark'], + D: ['movePrevious', 'Landmark'], + e: ['moveNext', 'Entry'], + E: ['movePrevious', 'Entry'], + f: ['moveNext', 'FormElement'], + F: ['movePrevious', 'FormElement'], + g: ['moveNext', 'Graphic'], + G: ['movePrevious', 'Graphic'], + h: ['moveNext', 'Heading'], + H: ['movePrevious', 'Heading'], + i: ['moveNext', 'ListItem'], + I: ['movePrevious', 'ListItem'], + k: ['moveNext', 'Link'], + K: ['movePrevious', 'Link'], + l: ['moveNext', 'List'], + L: ['movePrevious', 'List'], + p: ['moveNext', 'PageTab'], + P: ['movePrevious', 'PageTab'], + r: ['moveNext', 'RadioButton'], + R: ['movePrevious', 'RadioButton'], + s: ['moveNext', 'Separator'], + S: ['movePrevious', 'Separator'], + t: ['moveNext', 'Table'], + T: ['movePrevious', 'Table'], + x: ['moveNext', 'Checkbox'], + X: ['movePrevious', 'Checkbox'] + }; + + return this.keyMap; + }, + + quickNavMode: { + get current() { + return this.modes[this._currentIndex]; + }, + + previous: function quickNavMode_previous() { + Services.prefs.setIntPref(QUICKNAV_INDEX_PREF, + this._currentIndex > 0 ? + this._currentIndex - 1 : this.modes.length - 1); + }, + + next: function quickNavMode_next() { + Services.prefs.setIntPref(QUICKNAV_INDEX_PREF, + this._currentIndex + 1 >= this.modes.length ? + 0 : this._currentIndex + 1); + }, + + updateModes: function updateModes(aModes) { + if (aModes) { + this.modes = aModes.split(','); + } else { + this.modes = []; + } + }, + + updateCurrentMode: function updateCurrentMode(aModeIndex) { + Logger.debug('Quicknav mode:', this.modes[aModeIndex]); + this._currentIndex = aModeIndex; + } + } +}; +AccessFu.Input = Input; |