summaryrefslogtreecommitdiffstats
path: root/widget/cocoa/TextInputHandler.mm
diff options
context:
space:
mode:
Diffstat (limited to 'widget/cocoa/TextInputHandler.mm')
-rw-r--r--widget/cocoa/TextInputHandler.mm5073
1 files changed, 5073 insertions, 0 deletions
diff --git a/widget/cocoa/TextInputHandler.mm b/widget/cocoa/TextInputHandler.mm
new file mode 100644
index 0000000000..a6d9dd3c02
--- /dev/null
+++ b/widget/cocoa/TextInputHandler.mm
@@ -0,0 +1,5073 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et 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 "TextInputHandler.h"
+
+#include "mozilla/Logging.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/MiscEvents.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/StaticPrefs_intl.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/ToString.h"
+
+#include "nsChildView.h"
+#include "nsCocoaFeatures.h"
+#include "nsObjCExceptions.h"
+#include "nsBidiUtils.h"
+#include "nsToolkit.h"
+#include "nsCocoaUtils.h"
+#include "WidgetUtils.h"
+#include "nsPrintfCString.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+// For collecting other people's log, tell them `MOZ_LOG=IMEHandler:4,sync`
+// rather than `MOZ_LOG=IMEHandler:5,sync` since using `5` may create too
+// big file.
+// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior.
+mozilla::LazyLogModule gIMELog("IMEHandler");
+
+// For collecting other people's log, tell them `MOZ_LOG=KeyboardHandler:4,sync`
+// rather than `MOZ_LOG=KeyboardHandler:5,sync` since using `5` may create too
+// big file.
+// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior.
+mozilla::LazyLogModule gKeyLog("KeyboardHandler");
+
+// The behavior of `TextInputHandler` class is important both for logging
+// keyboard handler and IME handler. Therefore, the behavior is logged when
+// either `IMEHandler` or `KeyboardHandler` is set to `MOZ_LOG`. Therefore,
+// you may not need to tell people `MOZ_LOG=IMEHandler:4,KeyboardHandler:4,sync`.
+#define MOZ_LOG_KEY_OR_IME(aLogLevel, aArgs) \
+ MOZ_LOG(MOZ_LOG_TEST(gIMELog, aLogLevel) ? gIMELog : gKeyLog, aLogLevel, aArgs)
+
+static const char* OnOrOff(bool aBool) { return aBool ? "ON" : "off"; }
+
+static const char* TrueOrFalse(bool aBool) { return aBool ? "TRUE" : "FALSE"; }
+
+static const char* GetKeyNameForNativeKeyCode(unsigned short aNativeKeyCode) {
+ switch (aNativeKeyCode) {
+ case kVK_Escape:
+ return "Escape";
+ case kVK_RightCommand:
+ return "Right-Command";
+ case kVK_Command:
+ return "Command";
+ case kVK_Shift:
+ return "Shift";
+ case kVK_CapsLock:
+ return "CapsLock";
+ case kVK_Option:
+ return "Option";
+ case kVK_Control:
+ return "Control";
+ case kVK_RightShift:
+ return "Right-Shift";
+ case kVK_RightOption:
+ return "Right-Option";
+ case kVK_RightControl:
+ return "Right-Control";
+ case kVK_ANSI_KeypadClear:
+ return "Clear";
+
+ case kVK_F1:
+ return "F1";
+ case kVK_F2:
+ return "F2";
+ case kVK_F3:
+ return "F3";
+ case kVK_F4:
+ return "F4";
+ case kVK_F5:
+ return "F5";
+ case kVK_F6:
+ return "F6";
+ case kVK_F7:
+ return "F7";
+ case kVK_F8:
+ return "F8";
+ case kVK_F9:
+ return "F9";
+ case kVK_F10:
+ return "F10";
+ case kVK_F11:
+ return "F11";
+ case kVK_F12:
+ return "F12";
+ case kVK_F13:
+ return "F13/PrintScreen";
+ case kVK_F14:
+ return "F14/ScrollLock";
+ case kVK_F15:
+ return "F15/Pause";
+
+ case kVK_ANSI_Keypad0:
+ return "NumPad-0";
+ case kVK_ANSI_Keypad1:
+ return "NumPad-1";
+ case kVK_ANSI_Keypad2:
+ return "NumPad-2";
+ case kVK_ANSI_Keypad3:
+ return "NumPad-3";
+ case kVK_ANSI_Keypad4:
+ return "NumPad-4";
+ case kVK_ANSI_Keypad5:
+ return "NumPad-5";
+ case kVK_ANSI_Keypad6:
+ return "NumPad-6";
+ case kVK_ANSI_Keypad7:
+ return "NumPad-7";
+ case kVK_ANSI_Keypad8:
+ return "NumPad-8";
+ case kVK_ANSI_Keypad9:
+ return "NumPad-9";
+
+ case kVK_ANSI_KeypadMultiply:
+ return "NumPad-*";
+ case kVK_ANSI_KeypadPlus:
+ return "NumPad-+";
+ case kVK_ANSI_KeypadMinus:
+ return "NumPad--";
+ case kVK_ANSI_KeypadDecimal:
+ return "NumPad-.";
+ case kVK_ANSI_KeypadDivide:
+ return "NumPad-/";
+ case kVK_ANSI_KeypadEquals:
+ return "NumPad-=";
+ case kVK_ANSI_KeypadEnter:
+ return "NumPad-Enter";
+ case kVK_Return:
+ return "Return";
+ case kVK_Powerbook_KeypadEnter:
+ return "NumPad-EnterOnPowerBook";
+
+ case kVK_PC_Insert:
+ return "Insert/Help";
+ case kVK_PC_Delete:
+ return "Delete";
+ case kVK_Tab:
+ return "Tab";
+ case kVK_PC_Backspace:
+ return "Backspace";
+ case kVK_Home:
+ return "Home";
+ case kVK_End:
+ return "End";
+ case kVK_PageUp:
+ return "PageUp";
+ case kVK_PageDown:
+ return "PageDown";
+ case kVK_LeftArrow:
+ return "LeftArrow";
+ case kVK_RightArrow:
+ return "RightArrow";
+ case kVK_UpArrow:
+ return "UpArrow";
+ case kVK_DownArrow:
+ return "DownArrow";
+ case kVK_PC_ContextMenu:
+ return "ContextMenu";
+
+ case kVK_Function:
+ return "Function";
+ case kVK_VolumeUp:
+ return "VolumeUp";
+ case kVK_VolumeDown:
+ return "VolumeDown";
+ case kVK_Mute:
+ return "Mute";
+
+ case kVK_ISO_Section:
+ return "ISO_Section";
+
+ case kVK_JIS_Yen:
+ return "JIS_Yen";
+ case kVK_JIS_Underscore:
+ return "JIS_Underscore";
+ case kVK_JIS_KeypadComma:
+ return "JIS_KeypadComma";
+ case kVK_JIS_Eisu:
+ return "JIS_Eisu";
+ case kVK_JIS_Kana:
+ return "JIS_Kana";
+
+ case kVK_ANSI_A:
+ return "A";
+ case kVK_ANSI_B:
+ return "B";
+ case kVK_ANSI_C:
+ return "C";
+ case kVK_ANSI_D:
+ return "D";
+ case kVK_ANSI_E:
+ return "E";
+ case kVK_ANSI_F:
+ return "F";
+ case kVK_ANSI_G:
+ return "G";
+ case kVK_ANSI_H:
+ return "H";
+ case kVK_ANSI_I:
+ return "I";
+ case kVK_ANSI_J:
+ return "J";
+ case kVK_ANSI_K:
+ return "K";
+ case kVK_ANSI_L:
+ return "L";
+ case kVK_ANSI_M:
+ return "M";
+ case kVK_ANSI_N:
+ return "N";
+ case kVK_ANSI_O:
+ return "O";
+ case kVK_ANSI_P:
+ return "P";
+ case kVK_ANSI_Q:
+ return "Q";
+ case kVK_ANSI_R:
+ return "R";
+ case kVK_ANSI_S:
+ return "S";
+ case kVK_ANSI_T:
+ return "T";
+ case kVK_ANSI_U:
+ return "U";
+ case kVK_ANSI_V:
+ return "V";
+ case kVK_ANSI_W:
+ return "W";
+ case kVK_ANSI_X:
+ return "X";
+ case kVK_ANSI_Y:
+ return "Y";
+ case kVK_ANSI_Z:
+ return "Z";
+
+ case kVK_ANSI_1:
+ return "1";
+ case kVK_ANSI_2:
+ return "2";
+ case kVK_ANSI_3:
+ return "3";
+ case kVK_ANSI_4:
+ return "4";
+ case kVK_ANSI_5:
+ return "5";
+ case kVK_ANSI_6:
+ return "6";
+ case kVK_ANSI_7:
+ return "7";
+ case kVK_ANSI_8:
+ return "8";
+ case kVK_ANSI_9:
+ return "9";
+ case kVK_ANSI_0:
+ return "0";
+ case kVK_ANSI_Equal:
+ return "Equal";
+ case kVK_ANSI_Minus:
+ return "Minus";
+ case kVK_ANSI_RightBracket:
+ return "RightBracket";
+ case kVK_ANSI_LeftBracket:
+ return "LeftBracket";
+ case kVK_ANSI_Quote:
+ return "Quote";
+ case kVK_ANSI_Semicolon:
+ return "Semicolon";
+ case kVK_ANSI_Backslash:
+ return "Backslash";
+ case kVK_ANSI_Comma:
+ return "Comma";
+ case kVK_ANSI_Slash:
+ return "Slash";
+ case kVK_ANSI_Period:
+ return "Period";
+ case kVK_ANSI_Grave:
+ return "Grave";
+
+ default:
+ return "undefined";
+ }
+}
+
+static const char* GetCharacters(const nsAString& aString) {
+ if (aString.IsEmpty()) {
+ return "";
+ }
+ nsAutoString escapedStr;
+ for (uint32_t i = 0; i < aString.Length(); i++) {
+ char16_t ch = aString.CharAt(i);
+ if (ch < 0x20) {
+ nsPrintfCString utf8str("(U+%04X)", ch);
+ escapedStr += NS_ConvertUTF8toUTF16(utf8str);
+ } else if (ch <= 0x7E) {
+ escapedStr += ch;
+ } else {
+ nsPrintfCString utf8str("(U+%04X)", ch);
+ escapedStr += ch;
+ escapedStr += NS_ConvertUTF8toUTF16(utf8str);
+ }
+ }
+
+ // the result will be freed automatically by cocoa.
+ NSString* result = nsCocoaUtils::ToNSString(escapedStr);
+ return [result UTF8String];
+}
+
+static const char* GetCharacters(const NSString* aString) {
+ nsAutoString str;
+ nsCocoaUtils::GetStringForNSString(aString, str);
+ return GetCharacters(str);
+}
+
+static const char* GetCharacters(const CFStringRef aString) {
+ const NSString* str = reinterpret_cast<const NSString*>(aString);
+ return GetCharacters(str);
+}
+
+static const char* GetNativeKeyEventType(NSEvent* aNativeEvent) {
+ switch ([aNativeEvent type]) {
+ case NSEventTypeKeyDown:
+ return "NSEventTypeKeyDown";
+ case NSEventTypeKeyUp:
+ return "NSEventTypeKeyUp";
+ case NSEventTypeFlagsChanged:
+ return "NSEventTypeFlagsChanged";
+ default:
+ return "not key event";
+ }
+}
+
+static const char* GetGeckoKeyEventType(const WidgetEvent& aEvent) {
+ switch (aEvent.mMessage) {
+ case eKeyDown:
+ return "eKeyDown";
+ case eKeyUp:
+ return "eKeyUp";
+ case eKeyPress:
+ return "eKeyPress";
+ default:
+ return "not key event";
+ }
+}
+
+static const char* GetWindowLevelName(NSInteger aWindowLevel) {
+ switch (aWindowLevel) {
+ case kCGBaseWindowLevelKey:
+ return "kCGBaseWindowLevelKey (NSNormalWindowLevel)";
+ case kCGMinimumWindowLevelKey:
+ return "kCGMinimumWindowLevelKey";
+ case kCGDesktopWindowLevelKey:
+ return "kCGDesktopWindowLevelKey";
+ case kCGBackstopMenuLevelKey:
+ return "kCGBackstopMenuLevelKey";
+ case kCGNormalWindowLevelKey:
+ return "kCGNormalWindowLevelKey";
+ case kCGFloatingWindowLevelKey:
+ return "kCGFloatingWindowLevelKey (NSFloatingWindowLevel)";
+ case kCGTornOffMenuWindowLevelKey:
+ return "kCGTornOffMenuWindowLevelKey (NSSubmenuWindowLevel, NSTornOffMenuWindowLevel)";
+ case kCGDockWindowLevelKey:
+ return "kCGDockWindowLevelKey (NSDockWindowLevel)";
+ case kCGMainMenuWindowLevelKey:
+ return "kCGMainMenuWindowLevelKey (NSMainMenuWindowLevel)";
+ case kCGStatusWindowLevelKey:
+ return "kCGStatusWindowLevelKey (NSStatusWindowLevel)";
+ case kCGModalPanelWindowLevelKey:
+ return "kCGModalPanelWindowLevelKey (NSModalPanelWindowLevel)";
+ case kCGPopUpMenuWindowLevelKey:
+ return "kCGPopUpMenuWindowLevelKey (NSPopUpMenuWindowLevel)";
+ case kCGDraggingWindowLevelKey:
+ return "kCGDraggingWindowLevelKey";
+ case kCGScreenSaverWindowLevelKey:
+ return "kCGScreenSaverWindowLevelKey (NSScreenSaverWindowLevel)";
+ case kCGMaximumWindowLevelKey:
+ return "kCGMaximumWindowLevelKey";
+ case kCGOverlayWindowLevelKey:
+ return "kCGOverlayWindowLevelKey";
+ case kCGHelpWindowLevelKey:
+ return "kCGHelpWindowLevelKey";
+ case kCGUtilityWindowLevelKey:
+ return "kCGUtilityWindowLevelKey";
+ case kCGDesktopIconWindowLevelKey:
+ return "kCGDesktopIconWindowLevelKey";
+ case kCGCursorWindowLevelKey:
+ return "kCGCursorWindowLevelKey";
+ case kCGNumberOfWindowLevelKeys:
+ return "kCGNumberOfWindowLevelKeys";
+ default:
+ return "unknown window level";
+ }
+}
+
+static bool IsControlChar(uint32_t aCharCode) { return aCharCode < ' ' || aCharCode == 0x7F; }
+
+static uint32_t gHandlerInstanceCount = 0;
+
+static void EnsureToLogAllKeyboardLayoutsAndIMEs() {
+ static bool sDone = false;
+ if (!sDone) {
+ sDone = true;
+ TextInputHandler::DebugPrintAllKeyboardLayouts();
+ IMEInputHandler::DebugPrintAllIMEModes();
+ }
+}
+
+inline NSRange MakeNSRangeFrom(const Maybe<OffsetAndData<uint32_t>>& aOffsetAndData) {
+ if (aOffsetAndData.isNothing()) {
+ return NSMakeRange(NSNotFound, 0);
+ }
+ return NSMakeRange(aOffsetAndData->StartOffset(), aOffsetAndData->Length());
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TISInputSourceWrapper implementation
+ *
+ ******************************************************************************/
+
+TISInputSourceWrapper* TISInputSourceWrapper::sCurrentInputSource = nullptr;
+
+// static
+TISInputSourceWrapper& TISInputSourceWrapper::CurrentInputSource() {
+ if (!sCurrentInputSource) {
+ sCurrentInputSource = new TISInputSourceWrapper();
+ }
+ if (!sCurrentInputSource->IsInitializedByCurrentInputSource()) {
+ sCurrentInputSource->InitByCurrentInputSource();
+ }
+ return *sCurrentInputSource;
+}
+
+// static
+void TISInputSourceWrapper::Shutdown() {
+ if (!sCurrentInputSource) {
+ return;
+ }
+ sCurrentInputSource->Clear();
+ delete sCurrentInputSource;
+ sCurrentInputSource = nullptr;
+}
+
+bool TISInputSourceWrapper::TranslateToString(UInt32 aKeyCode, UInt32 aModifiers, UInt32 aKbType,
+ nsAString& aStr) {
+ aStr.Truncate();
+
+ const UCKeyboardLayout* UCKey = GetUCKeyboardLayout();
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::TranslateToString, aKeyCode=0x%X, "
+ "aModifiers=0x%X, aKbType=0x%X UCKey=%p\n "
+ "Shift: %s, Ctrl: %s, Opt: %s, Cmd: %s, CapsLock: %s, NumLock: %s",
+ this, static_cast<unsigned int>(aKeyCode), static_cast<unsigned int>(aModifiers),
+ static_cast<unsigned int>(aKbType), UCKey, OnOrOff(aModifiers & shiftKey),
+ OnOrOff(aModifiers & controlKey), OnOrOff(aModifiers & optionKey),
+ OnOrOff(aModifiers & cmdKey), OnOrOff(aModifiers & alphaLock),
+ OnOrOff(aModifiers & kEventKeyModifierNumLockMask)));
+
+ NS_ENSURE_TRUE(UCKey, false);
+
+ UInt32 deadKeyState = 0;
+ UniCharCount len;
+ UniChar chars[5];
+ OSStatus err = ::UCKeyTranslate(UCKey, aKeyCode, kUCKeyActionDown, aModifiers >> 8, aKbType,
+ kUCKeyTranslateNoDeadKeysMask, &deadKeyState, 5, &len, chars);
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::TranslateToString, err=0x%X, len=%zu", this,
+ static_cast<int>(err), len));
+
+ NS_ENSURE_TRUE(err == noErr, false);
+ if (len == 0) {
+ return true;
+ }
+ if (!aStr.SetLength(len, fallible)) {
+ return false;
+ }
+ NS_ASSERTION(sizeof(char16_t) == sizeof(UniChar),
+ "size of char16_t and size of UniChar are different");
+ memcpy(aStr.BeginWriting(), chars, len * sizeof(char16_t));
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::TranslateToString, aStr=\"%s\"", this,
+ NS_ConvertUTF16toUTF8(aStr).get()));
+
+ return true;
+}
+
+uint32_t TISInputSourceWrapper::TranslateToChar(UInt32 aKeyCode, UInt32 aModifiers,
+ UInt32 aKbType) {
+ nsAutoString str;
+ if (!TranslateToString(aKeyCode, aModifiers, aKbType, str) || str.Length() != 1) {
+ return 0;
+ }
+ return static_cast<uint32_t>(str.CharAt(0));
+}
+
+bool TISInputSourceWrapper::IsDeadKey(NSEvent* aNativeKeyEvent) {
+ if ([[aNativeKeyEvent characters] length]) {
+ return false;
+ }
+
+ // Assmue that if control key or command key is pressed, it's not a dead key.
+ NSUInteger cocoaState = [aNativeKeyEvent modifierFlags];
+ if (cocoaState & (NSEventModifierFlagControl | NSEventModifierFlagCommand)) {
+ return false;
+ }
+
+ UInt32 nativeKeyCode = [aNativeKeyEvent keyCode];
+ switch (nativeKeyCode) {
+ case kVK_ANSI_A:
+ case kVK_ANSI_B:
+ case kVK_ANSI_C:
+ case kVK_ANSI_D:
+ case kVK_ANSI_E:
+ case kVK_ANSI_F:
+ case kVK_ANSI_G:
+ case kVK_ANSI_H:
+ case kVK_ANSI_I:
+ case kVK_ANSI_J:
+ case kVK_ANSI_K:
+ case kVK_ANSI_L:
+ case kVK_ANSI_M:
+ case kVK_ANSI_N:
+ case kVK_ANSI_O:
+ case kVK_ANSI_P:
+ case kVK_ANSI_Q:
+ case kVK_ANSI_R:
+ case kVK_ANSI_S:
+ case kVK_ANSI_T:
+ case kVK_ANSI_U:
+ case kVK_ANSI_V:
+ case kVK_ANSI_W:
+ case kVK_ANSI_X:
+ case kVK_ANSI_Y:
+ case kVK_ANSI_Z:
+ case kVK_ANSI_1:
+ case kVK_ANSI_2:
+ case kVK_ANSI_3:
+ case kVK_ANSI_4:
+ case kVK_ANSI_5:
+ case kVK_ANSI_6:
+ case kVK_ANSI_7:
+ case kVK_ANSI_8:
+ case kVK_ANSI_9:
+ case kVK_ANSI_0:
+ case kVK_ANSI_Equal:
+ case kVK_ANSI_Minus:
+ case kVK_ANSI_RightBracket:
+ case kVK_ANSI_LeftBracket:
+ case kVK_ANSI_Quote:
+ case kVK_ANSI_Semicolon:
+ case kVK_ANSI_Backslash:
+ case kVK_ANSI_Comma:
+ case kVK_ANSI_Slash:
+ case kVK_ANSI_Period:
+ case kVK_ANSI_Grave:
+ case kVK_JIS_Yen:
+ case kVK_JIS_Underscore:
+ break;
+ default:
+ // Let's assume that dead key can be only a printable key in standard
+ // position.
+ return false;
+ }
+
+ // If TranslateToChar() returns non-zero value, that means that
+ // the key may input a character with different dead key state.
+ UInt32 kbType = GetKbdType();
+ UInt32 carbonState = nsCocoaUtils::ConvertToCarbonModifier(cocoaState);
+ return IsDeadKey(nativeKeyCode, carbonState, kbType);
+}
+
+bool TISInputSourceWrapper::IsDeadKey(UInt32 aKeyCode, UInt32 aModifiers, UInt32 aKbType) {
+ const UCKeyboardLayout* UCKey = GetUCKeyboardLayout();
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::IsDeadKey, aKeyCode=0x%X, "
+ "aModifiers=0x%X, aKbType=0x%X UCKey=%p\n "
+ "Shift: %s, Ctrl: %s, Opt: %s, Cmd: %s, CapsLock: %s, NumLock: %s",
+ this, static_cast<unsigned int>(aKeyCode), static_cast<unsigned int>(aModifiers),
+ static_cast<unsigned int>(aKbType), UCKey, OnOrOff(aModifiers & shiftKey),
+ OnOrOff(aModifiers & controlKey), OnOrOff(aModifiers & optionKey),
+ OnOrOff(aModifiers & cmdKey), OnOrOff(aModifiers & alphaLock),
+ OnOrOff(aModifiers & kEventKeyModifierNumLockMask)));
+
+ if (NS_WARN_IF(!UCKey)) {
+ return false;
+ }
+
+ UInt32 deadKeyState = 0;
+ UniCharCount len;
+ UniChar chars[5];
+ OSStatus err = ::UCKeyTranslate(UCKey, aKeyCode, kUCKeyActionDown, aModifiers >> 8, aKbType, 0,
+ &deadKeyState, 5, &len, chars);
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::IsDeadKey, err=0x%X, "
+ "len=%zu, deadKeyState=%u",
+ this, static_cast<int>(err), len, deadKeyState));
+
+ if (NS_WARN_IF(err != noErr)) {
+ return false;
+ }
+
+ return deadKeyState != 0;
+}
+
+void TISInputSourceWrapper::InitByInputSourceID(const char* aID) {
+ Clear();
+ if (!aID) return;
+
+ CFStringRef idstr = ::CFStringCreateWithCString(kCFAllocatorDefault, aID, kCFStringEncodingASCII);
+ InitByInputSourceID(idstr);
+ ::CFRelease(idstr);
+}
+
+void TISInputSourceWrapper::InitByInputSourceID(const nsString& aID) {
+ Clear();
+ if (aID.IsEmpty()) return;
+ CFStringRef idstr = ::CFStringCreateWithCharacters(
+ kCFAllocatorDefault, reinterpret_cast<const UniChar*>(aID.get()), aID.Length());
+ InitByInputSourceID(idstr);
+ ::CFRelease(idstr);
+}
+
+void TISInputSourceWrapper::InitByInputSourceID(const CFStringRef aID) {
+ Clear();
+ if (!aID) return;
+ const void* keys[] = {kTISPropertyInputSourceID};
+ const void* values[] = {aID};
+ CFDictionaryRef filter = ::CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1, NULL, NULL);
+ NS_ASSERTION(filter, "failed to create the filter");
+ mInputSourceList = ::TISCreateInputSourceList(filter, true);
+ ::CFRelease(filter);
+ if (::CFArrayGetCount(mInputSourceList) > 0) {
+ mInputSource = static_cast<TISInputSourceRef>(
+ const_cast<void*>(::CFArrayGetValueAtIndex(mInputSourceList, 0)));
+ if (IsKeyboardLayout()) {
+ mKeyboardLayout = mInputSource;
+ }
+ }
+}
+
+void TISInputSourceWrapper::InitByLayoutID(SInt32 aLayoutID, bool aOverrideKeyboard) {
+ // NOTE: Doument new layout IDs in TextInputHandler.h when you add ones.
+ switch (aLayoutID) {
+ case 0:
+ InitByInputSourceID("com.apple.keylayout.US");
+ break;
+ case 1:
+ InitByInputSourceID("com.apple.keylayout.Greek");
+ break;
+ case 2:
+ InitByInputSourceID("com.apple.keylayout.German");
+ break;
+ case 3:
+ InitByInputSourceID("com.apple.keylayout.Swedish-Pro");
+ break;
+ case 4:
+ InitByInputSourceID("com.apple.keylayout.DVORAK-QWERTYCMD");
+ break;
+ case 5:
+ InitByInputSourceID("com.apple.keylayout.Thai");
+ break;
+ case 6:
+ InitByInputSourceID("com.apple.keylayout.Arabic");
+ break;
+ case 7:
+ InitByInputSourceID("com.apple.keylayout.ArabicPC");
+ break;
+ case 8:
+ InitByInputSourceID("com.apple.keylayout.French");
+ break;
+ case 9:
+ InitByInputSourceID("com.apple.keylayout.Hebrew");
+ break;
+ case 10:
+ InitByInputSourceID("com.apple.keylayout.Lithuanian");
+ break;
+ case 11:
+ InitByInputSourceID("com.apple.keylayout.Norwegian");
+ break;
+ case 12:
+ InitByInputSourceID("com.apple.keylayout.Spanish");
+ break;
+ default:
+ Clear();
+ break;
+ }
+ mOverrideKeyboard = aOverrideKeyboard;
+}
+
+void TISInputSourceWrapper::InitByCurrentInputSource() {
+ Clear();
+ mInputSource = ::TISCopyCurrentKeyboardInputSource();
+ mKeyboardLayout = ::TISCopyInputMethodKeyboardLayoutOverride();
+ if (!mKeyboardLayout) {
+ mKeyboardLayout = ::TISCopyCurrentKeyboardLayoutInputSource();
+ }
+ // If this causes composition, the current keyboard layout may input non-ASCII
+ // characters such as Japanese Kana characters or Hangul characters.
+ // However, we need to set ASCII characters to DOM key events for consistency
+ // with other platforms.
+ if (IsOpenedIMEMode()) {
+ TISInputSourceWrapper tis(mKeyboardLayout);
+ if (!tis.IsASCIICapable()) {
+ mKeyboardLayout = ::TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
+ }
+ }
+}
+
+void TISInputSourceWrapper::InitByCurrentKeyboardLayout() {
+ Clear();
+ mInputSource = ::TISCopyCurrentKeyboardLayoutInputSource();
+ mKeyboardLayout = mInputSource;
+}
+
+void TISInputSourceWrapper::InitByCurrentASCIICapableInputSource() {
+ Clear();
+ mInputSource = ::TISCopyCurrentASCIICapableKeyboardInputSource();
+ mKeyboardLayout = ::TISCopyInputMethodKeyboardLayoutOverride();
+ if (mKeyboardLayout) {
+ TISInputSourceWrapper tis(mKeyboardLayout);
+ if (!tis.IsASCIICapable()) {
+ mKeyboardLayout = nullptr;
+ }
+ }
+ if (!mKeyboardLayout) {
+ mKeyboardLayout = ::TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
+ }
+}
+
+void TISInputSourceWrapper::InitByCurrentASCIICapableKeyboardLayout() {
+ Clear();
+ mInputSource = ::TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
+ mKeyboardLayout = mInputSource;
+}
+
+void TISInputSourceWrapper::InitByCurrentInputMethodKeyboardLayoutOverride() {
+ Clear();
+ mInputSource = ::TISCopyInputMethodKeyboardLayoutOverride();
+ mKeyboardLayout = mInputSource;
+}
+
+void TISInputSourceWrapper::InitByTISInputSourceRef(TISInputSourceRef aInputSource) {
+ Clear();
+ mInputSource = aInputSource;
+ if (IsKeyboardLayout()) {
+ mKeyboardLayout = mInputSource;
+ }
+}
+
+void TISInputSourceWrapper::InitByLanguage(CFStringRef aLanguage) {
+ Clear();
+ mInputSource = ::TISCopyInputSourceForLanguage(aLanguage);
+ if (IsKeyboardLayout()) {
+ mKeyboardLayout = mInputSource;
+ }
+}
+
+const UCKeyboardLayout* TISInputSourceWrapper::GetUCKeyboardLayout() {
+ NS_ENSURE_TRUE(mKeyboardLayout, nullptr);
+ if (mUCKeyboardLayout) {
+ return mUCKeyboardLayout;
+ }
+ CFDataRef uchr = static_cast<CFDataRef>(
+ ::TISGetInputSourceProperty(mKeyboardLayout, kTISPropertyUnicodeKeyLayoutData));
+
+ // We should be always able to get the layout here.
+ NS_ENSURE_TRUE(uchr, nullptr);
+ mUCKeyboardLayout = reinterpret_cast<const UCKeyboardLayout*>(CFDataGetBytePtr(uchr));
+ return mUCKeyboardLayout;
+}
+
+bool TISInputSourceWrapper::GetBoolProperty(const CFStringRef aKey) {
+ CFBooleanRef ret = static_cast<CFBooleanRef>(::TISGetInputSourceProperty(mInputSource, aKey));
+ return ::CFBooleanGetValue(ret);
+}
+
+bool TISInputSourceWrapper::GetStringProperty(const CFStringRef aKey, CFStringRef& aStr) {
+ aStr = static_cast<CFStringRef>(::TISGetInputSourceProperty(mInputSource, aKey));
+ return aStr != nullptr;
+}
+
+bool TISInputSourceWrapper::GetStringProperty(const CFStringRef aKey, nsAString& aStr) {
+ CFStringRef str;
+ GetStringProperty(aKey, str);
+ nsCocoaUtils::GetStringForNSString((const NSString*)str, aStr);
+ return !aStr.IsEmpty();
+}
+
+bool TISInputSourceWrapper::IsOpenedIMEMode() {
+ NS_ENSURE_TRUE(mInputSource, false);
+ if (!IsIMEMode()) return false;
+ return !IsASCIICapable();
+}
+
+bool TISInputSourceWrapper::IsIMEMode() {
+ NS_ENSURE_TRUE(mInputSource, false);
+ CFStringRef str;
+ GetInputSourceType(str);
+ NS_ENSURE_TRUE(str, false);
+ return ::CFStringCompare(kTISTypeKeyboardInputMode, str, 0) == kCFCompareEqualTo;
+}
+
+bool TISInputSourceWrapper::IsKeyboardLayout() {
+ NS_ENSURE_TRUE(mInputSource, false);
+ CFStringRef str;
+ GetInputSourceType(str);
+ NS_ENSURE_TRUE(str, false);
+ return ::CFStringCompare(kTISTypeKeyboardLayout, str, 0) == kCFCompareEqualTo;
+}
+
+bool TISInputSourceWrapper::GetLanguageList(CFArrayRef& aLanguageList) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ aLanguageList = static_cast<CFArrayRef>(
+ ::TISGetInputSourceProperty(mInputSource, kTISPropertyInputSourceLanguages));
+ return aLanguageList != nullptr;
+}
+
+bool TISInputSourceWrapper::GetPrimaryLanguage(CFStringRef& aPrimaryLanguage) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ CFArrayRef langList;
+ NS_ENSURE_TRUE(GetLanguageList(langList), false);
+ if (::CFArrayGetCount(langList) == 0) return false;
+ aPrimaryLanguage = static_cast<CFStringRef>(::CFArrayGetValueAtIndex(langList, 0));
+ return aPrimaryLanguage != nullptr;
+}
+
+bool TISInputSourceWrapper::GetPrimaryLanguage(nsAString& aPrimaryLanguage) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ CFStringRef primaryLanguage;
+ NS_ENSURE_TRUE(GetPrimaryLanguage(primaryLanguage), false);
+ nsCocoaUtils::GetStringForNSString((const NSString*)primaryLanguage, aPrimaryLanguage);
+ return !aPrimaryLanguage.IsEmpty();
+}
+
+bool TISInputSourceWrapper::IsForRTLLanguage() {
+ if (mIsRTL < 0) {
+ // Get the input character of the 'A' key of ANSI keyboard layout.
+ nsAutoString str;
+ bool ret = TranslateToString(kVK_ANSI_A, 0, eKbdType_ANSI, str);
+ NS_ENSURE_TRUE(ret, ret);
+ char16_t ch = str.IsEmpty() ? char16_t(0) : str.CharAt(0);
+ mIsRTL = UTF16_CODE_UNIT_IS_BIDI(ch);
+ }
+ return mIsRTL != 0;
+}
+
+bool TISInputSourceWrapper::IsForJapaneseLanguage() {
+ nsAutoString lang;
+ GetPrimaryLanguage(lang);
+ return lang.EqualsLiteral("ja");
+}
+
+bool TISInputSourceWrapper::IsInitializedByCurrentInputSource() {
+ return mInputSource == ::TISCopyCurrentKeyboardInputSource();
+}
+
+void TISInputSourceWrapper::Select() {
+ if (!mInputSource) return;
+ ::TISSelectInputSource(mInputSource);
+}
+
+void TISInputSourceWrapper::Clear() {
+ // Clear() is always called when TISInputSourceWrappper is created.
+ EnsureToLogAllKeyboardLayoutsAndIMEs();
+
+ if (mInputSourceList) {
+ ::CFRelease(mInputSourceList);
+ }
+ mInputSourceList = nullptr;
+ mInputSource = nullptr;
+ mKeyboardLayout = nullptr;
+ mIsRTL = -1;
+ mUCKeyboardLayout = nullptr;
+ mOverrideKeyboard = false;
+}
+
+bool TISInputSourceWrapper::IsPrintableKeyEvent(NSEvent* aNativeKeyEvent) const {
+ UInt32 nativeKeyCode = [aNativeKeyEvent keyCode];
+
+ bool isPrintableKey = !TextInputHandler::IsSpecialGeckoKey(nativeKeyCode);
+ if (isPrintableKey && [aNativeKeyEvent type] != NSEventTypeKeyDown &&
+ [aNativeKeyEvent type] != NSEventTypeKeyUp) {
+ NS_WARNING("Why would a printable key not be an NSEventTypeKeyDown or NSEventTypeKeyUp event?");
+ isPrintableKey = false;
+ }
+ return isPrintableKey;
+}
+
+UInt32 TISInputSourceWrapper::GetKbdType() const {
+ // If a keyboard layout override is set, we also need to force the keyboard
+ // type to something ANSI to avoid test failures on machines with JIS
+ // keyboards (since the pair of keyboard layout and physical keyboard type
+ // form the actual key layout). This assumes that the test setting the
+ // override was written assuming an ANSI keyboard.
+ return mOverrideKeyboard ? eKbdType_ANSI : ::LMGetKbdType();
+}
+
+void TISInputSourceWrapper::ComputeInsertStringForCharCode(NSEvent* aNativeKeyEvent,
+ const WidgetKeyboardEvent& aKeyEvent,
+ const nsAString* aInsertString,
+ nsAString& aResult) {
+ if (aInsertString) {
+ // If the caller expects that the aInsertString will be input, we shouldn't
+ // change it.
+ aResult = *aInsertString;
+ } else if (IsPrintableKeyEvent(aNativeKeyEvent)) {
+ // If IME is open, [aNativeKeyEvent characters] may be a character
+ // which will be appended to the composition string. However, especially,
+ // while IME is disabled, most users and developers expect the key event
+ // works as IME closed. So, we should compute the aResult with
+ // the ASCII capable keyboard layout.
+ // NOTE: Such keyboard layouts typically change the layout to its ASCII
+ // capable layout when Command key is pressed. And we don't worry
+ // when Control key is pressed too because it causes inputting
+ // control characters.
+ // Additionally, if the key event doesn't input any text, the event may be
+ // dead key event. In this case, the charCode value should be the dead
+ // character.
+ UInt32 nativeKeyCode = [aNativeKeyEvent keyCode];
+ if ((!aKeyEvent.IsMeta() && !aKeyEvent.IsControl() && IsOpenedIMEMode()) ||
+ ![[aNativeKeyEvent characters] length]) {
+ UInt32 state = nsCocoaUtils::ConvertToCarbonModifier([aNativeKeyEvent modifierFlags]);
+ uint32_t ch = TranslateToChar(nativeKeyCode, state, GetKbdType());
+ if (ch) {
+ aResult = ch;
+ }
+ } else {
+ // If the caller isn't sure what string will be input, let's use
+ // characters of NSEvent.
+ nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], aResult);
+ }
+
+ // If control key is pressed and the eventChars is a non-printable control
+ // character, we should convert it to ASCII alphabet.
+ if (aKeyEvent.IsControl() && !aResult.IsEmpty() && aResult[0] <= char16_t(26)) {
+ aResult = (aKeyEvent.IsShift() ^ aKeyEvent.IsCapsLocked())
+ ? static_cast<char16_t>(aResult[0] + ('A' - 1))
+ : static_cast<char16_t>(aResult[0] + ('a' - 1));
+ }
+ // If Meta key is pressed, it may cause to switch the keyboard layout like
+ // Arabic, Russian, Hebrew, Greek and Dvorak-QWERTY.
+ else if (aKeyEvent.IsMeta() && !(aKeyEvent.IsControl() || aKeyEvent.IsAlt())) {
+ UInt32 kbType = GetKbdType();
+ UInt32 numLockState = aKeyEvent.IsNumLocked() ? kEventKeyModifierNumLockMask : 0;
+ UInt32 capsLockState = aKeyEvent.IsCapsLocked() ? alphaLock : 0;
+ UInt32 shiftState = aKeyEvent.IsShift() ? shiftKey : 0;
+ uint32_t uncmdedChar = TranslateToChar(nativeKeyCode, numLockState, kbType);
+ uint32_t cmdedChar = TranslateToChar(nativeKeyCode, cmdKey | numLockState, kbType);
+ // If we can make a good guess at the characters that the user would
+ // expect this key combination to produce (with and without Shift) then
+ // use those characters. This also corrects for CapsLock.
+ uint32_t ch = 0;
+ if (uncmdedChar == cmdedChar) {
+ // The characters produced with Command seem similar to those without
+ // Command.
+ ch = TranslateToChar(nativeKeyCode, shiftState | capsLockState | numLockState, kbType);
+ } else {
+ TISInputSourceWrapper USLayout("com.apple.keylayout.US");
+ uint32_t uncmdedUSChar = USLayout.TranslateToChar(nativeKeyCode, numLockState, kbType);
+ // If it looks like characters from US keyboard layout when Command key
+ // is pressed, we should compute a character in the layout.
+ if (uncmdedUSChar == cmdedChar) {
+ ch = USLayout.TranslateToChar(nativeKeyCode, shiftState | capsLockState | numLockState,
+ kbType);
+ }
+ }
+
+ // If there is a more preferred character for the commanded key event,
+ // we should use it.
+ if (ch) {
+ aResult = ch;
+ }
+ }
+ }
+
+ // Remove control characters which shouldn't be inputted on editor.
+ // XXX Currently, we don't find any cases inserting control characters with
+ // printable character. So, just checking first character is enough.
+ if (!aResult.IsEmpty() && IsControlChar(aResult[0])) {
+ aResult.Truncate();
+ }
+}
+
+void TISInputSourceWrapper::InitKeyEvent(NSEvent* aNativeKeyEvent, WidgetKeyboardEvent& aKeyEvent,
+ bool aIsProcessedByIME, const nsAString* aInsertString) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_ASSERT(!aIsProcessedByIME || aKeyEvent.mMessage != eKeyPress,
+ "eKeyPress event should not be marked as proccessed by IME");
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::InitKeyEvent, aNativeKeyEvent=%p, "
+ "aKeyEvent.mMessage=%s, aProcessedByIME=%s, aInsertString=%p, "
+ "IsOpenedIMEMode()=%s",
+ this, aNativeKeyEvent, GetGeckoKeyEventType(aKeyEvent), TrueOrFalse(aIsProcessedByIME),
+ aInsertString, TrueOrFalse(IsOpenedIMEMode())));
+
+ if (NS_WARN_IF(!aNativeKeyEvent)) {
+ return;
+ }
+
+ nsCocoaUtils::InitInputEvent(aKeyEvent, aNativeKeyEvent);
+
+ // This is used only while dispatching the event (which is a synchronous
+ // call), so there is no need to retain and release this data.
+ aKeyEvent.mNativeKeyEvent = aNativeKeyEvent;
+
+ aKeyEvent.mRefPoint = LayoutDeviceIntPoint(0, 0);
+
+ UInt32 kbType = GetKbdType();
+ UInt32 nativeKeyCode = [aNativeKeyEvent keyCode];
+
+ // macOS handles dead key as IME. If the key is first key press of dead
+ // key, we should use KEY_NAME_INDEX_Dead for first (dead) key event.
+ // So, if aIsProcessedByIME is true, it may be dead key. Let's check
+ // if current key event is a dead key's keydown event.
+ bool isProcessedByIME =
+ aIsProcessedByIME && !TISInputSourceWrapper::CurrentInputSource().IsDeadKey(aNativeKeyEvent);
+
+ aKeyEvent.mKeyCode = isProcessedByIME
+ ? NS_VK_PROCESSKEY
+ : ComputeGeckoKeyCode(nativeKeyCode, kbType, aKeyEvent.IsMeta());
+
+ switch (nativeKeyCode) {
+ case kVK_Command:
+ case kVK_Shift:
+ case kVK_Option:
+ case kVK_Control:
+ aKeyEvent.mLocation = eKeyLocationLeft;
+ break;
+
+ case kVK_RightCommand:
+ case kVK_RightShift:
+ case kVK_RightOption:
+ case kVK_RightControl:
+ aKeyEvent.mLocation = eKeyLocationRight;
+ break;
+
+ case kVK_ANSI_Keypad0:
+ case kVK_ANSI_Keypad1:
+ case kVK_ANSI_Keypad2:
+ case kVK_ANSI_Keypad3:
+ case kVK_ANSI_Keypad4:
+ case kVK_ANSI_Keypad5:
+ case kVK_ANSI_Keypad6:
+ case kVK_ANSI_Keypad7:
+ case kVK_ANSI_Keypad8:
+ case kVK_ANSI_Keypad9:
+ case kVK_ANSI_KeypadMultiply:
+ case kVK_ANSI_KeypadPlus:
+ case kVK_ANSI_KeypadMinus:
+ case kVK_ANSI_KeypadDecimal:
+ case kVK_ANSI_KeypadDivide:
+ case kVK_ANSI_KeypadEquals:
+ case kVK_ANSI_KeypadEnter:
+ case kVK_JIS_KeypadComma:
+ case kVK_Powerbook_KeypadEnter:
+ aKeyEvent.mLocation = eKeyLocationNumpad;
+ break;
+
+ default:
+ aKeyEvent.mLocation = eKeyLocationStandard;
+ break;
+ }
+
+ aKeyEvent.mIsRepeat =
+ ([aNativeKeyEvent type] == NSEventTypeKeyDown) ? [aNativeKeyEvent isARepeat] : false;
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::InitKeyEvent, "
+ "shift=%s, ctrl=%s, alt=%s, meta=%s",
+ this, OnOrOff(aKeyEvent.IsShift()), OnOrOff(aKeyEvent.IsControl()),
+ OnOrOff(aKeyEvent.IsAlt()), OnOrOff(aKeyEvent.IsMeta())));
+
+ if (isProcessedByIME) {
+ aKeyEvent.mKeyNameIndex = KEY_NAME_INDEX_Process;
+ } else if (IsPrintableKeyEvent(aNativeKeyEvent)) {
+ aKeyEvent.mKeyNameIndex = KEY_NAME_INDEX_USE_STRING;
+ // If insertText calls this method, let's use the string.
+ if (aInsertString && !aInsertString->IsEmpty() && !IsControlChar((*aInsertString)[0])) {
+ aKeyEvent.mKeyValue = *aInsertString;
+ }
+ // If meta key is pressed, the printable key layout may be switched from
+ // non-ASCII capable layout to ASCII capable, or from Dvorak to QWERTY.
+ // KeyboardEvent.key value should be the switched layout's character.
+ else if (aKeyEvent.IsMeta()) {
+ nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], aKeyEvent.mKeyValue);
+ }
+ // If control key is pressed, some keys may produce printable character via
+ // [aNativeKeyEvent characters]. Otherwise, translate input character of
+ // the key without control key.
+ else if (aKeyEvent.IsControl()) {
+ NSUInteger cocoaState = [aNativeKeyEvent modifierFlags] & ~NSEventModifierFlagControl;
+ UInt32 carbonState = nsCocoaUtils::ConvertToCarbonModifier(cocoaState);
+ if (IsDeadKey(nativeKeyCode, carbonState, kbType)) {
+ aKeyEvent.mKeyNameIndex = KEY_NAME_INDEX_Dead;
+ } else {
+ aKeyEvent.mKeyValue = TranslateToChar(nativeKeyCode, carbonState, kbType);
+ if (!aKeyEvent.mKeyValue.IsEmpty() && IsControlChar(aKeyEvent.mKeyValue[0])) {
+ // Don't expose control character to the web.
+ aKeyEvent.mKeyValue.Truncate();
+ }
+ }
+ }
+ // Otherwise, KeyboardEvent.key expose
+ // [aNativeKeyEvent characters] value. However, if IME is open and the
+ // keyboard layout isn't ASCII capable, exposing the non-ASCII character
+ // doesn't match with other platform's behavior. For the compatibility
+ // with other platform's Gecko, we need to set a translated character.
+ else if (IsOpenedIMEMode()) {
+ UInt32 state = nsCocoaUtils::ConvertToCarbonModifier([aNativeKeyEvent modifierFlags]);
+ aKeyEvent.mKeyValue = TranslateToChar(nativeKeyCode, state, kbType);
+ } else {
+ nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], aKeyEvent.mKeyValue);
+ // If the key value is empty, the event may be a dead key event.
+ // If TranslateToChar() returns non-zero value, that means that
+ // the key may input a character with different dead key state.
+ if (aKeyEvent.mKeyValue.IsEmpty()) {
+ NSUInteger cocoaState = [aNativeKeyEvent modifierFlags];
+ UInt32 carbonState = nsCocoaUtils::ConvertToCarbonModifier(cocoaState);
+ if (TranslateToChar(nativeKeyCode, carbonState, kbType)) {
+ aKeyEvent.mKeyNameIndex = KEY_NAME_INDEX_Dead;
+ }
+ }
+ }
+
+ // Last resort. If .key value becomes empty string, we should use
+ // charactersIgnoringModifiers, if it's available.
+ if (aKeyEvent.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING &&
+ (aKeyEvent.mKeyValue.IsEmpty() || IsControlChar(aKeyEvent.mKeyValue[0]))) {
+ nsCocoaUtils::GetStringForNSString([aNativeKeyEvent charactersIgnoringModifiers],
+ aKeyEvent.mKeyValue);
+ // But don't expose it if it's a control character.
+ if (!aKeyEvent.mKeyValue.IsEmpty() && IsControlChar(aKeyEvent.mKeyValue[0])) {
+ aKeyEvent.mKeyValue.Truncate();
+ }
+ }
+ } else {
+ // Compute the key for non-printable keys and some special printable keys.
+ aKeyEvent.mKeyNameIndex = ComputeGeckoKeyNameIndex(nativeKeyCode);
+ }
+
+ aKeyEvent.mCodeNameIndex = ComputeGeckoCodeNameIndex(nativeKeyCode, kbType);
+ MOZ_ASSERT(aKeyEvent.mCodeNameIndex != CODE_NAME_INDEX_USE_STRING);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+}
+
+void TISInputSourceWrapper::WillDispatchKeyboardEvent(NSEvent* aNativeKeyEvent,
+ const nsAString* aInsertString,
+ uint32_t aIndexOfKeypress,
+ WidgetKeyboardEvent& aKeyEvent) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Nothing to do here if the native key event is neither NSEventTypeKeyDown nor
+ // NSEventTypeKeyUp because accessing [aNativeKeyEvent characters] causes throwing
+ // an exception.
+ if ([aNativeKeyEvent type] != NSEventTypeKeyDown && [aNativeKeyEvent type] != NSEventTypeKeyUp) {
+ return;
+ }
+
+ UInt32 kbType = GetKbdType();
+
+ if (MOZ_LOG_TEST(gKeyLog, LogLevel::Info)) {
+ nsAutoString chars;
+ nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], chars);
+ NS_ConvertUTF16toUTF8 utf8Chars(chars);
+ char16_t uniChar = static_cast<char16_t>(aKeyEvent.mCharCode);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "aNativeKeyEvent=%p, aInsertString=%p (\"%s\"), "
+ "aIndexOfKeypress=%u, [aNativeKeyEvent characters]=\"%s\", "
+ "aKeyEvent={ mMessage=%s, mCharCode=0x%X(%s) }, kbType=0x%X, "
+ "IsOpenedIMEMode()=%s",
+ this, aNativeKeyEvent, aInsertString,
+ aInsertString ? GetCharacters(*aInsertString) : "", aIndexOfKeypress,
+ GetCharacters([aNativeKeyEvent characters]), GetGeckoKeyEventType(aKeyEvent),
+ aKeyEvent.mCharCode, uniChar ? NS_ConvertUTF16toUTF8(&uniChar, 1).get() : "",
+ static_cast<unsigned int>(kbType), TrueOrFalse(IsOpenedIMEMode())));
+ }
+
+ nsAutoString insertStringForCharCode;
+ ComputeInsertStringForCharCode(aNativeKeyEvent, aKeyEvent, aInsertString,
+ insertStringForCharCode);
+
+ // The mCharCode was set from mKeyValue. However, for example, when Ctrl key
+ // is pressed, its value should indicate an ASCII character for backward
+ // compatibility rather than inputting character without the modifiers.
+ // Therefore, we need to modify mCharCode value here.
+ uint32_t charCode = 0;
+ if (aIndexOfKeypress < insertStringForCharCode.Length()) {
+ charCode = insertStringForCharCode[aIndexOfKeypress];
+ }
+ aKeyEvent.SetCharCode(charCode);
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "aKeyEvent.mKeyCode=0x%X, aKeyEvent.mCharCode=0x%X",
+ this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode));
+
+ // If aInsertString is not nullptr (it means InsertText() is called)
+ // and it acutally inputs a character, we don't need to append alternative
+ // charCode values since such keyboard event shouldn't be handled as
+ // a shortcut key.
+ if (aInsertString && charCode) {
+ return;
+ }
+
+ TISInputSourceWrapper USLayout("com.apple.keylayout.US");
+ bool isRomanKeyboardLayout = IsASCIICapable();
+
+ UInt32 key = [aNativeKeyEvent keyCode];
+
+ // Caps lock and num lock modifier state:
+ UInt32 lockState = 0;
+ if ([aNativeKeyEvent modifierFlags] & NSEventModifierFlagCapsLock) {
+ lockState |= alphaLock;
+ }
+ if ([aNativeKeyEvent modifierFlags] & NSEventModifierFlagNumericPad) {
+ lockState |= kEventKeyModifierNumLockMask;
+ }
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "isRomanKeyboardLayout=%s, kbType=0x%X, key=0x%X",
+ this, TrueOrFalse(isRomanKeyboardLayout), static_cast<unsigned int>(kbType),
+ static_cast<unsigned int>(key)));
+
+ nsString str;
+
+ // normal chars
+ uint32_t unshiftedChar = TranslateToChar(key, lockState, kbType);
+ UInt32 shiftLockMod = shiftKey | lockState;
+ uint32_t shiftedChar = TranslateToChar(key, shiftLockMod, kbType);
+
+ // characters generated with Cmd key
+ // XXX we should remove CapsLock state, which changes characters from
+ // Latin to Cyrillic with Russian layout on 10.4 only when Cmd key
+ // is pressed.
+ UInt32 numState = (lockState & ~alphaLock); // only num lock state
+ uint32_t uncmdedChar = TranslateToChar(key, numState, kbType);
+ UInt32 shiftNumMod = numState | shiftKey;
+ uint32_t uncmdedShiftChar = TranslateToChar(key, shiftNumMod, kbType);
+ uint32_t uncmdedUSChar = USLayout.TranslateToChar(key, numState, kbType);
+ UInt32 cmdNumMod = cmdKey | numState;
+ uint32_t cmdedChar = TranslateToChar(key, cmdNumMod, kbType);
+ UInt32 cmdShiftNumMod = shiftKey | cmdNumMod;
+ uint32_t cmdedShiftChar = TranslateToChar(key, cmdShiftNumMod, kbType);
+
+ // Is the keyboard layout changed by Cmd key?
+ // E.g., Arabic, Russian, Hebrew, Greek and Dvorak-QWERTY.
+ bool isCmdSwitchLayout = uncmdedChar != cmdedChar;
+ // Is the keyboard layout for Latin, but Cmd key switches the layout?
+ // I.e., Dvorak-QWERTY
+ bool isDvorakQWERTY = isCmdSwitchLayout && isRomanKeyboardLayout;
+
+ // If the current keyboard is not Dvorak-QWERTY or Cmd is not pressed,
+ // we should append unshiftedChar and shiftedChar for handling the
+ // normal characters. These are the characters that the user is most
+ // likely to associate with this key.
+ if ((unshiftedChar || shiftedChar) && (!aKeyEvent.IsMeta() || !isDvorakQWERTY)) {
+ AlternativeCharCode altCharCodes(unshiftedChar, shiftedChar);
+ aKeyEvent.mAlternativeCharCodes.AppendElement(altCharCodes);
+ }
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "aKeyEvent.isMeta=%s, isDvorakQWERTY=%s, "
+ "unshiftedChar=U+%X, shiftedChar=U+%X",
+ this, OnOrOff(aKeyEvent.IsMeta()), TrueOrFalse(isDvorakQWERTY), unshiftedChar, shiftedChar));
+
+ // Most keyboard layouts provide the same characters in the NSEvents
+ // with Command+Shift as with Command. However, with Command+Shift we
+ // want the character on the second level. e.g. With a US QWERTY
+ // layout, we want "?" when the "/","?" key is pressed with
+ // Command+Shift.
+
+ // On a German layout, the OS gives us '/' with Cmd+Shift+SS(eszett)
+ // even though Cmd+SS is 'SS' and Shift+'SS' is '?'. This '/' seems
+ // like a hack to make the Cmd+"?" event look the same as the Cmd+"?"
+ // event on a US keyboard. The user thinks they are typing Cmd+"?", so
+ // we'll prefer the "?" character, replacing mCharCode with shiftedChar
+ // when Shift is pressed. However, in case there is a layout where the
+ // character unique to Cmd+Shift is the character that the user expects,
+ // we'll send it as an alternative char.
+ bool hasCmdShiftOnlyChar = cmdedChar != cmdedShiftChar && uncmdedShiftChar != cmdedShiftChar;
+ uint32_t originalCmdedShiftChar = cmdedShiftChar;
+
+ // If we can make a good guess at the characters that the user would
+ // expect this key combination to produce (with and without Shift) then
+ // use those characters. This also corrects for CapsLock, which was
+ // ignored above.
+ if (!isCmdSwitchLayout) {
+ // The characters produced with Command seem similar to those without
+ // Command.
+ if (unshiftedChar) {
+ cmdedChar = unshiftedChar;
+ }
+ if (shiftedChar) {
+ cmdedShiftChar = shiftedChar;
+ }
+ } else if (uncmdedUSChar == cmdedChar) {
+ // It looks like characters from a US layout are provided when Command
+ // is down.
+ uint32_t ch = USLayout.TranslateToChar(key, lockState, kbType);
+ if (ch) {
+ cmdedChar = ch;
+ }
+ ch = USLayout.TranslateToChar(key, shiftLockMod, kbType);
+ if (ch) {
+ cmdedShiftChar = ch;
+ }
+ }
+
+ // If the current keyboard layout is switched by the Cmd key,
+ // we should append cmdedChar and shiftedCmdChar that are
+ // Latin char for the key.
+ // If the keyboard layout is Dvorak-QWERTY, we should append them only when
+ // command key is pressed because when command key isn't pressed, uncmded
+ // chars have been appended already.
+ if ((cmdedChar || cmdedShiftChar) && isCmdSwitchLayout &&
+ (aKeyEvent.IsMeta() || !isDvorakQWERTY)) {
+ AlternativeCharCode altCharCodes(cmdedChar, cmdedShiftChar);
+ aKeyEvent.mAlternativeCharCodes.AppendElement(altCharCodes);
+ }
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "hasCmdShiftOnlyChar=%s, isCmdSwitchLayout=%s, isDvorakQWERTY=%s, "
+ "cmdedChar=U+%X, cmdedShiftChar=U+%X",
+ this, TrueOrFalse(hasCmdShiftOnlyChar), TrueOrFalse(isDvorakQWERTY),
+ TrueOrFalse(isDvorakQWERTY), cmdedChar, cmdedShiftChar));
+ // Special case for 'SS' key of German layout. See the comment of
+ // hasCmdShiftOnlyChar definition for the detail.
+ if (hasCmdShiftOnlyChar && originalCmdedShiftChar) {
+ AlternativeCharCode altCharCodes(0, originalCmdedShiftChar);
+ aKeyEvent.mAlternativeCharCodes.AppendElement(altCharCodes);
+ }
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "hasCmdShiftOnlyChar=%s, originalCmdedShiftChar=U+%X",
+ this, TrueOrFalse(hasCmdShiftOnlyChar), originalCmdedShiftChar));
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+}
+
+uint32_t TISInputSourceWrapper::ComputeGeckoKeyCode(UInt32 aNativeKeyCode, UInt32 aKbType,
+ bool aCmdIsPressed) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::ComputeGeckoKeyCode, aNativeKeyCode=0x%X, "
+ "aKbType=0x%X, aCmdIsPressed=%s, IsOpenedIMEMode()=%s, "
+ "IsASCIICapable()=%s",
+ this, static_cast<unsigned int>(aNativeKeyCode), static_cast<unsigned int>(aKbType),
+ TrueOrFalse(aCmdIsPressed), TrueOrFalse(IsOpenedIMEMode()), TrueOrFalse(IsASCIICapable())));
+
+ switch (aNativeKeyCode) {
+ case kVK_Space:
+ return NS_VK_SPACE;
+ case kVK_Escape:
+ return NS_VK_ESCAPE;
+
+ // modifiers
+ case kVK_RightCommand:
+ case kVK_Command:
+ return NS_VK_META;
+ case kVK_RightShift:
+ case kVK_Shift:
+ return NS_VK_SHIFT;
+ case kVK_CapsLock:
+ return NS_VK_CAPS_LOCK;
+ case kVK_RightControl:
+ case kVK_Control:
+ return NS_VK_CONTROL;
+ case kVK_RightOption:
+ case kVK_Option:
+ return NS_VK_ALT;
+
+ case kVK_ANSI_KeypadClear:
+ return NS_VK_CLEAR;
+
+ // function keys
+ case kVK_F1:
+ return NS_VK_F1;
+ case kVK_F2:
+ return NS_VK_F2;
+ case kVK_F3:
+ return NS_VK_F3;
+ case kVK_F4:
+ return NS_VK_F4;
+ case kVK_F5:
+ return NS_VK_F5;
+ case kVK_F6:
+ return NS_VK_F6;
+ case kVK_F7:
+ return NS_VK_F7;
+ case kVK_F8:
+ return NS_VK_F8;
+ case kVK_F9:
+ return NS_VK_F9;
+ case kVK_F10:
+ return NS_VK_F10;
+ case kVK_F11:
+ return NS_VK_F11;
+ case kVK_F12:
+ return NS_VK_F12;
+ // case kVK_F13: return NS_VK_F13; // clash with the 3 below
+ // case kVK_F14: return NS_VK_F14;
+ // case kVK_F15: return NS_VK_F15;
+ case kVK_F16:
+ return NS_VK_F16;
+ case kVK_F17:
+ return NS_VK_F17;
+ case kVK_F18:
+ return NS_VK_F18;
+ case kVK_F19:
+ return NS_VK_F19;
+
+ case kVK_PC_Pause:
+ return NS_VK_PAUSE;
+ case kVK_PC_ScrollLock:
+ return NS_VK_SCROLL_LOCK;
+ case kVK_PC_PrintScreen:
+ return NS_VK_PRINTSCREEN;
+
+ // keypad
+ case kVK_ANSI_Keypad0:
+ return NS_VK_NUMPAD0;
+ case kVK_ANSI_Keypad1:
+ return NS_VK_NUMPAD1;
+ case kVK_ANSI_Keypad2:
+ return NS_VK_NUMPAD2;
+ case kVK_ANSI_Keypad3:
+ return NS_VK_NUMPAD3;
+ case kVK_ANSI_Keypad4:
+ return NS_VK_NUMPAD4;
+ case kVK_ANSI_Keypad5:
+ return NS_VK_NUMPAD5;
+ case kVK_ANSI_Keypad6:
+ return NS_VK_NUMPAD6;
+ case kVK_ANSI_Keypad7:
+ return NS_VK_NUMPAD7;
+ case kVK_ANSI_Keypad8:
+ return NS_VK_NUMPAD8;
+ case kVK_ANSI_Keypad9:
+ return NS_VK_NUMPAD9;
+
+ case kVK_ANSI_KeypadMultiply:
+ return NS_VK_MULTIPLY;
+ case kVK_ANSI_KeypadPlus:
+ return NS_VK_ADD;
+ case kVK_ANSI_KeypadMinus:
+ return NS_VK_SUBTRACT;
+ case kVK_ANSI_KeypadDecimal:
+ return NS_VK_DECIMAL;
+ case kVK_ANSI_KeypadDivide:
+ return NS_VK_DIVIDE;
+
+ case kVK_JIS_KeypadComma:
+ return NS_VK_SEPARATOR;
+
+ // IME keys
+ case kVK_JIS_Eisu:
+ return NS_VK_EISU;
+ case kVK_JIS_Kana:
+ return NS_VK_KANA;
+
+ // these may clash with forward delete and help
+ case kVK_PC_Insert:
+ return NS_VK_INSERT;
+ case kVK_PC_Delete:
+ return NS_VK_DELETE;
+
+ case kVK_PC_Backspace:
+ return NS_VK_BACK;
+ case kVK_Tab:
+ return NS_VK_TAB;
+
+ case kVK_Home:
+ return NS_VK_HOME;
+ case kVK_End:
+ return NS_VK_END;
+
+ case kVK_PageUp:
+ return NS_VK_PAGE_UP;
+ case kVK_PageDown:
+ return NS_VK_PAGE_DOWN;
+
+ case kVK_LeftArrow:
+ return NS_VK_LEFT;
+ case kVK_RightArrow:
+ return NS_VK_RIGHT;
+ case kVK_UpArrow:
+ return NS_VK_UP;
+ case kVK_DownArrow:
+ return NS_VK_DOWN;
+
+ case kVK_PC_ContextMenu:
+ return NS_VK_CONTEXT_MENU;
+
+ case kVK_ANSI_1:
+ return NS_VK_1;
+ case kVK_ANSI_2:
+ return NS_VK_2;
+ case kVK_ANSI_3:
+ return NS_VK_3;
+ case kVK_ANSI_4:
+ return NS_VK_4;
+ case kVK_ANSI_5:
+ return NS_VK_5;
+ case kVK_ANSI_6:
+ return NS_VK_6;
+ case kVK_ANSI_7:
+ return NS_VK_7;
+ case kVK_ANSI_8:
+ return NS_VK_8;
+ case kVK_ANSI_9:
+ return NS_VK_9;
+ case kVK_ANSI_0:
+ return NS_VK_0;
+
+ case kVK_ANSI_KeypadEnter:
+ case kVK_Return:
+ case kVK_Powerbook_KeypadEnter:
+ return NS_VK_RETURN;
+ }
+
+ // If Cmd key is pressed, that causes switching keyboard layout temporarily.
+ // E.g., Dvorak-QWERTY. Therefore, if Cmd key is pressed, we should honor it.
+ UInt32 modifiers = aCmdIsPressed ? cmdKey : 0;
+
+ uint32_t charCode = TranslateToChar(aNativeKeyCode, modifiers, aKbType);
+
+ // Special case for Mac. Mac inputs Yen sign (U+00A5) directly instead of
+ // Back slash (U+005C). We should return NS_VK_BACK_SLASH for compatibility
+ // with other platforms.
+ // XXX How about Won sign (U+20A9) which has same problem as Yen sign?
+ if (charCode == 0x00A5) {
+ return NS_VK_BACK_SLASH;
+ }
+
+ uint32_t keyCode = WidgetUtils::ComputeKeyCodeFromChar(charCode);
+ if (keyCode) {
+ return keyCode;
+ }
+
+ // If the unshifed char isn't an ASCII character, use shifted char.
+ charCode = TranslateToChar(aNativeKeyCode, modifiers | shiftKey, aKbType);
+ keyCode = WidgetUtils::ComputeKeyCodeFromChar(charCode);
+ if (keyCode) {
+ return keyCode;
+ }
+
+ if (!IsASCIICapable()) {
+ // Retry with ASCII capable keyboard layout.
+ TISInputSourceWrapper currentKeyboardLayout;
+ currentKeyboardLayout.InitByCurrentASCIICapableKeyboardLayout();
+ NS_ENSURE_TRUE(mInputSource != currentKeyboardLayout.mInputSource, 0);
+ keyCode = currentKeyboardLayout.ComputeGeckoKeyCode(aNativeKeyCode, aKbType, aCmdIsPressed);
+ // We've returned 0 for long time if keyCode isn't for an alphabet keys or
+ // a numeric key even in alternative ASCII capable keyboard layout because
+ // we decided that we should avoid setting same keyCode value to 2 or
+ // more keys since active keyboard layout may have a key to input the
+ // punctuation with different key. However, setting keyCode to 0 makes
+ // some web applications which are aware of neither KeyboardEvent.key nor
+ // KeyboardEvent.code not work with Firefox when user selects non-ASCII
+ // capable keyboard layout such as Russian and Thai. So, if alternative
+ // ASCII capable keyboard layout has keyCode value for the key, we should
+ // use it. In other words, this behavior does that non-ASCII capable
+ // keyboard layout overrides some keys' keyCode value only if the key
+ // produces ASCII character by itself or with Shift key.
+ if (keyCode) {
+ return keyCode;
+ }
+ }
+
+ // Otherwise, let's decide keyCode value from the native virtual keycode
+ // value on major keyboard layout.
+ CodeNameIndex code = ComputeGeckoCodeNameIndex(aNativeKeyCode, aKbType);
+ return WidgetKeyboardEvent::GetFallbackKeyCodeOfPunctuationKey(code);
+}
+
+// static
+KeyNameIndex TISInputSourceWrapper::ComputeGeckoKeyNameIndex(UInt32 aNativeKeyCode) {
+ // NOTE:
+ // When unsupported keys like Convert, Nonconvert of Japanese keyboard is
+ // pressed:
+ // on 10.6.x, 'A' key event is fired (and also actually 'a' is inserted).
+ // on 10.7.x, Nothing happens.
+ // on 10.8.x, Nothing happens.
+ // on 10.9.x, FlagsChanged event is fired with keyCode 0xFF.
+ switch (aNativeKeyCode) {
+#define NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, aKeyNameIndex) \
+ case aNativeKey: \
+ return aKeyNameIndex;
+
+#include "NativeKeyToDOMKeyName.h"
+
+#undef NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX
+
+ default:
+ return KEY_NAME_INDEX_Unidentified;
+ }
+}
+
+// static
+CodeNameIndex TISInputSourceWrapper::ComputeGeckoCodeNameIndex(UInt32 aNativeKeyCode,
+ UInt32 aKbType) {
+ // macOS swaps native key code between Backquote key and IntlBackslash key
+ // only when the keyboard type is ISO. Let's treat the key code after
+ // swapping them here because Chromium does so only when computing .code
+ // value.
+ if (::KBGetLayoutType(aKbType) == kKeyboardISO) {
+ if (aNativeKeyCode == kVK_ISO_Section) {
+ aNativeKeyCode = kVK_ANSI_Grave;
+ } else if (aNativeKeyCode == kVK_ANSI_Grave) {
+ aNativeKeyCode = kVK_ISO_Section;
+ }
+ }
+
+ switch (aNativeKeyCode) {
+#define NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX(aNativeKey, aCodeNameIndex) \
+ case aNativeKey: \
+ return aCodeNameIndex;
+
+#include "NativeKeyToDOMCodeName.h"
+
+#undef NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX
+
+ default:
+ return CODE_NAME_INDEX_UNKNOWN;
+ }
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TextInputHandler implementation (static methods)
+ *
+ ******************************************************************************/
+
+NSUInteger TextInputHandler::sLastModifierState = 0;
+
+// static
+CFArrayRef TextInputHandler::CreateAllKeyboardLayoutList() {
+ const void* keys[] = {kTISPropertyInputSourceType};
+ const void* values[] = {kTISTypeKeyboardLayout};
+ CFDictionaryRef filter = ::CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1, NULL, NULL);
+ NS_ASSERTION(filter, "failed to create the filter");
+ CFArrayRef list = ::TISCreateInputSourceList(filter, true);
+ ::CFRelease(filter);
+ return list;
+}
+
+// static
+void TextInputHandler::DebugPrintAllKeyboardLayouts() {
+ if (MOZ_LOG_TEST(gKeyLog, LogLevel::Info)) {
+ CFArrayRef list = CreateAllKeyboardLayoutList();
+ MOZ_LOG(gKeyLog, LogLevel::Info, ("Keyboard layout configuration:"));
+ CFIndex idx = ::CFArrayGetCount(list);
+ TISInputSourceWrapper tis;
+ for (CFIndex i = 0; i < idx; ++i) {
+ TISInputSourceRef inputSource =
+ static_cast<TISInputSourceRef>(const_cast<void*>(::CFArrayGetValueAtIndex(list, i)));
+ tis.InitByTISInputSourceRef(inputSource);
+ nsAutoString name, isid;
+ tis.GetLocalizedName(name);
+ tis.GetInputSourceID(isid);
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ (" %s\t<%s>%s%s\n", NS_ConvertUTF16toUTF8(name).get(), NS_ConvertUTF16toUTF8(isid).get(),
+ tis.IsASCIICapable() ? "" : "\t(Isn't ASCII capable)",
+ tis.IsKeyboardLayout() && tis.GetUCKeyboardLayout() ? "" : "\t(uchr is NOT AVAILABLE)"));
+ }
+ ::CFRelease(list);
+ }
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TextInputHandler implementation
+ *
+ ******************************************************************************/
+
+TextInputHandler::TextInputHandler(nsChildView* aWidget, NSView<mozView>* aNativeView)
+ : IMEInputHandler(aWidget, aNativeView) {
+ EnsureToLogAllKeyboardLayoutsAndIMEs();
+ [mView installTextInputHandler:this];
+}
+
+TextInputHandler::~TextInputHandler() { [mView uninstallTextInputHandler]; }
+
+bool TextInputHandler::HandleKeyDownEvent(NSEvent* aNativeEvent, uint32_t aUniqueId) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (Destroyed()) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info, ("%p TextInputHandler::HandleKeyDownEvent, "
+ "widget has been already destroyed",
+ this));
+ return false;
+ }
+
+ // Insert empty line to the log for easier to read.
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info, (""));
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, aNativeEvent=%p, "
+ "type=%s, keyCode=%u (0x%X), modifierFlags=0x%lX, characters=\"%s\", "
+ "charactersIgnoringModifiers=\"%s\"",
+ this, aNativeEvent, GetNativeKeyEventType(aNativeEvent), [aNativeEvent keyCode],
+ [aNativeEvent keyCode], static_cast<unsigned long>([aNativeEvent modifierFlags]),
+ GetCharacters([aNativeEvent characters]),
+ GetCharacters([aNativeEvent charactersIgnoringModifiers])));
+
+ // Except when Command key is pressed, we should hide mouse cursor until
+ // next mousemove. Handling here means that:
+ // - Don't hide mouse cursor at pressing modifier key
+ // - Hide mouse cursor even if the key event will be handled by IME (i.e.,
+ // even without dispatching eKeyPress events)
+ // - Hide mouse cursor even when a plugin has focus
+ if (!([aNativeEvent modifierFlags] & NSEventModifierFlagCommand)) {
+ [NSCursor setHiddenUntilMouseMoves:YES];
+ }
+
+ RefPtr<nsChildView> widget(mWidget);
+
+ KeyEventState* currentKeyEvent = PushKeyEvent(aNativeEvent, aUniqueId);
+ AutoKeyEventStateCleaner remover(this);
+
+ RefPtr<TextInputHandler> kungFuDeathGrip(this);
+
+ // When we're already in a composition, we need always to mark the eKeyDown
+ // event as "processed by IME". So, let's dispatch eKeyDown event here in
+ // such case.
+ if (IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(true)) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, eKeyDown caused focus move or "
+ "something and canceling the composition",
+ this));
+ return false;
+ }
+
+ // Let Cocoa interpret the key events, caching IsIMEComposing first.
+ bool wasComposing = IsIMEComposing();
+ bool interpretKeyEventsCalled = false;
+ // Don't call interpretKeyEvents when a plugin has focus. If we call it,
+ // for example, a character is inputted twice during a composition in e10s
+ // mode.
+ if (IsIMEEnabled() || IsASCIICapableOnly()) {
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, calling interpretKeyEvents", this));
+ [mView interpretKeyEvents:[NSArray arrayWithObject:aNativeEvent]];
+ interpretKeyEventsCalled = true;
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, called interpretKeyEvents", this));
+ }
+
+ if (Destroyed()) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, widget was destroyed", this));
+ return currentKeyEvent->IsDefaultPrevented();
+ }
+
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, wasComposing=%s, "
+ "IsIMEComposing()=%s",
+ this, TrueOrFalse(wasComposing), TrueOrFalse(IsIMEComposing())));
+
+ if (currentKeyEvent->CanDispatchKeyDownEvent()) {
+ // Dispatch eKeyDown event if nobody has dispatched it yet.
+ // NOTE: Although reaching here means that the native keydown event may
+ // not be handled by IME. However, we cannot know if it is.
+ // For example, Japanese IME of Apple shows candidate window for
+ // typing window. They, you can switch the sort order with Tab key.
+ // However, when you choose "Symbol" of the sort order, there may
+ // be no candiate words. In this case, IME handles the Tab key
+ // actually, but we cannot know it because composition string is
+ // not updated. So, let's mark eKeyDown event as "processed by IME"
+ // when there is composition string. This is same as Chrome.
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, trying to dispatch eKeyDown "
+ "event since it's not yet dispatched",
+ this));
+ if (!MaybeDispatchCurrentKeydownEvent(IsIMEComposing())) {
+ return true; // treat the eKeydDown event as consumed.
+ }
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, eKeyDown event has been "
+ "dispatched",
+ this));
+ }
+
+ if (currentKeyEvent->CanDispatchKeyPressEvent() && !wasComposing && !IsIMEComposing()) {
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Error, ("%p TextInputHandler::HandleKeyDownEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure "
+ "at dispatching keypress",
+ this));
+ return false;
+ }
+
+ WidgetKeyboardEvent keypressEvent(true, eKeyPress, widget);
+ currentKeyEvent->InitKeyEvent(this, keypressEvent, false);
+
+ // If we called interpretKeyEvents and this isn't normal character input
+ // then IME probably ate the event for some reason. We do not want to
+ // send a key press event in that case.
+ // TODO:
+ // There are some other cases which IME eats the current event.
+ // 1. If key events were nested during calling interpretKeyEvents, it means
+ // that IME did something. Then, we should do nothing.
+ // 2. If one or more commands are called like "deleteBackward", we should
+ // dispatch keypress event at that time. Note that the command may have
+ // been a converted or generated action by IME. Then, we shouldn't do
+ // our default action for this key.
+ if (!(interpretKeyEventsCalled && IsNormalCharInputtingEvent(aNativeEvent))) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, trying to dispatch "
+ "eKeyPress event since it's not yet dispatched",
+ this));
+ nsEventStatus status = nsEventStatus_eIgnore;
+ currentKeyEvent->mKeyPressDispatched =
+ mDispatcher->MaybeDispatchKeypressEvents(keypressEvent, status, currentKeyEvent);
+ currentKeyEvent->mKeyPressHandled = (status == nsEventStatus_eConsumeNoDefault);
+ currentKeyEvent->mKeyPressDispatched = true;
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, eKeyPress event has been "
+ "dispatched",
+ this));
+ }
+ }
+
+ // Note: mWidget might have become null here. Don't count on it from here on.
+
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, "
+ "keydown handled=%s, keypress handled=%s, causedOtherKeyEvents=%s, "
+ "compositionDispatched=%s",
+ this, TrueOrFalse(currentKeyEvent->mKeyDownHandled),
+ TrueOrFalse(currentKeyEvent->mKeyPressHandled),
+ TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents),
+ TrueOrFalse(currentKeyEvent->mCompositionDispatched)));
+ // Insert empty line to the log for easier to read.
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info, (""));
+ return currentKeyEvent->IsDefaultPrevented();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+void TextInputHandler::HandleKeyUpEvent(NSEvent* aNativeEvent) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyUpEvent, aNativeEvent=%p, "
+ "type=%s, keyCode=%u (0x%X), modifierFlags=0x%lX, characters=\"%s\", "
+ "charactersIgnoringModifiers=\"%s\", "
+ "IsIMEComposing()=%s",
+ this, aNativeEvent, GetNativeKeyEventType(aNativeEvent), [aNativeEvent keyCode],
+ [aNativeEvent keyCode], static_cast<unsigned long>([aNativeEvent modifierFlags]),
+ GetCharacters([aNativeEvent characters]),
+ GetCharacters([aNativeEvent charactersIgnoringModifiers]), TrueOrFalse(IsIMEComposing())));
+
+ if (Destroyed()) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info, ("%p TextInputHandler::HandleKeyUpEvent, "
+ "widget has been already destroyed",
+ this));
+ return;
+ }
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Error, ("%p TextInputHandler::HandleKeyUpEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return;
+ }
+
+ // Neither Chrome for macOS nor Safari marks "keyup" event as "processed by
+ // IME" even during composition. So, let's follow this behavior.
+ WidgetKeyboardEvent keyupEvent(true, eKeyUp, mWidget);
+ InitKeyEvent(aNativeEvent, keyupEvent, false);
+
+ KeyEventState currentKeyEvent(aNativeEvent);
+ nsEventStatus status = nsEventStatus_eIgnore;
+ mDispatcher->DispatchKeyboardEvent(eKeyUp, keyupEvent, status, &currentKeyEvent);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void TextInputHandler::HandleFlagsChanged(NSEvent* aNativeEvent) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (Destroyed()) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info, ("%p TextInputHandler::HandleFlagsChanged, "
+ "widget has been already destroyed",
+ this));
+ return;
+ }
+
+ RefPtr<nsChildView> kungFuDeathGrip(mWidget);
+ mozilla::Unused << kungFuDeathGrip; // Not referenced within this function
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleFlagsChanged, aNativeEvent=%p, "
+ "type=%s, keyCode=%s (0x%X), modifierFlags=0x%08lX, "
+ "sLastModifierState=0x%08lX, IsIMEComposing()=%s",
+ this, aNativeEvent, GetNativeKeyEventType(aNativeEvent),
+ GetKeyNameForNativeKeyCode([aNativeEvent keyCode]), [aNativeEvent keyCode],
+ static_cast<unsigned long>([aNativeEvent modifierFlags]),
+ static_cast<unsigned long>(sLastModifierState), TrueOrFalse(IsIMEComposing())));
+
+ MOZ_ASSERT([aNativeEvent type] == NSEventTypeFlagsChanged);
+
+ NSUInteger diff = [aNativeEvent modifierFlags] ^ sLastModifierState;
+ // Device dependent flags for left-control key, both shift keys, both command
+ // keys and both option keys have been defined in Next's SDK. But we
+ // shouldn't use it directly as far as possible since Cocoa SDK doesn't
+ // define them. Fortunately, we need them only when we dispatch keyup
+ // events. So, we can usually know the actual relation between keyCode and
+ // device dependent flags. However, we need to remove following flags first
+ // since the differences don't indicate modifier key state.
+ // NX_STYLUSPROXIMITYMASK: Probably used for pen like device.
+ // kCGEventFlagMaskNonCoalesced (= NX_NONCOALSESCEDMASK): See the document for
+ // Quartz Event Services.
+ diff &= ~(NX_STYLUSPROXIMITYMASK | kCGEventFlagMaskNonCoalesced);
+
+ switch ([aNativeEvent keyCode]) {
+ // CapsLock state and other modifier states are different:
+ // CapsLock state does not revert when the CapsLock key goes up, as the
+ // modifier state does for other modifier keys on key up.
+ case kVK_CapsLock: {
+ // Fire key down event for caps lock.
+ DispatchKeyEventForFlagsChanged(aNativeEvent, true);
+ // XXX should we fire keyup event too? The keyup event for CapsLock key
+ // is never dispatched on Gecko.
+ // XXX WebKit dispatches keydown event when CapsLock is locked, otherwise,
+ // keyup event. If we do so, we cannot keep the consistency with other
+ // platform's behavior...
+ break;
+ }
+
+ // If the event is caused by pressing or releasing a modifier key, just
+ // dispatch the key's event.
+ case kVK_Shift:
+ case kVK_RightShift:
+ case kVK_Command:
+ case kVK_RightCommand:
+ case kVK_Control:
+ case kVK_RightControl:
+ case kVK_Option:
+ case kVK_RightOption:
+ case kVK_Help: {
+ // We assume that at most one modifier is changed per event if the event
+ // is caused by pressing or releasing a modifier key.
+ bool isKeyDown = ([aNativeEvent modifierFlags] & diff) != 0;
+ DispatchKeyEventForFlagsChanged(aNativeEvent, isKeyDown);
+ // XXX Some applications might send the event with incorrect device-
+ // dependent flags.
+ if (isKeyDown && ((diff & ~NSEventModifierFlagDeviceIndependentFlagsMask) != 0)) {
+ unsigned short keyCode = [aNativeEvent keyCode];
+ const ModifierKey* modifierKey = GetModifierKeyForDeviceDependentFlags(diff);
+ if (modifierKey && modifierKey->keyCode != keyCode) {
+ // Although, we're not sure the actual cause of this case, the stored
+ // modifier information and the latest key event information may be
+ // mismatched. Then, let's reset the stored information.
+ // NOTE: If this happens, it may fail to handle NSEventTypeFlagsChanged event
+ // in the default case (below). However, it's the rare case handler
+ // and this case occurs rarely. So, we can ignore the edge case bug.
+ NS_WARNING("Resetting stored modifier key information");
+ mModifierKeys.Clear();
+ modifierKey = nullptr;
+ }
+ if (!modifierKey) {
+ mModifierKeys.AppendElement(ModifierKey(diff, keyCode));
+ }
+ }
+ break;
+ }
+
+ // Currently we don't support Fn key since other browsers don't dispatch
+ // events for it and we don't have keyCode for this key.
+ // It should be supported when we implement .key and .char.
+ case kVK_Function:
+ break;
+
+ // If the event is caused by something else than pressing or releasing a
+ // single modifier key (for example by the app having been deactivated
+ // using command-tab), use the modifiers themselves to determine which
+ // key's event to dispatch, and whether it's a keyup or keydown event.
+ // In all cases we assume one or more modifiers are being deactivated
+ // (never activated) -- otherwise we'd have received one or more events
+ // corresponding to a single modifier key being pressed.
+ default: {
+ NSUInteger modifiers = sLastModifierState;
+ AutoTArray<unsigned short, 10> dispatchedKeyCodes;
+ for (int32_t bit = 0; bit < 32; ++bit) {
+ NSUInteger flag = 1 << bit;
+ if (!(diff & flag)) {
+ continue;
+ }
+
+ // Given correct information from the application, a flag change here
+ // will normally be a deactivation (except for some lockable modifiers
+ // such as CapsLock). But some applications (like VNC) can send an
+ // activating event with a zero keyCode. So we need to check for that
+ // here.
+ bool dispatchKeyDown = ((flag & [aNativeEvent modifierFlags]) != 0);
+
+ unsigned short keyCode = 0;
+ if (flag & NSEventModifierFlagDeviceIndependentFlagsMask) {
+ switch (flag) {
+ case NSEventModifierFlagCapsLock:
+ keyCode = kVK_CapsLock;
+ dispatchKeyDown = true;
+ break;
+
+ case NSEventModifierFlagNumericPad:
+ // NSEventModifierFlagNumericPad is fired by VNC a lot. But not all of
+ // these events can really be Clear key events, so we just ignore
+ // them.
+ continue;
+
+ case NSEventModifierFlagHelp:
+ keyCode = kVK_Help;
+ break;
+
+ case NSEventModifierFlagFunction:
+ // An NSEventModifierFlagFunction change here will normally be a
+ // deactivation. But sometimes it will be an activation send (by
+ // VNC for example) with a zero keyCode.
+ continue;
+
+ // These cases (NSEventModifierFlagShift, NSEventModifierFlagControl,
+ // NSEventModifierFlagOption and NSEventModifierFlagCommand) should be handled by the
+ // other branch of the if statement, below (which handles device dependent flags).
+ // However, some applications (like VNC) can send key events without
+ // any device dependent flags, so we handle them here instead.
+ case NSEventModifierFlagShift:
+ keyCode = (modifiers & 0x0004) ? kVK_RightShift : kVK_Shift;
+ break;
+ case NSEventModifierFlagControl:
+ keyCode = (modifiers & 0x2000) ? kVK_RightControl : kVK_Control;
+ break;
+ case NSEventModifierFlagOption:
+ keyCode = (modifiers & 0x0040) ? kVK_RightOption : kVK_Option;
+ break;
+ case NSEventModifierFlagCommand:
+ keyCode = (modifiers & 0x0010) ? kVK_RightCommand : kVK_Command;
+ break;
+
+ default:
+ continue;
+ }
+ } else {
+ const ModifierKey* modifierKey = GetModifierKeyForDeviceDependentFlags(flag);
+ if (!modifierKey) {
+ // See the note above (in the other branch of the if statement)
+ // about the NSEventModifierFlagShift, NSEventModifierFlagControl,
+ // NSEventModifierFlagOption and NSEventModifierFlagCommand cases.
+ continue;
+ }
+ keyCode = modifierKey->keyCode;
+ }
+
+ // Remove flags
+ modifiers &= ~flag;
+ switch (keyCode) {
+ case kVK_Shift: {
+ const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_RightShift);
+ if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagShift;
+ }
+ break;
+ }
+ case kVK_RightShift: {
+ const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_Shift);
+ if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagShift;
+ }
+ break;
+ }
+ case kVK_Command: {
+ const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_RightCommand);
+ if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagCommand;
+ }
+ break;
+ }
+ case kVK_RightCommand: {
+ const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_Command);
+ if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagCommand;
+ }
+ break;
+ }
+ case kVK_Control: {
+ const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_RightControl);
+ if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagControl;
+ }
+ break;
+ }
+ case kVK_RightControl: {
+ const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_Control);
+ if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagControl;
+ }
+ break;
+ }
+ case kVK_Option: {
+ const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_RightOption);
+ if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagOption;
+ }
+ break;
+ }
+ case kVK_RightOption: {
+ const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_Option);
+ if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagOption;
+ }
+ break;
+ }
+ case kVK_Help:
+ modifiers &= ~NSEventModifierFlagHelp;
+ break;
+ default:
+ break;
+ }
+
+ // Avoid dispatching same keydown/keyup events twice or more.
+ // We must be able to assume that there is no case to dispatch
+ // both keydown and keyup events with same key code value here.
+ if (dispatchedKeyCodes.Contains(keyCode)) {
+ continue;
+ }
+ dispatchedKeyCodes.AppendElement(keyCode);
+
+ NSEvent* event = [NSEvent keyEventWithType:NSEventTypeFlagsChanged
+ location:[aNativeEvent locationInWindow]
+ modifierFlags:modifiers
+ timestamp:[aNativeEvent timestamp]
+ windowNumber:[aNativeEvent windowNumber]
+ context:nil
+ characters:@""
+ charactersIgnoringModifiers:@""
+ isARepeat:NO
+ keyCode:keyCode];
+ DispatchKeyEventForFlagsChanged(event, dispatchKeyDown);
+ if (Destroyed()) {
+ break;
+ }
+
+ // Stop if focus has changed.
+ // Check to see if mView is still the first responder.
+ if (![mView isFirstResponder]) {
+ break;
+ }
+ }
+ break;
+ }
+ }
+
+ // Be aware, the widget may have been destroyed.
+ sLastModifierState = [aNativeEvent modifierFlags];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+const TextInputHandler::ModifierKey* TextInputHandler::GetModifierKeyForNativeKeyCode(
+ unsigned short aKeyCode) const {
+ for (ModifierKeyArray::index_type i = 0; i < mModifierKeys.Length(); ++i) {
+ if (mModifierKeys[i].keyCode == aKeyCode) {
+ return &((ModifierKey&)mModifierKeys[i]);
+ }
+ }
+ return nullptr;
+}
+
+const TextInputHandler::ModifierKey* TextInputHandler::GetModifierKeyForDeviceDependentFlags(
+ NSUInteger aFlags) const {
+ for (ModifierKeyArray::index_type i = 0; i < mModifierKeys.Length(); ++i) {
+ if (mModifierKeys[i].GetDeviceDependentFlags() ==
+ (aFlags & ~NSEventModifierFlagDeviceIndependentFlagsMask)) {
+ return &((ModifierKey&)mModifierKeys[i]);
+ }
+ }
+ return nullptr;
+}
+
+void TextInputHandler::DispatchKeyEventForFlagsChanged(NSEvent* aNativeEvent,
+ bool aDispatchKeyDown) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (Destroyed()) {
+ return;
+ }
+
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::DispatchKeyEventForFlagsChanged, aNativeEvent=%p, "
+ "type=%s, keyCode=%s (0x%X), aDispatchKeyDown=%s, IsIMEComposing()=%s",
+ this, aNativeEvent, GetNativeKeyEventType(aNativeEvent),
+ GetKeyNameForNativeKeyCode([aNativeEvent keyCode]), [aNativeEvent keyCode],
+ TrueOrFalse(aDispatchKeyDown), TrueOrFalse(IsIMEComposing())));
+
+ if ([aNativeEvent type] != NSEventTypeFlagsChanged) {
+ return;
+ }
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Error, ("%p TextInputHandler::DispatchKeyEventForFlagsChanged, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return;
+ }
+
+ EventMessage message = aDispatchKeyDown ? eKeyDown : eKeyUp;
+
+ // Fire a key event for the modifier key. Note that even if modifier key
+ // is pressed during composition, we shouldn't mark the keyboard event as
+ // "processed by IME" since neither Chrome for macOS nor Safari does it.
+ WidgetKeyboardEvent keyEvent(true, message, mWidget);
+ InitKeyEvent(aNativeEvent, keyEvent, false);
+
+ KeyEventState currentKeyEvent(aNativeEvent);
+ nsEventStatus status = nsEventStatus_eIgnore;
+ mDispatcher->DispatchKeyboardEvent(message, keyEvent, status, &currentKeyEvent);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void TextInputHandler::InsertText(NSAttributedString* aAttrString, NSRange* aReplacementRange) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (Destroyed()) {
+ return;
+ }
+
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::InsertText, aAttrString=\"%s\", "
+ "aReplacementRange=%p { location=%lu, length=%lu }, "
+ "IsIMEComposing()=%s, "
+ "keyevent=%p, keydownDispatched=%s, "
+ "keydownHandled=%s, keypressDispatched=%s, "
+ "causedOtherKeyEvents=%s, compositionDispatched=%s",
+ this, GetCharacters([aAttrString string]), aReplacementRange,
+ static_cast<unsigned long>(aReplacementRange ? aReplacementRange->location : 0),
+ static_cast<unsigned long>(aReplacementRange ? aReplacementRange->length : 0),
+ TrueOrFalse(IsIMEComposing()), currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr,
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownDispatched) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressDispatched) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCompositionDispatched) : "N/A"));
+
+ InputContext context = mWidget->GetInputContext();
+ bool isEditable = (context.mIMEState.mEnabled == IMEEnabled::Enabled ||
+ context.mIMEState.mEnabled == IMEEnabled::Password);
+ NSRange selectedRange = SelectedRange();
+
+ nsAutoString str;
+ nsCocoaUtils::GetStringForNSString([aAttrString string], str);
+
+ AutoInsertStringClearer clearer(currentKeyEvent);
+ if (currentKeyEvent) {
+ currentKeyEvent->mInsertString = &str;
+ }
+
+ if (!IsIMEComposing() && str.IsEmpty()) {
+ // 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;
+ }
+
+ // When current keydown event causes this empty text input, let's
+ // dispatch eKeyDown event before any other events. Note that if we're
+ // in a composition, we've already dispatched eKeyDown event from
+ // TextInputHandler::HandleKeyDownEvent().
+ // XXX Should we mark this eKeyDown event as "processed by IME"?
+ RefPtr<TextInputHandler> kungFuDeathGrip(this);
+ if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(false)) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::InsertText, eKeyDown caused focus move or "
+ "something and canceling the composition",
+ this));
+ return;
+ }
+
+ // Delete the selected range.
+ WidgetContentCommandEvent deleteCommandEvent(true, eContentCommandDelete, mWidget);
+ DispatchEvent(deleteCommandEvent);
+ NS_ENSURE_TRUE_VOID(deleteCommandEvent.mSucceeded);
+ // Be aware! The widget might be destroyed here.
+ return;
+ }
+
+ bool isReplacingSpecifiedRange = isEditable && aReplacementRange &&
+ aReplacementRange->location != NSNotFound &&
+ !NSEqualRanges(selectedRange, *aReplacementRange);
+
+ // If this is not caused by pressing a key, there is a composition or
+ // replacing a range which is different from current selection, let's
+ // insert the text as committing a composition.
+ // If InsertText() is called two or more times, we should insert all
+ // text with composition events.
+ // XXX When InsertText() is called multiple times, Chromium dispatches
+ // only one composition event. So, we need to store InsertText()
+ // calls and flush later.
+ if (!currentKeyEvent || currentKeyEvent->mCompositionDispatched || IsIMEComposing() ||
+ isReplacingSpecifiedRange) {
+ InsertTextAsCommittingComposition(aAttrString, aReplacementRange);
+ if (currentKeyEvent) {
+ currentKeyEvent->mCompositionDispatched = true;
+ }
+ return;
+ }
+
+ // Don't let the same event be fired twice when hitting
+ // enter/return for Bug 420502. However, Korean IME (or some other
+ // simple IME) may work without marked text. For example, composing
+ // character may be inserted as committed text and it's modified with
+ // aReplacementRange. When a keydown starts new composition with
+ // committing previous character, InsertText() may be called twice,
+ // one is for committing previous character and then, inserting new
+ // composing character as committed character. In the latter case,
+ // |CanDispatchKeyPressEvent()| returns true but we need to dispatch
+ // keypress event for the new character. So, when IME tries to insert
+ // printable characters, we should ignore current key event state even
+ // after the keydown has already caused dispatching composition event.
+ // XXX Anyway, we should sort out around this at fixing bug 1338460.
+ if (currentKeyEvent && !currentKeyEvent->CanDispatchKeyPressEvent() &&
+ (str.IsEmpty() || (str.Length() == 1 && !IsPrintableChar(str[0])))) {
+ return;
+ }
+
+ // This is the normal path to input a character when you press a key.
+ // Let's dispatch eKeyDown event now.
+ RefPtr<TextInputHandler> kungFuDeathGrip(this);
+ if (!MaybeDispatchCurrentKeydownEvent(false)) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::InsertText, eKeyDown caused focus move or "
+ "something and canceling the composition",
+ this));
+ return;
+ }
+
+ // XXX Shouldn't we hold mDispatcher instead of mWidget?
+ RefPtr<nsChildView> widget(mWidget);
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Error, ("%p TextInputHandler::InsertText, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return;
+ }
+
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::InsertText, "
+ "maybe dispatches eKeyPress event without control, alt and meta modifiers",
+ this));
+
+ // Dispatch keypress event with char instead of compositionchange event
+ WidgetKeyboardEvent keypressEvent(true, eKeyPress, widget);
+ // XXX Why do we need to dispatch keypress event for not inputting any
+ // string? If it wants to delete the specified range, should we
+ // dispatch an eContentCommandDelete event instead? Because this
+ // must not be caused by a key operation, a part of IME's processing.
+
+ // 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.
+
+ if (currentKeyEvent) {
+ currentKeyEvent->InitKeyEvent(this, keypressEvent, false);
+ } else {
+ nsCocoaUtils::InitInputEvent(keypressEvent, static_cast<NSEvent*>(nullptr));
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_USE_STRING;
+ keypressEvent.mKeyValue = str;
+ // FYI: TextEventDispatcher will set mKeyCode to 0 for printable key's
+ // keypress events even if they don't cause inputting non-empty string.
+ }
+
+ // TODO:
+ // If mCurrentKeyEvent.mKeyEvent is null, the text should be inputted as
+ // composition events.
+ nsEventStatus status = nsEventStatus_eIgnore;
+ bool keyPressDispatched =
+ mDispatcher->MaybeDispatchKeypressEvents(keypressEvent, status, currentKeyEvent);
+ bool keyPressHandled = (status == nsEventStatus_eConsumeNoDefault);
+
+ // Note: mWidget might have become null here. Don't count on it from here on.
+
+ if (currentKeyEvent) {
+ currentKeyEvent->mKeyPressHandled = keyPressHandled;
+ currentKeyEvent->mKeyPressDispatched = keyPressDispatched;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+bool TextInputHandler::HandleCommand(Command aCommand) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (Destroyed()) {
+ return false;
+ }
+
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleCommand, "
+ "aCommand=%s, IsIMEComposing()=%s, "
+ "keyevent=%p, keydownHandled=%s, keypressDispatched=%s, "
+ "causedOtherKeyEvents=%s, compositionDispatched=%s",
+ this, ToChar(aCommand), TrueOrFalse(IsIMEComposing()),
+ 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"));
+
+ // The command shouldn't be handled, let's ignore it.
+ if (currentKeyEvent && !currentKeyEvent->CanHandleCommand()) {
+ return false;
+ }
+
+ // When current keydown event causes this command, let's dispatch
+ // eKeyDown event before any other events. Note that if we're in a
+ // composition, we've already dispatched eKeyDown event from
+ // TextInputHandler::HandleKeyDownEvent().
+ RefPtr<TextInputHandler> kungFuDeathGrip(this);
+ if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(false)) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::SetMarkedText, eKeyDown caused focus move or "
+ "something and canceling the composition",
+ this));
+ return false;
+ }
+
+ // If it's in composition, we cannot dispatch keypress event.
+ // Therefore, we should use different approach or give up to handle
+ // the command.
+ if (IsIMEComposing()) {
+ switch (aCommand) {
+ case Command::InsertLineBreak:
+ case Command::InsertParagraph: {
+ // Insert '\n' as committing composition.
+ // 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
+ // NSEventTypeKeyDown event or should insert it with committing composition.
+ NSAttributedString* lineBreaker = [[NSAttributedString alloc] initWithString:@"\n"];
+ InsertTextAsCommittingComposition(lineBreaker, nullptr);
+ if (currentKeyEvent) {
+ currentKeyEvent->mCompositionDispatched = true;
+ }
+ [lineBreaker release];
+ return true;
+ }
+ case Command::DeleteCharBackward:
+ case Command::DeleteCharForward:
+ case Command::DeleteToBeginningOfLine:
+ case Command::DeleteWordBackward:
+ case Command::DeleteWordForward:
+ // Don't remove any contents during composition.
+ return false;
+ case Command::InsertTab:
+ case Command::InsertBacktab:
+ // Don't move focus during composition.
+ return false;
+ case Command::CharNext:
+ case Command::SelectCharNext:
+ case Command::WordNext:
+ case Command::SelectWordNext:
+ case Command::EndLine:
+ case Command::SelectEndLine:
+ case Command::CharPrevious:
+ case Command::SelectCharPrevious:
+ case Command::WordPrevious:
+ case Command::SelectWordPrevious:
+ case Command::BeginLine:
+ case Command::SelectBeginLine:
+ case Command::LinePrevious:
+ case Command::SelectLinePrevious:
+ case Command::MoveTop:
+ case Command::LineNext:
+ case Command::SelectLineNext:
+ case Command::MoveBottom:
+ case Command::SelectBottom:
+ case Command::SelectPageUp:
+ case Command::SelectPageDown:
+ case Command::ScrollBottom:
+ case Command::ScrollTop:
+ // Don't move selection during composition.
+ return false;
+ case Command::CancelOperation:
+ case Command::Complete:
+ // Don't handle Escape key by ourselves during composition.
+ return false;
+ case Command::ScrollPageUp:
+ case Command::ScrollPageDown:
+ // Allow to scroll.
+ break;
+ default:
+ break;
+ }
+ }
+
+ RefPtr<nsChildView> widget(mWidget);
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Error, ("%p, TextInputHandler::HandleCommand, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ // TODO: If it's not appropriate keypress but user customized the OS
+ // settings to do the command with other key, we should just set
+ // command to the keypress event and it should be handled as
+ // the key press in editor.
+
+ // If it's handling actual key event and hasn't cause any composition
+ // events nor other key events, we should expose actual modifier state.
+ // Otherwise, we should adjust Control, Option and Command state since
+ // editor may behave differently if some of them are active.
+ bool dispatchFakeKeyPress = !(currentKeyEvent && currentKeyEvent->IsProperKeyEvent(aCommand));
+
+ WidgetKeyboardEvent keydownEvent(true, eKeyDown, widget);
+ WidgetKeyboardEvent keypressEvent(true, eKeyPress, widget);
+ if (!dispatchFakeKeyPress) {
+ // If we're acutally handling a key press, we should dispatch
+ // the keypress event as-is.
+ currentKeyEvent->InitKeyEvent(this, keydownEvent, false);
+ currentKeyEvent->InitKeyEvent(this, keypressEvent, false);
+ } else {
+ // Otherwise, we should dispatch "fake" keypress event.
+ // However, for making it possible to compute edit commands, we need to
+ // set current native key event to the fake keyboard event even if it's
+ // not same as what we expect since the native keyboard event caused
+ // this command.
+ NSEvent* keyEvent = currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr;
+ keydownEvent.mNativeKeyEvent = keypressEvent.mNativeKeyEvent = keyEvent;
+ NS_WARNING_ASSERTION(keypressEvent.mNativeKeyEvent,
+ "Without native key event, NativeKeyBindings cannot compute aCommand");
+ switch (aCommand) {
+ case Command::InsertLineBreak:
+ case Command::InsertParagraph: {
+ // 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.
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_RETURN;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Enter;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::InsertLineBreak) {
+ // In default settings, Ctrl + Enter causes insertLineBreak command.
+ // So, let's make Ctrl state active of the keypress event.
+ keypressEvent.mModifiers |= MODIFIER_CONTROL;
+ }
+ break;
+ }
+ case Command::InsertTab:
+ case Command::InsertBacktab:
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_TAB;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Tab;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::InsertBacktab) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ break;
+ case Command::DeleteCharBackward:
+ case Command::DeleteToBeginningOfLine:
+ case Command::DeleteWordBackward: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_BACK;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Backspace;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::DeleteToBeginningOfLine) {
+ keypressEvent.mModifiers |= MODIFIER_META;
+ } else if (aCommand == Command::DeleteWordBackward) {
+ keypressEvent.mModifiers |= MODIFIER_ALT;
+ }
+ break;
+ }
+ case Command::DeleteCharForward:
+ case Command::DeleteWordForward: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_DELETE;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Delete;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::DeleteWordForward) {
+ keypressEvent.mModifiers |= MODIFIER_ALT;
+ }
+ break;
+ }
+ case Command::CharNext:
+ case Command::SelectCharNext:
+ case Command::WordNext:
+ case Command::SelectWordNext:
+ case Command::EndLine:
+ case Command::SelectEndLine: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_RIGHT;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_ArrowRight;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectCharNext || aCommand == Command::SelectWordNext ||
+ aCommand == Command::SelectEndLine) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ if (aCommand == Command::WordNext || aCommand == Command::SelectWordNext) {
+ keypressEvent.mModifiers |= MODIFIER_ALT;
+ }
+ if (aCommand == Command::EndLine || aCommand == Command::SelectEndLine) {
+ keypressEvent.mModifiers |= MODIFIER_META;
+ }
+ break;
+ }
+ case Command::CharPrevious:
+ case Command::SelectCharPrevious:
+ case Command::WordPrevious:
+ case Command::SelectWordPrevious:
+ case Command::BeginLine:
+ case Command::SelectBeginLine: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_LEFT;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_ArrowLeft;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectCharPrevious || aCommand == Command::SelectWordPrevious ||
+ aCommand == Command::SelectBeginLine) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ if (aCommand == Command::WordPrevious || aCommand == Command::SelectWordPrevious) {
+ keypressEvent.mModifiers |= MODIFIER_ALT;
+ }
+ if (aCommand == Command::BeginLine || aCommand == Command::SelectBeginLine) {
+ keypressEvent.mModifiers |= MODIFIER_META;
+ }
+ break;
+ }
+ case Command::LinePrevious:
+ case Command::SelectLinePrevious:
+ case Command::MoveTop:
+ case Command::SelectTop: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_UP;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_ArrowUp;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectLinePrevious || aCommand == Command::SelectTop) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ if (aCommand == Command::MoveTop || aCommand == Command::SelectTop) {
+ keypressEvent.mModifiers |= MODIFIER_META;
+ }
+ break;
+ }
+ case Command::LineNext:
+ case Command::SelectLineNext:
+ case Command::MoveBottom:
+ case Command::SelectBottom: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_DOWN;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_ArrowDown;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectLineNext || aCommand == Command::SelectBottom) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ if (aCommand == Command::MoveBottom || aCommand == Command::SelectBottom) {
+ keypressEvent.mModifiers |= MODIFIER_META;
+ }
+ break;
+ }
+ case Command::ScrollPageUp:
+ case Command::SelectPageUp: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_PAGE_UP;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_PageUp;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectPageUp) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ break;
+ }
+ case Command::ScrollPageDown:
+ case Command::SelectPageDown: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_PAGE_DOWN;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_PageDown;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectPageDown) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ break;
+ }
+ case Command::ScrollBottom:
+ case Command::ScrollTop: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ if (aCommand == Command::ScrollBottom) {
+ keypressEvent.mKeyCode = NS_VK_END;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_End;
+ } else {
+ keypressEvent.mKeyCode = NS_VK_HOME;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Home;
+ }
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ break;
+ }
+ case Command::CancelOperation:
+ case Command::Complete: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_ESCAPE;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Escape;
+ keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::Complete) {
+ keypressEvent.mModifiers |= MODIFIER_ALT;
+ }
+ break;
+ }
+ default:
+ return false;
+ }
+
+ nsCocoaUtils::InitInputEvent(keydownEvent, keyEvent);
+ keydownEvent.mKeyCode = keypressEvent.mKeyCode;
+ keydownEvent.mKeyNameIndex = keypressEvent.mKeyNameIndex;
+ keydownEvent.mModifiers = keypressEvent.mModifiers;
+ }
+
+ // We've stopped dispatching "keypress" events of non-printable keys on
+ // the web. Therefore, we need to dispatch eKeyDown event here for web
+ // apps. This is non-standard behavior if we've already dispatched a
+ // "keydown" event. However, Chrome also dispatches such fake "keydown"
+ // (and "keypress") event for making same behavior as Safari.
+ nsEventStatus status = nsEventStatus_eIgnore;
+ if (mDispatcher->DispatchKeyboardEvent(eKeyDown, keydownEvent, status, nullptr)) {
+ bool keydownHandled = status == nsEventStatus_eConsumeNoDefault;
+ if (currentKeyEvent) {
+ currentKeyEvent->mKeyDownDispatched = true;
+ currentKeyEvent->mKeyDownHandled |= keydownHandled;
+ }
+ if (keydownHandled) {
+ // Don't dispatch eKeyPress event if preceding eKeyDown event is
+ // consumed for conforming to UI Events.
+ // XXX Perhaps, we should ignore previous eKeyDown event result
+ // even if we've already dispatched because it may notify web apps
+ // of different key information, e.g., it's handled by IME, but
+ // web apps want to handle only this key.
+ return true;
+ }
+ }
+
+ 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 true;
+ }
+
+ // If keypress event isn't dispatched as expected, we should fallback to
+ // using composition events.
+ if (aCommand == Command::InsertLineBreak || aCommand == Command::InsertParagraph) {
+ NSAttributedString* lineBreaker = [[NSAttributedString alloc] initWithString:@"\n"];
+ InsertTextAsCommittingComposition(lineBreaker, nullptr);
+ if (currentKeyEvent) {
+ currentKeyEvent->mCompositionDispatched = true;
+ }
+ [lineBreaker release];
+ return true;
+ }
+
+ return false;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+bool TextInputHandler::DoCommandBySelector(const char* aSelector) {
+ RefPtr<nsChildView> widget(mWidget);
+
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::DoCommandBySelector, aSelector=\"%s\", "
+ "Destroyed()=%s, keydownDispatched=%s, keydownHandled=%s, "
+ "keypressDispatched=%s, keypressHandled=%s, causedOtherKeyEvents=%s",
+ this, aSelector ? aSelector : "", TrueOrFalse(Destroyed()),
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownDispatched) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressDispatched) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressHandled) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents) : "N/A"));
+
+ // If the command isn't caused by key operation, the command should
+ // be handled in the super class of the caller.
+ if (!currentKeyEvent) {
+ return Destroyed();
+ }
+
+ // When current keydown event causes this command, let's dispatch
+ // eKeyDown event before any other events. Note that if we're in a
+ // composition, we've already dispatched eKeyDown event from
+ // TextInputHandler::HandleKeyDownEvent().
+ RefPtr<TextInputHandler> kungFuDeathGrip(this);
+ if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(false)) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::SetMarkedText, eKeyDown caused focus move or "
+ "something and canceling the composition",
+ this));
+ return true;
+ }
+
+ // If the key operation causes this command, should dispatch a keypress
+ // event.
+ // XXX This must be worng. Even if this command is caused by the key
+ // operation, its our default action can be different from the
+ // command. So, in this case, we should dispatch a keypress event
+ // which have the command and editor should handle it.
+ if (currentKeyEvent->CanDispatchKeyPressEvent()) {
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Error, ("%p TextInputHandler::DoCommandBySelector, "
+ "FAILED, due to BeginNativeInputTransaction() failure "
+ "at dispatching keypress",
+ this));
+ return Destroyed();
+ }
+
+ WidgetKeyboardEvent keypressEvent(true, eKeyPress, widget);
+ currentKeyEvent->InitKeyEvent(this, keypressEvent, false);
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ currentKeyEvent->mKeyPressDispatched =
+ mDispatcher->MaybeDispatchKeypressEvents(keypressEvent, status, currentKeyEvent);
+ currentKeyEvent->mKeyPressHandled = (status == nsEventStatus_eConsumeNoDefault);
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::DoCommandBySelector, keypress event "
+ "dispatched, Destroyed()=%s, keypressHandled=%s",
+ this, TrueOrFalse(Destroyed()), TrueOrFalse(currentKeyEvent->mKeyPressHandled)));
+ // This command is now dispatched with keypress event.
+ // So, this shouldn't be handled by nobody anymore.
+ return true;
+ }
+
+ // If the key operation didn't cause keypress event or caused keypress event
+ // but not prevented its default, we need to honor the command. For example,
+ // Korean IME sends "insertNewline:" when committing existing composition
+ // with Enter key press. In such case, the key operation has been consumed
+ // by the committing composition but we still need to handle the command.
+ if (Destroyed() || !currentKeyEvent->CanHandleCommand()) {
+ return true;
+ }
+
+ // cancelOperation: command is fired after Escape or Command + Period.
+ // However, if ChildView implements cancelOperation:, calling
+ // [[ChildView super] doCommandBySelector:aSelector] when Command + Period
+ // causes only a call of [ChildView cancelOperation:sender]. I.e.,
+ // [ChildView keyDown:theEvent] becomes to be never called. For avoiding
+ // this odd behavior, we need to handle the command before super class of
+ // ChildView only when current key event is proper event to fire Escape
+ // keypress event.
+ if (!strcmp(aSelector, "cancelOperation:") && currentKeyEvent &&
+ currentKeyEvent->IsProperKeyEvent(Command::CancelOperation)) {
+ return HandleCommand(Command::CancelOperation);
+ }
+
+ // Otherwise, we've not handled the command yet. Propagate the command
+ // to the super class of ChildView.
+ return false;
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * IMEInputHandler implementation (static methods)
+ *
+ ******************************************************************************/
+
+bool IMEInputHandler::sStaticMembersInitialized = false;
+bool IMEInputHandler::sCachedIsForRTLLangage = false;
+CFStringRef IMEInputHandler::sLatestIMEOpenedModeInputSourceID = nullptr;
+IMEInputHandler* IMEInputHandler::sFocusedIMEHandler = nullptr;
+
+// static
+void IMEInputHandler::InitStaticMembers() {
+ if (sStaticMembersInitialized) return;
+ sStaticMembersInitialized = true;
+ // We need to check the keyboard layout changes on all applications.
+ CFNotificationCenterRef center = ::CFNotificationCenterGetDistributedCenter();
+ // XXX Don't we need to remove the observer at shut down?
+ // Mac Dev Center's document doesn't say how to remove the observer if
+ // the second parameter is NULL.
+ ::CFNotificationCenterAddObserver(center, NULL, OnCurrentTextInputSourceChange,
+ kTISNotifySelectedKeyboardInputSourceChanged, NULL,
+ CFNotificationSuspensionBehaviorDeliverImmediately);
+ // Initiailize with the current keyboard layout
+ OnCurrentTextInputSourceChange(NULL, NULL, kTISNotifySelectedKeyboardInputSourceChanged, NULL,
+ NULL);
+}
+
+// static
+void IMEInputHandler::OnCurrentTextInputSourceChange(CFNotificationCenterRef aCenter,
+ void* aObserver, CFStringRef aName,
+ const void* aObject,
+ CFDictionaryRef aUserInfo) {
+ // Cache the latest IME opened mode to sLatestIMEOpenedModeInputSourceID.
+ TISInputSourceWrapper tis;
+ tis.InitByCurrentInputSource();
+ if (tis.IsOpenedIMEMode()) {
+ tis.GetInputSourceID(sLatestIMEOpenedModeInputSourceID);
+ // Collect Input Source ID which includes input mode in most cases.
+ // However, if it's Japanese IME, collecting input mode (e.g.,
+ // "HiraganaKotei") does not make sense because in most languages,
+ // input mode changes "how to input", but Japanese IME changes
+ // "which type of characters to input". I.e., only Japanese IME
+ // users may use multiple input modes. If we'd collect each type of
+ // input mode of Japanese IMEs, it'd be difficult to count actual
+ // users of each IME from the result. So, only when active IME is
+ // a Japanese IME, we should use Bundle ID which does not contain
+ // input mode instead.
+ nsAutoString key;
+ if (tis.IsForJapaneseLanguage()) {
+ tis.GetBundleID(key);
+ } else {
+ tis.GetInputSourceID(key);
+ }
+ // 72 is kMaximumKeyStringLength in TelemetryScalar.cpp
+ if (key.Length() > 72) {
+ if (NS_IS_SURROGATE_PAIR(key[72 - 2], key[72 - 1])) {
+ key.Truncate(72 - 2);
+ } else {
+ key.Truncate(72 - 1);
+ }
+ // U+2026 is "..."
+ key.Append(char16_t(0x2026));
+ }
+ Telemetry::ScalarSet(Telemetry::ScalarID::WIDGET_IME_NAME_ON_MAC, key, true);
+ }
+
+ if (MOZ_LOG_TEST(gIMELog, LogLevel::Info)) {
+ static CFStringRef sLastTIS = nullptr;
+ CFStringRef newTIS;
+ tis.GetInputSourceID(newTIS);
+ if (!sLastTIS || ::CFStringCompare(sLastTIS, newTIS, 0) != kCFCompareEqualTo) {
+ TISInputSourceWrapper tis1, tis2, tis3, tis4, tis5;
+ tis1.InitByCurrentKeyboardLayout();
+ tis2.InitByCurrentASCIICapableInputSource();
+ tis3.InitByCurrentASCIICapableKeyboardLayout();
+ tis4.InitByCurrentInputMethodKeyboardLayoutOverride();
+ tis5.InitByTISInputSourceRef(tis.GetKeyboardLayoutInputSource());
+ CFStringRef is0 = nullptr, is1 = nullptr, is2 = nullptr, is3 = nullptr, is4 = nullptr,
+ is5 = nullptr, type0 = nullptr, lang0 = nullptr, bundleID0 = nullptr;
+ tis.GetInputSourceID(is0);
+ tis1.GetInputSourceID(is1);
+ tis2.GetInputSourceID(is2);
+ tis3.GetInputSourceID(is3);
+ tis4.GetInputSourceID(is4);
+ tis5.GetInputSourceID(is5);
+ tis.GetInputSourceType(type0);
+ tis.GetPrimaryLanguage(lang0);
+ tis.GetBundleID(bundleID0);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMEInputHandler::OnCurrentTextInputSourceChange,\n"
+ " Current Input Source is changed to:\n"
+ " currentInputContext=%p\n"
+ " %s\n"
+ " type=%s %s\n"
+ " overridden keyboard layout=%s\n"
+ " used keyboard layout for translation=%s\n"
+ " primary language=%s\n"
+ " bundle ID=%s\n"
+ " current ASCII capable Input Source=%s\n"
+ " current Keyboard Layout=%s\n"
+ " current ASCII capable Keyboard Layout=%s",
+ [NSTextInputContext currentInputContext], GetCharacters(is0), GetCharacters(type0),
+ tis.IsASCIICapable() ? "- ASCII capable " : "", GetCharacters(is4),
+ GetCharacters(is5), GetCharacters(lang0), GetCharacters(bundleID0),
+ GetCharacters(is2), GetCharacters(is1), GetCharacters(is3)));
+ }
+ sLastTIS = newTIS;
+ }
+
+ /**
+ * When the direction is changed, all the children are notified.
+ * No need to treat the initial case separately because it is covered
+ * by the general case (sCachedIsForRTLLangage is initially false)
+ */
+ if (sCachedIsForRTLLangage != tis.IsForRTLLanguage()) {
+ WidgetUtils::SendBidiKeyboardInfoToContent();
+ sCachedIsForRTLLangage = tis.IsForRTLLanguage();
+ }
+}
+
+// static
+void IMEInputHandler::FlushPendingMethods(nsITimer* aTimer, void* aClosure) {
+ NS_ASSERTION(aClosure, "aClosure is null");
+ static_cast<IMEInputHandler*>(aClosure)->ExecutePendingMethods();
+}
+
+// static
+CFArrayRef IMEInputHandler::CreateAllIMEModeList() {
+ const void* keys[] = {kTISPropertyInputSourceType};
+ const void* values[] = {kTISTypeKeyboardInputMode};
+ CFDictionaryRef filter = ::CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1, NULL, NULL);
+ NS_ASSERTION(filter, "failed to create the filter");
+ CFArrayRef list = ::TISCreateInputSourceList(filter, true);
+ ::CFRelease(filter);
+ return list;
+}
+
+// static
+void IMEInputHandler::DebugPrintAllIMEModes() {
+ if (MOZ_LOG_TEST(gIMELog, LogLevel::Info)) {
+ CFArrayRef list = CreateAllIMEModeList();
+ MOZ_LOG(gIMELog, LogLevel::Info, ("IME mode configuration:"));
+ CFIndex idx = ::CFArrayGetCount(list);
+ TISInputSourceWrapper tis;
+ for (CFIndex i = 0; i < idx; ++i) {
+ TISInputSourceRef inputSource =
+ static_cast<TISInputSourceRef>(const_cast<void*>(::CFArrayGetValueAtIndex(list, i)));
+ tis.InitByTISInputSourceRef(inputSource);
+ nsAutoString name, isid, bundleID;
+ tis.GetLocalizedName(name);
+ tis.GetInputSourceID(isid);
+ tis.GetBundleID(bundleID);
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" %s\t<%s>%s%s\n"
+ " bundled in <%s>\n",
+ NS_ConvertUTF16toUTF8(name).get(), NS_ConvertUTF16toUTF8(isid).get(),
+ tis.IsASCIICapable() ? "" : "\t(Isn't ASCII capable)",
+ tis.IsEnabled() ? "" : "\t(Isn't Enabled)", NS_ConvertUTF16toUTF8(bundleID).get()));
+ }
+ ::CFRelease(list);
+ }
+}
+
+// static
+TSMDocumentID IMEInputHandler::GetCurrentTSMDocumentID() {
+ // At least on Mac OS X 10.6.x and 10.7.x, ::TSMGetActiveDocument() has a bug.
+ // The result of ::TSMGetActiveDocument() isn't modified for new active text
+ // input context until [NSTextInputContext currentInputContext] is called.
+ // Therefore, we need to call it here.
+ [NSTextInputContext currentInputContext];
+ return ::TSMGetActiveDocument();
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * IMEInputHandler implementation #1
+ * The methods are releated to the pending methods. Some jobs should be
+ * run after the stack is finished, e.g, some methods cannot run the jobs
+ * during processing the focus event. And also some other jobs should be
+ * run at the next focus event is processed.
+ * The pending methods are recorded in mPendingMethods. They are executed
+ * by ExecutePendingMethods via FlushPendingMethods.
+ *
+ ******************************************************************************/
+
+nsresult IMEInputHandler::NotifyIME(TextEventDispatcher* aTextEventDispatcher,
+ const IMENotification& aNotification) {
+ switch (aNotification.mMessage) {
+ case REQUEST_TO_COMMIT_COMPOSITION:
+ CommitIMEComposition();
+ return NS_OK;
+ case REQUEST_TO_CANCEL_COMPOSITION:
+ CancelIMEComposition();
+ return NS_OK;
+ case NOTIFY_IME_OF_FOCUS:
+ if (IsFocused()) {
+ nsIWidget* widget = aTextEventDispatcher->GetWidget();
+ if (widget && widget->GetInputContext().IsPasswordEditor()) {
+ EnableSecureEventInput();
+ } else {
+ EnsureSecureEventInputDisabled();
+ }
+ }
+ OnFocusChangeInGecko(true);
+ return NS_OK;
+ case NOTIFY_IME_OF_BLUR:
+ OnFocusChangeInGecko(false);
+ return NS_OK;
+ case NOTIFY_IME_OF_SELECTION_CHANGE:
+ OnSelectionChange(aNotification);
+ return NS_OK;
+ case NOTIFY_IME_OF_POSITION_CHANGE:
+ OnLayoutChange();
+ return NS_OK;
+ default:
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+}
+
+NS_IMETHODIMP_(IMENotificationRequests)
+IMEInputHandler::GetIMENotificationRequests() {
+ // XXX Shouldn't we move floating window which shows composition string
+ // when plugin has focus and its parent is scrolled or the window is
+ // moved?
+ return IMENotificationRequests();
+}
+
+NS_IMETHODIMP_(void)
+IMEInputHandler::OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) {
+ // XXX When input transaction is being stolen by add-on, what should we do?
+}
+
+NS_IMETHODIMP_(void)
+IMEInputHandler::WillDispatchKeyboardEvent(TextEventDispatcher* aTextEventDispatcher,
+ WidgetKeyboardEvent& aKeyboardEvent,
+ uint32_t aIndexOfKeypress, void* aData) {
+ // If the keyboard event is not caused by a native key event, we can do
+ // nothing here.
+ if (!aData) {
+ return;
+ }
+
+ KeyEventState* currentKeyEvent = static_cast<KeyEventState*>(aData);
+ NSEvent* nativeEvent = currentKeyEvent->mKeyEvent;
+ nsAString* insertString = currentKeyEvent->mInsertString;
+ if (aKeyboardEvent.mMessage == eKeyPress && aIndexOfKeypress == 0 &&
+ (!insertString || insertString->IsEmpty())) {
+ // Inform the child process that this is an event that we want a reply
+ // from.
+ // XXX This should be called only when the target is a remote process.
+ // However, it's difficult to check it under widget/.
+ // So, let's do this here for now, then,
+ // EventStateManager::PreHandleEvent() will reset the flags if
+ // the event target isn't in remote process.
+ aKeyboardEvent.MarkAsWaitingReplyFromRemoteProcess();
+ }
+ if (KeyboardLayoutOverrideRef().mOverrideEnabled) {
+ TISInputSourceWrapper tis;
+ tis.InitByLayoutID(KeyboardLayoutOverrideRef().mKeyboardLayout, true);
+ tis.WillDispatchKeyboardEvent(nativeEvent, insertString, aIndexOfKeypress, aKeyboardEvent);
+ } else {
+ TISInputSourceWrapper::CurrentInputSource().WillDispatchKeyboardEvent(
+ nativeEvent, insertString, aIndexOfKeypress, aKeyboardEvent);
+ }
+
+ // Remove basic modifiers from keypress event because if they are included
+ // but this causes inputting text, since TextEditor won't handle eKeyPress
+ // events whose ctrlKey, altKey or metaKey is true as text input.
+ // Note that this hack should be used only when an editor has focus because
+ // this is a hack for TextEditor and modifier key information may be
+ // important for current web app.
+ if (IsEditableContent() && insertString && aKeyboardEvent.mMessage == eKeyPress &&
+ aKeyboardEvent.mCharCode) {
+ aKeyboardEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ }
+}
+
+void IMEInputHandler::NotifyIMEOfFocusChangeInGecko() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::NotifyIMEOfFocusChangeInGecko, "
+ "Destroyed()=%s, IsFocused()=%s, inputContext=%p",
+ this, TrueOrFalse(Destroyed()), TrueOrFalse(IsFocused()),
+ mView ? [mView inputContext] : nullptr));
+
+ if (Destroyed()) {
+ return;
+ }
+
+ if (!IsFocused()) {
+ // retry at next focus event
+ mPendingMethods |= kNotifyIMEOfFocusChangeInGecko;
+ return;
+ }
+
+ MOZ_ASSERT(mView);
+ NSTextInputContext* inputContext = [mView inputContext];
+ NS_ENSURE_TRUE_VOID(inputContext);
+
+ // When an <input> element on a XUL <panel> element gets focus from an <input>
+ // element on the opener window of the <panel> element, the owner window
+ // still has native focus. Therefore, IMEs may store the opener window's
+ // level at this time because they don't know the actual focus is moved to
+ // different window. If IMEs try to get the newest window level after the
+ // focus change, we return the window level of the XUL <panel>'s widget.
+ // Therefore, let's emulate the native focus change. Then, IMEs can refresh
+ // the stored window level.
+ [inputContext deactivate];
+ [inputContext activate];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::SyncASCIICapableOnly() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SyncASCIICapableOnly, "
+ "Destroyed()=%s, IsFocused()=%s, mIsASCIICapableOnly=%s, "
+ "GetCurrentTSMDocumentID()=%p",
+ this, TrueOrFalse(Destroyed()), TrueOrFalse(IsFocused()),
+ TrueOrFalse(mIsASCIICapableOnly), GetCurrentTSMDocumentID()));
+
+ if (Destroyed()) {
+ return;
+ }
+
+ if (!IsFocused()) {
+ // retry at next focus event
+ mPendingMethods |= kSyncASCIICapableOnly;
+ return;
+ }
+
+ TSMDocumentID doc = GetCurrentTSMDocumentID();
+ if (!doc) {
+ // retry
+ mPendingMethods |= kSyncASCIICapableOnly;
+ NS_WARNING("Application is active but there is no active document");
+ ResetTimer();
+ return;
+ }
+
+ if (mIsASCIICapableOnly) {
+ CFArrayRef ASCIICapableTISList = ::TISCreateASCIICapableInputSourceList();
+ ::TSMSetDocumentProperty(doc, kTSMDocumentEnabledInputSourcesPropertyTag, sizeof(CFArrayRef),
+ &ASCIICapableTISList);
+ ::CFRelease(ASCIICapableTISList);
+ } else {
+ ::TSMRemoveDocumentProperty(doc, kTSMDocumentEnabledInputSourcesPropertyTag);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::ResetTimer() {
+ NS_ASSERTION(mPendingMethods != 0, "There are not pending methods, why this is called?");
+ if (mTimer) {
+ mTimer->Cancel();
+ } else {
+ mTimer = NS_NewTimer();
+ NS_ENSURE_TRUE(mTimer, );
+ }
+ mTimer->InitWithNamedFuncCallback(FlushPendingMethods, this, 0, nsITimer::TYPE_ONE_SHOT,
+ "IMEInputHandler::FlushPendingMethods");
+}
+
+void IMEInputHandler::ExecutePendingMethods() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+
+ if (![[NSApplication sharedApplication] isActive]) {
+ // If we're not active, we should retry at focus event
+ return;
+ }
+
+ uint32_t pendingMethods = mPendingMethods;
+ // First, reset the pending method flags because if each methods cannot
+ // run now, they can reentry to the pending flags by theirselves.
+ mPendingMethods = 0;
+
+ if (pendingMethods & kSyncASCIICapableOnly) SyncASCIICapableOnly();
+ if (pendingMethods & kNotifyIMEOfFocusChangeInGecko) {
+ NotifyIMEOfFocusChangeInGecko();
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * IMEInputHandler implementation (native event handlers)
+ *
+ ******************************************************************************/
+
+TextRangeType IMEInputHandler::ConvertToTextRangeType(uint32_t aUnderlineStyle,
+ NSRange& aSelectedRange) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::ConvertToTextRangeType, "
+ "aUnderlineStyle=%u, aSelectedRange.length=%lu,",
+ this, aUnderlineStyle, static_cast<unsigned long>(aSelectedRange.length)));
+
+ // We assume that aUnderlineStyle is NSUnderlineStyleSingle or
+ // NSUnderlineStyleThick. NSUnderlineStyleThick should indicate a selected
+ // clause. Otherwise, should indicate non-selected clause.
+
+ if (aSelectedRange.length == 0) {
+ switch (aUnderlineStyle) {
+ case NSUnderlineStyleSingle:
+ return TextRangeType::eRawClause;
+ case NSUnderlineStyleThick:
+ return TextRangeType::eSelectedRawClause;
+ default:
+ NS_WARNING("Unexpected line style");
+ return TextRangeType::eSelectedRawClause;
+ }
+ }
+
+ switch (aUnderlineStyle) {
+ case NSUnderlineStyleSingle:
+ return TextRangeType::eConvertedClause;
+ case NSUnderlineStyleThick:
+ return TextRangeType::eSelectedClause;
+ default:
+ NS_WARNING("Unexpected line style");
+ return TextRangeType::eSelectedClause;
+ }
+}
+
+uint32_t IMEInputHandler::GetRangeCount(NSAttributedString* aAttrString) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Iterate through aAttrString for the NSUnderlineStyleAttributeName and
+ // count the different segments adjusting limitRange as we go.
+ uint32_t count = 0;
+ NSRange effectiveRange;
+ NSRange limitRange = NSMakeRange(0, [aAttrString length]);
+ while (limitRange.length > 0) {
+ [aAttrString attribute:NSUnderlineStyleAttributeName
+ atIndex:limitRange.location
+ longestEffectiveRange:&effectiveRange
+ inRange:limitRange];
+ limitRange = NSMakeRange(NSMaxRange(effectiveRange),
+ NSMaxRange(limitRange) - NSMaxRange(effectiveRange));
+ count++;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::GetRangeCount, aAttrString=\"%s\", count=%u", this,
+ GetCharacters([aAttrString string]), count));
+
+ return count;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(0);
+}
+
+already_AddRefed<mozilla::TextRangeArray> IMEInputHandler::CreateTextRangeArray(
+ NSAttributedString* aAttrString, NSRange& aSelectedRange) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ RefPtr<mozilla::TextRangeArray> textRangeArray = new mozilla::TextRangeArray();
+
+ // Note that we shouldn't append ranges when composition string
+ // is empty because it may cause TextComposition confused.
+ if (![aAttrString length]) {
+ return textRangeArray.forget();
+ }
+
+ // Convert the Cocoa range into the TextRange Array used in Gecko.
+ // Iterate through the attributed string and map the underline attribute to
+ // Gecko IME textrange attributes. We may need to change the code here if
+ // we change the implementation of validAttributesForMarkedText.
+ NSRange limitRange = NSMakeRange(0, [aAttrString length]);
+ uint32_t rangeCount = GetRangeCount(aAttrString);
+ for (uint32_t i = 0; i < rangeCount && limitRange.length > 0; i++) {
+ NSRange effectiveRange;
+ id attributeValue = [aAttrString attribute:NSUnderlineStyleAttributeName
+ atIndex:limitRange.location
+ longestEffectiveRange:&effectiveRange
+ inRange:limitRange];
+
+ TextRange range;
+ range.mStartOffset = effectiveRange.location;
+ range.mEndOffset = NSMaxRange(effectiveRange);
+ range.mRangeType = ConvertToTextRangeType([attributeValue intValue], aSelectedRange);
+ textRangeArray->AppendElement(range);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::CreateTextRangeArray, "
+ "range={ mStartOffset=%u, mEndOffset=%u, mRangeType=%s }",
+ this, range.mStartOffset, range.mEndOffset, ToChar(range.mRangeType)));
+
+ limitRange = NSMakeRange(NSMaxRange(effectiveRange),
+ NSMaxRange(limitRange) - NSMaxRange(effectiveRange));
+ }
+
+ // Get current caret position.
+ TextRange range;
+ range.mStartOffset = aSelectedRange.location + aSelectedRange.length;
+ range.mEndOffset = range.mStartOffset;
+ range.mRangeType = TextRangeType::eCaret;
+ textRangeArray->AppendElement(range);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::CreateTextRangeArray, "
+ "range={ mStartOffset=%u, mEndOffset=%u, mRangeType=%s }",
+ this, range.mStartOffset, range.mEndOffset, ToChar(range.mRangeType)));
+
+ return textRangeArray.forget();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nullptr);
+}
+
+bool IMEInputHandler::DispatchCompositionStartEvent() {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionStartEvent, "
+ "mSelectedRange={ location=%lu, length=%lu }, Destroyed()=%s, "
+ "mView=%p, mWidget=%p, inputContext=%p, mIsIMEComposing=%s",
+ this, static_cast<unsigned long>(SelectedRange().location),
+ static_cast<unsigned long>(mSelectedRange.length), TrueOrFalse(Destroyed()), mView,
+ mWidget, mView ? [mView inputContext] : nullptr, TrueOrFalse(mIsIMEComposing)));
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionStartEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ NS_ASSERTION(!mIsIMEComposing, "There is a composition already");
+ mIsIMEComposing = true;
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+ mIsDeadKeyComposing =
+ currentKeyEvent && currentKeyEvent->mKeyEvent &&
+ TISInputSourceWrapper::CurrentInputSource().IsDeadKey(currentKeyEvent->mKeyEvent);
+
+ nsEventStatus status;
+ rv = mDispatcher->StartComposition(status);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionStartEvent, "
+ "FAILED, due to StartComposition() failure",
+ this));
+ return false;
+ }
+
+ if (Destroyed()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionStartEvent, "
+ "destroyed by compositionstart event",
+ this));
+ return false;
+ }
+
+ // FYI: compositionstart may cause committing composition by the webapp.
+ if (!mIsIMEComposing) {
+ return false;
+ }
+
+ // FYI: The selection range might have been modified by a compositionstart
+ // event handler.
+ mIMECompositionStart = SelectedRange().location;
+ return true;
+}
+
+bool IMEInputHandler::DispatchCompositionChangeEvent(const nsString& aText,
+ NSAttributedString* aAttrString,
+ NSRange& aSelectedRange) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionChangeEvent, "
+ "aText=\"%s\", aAttrString=\"%s\", "
+ "aSelectedRange={ location=%lu, length=%lu }, Destroyed()=%s, mView=%p, "
+ "mWidget=%p, inputContext=%p, mIsIMEComposing=%s",
+ this, NS_ConvertUTF16toUTF8(aText).get(), GetCharacters([aAttrString string]),
+ static_cast<unsigned long>(aSelectedRange.location),
+ static_cast<unsigned long>(aSelectedRange.length), TrueOrFalse(Destroyed()), mView,
+ mWidget, mView ? [mView inputContext] : nullptr, TrueOrFalse(mIsIMEComposing)));
+
+ NS_ENSURE_TRUE(!Destroyed(), false);
+
+ NS_ASSERTION(mIsIMEComposing, "We're not in composition");
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionChangeEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ RefPtr<TextRangeArray> rangeArray = CreateTextRangeArray(aAttrString, aSelectedRange);
+
+ rv = mDispatcher->SetPendingComposition(aText, rangeArray);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionChangeEvent, "
+ "FAILED, due to SetPendingComposition() failure",
+ this));
+ return false;
+ }
+
+ mSelectedRange.location = mIMECompositionStart + aSelectedRange.location;
+ mSelectedRange.length = aSelectedRange.length;
+
+ if (mIMECompositionString) {
+ [mIMECompositionString release];
+ }
+ mIMECompositionString = [[aAttrString string] retain];
+
+ nsEventStatus status;
+ rv = mDispatcher->FlushPendingComposition(status);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionChangeEvent, "
+ "FAILED, due to FlushPendingComposition() failure",
+ this));
+ return false;
+ }
+
+ if (Destroyed()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionChangeEvent, "
+ "destroyed by compositionchange event",
+ this));
+ return false;
+ }
+
+ // FYI: compositionstart may cause committing composition by the webapp.
+ return mIsIMEComposing;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+bool IMEInputHandler::DispatchCompositionCommitEvent(const nsAString* aCommitString) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionCommitEvent, "
+ "aCommitString=0x%p (\"%s\"), Destroyed()=%s, mView=%p, mWidget=%p, "
+ "inputContext=%p, mIsIMEComposing=%s",
+ this, aCommitString, aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : "",
+ TrueOrFalse(Destroyed()), mView, mWidget, mView ? [mView inputContext] : nullptr,
+ TrueOrFalse(mIsIMEComposing)));
+
+ NS_ASSERTION(mIsIMEComposing, "We're not in composition");
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ if (!Destroyed()) {
+ // IME may query selection immediately after this, however, in e10s mode,
+ // OnSelectionChange() will be called asynchronously. Until then, we
+ // should emulate expected selection range if the webapp does nothing.
+ mSelectedRange.location = mIMECompositionStart;
+ if (aCommitString) {
+ mSelectedRange.location += aCommitString->Length();
+ } else if (mIMECompositionString) {
+ nsAutoString commitString;
+ nsCocoaUtils::GetStringForNSString(mIMECompositionString, commitString);
+ mSelectedRange.location += commitString.Length();
+ }
+ mSelectedRange.length = 0;
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionCommitEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ } else {
+ nsEventStatus status;
+ rv = mDispatcher->CommitComposition(status, aCommitString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionCommitEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ }
+ }
+ }
+
+ mIsIMEComposing = mIsDeadKeyComposing = false;
+ mIMECompositionStart = UINT32_MAX;
+ if (mIMECompositionString) {
+ [mIMECompositionString release];
+ mIMECompositionString = nullptr;
+ }
+
+ if (Destroyed()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionCommitEvent, "
+ "destroyed by compositioncommit event",
+ this));
+ return false;
+ }
+
+ return true;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+bool IMEInputHandler::MaybeDispatchCurrentKeydownEvent(bool aIsProcessedByIME) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (Destroyed()) {
+ return false;
+ }
+ MOZ_ASSERT(mWidget);
+
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+ if (!currentKeyEvent || !currentKeyEvent->CanDispatchKeyDownEvent()) {
+ return true;
+ }
+
+ NSEvent* nativeEvent = currentKeyEvent->mKeyEvent;
+ if (NS_WARN_IF(!nativeEvent) || [nativeEvent type] != NSEventTypeKeyDown) {
+ return true;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::MaybeDispatchKeydownEvent, aIsProcessedByIME=%s "
+ "currentKeyEvent={ mKeyEvent(%p)={ type=%s, keyCode=%s (0x%X) } }, "
+ "aIsProcessedBy=%s, IsDeadKeyComposing()=%s",
+ this, TrueOrFalse(aIsProcessedByIME), nativeEvent, GetNativeKeyEventType(nativeEvent),
+ GetKeyNameForNativeKeyCode([nativeEvent keyCode]), [nativeEvent keyCode],
+ TrueOrFalse(IsIMEComposing()), TrueOrFalse(IsDeadKeyComposing())));
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+ RefPtr<TextEventDispatcher> dispatcher(mDispatcher);
+ nsresult rv = dispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchKeyEventForFlagsChanged, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ NSResponder* firstResponder = [[mView window] firstResponder];
+
+ // Mark currentKeyEvent as "dispatched eKeyDown event" and actually do it.
+ currentKeyEvent->mKeyDownDispatched = true;
+
+ RefPtr<nsChildView> widget(mWidget);
+
+ WidgetKeyboardEvent keydownEvent(true, eKeyDown, widget);
+ // Don't mark the eKeyDown event as "processed by IME" if the composition
+ // is started with dead key.
+ currentKeyEvent->InitKeyEvent(this, keydownEvent, aIsProcessedByIME && !IsDeadKeyComposing());
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ dispatcher->DispatchKeyboardEvent(eKeyDown, keydownEvent, status, currentKeyEvent);
+ currentKeyEvent->mKeyDownHandled = (status == nsEventStatus_eConsumeNoDefault);
+
+ if (Destroyed()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::MaybeDispatchKeydownEvent, "
+ "widget was destroyed by keydown event",
+ this));
+ return false;
+ }
+
+ // The key down event may have shifted the focus, in which case, we should
+ // not continue to handle current key sequence and let's commit current
+ // composition.
+ if (firstResponder != [[mView window] firstResponder]) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::MaybeDispatchKeydownEvent, "
+ "view lost focus by keydown event",
+ this));
+ CommitIMEComposition();
+ return false;
+ }
+
+ return true;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+void IMEInputHandler::InsertTextAsCommittingComposition(NSAttributedString* aAttrString,
+ NSRange* aReplacementRange) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::InsertTextAsCommittingComposition, "
+ "aAttrString=\"%s\", aReplacementRange=%p { location=%lu, length=%lu }, "
+ "Destroyed()=%s, IsIMEComposing()=%s, "
+ "mMarkedRange={ location=%lu, length=%lu }",
+ this, GetCharacters([aAttrString string]), aReplacementRange,
+ static_cast<unsigned long>(aReplacementRange ? aReplacementRange->location : 0),
+ static_cast<unsigned long>(aReplacementRange ? aReplacementRange->length : 0),
+ TrueOrFalse(Destroyed()), TrueOrFalse(IsIMEComposing()),
+ static_cast<unsigned long>(mMarkedRange.location),
+ static_cast<unsigned long>(mMarkedRange.length)));
+
+ if (IgnoreIMECommit()) {
+ MOZ_CRASH("IMEInputHandler::InsertTextAsCommittingComposition() must not"
+ "be called while canceling the composition");
+ }
+
+ if (Destroyed()) {
+ return;
+ }
+
+ // When current keydown event causes this text input, let's dispatch
+ // eKeyDown event before any other events. Note that if we're in a
+ // composition, we've already dispatched eKeyDown event from
+ // TextInputHandler::HandleKeyDownEvent().
+ // XXX Should we mark the eKeyDown event as "processed by IME"?
+ // However, if the key causes two or more Unicode characters as
+ // UTF-16 string, this is used. So, perhaps, we need to improve
+ // HandleKeyDownEvent() before do that.
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+ if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(false)) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::InsertTextAsCommittingComposition, eKeyDown "
+ "caused focus move or something and canceling the composition",
+ this));
+ 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)) {
+ if (!DispatchCompositionCommitEvent()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::InsertTextAsCommittingComposition, "
+ "destroyed by commiting composition for setting replacement range",
+ this));
+ return;
+ }
+ }
+
+ nsString str;
+ nsCocoaUtils::GetStringForNSString([aAttrString string], str);
+
+ if (!IsIMEComposing()) {
+ MOZ_DIAGNOSTIC_ASSERT(!str.IsEmpty());
+
+ // 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));
+ }
+
+ if (!StaticPrefs::intl_ime_use_composition_events_for_insert_text()) {
+ // In the default settings, we should not use composition events for
+ // inserting text without key press nor IME composition because the
+ // other browsers do so. This will cause only a cancelable `beforeinput`
+ // event whose `inputType` is `insertText`.
+ WidgetContentCommandEvent insertTextEvent(true, eContentCommandInsertText, mWidget);
+ insertTextEvent.mString = Some(str);
+ DispatchEvent(insertTextEvent);
+ return;
+ }
+
+ // Otherise, emulate an IME composition. This is our traditional behavior,
+ // but `beforeinput` events are not cancelable since they should be so for
+ // native IME limitation. So, this is now seriously imcompatible with the
+ // other browsers.
+ if (!DispatchCompositionStartEvent()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::InsertTextAsCommittingComposition, "
+ "cannot continue handling composition after compositionstart",
+ this));
+ return;
+ }
+ }
+
+ if (!DispatchCompositionCommitEvent(&str)) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::InsertTextAsCommittingComposition, "
+ "destroyed by compositioncommit event",
+ this));
+ return;
+ }
+
+ mMarkedRange = NSMakeRange(NSNotFound, 0);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::SetMarkedText(NSAttributedString* aAttrString, NSRange& aSelectedRange,
+ NSRange* aReplacementRange) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, "
+ "aAttrString=\"%s\", aSelectedRange={ location=%lu, length=%lu }, "
+ "aReplacementRange=%p { location=%lu, length=%lu }, "
+ "Destroyed()=%s, IsIMEComposing()=%s, "
+ "mMarkedRange={ location=%lu, length=%lu }, keyevent=%p, "
+ "keydownDispatched=%s, keydownHandled=%s, "
+ "keypressDispatched=%s, causedOtherKeyEvents=%s, "
+ "compositionDispatched=%s",
+ this, GetCharacters([aAttrString string]),
+ static_cast<unsigned long>(aSelectedRange.location),
+ static_cast<unsigned long>(aSelectedRange.length), aReplacementRange,
+ static_cast<unsigned long>(aReplacementRange ? aReplacementRange->location : 0),
+ static_cast<unsigned long>(aReplacementRange ? aReplacementRange->length : 0),
+ TrueOrFalse(Destroyed()), TrueOrFalse(IsIMEComposing()),
+ static_cast<unsigned long>(mMarkedRange.location),
+ static_cast<unsigned long>(mMarkedRange.length),
+ currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr,
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownDispatched) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressDispatched) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCompositionDispatched) : "N/A"));
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ // If SetMarkedText() is called during handling a key press, that means that
+ // the key event caused this composition. So, keypress event shouldn't
+ // be dispatched later, let's mark the key event causing composition event.
+ if (currentKeyEvent) {
+ currentKeyEvent->mCompositionDispatched = true;
+
+ // When current keydown event causes this text input, let's dispatch
+ // eKeyDown event before any other events. Note that if we're in a
+ // composition, we've already dispatched eKeyDown event from
+ // TextInputHandler::HandleKeyDownEvent(). On the other hand, if we're
+ // not in composition, the key event starts new composition. So, we
+ // need to mark the eKeyDown event as "processed by IME".
+ if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(true)) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, eKeyDown caused focus move or "
+ "something and canceling the composition",
+ this));
+ return;
+ }
+ }
+
+ 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)) {
+ AutoRestore<bool> ignoreIMECommit(mIgnoreIMECommit);
+ mIgnoreIMECommit = false;
+ if (!DispatchCompositionCommitEvent()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, "
+ "destroyed by commiting composition for setting replacement range",
+ this));
+ return;
+ }
+ }
+
+ nsString str;
+ nsCocoaUtils::GetStringForNSString([aAttrString string], str);
+
+ mMarkedRange.length = str.Length();
+
+ if (!IsIMEComposing() && !str.IsEmpty()) {
+ // If there is no selection and replacement range is specified, set the
+ // range as selection.
+ if (aReplacementRange && aReplacementRange->location != NSNotFound &&
+ !NSEqualRanges(SelectedRange(), *aReplacementRange)) {
+ // Set temporary selection range since OnSelectionChange is async.
+ mSelectedRange = *aReplacementRange;
+ if (NS_WARN_IF(!SetSelection(*aReplacementRange))) {
+ mSelectedRange.location = NSNotFound; // Marking dirty
+ return;
+ }
+ }
+
+ mMarkedRange.location = SelectedRange().location;
+
+ if (!DispatchCompositionStartEvent()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, cannot continue handling "
+ "composition after dispatching compositionstart",
+ this));
+ return;
+ }
+ }
+
+ if (!str.IsEmpty()) {
+ if (!DispatchCompositionChangeEvent(str, aAttrString, aSelectedRange)) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, cannot continue handling "
+ "composition after dispatching compositionchange",
+ this));
+ }
+ return;
+ }
+
+ // If the composition string becomes empty string, we should commit
+ // current composition.
+ if (!DispatchCompositionCommitEvent(&EmptyString())) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, "
+ "destroyed by compositioncommit event",
+ this));
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+NSAttributedString* IMEInputHandler::GetAttributedSubstringFromRange(NSRange& aRange,
+ NSRange* aActualRange) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::GetAttributedSubstringFromRange, "
+ "aRange={ location=%lu, length=%lu }, aActualRange=%p, Destroyed()=%s",
+ this, static_cast<unsigned long>(aRange.location),
+ static_cast<unsigned long>(aRange.length), aActualRange, TrueOrFalse(Destroyed())));
+
+ if (aActualRange) {
+ *aActualRange = NSMakeRange(NSNotFound, 0);
+ }
+
+ if (Destroyed() || aRange.location == NSNotFound || aRange.length == 0) {
+ return nil;
+ }
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ // If we're in composing, the queried range may be in the composition string.
+ // In such case, we should use mIMECompositionString since if the composition
+ // string is handled by a remote process, the content cache may be out of
+ // date.
+ // XXX Should we set composition string attributes? Although, Blink claims
+ // that some attributes of marked text are supported, but they return
+ // just marked string without any style. So, let's keep current behavior
+ // at least for now.
+ NSUInteger compositionLength = mIMECompositionString ? [mIMECompositionString length] : 0;
+ if (mIMECompositionStart != UINT32_MAX && aRange.location >= mIMECompositionStart &&
+ aRange.location + aRange.length <= mIMECompositionStart + compositionLength) {
+ NSRange range = NSMakeRange(aRange.location - mIMECompositionStart, aRange.length);
+ NSString* nsstr = [mIMECompositionString substringWithRange:range];
+ NSMutableAttributedString* result =
+ [[[NSMutableAttributedString alloc] initWithString:nsstr attributes:nil] autorelease];
+ // XXX We cannot return font information in this case. However, this
+ // case must occur only when IME tries to confirm if composing string
+ // is handled as expected.
+ if (aActualRange) {
+ *aActualRange = aRange;
+ }
+
+ if (MOZ_LOG_TEST(gIMELog, LogLevel::Info)) {
+ nsAutoString str;
+ nsCocoaUtils::GetStringForNSString(nsstr, str);
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::GetAttributedSubstringFromRange, "
+ "computed with mIMECompositionString (result string=\"%s\")",
+ this, NS_ConvertUTF16toUTF8(str).get()));
+ }
+ return result;
+ }
+
+ nsAutoString str;
+ WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent, mWidget);
+ WidgetQueryContentEvent::Options options;
+ int64_t startOffset = aRange.location;
+ if (IsIMEComposing()) {
+ // The composition may be at different offset from the selection start
+ // offset at dispatching compositionstart because start of composition
+ // is fixed when composition string becomes non-empty in the editor.
+ // Therefore, we need to use query event which is relative to insertion
+ // point.
+ options.mRelativeToInsertionPoint = true;
+ startOffset -= mIMECompositionStart;
+ }
+ queryTextContentEvent.InitForQueryTextContent(startOffset, aRange.length, options);
+ queryTextContentEvent.RequestFontRanges();
+ DispatchEvent(queryTextContentEvent);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::GetAttributedSubstringFromRange, "
+ "queryTextContentEvent={ mReply=%s }",
+ this, ToString(queryTextContentEvent.mReply).c_str()));
+
+ if (queryTextContentEvent.Failed()) {
+ return nil;
+ }
+
+ // We don't set vertical information at this point. If required,
+ // OS will calls drawsVerticallyForCharacterAtIndex.
+ NSMutableAttributedString* result = nsCocoaUtils::GetNSMutableAttributedString(
+ queryTextContentEvent.mReply->DataRef(), queryTextContentEvent.mReply->mFontRanges, false,
+ mWidget->BackingScaleFactor());
+ if (aActualRange) {
+ *aActualRange = MakeNSRangeFrom(queryTextContentEvent.mReply->mOffsetAndData);
+ }
+ return result;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+bool IMEInputHandler::HasMarkedText() {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::HasMarkedText, "
+ "mMarkedRange={ location=%lu, length=%lu }",
+ this, static_cast<unsigned long>(mMarkedRange.location),
+ static_cast<unsigned long>(mMarkedRange.length)));
+
+ return (mMarkedRange.location != NSNotFound) && (mMarkedRange.length != 0);
+}
+
+NSRange IMEInputHandler::MarkedRange() {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::MarkedRange, "
+ "mMarkedRange={ location=%lu, length=%lu }",
+ this, static_cast<unsigned long>(mMarkedRange.location),
+ static_cast<unsigned long>(mMarkedRange.length)));
+
+ if (!HasMarkedText()) {
+ return NSMakeRange(NSNotFound, 0);
+ }
+ return mMarkedRange;
+}
+
+NSRange IMEInputHandler::SelectedRange() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SelectedRange, Destroyed()=%s, mSelectedRange={ "
+ "location=%lu, length=%lu }",
+ this, TrueOrFalse(Destroyed()), static_cast<unsigned long>(mSelectedRange.location),
+ static_cast<unsigned long>(mSelectedRange.length)));
+
+ if (Destroyed()) {
+ return mSelectedRange;
+ }
+
+ if (mSelectedRange.location != NSNotFound) {
+ MOZ_ASSERT(mIMEHasFocus);
+ return mSelectedRange;
+ }
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, mWidget);
+ DispatchEvent(querySelectedTextEvent);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SelectedRange, querySelectedTextEvent={ mReply=%s }", this,
+ ToString(querySelectedTextEvent.mReply).c_str()));
+
+ if (querySelectedTextEvent.Failed()) {
+ return mSelectedRange;
+ }
+
+ mWritingMode = querySelectedTextEvent.mReply->WritingModeRef();
+ mRangeForWritingMode = MakeNSRangeFrom(querySelectedTextEvent.mReply->mOffsetAndData);
+
+ if (mIMEHasFocus) {
+ mSelectedRange = mRangeForWritingMode;
+ }
+
+ return mRangeForWritingMode;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(mSelectedRange);
+}
+
+bool IMEInputHandler::DrawsVerticallyForCharacterAtIndex(uint32_t aCharIndex) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (Destroyed()) {
+ return false;
+ }
+
+ if (mRangeForWritingMode.location == NSNotFound) {
+ // Update cached writing-mode value for the current selection.
+ SelectedRange();
+ }
+
+ if (aCharIndex < mRangeForWritingMode.location ||
+ aCharIndex > mRangeForWritingMode.location + mRangeForWritingMode.length) {
+ // It's not clear to me whether this ever happens in practice, but if an
+ // IME ever wants to query writing mode at an offset outside the current
+ // selection, the writing-mode value may not be correct for the index.
+ // In that case, use FirstRectForCharacterRange to get a fresh value.
+ // This does more work than strictly necessary (we don't need the rect here),
+ // but should be a rare case.
+ NS_WARNING("DrawsVerticallyForCharacterAtIndex not using cached writing mode");
+ NSRange range = NSMakeRange(aCharIndex, 1);
+ NSRange actualRange;
+ FirstRectForCharacterRange(range, &actualRange);
+ }
+
+ return mWritingMode.IsVertical();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+NSRect IMEInputHandler::FirstRectForCharacterRange(NSRange& aRange, NSRange* aActualRange) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::FirstRectForCharacterRange, Destroyed()=%s, "
+ "aRange={ location=%lu, length=%lu }, aActualRange=%p }",
+ this, TrueOrFalse(Destroyed()), static_cast<unsigned long>(aRange.location),
+ static_cast<unsigned long>(aRange.length), aActualRange));
+
+ // XXX this returns first character rect or caret rect, it is limitation of
+ // now. We need more work for returns first line rect. But current
+ // implementation is enough for IMEs.
+
+ NSRect rect = NSMakeRect(0.0, 0.0, 0.0, 0.0);
+ NSRange actualRange = NSMakeRange(NSNotFound, 0);
+ if (aActualRange) {
+ *aActualRange = actualRange;
+ }
+ if (Destroyed() || aRange.location == NSNotFound) {
+ return rect;
+ }
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ LayoutDeviceIntRect r;
+ bool useCaretRect = (aRange.length == 0);
+ if (!useCaretRect) {
+ WidgetQueryContentEvent queryTextRectEvent(true, eQueryTextRect, mWidget);
+ WidgetQueryContentEvent::Options options;
+ int64_t startOffset = aRange.location;
+ if (IsIMEComposing()) {
+ // The composition may be at different offset from the selection start
+ // offset at dispatching compositionstart because start of composition
+ // is fixed when composition string becomes non-empty in the editor.
+ // Therefore, we need to use query event which is relative to insertion
+ // point.
+ options.mRelativeToInsertionPoint = true;
+ startOffset -= mIMECompositionStart;
+ }
+ queryTextRectEvent.InitForQueryTextRect(startOffset, 1, options);
+ DispatchEvent(queryTextRectEvent);
+ if (queryTextRectEvent.Succeeded()) {
+ r = queryTextRectEvent.mReply->mRect;
+ actualRange = MakeNSRangeFrom(queryTextRectEvent.mReply->mOffsetAndData);
+ mWritingMode = queryTextRectEvent.mReply->WritingModeRef();
+ mRangeForWritingMode = actualRange;
+ } else {
+ useCaretRect = true;
+ }
+ }
+
+ if (useCaretRect) {
+ WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, mWidget);
+ WidgetQueryContentEvent::Options options;
+ int64_t startOffset = aRange.location;
+ if (IsIMEComposing()) {
+ // The composition may be at different offset from the selection start
+ // offset at dispatching compositionstart because start of composition
+ // is fixed when composition string becomes non-empty in the editor.
+ // Therefore, we need to use query event which is relative to insertion
+ // point.
+ options.mRelativeToInsertionPoint = true;
+ startOffset -= mIMECompositionStart;
+ }
+ queryCaretRectEvent.InitForQueryCaretRect(startOffset, options);
+ DispatchEvent(queryCaretRectEvent);
+ if (queryCaretRectEvent.Failed()) {
+ return rect;
+ }
+ r = queryCaretRectEvent.mReply->mRect;
+ r.width = 0;
+ actualRange.location = queryCaretRectEvent.mReply->StartOffset();
+ actualRange.length = 0;
+ }
+
+ nsIWidget* rootWidget = mWidget->GetTopLevelWidget();
+ NSWindow* rootWindow = static_cast<NSWindow*>(rootWidget->GetNativeData(NS_NATIVE_WINDOW));
+ NSView* rootView = static_cast<NSView*>(rootWidget->GetNativeData(NS_NATIVE_WIDGET));
+ if (!rootWindow || !rootView) {
+ return rect;
+ }
+ rect = nsCocoaUtils::DevPixelsToCocoaPoints(r, mWidget->BackingScaleFactor());
+ rect = [rootView convertRect:rect toView:nil];
+ rect.origin = nsCocoaUtils::ConvertPointToScreen(rootWindow, rect.origin);
+
+ if (aActualRange) {
+ *aActualRange = actualRange;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::FirstRectForCharacterRange, "
+ "useCaretRect=%s rect={ x=%f, y=%f, width=%f, height=%f }, "
+ "actualRange={ location=%lu, length=%lu }",
+ this, TrueOrFalse(useCaretRect), rect.origin.x, rect.origin.y, rect.size.width,
+ rect.size.height, static_cast<unsigned long>(actualRange.location),
+ static_cast<unsigned long>(actualRange.length)));
+
+ return rect;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSMakeRect(0.0, 0.0, 0.0, 0.0));
+}
+
+NSUInteger IMEInputHandler::CharacterIndexForPoint(NSPoint& aPoint) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::CharacterIndexForPoint, aPoint={ x=%f, y=%f }", this, aPoint.x,
+ aPoint.y));
+
+ NSWindow* mainWindow = [NSApp mainWindow];
+ if (!mWidget || !mainWindow) {
+ return NSNotFound;
+ }
+
+ WidgetQueryContentEvent queryCharAtPointEvent(true, eQueryCharacterAtPoint, mWidget);
+ NSPoint ptInWindow = nsCocoaUtils::ConvertPointFromScreen(mainWindow, aPoint);
+ NSPoint ptInView = [mView convertPoint:ptInWindow fromView:nil];
+ queryCharAtPointEvent.mRefPoint.x =
+ static_cast<int32_t>(ptInView.x) * mWidget->BackingScaleFactor();
+ queryCharAtPointEvent.mRefPoint.y =
+ static_cast<int32_t>(ptInView.y) * mWidget->BackingScaleFactor();
+ mWidget->DispatchWindowEvent(queryCharAtPointEvent);
+ if (queryCharAtPointEvent.Failed() || queryCharAtPointEvent.DidNotFindChar() ||
+ queryCharAtPointEvent.mReply->StartOffset() >= static_cast<uint32_t>(NSNotFound)) {
+ return NSNotFound;
+ }
+
+ return queryCharAtPointEvent.mReply->StartOffset();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSNotFound);
+}
+
+extern "C" {
+extern NSString* NSTextInputReplacementRangeAttributeName;
+}
+
+NSArray* IMEInputHandler::GetValidAttributesForMarkedText() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info, ("%p IMEInputHandler::GetValidAttributesForMarkedText", this));
+
+ // Return same attributes as Chromium (see render_widget_host_view_mac.mm)
+ // because most IMEs must be tested with Safari (OS default) and Chrome
+ // (having most market share). Therefore, we need to follow their behavior.
+ // XXX It might be better to reuse an array instance for this result because
+ // this may be called a lot. Note that Chromium does so.
+ return [NSArray arrayWithObjects:NSUnderlineStyleAttributeName, NSUnderlineColorAttributeName,
+ NSMarkedClauseSegmentAttributeName,
+ NSTextInputReplacementRangeAttributeName, nil];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * IMEInputHandler implementation #2
+ *
+ ******************************************************************************/
+
+IMEInputHandler::IMEInputHandler(nsChildView* aWidget, NSView<mozView>* aNativeView)
+ : TextInputHandlerBase(aWidget, aNativeView),
+ mPendingMethods(0),
+ mIMECompositionString(nullptr),
+ mIMECompositionStart(UINT32_MAX),
+ mRangeForWritingMode(),
+ mIsIMEComposing(false),
+ mIsDeadKeyComposing(false),
+ mIsIMEEnabled(true),
+ mIsASCIICapableOnly(false),
+ mIgnoreIMECommit(false),
+ mIMEHasFocus(false) {
+ InitStaticMembers();
+
+ mMarkedRange.location = NSNotFound;
+ mMarkedRange.length = 0;
+ mSelectedRange.location = NSNotFound;
+ mSelectedRange.length = 0;
+}
+
+IMEInputHandler::~IMEInputHandler() {
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+ if (sFocusedIMEHandler == this) {
+ sFocusedIMEHandler = nullptr;
+ }
+ if (mIMECompositionString) {
+ [mIMECompositionString release];
+ mIMECompositionString = nullptr;
+ }
+}
+
+void IMEInputHandler::OnFocusChangeInGecko(bool aFocus) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::OnFocusChangeInGecko, aFocus=%s, Destroyed()=%s, "
+ "sFocusedIMEHandler=%p",
+ this, TrueOrFalse(aFocus), TrueOrFalse(Destroyed()), sFocusedIMEHandler));
+
+ mSelectedRange.location = NSNotFound; // Marking dirty
+ mIMEHasFocus = aFocus;
+
+ // This is called when the native focus is changed and when the native focus
+ // isn't changed but the focus is changed in Gecko.
+ if (!aFocus) {
+ if (sFocusedIMEHandler == this) sFocusedIMEHandler = nullptr;
+ return;
+ }
+
+ sFocusedIMEHandler = this;
+
+ // We need to notify IME of focus change in Gecko as native focus change
+ // because the window level of the focused element in Gecko may be changed.
+ mPendingMethods |= kNotifyIMEOfFocusChangeInGecko;
+ ResetTimer();
+}
+
+bool IMEInputHandler::OnDestroyWidget(nsChildView* aDestroyingWidget) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::OnDestroyWidget, aDestroyingWidget=%p, "
+ "sFocusedIMEHandler=%p, IsIMEComposing()=%s",
+ this, aDestroyingWidget, sFocusedIMEHandler, TrueOrFalse(IsIMEComposing())));
+
+ // If we're not focused, the focused IMEInputHandler may have been
+ // created by another widget/nsChildView.
+ if (sFocusedIMEHandler && sFocusedIMEHandler != this) {
+ sFocusedIMEHandler->OnDestroyWidget(aDestroyingWidget);
+ }
+
+ if (!TextInputHandlerBase::OnDestroyWidget(aDestroyingWidget)) {
+ return false;
+ }
+
+ if (IsIMEComposing()) {
+ // If our view is in the composition, we should clean up it.
+ CancelIMEComposition();
+ }
+
+ mSelectedRange.location = NSNotFound; // Marking dirty
+ mIMEHasFocus = false;
+
+ return true;
+}
+
+void IMEInputHandler::SendCommittedText(NSString* aString) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SendCommittedText, mView=%p, mWidget=%p, "
+ "inputContext=%p, mIsIMEComposing=%s",
+ this, mView, mWidget, mView ? [mView inputContext] : nullptr, TrueOrFalse(mIsIMEComposing)));
+
+ NS_ENSURE_TRUE(mWidget, );
+ // XXX We should send the string without mView.
+ if (!mView) {
+ return;
+ }
+
+ NSAttributedString* attrStr = [[NSAttributedString alloc] initWithString:aString];
+ if ([mView conformsToProtocol:@protocol(NSTextInputClient)]) {
+ NSObject<NSTextInputClient>* textInputClient = static_cast<NSObject<NSTextInputClient>*>(mView);
+ [textInputClient insertText:attrStr replacementRange:NSMakeRange(NSNotFound, 0)];
+ }
+
+ // Last resort. If we cannot retrieve NSTextInputProtocol from mView
+ // or blocking to call our InsertText(), we should call InsertText()
+ // directly to commit composition forcibly.
+ if (mIsIMEComposing) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SendCommittedText, trying to insert text directly "
+ "due to IME not calling our InsertText()",
+ this));
+ static_cast<TextInputHandler*>(this)->InsertText(attrStr);
+ MOZ_ASSERT(!mIsIMEComposing);
+ }
+
+ [attrStr release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::KillIMEComposition() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::KillIMEComposition, mView=%p, mWidget=%p, "
+ "inputContext=%p, mIsIMEComposing=%s, "
+ "Destroyed()=%s, IsFocused()=%s",
+ this, mView, mWidget, mView ? [mView inputContext] : nullptr,
+ TrueOrFalse(mIsIMEComposing), TrueOrFalse(Destroyed()), TrueOrFalse(IsFocused())));
+
+ if (Destroyed() || NS_WARN_IF(!mView)) {
+ return;
+ }
+
+ NSTextInputContext* inputContext = [mView inputContext];
+ if (NS_WARN_IF(!inputContext)) {
+ return;
+ }
+ [inputContext discardMarkedText];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::CommitIMEComposition() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::CommitIMEComposition, mIMECompositionString=%s", this,
+ GetCharacters(mIMECompositionString)));
+
+ // If this is called before dispatching eCompositionStart, IsIMEComposing()
+ // returns false. Even in such case, we need to commit composition *in*
+ // IME if this is called by preceding eKeyDown event of eCompositionStart.
+ // So, we need to call KillIMEComposition() even when IsIMEComposing()
+ // returns false.
+ KillIMEComposition();
+
+ if (!IsIMEComposing()) return;
+
+ // If the composition is still there, KillIMEComposition only kills the
+ // composition in TSM. We also need to finish the our composition too.
+ SendCommittedText(mIMECompositionString);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::CancelIMEComposition() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!IsIMEComposing()) return;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::CancelIMEComposition, mIMECompositionString=%s", this,
+ GetCharacters(mIMECompositionString)));
+
+ // For canceling the current composing, we need to ignore the param of
+ // insertText. But this code is ugly...
+ mIgnoreIMECommit = true;
+ KillIMEComposition();
+ mIgnoreIMECommit = false;
+
+ if (!IsIMEComposing()) return;
+
+ // If the composition is still there, KillIMEComposition only kills the
+ // composition in TSM. We also need to kill the our composition too.
+ SendCommittedText(@"");
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+bool IMEInputHandler::IsFocused() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NS_ENSURE_TRUE(!Destroyed(), false);
+ NSWindow* window = [mView window];
+ NS_ENSURE_TRUE(window, false);
+ return [window firstResponder] == mView && [window isKeyWindow] &&
+ [[NSApplication sharedApplication] isActive];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+bool IMEInputHandler::IsIMEOpened() {
+ TISInputSourceWrapper tis;
+ tis.InitByCurrentInputSource();
+ return tis.IsOpenedIMEMode();
+}
+
+void IMEInputHandler::SetASCIICapableOnly(bool aASCIICapableOnly) {
+ if (aASCIICapableOnly == mIsASCIICapableOnly) return;
+
+ CommitIMEComposition();
+ mIsASCIICapableOnly = aASCIICapableOnly;
+ SyncASCIICapableOnly();
+}
+
+void IMEInputHandler::EnableIME(bool aEnableIME) {
+ if (aEnableIME == mIsIMEEnabled) return;
+
+ CommitIMEComposition();
+ mIsIMEEnabled = aEnableIME;
+}
+
+void IMEInputHandler::SetIMEOpenState(bool aOpenIME) {
+ if (!IsFocused() || IsIMEOpened() == aOpenIME) return;
+
+ if (!aOpenIME) {
+ TISInputSourceWrapper tis;
+ tis.InitByCurrentASCIICapableInputSource();
+ tis.Select();
+ return;
+ }
+
+ // If we know the latest IME opened mode, we should select it.
+ if (sLatestIMEOpenedModeInputSourceID) {
+ TISInputSourceWrapper tis;
+ tis.InitByInputSourceID(sLatestIMEOpenedModeInputSourceID);
+ tis.Select();
+ return;
+ }
+
+ // XXX If the current input source is a mode of IME, we should turn on it,
+ // but we haven't found such way...
+
+ // Finally, we should refer the system locale but this is a little expensive,
+ // we shouldn't retry this (if it was succeeded, we already set
+ // sLatestIMEOpenedModeInputSourceID at that time).
+ static bool sIsPrefferredIMESearched = false;
+ if (sIsPrefferredIMESearched) return;
+ sIsPrefferredIMESearched = true;
+ OpenSystemPreferredLanguageIME();
+}
+
+void IMEInputHandler::OpenSystemPreferredLanguageIME() {
+ MOZ_LOG(gIMELog, LogLevel::Info, ("%p IMEInputHandler::OpenSystemPreferredLanguageIME", this));
+
+ CFArrayRef langList = ::CFLocaleCopyPreferredLanguages();
+ if (!langList) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::OpenSystemPreferredLanguageIME, langList is NULL", this));
+ return;
+ }
+ CFIndex count = ::CFArrayGetCount(langList);
+ for (CFIndex i = 0; i < count; i++) {
+ CFLocaleRef locale = ::CFLocaleCreate(
+ kCFAllocatorDefault, static_cast<CFStringRef>(::CFArrayGetValueAtIndex(langList, i)));
+ if (!locale) {
+ continue;
+ }
+
+ bool changed = false;
+ CFStringRef lang = static_cast<CFStringRef>(::CFLocaleGetValue(locale, kCFLocaleLanguageCode));
+ NS_ASSERTION(lang, "lang is null");
+ if (lang) {
+ TISInputSourceWrapper tis;
+ tis.InitByLanguage(lang);
+ if (tis.IsOpenedIMEMode()) {
+ if (MOZ_LOG_TEST(gIMELog, LogLevel::Info)) {
+ CFStringRef foundTIS;
+ tis.GetInputSourceID(foundTIS);
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::OpenSystemPreferredLanguageIME, "
+ "foundTIS=%s, lang=%s",
+ this, GetCharacters(foundTIS), GetCharacters(lang)));
+ }
+ tis.Select();
+ changed = true;
+ }
+ }
+ ::CFRelease(locale);
+ if (changed) {
+ break;
+ }
+ }
+ ::CFRelease(langList);
+}
+
+void IMEInputHandler::OnSelectionChange(const IMENotification& aIMENotification) {
+ MOZ_ASSERT(aIMENotification.mSelectionChangeData.IsInitialized());
+ MOZ_LOG(gIMELog, LogLevel::Info, ("%p IMEInputHandler::OnSelectionChange", this));
+
+ if (!aIMENotification.mSelectionChangeData.HasRange()) {
+ mSelectedRange.location = NSNotFound;
+ mSelectedRange.length = 0;
+ mRangeForWritingMode.location = NSNotFound;
+ mRangeForWritingMode.length = 0;
+ return;
+ }
+
+ mWritingMode = aIMENotification.mSelectionChangeData.GetWritingMode();
+ mRangeForWritingMode = NSMakeRange(aIMENotification.mSelectionChangeData.mOffset,
+ aIMENotification.mSelectionChangeData.Length());
+ if (mIMEHasFocus) {
+ mSelectedRange = mRangeForWritingMode;
+ }
+}
+
+void IMEInputHandler::OnLayoutChange() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!IsFocused()) {
+ return;
+ }
+ NSTextInputContext* inputContext = [mView inputContext];
+ [inputContext invalidateCharacterCoordinates];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+bool IMEInputHandler::OnHandleEvent(NSEvent* aEvent) {
+ if (!IsFocused()) {
+ return false;
+ }
+
+ bool allowConsumeEvent = true;
+ if (nsCocoaFeatures::OnCatalinaOrLater() && !IsIMEComposing()) {
+ // Hack for bug of Korean IMEs on Catalina (10.15).
+ // If we are inactivated during composition, active Korean IME keeps
+ // consuming all mousedown events of any mouse buttons. So, we should
+ // allow Korean IMEs to handle mousedown events only when there is
+ // composition string.
+ // List of ID of Korean IME:
+ // * com.apple.inputmethod.Korean.2SetKorean
+ // * com.apple.inputmethod.Korean.3SetKorean
+ // * com.apple.inputmethod.Korean.390Sebulshik
+ // * com.apple.inputmethod.Korean.GongjinCheongRomaja
+ // * com.apple.inputmethod.Korean.HNCRomaja
+ TISInputSourceWrapper tis;
+ tis.InitByCurrentInputSource();
+ nsAutoString inputSourceID;
+ tis.GetInputSourceID(inputSourceID);
+ allowConsumeEvent = !StringBeginsWith(inputSourceID, u"com.apple.inputmethod.Korean."_ns);
+ }
+ NSTextInputContext* inputContext = [mView inputContext];
+ return [inputContext handleEvent:aEvent] && allowConsumeEvent;
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TextInputHandlerBase implementation
+ *
+ ******************************************************************************/
+
+int32_t TextInputHandlerBase::sSecureEventInputCount = 0;
+
+NS_IMPL_ISUPPORTS(TextInputHandlerBase, TextEventDispatcherListener, nsISupportsWeakReference)
+
+TextInputHandlerBase::TextInputHandlerBase(nsChildView* aWidget, NSView<mozView>* aNativeView)
+ : mWidget(aWidget), mDispatcher(aWidget->GetTextEventDispatcher()) {
+ gHandlerInstanceCount++;
+ mView = [aNativeView retain];
+}
+
+TextInputHandlerBase::~TextInputHandlerBase() {
+ [mView release];
+ if (--gHandlerInstanceCount == 0) {
+ TISInputSourceWrapper::Shutdown();
+ }
+}
+
+bool TextInputHandlerBase::OnDestroyWidget(nsChildView* aDestroyingWidget) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info, ("%p TextInputHandlerBase::OnDestroyWidget, "
+ "aDestroyingWidget=%p, mWidget=%p",
+ this, aDestroyingWidget, mWidget));
+
+ if (aDestroyingWidget != mWidget) {
+ return false;
+ }
+
+ mWidget = nullptr;
+ mDispatcher = nullptr;
+ return true;
+}
+
+bool TextInputHandlerBase::DispatchEvent(WidgetGUIEvent& aEvent) {
+ return mWidget->DispatchWindowEvent(aEvent);
+}
+
+void TextInputHandlerBase::InitKeyEvent(NSEvent* aNativeKeyEvent, WidgetKeyboardEvent& aKeyEvent,
+ bool aIsProcessedByIME, const nsAString* aInsertString) {
+ NS_ASSERTION(aNativeKeyEvent, "aNativeKeyEvent must not be NULL");
+
+ if (mKeyboardOverride.mOverrideEnabled) {
+ TISInputSourceWrapper tis;
+ tis.InitByLayoutID(mKeyboardOverride.mKeyboardLayout, true);
+ tis.InitKeyEvent(aNativeKeyEvent, aKeyEvent, aIsProcessedByIME, aInsertString);
+ return;
+ }
+ TISInputSourceWrapper::CurrentInputSource().InitKeyEvent(aNativeKeyEvent, aKeyEvent,
+ aIsProcessedByIME, aInsertString);
+}
+
+nsresult TextInputHandlerBase::SynthesizeNativeKeyEvent(int32_t aNativeKeyboardLayout,
+ int32_t aNativeKeyCode,
+ uint32_t aModifierFlags,
+ const nsAString& aCharacters,
+ const nsAString& aUnmodifiedCharacters) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ uint32_t modifierFlags = nsCocoaUtils::ConvertWidgetModifiersToMacModifierFlags(
+ static_cast<nsIWidget::Modifiers>(aModifierFlags));
+ NSInteger windowNumber = [[mView window] windowNumber];
+ bool sendFlagsChangedEvent = IsModifierKey(aNativeKeyCode);
+ NSEventType eventType = sendFlagsChangedEvent ? NSEventTypeFlagsChanged : NSEventTypeKeyDown;
+ NSEvent* downEvent = [NSEvent keyEventWithType:eventType
+ location:NSMakePoint(0, 0)
+ modifierFlags:modifierFlags
+ timestamp:0
+ windowNumber:windowNumber
+ context:nil
+ characters:nsCocoaUtils::ToNSString(aCharacters)
+ charactersIgnoringModifiers:nsCocoaUtils::ToNSString(aUnmodifiedCharacters)
+ isARepeat:NO
+ keyCode:aNativeKeyCode];
+
+ NSEvent* upEvent = sendFlagsChangedEvent
+ ? nil
+ : nsCocoaUtils::MakeNewCocoaEventWithType(NSEventTypeKeyUp, downEvent);
+
+ if (downEvent && (sendFlagsChangedEvent || upEvent)) {
+ KeyboardLayoutOverride currentLayout = mKeyboardOverride;
+ mKeyboardOverride.mKeyboardLayout = aNativeKeyboardLayout;
+ mKeyboardOverride.mOverrideEnabled = true;
+ [NSApp sendEvent:downEvent];
+ if (upEvent) {
+ [NSApp sendEvent:upEvent];
+ }
+ // processKeyDownEvent and keyUp block exceptions so we're sure to
+ // reach here to restore mKeyboardOverride
+ mKeyboardOverride = currentLayout;
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NSInteger TextInputHandlerBase::GetWindowLevel() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info, ("%p TextInputHandlerBase::GetWindowLevel, Destryoed()=%s",
+ this, TrueOrFalse(Destroyed())));
+
+ if (Destroyed()) {
+ return NSNormalWindowLevel;
+ }
+
+ // When an <input> element on a XUL <panel> is focused, the actual focused view
+ // is the panel's parent view (mView). But the editor is displayed on the
+ // popped-up widget's view (editorView). We want the latter's window level.
+ NSView<mozView>* editorView = mWidget->GetEditorView();
+ NS_ENSURE_TRUE(editorView, NSNormalWindowLevel);
+ NSInteger windowLevel = [[editorView window] level];
+
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandlerBase::GetWindowLevel, windowLevel=%s (%lX)", this,
+ GetWindowLevelName(windowLevel), static_cast<unsigned long>(windowLevel)));
+
+ return windowLevel;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSNormalWindowLevel);
+}
+
+NS_IMETHODIMP
+TextInputHandlerBase::AttachNativeKeyEvent(WidgetKeyboardEvent& aKeyEvent) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Don't try to replace a native event if one already exists.
+ // OS X doesn't have an OS modifier, can't make a native event.
+ if (aKeyEvent.mNativeKeyEvent || aKeyEvent.mModifiers & MODIFIER_OS) {
+ return NS_OK;
+ }
+
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandlerBase::AttachNativeKeyEvent, key=0x%X, char=0x%X, "
+ "mod=0x%X",
+ this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode, aKeyEvent.mModifiers));
+
+ NSInteger windowNumber = [[mView window] windowNumber];
+ NSGraphicsContext* context = [NSGraphicsContext currentContext];
+ aKeyEvent.mNativeKeyEvent =
+ nsCocoaUtils::MakeNewCococaEventFromWidgetEvent(aKeyEvent, windowNumber, context);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+bool TextInputHandlerBase::SetSelection(NSRange& aRange) {
+ MOZ_ASSERT(!Destroyed());
+
+ RefPtr<TextInputHandlerBase> kungFuDeathGrip(this);
+ WidgetSelectionEvent selectionEvent(true, eSetSelection, 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(char16_t aChar) {
+ return (aChar >= 0x20 && aChar <= 0x7E) || aChar >= 0xA0;
+}
+
+/* static */ bool TextInputHandlerBase::IsSpecialGeckoKey(UInt32 aNativeKeyCode) {
+ // this table is used to determine which keys are special and should not
+ // generate a charCode
+ switch (aNativeKeyCode) {
+ // modifiers - we don't get separate events for these yet
+ case kVK_Escape:
+ case kVK_Shift:
+ case kVK_RightShift:
+ case kVK_Command:
+ case kVK_RightCommand:
+ case kVK_CapsLock:
+ case kVK_Control:
+ case kVK_RightControl:
+ case kVK_Option:
+ case kVK_RightOption:
+ case kVK_ANSI_KeypadClear:
+ case kVK_Function:
+
+ // function keys
+ case kVK_F1:
+ case kVK_F2:
+ case kVK_F3:
+ case kVK_F4:
+ case kVK_F5:
+ case kVK_F6:
+ case kVK_F7:
+ case kVK_F8:
+ case kVK_F9:
+ case kVK_F10:
+ case kVK_F11:
+ case kVK_F12:
+ case kVK_PC_Pause:
+ case kVK_PC_ScrollLock:
+ case kVK_PC_PrintScreen:
+ case kVK_F16:
+ case kVK_F17:
+ case kVK_F18:
+ case kVK_F19:
+
+ case kVK_PC_Insert:
+ case kVK_PC_Delete:
+ case kVK_Tab:
+ case kVK_PC_Backspace:
+ case kVK_PC_ContextMenu:
+
+ case kVK_JIS_Eisu:
+ case kVK_JIS_Kana:
+
+ case kVK_Home:
+ case kVK_End:
+ case kVK_PageUp:
+ case kVK_PageDown:
+ case kVK_LeftArrow:
+ case kVK_RightArrow:
+ case kVK_UpArrow:
+ case kVK_DownArrow:
+ case kVK_Return:
+ case kVK_ANSI_KeypadEnter:
+ case kVK_Powerbook_KeypadEnter:
+ return true;
+ }
+ return false;
+}
+
+/* static */ bool TextInputHandlerBase::IsNormalCharInputtingEvent(NSEvent* aNativeEvent) {
+ if ([aNativeEvent type] != NSEventTypeKeyDown && [aNativeEvent type] != NSEventTypeKeyUp) {
+ return false;
+ }
+ nsAutoString nativeChars;
+ nsCocoaUtils::GetStringForNSString([aNativeEvent characters], nativeChars);
+
+ // this is not character inputting event, simply.
+ if (nativeChars.IsEmpty() || ([aNativeEvent modifierFlags] & NSEventModifierFlagCommand)) {
+ return false;
+ }
+ return !IsControlChar(nativeChars[0]);
+}
+
+/* static */ bool TextInputHandlerBase::IsModifierKey(UInt32 aNativeKeyCode) {
+ switch (aNativeKeyCode) {
+ case kVK_CapsLock:
+ case kVK_RightCommand:
+ case kVK_Command:
+ case kVK_Shift:
+ case kVK_Option:
+ case kVK_Control:
+ case kVK_RightShift:
+ case kVK_RightOption:
+ case kVK_RightControl:
+ case kVK_Function:
+ return true;
+ }
+ return false;
+}
+
+/* static */ void TextInputHandlerBase::EnableSecureEventInput() {
+ sSecureEventInputCount++;
+ ::EnableSecureEventInput();
+}
+
+/* static */ void TextInputHandlerBase::DisableSecureEventInput() {
+ if (!sSecureEventInputCount) {
+ return;
+ }
+ sSecureEventInputCount--;
+ ::DisableSecureEventInput();
+}
+
+/* static */ bool TextInputHandlerBase::IsSecureEventInputEnabled() {
+ // sSecureEventInputCount is our mechanism to track when Secure Event Input
+ // is enabled. Non-zero indicates we have enabled Secure Input. But
+ // zero does not mean that Secure Input is _disabled_ because another
+ // application may have enabled it. If the OS reports Secure Event
+ // Input is disabled though, a non-zero sSecureEventInputCount is an error.
+ NS_ASSERTION(
+ ::IsSecureEventInputEnabled() || 0 == sSecureEventInputCount,
+ "sSecureEventInputCount is not zero when the OS thinks SecureEventInput is disabled.");
+ return !!sSecureEventInputCount;
+}
+
+/* static */ void TextInputHandlerBase::EnsureSecureEventInputDisabled() {
+ while (sSecureEventInputCount) {
+ TextInputHandlerBase::DisableSecureEventInput();
+ }
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TextInputHandlerBase::KeyEventState implementation
+ *
+ ******************************************************************************/
+
+void TextInputHandlerBase::KeyEventState::InitKeyEvent(TextInputHandlerBase* aHandler,
+ WidgetKeyboardEvent& aKeyEvent,
+ bool aIsProcessedByIME) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_ASSERT(aHandler);
+ MOZ_RELEASE_ASSERT(mKeyEvent);
+
+ NSEvent* nativeEvent = mKeyEvent;
+ if (!mInsertedString.IsEmpty()) {
+ nsAutoString unhandledString;
+ GetUnhandledString(unhandledString);
+ NSString* unhandledNSString = nsCocoaUtils::ToNSString(unhandledString);
+ // If the key event's some characters were already handled by
+ // InsertString() calls, we need to create a dummy event which doesn't
+ // include the handled characters.
+ nativeEvent = [NSEvent keyEventWithType:[mKeyEvent type]
+ location:[mKeyEvent locationInWindow]
+ modifierFlags:[mKeyEvent modifierFlags]
+ timestamp:[mKeyEvent timestamp]
+ windowNumber:[mKeyEvent windowNumber]
+ context:nil
+ characters:unhandledNSString
+ charactersIgnoringModifiers:[mKeyEvent charactersIgnoringModifiers]
+ isARepeat:[mKeyEvent isARepeat]
+ keyCode:[mKeyEvent keyCode]];
+ }
+
+ aKeyEvent.mUniqueId = mUniqueId;
+ aHandler->InitKeyEvent(nativeEvent, aKeyEvent, aIsProcessedByIME, mInsertString);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void TextInputHandlerBase::KeyEventState::GetUnhandledString(nsAString& aUnhandledString) const {
+ aUnhandledString.Truncate();
+ if (NS_WARN_IF(!mKeyEvent)) {
+ return;
+ }
+ nsAutoString characters;
+ nsCocoaUtils::GetStringForNSString([mKeyEvent characters], characters);
+ if (characters.IsEmpty()) {
+ return;
+ }
+ if (mInsertedString.IsEmpty()) {
+ aUnhandledString = characters;
+ return;
+ }
+
+ // The insertes string must match with the start of characters.
+ MOZ_ASSERT(StringBeginsWith(characters, mInsertedString));
+
+ aUnhandledString = nsDependentSubstring(characters, mInsertedString.Length());
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TextInputHandlerBase::AutoInsertStringClearer implementation
+ *
+ ******************************************************************************/
+
+TextInputHandlerBase::AutoInsertStringClearer::~AutoInsertStringClearer() {
+ if (mState && mState->mInsertString) {
+ // If inserting string is a part of characters of the event,
+ // we should record it as inserted string.
+ nsAutoString characters;
+ nsCocoaUtils::GetStringForNSString([mState->mKeyEvent characters], characters);
+ nsAutoString insertedString(mState->mInsertedString);
+ insertedString += *mState->mInsertString;
+ if (StringBeginsWith(characters, insertedString)) {
+ mState->mInsertedString = insertedString;
+ }
+ }
+ if (mState) {
+ mState->mInsertString = nullptr;
+ }
+}
+
+#undef MOZ_LOG_KEY_OR_IME