Bug 1125934 - Discard redundant NS_COMPOSITION_CHANGE event which is send just before NS_COMPOSITION_END on TSF. r=masayuki
authorTooru Fujisawa <arai_a@mac.com>
Wed, 11 Feb 2015 12:20:02 +0900
changeset 228572 bd14f3a87117fff507d965a8648d76b3254bf0e6
parent 228571 0351fbdeb37c6426184c81bd3ab138a1489032b8
child 228573 797821677a2740f9055c02b8e83bcd3add87b140
push id28264
push usercbook@mozilla.com
push dateWed, 11 Feb 2015 13:58:35 +0000
treeherdermozilla-central@38058cb42a0e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmasayuki
bugs1125934
milestone38.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 1125934 - Discard redundant NS_COMPOSITION_CHANGE event which is send just before NS_COMPOSITION_END on TSF. r=masayuki
dom/events/TextComposition.cpp
widget/TextRange.h
widget/tests/window_composition_text_querycontent.xul
--- a/dom/events/TextComposition.cpp
+++ b/dom/events/TextComposition.cpp
@@ -219,16 +219,27 @@ TextComposition::DispatchCompositionEven
   // composition string empty or didn't have clause information), we don't
   // need to dispatch redundant DOM text event.
   if (dispatchDOMTextEvent &&
       aCompositionEvent->message != NS_COMPOSITION_CHANGE &&
       !mIsComposing && mLastData == aCompositionEvent->mData) {
     dispatchEvent = dispatchDOMTextEvent = false;
   }
 
+  // widget may dispatch redundant NS_COMPOSITION_CHANGE event
+  // which modifies neither composition string, clauses nor caret
+  // position.  In such case, we shouldn't dispatch DOM events.
+  if (dispatchDOMTextEvent &&
+      aCompositionEvent->message == NS_COMPOSITION_CHANGE &&
+      mLastData == aCompositionEvent->mData &&
+      mRanges && aCompositionEvent->mRanges &&
+      mRanges->Equals(*aCompositionEvent->mRanges)) {
+    dispatchEvent = dispatchDOMTextEvent = false;
+  }
+
   if (dispatchDOMTextEvent) {
     if (!MaybeDispatchCompositionUpdate(aCompositionEvent)) {
       return;
     }
   }
 
   if (dispatchEvent) {
     // If the composition event should cause a DOM text event, we should
--- a/widget/TextRange.h
+++ b/widget/TextRange.h
@@ -79,17 +79,17 @@ struct TextRangeStyle
   }
 
   bool IsNoChangeStyle() const
   {
     return !IsForegroundColorDefined() && !IsBackgroundColorDefined() &&
            IsLineStyleDefined() && mLineStyle == LINESTYLE_NONE;
   }
 
-  bool Equals(const TextRangeStyle& aOther)
+  bool Equals(const TextRangeStyle& aOther) const
   {
     if (mDefinedStyles != aOther.mDefinedStyles)
       return false;
     if (IsLineStyleDefined() && (mLineStyle != aOther.mLineStyle ||
                                  !mIsBoldLine != !aOther.mIsBoldLine))
       return false;
     if (IsForegroundColorDefined() &&
         (mForegroundColor != aOther.mForegroundColor))
@@ -98,22 +98,22 @@ struct TextRangeStyle
         (mBackgroundColor != aOther.mBackgroundColor))
       return false;
     if (IsUnderlineColorDefined() &&
         (mUnderlineColor != aOther.mUnderlineColor))
       return false;
     return true;
   }
 
-  bool operator !=(const TextRangeStyle &aOther)
+  bool operator !=(const TextRangeStyle &aOther) const
   {
     return !Equals(aOther);
   }
 
-  bool operator ==(const TextRangeStyle &aOther)
+  bool operator ==(const TextRangeStyle &aOther) const
   {
     return Equals(aOther);
   }
 
   uint8_t mDefinedStyles;
   uint8_t mLineStyle;        // DEFINED_LINESTYLE
 
   bool mIsBoldLine;  // DEFINED_LINESTYLE
@@ -161,16 +161,24 @@ struct TextRange
 
   bool IsClause() const
   {
     MOZ_ASSERT(mRangeType >= NS_TEXTRANGE_CARETPOSITION &&
                  mRangeType <= NS_TEXTRANGE_SELECTEDCONVERTEDTEXT,
                "Invalid range type");
     return mRangeType != NS_TEXTRANGE_CARETPOSITION;
   }
