diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /widget/cocoa/TextInputHandler.mm | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget/cocoa/TextInputHandler.mm')
-rw-r--r-- | widget/cocoa/TextInputHandler.mm | 5061 |
1 files changed, 5061 insertions, 0 deletions
diff --git a/widget/cocoa/TextInputHandler.mm b/widget/cocoa/TextInputHandler.mm new file mode 100644 index 0000000000..c833a4cb9e --- /dev/null +++ b/widget/cocoa/TextInputHandler.mm @@ -0,0 +1,5061 @@ +/* -*- 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/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; + +LazyLogModule gLog("TextInputHandlerWidgets"); + +static const char* OnOrOff(bool aBool) { return aBool ? "ON" : "off"; } + +static const char* TrueOrFalse(bool aBool) { return aBool ? "TRUE" : "FALSE"; } + +static const char* GetKeyNameForNativeKeyCode(unsigned short aNativeKeyCode) { + switch (aNativeKeyCode) { + case kVK_Escape: + return "Escape"; + case kVK_RightCommand: + return "Right-Command"; + case kVK_Command: + return "Command"; + case kVK_Shift: + return "Shift"; + case kVK_CapsLock: + return "CapsLock"; + case kVK_Option: + return "Option"; + case kVK_Control: + return "Control"; + case kVK_RightShift: + return "Right-Shift"; + case kVK_RightOption: + return "Right-Option"; + case kVK_RightControl: + return "Right-Control"; + case kVK_ANSI_KeypadClear: + return "Clear"; + + case kVK_F1: + return "F1"; + case kVK_F2: + return "F2"; + case kVK_F3: + return "F3"; + case kVK_F4: + return "F4"; + case kVK_F5: + return "F5"; + case kVK_F6: + return "F6"; + case kVK_F7: + return "F7"; + case kVK_F8: + return "F8"; + case kVK_F9: + return "F9"; + case kVK_F10: + return "F10"; + case kVK_F11: + return "F11"; + case kVK_F12: + return "F12"; + case kVK_F13: + return "F13/PrintScreen"; + case kVK_F14: + return "F14/ScrollLock"; + case kVK_F15: + return "F15/Pause"; + + case kVK_ANSI_Keypad0: + return "NumPad-0"; + case kVK_ANSI_Keypad1: + return "NumPad-1"; + case kVK_ANSI_Keypad2: + return "NumPad-2"; + case kVK_ANSI_Keypad3: + return "NumPad-3"; + case kVK_ANSI_Keypad4: + return "NumPad-4"; + case kVK_ANSI_Keypad5: + return "NumPad-5"; + case kVK_ANSI_Keypad6: + return "NumPad-6"; + case kVK_ANSI_Keypad7: + return "NumPad-7"; + case kVK_ANSI_Keypad8: + return "NumPad-8"; + case kVK_ANSI_Keypad9: + return "NumPad-9"; + + case kVK_ANSI_KeypadMultiply: + return "NumPad-*"; + case kVK_ANSI_KeypadPlus: + return "NumPad-+"; + case kVK_ANSI_KeypadMinus: + return "NumPad--"; + case kVK_ANSI_KeypadDecimal: + return "NumPad-."; + case kVK_ANSI_KeypadDivide: + return "NumPad-/"; + case kVK_ANSI_KeypadEquals: + return "NumPad-="; + case kVK_ANSI_KeypadEnter: + return "NumPad-Enter"; + case kVK_Return: + return "Return"; + case kVK_Powerbook_KeypadEnter: + return "NumPad-EnterOnPowerBook"; + + case kVK_PC_Insert: + return "Insert/Help"; + case kVK_PC_Delete: + return "Delete"; + case kVK_Tab: + return "Tab"; + case kVK_PC_Backspace: + return "Backspace"; + case kVK_Home: + return "Home"; + case kVK_End: + return "End"; + case kVK_PageUp: + return "PageUp"; + case kVK_PageDown: + return "PageDown"; + case kVK_LeftArrow: + return "LeftArrow"; + case kVK_RightArrow: + return "RightArrow"; + case kVK_UpArrow: + return "UpArrow"; + case kVK_DownArrow: + return "DownArrow"; + case kVK_PC_ContextMenu: + return "ContextMenu"; + + case kVK_Function: + return "Function"; + case kVK_VolumeUp: + return "VolumeUp"; + case kVK_VolumeDown: + return "VolumeDown"; + case kVK_Mute: + return "Mute"; + + case kVK_ISO_Section: + return "ISO_Section"; + + case kVK_JIS_Yen: + return "JIS_Yen"; + case kVK_JIS_Underscore: + return "JIS_Underscore"; + case kVK_JIS_KeypadComma: + return "JIS_KeypadComma"; + case kVK_JIS_Eisu: + return "JIS_Eisu"; + case kVK_JIS_Kana: + return "JIS_Kana"; + + case kVK_ANSI_A: + return "A"; + case kVK_ANSI_B: + return "B"; + case kVK_ANSI_C: + return "C"; + case kVK_ANSI_D: + return "D"; + case kVK_ANSI_E: + return "E"; + case kVK_ANSI_F: + return "F"; + case kVK_ANSI_G: + return "G"; + case kVK_ANSI_H: + return "H"; + case kVK_ANSI_I: + return "I"; + case kVK_ANSI_J: + return "J"; + case kVK_ANSI_K: + return "K"; + case kVK_ANSI_L: + return "L"; + case kVK_ANSI_M: + return "M"; + case kVK_ANSI_N: + return "N"; + case kVK_ANSI_O: + return "O"; + case kVK_ANSI_P: + return "P"; + case kVK_ANSI_Q: + return "Q"; + case kVK_ANSI_R: + return "R"; + case kVK_ANSI_S: + return "S"; + case kVK_ANSI_T: + return "T"; + case kVK_ANSI_U: + return "U"; + case kVK_ANSI_V: + return "V"; + case kVK_ANSI_W: + return "W"; + case kVK_ANSI_X: + return "X"; + case kVK_ANSI_Y: + return "Y"; + case kVK_ANSI_Z: + return "Z"; + + case kVK_ANSI_1: + return "1"; + case kVK_ANSI_2: + return "2"; + case kVK_ANSI_3: + return "3"; + case kVK_ANSI_4: + return "4"; + case kVK_ANSI_5: + return "5"; + case kVK_ANSI_6: + return "6"; + case kVK_ANSI_7: + return "7"; + case kVK_ANSI_8: + return "8"; + case kVK_ANSI_9: + return "9"; + case kVK_ANSI_0: + return "0"; + case kVK_ANSI_Equal: + return "Equal"; + case kVK_ANSI_Minus: + return "Minus"; + case kVK_ANSI_RightBracket: + return "RightBracket"; + case kVK_ANSI_LeftBracket: + return "LeftBracket"; + case kVK_ANSI_Quote: + return "Quote"; + case kVK_ANSI_Semicolon: + return "Semicolon"; + case kVK_ANSI_Backslash: + return "Backslash"; + case kVK_ANSI_Comma: + return "Comma"; + case kVK_ANSI_Slash: + return "Slash"; + case kVK_ANSI_Period: + return "Period"; + case kVK_ANSI_Grave: + return "Grave"; + + default: + return "undefined"; + } +} + +static const char* GetCharacters(const nsAString& aString) { + if (aString.IsEmpty()) { + return ""; + } + nsAutoString escapedStr; + for (uint32_t i = 0; i < aString.Length(); i++) { + char16_t ch = aString.CharAt(i); + if (ch < 0x20) { + nsPrintfCString utf8str("(U+%04X)", ch); + escapedStr += NS_ConvertUTF8toUTF16(utf8str); + } else if (ch <= 0x7E) { + escapedStr += ch; + } else { + nsPrintfCString utf8str("(U+%04X)", ch); + escapedStr += ch; + escapedStr += NS_ConvertUTF8toUTF16(utf8str); + } + } + + // the result will be freed automatically by cocoa. + NSString* result = nsCocoaUtils::ToNSString(escapedStr); + return [result UTF8String]; +} + +static const char* GetCharacters(const NSString* aString) { + nsAutoString str; + nsCocoaUtils::GetStringForNSString(aString, str); + return GetCharacters(str); +} + +static const char* GetCharacters(const CFStringRef aString) { + const NSString* str = reinterpret_cast<const NSString*>(aString); + return GetCharacters(str); +} + +static const char* GetNativeKeyEventType(NSEvent* aNativeEvent) { + switch ([aNativeEvent type]) { + case NSEventTypeKeyDown: + return "NSEventTypeKeyDown"; + case NSEventTypeKeyUp: + return "NSEventTypeKeyUp"; + case NSEventTypeFlagsChanged: + return "NSEventTypeFlagsChanged"; + default: + return "not key event"; + } +} + +static const char* GetGeckoKeyEventType(const WidgetEvent& aEvent) { + switch (aEvent.mMessage) { + case eKeyDown: + return "eKeyDown"; + case eKeyUp: + return "eKeyUp"; + case eKeyPress: + return "eKeyPress"; + default: + return "not key event"; + } +} + +static const char* GetWindowLevelName(NSInteger aWindowLevel) { + switch (aWindowLevel) { + case kCGBaseWindowLevelKey: + return "kCGBaseWindowLevelKey (NSNormalWindowLevel)"; + case kCGMinimumWindowLevelKey: + return "kCGMinimumWindowLevelKey"; + case kCGDesktopWindowLevelKey: + return "kCGDesktopWindowLevelKey"; + case kCGBackstopMenuLevelKey: + return "kCGBackstopMenuLevelKey"; + case kCGNormalWindowLevelKey: + return "kCGNormalWindowLevelKey"; + case kCGFloatingWindowLevelKey: + return "kCGFloatingWindowLevelKey (NSFloatingWindowLevel)"; + case kCGTornOffMenuWindowLevelKey: + return "kCGTornOffMenuWindowLevelKey (NSSubmenuWindowLevel, NSTornOffMenuWindowLevel)"; + case kCGDockWindowLevelKey: + return "kCGDockWindowLevelKey (NSDockWindowLevel)"; + case kCGMainMenuWindowLevelKey: + return "kCGMainMenuWindowLevelKey (NSMainMenuWindowLevel)"; + case kCGStatusWindowLevelKey: + return "kCGStatusWindowLevelKey (NSStatusWindowLevel)"; + case kCGModalPanelWindowLevelKey: + return "kCGModalPanelWindowLevelKey (NSModalPanelWindowLevel)"; + case kCGPopUpMenuWindowLevelKey: + return "kCGPopUpMenuWindowLevelKey (NSPopUpMenuWindowLevel)"; + case kCGDraggingWindowLevelKey: + return "kCGDraggingWindowLevelKey"; + case kCGScreenSaverWindowLevelKey: + return "kCGScreenSaverWindowLevelKey (NSScreenSaverWindowLevel)"; + case kCGMaximumWindowLevelKey: + return "kCGMaximumWindowLevelKey"; + case kCGOverlayWindowLevelKey: + return "kCGOverlayWindowLevelKey"; + case kCGHelpWindowLevelKey: + return "kCGHelpWindowLevelKey"; + case kCGUtilityWindowLevelKey: + return "kCGUtilityWindowLevelKey"; + case kCGDesktopIconWindowLevelKey: + return "kCGDesktopIconWindowLevelKey"; + case kCGCursorWindowLevelKey: + return "kCGCursorWindowLevelKey"; + case kCGNumberOfWindowLevelKeys: + return "kCGNumberOfWindowLevelKeys"; + default: + return "unknown window level"; + } +} + +static bool IsControlChar(uint32_t aCharCode) { return aCharCode < ' ' || aCharCode == 0x7F; } + +static uint32_t gHandlerInstanceCount = 0; + +static void EnsureToLogAllKeyboardLayoutsAndIMEs() { + static bool sDone = false; + if (!sDone) { + sDone = true; + TextInputHandler::DebugPrintAllKeyboardLayouts(); + IMEInputHandler::DebugPrintAllIMEModes(); + } +} + +inline NSRange MakeNSRangeFrom(const Maybe<OffsetAndData<uint32_t>>& aOffsetAndData) { + if (aOffsetAndData.isNothing()) { + return NSMakeRange(NSNotFound, 0); + } + return NSMakeRange(aOffsetAndData->StartOffset(), aOffsetAndData->Length()); +} + +#pragma mark - + +/****************************************************************************** + * + * TISInputSourceWrapper implementation + * + ******************************************************************************/ + +TISInputSourceWrapper* TISInputSourceWrapper::sCurrentInputSource = nullptr; + +// static +TISInputSourceWrapper& TISInputSourceWrapper::CurrentInputSource() { + if (!sCurrentInputSource) { + sCurrentInputSource = new TISInputSourceWrapper(); + } + if (!sCurrentInputSource->IsInitializedByCurrentInputSource()) { + sCurrentInputSource->InitByCurrentInputSource(); + } + return *sCurrentInputSource; +} + +// static +void TISInputSourceWrapper::Shutdown() { + if (!sCurrentInputSource) { + return; + } + sCurrentInputSource->Clear(); + delete sCurrentInputSource; + sCurrentInputSource = nullptr; +} + +bool TISInputSourceWrapper::TranslateToString(UInt32 aKeyCode, UInt32 aModifiers, UInt32 aKbType, + nsAString& aStr) { + aStr.Truncate(); + + const UCKeyboardLayout* UCKey = GetUCKeyboardLayout(); + + MOZ_LOG(gLog, LogLevel::Info, + ("%p TISInputSourceWrapper::TranslateToString, aKeyCode=0x%X, " + "aModifiers=0x%X, aKbType=0x%X UCKey=%p\n " + "Shift: %s, Ctrl: %s, Opt: %s, Cmd: %s, CapsLock: %s, NumLock: %s", + this, static_cast<unsigned int>(aKeyCode), static_cast<unsigned int>(aModifiers), + static_cast<unsigned int>(aKbType), UCKey, OnOrOff(aModifiers & shiftKey), + OnOrOff(aModifiers & controlKey), OnOrOff(aModifiers & optionKey), + OnOrOff(aModifiers & cmdKey), OnOrOff(aModifiers & alphaLock), + OnOrOff(aModifiers & kEventKeyModifierNumLockMask))); + + NS_ENSURE_TRUE(UCKey, false); + + UInt32 deadKeyState = 0; + UniCharCount len; + UniChar chars[5]; + OSStatus err = ::UCKeyTranslate(UCKey, aKeyCode, kUCKeyActionDown, aModifiers >> 8, aKbType, + kUCKeyTranslateNoDeadKeysMask, &deadKeyState, 5, &len, chars); + + MOZ_LOG(gLog, LogLevel::Info, + ("%p TISInputSourceWrapper::TranslateToString, err=0x%X, len=%zu", this, + static_cast<int>(err), len)); + + NS_ENSURE_TRUE(err == noErr, false); + if (len == 0) { + return true; + } + if (!aStr.SetLength(len, fallible)) { + return false; + } + NS_ASSERTION(sizeof(char16_t) == sizeof(UniChar), + "size of char16_t and size of UniChar are different"); + memcpy(aStr.BeginWriting(), chars, len * sizeof(char16_t)); + + MOZ_LOG(gLog, LogLevel::Info, + ("%p TISInputSourceWrapper::TranslateToString, aStr=\"%s\"", this, + NS_ConvertUTF16toUTF8(aStr).get())); + + return true; +} + +uint32_t TISInputSourceWrapper::TranslateToChar(UInt32 aKeyCode, UInt32 aModifiers, + UInt32 aKbType) { + nsAutoString str; + if (!TranslateToString(aKeyCode, aModifiers, aKbType, str) || str.Length() != 1) { + return 0; + } + return static_cast<uint32_t>(str.CharAt(0)); +} + +bool TISInputSourceWrapper::IsDeadKey(NSEvent* aNativeKeyEvent) { + if ([[aNativeKeyEvent characters] length]) { + return false; + } + + // Assmue that if control key or command key is pressed, it's not a dead key. + NSUInteger cocoaState = [aNativeKeyEvent modifierFlags]; + if (cocoaState & (NSEventModifierFlagControl | NSEventModifierFlagCommand)) { + return false; + } + + UInt32 nativeKeyCode = [aNativeKeyEvent keyCode]; + switch (nativeKeyCode) { + case kVK_ANSI_A: + case kVK_ANSI_B: + case kVK_ANSI_C: + case kVK_ANSI_D: + case kVK_ANSI_E: + case kVK_ANSI_F: + case kVK_ANSI_G: + case kVK_ANSI_H: + case kVK_ANSI_I: + case kVK_ANSI_J: + case kVK_ANSI_K: + case kVK_ANSI_L: + case kVK_ANSI_M: + case kVK_ANSI_N: + case kVK_ANSI_O: + case kVK_ANSI_P: + case kVK_ANSI_Q: + case kVK_ANSI_R: + case kVK_ANSI_S: + case kVK_ANSI_T: + case kVK_ANSI_U: + case kVK_ANSI_V: + case kVK_ANSI_W: + case kVK_ANSI_X: + case kVK_ANSI_Y: + case kVK_ANSI_Z: + case kVK_ANSI_1: + case kVK_ANSI_2: + case kVK_ANSI_3: + case kVK_ANSI_4: + case kVK_ANSI_5: + case kVK_ANSI_6: + case kVK_ANSI_7: + case kVK_ANSI_8: + case kVK_ANSI_9: + case kVK_ANSI_0: + case kVK_ANSI_Equal: + case kVK_ANSI_Minus: + case kVK_ANSI_RightBracket: + case kVK_ANSI_LeftBracket: + case kVK_ANSI_Quote: + case kVK_ANSI_Semicolon: + case kVK_ANSI_Backslash: + case kVK_ANSI_Comma: + case kVK_ANSI_Slash: + case kVK_ANSI_Period: + case kVK_ANSI_Grave: + case kVK_JIS_Yen: + case kVK_JIS_Underscore: + break; + default: + // Let's assume that dead key can be only a printable key in standard + // position. + return false; + } + + // If TranslateToChar() returns non-zero value, that means that + // the key may input a character with different dead key state. + UInt32 kbType = GetKbdType(); + UInt32 carbonState = nsCocoaUtils::ConvertToCarbonModifier(cocoaState); + return IsDeadKey(nativeKeyCode, carbonState, kbType); +} + +bool TISInputSourceWrapper::IsDeadKey(UInt32 aKeyCode, UInt32 aModifiers, UInt32 aKbType) { + const UCKeyboardLayout* UCKey = GetUCKeyboardLayout(); + + MOZ_LOG(gLog, LogLevel::Info, + ("%p TISInputSourceWrapper::IsDeadKey, aKeyCode=0x%X, " + "aModifiers=0x%X, aKbType=0x%X UCKey=%p\n " + "Shift: %s, Ctrl: %s, Opt: %s, Cmd: %s, CapsLock: %s, NumLock: %s", + this, static_cast<unsigned int>(aKeyCode), static_cast<unsigned int>(aModifiers), + static_cast<unsigned int>(aKbType), UCKey, OnOrOff(aModifiers & shiftKey), + OnOrOff(aModifiers & controlKey), OnOrOff(aModifiers & optionKey), + OnOrOff(aModifiers & cmdKey), OnOrOff(aModifiers & alphaLock), + OnOrOff(aModifiers & kEventKeyModifierNumLockMask))); + + if (NS_WARN_IF(!UCKey)) { + return false; + } + + UInt32 deadKeyState = 0; + UniCharCount len; + UniChar chars[5]; + OSStatus err = ::UCKeyTranslate(UCKey, aKeyCode, kUCKeyActionDown, aModifiers >> 8, aKbType, 0, + &deadKeyState, 5, &len, chars); + + MOZ_LOG(gLog, LogLevel::Info, + ("%p TISInputSourceWrapper::IsDeadKey, err=0x%X, " + "len=%zu, deadKeyState=%u", + this, static_cast<int>(err), len, deadKeyState)); + + if (NS_WARN_IF(err != noErr)) { + return false; + } + + return deadKeyState != 0; +} + +void TISInputSourceWrapper::InitByInputSourceID(const char* aID) { + Clear(); + if (!aID) return; + + CFStringRef idstr = ::CFStringCreateWithCString(kCFAllocatorDefault, aID, kCFStringEncodingASCII); + InitByInputSourceID(idstr); + ::CFRelease(idstr); +} + +void TISInputSourceWrapper::InitByInputSourceID(const nsString& aID) { + Clear(); + if (aID.IsEmpty()) return; + CFStringRef idstr = ::CFStringCreateWithCharacters( + kCFAllocatorDefault, reinterpret_cast<const UniChar*>(aID.get()), aID.Length()); + InitByInputSourceID(idstr); + ::CFRelease(idstr); +} + +void TISInputSourceWrapper::InitByInputSourceID(const CFStringRef aID) { + Clear(); + if (!aID) return; + const void* keys[] = {kTISPropertyInputSourceID}; + const void* values[] = {aID}; + CFDictionaryRef filter = ::CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1, NULL, NULL); + NS_ASSERTION(filter, "failed to create the filter"); + mInputSourceList = ::TISCreateInputSourceList(filter, true); + ::CFRelease(filter); + if (::CFArrayGetCount(mInputSourceList) > 0) { + mInputSource = static_cast<TISInputSourceRef>( + const_cast<void*>(::CFArrayGetValueAtIndex(mInputSourceList, 0))); + if (IsKeyboardLayout()) { + mKeyboardLayout = mInputSource; + } + } +} + +void TISInputSourceWrapper::InitByLayoutID(SInt32 aLayoutID, bool aOverrideKeyboard) { + // NOTE: Doument new layout IDs in TextInputHandler.h when you add ones. + switch (aLayoutID) { + case 0: + InitByInputSourceID("com.apple.keylayout.US"); + break; + case 1: + InitByInputSourceID("com.apple.keylayout.Greek"); + break; + case 2: + InitByInputSourceID("com.apple.keylayout.German"); + break; + case 3: + InitByInputSourceID("com.apple.keylayout.Swedish-Pro"); + break; + case 4: + InitByInputSourceID("com.apple.keylayout.DVORAK-QWERTYCMD"); + break; + case 5: + InitByInputSourceID("com.apple.keylayout.Thai"); + break; + case 6: + InitByInputSourceID("com.apple.keylayout.Arabic"); + break; + case 7: + InitByInputSourceID("com.apple.keylayout.ArabicPC"); + break; + case 8: + InitByInputSourceID("com.apple.keylayout.French"); + break; + case 9: + InitByInputSourceID("com.apple.keylayout.Hebrew"); + break; + case 10: + InitByInputSourceID("com.apple.keylayout.Lithuanian"); + break; + case 11: + InitByInputSourceID("com.apple.keylayout.Norwegian"); + break; + case 12: + InitByInputSourceID("com.apple.keylayout.Spanish"); + break; + default: + Clear(); + break; + } + mOverrideKeyboard = aOverrideKeyboard; +} + +void TISInputSourceWrapper::InitByCurrentInputSource() { + Clear(); + mInputSource = ::TISCopyCurrentKeyboardInputSource(); + mKeyboardLayout = ::TISCopyInputMethodKeyboardLayoutOverride(); + if (!mKeyboardLayout) { + mKeyboardLayout = ::TISCopyCurrentKeyboardLayoutInputSource(); + } + // If this causes composition, the current keyboard layout may input non-ASCII + // characters such as Japanese Kana characters or Hangul characters. + // However, we need to set ASCII characters to DOM key events for consistency + // with other platforms. + if (IsOpenedIMEMode()) { + TISInputSourceWrapper tis(mKeyboardLayout); + if (!tis.IsASCIICapable()) { + mKeyboardLayout = ::TISCopyCurrentASCIICapableKeyboardLayoutInputSource(); + } + } +} + +void TISInputSourceWrapper::InitByCurrentKeyboardLayout() { + Clear(); + mInputSource = ::TISCopyCurrentKeyboardLayoutInputSource(); + mKeyboardLayout = mInputSource; +} + +void TISInputSourceWrapper::InitByCurrentASCIICapableInputSource() { + Clear(); + mInputSource = ::TISCopyCurrentASCIICapableKeyboardInputSource(); + mKeyboardLayout = ::TISCopyInputMethodKeyboardLayoutOverride(); + if (mKeyboardLayout) { + TISInputSourceWrapper tis(mKeyboardLayout); + if (!tis.IsASCIICapable()) { + mKeyboardLayout = nullptr; + } + } + if (!mKeyboardLayout) { + mKeyboardLayout = ::TISCopyCurrentASCIICapableKeyboardLayoutInputSource(); + } +} + +void TISInputSourceWrapper::InitByCurrentASCIICapableKeyboardLayout() { + Clear(); + mInputSource = ::TISCopyCurrentASCIICapableKeyboardLayoutInputSource(); + mKeyboardLayout = mInputSource; +} + +void TISInputSourceWrapper::InitByCurrentInputMethodKeyboardLayoutOverride() { + Clear(); + mInputSource = ::TISCopyInputMethodKeyboardLayoutOverride(); + mKeyboardLayout = mInputSource; +} + +void TISInputSourceWrapper::InitByTISInputSourceRef(TISInputSourceRef aInputSource) { + Clear(); + mInputSource = aInputSource; + if (IsKeyboardLayout()) { + mKeyboardLayout = mInputSource; + } +} + +void TISInputSourceWrapper::InitByLanguage(CFStringRef aLanguage) { + Clear(); + mInputSource = ::TISCopyInputSourceForLanguage(aLanguage); + if (IsKeyboardLayout()) { + mKeyboardLayout = mInputSource; + } +} + +const UCKeyboardLayout* TISInputSourceWrapper::GetUCKeyboardLayout() { + NS_ENSURE_TRUE(mKeyboardLayout, nullptr); + if (mUCKeyboardLayout) { + return mUCKeyboardLayout; + } + CFDataRef uchr = static_cast<CFDataRef>( + ::TISGetInputSourceProperty(mKeyboardLayout, kTISPropertyUnicodeKeyLayoutData)); + + // We should be always able to get the layout here. + NS_ENSURE_TRUE(uchr, nullptr); + mUCKeyboardLayout = reinterpret_cast<const UCKeyboardLayout*>(CFDataGetBytePtr(uchr)); + return mUCKeyboardLayout; +} + +bool TISInputSourceWrapper::GetBoolProperty(const CFStringRef aKey) { + CFBooleanRef ret = static_cast<CFBooleanRef>(::TISGetInputSourceProperty(mInputSource, aKey)); + return ::CFBooleanGetValue(ret); +} + +bool TISInputSourceWrapper::GetStringProperty(const CFStringRef aKey, CFStringRef& aStr) { + aStr = static_cast<CFStringRef>(::TISGetInputSourceProperty(mInputSource, aKey)); + return aStr != nullptr; +} + +bool TISInputSourceWrapper::GetStringProperty(const CFStringRef aKey, nsAString& aStr) { + CFStringRef str; + GetStringProperty(aKey, str); + nsCocoaUtils::GetStringForNSString((const NSString*)str, aStr); + return !aStr.IsEmpty(); +} + +bool TISInputSourceWrapper::IsOpenedIMEMode() { + NS_ENSURE_TRUE(mInputSource, false); + if (!IsIMEMode()) return false; + return !IsASCIICapable(); +} + +bool TISInputSourceWrapper::IsIMEMode() { + NS_ENSURE_TRUE(mInputSource, false); + CFStringRef str; + GetInputSourceType(str); + NS_ENSURE_TRUE(str, false); + return ::CFStringCompare(kTISTypeKeyboardInputMode, str, 0) == kCFCompareEqualTo; +} + +bool TISInputSourceWrapper::IsKeyboardLayout() { + NS_ENSURE_TRUE(mInputSource, false); + CFStringRef str; + GetInputSourceType(str); + NS_ENSURE_TRUE(str, false); + return ::CFStringCompare(kTISTypeKeyboardLayout, str, 0) == kCFCompareEqualTo; +} + +bool TISInputSourceWrapper::GetLanguageList(CFArrayRef& aLanguageList) { + NS_ENSURE_TRUE(mInputSource, false); + aLanguageList = static_cast<CFArrayRef>( + ::TISGetInputSourceProperty(mInputSource, kTISPropertyInputSourceLanguages)); + return aLanguageList != nullptr; +} + +bool TISInputSourceWrapper::GetPrimaryLanguage(CFStringRef& aPrimaryLanguage) { + NS_ENSURE_TRUE(mInputSource, false); + CFArrayRef langList; + NS_ENSURE_TRUE(GetLanguageList(langList), false); + if (::CFArrayGetCount(langList) == 0) return false; + aPrimaryLanguage = static_cast<CFStringRef>(::CFArrayGetValueAtIndex(langList, 0)); + return aPrimaryLanguage != nullptr; +} + +bool TISInputSourceWrapper::GetPrimaryLanguage(nsAString& aPrimaryLanguage) { + NS_ENSURE_TRUE(mInputSource, false); + CFStringRef primaryLanguage; + NS_ENSURE_TRUE(GetPrimaryLanguage(primaryLanguage), false); + nsCocoaUtils::GetStringForNSString((const NSString*)primaryLanguage, aPrimaryLanguage); + return !aPrimaryLanguage.IsEmpty(); +} + +bool TISInputSourceWrapper::IsForRTLLanguage() { + if (mIsRTL < 0) { + // Get the input character of the 'A' key of ANSI keyboard layout. + nsAutoString str; + bool ret = TranslateToString(kVK_ANSI_A, 0, eKbdType_ANSI, str); + NS_ENSURE_TRUE(ret, ret); + char16_t ch = str.IsEmpty() ? char16_t(0) : str.CharAt(0); + mIsRTL = UTF16_CODE_UNIT_IS_BIDI(ch); + } + return mIsRTL != 0; +} + +bool TISInputSourceWrapper::IsForJapaneseLanguage() { + nsAutoString lang; + GetPrimaryLanguage(lang); + return lang.EqualsLiteral("ja"); +} + +bool TISInputSourceWrapper::IsInitializedByCurrentInputSource() { + return mInputSource == ::TISCopyCurrentKeyboardInputSource(); +} + +void TISInputSourceWrapper::Select() { + if (!mInputSource) return; + ::TISSelectInputSource(mInputSource); +} + +void TISInputSourceWrapper::Clear() { + // Clear() is always called when TISInputSourceWrappper is created. + EnsureToLogAllKeyboardLayoutsAndIMEs(); + + if (mInputSourceList) { + ::CFRelease(mInputSourceList); + } + mInputSourceList = nullptr; + mInputSource = nullptr; + mKeyboardLayout = nullptr; + mIsRTL = -1; + mUCKeyboardLayout = nullptr; + mOverrideKeyboard = false; +} + +bool TISInputSourceWrapper::IsPrintableKeyEvent(NSEvent* aNativeKeyEvent) const { + UInt32 nativeKeyCode = [aNativeKeyEvent keyCode]; + + bool isPrintableKey = !TextInputHandler::IsSpecialGeckoKey(nativeKeyCode); + if (isPrintableKey && [aNativeKeyEvent type] != NSEventTypeKeyDown && + [aNativeKeyEvent type] != NSEventTypeKeyUp) { + NS_WARNING("Why would a printable key not be an NSEventTypeKeyDown or NSEventTypeKeyUp event?"); + isPrintableKey = false; + } + return isPrintableKey; +} + +UInt32 TISInputSourceWrapper::GetKbdType() const { + // If a keyboard layout override is set, we also need to force the keyboard + // type to something ANSI to avoid test failures on machines with JIS + // keyboards (since the pair of keyboard layout and physical keyboard type + // form the actual key layout). This assumes that the test setting the + // override was written assuming an ANSI keyboard. + return mOverrideKeyboard ? eKbdType_ANSI : ::LMGetKbdType(); +} + +void TISInputSourceWrapper::ComputeInsertStringForCharCode(NSEvent* aNativeKeyEvent, + const WidgetKeyboardEvent& aKeyEvent, + const nsAString* aInsertString, + nsAString& aResult) { + if (aInsertString) { + // If the caller expects that the aInsertString will be input, we shouldn't + // change it. + aResult = *aInsertString; + } else if (IsPrintableKeyEvent(aNativeKeyEvent)) { + // If IME is open, [aNativeKeyEvent characters] may be a character + // which will be appended to the composition string. However, especially, + // while IME is disabled, most users and developers expect the key event + // works as IME closed. So, we should compute the aResult with + // the ASCII capable keyboard layout. + // NOTE: Such keyboard layouts typically change the layout to its ASCII + // capable layout when Command key is pressed. And we don't worry + // when Control key is pressed too because it causes inputting + // control characters. + // Additionally, if the key event doesn't input any text, the event may be + // dead key event. In this case, the charCode value should be the dead + // character. + UInt32 nativeKeyCode = [aNativeKeyEvent keyCode]; + if ((!aKeyEvent.IsMeta() && !aKeyEvent.IsControl() && IsOpenedIMEMode()) || + ![[aNativeKeyEvent characters] length]) { + UInt32 state = nsCocoaUtils::ConvertToCarbonModifier([aNativeKeyEvent modifierFlags]); + uint32_t ch = TranslateToChar(nativeKeyCode, state, GetKbdType()); + if (ch) { + aResult = ch; + } + } else { + // If the caller isn't sure what string will be input, let's use + // characters of NSEvent. + nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], aResult); + } + + // If control key is pressed and the eventChars is a non-printable control + // character, we should convert it to ASCII alphabet. + if (aKeyEvent.IsControl() && !aResult.IsEmpty() && aResult[0] <= char16_t(26)) { + aResult = (aKeyEvent.IsShift() ^ aKeyEvent.IsCapsLocked()) + ? static_cast<char16_t>(aResult[0] + ('A' - 1)) + : static_cast<char16_t>(aResult[0] + ('a' - 1)); + } + // If Meta key is pressed, it may cause to switch the keyboard layout like + // Arabic, Russian, Hebrew, Greek and Dvorak-QWERTY. + else if (aKeyEvent.IsMeta() && !(aKeyEvent.IsControl() || aKeyEvent.IsAlt())) { + UInt32 kbType = GetKbdType(); + UInt32 numLockState = aKeyEvent.IsNumLocked() ? kEventKeyModifierNumLockMask : 0; + UInt32 capsLockState = aKeyEvent.IsCapsLocked() ? alphaLock : 0; + UInt32 shiftState = aKeyEvent.IsShift() ? shiftKey : 0; + uint32_t uncmdedChar = TranslateToChar(nativeKeyCode, numLockState, kbType); + uint32_t cmdedChar = TranslateToChar(nativeKeyCode, cmdKey | numLockState, kbType); + // If we can make a good guess at the characters that the user would + // expect this key combination to produce (with and without Shift) then + // use those characters. This also corrects for CapsLock. + uint32_t ch = 0; + if (uncmdedChar == cmdedChar) { + // The characters produced with Command seem similar to those without + // Command. + ch = TranslateToChar(nativeKeyCode, shiftState | capsLockState | numLockState, kbType); + } else { + TISInputSourceWrapper USLayout("com.apple.keylayout.US"); + uint32_t uncmdedUSChar = USLayout.TranslateToChar(nativeKeyCode, numLockState, kbType); + // If it looks like characters from US keyboard layout when Command key + // is pressed, we should compute a character in the layout. + if (uncmdedUSChar == cmdedChar) { + ch = USLayout.TranslateToChar(nativeKeyCode, shiftState | capsLockState | numLockState, + kbType); + } + } + + // If there is a more preferred character for the commanded key event, + // we should use it. + if (ch) { + aResult = ch; + } + } + } + + // Remove control characters which shouldn't be inputted on editor. + // XXX Currently, we don't find any cases inserting control characters with + // printable character. So, just checking first character is enough. + if (!aResult.IsEmpty() && IsControlChar(aResult[0])) { + aResult.Truncate(); + } +} + +void TISInputSourceWrapper::InitKeyEvent(NSEvent* aNativeKeyEvent, WidgetKeyboardEvent& aKeyEvent, + bool aIsProcessedByIME, const nsAString* aInsertString) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + MOZ_ASSERT(!aIsProcessedByIME || aKeyEvent.mMessage != eKeyPress, + "eKeyPress event should not be marked as proccessed by IME"); + + MOZ_LOG(gLog, 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(gLog, 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_ABORT_BLOCK +} + +void TISInputSourceWrapper::WillDispatchKeyboardEvent(NSEvent* aNativeKeyEvent, + const nsAString* aInsertString, + uint32_t aIndexOfKeypress, + WidgetKeyboardEvent& aKeyEvent) { + NS_OBJC_BEGIN_TRY_ABORT_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(gLog, LogLevel::Info)) { + nsAutoString chars; + nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], chars); + NS_ConvertUTF16toUTF8 utf8Chars(chars); + char16_t uniChar = static_cast<char16_t>(aKeyEvent.mCharCode); + MOZ_LOG(gLog, LogLevel::Info, + ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, " + "aNativeKeyEvent=%p, aInsertString=%p (\"%s\"), " + "aIndexOfKeypress=%u, [aNativeKeyEvent characters]=\"%s\", " + "aKeyEvent={ mMessage=%s, mCharCode=0x%X(%s) }, kbType=0x%X, " + "IsOpenedIMEMode()=%s", + this, aNativeKeyEvent, aInsertString, + aInsertString ? GetCharacters(*aInsertString) : "", aIndexOfKeypress, + GetCharacters([aNativeKeyEvent characters]), GetGeckoKeyEventType(aKeyEvent), + aKeyEvent.mCharCode, uniChar ? NS_ConvertUTF16toUTF8(&uniChar, 1).get() : "", + static_cast<unsigned int>(kbType), TrueOrFalse(IsOpenedIMEMode()))); + } + + nsAutoString insertStringForCharCode; + ComputeInsertStringForCharCode(aNativeKeyEvent, aKeyEvent, aInsertString, + insertStringForCharCode); + + // The mCharCode was set from mKeyValue. However, for example, when Ctrl key + // is pressed, its value should indicate an ASCII character for backward + // compatibility rather than inputting character without the modifiers. + // Therefore, we need to modify mCharCode value here. + uint32_t charCode = 0; + if (aIndexOfKeypress < insertStringForCharCode.Length()) { + charCode = insertStringForCharCode[aIndexOfKeypress]; + } + aKeyEvent.SetCharCode(charCode); + + MOZ_LOG(gLog, 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(gLog, LogLevel::Info, + ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, " + "isRomanKeyboardLayout=%s, kbType=0x%X, key=0x%X", + this, TrueOrFalse(isRomanKeyboardLayout), static_cast<unsigned int>(kbType), + static_cast<unsigned int>(key))); + + nsString str; + + // normal chars + uint32_t unshiftedChar = TranslateToChar(key, lockState, kbType); + UInt32 shiftLockMod = shiftKey | lockState; + uint32_t shiftedChar = TranslateToChar(key, shiftLockMod, kbType); + + // characters generated with Cmd key + // XXX we should remove CapsLock state, which changes characters from + // Latin to Cyrillic with Russian layout on 10.4 only when Cmd key + // is pressed. + UInt32 numState = (lockState & ~alphaLock); // only num lock state + uint32_t uncmdedChar = TranslateToChar(key, numState, kbType); + UInt32 shiftNumMod = numState | shiftKey; + uint32_t uncmdedShiftChar = TranslateToChar(key, shiftNumMod, kbType); + uint32_t uncmdedUSChar = USLayout.TranslateToChar(key, numState, kbType); + UInt32 cmdNumMod = cmdKey | numState; + uint32_t cmdedChar = TranslateToChar(key, cmdNumMod, kbType); + UInt32 cmdShiftNumMod = shiftKey | cmdNumMod; + uint32_t cmdedShiftChar = TranslateToChar(key, cmdShiftNumMod, kbType); + + // Is the keyboard layout changed by Cmd key? + // E.g., Arabic, Russian, Hebrew, Greek and Dvorak-QWERTY. + bool isCmdSwitchLayout = uncmdedChar != cmdedChar; + // Is the keyboard layout for Latin, but Cmd key switches the layout? + // I.e., Dvorak-QWERTY + bool isDvorakQWERTY = isCmdSwitchLayout && isRomanKeyboardLayout; + + // If the current keyboard is not Dvorak-QWERTY or Cmd is not pressed, + // we should append unshiftedChar and shiftedChar for handling the + // normal characters. These are the characters that the user is most + // likely to associate with this key. + if ((unshiftedChar || shiftedChar) && (!aKeyEvent.IsMeta() || !isDvorakQWERTY)) { + AlternativeCharCode altCharCodes(unshiftedChar, shiftedChar); + aKeyEvent.mAlternativeCharCodes.AppendElement(altCharCodes); + } + MOZ_LOG( + gLog, 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(gLog, 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(gLog, LogLevel::Info, + ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, " + "hasCmdShiftOnlyChar=%s, originalCmdedShiftChar=U+%X", + this, TrueOrFalse(hasCmdShiftOnlyChar), originalCmdedShiftChar)); + + NS_OBJC_END_TRY_ABORT_BLOCK +} + +uint32_t TISInputSourceWrapper::ComputeGeckoKeyCode(UInt32 aNativeKeyCode, UInt32 aKbType, + bool aCmdIsPressed) { + MOZ_LOG( + gLog, LogLevel::Info, + ("%p TISInputSourceWrapper::ComputeGeckoKeyCode, aNativeKeyCode=0x%X, " + "aKbType=0x%X, aCmdIsPressed=%s, IsOpenedIMEMode()=%s, " + "IsASCIICapable()=%s", + this, static_cast<unsigned int>(aNativeKeyCode), static_cast<unsigned int>(aKbType), + TrueOrFalse(aCmdIsPressed), TrueOrFalse(IsOpenedIMEMode()), TrueOrFalse(IsASCIICapable()))); + + switch (aNativeKeyCode) { + case kVK_Space: + return NS_VK_SPACE; + case kVK_Escape: + return NS_VK_ESCAPE; + + // modifiers + case kVK_RightCommand: + case kVK_Command: + return NS_VK_META; + case kVK_RightShift: + case kVK_Shift: + return NS_VK_SHIFT; + case kVK_CapsLock: + return NS_VK_CAPS_LOCK; + case kVK_RightControl: + case kVK_Control: + return NS_VK_CONTROL; + case kVK_RightOption: + case kVK_Option: + return NS_VK_ALT; + + case kVK_ANSI_KeypadClear: + return NS_VK_CLEAR; + + // function keys + case kVK_F1: + return NS_VK_F1; + case kVK_F2: + return NS_VK_F2; + case kVK_F3: + return NS_VK_F3; + case kVK_F4: + return NS_VK_F4; + case kVK_F5: + return NS_VK_F5; + case kVK_F6: + return NS_VK_F6; + case kVK_F7: + return NS_VK_F7; + case kVK_F8: + return NS_VK_F8; + case kVK_F9: + return NS_VK_F9; + case kVK_F10: + return NS_VK_F10; + case kVK_F11: + return NS_VK_F11; + case kVK_F12: + return NS_VK_F12; + // case kVK_F13: return NS_VK_F13; // clash with the 3 below + // case kVK_F14: return NS_VK_F14; + // case kVK_F15: return NS_VK_F15; + case kVK_F16: + return NS_VK_F16; + case kVK_F17: + return NS_VK_F17; + case kVK_F18: + return NS_VK_F18; + case kVK_F19: + return NS_VK_F19; + + case kVK_PC_Pause: + return NS_VK_PAUSE; + case kVK_PC_ScrollLock: + return NS_VK_SCROLL_LOCK; + case kVK_PC_PrintScreen: + return NS_VK_PRINTSCREEN; + + // keypad + case kVK_ANSI_Keypad0: + return NS_VK_NUMPAD0; + case kVK_ANSI_Keypad1: + return NS_VK_NUMPAD1; + case kVK_ANSI_Keypad2: + return NS_VK_NUMPAD2; + case kVK_ANSI_Keypad3: + return NS_VK_NUMPAD3; + case kVK_ANSI_Keypad4: + return NS_VK_NUMPAD4; + case kVK_ANSI_Keypad5: + return NS_VK_NUMPAD5; + case kVK_ANSI_Keypad6: + return NS_VK_NUMPAD6; + case kVK_ANSI_Keypad7: + return NS_VK_NUMPAD7; + case kVK_ANSI_Keypad8: + return NS_VK_NUMPAD8; + case kVK_ANSI_Keypad9: + return NS_VK_NUMPAD9; + + case kVK_ANSI_KeypadMultiply: + return NS_VK_MULTIPLY; + case kVK_ANSI_KeypadPlus: + return NS_VK_ADD; + case kVK_ANSI_KeypadMinus: + return NS_VK_SUBTRACT; + case kVK_ANSI_KeypadDecimal: + return NS_VK_DECIMAL; + case kVK_ANSI_KeypadDivide: + return NS_VK_DIVIDE; + + case kVK_JIS_KeypadComma: + return NS_VK_SEPARATOR; + + // IME keys + case kVK_JIS_Eisu: + return NS_VK_EISU; + case kVK_JIS_Kana: + return NS_VK_KANA; + + // these may clash with forward delete and help + case kVK_PC_Insert: + return NS_VK_INSERT; + case kVK_PC_Delete: + return NS_VK_DELETE; + + case kVK_PC_Backspace: + return NS_VK_BACK; + case kVK_Tab: + return NS_VK_TAB; + + case kVK_Home: + return NS_VK_HOME; + case kVK_End: + return NS_VK_END; + + case kVK_PageUp: + return NS_VK_PAGE_UP; + case kVK_PageDown: + return NS_VK_PAGE_DOWN; + + case kVK_LeftArrow: + return NS_VK_LEFT; + case kVK_RightArrow: + return NS_VK_RIGHT; + case kVK_UpArrow: + return NS_VK_UP; + case kVK_DownArrow: + return NS_VK_DOWN; + + case kVK_PC_ContextMenu: + return NS_VK_CONTEXT_MENU; + + case kVK_ANSI_1: + return NS_VK_1; + case kVK_ANSI_2: + return NS_VK_2; + case kVK_ANSI_3: + return NS_VK_3; + case kVK_ANSI_4: + return NS_VK_4; + case kVK_ANSI_5: + return NS_VK_5; + case kVK_ANSI_6: + return NS_VK_6; + case kVK_ANSI_7: + return NS_VK_7; + case kVK_ANSI_8: + return NS_VK_8; + case kVK_ANSI_9: + return NS_VK_9; + case kVK_ANSI_0: + return NS_VK_0; + + case kVK_ANSI_KeypadEnter: + case kVK_Return: + case kVK_Powerbook_KeypadEnter: + return NS_VK_RETURN; + } + + // If Cmd key is pressed, that causes switching keyboard layout temporarily. + // E.g., Dvorak-QWERTY. Therefore, if Cmd key is pressed, we should honor it. + UInt32 modifiers = aCmdIsPressed ? cmdKey : 0; + + uint32_t charCode = TranslateToChar(aNativeKeyCode, modifiers, aKbType); + + // Special case for Mac. Mac inputs Yen sign (U+00A5) directly instead of + // Back slash (U+005C). We should return NS_VK_BACK_SLASH for compatibility + // with other platforms. + // XXX How about Won sign (U+20A9) which has same problem as Yen sign? + if (charCode == 0x00A5) { + return NS_VK_BACK_SLASH; + } + + uint32_t keyCode = WidgetUtils::ComputeKeyCodeFromChar(charCode); + if (keyCode) { + return keyCode; + } + + // If the unshifed char isn't an ASCII character, use shifted char. + charCode = TranslateToChar(aNativeKeyCode, modifiers | shiftKey, aKbType); + keyCode = WidgetUtils::ComputeKeyCodeFromChar(charCode); + if (keyCode) { + return keyCode; + } + + if (!IsASCIICapable()) { + // Retry with ASCII capable keyboard layout. + TISInputSourceWrapper currentKeyboardLayout; + currentKeyboardLayout.InitByCurrentASCIICapableKeyboardLayout(); + NS_ENSURE_TRUE(mInputSource != currentKeyboardLayout.mInputSource, 0); + keyCode = currentKeyboardLayout.ComputeGeckoKeyCode(aNativeKeyCode, aKbType, aCmdIsPressed); + // We've returned 0 for long time if keyCode isn't for an alphabet keys or + // a numeric key even in alternative ASCII capable keyboard layout because + // we decided that we should avoid setting same keyCode value to 2 or + // more keys since active keyboard layout may have a key to input the + // punctuation with different key. However, setting keyCode to 0 makes + // some web applications which are aware of neither KeyboardEvent.key nor + // KeyboardEvent.code not work with Firefox when user selects non-ASCII + // capable keyboard layout such as Russian and Thai. So, if alternative + // ASCII capable keyboard layout has keyCode value for the key, we should + // use it. In other words, this behavior does that non-ASCII capable + // keyboard layout overrides some keys' keyCode value only if the key + // produces ASCII character by itself or with Shift key. + if (keyCode) { + return keyCode; + } + } + + // Otherwise, let's decide keyCode value from the native virtual keycode + // value on major keyboard layout. + CodeNameIndex code = ComputeGeckoCodeNameIndex(aNativeKeyCode, aKbType); + return WidgetKeyboardEvent::GetFallbackKeyCodeOfPunctuationKey(code); +} + +// static +KeyNameIndex TISInputSourceWrapper::ComputeGeckoKeyNameIndex(UInt32 aNativeKeyCode) { + // NOTE: + // When unsupported keys like Convert, Nonconvert of Japanese keyboard is + // pressed: + // on 10.6.x, 'A' key event is fired (and also actually 'a' is inserted). + // on 10.7.x, Nothing happens. + // on 10.8.x, Nothing happens. + // on 10.9.x, FlagsChanged event is fired with keyCode 0xFF. + switch (aNativeKeyCode) { +#define NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, aKeyNameIndex) \ + case aNativeKey: \ + return aKeyNameIndex; + +#include "NativeKeyToDOMKeyName.h" + +#undef NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX + + default: + return KEY_NAME_INDEX_Unidentified; + } +} + +// static +CodeNameIndex TISInputSourceWrapper::ComputeGeckoCodeNameIndex(UInt32 aNativeKeyCode, + UInt32 aKbType) { + // macOS swaps native key code between Backquote key and IntlBackslash key + // only when the keyboard type is ISO. Let's treat the key code after + // swapping them here because Chromium does so only when computing .code + // value. + if (::KBGetLayoutType(aKbType) == kKeyboardISO) { + if (aNativeKeyCode == kVK_ISO_Section) { + aNativeKeyCode = kVK_ANSI_Grave; + } else if (aNativeKeyCode == kVK_ANSI_Grave) { + aNativeKeyCode = kVK_ISO_Section; + } + } + + switch (aNativeKeyCode) { +#define NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX(aNativeKey, aCodeNameIndex) \ + case aNativeKey: \ + return aCodeNameIndex; + +#include "NativeKeyToDOMCodeName.h" + +#undef NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX + + default: + return CODE_NAME_INDEX_UNKNOWN; + } +} + +#pragma mark - + +/****************************************************************************** + * + * TextInputHandler implementation (static methods) + * + ******************************************************************************/ + +NSUInteger TextInputHandler::sLastModifierState = 0; + +// static +CFArrayRef TextInputHandler::CreateAllKeyboardLayoutList() { + const void* keys[] = {kTISPropertyInputSourceType}; + const void* values[] = {kTISTypeKeyboardLayout}; + CFDictionaryRef filter = ::CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1, NULL, NULL); + NS_ASSERTION(filter, "failed to create the filter"); + CFArrayRef list = ::TISCreateInputSourceList(filter, true); + ::CFRelease(filter); + return list; +} + +// static +void TextInputHandler::DebugPrintAllKeyboardLayouts() { + if (MOZ_LOG_TEST(gLog, LogLevel::Info)) { + CFArrayRef list = CreateAllKeyboardLayoutList(); + MOZ_LOG(gLog, LogLevel::Info, ("Keyboard layout configuration:")); + CFIndex idx = ::CFArrayGetCount(list); + TISInputSourceWrapper tis; + for (CFIndex i = 0; i < idx; ++i) { + TISInputSourceRef inputSource = + static_cast<TISInputSourceRef>(const_cast<void*>(::CFArrayGetValueAtIndex(list, i))); + tis.InitByTISInputSourceRef(inputSource); + nsAutoString name, isid; + tis.GetLocalizedName(name); + tis.GetInputSourceID(isid); + MOZ_LOG( + gLog, LogLevel::Info, + (" %s\t<%s>%s%s\n", NS_ConvertUTF16toUTF8(name).get(), NS_ConvertUTF16toUTF8(isid).get(), + tis.IsASCIICapable() ? "" : "\t(Isn't ASCII capable)", + tis.IsKeyboardLayout() && tis.GetUCKeyboardLayout() ? "" : "\t(uchr is NOT AVAILABLE)")); + } + ::CFRelease(list); + } +} + +#pragma mark - + +/****************************************************************************** + * + * TextInputHandler implementation + * + ******************************************************************************/ + +TextInputHandler::TextInputHandler(nsChildView* aWidget, NSView<mozView>* aNativeView) + : IMEInputHandler(aWidget, aNativeView) { + EnsureToLogAllKeyboardLayoutsAndIMEs(); + [mView installTextInputHandler:this]; +} + +TextInputHandler::~TextInputHandler() { [mView uninstallTextInputHandler]; } + +bool TextInputHandler::HandleKeyDownEvent(NSEvent* aNativeEvent, uint32_t aUniqueId) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + if (Destroyed()) { + MOZ_LOG(gLog, 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(gLog, LogLevel::Info, ("")); + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleKeyDownEvent, aNativeEvent=%p, " + "type=%s, keyCode=%u (0x%X), modifierFlags=0x%lX, characters=\"%s\", " + "charactersIgnoringModifiers=\"%s\"", + this, aNativeEvent, GetNativeKeyEventType(aNativeEvent), [aNativeEvent keyCode], + [aNativeEvent keyCode], static_cast<unsigned long>([aNativeEvent modifierFlags]), + GetCharacters([aNativeEvent characters]), + GetCharacters([aNativeEvent charactersIgnoringModifiers]))); + + // Except when Command key is pressed, we should hide mouse cursor until + // next mousemove. Handling here means that: + // - Don't hide mouse cursor at pressing modifier key + // - Hide mouse cursor even if the key event will be handled by IME (i.e., + // even without dispatching eKeyPress events) + // - Hide mouse cursor even when a plugin has focus + if (!([aNativeEvent modifierFlags] & NSEventModifierFlagCommand)) { + [NSCursor setHiddenUntilMouseMoves:YES]; + } + + RefPtr<nsChildView> widget(mWidget); + + KeyEventState* currentKeyEvent = PushKeyEvent(aNativeEvent, aUniqueId); + AutoKeyEventStateCleaner remover(this); + + RefPtr<TextInputHandler> kungFuDeathGrip(this); + + // When we're already in a composition, we need always to mark the eKeyDown + // event as "processed by IME". So, let's dispatch eKeyDown event here in + // such case. + if (IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(true)) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::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(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleKeyDownEvent, calling interpretKeyEvents", this)); + [mView interpretKeyEvents:[NSArray arrayWithObject:aNativeEvent]]; + interpretKeyEventsCalled = true; + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleKeyDownEvent, called interpretKeyEvents", this)); + } + + if (Destroyed()) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleKeyDownEvent, widget was destroyed", this)); + return currentKeyEvent->IsDefaultPrevented(); + } + + MOZ_LOG(gLog, 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(gLog, 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(gLog, 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(gLog, LogLevel::Error, + ("%p IMEInputHandler::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(gLog, 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(gLog, 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(gLog, 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(gLog, LogLevel::Info, ("")); + return currentKeyEvent->IsDefaultPrevented(); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(false); +} + +void TextInputHandler::HandleKeyUpEvent(NSEvent* aNativeEvent) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + MOZ_LOG( + gLog, LogLevel::Info, + ("%p TextInputHandler::HandleKeyUpEvent, aNativeEvent=%p, " + "type=%s, keyCode=%u (0x%X), modifierFlags=0x%lX, characters=\"%s\", " + "charactersIgnoringModifiers=\"%s\", " + "IsIMEComposing()=%s", + this, aNativeEvent, GetNativeKeyEventType(aNativeEvent), [aNativeEvent keyCode], + [aNativeEvent keyCode], static_cast<unsigned long>([aNativeEvent modifierFlags]), + GetCharacters([aNativeEvent characters]), + GetCharacters([aNativeEvent charactersIgnoringModifiers]), TrueOrFalse(IsIMEComposing()))); + + if (Destroyed()) { + MOZ_LOG(gLog, 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(gLog, LogLevel::Error, + ("%p IMEInputHandler::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_ABORT_BLOCK; +} + +void TextInputHandler::HandleFlagsChanged(NSEvent* aNativeEvent) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (Destroyed()) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleFlagsChanged, " + "widget has been already destroyed", + this)); + return; + } + + RefPtr<nsChildView> kungFuDeathGrip(mWidget); + mozilla::Unused << kungFuDeathGrip; // Not referenced within this function + + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleFlagsChanged, aNativeEvent=%p, " + "type=%s, keyCode=%s (0x%X), modifierFlags=0x%08lX, " + "sLastModifierState=0x%08lX, IsIMEComposing()=%s", + this, aNativeEvent, GetNativeKeyEventType(aNativeEvent), + GetKeyNameForNativeKeyCode([aNativeEvent keyCode]), [aNativeEvent keyCode], + static_cast<unsigned long>([aNativeEvent modifierFlags]), + static_cast<unsigned long>(sLastModifierState), TrueOrFalse(IsIMEComposing()))); + + MOZ_ASSERT([aNativeEvent type] == NSEventTypeFlagsChanged); + + NSUInteger diff = [aNativeEvent modifierFlags] ^ sLastModifierState; + // Device dependent flags for left-control key, both shift keys, both command + // keys and both option keys have been defined in Next's SDK. But we + // shouldn't use it directly as far as possible since Cocoa SDK doesn't + // define them. Fortunately, we need them only when we dispatch keyup + // events. So, we can usually know the actual relation between keyCode and + // device dependent flags. However, we need to remove following flags first + // since the differences don't indicate modifier key state. + // NX_STYLUSPROXIMITYMASK: Probably used for pen like device. + // kCGEventFlagMaskNonCoalesced (= NX_NONCOALSESCEDMASK): See the document for + // Quartz Event Services. + diff &= ~(NX_STYLUSPROXIMITYMASK | kCGEventFlagMaskNonCoalesced); + + switch ([aNativeEvent keyCode]) { + // CapsLock state and other modifier states are different: + // CapsLock state does not revert when the CapsLock key goes up, as the + // modifier state does for other modifier keys on key up. + case kVK_CapsLock: { + // Fire key down event for caps lock. + DispatchKeyEventForFlagsChanged(aNativeEvent, true); + // XXX should we fire keyup event too? The keyup event for CapsLock key + // is never dispatched on Gecko. + // XXX WebKit dispatches keydown event when CapsLock is locked, otherwise, + // keyup event. If we do so, we cannot keep the consistency with other + // platform's behavior... + break; + } + + // If the event is caused by pressing or releasing a modifier key, just + // dispatch the key's event. + case kVK_Shift: + case kVK_RightShift: + case kVK_Command: + case kVK_RightCommand: + case kVK_Control: + case kVK_RightControl: + case kVK_Option: + case kVK_RightOption: + case kVK_Help: { + // We assume that at most one modifier is changed per event if the event + // is caused by pressing or releasing a modifier key. + bool isKeyDown = ([aNativeEvent modifierFlags] & diff) != 0; + DispatchKeyEventForFlagsChanged(aNativeEvent, isKeyDown); + // XXX Some applications might send the event with incorrect device- + // dependent flags. + if (isKeyDown && ((diff & ~NSEventModifierFlagDeviceIndependentFlagsMask) != 0)) { + unsigned short keyCode = [aNativeEvent keyCode]; + const ModifierKey* modifierKey = GetModifierKeyForDeviceDependentFlags(diff); + if (modifierKey && modifierKey->keyCode != keyCode) { + // Although, we're not sure the actual cause of this case, the stored + // modifier information and the latest key event information may be + // mismatched. Then, let's reset the stored information. + // NOTE: If this happens, it may fail to handle NSEventTypeFlagsChanged event + // in the default case (below). However, it's the rare case handler + // and this case occurs rarely. So, we can ignore the edge case bug. + NS_WARNING("Resetting stored modifier key information"); + mModifierKeys.Clear(); + modifierKey = nullptr; + } + if (!modifierKey) { + mModifierKeys.AppendElement(ModifierKey(diff, keyCode)); + } + } + break; + } + + // Currently we don't support Fn key since other browsers don't dispatch + // events for it and we don't have keyCode for this key. + // It should be supported when we implement .key and .char. + case kVK_Function: + break; + + // If the event is caused by something else than pressing or releasing a + // single modifier key (for example by the app having been deactivated + // using command-tab), use the modifiers themselves to determine which + // key's event to dispatch, and whether it's a keyup or keydown event. + // In all cases we assume one or more modifiers are being deactivated + // (never activated) -- otherwise we'd have received one or more events + // corresponding to a single modifier key being pressed. + default: { + NSUInteger modifiers = sLastModifierState; + AutoTArray<unsigned short, 10> dispatchedKeyCodes; + for (int32_t bit = 0; bit < 32; ++bit) { + NSUInteger flag = 1 << bit; + if (!(diff & flag)) { + continue; + } + + // Given correct information from the application, a flag change here + // will normally be a deactivation (except for some lockable modifiers + // such as CapsLock). But some applications (like VNC) can send an + // activating event with a zero keyCode. So we need to check for that + // here. + bool dispatchKeyDown = ((flag & [aNativeEvent modifierFlags]) != 0); + + unsigned short keyCode = 0; + if (flag & NSEventModifierFlagDeviceIndependentFlagsMask) { + switch (flag) { + case NSEventModifierFlagCapsLock: + keyCode = kVK_CapsLock; + dispatchKeyDown = true; + break; + + case NSEventModifierFlagNumericPad: + // NSEventModifierFlagNumericPad is fired by VNC a lot. But not all of + // these events can really be Clear key events, so we just ignore + // them. + continue; + + case NSEventModifierFlagHelp: + keyCode = kVK_Help; + break; + + case NSEventModifierFlagFunction: + // An NSEventModifierFlagFunction change here will normally be a + // deactivation. But sometimes it will be an activation send (by + // VNC for example) with a zero keyCode. + continue; + + // These cases (NSEventModifierFlagShift, NSEventModifierFlagControl, + // NSEventModifierFlagOption and NSEventModifierFlagCommand) should be handled by the + // other branch of the if statement, below (which handles device dependent flags). + // However, some applications (like VNC) can send key events without + // any device dependent flags, so we handle them here instead. + case NSEventModifierFlagShift: + keyCode = (modifiers & 0x0004) ? kVK_RightShift : kVK_Shift; + break; + case NSEventModifierFlagControl: + keyCode = (modifiers & 0x2000) ? kVK_RightControl : kVK_Control; + break; + case NSEventModifierFlagOption: + keyCode = (modifiers & 0x0040) ? kVK_RightOption : kVK_Option; + break; + case NSEventModifierFlagCommand: + keyCode = (modifiers & 0x0010) ? kVK_RightCommand : kVK_Command; + break; + + default: + continue; + } + } else { + const ModifierKey* modifierKey = GetModifierKeyForDeviceDependentFlags(flag); + if (!modifierKey) { + // See the note above (in the other branch of the if statement) + // about the NSEventModifierFlagShift, NSEventModifierFlagControl, + // NSEventModifierFlagOption and NSEventModifierFlagCommand cases. + continue; + } + keyCode = modifierKey->keyCode; + } + + // Remove flags + modifiers &= ~flag; + switch (keyCode) { + case kVK_Shift: { + const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_RightShift); + if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSEventModifierFlagShift; + } + break; + } + case kVK_RightShift: { + const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_Shift); + if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSEventModifierFlagShift; + } + break; + } + case kVK_Command: { + const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_RightCommand); + if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSEventModifierFlagCommand; + } + break; + } + case kVK_RightCommand: { + const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_Command); + if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSEventModifierFlagCommand; + } + break; + } + case kVK_Control: { + const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_RightControl); + if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSEventModifierFlagControl; + } + break; + } + case kVK_RightControl: { + const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_Control); + if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSEventModifierFlagControl; + } + break; + } + case kVK_Option: { + const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_RightOption); + if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSEventModifierFlagOption; + } + break; + } + case kVK_RightOption: { + const ModifierKey* modifierKey = GetModifierKeyForNativeKeyCode(kVK_Option); + if (!modifierKey || !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSEventModifierFlagOption; + } + break; + } + case kVK_Help: + modifiers &= ~NSEventModifierFlagHelp; + break; + default: + break; + } + + // Avoid dispatching same keydown/keyup events twice or more. + // We must be able to assume that there is no case to dispatch + // both keydown and keyup events with same key code value here. + if (dispatchedKeyCodes.Contains(keyCode)) { + continue; + } + dispatchedKeyCodes.AppendElement(keyCode); + + NSEvent* event = [NSEvent keyEventWithType:NSEventTypeFlagsChanged + location:[aNativeEvent locationInWindow] + modifierFlags:modifiers + timestamp:[aNativeEvent timestamp] + windowNumber:[aNativeEvent windowNumber] + context:[aNativeEvent context] + 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_ABORT_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_ABORT_BLOCK; + + if (Destroyed()) { + return; + } + + MOZ_LOG(gLog, 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(gLog, LogLevel::Error, + ("%p IMEInputHandler::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); + + // Attach a plugin event, in case keyEvent gets dispatched to a plugin. Only + // one field is needed -- the type. The other fields can be constructed as + // the need arises. But Gecko doesn't have anything equivalent to the + // NPCocoaEventFlagsChanged type, and this needs to be passed accurately to + // any plugin to which this event is sent. + NPCocoaEvent cocoaEvent; + nsCocoaUtils::InitNPCocoaEvent(&cocoaEvent); + cocoaEvent.type = NPCocoaEventFlagsChanged; + keyEvent.mPluginEvent.Copy(cocoaEvent); + + KeyEventState currentKeyEvent(aNativeEvent); + nsEventStatus status = nsEventStatus_eIgnore; + mDispatcher->DispatchKeyboardEvent(message, keyEvent, status, ¤tKeyEvent); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void TextInputHandler::InsertText(NSAttributedString* aAttrString, NSRange* aReplacementRange) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (Destroyed()) { + return; + } + + KeyEventState* currentKeyEvent = GetCurrentKeyEvent(); + + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::InsertText, aAttrString=\"%s\", " + "aReplacementRange=%p { location=%lu, length=%lu }, " + "IsIMEComposing()=%s, " + "keyevent=%p, keydownDispatched=%s, " + "keydownHandled=%s, keypressDispatched=%s, " + "causedOtherKeyEvents=%s, compositionDispatched=%s", + this, GetCharacters([aAttrString string]), aReplacementRange, + static_cast<unsigned long>(aReplacementRange ? aReplacementRange->location : 0), + static_cast<unsigned long>(aReplacementRange ? aReplacementRange->length : 0), + TrueOrFalse(IsIMEComposing()), currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr, + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownDispatched) : "N/A", + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A", + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressDispatched) : "N/A", + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents) : "N/A", + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCompositionDispatched) : "N/A")); + + InputContext context = mWidget->GetInputContext(); + bool isEditable = (context.mIMEState.mEnabled == IMEEnabled::Enabled || + context.mIMEState.mEnabled == IMEEnabled::Password); + NSRange selectedRange = SelectedRange(); + + nsAutoString str; + nsCocoaUtils::GetStringForNSString([aAttrString string], str); + + AutoInsertStringClearer clearer(currentKeyEvent); + if (currentKeyEvent) { + currentKeyEvent->mInsertString = &str; + } + + if (!IsIMEComposing() && str.IsEmpty()) { + // nothing to do if there is no content which can be removed. + if (!isEditable) { + return; + } + // If replacement range is specified, we need to remove the range. + // Otherwise, we need to remove the selected range if it's not collapsed. + if (aReplacementRange && aReplacementRange->location != NSNotFound) { + // nothing to do since the range is collapsed. + if (aReplacementRange->length == 0) { + return; + } + // If the replacement range is different from current selected range, + // select the range. + if (!NSEqualRanges(selectedRange, *aReplacementRange)) { + NS_ENSURE_TRUE_VOID(SetSelection(*aReplacementRange)); + } + selectedRange = SelectedRange(); + } + NS_ENSURE_TRUE_VOID(selectedRange.location != NSNotFound); + if (selectedRange.length == 0) { + return; // nothing to do + } + // If this is caused by a key input, the keypress event which will be + // dispatched later should cause the delete. Therefore, nothing to do here. + // Although, we're not sure if such case is actually possible. + if (!currentKeyEvent) { + return; + } + + // When current keydown event causes this empty text input, let's + // dispatch eKeyDown event before any other events. Note that if we're + // in a composition, we've already dispatched eKeyDown event from + // TextInputHandler::HandleKeyDownEvent(). + // XXX Should we mark this eKeyDown event as "processed by IME"? + RefPtr<TextInputHandler> kungFuDeathGrip(this); + if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(false)) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::InsertText, eKeyDown caused focus move or " + "something and canceling the composition", + this)); + return; + } + + // Delete the selected range. + WidgetContentCommandEvent deleteCommandEvent(true, eContentCommandDelete, mWidget); + DispatchEvent(deleteCommandEvent); + NS_ENSURE_TRUE_VOID(deleteCommandEvent.mSucceeded); + // Be aware! The widget might be destroyed here. + return; + } + + bool isReplacingSpecifiedRange = isEditable && aReplacementRange && + aReplacementRange->location != NSNotFound && + !NSEqualRanges(selectedRange, *aReplacementRange); + + // If this is not caused by pressing a key, there is a composition or + // replacing a range which is different from current selection, let's + // insert the text as committing a composition. + // If InsertText() is called two or more times, we should insert all + // text with composition events. + // XXX When InsertText() is called multiple times, Chromium dispatches + // only one composition event. So, we need to store InsertText() + // calls and flush later. + if (!currentKeyEvent || currentKeyEvent->mCompositionDispatched || IsIMEComposing() || + isReplacingSpecifiedRange) { + InsertTextAsCommittingComposition(aAttrString, aReplacementRange); + if (currentKeyEvent) { + currentKeyEvent->mCompositionDispatched = true; + } + return; + } + + // Don't let the same event be fired twice when hitting + // enter/return for Bug 420502. However, Korean IME (or some other + // simple IME) may work without marked text. For example, composing + // character may be inserted as committed text and it's modified with + // aReplacementRange. When a keydown starts new composition with + // committing previous character, InsertText() may be called twice, + // one is for committing previous character and then, inserting new + // composing character as committed character. In the latter case, + // |CanDispatchKeyPressEvent()| returns true but we need to dispatch + // keypress event for the new character. So, when IME tries to insert + // printable characters, we should ignore current key event state even + // after the keydown has already caused dispatching composition event. + // XXX Anyway, we should sort out around this at fixing bug 1338460. + if (currentKeyEvent && !currentKeyEvent->CanDispatchKeyPressEvent() && + (str.IsEmpty() || (str.Length() == 1 && !IsPrintableChar(str[0])))) { + return; + } + + // This is the normal path to input a character when you press a key. + // Let's dispatch eKeyDown event now. + RefPtr<TextInputHandler> kungFuDeathGrip(this); + if (!MaybeDispatchCurrentKeydownEvent(false)) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::InsertText, eKeyDown caused focus move or " + "something and canceling the composition", + this)); + return; + } + + // XXX Shouldn't we hold mDispatcher instead of mWidget? + RefPtr<nsChildView> widget(mWidget); + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gLog, LogLevel::Error, + ("%p IMEInputHandler::InsertText, " + "FAILED, due to BeginNativeInputTransaction() failure", + this)); + return; + } + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::InsertText, " + "maybe dispatches eKeyPress event without control, alt and meta modifiers", + this)); + + // Dispatch keypress event with char instead of compositionchange event + WidgetKeyboardEvent keypressEvent(true, eKeyPress, widget); + // XXX Why do we need to dispatch keypress event for not inputting any + // string? If it wants to delete the specified range, should we + // dispatch an eContentCommandDelete event instead? Because this + // must not be caused by a key operation, a part of IME's processing. + + // Don't set other modifiers from the current event, because here in + // -insertText: they've already been taken into account in creating + // the input string. + + if (currentKeyEvent) { + currentKeyEvent->InitKeyEvent(this, keypressEvent, false); + } else { + nsCocoaUtils::InitInputEvent(keypressEvent, static_cast<NSEvent*>(nullptr)); + keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_USE_STRING; + keypressEvent.mKeyValue = str; + // FYI: TextEventDispatcher will set mKeyCode to 0 for printable key's + // keypress events even if they don't cause inputting non-empty string. + } + + // TODO: + // If mCurrentKeyEvent.mKeyEvent is null, the text should be inputted as + // composition events. + nsEventStatus status = nsEventStatus_eIgnore; + bool keyPressDispatched = + mDispatcher->MaybeDispatchKeypressEvents(keypressEvent, status, currentKeyEvent); + bool keyPressHandled = (status == nsEventStatus_eConsumeNoDefault); + + // Note: mWidget might have become null here. Don't count on it from here on. + + if (currentKeyEvent) { + currentKeyEvent->mKeyPressHandled = keyPressHandled; + currentKeyEvent->mKeyPressDispatched = keyPressDispatched; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +bool TextInputHandler::HandleCommand(Command aCommand) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + if (Destroyed()) { + return false; + } + + KeyEventState* currentKeyEvent = GetCurrentKeyEvent(); + + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::HandleCommand, " + "aCommand=%s, IsIMEComposing()=%s, " + "keyevent=%p, keydownHandled=%s, keypressDispatched=%s, " + "causedOtherKeyEvents=%s, compositionDispatched=%s", + this, ToChar(aCommand), TrueOrFalse(IsIMEComposing()), + currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr, + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A", + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressDispatched) : "N/A", + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents) : "N/A", + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCompositionDispatched) : "N/A")); + + // The command shouldn't be handled, let's ignore it. + if (currentKeyEvent && !currentKeyEvent->CanHandleCommand()) { + return false; + } + + // When current keydown event causes this command, let's dispatch + // eKeyDown event before any other events. Note that if we're in a + // composition, we've already dispatched eKeyDown event from + // TextInputHandler::HandleKeyDownEvent(). + RefPtr<TextInputHandler> kungFuDeathGrip(this); + if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(false)) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::SetMarkedText, eKeyDown caused focus move or " + "something and canceling the composition", + this)); + return false; + } + + // If it's in composition, we cannot dispatch keypress event. + // Therefore, we should use different approach or give up to handle + // the command. + if (IsIMEComposing()) { + switch (aCommand) { + case Command::InsertLineBreak: + case Command::InsertParagraph: { + // Insert '\n' as committing composition. + // Otherwise, we need to dispatch keypress event because HTMLEditor + // doesn't treat "\n" in composition string as a line break unless + // the whitespace is treated as pre (see bug 1350541). In strictly + // speaking, we should dispatch keypress event as-is if it's handling + // NSEventTypeKeyDown event or should insert it with committing composition. + NSAttributedString* lineBreaker = [[NSAttributedString alloc] initWithString:@"\n"]; + InsertTextAsCommittingComposition(lineBreaker, nullptr); + if (currentKeyEvent) { + currentKeyEvent->mCompositionDispatched = true; + } + [lineBreaker release]; + return true; + } + case Command::DeleteCharBackward: + case Command::DeleteCharForward: + case Command::DeleteToBeginningOfLine: + case Command::DeleteWordBackward: + case Command::DeleteWordForward: + // Don't remove any contents during composition. + return false; + case Command::InsertTab: + case Command::InsertBacktab: + // Don't move focus during composition. + return false; + case Command::CharNext: + case Command::SelectCharNext: + case Command::WordNext: + case Command::SelectWordNext: + case Command::EndLine: + case Command::SelectEndLine: + case Command::CharPrevious: + case Command::SelectCharPrevious: + case Command::WordPrevious: + case Command::SelectWordPrevious: + case Command::BeginLine: + case Command::SelectBeginLine: + case Command::LinePrevious: + case Command::SelectLinePrevious: + case Command::MoveTop: + case Command::LineNext: + case Command::SelectLineNext: + case Command::MoveBottom: + case Command::SelectBottom: + case Command::SelectPageUp: + case Command::SelectPageDown: + case Command::ScrollBottom: + case Command::ScrollTop: + // Don't move selection during composition. + return false; + case Command::CancelOperation: + case Command::Complete: + // Don't handle Escape key by ourselves during composition. + return false; + case Command::ScrollPageUp: + case Command::ScrollPageDown: + // Allow to scroll. + break; + default: + break; + } + } + + RefPtr<nsChildView> widget(mWidget); + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gLog, LogLevel::Error, + ("%p, IMEInputHandler::HandleCommand, " + "FAILED, due to BeginNativeInputTransaction() failure", + this)); + return false; + } + + // TODO: If it's not appropriate keypress but user customized the OS + // settings to do the command with other key, we should just set + // command to the keypress event and it should be handled as + // the key press in editor. + + // If it's handling actual key event and hasn't cause any composition + // events nor other key events, we should expose actual modifier state. + // Otherwise, we should adjust Control, Option and Command state since + // editor may behave differently if some of them are active. + bool dispatchFakeKeyPress = !(currentKeyEvent && currentKeyEvent->IsProperKeyEvent(aCommand)); + + WidgetKeyboardEvent keydownEvent(true, eKeyDown, widget); + WidgetKeyboardEvent keypressEvent(true, eKeyPress, widget); + if (!dispatchFakeKeyPress) { + // If we're acutally handling a key press, we should dispatch + // the keypress event as-is. + currentKeyEvent->InitKeyEvent(this, keydownEvent, false); + currentKeyEvent->InitKeyEvent(this, keypressEvent, false); + } else { + // Otherwise, we should dispatch "fake" keypress event. + // However, for making it possible to compute edit commands, we need to + // set current native key event to the fake keyboard event even if it's + // not same as what we expect since the native keyboard event caused + // this command. + NSEvent* keyEvent = currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr; + keydownEvent.mNativeKeyEvent = keypressEvent.mNativeKeyEvent = keyEvent; + NS_WARNING_ASSERTION(keypressEvent.mNativeKeyEvent, + "Without native key event, NativeKeyBindings cannot compute aCommand"); + switch (aCommand) { + case Command::InsertLineBreak: + case Command::InsertParagraph: { + // Although, Shift+Enter and Enter are work differently in HTML + // editor, we should expose actual Shift state if it's caused by + // Enter key for compatibility with Chromium. Chromium breaks + // line in HTML editor with default pargraph separator when Enter + // is pressed, with <br> element when Shift+Enter. Safari breaks + // line in HTML editor with default paragraph separator when + // Enter, Shift+Enter or Option+Enter. So, we should not change + // Shift+Enter meaning when there was composition string or not. + nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent); + keypressEvent.mKeyCode = NS_VK_RETURN; + keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Enter; + keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META); + if (aCommand == Command::InsertLineBreak) { + // In default settings, Ctrl + Enter causes insertLineBreak command. + // So, let's make Ctrl state active of the keypress event. + keypressEvent.mModifiers |= MODIFIER_CONTROL; + } + break; + } + case Command::InsertTab: + case Command::InsertBacktab: + nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent); + keypressEvent.mKeyCode = NS_VK_TAB; + keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Tab; + keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META); + if (aCommand == Command::InsertBacktab) { + keypressEvent.mModifiers |= MODIFIER_SHIFT; + } + break; + case Command::DeleteCharBackward: + case Command::DeleteToBeginningOfLine: + case Command::DeleteWordBackward: { + nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent); + keypressEvent.mKeyCode = NS_VK_BACK; + keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Backspace; + keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META); + if (aCommand == Command::DeleteToBeginningOfLine) { + keypressEvent.mModifiers |= MODIFIER_META; + } else if (aCommand == Command::DeleteWordBackward) { + keypressEvent.mModifiers |= MODIFIER_ALT; + } + break; + } + case Command::DeleteCharForward: + case Command::DeleteWordForward: { + nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent); + keypressEvent.mKeyCode = NS_VK_DELETE; + keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Delete; + keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META); + if (aCommand == Command::DeleteWordForward) { + keypressEvent.mModifiers |= MODIFIER_ALT; + } + break; + } + case Command::CharNext: + case Command::SelectCharNext: + case Command::WordNext: + case Command::SelectWordNext: + case Command::EndLine: + case Command::SelectEndLine: { + nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent); + keypressEvent.mKeyCode = NS_VK_RIGHT; + keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_ArrowRight; + keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META); + if (aCommand == Command::SelectCharNext || aCommand == Command::SelectWordNext || + aCommand == Command::SelectEndLine) { + keypressEvent.mModifiers |= MODIFIER_SHIFT; + } + if (aCommand == Command::WordNext || aCommand == Command::SelectWordNext) { + keypressEvent.mModifiers |= MODIFIER_ALT; + } + if (aCommand == Command::EndLine || aCommand == Command::SelectEndLine) { + keypressEvent.mModifiers |= MODIFIER_META; + } + break; + } + case Command::CharPrevious: + case Command::SelectCharPrevious: + case Command::WordPrevious: + case Command::SelectWordPrevious: + case Command::BeginLine: + case Command::SelectBeginLine: { + nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent); + keypressEvent.mKeyCode = NS_VK_LEFT; + keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_ArrowLeft; + keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META); + if (aCommand == Command::SelectCharPrevious || aCommand == Command::SelectWordPrevious || + aCommand == Command::SelectBeginLine) { + keypressEvent.mModifiers |= MODIFIER_SHIFT; + } + if (aCommand == Command::WordPrevious || aCommand == Command::SelectWordPrevious) { + keypressEvent.mModifiers |= MODIFIER_ALT; + } + if (aCommand == Command::BeginLine || aCommand == Command::SelectBeginLine) { + keypressEvent.mModifiers |= MODIFIER_META; + } + break; + } + case Command::LinePrevious: + case Command::SelectLinePrevious: + case Command::MoveTop: + case Command::SelectTop: { + nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent); + keypressEvent.mKeyCode = NS_VK_UP; + keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_ArrowUp; + keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META); + if (aCommand == Command::SelectLinePrevious || aCommand == Command::SelectTop) { + keypressEvent.mModifiers |= MODIFIER_SHIFT; + } + if (aCommand == Command::MoveTop || aCommand == Command::SelectTop) { + keypressEvent.mModifiers |= MODIFIER_META; + } + break; + } + case Command::LineNext: + case Command::SelectLineNext: + case Command::MoveBottom: + case Command::SelectBottom: { + nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent); + keypressEvent.mKeyCode = NS_VK_DOWN; + keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_ArrowDown; + keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META); + if (aCommand == Command::SelectLineNext || aCommand == Command::SelectBottom) { + keypressEvent.mModifiers |= MODIFIER_SHIFT; + } + if (aCommand == Command::MoveBottom || aCommand == Command::SelectBottom) { + keypressEvent.mModifiers |= MODIFIER_META; + } + break; + } + case Command::ScrollPageUp: + case Command::SelectPageUp: { + nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent); + keypressEvent.mKeyCode = NS_VK_PAGE_UP; + keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_PageUp; + keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META); + if (aCommand == Command::SelectPageUp) { + keypressEvent.mModifiers |= MODIFIER_SHIFT; + } + break; + } + case Command::ScrollPageDown: + case Command::SelectPageDown: { + nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent); + keypressEvent.mKeyCode = NS_VK_PAGE_DOWN; + keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_PageDown; + keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META); + if (aCommand == Command::SelectPageDown) { + keypressEvent.mModifiers |= MODIFIER_SHIFT; + } + break; + } + case Command::ScrollBottom: + case Command::ScrollTop: { + nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent); + if (aCommand == Command::ScrollBottom) { + keypressEvent.mKeyCode = NS_VK_END; + keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_End; + } else { + keypressEvent.mKeyCode = NS_VK_HOME; + keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Home; + } + keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META); + break; + } + case Command::CancelOperation: + case Command::Complete: { + nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent); + keypressEvent.mKeyCode = NS_VK_ESCAPE; + keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Escape; + keypressEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META); + if (aCommand == Command::Complete) { + keypressEvent.mModifiers |= MODIFIER_ALT; + } + break; + } + default: + return false; + } + + nsCocoaUtils::InitInputEvent(keydownEvent, keyEvent); + keydownEvent.mKeyCode = keypressEvent.mKeyCode; + keydownEvent.mKeyNameIndex = keypressEvent.mKeyNameIndex; + keydownEvent.mModifiers = keypressEvent.mModifiers; + } + + // We've stopped dispatching "keypress" events of non-printable keys on + // the web. Therefore, we need to dispatch eKeyDown event here for web + // apps. This is non-standard behavior if we've already dispatched a + // "keydown" event. However, Chrome also dispatches such fake "keydown" + // (and "keypress") event for making same behavior as Safari. + nsEventStatus status = nsEventStatus_eIgnore; + if (mDispatcher->DispatchKeyboardEvent(eKeyDown, keydownEvent, status, nullptr)) { + bool keydownHandled = status == nsEventStatus_eConsumeNoDefault; + if (currentKeyEvent) { + currentKeyEvent->mKeyDownDispatched = true; + currentKeyEvent->mKeyDownHandled |= keydownHandled; + } + if (keydownHandled) { + // Don't dispatch eKeyPress event if preceding eKeyDown event is + // consumed for conforming to UI Events. + // XXX Perhaps, we should ignore previous eKeyDown event result + // even if we've already dispatched because it may notify web apps + // of different key information, e.g., it's handled by IME, but + // web apps want to handle only this key. + return true; + } + } + + bool keyPressDispatched = + mDispatcher->MaybeDispatchKeypressEvents(keypressEvent, status, currentKeyEvent); + bool keyPressHandled = (status == nsEventStatus_eConsumeNoDefault); + + // NOTE: mWidget might have become null here. + + if (keyPressDispatched) { + // Record the keypress event state only when it dispatched actual Enter + // keypress event because in other cases, the keypress event just a + // messenger. E.g., if it's caused by different key, keypress event for + // the actual key should be dispatched. + if (!dispatchFakeKeyPress && currentKeyEvent) { + currentKeyEvent->mKeyPressHandled = keyPressHandled; + currentKeyEvent->mKeyPressDispatched = keyPressDispatched; + } + return true; + } + + // If keypress event isn't dispatched as expected, we should fallback to + // using composition events. + if (aCommand == Command::InsertLineBreak || aCommand == Command::InsertParagraph) { + NSAttributedString* lineBreaker = [[NSAttributedString alloc] initWithString:@"\n"]; + InsertTextAsCommittingComposition(lineBreaker, nullptr); + if (currentKeyEvent) { + currentKeyEvent->mCompositionDispatched = true; + } + [lineBreaker release]; + return true; + } + + return false; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(false); +} + +bool TextInputHandler::DoCommandBySelector(const char* aSelector) { + RefPtr<nsChildView> widget(mWidget); + + KeyEventState* currentKeyEvent = GetCurrentKeyEvent(); + + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandler::DoCommandBySelector, aSelector=\"%s\", " + "Destroyed()=%s, keydownDispatched=%s, keydownHandled=%s, " + "keypressDispatched=%s, keypressHandled=%s, causedOtherKeyEvents=%s", + this, aSelector ? aSelector : "", TrueOrFalse(Destroyed()), + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownDispatched) : "N/A", + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A", + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressDispatched) : "N/A", + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressHandled) : "N/A", + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents) : "N/A")); + + // If the command isn't caused by key operation, the command should + // be handled in the super class of the caller. + if (!currentKeyEvent) { + return Destroyed(); + } + + // When current keydown event causes this command, let's dispatch + // eKeyDown event before any other events. Note that if we're in a + // composition, we've already dispatched eKeyDown event from + // TextInputHandler::HandleKeyDownEvent(). + RefPtr<TextInputHandler> kungFuDeathGrip(this); + if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(false)) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::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(gLog, LogLevel::Error, + ("%p IMEInputHandler::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(gLog, 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, "cancelOperatiorn:") && 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(gLog, 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(gLog, LogLevel::Info, + ("IMEInputHandler::OnCurrentTextInputSourceChange,\n" + " Current Input Source is changed to:\n" + " currentInputContext=%p\n" + " %s\n" + " type=%s %s\n" + " overridden keyboard layout=%s\n" + " used keyboard layout for translation=%s\n" + " primary language=%s\n" + " bundle ID=%s\n" + " current ASCII capable Input Source=%s\n" + " current Keyboard Layout=%s\n" + " current ASCII capable Keyboard Layout=%s", + [NSTextInputContext currentInputContext], GetCharacters(is0), GetCharacters(type0), + tis.IsASCIICapable() ? "- ASCII capable " : "", GetCharacters(is4), + GetCharacters(is5), GetCharacters(lang0), GetCharacters(bundleID0), + GetCharacters(is2), GetCharacters(is1), GetCharacters(is3))); + } + sLastTIS = newTIS; + } + + /** + * When the direction is changed, all the children are notified. + * No need to treat the initial case separately because it is covered + * by the general case (sCachedIsForRTLLangage is initially false) + */ + if (sCachedIsForRTLLangage != tis.IsForRTLLanguage()) { + WidgetUtils::SendBidiKeyboardInfoToContent(); + sCachedIsForRTLLangage = tis.IsForRTLLanguage(); + } +} + +// static +void IMEInputHandler::FlushPendingMethods(nsITimer* aTimer, void* aClosure) { + NS_ASSERTION(aClosure, "aClosure is null"); + static_cast<IMEInputHandler*>(aClosure)->ExecutePendingMethods(); +} + +// static +CFArrayRef IMEInputHandler::CreateAllIMEModeList() { + const void* keys[] = {kTISPropertyInputSourceType}; + const void* values[] = {kTISTypeKeyboardInputMode}; + CFDictionaryRef filter = ::CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1, NULL, NULL); + NS_ASSERTION(filter, "failed to create the filter"); + CFArrayRef list = ::TISCreateInputSourceList(filter, true); + ::CFRelease(filter); + return list; +} + +// static +void IMEInputHandler::DebugPrintAllIMEModes() { + if (MOZ_LOG_TEST(gLog, LogLevel::Info)) { + CFArrayRef list = CreateAllIMEModeList(); + MOZ_LOG(gLog, LogLevel::Info, ("IME mode configuration:")); + CFIndex idx = ::CFArrayGetCount(list); + TISInputSourceWrapper tis; + for (CFIndex i = 0; i < idx; ++i) { + TISInputSourceRef inputSource = + static_cast<TISInputSourceRef>(const_cast<void*>(::CFArrayGetValueAtIndex(list, i))); + tis.InitByTISInputSourceRef(inputSource); + nsAutoString name, isid, bundleID; + tis.GetLocalizedName(name); + tis.GetInputSourceID(isid); + tis.GetBundleID(bundleID); + MOZ_LOG(gLog, LogLevel::Info, + (" %s\t<%s>%s%s\n" + " bundled in <%s>\n", + NS_ConvertUTF16toUTF8(name).get(), NS_ConvertUTF16toUTF8(isid).get(), + tis.IsASCIICapable() ? "" : "\t(Isn't ASCII capable)", + tis.IsEnabled() ? "" : "\t(Isn't Enabled)", NS_ConvertUTF16toUTF8(bundleID).get())); + } + ::CFRelease(list); + } +} + +// static +TSMDocumentID IMEInputHandler::GetCurrentTSMDocumentID() { + // At least on Mac OS X 10.6.x and 10.7.x, ::TSMGetActiveDocument() has a bug. + // The result of ::TSMGetActiveDocument() isn't modified for new active text + // input context until [NSTextInputContext currentInputContext] is called. + // Therefore, we need to call it here. + [NSTextInputContext currentInputContext]; + return ::TSMGetActiveDocument(); +} + +#pragma mark - + +/****************************************************************************** + * + * IMEInputHandler implementation #1 + * The methods are releated to the pending methods. Some jobs should be + * run after the stack is finished, e.g, some methods cannot run the jobs + * during processing the focus event. And also some other jobs should be + * run at the next focus event is processed. + * The pending methods are recorded in mPendingMethods. They are executed + * by ExecutePendingMethods via FlushPendingMethods. + * + ******************************************************************************/ + +nsresult IMEInputHandler::NotifyIME(TextEventDispatcher* aTextEventDispatcher, + const IMENotification& aNotification) { + switch (aNotification.mMessage) { + case REQUEST_TO_COMMIT_COMPOSITION: + CommitIMEComposition(); + return NS_OK; + case REQUEST_TO_CANCEL_COMPOSITION: + CancelIMEComposition(); + return NS_OK; + case NOTIFY_IME_OF_FOCUS: + if (IsFocused()) { + nsIWidget* widget = aTextEventDispatcher->GetWidget(); + if (widget && widget->GetInputContext().IsPasswordEditor()) { + EnableSecureEventInput(); + } else { + EnsureSecureEventInputDisabled(); + } + } + OnFocusChangeInGecko(true); + return NS_OK; + case NOTIFY_IME_OF_BLUR: + OnFocusChangeInGecko(false); + return NS_OK; + case NOTIFY_IME_OF_SELECTION_CHANGE: + OnSelectionChange(aNotification); + return NS_OK; + case NOTIFY_IME_OF_POSITION_CHANGE: + OnLayoutChange(); + return NS_OK; + default: + return NS_ERROR_NOT_IMPLEMENTED; + } +} + +NS_IMETHODIMP_(IMENotificationRequests) +IMEInputHandler::GetIMENotificationRequests() { + // XXX Shouldn't we move floating window which shows composition string + // when plugin has focus and its parent is scrolled or the window is + // moved? + return IMENotificationRequests(); +} + +NS_IMETHODIMP_(void) +IMEInputHandler::OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) { + // XXX When input transaction is being stolen by add-on, what should we do? +} + +NS_IMETHODIMP_(void) +IMEInputHandler::WillDispatchKeyboardEvent(TextEventDispatcher* aTextEventDispatcher, + WidgetKeyboardEvent& aKeyboardEvent, + uint32_t aIndexOfKeypress, void* aData) { + // If the keyboard event is not caused by a native key event, we can do + // nothing here. + if (!aData) { + return; + } + + KeyEventState* currentKeyEvent = static_cast<KeyEventState*>(aData); + NSEvent* nativeEvent = currentKeyEvent->mKeyEvent; + nsAString* insertString = currentKeyEvent->mInsertString; + if (aKeyboardEvent.mMessage == eKeyPress && aIndexOfKeypress == 0 && + (!insertString || insertString->IsEmpty())) { + // Inform the child process that this is an event that we want a reply + // from. + // XXX This should be called only when the target is a remote process. + // However, it's difficult to check it under widget/. + // So, let's do this here for now, then, + // EventStateManager::PreHandleEvent() will reset the flags if + // the event target isn't in remote process. + aKeyboardEvent.MarkAsWaitingReplyFromRemoteProcess(); + } + if (KeyboardLayoutOverrideRef().mOverrideEnabled) { + TISInputSourceWrapper tis; + tis.InitByLayoutID(KeyboardLayoutOverrideRef().mKeyboardLayout, true); + tis.WillDispatchKeyboardEvent(nativeEvent, insertString, aIndexOfKeypress, aKeyboardEvent); + } else { + TISInputSourceWrapper::CurrentInputSource().WillDispatchKeyboardEvent( + nativeEvent, insertString, aIndexOfKeypress, aKeyboardEvent); + } + + // Remove basic modifiers from keypress event because if they are included + // but this causes inputting text, since TextEditor won't handle eKeyPress + // events whose ctrlKey, altKey or metaKey is true as text input. + // Note that this hack should be used only when an editor has focus because + // this is a hack for TextEditor and modifier key information may be + // important for current web app. + if (IsEditableContent() && insertString && aKeyboardEvent.mMessage == eKeyPress && + aKeyboardEvent.mCharCode) { + aKeyboardEvent.mModifiers &= ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META); + } +} + +void IMEInputHandler::NotifyIMEOfFocusChangeInGecko() { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::NotifyIMEOfFocusChangeInGecko, " + "Destroyed()=%s, IsFocused()=%s, inputContext=%p", + this, TrueOrFalse(Destroyed()), TrueOrFalse(IsFocused()), + mView ? [mView inputContext] : nullptr)); + + if (Destroyed()) { + return; + } + + if (!IsFocused()) { + // retry at next focus event + mPendingMethods |= kNotifyIMEOfFocusChangeInGecko; + return; + } + + MOZ_ASSERT(mView); + NSTextInputContext* inputContext = [mView inputContext]; + NS_ENSURE_TRUE_VOID(inputContext); + + // When an <input> element on a XUL <panel> element gets focus from an <input> + // element on the opener window of the <panel> element, the owner window + // still has native focus. Therefore, IMEs may store the opener window's + // level at this time because they don't know the actual focus is moved to + // different window. If IMEs try to get the newest window level after the + // focus change, we return the window level of the XUL <panel>'s widget. + // Therefore, let's emulate the native focus change. Then, IMEs can refresh + // the stored window level. + [inputContext deactivate]; + [inputContext activate]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void IMEInputHandler::SyncASCIICapableOnly() { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + MOZ_LOG(gLog, 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_ABORT_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_ABORT_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_ABORT_BLOCK; +} + +#pragma mark - + +/****************************************************************************** + * + * IMEInputHandler implementation (native event handlers) + * + ******************************************************************************/ + +TextRangeType IMEInputHandler::ConvertToTextRangeType(uint32_t aUnderlineStyle, + NSRange& aSelectedRange) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::ConvertToTextRangeType, " + "aUnderlineStyle=%u, aSelectedRange.length=%lu,", + this, aUnderlineStyle, static_cast<unsigned long>(aSelectedRange.length))); + + // We assume that aUnderlineStyle is NSUnderlineStyleSingle or + // NSUnderlineStyleThick. NSUnderlineStyleThick should indicate a selected + // clause. Otherwise, should indicate non-selected clause. + + if (aSelectedRange.length == 0) { + switch (aUnderlineStyle) { + case NSUnderlineStyleSingle: + return TextRangeType::eRawClause; + case NSUnderlineStyleThick: + return TextRangeType::eSelectedRawClause; + default: + NS_WARNING("Unexpected line style"); + return TextRangeType::eSelectedRawClause; + } + } + + switch (aUnderlineStyle) { + case NSUnderlineStyleSingle: + return TextRangeType::eConvertedClause; + case NSUnderlineStyleThick: + return TextRangeType::eSelectedClause; + default: + NS_WARNING("Unexpected line style"); + return TextRangeType::eSelectedClause; + } +} + +uint32_t IMEInputHandler::GetRangeCount(NSAttributedString* aAttrString) { + NS_OBJC_BEGIN_TRY_ABORT_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(gLog, LogLevel::Info, + ("%p IMEInputHandler::GetRangeCount, aAttrString=\"%s\", count=%u", this, + GetCharacters([aAttrString string]), count)); + + return count; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0); +} + +already_AddRefed<mozilla::TextRangeArray> IMEInputHandler::CreateTextRangeArray( + NSAttributedString* aAttrString, NSRange& aSelectedRange) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + RefPtr<mozilla::TextRangeArray> textRangeArray = new mozilla::TextRangeArray(); + + // Note that we shouldn't append ranges when composition string + // is empty because it may cause TextComposition confused. + if (![aAttrString length]) { + return textRangeArray.forget(); + } + + // Convert the Cocoa range into the TextRange Array used in Gecko. + // Iterate through the attributed string and map the underline attribute to + // Gecko IME textrange attributes. We may need to change the code here if + // we change the implementation of validAttributesForMarkedText. + NSRange limitRange = NSMakeRange(0, [aAttrString length]); + uint32_t rangeCount = GetRangeCount(aAttrString); + for (uint32_t i = 0; i < rangeCount && limitRange.length > 0; i++) { + NSRange effectiveRange; + id attributeValue = [aAttrString attribute:NSUnderlineStyleAttributeName + atIndex:limitRange.location + longestEffectiveRange:&effectiveRange + inRange:limitRange]; + + TextRange range; + range.mStartOffset = effectiveRange.location; + range.mEndOffset = NSMaxRange(effectiveRange); + range.mRangeType = ConvertToTextRangeType([attributeValue intValue], aSelectedRange); + textRangeArray->AppendElement(range); + + MOZ_LOG(gLog, 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(gLog, 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_ABORT_BLOCK_NSNULL; +} + +bool IMEInputHandler::DispatchCompositionStartEvent() { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::DispatchCompositionStartEvent, " + "mSelectedRange={ location=%lu, length=%lu }, Destroyed()=%s, " + "mView=%p, mWidget=%p, inputContext=%p, mIsIMEComposing=%s", + this, static_cast<unsigned long>(SelectedRange().location), + static_cast<unsigned long>(mSelectedRange.length), TrueOrFalse(Destroyed()), mView, + mWidget, mView ? [mView inputContext] : nullptr, TrueOrFalse(mIsIMEComposing))); + + RefPtr<IMEInputHandler> kungFuDeathGrip(this); + + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gLog, 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(gLog, LogLevel::Error, + ("%p IMEInputHandler::DispatchCompositionStartEvent, " + "FAILED, due to StartComposition() failure", + this)); + return false; + } + + if (Destroyed()) { + MOZ_LOG(gLog, 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_ABORT_BLOCK_RETURN; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::DispatchCompositionChangeEvent, " + "aText=\"%s\", aAttrString=\"%s\", " + "aSelectedRange={ location=%lu, length=%lu }, Destroyed()=%s, mView=%p, " + "mWidget=%p, inputContext=%p, mIsIMEComposing=%s", + this, NS_ConvertUTF16toUTF8(aText).get(), GetCharacters([aAttrString string]), + static_cast<unsigned long>(aSelectedRange.location), + static_cast<unsigned long>(aSelectedRange.length), TrueOrFalse(Destroyed()), mView, + mWidget, mView ? [mView inputContext] : nullptr, TrueOrFalse(mIsIMEComposing))); + + NS_ENSURE_TRUE(!Destroyed(), false); + + NS_ASSERTION(mIsIMEComposing, "We're not in composition"); + + RefPtr<IMEInputHandler> kungFuDeathGrip(this); + + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gLog, LogLevel::Error, + ("%p IMEInputHandler::DispatchCompositionChangeEvent, " + "FAILED, due to BeginNativeInputTransaction() failure", + this)); + return false; + } + + RefPtr<TextRangeArray> rangeArray = CreateTextRangeArray(aAttrString, aSelectedRange); + + rv = mDispatcher->SetPendingComposition(aText, rangeArray); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gLog, 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(gLog, LogLevel::Error, + ("%p IMEInputHandler::DispatchCompositionChangeEvent, " + "FAILED, due to FlushPendingComposition() failure", + this)); + return false; + } + + if (Destroyed()) { + MOZ_LOG(gLog, 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_ABORT_BLOCK_RETURN(false); +} + +bool IMEInputHandler::DispatchCompositionCommitEvent(const nsAString* aCommitString) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::DispatchCompositionCommitEvent, " + "aCommitString=0x%p (\"%s\"), Destroyed()=%s, mView=%p, mWidget=%p, " + "inputContext=%p, mIsIMEComposing=%s", + this, aCommitString, aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : "", + TrueOrFalse(Destroyed()), mView, mWidget, mView ? [mView inputContext] : nullptr, + TrueOrFalse(mIsIMEComposing))); + + NS_ASSERTION(mIsIMEComposing, "We're not in composition"); + + RefPtr<IMEInputHandler> kungFuDeathGrip(this); + + if (!Destroyed()) { + // IME may query selection immediately after this, however, in e10s mode, + // OnSelectionChange() will be called asynchronously. Until then, we + // should emulate expected selection range if the webapp does nothing. + mSelectedRange.location = mIMECompositionStart; + if (aCommitString) { + mSelectedRange.location += aCommitString->Length(); + } else if (mIMECompositionString) { + nsAutoString commitString; + nsCocoaUtils::GetStringForNSString(mIMECompositionString, commitString); + mSelectedRange.location += commitString.Length(); + } + mSelectedRange.length = 0; + + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gLog, 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(gLog, 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(gLog, LogLevel::Info, + ("%p IMEInputHandler::DispatchCompositionCommitEvent, " + "destroyed by compositioncommit event", + this)); + return false; + } + + return true; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(false); +} + +bool IMEInputHandler::MaybeDispatchCurrentKeydownEvent(bool aIsProcessedByIME) { + NS_OBJC_BEGIN_TRY_ABORT_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(gLog, LogLevel::Info, + ("%p IMEInputHandler::MaybeDispatchKeydownEvent, aIsProcessedByIME=%s " + "currentKeyEvent={ mKeyEvent(%p)={ type=%s, keyCode=%s (0x%X) } }, " + "aIsProcessedBy=%s, IsDeadKeyComposing()=%s", + this, TrueOrFalse(aIsProcessedByIME), nativeEvent, GetNativeKeyEventType(nativeEvent), + GetKeyNameForNativeKeyCode([nativeEvent keyCode]), [nativeEvent keyCode], + TrueOrFalse(IsIMEComposing()), TrueOrFalse(IsDeadKeyComposing()))); + + RefPtr<IMEInputHandler> kungFuDeathGrip(this); + RefPtr<TextEventDispatcher> dispatcher(mDispatcher); + nsresult rv = dispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gLog, LogLevel::Error, + ("%p IMEInputHandler::DispatchKeyEventForFlagsChanged, " + "FAILED, due to BeginNativeInputTransaction() failure", + this)); + return false; + } + + NSResponder* firstResponder = [[mView window] firstResponder]; + + // Mark currentKeyEvent as "dispatched eKeyDown event" and actually do it. + currentKeyEvent->mKeyDownDispatched = true; + + RefPtr<nsChildView> widget(mWidget); + + WidgetKeyboardEvent keydownEvent(true, eKeyDown, widget); + // Don't mark the eKeyDown event as "processed by IME" if the composition + // is started with dead key. + currentKeyEvent->InitKeyEvent(this, keydownEvent, aIsProcessedByIME && !IsDeadKeyComposing()); + + nsEventStatus status = nsEventStatus_eIgnore; + dispatcher->DispatchKeyboardEvent(eKeyDown, keydownEvent, status, currentKeyEvent); + currentKeyEvent->mKeyDownHandled = (status == nsEventStatus_eConsumeNoDefault); + + if (Destroyed()) { + MOZ_LOG(gLog, 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(gLog, LogLevel::Info, + ("%p IMEInputHandler::MaybeDispatchKeydownEvent, " + "view lost focus by keydown event", + this)); + CommitIMEComposition(); + return false; + } + + return true; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(false); +} + +void IMEInputHandler::InsertTextAsCommittingComposition(NSAttributedString* aAttrString, + NSRange* aReplacementRange) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::InsertTextAsCommittingComposition, " + "aAttrString=\"%s\", aReplacementRange=%p { location=%lu, length=%lu }, " + "Destroyed()=%s, IsIMEComposing()=%s, " + "mMarkedRange={ location=%lu, length=%lu }", + this, GetCharacters([aAttrString string]), aReplacementRange, + static_cast<unsigned long>(aReplacementRange ? aReplacementRange->location : 0), + static_cast<unsigned long>(aReplacementRange ? aReplacementRange->length : 0), + TrueOrFalse(Destroyed()), TrueOrFalse(IsIMEComposing()), + static_cast<unsigned long>(mMarkedRange.location), + static_cast<unsigned long>(mMarkedRange.length))); + + if (IgnoreIMECommit()) { + MOZ_CRASH("IMEInputHandler::InsertTextAsCommittingComposition() must not" + "be called while canceling the composition"); + } + + if (Destroyed()) { + return; + } + + // When current keydown event causes this text input, let's dispatch + // eKeyDown event before any other events. Note that if we're in a + // composition, we've already dispatched eKeyDown event from + // TextInputHandler::HandleKeyDownEvent(). + // XXX Should we mark the eKeyDown event as "processed by IME"? + // However, if the key causes two or more Unicode characters as + // UTF-16 string, this is used. So, perhaps, we need to improve + // HandleKeyDownEvent() before do that. + RefPtr<IMEInputHandler> kungFuDeathGrip(this); + if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(false)) { + MOZ_LOG(gLog, 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(gLog, LogLevel::Info, + ("%p IMEInputHandler::InsertTextAsCommittingComposition, " + "destroyed by commiting composition for setting replacement range", + this)); + return; + } + } + + nsString str; + nsCocoaUtils::GetStringForNSString([aAttrString string], str); + + if (!IsIMEComposing()) { + // If there is no selection and replacement range is specified, set the + // range as selection. + if (aReplacementRange && aReplacementRange->location != NSNotFound && + !NSEqualRanges(SelectedRange(), *aReplacementRange)) { + NS_ENSURE_TRUE_VOID(SetSelection(*aReplacementRange)); + } + + if (!DispatchCompositionStartEvent()) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::InsertTextAsCommittingComposition, " + "cannot continue handling composition after compositionstart", + this)); + return; + } + } + + if (!DispatchCompositionCommitEvent(&str)) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::InsertTextAsCommittingComposition, " + "destroyed by compositioncommit event", + this)); + return; + } + + mMarkedRange = NSMakeRange(NSNotFound, 0); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void IMEInputHandler::SetMarkedText(NSAttributedString* aAttrString, NSRange& aSelectedRange, + NSRange* aReplacementRange) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + KeyEventState* currentKeyEvent = GetCurrentKeyEvent(); + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::SetMarkedText, " + "aAttrString=\"%s\", aSelectedRange={ location=%lu, length=%lu }, " + "aReplacementRange=%p { location=%lu, length=%lu }, " + "Destroyed()=%s, IsIMEComposing()=%s, " + "mMarkedRange={ location=%lu, length=%lu }, keyevent=%p, " + "keydownDispatched=%s, keydownHandled=%s, " + "keypressDispatched=%s, causedOtherKeyEvents=%s, " + "compositionDispatched=%s", + this, GetCharacters([aAttrString string]), + static_cast<unsigned long>(aSelectedRange.location), + static_cast<unsigned long>(aSelectedRange.length), aReplacementRange, + static_cast<unsigned long>(aReplacementRange ? aReplacementRange->location : 0), + static_cast<unsigned long>(aReplacementRange ? aReplacementRange->length : 0), + TrueOrFalse(Destroyed()), TrueOrFalse(IsIMEComposing()), + static_cast<unsigned long>(mMarkedRange.location), + static_cast<unsigned long>(mMarkedRange.length), + currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr, + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownDispatched) : "N/A", + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A", + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressDispatched) : "N/A", + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents) : "N/A", + currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCompositionDispatched) : "N/A")); + + RefPtr<IMEInputHandler> kungFuDeathGrip(this); + + // If SetMarkedText() is called during handling a key press, that means that + // the key event caused this composition. So, keypress event shouldn't + // be dispatched later, let's mark the key event causing composition event. + if (currentKeyEvent) { + currentKeyEvent->mCompositionDispatched = true; + + // When current keydown event causes this text input, let's dispatch + // eKeyDown event before any other events. Note that if we're in a + // composition, we've already dispatched eKeyDown event from + // TextInputHandler::HandleKeyDownEvent(). On the other hand, if we're + // not in composition, the key event starts new composition. So, we + // need to mark the eKeyDown event as "processed by IME". + if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(true)) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::SetMarkedText, eKeyDown caused focus move or " + "something and canceling the composition", + this)); + return; + } + } + + if (Destroyed()) { + return; + } + + // First, commit current composition with the latest composition string if the + // replacement range is different from marked range. + if (IsIMEComposing() && aReplacementRange && aReplacementRange->location != NSNotFound && + !NSEqualRanges(MarkedRange(), *aReplacementRange)) { + AutoRestore<bool> ignoreIMECommit(mIgnoreIMECommit); + mIgnoreIMECommit = false; + if (!DispatchCompositionCommitEvent()) { + MOZ_LOG(gLog, 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(gLog, LogLevel::Info, + ("%p IMEInputHandler::SetMarkedText, cannot continue handling " + "composition after dispatching compositionstart", + this)); + return; + } + } + + if (!str.IsEmpty()) { + if (!DispatchCompositionChangeEvent(str, aAttrString, aSelectedRange)) { + MOZ_LOG(gLog, 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(gLog, LogLevel::Info, + ("%p IMEInputHandler::SetMarkedText, " + "destroyed by compositioncommit event", + this)); + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +NSAttributedString* IMEInputHandler::GetAttributedSubstringFromRange(NSRange& aRange, + NSRange* aActualRange) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::GetAttributedSubstringFromRange, " + "aRange={ location=%lu, length=%lu }, aActualRange=%p, Destroyed()=%s", + this, static_cast<unsigned long>(aRange.location), + static_cast<unsigned long>(aRange.length), aActualRange, TrueOrFalse(Destroyed()))); + + if (aActualRange) { + *aActualRange = NSMakeRange(NSNotFound, 0); + } + + if (Destroyed() || aRange.location == NSNotFound || aRange.length == 0) { + return nil; + } + + RefPtr<IMEInputHandler> kungFuDeathGrip(this); + + // If we're in composing, the queried range may be in the composition string. + // In such case, we should use mIMECompositionString since if the composition + // string is handled by a remote process, the content cache may be out of + // date. + // XXX Should we set composition string attributes? Although, Blink claims + // that some attributes of marked text are supported, but they return + // just marked string without any style. So, let's keep current behavior + // at least for now. + NSUInteger compositionLength = mIMECompositionString ? [mIMECompositionString length] : 0; + if (mIMECompositionStart != UINT32_MAX && mIMECompositionStart >= aRange.location && + mIMECompositionStart + compositionLength <= aRange.location + aRange.length) { + 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(gLog, LogLevel::Info)) { + nsAutoString str; + nsCocoaUtils::GetStringForNSString(nsstr, str); + MOZ_LOG(gLog, 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(gLog, 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_ABORT_BLOCK_NIL; +} + +bool IMEInputHandler::HasMarkedText() { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::HasMarkedText, " + "mMarkedRange={ location=%lu, length=%lu }", + this, static_cast<unsigned long>(mMarkedRange.location), + static_cast<unsigned long>(mMarkedRange.length))); + + return (mMarkedRange.location != NSNotFound) && (mMarkedRange.length != 0); +} + +NSRange IMEInputHandler::MarkedRange() { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::MarkedRange, " + "mMarkedRange={ location=%lu, length=%lu }", + this, static_cast<unsigned long>(mMarkedRange.location), + static_cast<unsigned long>(mMarkedRange.length))); + + if (!HasMarkedText()) { + return NSMakeRange(NSNotFound, 0); + } + return mMarkedRange; +} + +NSRange IMEInputHandler::SelectedRange() { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::SelectedRange, Destroyed()=%s, mSelectedRange={ " + "location=%lu, length=%lu }", + this, TrueOrFalse(Destroyed()), static_cast<unsigned long>(mSelectedRange.location), + static_cast<unsigned long>(mSelectedRange.length))); + + if (Destroyed()) { + return mSelectedRange; + } + + if (mSelectedRange.location != NSNotFound) { + MOZ_ASSERT(mIMEHasFocus); + return mSelectedRange; + } + + RefPtr<IMEInputHandler> kungFuDeathGrip(this); + + WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, mWidget); + DispatchEvent(querySelectedTextEvent); + + MOZ_LOG(gLog, 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_ABORT_BLOCK_RETURN(mSelectedRange); +} + +bool IMEInputHandler::DrawsVerticallyForCharacterAtIndex(uint32_t aCharIndex) { + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_BLOCK_RETURN(false); +} + +NSRect IMEInputHandler::FirstRectForCharacterRange(NSRange& aRange, NSRange* aActualRange) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::FirstRectForCharacterRange, Destroyed()=%s, " + "aRange={ location=%lu, length=%lu }, aActualRange=%p }", + this, TrueOrFalse(Destroyed()), static_cast<unsigned long>(aRange.location), + static_cast<unsigned long>(aRange.length), aActualRange)); + + // XXX this returns first character rect or caret rect, it is limitation of + // now. We need more work for returns first line rect. But current + // implementation is enough for IMEs. + + NSRect rect = NSMakeRect(0.0, 0.0, 0.0, 0.0); + NSRange actualRange = NSMakeRange(NSNotFound, 0); + if (aActualRange) { + *aActualRange = actualRange; + } + if (Destroyed() || aRange.location == NSNotFound) { + return rect; + } + + RefPtr<IMEInputHandler> kungFuDeathGrip(this); + + LayoutDeviceIntRect r; + bool useCaretRect = (aRange.length == 0); + if (!useCaretRect) { + WidgetQueryContentEvent queryTextRectEvent(true, eQueryTextRect, mWidget); + WidgetQueryContentEvent::Options options; + int64_t startOffset = aRange.location; + if (IsIMEComposing()) { + // The composition may be at different offset from the selection start + // offset at dispatching compositionstart because start of composition + // is fixed when composition string becomes non-empty in the editor. + // Therefore, we need to use query event which is relative to insertion + // point. + options.mRelativeToInsertionPoint = true; + startOffset -= mIMECompositionStart; + } + queryTextRectEvent.InitForQueryTextRect(startOffset, 1, options); + DispatchEvent(queryTextRectEvent); + if (queryTextRectEvent.Succeeded()) { + r = queryTextRectEvent.mReply->mRect; + actualRange = MakeNSRangeFrom(queryTextRectEvent.mReply->mOffsetAndData); + mWritingMode = queryTextRectEvent.mReply->WritingModeRef(); + mRangeForWritingMode = actualRange; + } else { + useCaretRect = true; + } + } + + if (useCaretRect) { + WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, mWidget); + WidgetQueryContentEvent::Options options; + int64_t startOffset = aRange.location; + if (IsIMEComposing()) { + // The composition may be at different offset from the selection start + // offset at dispatching compositionstart because start of composition + // is fixed when composition string becomes non-empty in the editor. + // Therefore, we need to use query event which is relative to insertion + // point. + options.mRelativeToInsertionPoint = true; + startOffset -= mIMECompositionStart; + } + queryCaretRectEvent.InitForQueryCaretRect(startOffset, options); + DispatchEvent(queryCaretRectEvent); + if (queryCaretRectEvent.Failed()) { + return rect; + } + r = queryCaretRectEvent.mReply->mRect; + r.width = 0; + actualRange.location = queryCaretRectEvent.mReply->StartOffset(); + actualRange.length = 0; + } + + nsIWidget* rootWidget = mWidget->GetTopLevelWidget(); + NSWindow* rootWindow = static_cast<NSWindow*>(rootWidget->GetNativeData(NS_NATIVE_WINDOW)); + NSView* rootView = static_cast<NSView*>(rootWidget->GetNativeData(NS_NATIVE_WIDGET)); + if (!rootWindow || !rootView) { + return rect; + } + rect = nsCocoaUtils::DevPixelsToCocoaPoints(r, mWidget->BackingScaleFactor()); + rect = [rootView convertRect:rect toView:nil]; + rect.origin = nsCocoaUtils::ConvertPointToScreen(rootWindow, rect.origin); + + if (aActualRange) { + *aActualRange = actualRange; + } + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::FirstRectForCharacterRange, " + "useCaretRect=%s rect={ x=%f, y=%f, width=%f, height=%f }, " + "actualRange={ location=%lu, length=%lu }", + this, TrueOrFalse(useCaretRect), rect.origin.x, rect.origin.y, rect.size.width, + rect.size.height, static_cast<unsigned long>(actualRange.location), + static_cast<unsigned long>(actualRange.length))); + + return rect; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NSMakeRect(0.0, 0.0, 0.0, 0.0)); +} + +NSUInteger IMEInputHandler::CharacterIndexForPoint(NSPoint& aPoint) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::CharacterIndexForPoint, aPoint={ x=%f, y=%f }", this, aPoint.x, + aPoint.y)); + + NSWindow* mainWindow = [NSApp mainWindow]; + if (!mWidget || !mainWindow) { + return NSNotFound; + } + + WidgetQueryContentEvent queryCharAtPointEvent(true, eQueryCharacterAtPoint, mWidget); + NSPoint ptInWindow = nsCocoaUtils::ConvertPointFromScreen(mainWindow, aPoint); + NSPoint ptInView = [mView convertPoint:ptInWindow fromView:nil]; + queryCharAtPointEvent.mRefPoint.x = + static_cast<int32_t>(ptInView.x) * mWidget->BackingScaleFactor(); + queryCharAtPointEvent.mRefPoint.y = + static_cast<int32_t>(ptInView.y) * mWidget->BackingScaleFactor(); + mWidget->DispatchWindowEvent(queryCharAtPointEvent); + if (queryCharAtPointEvent.Failed() || queryCharAtPointEvent.DidNotFindChar() || + queryCharAtPointEvent.mReply->StartOffset() >= static_cast<uint32_t>(NSNotFound)) { + return NSNotFound; + } + + return queryCharAtPointEvent.mReply->StartOffset(); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NSNotFound); +} + +extern "C" { +extern NSString* NSTextInputReplacementRangeAttributeName; +} + +NSArray* IMEInputHandler::GetValidAttributesForMarkedText() { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + MOZ_LOG(gLog, 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_ABORT_BLOCK_NIL; +} + +#pragma mark - + +/****************************************************************************** + * + * IMEInputHandler implementation #2 + * + ******************************************************************************/ + +IMEInputHandler::IMEInputHandler(nsChildView* aWidget, NSView<mozView>* aNativeView) + : TextInputHandlerBase(aWidget, aNativeView), + mPendingMethods(0), + mIMECompositionString(nullptr), + mIMECompositionStart(UINT32_MAX), + mRangeForWritingMode(), + mIsIMEComposing(false), + mIsDeadKeyComposing(false), + mIsIMEEnabled(true), + mIsASCIICapableOnly(false), + mIgnoreIMECommit(false), + mIMEHasFocus(false) { + InitStaticMembers(); + + mMarkedRange.location = NSNotFound; + mMarkedRange.length = 0; + mSelectedRange.location = NSNotFound; + mSelectedRange.length = 0; +} + +IMEInputHandler::~IMEInputHandler() { + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + if (sFocusedIMEHandler == this) { + sFocusedIMEHandler = nullptr; + } + if (mIMECompositionString) { + [mIMECompositionString release]; + mIMECompositionString = nullptr; + } +} + +void IMEInputHandler::OnFocusChangeInGecko(bool aFocus) { + MOZ_LOG(gLog, 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(gLog, 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_ABORT_BLOCK; + + MOZ_LOG( + gLog, LogLevel::Info, + ("%p IMEInputHandler::SendCommittedText, mView=%p, mWidget=%p, " + "inputContext=%p, mIsIMEComposing=%s", + this, mView, mWidget, mView ? [mView inputContext] : nullptr, TrueOrFalse(mIsIMEComposing))); + + NS_ENSURE_TRUE(mWidget, ); + // XXX We should send the string without mView. + if (!mView) { + return; + } + + NSAttributedString* attrStr = [[NSAttributedString alloc] initWithString:aString]; + if ([mView conformsToProtocol:@protocol(NSTextInputClient)]) { + NSObject<NSTextInputClient>* textInputClient = static_cast<NSObject<NSTextInputClient>*>(mView); + [textInputClient insertText:attrStr replacementRange:NSMakeRange(NSNotFound, 0)]; + } + + // Last resort. If we cannot retrieve NSTextInputProtocol from mView + // or blocking to call our InsertText(), we should call InsertText() + // directly to commit composition forcibly. + if (mIsIMEComposing) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::SendCommittedText, trying to insert text directly " + "due to IME not calling our InsertText()", + this)); + static_cast<TextInputHandler*>(this)->InsertText(attrStr); + MOZ_ASSERT(!mIsIMEComposing); + } + + [attrStr release]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void IMEInputHandler::KillIMEComposition() { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + MOZ_LOG(gLog, 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_ABORT_BLOCK; +} + +void IMEInputHandler::CommitIMEComposition() { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + MOZ_LOG(gLog, 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_ABORT_BLOCK; +} + +void IMEInputHandler::CancelIMEComposition() { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!IsIMEComposing()) return; + + MOZ_LOG(gLog, 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_ABORT_BLOCK; +} + +bool IMEInputHandler::IsFocused() { + NS_OBJC_BEGIN_TRY_ABORT_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_ABORT_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(gLog, LogLevel::Info, ("%p IMEInputHandler::OpenSystemPreferredLanguageIME", this)); + + CFArrayRef langList = ::CFLocaleCopyPreferredLanguages(); + if (!langList) { + MOZ_LOG(gLog, LogLevel::Info, + ("%p IMEInputHandler::OpenSystemPreferredLanguageIME, langList is NULL", this)); + return; + } + CFIndex count = ::CFArrayGetCount(langList); + for (CFIndex i = 0; i < count; i++) { + CFLocaleRef locale = ::CFLocaleCreate( + kCFAllocatorDefault, static_cast<CFStringRef>(::CFArrayGetValueAtIndex(langList, i))); + if (!locale) { + continue; + } + + bool changed = false; + CFStringRef lang = static_cast<CFStringRef>(::CFLocaleGetValue(locale, kCFLocaleLanguageCode)); + NS_ASSERTION(lang, "lang is null"); + if (lang) { + TISInputSourceWrapper tis; + tis.InitByLanguage(lang); + if (tis.IsOpenedIMEMode()) { + if (MOZ_LOG_TEST(gLog, LogLevel::Info)) { + CFStringRef foundTIS; + tis.GetInputSourceID(foundTIS); + MOZ_LOG(gLog, 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_LOG(gLog, LogLevel::Info, ("%p IMEInputHandler::OnSelectionChange", this)); + + if (aIMENotification.mSelectionChangeData.mOffset == UINT32_MAX) { + 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_ABORT_BLOCK; + + if (!IsFocused()) { + return; + } + NSTextInputContext* inputContext = [mView inputContext]; + [inputContext invalidateCharacterCoordinates]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +bool IMEInputHandler::OnHandleEvent(NSEvent* aEvent) { + if (!IsFocused()) { + return false; + } + + bool allowConsumeEvent = true; + if (nsCocoaFeatures::OnCatalinaOrLater() && !IsIMEComposing()) { + // Hack for bug of Korean IMEs on Catalina (10.15). + // If we are inactivated during composition, active Korean IME keeps + // consuming all mousedown events of any mouse buttons. So, we should + // allow Korean IMEs to handle mousedown events only when there is + // composition string. + // List of ID of Korean IME: + // * com.apple.inputmethod.Korean.2SetKorean + // * com.apple.inputmethod.Korean.3SetKorean + // * com.apple.inputmethod.Korean.390Sebulshik + // * com.apple.inputmethod.Korean.GongjinCheongRomaja + // * com.apple.inputmethod.Korean.HNCRomaja + TISInputSourceWrapper tis; + tis.InitByCurrentInputSource(); + nsAutoString inputSourceID; + tis.GetInputSourceID(inputSourceID); + allowConsumeEvent = !StringBeginsWith(inputSourceID, u"com.apple.inputmethod.Korean."_ns); + } + NSTextInputContext* inputContext = [mView inputContext]; + return [inputContext handleEvent:aEvent] && allowConsumeEvent; +} + +#pragma mark - + +/****************************************************************************** + * + * TextInputHandlerBase implementation + * + ******************************************************************************/ + +int32_t TextInputHandlerBase::sSecureEventInputCount = 0; + +NS_IMPL_ISUPPORTS(TextInputHandlerBase, TextEventDispatcherListener, nsISupportsWeakReference) + +TextInputHandlerBase::TextInputHandlerBase(nsChildView* aWidget, NSView<mozView>* aNativeView) + : mWidget(aWidget), mDispatcher(aWidget->GetTextEventDispatcher()) { + gHandlerInstanceCount++; + mView = [aNativeView retain]; +} + +TextInputHandlerBase::~TextInputHandlerBase() { + [mView release]; + if (--gHandlerInstanceCount == 0) { + TISInputSourceWrapper::Shutdown(); + } +} + +bool TextInputHandlerBase::OnDestroyWidget(nsChildView* aDestroyingWidget) { + MOZ_LOG(gLog, 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_ABORT_BLOCK_NSRESULT; + + static const uint32_t sModifierFlagMap[][2] = { + {nsIWidget::CAPS_LOCK, NSEventModifierFlagCapsLock}, + {nsIWidget::SHIFT_L, NSEventModifierFlagShift | 0x0002}, + {nsIWidget::SHIFT_R, NSEventModifierFlagShift | 0x0004}, + {nsIWidget::CTRL_L, NSEventModifierFlagControl | 0x0001}, + {nsIWidget::CTRL_R, NSEventModifierFlagControl | 0x2000}, + {nsIWidget::ALT_L, NSEventModifierFlagOption | 0x0020}, + {nsIWidget::ALT_R, NSEventModifierFlagOption | 0x0040}, + {nsIWidget::COMMAND_L, NSEventModifierFlagCommand | 0x0008}, + {nsIWidget::COMMAND_R, NSEventModifierFlagCommand | 0x0010}, + {nsIWidget::NUMERIC_KEY_PAD, NSEventModifierFlagNumericPad}, + {nsIWidget::HELP, NSEventModifierFlagHelp}, + {nsIWidget::FUNCTION, NSEventModifierFlagFunction}}; + + uint32_t modifierFlags = 0; + for (uint32_t i = 0; i < ArrayLength(sModifierFlagMap); ++i) { + if (aModifierFlags & sModifierFlagMap[i][0]) { + modifierFlags |= sModifierFlagMap[i][1]; + } + } + + 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:[NSGraphicsContext currentContext] + 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_ABORT_BLOCK_NSRESULT; +} + +NSInteger TextInputHandlerBase::GetWindowLevel() { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + MOZ_LOG( + gLog, LogLevel::Info, + ("%p TextInputHandlerBase::GetWindowLevel, Destryoed()=%s", this, TrueOrFalse(Destroyed()))); + + if (Destroyed()) { + return NSNormalWindowLevel; + } + + // When an <input> element on a XUL <panel> is focused, the actual focused view + // is the panel's parent view (mView). But the editor is displayed on the + // popped-up widget's view (editorView). We want the latter's window level. + NSView<mozView>* editorView = mWidget->GetEditorView(); + NS_ENSURE_TRUE(editorView, NSNormalWindowLevel); + NSInteger windowLevel = [[editorView window] level]; + + MOZ_LOG(gLog, LogLevel::Info, + ("%p TextInputHandlerBase::GetWindowLevel, windowLevel=%s (%lX)", this, + GetWindowLevelName(windowLevel), static_cast<unsigned long>(windowLevel))); + + return windowLevel; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NSNormalWindowLevel); +} + +NS_IMETHODIMP +TextInputHandlerBase::AttachNativeKeyEvent(WidgetKeyboardEvent& aKeyEvent) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + // 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(gLog, 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_ABORT_BLOCK_NSRESULT; +} + +bool TextInputHandlerBase::SetSelection(NSRange& aRange) { + MOZ_ASSERT(!Destroyed()); + + RefPtr<TextInputHandlerBase> kungFuDeathGrip(this); + WidgetSelectionEvent selectionEvent(true, eSetSelection, mWidget); + selectionEvent.mOffset = aRange.location; + selectionEvent.mLength = aRange.length; + selectionEvent.mReversed = false; + selectionEvent.mExpandToClusterBoundary = false; + DispatchEvent(selectionEvent); + NS_ENSURE_TRUE(selectionEvent.mSucceeded, false); + return !Destroyed(); +} + +/* static */ bool TextInputHandlerBase::IsPrintableChar(char16_t aChar) { + return (aChar >= 0x20 && aChar <= 0x7E) || aChar >= 0xA0; +} + +/* static */ bool TextInputHandlerBase::IsSpecialGeckoKey(UInt32 aNativeKeyCode) { + // this table is used to determine which keys are special and should not + // generate a charCode + switch (aNativeKeyCode) { + // modifiers - we don't get separate events for these yet + case kVK_Escape: + case kVK_Shift: + case kVK_RightShift: + case kVK_Command: + case kVK_RightCommand: + case kVK_CapsLock: + case kVK_Control: + case kVK_RightControl: + case kVK_Option: + case kVK_RightOption: + case kVK_ANSI_KeypadClear: + case kVK_Function: + + // function keys + case kVK_F1: + case kVK_F2: + case kVK_F3: + case kVK_F4: + case kVK_F5: + case kVK_F6: + case kVK_F7: + case kVK_F8: + case kVK_F9: + case kVK_F10: + case kVK_F11: + case kVK_F12: + case kVK_PC_Pause: + case kVK_PC_ScrollLock: + case kVK_PC_PrintScreen: + case kVK_F16: + case kVK_F17: + case kVK_F18: + case kVK_F19: + + case kVK_PC_Insert: + case kVK_PC_Delete: + case kVK_Tab: + case kVK_PC_Backspace: + case kVK_PC_ContextMenu: + + case kVK_JIS_Eisu: + case kVK_JIS_Kana: + + case kVK_Home: + case kVK_End: + case kVK_PageUp: + case kVK_PageDown: + case kVK_LeftArrow: + case kVK_RightArrow: + case kVK_UpArrow: + case kVK_DownArrow: + case kVK_Return: + case kVK_ANSI_KeypadEnter: + case kVK_Powerbook_KeypadEnter: + return true; + } + return false; +} + +/* static */ bool TextInputHandlerBase::IsNormalCharInputtingEvent(NSEvent* aNativeEvent) { + if ([aNativeEvent type] != NSEventTypeKeyDown && [aNativeEvent type] != NSEventTypeKeyUp) { + return false; + } + nsAutoString nativeChars; + nsCocoaUtils::GetStringForNSString([aNativeEvent characters], nativeChars); + + // this is not character inputting event, simply. + if (nativeChars.IsEmpty() || ([aNativeEvent modifierFlags] & NSEventModifierFlagCommand)) { + return false; + } + return !IsControlChar(nativeChars[0]); +} + +/* static */ bool TextInputHandlerBase::IsModifierKey(UInt32 aNativeKeyCode) { + switch (aNativeKeyCode) { + case kVK_CapsLock: + case kVK_RightCommand: + case kVK_Command: + case kVK_Shift: + case kVK_Option: + case kVK_Control: + case kVK_RightShift: + case kVK_RightOption: + case kVK_RightControl: + case kVK_Function: + return true; + } + return false; +} + +/* static */ void TextInputHandlerBase::EnableSecureEventInput() { + sSecureEventInputCount++; + ::EnableSecureEventInput(); +} + +/* static */ void TextInputHandlerBase::DisableSecureEventInput() { + if (!sSecureEventInputCount) { + return; + } + sSecureEventInputCount--; + ::DisableSecureEventInput(); +} + +/* static */ bool TextInputHandlerBase::IsSecureEventInputEnabled() { + NS_ASSERTION(!!sSecureEventInputCount == !!::IsSecureEventInputEnabled(), + "Some other process has enabled secure event input"); + 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_ABORT_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:[mKeyEvent context] + characters:unhandledNSString + charactersIgnoringModifiers:[mKeyEvent charactersIgnoringModifiers] + isARepeat:[mKeyEvent isARepeat] + keyCode:[mKeyEvent keyCode]]; + } + + aKeyEvent.mUniqueId = mUniqueId; + aHandler->InitKeyEvent(nativeEvent, aKeyEvent, aIsProcessedByIME, mInsertString); + + NS_OBJC_END_TRY_ABORT_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; + } +} |