Bug 875674 part.6 Implement insertText:replacementRange: of NSTextInputClient r=smichaud
authorMasayuki Nakano <masayuki@d-toybox.com>
Thu, 11 Jul 2013 16:46:35 +0900
changeset 138090 71c683c5f7d769dba562d42b817d62f22925637c
parent 138089 61959adde7611ed662cefdb79881e3c24dd78387
child 138091 2323d0f079f9c046257c4db81e2a566861e5b32e
push id30823
push usermasayuki@d-toybox.com
push dateThu, 11 Jul 2013 07:47:10 +0000
treeherdermozilla-inbound@86a041d63d65 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmichaud
bugs875674
milestone25.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 875674 part.6 Implement insertText:replacementRange: of NSTextInputClient r=smichaud
widget/cocoa/TextInputHandler.h
widget/cocoa/TextInputHandler.mm
widget/cocoa/nsChildView.mm
--- a/widget/cocoa/TextInputHandler.h
+++ b/widget/cocoa/TextInputHandler.h
@@ -336,16 +336,26 @@ public:
    *
    * @param aEvent                An event which you want to dispatch.
    * @return                      TRUE if the event is consumed by web contents
    *                              or chrome contents.  Otherwise, FALSE.
    */
   bool DispatchEvent(nsGUIEvent& aEvent);
 
   /**
+   * SetSelection() dispatches NS_SELECTION_SET event for the aRange.
+   *
+   * @param aRange                The range which will be selected.
+   * @return                      TRUE if setting selection is succeeded and
+   *                              the widget hasn't been destroyed.
+   *                              Otherwise, FALSE.
+   */
+  bool SetSelection(NSRange& aRange);
+
+  /**
    * InitKeyEvent() initializes aKeyEvent for aNativeKeyEvent.
    *
    * @param aNativeKeyEvent       A native key event for which you want to
    *                              dispatch a Gecko key event.
    * @param aKeyEvent             The result -- a Gecko key event initialized
    *                              from the native key event.
    * @param aInsertString         If caller expects that the event will cause
    *                              a character to be input (say in an editor),
@@ -956,18 +966,21 @@ protected:
 
   virtual void ExecutePendingMethods();
 
   /**
    * InsertTextAsCommittingComposition() commits current composition.  If there
    * is no composition, this starts a composition and commits it immediately.
    *
    * @param aAttrString           A string which is committed.
+   * @param aReplacementRange     The range which will be replaced with the
+   *                              aAttrString instead of current selection.
    */
-  void InsertTextAsCommittingComposition(NSAttributedString* aAttrString);
+  void InsertTextAsCommittingComposition(NSAttributedString* aAttrString,
+                                         NSRange* aReplacementRange);
 
 private:
   // If mIsIMEComposing is true, the composition string is stored here.
   NSString* mIMECompositionString;
   // mLastDispatchedCompositionString stores the lastest dispatched composition
   // string by compositionupdate event.
   nsString mLastDispatchedCompositionString;
 
@@ -1115,18 +1128,21 @@ public:
 
   /**
    * Insert the string to content.  I.e., this is a text input event handler.
    * If this is called during keydown event handling, this may dispatch a
    * NS_KEY_PRESS event.  If this is called during composition, this commits
    * the composition by the aAttrString.
    *
    * @param aAttrString           An inserted string.
+   * @param aReplacementRange     The range which will be replaced with the
+   *                              aAttrString instead of current selection.
    */
-  void InsertText(NSAttributedString *aAttrString);
+  void InsertText(NSAttributedString *aAttrString,
+                  NSRange* aReplacementRange = nullptr);
 
   /**
    * doCommandBySelector event handler.
    *
    * @param aSelector             A selector of the command.
    * @return                      TRUE if the command is consumed.  Otherwise,
    *                              FALSE.
    */
--- a/widget/cocoa/TextInputHandler.mm
+++ b/widget/cocoa/TextInputHandler.mm
@@ -1924,59 +1924,111 @@ TextInputHandler::DispatchKeyEventForFla
   }
 
   DispatchEvent(keyEvent);
 
   NS_OBJC_END_TRY_ABORT_BLOCK;
 }
 
 void