+
+  bool Equals(const TextRange& aOther) const
+  {
+    return mStartOffset == aOther.mStartOffset &&
+           mEndOffset == aOther.mEndOffset &&
+           mRangeType == aOther.mRangeType &&
+           mRangeStyle == aOther.mRangeStyle;
+  }
 };
 
 /******************************************************************************
  * mozilla::TextRangeArray
  ******************************************************************************/
 class TextRangeArray MOZ_FINAL : public nsAutoTArray<TextRange, 10>
 {
   ~TextRangeArray() {}
@@ -196,13 +204,27 @@ public:
       const TextRange& range = ElementAt(i);
       if (range.mRangeType == NS_TEXTRANGE_SELECTEDRAWTEXT ||
           range.mRangeType == NS_TEXTRANGE_SELECTEDCONVERTEDTEXT) {
         return range.mStartOffset;
       }
     }
     return 0;
   }
+
+  bool Equals(const TextRangeArray& aOther) const
+  {
+    size_t len = Length();
+    if (len != aOther.Length()) {
+      return false;
+    }
+    for (size_t i = 0; i < len; i++) {
+      if (!ElementAt(i).Equals(aOther.ElementAt(i))) {
+        return false;
+      }
+    }
+    return true;
+  }
 };
 
 } // namespace mozilla
 
 #endif // mozilla_TextRage_h_
--- a/widget/tests/window_composition_text_querycontent.xul
+++ b/widget/tests/window_composition_text_querycontent.xul
@@ -2973,16 +2973,188 @@ function runIsComposingTest()
   textarea.removeEventListener("keyup", eventHandler, true);
   textarea.removeEventListener("input", eventHandler, true);
   textarea.removeEventListener("compositionstart", onComposition, true);
   textarea.removeEventListener("compositionend", onComposition, true);
 
   textarea.value = "";
 }
 
