Bug 1011886 - [AccessFu] Introduce key echo by character, word, and character and word. r=eeejay
authorMax Li <maxli@maxli.ca>
Fri, 31 Oct 2014 08:48:21 -0700
changeset 237725 b5a7134e728ced2c2314726e65414ddf68c90a89
parent 237724 92fef8eaceea3ed166faa080a790d577fe7eb160
child 237726 8cecdc9616a4e56e184ba304ba75b981cc10b8ed
push id4311
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 19:37:41 +0000
treeherdermozilla-beta@150c9fed433b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerseeejay
bugs1011886
milestone36.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1011886 - [AccessFu] Introduce key echo by character, word, and character and word. r=eeejay
accessible/jsat/EventManager.jsm
accessible/jsat/Presentation.jsm
accessible/tests/mochitest/jsat/jsatcommon.js
accessible/tests/mochitest/jsat/test_content_text.html
b2g/app/b2g.js
--- a/accessible/jsat/EventManager.jsm
+++ b/accessible/jsat/EventManager.jsm
@@ -397,32 +397,33 @@ this.EventManager.prototype = {
       // zero-length text. If we did, ignore it (bug #749810).
       if (txtIface.characterCount) {
         throw x;
       }
     }
     // If there are embedded objects in the text, ignore them.
     // Assuming changes to the descendants would already be handled by the
     // show/hide event.
