Bug 1026997 - Use nsISelectionPrivate to track selection changes in forms.js. r=yxl
☠☠ backed out by 85b5b33a4dc2 ☠ ☠
authorJan Jongboom <janjongboom@gmail.com>
Tue, 08 Jul 2014 03:31:00 -0400
changeset 192736 ad9e35d75c0842e2de2a87cf68c72aaab499a4ce
parent 192735 861a8e0774f3d9712ce15547ee8747177e03f4f3
child 192737 e2e2e7217ce260b5f6cabc1c54572fb425c7167a
push id8695
push userryanvm@gmail.com
push dateTue, 08 Jul 2014 12:07:45 +0000
treeherderb2g-inbound@e2e2e7217ce2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyxl
bugs1026997
milestone33.0a1
Bug 1026997 - Use nsISelectionPrivate to track selection changes in forms.js. r=yxl
dom/inputmethod/MozKeyboard.js
dom/inputmethod/forms.js
dom/inputmethod/mochitest/mochitest.ini
dom/inputmethod/mochitest/test_bug1026997.html
--- a/dom/inputmethod/MozKeyboard.js
+++ b/dom/inputmethod/MozKeyboard.js
@@ -389,16 +389,21 @@ MozInputContext.prototype = {
 
     let json = msg.json;
     let resolver = this.takePromiseResolver(json.requestId);
 
     if (!resolver) {
       return;
     }
 
+    // Update context first before resolving promise to avoid race condition
+    if (json.selectioninfo) {
+      this.updateSelectionContext(json.selectioninfo);
+    }
+
     switch (msg.name) {
       case "Keyboard:SendKey:Result:OK":
         resolver.resolve();
         break;
       case "Keyboard:SendKey:Result:Error":
         resolver.reject(json.error);
         break;
       case "Keyboard:GetText:Result:OK":
--- a/dom/inputmethod/forms.js
+++ b/dom/inputmethod/forms.js
@@ -219,16 +219,17 @@ let FormAssistant = {
   textAfterCursor: "",
   scrollIntoViewTimeout: null,
   _focusedElement: null,
   _focusCounter: 0, // up one for every time we focus a new element
   _observer: null,
   _documentEncoder: null,
   _editor: null,
   _editing: false,
+  _selectionPrivate: null,
 
   get focusedElement() {
     if (this._focusedElement && Cu.isDeadWrapper(this._focusedElement))
       this._focusedElement = null;
 
     return this._focusedElement;
   },
 
@@ -239,52 +240,58 @@ let FormAssistant = {
 
   setFocusedElement: function fa_setFocusedElement(element) {
     let self = this;
 
     if (element === this.focusedElement)
       return;
 
     if (this.focusedElement) {
-      this.focusedElement.removeEventListener('mousedown', this);
-      this.focusedElement.removeEventListener('mouseup', this);
       this.focusedElement.removeEventListener('compositionend', this);
       if (this._observer) {
         this._observer.disconnect();
         this._observer = null;
       }
       if (!element) {
         this.focusedElement.blur();
       }
+      if (this._selectionPrivate) {
+        this._selectionPrivate.removeSelectionListener(this);
+        this._selectionPrivate = null;
+      }
     }
 
     this._documentEncoder = null;
     if (this._editor) {
       // When the nsIFrame of the input element is reconstructed by
       // CSS restyling, the editor observers are removed. Catch
       // [nsIEditor.removeEditorObserver] failure exception if that
       // happens.
       try {
         this._editor.removeEditorObserver(this);
       } catch (e) {}
       this._editor = null;
     }
 
     if (element) {
-      element.addEventListener('mousedown', this);
-      element.addEventListener('mouseup', this);
       element.addEventListener('compositionend', this);
       if (isContentEditable(element)) {
         this._documentEncoder = getDocumentEncoder(element);
       }
       this._editor = getPlaintextEditor(element);
       if (this._editor) {
         // Add a nsIEditorObserver to monitor the text content of the focused
         // element.
         this._editor.addEditorObserver(this);
+
+        let selection = this._editor.selection;
+        if (selection) {
+          this._selectionPrivate = selection.QueryInterface(Ci.nsISelectionPrivate);
+          this._selectionPrivate.addSelectionListener(this);
+        }
       }
 
       // If our focusedElement is removed from DOM we want to handle it properly
       let MutationObserver = element.ownerDocument.defaultView.MutationObserver;
       this._observer = new MutationObserver(function(mutations) {
         var del = [].some.call(mutations, function(m) {
           return [].some.call(m.removedNodes, function(n) {
             return n.contains(element);
@@ -300,16 +307,20 @@ let FormAssistant = {
         childList: true,
         subtree: true
       });
     }
 
     this.focusedElement = element;
   },
 
+  notifySelectionChanged: function(aDocument, aSelection, aReason) {
+    this.updateSelection();
+  },
+
   get documentEncoder() {
     return this._documentEncoder;
   },
 
   // Get the nsIPlaintextEditor object of current input field.
   get editor() {
     return this._editor;
   },
@@ -371,42 +382,16 @@ let FormAssistant = {
       case "submit":
         if (this.focusedElement) {
           this.hideKeyboard();
           this.selectionStart = -1;
           this.selectionEnd = -1;
         }
         break;
 
-      case 'mousedown':
-         if (!this.focusedElement) {
-          break;
-        }
-
-        // We only listen for this event on the currently focused element.
-        // When the mouse goes down, note the cursor/selection position
-        this.updateSelection();
-        break;
-
-      case 'mouseup':
-        if (!this.focusedElement) {
-          break;
-        }
-
-        // We only listen for this event on the currently focused element.
-        // When the mouse goes up, see if the cursor has moved (or the
-        // selection changed) since the mouse went down. If it has, we
-        // need to tell the keyboard about it
-        range = getSelectionRange(this.focusedElement);
-        if (range[0] !== this.selectionStart ||
-            range[1] !== this.selectionEnd) {
-          this.updateSelection();
-        }
-        break;
-
       case "resize":
         if (!this.isKeyboardOpened)
           return;
 
         if (this.scrollIntoViewTimeout) {
           content.clearTimeout(this.scrollIntoViewTimeout);
           this.scrollIntoViewTimeout = null;
         }
@@ -418,44 +403,30 @@ let FormAssistant = {
             this.scrollIntoViewTimeout = null;
             if (this.focusedElement && !FormVisibility.isVisible(this.focusedElement)) {
               scrollSelectionOrElementIntoView(this.focusedElement);
             }
           }.bind(this), RESIZE_SCROLL_DELAY);
         }
         break;
 
-      case "input":
-        if (this.focusedElement) {
-          // When the text content changes, notify the keyboard
-          this.updateSelection();
-        }
-        break;
-
       case "keydown":
         if (!this.focusedElement) {
           break;
         }
 
         CompositionManager.endComposition('');
-
-        // We use 'setTimeout' to wait until the input element accomplishes the
-        // change in selection range.
-        content.setTimeout(function() {
-          this.updateSelection();
-        }.bind(this), 0);
         break;
 
       case "keyup":
         if (!this.focusedElement) {
           break;
         }
 
         CompositionManager.endComposition('');
-
         break;
 
       case "compositionend":
         if (!this.focusedElement) {
           break;
         }
 
         CompositionManager.onCompositionEnd();
@@ -520,17 +491,18 @@ let FormAssistant = {
           domWindowUtils.sendKeyEvent('keyup', json.keyCode,
                                     json.charCode, json.modifiers);
         }
 
         this._editing = false;
 
         if (json.requestId && doKeypress) {
           sendAsyncMessage("Forms:SendKey:Result:OK", {
-            requestId: json.requestId
+            requestId: json.requestId,
+            selectioninfo: this.getSelectionInfo()
           });
         }
         else if (json.requestId && !doKeypress) {
           sendAsyncMessage("Forms:SendKey:Result:Error", {
             requestId: json.requestId,
             error: "Keydown event got canceled"
           });
         }
@@ -578,18 +550,16 @@ let FormAssistant = {
             sendAsyncMessage("Forms:SetSelectionRange:Result:Error", {
               requestId: json.requestId,
               error: "failed"
             });
           }
           break;
         }
 
-        this.updateSelection();
-
         if (json.requestId) {
           sendAsyncMessage("Forms:SetSelectionRange:Result:OK", {
             requestId: json.requestId,
             selectioninfo: this.getSelectionInfo()
           });
         }
         break;
       }
@@ -646,24 +616,26 @@ let FormAssistant = {
         break;
       }
 
       case "Forms:SetComposition": {
         CompositionManager.setComposition(target, json.text, json.cursor,
                                           json.clauses);
         sendAsyncMessage("Forms:SetComposition:Result:OK", {
           requestId: json.requestId,
+          selectioninfo: this.getSelectionInfo()
         });
         break;
       }
 
       case "Forms:EndComposition": {
         CompositionManager.endComposition(json.text);
         sendAsyncMessage("Forms:EndComposition:Result:OK", {
           requestId: json.requestId,
+          selectioninfo: this.getSelectionInfo()
         });
         break;
       }
     }
     this._editing = false;
 
   },
 
@@ -752,24 +724,35 @@ let FormAssistant = {
       selectionStart: range[0],
       selectionEnd: range[1],
       textBeforeCursor: textAround.before,
       textAfterCursor: textAround.after,
       changed: changed
     };
   },
 
+  _selectionTimeout: null,
+
   // Notify when the selection range changes
   updateSelection: function fa_updateSelection() {
     if (!this.focusedElement) {
       return;
     }
     let selectionInfo = this.getSelectionInfo();
     if (selectionInfo.changed) {
-      sendAsyncMessage("Forms:SelectionChange", this.getSelectionInfo());
+      // A call to setSelectionRange on input field causes 2 selection changes
+      // one to [0,0] and one to actual value. Both are sent in same tick.
+      // Prevent firing two events in that scenario, always only use the last 1
+      if (this._selectionTimeout) {
+        content.clearTimeout(this._selectionTimeout);
+      }
+
+      this._selectionTimeout = content.setTimeout(function() {
+        sendAsyncMessage("Forms:SelectionChange", selectionInfo);
+      });
     }
   }
 };
 
 FormAssistant.init();
 
 function isContentEditable(element) {
   if (!element) {
--- a/dom/inputmethod/mochitest/mochitest.ini
+++ b/dom/inputmethod/mochitest/mochitest.ini
@@ -8,10 +8,11 @@ support-files =
   file_test_sendkey_cancel.html
   file_test_sms_app.html
 
 [test_basic.html]
 [test_bug944397.html]
 [test_bug949059.html]
 [test_bug960946.html]
 [test_bug978918.html]
+[test_bug1026997.html]
 [test_delete_focused_element.html]
 [test_sendkey_cancel.html]
new file mode 100644
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_bug1026997.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1026997
+-->
+<head>
+  <title>SelectionChange on InputMethod API.</title>
+  <script type="application/javascript;version=1.7" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1026997">Mozilla Bug 1026997</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+inputmethod_setup(function() {
+  runTest();
+});
+
+// The frame script running in file_test_app.html.
+function appFrameScript() {
+  let input = content.document.getElementById('test-input');
+
+  input.focus();
+
+  function next(start, end) {
+    input.setSelectionRange(start, end);
+  }
+
+  addMessageListener("test:KeyBoard:nextSelection", function(event) {
+    let json = event.json;
+    next(json[0], json[1]);
+  });
+}
+
+function runTest() {
+  let actions = [
+    [0, 4],
+    [1, 1],
+    [3, 3],
+    [2, 3]
+  ];
+
+  let counter = 0;
+  let mm = null;
+  let ic = null;
+
+  let im = navigator.mozInputMethod;
+  im.oninputcontextchange = function() {
+    ok(true, 'inputcontextchange event was fired.');
+    im.oninputcontextchange = null;
+
+    ic = im.inputcontext;
+    if (!ic) {
+      ok(false, 'Should have a non-null inputcontext.');
+      inputmethod_cleanup();
+      return;
+    }
+
+    ic.onselectionchange = function() {
+      is(ic.selectionStart, actions[counter][0], "start");
+      is(ic.selectionEnd, actions[counter][1], "end");
+
+      if (++counter === actions.length) {
+        inputmethod_cleanup();
+        return;
+      }
+
+      next();
+    };
+
+    next();
+  };
+
+  // Set current page as an input method.
+  SpecialPowers.wrap(im).setActive(true);
+
+  // Create an app frame to recieve keyboard inputs.
+  let app = document.createElement('iframe');
+  app.src = 'file_test_app.html';
+  app.setAttribute('mozbrowser', true);
+  document.body.appendChild(app);
+  app.addEventListener('mozbrowserloadend', function() {
+    mm = SpecialPowers.getBrowserFrameMessageManager(app);
+    mm.loadFrameScript('data:,(' + appFrameScript.toString() + ')();', false);
+    next();
+  });
+
+  function next() {
+    if (ic && mm) {
+      mm.sendAsyncMessage('test:KeyBoard:nextSelection', actions[counter]);
+    }
+  }
+}
+</script>
+</pre>
+</body>
+</html>
+