Bug 757372 - Announce "editing" when in editing mode. Use a11y states and focus to determine it. r=davidb
authorEitan Isaacson <eitan@monotonous.org>
Tue, 10 Jul 2012 16:10:15 -0700
changeset 98886 d540e12b6c6b5a29b94ea9048d3eadb875b349dc
parent 98885 9cb90658bd51b26929099daa3bbf52ba7652cb1f
child 98887 e3c42c6b420e1a7c9faddc33033f9dca20c108fe
push id11706
push usereisaacson@mozilla.com
push dateTue, 10 Jul 2012 23:10:32 +0000
treeherdermozilla-inbound@e3c42c6b420e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdavidb
bugs757372
milestone16.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 757372 - Announce "editing" when in editing mode. Use a11y states and focus to determine it. r=davidb
accessible/src/jsat/AccessFu.jsm
accessible/src/jsat/Presenters.jsm
accessible/src/jsat/Utils.jsm
accessible/src/jsat/UtteranceGenerator.jsm
accessible/src/jsat/VirtualCursorController.jsm
dom/locales/en-US/chrome/accessibility/AccessFu.properties
--- a/accessible/src/jsat/AccessFu.jsm
+++ b/accessible/src/jsat/AccessFu.jsm
@@ -360,16 +360,31 @@ var AccessFu = {
       }
       case Ci.nsIAccessibleEvent.EVENT_FOCUS:
       {
         let acc = aEvent.accessible;
         let doc = aEvent.accessibleDocument;
         if (acc.role != Ci.nsIAccessibleRole.ROLE_DOCUMENT &&
             doc.role != Ci.nsIAccessibleRole.ROLE_CHROME_WINDOW)
           VirtualCursorController.moveCursorToObject(acc);
+
+        let [,extState] = Utils.getStates(acc);
+        let editableState = extState &
+          (Ci.nsIAccessibleStates.EXT_STATE_EDITABLE |
+           Ci.nsIAccessibleStates.EXT_STATE_MULTI_LINE);
+
+        if (editableState != VirtualCursorController.editableState) {
+          if (!VirtualCursorController.editableState)
+            this.presenters.forEach(
+              function(p) {
+                p.editingModeChanged(true);
+              }
+            );
+        }
+        VirtualCursorController.editableState = editableState;
         break;
       }
       default:
         break;
     }
   },
 
   /**
--- a/accessible/src/jsat/Presenters.jsm
+++ b/accessible/src/jsat/Presenters.jsm
@@ -86,17 +86,22 @@ Presenter.prototype = {
    *   virtual cursor position.
    */
   tabSelected: function tabSelected(aDocContext, aVCContext) {},
 
   /**
    * The viewport has changed, either a scroll, pan, zoom, or
    *    landscape/portrait toggle.
    */
-  viewportChanged: function viewportChanged() {}
+  viewportChanged: function viewportChanged() {},
+
+  /**
+   * We have entered or left text editing mode.
+   */
+  editingModeChanged: function editingModeChanged(aIsEditing) {}
 };
 
 /**
  * Visual presenter. Draws a box around the virtual cursor's position.
  */
 
 function VisualPresenter() {}
 
@@ -298,32 +303,18 @@ AndroidPresenter.prototype = {
 
   tabSelected: function AndroidPresenter_tabSelected(aDocContext, aVCContext) {
     // Send a pivot change message with the full context utterance for this doc.
     this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE);
   },
 
   tabStateChanged: function AndroidPresenter_tabStateChanged(aDocObj,
                                                              aPageState) {
-    let stateUtterance = UtteranceGenerator.
-      genForTabStateChange(aDocObj, aPageState);
-
-    if (!stateUtterance.length)
-      return;
-
-    this.sendMessageToJava({
-      gecko: {
-        type: 'Accessibility:Event',
-        eventType: this.ANDROID_VIEW_TEXT_CHANGED,
-        text: stateUtterance,
-        addedCount: stateUtterance.join(' ').length,
-        removedCount: 0,
-        fromIndex: 0
-      }
-    });
+    this._appAnnounce(
+      UtteranceGenerator.genForTabStateChange(aDocObj, aPageState));
   },
 
   textChanged: function AndroidPresenter_textChanged(aIsInserted, aStart,
                                                      aLength, aText,
                                                      aModifiedText) {
     let androidEvent = {
       type: 'Accessibility:Event',
       eventType: this.ANDROID_VIEW_TEXT_CHANGED,
@@ -359,16 +350,36 @@ AndroidPresenter.prototype = {
         scrollX: win.scrollX,
         scrollY: win.scrollY,
         maxScrollX: win.scrollMaxX,
         maxScrollY: win.scrollMaxY
       }
     });
   },
 