-TextInputHandler::InsertText(NSAttributedString *aAttrString)
+TextInputHandler::InsertText(NSAttributedString* aAttrString,
+                             NSRange* aReplacementRange)
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
 
   if (Destroyed()) {
     return;
   }
 
   KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
 
   PR_LOG(gLog, PR_LOG_ALWAYS,
     ("%p TextInputHandler::InsertText, aAttrString=\"%s\", "
+     "aReplacementRange=%p { location=%llu, length=%llu }, "
      "IsIMEComposing()=%s, IgnoreIMEComposition()=%s, "
      "keyevent=%p, keypressDispatched=%s",
-     this, GetCharacters([aAttrString string]), TrueOrFalse(IsIMEComposing()),
-     TrueOrFalse(IgnoreIMEComposition()),
+     this, GetCharacters([aAttrString string]), aReplacementRange,
+     aReplacementRange ? aReplacementRange->location : 0,
+     aReplacementRange ? aReplacementRange->length : 0,
+     TrueOrFalse(IsIMEComposing()), TrueOrFalse(IgnoreIMEComposition()),
      currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr,
      currentKeyEvent ?
        TrueOrFalse(currentKeyEvent->mKeyPressDispatched) : "N/A"));
 
   if (IgnoreIMEComposition()) {
     return;
   }
 
+  InputContext context = mWidget->GetInputContext();
+  bool isEditable = (context.mIMEState.mEnabled == IMEState::ENABLED ||
+                     context.mIMEState.mEnabled == IMEState::PASSWORD);
+  NSRange selectedRange = SelectedRange();
+
   nsAutoString str;
   nsCocoaUtils::GetStringForNSString([aAttrString string], str);
   if (!IsIMEComposing() && str.IsEmpty()) {
-    return; // nothing to do
+    // nothing to do if there is no content which can be removed.
+    if (!isEditable) {
+      return;
+    }
+    // If replacement range is specified, we need to remove the range.
+    // Otherwise, we need to remove the selected range if it's not collapsed.
+    if (aReplacementRange && aReplacementRange->location != NSNotFound) {
+      // nothing to do since the range is collapsed.
+      if (aReplacementRange->length == 0) {
+        return;
+      }
+      // If the replacement range is different from current selected range,
+      // select the range.
+      if (!NSEqualRanges(selectedRange, *aReplacementRange)) {
+        NS_ENSURE_TRUE_VOID(SetSelection(*aReplacementRange));
+      }
+      selectedRange = SelectedRange();
+    }
+    NS_ENSURE_TRUE_VOID(selectedRange.location != NSNotFound);
+    if (selectedRange.length == 0) {
+      return; // nothing to do
+    }
+    // If this is caused by a key input, the keypress event which will be
+    // dispatched later should cause the delete.  Therefore, nothing to do here.
+    // Although, we're not sure if such case is actually possible.
+    if (!currentKeyEvent) {
+      return;
+    }
+    // Delete the selected range.
+    nsRefPtr<TextInputHandler> kungFuDeathGrip(this);
+    nsContentCommandEvent deleteCommandEvent(true, NS_CONTENT_COMMAND_DELETE,
+                                             mWidget);
+    DispatchEvent(deleteCommandEvent);
+    NS_ENSURE_TRUE_VOID(deleteCommandEvent.mSucceeded);
+    // Be aware! The widget might be destroyed here.
+    return;
   }
 
   if (str.Length() != 1 || IsIMEComposing()) {
-    InsertTextAsCommittingComposition(aAttrString);
+    InsertTextAsCommittingComposition(aAttrString, aReplacementRange);
     return;
   }
 
   // Don't let the same event be fired twice when hitting
   // enter/return! (Bug 420502)
   if (currentKeyEvent && currentKeyEvent->mKeyPressDispatched) {
     return;
   }
 
   nsRefPtr<nsChildView> kungFuDeathGrip(mWidget);
 
