accessible/src/jsat/AccessFu.jsm

Wed, 31 Dec 2014 07:16:47 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:16:47 +0100
branch
TOR_BUG_9701
changeset 3
141e0f1194b1
permissions
-rw-r--r--

Revert simplistic fix pending revisit of Mozilla integration attempt.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 'use strict';
     7 const Cc = Components.classes;
     8 const Ci = Components.interfaces;
     9 const Cu = Components.utils;
    10 const Cr = Components.results;
    12 this.EXPORTED_SYMBOLS = ['AccessFu'];
    14 Cu.import('resource://gre/modules/Services.jsm');
    16 Cu.import('resource://gre/modules/accessibility/Utils.jsm');
    18 const ACCESSFU_DISABLE = 0;
    19 const ACCESSFU_ENABLE = 1;
    20 const ACCESSFU_AUTO = 2;
    22 const SCREENREADER_SETTING = 'accessibility.screenreader';
    24 this.AccessFu = {
    25   /**
    26    * Initialize chrome-layer accessibility functionality.
    27    * If accessibility is enabled on the platform, then a special accessibility
    28    * mode is started.
    29    */
    30   attach: function attach(aWindow) {
    31     Utils.init(aWindow);
    33     try {
    34       Services.androidBridge.handleGeckoMessage(
    35           { type: 'Accessibility:Ready' });
    36       Services.obs.addObserver(this, 'Accessibility:Settings', false);
    37     } catch (x) {
    38       // Not on Android
    39       if (aWindow.navigator.mozSettings) {
    40         let lock = aWindow.navigator.mozSettings.createLock();
    41         let req = lock.get(SCREENREADER_SETTING);
    42         req.addEventListener('success', () => {
    43           this._systemPref = req.result[SCREENREADER_SETTING];
    44           this._enableOrDisable();
    45         });
    46         aWindow.navigator.mozSettings.addObserver(
    47           SCREENREADER_SETTING, this.handleEvent.bind(this));
    48       }
    49     }
    51     this._activatePref = new PrefCache(
    52       'accessibility.accessfu.activate', this._enableOrDisable.bind(this));
    54     this._enableOrDisable();
    55   },
    57   /**
    58    * Shut down chrome-layer accessibility functionality from the outside.
    59    */
    60   detach: function detach() {
    61     // Avoid disabling twice.
    62     if (this._enabled) {
    63       this._disable();
    64     }
    65     if (Utils.MozBuildApp === 'mobile/android') {
    66       Services.obs.removeObserver(this, 'Accessibility:Settings');
    67     } else if (Utils.win.navigator.mozSettings) {
    68       Utils.win.navigator.mozSettings.removeObserver(
    69         SCREENREADER_SETTING, this.handleEvent.bind(this));
    70     }
    71     delete this._activatePref;
    72     Utils.uninit();
    73   },
    75   /**
    76    * Start AccessFu mode, this primarily means controlling the virtual cursor
    77    * with arrow keys.
    78    */
    79   _enable: function _enable() {
    80     if (this._enabled)
    81       return;
    82     this._enabled = true;
    84     Cu.import('resource://gre/modules/accessibility/Utils.jsm');
    85     Cu.import('resource://gre/modules/accessibility/PointerAdapter.jsm');
    86     Cu.import('resource://gre/modules/accessibility/Presentation.jsm');
    88     Logger.info('Enabled');
    90     for each (let mm in Utils.AllMessageManagers) {
    91       this._addMessageListeners(mm);
    92       this._loadFrameScript(mm);
    93     }
    95     // Add stylesheet
    96     let stylesheetURL = 'chrome://global/content/accessibility/AccessFu.css';
    97     let stylesheet = Utils.win.document.createProcessingInstruction(
    98       'xml-stylesheet', 'href="' + stylesheetURL + '" type="text/css"');
    99     Utils.win.document.insertBefore(stylesheet, Utils.win.document.firstChild);
   100     this.stylesheet = Cu.getWeakReference(stylesheet);
   103     // Populate quicknav modes
   104     this._quicknavModesPref =
   105       new PrefCache(
   106         'accessibility.accessfu.quicknav_modes',
   107         (aName, aValue) => {
   108           this.Input.quickNavMode.updateModes(aValue);
   109         }, true);
   111     // Check for output notification
   112     this._notifyOutputPref =
   113       new PrefCache('accessibility.accessfu.notify_output');
   116     this.Input.start();
   117     Output.start();
   118     PointerAdapter.start();
   120     Services.obs.addObserver(this, 'remote-browser-shown', false);
   121     Services.obs.addObserver(this, 'inprocess-browser-shown', false);
   122     Services.obs.addObserver(this, 'Accessibility:NextObject', false);
   123     Services.obs.addObserver(this, 'Accessibility:PreviousObject', false);
   124     Services.obs.addObserver(this, 'Accessibility:Focus', false);
   125     Services.obs.addObserver(this, 'Accessibility:ActivateObject', false);
   126     Services.obs.addObserver(this, 'Accessibility:LongPress', false);
   127     Services.obs.addObserver(this, 'Accessibility:MoveByGranularity', false);
   128     Utils.win.addEventListener('TabOpen', this);
   129     Utils.win.addEventListener('TabClose', this);
   130     Utils.win.addEventListener('TabSelect', this);
   132     if (this.readyCallback) {
   133       this.readyCallback();
   134       delete this.readyCallback;
   135     }
   137     if (Utils.MozBuildApp !== 'mobile/android') {
   138       this.announce(
   139         Utils.stringBundle.GetStringFromName('screenReaderStarted'));
   140     }
   141   },
   143   /**
   144    * Disable AccessFu and return to default interaction mode.
   145    */
   146   _disable: function _disable() {
   147     if (!this._enabled)
   148       return;
   150     this._enabled = false;
   152     Logger.info('Disabled');
   154     Utils.win.document.removeChild(this.stylesheet.get());
   156     if (Utils.MozBuildApp !== 'mobile/android') {
   157       this.announce(
   158         Utils.stringBundle.GetStringFromName('screenReaderStopped'));
   159     }
   161     for each (let mm in Utils.AllMessageManagers) {
   162       mm.sendAsyncMessage('AccessFu:Stop');
   163       this._removeMessageListeners(mm);
   164     }
   166     this.Input.stop();
   167     Output.stop();
   168     PointerAdapter.stop();
   170     Utils.win.removeEventListener('TabOpen', this);
   171     Utils.win.removeEventListener('TabClose', this);
   172     Utils.win.removeEventListener('TabSelect', this);
   174     Services.obs.removeObserver(this, 'remote-browser-shown');
   175     Services.obs.removeObserver(this, 'inprocess-browser-shown');
   176     Services.obs.removeObserver(this, 'Accessibility:NextObject');
   177     Services.obs.removeObserver(this, 'Accessibility:PreviousObject');
   178     Services.obs.removeObserver(this, 'Accessibility:Focus');
   179     Services.obs.removeObserver(this, 'Accessibility:ActivateObject');
   180     Services.obs.removeObserver(this, 'Accessibility:LongPress');
   181     Services.obs.removeObserver(this, 'Accessibility:MoveByGranularity');
   183     delete this._quicknavModesPref;
   184     delete this._notifyOutputPref;
   186     if (this.doneCallback) {
   187       this.doneCallback();
   188       delete this.doneCallback;
   189     }
   190   },
   192   _enableOrDisable: function _enableOrDisable() {
   193     try {
   194       if (!this._activatePref) {
   195         return;
   196       }
   197       let activatePref = this._activatePref.value;
   198       if (activatePref == ACCESSFU_ENABLE ||
   199           this._systemPref && activatePref == ACCESSFU_AUTO)
   200         this._enable();
   201       else
   202         this._disable();
   203     } catch (x) {
   204       dump('Error ' + x.message + ' ' + x.fileName + ':' + x.lineNumber);
   205     }
   206   },
   208   receiveMessage: function receiveMessage(aMessage) {
   209     Logger.debug(() => {
   210       return ['Recieved', aMessage.name, JSON.stringify(aMessage.json)];
   211     });
   213     switch (aMessage.name) {
   214       case 'AccessFu:Ready':
   215         let mm = Utils.getMessageManager(aMessage.target);
   216         if (this._enabled) {
   217           mm.sendAsyncMessage('AccessFu:Start',
   218                               {method: 'start', buildApp: Utils.MozBuildApp});
   219         }
   220         break;
   221       case 'AccessFu:Present':
   222         this._output(aMessage.json, aMessage.target);
   223         break;
   224       case 'AccessFu:Input':
   225         this.Input.setEditState(aMessage.json);
   226         break;
   227       case 'AccessFu:ActivateContextMenu':
   228         this.Input.activateContextMenu(aMessage.json);
   229         break;
   230       case 'AccessFu:DoScroll':
   231         this.Input.doScroll(aMessage.json);
   232         break;
   233     }
   234   },
   236   _output: function _output(aPresentationData, aBrowser) {
   237     for each (let presenter in aPresentationData) {
   238       if (!presenter)
   239         continue;
   241       try {
   242         Output[presenter.type](presenter.details, aBrowser);
   243       } catch (x) {
   244         Logger.logException(x);
   245       }
   246     }
   248     if (this._notifyOutputPref.value) {
   249       Services.obs.notifyObservers(null, 'accessfu-output',
   250                                    JSON.stringify(aPresentationData));
   251     }
   252   },
   254   _loadFrameScript: function _loadFrameScript(aMessageManager) {
   255     if (this._processedMessageManagers.indexOf(aMessageManager) < 0) {
   256       aMessageManager.loadFrameScript(
   257         'chrome://global/content/accessibility/content-script.js', true);
   258       this._processedMessageManagers.push(aMessageManager);
   259     } else if (this._enabled) {
   260       // If the content-script is already loaded and AccessFu is enabled,
   261       // send an AccessFu:Start message.
   262       aMessageManager.sendAsyncMessage('AccessFu:Start',
   263         {method: 'start', buildApp: Utils.MozBuildApp});
   264     }
   265   },
   267   _addMessageListeners: function _addMessageListeners(aMessageManager) {
   268     aMessageManager.addMessageListener('AccessFu:Present', this);
   269     aMessageManager.addMessageListener('AccessFu:Input', this);
   270     aMessageManager.addMessageListener('AccessFu:Ready', this);
   271     aMessageManager.addMessageListener('AccessFu:ActivateContextMenu', this);
   272     aMessageManager.addMessageListener('AccessFu:DoScroll', this);
   273   },
   275   _removeMessageListeners: function _removeMessageListeners(aMessageManager) {
   276     aMessageManager.removeMessageListener('AccessFu:Present', this);
   277     aMessageManager.removeMessageListener('AccessFu:Input', this);
   278     aMessageManager.removeMessageListener('AccessFu:Ready', this);
   279     aMessageManager.removeMessageListener('AccessFu:ActivateContextMenu', this);
   280     aMessageManager.removeMessageListener('AccessFu:DoScroll', this);
   281   },
   283   _handleMessageManager: function _handleMessageManager(aMessageManager) {
   284     if (this._enabled) {
   285       this._addMessageListeners(aMessageManager);
   286     }
   287     this._loadFrameScript(aMessageManager);
   288   },
   290   observe: function observe(aSubject, aTopic, aData) {
   291     switch (aTopic) {
   292       case 'Accessibility:Settings':
   293         this._systemPref = JSON.parse(aData).enabled;
   294         this._enableOrDisable();
   295         break;
   296       case 'Accessibility:NextObject':
   297         this.Input.moveCursor('moveNext', 'Simple', 'gesture');
   298         break;
   299       case 'Accessibility:PreviousObject':
   300         this.Input.moveCursor('movePrevious', 'Simple', 'gesture');
   301         break;
   302       case 'Accessibility:ActivateObject':
   303         this.Input.activateCurrent(JSON.parse(aData));
   304         break;
   305       case 'Accessibility:LongPress':
   306         this.Input.sendContextMenuMessage();
   307         break;
   308       case 'Accessibility:Focus':
   309         this._focused = JSON.parse(aData);
   310         if (this._focused) {
   311           this.autoMove({ forcePresent: true, noOpIfOnScreen: true });
   312         }
   313         break;
   314       case 'Accessibility:MoveByGranularity':
   315         this.Input.moveByGranularity(JSON.parse(aData));
   316         break;
   317       case 'remote-browser-shown':
   318       case 'inprocess-browser-shown':
   319       {
   320         // Ignore notifications that aren't from a BrowserOrApp
   321         let frameLoader = aSubject.QueryInterface(Ci.nsIFrameLoader);
   322         if (!frameLoader.ownerIsBrowserOrAppFrame) {
   323           return;
   324         }
   325         this._handleMessageManager(frameLoader.messageManager);
   326         break;
   327       }
   328     }
   329   },
   331   handleEvent: function handleEvent(aEvent) {
   332     switch (aEvent.type) {
   333       case 'TabOpen':
   334       {
   335         let mm = Utils.getMessageManager(aEvent.target);
   336         this._handleMessageManager(mm);
   337         break;
   338       }
   339       case 'TabClose':
   340       {
   341         let mm = Utils.getMessageManager(aEvent.target);
   342         let mmIndex = this._processedMessageManagers.indexOf(mm);
   343         if (mmIndex > -1) {
   344           this._removeMessageListeners(mm);
   345           this._processedMessageManagers.splice(mmIndex, 1);
   346         }
   347         break;
   348       }
   349       case 'TabSelect':
   350       {
   351         if (this._focused) {
   352           // We delay this for half a second so the awesomebar could close,
   353           // and we could use the current coordinates for the content item.
   354           // XXX TODO figure out how to avoid magic wait here.
   355 	  this.autoMove({
   356 	    delay: 500,
   357 	    forcePresent: true,
   358 	    noOpIfOnScreen: true,
   359 	    moveMethod: 'moveFirst' });
   360         }
   361         break;
   362       }
   363       default:
   364       {
   365         // A settings change, it does not have an event type
   366         if (aEvent.settingName == SCREENREADER_SETTING) {
   367           this._systemPref = aEvent.settingValue;
   368           this._enableOrDisable();
   369         }
   370         break;
   371       }
   372     }
   373   },
   375   autoMove: function autoMove(aOptions) {
   376     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
   377     mm.sendAsyncMessage('AccessFu:AutoMove', aOptions);
   378   },
   380   announce: function announce(aAnnouncement) {
   381     this._output(Presentation.announce(aAnnouncement), Utils.CurrentBrowser);
   382   },
   384   // So we don't enable/disable twice
   385   _enabled: false,
   387   // Layerview is focused
   388   _focused: false,
   390   // Keep track of message managers tha already have a 'content-script.js'
   391   // injected.
   392   _processedMessageManagers: [],
   394   /**
   395    * Adjusts the given bounds relative to the given browser. Converts from screen
   396    * or device pixels to either device or CSS pixels.
   397    * @param {Rect} aJsonBounds the bounds to adjust
   398    * @param {browser} aBrowser the browser we want the bounds relative to
   399    * @param {bool} aToCSSPixels whether to convert to CSS pixels (as opposed to
   400    *               device pixels)
   401    * @param {bool} aFromDevicePixels whether to convert from device pixels (as
   402    *               opposed to screen pixels)
   403    */
   404   adjustContentBounds: function(aJsonBounds, aBrowser, aToCSSPixels, aFromDevicePixels) {
   405     let bounds = new Rect(aJsonBounds.left, aJsonBounds.top,
   406                           aJsonBounds.right - aJsonBounds.left,
   407                           aJsonBounds.bottom - aJsonBounds.top);
   408     let win = Utils.win;
   409     let dpr = win.devicePixelRatio;
   410     let vp = Utils.getViewport(win);
   411     let offset = { left: -win.mozInnerScreenX, top: -win.mozInnerScreenY };
   413     if (!aBrowser.contentWindow) {
   414       // OOP browser, add offset of browser.
   415       // The offset of the browser element in relation to its parent window.
   416       let clientRect = aBrowser.getBoundingClientRect();
   417       let win = aBrowser.ownerDocument.defaultView;
   418       offset.left += clientRect.left + win.mozInnerScreenX;
   419       offset.top += clientRect.top + win.mozInnerScreenY;
   420     }
   422     // Here we scale from screen pixels to layout device pixels by dividing by
   423     // the resolution (caused by pinch-zooming). The resolution is the viewport
   424     // zoom divided by the devicePixelRatio. If there's no viewport, then we're
   425     // on a platform without pinch-zooming and we can just ignore this.
   426     if (!aFromDevicePixels && vp) {
   427       bounds = bounds.scale(vp.zoom / dpr, vp.zoom / dpr);
   428     }
   430     // Add the offset; the offset is in CSS pixels, so multiply the
   431     // devicePixelRatio back in before adding to preserve unit consistency.
   432     bounds = bounds.translate(offset.left * dpr, offset.top * dpr);
   434     // If we want to get to CSS pixels from device pixels, this needs to be
   435     // further divided by the devicePixelRatio due to widget scaling.
   436     if (aToCSSPixels) {
   437       bounds = bounds.scale(1 / dpr, 1 / dpr);
   438     }
   440     return bounds.expandToIntegers();
   441   }
   442 };
   444 var Output = {
   445   brailleState: {
   446     startOffset: 0,
   447     endOffset: 0,
   448     text: '',
   449     selectionStart: 0,
   450     selectionEnd: 0,
   452     init: function init(aOutput) {
   453       if (aOutput && 'output' in aOutput) {
   454         this.startOffset = aOutput.startOffset;
   455         this.endOffset = aOutput.endOffset;
   456         // We need to append a space at the end so that the routing key corresponding
   457         // to the end of the output (i.e. the space) can be hit to move the caret there.
   458         this.text = aOutput.output + ' ';
   459         this.selectionStart = typeof aOutput.selectionStart === 'number' ?
   460                               aOutput.selectionStart : this.selectionStart;
   461         this.selectionEnd = typeof aOutput.selectionEnd === 'number' ?
   462                             aOutput.selectionEnd : this.selectionEnd;
   464         return { text: this.text,
   465                  selectionStart: this.selectionStart,
   466                  selectionEnd: this.selectionEnd };
   467       }
   469       return null;
   470     },
   472     adjustText: function adjustText(aText) {
   473       let newBraille = [];
   474       let braille = {};
   476       let prefix = this.text.substring(0, this.startOffset).trim();
   477       if (prefix) {
   478         prefix += ' ';
   479         newBraille.push(prefix);
   480       }
   482       newBraille.push(aText);
   484       let suffix = this.text.substring(this.endOffset).trim();
   485       if (suffix) {
   486         suffix = ' ' + suffix;
   487         newBraille.push(suffix);
   488       }
   490       this.startOffset = braille.startOffset = prefix.length;
   491       this.text = braille.text = newBraille.join('') + ' ';
   492       this.endOffset = braille.endOffset = braille.text.length - suffix.length;
   493       braille.selectionStart = this.selectionStart;
   494       braille.selectionEnd = this.selectionEnd;
   496       return braille;
   497     },
   499     adjustSelection: function adjustSelection(aSelection) {
   500       let braille = {};
   502       braille.startOffset = this.startOffset;
   503       braille.endOffset = this.endOffset;
   504       braille.text = this.text;
   505       this.selectionStart = braille.selectionStart = aSelection.selectionStart + this.startOffset;
   506       this.selectionEnd = braille.selectionEnd = aSelection.selectionEnd + this.startOffset;
   508       return braille;
   509     }
   510   },
   512   speechHelper: {
   513     EARCONS: ['virtual_cursor_move.ogg',
   514               'virtual_cursor_key.ogg',
   515               'clicked.ogg'],
   517     earconBuffers: {},
   519     inited: false,
   521     webspeechEnabled: false,
   523     deferredOutputs: [],
   525     init: function init() {
   526       let window = Utils.win;
   527       this.webspeechEnabled = !!window.speechSynthesis &&
   528         !!window.SpeechSynthesisUtterance;
   530       let settingsToGet = 2;
   531       let settingsCallback = (aName, aSetting) => {
   532         if (--settingsToGet > 0) {
   533           return;
   534         }
   536         this.inited = true;
   538         for (let actions of this.deferredOutputs) {
   539           this.output(actions);
   540         }
   541       };
   543       this._volumeSetting = new SettingCache(
   544         'accessibility.screenreader-volume', settingsCallback,
   545         { defaultValue: 1, callbackNow: true, callbackOnce: true });
   546       this._rateSetting = new SettingCache(
   547         'accessibility.screenreader-rate', settingsCallback,
   548         { defaultValue: 0, callbackNow: true, callbackOnce: true });
   550       for (let earcon of this.EARCONS) {
   551         let earconName = /(^.*)\..*$/.exec(earcon)[1];
   552         this.earconBuffers[earconName] = new WeakMap();
   553         this.earconBuffers[earconName].set(
   554           window, new window.Audio('chrome://global/content/accessibility/' + earcon));
   555       }
   556     },
   558     uninit: function uninit() {
   559       if (this.inited) {
   560         delete this._volumeSetting;
   561         delete this._rateSetting;
   562       }
   563       this.inited = false;
   564     },
   566     output: function output(aActions) {
   567       if (!this.inited) {
   568         this.deferredOutputs.push(aActions);
   569         return;
   570       }
   572       for (let action of aActions) {
   573         let window = Utils.win;
   574         Logger.debug('tts.' + action.method, '"' + action.data + '"',
   575                      JSON.stringify(action.options));
   577         if (!action.options.enqueue && this.webspeechEnabled) {
   578           window.speechSynthesis.cancel();
   579         }
   581         if (action.method === 'speak' && this.webspeechEnabled) {
   582           let utterance = new window.SpeechSynthesisUtterance(action.data);
   583           let requestedRate = this._rateSetting.value;
   584           utterance.volume = this._volumeSetting.value;
   585           utterance.rate = requestedRate >= 0 ?
   586             requestedRate + 1 : 1 / (Math.abs(requestedRate) + 1);
   587           window.speechSynthesis.speak(utterance);
   588         } else if (action.method === 'playEarcon') {
   589           let audioBufferWeakMap = this.earconBuffers[action.data];
   590           if (audioBufferWeakMap) {
   591             let node = audioBufferWeakMap.get(window).cloneNode(false);
   592             node.volume = this._volumeSetting.value;
   593             node.play();
   594           }
   595         }
   596       }
   597     }
   598   },
   600   start: function start() {
   601     Cu.import('resource://gre/modules/Geometry.jsm');
   602     this.speechHelper.init();
   603   },
   605   stop: function stop() {
   606     if (this.highlightBox) {
   607       Utils.win.document.documentElement.removeChild(this.highlightBox.get());
   608       delete this.highlightBox;
   609     }
   611     if (this.announceBox) {
   612       Utils.win.document.documentElement.removeChild(this.announceBox.get());
   613       delete this.announceBox;
   614     }
   616     this.speechHelper.uninit();
   617   },
   619   Speech: function Speech(aDetails, aBrowser) {
   620     this.speechHelper.output(aDetails.actions);
   621   },
   623   Visual: function Visual(aDetails, aBrowser) {
   624     switch (aDetails.method) {
   625       case 'showBounds':
   626       {
   627         let highlightBox = null;
   628         if (!this.highlightBox) {
   629           // Add highlight box
   630           highlightBox = Utils.win.document.
   631             createElementNS('http://www.w3.org/1999/xhtml', 'div');
   632           Utils.win.document.documentElement.appendChild(highlightBox);
   633           highlightBox.id = 'virtual-cursor-box';
   635           // Add highlight inset for inner shadow
   636           let inset = Utils.win.document.
   637             createElementNS('http://www.w3.org/1999/xhtml', 'div');
   638           inset.id = 'virtual-cursor-inset';
   640           highlightBox.appendChild(inset);
   641           this.highlightBox = Cu.getWeakReference(highlightBox);
   642         } else {
   643           highlightBox = this.highlightBox.get();
   644         }
   646         let padding = aDetails.padding;
   647         let r = AccessFu.adjustContentBounds(aDetails.bounds, aBrowser, true);
   649         // First hide it to avoid flickering when changing the style.
   650         highlightBox.style.display = 'none';
   651         highlightBox.style.top = (r.top - padding) + 'px';
   652         highlightBox.style.left = (r.left - padding) + 'px';
   653         highlightBox.style.width = (r.width + padding*2) + 'px';
   654         highlightBox.style.height = (r.height + padding*2) + 'px';
   655         highlightBox.style.display = 'block';
   657         break;
   658       }
   659       case 'hideBounds':
   660       {
   661         let highlightBox = this.highlightBox ? this.highlightBox.get() : null;
   662         if (highlightBox)
   663           highlightBox.style.display = 'none';
   664         break;
   665       }
   666       case 'showAnnouncement':
   667       {
   668         let announceBox = this.announceBox ? this.announceBox.get() : null;
   669         if (!announceBox) {
   670           announceBox = Utils.win.document.
   671             createElementNS('http://www.w3.org/1999/xhtml', 'div');
   672           announceBox.id = 'announce-box';
   673           Utils.win.document.documentElement.appendChild(announceBox);
   674           this.announceBox = Cu.getWeakReference(announceBox);
   675         }
   677         announceBox.innerHTML = '<div>' + aDetails.text + '</div>';
   678         announceBox.classList.add('showing');
   680         if (this._announceHideTimeout)
   681           Utils.win.clearTimeout(this._announceHideTimeout);
   683         if (aDetails.duration > 0)
   684           this._announceHideTimeout = Utils.win.setTimeout(
   685             function () {
   686               announceBox.classList.remove('showing');
   687               this._announceHideTimeout = 0;
   688             }.bind(this), aDetails.duration);
   689         break;
   690       }
   691       case 'hideAnnouncement':
   692       {
   693         let announceBox = this.announceBox ? this.announceBox.get() : null;
   694         if (announceBox)
   695           announceBox.classList.remove('showing');
   696         break;
   697       }
   698     }
   699   },
   701   get androidBridge() {
   702     delete this.androidBridge;
   703     if (Utils.MozBuildApp === 'mobile/android') {
   704       this.androidBridge = Services.androidBridge;
   705     } else {
   706       this.androidBridge = null;
   707     }
   708     return this.androidBridge;
   709   },
   711   Android: function Android(aDetails, aBrowser) {
   712     const ANDROID_VIEW_TEXT_CHANGED = 0x10;
   713     const ANDROID_VIEW_TEXT_SELECTION_CHANGED = 0x2000;
   715     if (!this.androidBridge) {
   716       return;
   717     }
   719     for each (let androidEvent in aDetails) {
   720       androidEvent.type = 'Accessibility:Event';
   721       if (androidEvent.bounds)
   722         androidEvent.bounds = AccessFu.adjustContentBounds(androidEvent.bounds, aBrowser);
   724       switch(androidEvent.eventType) {
   725         case ANDROID_VIEW_TEXT_CHANGED:
   726           androidEvent.brailleOutput = this.brailleState.adjustText(androidEvent.text);
   727           break;
   728         case ANDROID_VIEW_TEXT_SELECTION_CHANGED:
   729           androidEvent.brailleOutput = this.brailleState.adjustSelection(androidEvent.brailleOutput);
   730           break;
   731         default:
   732           androidEvent.brailleOutput = this.brailleState.init(androidEvent.brailleOutput);
   733           break;
   734       }
   735       this.androidBridge.handleGeckoMessage(androidEvent);
   736     }
   737   },
   739   Haptic: function Haptic(aDetails, aBrowser) {
   740     Utils.win.navigator.vibrate(aDetails.pattern);
   741   },
   743   Braille: function Braille(aDetails, aBrowser) {
   744     Logger.debug('Braille output: ' + aDetails.text);
   745   }
   746 };
   748 var Input = {
   749   editState: {},
   751   start: function start() {
   752     // XXX: This is too disruptive on desktop for now.
   753     // Might need to add special modifiers.
   754     if (Utils.MozBuildApp != 'browser') {
   755       Utils.win.document.addEventListener('keypress', this, true);
   756     }
   757     Utils.win.addEventListener('mozAccessFuGesture', this, true);
   758   },
   760   stop: function stop() {
   761     if (Utils.MozBuildApp != 'browser') {
   762       Utils.win.document.removeEventListener('keypress', this, true);
   763     }
   764     Utils.win.removeEventListener('mozAccessFuGesture', this, true);
   765   },
   767   handleEvent: function Input_handleEvent(aEvent) {
   768     try {
   769       switch (aEvent.type) {
   770       case 'keypress':
   771         this._handleKeypress(aEvent);
   772         break;
   773       case 'mozAccessFuGesture':
   774         this._handleGesture(aEvent.detail);
   775         break;
   776       }
   777     } catch (x) {
   778       Logger.logException(x);
   779     }
   780   },
   782   _handleGesture: function _handleGesture(aGesture) {
   783     let gestureName = aGesture.type + aGesture.touches.length;
   784     Logger.debug('Gesture', aGesture.type,
   785                  '(fingers: ' + aGesture.touches.length + ')');
   787     switch (gestureName) {
   788       case 'dwell1':
   789       case 'explore1':
   790         this.moveToPoint('Simple', aGesture.touches[0].x,
   791           aGesture.touches[0].y);
   792         break;
   793       case 'doubletap1':
   794         this.activateCurrent();
   795         break;
   796       case 'doubletaphold1':
   797         this.sendContextMenuMessage();
   798         break;
   799       case 'swiperight1':
   800         this.moveCursor('moveNext', 'Simple', 'gestures');
   801         break;
   802       case 'swipeleft1':
   803         this.moveCursor('movePrevious', 'Simple', 'gesture');
   804         break;
   805       case 'swipeup1':
   806         this.contextAction('backward');
   807         break;
   808       case 'swipedown1':
   809         this.contextAction('forward');
   810         break;
   811       case 'exploreend1':
   812       case 'dwellend1':
   813         this.activateCurrent(null, true);
   814         break;
   815       case 'swiperight2':
   816         this.sendScrollMessage(-1, true);
   817         break;
   818       case 'swipedown2':
   819         this.sendScrollMessage(-1);
   820         break;
   821       case 'swipeleft2':
   822         this.sendScrollMessage(1, true);
   823         break;
   824       case 'swipeup2':
   825         this.sendScrollMessage(1);
   826         break;
   827       case 'explore2':
   828         Utils.CurrentBrowser.contentWindow.scrollBy(
   829           -aGesture.deltaX, -aGesture.deltaY);
   830         break;
   831       case 'swiperight3':
   832         this.moveCursor('moveNext', this.quickNavMode.current, 'gesture');
   833         break;
   834       case 'swipeleft3':
   835         this.moveCursor('movePrevious', this.quickNavMode.current, 'gesture');
   836         break;
   837       case 'swipedown3':
   838         this.quickNavMode.next();
   839         AccessFu.announce('quicknav_' + this.quickNavMode.current);
   840         break;
   841       case 'swipeup3':
   842         this.quickNavMode.previous();
   843         AccessFu.announce('quicknav_' + this.quickNavMode.current);
   844         break;
   845     }
   846   },
   848   _handleKeypress: function _handleKeypress(aEvent) {
   849     let target = aEvent.target;
   851     // Ignore keys with modifiers so the content could take advantage of them.
   852     if (aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey)
   853       return;
   855     switch (aEvent.keyCode) {
   856       case 0:
   857         // an alphanumeric key was pressed, handle it separately.
   858         // If it was pressed with either alt or ctrl, just pass through.
   859         // If it was pressed with meta, pass the key on without the meta.
   860         if (this.editState.editing)
   861           return;
   863         let key = String.fromCharCode(aEvent.charCode);
   864         try {
   865           let [methodName, rule] = this.keyMap[key];
   866           this.moveCursor(methodName, rule, 'keyboard');
   867         } catch (x) {
   868           return;
   869         }
   870         break;
   871       case aEvent.DOM_VK_RIGHT:
   872         if (this.editState.editing) {
   873           if (!this.editState.atEnd)
   874             // Don't move forward if caret is not at end of entry.
   875             // XXX: Fix for rtl
   876             return;
   877           else
   878             target.blur();
   879         }
   880         this.moveCursor(aEvent.shiftKey ? 'moveLast' : 'moveNext', 'Simple', 'keyboard');
   881         break;
   882       case aEvent.DOM_VK_LEFT:
   883         if (this.editState.editing) {
   884           if (!this.editState.atStart)
   885             // Don't move backward if caret is not at start of entry.
   886             // XXX: Fix for rtl
   887             return;
   888           else
   889             target.blur();
   890         }
   891         this.moveCursor(aEvent.shiftKey ? 'moveFirst' : 'movePrevious', 'Simple', 'keyboard');
   892         break;
   893       case aEvent.DOM_VK_UP:
   894         if (this.editState.multiline) {
   895           if (!this.editState.atStart)
   896             // Don't blur content if caret is not at start of text area.
   897             return;
   898           else
   899             target.blur();
   900         }
   902         if (Utils.MozBuildApp == 'mobile/android')
   903           // Return focus to native Android browser chrome.
   904           Services.androidBridge.handleGeckoMessage(
   905               { type: 'ToggleChrome:Focus' });
   906         break;
   907       case aEvent.DOM_VK_RETURN:
   908         if (this.editState.editing)
   909           return;
   910         this.activateCurrent();
   911         break;
   912     default:
   913       return;
   914     }
   916     aEvent.preventDefault();
   917     aEvent.stopPropagation();
   918   },
   920   moveToPoint: function moveToPoint(aRule, aX, aY) {
   921     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
   922     mm.sendAsyncMessage('AccessFu:MoveToPoint', {rule: aRule,
   923                                                  x: aX, y: aY,
   924                                                  origin: 'top'});
   925   },
   927   moveCursor: function moveCursor(aAction, aRule, aInputType) {
   928     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
   929     mm.sendAsyncMessage('AccessFu:MoveCursor',
   930                         {action: aAction, rule: aRule,
   931                          origin: 'top', inputType: aInputType});
   932   },
   934   contextAction: function contextAction(aDirection) {
   935     // XXX: For now, the only supported context action is adjusting a range.
   936     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
   937     mm.sendAsyncMessage('AccessFu:AdjustRange', {direction: aDirection});
   938   },
   940   moveByGranularity: function moveByGranularity(aDetails) {
   941     const MOVEMENT_GRANULARITY_PARAGRAPH = 8;
   943     if (!this.editState.editing) {
   944       if (aDetails.granularity === MOVEMENT_GRANULARITY_PARAGRAPH) {
   945         this.moveCursor('move' + aDetails.direction, 'Paragraph', 'gesture');
   946         return;
   947       }
   948     } else {
   949       aDetails.atStart = this.editState.atStart;
   950       aDetails.atEnd = this.editState.atEnd;
   951     }
   953     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
   954     let type = this.editState.editing ? 'AccessFu:MoveCaret' :
   955                                         'AccessFu:MoveByGranularity';
   956     mm.sendAsyncMessage(type, aDetails);
   957   },
   959   activateCurrent: function activateCurrent(aData, aActivateIfKey = false) {
   960     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
   961     let offset = aData && typeof aData.keyIndex === 'number' ?
   962                  aData.keyIndex - Output.brailleState.startOffset : -1;
   964     mm.sendAsyncMessage('AccessFu:Activate',
   965                         {offset: offset, activateIfKey: aActivateIfKey});
   966   },
   968   sendContextMenuMessage: function sendContextMenuMessage() {
   969     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
   970     mm.sendAsyncMessage('AccessFu:ContextMenu', {});
   971   },
   973   activateContextMenu: function activateContextMenu(aDetails) {
   974     if (Utils.MozBuildApp === 'mobile/android') {
   975       let p = AccessFu.adjustContentBounds(aDetails.bounds, Utils.CurrentBrowser,
   976                                            true, true).center();
   977       Services.obs.notifyObservers(null, 'Gesture:LongPress',
   978                                    JSON.stringify({x: p.x, y: p.y}));
   979     }
   980   },
   982   setEditState: function setEditState(aEditState) {
   983     this.editState = aEditState;
   984   },
   986   // XXX: This is here for backwards compatability with screen reader simulator
   987   // it should be removed when the extension is updated on amo.
   988   scroll: function scroll(aPage, aHorizontal) {
   989     this.sendScrollMessage(aPage, aHorizontal);
   990   },
   992   sendScrollMessage: function sendScrollMessage(aPage, aHorizontal) {
   993     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
   994     mm.sendAsyncMessage('AccessFu:Scroll', {page: aPage, horizontal: aHorizontal, origin: 'top'});
   995   },
   997   doScroll: function doScroll(aDetails) {
   998     let horizontal = aDetails.horizontal;
   999     let page = aDetails.page;
  1000     let p = AccessFu.adjustContentBounds(aDetails.bounds, Utils.CurrentBrowser,
  1001                                          true, true).center();
  1002     let wu = Utils.win.QueryInterface(Ci.nsIInterfaceRequestor).
  1003       getInterface(Ci.nsIDOMWindowUtils);
  1004     wu.sendWheelEvent(p.x, p.y,
  1005                       horizontal ? page : 0, horizontal ? 0 : page, 0,
  1006                       Utils.win.WheelEvent.DOM_DELTA_PAGE, 0, 0, 0, 0);
  1007   },
  1009   get keyMap() {
  1010     delete this.keyMap;
  1011     this.keyMap = {
  1012       a: ['moveNext', 'Anchor'],
  1013       A: ['movePrevious', 'Anchor'],
  1014       b: ['moveNext', 'Button'],
  1015       B: ['movePrevious', 'Button'],
  1016       c: ['moveNext', 'Combobox'],
  1017       C: ['movePrevious', 'Combobox'],
  1018       d: ['moveNext', 'Landmark'],
  1019       D: ['movePrevious', 'Landmark'],
  1020       e: ['moveNext', 'Entry'],
  1021       E: ['movePrevious', 'Entry'],
  1022       f: ['moveNext', 'FormElement'],
  1023       F: ['movePrevious', 'FormElement'],
  1024       g: ['moveNext', 'Graphic'],
  1025       G: ['movePrevious', 'Graphic'],
  1026       h: ['moveNext', 'Heading'],
  1027       H: ['movePrevious', 'Heading'],
  1028       i: ['moveNext', 'ListItem'],
  1029       I: ['movePrevious', 'ListItem'],
  1030       k: ['moveNext', 'Link'],
  1031       K: ['movePrevious', 'Link'],
  1032       l: ['moveNext', 'List'],
  1033       L: ['movePrevious', 'List'],
  1034       p: ['moveNext', 'PageTab'],
  1035       P: ['movePrevious', 'PageTab'],
  1036       r: ['moveNext', 'RadioButton'],
  1037       R: ['movePrevious', 'RadioButton'],
  1038       s: ['moveNext', 'Separator'],
  1039       S: ['movePrevious', 'Separator'],
  1040       t: ['moveNext', 'Table'],
  1041       T: ['movePrevious', 'Table'],
  1042       x: ['moveNext', 'Checkbox'],
  1043       X: ['movePrevious', 'Checkbox']
  1044     };
  1046     return this.keyMap;
  1047   },
  1049   quickNavMode: {
  1050     get current() {
  1051       return this.modes[this._currentIndex];
  1052     },
  1054     previous: function quickNavMode_previous() {
  1055       if (--this._currentIndex < 0)
  1056         this._currentIndex = this.modes.length - 1;
  1057     },
  1059     next: function quickNavMode_next() {
  1060       if (++this._currentIndex >= this.modes.length)
  1061         this._currentIndex = 0;
  1062     },
  1064     updateModes: function updateModes(aModes) {
  1065       if (aModes) {
  1066         this.modes = aModes.split(',');
  1067       } else {
  1068         this.modes = [];
  1070     },
  1072     _currentIndex: -1
  1074 };
  1075 AccessFu.Input = Input;

mercurial