Bug 791300 KeyboardLayout should respect following WM_(SYS)DEADCHAR messages for supporting chained dead keys r?m_kato draft
authorMasayuki Nakano <masayuki@d-toybox.com>
Thu, 10 Nov 2016 23:24:33 +0900
changeset 437616 72e36743dd8dc492424f5698f373061fe8ddcd85
parent 437577 6523da7387af8853a3d22afc54c297aed4402e3e
child 437630 19cc7ff09e68ec4a0719432b0d99c241742df384
push id35460
push usermasayuki@d-toybox.com
push dateFri, 11 Nov 2016 05:03:18 +0000
reviewersm_kato
bugs791300
milestone52.0a1
Bug 791300 KeyboardLayout should respect following WM_(SYS)DEADCHAR messages for supporting chained dead keys r?m_kato Currently, KeyboardLayout doesn't support chained dead keys because probably, the initial developer didn't expect there are such keyboard layout. Additionally, if we'd try to handle them with KeyboardLayout, it'd need to create too big and too complicated table at loading such keyboard layout. It's really nightmare. Therefore, this patch takes different approach. Currently, when WM_(SYS)KEYDOWN is received, KeyboardLayout (and NativeKey) respects following WM_(SYS)CHAR. Similarly, this patch makes KeyboardLayout respect WM_(SYS)DEADCHAR when it handles dead key. If WM_(SYS)KEYDOWN is followed by WM_DEADCHAR, that means that the key press is in a dead key sequence and not finishing the existing dead key sequence. Therefore, when WM_(SYS)KEYDOWN is followed by WM_(SYS)DEADCHAR, KeyboardLayout activates dead key sequence. For supporting dead key chain, this patch makes KeyboardLayout::mActiveDeadKey and KeyboardLayout::mDeadKeyShiftState arrays. When dead keydown message is received, KeyboardLayout appends an item to each of them. (I.e., when the array is not empty, it's in a dead key sequence.) When WM_(SYS)KEYUP is received, KeyboardLayout checks if it's in mActiveDeadKey. If it's included in the array, it initializes NativeKey as a dead keyup event. Otherwise, when non-printable key (probably) is received in a dead key sequence, KeyboardLayout doesn't handle it as a part of the dead key sequence. For example, a modifier key may be pressed for next key. (Even if the keyboard layout maps text input to a non-printable key, we can ignore them because such key's KeyboardEvent.key value should be decided only with the virtual keyboard.) MozReview-Commit-ID: 9n8B0YYuKCO
widget/windows/KeyboardLayout.cpp
widget/windows/KeyboardLayout.h
--- a/widget/windows/KeyboardLayout.cpp
+++ b/widget/windows/KeyboardLayout.cpp
@@ -3492,21 +3492,28 @@ KeyboardLayout::Shutdown()
 
 // static
 void
 KeyboardLayout::NotifyIdleServiceOfUserActivity()
 {
   sIdleService->ResetIdleTimeOut(0);
 }
 
-KeyboardLayout::KeyboardLayout() :
-  mKeyboardLayout(0), mIsOverridden(false),
-  mIsPendingToRestoreKeyboardLayout(false)
+KeyboardLayout::KeyboardLayout()
+  : mKeyboardLayout(0)
+  , mIsOverridden(false)
+  , mIsPendingToRestoreKeyboardLayout(false)
 {
   mDeadKeyTableListHead = nullptr;
+  // A dead key sequence should be made from up to 5 keys.  Therefore, 4 is
+  // enough and makes sense because the item is uint8_t.
+  // (Although, even if it's possible to be 6 keys or more in a sequence,
+  // this array will be re-allocated).
+  mActiveDeadKeys.SetCapacity(4);
+  mDeadKeyShiftStates.SetCapacity(4);
 
   // NOTE: LoadLayout() should be called via OnLayoutChange().
 }
 
 KeyboardLayout::~KeyboardLayout()
 {
   ReleaseDeadKeyTables();
 }