+  // If the replacement range is specified, select the range.  Then, the
+  // selection will be replaced by the later keypress event.
+  if (isEditable &&
+      aReplacementRange && aReplacementRange->location != NSNotFound &&
+      !NSEqualRanges(selectedRange, *aReplacementRange)) {
+    NS_ENSURE_TRUE_VOID(SetSelection(*aReplacementRange));
+  }
+
   // Dispatch keypress event with char instead of textEvent
   nsKeyEvent keypressEvent(true, NS_KEY_PRESS, mWidget);
   keypressEvent.isChar = IsPrintableChar(str.CharAt(0));
 
   // Don't set other modifiers from the current event, because here in
   // -insertText: they've already been taken into account in creating
   // the input string.
 
@@ -2597,61 +2649,89 @@ IMEInputHandler::DispatchTextEvent(const
 void
 IMEInputHandler::InitCompositionEvent(nsCompositionEvent& aCompositionEvent)
 {
   aCompositionEvent.time = PR_IntervalNow();
 }
 
 void
 IMEInputHandler::InsertTextAsCommittingComposition(
-                   NSAttributedString* aAttrString)
+                   NSAttributedString* aAttrString,
+                   NSRange* aReplacementRange)
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
 
   PR_LOG(gLog, PR_LOG_ALWAYS,
     ("%p IMEInputHandler::InsertTextAsCommittingComposition, "
-     "aAttrString=\"%s\", Destroyed()=%s, IsIMEComposing()=%s, "
+     "aAttrString=\"%s\", aReplacementRange=%p { location=%llu, length=%llu }, "
+     "Destroyed()=%s, IsIMEComposing()=%s, "
      "mMarkedRange={ location=%llu, length=%llu }",
-     this, GetCharacters([aAttrString string]), TrueOrFalse(Destroyed()),
-     TrueOrFalse(IsIMEComposing()),
+     this, GetCharacters([aAttrString string]), aReplacementRange,
+     aReplacementRange ? aReplacementRange->location : 0,
+     aReplacementRange ? aReplacementRange->length : 0,
+     TrueOrFalse(Destroyed()), TrueOrFalse(IsIMEComposing()),
      mMarkedRange.location, mMarkedRange.length));
 
+  if (IgnoreIMECommit()) {
+    MOZ_CRASH("IMEInputHandler::InsertTextAsCommittingComposition() must not"
+              "be called while canceling the composition");
+  }
+
   if (Destroyed()) {
     return;
   }
 
+  // First, commit current composition with the latest composition string if the
+  // replacement range is different from marked range.
+  if (IsIMEComposing() && aReplacementRange &&
+      aReplacementRange->location != NSNotFound &&
+      !NSEqualRanges(MarkedRange(), *aReplacementRange)) {
+    NSString* latestStr =
+      nsCocoaUtils::ToNSString(mLastDispatchedCompositionString);
+    NSAttributedString* attrLatestStr =
+      [[[NSAttributedString alloc] initWithString:latestStr] autorelease];
+    InsertTextAsCommittingComposition(attrLatestStr, nullptr);
+    if (Destroyed()) {
+      PR_LOG(gLog, PR_LOG_ALWAYS,
+        ("%p IMEInputHandler::InsertTextAsCommittingComposition, "
+         "destroyed by commiting composition for setting replacement range",
+         this));
+      return;
+    }
+  }
+
   nsRefPtr<IMEInputHandler> kungFuDeathGrip(this);
 
   nsString str;
   nsCocoaUtils::GetStringForNSString([aAttrString string], str);
 
   if (!IsIMEComposing()) {
+    // If there is no selection and replacement range is specified, set the
+    // range as selection.
+    if (aReplacementRange && aReplacementRange->location != NSNotFound &&
+        !NSEqualRanges(SelectedRange(), *aReplacementRange)) {
+      NS_ENSURE_TRUE_VOID(SetSelection(*aReplacementRange));
+    }
+
     // XXXmnakano Probably, we shouldn't emulate composition in this case.
     // I think that we should just fire DOM3 textInput event if we implement it.
     nsCompositionEvent compStart(true, NS_COMPOSITION_START, mWidget);
     InitCompositionEvent(compStart);
 
     DispatchEvent(compStart);
     if (Destroyed()) {
       PR_LOG(gLog, PR_LOG_ALWAYS,
         ("%p IMEInputHandler::InsertTextAsCommittingComposition, "
          "destroyed by compositionstart event", this));
       return;
     }
 
     OnStartIMEComposition();
   }
 