+  editingModeChanged: function AndroidPresenter_editingModeChanged(aIsEditing) {
+    this._appAnnounce(UtteranceGenerator.genForEditingMode(aIsEditing));
+  },
+
+  _appAnnounce: function _appAnnounce(aUtterance) {
+    if (!aUtterance.length)
+      return;
+
+    this.sendMessageToJava({
+      gecko: {
+        type: 'Accessibility:Event',
+        eventType: this.ANDROID_VIEW_TEXT_CHANGED,
+        text: aUtterance,
+        addedCount: aUtterance.join(' ').length,
+        removedCount: 0,
+        fromIndex: 0
+      }
+    });
+  },
+
   sendMessageToJava: function AndroidPresenter_sendMessageTojava(aMessage) {
     return Cc['@mozilla.org/android/bridge;1'].
       getService(Ci.nsIAndroidBridge).
       handleGeckoMessage(JSON.stringify(aMessage));
   }
 };
 
 /**
--- a/accessible/src/jsat/Utils.jsm
+++ b/accessible/src/jsat/Utils.jsm
@@ -54,16 +54,26 @@ var Utils = {
 
   getViewport: function getViewport(aWindow) {
     switch (this.OS) {
       case 'Android':
         return aWindow.BrowserApp.selectedTab.getViewport();
       default:
         return null;
     }
+  },
+
+  getStates: function getStates(aAccessible) {
+    if (!aAccessible)
+      return [0, 0];
+
+    let state = {};
+    let extState = {};
+    aAccessible.getState(state, extState);
+    return [state.value, extState.value];
   }
 };
 
 var Logger = {
   DEBUG: 0,
   INFO: 1,
   WARNING: 2,
   ERROR: 3,
--- a/accessible/src/jsat/UtteranceGenerator.jsm
+++ b/accessible/src/jsat/UtteranceGenerator.jsm
@@ -118,16 +118,26 @@ var UtteranceGenerator = {
         return [gStringBundle.GetStringFromName('tabLoadStopped')];
       case 'reload':
         return [gStringBundle.GetStringFromName('tabReload')];
       default:
         return [];
     }
   },
 
+  /**
+   * Generates an utterance for announcing entering and leaving editing mode.
+   * @param {aIsEditing} boolean true if we are in editing mode
+   * @return {Array} The mode utterance
+   */
+  genForEditingMode: function genForEditingMode(aIsEditing) {
+    return [gStringBundle.GetStringFromName(
+              aIsEditing ? 'editingMode' : 'navigationMode')];
+  },
+
   verbosityRoleMap: {
     'menubar': INCLUDE_DESC,
     'scrollbar': INCLUDE_DESC,
     'grip': INCLUDE_DESC,
     'alert': INCLUDE_DESC | INCLUDE_NAME,
     'menupopup': INCLUDE_DESC,
     'menuitem': INCLUDE_DESC,
     'tooltip': INCLUDE_DESC,
--- a/accessible/src/jsat/VirtualCursorController.jsm
+++ b/accessible/src/jsat/VirtualCursorController.jsm
@@ -399,21 +399,18 @@ var TraversalRules = {
       return Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
     },
 
     QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessibleTraversalRule])
   }
 };
 
 var VirtualCursorController = {
-  NOT_EDITABLE: 0,
-  SINGLE_LINE_EDITABLE: 1,
-  MULTI_LINE_EDITABLE: 2,
-
   exploreByTouch: false,
+  editableState: 0,
 
   attach: function attach(aWindow) {
     this.chromeWin = aWindow;
     this.chromeWin.document.addEventListener('keypress', this, true);
     this.chromeWin.document.addEventListener('mousemove', this, true);
   },
 
   detach: function detach() {
@@ -458,103 +455,107 @@ var VirtualCursorController = {
     let document = Utils.getCurrentContentDoc(this.chromeWin);
     let target = aEvent.target;
 
     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._isEditableText(target) ||
+        if (this.editableState ||
             aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey)
           return;
 
         let key = String.fromCharCode(aEvent.charCode);
         let methodName = '', rule = {};
         try {
           [methodName, rule] = this.keyMap[key];
         } catch (x) {
           return;
         }
         this[methodName](document, false, rule);
         break;
       case aEvent.DOM_VK_END:
+        if (this.editableState) {
+          if (target.selectionEnd != target.textLength)
+            // Don't move forward if caret is not at end of entry.
+            // XXX: Fix for rtl
+            return;
+          else
+            target.blur();
+        }
         this.moveForward(document, true);
         break;
       case aEvent.DOM_VK_HOME:
+        if (this.editableState) {
+          if (target.selectionEnd != 0)
+            // Don't move backward if caret is not at start of entry.
+            // XXX: Fix for rtl
+            return;
+          else
+            target.blur();
+        }
         this.moveBackward(document, true);
         break;
       case aEvent.DOM_VK_RIGHT:
-        if (this._isEditableText(target)) {
+        if (this.editableState) {
           if (target.selectionEnd != target.textLength)
             // Don't move forward if caret is not at end of entry.
             // XXX: Fix for rtl
             return;
           else
             target.blur();
         }
         this.moveForward(document, aEvent.shiftKey);
         break;
       case aEvent.DOM_VK_LEFT:
-        if (this._isEditableText(target)) {
+        if (this.editableState) {
           if (target.selectionEnd != 0)
             // Don't move backward if caret is not at start of entry.
             // XXX: Fix for rtl
             return;
           else
             target.blur();
         }
         this.moveBackward(document, aEvent.shiftKey);
         break;
       case aEvent.DOM_VK_UP:
-        if (this._isEditableText(target) == this.MULTI_LINE_EDITABLE) {
+        if (this.editableState & Ci.nsIAccessibleStates.EXT_STATE_MULTI_LINE) {
           if (target.selectionEnd != 0)
             // Don't blur content if caret is not at start of text area.
             return;
           else
             target.blur();
         }
 
         if (Utils.OS == 'Android')
           // Return focus to native Android browser chrome.
           Cc['@mozilla.org/android/bridge;1'].
             getService(Ci.nsIAndroidBridge).handleGeckoMessage(
               JSON.stringify({ gecko: { type: 'ToggleChrome:Focus' } }));
         break;
       case aEvent.DOM_VK_RETURN:
       case aEvent.DOM_VK_ENTER:
-        if (this._isEditableText(target))
+        if (this.editableState)
           return;
         this.activateCurrent(document);
         break;
       default:
         return;
     }
 
     aEvent.preventDefault();
     aEvent.stopPropagation();
   },
 
   moveToPoint: function moveToPoint(aDocument, aX, aY) {
     this.getVirtualCursor(aDocument).moveToPoint(TraversalRules.Simple,
                                                  aX, aY, true);
   },
 
-  _isEditableText: function _isEditableText(aElement) {
-    // XXX: Support contentEditable and design mode
-    if (aElement instanceof Ci.nsIDOMHTMLInputElement &&
-        aElement.mozIsTextField(false))
-      return this.SINGLE_LINE_EDITABLE;
-
-    if (aElement instanceof Ci.nsIDOMHTMLTextAreaElement)
-      return this.MULTI_LINE_EDITABLE;
-
-    return this.NOT_EDITABLE;
-  },
-
   moveForward: function moveForward(aDocument, aLast, aRule) {
     let virtualCursor = this.getVirtualCursor(aDocument);
     if (aLast) {
       virtualCursor.moveLast(TraversalRules.Simple);
     } else {
       try {
         virtualCursor.moveNext(aRule || TraversalRules.Simple);
       } catch (x) {
--- a/dom/locales/en-US/chrome/accessibility/AccessFu.properties
+++ b/dom/locales/en-US/chrome/accessibility/AccessFu.properties
@@ -102,8 +102,12 @@ tabReload      =      reloading
 # Object states
 stateChecked     =    checked
 stateNotChecked  =    not checked
 stateExpanded    =    expanded
 stateCollapsed   =    collapsed
 stateUnavailable =    unavailable
 stateRequired    =    required
 stateTraversed   =    visited
+
+# App modes
+editingMode    =      editing
+navigationMode =      navigating
\ No newline at end of file