diff -r 000000000000 -r 6474c204b198 widget/cocoa/TextInputHandler.mm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/widget/cocoa/TextInputHandler.mm Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,4588 @@ +/* -*- 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 "mozilla/ArrayUtils.h" + +#include "TextInputHandler.h" + +#ifdef MOZ_LOGGING +#define FORCE_PR_LOG /* Allow logging in the release build */ +#endif // MOZ_LOGGING +#include "prlog.h" + +#include "mozilla/MiscEvents.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/TextEvents.h" + +#include "nsChildView.h" +#include "nsObjCExceptions.h" +#include "nsBidiUtils.h" +#include "nsToolkit.h" +#include "nsCocoaUtils.h" +#include "WidgetUtils.h" +#include "nsPrintfCString.h" + +#ifdef __LP64__ +#include "ComplexTextInputPanel.h" +#include +#endif // __LP64__ + +#ifdef MOZ_LOGGING +#define FORCE_PR_LOG +#endif +#include "prlog.h" + +#ifndef __LP64__ +enum { + // Currently focused ChildView (while this TSM document is active). + // Transient (only set while TSMProcessRawKeyEvent() is processing a key + // event), and the ChildView will be retained and released around the call + // to TSMProcessRawKeyEvent() -- so it can be weak. + kFocusedChildViewTSMDocPropertyTag = 'GKFV', // type ChildView* [WEAK] +}; + +// Undocumented HIToolbox function used by WebKit to allow Carbon-based IME +// to work in a Cocoa-based browser (like Safari or Cocoa-widgets Firefox). +// (Recent WebKit versions actually use a thin wrapper around this function +// called WKSendKeyEventToTSM().) +// +// Calling TSMProcessRawKeyEvent() from ChildView's keyDown: and keyUp: +// methods (when the ChildView is a plugin view) bypasses Cocoa's IME +// infrastructure and (instead) causes Carbon TSM events to be sent on each +// NSKeyDown event. We install a Carbon event handler +// (PluginKeyEventsHandler()) to catch these events and pass them to Gecko +// (which in turn passes them to the plugin). +extern "C" long TSMProcessRawKeyEvent(EventRef carbonEvent); +#endif // __LP64__ + +using namespace mozilla; +using namespace mozilla::widget; + +#ifdef PR_LOGGING + +PRLogModuleInfo* gLog = nullptr; + +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 NSString* aString) +{ + nsAutoString str; + nsCocoaUtils::GetStringForNSString(aString, str); + if (str.IsEmpty()) { + return ""; + } + + nsAutoString escapedStr; + for (uint32_t i = 0; i < str.Length(); i++) { + char16_t ch = str[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 CFStringRef aString) +{ + const NSString* str = reinterpret_cast(aString); + return GetCharacters(str); +} + +static const char* +GetNativeKeyEventType(NSEvent* aNativeEvent) +{ + switch ([aNativeEvent type]) { + case NSKeyDown: return "NSKeyDown"; + case NSKeyUp: return "NSKeyUp"; + case NSFlagsChanged: return "NSFlagsChanged"; + default: return "not key event"; + } +} + +static const char* +GetGeckoKeyEventType(const WidgetEvent& aEvent) +{ + switch (aEvent.message) { + case NS_KEY_DOWN: return "NS_KEY_DOWN"; + case NS_KEY_UP: return "NS_KEY_UP"; + case NS_KEY_PRESS: return "NS_KEY_PRESS"; + default: return "not key event"; + } +} + +static const char* +GetRangeTypeName(uint32_t aRangeType) +{ + switch (aRangeType) { + case NS_TEXTRANGE_RAWINPUT: + return "NS_TEXTRANGE_RAWINPUT"; + case NS_TEXTRANGE_CONVERTEDTEXT: + return "NS_TEXTRANGE_CONVERTEDTEXT"; + case NS_TEXTRANGE_SELECTEDRAWTEXT: + return "NS_TEXTRANGE_SELECTEDRAWTEXT"; + case NS_TEXTRANGE_SELECTEDCONVERTEDTEXT: + return "NS_TEXTRANGE_SELECTEDCONVERTEDTEXT"; + case NS_TEXTRANGE_CARETPOSITION: + return "NS_TEXTRANGE_CARETPOSITION"; + default: + return "invalid range type"; + } +} + +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"; + } +} + +#endif // #ifdef PR_LOGGING + +static bool +IsControlChar(uint32_t aCharCode) +{ + return aCharCode < ' ' || aCharCode == 0x7F; +} + +static uint32_t gHandlerInstanceCount = 0; +static TISInputSourceWrapper gCurrentInputSource; + +static void +InitLogModule() +{ +#ifdef PR_LOGGING + // Clear() is always called when TISInputSourceWrappper is created. + if (!gLog) { + gLog = PR_NewLogModule("TextInputHandlerWidgets"); + TextInputHandler::DebugPrintAllKeyboardLayouts(); + IMEInputHandler::DebugPrintAllIMEModes(); + } +#endif +} + +static void +InitCurrentInputSource() +{ + if (gHandlerInstanceCount > 0 && + !gCurrentInputSource.IsInitializedByCurrentInputSource()) { + gCurrentInputSource.InitByCurrentInputSource(); + } +} + +static void +FinalizeCurrentInputSource() +{ + gCurrentInputSource.Clear(); +} + + +#pragma mark - + + +/****************************************************************************** + * + * TISInputSourceWrapper implementation + * + ******************************************************************************/ + +// static +TISInputSourceWrapper& +TISInputSourceWrapper::CurrentInputSource() +{ + InitCurrentInputSource(); + return gCurrentInputSource; +} + +bool +TISInputSourceWrapper::TranslateToString(UInt32 aKeyCode, UInt32 aModifiers, + UInt32 aKbType, nsAString &aStr) +{ + aStr.Truncate(); + + const UCKeyboardLayout* UCKey = GetUCKeyboardLayout(); + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%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, aKeyCode, aModifiers, 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); + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TISInputSourceWrapper::TranslateToString, err=0x%X, len=%llu", + this, err, len)); + + NS_ENSURE_TRUE(err == noErr, false); + if (len == 0) { + return true; + } + NS_ENSURE_TRUE(EnsureStringLength(aStr, len), 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)); + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TISInputSourceWrapper::TranslateToString, aStr=\"%s\"", + this, NS_ConvertUTF16toUTF8(aStr).get())); + + return true; +} + +uint32_t +TISInputSourceWrapper::TranslateToChar(UInt32 aKeyCode, UInt32 aModifiers, + UInt32 aKbType) +{ + nsAutoString str; + if (!TranslateToString(aKeyCode, aModifiers, aKbType, str) || + str.Length() != 1) { + return 0; + } + return static_cast(str.CharAt(0)); +} + +void +TISInputSourceWrapper::InitByInputSourceID(const char* aID) +{ + Clear(); + if (!aID) + return; + + CFStringRef idstr = ::CFStringCreateWithCString(kCFAllocatorDefault, aID, + kCFStringEncodingASCII); + InitByInputSourceID(idstr); + ::CFRelease(idstr); +} + +void +TISInputSourceWrapper::InitByInputSourceID(const nsAFlatString &aID) +{ + Clear(); + if (aID.IsEmpty()) + return; + CFStringRef idstr = ::CFStringCreateWithCharacters(kCFAllocatorDefault, + reinterpret_cast(aID.get()), + aID.Length()); + InitByInputSourceID(idstr); + ::CFRelease(idstr); +} + +void +TISInputSourceWrapper::InitByInputSourceID(const CFStringRef aID) +{ + Clear(); + if (!aID) + return; + const void* keys[] = { kTISPropertyInputSourceID }; + const void* values[] = { aID }; + CFDictionaryRef filter = + ::CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1, NULL, NULL); + NS_ASSERTION(filter, "failed to create the filter"); + mInputSourceList = ::TISCreateInputSourceList(filter, true); + ::CFRelease(filter); + if (::CFArrayGetCount(mInputSourceList) > 0) { + mInputSource = static_cast( + const_cast(::CFArrayGetValueAtIndex(mInputSourceList, 0))); + if (IsKeyboardLayout()) { + mKeyboardLayout = mInputSource; + } + } +} + +void +TISInputSourceWrapper::InitByLayoutID(SInt32 aLayoutID, + bool aOverrideKeyboard) +{ + // NOTE: Doument new layout IDs in TextInputHandler.h when you add ones. + switch (aLayoutID) { + case 0: + InitByInputSourceID("com.apple.keylayout.US"); + break; + case 1: + InitByInputSourceID("com.apple.keylayout.Greek"); + break; + case 2: + InitByInputSourceID("com.apple.keylayout.German"); + break; + case 3: + InitByInputSourceID("com.apple.keylayout.Swedish-Pro"); + break; + case 4: + InitByInputSourceID("com.apple.keylayout.DVORAK-QWERTYCMD"); + break; + case 5: + InitByInputSourceID("com.apple.keylayout.Thai"); + break; + case 6: + InitByInputSourceID("com.apple.keylayout.Arabic"); + break; + case 7: + InitByInputSourceID("com.apple.keylayout.French"); + break; + case 8: + InitByInputSourceID("com.apple.keylayout.Hebrew"); + break; + case 9: + InitByInputSourceID("com.apple.keylayout.Lithuanian"); + break; + case 10: + InitByInputSourceID("com.apple.keylayout.Norwegian"); + break; + case 11: + InitByInputSourceID("com.apple.keylayout.Spanish"); + break; + default: + Clear(); + break; + } + mOverrideKeyboard = aOverrideKeyboard; +} + +void +TISInputSourceWrapper::InitByCurrentInputSource() +{ + Clear(); + mInputSource = ::TISCopyCurrentKeyboardInputSource(); + mKeyboardLayout = ::TISCopyInputMethodKeyboardLayoutOverride(); + if (!mKeyboardLayout) { + mKeyboardLayout = ::TISCopyCurrentKeyboardLayoutInputSource(); + } + // If this causes composition, the current keyboard layout may input non-ASCII + // characters such as Japanese Kana characters or Hangul characters. + // However, we need to set ASCII characters to DOM key events for consistency + // with other platforms. + if (IsOpenedIMEMode()) { + TISInputSourceWrapper tis(mKeyboardLayout); + if (!tis.IsASCIICapable()) { + mKeyboardLayout = + ::TISCopyCurrentASCIICapableKeyboardLayoutInputSource(); + } + } +} + +void +TISInputSourceWrapper::InitByCurrentKeyboardLayout() +{ + Clear(); + mInputSource = ::TISCopyCurrentKeyboardLayoutInputSource(); + mKeyboardLayout = mInputSource; +} + +void +TISInputSourceWrapper::InitByCurrentASCIICapableInputSource() +{ + Clear(); + mInputSource = ::TISCopyCurrentASCIICapableKeyboardInputSource(); + mKeyboardLayout = ::TISCopyInputMethodKeyboardLayoutOverride(); + if (mKeyboardLayout) { + TISInputSourceWrapper tis(mKeyboardLayout); + if (!tis.IsASCIICapable()) { + mKeyboardLayout = nullptr; + } + } + if (!mKeyboardLayout) { + mKeyboardLayout = + ::TISCopyCurrentASCIICapableKeyboardLayoutInputSource(); + } +} + +void +TISInputSourceWrapper::InitByCurrentASCIICapableKeyboardLayout() +{ + Clear(); + mInputSource = ::TISCopyCurrentASCIICapableKeyboardLayoutInputSource(); + mKeyboardLayout = mInputSource; +} + +void +TISInputSourceWrapper::InitByCurrentInputMethodKeyboardLayoutOverride() +{ + Clear(); + mInputSource = ::TISCopyInputMethodKeyboardLayoutOverride(); + mKeyboardLayout = mInputSource; +} + +void +TISInputSourceWrapper::InitByTISInputSourceRef(TISInputSourceRef aInputSource) +{ + Clear(); + mInputSource = aInputSource; + if (IsKeyboardLayout()) { + mKeyboardLayout = mInputSource; + } +} + +void +TISInputSourceWrapper::InitByLanguage(CFStringRef aLanguage) +{ + Clear(); + mInputSource = ::TISCopyInputSourceForLanguage(aLanguage); + if (IsKeyboardLayout()) { + mKeyboardLayout = mInputSource; + } +} + +const UCKeyboardLayout* +TISInputSourceWrapper::GetUCKeyboardLayout() +{ + NS_ENSURE_TRUE(mKeyboardLayout, nullptr); + if (mUCKeyboardLayout) { + return mUCKeyboardLayout; + } + CFDataRef uchr = static_cast( + ::TISGetInputSourceProperty(mKeyboardLayout, + kTISPropertyUnicodeKeyLayoutData)); + + // We should be always able to get the layout here. + NS_ENSURE_TRUE(uchr, nullptr); + mUCKeyboardLayout = + reinterpret_cast(CFDataGetBytePtr(uchr)); + return mUCKeyboardLayout; +} + +bool +TISInputSourceWrapper::GetBoolProperty(const CFStringRef aKey) +{ + CFBooleanRef ret = static_cast( + ::TISGetInputSourceProperty(mInputSource, aKey)); + return ::CFBooleanGetValue(ret); +} + +bool +TISInputSourceWrapper::GetStringProperty(const CFStringRef aKey, + CFStringRef &aStr) +{ + aStr = static_cast( + ::TISGetInputSourceProperty(mInputSource, aKey)); + return aStr != nullptr; +} + +bool +TISInputSourceWrapper::GetStringProperty(const CFStringRef aKey, + nsAString &aStr) +{ + CFStringRef str; + GetStringProperty(aKey, str); + nsCocoaUtils::GetStringForNSString((const NSString*)str, aStr); + return !aStr.IsEmpty(); +} + +bool +TISInputSourceWrapper::IsOpenedIMEMode() +{ + NS_ENSURE_TRUE(mInputSource, false); + if (!IsIMEMode()) + return false; + return !IsASCIICapable(); +} + +bool +TISInputSourceWrapper::IsIMEMode() +{ + NS_ENSURE_TRUE(mInputSource, false); + CFStringRef str; + GetInputSourceType(str); + NS_ENSURE_TRUE(str, false); + return ::CFStringCompare(kTISTypeKeyboardInputMode, + str, 0) == kCFCompareEqualTo; +} + +bool +TISInputSourceWrapper::IsKeyboardLayout() +{ + NS_ENSURE_TRUE(mInputSource, false); + CFStringRef str; + GetInputSourceType(str); + NS_ENSURE_TRUE(str, false); + return ::CFStringCompare(kTISTypeKeyboardLayout, + str, 0) == kCFCompareEqualTo; +} + +bool +TISInputSourceWrapper::GetLanguageList(CFArrayRef &aLanguageList) +{ + NS_ENSURE_TRUE(mInputSource, false); + aLanguageList = static_cast( + ::TISGetInputSourceProperty(mInputSource, + kTISPropertyInputSourceLanguages)); + return aLanguageList != nullptr; +} + +bool +TISInputSourceWrapper::GetPrimaryLanguage(CFStringRef &aPrimaryLanguage) +{ + NS_ENSURE_TRUE(mInputSource, false); + CFArrayRef langList; + NS_ENSURE_TRUE(GetLanguageList(langList), false); + if (::CFArrayGetCount(langList) == 0) + return false; + aPrimaryLanguage = + static_cast(::CFArrayGetValueAtIndex(langList, 0)); + return aPrimaryLanguage != nullptr; +} + +bool +TISInputSourceWrapper::GetPrimaryLanguage(nsAString &aPrimaryLanguage) +{ + NS_ENSURE_TRUE(mInputSource, false); + CFStringRef primaryLanguage; + NS_ENSURE_TRUE(GetPrimaryLanguage(primaryLanguage), false); + nsCocoaUtils::GetStringForNSString((const NSString*)primaryLanguage, + aPrimaryLanguage); + return !aPrimaryLanguage.IsEmpty(); +} + +bool +TISInputSourceWrapper::IsForRTLLanguage() +{ + if (mIsRTL < 0) { + // Get the input character of the 'A' key of ANSI keyboard layout. + nsAutoString str; + bool ret = TranslateToString(kVK_ANSI_A, 0, eKbdType_ANSI, str); + NS_ENSURE_TRUE(ret, ret); + char16_t ch = str.IsEmpty() ? char16_t(0) : str.CharAt(0); + mIsRTL = UCS2_CHAR_IS_BIDI(ch) || ch == 0xD802 || ch == 0xD803; + } + return mIsRTL != 0; +} + +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. + InitLogModule(); + + if (mInputSourceList) { + ::CFRelease(mInputSourceList); + } + mInputSourceList = nullptr; + mInputSource = nullptr; + mKeyboardLayout = nullptr; + mIsRTL = -1; + mUCKeyboardLayout = nullptr; + mOverrideKeyboard = false; +} + +void +TISInputSourceWrapper::InitKeyEvent(NSEvent *aNativeKeyEvent, + WidgetKeyboardEvent& aKeyEvent, + const nsAString *aInsertString) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TISInputSourceWrapper::InitKeyEvent, aNativeKeyEvent=%p, " + "aKeyEvent.message=%s, aInsertString=%p, IsOpenedIMEMode()=%s", + this, aNativeKeyEvent, GetGeckoKeyEventType(aKeyEvent), aInsertString, + TrueOrFalse(IsOpenedIMEMode()))); + + NS_ENSURE_TRUE(aNativeKeyEvent, ); + + 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.refPoint = LayoutDeviceIntPoint(0, 0); + + // 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. + UInt32 kbType = mOverrideKeyboard ? eKbdType_ANSI : ::LMGetKbdType(); + + UInt32 nativeKeyCode = [aNativeKeyEvent keyCode]; + + bool isPrintableKey = !TextInputHandler::IsSpecialGeckoKey(nativeKeyCode); + if (isPrintableKey && + [aNativeKeyEvent type] != NSKeyDown && + [aNativeKeyEvent type] != NSKeyUp) { + NS_WARNING("Why the printable key doesn't cause NSKeyDown or NSKeyUp?"); + isPrintableKey = false; + } + + // Decide what string will be input. + nsAutoString insertString; + if (aInsertString) { + // If the caller expects that the aInsertString will be input, we shouldn't + // change it. + insertString = *aInsertString; + } else if (isPrintableKey) { + // 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 insertString 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. + if (!aKeyEvent.IsMeta() && !aKeyEvent.IsControl() && IsOpenedIMEMode()) { + UInt32 state = + nsCocoaUtils::ConvertToCarbonModifier([aNativeKeyEvent modifierFlags]); + uint32_t ch = TranslateToChar(nativeKeyCode, state, kbType); + if (ch) { + insertString = ch; + } + } else { + // If the caller isn't sure what string will be input, let's use + // characters of NSEvent. + nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], + insertString); + } + + // If control key is pressed and the eventChars is a non-printable control + // character, we should convert it to ASCII alphabet. + if (aKeyEvent.IsControl() && + !insertString.IsEmpty() && insertString[0] <= char16_t(26)) { + insertString = (aKeyEvent.IsShift() ^ aKeyEvent.IsCapsLocked()) ? + static_cast(insertString[0] + ('A' - 1)) : + static_cast(insertString[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 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) { + insertString = 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 (!insertString.IsEmpty() && IsControlChar(insertString[0])) { + insertString.Truncate(); + } + + aKeyEvent.keyCode = + ComputeGeckoKeyCode(nativeKeyCode, kbType, aKeyEvent.IsMeta()); + + switch (nativeKeyCode) { + case kVK_Command: + case kVK_Shift: + case kVK_Option: + case kVK_Control: + aKeyEvent.location = nsIDOMKeyEvent::DOM_KEY_LOCATION_LEFT; + break; + + case kVK_RightCommand: + case kVK_RightShift: + case kVK_RightOption: + case kVK_RightControl: + aKeyEvent.location = nsIDOMKeyEvent::DOM_KEY_LOCATION_RIGHT; + 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.location = nsIDOMKeyEvent::DOM_KEY_LOCATION_NUMPAD; + break; + + default: + aKeyEvent.location = nsIDOMKeyEvent::DOM_KEY_LOCATION_STANDARD; + break; + } + + aKeyEvent.mIsRepeat = + ([aNativeKeyEvent type] == NSKeyDown) ? [aNativeKeyEvent isARepeat] : false; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%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 (aKeyEvent.message == NS_KEY_PRESS && + (isPrintableKey || !insertString.IsEmpty())) { + InitKeyPressEvent(aNativeKeyEvent, + insertString.IsEmpty() ? 0 : insertString[0], + aKeyEvent, kbType); + MOZ_ASSERT(!aKeyEvent.charCode || !IsControlChar(aKeyEvent.charCode), + "charCode must not be a control character"); + } else { + aKeyEvent.charCode = 0; + aKeyEvent.isChar = false; // XXX not used in XP level + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TISInputSourceWrapper::InitKeyEvent, keyCode=0x%X charCode=0x0", + this, aKeyEvent.keyCode)); + } + + if (isPrintableKey) { + 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()) { + nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], + aKeyEvent.mKeyValue); + if (aKeyEvent.mKeyValue.IsEmpty() || + IsControlChar(aKeyEvent.mKeyValue[0])) { + NSUInteger cocoaState = + [aNativeKeyEvent modifierFlags] & ~NSControlKeyMask; + UInt32 carbonState = nsCocoaUtils::ConvertToCarbonModifier(cocoaState); + aKeyEvent.mKeyValue = + TranslateToChar(nativeKeyCode, carbonState, kbType); + } + } + // 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); + } + + // Last resort. If .key value becomes empty string, we should use + // charactersIgnoringModifiers, if it's available. + if (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); + } + + NS_OBJC_END_TRY_ABORT_BLOCK +} + +void +TISInputSourceWrapper::InitKeyPressEvent(NSEvent *aNativeKeyEvent, + char16_t aInsertChar, + WidgetKeyboardEvent& aKeyEvent, + UInt32 aKbType) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + NS_ASSERTION(aKeyEvent.message == NS_KEY_PRESS, + "aKeyEvent must be NS_KEY_PRESS event"); + +#ifdef PR_LOGGING + if (PR_LOG_TEST(gLog, PR_LOG_ALWAYS)) { + nsAutoString chars; + nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], chars); + NS_ConvertUTF16toUTF8 utf8Chars(chars); + char16_t expectedChar = static_cast(aInsertChar); + NS_ConvertUTF16toUTF8 utf8ExpectedChar(&expectedChar, 1); + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TISInputSourceWrapper::InitKeyPressEvent, aNativeKeyEvent=%p, " + "[aNativeKeyEvent characters]=\"%s\", aInsertChar=0x%X(%s), " + "aKeyEvent.message=%s, aKbType=0x%X, IsOpenedIMEMode()=%s", + this, aNativeKeyEvent, utf8Chars.get(), aInsertChar, + utf8ExpectedChar.get(), GetGeckoKeyEventType(aKeyEvent), aKbType, + TrueOrFalse(IsOpenedIMEMode()))); + } +#endif // #ifdef PR_LOGGING + + aKeyEvent.isChar = true; // this is not a special key XXX not used in XP + aKeyEvent.charCode = aInsertChar; + if (aKeyEvent.charCode != 0) { + aKeyEvent.keyCode = 0; + } + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TISInputSourceWrapper::InitKeyPressEvent, " + "aKeyEvent.keyCode=0x%X, aKeyEvent.charCode=0x%X", + this, aKeyEvent.keyCode, aKeyEvent.charCode)); + + if (!aKeyEvent.IsControl() && !aKeyEvent.IsMeta() && !aKeyEvent.IsAlt()) { + 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] & NSAlphaShiftKeyMask) { + lockState |= alphaLock; + } + if ([aNativeKeyEvent modifierFlags] & NSNumericPadKeyMask) { + lockState |= kEventKeyModifierNumLockMask; + } + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TISInputSourceWrapper::InitKeyPressEvent, " + "isRomanKeyboardLayout=%s, key=0x%X", + this, TrueOrFalse(isRomanKeyboardLayout), aKbType, key)); + + nsString str; + + // normal chars + uint32_t unshiftedChar = TranslateToChar(key, lockState, aKbType); + UInt32 shiftLockMod = shiftKey | lockState; + uint32_t shiftedChar = TranslateToChar(key, shiftLockMod, aKbType); + + // 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, aKbType); + UInt32 shiftNumMod = numState | shiftKey; + uint32_t uncmdedShiftChar = TranslateToChar(key, shiftNumMod, aKbType); + uint32_t uncmdedUSChar = USLayout.TranslateToChar(key, numState, aKbType); + UInt32 cmdNumMod = cmdKey | numState; + uint32_t cmdedChar = TranslateToChar(key, cmdNumMod, aKbType); + UInt32 cmdShiftNumMod = shiftKey | cmdNumMod; + uint32_t cmdedShiftChar = TranslateToChar(key, cmdShiftNumMod, aKbType); + + // 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.alternativeCharCodes.AppendElement(altCharCodes); + } + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TISInputSourceWrapper::InitKeyPressEvent, " + "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 charCode 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, aKbType); + if (ch) { + cmdedChar = ch; + } + ch = USLayout.TranslateToChar(key, shiftLockMod, aKbType); + 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.alternativeCharCodes.AppendElement(altCharCodes); + } + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TISInputSourceWrapper::InitKeyPressEvent, " + "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.alternativeCharCodes.AppendElement(altCharCodes); + } + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TISInputSourceWrapper::InitKeyPressEvent, " + "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) +{ + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TISInputSourceWrapper::ComputeGeckoKeyCode, aNativeKeyCode=0x%X, " + "aKbType=0x%X, aCmdIsPressed=%s, IsOpenedIMEMode()=%s, " + "IsASCIICapable()=%s", + this, aNativeKeyCode, 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 this is ASCII capable, give up to compute it. + if (IsASCIICapable()) { + return 0; + } + + // Retry with ASCII capable keyboard layout. + TISInputSourceWrapper currentKeyboardLayout; + currentKeyboardLayout.InitByCurrentASCIICapableKeyboardLayout(); + NS_ENSURE_TRUE(mInputSource != currentKeyboardLayout.mInputSource, 0); + keyCode = currentKeyboardLayout.ComputeGeckoKeyCode(aNativeKeyCode, aKbType, + aCmdIsPressed); + + // However, if keyCode isn't for an alphabet keys or a numeric key, we should + // ignore it. For example, comma key of Thai layout is same as close-square- + // bracket key of US layout and an unicode character key of Thai layout is + // same as comma key of US layout. If we return NS_VK_COMMA for latter key, + // web application developers cannot distinguish with the former key. + return ((keyCode >= NS_VK_A && keyCode <= NS_VK_Z) || + (keyCode >= NS_VK_0 && keyCode <= NS_VK_9)) ? keyCode : 0; +} + +// static +KeyNameIndex +TISInputSourceWrapper::ComputeGeckoKeyNameIndex(UInt32 aNativeKeyCode) +{ + 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; + } +} + + +#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() +{ +#ifdef PR_LOGGING + if (PR_LOG_TEST(gLog, PR_LOG_ALWAYS)) { + CFArrayRef list = CreateAllKeyboardLayoutList(); + PR_LOG(gLog, PR_LOG_ALWAYS, ("Keyboard layout configuration:")); + CFIndex idx = ::CFArrayGetCount(list); + TISInputSourceWrapper tis; + for (CFIndex i = 0; i < idx; ++i) { + TISInputSourceRef inputSource = static_cast( + const_cast(::CFArrayGetValueAtIndex(list, i))); + tis.InitByTISInputSourceRef(inputSource); + nsAutoString name, isid; + tis.GetLocalizedName(name); + tis.GetInputSourceID(isid); + PR_LOG(gLog, PR_LOG_ALWAYS, + (" %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); + } +#endif // #ifdef PR_LOGGING +} + + +#pragma mark - + + +/****************************************************************************** + * + * TextInputHandler implementation + * + ******************************************************************************/ + +TextInputHandler::TextInputHandler(nsChildView* aWidget, + NSView *aNativeView) : + IMEInputHandler(aWidget, aNativeView) +{ + InitLogModule(); + [mView installTextInputHandler:this]; +} + +TextInputHandler::~TextInputHandler() +{ + [mView uninstallTextInputHandler]; +} + +bool +TextInputHandler::HandleKeyDownEvent(NSEvent* aNativeEvent) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + if (Destroyed()) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::HandleKeyDownEvent, " + "widget has been already destroyed", this)); + return false; + } + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::HandleKeyDownEvent, aNativeEvent=%p, " + "type=%s, keyCode=%lld (0x%X), modifierFlags=0x%X, characters=\"%s\", " + "charactersIgnoringModifiers=\"%s\"", + this, aNativeEvent, GetNativeKeyEventType(aNativeEvent), + [aNativeEvent keyCode], [aNativeEvent keyCode], + [aNativeEvent modifierFlags], GetCharacters([aNativeEvent characters]), + GetCharacters([aNativeEvent charactersIgnoringModifiers]))); + + nsRefPtr kungFuDeathGrip(mWidget); + + KeyEventState* currentKeyEvent = PushKeyEvent(aNativeEvent); + AutoKeyEventStateCleaner remover(this); + + if (!IsIMEComposing()) { + NSResponder* firstResponder = [[mView window] firstResponder]; + + WidgetKeyboardEvent keydownEvent(true, NS_KEY_DOWN, mWidget); + InitKeyEvent(aNativeEvent, keydownEvent); + + currentKeyEvent->mKeyDownHandled = DispatchEvent(keydownEvent); + if (Destroyed()) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::HandleKeyDownEvent, " + "widget was destroyed by keydown event", this)); + return currentKeyEvent->IsDefaultPrevented(); + } + + // The key down event may have shifted the focus, in which + // case we should not fire the key press. + // XXX This is a special code only on Cocoa widget, why is this needed? + if (firstResponder != [[mView window] firstResponder]) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::HandleKeyDownEvent, " + "view lost focus by keydown event", this)); + return currentKeyEvent->IsDefaultPrevented(); + } + + if (currentKeyEvent->IsDefaultPrevented()) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::HandleKeyDownEvent, " + "keydown event's default is prevented", this)); + return true; + } + } + + // Let Cocoa interpret the key events, caching IsIMEComposing first. + bool wasComposing = IsIMEComposing(); + bool interpretKeyEventsCalled = false; + if (IsIMEEnabled() || IsASCIICapableOnly()) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::HandleKeyDownEvent, calling interpretKeyEvents", + this)); + [mView interpretKeyEvents:[NSArray arrayWithObject:aNativeEvent]]; + interpretKeyEventsCalled = true; + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::HandleKeyDownEvent, called interpretKeyEvents", + this)); + } + + if (Destroyed()) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::HandleKeyDownEvent, widget was destroyed", + this)); + return currentKeyEvent->IsDefaultPrevented(); + } + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::HandleKeyDownEvent, wasComposing=%s, " + "IsIMEComposing()=%s", + this, TrueOrFalse(wasComposing), TrueOrFalse(IsIMEComposing()))); + + if (currentKeyEvent->CanDispatchKeyPressEvent() && + !wasComposing && !IsIMEComposing()) { + WidgetKeyboardEvent keypressEvent(true, NS_KEY_PRESS, mWidget); + InitKeyEvent(aNativeEvent, keypressEvent); + + // 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(keypressEvent))) { + currentKeyEvent->mKeyPressHandled = DispatchEvent(keypressEvent); + currentKeyEvent->mKeyPressDispatched = true; + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::HandleKeyDownEvent, keypress event dispatched", + this)); + } + } + + // Note: mWidget might have become null here. Don't count on it from here on. + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::HandleKeyDownEvent, " + "keydown handled=%s, keypress handled=%s, causedOtherKeyEvents=%s", + this, TrueOrFalse(currentKeyEvent->mKeyDownHandled), + TrueOrFalse(currentKeyEvent->mKeyPressHandled), + TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents))); + return currentKeyEvent->IsDefaultPrevented(); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(false); +} + +void +TextInputHandler::HandleKeyUpEvent(NSEvent* aNativeEvent) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::HandleKeyUpEvent, aNativeEvent=%p, " + "type=%s, keyCode=%lld (0x%X), modifierFlags=0x%X, characters=\"%s\", " + "charactersIgnoringModifiers=\"%s\", " + "mIgnoreNextKeyUpEvent=%s, IsIMEComposing()=%s", + this, aNativeEvent, GetNativeKeyEventType(aNativeEvent), + [aNativeEvent keyCode], [aNativeEvent keyCode], + [aNativeEvent modifierFlags], GetCharacters([aNativeEvent characters]), + GetCharacters([aNativeEvent charactersIgnoringModifiers]), + TrueOrFalse(mIgnoreNextKeyUpEvent), TrueOrFalse(IsIMEComposing()))); + + if (mIgnoreNextKeyUpEvent) { + mIgnoreNextKeyUpEvent = false; + return; + } + + if (Destroyed()) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::HandleKeyUpEvent, " + "widget has been already destroyed", this)); + return; + } + + // if we don't have any characters we can't generate a keyUp event + if (IsIMEComposing()) { + return; + } + + WidgetKeyboardEvent keyupEvent(true, NS_KEY_UP, mWidget); + InitKeyEvent(aNativeEvent, keyupEvent); + + DispatchEvent(keyupEvent); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +TextInputHandler::HandleFlagsChanged(NSEvent* aNativeEvent) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (Destroyed()) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::HandleFlagsChanged, " + "widget has been already destroyed", this)); + return; + } + + nsRefPtr kungFuDeathGrip(mWidget); + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::HandleFlagsChanged, aNativeEvent=%p, " + "type=%s, keyCode=%s (0x%X), modifierFlags=0x%08X, " + "sLastModifierState=0x%08X, IsIMEComposing()=%s", + this, aNativeEvent, GetNativeKeyEventType(aNativeEvent), + GetKeyNameForNativeKeyCode([aNativeEvent keyCode]), [aNativeEvent keyCode], + [aNativeEvent modifierFlags], sLastModifierState, + TrueOrFalse(IsIMEComposing()))); + + MOZ_ASSERT([aNativeEvent type] == NSFlagsChanged); + + 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 & ~NSDeviceIndependentModifierFlagsMask) != 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 NSFlagsChanged 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; + 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 & NSDeviceIndependentModifierFlagsMask) { + switch (flag) { + case NSAlphaShiftKeyMask: + keyCode = kVK_CapsLock; + dispatchKeyDown = true; + break; + + case NSNumericPadKeyMask: + // NSNumericPadKeyMask 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 NSHelpKeyMask: + keyCode = kVK_Help; + break; + + case NSFunctionKeyMask: + // An NSFunctionKeyMask 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 (NSShiftKeyMask, NSControlKeyMask, NSAlternateKeyMask + // and NSCommandKeyMask) 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 NSShiftKeyMask: + keyCode = (modifiers & 0x0004) ? kVK_RightShift : kVK_Shift; + break; + case NSControlKeyMask: + keyCode = (modifiers & 0x2000) ? kVK_RightControl : kVK_Control; + break; + case NSAlternateKeyMask: + keyCode = (modifiers & 0x0040) ? kVK_RightOption : kVK_Option; + break; + case NSCommandKeyMask: + 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 NSShiftKeyMask, NSControlKeyMask, NSAlternateKeyMask + // and NSCommandKeyMask 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 &= ~NSShiftKeyMask; + } + break; + } + case kVK_RightShift: { + const ModifierKey* modifierKey = + GetModifierKeyForNativeKeyCode(kVK_Shift); + if (!modifierKey || + !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSShiftKeyMask; + } + break; + } + case kVK_Command: { + const ModifierKey* modifierKey = + GetModifierKeyForNativeKeyCode(kVK_RightCommand); + if (!modifierKey || + !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSCommandKeyMask; + } + break; + } + case kVK_RightCommand: { + const ModifierKey* modifierKey = + GetModifierKeyForNativeKeyCode(kVK_Command); + if (!modifierKey || + !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSCommandKeyMask; + } + break; + } + case kVK_Control: { + const ModifierKey* modifierKey = + GetModifierKeyForNativeKeyCode(kVK_RightControl); + if (!modifierKey || + !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSControlKeyMask; + } + break; + } + case kVK_RightControl: { + const ModifierKey* modifierKey = + GetModifierKeyForNativeKeyCode(kVK_Control); + if (!modifierKey || + !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSControlKeyMask; + } + break; + } + case kVK_Option: { + const ModifierKey* modifierKey = + GetModifierKeyForNativeKeyCode(kVK_RightOption); + if (!modifierKey || + !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSAlternateKeyMask; + } + break; + } + case kVK_RightOption: { + const ModifierKey* modifierKey = + GetModifierKeyForNativeKeyCode(kVK_Option); + if (!modifierKey || + !(modifiers & modifierKey->GetDeviceDependentFlags())) { + modifiers &= ~NSAlternateKeyMask; + } + break; + } + case kVK_Help: + modifiers &= ~NSHelpKeyMask; + break; + default: + break; + } + + NSEvent* event = + [NSEvent keyEventWithType:NSFlagsChanged + location:[aNativeEvent locationInWindow] + modifierFlags:modifiers + timestamp:[aNativeEvent timestamp] + windowNumber:[aNativeEvent windowNumber] + context:[aNativeEvent context] + characters:nil + charactersIgnoringModifiers:nil + 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 & ~NSDeviceIndependentModifierFlagsMask)) { + return &((ModifierKey&)mModifierKeys[i]); + } + } + return nullptr; +} + +void +TextInputHandler::DispatchKeyEventForFlagsChanged(NSEvent* aNativeEvent, + bool aDispatchKeyDown) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (Destroyed()) { + return; + } + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%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] != NSFlagsChanged || IsIMEComposing()) { + return; + } + + uint32_t message = aDispatchKeyDown ? NS_KEY_DOWN : NS_KEY_UP; + + NPCocoaEvent cocoaEvent; + + // Fire a key event. + WidgetKeyboardEvent keyEvent(true, message, mWidget); + InitKeyEvent(aNativeEvent, keyEvent); + + // create event for use by plugins + if ([mView isPluginView]) { + if ([mView pluginEventModel] == NPEventModelCocoa) { + ConvertCocoaKeyEventToNPCocoaEvent(aNativeEvent, cocoaEvent); + keyEvent.pluginEvent = &cocoaEvent; + } + } + + DispatchEvent(keyEvent); + + 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(); + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::InsertText, aAttrString=\"%s\", " + "aReplacementRange=%p { location=%llu, length=%llu }, " + "IsIMEComposing()=%s, IgnoreIMEComposition()=%s, " + "keyevent=%p, keydownHandled=%s, keypressDispatched=%s, " + "causedOtherKeyEvents=%s", + this, GetCharacters([aAttrString string]), aReplacementRange, + aReplacementRange ? aReplacementRange->location : 0, + aReplacementRange ? aReplacementRange->length : 0, + TrueOrFalse(IsIMEComposing()), TrueOrFalse(IgnoreIMEComposition()), + currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr, + currentKeyEvent ? + TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A", + currentKeyEvent ? + TrueOrFalse(currentKeyEvent->mKeyPressDispatched) : "N/A", + currentKeyEvent ? + TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents) : "N/A")); + + if (IgnoreIMEComposition()) { + return; + } + + InputContext context = mWidget->GetInputContext(); + bool isEditable = (context.mIMEState.mEnabled == IMEState::ENABLED || + context.mIMEState.mEnabled == IMEState::PASSWORD); + NSRange selectedRange = SelectedRange(); + + nsAutoString str; + nsCocoaUtils::GetStringForNSString([aAttrString string], 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; + } + // Delete the selected range. + nsRefPtr kungFuDeathGrip(this); + WidgetContentCommandEvent deleteCommandEvent(true, + NS_CONTENT_COMMAND_DELETE, + mWidget); + DispatchEvent(deleteCommandEvent); + NS_ENSURE_TRUE_VOID(deleteCommandEvent.mSucceeded); + // Be aware! The widget might be destroyed here. + return; + } + + if (str.Length() != 1 || IsIMEComposing()) { + InsertTextAsCommittingComposition(aAttrString, aReplacementRange); + return; + } + + // Don't let the same event be fired twice when hitting + // enter/return! (Bug 420502) + if (currentKeyEvent && !currentKeyEvent->CanDispatchKeyPressEvent()) { + return; + } + + nsRefPtr kungFuDeathGrip(mWidget); + + // If the replacement range is specified, select the range. Then, the + // selection will be replaced by the later keypress event. + if (isEditable && + aReplacementRange && aReplacementRange->location != NSNotFound && + !NSEqualRanges(selectedRange, *aReplacementRange)) { + NS_ENSURE_TRUE_VOID(SetSelection(*aReplacementRange)); + } + + // Dispatch keypress event with char instead of textEvent + WidgetKeyboardEvent keypressEvent(true, NS_KEY_PRESS, mWidget); + keypressEvent.isChar = IsPrintableChar(str.CharAt(0)); + + // 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) { + NSEvent* keyEvent = currentKeyEvent->mKeyEvent; + InitKeyEvent(keyEvent, keypressEvent, &str); + } else { + nsCocoaUtils::InitInputEvent(keypressEvent, static_cast(nullptr)); + if (keypressEvent.isChar) { + keypressEvent.charCode = str.CharAt(0); + } + // Note that insertText is not called only at key pressing. + if (!keypressEvent.charCode) { + keypressEvent.keyCode = + WidgetUtils::ComputeKeyCodeFromChar(keypressEvent.charCode); + } + } + + // Remove basic modifiers from keypress event because if they are included, + // nsPlaintextEditor ignores the event. + keypressEvent.modifiers &= ~(MODIFIER_CONTROL | + MODIFIER_ALT | + MODIFIER_META); + + // TODO: + // If mCurrentKeyEvent.mKeyEvent is null and when we implement textInput + // event of DOM3 Events, we should dispatch it instead of keypress event. + bool keyPressHandled = DispatchEvent(keypressEvent); + + // Note: mWidget might have become null here. Don't count on it from here on. + + if (currentKeyEvent) { + currentKeyEvent->mKeyPressHandled = keyPressHandled; + currentKeyEvent->mKeyPressDispatched = true; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +bool +TextInputHandler::DoCommandBySelector(const char* aSelector) +{ + nsRefPtr kungFuDeathGrip(mWidget); + + KeyEventState* currentKeyEvent = GetCurrentKeyEvent(); + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::DoCommandBySelector, aSelector=\"%s\", " + "Destroyed()=%s, keydownHandled=%s, keypressHandled=%s, " + "causedOtherKeyEvents=%s", + this, aSelector ? aSelector : "", TrueOrFalse(Destroyed()), + currentKeyEvent ? + TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A", + currentKeyEvent ? + TrueOrFalse(currentKeyEvent->mKeyPressHandled) : "N/A", + currentKeyEvent ? + TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents) : "N/A")); + + if (currentKeyEvent && currentKeyEvent->CanDispatchKeyPressEvent()) { + WidgetKeyboardEvent keypressEvent(true, NS_KEY_PRESS, mWidget); + InitKeyEvent(currentKeyEvent->mKeyEvent, keypressEvent); + currentKeyEvent->mKeyPressHandled = DispatchEvent(keypressEvent); + currentKeyEvent->mKeyPressDispatched = true; + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandler::DoCommandBySelector, keypress event " + "dispatched, Destroyed()=%s, keypressHandled=%s", + this, TrueOrFalse(Destroyed()), + TrueOrFalse(currentKeyEvent->mKeyPressHandled))); + } + + return (!Destroyed() && currentKeyEvent && + currentKeyEvent->IsDefaultPrevented()); +} + + +#pragma mark - + + +/****************************************************************************** + * + * IMEInputHandler implementation (static methods) + * + ******************************************************************************/ + +bool IMEInputHandler::sStaticMembersInitialized = 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); + } + +#ifdef PR_LOGGING + if (PR_LOG_TEST(gLog, PR_LOG_ALWAYS)) { + 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); + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("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; + } +#endif // #ifdef PR_LOGGING +} + +// static +void +IMEInputHandler::FlushPendingMethods(nsITimer* aTimer, void* aClosure) +{ + NS_ASSERTION(aClosure, "aClosure is null"); + static_cast(aClosure)->ExecutePendingMethods(); +} + +// static +CFArrayRef +IMEInputHandler::CreateAllIMEModeList() +{ + const void* keys[] = { kTISPropertyInputSourceType }; + const void* values[] = { kTISTypeKeyboardInputMode }; + CFDictionaryRef filter = + ::CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1, NULL, NULL); + NS_ASSERTION(filter, "failed to create the filter"); + CFArrayRef list = ::TISCreateInputSourceList(filter, true); + ::CFRelease(filter); + return list; +} + +// static +void +IMEInputHandler::DebugPrintAllIMEModes() +{ +#ifdef PR_LOGGING + if (PR_LOG_TEST(gLog, PR_LOG_ALWAYS)) { + CFArrayRef list = CreateAllIMEModeList(); + PR_LOG(gLog, PR_LOG_ALWAYS, ("IME mode configuration:")); + CFIndex idx = ::CFArrayGetCount(list); + TISInputSourceWrapper tis; + for (CFIndex i = 0; i < idx; ++i) { + TISInputSourceRef inputSource = static_cast( + const_cast(::CFArrayGetValueAtIndex(list, i))); + tis.InitByTISInputSourceRef(inputSource); + nsAutoString name, isid; + tis.GetLocalizedName(name); + tis.GetInputSourceID(isid); + PR_LOG(gLog, PR_LOG_ALWAYS, + (" %s\t<%s>%s%s\n", + NS_ConvertUTF16toUTF8(name).get(), + NS_ConvertUTF16toUTF8(isid).get(), + tis.IsASCIICapable() ? "" : "\t(Isn't ASCII capable)", + tis.IsEnabled() ? "" : "\t(Isn't Enabled)")); + } + ::CFRelease(list); + } +#endif // #ifdef PR_LOGGING +} + +//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. + * + ******************************************************************************/ + +void +IMEInputHandler::NotifyIMEOfFocusChangeInGecko() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::NotifyIMEOfFocusChangeInGecko, " + "Destroyed()=%s, IsFocused()=%s, inputContext=%p", + this, TrueOrFalse(Destroyed()), TrueOrFalse(IsFocused()), + mView ? [mView inputContext] : nullptr)); + + if (Destroyed()) { + return; + } + + if (!IsFocused()) { + // retry at next focus event + mPendingMethods |= kNotifyIMEOfFocusChangeInGecko; + return; + } + + MOZ_ASSERT(mView); + NSTextInputContext* inputContext = [mView inputContext]; + NS_ENSURE_TRUE_VOID(inputContext); + + // When an element on a XUL element gets focus from an + // element on the opener window of the element, the owner window + // still has native focus. Therefore, IMEs may store the opener window's + // level at this time because they don't know the actual focus is moved to + // different window. If IMEs try to get the newest window level after the + // focus change, we return the window level of the XUL 's widget. + // Therefore, let's emulate the native focus change. Then, IMEs can refresh + // the stored window level. + [inputContext deactivate]; + [inputContext activate]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +IMEInputHandler::DiscardIMEComposition() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::DiscardIMEComposition, " + "Destroyed()=%s, IsFocused()=%s, mView=%p, inputContext=%p", + this, TrueOrFalse(Destroyed()), TrueOrFalse(IsFocused()), + mView, mView ? [mView inputContext] : nullptr)); + + if (Destroyed()) { + return; + } + + if (!IsFocused()) { + // retry at next focus event + mPendingMethods |= kDiscardIMEComposition; + return; + } + + NS_ENSURE_TRUE_VOID(mView); + NSTextInputContext* inputContext = [mView inputContext]; + NS_ENSURE_TRUE_VOID(inputContext); + mIgnoreIMECommit = true; + [inputContext discardMarkedText]; + mIgnoreIMECommit = false; + + NS_OBJC_END_TRY_ABORT_BLOCK +} + +void +IMEInputHandler::SyncASCIICapableOnly() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%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 = do_CreateInstance(NS_TIMER_CONTRACTID); + NS_ENSURE_TRUE(mTimer, ); + } + mTimer->InitWithFuncCallback(FlushPendingMethods, this, 0, + nsITimer::TYPE_ONE_SHOT); +} + +void +IMEInputHandler::ExecutePendingMethods() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + + if (![[NSApplication sharedApplication] isActive]) { + mIsInFocusProcessing = false; + // 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 & kDiscardIMEComposition) + DiscardIMEComposition(); + if (pendingMethods & kSyncASCIICapableOnly) + SyncASCIICapableOnly(); + if (pendingMethods & kNotifyIMEOfFocusChangeInGecko) { + NotifyIMEOfFocusChangeInGecko(); + } + + mIsInFocusProcessing = false; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +#pragma mark - + + +/****************************************************************************** + * + * IMEInputHandler implementation (native event handlers) + * + ******************************************************************************/ + +uint32_t +IMEInputHandler::ConvertToTextRangeType(uint32_t aUnderlineStyle, + NSRange& aSelectedRange) +{ + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::ConvertToTextRangeType, " + "aUnderlineStyle=%llu, aSelectedRange.length=%llu,", + this, aUnderlineStyle, 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 NS_TEXTRANGE_RAWINPUT; + case NSUnderlineStyleThick: + return NS_TEXTRANGE_SELECTEDRAWTEXT; + default: + NS_WARNING("Unexpected line style"); + return NS_TEXTRANGE_SELECTEDRAWTEXT; + } + } + + switch (aUnderlineStyle) { + case NSUnderlineStyleSingle: + return NS_TEXTRANGE_CONVERTEDTEXT; + case NSUnderlineStyleThick: + return NS_TEXTRANGE_SELECTEDCONVERTEDTEXT; + default: + NS_WARNING("Unexpected line style"); + return NS_TEXTRANGE_SELECTEDCONVERTEDTEXT; + } +} + +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++; + } + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::GetRangeCount, aAttrString=\"%s\", count=%llu", + this, GetCharacters([aAttrString string]), count)); + + return count; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0); +} + +already_AddRefed +IMEInputHandler::CreateTextRangeArray(NSAttributedString *aAttrString, + NSRange& aSelectedRange) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + // 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); + nsRefPtr textRangeArray = + new mozilla::TextRangeArray(); + 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); + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::CreateTextRangeArray, " + "range={ mStartOffset=%llu, mEndOffset=%llu, mRangeType=%s }", + this, range.mStartOffset, range.mEndOffset, + GetRangeTypeName(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 = NS_TEXTRANGE_CARETPOSITION; + textRangeArray->AppendElement(range); + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::CreateTextRangeArray, " + "range={ mStartOffset=%llu, mEndOffset=%llu, mRangeType=%s }", + this, range.mStartOffset, range.mEndOffset, + GetRangeTypeName(range.mRangeType))); + + return textRangeArray.forget(); + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +bool +IMEInputHandler::DispatchTextEvent(const nsString& aText, + NSAttributedString* aAttrString, + NSRange& aSelectedRange, + bool aDoCommit) +{ + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::DispatchTextEvent, " + "aText=\"%s\", aAttrString=\"%s\", " + "aSelectedRange={ location=%llu, length=%llu }, " + "aDoCommit=%s, Destroyed()=%s", + this, NS_ConvertUTF16toUTF8(aText).get(), + GetCharacters([aAttrString string]), + aSelectedRange.location, aSelectedRange.length, + TrueOrFalse(aDoCommit), TrueOrFalse(Destroyed()))); + + NS_ENSURE_TRUE(!Destroyed(), false); + + nsRefPtr kungFuDeathGrip(this); + + WidgetTextEvent textEvent(true, NS_TEXT_TEXT, mWidget); + textEvent.time = PR_IntervalNow(); + textEvent.theText = aText; + if (!aDoCommit) { + textEvent.mRanges = CreateTextRangeArray(aAttrString, aSelectedRange); + } + + if (textEvent.theText != mLastDispatchedCompositionString) { + WidgetCompositionEvent compositionUpdate(true, NS_COMPOSITION_UPDATE, + mWidget); + compositionUpdate.time = textEvent.time; + compositionUpdate.data = textEvent.theText; + mLastDispatchedCompositionString = textEvent.theText; + DispatchEvent(compositionUpdate); + if (mIsInFocusProcessing || Destroyed()) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::DispatchTextEvent, compositionupdate causes " + "aborting the composition, mIsInFocusProcessing=%s, Destryoed()=%s", + this, TrueOrFalse(mIsInFocusProcessing), TrueOrFalse(Destroyed()))); + if (Destroyed()) { + return true; + } + } + } + + return DispatchEvent(textEvent); +} + +void +IMEInputHandler::InitCompositionEvent(WidgetCompositionEvent& aCompositionEvent) +{ + aCompositionEvent.time = PR_IntervalNow(); +} + +void +IMEInputHandler::InsertTextAsCommittingComposition( + NSAttributedString* aAttrString, + NSRange* aReplacementRange) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::InsertTextAsCommittingComposition, " + "aAttrString=\"%s\", aReplacementRange=%p { location=%llu, length=%llu }, " + "Destroyed()=%s, IsIMEComposing()=%s, " + "mMarkedRange={ location=%llu, length=%llu }", + this, GetCharacters([aAttrString string]), aReplacementRange, + aReplacementRange ? aReplacementRange->location : 0, + aReplacementRange ? aReplacementRange->length : 0, + TrueOrFalse(Destroyed()), TrueOrFalse(IsIMEComposing()), + mMarkedRange.location, mMarkedRange.length)); + + if (IgnoreIMECommit()) { + MOZ_CRASH("IMEInputHandler::InsertTextAsCommittingComposition() must not" + "be called while canceling the composition"); + } + + 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)) { + NSString* latestStr = + nsCocoaUtils::ToNSString(mLastDispatchedCompositionString); + NSAttributedString* attrLatestStr = + [[[NSAttributedString alloc] initWithString:latestStr] autorelease]; + InsertTextAsCommittingComposition(attrLatestStr, nullptr); + if (Destroyed()) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::InsertTextAsCommittingComposition, " + "destroyed by commiting composition for setting replacement range", + this)); + return; + } + } + + nsRefPtr kungFuDeathGrip(this); + + 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)); + } + + // XXXmnakano Probably, we shouldn't emulate composition in this case. + // I think that we should just fire DOM3 textInput event if we implement it. + WidgetCompositionEvent compStart(true, NS_COMPOSITION_START, mWidget); + InitCompositionEvent(compStart); + + DispatchEvent(compStart); + if (Destroyed()) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::InsertTextAsCommittingComposition, " + "destroyed by compositionstart event", this)); + return; + } + + OnStartIMEComposition(); + } + + NSRange range = NSMakeRange(0, str.Length()); + DispatchTextEvent(str, aAttrString, range, true); + if (Destroyed()) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::InsertTextAsCommittingComposition, " + "destroyed by text event", this)); + return; + } + + OnUpdateIMEComposition([aAttrString string]); + + WidgetCompositionEvent compEnd(true, NS_COMPOSITION_END, mWidget); + InitCompositionEvent(compEnd); + compEnd.data = mLastDispatchedCompositionString; + DispatchEvent(compEnd); + if (Destroyed()) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::InsertTextAsCommittingComposition, " + "destroyed by compositionend event", this)); + return; + } + + OnEndIMEComposition(); + + 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; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::SetMarkedText, " + "aAttrString=\"%s\", aSelectedRange={ location=%llu, length=%llu }, " + "aReplacementRange=%p { location=%llu, length=%llu }, " + "Destroyed()=%s, IgnoreIMEComposition()=%s, IsIMEComposing()=%s, " + "mMarkedRange={ location=%llu, length=%llu }", + this, GetCharacters([aAttrString string]), + aSelectedRange.location, aSelectedRange.length, aReplacementRange, + aReplacementRange ? aReplacementRange->location : 0, + aReplacementRange ? aReplacementRange->length : 0, + TrueOrFalse(Destroyed()), TrueOrFalse(IgnoreIMEComposition()), + TrueOrFalse(IsIMEComposing()), + mMarkedRange.location, mMarkedRange.length)); + + if (Destroyed() || IgnoreIMEComposition()) { + return; + } + + nsRefPtr kungFuDeathGrip(this); + + // 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)) { + NSString* latestStr = + nsCocoaUtils::ToNSString(mLastDispatchedCompositionString); + NSAttributedString* attrLatestStr = + [[[NSAttributedString alloc] initWithString:latestStr] autorelease]; + bool ignoreIMECommit = mIgnoreIMECommit; + mIgnoreIMECommit = false; + InsertTextAsCommittingComposition(attrLatestStr, nullptr); + mIgnoreIMECommit = ignoreIMECommit; + if (Destroyed()) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%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)) { + NS_ENSURE_TRUE_VOID(SetSelection(*aReplacementRange)); + } + + mMarkedRange.location = SelectedRange().location; + + WidgetCompositionEvent compStart(true, NS_COMPOSITION_START, mWidget); + InitCompositionEvent(compStart); + + DispatchEvent(compStart); + if (Destroyed()) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::SetMarkedText, " + "destroyed by compositionstart event", this)); + return; + } + + OnStartIMEComposition(); + } + + if (IsIMEComposing()) { + OnUpdateIMEComposition([aAttrString string]); + + bool doCommit = str.IsEmpty(); + DispatchTextEvent(str, aAttrString, aSelectedRange, doCommit); + if (Destroyed()) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::SetMarkedText, " + "destroyed by text event", this)); + return; + } + + if (doCommit) { + WidgetCompositionEvent compEnd(true, NS_COMPOSITION_END, mWidget); + InitCompositionEvent(compEnd); + compEnd.data = mLastDispatchedCompositionString; + DispatchEvent(compEnd); + if (Destroyed()) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::SetMarkedText, " + "destroyed by compositionend event", this)); + return; + } + OnEndIMEComposition(); + } + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +NSInteger +IMEInputHandler::ConversationIdentifier() +{ + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::ConversationIdentifier, Destroyed()=%s", + this, TrueOrFalse(Destroyed()))); + + if (Destroyed()) { + return reinterpret_cast(mView); + } + + nsRefPtr kungFuDeathGrip(this); + + // NOTE: The size of NSInteger is same as pointer size. + WidgetQueryContentEvent textContent(true, NS_QUERY_TEXT_CONTENT, mWidget); + textContent.InitForQueryTextContent(0, 0); + DispatchEvent(textContent); + if (!textContent.mSucceeded) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::ConversationIdentifier, Failed", this)); + return reinterpret_cast(mView); + } + // XXX This might return same ID as a previously existing editor if the + // deleted editor was created at the same address. Is there a better way? + return reinterpret_cast(textContent.mReply.mContentsRoot); +} + +NSAttributedString* +IMEInputHandler::GetAttributedSubstringFromRange(NSRange& aRange, + NSRange* aActualRange) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::GetAttributedSubstringFromRange, " + "aRange={ location=%llu, length=%llu }, aActualRange=%p, Destroyed()=%s", + this, aRange.location, aRange.length, aActualRange, + TrueOrFalse(Destroyed()))); + + if (aActualRange) { + *aActualRange = NSMakeRange(NSNotFound, 0); + } + + if (Destroyed() || aRange.location == NSNotFound || aRange.length == 0) { + return nil; + } + + nsRefPtr kungFuDeathGrip(this); + + nsAutoString str; + WidgetQueryContentEvent textContent(true, NS_QUERY_TEXT_CONTENT, mWidget); + textContent.InitForQueryTextContent(aRange.location, aRange.length); + DispatchEvent(textContent); + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::GetAttributedSubstringFromRange, " + "textContent={ mSucceeded=%s, mReply={ mString=\"%s\", mOffset=%llu } }", + this, TrueOrFalse(textContent.mSucceeded), + NS_ConvertUTF16toUTF8(textContent.mReply.mString).get(), + textContent.mReply.mOffset)); + + if (!textContent.mSucceeded) { + return nil; + } + + NSString* nsstr = nsCocoaUtils::ToNSString(textContent.mReply.mString); + NSAttributedString* result = + [[[NSAttributedString alloc] initWithString:nsstr + attributes:nil] autorelease]; + if (aActualRange) { + aActualRange->location = textContent.mReply.mOffset; + aActualRange->length = textContent.mReply.mString.Length(); + } + return result; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +bool +IMEInputHandler::HasMarkedText() +{ + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::HasMarkedText, " + "mMarkedRange={ location=%llu, length=%llu }", + this, mMarkedRange.location, mMarkedRange.length)); + + return (mMarkedRange.location != NSNotFound) && (mMarkedRange.length != 0); +} + +NSRange +IMEInputHandler::MarkedRange() +{ + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::MarkedRange, " + "mMarkedRange={ location=%llu, length=%llu }", + this, mMarkedRange.location, mMarkedRange.length)); + + if (!HasMarkedText()) { + return NSMakeRange(NSNotFound, 0); + } + return mMarkedRange; +} + +NSRange +IMEInputHandler::SelectedRange() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::SelectedRange, Destroyed()=%s, mSelectedRange={ " + "location=%llu, length=%llu }", + this, TrueOrFalse(Destroyed()), mSelectedRange.location, + mSelectedRange.length)); + + if (Destroyed()) { + return mSelectedRange; + } + + if (mSelectedRange.location != NSNotFound) { + MOZ_ASSERT(mIMEHasFocus); + return mSelectedRange; + } + + nsRefPtr kungFuDeathGrip(this); + + WidgetQueryContentEvent selection(true, NS_QUERY_SELECTED_TEXT, mWidget); + DispatchEvent(selection); + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::SelectedRange, selection={ mSucceeded=%s, " + "mReply={ mOffset=%llu, mString.Length()=%llu } }", + this, TrueOrFalse(selection.mSucceeded), selection.mReply.mOffset, + selection.mReply.mString.Length())); + + if (!selection.mSucceeded) { + return mSelectedRange; + } + + if (mIMEHasFocus) { + mSelectedRange.location = selection.mReply.mOffset; + mSelectedRange.length = selection.mReply.mString.Length(); + return mSelectedRange; + } + + return NSMakeRange(selection.mReply.mOffset, + selection.mReply.mString.Length()); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(mSelectedRange); +} + +NSRect +IMEInputHandler::FirstRectForCharacterRange(NSRange& aRange, + NSRange* aActualRange) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::FirstRectForCharacterRange, Destroyed()=%s, " + "aRange={ location=%llu, length=%llu }, aActualRange=%p }", + this, TrueOrFalse(Destroyed()), aRange.location, 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; + } + + nsRefPtr kungFuDeathGrip(this); + + nsIntRect r; + bool useCaretRect = (aRange.length == 0); + if (!useCaretRect) { + WidgetQueryContentEvent charRect(true, NS_QUERY_TEXT_RECT, mWidget); + charRect.InitForQueryTextRect(aRange.location, 1); + DispatchEvent(charRect); + if (charRect.mSucceeded) { + r = charRect.mReply.mRect; + actualRange.location = charRect.mReply.mOffset; + actualRange.length = charRect.mReply.mString.Length(); + } else { + useCaretRect = true; + } + } + + if (useCaretRect) { + WidgetQueryContentEvent caretRect(true, NS_QUERY_CARET_RECT, mWidget); + caretRect.InitForQueryCaretRect(aRange.location); + DispatchEvent(caretRect); + if (!caretRect.mSucceeded) { + return rect; + } + r = caretRect.mReply.mRect; + r.width = 0; + actualRange.location = caretRect.mReply.mOffset; + actualRange.length = 0; + } + + nsIWidget* rootWidget = mWidget->GetTopLevelWidget(); + NSWindow* rootWindow = + static_cast(rootWidget->GetNativeData(NS_NATIVE_WINDOW)); + NSView* rootView = + static_cast(rootWidget->GetNativeData(NS_NATIVE_WIDGET)); + if (!rootWindow || !rootView) { + return rect; + } + rect = nsCocoaUtils::DevPixelsToCocoaPoints(r, mWidget->BackingScaleFactor()); + rect = [rootView convertRect:rect toView:nil]; + rect.origin = [rootWindow convertBaseToScreen:rect.origin]; + + if (aActualRange) { + *aActualRange = actualRange; + } + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::FirstRectForCharacterRange, " + "useCaretRect=%s rect={ x=%f, y=%f, width=%f, height=%f }, " + "actualRange={ location=%llu, length=%llu }", + this, TrueOrFalse(useCaretRect), rect.origin.x, rect.origin.y, + rect.size.width, rect.size.height, actualRange.location, + 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) +{ + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::CharacterIndexForPoint, aPoint={ x=%f, y=%f }", + this, aPoint.x, aPoint.y)); + + //nsRefPtr kungFuDeathGrip(this); + + // To implement this, we'd have to grovel in text frames looking at text + // offsets. + return 0; +} + +NSArray* +IMEInputHandler::GetValidAttributesForMarkedText() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::GetValidAttributesForMarkedText", this)); + + //nsRefPtr kungFuDeathGrip(this); + + //return [NSArray arrayWithObjects:NSUnderlineStyleAttributeName, + // NSMarkedClauseSegmentAttributeName, + // NSTextInputReplacementRangeAttributeName, + // nil]; + // empty array; we don't support any attributes right now + return [NSArray array]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + + +#pragma mark - + + +/****************************************************************************** + * + * IMEInputHandler implementation #2 + * + ******************************************************************************/ + +IMEInputHandler::IMEInputHandler(nsChildView* aWidget, + NSView *aNativeView) : + PluginTextInputHandler(aWidget, aNativeView), + mPendingMethods(0), mIMECompositionString(nullptr), + mIsIMEComposing(false), mIsIMEEnabled(true), + mIsASCIICapableOnly(false), mIgnoreIMECommit(false), + mIsInFocusProcessing(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; + } +} + +void +IMEInputHandler::OnFocusChangeInGecko(bool aFocus) +{ + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%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; + mIsInFocusProcessing = true; + + // 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) +{ + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%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 (!PluginTextInputHandler::OnDestroyWidget(aDestroyingWidget)) { + return false; + } + + if (IsIMEComposing()) { + // If our view is in the composition, we should clean up it. + CancelIMEComposition(); + OnEndIMEComposition(); + } + + mSelectedRange.location = NSNotFound; // Marking dirty + mIMEHasFocus = false; + + return true; +} + +void +IMEInputHandler::OnStartIMEComposition() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::OnStartIMEComposition, mView=%p, mWidget=%p" + "inputContext=%p, mIsIMEComposing=%s", + this, mView, mWidget, mView ? [mView inputContext] : nullptr, + TrueOrFalse(mIsIMEComposing))); + + NS_ASSERTION(!mIsIMEComposing, "There is a composition already"); + mIsIMEComposing = true; + + mLastDispatchedCompositionString.Truncate(); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +IMEInputHandler::OnUpdateIMEComposition(NSString* aIMECompositionString) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::OnUpdateIMEComposition, mView=%p, mWidget=%p, " + "inputContext=%p, mIsIMEComposing=%s, aIMECompositionString=\"%s\"", + this, mView, mWidget, mView ? [mView inputContext] : nullptr, + TrueOrFalse(mIsIMEComposing), GetCharacters(aIMECompositionString))); + + NS_ASSERTION(mIsIMEComposing, "We're not in composition"); + + if (mIMECompositionString) + [mIMECompositionString release]; + mIMECompositionString = [aIMECompositionString retain]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +IMEInputHandler::OnEndIMEComposition() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::OnEndIMEComposition, mView=%p, mWidget=%p, " + "inputContext=%p, mIsIMEComposing=%s", + this, mView, mWidget, mView ? [mView inputContext] : nullptr, + TrueOrFalse(mIsIMEComposing))); + + NS_ASSERTION(mIsIMEComposing, "We're not in composition"); + + mIsIMEComposing = false; + + if (mIMECompositionString) { + [mIMECompositionString release]; + mIMECompositionString = nullptr; + } + + mLastDispatchedCompositionString.Truncate(); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +IMEInputHandler::SendCommittedText(NSString *aString) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::SendCommittedText, mView=%p, mWidget=%p, " + "inputContext=%p, mIsIMEComposing=%s", + this, mView, mWidget, mView ? [mView inputContext] : nullptr, + TrueOrFalse(mIsIMEComposing), mWidget)); + + NS_ENSURE_TRUE(mWidget, ); + // XXX We should send the string without mView. + if (!mView) { + return; + } + + NSAttributedString* attrStr = + [[NSAttributedString alloc] initWithString:aString]; + [mView insertText:attrStr]; + [attrStr release]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +IMEInputHandler::KillIMEComposition() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%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()) { + return; + } + + if (IsFocused()) { + NS_ENSURE_TRUE_VOID(mView); + NSTextInputContext* inputContext = [mView inputContext]; + NS_ENSURE_TRUE_VOID(inputContext); + [inputContext discardMarkedText]; + return; + } + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::KillIMEComposition, Pending...", this)); + + // Commit the composition internally. + SendCommittedText(mIMECompositionString); + NS_ASSERTION(!mIsIMEComposing, "We're still in a composition"); + // The pending method will be fired by the next focus event. + mPendingMethods |= kDiscardIMEComposition; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +IMEInputHandler::CommitIMEComposition() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!IsIMEComposing()) + return; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::CommitIMEComposition, mIMECompositionString=%s", + this, GetCharacters(mIMECompositionString))); + + 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; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%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() +{ + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::OpenSystemPreferredLanguageIME", this)); + + CFArrayRef langList = ::CFLocaleCopyPreferredLanguages(); + if (!langList) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::OpenSystemPreferredLanguageIME, langList is NULL", + this)); + return; + } + CFIndex count = ::CFArrayGetCount(langList); + for (CFIndex i = 0; i < count; i++) { + CFLocaleRef locale = + ::CFLocaleCreate(kCFAllocatorDefault, + static_cast(::CFArrayGetValueAtIndex(langList, i))); + if (!locale) { + continue; + } + + bool changed = false; + CFStringRef lang = static_cast( + ::CFLocaleGetValue(locale, kCFLocaleLanguageCode)); + NS_ASSERTION(lang, "lang is null"); + if (lang) { + TISInputSourceWrapper tis; + tis.InitByLanguage(lang); + if (tis.IsOpenedIMEMode()) { +#ifdef PR_LOGGING + if (PR_LOG_TEST(gLog, PR_LOG_ALWAYS)) { + CFStringRef foundTIS; + tis.GetInputSourceID(foundTIS); + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p IMEInputHandler::OpenSystemPreferredLanguageIME, " + "foundTIS=%s, lang=%s", + this, GetCharacters(foundTIS), GetCharacters(lang))); + } +#endif // #ifdef PR_LOGGING + tis.Select(); + changed = true; + } + } + ::CFRelease(locale); + if (changed) { + break; + } + } + ::CFRelease(langList); +} + + +#pragma mark - + + +/****************************************************************************** + * + * PluginTextInputHandler implementation + * + ******************************************************************************/ + +PluginTextInputHandler::PluginTextInputHandler(nsChildView* aWidget, + NSView *aNativeView) : + TextInputHandlerBase(aWidget, aNativeView), + mIgnoreNextKeyUpEvent(false), +#ifndef __LP64__ + mPluginTSMDoc(0), mPluginTSMInComposition(false), +#endif // #ifndef __LP64__ + mPluginComplexTextInputRequested(false) +{ +} + +PluginTextInputHandler::~PluginTextInputHandler() +{ +#ifndef __LP64__ + if (mPluginTSMDoc) { + ::DeleteTSMDocument(mPluginTSMDoc); + } +#endif // #ifndef __LP64__ +} + +/* static */ void +PluginTextInputHandler::ConvertCocoaKeyEventToNPCocoaEvent( + NSEvent* aCocoaEvent, + NPCocoaEvent& aPluginEvent) +{ + nsCocoaUtils::InitNPCocoaEvent(&aPluginEvent); + NSEventType nativeType = [aCocoaEvent type]; + switch (nativeType) { + case NSKeyDown: + aPluginEvent.type = NPCocoaEventKeyDown; + break; + case NSKeyUp: + aPluginEvent.type = NPCocoaEventKeyUp; + break; + case NSFlagsChanged: + aPluginEvent.type = NPCocoaEventFlagsChanged; + break; + default: + NS_WARNING("Asked to convert key event of unknown type to Cocoa plugin event!"); + } + aPluginEvent.data.key.modifierFlags = [aCocoaEvent modifierFlags]; + aPluginEvent.data.key.keyCode = [aCocoaEvent keyCode]; + // don't try to access character data for flags changed events, + // it will raise an exception + if (nativeType != NSFlagsChanged) { + aPluginEvent.data.key.characters = (NPNSString*)[aCocoaEvent characters]; + aPluginEvent.data.key.charactersIgnoringModifiers = + (NPNSString*)[aCocoaEvent charactersIgnoringModifiers]; + aPluginEvent.data.key.isARepeat = [aCocoaEvent isARepeat]; + } +} + +#ifndef __LP64__ + +EventHandlerRef PluginTextInputHandler::sPluginKeyEventsHandler = NULL; + +/* static */ void +PluginTextInputHandler::InstallPluginKeyEventsHandler() +{ + if (sPluginKeyEventsHandler) { + return; + } + static const EventTypeSpec sTSMEvents[] = + { { kEventClassTextInput, kEventTextInputUnicodeForKeyEvent } }; + ::InstallEventHandler(::GetEventDispatcherTarget(), + ::NewEventHandlerUPP(PluginKeyEventsHandler), + GetEventTypeCount(sTSMEvents), + sTSMEvents, + NULL, + &sPluginKeyEventsHandler); +} + +/* static */ void +PluginTextInputHandler::RemovePluginKeyEventsHandler() +{ + if (!sPluginKeyEventsHandler) { + return; + } + ::RemoveEventHandler(sPluginKeyEventsHandler); + sPluginKeyEventsHandler = NULL; +} + +/* static */ void +PluginTextInputHandler::SwizzleMethods() +{ + Class IMKInputSessionClass = ::NSClassFromString(@"IMKInputSession"); + nsToolkit::SwizzleMethods(IMKInputSessionClass, @selector(handleEvent:), + @selector(PluginTextInputHandler_IMKInputSession_handleEvent:)); + nsToolkit::SwizzleMethods(IMKInputSessionClass, @selector(commitComposition), + @selector(PluginTextInputHandler_IMKInputSession_commitComposition)); + nsToolkit::SwizzleMethods(IMKInputSessionClass, @selector(finishSession), + @selector(PluginTextInputHandler_IMKInputSession_finishSession)); +} + +/* static */ OSStatus +PluginTextInputHandler::PluginKeyEventsHandler(EventHandlerCallRef aHandlerRef, + EventRef aEvent, + void *aUserData) +{ + nsAutoreleasePool localPool; + + TSMDocumentID activeDoc = ::TSMGetActiveDocument(); + NS_ENSURE_TRUE(activeDoc, eventNotHandledErr); + + ChildView *target = nil; + OSStatus status = ::TSMGetDocumentProperty(activeDoc, + kFocusedChildViewTSMDocPropertyTag, + sizeof(ChildView *), nil, &target); + NS_ENSURE_TRUE(status == noErr, eventNotHandledErr); + NS_ENSURE_TRUE(target, eventNotHandledErr); + + EventRef keyEvent = NULL; + status = ::GetEventParameter(aEvent, kEventParamTextInputSendKeyboardEvent, + typeEventRef, NULL, sizeof(EventRef), NULL, + &keyEvent); + NS_ENSURE_TRUE(status == noErr, eventNotHandledErr); + NS_ENSURE_TRUE(keyEvent, eventNotHandledErr); + + nsIWidget* widget = [target widget]; + NS_ENSURE_TRUE(widget, eventNotHandledErr); + TextInputHandler* handler = + static_cast(widget)->GetTextInputHandler(); + NS_ENSURE_TRUE(handler, eventNotHandledErr); + handler->HandleCarbonPluginKeyEvent(keyEvent); + + return noErr; +} + +// Called from PluginKeyEventsHandler() (a handler for Carbon TSM events) to +// process a Carbon key event for the currently focused plugin. Both Unicode +// characters and "Mac encoding characters" (in the MBCS or "multibyte +// character system") are (or should be) available from aKeyEvent, but here we +// use the MCBS characters. This is how the WebKit does things, and seems to +// be what plugins expect. +void +PluginTextInputHandler::HandleCarbonPluginKeyEvent(EventRef aKeyEvent) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (Destroyed()) { + return; + } + + NS_ASSERTION(mView, "mView must not be NULL"); + + nsRefPtr kungFuDeathGrip(mWidget); + + if ([mView pluginEventModel] == NPEventModelCocoa) { + UInt32 size; + OSStatus status = + ::GetEventParameter(aKeyEvent, kEventParamKeyUnicodes, typeUnicodeText, + NULL, 0, &size, NULL); + NS_ENSURE_TRUE(status == noErr, ); + + UniChar* chars = (UniChar*)malloc(size); + NS_ENSURE_TRUE(chars, ); + + status = ::GetEventParameter(aKeyEvent, kEventParamKeyUnicodes, + typeUnicodeText, NULL, size, NULL, chars); + if (status != noErr) { + free(chars); + return; + } + + CFStringRef text = + ::CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault, chars, + (size / sizeof(UniChar)), + kCFAllocatorNull); + if (!text) { + free(chars); + return; + } + + NPCocoaEvent cocoaTextEvent; + nsCocoaUtils::InitNPCocoaEvent(&cocoaTextEvent); + cocoaTextEvent.type = NPCocoaEventTextInput; + cocoaTextEvent.data.text.text = (NPNSString*)text; + + WidgetPluginEvent pluginEvent(true, NS_PLUGIN_INPUT_EVENT, mWidget); + nsCocoaUtils::InitPluginEvent(pluginEvent, cocoaTextEvent); + DispatchEvent(pluginEvent); + + ::CFRelease(text); + free(chars); + + return; + } + + UInt32 numCharCodes; + OSStatus status = ::GetEventParameter(aKeyEvent, kEventParamKeyMacCharCodes, + typeChar, NULL, 0, &numCharCodes, NULL); + NS_ENSURE_TRUE(status == noErr, ); + + nsAutoTArray charCodes; + charCodes.SetLength(numCharCodes); + status = ::GetEventParameter(aKeyEvent, kEventParamKeyMacCharCodes, + typeChar, NULL, numCharCodes, NULL, + charCodes.Elements()); + NS_ENSURE_TRUE(status == noErr, ); + + UInt32 modifiers; + status = ::GetEventParameter(aKeyEvent, kEventParamKeyModifiers, + typeUInt32, NULL, sizeof(modifiers), NULL, + &modifiers); + NS_ENSURE_TRUE(status == noErr, ); + + NSUInteger cocoaModifiers = 0; + if (modifiers & shiftKey) { + cocoaModifiers |= NSShiftKeyMask; + } + if (modifiers & controlKey) { + cocoaModifiers |= NSControlKeyMask; + } + if (modifiers & optionKey) { + cocoaModifiers |= NSAlternateKeyMask; + } + if (modifiers & cmdKey) { // Should never happen + cocoaModifiers |= NSCommandKeyMask; + } + + UInt32 macKeyCode; + status = ::GetEventParameter(aKeyEvent, kEventParamKeyCode, + typeUInt32, NULL, sizeof(macKeyCode), NULL, + &macKeyCode); + NS_ENSURE_TRUE(status == noErr, ); + + TISInputSourceWrapper& currentInputSource = + TISInputSourceWrapper::CurrentInputSource(); + + EventRef cloneEvent = ::CopyEvent(aKeyEvent); + for (uint32_t i = 0; i < numCharCodes; ++i) { + status = ::SetEventParameter(cloneEvent, kEventParamKeyMacCharCodes, + typeChar, 1, charCodes.Elements() + i); + NS_ENSURE_TRUE(status == noErr, ); + + EventRecord eventRec; + if (::ConvertEventRefToEventRecord(cloneEvent, &eventRec)) { + WidgetKeyboardEvent keydownEvent(true, NS_KEY_DOWN, mWidget); + nsCocoaUtils::InitInputEvent(keydownEvent, cocoaModifiers); + + uint32_t keyCode = + currentInputSource.ComputeGeckoKeyCode(macKeyCode, ::LMGetKbdType(), + keydownEvent.IsMeta()); + uint32_t charCode(charCodes.ElementAt(i)); + + keydownEvent.time = PR_IntervalNow(); + keydownEvent.pluginEvent = &eventRec; + if (IsSpecialGeckoKey(macKeyCode)) { + keydownEvent.keyCode = keyCode; + } else { + // XXX This is wrong. charCode must be 0 for keydown event. + keydownEvent.charCode = charCode; + keydownEvent.isChar = true; + } + DispatchEvent(keydownEvent); + if (Destroyed()) { + break; + } + } + } + + ::ReleaseEvent(cloneEvent); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +PluginTextInputHandler::ActivatePluginTSMDocument() +{ + if (!mPluginTSMDoc) { + // Create a TSM document that supports both non-Unicode and Unicode input. + // Though ProcessPluginKeyEvent() only sends Mac char codes to + // the plugin, this makes the input window behave better when it contains + // more than one kind of input (say Hiragana and Romaji). This is what + // the OS does when it creates a TSM document for use by an + // NSTSMInputContext class. + InterfaceTypeList supportedServices; + supportedServices[0] = kTextServiceDocumentInterfaceType; + supportedServices[1] = kUnicodeDocumentInterfaceType; + ::NewTSMDocument(2, supportedServices, &mPluginTSMDoc, 0); + // We'll need to use the "input window". + ::UseInputWindow(mPluginTSMDoc, YES); + ::ActivateTSMDocument(mPluginTSMDoc); + } else if (::TSMGetActiveDocument() != mPluginTSMDoc) { + ::ActivateTSMDocument(mPluginTSMDoc); + } +} + +#endif // #ifndef __LP64__ + +void +PluginTextInputHandler::HandleKeyDownEventForPlugin(NSEvent* aNativeKeyEvent) +{ + if (Destroyed()) { + return; + } + + NS_ASSERTION(mView, "mView must not be NULL"); + +#ifdef __LP64__ + + if ([mView pluginEventModel] != NPEventModelCocoa) { + return; + } + + ComplexTextInputPanel* ctiPanel = + [ComplexTextInputPanel sharedComplexTextInputPanel]; + [ctiPanel adjustTo:mView]; + + // If a composition is in progress then simply let the input panel continue + // it. + if (IsInPluginComposition()) { + // Don't send key up events for key downs associated with compositions. + mIgnoreNextKeyUpEvent = true; + + NSString* textString = nil; + [ctiPanel interpretKeyEvent:aNativeKeyEvent string:&textString]; + if (textString) { + DispatchCocoaNPAPITextEvent(textString); + } + + return; + } + + // Reset complex text input request flag. + mPluginComplexTextInputRequested = false; + + // Send key down event to the plugin. + WidgetPluginEvent pluginEvent(true, NS_PLUGIN_INPUT_EVENT, mWidget); + NPCocoaEvent cocoaEvent; + ConvertCocoaKeyEventToNPCocoaEvent(aNativeKeyEvent, cocoaEvent); + nsCocoaUtils::InitPluginEvent(pluginEvent, cocoaEvent); + DispatchEvent(pluginEvent); + if (Destroyed()) { + return; + } + + // Start complex text composition if requested. + if (mPluginComplexTextInputRequested) { + // Don't send key up events for key downs associated with compositions. + mIgnoreNextKeyUpEvent = true; + + NSString* textString = nil; + [ctiPanel interpretKeyEvent:aNativeKeyEvent string:&textString]; + if (textString) { + DispatchCocoaNPAPITextEvent(textString); + } + } + +#else // #ifdef __LP64__ + + bool wasInComposition = false; + if ([mView pluginEventModel] == NPEventModelCocoa) { + if (IsInPluginComposition()) { + wasInComposition = true; + + // Don't send key up events for key downs associated with compositions. + mIgnoreNextKeyUpEvent = true; + } else { + // Reset complex text input request flag. + mPluginComplexTextInputRequested = false; + + // Send key down event to the plugin. + WidgetPluginEvent pluginEvent(true, NS_PLUGIN_INPUT_EVENT, mWidget); + NPCocoaEvent cocoaEvent; + ConvertCocoaKeyEventToNPCocoaEvent(aNativeKeyEvent, cocoaEvent); + nsCocoaUtils::InitPluginEvent(pluginEvent, cocoaEvent); + DispatchEvent(pluginEvent); + if (Destroyed()) { + return; + } + + // Only continue if plugin wants complex text input. + if (!mPluginComplexTextInputRequested) { + return; + } + + // Don't send key up events for key downs associated with compositions. + mIgnoreNextKeyUpEvent = true; + } + + // Don't send complex text input to a plugin in Cocoa event mode if + // either the Control key or the Command key is pressed -- even if the + // plugin has requested it, or we are already in IME composition. This + // conforms to our behavior in 64-bit mode and fixes bug 619217. + NSUInteger modifierFlags = [aNativeKeyEvent modifierFlags]; + if ((modifierFlags & NSControlKeyMask) || + (modifierFlags & NSCommandKeyMask)) { + return; + } + } + + // This will take care of all Carbon plugin events and also send Cocoa plugin + // text events when NSInputContext is not available (ifndef NP_NO_CARBON). + ActivatePluginTSMDocument(); + + // We use the active TSM document to pass a pointer to ourselves (the + // currently focused ChildView) to PluginKeyEventsHandler(). Because this + // pointer is weak, we should retain and release ourselves around the call + // to TSMProcessRawKeyEvent(). + nsAutoRetainCocoaObject kungFuDeathGrip(mView); + ::TSMSetDocumentProperty(mPluginTSMDoc, + kFocusedChildViewTSMDocPropertyTag, + sizeof(ChildView *), &mView); + ::TSMProcessRawKeyEvent([aNativeKeyEvent _eventRef]); + ::TSMRemoveDocumentProperty(mPluginTSMDoc, + kFocusedChildViewTSMDocPropertyTag); + +#endif // #ifdef __LP64__ else +} + +void +PluginTextInputHandler::HandleKeyUpEventForPlugin(NSEvent* aNativeKeyEvent) +{ + if (mIgnoreNextKeyUpEvent) { + mIgnoreNextKeyUpEvent = false; + return; + } + + if (Destroyed()) { + return; + } + + NS_ASSERTION(mView, "mView must not be NULL"); + + NPEventModel eventModel = [mView pluginEventModel]; + if (eventModel == NPEventModelCocoa) { + // Don't send key up events to Cocoa plugins during composition. + if (IsInPluginComposition()) { + return; + } + + WidgetKeyboardEvent keyupEvent(true, NS_KEY_UP, mWidget); + InitKeyEvent(aNativeKeyEvent, keyupEvent); + NPCocoaEvent pluginEvent; + ConvertCocoaKeyEventToNPCocoaEvent(aNativeKeyEvent, pluginEvent); + keyupEvent.pluginEvent = &pluginEvent; + DispatchEvent(keyupEvent); + return; + } +} + +bool +PluginTextInputHandler::IsInPluginComposition() +{ + return +#ifdef __LP64__ + [[ComplexTextInputPanel sharedComplexTextInputPanel] inComposition] != NO; +#else // #ifdef __LP64__ + mPluginTSMInComposition; +#endif // #ifdef __LP64__ else +} + +bool +PluginTextInputHandler::DispatchCocoaNPAPITextEvent(NSString* aString) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + NPCocoaEvent cocoaTextEvent; + nsCocoaUtils::InitNPCocoaEvent(&cocoaTextEvent); + cocoaTextEvent.type = NPCocoaEventTextInput; + cocoaTextEvent.data.text.text = (NPNSString*)aString; + + WidgetPluginEvent pluginEvent(true, NS_PLUGIN_INPUT_EVENT, mWidget); + nsCocoaUtils::InitPluginEvent(pluginEvent, cocoaTextEvent); + return DispatchEvent(pluginEvent); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(false); +} + + +#pragma mark - + + +#ifndef __LP64__ + +/****************************************************************************** + * + * PluginTextInputHandler_IMKInputSession_* implementation + * + ******************************************************************************/ + +// IMKInputSession is an undocumented class in the HIToolbox framework. It's +// present on both Leopard and SnowLeopard, and is used at a low level to +// process IME input regardless of which high-level API is used (Text Services +// Manager or Cocoa). It works the same way in both 32-bit and 64-bit code. +@interface NSObject (IMKInputSessionMethodSwizzling) +- (BOOL)PluginTextInputHandler_IMKInputSession_handleEvent:(EventRef)theEvent; +- (void)PluginTextInputHandler_IMKInputSession_commitComposition; +- (void)PluginTextInputHandler_IMKInputSession_finishSession; +@end + +@implementation NSObject (IMKInputSessionMethodSwizzling) + +- (BOOL)PluginTextInputHandler_IMKInputSession_handleEvent:(EventRef)theEvent +{ + [self retain]; + BOOL retval = + [self PluginTextInputHandler_IMKInputSession_handleEvent:theEvent]; + NSUInteger retainCount = [self retainCount]; + [self release]; + // Return without doing anything if we've been deleted. + if (retainCount == 1) { + return retval; + } + + NSWindow *mainWindow = [NSApp mainWindow]; + NSResponder *firstResponder = [mainWindow firstResponder]; + if (![firstResponder isKindOfClass:[ChildView class]]) { + return retval; + } + + // 'charactersEntered' is the length (in bytes) of currently "marked text" + // -- text that's been entered in IME but not yet committed. If it's + // non-zero we're composing text in an IME session; if it's zero we're + // not in an IME session. + NSInteger entered = 0; + object_getInstanceVariable(self, "charactersEntered", + (void **) &entered); + nsIWidget* widget = [(ChildView*)firstResponder widget]; + NS_ENSURE_TRUE(widget, retval); + TextInputHandler* handler = + static_cast(widget)->GetTextInputHandler(); + NS_ENSURE_TRUE(handler, retval); + handler->SetPluginTSMInComposition(entered != 0); + + return retval; +} + +// This method is called whenever IME input is committed as a result of an +// "abnormal" termination -- for example when changing the keyboard focus from +// one input field to another. +- (void)PluginTextInputHandler_IMKInputSession_commitComposition +{ + NSWindow *mainWindow = [NSApp mainWindow]; + NSResponder *firstResponder = [mainWindow firstResponder]; + if ([firstResponder isKindOfClass:[ChildView class]]) { + nsIWidget* widget = [(ChildView*)firstResponder widget]; + if (widget) { + TextInputHandler* handler = + static_cast(widget)->GetTextInputHandler(); + if (handler) { + handler->SetPluginTSMInComposition(false); + } + } + } + [self PluginTextInputHandler_IMKInputSession_commitComposition]; +} + +// This method is called just before we're deallocated. +- (void)PluginTextInputHandler_IMKInputSession_finishSession +{ + NSWindow *mainWindow = [NSApp mainWindow]; + NSResponder *firstResponder = [mainWindow firstResponder]; + if ([firstResponder isKindOfClass:[ChildView class]]) { + nsIWidget* widget = [(ChildView*)firstResponder widget]; + if (widget) { + TextInputHandler* handler = + static_cast(widget)->GetTextInputHandler(); + if (handler) { + handler->SetPluginTSMInComposition(false); + } + } + } + [self PluginTextInputHandler_IMKInputSession_finishSession]; +} + +@end + +#endif // #ifndef __LP64__ + + +#pragma mark - + + +/****************************************************************************** + * + * TextInputHandlerBase implementation + * + ******************************************************************************/ + +int32_t TextInputHandlerBase::sSecureEventInputCount = 0; + +TextInputHandlerBase::TextInputHandlerBase(nsChildView* aWidget, + NSView *aNativeView) : + mWidget(aWidget) +{ + gHandlerInstanceCount++; + mView = [aNativeView retain]; +} + +TextInputHandlerBase::~TextInputHandlerBase() +{ + [mView release]; + if (--gHandlerInstanceCount == 0) { + FinalizeCurrentInputSource(); + } +} + +bool +TextInputHandlerBase::OnDestroyWidget(nsChildView* aDestroyingWidget) +{ + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandlerBase::OnDestroyWidget, " + "aDestroyingWidget=%p, mWidget=%p", + this, aDestroyingWidget, mWidget)); + + if (aDestroyingWidget != mWidget) { + return false; + } + + mWidget = nullptr; + return true; +} + +bool +TextInputHandlerBase::DispatchEvent(WidgetGUIEvent& aEvent) +{ + if (aEvent.message == NS_KEY_PRESS) { + WidgetInputEvent& inputEvent = *aEvent.AsInputEvent(); + if (!inputEvent.IsMeta()) { + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandlerBase::DispatchEvent, hiding mouse cursor", this)); + [NSCursor setHiddenUntilMouseMoves:YES]; + } + } + return mWidget->DispatchWindowEvent(aEvent); +} + +void +TextInputHandlerBase::InitKeyEvent(NSEvent *aNativeKeyEvent, + WidgetKeyboardEvent& aKeyEvent, + 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, aInsertString); + return; + } + TISInputSourceWrapper::CurrentInputSource(). + InitKeyEvent(aNativeKeyEvent, aKeyEvent, 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, NSAlphaShiftKeyMask }, + { nsIWidget::SHIFT_L, NSShiftKeyMask | 0x0002 }, + { nsIWidget::SHIFT_R, NSShiftKeyMask | 0x0004 }, + { nsIWidget::CTRL_L, NSControlKeyMask | 0x0001 }, + { nsIWidget::CTRL_R, NSControlKeyMask | 0x2000 }, + { nsIWidget::ALT_L, NSAlternateKeyMask | 0x0020 }, + { nsIWidget::ALT_R, NSAlternateKeyMask | 0x0040 }, + { nsIWidget::COMMAND_L, NSCommandKeyMask | 0x0008 }, + { nsIWidget::COMMAND_R, NSCommandKeyMask | 0x0010 }, + { nsIWidget::NUMERIC_KEY_PAD, NSNumericPadKeyMask }, + { nsIWidget::HELP, NSHelpKeyMask }, + { nsIWidget::FUNCTION, NSFunctionKeyMask } + }; + + 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 ? NSFlagsChanged : NSKeyDown; + 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(NSKeyUp, 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; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandlerBase::GetWindowLevel, Destryoed()=%s", + this, TrueOrFalse(Destroyed()))); + + if (Destroyed()) { + return NSNormalWindowLevel; + } + + // When an element on a XUL is focused, the actual focused view + // is the panel's parent view (mView). But the editor is displayed on the + // popped-up widget's view (editorView). We want the latter's window level. + NSView* editorView = mWidget->GetEditorView(); + NS_ENSURE_TRUE(editorView, NSNormalWindowLevel); + NSInteger windowLevel = [[editorView window] level]; + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandlerBase::GetWindowLevel, windowLevel=%s (%X)", + this, GetWindowLevelName(windowLevel), 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.modifiers & MODIFIER_OS) { + return NS_OK; + } + + PR_LOG(gLog, PR_LOG_ALWAYS, + ("%p TextInputHandlerBase::AttachNativeKeyEvent, key=0x%X, char=0x%X, " + "mod=0x%X", this, aKeyEvent.keyCode, aKeyEvent.charCode, + aKeyEvent.modifiers)); + + NSEventType eventType; + if (aKeyEvent.message == NS_KEY_UP) { + eventType = NSKeyUp; + } else { + eventType = NSKeyDown; + } + + static const uint32_t sModifierFlagMap[][2] = { + { MODIFIER_SHIFT, NSShiftKeyMask }, + { MODIFIER_CONTROL, NSControlKeyMask }, + { MODIFIER_ALT, NSAlternateKeyMask }, + { MODIFIER_ALTGRAPH, NSAlternateKeyMask }, + { MODIFIER_META, NSCommandKeyMask }, + { MODIFIER_CAPSLOCK, NSAlphaShiftKeyMask }, + { MODIFIER_NUMLOCK, NSNumericPadKeyMask } + }; + + NSUInteger modifierFlags = 0; + for (uint32_t i = 0; i < ArrayLength(sModifierFlagMap); ++i) { + if (aKeyEvent.modifiers & sModifierFlagMap[i][0]) { + modifierFlags |= sModifierFlagMap[i][1]; + } + } + + NSInteger windowNumber = [[mView window] windowNumber]; + + NSString* characters; + if (aKeyEvent.charCode) { + characters = [NSString stringWithCharacters: + reinterpret_cast(&(aKeyEvent.charCode)) length:1]; + } else { + uint32_t cocoaCharCode = + nsCocoaUtils::ConvertGeckoKeyCodeToMacCharCode(aKeyEvent.keyCode); + characters = [NSString stringWithCharacters: + reinterpret_cast(&cocoaCharCode) length:1]; + } + + aKeyEvent.mNativeKeyEvent = + [NSEvent keyEventWithType:eventType + location:NSMakePoint(0,0) + modifierFlags:modifierFlags + timestamp:0 + windowNumber:windowNumber + context:[NSGraphicsContext currentContext] + characters:characters + charactersIgnoringModifiers:characters + isARepeat:NO + keyCode:0]; // Native key code not currently needed + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +bool +TextInputHandlerBase::SetSelection(NSRange& aRange) +{ + MOZ_ASSERT(!Destroyed()); + + nsRefPtr kungFuDeathGrip(this); + WidgetSelectionEvent selectionEvent(true, NS_SELECTION_SET, 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( + const WidgetKeyboardEvent& aKeyEvent) +{ + // this is not character inputting event, simply. + if (!aKeyEvent.isChar || !aKeyEvent.charCode || aKeyEvent.IsMeta()) { + return false; + } + // if this is unicode char inputting event, we don't need to check + // ctrl/alt/command keys + if (aKeyEvent.charCode > 0x7F) { + return true; + } + // ASCII chars should be inputted without ctrl/alt/command keys + return !aKeyEvent.IsControl() && !aKeyEvent.IsAlt(); +} + +/* 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(); + } +}