-  if (IgnoreIMECommit()) {
-    PR_LOG(gLog, PR_LOG_ALWAYS,
-      ("%p IMEInputHandler::InsertTextAsCommittingComposition, "
-       "IgnoreIMECommit()=%s", this, TrueOrFalse(IgnoreIMECommit())));
-    str.Truncate();
-  }
-
   NSRange range = NSMakeRange(0, str.Length());
   DispatchTextEvent(str, aAttrString, range, true);
   if (Destroyed()) {
     PR_LOG(gLog, PR_LOG_ALWAYS,
       ("%p IMEInputHandler::InsertTextAsCommittingComposition, "
        "destroyed by text event", this));
     return;
   }
@@ -4171,16 +4251,32 @@ TextInputHandlerBase::GetWindowLevel()
     ("%p TextInputHandlerBase::GetWindowLevel, windowLevel=%s (%X)",
      this, GetWindowLevelName(windowLevel), windowLevel));
 
   return windowLevel;
 
   NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NSNormalWindowLevel);
 }
 
+bool
+TextInputHandlerBase::SetSelection(NSRange& aRange)
+{
+  MOZ_ASSERT(!Destroyed());
+
+  nsRefPtr<TextInputHandlerBase> kungFuDeathGrip(this);
+  nsSelectionEvent selectionEvent(true, NS_SELECTION_SET, mWidget);
+  selectionEvent.mOffset = aRange.location;
+  selectionEvent.mLength = aRange.length;
+  selectionEvent.mReversed = false;
+  selectionEvent.mExpandToClusterBoundary = false;
+  DispatchEvent(selectionEvent);
+  NS_ENSURE_TRUE(selectionEvent.mSucceeded, false);
+  return !Destroyed();
+}
+
 /* static */ bool
 TextInputHandlerBase::IsPrintableChar(PRUnichar aChar)
 {
   return (aChar >= 0x20 && aChar <= 0x7E) || aChar >= 0xA0;
 }
 
 
 /* static */ bool
--- a/widget/cocoa/nsChildView.mm
+++ b/widget/cocoa/nsChildView.mm
@@ -4951,17 +4951,17 @@ static int32_t RoundUp(double aDouble)
 
 #pragma mark -
 // NSTextInput implementation
 
 - (void)insertText:(id)insertString
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
 
-  NS_ENSURE_TRUE(mGeckoChild, );
+  NS_ENSURE_TRUE_VOID(mGeckoChild);
 
   nsAutoRetainCocoaObject kungFuDeathGrip(self);
 
   NSAttributedString* attrStr;
   if ([insertString isKindOfClass:[NSAttributedString class]]) {
     attrStr = static_cast<NSAttributedString*>(insertString);
   } else {
     attrStr =
@@ -5090,16 +5090,36 @@ static int32_t RoundUp(double aDouble)
   return mTextInputHandler->GetValidAttributesForMarkedText();
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
 }
 
 #pragma mark -
 // NSTextInputClient implementation
 
+- (void)insertText:(id)aString replacementRange:(NSRange)replacementRange
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+  NS_ENSURE_TRUE_VOID(mGeckoChild);
+
+  nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+  NSAttributedString* attrStr;
+  if ([aString isKindOfClass:[NSAttributedString class]]) {
+    attrStr = static_cast<NSAttributedString*>(aString);
+  } else {
+    attrStr = [[[NSAttributedString alloc] initWithString:aString] autorelease];
+  }
+
+  mTextInputHandler->InsertText(attrStr, &replacementRange);
+
+  NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
 - (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)aRange
                                         actualRange:(NSRangePointer)actualRange
 {
   NS_ENSURE_TRUE(mTextInputHandler, nil);
   return mTextInputHandler->GetAttributedSubstringFromRange(aRange,
                                                             actualRange);
 }