/* -*- 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(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>& 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(aKeyCode), static_cast(aModifiers), static_cast(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(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(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(aKeyCode), static_cast(aModifiers), static_cast(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(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(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( const_cast(::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( ::TISGetInputSourceProperty(mKeyboardLayout, kTISPropertyUnicodeKeyLayoutData)); // We should be always able to get the layout here. NS_ENSURE_TRUE(uchr, nullptr); mUCKeyboardLayout = reinterpret_cast(CFDataGetBytePtr(uchr)); return mUCKeyboardLayout; } bool TISInputSourceWrapper::GetBoolProperty(const CFStringRef aKey) { CFBooleanRef ret = static_cast(::TISGetInputSourceProperty(mInputSource, aKey)); return ::CFBooleanGetValue(ret); } bool TISInputSourceWrapper::GetStringProperty(const CFStringRef aKey, CFStringRef& aStr) { aStr = static_cast(::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( ::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(::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(aResult[0] + ('A' - 1)) : static_cast(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(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(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(kbType), static_cast(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(aNativeKeyCode), static_cast(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(const_cast(::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* 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([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 widget(mWidget); KeyEventState* currentKeyEvent = PushKeyEvent(aNativeEvent, aUniqueId); AutoKeyEventStateCleaner remover(this); RefPtr 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([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, ¤tKeyEvent); 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 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([aNativeEvent modifierFlags]), static_cast(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 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, ¤tKeyEvent); 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(aReplacementRange ? aReplacementRange->location : 0), static_cast(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 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 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 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(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 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 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
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 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 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(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(const_cast(::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(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 element on a XUL element gets focus from an // element on the opener window of the 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 '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(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 IMEInputHandler::CreateTextRangeArray( NSAttributedString* aAttrString, NSRange& aSelectedRange) { NS_OBJC_BEGIN_TRY_BLOCK_RETURN; RefPtr 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(SelectedRange().location), static_cast(mSelectedRange.length), TrueOrFalse(Destroyed()), mView, mWidget, mView ? [mView inputContext] : nullptr, TrueOrFalse(mIsIMEComposing))); RefPtr 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(aSelectedRange.location), static_cast(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 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 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 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 kungFuDeathGrip(this); RefPtr 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 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(aReplacementRange ? aReplacementRange->location : 0), static_cast(aReplacementRange ? aReplacementRange->length : 0), TrueOrFalse(Destroyed()), TrueOrFalse(IsIMEComposing()), static_cast(mMarkedRange.location), static_cast(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 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(aSelectedRange.location), static_cast(aSelectedRange.length), aReplacementRange, static_cast(aReplacementRange ? aReplacementRange->location : 0), static_cast(aReplacementRange ? aReplacementRange->length : 0), TrueOrFalse(Destroyed()), TrueOrFalse(IsIMEComposing()), static_cast(mMarkedRange.location), static_cast(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 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 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(aRange.location), static_cast(aRange.length), aActualRange, TrueOrFalse(Destroyed()))); if (aActualRange) { *aActualRange = NSMakeRange(NSNotFound, 0); } if (Destroyed() || aRange.location == NSNotFound || aRange.length == 0) { return nil; } RefPtr 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(mMarkedRange.location), static_cast(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(mMarkedRange.location), static_cast(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(mSelectedRange.location), static_cast(mSelectedRange.length))); if (Destroyed()) { return mSelectedRange; } if (mSelectedRange.location != NSNotFound) { MOZ_ASSERT(mIMEHasFocus); return mSelectedRange; } RefPtr 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(aRange.location), static_cast(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 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(rootWidget->GetNativeData(NS_NATIVE_WINDOW)); NSView* rootView = static_cast(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(actualRange.location), static_cast(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(ptInView.x) * mWidget->BackingScaleFactor(); queryCharAtPointEvent.mRefPoint.y = static_cast(ptInView.y) * mWidget->BackingScaleFactor(); mWidget->DispatchWindowEvent(queryCharAtPointEvent); if (queryCharAtPointEvent.Failed() || queryCharAtPointEvent.DidNotFindChar() || queryCharAtPointEvent.mReply->StartOffset() >= static_cast(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* 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* textInputClient = static_cast*>(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(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(::CFArrayGetValueAtIndex(langList, i))); if (!locale) { continue; } bool changed = false; CFStringRef lang = static_cast(::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* 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(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 element on a XUL 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* 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(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 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