Bug 1275528 part.1 Support a way to query content relative to insertion point r?smaug draft
authorMasayuki Nakano <masayuki@d-toybox.com>
Thu, 16 Jun 2016 14:10:49 +0900
changeset 380000 3cd96da5b7d335833fad504c20fd9e6d3a57bddd
parent 379999 6698511e477f8a18c1a52aa479d37dd5ae8f0826
child 380001 48e9de3ea7f2336b63fdf5d19736277abe90a55c
push id21107
push usermasayuki@d-toybox.com
push dateMon, 20 Jun 2016 10:09:39 +0000
reviewerssmaug
bugs1275528
milestone50.0a1
Bug 1275528 part.1 Support a way to query content relative to insertion point r?smaug Native IME handler may want to query content relative to start of selection (or composition if there is it). Additionally, in e10s mode, insertion point in actual content may be different from the cache in parent. Therefore, in some cases, it does make sense to query content with offset relative to start of selection or composition. This patch implements it simply and only in non-e10s mode. Additionally, this fixes a bug of nsQueryContentEventResult::GetOffset() which hasn't been accepted its calls even if the event message is valid (eQueryTextContent, eQueryTextRect and eQueryCaretRect). MozReview-Commit-ID: 34I7vyTUAgO
dom/base/nsDOMWindowUtils.cpp
dom/base/nsQueryContentEventResult.cpp
dom/events/ContentEventHandler.cpp
dom/interfaces/base/nsIDOMWindowUtils.idl
testing/mochitest/tests/SimpleTest/ChromeUtils.js
testing/mochitest/tests/SimpleTest/EventUtils.js
widget/TextEvents.h
widget/tests/window_composition_text_querycontent.xul
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -1818,17 +1818,17 @@ InitEvent(WidgetGUIEvent& aEvent, Layout
   if (aPt) {
     aEvent.mRefPoint = *aPt;
   }
   aEvent.mTime = PR_IntervalNow();
 }
 
 NS_IMETHODIMP
 nsDOMWindowUtils::SendQueryContentEvent(uint32_t aType,
-                                        uint32_t aOffset, uint32_t aLength,
+                                        int64_t aOffset, uint32_t aLength,
                                         int32_t aX, int32_t aY,
                                         uint32_t aAdditionalFlags,
                                         nsIQueryContentEventResult **aResult)
 {
   *aResult = nullptr;
 
   nsCOMPtr<nsPIDOMWindowOuter> window = do_QueryReferent(mWindow);
   NS_ENSURE_TRUE(window, NS_ERROR_FAILURE);
@@ -1920,23 +1920,39 @@ nsDOMWindowUtils::SendQueryContentEvent(
   if (selectionType != SelectionType::eNormal &&
       message != eQuerySelectedText) {
     return NS_ERROR_INVALID_ARG;
   }
 
   nsCOMPtr<nsIWidget> targetWidget = widget;
   LayoutDeviceIntPoint pt(aX, aY);
 
-  bool useNativeLineBreak =
+  WidgetQueryContentEvent::Options options;
+  options.mUseNativeLineBreak =
     !(aAdditionalFlags & QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK);
+  options.mRelativeToInsertionPoint =
+    (aAdditionalFlags &
+       QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT) != 0;
+  if (options.mRelativeToInsertionPoint) {
+    switch (message) {
+      case eQueryTextContent:
+      case eQueryCaretRect:
+      case eQueryTextRect:
+        break;
+      default:
+        return NS_ERROR_INVALID_ARG;
+    }
+  } else if (aOffset < 0) {
+    return NS_ERROR_INVALID_ARG;
+  }
 
   if (message == eQueryCharacterAtPoint) {
     // Looking for the widget at the point.
     WidgetQueryContentEvent dummyEvent(true, eQueryContentState, widget);
-    dummyEvent.mUseNativeLineBreak = useNativeLineBreak;
+    dummyEvent.Init(options);
     InitEvent(dummyEvent, &pt);
     nsIFrame* popupFrame =
       nsLayoutUtils::GetPopupFrameForEventCoordinates(presContext->GetRootPresContext(), &dummyEvent);
 
     LayoutDeviceIntRect widgetBounds;
     nsresult rv = widget->GetClientBounds(widgetBounds);
     NS_ENSURE_SUCCESS(rv, rv);
     widgetBounds.MoveTo(0, 0);
@@ -1953,29 +1969,29 @@ nsDOMWindowUtils::SendQueryContentEvent(
 
   pt += widget->WidgetToScreenOffset() - targetWidget->WidgetToScreenOffset();
 
   WidgetQueryContentEvent queryEvent(true, message, targetWidget);
   InitEvent(queryEvent, &pt);
 
   switch (message) {
     case eQueryTextContent:
-      queryEvent.InitForQueryTextContent(aOffset, aLength, useNativeLineBreak);
+      queryEvent.InitForQueryTextContent(aOffset, aLength, options);
       break;
     case eQueryCaretRect:
-      queryEvent.InitForQueryCaretRect(aOffset, useNativeLineBreak);
+      queryEvent.InitForQueryCaretRect(aOffset, options);
       break;
     case eQueryTextRect:
-      queryEvent.InitForQueryTextRect(aOffset, aLength, useNativeLineBreak);
+      queryEvent.InitForQueryTextRect(aOffset, aLength, options);
       break;
     case eQuerySelectedText:
-      queryEvent.InitForQuerySelectedText(selectionType, useNativeLineBreak);
+      queryEvent.InitForQuerySelectedText(selectionType, options);
       break;
     default:
-      queryEvent.mUseNativeLineBreak = useNativeLineBreak;
+      queryEvent.Init(options);
       break;
   }
 
   nsEventStatus status;
   nsresult rv = targetWidget->DispatchEvent(&queryEvent, status);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsQueryContentEventResult* result = new nsQueryContentEventResult();
--- a/dom/base/nsQueryContentEventResult.cpp
+++ b/dom/base/nsQueryContentEventResult.cpp
@@ -6,16 +6,47 @@
 
 #include "nsQueryContentEventResult.h"
 #include "nsIWidget.h"
 #include "nsPoint.h"
 #include "mozilla/TextEvents.h"
 
 using namespace mozilla;
 
+/******************************************************************************
+ * Is*PropertyAvailable() methods which check if the property is available
+ * (valid) with the event message.
+ ******************************************************************************/
+
+static bool IsNotFoundPropertyAvailable(EventMessage aEventMessage)
+{
+  return aEventMessage == eQuerySelectedText ||
+         aEventMessage == eQueryCharacterAtPoint;
+}
+
+static bool IsOffsetPropertyAvailable(EventMessage aEventMessage)
+{
+  return aEventMessage == eQueryTextContent ||
+         aEventMessage == eQueryTextRect ||
+         aEventMessage == eQueryCaretRect ||
+         IsNotFoundPropertyAvailable(aEventMessage);
+}
+
+static bool IsRectRelatedPropertyAvailable(EventMessage aEventMessage)
+{
+  return aEventMessage == eQueryCaretRect ||
+         aEventMessage == eQueryTextRect ||
+         aEventMessage == eQueryEditorRect ||
+         aEventMessage == eQueryCharacterAtPoint;
+}
+
+/******************************************************************************
+ * nsQueryContentEventResult
+ ******************************************************************************/
+
 NS_INTERFACE_MAP_BEGIN(nsQueryContentEventResult)
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIQueryContentEventResult)
   NS_INTERFACE_MAP_ENTRY(nsIQueryContentEventResult)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_ADDREF(nsQueryContentEventResult)
 NS_IMPL_RELEASE(nsQueryContentEventResult)
 
@@ -25,22 +56,43 @@ nsQueryContentEventResult::nsQueryConten
 {
 }
 
 nsQueryContentEventResult::~nsQueryContentEventResult()
 {
 }
 
 NS_IMETHODIMP
-nsQueryContentEventResult::GetOffset(uint32_t *aOffset)
+nsQueryContentEventResult::GetOffset(uint32_t* aOffset)
 {
-  bool notFound;
-  nsresult rv = GetNotFound(&notFound);
-  NS_ENSURE_SUCCESS(rv, rv);
-  NS_ENSURE_TRUE(!notFound, NS_ERROR_NOT_AVAILABLE);
+  if (NS_WARN_IF(!mSucceeded)) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  if (NS_WARN_IF(!IsOffsetPropertyAvailable(mEventMessage))) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  // With some event message, both offset and notFound properties are available.
+  // In that case, offset value may mean "not found".  If so, this method
+  // shouldn't return mOffset as the result because it's a special value for
+  // "not found".
+  if (IsNotFoundPropertyAvailable(mEventMessage)) {
+    bool notFound;
+    nsresult rv = GetNotFound(&notFound);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv; // Just an unexpected case...
+    }
+    // As said above, if mOffset means "not found", offset property shouldn't
+    // return its value without any errors.
+    if (NS_WARN_IF(notFound)) {
+      return NS_ERROR_NOT_AVAILABLE;
+    }
+  }
+
   *aOffset = mOffset;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsQueryContentEventResult::GetTentativeCaretOffset(uint32_t* aOffset)
 {
   bool notFound;
@@ -50,68 +102,60 @@ nsQueryContentEventResult::GetTentativeC
   }
   if (NS_WARN_IF(notFound)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
   *aOffset = mTentativeCaretOffset;
   return NS_OK;
 }
 
-static bool IsRectEnabled(EventMessage aEventMessage)
-{
-  return aEventMessage == eQueryCaretRect ||
-         aEventMessage == eQueryTextRect ||
-         aEventMessage == eQueryEditorRect ||
-         aEventMessage == eQueryCharacterAtPoint;
-}
-
 NS_IMETHODIMP
 nsQueryContentEventResult::GetReversed(bool *aReversed)
 {
   NS_ENSURE_TRUE(mSucceeded, NS_ERROR_NOT_AVAILABLE);
   NS_ENSURE_TRUE(mEventMessage == eQuerySelectedText, NS_ERROR_NOT_AVAILABLE);
   *aReversed = mReversed;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsQueryContentEventResult::GetLeft(int32_t *aLeft)
 {
   NS_ENSURE_TRUE(mSucceeded, NS_ERROR_NOT_AVAILABLE);
-  NS_ENSURE_TRUE(IsRectEnabled(mEventMessage),
+  NS_ENSURE_TRUE(IsRectRelatedPropertyAvailable(mEventMessage),
                  NS_ERROR_NOT_AVAILABLE);
   *aLeft = mRect.x;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsQueryContentEventResult::GetWidth(int32_t *aWidth)
 {
   NS_ENSURE_TRUE(mSucceeded, NS_ERROR_NOT_AVAILABLE);
-  NS_ENSURE_TRUE(IsRectEnabled(mEventMessage),
+  NS_ENSURE_TRUE(IsRectRelatedPropertyAvailable(mEventMessage),
                  NS_ERROR_NOT_AVAILABLE);
   *aWidth = mRect.width;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsQueryContentEventResult::GetTop(int32_t *aTop)
 {
   NS_ENSURE_TRUE(mSucceeded, NS_ERROR_NOT_AVAILABLE);
-  NS_ENSURE_TRUE(IsRectEnabled(mEventMessage),
+  NS_ENSURE_TRUE(IsRectRelatedPropertyAvailable(mEventMessage),
                  NS_ERROR_NOT_AVAILABLE);
   *aTop = mRect.y;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsQueryContentEventResult::GetHeight(int32_t *aHeight)
 {
   NS_ENSURE_TRUE(mSucceeded, NS_ERROR_NOT_AVAILABLE);
-  NS_ENSURE_TRUE(IsRectEnabled(mEventMessage),
+  NS_ENSURE_TRUE(IsRectRelatedPropertyAvailable(mEventMessage),
                  NS_ERROR_NOT_AVAILABLE);
   *aHeight = mRect.height;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsQueryContentEventResult::GetText(nsAString &aText)
 {
@@ -127,22 +171,22 @@ NS_IMETHODIMP
 nsQueryContentEventResult::GetSucceeded(bool *aSucceeded)
 {
   NS_ENSURE_TRUE(mEventMessage != eVoidEvent, NS_ERROR_NOT_INITIALIZED);
   *aSucceeded = mSucceeded;
   return NS_OK;
 }
 
 NS_IMETHODIMP
-nsQueryContentEventResult::GetNotFound(bool *aNotFound)
+nsQueryContentEventResult::GetNotFound(bool* aNotFound)
 {
-  NS_ENSURE_TRUE(mSucceeded, NS_ERROR_NOT_AVAILABLE);
-  NS_ENSURE_TRUE(mEventMessage == eQuerySelectedText ||
-                 mEventMessage == eQueryCharacterAtPoint,
-                 NS_ERROR_NOT_AVAILABLE);
+  if (NS_WARN_IF(!mSucceeded) ||
+      NS_WARN_IF(!IsNotFoundPropertyAvailable(mEventMessage))) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
   *aNotFound = (mOffset == WidgetQueryContentEvent::NOT_FOUND);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsQueryContentEventResult::GetTentativeCaretOffsetNotFound(bool* aNotFound)
 {
   if (NS_WARN_IF(!mSucceeded)) {
@@ -162,17 +206,18 @@ nsQueryContentEventResult::SetEventResul
   mEventMessage = aEvent.mMessage;
   mSucceeded = aEvent.mSucceeded;
   mReversed = aEvent.mReply.mReversed;
   mRect = aEvent.mReply.mRect;
   mOffset = aEvent.mReply.mOffset;
   mTentativeCaretOffset = aEvent.mReply.mTentativeCaretOffset;
   mString = aEvent.mReply.mString;
 
-  if (!IsRectEnabled(mEventMessage) || !aWidget || !mSucceeded) {
+  if (!IsRectRelatedPropertyAvailable(mEventMessage) ||
+      !aWidget || !mSucceeded) {
     return;
   }
 
   nsIWidget* topWidget = aWidget->GetTopLevelWidget();
   if (!topWidget || topWidget == aWidget) {
     return;
   }
 
--- a/dom/events/ContentEventHandler.cpp
+++ b/dom/events/ContentEventHandler.cpp
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "ContentEventHandler.h"
 #include "mozilla/IMEStateManager.h"
+#include "mozilla/TextComposition.h"
 #include "mozilla/TextEvents.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/HTMLUnknownElement.h"
 #include "mozilla/dom/Selection.h"
 #include "nsCaret.h"
 #include "nsCOMPtr.h"
 #include "nsContentUtils.h"
 #include "nsCopySupport.h"
@@ -264,28 +265,60 @@ ContentEventHandler::InitCommon(Selectio
   return NS_OK;
 }
 
 nsresult
 ContentEventHandler::Init(WidgetQueryContentEvent* aEvent)
 {
   NS_ASSERTION(aEvent, "aEvent must not be null");
 
+  if (NS_WARN_IF(!aEvent->mInput.IsValidOffset()) ||
+      NS_WARN_IF(!aEvent->mInput.IsValidEventMessage(aEvent->mMessage))) {
+    return NS_ERROR_FAILURE;
+  }
+
   // Note that we should ignore WidgetQueryContentEvent::Input::mSelectionType
   // if the event isn't eQuerySelectedText.
   SelectionType selectionType =
     aEvent->mMessage == eQuerySelectedText ? aEvent->mInput.mSelectionType :
                                              SelectionType::eNormal;
   if (NS_WARN_IF(selectionType == SelectionType::eNone)) {
     return NS_ERROR_FAILURE;
   }
 
   nsresult rv = InitCommon(selectionType);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  // Be aware, WidgetQueryContentEvent::mInput::mOffset should be made absolute
+  // offset before sending it to ContentEventHandler because querying selection
+  // every time may be expensive.  So, if the caller caches selection, it
+  // should initialize the event with the cached value.
+  if (aEvent->mInput.mRelativeToInsertionPoint) {
+    MOZ_ASSERT(selectionType == SelectionType::eNormal);
+    RefPtr<TextComposition> composition =
+      IMEStateManager::GetTextCompositionFor(aEvent->mWidget);
+    if (composition) {
+      uint32_t compositionStart = composition->NativeOffsetOfStartComposition();
+      if (NS_WARN_IF(!aEvent->mInput.MakeOffsetAbsolute(compositionStart))) {
+        return NS_ERROR_FAILURE;
+      }
+    } else {
+      LineBreakType lineBreakType = GetLineBreakType(aEvent);
+      uint32_t selectionStart = 0;
+      rv = GetFlatTextLengthBefore(mFirstSelectedRange,
+                                   &selectionStart, lineBreakType);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return NS_ERROR_FAILURE;
+      }
+      if (NS_WARN_IF(!aEvent->mInput.MakeOffsetAbsolute(selectionStart))) {
+        return NS_ERROR_FAILURE;
+      }
+    }
+  }
+
   aEvent->mSucceeded = false;
 
   aEvent->mReply.mContentsRoot = mRootContent.get();
 
   aEvent->mReply.mHasSelection = !mSelection->IsCollapsed();
 
   nsRect r;
   nsIFrame* frame = nsCaret::GetGeometry(mSelection, &r);
@@ -1692,17 +1725,18 @@ ContentEventHandler::OnQueryCharacterAtP
                                 NodePosition(contentOffsets),
                                 mRootContent, &offset,
                                 GetLineBreakType(aEvent));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   WidgetQueryContentEvent textRect(true, eQueryTextRect, aEvent->mWidget);
-  textRect.InitForQueryTextRect(offset, 1, aEvent->mUseNativeLineBreak);
+  WidgetQueryContentEvent::Options options(*aEvent);
+  textRect.InitForQueryTextRect(offset, 1, options);
   rv = OnQueryTextRect(&textRect);
   NS_ENSURE_SUCCESS(rv, rv);
   NS_ENSURE_TRUE(textRect.mSucceeded, NS_ERROR_FAILURE);
 
   // currently, we don't need to get the actual text.
   aEvent->mReply.mOffset = offset;
   aEvent->mReply.mRect = textRect.mReply.mRect;
   aEvent->mSucceeded = true;
--- a/dom/interfaces/base/nsIDOMWindowUtils.idl
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
@@ -1098,26 +1098,35 @@ interface nsIDOMWindowUtils : nsISupport
   const unsigned long QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDCONVERTEDTEXT =
                                                                          0x0020;
   const unsigned long QUERY_CONTENT_FLAG_SELECTION_ACCESSIBILITY       = 0x0040;
   const unsigned long QUERY_CONTENT_FLAG_SELECTION_FIND                = 0x0080;
   const unsigned long QUERY_CONTENT_FLAG_SELECTION_URLSECONDARY        = 0x0100;
   const unsigned long QUERY_CONTENT_FLAG_SELECTION_URLSTRIKEOUT        = 0x0200;
 
   /**
+   * One of sendQueryContentEvent()'s aAdditionalFlags.  If this is specified,
+   * aOffset is relative to start of selection or composition.
+   * Note that this is supported only when QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK
+   * is not specified for now.
+   */
+  const unsigned long QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT =
+                                                                         0x0400;
+
+  /**
    * Synthesize a query content event. Note that the result value returned here
    * is in LayoutDevice pixels rather than CSS pixels.
    *
    * @param aType  One of the following const values.  And see also each comment
    *               for the other parameters and the result.
    * @param aAdditionalFlags See the description of QUERY_CONTENT_FLAG_*.
    */
   nsIQueryContentEventResult sendQueryContentEvent(
                                in unsigned long aType,
-                               in unsigned long aOffset,
+                               in long long aOffset,
                                in unsigned long aLength,
                                in long aX,
                                in long aY,
                                [optional] in unsigned long aAdditionalFlags);
 
   /**
    * QUERY_SELECTED_TEXT queries the first selection range's information.
    *
--- a/testing/mochitest/tests/SimpleTest/ChromeUtils.js
+++ b/testing/mochitest/tests/SimpleTest/ChromeUtils.js
@@ -8,38 +8,16 @@
  */
 
 const EventUtils = {};
 const scriptLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"].
                    getService(Components.interfaces.mozIJSSubScriptLoader);
 scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
 
 /**
- * Synthesize a query text content event.
- *
- * @param aOffset  The character offset.  0 means the first character in the
- *                 selection root.
- * @param aLength  The length of getting text.  If the length is too long,
- *                 the extra length is ignored.
- * @param aWindow  Optional (If null, current |window| will be used)
- * @return         An nsIQueryContentEventResult object.  If this failed,
- *                 the result might be null.
- */
-function synthesizeQueryTextContent(aOffset, aLength, aWindow)
-{
-  var utils = _getDOMWindowUtils(aWindow);
-  if (!utils) {
-    return nullptr;
-  }
-  return utils.sendQueryContentEvent(utils.QUERY_TEXT_CONTENT,
-                                     aOffset, aLength, 0, 0,
-                                     QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK);
-}
-
-/**
  * Synthesize a query text rect event.
  *
  * @param aOffset  The character offset.  0 means the first character in the
  *                 selection root.
  * @param aLength  The length of the text.  If the length is too long,
  *                 the extra length is ignored.
  * @param aWindow  Optional (If null, current |window| will be used)
  * @return         An nsIQueryContentEventResult object.  If this failed,
--- a/testing/mochitest/tests/SimpleTest/EventUtils.js
+++ b/testing/mochitest/tests/SimpleTest/EventUtils.js
@@ -1564,21 +1564,50 @@ const QUERY_CONTENT_FLAG_SELECTION_IME_R
 const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDRAWTEXT       = 0x0008;
 const QUERY_CONTENT_FLAG_SELECTION_IME_CONVERTEDTEXT         = 0x0010;
 const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDCONVERTEDTEXT = 0x0020;
 const QUERY_CONTENT_FLAG_SELECTION_ACCESSIBILITY             = 0x0040;
 const QUERY_CONTENT_FLAG_SELECTION_FIND                      = 0x0080;
 const QUERY_CONTENT_FLAG_SELECTION_URLSECONDARY              = 0x0100;
 const QUERY_CONTENT_FLAG_SELECTION_URLSTRIKEOUT              = 0x0200;
 
+const QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT  = 0x0400;
+
 const SELECTION_SET_FLAG_USE_NATIVE_LINE_BREAK          = 0x0000;
 const SELECTION_SET_FLAG_USE_XP_LINE_BREAK              = 0x0001;
 const SELECTION_SET_FLAG_REVERSE                        = 0x0002;
 
 /**
+ * Synthesize a query text content event.
+ *
+ * @param aOffset  The character offset.  0 means the first character in the
+ *                 selection root.
+ * @param aLength  The length of getting text.  If the length is too long,
+ *                 the extra length is ignored.
+ * @param aIsRelative   Optional (If true, aOffset is relative to start of
+ *                      composition if there is, or start of selection.)
+ * @param aWindow  Optional (If null, current |window| will be used)
+ * @return         An nsIQueryContentEventResult object.  If this failed,
+ *                 the result might be null.
+ */
+function synthesizeQueryTextContent(aOffset, aLength, aIsRelative, aWindow)
+{
+  var utils = _getDOMWindowUtils(aWindow);
+  if (!utils) {
+    return nullptr;
+  }
+  var flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK;
+  if (aIsRelative === true) {
+    flags |= QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT;
+  }
+  return utils.sendQueryContentEvent(utils.QUERY_TEXT_CONTENT,
+                                     aOffset, aLength, 0, 0, flags);
+}
+
+/**
  * Synthesize a query selected text event.
  *
  * @param aSelectionType    Optional, one of QUERY_CONTENT_FLAG_SELECTION_*.
  *                          If null, QUERY_CONTENT_FLAG_SELECTION_NORMAL will
  *                          be used.
  * @param aWindow  Optional (If null, current |window| will be used)
  * @return         An nsIQueryContentEventResult object.  If this failed,
  *                 the result might be null.
--- a/widget/TextEvents.h
+++ b/widget/TextEvents.h
@@ -634,52 +634,80 @@ public:
   virtual WidgetEvent* Duplicate() const override
   {
     // This event isn't an internal event of any DOM event.
     NS_ASSERTION(!IsAllowedToDispatchDOMEvent(),
       "WidgetQueryContentEvent needs to support Duplicate()");
     MOZ_CRASH("WidgetQueryContentEvent doesn't support Duplicate()");
   }
 
-  void InitForQueryTextContent(uint32_t aOffset, uint32_t aLength,
-                               bool aUseNativeLineBreak = true)
+  struct Options final
+  {
+    bool mUseNativeLineBreak;
+    bool mRelativeToInsertionPoint;
+
+    explicit Options()
+      : mUseNativeLineBreak(true)
+      , mRelativeToInsertionPoint(false)
+    {
+    }
+
+    explicit Options(const WidgetQueryContentEvent& aEvent)
+      : mUseNativeLineBreak(aEvent.mUseNativeLineBreak)
+      , mRelativeToInsertionPoint(aEvent.mInput.mRelativeToInsertionPoint)
+    {
+    }
+  };
+
+  void Init(const Options& aOptions)
+  {
+    mUseNativeLineBreak = aOptions.mUseNativeLineBreak;
+    mInput.mRelativeToInsertionPoint = aOptions.mRelativeToInsertionPoint;
+    MOZ_ASSERT(mInput.IsValidEventMessage(mMessage));
+  }
+
+  void InitForQueryTextContent(int64_t aOffset, uint32_t aLength,
+                               const Options& aOptions = Options())
   {
     NS_ASSERTION(mMessage == eQueryTextContent,
                  "wrong initializer is called");
     mInput.mOffset = aOffset;
     mInput.mLength = aLength;
-    mUseNativeLineBreak = aUseNativeLineBreak;
+    Init(aOptions);
+    MOZ_ASSERT(mInput.IsValidOffset());
   }
 
-  void InitForQueryCaretRect(uint32_t aOffset,
-                             bool aUseNativeLineBreak = true)
+  void InitForQueryCaretRect(int64_t aOffset,
+                             const Options& aOptions = Options())
   {
     NS_ASSERTION(mMessage == eQueryCaretRect,
                  "wrong initializer is called");
     mInput.mOffset = aOffset;
-    mUseNativeLineBreak = aUseNativeLineBreak;
+    Init(aOptions);
+    MOZ_ASSERT(mInput.IsValidOffset());
   }
 
-  void InitForQueryTextRect(uint32_t aOffset, uint32_t aLength,
-                            bool aUseNativeLineBreak = true)
+  void InitForQueryTextRect(int64_t aOffset, uint32_t aLength,
+                            const Options& aOptions = Options())
   {
     NS_ASSERTION(mMessage == eQueryTextRect,
                  "wrong initializer is called");
     mInput.mOffset = aOffset;
     mInput.mLength = aLength;
-    mUseNativeLineBreak = aUseNativeLineBreak;
+    Init(aOptions);
+    MOZ_ASSERT(mInput.IsValidOffset());
   }
 
   void InitForQuerySelectedText(SelectionType aSelectionType,
-                                bool aUseNativeLineBreak = true)
+                                const Options& aOptions = Options())
   {
     MOZ_ASSERT(mMessage == eQuerySelectedText);
     MOZ_ASSERT(aSelectionType != SelectionType::eNone);
     mInput.mSelectionType = aSelectionType;
-    mUseNativeLineBreak = aUseNativeLineBreak;
+    Init(aOptions);
   }
 
   void InitForQueryDOMWidgetHittest(const mozilla::LayoutDeviceIntPoint& aPoint)
   {
     NS_ASSERTION(mMessage == eQueryDOMWidgetHittest,
                  "wrong initializer is called");
     mRefPoint = aPoint;
   }
@@ -721,26 +749,71 @@ public:
   {
     uint32_t EndOffset() const
     {
       CheckedInt<uint32_t> endOffset =
         CheckedInt<uint32_t>(mOffset) + mLength;
       return NS_WARN_IF(!endOffset.isValid()) ? UINT32_MAX : endOffset.value();
     }
 
-    uint32_t mOffset;
+    int64_t mOffset;
     uint32_t mLength;
     SelectionType mSelectionType;
+    // If mOffset is true, mOffset is relative to the start offset of
+    // composition if there is, otherwise, the start of the first selection
+    // range.
+    bool mRelativeToInsertionPoint;
 
     Input()
       : mOffset(0)
       , mLength(0)
       , mSelectionType(SelectionType::eNormal)
+      , mRelativeToInsertionPoint(false)
     {
     }
+
+    bool IsValidOffset() const
+    {
+      return mRelativeToInsertionPoint || mOffset >= 0;
+    }
+    bool IsValidEventMessage(EventMessage aEventMessage) const
+    {
+      if (!mRelativeToInsertionPoint) {
+        return true;
+      }
+      switch (aEventMessage) {
+        case eQueryTextContent:
+        case eQueryCaretRect:
+        case eQueryTextRect:
+          return true;
+        default:
+          return false;
+      }
+    }
+    bool MakeOffsetAbsolute(uint32_t aInsertionPointOffset)
+    {
+      if (NS_WARN_IF(!mRelativeToInsertionPoint)) {
+        return true;
+      }
+      mRelativeToInsertionPoint = false;
+      // If mOffset + aInsertionPointOffset becomes negative value,
+      // we should assume the absolute offset is 0.
+      if (mOffset < 0 && -mOffset > aInsertionPointOffset) {
+        mOffset = 0;
+        return true;
+      }
+      // Otherwise, we don't allow too large offset.
+      CheckedInt<uint32_t> absOffset = mOffset + aInsertionPointOffset;
+      if (NS_WARN_IF(!absOffset.isValid())) {
+        mOffset = UINT32_MAX;
+        return false;
+      }
+      mOffset = absOffset.value();
+      return true;
+    }
   } mInput;
 
   struct Reply final
   {
     void* mContentsRoot;
     uint32_t mOffset;
     // mTentativeCaretOffset is used by only eQueryCharacterAtPoint.
     // This is the offset where caret would be if user clicked at the mRefPoint.
--- a/widget/tests/window_composition_text_querycontent.xul
+++ b/widget/tests/window_composition_text_querycontent.xul
@@ -143,16 +143,35 @@ function checkContent(aExpectedText, aMe
                                ": synthesizeQueryTextContent " + aID)) {
     return false;
   }
   is(textContent.text, aExpectedText,
      aMessage + ": composition string is wrong " + aID);
   return textContent.text == aExpectedText;
 }
 
+function checkContentRelativeToSelection(aRelativeOffset, aLength, aExpectedOffset, aExpectedText, aMessage, aID)
+{
+  if (!aID) {
+    aID = "";
+  }
+  aMessage += " (aRelativeOffset=" + aRelativeOffset + "): "
+  var textContent = synthesizeQueryTextContent(aRelativeOffset, aLength, true);
+  if (!checkQueryContentResult(textContent, aMessage +
+                               "synthesizeQueryTextContent " + aID)) {
+    return false;
+  }
+  is(textContent.offset, aExpectedOffset,
+     aMessage + "offset is wrong " + aID);
+  is(textContent.text, aExpectedText,
+     aMessage + "text is wrong " + aID);
+  return textContent.offset == aExpectedOffset &&
+         textContent.text == aExpectedText;
+}
+
 function checkSelection(aExpectedOffset, aExpectedText, aMessage, aID)
 {
   if (!aID) {
     aID = "";
   }
   var selectedText = synthesizeQuerySelectedText();
   if (!checkQueryContentResult(selectedText, aMessage +
                                ": synthesizeQuerySelectedText " + aID)) {
@@ -3580,16 +3599,104 @@ function runQueryIMESelectionTest()
       !checkIMESelection("SelectedClause", true, startoffset + 3, "d", "runQueryIMESelectionTest: unrealistic testcase")) {
     synthesizeComposition({ type: "compositioncommitasis" });
     return;
   }
 
   synthesizeComposition({ type: "compositioncommitasis" });
 }
 
+function runQueryContentEventRelativeToInsertionPoint()
+{
+  textarea.focus();
+  textarea.value = "0123456789";
+
+  var startoffset = textarea.selectionStart = textarea.selectionEnd = 0;
+
+  if (!checkContentRelativeToSelection(0, 1, 0, "0", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#1") ||
+      !checkContentRelativeToSelection(-1, 1, 0, "0", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#2") ||
+      !checkContentRelativeToSelection(1, 1, 1, "1", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#3") ||
+      !checkContentRelativeToSelection(5, 10, 5, "56789", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#4") ||
+      !checkContentRelativeToSelection(10, 1, 10, "", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#5")) {
+    return;
+  }
+
+  textarea.selectionEnd = 5;
+
+  if (!checkContentRelativeToSelection(0, 1, 0, "0", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#1") ||
+      !checkContentRelativeToSelection(-1, 1, 0, "0", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#2") ||
+      !checkContentRelativeToSelection(1, 1, 1, "1", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#3") ||
+      !checkContentRelativeToSelection(5, 10, 5, "56789", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#4") ||
+      !checkContentRelativeToSelection(10, 1, 10, "", "runQueryContentEventRelativeToInsertionPoint[0-5]"), "#5") {
+    return;
+  }
+
+  startoffset = textarea.selectionStart = textarea.selectionEnd = 4;
+
+  if (!checkContentRelativeToSelection(0, 1, startOffset + 0, "4", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#1") ||
+      !checkContentRelativeToSelection(-1, 1, startOffset - 1, "3", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#2") ||
+      !checkContentRelativeToSelection(1, 1, startOffset + 1, "5", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#3") ||
+      !checkContentRelativeToSelection(5, 10, startOffset + 5, "9", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#4") ||
+      !checkContentRelativeToSelection(10, 1, 10, "", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#5")) {
+    return;
+  }
+
+  synthesizeCompositionChange(
+    { "composition":
+      { "string": "a",
+        "clauses":
+        [
+          { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+        ]
+      },
+      "caret": { "start": 1, "length": 0 }
+    });
+
+  if (!checkContentRelativeToSelection(0, 1, startOffset + 0, "a", "runQueryContentEventRelativeToInsertionPoint[composition at 4]", "#1") ||
+      !checkContentRelativeToSelection(-1, 1, startOffset - 1, "3", "runQueryContentEventRelativeToInsertionPoint[composition at 4]", "#2") ||
+      !checkContentRelativeToSelection(1, 1, startOffset + 1, "4", "runQueryContentEventRelativeToInsertionPoint[composition at 4]", "#3") ||
+      !checkContentRelativeToSelection(5, 10, startOffset + 5, "89", "runQueryContentEventRelativeToInsertionPoint[composition at 4]", "#4") ||
+      !checkContentRelativeToSelection(11, 1, 11, "", "runQueryContentEventRelativeToInsertionPoint[composition at 4]")) {
+    synthesizeComposition({ type: "compositioncommitasis" });
+    return;
+  }
+
+  synthesizeComposition({ type: "compositioncommitasis" });
+
+  // Move start of composition at first compositionupdate event.
+  function onCompositionUpdate(aEvent)
+  {
+    startoffset = textarea.selectionStart = textarea.selectionEnd = textarea.selectionStart - 1;
+    textarea.removeEventListener("compositionupdate", onCompositionUpdate);
+  }
+  textarea.addEventListener("compositionupdate", onCompositionUpdate);
+
+  synthesizeCompositionChange(
+    { "composition":
+      { "string": "a",
+        "clauses":
+        [
+          { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+        ]
+      },
+      "caret": { "start": 1, "length": 0 }
+    });
+
+  if (!checkContentRelativeToSelection(0, 1, startOffset + 0, "b", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#1") ||
+      !checkContentRelativeToSelection(-1, 1, startOffset - 1, "3", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#2") ||
+      !checkContentRelativeToSelection(1, 1, startOffset + 1, "a", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#3") ||
+      !checkContentRelativeToSelection(5, 10, startOffset + 5, "789", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#4") ||
+      !checkContentRelativeToSelection(12, 1, 12, "", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#5")) {
+    synthesizeComposition({ type: "compositioncommitasis" });
+    return;
+  }
+
+  synthesizeComposition({ type: "compositioncommitasis" });
+}
+
 function runCSSTransformTest()
 {
   textarea.focus();
   textarea.value = "some text";
   textarea.selectionStart = textarea.selectionEnd = textarea.value.length;
   var editorRect = synthesizeQueryEditorRect();
   if (!checkQueryContentResult(editorRect,
         "runCSSTransformTest: editorRect")) {
@@ -5924,16 +6031,17 @@ function runTest()
   runCompositionCommitTest();
   runCompositionTest();
   runCompositionEventTest();
   runCharAtPointTest(textarea, "textarea in the document");
   runCharAtPointAtOutsideTest();
   runSetSelectionEventTest();
   runQueryTextContentEventTest();
   runQueryIMESelectionTest();
+  runQueryContentEventRelativeToInsertionPoint();
   runCSSTransformTest();
   runBug722639Test();
   runForceCommitTest();
   runNestedSettingValue();
   runBug811755Test();
   runIsComposingTest();
   runRedundantChangeTest();
   runNotRedundantChangeTest();