gecko/widget/src/cocoa/nsCocoaTextInputHandler.mm

1423 lines
41 KiB
Plaintext
Raw Normal View History

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 sw=2 et tw=80: */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is mozilla.org code.
*
* The Initial Developer of the Original Code is
* Mozilla Japan.
* Portions created by the Initial Developer are Copyright (C) 2009
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Masayuki Nakano <masayuki@d-toybox.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either of the GNU General Public License Version 2 or later (the "GPL"),
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
#include "nsCocoaTextInputHandler.h"
#include "nsChildView.h"
#include "nsObjCExceptions.h"
#include "nsBidiUtils.h"
#ifdef MOZ_LOGGING
#define FORCE_PR_LOG
#endif
#include "prlog.h"
#undef DEBUG_IME_HANDLER
#undef DEBUG_TEXT_INPUT_HANDLER
//#define DEBUG_IME_HANDLER 1
//#define DEBUG_TEXT_INPUT_HANDLER 1
static void
GetStringForNSString(const NSString *aSrc, nsAString& aDist)
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
if (!aSrc) {
aDist.Truncate();
return;
}
aDist.SetLength([aSrc length]);
[aSrc getCharacters: aDist.BeginWriting()];
NS_OBJC_END_TRY_ABORT_BLOCK;
}
#ifdef DEBUG_IME_HANDLER
static const char*
TrueOrFalse(PRBool aBool)
{
return aBool ? "TRUE" : "FALSE";
}
static void
DebugPrintPointer(nsCocoaIMEHandler* aHandler)
{
static nsCocoaIMEHandler* sLastHandler = nsnull;
if (aHandler == sLastHandler)
return;
sLastHandler = aHandler;
NSLog(@"%8p ************************************************", aHandler);
}
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 // DEBUG_IME_HANDLER
static PRUint32 gHandlerInstanceCount = 0;
static nsTISInputSource gCurrentKeyboardLayout;
static void
InitCurrentKeyboardLayout()
{
if (gHandlerInstanceCount > 0 &&
!gCurrentKeyboardLayout.IsInitializedByCurrentKeyboardLayout()) {
gCurrentKeyboardLayout.InitByCurrentKeyboardLayout();
}
}
static void
FinalizeCurrentKeyboardLayout()
{
gCurrentKeyboardLayout.Clear();
}
#pragma mark -
/******************************************************************************
*
* nsTISInputSource implementation
*
******************************************************************************/
// static
nsTISInputSource&
nsTISInputSource::CurrentKeyboardLayout()
{
InitCurrentKeyboardLayout();
return gCurrentKeyboardLayout;
}
// static
PRBool
nsTISInputSource::UCKeyTranslateToString(const UCKeyboardLayout* aHandle,
UInt32 aKeyCode, UInt32 aModifiers,
UInt32 aKbType, nsAString &aStr)
{
#ifdef DEBUG_TEXT_INPUT_HANDLER
NSLog(@"**** nsTISInputSource::UCKeyTranslateToString: aHandle: %p, aKeyCode: %X, aModifiers: %X, aKbType: %X",
aHandle, aKeyCode, aModifiers, aKbType);
PRBool isShift = aModifiers & shiftKey;
PRBool isCtrl = aModifiers & controlKey;
PRBool isOpt = aModifiers & optionKey;
PRBool isCmd = aModifiers & cmdKey;
PRBool isCL = aModifiers & alphaLock;
PRBool isNL = aModifiers & kEventKeyModifierNumLockMask;
NSLog(@" Shift: %s, Ctrl: %s, Opt: %s, Cmd: %s, CapsLock: %s, NumLock: %s",
isShift ? "ON" : "off", isCtrl ? "ON" : "off", isOpt ? "ON" : "off",
isCmd ? "ON" : "off", isCL ? "ON" : "off", isNL ? "ON" : "off");
#endif
NS_PRECONDITION(aStr.IsEmpty(), "aStr isn't empty");
UInt32 deadKeyState = 0;
UniCharCount len;
UniChar chars[5];
OSStatus err = ::UCKeyTranslate(aHandle, aKeyCode,
kUCKeyActionDown, aModifiers >> 8,
aKbType, kUCKeyTranslateNoDeadKeysMask,
&deadKeyState, 5, &len, chars);
if (err != noErr) {
return PR_FALSE;
}
if (len == 0) {
return PR_TRUE;
}
if (!EnsureStringLength(aStr, len)) {
return PR_FALSE;
}
NS_ASSERTION(sizeof(PRUnichar) == sizeof(UniChar),
"size of PRUnichar and size of UniChar are different");
memcpy(aStr.BeginWriting(), chars, len * sizeof(PRUnichar));
#ifdef DEBUG_TEXT_INPUT_HANDLER
for (PRUint32 i = 0; i < PRUint32(len); i++) {
NSLog(@" result: %X(%C)", chars[i], chars[i] > ' ' ? chars[i] : ' ');
}
#endif
return PR_TRUE;
}
void
nsTISInputSource::InitByInputSourceID(const char* aID)
{
Clear();
if (!aID)
return;
CFStringRef idstr = ::CFStringCreateWithCString(kCFAllocatorDefault, aID,
kCFStringEncodingASCII);
InitByInputSourceID(idstr);
::CFRelease(idstr);
}
void
nsTISInputSource::InitByInputSourceID(const nsAFlatString &aID)
{
Clear();
if (aID.IsEmpty())
return;
CFStringRef idstr = ::CFStringCreateWithCharacters(kCFAllocatorDefault,
aID.get(), aID.Length());
InitByInputSourceID(idstr);
::CFRelease(idstr);
}
void
nsTISInputSource::InitByInputSourceID(const CFStringRef aID)
{
Clear();
if (!aID)
return;
const void* keys[] = { kTISPropertyInputSourceID };
const void* values[] = { aID };
CFDictionaryRef filter =
::CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1, NULL, NULL);
NS_ASSERTION(filter, "failed to create the filter");
mInputSourceList = ::TISCreateInputSourceList(filter, true);
::CFRelease(filter);
if (::CFArrayGetCount(mInputSourceList) > 0) {
mInputSource = static_cast<TISInputSourceRef>(
const_cast<void *>(::CFArrayGetValueAtIndex(mInputSourceList, 0)));
}
}
void
nsTISInputSource::InitByLayoutID(SInt32 aLayoutID)
{
Clear();
// XXX On TIS, the LayoutID is abolished completely. Therefore, the numbers
// have no meaning. If you need to add other keyboard layouts for
// nsIWidget::SynthesizeNativeKeyEvent, you don't need to check the old
// resource, you can use any numbers.
switch (aLayoutID) {
case 0:
InitByInputSourceID("com.apple.keylayout.US");
break;
case -18944:
InitByInputSourceID("com.apple.keylayout.Greek");
break;
case 3:
InitByInputSourceID("com.apple.keylayout.German");
break;
case 224:
InitByInputSourceID("com.apple.keylayout.Swedish-Pro");
break;
}
}
void
nsTISInputSource::InitByCurrentInputSource()
{
Clear();
mInputSource = ::TISCopyCurrentKeyboardInputSource();
}
void
nsTISInputSource::InitByCurrentKeyboardLayout()
{
Clear();
mInputSource = ::TISCopyCurrentKeyboardLayoutInputSource();
}
void
nsTISInputSource::InitByCurrentASCIICapableInputSource()
{
Clear();
mInputSource = ::TISCopyCurrentASCIICapableKeyboardInputSource();
}
void
nsTISInputSource::InitByCurrentASCIICapableKeyboardLayout()
{
Clear();
mInputSource = ::TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
}
void
nsTISInputSource::InitByTISInputSourceRef(TISInputSourceRef aInputSource)
{
Clear();
mInputSource = aInputSource;
}
void
nsTISInputSource::InitByLanguage(CFStringRef aLanguage)
{
Clear();
mInputSource = ::TISCopyInputSourceForLanguage(aLanguage);
}
const UCKeyboardLayout*
nsTISInputSource::GetUCKeyboardLayout()
{
NS_ENSURE_TRUE(mInputSource, nsnull);
if (mUCKeyboardLayout) {
return mUCKeyboardLayout;
}
CFDataRef uchr = static_cast<CFDataRef>(
::TISGetInputSourceProperty(mInputSource,
kTISPropertyUnicodeKeyLayoutData));
// We should be always able to get the layout here.
NS_ENSURE_TRUE(uchr, nsnull);
mUCKeyboardLayout =
reinterpret_cast<const UCKeyboardLayout*>(CFDataGetBytePtr(uchr));
return mUCKeyboardLayout;
}
PRBool
nsTISInputSource::GetBoolProperty(const CFStringRef aKey)
{
CFBooleanRef ret = static_cast<CFBooleanRef>(
::TISGetInputSourceProperty(mInputSource, aKey));
return ::CFBooleanGetValue(ret);
}
PRBool
nsTISInputSource::GetStringProperty(const CFStringRef aKey, CFStringRef &aStr)
{
aStr = static_cast<CFStringRef>(
::TISGetInputSourceProperty(mInputSource, aKey));
return aStr != nsnull;
}
PRBool
nsTISInputSource::GetStringProperty(const CFStringRef aKey, nsAString &aStr)
{
CFStringRef str;
GetStringProperty(aKey, str);
GetStringForNSString((const NSString*)str, aStr);
return !aStr.IsEmpty();
}
PRBool
nsTISInputSource::IsOpenedIMEMode()
{
NS_ENSURE_TRUE(mInputSource, PR_FALSE);
if (!IsIMEMode())
return PR_FALSE;
return !IsASCIICapable();
}
PRBool
nsTISInputSource::IsIMEMode()
{
NS_ENSURE_TRUE(mInputSource, PR_FALSE);
CFStringRef str;
GetInputSourceType(str);
NS_ENSURE_TRUE(str, PR_FALSE);
return ::CFStringCompare(kTISTypeKeyboardInputMode,
str, 0) == kCFCompareEqualTo;
}
PRBool
nsTISInputSource::GetLanguageList(CFArrayRef &aLanguageList)
{
NS_ENSURE_TRUE(mInputSource, PR_FALSE);
aLanguageList = static_cast<CFArrayRef>(
::TISGetInputSourceProperty(mInputSource,
kTISPropertyInputSourceLanguages));
return aLanguageList != nsnull;
}
PRBool
nsTISInputSource::GetPrimaryLanguage(CFStringRef &aPrimaryLanguage)
{
NS_ENSURE_TRUE(mInputSource, PR_FALSE);
CFArrayRef langList;
NS_ENSURE_TRUE(GetLanguageList(langList), PR_FALSE);
if (::CFArrayGetCount(langList) == 0)
return PR_FALSE;
aPrimaryLanguage =
static_cast<CFStringRef>(::CFArrayGetValueAtIndex(langList, 0));
return aPrimaryLanguage != nsnull;
}
PRBool
nsTISInputSource::GetPrimaryLanguage(nsAString &aPrimaryLanguage)
{
NS_ENSURE_TRUE(mInputSource, PR_FALSE);
CFStringRef primaryLanguage;
NS_ENSURE_TRUE(GetPrimaryLanguage(primaryLanguage), PR_FALSE);
GetStringForNSString((const NSString*)primaryLanguage, aPrimaryLanguage);
return !aPrimaryLanguage.IsEmpty();
}
PRBool
nsTISInputSource::IsForRTLLanguage()
{
if (mIsRTL < 0) {
// Get the input character of the 'A' key of ANSI keyboard layout.
nsAutoString str;
PRBool ret = TranslateToString(kVK_ANSI_A, 0, eKbdType_ANSI, str);
NS_ENSURE_TRUE(ret, ret);
PRUnichar ch = str.IsEmpty() ? PRUnichar(0) : str.CharAt(0);
mIsRTL = UCS2_CHAR_IS_BIDI(ch) || ch == 0xD802 || ch == 0xD803;
}
return mIsRTL != 0;
}
PRBool
nsTISInputSource::IsInitializedByCurrentKeyboardLayout()
{
return mInputSource == ::TISCopyCurrentKeyboardLayoutInputSource();
}
PRBool
nsTISInputSource::TranslateToString(UInt32 aKeyCode, UInt32 aModifiers,
UInt32 aKbdType, nsAString &aStr)
{
aStr.Truncate();
const UCKeyboardLayout* UCKey = GetUCKeyboardLayout();
NS_ENSURE_TRUE(UCKey, PR_FALSE);
return UCKeyTranslateToString(UCKey, aKeyCode, aModifiers, aKbdType, aStr);
}
void
nsTISInputSource::Select()
{
if (!mInputSource)
return;
::TISSelectInputSource(mInputSource);
}
void
nsTISInputSource::Clear()
{
if (mInputSourceList) {
::CFRelease(mInputSourceList);
}
mInputSourceList = nsnull;
mInputSource = nsnull;
mIsRTL = -1;
mUCKeyboardLayout = nsnull;
}
#pragma mark -
/******************************************************************************
*
* nsCocoaTextInputHandler implementation (static methods)
*
******************************************************************************/
// static
CFArrayRef
nsCocoaTextInputHandler::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
nsCocoaTextInputHandler::DebugPrintAllKeyboardLayouts(
PRLogModuleInfo* aLogModuleInfo)
{
CFArrayRef list = CreateAllKeyboardLayoutList();
PR_LOG(aLogModuleInfo, PR_LOG_ALWAYS, ("Keyboard layout configuration:"));
CFIndex idx = ::CFArrayGetCount(list);
nsTISInputSource tis;
for (CFIndex i = 0; i < idx; ++i) {
TISInputSourceRef inputSource = static_cast<TISInputSourceRef>(
const_cast<void *>(::CFArrayGetValueAtIndex(list, i)));
tis.InitByTISInputSourceRef(inputSource);
nsAutoString name, isid;
tis.GetLocalizedName(name);
tis.GetInputSourceID(isid);
PR_LOG(aLogModuleInfo, 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.GetUCKeyboardLayout() ? "" : "\t(uchr is NOT AVAILABLE)"));
}
::CFRelease(list);
}
#pragma mark -
/******************************************************************************
*
* nsCocoaTextInputHandler implementation
*
******************************************************************************/
nsCocoaTextInputHandler::nsCocoaTextInputHandler() :
nsCocoaIMEHandler()
{
}
nsCocoaTextInputHandler::~nsCocoaTextInputHandler()
{
}
#pragma mark -
/******************************************************************************
*
* nsCocoaIMEHandler implementation (static methods)
*
******************************************************************************/
PRBool nsCocoaIMEHandler::sStaticMembersInitialized = PR_FALSE;
CFStringRef nsCocoaIMEHandler::sLatestIMEOpenedModeInputSourceID = nsnull;
nsCocoaIMEHandler* nsCocoaIMEHandler::sFocusedIMEHandler = nsnull;
// static
void
nsCocoaIMEHandler::InitStaticMembers()
{
if (sStaticMembersInitialized)
return;
sStaticMembersInitialized = PR_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
nsCocoaIMEHandler::OnCurrentTextInputSourceChange(
CFNotificationCenterRef aCenter,
void* aObserver,
CFStringRef aName,
const void* aObject,
CFDictionaryRef aUserInfo)
{
// Cache the latest IME opened mode to sLatestIMEOpenedModeInputSourceID.
nsTISInputSource tis;
tis.InitByCurrentInputSource();
if (tis.IsOpenedIMEMode()) {
tis.GetInputSourceID(sLatestIMEOpenedModeInputSourceID);
}
#ifdef DEBUG_IME_HANDLER
static CFStringRef sLastTIS = nsnull;
CFStringRef newTIS;
tis.GetInputSourceID(newTIS);
if (!sLastTIS ||
::CFStringCompare(sLastTIS, newTIS, 0) != kCFCompareEqualTo) {
nsTISInputSource tis1, tis2, tis3;
tis1.InitByCurrentKeyboardLayout();
tis2.InitByCurrentASCIICapableInputSource();
tis3.InitByCurrentASCIICapableKeyboardLayout();
CFStringRef is0, is1, is2, is3, type0, lang0, bundleID0;
tis.GetInputSourceID(is0);
tis1.GetInputSourceID(is1);
tis2.GetInputSourceID(is2);
tis3.GetInputSourceID(is3);
tis.GetInputSourceType(type0);
tis.GetPrimaryLanguage(lang0);
tis.GetBundleID(bundleID0);
NSLog(@"********************************************");
NSLog(@"Current Input Source is changed to:");
NSLog(@" current InputManager: %p", [NSInputManager currentInputManager]);
NSLog(@" %@", is0);
NSLog(@" type: %@ %s", type0,
tis.IsASCIICapable() ? "- ASCII capable " : "");
NSLog(@" primary language: %@", lang0);
NSLog(@" bundle ID: %@", bundleID0);
NSLog(@" current ASCII capable Input Source: %@", is2);
NSLog(@" current Keyboard Layout: %@", is1);
NSLog(@" current ASCII capable Keyboard Layout: %@", is3);
NSLog(@"********************************************");
}
sLastTIS = newTIS;
#endif // DEBUG_IME_HANDLER
}
// static
void
nsCocoaIMEHandler::FlushPendingMethods(nsITimer* aTimer, void* aClosure)
{
NS_ASSERTION(aClosure, "aClosure is null");
static_cast<nsCocoaIMEHandler*>(aClosure)->ExecutePendingMethods();
}
// static
CFArrayRef
nsCocoaIMEHandler::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
nsCocoaIMEHandler::DebugPrintAllIMEModes(PRLogModuleInfo* aLogModuleInfo)
{
CFArrayRef list = CreateAllIMEModeList();
PR_LOG(aLogModuleInfo, PR_LOG_ALWAYS, ("IME mode configuration:"));
CFIndex idx = ::CFArrayGetCount(list);
nsTISInputSource tis;
for (CFIndex i = 0; i < idx; ++i) {
TISInputSourceRef inputSource = static_cast<TISInputSourceRef>(
const_cast<void *>(::CFArrayGetValueAtIndex(list, i)));
tis.InitByTISInputSourceRef(inputSource);
nsAutoString name, isid;
tis.GetLocalizedName(name);
tis.GetInputSourceID(isid);
PR_LOG(aLogModuleInfo, 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);
}
//static
TSMDocumentID
nsCocoaIMEHandler::GetCurrentTSMDocumentID()
{
// On OS X 10.6.x at least, ::TSMGetActiveDocument() has a bug that prevents
// it from returning accurate results unless
// [NSInputManager currentInputManager] is called first.
// So, we need to call [NSInputManager currentInputManager] first here.
[NSInputManager currentInputManager];
return ::TSMGetActiveDocument();
}
#pragma mark -
/******************************************************************************
*
* nsCocoaIMEHandler 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
nsCocoaIMEHandler::ResetIMEWindowLevel()
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
#ifdef DEBUG_IME_HANDLER
DebugPrintPointer(this);
NSLog(@"nsCocoaIMEHandler::ResetIMEWindowLevel");
NSLog(@" IsFocused:%s GetCurrentTSMDocumentID():%p",
TrueOrFalse(IsFocused()), GetCurrentTSMDocumentID());
#endif // DEBUG_IME_HANDLER
if (!mView)
return;
if (!IsFocused()) {
// retry at next focus event
mPendingMethods |= kResetIMEWindowLevel;
return;
}
TSMDocumentID doc = GetCurrentTSMDocumentID();
if (!doc) {
// retry
mPendingMethods |= kResetIMEWindowLevel;
NS_WARNING("Application is active but there is no active document");
ResetTimer();
return;
}
// When the focus of Gecko is on a text box of a popup panel, the actual
// focused view is the panel's parent view (mView). But the editor is
// displayed on the popuped widget's view (editorView). So, their window
// level may be different.
NSView<mozView>* editorView = mOwnerWidget->GetEditorView();
if (!editorView) {
NS_ERROR("editorView is null");
return;
}
// We need to set the focused window level to TSMDocument. Then, the popup
// windows of IME (E.g., a candidate list window) will be over the focused
// view. See http://developer.apple.com/technotes/tn2005/tn2128.html#TNTAG1
NSInteger windowLevel = [[editorView window] level];
#ifdef DEBUG_IME_HANDLER
NSLog(@" windowLevel:%s (%x)",
GetWindowLevelName(windowLevel), windowLevel);
#endif // DEBUG_IME_HANDLER
// Chinese IMEs on 10.5 don't work fine if the level is NSNormalWindowLevel,
// then, we need to increment the value.
if (windowLevel == NSNormalWindowLevel)
windowLevel++;
::TSMSetDocumentProperty(GetCurrentTSMDocumentID(),
kTSMDocumentWindowLevelPropertyTag,
sizeof(windowLevel), &windowLevel);
NS_OBJC_END_TRY_ABORT_BLOCK;
}
void
nsCocoaIMEHandler::DiscardIMEComposition()
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
#ifdef DEBUG_IME_HANDLER
DebugPrintPointer(this);
NSLog(@"nsCocoaIMEHandler::DiscardIMEComposition");
NSLog(@" currentInputManager:%p", [NSInputManager currentInputManager]);
#endif // DEBUG_IME_HANDLER
if (!mView)
return;
if (!IsFocused()) {
// retry at next focus event
mPendingMethods |= kDiscardIMEComposition;
return;
}
NSInputManager* im = [NSInputManager currentInputManager];
if (!im) {
// retry
mPendingMethods |= kDiscardIMEComposition;
NS_WARNING("Application is active but there is no currentInputManager");
ResetTimer();
return;
}
mIgnoreIMECommit = PR_TRUE;
[im markedTextAbandoned: mView];
mIgnoreIMECommit = PR_FALSE;
NS_OBJC_END_TRY_ABORT_BLOCK
}
void
nsCocoaIMEHandler::SyncASCIICapableOnly()
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
#ifdef DEBUG_IME_HANDLER
DebugPrintPointer(this);
NSLog(@"nsCocoaIMEHandler::SyncASCIICapableOnly");
NSLog(@" IsFocused:%s GetCurrentTSMDocumentID():%p",
TrueOrFalse(IsFocused()), GetCurrentTSMDocumentID());
#endif
if (!mView)
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
nsCocoaIMEHandler::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
nsCocoaIMEHandler::ExecutePendingMethods()
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
if (mTimer) {
mTimer->Cancel();
mTimer = nsnull;
}
if (![[NSApplication sharedApplication] isActive]) {
mIsInFocusProcessing = PR_FALSE;
// If we're not active, we should retry at focus event
return;
}
PRUint32 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 & kResetIMEWindowLevel)
ResetIMEWindowLevel();
mIsInFocusProcessing = PR_FALSE;
NS_OBJC_END_TRY_ABORT_BLOCK;
}
#pragma mark -
/******************************************************************************
*
* nsCocoaIMEHandler implementation (native event handlers)
*
******************************************************************************/
PRUint32
nsCocoaIMEHandler::ConvertToTextRangeType(PRUint32 aUnderlineStyle,
NSRange& aSelectedRange)
{
#ifdef DEBUG_IME_HANDLER
NSLog(@"****in ConvertToTextRangeType = %d", aUnderlineStyle);
#endif
// 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;
}
}
PRUint32
nsCocoaIMEHandler::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.
PRUint32 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++;
}
return count;
NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0);
}
void
nsCocoaIMEHandler::SetTextRangeList(nsTArray<nsTextRange>& aTextRangeList,
NSAttributedString *aAttrString,
NSRange& aSelectedRange)
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
// Convert the Cocoa range into the nsTextRange 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]);
PRUint32 rangeCount = GetRangeCount(aAttrString);
for (PRUint32 i = 0; i < rangeCount && limitRange.length > 0; i++) {
NSRange effectiveRange;
id attributeValue = [aAttrString attribute:NSUnderlineStyleAttributeName
atIndex:limitRange.location
longestEffectiveRange:&effectiveRange
inRange:limitRange];
nsTextRange range;
range.mStartOffset = effectiveRange.location;
range.mEndOffset = NSMaxRange(effectiveRange);
range.mRangeType =
ConvertToTextRangeType([attributeValue intValue], aSelectedRange);
aTextRangeList.AppendElement(range);
limitRange =
NSMakeRange(NSMaxRange(effectiveRange),
NSMaxRange(limitRange) - NSMaxRange(effectiveRange));
}
// Get current caret position.
nsTextRange range;
range.mStartOffset = aSelectedRange.location + aSelectedRange.length;
range.mEndOffset = range.mStartOffset;
range.mRangeType = NS_TEXTRANGE_CARETPOSITION;
aTextRangeList.AppendElement(range);
NS_OBJC_END_TRY_ABORT_BLOCK;
}
PRBool
nsCocoaIMEHandler::DispatchTextEvent(const nsString& aText,
NSAttributedString* aAttrString,
NSRange& aSelectedRange,
PRBool aDoCommit)
{
#ifdef DEBUG_IME_HANDLER
NSLog(@"****in DispatchTextEvent; string = '%@'", aAttrString);
NSLog(@" aSelectedRange = %d, %d",
aSelectedRange.location, aSelectedRange.length);
#endif
nsTextEvent textEvent(PR_TRUE, NS_TEXT_TEXT, mOwnerWidget);
textEvent.time = PR_IntervalNow();
textEvent.theText = aText;
nsAutoTArray<nsTextRange, 4> textRanges;
if (!aDoCommit) {
SetTextRangeList(textRanges, aAttrString, aSelectedRange);
}
textEvent.rangeArray = textRanges.Elements();
textEvent.rangeCount = textRanges.Length();
return mOwnerWidget->DispatchWindowEvent(textEvent);
}
#pragma mark -
/******************************************************************************
*
* nsCocoaIMEHandler implementation #2
*
******************************************************************************/
nsCocoaIMEHandler::nsCocoaIMEHandler() :
mOwnerWidget(nsnull), mView(nsnull),
mPendingMethods(0), mIMECompositionString(nsnull),
mIsIMEComposing(PR_FALSE), mIsIMEEnabled(PR_TRUE),
mIsASCIICapableOnly(PR_FALSE), mIgnoreIMECommit(PR_FALSE),
mIsInFocusProcessing(PR_FALSE)
{
gHandlerInstanceCount++;
InitStaticMembers();
}
nsCocoaIMEHandler::~nsCocoaIMEHandler()
{
if (mTimer) {
mTimer->Cancel();
mTimer = nsnull;
}
if (sFocusedIMEHandler == this) {
sFocusedIMEHandler = nsnull;
}
if (--gHandlerInstanceCount == 0) {
FinalizeCurrentKeyboardLayout();
}
}
void
nsCocoaIMEHandler::Init(nsChildView* aOwner)
{
mOwnerWidget = aOwner;
mView =
static_cast<NSView<mozView>*>(aOwner->GetNativeData(NS_NATIVE_WIDGET));
}
void
nsCocoaIMEHandler::OnFocusChangeInGecko(PRBool aFocus)
{
#ifdef DEBUG_IME_HANDLER
DebugPrintPointer(this);
NSLog(@"nsCocoaIMEHandler::OnFocusChangeInGecko");
NSLog(@" aFocus:%s sFocusedIMEHandler:%p",
TrueOrFalse(aFocus), sFocusedIMEHandler);
#endif // DEBUG_IME_HANDLER
// This is called when the native focus is changed and when the native focus
// isn't changed but the focus is changed in Gecko.
// XXX currently, we're not called this method with PR_FALSE, we need to
// improve the nsIMEStateManager implementation.
if (!aFocus) {
if (sFocusedIMEHandler == this)
sFocusedIMEHandler = nsnull;
return;
}
sFocusedIMEHandler = this;
mIsInFocusProcessing = PR_TRUE;
// We need to reset the IME's window level by the current focused view of
// Gecko. It may be different from mView. However, we cannot get the
// new focused view here because the focus change process in Gecko hasn't
// been finished yet. So, we should post the job to the todo list.
mPendingMethods |= kResetIMEWindowLevel;
ResetTimer();
}
void
nsCocoaIMEHandler::OnDestroyView(NSView<mozView> *aDestroyingView)
{
// If we're not focused, the destroying view might be composing with it in
// another instance.
if (sFocusedIMEHandler && sFocusedIMEHandler != this) {
sFocusedIMEHandler->OnDestroyView(aDestroyingView);
}
if (aDestroyingView != mView) {
return;
}
if (IsIMEComposing()) {
// If our view is in the composition, we should clean up it.
// XXX Might CancelIMEComposition() fail because mView is being destroyed?
CancelIMEComposition();
OnEndIMEComposition();
}
mView = nsnull;
}
void
nsCocoaIMEHandler::OnStartIMEComposition(NSView<mozView> *aView)
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
#ifdef DEBUG_IME_HANDLER
DebugPrintPointer(this);
NSLog(@"nsCocoaIMEHandler::OnStartIMEComposition");
NSLog(@" aView:%p mView:%p currentInputManager:%p",
aView, mView, [NSInputManager currentInputManager]);
#endif // DEBUG_IME_HANDLER
NS_ASSERTION(!mIsIMEComposing, "There is a composition already");
NS_ASSERTION(aView == mView, "The composition is started on another view");
mIsIMEComposing = PR_TRUE;
NS_OBJC_END_TRY_ABORT_BLOCK;
}
void
nsCocoaIMEHandler::OnUpdateIMEComposition(NSString* aIMECompositionString)
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
#ifdef DEBUG_IME_HANDLER
DebugPrintPointer(this);
NSLog(@"nsCocoaIMEHandler::OnUpdateIMEComposition");
NSLog(@" aIMECompositionString:%@ currentInputManager:%p",
aIMECompositionString, [NSInputManager currentInputManager]);
#endif // DEBUG_IME_HANDLER
NS_ASSERTION(mIsIMEComposing, "We're not in composition");
if (mIMECompositionString)
[mIMECompositionString release];
mIMECompositionString = [aIMECompositionString retain];
NS_OBJC_END_TRY_ABORT_BLOCK;
}
void
nsCocoaIMEHandler::OnEndIMEComposition()
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
#ifdef DEBUG_IME_HANDLER
DebugPrintPointer(this);
NSLog(@"nsCocoaIMEHandler::OnEndIMEComposition");
NSLog(@" mIMECompositionString:%@ currentInputManager:%p",
mIMECompositionString, [NSInputManager currentInputManager]);
#endif // DEBUG_IME_HANDLER
NS_ASSERTION(mIsIMEComposing, "We're not in composition");
mIsIMEComposing = PR_FALSE;
if (mIMECompositionString) {
[mIMECompositionString release];
mIMECompositionString = nsnull;
}
NS_OBJC_END_TRY_ABORT_BLOCK;
}
void
nsCocoaIMEHandler::SendCommittedText(NSString *aString)
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
NS_ASSERTION(mView, "mView is null");
NSAttributedString* attrStr =
[[NSAttributedString alloc] initWithString:aString];
[mView insertText:attrStr];
[attrStr release];
NS_OBJC_END_TRY_ABORT_BLOCK;
}
void
nsCocoaIMEHandler::KillIMEComposition()
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
#ifdef DEBUG_IME_HANDLER
DebugPrintPointer(this);
NSLog(@"nsCocoaIMEHandler::KillIMEComposition");
NSLog(@" currentInputManager:%p", [NSInputManager currentInputManager]);
#endif // DEBUG_IME_HANDLER
if (!mView)
return;
if (IsFocused()) {
[[NSInputManager currentInputManager] markedTextAbandoned: mView];
return;
}
#ifdef DEBUG_IME_HANDLER
DebugPrintPointer(this);
NSLog(@"Pending nsCocoaIMEHandler::KillIMEComposition...");
#endif // DEBUG_IME_HANDLER
// 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
nsCocoaIMEHandler::CommitIMEComposition()
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
if (!IsIMEComposing())
return;
#ifdef DEBUG_IME_HANDLER
DebugPrintPointer(this);
NSLog(@"nsCocoaIMEHandler::CommitIMEComposition");
NSLog(@" mIMECompositionString:%@ currentInputManager:%p",
mIMECompositionString, [NSInputManager currentInputManager]);
#endif // DEBUG_IME_HANDLER
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
nsCocoaIMEHandler::CancelIMEComposition()
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
if (!IsIMEComposing())
return;
#ifdef DEBUG_IME_HANDLER
DebugPrintPointer(this);
NSLog(@"nsCocoaIMEHandler::CancelIMEComposition");
NSLog(@" mIMECompositionString:%@ currentInputManager:%p",
mIMECompositionString, [NSInputManager currentInputManager]);
#endif // DEBUG_IME_HANDLER
// For canceling the current composing, we need to ignore the param of
// insertText. But this code is ugly...
mIgnoreIMECommit = PR_TRUE;
KillIMEComposition();
mIgnoreIMECommit = PR_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;
}
PRBool
nsCocoaIMEHandler::IsFocused()
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
NS_ENSURE_TRUE(mView, PR_FALSE);
NSWindow* window = [mView window];
NS_ENSURE_TRUE(window, PR_FALSE);
return [window firstResponder] == mView &&
([window isMainWindow] || [window isSheet]) &&
[[NSApplication sharedApplication] isActive];
NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(PR_FALSE);
}
PRBool
nsCocoaIMEHandler::IsIMEOpened()
{
nsTISInputSource tis;
tis.InitByCurrentInputSource();
return tis.IsOpenedIMEMode();
}
void
nsCocoaIMEHandler::SetASCIICapableOnly(PRBool aASCIICapableOnly)
{
if (aASCIICapableOnly == mIsASCIICapableOnly)
return;
CommitIMEComposition();
mIsASCIICapableOnly = aASCIICapableOnly;
SyncASCIICapableOnly();
}
void
nsCocoaIMEHandler::EnableIME(PRBool aEnableIME)
{
if (aEnableIME == mIsIMEEnabled)
return;
CommitIMEComposition();
mIsIMEEnabled = aEnableIME;
}
void
nsCocoaIMEHandler::SetIMEOpenState(PRBool aOpenIME)
{
if (!IsFocused() || IsIMEOpened() == aOpenIME)
return;
if (!aOpenIME) {
nsTISInputSource tis;
tis.InitByCurrentASCIICapableInputSource();
tis.Select();
return;
}
// If we know the latest IME opened mode, we should select it.
if (sLatestIMEOpenedModeInputSourceID) {
nsTISInputSource 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 PRBool sIsPrefferredIMESearched = PR_FALSE;
if (sIsPrefferredIMESearched)
return;
sIsPrefferredIMESearched = PR_TRUE;
OpenSystemPreferredLanguageIME();
}
void
nsCocoaIMEHandler::OpenSystemPreferredLanguageIME()
{
CFArrayRef langList = ::CFLocaleCopyPreferredLanguages();
if (!langList) {
return;
}
CFIndex count = ::CFArrayGetCount(langList);
for (CFIndex i = 0; i < count; i++) {
CFLocaleRef locale =
::CFLocaleCreate(kCFAllocatorDefault,
static_cast<CFStringRef>(::CFArrayGetValueAtIndex(langList, i)));
if (!locale) {
continue;
}
PRBool changed = PR_FALSE;
CFStringRef lang = static_cast<CFStringRef>(
::CFLocaleGetValue(locale, kCFLocaleLanguageCode));
NS_ASSERTION(lang, "lang is null");
if (lang) {
nsTISInputSource tis;
tis.InitByLanguage(lang);
if (tis.IsOpenedIMEMode()) {
#ifdef DEBUG_IME_HANDLER
CFStringRef foundTIS;
tis.GetInputSourceID(foundTIS);
NSLog(@"nsCocoaIMEHandler::OpenSystemPreferredLanguageIME");
NSLog(@" found Input Source: %@ by %@",
(const NSString*)foundTIS, lang);
#endif // DEBUG_IME_HANDLER
tis.Select();
changed = PR_TRUE;
}
}
::CFRelease(locale);
if (changed) {
break;
}
}
::CFRelease(langList);
}