-    let modifiedText = event.modifiedText.replace(/\uFFFC/g, '').trim();
-    if (!modifiedText) {
+    let modifiedText = event.modifiedText.replace(/\uFFFC/g, '');
+    if (modifiedText != event.modifiedText && !modifiedText.trim()) {
       return;
     }
+
     if (aLiveRegion) {
       if (aEvent.eventType === Events.TEXT_REMOVED) {
         this._queueLiveEvent(Events.TEXT_REMOVED, aLiveRegion, aIsPolite,
           modifiedText);
       } else {
         this._dequeueLiveEvent(Events.TEXT_REMOVED, aLiveRegion);
         this.present(Presentation.liveRegion(aLiveRegion, aIsPolite, false,
           modifiedText));
       }
     } else {
-      this.present(Presentation.textChanged(isInserted, event.start,
-        event.length, text, modifiedText));
+      this.present(Presentation.textChanged(aEvent.accessible, isInserted,
+        event.start, event.length, text, modifiedText));
     }
   },
 
   _handleLiveRegion: function _handleLiveRegion(aEvent, aRelevant) {
     if (aEvent.isFromUserInput) {
       return {};
     }
     let parseLiveAttrs = function parseLiveAttrs(aAccessible) {
--- a/accessible/jsat/Presentation.jsm
+++ b/accessible/jsat/Presentation.jsm
@@ -6,18 +6,17 @@
           UtteranceGenerator, BrailleGenerator, States, Roles, PivotContext */
 /* exported Presentation */
 
 'use strict';
 
 const {utils: Cu, interfaces: Ci} = Components;
 
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
-XPCOMUtils.defineLazyModuleGetter(this, 'Utils', // jshint ignore:line
-  'resource://gre/modules/accessibility/Utils.jsm');
+Cu.import('resource://gre/modules/accessibility/Utils.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'Logger', // jshint ignore:line
   'resource://gre/modules/accessibility/Utils.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'PivotContext', // jshint ignore:line
   'resource://gre/modules/accessibility/Utils.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'UtteranceGenerator', // jshint ignore:line
   'resource://gre/modules/accessibility/OutputGenerator.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'BrailleGenerator', // jshint ignore:line
   'resource://gre/modules/accessibility/OutputGenerator.jsm');
@@ -55,18 +54,18 @@ Presenter.prototype = {
    * @param {nsIAccessible} aObject the object that has been invoked.
    * @param {string} aActionName the name of the action.
    */
   actionInvoked: function actionInvoked(aObject, aActionName) {}, // jshint ignore:line
 
   /**
    * Text has changed, either by the user or by the system. TODO.
    */
-  textChanged: function textChanged(aIsInserted, aStartOffset, aLength, aText, // jshint ignore:line
-                                    aModifiedText) {}, // jshint ignore:line
+  textChanged: function textChanged(aAccessible, aIsInserted, aStartOffset, // jshint ignore:line
+                                    aLength, aText, aModifiedText) {}, // jshint ignore:line
 
   /**
    * Text selection has changed. TODO.
    */
   textSelectionChanged: function textSelectionChanged(
     aText, aStart, aEnd, aOldStart, aOldEnd, aIsFromUserInput) {}, // jshint ignore:line
 
   /**
@@ -339,17 +338,17 @@ AndroidPresenter.prototype.tabSelected =
 
 AndroidPresenter.prototype.tabStateChanged =
   function AndroidPresenter_tabStateChanged(aDocObj, aPageState) {
     return this.announce(
       UtteranceGenerator.genForTabStateChange(aDocObj, aPageState));
   };
 
 AndroidPresenter.prototype.textChanged = function AndroidPresenter_textChanged(
-  aIsInserted, aStart, aLength, aText, aModifiedText) {
+  aAccessible, aIsInserted, aStart, aLength, aText, aModifiedText) {
     let eventDetails = {
       eventType: this.ANDROID_VIEW_TEXT_CHANGED,
       text: [aText],
       fromIndex: aStart,
       removedCount: 0,
       addedCount: 0
     };
 
@@ -456,16 +455,23 @@ AndroidPresenter.prototype.liveRegion =
  * A B2G presenter for Gaia.
  */
 function B2GPresenter() {}
 
 B2GPresenter.prototype = Object.create(Presenter.prototype);
 
 B2GPresenter.prototype.type = 'B2G';
 
+B2GPresenter.prototype.keyboardEchoSetting =
+  new PrefCache('accessibility.accessfu.keyboard_echo');
+B2GPresenter.prototype.NO_ECHO = 0;
+B2GPresenter.prototype.CHARACTER_ECHO = 1;
+B2GPresenter.prototype.WORD_ECHO = 2;
+B2GPresenter.prototype.CHARACTER_AND_WORD_ECHO = 3;
+
 /**
  * A pattern used for haptic feedback.
  * @type {Array}
  */
 B2GPresenter.prototype.PIVOT_CHANGE_HAPTIC_PATTERN = [40];
 
 /**
  * Pivot move reasons.
@@ -492,25 +498,67 @@ B2GPresenter.prototype.pivotChanged =
           isUserInput: aIsUserInput
         }
       }
     };
   };
 
 B2GPresenter.prototype.valueChanged =
   function B2GPresenter_valueChanged(aAccessible) {
+
+    // the editable value changes are handled in the text changed presenter
+    if (Utils.getState(aAccessible).contains(States.EDITABLE)) {
+      return null;
+    }
+
     return {
       type: this.type,
       details: {
         eventType: 'value-change',
         data: aAccessible.value
       }
     };
   };
 
+B2GPresenter.prototype.textChanged = function B2GPresenter_textChanged(
+  aAccessible, aIsInserted, aStart, aLength, aText, aModifiedText) {
+    let echoSetting = this.keyboardEchoSetting.value;
+    let text = '';
+
+    if (echoSetting == this.CHARACTER_ECHO ||
+        echoSetting == this.CHARACTER_AND_WORD_ECHO) {
+      text = aModifiedText;
+    }
+
+    // add word if word boundary is added
+    if ((echoSetting == this.WORD_ECHO ||
+        echoSetting == this.CHARACTER_AND_WORD_ECHO) &&
+        aIsInserted && aLength === 1) {
+      let accText = aAccessible.QueryInterface(Ci.nsIAccessibleText);
+      let startBefore = {}, endBefore = {};
+      let startAfter = {}, endAfter = {};
+      accText.getTextBeforeOffset(aStart,
+        Ci.nsIAccessibleText.BOUNDARY_WORD_END, startBefore, endBefore);
+      let maybeWord = accText.getTextBeforeOffset(aStart + 1,
+        Ci.nsIAccessibleText.BOUNDARY_WORD_END, startAfter, endAfter);
+      if (endBefore.value !== endAfter.value) {
+        text += maybeWord;
+      }
+    }
+
+    return {
+      type: this.type,
+      details: {
+        eventType: 'text-change',
+        data: text
+      }
+    };
+
+  };
+
 B2GPresenter.prototype.actionInvoked =
   function B2GPresenter_actionInvoked(aObject, aActionName) {
     return {
       type: this.type,
       details: {
         eventType: 'action',
         data: UtteranceGenerator.genForAction(aObject, aActionName)
       }
@@ -609,21 +657,21 @@ this.Presentation = { // jshint ignore:l
       for each (p in this.presenters)]; // jshint ignore:line
   },
 
   actionInvoked: function Presentation_actionInvoked(aObject, aActionName) {
     return [p.actionInvoked(aObject, aActionName) // jshint ignore:line
       for each (p in this.presenters)]; // jshint ignore:line
   },
 
-  textChanged: function Presentation_textChanged(aIsInserted, aStartOffset,
-                                    aLength, aText,
+  textChanged: function Presentation_textChanged(aAccessible, aIsInserted,
+                                    aStartOffset, aLength, aText,
                                     aModifiedText) {
-    return [p.textChanged(aIsInserted, aStartOffset, aLength, aText, // jshint ignore:line
-      aModifiedText) for each (p in this.presenters)]; // jshint ignore:line
+    return [p.textChanged(aAccessible, aIsInserted, aStartOffset, aLength, // jshint ignore:line
+      aText, aModifiedText) for each (p in this.presenters)]; // jshint ignore:line
   },
 
   textSelectionChanged: function textSelectionChanged(aText, aStart, aEnd,
                                                       aOldStart, aOldEnd,
                                                       aIsFromUserInput) {
     return [p.textSelectionChanged(aText, aStart, aEnd, aOldStart, aOldEnd, // jshint ignore:line
       aIsFromUserInput) for each (p in this.presenters)]; // jshint ignore:line
   },
--- a/accessible/tests/mochitest/jsat/jsatcommon.js
+++ b/accessible/tests/mochitest/jsat/jsatcommon.js
@@ -621,16 +621,25 @@ function ExpectedValueChange(aValue, aOp
   ExpectedPresent.call(this, {
     eventType: 'value-change',
     data: [aValue]
   }, null, aOptions);
 }
 
 ExpectedValueChange.prototype = Object.create(ExpectedPresent.prototype);
 
+function ExpectedTextChanged(aValue, aOptions) {
+  ExpectedPresent.call(this, {
+    eventType: 'text-change',
+    data: aValue
+  }, null, aOptions);
+}
+
+ExpectedTextChanged.prototype = Object.create(ExpectedPresent.prototype);
+
 function ExpectedEditState(aEditState, aOptions) {
   ExpectedMessage.call(this, 'AccessFu:Input', aOptions);
   this.json = aEditState;
 }
 
 ExpectedEditState.prototype = Object.create(ExpectedMessage.prototype);
 
 function ExpectedTextSelectionChanged(aStart, aEnd, aOptions) {
--- a/accessible/tests/mochitest/jsat/test_content_text.html
+++ b/accessible/tests/mochitest/jsat/test_content_text.html
@@ -4,16 +4,19 @@
   <title>Tests AccessFu content integration</title>
   <meta charset="utf-8" />
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
 
   <script type="application/javascript"
           src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js">
   </script>
   <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js">
+  </script>
+  <script type="application/javascript"
           src="chrome://mochikit/content/chrome-harness.js">
   </script>
 
   <script type="application/javascript" src="../common.js"></script>
   <script type="application/javascript" src="../browser.js"></script>
   <script type="application/javascript" src="../events.js"></script>
   <script type="application/javascript" src="../role.js"></script>
   <script type="application/javascript" src="../states.js"></script>
@@ -164,19 +167,108 @@
            new ExpectedCursorChange(
             [ 'So we don\'t get dessert?', {string: 'label'} ]),
            new ExpectedAnnouncement('navigating'),
            new ExpectedEditState({
             editing: false,
             multiline: false,
             atStart: true,
             atEnd: false
-           }, { focused: 'html' })]
+           }, { focused: 'html' })],
+
+          [ContentMessages.focusSelector('input'),
+           new ExpectedAnnouncement('editing'),
+           new ExpectedEditState({
+            editing: true,
+            multiline: false,
+            atStart: true,
+            atEnd: true
+           }),
+           new ExpectedCursorChange([{string: 'entry'}]),
+           new ExpectedTextSelectionChanged(0, 0)
+          ],
+          [function() {
+             SpecialPowers.setIntPref(KEYBOARD_ECHO_SETTING, 3);
+             typeKey('a')();
+           },
+           new ExpectedTextChanged('a'),
+           new ExpectedTextSelectionChanged(1, 1),
+          ],
+          [typeKey('b'),
+           new ExpectedTextChanged('b'),
+           new ExpectedTextSelectionChanged(2, 2),
+          ],
+          [typeKey('c'),
+           new ExpectedTextChanged('c'),
+           new ExpectedTextSelectionChanged(3, 3),
+          ],
+          [typeKey('d'),
+           new ExpectedTextChanged('d'),
+           new ExpectedTextSelectionChanged(4, 4),
+          ],
+          [typeKey(' '),
+           new ExpectedTextChanged(' abcd'),
+           new ExpectedTextSelectionChanged(5, 5),
+          ],
+          [typeKey('e'),
+           new ExpectedTextChanged('e'),
+           new ExpectedTextSelectionChanged(6, 6),
+          ],
+          [function() {
+             SpecialPowers.setIntPref(KEYBOARD_ECHO_SETTING, 2);
+             typeKey('a')();
+           },
+           new ExpectedTextChanged(''),
+           new ExpectedTextSelectionChanged(7, 7),
+          ],
+          [typeKey('d'),
+           new ExpectedTextChanged(''),
+           new ExpectedTextSelectionChanged(8, 8),
+          ],
+          [typeKey(' '),
+           new ExpectedTextChanged(' ead'),
+           new ExpectedTextSelectionChanged(9, 9),
+          ],
+          [function() {
+             SpecialPowers.setIntPref(KEYBOARD_ECHO_SETTING, 1);
+             typeKey('f')();
+           },
+           new ExpectedTextChanged('f'),
+           new ExpectedTextSelectionChanged(10, 10),
+          ],
+          [typeKey('g'),
+           new ExpectedTextChanged('g'),
+           new ExpectedTextSelectionChanged(11, 11),
+          ],
+          [typeKey(' '),
+           new ExpectedTextChanged(' '),
+           new ExpectedTextSelectionChanged(12, 12),
+          ],
+          [function() {
+             SpecialPowers.setIntPref(KEYBOARD_ECHO_SETTING, 0);
+             typeKey('f')();
+           },
+           new ExpectedTextChanged(''),
+           new ExpectedTextSelectionChanged(13, 13),
+          ],
+          [typeKey('g'),
+           new ExpectedTextChanged(''),
+           new ExpectedTextSelectionChanged(14, 14),
+          ],
+          [typeKey(' '),
+           new ExpectedTextChanged(''),
+           new ExpectedTextSelectionChanged(15, 15),
+          ],
         ]);
 
+      const KEYBOARD_ECHO_SETTING = 'accessibility.accessfu.keyboard_echo';
+      function typeKey(key) {
+        return function() { synthesizeKey(key, {}, currentTabWindow()); };
+      }
+
       addA11yLoadEvent(function() {
         textTest.start(function () {
           closeBrowserWindow();
           SimpleTest.finish();
         });
       }, doc.defaultView);
     }
 
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -792,16 +792,19 @@ pref("dom.disable_window_open_dialog_fea
 pref("accessibility.accessfu.activate", 2);
 pref("accessibility.accessfu.quicknav_modes", "Link,Heading,FormElement,Landmark,ListItem");
 // Active quicknav mode, index value of list from quicknav_modes
 pref("accessibility.accessfu.quicknav_index", 0);
 // Setting for an utterance order (0 - description first, 1 - description last).
 pref("accessibility.accessfu.utterance", 1);
 // Whether to skip images with empty alt text
 pref("accessibility.accessfu.skip_empty_images", true);
+// Setting to change the verbosity of entered text (0 - none, 1 - characters,
+// 2 - words, 3 - both)
+pref("accessibility.accessfu.keyboard_echo", 3);
 
 // Enable hit-target fluffing
 pref("ui.touch.radius.enabled", true);
 pref("ui.touch.radius.leftmm", 3);
 pref("ui.touch.radius.topmm", 5);
 pref("ui.touch.radius.rightmm", 3);
 pref("ui.touch.radius.bottommm", 2);