+function runRedundantChangeTest()
+{
+  textarea.focus();
+
+  var result = {};
+  function clearResult()
+  {
+    result = { compositionupdate: false, compositionend: false, text: false, input: false };
+  }
+
+  function handler(aEvent)
+  {
+    result[aEvent.type] = true;
+  }
+
+  textarea.addEventListener("compositionupdate", handler, true);
+  textarea.addEventListener("compositionend", handler, true);
+  textarea.addEventListener("input", handler, true);
+  textarea.addEventListener("text", handler, true);
+
+  textarea.value = "";
+
+  // synthesize change event
+  clearResult();
+  synthesizeCompositionChange(
+    { "composition":
+      { "string": "\u3042",
+        "clauses":
+        [
+          { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+        ]
+      },
+      "caret": { "start": 1, "length": 0 }
+    });
+
+  is(result.compositionupdate, true, "runRedundantChangeTest: compositionupdate should be fired after synthesizing composition change #1");
+  is(result.compositionend, false, "runRedundantChangeTest: compositionend shouldn't be fired after synthesizing composition change #1");
+  is(result.text, true, "runRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string #1");
+  is(result.input, true, "runRedundantChangeTest: input should be fired after synthesizing composition change #1");
+  is(textarea.value, "\u3042", "runRedundantChangeTest: textarea has uncommitted string #1");
+
+  // synthesize another change event
+  clearResult();
+  synthesizeCompositionChange(
+    { "composition":
+      { "string": "\u3042\u3044",
+        "clauses":
+        [
+          { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+        ]
+      },
+      "caret": { "start": 2, "length": 0 }
+    });
+
+  is(result.compositionupdate, true, "runRedundantChangeTest: compositionupdate should be fired after synthesizing composition change #2");
+  is(result.compositionend, false, "runRedundantChangeTest: compositionend shouldn't be fired after synthesizing composition change #2");
+  is(result.text, true, "runRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string #2");
+  is(result.input, true, "runRedundantChangeTest: input should be fired after synthesizing composition change #2");
+  is(textarea.value, "\u3042\u3044", "runRedundantChangeTest: textarea has uncommitted string #2");
+
+  // synthesize same change event again
+  clearResult();
+  synthesizeCompositionChange(
+    { "composition":
+      { "string": "\u3042\u3044",
+        "clauses":
+        [
+          { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+        ]
+      },
+      "caret": { "start": 2, "length": 0 }
+    });
+
+  is(result.compositionupdate, false, "runRedundantChangeTest: compositionupdate shouldn't be fired after synthesizing composition change again");
+  is(result.compositionend, false, "runRedundantChangeTest: compositionend shouldn't be fired after synthesizing composition change again");
+  is(result.text, false, "runRedundantChangeTest: text shouldn't be fired after synthesizing composition change again because it's dispatched when there is composing string");
+  is(result.input, false, "runRedundantChangeTest: input shouldn't be fired after synthesizing composition change again");
+  is(textarea.value, "\u3042\u3044", "runRedundantChangeTest: textarea has uncommitted string #3");
+
+  synthesizeComposition({ type: "compositioncommit" });
+
+  textarea.removeEventListener("compositionupdate", handler, true);
+  textarea.removeEventListener("compositionend", handler, true);
+  textarea.removeEventListener("input", handler, true);
+  textarea.removeEventListener("text", handler, true);
+}
+
+function runNotRedundantChangeTest()
+{
+  textarea.focus();
+
+  var result = {};
+  function clearResult()
+  {
+    result = { compositionupdate: false, compositionend: false, text: false, input: false };
+  }
+
+  function handler(aEvent)
+  {
+    result[aEvent.type] = true;
+  }
+
+  textarea.addEventListener("compositionupdate", handler, true);
+  textarea.addEventListener("compositionend", handler, true);
+  textarea.addEventListener("input", handler, true);
+  textarea.addEventListener("text", handler, true);
+
+  textarea.value = "abcde";
+
+  // synthesize change event with non-null ranges
+  clearResult();
+  synthesizeCompositionChange(
+    { "composition":
+      { "string": "ABCDE",
+        "clauses":
+        [
+          { "length": 5, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+        ]
+      },
+      "caret": { "start": 5, "length": 0 }
+    });
+
+  is(result.compositionupdate, true, "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with non-null ranges");
+  is(result.compositionend, false, "runNotRedundantChangeTest: compositionend shouldn't be fired after synthesizing composition change with non-null ranges");
+  is(result.text, true, "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges");
+  is(result.input, true, "runNotRedundantChangeTest: input should be fired after synthesizing composition change with non-null ranges");
+  is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #1");
+
+  // synthesize change event with null ranges
+  clearResult();
+  synthesizeCompositionChange(
+    { "composition":
+      { "string": "",
+        "clauses":
+        [
+          { "length": 0, "attr": 0 }
+        ]
+      },
+    });
+  is(result.compositionupdate, true, "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with null ranges after non-null ranges");
+  is(result.compositionend, false, "runNotRedundantChangeTest: compositionend shouldn't be fired after synthesizing composition change with null ranges after non-null ranges");
+  is(result.text, true, "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with null ranges after non-null ranges");
+  is(result.input, false, "runNotRedundantChangeTest: input shouldn't be fired after synthesizing composition change with null ranges after non-null ranges");
+  is(textarea.value, "abcde", "runNotRedundantChangeTest: textarea doesn't have uncommitted string");
+
+  // synthesize change event with non-null ranges
+  clearResult();
+  synthesizeCompositionChange(
+    { "composition":
+      { "string": "ABCDE",
+        "clauses":
+        [
+          { "length": 5, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+        ]
+      },
+      "caret": { "start": 5, "length": 0 }
+    });
+
+  is(result.compositionupdate, true, "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with non-null ranges after null ranges");
+  is(result.compositionend, false, "runNotRedundantChangeTest: compositionend shouldn't be fired after synthesizing composition change with non-null ranges after null ranges");
+  is(result.text, true, "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges after null ranges");
+  is(result.input, true, "runNotRedundantChangeTest: input should be fired after synthesizing composition change with non-null ranges after null ranges");
+  is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #2");
+
+  synthesizeComposition({ type: "compositioncommit", data: "" });
+
+  textarea.removeEventListener("compositionupdate", handler, true);
+  textarea.removeEventListener("compositionend", handler, true);
+  textarea.removeEventListener("input", handler, true);
+  textarea.removeEventListener("text", handler, true);
+}
+
 function runRemoveContentTest(aCallback)
 {
   var events = [];
   function eventHandler(aEvent)
   {
     events.push(aEvent);
   }
   textarea.addEventListener("compositionstart", eventHandler, true);
@@ -3545,16 +3717,18 @@ function runTest()
   runCompositionTest();
   runCompositionEventTest();
   runCharAtPointTest(textarea, "textarea in the document");
   runCharAtPointAtOutsideTest();
   runBug722639Test();
   runForceCommitTest();
   runBug811755Test();
   runIsComposingTest();
+  runRedundantChangeTest();
+  runNotRedundantChangeTest();
   runAsyncForceCommitTest(function () {
     runRemoveContentTest(function () {
       runFrameTest();
       runPanelTest();
       runMaxLengthTest();
     });
   });
 }