Bug 1358958 part.2 Implement TextInputHandler::InsertNewline() to handle "insertNewline:" command r?m_kato draft
authorMasayuki Nakano <masayuki@d-toybox.com>
Thu, 27 Apr 2017 21:44:58 +0900
changeset 569403 6c7205a89b42bf1a4b75994a8b08398d32face8e
parent 569400 7f5ae90bc1d7b6ec119042212e0762f469fdc9f4
child 626207 aace538a31eaf2f24b5bf5667ff62c66e9141412
push id56173
push usermasayuki@d-toybox.com
push dateThu, 27 Apr 2017 12:47:22 +0000
reviewersm_kato
bugs1358958
milestone55.0a1
Bug 1358958 part.2 Implement TextInputHandler::InsertNewline() to handle "insertNewline:" command r?m_kato Due to bug 1358958, simply inserting a line breaker with composition events doesn't work as expected in HTML editor. Therefore, we need to dispatch "fake" Enter keypress event even if it's not handling Enter key actually or shouldn't dispatch keypress event anymore. The method tries to dispatch Enter keypress event. If it's handling Enter key press actually and can dispatch keypress event normally, it dispatches Enter keypress event as-is. Otherwise, it tries to dispatch "fake" Enter keypress. It doesn't have Control, Option and Command key state for emulating to insert a line breaker. Additionally, its code value is not set to "Enter" because the fake key event isn't a physical key event. If it cannot dispatch Enter keypress event, it dispatches composition events to insert "\n" even though it won't work in HTML editor. MozReview-Commit-ID: 7AsJLKS8Tgz
widget/cocoa/TextInputHandler.h
widget/cocoa/TextInputHandler.mm
widget/cocoa/nsChildView.mm
--- a/widget/cocoa/TextInputHandler.h
+++ b/widget/cocoa/TextInputHandler.h
@@ -593,16 +593,26 @@ protected:
       return !mKeyPressDispatched && !IsDefaultPrevented();
     }
 
     bool CanHandleCommand() const
     {
       return !mKeyDownHandled && !mKeyPressHandled;
     }
 
+    bool IsEnterKeyEvent() const
+    {
+      if (NS_WARN_IF(!mKeyEvent)) {
+        return false;
+      }
+      KeyNameIndex keyNameIndex =
+        TISInputSourceWrapper::ComputeGeckoKeyNameIndex([mKeyEvent keyCode]);
+      return keyNameIndex == KEY_NAME_INDEX_Enter;
+    }
+
     void InitKeyEvent(TextInputHandlerBase* aHandler,
                       WidgetKeyboardEvent& aKeyEvent);
 
     /**
      * GetUnhandledString() returns characters of the event which have not been
      * handled with InsertText() yet. For example, if there is a composition
      * caused by a dead key press like '`' and it's committed by some key
      * combinations like |Cmd+v|, then, the |v|'s KeyDown event's |characters|
@@ -1129,16 +1139,25 @@ public:
    * @param aAttrString           An inserted string.
    * @param aReplacementRange     The range which will be replaced with the
    *                              aAttrString instead of current selection.
    */
   void InsertText(NSAttributedString *aAttrString,
                   NSRange* aReplacementRange = nullptr);
 
   /**
+   * Handles "insertNewline:" command.  Due to bug 1350541, this always
+   * dispatches a keypress event of Enter key unless there is composition.
+   * If it's handling Enter key event, this dispatches "actual" Enter
+   * keypress event.  Otherwise, dispatches "fake" Enter keypress event
+   * whose code value is unidentified.
+   */
+  void InsertNewline();
+
+  /**
    * doCommandBySelector event handler.
    *
    * @param aSelector             A selector of the command.
    * @return                      TRUE if the command is consumed.  Otherwise,
    *                              FALSE.
    */
   bool DoCommandBySelector(const char* aSelector);
 
--- a/widget/cocoa/TextInputHandler.mm
+++ b/widget/cocoa/TextInputHandler.mm
@@ -2349,16 +2349,150 @@ TextInputHandler::InsertText(NSAttribute
   if (currentKeyEvent) {
     currentKeyEvent->mKeyPressHandled = keyPressHandled;
     currentKeyEvent->mKeyPressDispatched = keyPressDispatched;
   }
 
   NS_OBJC_END_TRY_ABORT_BLOCK;
 }
 
