Bug 927882 - Standardize TextSelection caret positioning logic for soft keyboards, r=jchen, wesj, f=margaret
authorMark Capella <markcapella@twcny.rr.com>
Wed, 19 Feb 2014 22:19:43 -0500
changeset 170017 a10bb6a9da93110816ccce2fd0bf5249fae4a011
parent 170016 2fca335e35c612d6b08d1f22ec55cc39880f4b6e
child 170018 415e5d63cc3399e0e8aa5f8f56e795819c800d25
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
reviewersjchen, wesj
bugs927882
milestone30.0a1
Bug 927882 - Standardize TextSelection caret positioning logic for soft keyboards, r=jchen, wesj, f=margaret
mobile/android/chrome/content/SelectionHandler.js
widget/android/nsWindow.cpp
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -53,32 +53,42 @@ var SelectionHandler = {
   _addObservers: function sh_addObservers() {
     Services.obs.addObserver(this, "Gesture:SingleTap", false);
     Services.obs.addObserver(this, "Tab:Selected", false);
     Services.obs.addObserver(this, "after-viewport-change", false);
     Services.obs.addObserver(this, "TextSelection:Move", false);
     Services.obs.addObserver(this, "TextSelection:Position", false);
     Services.obs.addObserver(this, "TextSelection:End", false);
     Services.obs.addObserver(this, "TextSelection:Action", false);
-    BrowserApp.deck.addEventListener("compositionend", this, false);
+
+    BrowserApp.deck.addEventListener("pagehide", this, false);
+    BrowserApp.deck.addEventListener("blur", this, true);
   },
 
   _removeObservers: function sh_removeObservers() {
     Services.obs.removeObserver(this, "Gesture:SingleTap");
     Services.obs.removeObserver(this, "Tab:Selected");
     Services.obs.removeObserver(this, "after-viewport-change");
     Services.obs.removeObserver(this, "TextSelection:Move");
     Services.obs.removeObserver(this, "TextSelection:Position");
     Services.obs.removeObserver(this, "TextSelection:End");
     Services.obs.removeObserver(this, "TextSelection:Action");
-    BrowserApp.deck.removeEventListener("compositionend", this);
+
+    BrowserApp.deck.removeEventListener("pagehide", this);
+    BrowserApp.deck.removeEventListener("blur", this);
   },
 
   observe: function sh_observe(aSubject, aTopic, aData) {
     switch (aTopic) {
+      // Update caret position on keyboard activity
+      case "TextSelection:UpdateCaretPos":
+        // Generated by IME close, autoCorrection / styling
+        this._positionHandles();
+        break;
+
       case "Gesture:SingleTap": {
         if (this._activeType == this.TYPE_SELECTION) {
           let data = JSON.parse(aData);
           if (!this._pointInSelection(data.x, data.y))
             this._closeSelection();
         } else if (this._activeType == this.TYPE_CURSOR) {
           // attachCaret() is called in the "Gesture:SingleTap" handler in BrowserEventHandler
           // We're guaranteed to call this first, because this observer was added last
@@ -170,26 +180,28 @@ var SelectionHandler = {
         });
         break;
     }
   },
 
   handleEvent: function sh_handleEvent(aEvent) {
     switch (aEvent.type) {
       case "pagehide":
-      // We only add keydown and blur listeners for TYPE_CURSOR
-      case "keydown":
       case "blur":
         this._closeSelection();
         break;
 
+      // Update caret position on keyboard activity
+      case "keyup":
+        // Not generated by Swiftkeyboard
+      case "compositionupdate":
       case "compositionend":
-        // compositionend messages normally terminate caret display
-        if (this._activeType == this.TYPE_CURSOR && !this._ignoreCompositionChanges) {
-          this._deactivate();
+        // Generated by SwiftKeyboard, et. al.
+        if (!this._ignoreCompositionChanges) {
+          this._positionHandles();
         }
         break;
     }
   },
 
   /** Returns true if the provided element can be selected in text selection, false otherwise. */
   canSelect: function sh_canSelect(aElement) {
     return !(aElement instanceof Ci.nsIDOMHTMLButtonElement ||
@@ -492,38 +504,42 @@ var SelectionHandler = {
    */
   attachCaret: function sh_attachCaret(aElement) {
     // See if its an input element, and it isn't disabled, nor handled by Android native dialog
     if (aElement.disabled ||
         InputWidgetHelper.hasInputWidget(aElement) ||
         !((aElement instanceof HTMLInputElement && aElement.mozIsTextField(false)) ||
           (aElement instanceof HTMLTextAreaElement)))
       return;
+
     this._initTargetInfo(aElement);
 
-    this._contentWindow.addEventListener("keydown", this, false);
-    this._contentWindow.addEventListener("blur", this, true);
+    // Caret-specific observer/listeners
+    Services.obs.addObserver(this, "TextSelection:UpdateCaretPos", false);
+    BrowserApp.deck.addEventListener("keyup", this, false);
+    BrowserApp.deck.addEventListener("compositionupdate", this, false);
+    BrowserApp.deck.addEventListener("compositionend", this, false);
 
     this._activeType = this.TYPE_CURSOR;
     this._positionHandles();
 
     this._sendMessage("TextSelection:ShowHandles", [this.HANDLE_TYPE_MIDDLE]);
   },
 
+  // Target initialization for both TYPE_CURSOR and TYPE_SELECTION
   _initTargetInfo: function sh_initTargetInfo(aElement) {
     this._targetElement = aElement;
     if (aElement instanceof Ci.nsIDOMNSEditableElement) {
       aElement.focus();
     }
 
     this._contentWindow = aElement.ownerDocument.defaultView;
     this._isRTL = (this._contentWindow.getComputedStyle(aElement, "").direction == "rtl");
 
     this._addObservers();
-    this._contentWindow.addEventListener("pagehide", this, false);
   },
 
   _getSelection: function sh_getSelection() {
     if (this._targetElement instanceof Ci.nsIDOMNSEditableElement)
       return this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement).editor.selection;
     else
       return this._contentWindow.getSelection();
   },
@@ -744,30 +760,36 @@ var SelectionHandler = {
       // Clear selection without clearing the anchorNode or focusNode
       if (selection.rangeCount != 0) {
         selection.collapseToStart();
       }
     }
   },
 
   _deactivate: function sh_deactivate() {
-    this._activeType = this.TYPE_NONE;
-
     sendMessageToJava({ type: "TextSelection:HideHandles" });
 
     this._removeObservers();
-    this._contentWindow.removeEventListener("pagehide", this, false);
-    this._contentWindow.removeEventListener("keydown", this, false);
-    this._contentWindow.removeEventListener("blur", this, true);
+
+    // Only observed for caret positioning
+    if (this._activeType == this.TYPE_CURSOR) {
+      Services.obs.removeObserver(this, "TextSelection:UpdateCaretPos");
+      BrowserApp.deck.removeEventListener("keyup", this);
+      BrowserApp.deck.removeEventListener("compositionupdate", this);
+      BrowserApp.deck.removeEventListener("compositionend", this);
+    }
+
     this._contentWindow = null;
     this._targetElement = null;
     this._isRTL = false;
     this._cache = null;
     this._ignoreSelectionChanges = false;
     this._ignoreCompositionChanges = false;
+
+    this._activeType = this.TYPE_NONE;
   },
 
   _getViewOffset: function sh_getViewOffset() {
     let offset = { x: 0, y: 0 };
     let win = this._contentWindow;
 
     // Recursively look through frames to compute the total position offset.
     while (win.frameElement) {
--- a/widget/android/nsWindow.cpp
+++ b/widget/android/nsWindow.cpp
@@ -1955,16 +1955,23 @@ nsWindow::OnIMEEvent(AndroidGeckoEvent *
             }
 
             selEvent.mOffset = std::min(start, end);
             selEvent.mLength = std::max(start, end) - selEvent.mOffset;
             selEvent.mReversed = start > end;
             selEvent.mExpandToClusterBoundary = false;
 
             DispatchEvent(&selEvent);
+
+            // Notify SelectionHandler of final caret position
+            // Required after IME hide via 'Back' button
+            AndroidGeckoEvent* broadcastEvent = AndroidGeckoEvent::MakeBroadcastEvent(
+                NS_LITERAL_CSTRING("TextSelection:UpdateCaretPos"),
+                NS_LITERAL_CSTRING(""));
+            nsAppShell::gAppShell->PostEvent(broadcastEvent);
         }
         break;
     case AndroidGeckoEvent::IME_ADD_COMPOSITION_RANGE:
         {
             TextRange range;
             range.mStartOffset = ae->Start();
             range.mEndOffset = ae->End();
             range.mRangeType = ae->RangeType();
@@ -2042,16 +2049,23 @@ nsWindow::OnIMEEvent(AndroidGeckoEvent *
             const NS_ConvertUTF16toUTF8 theText8(event.theText);
             const char* text = theText8.get();
             ALOGIME("IME: IME_SET_TEXT: text=\"%s\", length=%u, range=%u",
                     text, event.theText.Length(), mIMERanges.Length());
 #endif // DEBUG_ANDROID_IME
 
             DispatchEvent(&event);
             mIMERanges.Clear();
+
+            // Notify SelectionHandler of final caret position
+            // Required in cases of keyboards providing autoCorrections
+            AndroidGeckoEvent* broadcastEvent = AndroidGeckoEvent::MakeBroadcastEvent(
+                NS_LITERAL_CSTRING("TextSelection:UpdateCaretPos"),
+                NS_LITERAL_CSTRING(""));
+            nsAppShell::gAppShell->PostEvent(broadcastEvent);
         }
         break;
     case AndroidGeckoEvent::IME_REMOVE_COMPOSITION:
         {
             /*
              *  Remove any previous composition.  This is only used for
              *    visual indication and does not affect the text content.
              *