@@ -3608,45 +3615,44 @@ KeyboardLayout::InitNativeKey(NativeKey&
     // If it's not in dead key sequence, we don't need to do anymore here.
     if (!IsInDeadKeySequence()) {
       return;
     }
 
     // If it's in dead key sequence and dead char is inputted as is, we need to
     // set the previous modifier state which is stored when preceding dead key
     // is pressed.
-    UniCharsAndModifiers deadChars =
-      GetUniCharsAndModifiers(mActiveDeadKey, mDeadKeyShiftState);
+    UniCharsAndModifiers deadChars = GetDeadUniCharsAndModifiers();
     aNativeKey.mCommittedCharsAndModifiers.
                  OverwriteModifiersIfBeginsWith(deadChars);
     // Finish the dead key sequence.
     DeactivateDeadKeyState();
     return;
   }
 
+  // If it's a dead key, aNativeKey will be initialized by
+  // MaybeInitNativeKeyAsDeadKey().
+  if (MaybeInitNativeKeyAsDeadKey(aNativeKey, aModKeyState)) {
+    return;
+  }
+
   // If the key is not a usual printable key, KeyboardLayout class assume that
   // it's not cause dead char nor printable char.  Therefore, there are nothing
   // to do here fore such keys (e.g., function keys).
   // However, this should keep dead key state even if non-printable key is
   // pressed during a dead key sequence.
   if (!IsPrintableCharKey(aNativeKey.mOriginalVirtualKeyCode)) {
     return;
   }
 
   MOZ_ASSERT(aNativeKey.mOriginalVirtualKeyCode != VK_PACKET,
     "At handling VK_PACKET, we shouldn't refer keyboard layout");
   MOZ_ASSERT(aNativeKey.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING,
     "Printable key's key name index must be KEY_NAME_INDEX_USE_STRING");
 
-  // If it's a dead key, aNativeKey will be initialized by
-  // MaybeInitNativeKeyAsDeadKey().
-  if (MaybeInitNativeKeyAsDeadKey(aNativeKey, aModKeyState)) {
-    return;
-  }
-
   // If it's in dead key handling and the pressed key causes a composite
   // character, aNativeKey will be initialized by
   // MaybeInitNativeKeyWithCompositeChar().
   if (MaybeInitNativeKeyWithCompositeChar(aNativeKey, aModKeyState)) {
     return;
   }
 
   UniCharsAndModifiers baseChars =