+void
+TextInputHandler::InsertNewline()
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+  if (Destroyed()) {
+    return;
+  }
+
+  KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+
+  MOZ_LOG(gLog, LogLevel::Info,
+    ("%p TextInputHandler::InsertNewline, "
+     "IsIMEComposing()=%s, IgnoreIMEComposition()=%s, "
+     "keyevent=%p, keydownHandled=%s, keypressDispatched=%s, "
+     "causedOtherKeyEvents=%s, compositionDispatched=%s",
+     this, TrueOrFalse(IsIMEComposing()), TrueOrFalse(IgnoreIMEComposition()),
+     currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr,
+     currentKeyEvent ?
+       TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A",
+     currentKeyEvent ?
+       TrueOrFalse(currentKeyEvent->mKeyPressDispatched) : "N/A",
+     currentKeyEvent ?
+       TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents) : "N/A",
+     currentKeyEvent ?
+       TrueOrFalse(currentKeyEvent->mCompositionDispatched) : "N/A"));
+
+  if (IgnoreIMEComposition()) {
+    return;
+  }
+
+  // If "insertNewline:" command shouldn't be handled, let's ignore it.
+  if (currentKeyEvent && !currentKeyEvent->CanHandleCommand()) {
+    return;
+  }
+
+  // If it's in composition, we cannot dispatch keypress event.
+  // Therefore, we should insert '\n' as committing composition.
+  if (IsIMEComposing()) {
+    NSAttributedString* lineBreaker =
+      [[NSAttributedString alloc] initWithString:@"\n"];
+    InsertTextAsCommittingComposition(lineBreaker, nullptr);
+    if (currentKeyEvent) {
+      currentKeyEvent->mCompositionDispatched = true;
+    }
+    [lineBreaker release];
+    return;
+  }
+
+  // Otherwise, we need to dispatch keypress event because HTMLEditor doesn't
+  // treat "\n" in composition string as a line break unless the whitespace is
+  // treated as pre (see bug 1350541).  In strictly speaking, we should
+  // dispatch keypress event as-is if it's handling NSKeyDown event or
+  // should insert it with committing composition.
+
+  RefPtr<nsChildView> widget(mWidget);
+  nsresult rv = mDispatcher->BeginNativeInputTransaction();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    MOZ_LOG(gLog, LogLevel::Error,
+      ("%p, IMEInputHandler::InsertNewline, "
+       "FAILED, due to BeginNativeInputTransaction() failure", this));
+    return;
+  }
+
+  // TODO: If it's not Enter keypress but user customized the OS settings
+  //       to insert a line breaker with other key, we should just set
+  //       command to the keypress event and it should be handled as
+  //       Enter key press in editor.
+
+  // If it's handling actual Enter key event and hasn't cause any composition
+  // events nor other key events, we should expose actual modifier state.
+  // Otherwise, we should remove Control, Option and Command state since
+  // editor may behave differently if some of them are active.  Although,
+  // Shift+Enter and Enter are work differently in HTML editor, we should
+  // expose actual Shift state if it's caused by Enter key for compatibility
+  // with Chromium.  Chromium breaks line in HTML editor with default pargraph
+  // separator when Enter is pressed, with <br> element when Shift+Enter.
+  // Safari breaks line in HTML editor with default paragraph separator when
+  // Enter, Shift+Enter or Option+Enter.  So, we should not change Shift+Enter
+  // meaning when there was composition string or not.
+  bool dispatchFakeKeyPress =
+    !(currentKeyEvent && currentKeyEvent->IsEnterKeyEvent() &&
+      currentKeyEvent->CanDispatchKeyPressEvent());
+
+  WidgetKeyboardEvent keypressEvent(true, eKeyPress, widget);
+  if (!dispatchFakeKeyPress) {
+    // If we're acutally handling an Enter key press, we should dispatch
+    // Enter keypress event as-is.
+    currentKeyEvent->InitKeyEvent(this, keypressEvent);
+  } else {
+    // Otherwise, we should dispatch "fake" Enter keypress event.
+    // In this case, we shouldn't set code value to "Enter".
+    NSEvent* keyEvent = currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr;
+    nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+    keypressEvent.mKeyCode = NS_VK_RETURN;
+    keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Enter;
+    keypressEvent.mModifiers &= ~(MODIFIER_CONTROL |
+                                  MODIFIER_ALT |
+                                  MODIFIER_META);
+  }
+
+  nsEventStatus status = nsEventStatus_eIgnore;
+  bool keyPressDispatched =
+    mDispatcher->MaybeDispatchKeypressEvents(keypressEvent, status,
+                                             currentKeyEvent);
+  bool keyPressHandled = (status == nsEventStatus_eConsumeNoDefault);
+
+  // NOTE: mWidget might have become null here.
+
+  if (keyPressDispatched) {
+    // Record the keypress event state only when it dispatched actual Enter
+    // keypress event because in other cases, the keypress event just a
+    // messenger.  E.g., if it's caused by different key, keypress event for
+    // the actual key should be dispatched.
+    if (!dispatchFakeKeyPress && currentKeyEvent) {
+      currentKeyEvent->mKeyPressHandled = keyPressHandled;
+      currentKeyEvent->mKeyPressDispatched = keyPressDispatched;
+    }
+    return;
+  }
+
+  // If keypress event isn't dispatched as expected, we should fallback to
+  // using composition events.
+  NSAttributedString* lineBreaker =
+    [[NSAttributedString alloc] initWithString:@"\n"];
+  InsertTextAsCommittingComposition(lineBreaker, nullptr);
+  if (currentKeyEvent) {
+    currentKeyEvent->mCompositionDispatched = true;
+  }
+  [lineBreaker release];
+
+  NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
 bool
 TextInputHandler::DoCommandBySelector(const char* aSelector)
 {
   RefPtr<nsChildView> widget(mWidget);
 
   KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
 
   MOZ_LOG(gLog, LogLevel::Info,
--- a/widget/cocoa/nsChildView.mm
+++ b/widget/cocoa/nsChildView.mm
@@ -5506,19 +5506,17 @@ GetIntegerDeltaForEvent(NSEvent* aEvent)
   mTextInputHandler->HandleKeyUpEvent(theEvent);
 
   NS_OBJC_END_TRY_ABORT_BLOCK;
 }
 
 - (void)insertNewline:(id)sender
 {
   if (mTextInputHandler) {
-    NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:@"\n"];
-    mTextInputHandler->InsertText(attrStr);
-    [attrStr release];
+    mTextInputHandler->InsertNewline();
   }
 }
 
 - (void)flagsChanged:(NSEvent*)theEvent
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
 
   NS_ENSURE_TRUE(mGeckoChild, );