@@ -3654,59 +3660,53 @@ KeyboardLayout::InitNativeKey(NativeKey&
 
   // If the key press isn't related to any dead keys, initialize aNativeKey
   // with the characters which should be caused by the key.
   if (!IsInDeadKeySequence()) {
     aNativeKey.mCommittedCharsAndModifiers = baseChars;
     return;
   }
 
-  // Although, this shouldn't occur, if active dead key isn't a printable
-  // key, we cannot handle it because KeyboardLayout assumes that dead key
-  // is never mapped to non-printable keys (e.g., F4, etc).  Please be aware,
-  // it's possible, but we've not known such special keyboard layout yet.
-  if (NS_WARN_IF(!IsPrintableCharKey(mActiveDeadKey))) {
-    return;
-  }
-
   // If the key doesn't cause a composite character with preceding dead key,
   // initialize aNativeKey with the dead-key character followed by current
   // key's character.
-  UniCharsAndModifiers deadChars =
-    GetUniCharsAndModifiers(mActiveDeadKey, mDeadKeyShiftState);
+  UniCharsAndModifiers deadChars = GetDeadUniCharsAndModifiers();
   aNativeKey.mCommittedCharsAndModifiers = deadChars + baseChars;
   if (aNativeKey.IsKeyDownMessage()) {
     DeactivateDeadKeyState();
   }
 }
 
 bool
 KeyboardLayout::MaybeInitNativeKeyAsDeadKey(
                   NativeKey& aNativeKey,
                   const ModifierKeyState& aModKeyState)
 {
-  if (!IsDeadKey(aNativeKey.mOriginalVirtualKeyCode, aModKeyState)) {
+  // Only when it's not in dead key sequence, we can trust IsDeadKey() result.
+  if (!IsInDeadKeySequence() &&
+      !IsDeadKey(aNativeKey.mOriginalVirtualKeyCode, aModKeyState)) {
     return false;
   }
 
-  // If it's a keydown event but not in dead key sequence or it's a keyup
-  // event of a dead key which activated current dead key sequence,
-  // initialize aNativeKey as a dead key event.
-  if ((aNativeKey.IsKeyDownMessage() && !IsInDeadKeySequence()) ||
-      (!aNativeKey.IsKeyDownMessage() &&
-       mActiveDeadKey == aNativeKey.mOriginalVirtualKeyCode)) {
+  // When keydown message is followed by a dead char message, it should be
+  // initialized as dead key.
+  bool isDeadKeyDownEvent =
+    aNativeKey.IsKeyDownMessage() &&
+    aNativeKey.IsFollowedByDeadCharMessage();
+
+  // When keyup message is received, let's check if it's one of preceding
+  // dead keys because keydown message order and keyup message order may be
+  // different.
+  bool isDeadKeyUpEvent =
+    !aNativeKey.IsKeyDownMessage() &&
+    mActiveDeadKeys.Contains(aNativeKey.mOriginalVirtualKeyCode);
+
+  if (isDeadKeyDownEvent || isDeadKeyUpEvent) {
     ActivateDeadKeyState(aNativeKey, aModKeyState);
-#ifdef DEBUG
-    UniCharsAndModifiers deadChars =
-      GetNativeUniCharsAndModifiers(aNativeKey.mOriginalVirtualKeyCode,
-                                    aModKeyState);
-    MOZ_ASSERT(deadChars.Length() == 1,
-               "dead key must generate only one character");
-#endif
-    // First dead key event doesn't generate characters.  Dead key should
+    // Any dead key events don't generate characters.  So, a dead key should
     // cause only keydown event and keyup event whose KeyboardEvent.key
     // values are "Dead".
     aNativeKey.mCommittedCharsAndModifiers.Clear();
     aNativeKey.mKeyNameIndex = KEY_NAME_INDEX_Dead;
     return true;
   }
 
   // At keydown message handling, we need to forget the first dead key
@@ -3715,47 +3715,37 @@ KeyboardLayout::MaybeInitNativeKeyAsDead
   // another dead key before releasing current key.  Therefore, we can
   // set only a character for current key for keyup event.
   if (!IsInDeadKeySequence()) {
     aNativeKey.mCommittedCharsAndModifiers =
       GetUniCharsAndModifiers(aNativeKey.mOriginalVirtualKeyCode, aModKeyState);
     return true;
   }
 
+  // When non-printable key event comes during a dead key sequence, that must
+  // be a modifier key event.  So, such events shouldn't be handled as a part
+  // of the dead key sequence.
+  if (!IsDeadKey(aNativeKey.mOriginalVirtualKeyCode, aModKeyState)) {
+    return false;
+  }
+
   // FYI: Following code may run when the user doesn't input text actually
   //      but the key sequence is a dead key sequence.  For example,
   //      ` -> Ctrl+` with Spanish keyboard layout.  Let's keep using this
   //      complicated code for now because this runs really rarely.
 
-  if (NS_WARN_IF(!IsPrintableCharKey(mActiveDeadKey))) {
-#if defined(DEBUG) || defined(MOZ_CRASHREPORTER)
-    nsPrintfCString warning("The virtual key index (%d) of mActiveDeadKey "
-                            "(0x%02X) is not a printable key "
-                            "(aNativeKey.mOriginalVirtualKeyCode=0x%02X)",
-                            GetKeyIndex(mActiveDeadKey), mActiveDeadKey,
-                            aNativeKey.mOriginalVirtualKeyCode);
-    NS_WARNING(warning.get());
-#ifdef MOZ_CRASHREPORTER
-    CrashReporter::AppendAppNotesToCrashReport(
-                     NS_LITERAL_CSTRING("\n") + warning);
-#endif // #ifdef MOZ_CRASHREPORTER
-#endif // #if defined(DEBUG) || defined(MOZ_CRASHREPORTER)
-    MOZ_CRASH("Trying to reference out of range of mVirtualKeys");
-  }
-
   // Dead key followed by another dead key may cause a composed character
   // (e.g., "Russian - Mnemonic" keyboard layout's 's' -> 'c').
   if (MaybeInitNativeKeyWithCompositeChar(aNativeKey, aModKeyState)) {
     return true;
   }
 
   // Otherwise, dead key followed by another dead key causes inputting both
   // character.
-  UniCharsAndModifiers prevDeadChars =
-    GetUniCharsAndModifiers(mActiveDeadKey, mDeadKeyShiftState);
+  UniCharsAndModifiers prevDeadChars = GetDeadUniCharsAndModifiers();
   UniCharsAndModifiers newChars =
     GetUniCharsAndModifiers(aNativeKey.mOriginalVirtualKeyCode, aModKeyState);
   // But keypress events should be fired for each committed character.
   aNativeKey.mCommittedCharsAndModifiers = prevDeadChars + newChars;
   if (aNativeKey.IsKeyDownMessage()) {
     DeactivateDeadKeyState();
   }
   return true;
@@ -3765,29 +3755,27 @@ bool
 KeyboardLayout::MaybeInitNativeKeyWithCompositeChar(
                   NativeKey& aNativeKey,
                   const ModifierKeyState& aModKeyState)
 {
   if (!IsInDeadKeySequence()) {
     return false;
   }
 
-  if (NS_WARN_IF(!IsPrintableCharKey(mActiveDeadKey)) ||
-      NS_WARN_IF(!IsPrintableCharKey(aNativeKey.mOriginalVirtualKeyCode))) {
+  if (NS_WARN_IF(!IsPrintableCharKey(aNativeKey.mOriginalVirtualKeyCode))) {
     return false;
   }
 
   UniCharsAndModifiers baseChars =
     GetUniCharsAndModifiers(aNativeKey.mOriginalVirtualKeyCode, aModKeyState);
   if (baseChars.IsEmpty() || !baseChars.CharAt(0)) {
     return false;
   }
 
-  char16_t compositeChar =
-    GetCompositeChar(mActiveDeadKey, mDeadKeyShiftState, baseChars.CharAt(0));
+  char16_t compositeChar = GetCompositeChar(baseChars.CharAt(0));
   if (!compositeChar) {
     return false;
   }
 
   // Active dead-key and base character does produce exactly one composite
   // character.
   aNativeKey.mCommittedCharsAndModifiers.Append(compositeChar,
                                                 baseChars.ModifiersAt(0));
@@ -3819,26 +3807,53 @@ KeyboardLayout::GetNativeUniCharsAndModi
   if (key < 0) {
     return UniCharsAndModifiers();
   }
   VirtualKey::ShiftState shiftState =
     VirtualKey::ModifierKeyStateToShiftState(aModKeyState);
   return mVirtualKeys[key].GetNativeUniChars(shiftState);
 }
 
+UniCharsAndModifiers
+KeyboardLayout::GetDeadUniCharsAndModifiers() const
+{
+  MOZ_RELEASE_ASSERT(mActiveDeadKeys.Length() == mDeadKeyShiftStates.Length());
+
+  if (NS_WARN_IF(mActiveDeadKeys.IsEmpty())) {
+    return UniCharsAndModifiers();
+  }
+
+  UniCharsAndModifiers result;
+  for (size_t i = 0; i < mActiveDeadKeys.Length(); ++i) {
+    result +=
+      GetUniCharsAndModifiers(mActiveDeadKeys[i], mDeadKeyShiftStates[i]);
+  }
+  return result;
+}
+
 char16_t
-KeyboardLayout::GetCompositeChar(uint8_t aVirtualKeyOfDeadKey,
-                                 VirtualKey::ShiftState aShiftStateOfDeadKey,
-                                 char16_t aBaseChar) const
+KeyboardLayout::GetCompositeChar(char16_t aBaseChar) const
 {
-  int32_t key = GetKeyIndex(aVirtualKeyOfDeadKey);
+  if (NS_WARN_IF(mActiveDeadKeys.IsEmpty())) {
+    return 0;
+  }
+  // XXX Currently, we don't support computing a composite character with
+  //     two or more dead keys since it needs big table for supporting
+  //     long chained dead keys.  However, this should be a minor bug
+  //     because this runs only when the latest keydown event does not cause
+  //     WM_(SYS)CHAR messages.  So, when user wants to input a character,
+  //     this path never runs.
+  if (mActiveDeadKeys.Length() > 1) {
+    return 0;
+  }
+  int32_t key = GetKeyIndex(mActiveDeadKeys[0]);
   if (key < 0) {
     return 0;
   }
-  return mVirtualKeys[key].GetCompositeChar(aShiftStateOfDeadKey, aBaseChar);
+  return mVirtualKeys[key].GetCompositeChar(mDeadKeyShiftStates[0], aBaseChar);
 }
 
 void
 KeyboardLayout::LoadLayout(HKL aLayout)
 {
   mIsPendingToRestoreKeyboardLayout = false;
 
   if (mKeyboardLayout == aLayout) {
@@ -3855,17 +3870,18 @@ KeyboardLayout::LoadLayout(HKL aLayout)
 
   BYTE originalKbdState[256];
   // Bitfield with all shift states that have at least one dead-key.
   uint16_t shiftStatesWithDeadKeys = 0;
   // Bitfield with all shift states that produce any possible dead-key base
   // characters.
   uint16_t shiftStatesWithBaseChars = 0;
 
-  mActiveDeadKey = -1;
+  mActiveDeadKeys.Clear();
+  mDeadKeyShiftStates.Clear();
 
   ReleaseDeadKeyTables();
 
   ::GetKeyboardState(originalKbdState);
 
   // For each shift state gather all printable characters that are produced
   // for normal case when no any dead-key is active.
 
@@ -4094,36 +4110,36 @@ void
 KeyboardLayout::ActivateDeadKeyState(const NativeKey& aNativeKey,
                                      const ModifierKeyState& aModKeyState)
 {
   // Dead-key state should be activated at keydown.
   if (!aNativeKey.IsKeyDownMessage()) {
     return;
   }
 
-  MOZ_RELEASE_ASSERT(IsPrintableCharKey(aNativeKey.mOriginalVirtualKeyCode));
-
-  mActiveDeadKey = aNativeKey.mOriginalVirtualKeyCode;
-  mDeadKeyShiftState = VirtualKey::ModifierKeyStateToShiftState(aModKeyState);
+  mActiveDeadKeys.AppendElement(aNativeKey.mOriginalVirtualKeyCode);
+  mDeadKeyShiftStates.AppendElement(
+    VirtualKey::ModifierKeyStateToShiftState(aModKeyState));
 }
 
 void
 KeyboardLayout::DeactivateDeadKeyState()
 {
-  if (mActiveDeadKey < 0) {
+  if (mActiveDeadKeys.IsEmpty()) {
     return;
   }
 
   BYTE kbdState[256];
   memset(kbdState, 0, sizeof(kbdState));
 
-  VirtualKey::FillKbdState(kbdState, mDeadKeyShiftState);
-
-  EnsureDeadKeyActive(false, mActiveDeadKey, kbdState);
-  mActiveDeadKey = -1;
+  // Assume that the last dead key can finish dead key sequence.
+  VirtualKey::FillKbdState(kbdState, mDeadKeyShiftStates.LastElement());
+  EnsureDeadKeyActive(false, mActiveDeadKeys.LastElement(), kbdState);
+  mActiveDeadKeys.Clear();
+  mDeadKeyShiftStates.Clear();
 }
 
 bool
 KeyboardLayout::AddDeadKeyEntry(char16_t aBaseChar,
                                 char16_t aCompositeChar,
                                 DeadKeyEntry* aDeadKeyArray,
                                 uint32_t aEntries)
 {
--- a/widget/windows/KeyboardLayout.h
+++ b/widget/windows/KeyboardLayout.h
@@ -689,17 +689,17 @@ public:
   bool IsDeadKey(uint8_t aVirtualKey,
                  const ModifierKeyState& aModKeyState) const;
 
   /**
    * IsInDeadKeySequence() returns true when it's in a dead key sequence.
    * It starts when a dead key is down and ends when another key down causes
    * inactivating the dead key state.
    */
-  bool IsInDeadKeySequence() const { return mActiveDeadKey >= 0; }
+  bool IsInDeadKeySequence() const { return !mActiveDeadKeys.IsEmpty(); }
 
   /**
    * IsSysKey() returns true if aVirtualKey with aModKeyState causes WM_SYSKEY*
    * or WM_SYS*CHAR messages.
    */
   bool IsSysKey(uint8_t aVirtualKey,
                 const ModifierKeyState& aModKeyState) const;
 
@@ -806,18 +806,24 @@ private:
     DeadKeyTableListEntry* next;
     uint8_t data[1];
   };
 
   HKL mKeyboardLayout;
 
   VirtualKey mVirtualKeys[NS_NUM_OF_KEYS];
   DeadKeyTableListEntry* mDeadKeyTableListHead;
-  int32_t mActiveDeadKey;                 // -1 = no active dead-key
-  VirtualKey::ShiftState mDeadKeyShiftState;
+  // When mActiveDeadKeys is empty, it's not in dead key sequence.
+  // Otherwise, it contains virtual keycodes which are pressed in current
+  // dead key sequence.
+  nsTArray<uint8_t> mActiveDeadKeys;
+  // mDeadKeyShiftStates is always same length as mActiveDeadKeys.
+  // This stores shift states at pressing each dead key stored in
+  // mActiveDeadKeys.
+  nsTArray<VirtualKey::ShiftState> mDeadKeyShiftStates;
 
   bool mIsOverridden;
   bool mIsPendingToRestoreKeyboardLayout;
 
   static inline int32_t GetKeyIndex(uint8_t aVirtualKey);
   static int CompareDeadKeyEntries(const void* aArg1, const void* aArg2,
                                    void* aData);
   static bool AddDeadKeyEntry(char16_t aBaseChar, char16_t aCompositeChar,
@@ -879,25 +885,29 @@ private:
   /**
    * See the comment of GetUniCharsAndModifiers() below.
    */
   UniCharsAndModifiers GetUniCharsAndModifiers(
                          uint8_t aVirtualKey,
                          VirtualKey::ShiftState aShiftState) const;
 
   /**
+   * GetDeadUniCharsAndModifiers() returns dead chars which are stored in
+   * current dead key sequence.  So, this is stateful.
+   */
+  UniCharsAndModifiers GetDeadUniCharsAndModifiers() const;
+
+  /**
    * GetCompositeChar() returns a composite character with dead character
-   * caused by aVirtualKeyOfDeadKey and aShiftStateOfDeadKey and a base
-   * character (aBaseChar).
+   * caused by mActiveDeadKeys, mDeadKeyShiftStates and a base character
+   * (aBaseChar).
    * If the combination of the dead character and the base character doesn't
    * cause a composite character, this returns 0.
    */
-  char16_t GetCompositeChar(uint8_t aVirtualKeyOfDeadKey,
-                            VirtualKey::ShiftState aShiftStateOfDeadKey,
-                            char16_t aBaseChar) const;
+  char16_t GetCompositeChar(char16_t aBaseChar) const;
 
   // NativeKey class should access InitNativeKey() directly, but it shouldn't
   // be available outside of NativeKey.  So, let's make NativeKey a friend
   // class of this.
   friend class NativeKey;
 };
 
 class RedirectedKeyDownMessageManager