diff options
Diffstat (limited to '')
-rw-r--r-- | widget/windows/IMMHandler.cpp | 2384 |
1 files changed, 2384 insertions, 0 deletions
diff --git a/widget/windows/IMMHandler.cpp b/widget/windows/IMMHandler.cpp new file mode 100644 index 0000000000..338c3a64fd --- /dev/null +++ b/widget/windows/IMMHandler.cpp @@ -0,0 +1,2384 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sts=2 sw=2 et cin: */ +/* 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/Logging.h" + +#include "IMMHandler.h" +#include "nsWindow.h" +#include "nsWindowDefs.h" +#include "WinIMEHandler.h" +#include "WinUtils.h" +#include "KeyboardLayout.h" +#include <algorithm> + +#include "mozilla/CheckedInt.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/TextEvents.h" +#include "mozilla/WindowsVersion.h" + +#ifndef IME_PROP_ACCEPT_WIDE_VKEY +# define IME_PROP_ACCEPT_WIDE_VKEY 0x20 +#endif + +//------------------------------------------------------------------------- +// +// from +// http://download.microsoft.com/download/6/0/9/60908e9e-d2c1-47db-98f6-216af76a235f/msime.h +// The document for this has been removed from MSDN... +// +//------------------------------------------------------------------------- + +#define RWM_MOUSE TEXT("MSIMEMouseOperation") + +#define IMEMOUSE_NONE 0x00 // no mouse button was pushed +#define IMEMOUSE_LDOWN 0x01 +#define IMEMOUSE_RDOWN 0x02 +#define IMEMOUSE_MDOWN 0x04 +#define IMEMOUSE_WUP 0x10 // wheel up +#define IMEMOUSE_WDOWN 0x20 // wheel down + +static const char* GetBoolName(bool aBool) { return aBool ? "true" : "false"; } + +static void HandleSeparator(nsACString& aDesc) { + if (!aDesc.IsEmpty()) { + aDesc.AppendLiteral(" | "); + } +} + +class GetIMEGeneralPropertyName : public nsAutoCString { + public: + explicit GetIMEGeneralPropertyName(DWORD aFlags) { + if (!aFlags) { + AppendLiteral("no flags"); + return; + } + if (aFlags & IME_PROP_AT_CARET) { + AppendLiteral("IME_PROP_AT_CARET"); + } + if (aFlags & IME_PROP_SPECIAL_UI) { + HandleSeparator(*this); + AppendLiteral("IME_PROP_SPECIAL_UI"); + } + if (aFlags & IME_PROP_CANDLIST_START_FROM_1) { + HandleSeparator(*this); + AppendLiteral("IME_PROP_CANDLIST_START_FROM_1"); + } + if (aFlags & IME_PROP_UNICODE) { + HandleSeparator(*this); + AppendLiteral("IME_PROP_UNICODE"); + } + if (aFlags & IME_PROP_COMPLETE_ON_UNSELECT) { + HandleSeparator(*this); + AppendLiteral("IME_PROP_COMPLETE_ON_UNSELECT"); + } + if (aFlags & IME_PROP_ACCEPT_WIDE_VKEY) { + HandleSeparator(*this); + AppendLiteral("IME_PROP_ACCEPT_WIDE_VKEY"); + } + } + virtual ~GetIMEGeneralPropertyName() {} +}; + +class GetIMEUIPropertyName : public nsAutoCString { + public: + explicit GetIMEUIPropertyName(DWORD aFlags) { + if (!aFlags) { + AppendLiteral("no flags"); + return; + } + if (aFlags & UI_CAP_2700) { + AppendLiteral("UI_CAP_2700"); + } + if (aFlags & UI_CAP_ROT90) { + HandleSeparator(*this); + AppendLiteral("UI_CAP_ROT90"); + } + if (aFlags & UI_CAP_ROTANY) { + HandleSeparator(*this); + AppendLiteral("UI_CAP_ROTANY"); + } + } + virtual ~GetIMEUIPropertyName() {} +}; + +class GetWritingModeName : public nsAutoCString { + public: + explicit GetWritingModeName(const WritingMode& aWritingMode) { + if (!aWritingMode.IsVertical()) { + AssignLiteral("Horizontal"); + return; + } + if (aWritingMode.IsVerticalLR()) { + AssignLiteral("Vertical (LR)"); + return; + } + AssignLiteral("Vertical (RL)"); + } + virtual ~GetWritingModeName() {} +}; + +class GetReconvertStringLog : public nsAutoCString { + public: + explicit GetReconvertStringLog(RECONVERTSTRING* aReconv) { + AssignLiteral("{ dwSize="); + AppendInt(static_cast<uint32_t>(aReconv->dwSize)); + AppendLiteral(", dwVersion="); + AppendInt(static_cast<uint32_t>(aReconv->dwVersion)); + AppendLiteral(", dwStrLen="); + AppendInt(static_cast<uint32_t>(aReconv->dwStrLen)); + AppendLiteral(", dwStrOffset="); + AppendInt(static_cast<uint32_t>(aReconv->dwStrOffset)); + AppendLiteral(", dwCompStrLen="); + AppendInt(static_cast<uint32_t>(aReconv->dwCompStrLen)); + AppendLiteral(", dwCompStrOffset="); + AppendInt(static_cast<uint32_t>(aReconv->dwCompStrOffset)); + AppendLiteral(", dwTargetStrLen="); + AppendInt(static_cast<uint32_t>(aReconv->dwTargetStrLen)); + AppendLiteral(", dwTargetStrOffset="); + AppendInt(static_cast<uint32_t>(aReconv->dwTargetStrOffset)); + AppendLiteral(", result str=\""); + if (aReconv->dwStrLen) { + char16_t* strStart = reinterpret_cast<char16_t*>( + reinterpret_cast<char*>(aReconv) + aReconv->dwStrOffset); + nsDependentString str(strStart, aReconv->dwStrLen); + Append(NS_ConvertUTF16toUTF8(str)); + } + AppendLiteral("\" }"); + } + virtual ~GetReconvertStringLog() {} +}; + +namespace mozilla { +namespace widget { + +static IMMHandler* gIMMHandler = nullptr; + +LazyLogModule gIMMLog("nsIMM32HandlerWidgets"); + +/****************************************************************************** + * IMEContext + ******************************************************************************/ + +IMEContext::IMEContext(HWND aWnd) : mWnd(aWnd), mIMC(::ImmGetContext(aWnd)) {} + +IMEContext::IMEContext(nsWindowBase* aWindowBase) + : mWnd(aWindowBase->GetWindowHandle()), + mIMC(::ImmGetContext(aWindowBase->GetWindowHandle())) {} + +void IMEContext::Init(HWND aWnd) { + Clear(); + mWnd = aWnd; + mIMC = ::ImmGetContext(mWnd); +} + +void IMEContext::Init(nsWindowBase* aWindowBase) { + Init(aWindowBase->GetWindowHandle()); +} + +void IMEContext::Clear() { + if (mWnd && mIMC) { + ::ImmReleaseContext(mWnd, mIMC); + } + mWnd = nullptr; + mIMC = nullptr; +} + +/****************************************************************************** + * IMMHandler + ******************************************************************************/ + +static UINT sWM_MSIME_MOUSE = 0; // mouse message for MSIME 98/2000 + +WritingMode IMMHandler::sWritingModeOfCompositionFont; +nsString IMMHandler::sIMEName; +UINT IMMHandler::sCodePage = 0; +DWORD IMMHandler::sIMEProperty = 0; +DWORD IMMHandler::sIMEUIProperty = 0; +bool IMMHandler::sAssumeVerticalWritingModeNotSupported = false; +bool IMMHandler::sHasFocus = false; + +#define IMPL_IS_IME_ACTIVE(aReadableName, aActualName) \ + bool IMMHandler::Is##aReadableName##Active() { \ + return sIMEName.Equals(aActualName); \ + } + +IMPL_IS_IME_ACTIVE(ATOK2006, u"ATOK 2006") +IMPL_IS_IME_ACTIVE(ATOK2007, u"ATOK 2007") +IMPL_IS_IME_ACTIVE(ATOK2008, u"ATOK 2008") +IMPL_IS_IME_ACTIVE(ATOK2009, u"ATOK 2009") +IMPL_IS_IME_ACTIVE(ATOK2010, u"ATOK 2010") +// NOTE: Even on Windows for en-US, the name of Google Japanese Input is +// written in Japanese. +IMPL_IS_IME_ACTIVE(GoogleJapaneseInput, + u"Google \x65E5\x672C\x8A9E\x5165\x529B " + u"IMM32 \x30E2\x30B8\x30E5\x30FC\x30EB") +IMPL_IS_IME_ACTIVE(Japanist2003, u"Japanist 2003") + +#undef IMPL_IS_IME_ACTIVE + +// static +bool IMMHandler::IsActiveIMEInBlockList() { + if (sIMEName.IsEmpty()) { + return false; + } +#ifdef _WIN64 + // ATOK started to be TIP of TSF since 2011. Older than it, i.e., ATOK 2010 + // and earlier have a lot of problems even for daily use. Perhaps, the + // reason is Win 8 has a lot of changes around IMM-IME support and TSF, + // and ATOK 2010 is released earlier than Win 8. + // ATOK 2006 crashes while converting a word with candidate window. + // ATOK 2007 doesn't paint and resize suggest window and candidate window + // correctly (showing white window or too big window). + // ATOK 2008 and ATOK 2009 crash when user just opens their open state. + // ATOK 2010 isn't installable newly on Win 7 or later, but we have a lot of + // crash reports. + if (IsWin8OrLater() && + (IsATOK2006Active() || IsATOK2007Active() || IsATOK2008Active() || + IsATOK2009Active() || IsATOK2010Active())) { + return true; + } +#endif // #ifdef _WIN64 + return false; +} + +// static +void IMMHandler::EnsureHandlerInstance() { + if (!gIMMHandler) { + gIMMHandler = new IMMHandler(); + } +} + +// static +void IMMHandler::Initialize() { + if (!sWM_MSIME_MOUSE) { + sWM_MSIME_MOUSE = ::RegisterWindowMessage(RWM_MOUSE); + } + sAssumeVerticalWritingModeNotSupported = Preferences::GetBool( + "intl.imm.vertical_writing.always_assume_not_supported", false); + InitKeyboardLayout(nullptr, ::GetKeyboardLayout(0)); +} + +// static +void IMMHandler::Terminate() { + if (!gIMMHandler) return; + delete gIMMHandler; + gIMMHandler = nullptr; +} + +// static +bool IMMHandler::IsComposingOnOurEditor() { + return gIMMHandler && gIMMHandler->mIsComposing; +} + +// static +bool IMMHandler::IsComposingWindow(nsWindow* aWindow) { + return gIMMHandler && gIMMHandler->mComposingWindow == aWindow; +} + +// static +bool IMMHandler::IsTopLevelWindowOfComposition(nsWindow* aWindow) { + if (!gIMMHandler || !gIMMHandler->mComposingWindow) { + return false; + } + HWND wnd = gIMMHandler->mComposingWindow->GetWindowHandle(); + return WinUtils::GetTopLevelHWND(wnd, true) == aWindow->GetWindowHandle(); +} + +// static +bool IMMHandler::ShouldDrawCompositionStringOurselves() { + // If current IME has special UI or its composition window should not + // positioned to caret position, we should now draw composition string + // ourselves. + return !(sIMEProperty & IME_PROP_SPECIAL_UI) && + (sIMEProperty & IME_PROP_AT_CARET); +} + +// static +bool IMMHandler::IsVerticalWritingSupported() { + // Even if IME claims that they support vertical writing mode but it may not + // support vertical writing mode for its candidate window. + if (sAssumeVerticalWritingModeNotSupported) { + return false; + } + // Google Japanese Input doesn't support vertical writing mode. We should + // return false if it's active IME. + if (IsGoogleJapaneseInputActive()) { + return false; + } + return !!(sIMEUIProperty & (UI_CAP_2700 | UI_CAP_ROT90 | UI_CAP_ROTANY)); +} + +// static +void IMMHandler::InitKeyboardLayout(nsWindow* aWindow, HKL aKeyboardLayout) { + UINT IMENameLength = ::ImmGetDescriptionW(aKeyboardLayout, nullptr, 0); + if (IMENameLength) { + // Add room for the terminating null character + sIMEName.SetLength(++IMENameLength); + IMENameLength = + ::ImmGetDescriptionW(aKeyboardLayout, sIMEName.get(), IMENameLength); + // Adjust the length to ignore the terminating null character + sIMEName.SetLength(IMENameLength); + } else { + sIMEName.Truncate(); + } + + WORD langID = LOWORD(aKeyboardLayout); + ::GetLocaleInfoW(MAKELCID(langID, SORT_DEFAULT), + LOCALE_IDEFAULTANSICODEPAGE | LOCALE_RETURN_NUMBER, + (PWSTR)&sCodePage, sizeof(sCodePage) / sizeof(WCHAR)); + sIMEProperty = ::ImmGetProperty(aKeyboardLayout, IGP_PROPERTY); + sIMEUIProperty = ::ImmGetProperty(aKeyboardLayout, IGP_UI); + + // If active IME is a TIP of TSF, we cannot retrieve the name with IMM32 API. + // For hacking some bugs of some TIP, we should set an IME name from the + // pref. + if (sCodePage == 932 && sIMEName.IsEmpty()) { + Preferences::GetString("intl.imm.japanese.assume_active_tip_name_as", + sIMEName); + } + + // Whether the IME supports vertical writing mode might be changed or + // some IMEs may need specific font for their UI. Therefore, we should + // update composition font forcibly here. + if (aWindow) { + MaybeAdjustCompositionFont(aWindow, sWritingModeOfCompositionFont, true); + } + + MOZ_LOG(gIMMLog, LogLevel::Info, + ("InitKeyboardLayout, aKeyboardLayout=%08x (\"%s\"), sCodePage=%lu, " + "sIMEProperty=%s, sIMEUIProperty=%s", + aKeyboardLayout, NS_ConvertUTF16toUTF8(sIMEName).get(), sCodePage, + GetIMEGeneralPropertyName(sIMEProperty).get(), + GetIMEUIPropertyName(sIMEUIProperty).get())); +} + +// static +UINT IMMHandler::GetKeyboardCodePage() { return sCodePage; } + +// static +IMENotificationRequests IMMHandler::GetIMENotificationRequests() { + return IMENotificationRequests( + IMENotificationRequests::NOTIFY_POSITION_CHANGE | + IMENotificationRequests::NOTIFY_MOUSE_BUTTON_EVENT_ON_CHAR); +} + +// used for checking the lParam of WM_IME_COMPOSITION +#define IS_COMPOSING_LPARAM(lParam) \ + ((lParam) & (GCS_COMPSTR | GCS_COMPATTR | GCS_COMPCLAUSE | GCS_CURSORPOS)) +#define IS_COMMITTING_LPARAM(lParam) ((lParam)&GCS_RESULTSTR) +// Some IMEs (e.g., the standard IME for Korean) don't have caret position, +// then, we should not set caret position to compositionchange event. +#define NO_IME_CARET -1 + +IMMHandler::IMMHandler() + : mComposingWindow(nullptr), + mCursorPosition(NO_IME_CARET), + mCompositionStart(0), + mIsComposing(false) { + MOZ_LOG(gIMMLog, LogLevel::Debug, ("IMMHandler is created")); +} + +IMMHandler::~IMMHandler() { + if (mIsComposing) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("~IMMHandler, ERROR, the instance is still composing")); + } + MOZ_LOG(gIMMLog, LogLevel::Debug, ("IMMHandler is destroyed")); +} + +nsresult IMMHandler::EnsureClauseArray(int32_t aCount) { + NS_ENSURE_ARG_MIN(aCount, 0); + mClauseArray.SetCapacity(aCount + 32); + return NS_OK; +} + +nsresult IMMHandler::EnsureAttributeArray(int32_t aCount) { + NS_ENSURE_ARG_MIN(aCount, 0); + mAttributeArray.SetCapacity(aCount + 64); + return NS_OK; +} + +// static +void IMMHandler::CommitComposition(nsWindow* aWindow, bool aForce) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("CommitComposition, aForce=%s, aWindow=%p, hWnd=%08x, " + "mComposingWindow=%p%s", + GetBoolName(aForce), aWindow, aWindow->GetWindowHandle(), + gIMMHandler ? gIMMHandler->mComposingWindow : nullptr, + gIMMHandler && gIMMHandler->mComposingWindow + ? IsComposingOnOurEditor() ? " (composing on editor)" + : " (composing on plug-in)" + : "")); + if (!aForce && !IsComposingWindow(aWindow)) { + return; + } + + IMEContext context(aWindow); + bool associated = context.AssociateDefaultContext(); + MOZ_LOG(gIMMLog, LogLevel::Info, + ("CommitComposition, associated=%s", GetBoolName(associated))); + + if (context.IsValid()) { + ::ImmNotifyIME(context.get(), NI_COMPOSITIONSTR, CPS_COMPLETE, 0); + ::ImmNotifyIME(context.get(), NI_COMPOSITIONSTR, CPS_CANCEL, 0); + } + + if (associated) { + context.Disassociate(); + } +} + +// static +void IMMHandler::CancelComposition(nsWindow* aWindow, bool aForce) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("CancelComposition, aForce=%s, aWindow=%p, hWnd=%08x, " + "mComposingWindow=%p%s", + GetBoolName(aForce), aWindow, aWindow->GetWindowHandle(), + gIMMHandler ? gIMMHandler->mComposingWindow : nullptr, + gIMMHandler && gIMMHandler->mComposingWindow + ? IsComposingOnOurEditor() ? " (composing on editor)" + : " (composing on plug-in)" + : "")); + if (!aForce && !IsComposingWindow(aWindow)) { + return; + } + + IMEContext context(aWindow); + bool associated = context.AssociateDefaultContext(); + MOZ_LOG(gIMMLog, LogLevel::Info, + ("CancelComposition, associated=%s", GetBoolName(associated))); + + if (context.IsValid()) { + ::ImmNotifyIME(context.get(), NI_COMPOSITIONSTR, CPS_CANCEL, 0); + } + + if (associated) { + context.Disassociate(); + } +} + +// static +void IMMHandler::OnFocusChange(bool aFocus, nsWindow* aWindow) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnFocusChange(aFocus=%s, aWindow=%p), sHasFocus=%s, " + "IsComposingWindow(aWindow)=%s, aWindow->Destroyed()=%s", + GetBoolName(aFocus), aWindow, GetBoolName(sHasFocus), + GetBoolName(IsComposingWindow(aWindow)), + GetBoolName(aWindow->Destroyed()))); + + if (!aFocus) { + IMEHandler::MaybeDestroyNativeCaret(); + if (IsComposingWindow(aWindow) && aWindow->Destroyed()) { + CancelComposition(aWindow); + } + } + if (gIMMHandler) { + gIMMHandler->mSelection.Clear(); + } + sHasFocus = aFocus; +} + +// static +void IMMHandler::OnUpdateComposition(nsWindow* aWindow) { + if (!gIMMHandler) { + return; + } + + IMEContext context(aWindow); + gIMMHandler->SetIMERelatedWindowsPos(aWindow, context); +} + +// static +void IMMHandler::OnSelectionChange(nsWindow* aWindow, + const IMENotification& aIMENotification, + bool aIsIMMActive) { + if (!aIMENotification.mSelectionChangeData.mCausedByComposition && + aIsIMMActive) { + MaybeAdjustCompositionFont( + aWindow, aIMENotification.mSelectionChangeData.GetWritingMode()); + } + // MaybeAdjustCompositionFont() may create gIMMHandler. So, check it + // after a call of MaybeAdjustCompositionFont(). + if (gIMMHandler) { + gIMMHandler->mSelection.Update(aIMENotification); + } +} + +// static +void IMMHandler::MaybeAdjustCompositionFont(nsWindow* aWindow, + const WritingMode& aWritingMode, + bool aForceUpdate) { + switch (sCodePage) { + case 932: // Japanese Shift-JIS + case 936: // Simlified Chinese GBK + case 949: // Korean + case 950: // Traditional Chinese Big5 + EnsureHandlerInstance(); + break; + default: + // If there is no instance of nsIMM32Hander, we shouldn't waste footprint. + if (!gIMMHandler) { + return; + } + } + + // Like Navi-Bar of ATOK, some IMEs may require proper composition font even + // before sending WM_IME_STARTCOMPOSITION. + IMEContext context(aWindow); + gIMMHandler->AdjustCompositionFont(aWindow, context, aWritingMode, + aForceUpdate); +} + +// static +bool IMMHandler::ProcessInputLangChangeMessage(nsWindow* aWindow, WPARAM wParam, + LPARAM lParam, + MSGResult& aResult) { + aResult.mResult = 0; + aResult.mConsumed = false; + // We don't need to create the instance of the handler here. + if (gIMMHandler) { + gIMMHandler->OnInputLangChange(aWindow, wParam, lParam, aResult); + } + InitKeyboardLayout(aWindow, reinterpret_cast<HKL>(lParam)); + // We can release the instance here, because the instance may be never + // used. E.g., the new keyboard layout may not use IME, or it may use TSF. + Terminate(); + // Don't return as "processed", the messages should be processed on nsWindow + // too. + return false; +} + +// static +bool IMMHandler::ProcessMessage(nsWindow* aWindow, UINT msg, WPARAM& wParam, + LPARAM& lParam, MSGResult& aResult) { + // XXX We store the composing window in mComposingWindow. If IME messages are + // sent to different window, we should commit the old transaction. And also + // if the new window handle is not focused, probably, we should not start + // the composition, however, such case should not be, it's just bad scenario. + + aResult.mResult = 0; + switch (msg) { + case WM_INPUTLANGCHANGE: + return ProcessInputLangChangeMessage(aWindow, wParam, lParam, aResult); + case WM_IME_STARTCOMPOSITION: + EnsureHandlerInstance(); + return gIMMHandler->OnIMEStartComposition(aWindow, aResult); + case WM_IME_COMPOSITION: + EnsureHandlerInstance(); + return gIMMHandler->OnIMEComposition(aWindow, wParam, lParam, aResult); + case WM_IME_ENDCOMPOSITION: + EnsureHandlerInstance(); + return gIMMHandler->OnIMEEndComposition(aWindow, aResult); + case WM_IME_CHAR: + return OnIMEChar(aWindow, wParam, lParam, aResult); + case WM_IME_NOTIFY: + return OnIMENotify(aWindow, wParam, lParam, aResult); + case WM_IME_REQUEST: + EnsureHandlerInstance(); + return gIMMHandler->OnIMERequest(aWindow, wParam, lParam, aResult); + case WM_IME_SELECT: + return OnIMESelect(aWindow, wParam, lParam, aResult); + case WM_IME_SETCONTEXT: + return OnIMESetContext(aWindow, wParam, lParam, aResult); + case WM_KEYDOWN: + return OnKeyDownEvent(aWindow, wParam, lParam, aResult); + case WM_CHAR: + if (!gIMMHandler) { + return false; + } + return gIMMHandler->OnChar(aWindow, wParam, lParam, aResult); + default: + return false; + }; +} + +/**************************************************************************** + * message handlers + ****************************************************************************/ + +void IMMHandler::OnInputLangChange(nsWindow* aWindow, WPARAM wParam, + LPARAM lParam, MSGResult& aResult) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnInputLangChange, hWnd=%08x, wParam=%08x, lParam=%08x", + aWindow->GetWindowHandle(), wParam, lParam)); + + aWindow->NotifyIME(REQUEST_TO_COMMIT_COMPOSITION); + NS_ASSERTION(!mIsComposing, "ResetInputState failed"); + + if (mIsComposing) { + HandleEndComposition(aWindow); + } + + aResult.mConsumed = false; +} + +bool IMMHandler::OnIMEStartComposition(nsWindow* aWindow, MSGResult& aResult) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMEStartComposition, hWnd=%08x, mIsComposing=%s", + aWindow->GetWindowHandle(), GetBoolName(mIsComposing))); + aResult.mConsumed = ShouldDrawCompositionStringOurselves(); + if (mIsComposing) { + NS_WARNING("Composition has been already started"); + return true; + } + + IMEContext context(aWindow); + HandleStartComposition(aWindow, context); + return true; +} + +bool IMMHandler::OnIMEComposition(nsWindow* aWindow, WPARAM wParam, + LPARAM lParam, MSGResult& aResult) { + MOZ_LOG( + gIMMLog, LogLevel::Info, + ("OnIMEComposition, hWnd=%08x, lParam=%08x, mIsComposing=%s, " + "GCS_RESULTSTR=%s, GCS_COMPSTR=%s, GCS_COMPATTR=%s, GCS_COMPCLAUSE=%s, " + "GCS_CURSORPOS=%s,", + aWindow->GetWindowHandle(), lParam, GetBoolName(mIsComposing), + GetBoolName(lParam & GCS_RESULTSTR), GetBoolName(lParam & GCS_COMPSTR), + GetBoolName(lParam & GCS_COMPATTR), GetBoolName(lParam & GCS_COMPCLAUSE), + GetBoolName(lParam & GCS_CURSORPOS))); + + IMEContext context(aWindow); + aResult.mConsumed = HandleComposition(aWindow, context, lParam); + return true; +} + +bool IMMHandler::OnIMEEndComposition(nsWindow* aWindow, MSGResult& aResult) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMEEndComposition, hWnd=%08x, mIsComposing=%s", + aWindow->GetWindowHandle(), GetBoolName(mIsComposing))); + + aResult.mConsumed = ShouldDrawCompositionStringOurselves(); + if (!mIsComposing) { + return true; + } + + // Korean IME posts WM_IME_ENDCOMPOSITION first when we hit space during + // composition. Then, we should ignore the message and commit the composition + // string at following WM_IME_COMPOSITION. + MSG compositionMsg; + if (WinUtils::PeekMessage(&compositionMsg, aWindow->GetWindowHandle(), + WM_IME_STARTCOMPOSITION, WM_IME_COMPOSITION, + PM_NOREMOVE) && + compositionMsg.message == WM_IME_COMPOSITION && + IS_COMMITTING_LPARAM(compositionMsg.lParam)) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMEEndComposition, WM_IME_ENDCOMPOSITION is followed by " + "WM_IME_COMPOSITION, ignoring the message...")); + return true; + } + + // Otherwise, e.g., ChangJie doesn't post WM_IME_COMPOSITION before + // WM_IME_ENDCOMPOSITION when composition string becomes empty. + // Then, we should dispatch a compositionupdate event, a compositionchange + // event and a compositionend event. + // XXX Shouldn't we dispatch the compositionchange event with actual or + // latest composition string? + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMEEndComposition, mCompositionString=\"%s\"%s", + NS_ConvertUTF16toUTF8(mCompositionString).get(), + mCompositionString.IsEmpty() ? "" : ", but canceling it...")); + + HandleEndComposition(aWindow, &EmptyString()); + + return true; +} + +// static +bool IMMHandler::OnIMEChar(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult) { + MOZ_LOG( + gIMMLog, LogLevel::Info, + ("OnIMEChar, hWnd=%08x, char=%08x", aWindow->GetWindowHandle(), wParam)); + + // We don't need to fire any compositionchange events from here. This method + // will be called when the composition string of the current IME is not drawn + // by us and some characters are committed. In that case, the committed + // string was processed in nsWindow::OnIMEComposition already. + + // We need to consume the message so that Windows don't send two WM_CHAR msgs + aResult.mConsumed = true; + return true; +} + +// static +bool IMMHandler::OnIMECompositionFull(nsWindow* aWindow, MSGResult& aResult) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMECompositionFull, hWnd=%08x", aWindow->GetWindowHandle())); + + // not implement yet + aResult.mConsumed = false; + return true; +} + +// static +bool IMMHandler::OnIMENotify(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult) { + switch (wParam) { + case IMN_CHANGECANDIDATE: + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMENotify, hWnd=%08x, IMN_CHANGECANDIDATE, lParam=%08x", + aWindow->GetWindowHandle(), lParam)); + break; + case IMN_CLOSECANDIDATE: + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMENotify, hWnd=%08x, IMN_CLOSECANDIDATE, lParam=%08x", + aWindow->GetWindowHandle(), lParam)); + break; + case IMN_CLOSESTATUSWINDOW: + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMENotify, hWnd=%08x, IMN_CLOSESTATUSWINDOW", + aWindow->GetWindowHandle())); + break; + case IMN_GUIDELINE: + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMENotify, hWnd=%08x, IMN_GUIDELINE", + aWindow->GetWindowHandle())); + break; + case IMN_OPENCANDIDATE: + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMENotify, hWnd=%08x, IMN_OPENCANDIDATE, lParam=%08x", + aWindow->GetWindowHandle(), lParam)); + break; + case IMN_OPENSTATUSWINDOW: + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMENotify, hWnd=%08x, IMN_OPENSTATUSWINDOW", + aWindow->GetWindowHandle())); + break; + case IMN_SETCANDIDATEPOS: + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMENotify, hWnd=%08x, IMN_SETCANDIDATEPOS, lParam=%08x", + aWindow->GetWindowHandle(), lParam)); + break; + case IMN_SETCOMPOSITIONFONT: + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMENotify, hWnd=%08x, IMN_SETCOMPOSITIONFONT", + aWindow->GetWindowHandle())); + break; + case IMN_SETCOMPOSITIONWINDOW: + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMENotify, hWnd=%08x, IMN_SETCOMPOSITIONWINDOW", + aWindow->GetWindowHandle())); + break; + case IMN_SETCONVERSIONMODE: + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMENotify, hWnd=%08x, IMN_SETCONVERSIONMODE", + aWindow->GetWindowHandle())); + break; + case IMN_SETOPENSTATUS: + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMENotify, hWnd=%08x, IMN_SETOPENSTATUS", + aWindow->GetWindowHandle())); + break; + case IMN_SETSENTENCEMODE: + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMENotify, hWnd=%08x, IMN_SETSENTENCEMODE", + aWindow->GetWindowHandle())); + break; + case IMN_SETSTATUSWINDOWPOS: + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMENotify, hWnd=%08x, IMN_SETSTATUSWINDOWPOS", + aWindow->GetWindowHandle())); + break; + case IMN_PRIVATE: + MOZ_LOG( + gIMMLog, LogLevel::Info, + ("OnIMENotify, hWnd=%08x, IMN_PRIVATE", aWindow->GetWindowHandle())); + break; + } + + // not implement yet + aResult.mConsumed = false; + return true; +} + +bool IMMHandler::OnIMERequest(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult) { + switch (wParam) { + case IMR_RECONVERTSTRING: + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMERequest, hWnd=%08x, IMR_RECONVERTSTRING", + aWindow->GetWindowHandle())); + aResult.mConsumed = HandleReconvert(aWindow, lParam, &aResult.mResult); + return true; + case IMR_QUERYCHARPOSITION: + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMERequest, hWnd=%08x, IMR_QUERYCHARPOSITION", + aWindow->GetWindowHandle())); + aResult.mConsumed = + HandleQueryCharPosition(aWindow, lParam, &aResult.mResult); + return true; + case IMR_DOCUMENTFEED: + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMERequest, hWnd=%08x, IMR_DOCUMENTFEED", + aWindow->GetWindowHandle())); + aResult.mConsumed = HandleDocumentFeed(aWindow, lParam, &aResult.mResult); + return true; + default: + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMERequest, hWnd=%08x, wParam=%08x", + aWindow->GetWindowHandle(), wParam)); + aResult.mConsumed = false; + return true; + } +} + +// static +bool IMMHandler::OnIMESelect(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMESelect, hWnd=%08x, wParam=%08x, lParam=%08x", + aWindow->GetWindowHandle(), wParam, lParam)); + + // not implement yet + aResult.mConsumed = false; + return true; +} + +// static +bool IMMHandler::OnIMESetContext(nsWindow* aWindow, WPARAM wParam, + LPARAM lParam, MSGResult& aResult) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMESetContext, hWnd=%08x, %s, lParam=%08x", + aWindow->GetWindowHandle(), wParam ? "Active" : "Deactive", lParam)); + + aResult.mConsumed = false; + + // NOTE: If the aWindow is top level window of the composing window because + // when a window on deactive window gets focus, WM_IME_SETCONTEXT (wParam is + // TRUE) is sent to the top level window first. After that, + // WM_IME_SETCONTEXT (wParam is FALSE) is sent to the top level window. + // Finally, WM_IME_SETCONTEXT (wParam is TRUE) is sent to the focused window. + // The top level window never becomes composing window, so, we can ignore + // the WM_IME_SETCONTEXT on the top level window. + if (IsTopLevelWindowOfComposition(aWindow)) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMESetContext, hWnd=%08x is top level window")); + return true; + } + + // When IME context is activating on another window, + // we should commit the old composition on the old window. + bool cancelComposition = false; + if (wParam && gIMMHandler) { + cancelComposition = gIMMHandler->CommitCompositionOnPreviousWindow(aWindow); + } + + if (wParam && (lParam & ISC_SHOWUICOMPOSITIONWINDOW) && + ShouldDrawCompositionStringOurselves()) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnIMESetContext, ISC_SHOWUICOMPOSITIONWINDOW is removed")); + lParam &= ~ISC_SHOWUICOMPOSITIONWINDOW; + } + + // We should sent WM_IME_SETCONTEXT to the DefWndProc here because the + // ancestor windows shouldn't receive this message. If they receive the + // message, we cannot know whether which window is the target of the message. + aResult.mResult = ::DefWindowProc(aWindow->GetWindowHandle(), + WM_IME_SETCONTEXT, wParam, lParam); + + // Cancel composition on the new window if we committed our composition on + // another window. + if (cancelComposition) { + CancelComposition(aWindow, true); + } + + aResult.mConsumed = true; + return true; +} + +bool IMMHandler::OnChar(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult) { + // The return value must be same as aResult.mConsumed because only when we + // consume the message, the caller shouldn't do anything anymore but + // otherwise, the caller should handle the message. + aResult.mConsumed = false; + if (IsIMECharRecordsEmpty()) { + return aResult.mConsumed; + } + WPARAM recWParam; + LPARAM recLParam; + DequeueIMECharRecords(recWParam, recLParam); + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnChar, aWindow=%p, wParam=%08x, lParam=%08x, " + "recorded: wParam=%08x, lParam=%08x", + aWindow->GetWindowHandle(), wParam, lParam, recWParam, recLParam)); + // If an unexpected char message comes, we should reset the records, + // of course, this shouldn't happen. + if (recWParam != wParam || recLParam != lParam) { + ResetIMECharRecords(); + return aResult.mConsumed; + } + // Eat the char message which is caused by WM_IME_CHAR because we should + // have processed the IME messages, so, this message could be come from + // a windowless plug-in. + aResult.mConsumed = true; + return aResult.mConsumed; +} + +/**************************************************************************** + * others + ****************************************************************************/ + +TextEventDispatcher* IMMHandler::GetTextEventDispatcherFor(nsWindow* aWindow) { + return aWindow == mComposingWindow && mDispatcher + ? mDispatcher.get() + : aWindow->GetTextEventDispatcher(); +} + +void IMMHandler::HandleStartComposition(nsWindow* aWindow, + const IMEContext& aContext) { + MOZ_ASSERT(!mIsComposing, + "HandleStartComposition is called but mIsComposing is TRUE"); + + Selection& selection = GetSelection(); + if (!selection.EnsureValidSelection(aWindow)) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleStartComposition, FAILED, due to " + "Selection::EnsureValidSelection() failure")); + return; + } + + AdjustCompositionFont(aWindow, aContext, selection.mWritingMode); + + mCompositionStart = selection.mOffset; + mCursorPosition = NO_IME_CARET; + + RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcherFor(aWindow); + nsresult rv = dispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleStartComposition, FAILED due to " + "TextEventDispatcher::BeginNativeInputTransaction() failure")); + return; + } + WidgetEventTime eventTime = aWindow->CurrentMessageWidgetEventTime(); + nsEventStatus status; + rv = dispatcher->StartComposition(status, &eventTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleStartComposition, FAILED, due to " + "TextEventDispatcher::StartComposition() failure")); + return; + } + + mIsComposing = true; + mComposingWindow = aWindow; + mDispatcher = dispatcher; + + MOZ_LOG(gIMMLog, LogLevel::Info, + ("HandleStartComposition, START composition, mCompositionStart=%ld", + mCompositionStart)); +} + +bool IMMHandler::HandleComposition(nsWindow* aWindow, + const IMEContext& aContext, LPARAM lParam) { + // for bug #60050 + // MS-IME 95/97/98/2000 may send WM_IME_COMPOSITION with non-conversion + // mode before it send WM_IME_STARTCOMPOSITION. + // However, ATOK sends a WM_IME_COMPOSITION before WM_IME_STARTCOMPOSITION, + // and if we access ATOK via some APIs, ATOK will sometimes fail to + // initialize its state. If WM_IME_STARTCOMPOSITION is already in the + // message queue, we should ignore the strange WM_IME_COMPOSITION message and + // skip to the next. So, we should look for next composition message + // (WM_IME_STARTCOMPOSITION or WM_IME_ENDCOMPOSITION or WM_IME_COMPOSITION), + // and if it's WM_IME_STARTCOMPOSITION, and one more next composition message + // is WM_IME_COMPOSITION, current IME is ATOK, probably. Otherwise, we + // should start composition forcibly. + if (!mIsComposing) { + MSG msg1, msg2; + HWND wnd = aWindow->GetWindowHandle(); + if (WinUtils::PeekMessage(&msg1, wnd, WM_IME_STARTCOMPOSITION, + WM_IME_COMPOSITION, PM_NOREMOVE) && + msg1.message == WM_IME_STARTCOMPOSITION && + WinUtils::PeekMessage(&msg2, wnd, WM_IME_ENDCOMPOSITION, + WM_IME_COMPOSITION, PM_NOREMOVE) && + msg2.message == WM_IME_COMPOSITION) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("HandleComposition, Ignores due to find a " + "WM_IME_STARTCOMPOSITION")); + return ShouldDrawCompositionStringOurselves(); + } + } + + bool startCompositionMessageHasBeenSent = mIsComposing; + + // + // This catches a fixed result + // + if (IS_COMMITTING_LPARAM(lParam)) { + if (!mIsComposing) { + HandleStartComposition(aWindow, aContext); + } + + GetCompositionString(aContext, GCS_RESULTSTR, mCompositionString); + + MOZ_LOG(gIMMLog, LogLevel::Info, ("HandleComposition, GCS_RESULTSTR")); + + HandleEndComposition(aWindow, &mCompositionString); + + if (!IS_COMPOSING_LPARAM(lParam)) { + return ShouldDrawCompositionStringOurselves(); + } + } + + // + // This provides us with a composition string + // + if (!mIsComposing) { + HandleStartComposition(aWindow, aContext); + } + + //-------------------------------------------------------- + // 1. Get GCS_COMPSTR + //-------------------------------------------------------- + MOZ_LOG(gIMMLog, LogLevel::Info, ("HandleComposition, GCS_COMPSTR")); + + nsAutoString previousCompositionString(mCompositionString); + GetCompositionString(aContext, GCS_COMPSTR, mCompositionString); + + if (!IS_COMPOSING_LPARAM(lParam)) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("HandleComposition, lParam doesn't indicate composing, " + "mCompositionString=\"%s\", previousCompositionString=\"%s\"", + NS_ConvertUTF16toUTF8(mCompositionString).get(), + NS_ConvertUTF16toUTF8(previousCompositionString).get())); + + // If composition string isn't changed, we can trust the lParam. + // So, we need to do nothing. + if (previousCompositionString == mCompositionString) { + return ShouldDrawCompositionStringOurselves(); + } + + // IME may send WM_IME_COMPOSITION without composing lParam values + // when composition string becomes empty (e.g., using Backspace key). + // If composition string is empty, we should dispatch a compositionchange + // event with empty string and clear the clause information. + if (mCompositionString.IsEmpty()) { + mClauseArray.Clear(); + mAttributeArray.Clear(); + mCursorPosition = 0; + DispatchCompositionChangeEvent(aWindow, aContext); + return ShouldDrawCompositionStringOurselves(); + } + + // Otherwise, we cannot trust the lParam value. We might need to + // dispatch compositionchange event with the latest composition string + // information. + } + + // See https://bugzilla.mozilla.org/show_bug.cgi?id=296339 + if (mCompositionString.IsEmpty() && !startCompositionMessageHasBeenSent) { + // In this case, maybe, the sender is MSPinYin. That sends *only* + // WM_IME_COMPOSITION with GCS_COMP* and GCS_RESULT* when + // user inputted the Chinese full stop. So, that doesn't send + // WM_IME_STARTCOMPOSITION and WM_IME_ENDCOMPOSITION. + // If WM_IME_STARTCOMPOSITION was not sent and the composition + // string is null (it indicates the composition transaction ended), + // WM_IME_ENDCOMPOSITION may not be sent. If so, we cannot run + // HandleEndComposition() in other place. + MOZ_LOG(gIMMLog, LogLevel::Info, + ("HandleComposition, Aborting GCS_COMPSTR")); + HandleEndComposition(aWindow); + return IS_COMMITTING_LPARAM(lParam); + } + + //-------------------------------------------------------- + // 2. Get GCS_COMPCLAUSE + //-------------------------------------------------------- + long clauseArrayLength = + ::ImmGetCompositionStringW(aContext.get(), GCS_COMPCLAUSE, nullptr, 0); + clauseArrayLength /= sizeof(uint32_t); + + if (clauseArrayLength > 0) { + nsresult rv = EnsureClauseArray(clauseArrayLength); + NS_ENSURE_SUCCESS(rv, false); + + // Intelligent ABC IME (Simplified Chinese IME, the code page is 936) + // will crash in ImmGetCompositionStringW for GCS_COMPCLAUSE (bug 424663). + // See comment 35 of the bug for the detail. Therefore, we should use A + // API for it, however, we should not kill Unicode support on all IMEs. + bool useA_API = !(sIMEProperty & IME_PROP_UNICODE); + + MOZ_LOG(gIMMLog, LogLevel::Info, + ("HandleComposition, GCS_COMPCLAUSE, useA_API=%s", + useA_API ? "TRUE" : "FALSE")); + + long clauseArrayLength2 = + useA_API ? ::ImmGetCompositionStringA( + aContext.get(), GCS_COMPCLAUSE, mClauseArray.Elements(), + mClauseArray.Capacity() * sizeof(uint32_t)) + : ::ImmGetCompositionStringW( + aContext.get(), GCS_COMPCLAUSE, mClauseArray.Elements(), + mClauseArray.Capacity() * sizeof(uint32_t)); + clauseArrayLength2 /= sizeof(uint32_t); + + if (clauseArrayLength != clauseArrayLength2) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("HandleComposition, GCS_COMPCLAUSE, clauseArrayLength=%ld but " + "clauseArrayLength2=%ld", + clauseArrayLength, clauseArrayLength2)); + if (clauseArrayLength > clauseArrayLength2) + clauseArrayLength = clauseArrayLength2; + } + + if (useA_API && clauseArrayLength > 0) { + // Convert each values of sIMECompClauseArray. The values mean offset of + // the clauses in ANSI string. But we need the values in Unicode string. + nsAutoCString compANSIStr; + if (ConvertToANSIString(mCompositionString, GetKeyboardCodePage(), + compANSIStr)) { + uint32_t maxlen = compANSIStr.Length(); + mClauseArray.SetLength(clauseArrayLength); + mClauseArray[0] = 0; // first value must be 0 + for (int32_t i = 1; i < clauseArrayLength; i++) { + uint32_t len = std::min(mClauseArray[i], maxlen); + mClauseArray[i] = + ::MultiByteToWideChar(GetKeyboardCodePage(), MB_PRECOMPOSED, + (LPCSTR)compANSIStr.get(), len, nullptr, 0); + } + } + } + } + // compClauseArrayLength may be negative. I.e., ImmGetCompositionStringW + // may return an error code. + mClauseArray.SetLength(std::max<long>(0, clauseArrayLength)); + + MOZ_LOG(gIMMLog, LogLevel::Info, + ("HandleComposition, GCS_COMPCLAUSE, mClauseLength=%ld", + mClauseArray.Length())); + + //-------------------------------------------------------- + // 3. Get GCS_COMPATTR + //-------------------------------------------------------- + // This provides us with the attribute string necessary + // for doing hiliting + long attrArrayLength = + ::ImmGetCompositionStringW(aContext.get(), GCS_COMPATTR, nullptr, 0); + attrArrayLength /= sizeof(uint8_t); + + if (attrArrayLength > 0) { + nsresult rv = EnsureAttributeArray(attrArrayLength); + NS_ENSURE_SUCCESS(rv, false); + attrArrayLength = ::ImmGetCompositionStringW( + aContext.get(), GCS_COMPATTR, mAttributeArray.Elements(), + mAttributeArray.Capacity() * sizeof(uint8_t)); + } + + // attrStrLen may be negative. I.e., ImmGetCompositionStringW may return an + // error code. + mAttributeArray.SetLength(std::max<long>(0, attrArrayLength)); + + MOZ_LOG(gIMMLog, LogLevel::Info, + ("HandleComposition, GCS_COMPATTR, mAttributeLength=%ld", + mAttributeArray.Length())); + + //-------------------------------------------------------- + // 4. Get GCS_CURSOPOS + //-------------------------------------------------------- + // Some IMEs (e.g., the standard IME for Korean) don't have caret position. + if (lParam & GCS_CURSORPOS) { + mCursorPosition = + ::ImmGetCompositionStringW(aContext.get(), GCS_CURSORPOS, nullptr, 0); + if (mCursorPosition < 0) { + mCursorPosition = NO_IME_CARET; // The result is error + } + } else { + mCursorPosition = NO_IME_CARET; + } + + NS_ASSERTION(mCursorPosition <= (long)mCompositionString.Length(), + "illegal pos"); + + MOZ_LOG(gIMMLog, LogLevel::Info, + ("HandleComposition, GCS_CURSORPOS, mCursorPosition=%d", + mCursorPosition)); + + //-------------------------------------------------------- + // 5. Send the compositionchange event + //-------------------------------------------------------- + DispatchCompositionChangeEvent(aWindow, aContext); + + return ShouldDrawCompositionStringOurselves(); +} + +void IMMHandler::HandleEndComposition(nsWindow* aWindow, + const nsAString* aCommitString) { + MOZ_ASSERT(mIsComposing, + "HandleEndComposition is called but mIsComposing is FALSE"); + + MOZ_LOG(gIMMLog, LogLevel::Info, + ("HandleEndComposition(aWindow=0x%p, aCommitString=0x%p (\"%s\"))", + aWindow, aCommitString, + aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : "")); + + IMEHandler::MaybeDestroyNativeCaret(); + + RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcherFor(aWindow); + nsresult rv = dispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleEndComposition, FAILED due to " + "TextEventDispatcher::BeginNativeInputTransaction() failure")); + return; + } + WidgetEventTime eventTime = aWindow->CurrentMessageWidgetEventTime(); + nsEventStatus status; + rv = dispatcher->CommitComposition(status, aCommitString, &eventTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleStartComposition, FAILED, due to " + "TextEventDispatcher::CommitComposition() failure")); + return; + } + mIsComposing = false; + // XXX aWindow and mComposingWindow are always same?? + mComposingWindow = nullptr; + mDispatcher = nullptr; +} + +bool IMMHandler::HandleReconvert(nsWindow* aWindow, LPARAM lParam, + LRESULT* oResult) { + *oResult = 0; + RECONVERTSTRING* pReconv = reinterpret_cast<RECONVERTSTRING*>(lParam); + + Selection& selection = GetSelection(); + if (!selection.EnsureValidSelection(aWindow)) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleReconvert, FAILED, due to " + "Selection::EnsureValidSelection() failure")); + return false; + } + + uint32_t len = selection.Length(); + uint32_t needSize = sizeof(RECONVERTSTRING) + len * sizeof(WCHAR); + + if (!pReconv) { + // Return need size to reconvert. + if (len == 0) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleReconvert, There are not selected text")); + return false; + } + *oResult = needSize; + MOZ_LOG(gIMMLog, LogLevel::Info, + ("HandleReconvert, succeeded, result=%ld", *oResult)); + return true; + } + + if (pReconv->dwSize < needSize) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("HandleReconvert, FAILED, pReconv->dwSize=%ld, needSize=%ld", + pReconv->dwSize, needSize)); + return false; + } + + *oResult = needSize; + + // Fill reconvert struct + pReconv->dwVersion = 0; + pReconv->dwStrLen = len; + pReconv->dwStrOffset = sizeof(RECONVERTSTRING); + pReconv->dwCompStrLen = len; + pReconv->dwCompStrOffset = 0; + pReconv->dwTargetStrLen = len; + pReconv->dwTargetStrOffset = 0; + + ::CopyMemory(reinterpret_cast<LPVOID>(lParam + sizeof(RECONVERTSTRING)), + selection.mString.get(), len * sizeof(WCHAR)); + + MOZ_LOG(gIMMLog, LogLevel::Info, + ("HandleReconvert, SUCCEEDED, pReconv=%s, result=%ld", + GetReconvertStringLog(pReconv).get(), *oResult)); + + return true; +} + +bool IMMHandler::HandleQueryCharPosition(nsWindow* aWindow, LPARAM lParam, + LRESULT* oResult) { + uint32_t len = mIsComposing ? mCompositionString.Length() : 0; + *oResult = false; + IMECHARPOSITION* pCharPosition = reinterpret_cast<IMECHARPOSITION*>(lParam); + if (!pCharPosition) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleQueryCharPosition, FAILED, due to pCharPosition is null")); + return false; + } + if (pCharPosition->dwSize < sizeof(IMECHARPOSITION)) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleReconvert, FAILED, pCharPosition->dwSize=%ld, " + "sizeof(IMECHARPOSITION)=%ld", + pCharPosition->dwSize, sizeof(IMECHARPOSITION))); + return false; + } + if (::GetFocus() != aWindow->GetWindowHandle()) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleReconvert, FAILED, ::GetFocus()=%08x, OurWindowHandle=%08x", + ::GetFocus(), aWindow->GetWindowHandle())); + return false; + } + if (pCharPosition->dwCharPos > len) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleQueryCharPosition, FAILED, pCharPosition->dwCharPos=%ld, " + "len=%ld", + pCharPosition->dwCharPos, len)); + return false; + } + + LayoutDeviceIntRect r; + bool ret = + GetCharacterRectOfSelectedTextAt(aWindow, pCharPosition->dwCharPos, r); + NS_ENSURE_TRUE(ret, false); + + LayoutDeviceIntRect screenRect; + // We always need top level window that is owner window of the popup window + // even if the content of the popup window has focus. + ResolveIMECaretPos(aWindow->GetTopLevelWindow(false), r, nullptr, screenRect); + + // XXX This might need to check writing mode. However, MSDN doesn't explain + // how to set the values in vertical writing mode. Additionally, IME + // doesn't work well with top-left of the character (this is explicitly + // documented) and its horizontal width. So, it might be better to set + // top-right corner of the character and horizontal width, but we're not + // sure if it doesn't cause any problems with a lot of IMEs... + pCharPosition->pt.x = screenRect.X(); + pCharPosition->pt.y = screenRect.Y(); + + pCharPosition->cLineHeight = r.Height(); + + WidgetQueryContentEvent queryEditorRectEvent(true, eQueryEditorRect, aWindow); + aWindow->InitEvent(queryEditorRectEvent); + DispatchEvent(aWindow, queryEditorRectEvent); + if (NS_WARN_IF(queryEditorRectEvent.Failed())) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleQueryCharPosition, eQueryEditorRect failed")); + ::GetWindowRect(aWindow->GetWindowHandle(), &pCharPosition->rcDocument); + } else { + LayoutDeviceIntRect editorRectInWindow = queryEditorRectEvent.mReply->mRect; + nsWindow* window = !!queryEditorRectEvent.mReply->mFocusedWidget + ? static_cast<nsWindow*>( + queryEditorRectEvent.mReply->mFocusedWidget) + : aWindow; + LayoutDeviceIntRect editorRectInScreen; + ResolveIMECaretPos(window, editorRectInWindow, nullptr, editorRectInScreen); + ::SetRect(&pCharPosition->rcDocument, editorRectInScreen.X(), + editorRectInScreen.Y(), editorRectInScreen.XMost(), + editorRectInScreen.YMost()); + } + + *oResult = TRUE; + + MOZ_LOG(gIMMLog, LogLevel::Info, + ("HandleQueryCharPosition, SUCCEEDED, pCharPosition={ pt={ x=%d, " + "y=%d }, cLineHeight=%d, rcDocument={ left=%d, top=%d, right=%d, " + "bottom=%d } }", + pCharPosition->pt.x, pCharPosition->pt.y, pCharPosition->cLineHeight, + pCharPosition->rcDocument.left, pCharPosition->rcDocument.top, + pCharPosition->rcDocument.right, pCharPosition->rcDocument.bottom)); + return true; +} + +bool IMMHandler::HandleDocumentFeed(nsWindow* aWindow, LPARAM lParam, + LRESULT* oResult) { + *oResult = 0; + RECONVERTSTRING* pReconv = reinterpret_cast<RECONVERTSTRING*>(lParam); + + LayoutDeviceIntPoint point(0, 0); + + bool hasCompositionString = + mIsComposing && ShouldDrawCompositionStringOurselves(); + + int32_t targetOffset, targetLength; + if (!hasCompositionString) { + Selection& selection = GetSelection(); + if (!selection.EnsureValidSelection(aWindow)) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleDocumentFeed, FAILED, due to " + "Selection::EnsureValidSelection() failure")); + return false; + } + targetOffset = int32_t(selection.mOffset); + targetLength = int32_t(selection.Length()); + } else { + targetOffset = int32_t(mCompositionStart); + targetLength = int32_t(mCompositionString.Length()); + } + + // XXX nsString::Find and nsString::RFind take int32_t for offset, so, + // we cannot support this message when the current offset is larger than + // INT32_MAX. + if (targetOffset < 0 || targetLength < 0 || targetOffset + targetLength < 0) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleDocumentFeed, FAILED, due to the selection is out of " + "range")); + return false; + } + + // Get all contents of the focused editor. + WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent, + aWindow); + queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX); + aWindow->InitEvent(queryTextContentEvent, &point); + DispatchEvent(aWindow, queryTextContentEvent); + if (NS_WARN_IF(queryTextContentEvent.Failed())) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleDocumentFeed, FAILED, due to eQueryTextContent failure")); + return false; + } + + nsAutoString str(queryTextContentEvent.mReply->DataRef()); + if (targetOffset > static_cast<int32_t>(str.Length())) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleDocumentFeed, FAILED, due to the caret offset is invalid")); + return false; + } + + // Get the focused paragraph, we decide that it starts from the previous CRLF + // (or start of the editor) to the next one (or the end of the editor). + int32_t paragraphStart = str.RFind("\n", false, targetOffset, -1) + 1; + int32_t paragraphEnd = str.Find("\r", false, targetOffset + targetLength, -1); + if (paragraphEnd < 0) { + paragraphEnd = str.Length(); + } + nsDependentSubstring paragraph(str, paragraphStart, + paragraphEnd - paragraphStart); + + uint32_t len = paragraph.Length(); + uint32_t needSize = sizeof(RECONVERTSTRING) + len * sizeof(WCHAR); + + if (!pReconv) { + *oResult = needSize; + MOZ_LOG(gIMMLog, LogLevel::Info, + ("HandleDocumentFeed, succeeded, result=%ld", *oResult)); + return true; + } + + if (pReconv->dwSize < needSize) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleDocumentFeed, FAILED, pReconv->dwSize=%ld, needSize=%ld", + pReconv->dwSize, needSize)); + return false; + } + + // Fill reconvert struct + pReconv->dwVersion = 0; + pReconv->dwStrLen = len; + pReconv->dwStrOffset = sizeof(RECONVERTSTRING); + if (hasCompositionString) { + pReconv->dwCompStrLen = targetLength; + pReconv->dwCompStrOffset = (targetOffset - paragraphStart) * sizeof(WCHAR); + // Set composition target clause information + uint32_t offset, length; + if (!GetTargetClauseRange(&offset, &length)) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("HandleDocumentFeed, FAILED, due to GetTargetClauseRange() " + "failure")); + return false; + } + pReconv->dwTargetStrLen = length; + pReconv->dwTargetStrOffset = (offset - paragraphStart) * sizeof(WCHAR); + } else { + pReconv->dwTargetStrLen = targetLength; + pReconv->dwTargetStrOffset = + (targetOffset - paragraphStart) * sizeof(WCHAR); + // There is no composition string, so, the length is zero but we should + // set the cursor offset to the composition str offset. + pReconv->dwCompStrLen = 0; + pReconv->dwCompStrOffset = pReconv->dwTargetStrOffset; + } + + *oResult = needSize; + ::CopyMemory(reinterpret_cast<LPVOID>(lParam + sizeof(RECONVERTSTRING)), + paragraph.BeginReading(), len * sizeof(WCHAR)); + + MOZ_LOG(gIMMLog, LogLevel::Info, + ("HandleDocumentFeed, SUCCEEDED, pReconv=%s, result=%ld", + GetReconvertStringLog(pReconv).get(), *oResult)); + + return true; +} + +bool IMMHandler::CommitCompositionOnPreviousWindow(nsWindow* aWindow) { + if (!mComposingWindow || mComposingWindow == aWindow) { + return false; + } + + MOZ_LOG(gIMMLog, LogLevel::Info, + ("CommitCompositionOnPreviousWindow, mIsComposing=%s", + GetBoolName(mIsComposing))); + + // If we have composition, we should dispatch composition events internally. + if (mIsComposing) { + IMEContext context(mComposingWindow); + NS_ASSERTION(context.IsValid(), "IME context must be valid"); + + HandleEndComposition(mComposingWindow); + return true; + } + + return false; +} + +static TextRangeType PlatformToNSAttr(uint8_t aAttr) { + switch (aAttr) { + case ATTR_INPUT_ERROR: + // case ATTR_FIXEDCONVERTED: + case ATTR_INPUT: + return TextRangeType::eRawClause; + case ATTR_CONVERTED: + return TextRangeType::eConvertedClause; + case ATTR_TARGET_NOTCONVERTED: + return TextRangeType::eSelectedRawClause; + case ATTR_TARGET_CONVERTED: + return TextRangeType::eSelectedClause; + default: + NS_ASSERTION(false, "unknown attribute"); + return TextRangeType::eCaret; + } +} + +// static +void IMMHandler::DispatchEvent(nsWindow* aWindow, WidgetGUIEvent& aEvent) { + MOZ_LOG( + gIMMLog, LogLevel::Info, + ("DispatchEvent(aWindow=0x%p, aEvent={ mMessage=%s }, " + "aWindow->Destroyed()=%s", + aWindow, ToChar(aEvent.mMessage), GetBoolName(aWindow->Destroyed()))); + + if (aWindow->Destroyed()) { + return; + } + + aWindow->DispatchWindowEvent(&aEvent); +} + +void IMMHandler::DispatchCompositionChangeEvent(nsWindow* aWindow, + const IMEContext& aContext) { + NS_ASSERTION(mIsComposing, "conflict state"); + MOZ_LOG(gIMMLog, LogLevel::Info, ("DispatchCompositionChangeEvent")); + + // If we don't need to draw composition string ourselves, we don't need to + // fire compositionchange event during composing. + if (!ShouldDrawCompositionStringOurselves()) { + // But we need to adjust composition window pos and native caret pos, here. + SetIMERelatedWindowsPos(aWindow, aContext); + return; + } + + RefPtr<nsWindow> kungFuDeathGrip(aWindow); + RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcherFor(aWindow); + nsresult rv = dispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("DispatchCompositionChangeEvent, FAILED due to " + "TextEventDispatcher::BeginNativeInputTransaction() failure")); + return; + } + + // NOTE: Calling SetIMERelatedWindowsPos() from this method will be failure + // in e10s mode. compositionchange event will notify this of + // NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED, then + // SetIMERelatedWindowsPos() will be called. + + // XXX Sogou (Simplified Chinese IME) returns contradictory values: + // The cursor position is actual cursor position. However, other values + // (composition string and attributes) are empty. + + if (mCompositionString.IsEmpty()) { + // Don't append clause information if composition string is empty. + } else if (mClauseArray.IsEmpty()) { + // Some IMEs don't return clause array information, then, we assume that + // all characters in the composition string are in one clause. + MOZ_LOG(gIMMLog, LogLevel::Info, + ("DispatchCompositionChangeEvent, mClauseArray.Length()=0")); + rv = dispatcher->SetPendingComposition(mCompositionString, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("DispatchCompositionChangeEvent, FAILED due to" + "TextEventDispatcher::SetPendingComposition() failure")); + return; + } + } else { + // iterate over the attributes + rv = dispatcher->SetPendingCompositionString(mCompositionString); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("DispatchCompositionChangeEvent, FAILED due to" + "TextEventDispatcher::SetPendingCompositionString() failure")); + return; + } + uint32_t lastOffset = 0; + for (uint32_t i = 0; i < mClauseArray.Length() - 1; i++) { + uint32_t current = mClauseArray[i + 1]; + if (current > mCompositionString.Length()) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("DispatchCompositionChangeEvent, mClauseArray[%ld]=%lu. " + "This is larger than mCompositionString.Length()=%lu", + i + 1, current, mCompositionString.Length())); + current = int32_t(mCompositionString.Length()); + } + + uint32_t length = current - lastOffset; + if (NS_WARN_IF(lastOffset >= mAttributeArray.Length())) { + MOZ_LOG( + gIMMLog, LogLevel::Error, + ("DispatchCompositionChangeEvent, FAILED due to invalid data of " + "mClauseArray or mAttributeArray")); + return; + } + TextRangeType textRangeType = + PlatformToNSAttr(mAttributeArray[lastOffset]); + rv = dispatcher->AppendClauseToPendingComposition(length, textRangeType); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("DispatchCompositionChangeEvent, FAILED due to" + "TextEventDispatcher::AppendClauseToPendingComposition() " + "failure")); + return; + } + + lastOffset = current; + + MOZ_LOG(gIMMLog, LogLevel::Info, + ("DispatchCompositionChangeEvent, index=%ld, rangeType=%s, " + "range length=%lu", + i, ToChar(textRangeType), length)); + } + } + + if (mCursorPosition == NO_IME_CARET) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("DispatchCompositionChangeEvent, no caret")); + } else { + uint32_t cursor = static_cast<uint32_t>(mCursorPosition); + if (cursor > mCompositionString.Length()) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("CreateTextRangeArray, mCursorPosition=%ld. " + "This is larger than mCompositionString.Length()=%lu", + mCursorPosition, mCompositionString.Length())); + cursor = mCompositionString.Length(); + } + + // If caret is in the target clause, the target clause will be painted as + // normal selection range. Since caret shouldn't be in selection range on + // Windows, we shouldn't append caret range in such case. + const TextRangeArray* clauses = dispatcher->GetPendingCompositionClauses(); + const TextRange* targetClause = + clauses ? clauses->GetTargetClause() : nullptr; + if (targetClause && cursor >= targetClause->mStartOffset && + cursor <= targetClause->mEndOffset) { + // Forget the caret position specified by IME since Gecko's caret position + // will be at the end of composition string. + mCursorPosition = NO_IME_CARET; + MOZ_LOG(gIMMLog, LogLevel::Info, + ("CreateTextRangeArray, no caret due to it's in the target " + "clause, now, mCursorPosition is NO_IME_CARET")); + } + + if (mCursorPosition != NO_IME_CARET) { + rv = dispatcher->SetCaretInPendingComposition(cursor, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG( + gIMMLog, LogLevel::Error, + ("DispatchCompositionChangeEvent, FAILED due to" + "TextEventDispatcher::SetCaretInPendingComposition() failure")); + return; + } + } + } + + WidgetEventTime eventTime = aWindow->CurrentMessageWidgetEventTime(); + nsEventStatus status; + rv = dispatcher->FlushPendingComposition(status, &eventTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("DispatchCompositionChangeEvent, FAILED due to" + "TextEventDispatcher::FlushPendingComposition() failure")); + return; + } +} + +void IMMHandler::GetCompositionString(const IMEContext& aContext, DWORD aIndex, + nsAString& aCompositionString) const { + aCompositionString.Truncate(); + + // Retrieve the size of the required output buffer. + long lRtn = ::ImmGetCompositionStringW(aContext.get(), aIndex, nullptr, 0); + if (lRtn < 0 || !aCompositionString.SetLength((lRtn / sizeof(WCHAR)) + 1, + mozilla::fallible)) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("GetCompositionString, FAILED, due to OOM")); + return; // Error or out of memory. + } + + // Actually retrieve the composition string information. + lRtn = ::ImmGetCompositionStringW(aContext.get(), aIndex, + (LPVOID)aCompositionString.BeginWriting(), + lRtn + sizeof(WCHAR)); + aCompositionString.SetLength(lRtn / sizeof(WCHAR)); + + MOZ_LOG(gIMMLog, LogLevel::Info, + ("GetCompositionString, succeeded, aCompositionString=\"%s\"", + NS_ConvertUTF16toUTF8(aCompositionString).get())); +} + +bool IMMHandler::GetTargetClauseRange(uint32_t* aOffset, uint32_t* aLength) { + NS_ENSURE_TRUE(aOffset, false); + NS_ENSURE_TRUE(mIsComposing, false); + NS_ENSURE_TRUE(ShouldDrawCompositionStringOurselves(), false); + + bool found = false; + *aOffset = mCompositionStart; + for (uint32_t i = 0; i < mAttributeArray.Length(); i++) { + if (mAttributeArray[i] == ATTR_TARGET_NOTCONVERTED || + mAttributeArray[i] == ATTR_TARGET_CONVERTED) { + *aOffset = mCompositionStart + i; + found = true; + break; + } + } + + if (!aLength) { + return true; + } + + if (!found) { + // The all composition string is targetted when there is no ATTR_TARGET_* + // clause. E.g., there is only ATTR_INPUT + *aLength = mCompositionString.Length(); + return true; + } + + uint32_t offsetInComposition = *aOffset - mCompositionStart; + *aLength = mCompositionString.Length() - offsetInComposition; + for (uint32_t i = offsetInComposition; i < mAttributeArray.Length(); i++) { + if (mAttributeArray[i] != ATTR_TARGET_NOTCONVERTED && + mAttributeArray[i] != ATTR_TARGET_CONVERTED) { + *aLength = i - offsetInComposition; + break; + } + } + return true; +} + +bool IMMHandler::ConvertToANSIString(const nsString& aStr, UINT aCodePage, + nsACString& aANSIStr) { + int len = ::WideCharToMultiByte(aCodePage, 0, (LPCWSTR)aStr.get(), + aStr.Length(), nullptr, 0, nullptr, nullptr); + NS_ENSURE_TRUE(len >= 0, false); + + if (!aANSIStr.SetLength(len, mozilla::fallible)) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("ConvertToANSIString, FAILED, due to OOM")); + return false; + } + ::WideCharToMultiByte(aCodePage, 0, (LPCWSTR)aStr.get(), aStr.Length(), + (LPSTR)aANSIStr.BeginWriting(), len, nullptr, nullptr); + return true; +} + +bool IMMHandler::GetCharacterRectOfSelectedTextAt( + nsWindow* aWindow, uint32_t aOffset, LayoutDeviceIntRect& aCharRect, + WritingMode* aWritingMode) { + LayoutDeviceIntPoint point(0, 0); + + Selection& selection = GetSelection(); + if (!selection.EnsureValidSelection(aWindow)) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("GetCharacterRectOfSelectedTextAt, FAILED, due to " + "Selection::EnsureValidSelection() failure")); + return false; + } + + // If the offset is larger than the end of composition string or selected + // string, we should return false since such case must be a bug of the caller + // or the active IME. If it's an IME's bug, we need to set targetLength to + // aOffset. + uint32_t targetLength = + mIsComposing ? mCompositionString.Length() : selection.Length(); + if (NS_WARN_IF(aOffset > targetLength)) { + MOZ_LOG( + gIMMLog, LogLevel::Error, + ("GetCharacterRectOfSelectedTextAt, FAILED, due to " + "aOffset is too large (aOffset=%u, targetLength=%u, mIsComposing=%s)", + aOffset, targetLength, GetBoolName(mIsComposing))); + return false; + } + + // If there is caret, we might be able to use caret rect. + uint32_t caretOffset = UINT32_MAX; + // There is a caret only when the normal selection is collapsed. + if (selection.Collapsed()) { + if (mIsComposing) { + // If it's composing, mCursorPosition is the offset to caret in + // the composition string. + if (mCursorPosition != NO_IME_CARET) { + MOZ_ASSERT(mCursorPosition >= 0); + caretOffset = mCursorPosition; + } else if (!ShouldDrawCompositionStringOurselves() || + mCompositionString.IsEmpty()) { + // Otherwise, if there is no composition string, we should assume that + // there is a caret at the start of composition string. + caretOffset = 0; + } + } else { + // If there is no composition, the selection offset is the caret offset. + caretOffset = 0; + } + } + + // If there is a caret and retrieving offset is same as the caret offset, + // we should use the caret rect. + if (aOffset != caretOffset) { + WidgetQueryContentEvent queryTextRectEvent(true, eQueryTextRect, aWindow); + WidgetQueryContentEvent::Options options; + options.mRelativeToInsertionPoint = true; + queryTextRectEvent.InitForQueryTextRect(aOffset, 1, options); + aWindow->InitEvent(queryTextRectEvent, &point); + DispatchEvent(aWindow, queryTextRectEvent); + if (queryTextRectEvent.Succeeded()) { + aCharRect = queryTextRectEvent.mReply->mRect; + if (aWritingMode) { + *aWritingMode = queryTextRectEvent.mReply->WritingModeRef(); + } + MOZ_LOG( + gIMMLog, LogLevel::Debug, + ("GetCharacterRectOfSelectedTextAt, Succeeded, aOffset=%u, " + "aCharRect={ x: %ld, y: %ld, width: %ld, height: %ld }, " + "queryTextRectEvent={ mReply=%s }", + aOffset, aCharRect.X(), aCharRect.Y(), aCharRect.Width(), + aCharRect.Height(), ToString(queryTextRectEvent.mReply).c_str())); + return true; + } + } + + return GetCaretRect(aWindow, aCharRect, aWritingMode); +} + +bool IMMHandler::GetCaretRect(nsWindow* aWindow, + LayoutDeviceIntRect& aCaretRect, + WritingMode* aWritingMode) { + LayoutDeviceIntPoint point(0, 0); + + WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, aWindow); + WidgetQueryContentEvent::Options options; + options.mRelativeToInsertionPoint = true; + queryCaretRectEvent.InitForQueryCaretRect(0, options); + aWindow->InitEvent(queryCaretRectEvent, &point); + DispatchEvent(aWindow, queryCaretRectEvent); + if (queryCaretRectEvent.Failed()) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("GetCaretRect, FAILED, due to eQueryCaretRect failure")); + return false; + } + aCaretRect = queryCaretRectEvent.mReply->mRect; + if (aWritingMode) { + *aWritingMode = queryCaretRectEvent.mReply->WritingModeRef(); + } + MOZ_LOG(gIMMLog, LogLevel::Info, + ("GetCaretRect, SUCCEEDED, " + "aCaretRect={ x: %ld, y: %ld, width: %ld, height: %ld }, " + "queryCaretRectEvent={ mReply=%s }", + aCaretRect.X(), aCaretRect.Y(), aCaretRect.Width(), + aCaretRect.Height(), ToString(queryCaretRectEvent.mReply).c_str())); + return true; +} + +bool IMMHandler::SetIMERelatedWindowsPos(nsWindow* aWindow, + const IMEContext& aContext) { + // Get first character rect of current a normal selected text or a composing + // string. + WritingMode writingMode; + LayoutDeviceIntRect firstSelectedCharRectRelativeToWindow; + bool ret = GetCharacterRectOfSelectedTextAt( + aWindow, 0, firstSelectedCharRectRelativeToWindow, &writingMode); + NS_ENSURE_TRUE(ret, false); + nsWindow* toplevelWindow = aWindow->GetTopLevelWindow(false); + LayoutDeviceIntRect firstSelectedCharRect; + ResolveIMECaretPos(toplevelWindow, firstSelectedCharRectRelativeToWindow, + aWindow, firstSelectedCharRect); + + // Set native caret size/position to our caret. Some IMEs honor it. E.g., + // "Intelligent ABC" (Simplified Chinese) and "MS PinYin 3.0" (Simplified + // Chinese) on XP. But if a11y module is handling native caret, we shouldn't + // touch it. + if (!IMEHandler::IsA11yHandlingNativeCaret()) { + LayoutDeviceIntRect caretRect(firstSelectedCharRect), + caretRectRelativeToWindow; + if (GetCaretRect(aWindow, caretRectRelativeToWindow)) { + ResolveIMECaretPos(toplevelWindow, caretRectRelativeToWindow, aWindow, + caretRect); + } else { + NS_WARNING("failed to get caret rect"); + caretRect.SetWidth(1); + } + IMEHandler::CreateNativeCaret(aWindow, caretRect); + } + + if (ShouldDrawCompositionStringOurselves()) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("SetIMERelatedWindowsPos, Set candidate window")); + + // Get a rect of first character in current target in composition string. + LayoutDeviceIntRect firstTargetCharRect, lastTargetCharRect; + if (mIsComposing && !mCompositionString.IsEmpty()) { + // If there are no targetted selection, we should use it's first character + // rect instead. + uint32_t offset, length; + if (!GetTargetClauseRange(&offset, &length)) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("SetIMERelatedWindowsPos, FAILED, due to " + "GetTargetClauseRange() failure")); + return false; + } + ret = + GetCharacterRectOfSelectedTextAt(aWindow, offset - mCompositionStart, + firstTargetCharRect, &writingMode); + NS_ENSURE_TRUE(ret, false); + if (length) { + ret = GetCharacterRectOfSelectedTextAt( + aWindow, offset + length - 1 - mCompositionStart, + lastTargetCharRect); + NS_ENSURE_TRUE(ret, false); + } else { + lastTargetCharRect = firstTargetCharRect; + } + } else { + // If there are no composition string, we should use a first character + // rect. + ret = GetCharacterRectOfSelectedTextAt(aWindow, 0, firstTargetCharRect, + &writingMode); + NS_ENSURE_TRUE(ret, false); + lastTargetCharRect = firstTargetCharRect; + } + ResolveIMECaretPos(toplevelWindow, firstTargetCharRect, aWindow, + firstTargetCharRect); + ResolveIMECaretPos(toplevelWindow, lastTargetCharRect, aWindow, + lastTargetCharRect); + LayoutDeviceIntRect targetClauseRect; + targetClauseRect.UnionRect(firstTargetCharRect, lastTargetCharRect); + + // Move the candidate window to proper position from the target clause as + // far as possible. + CANDIDATEFORM candForm; + candForm.dwIndex = 0; + if (!writingMode.IsVertical() || IsVerticalWritingSupported()) { + candForm.dwStyle = CFS_EXCLUDE; + // Candidate window shouldn't overlap the target clause in any writing + // mode. + candForm.rcArea.left = targetClauseRect.X(); + candForm.rcArea.right = targetClauseRect.XMost(); + candForm.rcArea.top = targetClauseRect.Y(); + candForm.rcArea.bottom = targetClauseRect.YMost(); + if (!writingMode.IsVertical()) { + // In horizontal layout, current point of interest should be top-left + // of the first character. + candForm.ptCurrentPos.x = firstTargetCharRect.X(); + candForm.ptCurrentPos.y = firstTargetCharRect.Y(); + } else if (writingMode.IsVerticalRL()) { + // In vertical layout (RL), candidate window should be positioned right + // side of target clause. However, we don't set vertical writing font + // to the IME. Therefore, the candidate window may be positioned + // bottom-left of target clause rect with these information. + candForm.ptCurrentPos.x = targetClauseRect.X(); + candForm.ptCurrentPos.y = targetClauseRect.Y(); + } else { + MOZ_ASSERT(writingMode.IsVerticalLR(), "Did we miss some causes?"); + // In vertical layout (LR), candidate window should be poisitioned left + // side of target clause. Although, we don't set vertical writing font + // to the IME, the candidate window may be positioned bottom-right of + // the target clause rect with these information. + candForm.ptCurrentPos.x = targetClauseRect.XMost(); + candForm.ptCurrentPos.y = targetClauseRect.Y(); + } + } else { + // If vertical writing is not supported by IME, let's set candidate + // window position to the bottom-left of the target clause because + // the position must be the safest position to prevent the candidate + // window to overlap with the target clause. + candForm.dwStyle = CFS_CANDIDATEPOS; + candForm.ptCurrentPos.x = targetClauseRect.X(); + candForm.ptCurrentPos.y = targetClauseRect.YMost(); + } + MOZ_LOG(gIMMLog, LogLevel::Info, + ("SetIMERelatedWindowsPos, Calling ImmSetCandidateWindow()... " + "ptCurrentPos={ x=%d, y=%d }, " + "rcArea={ left=%d, top=%d, right=%d, bottom=%d }, " + "writingMode=%s", + candForm.ptCurrentPos.x, candForm.ptCurrentPos.y, + candForm.rcArea.left, candForm.rcArea.top, candForm.rcArea.right, + candForm.rcArea.bottom, GetWritingModeName(writingMode).get())); + ::ImmSetCandidateWindow(aContext.get(), &candForm); + } else { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("SetIMERelatedWindowsPos, Set composition window")); + + // Move the composition window to caret position (if selected some + // characters, we should use first character rect of them). + // And in this mode, IME adjusts the candidate window position + // automatically. So, we don't need to set it. + COMPOSITIONFORM compForm; + compForm.dwStyle = CFS_POINT; + compForm.ptCurrentPos.x = !writingMode.IsVerticalLR() + ? firstSelectedCharRect.X() + : firstSelectedCharRect.XMost(); + compForm.ptCurrentPos.y = firstSelectedCharRect.Y(); + ::ImmSetCompositionWindow(aContext.get(), &compForm); + } + + return true; +} + +void IMMHandler::ResolveIMECaretPos(nsIWidget* aReferenceWidget, + LayoutDeviceIntRect& aCursorRect, + nsIWidget* aNewOriginWidget, + LayoutDeviceIntRect& aOutRect) { + aOutRect = aCursorRect; + + if (aReferenceWidget == aNewOriginWidget) return; + + if (aReferenceWidget) + aOutRect.MoveBy(aReferenceWidget->WidgetToScreenOffset()); + + if (aNewOriginWidget) + aOutRect.MoveBy(-aNewOriginWidget->WidgetToScreenOffset()); +} + +static void SetHorizontalFontToLogFont(const nsAString& aFontFace, + LOGFONTW& aLogFont) { + aLogFont.lfEscapement = aLogFont.lfOrientation = 0; + if (NS_WARN_IF(aFontFace.Length() > LF_FACESIZE - 1)) { + memcpy(aLogFont.lfFaceName, L"System", sizeof(L"System")); + return; + } + memcpy(aLogFont.lfFaceName, aFontFace.BeginReading(), + aFontFace.Length() * sizeof(wchar_t)); + aLogFont.lfFaceName[aFontFace.Length()] = 0; +} + +static void SetVerticalFontToLogFont(const nsAString& aFontFace, + LOGFONTW& aLogFont) { + aLogFont.lfEscapement = aLogFont.lfOrientation = 2700; + if (NS_WARN_IF(aFontFace.Length() > LF_FACESIZE - 2)) { + memcpy(aLogFont.lfFaceName, L"@System", sizeof(L"@System")); + return; + } + aLogFont.lfFaceName[0] = '@'; + memcpy(&aLogFont.lfFaceName[1], aFontFace.BeginReading(), + aFontFace.Length() * sizeof(wchar_t)); + aLogFont.lfFaceName[aFontFace.Length() + 1] = 0; +} + +void IMMHandler::AdjustCompositionFont(nsWindow* aWindow, + const IMEContext& aContext, + const WritingMode& aWritingMode, + bool aForceUpdate) { + // An instance of IMMHandler is destroyed when active IME is changed. + // Therefore, we need to store the information which are set to the IM + // context to static variables since IM context is never recreated. + static bool sCompositionFontsInitialized = false; + static nsString sCompositionFont; + static bool sCompositionFontPrefDone = false; + if (!sCompositionFontPrefDone) { + sCompositionFontPrefDone = true; + Preferences::GetString("intl.imm.composition_font", sCompositionFont); + } + + // If composition font is customized by pref, we need to modify the + // composition font of the IME context at first time even if the writing mode + // is horizontal. + bool setCompositionFontForcibly = + aForceUpdate || + (!sCompositionFontsInitialized && !sCompositionFont.IsEmpty()); + + static WritingMode sCurrentWritingMode; + static nsString sCurrentIMEName; + if (!setCompositionFontForcibly && + sWritingModeOfCompositionFont == aWritingMode && + sCurrentIMEName == sIMEName) { + // Nothing to do if writing mode isn't being changed. + return; + } + + // Decide composition fonts for both horizontal writing mode and vertical + // writing mode. If the font isn't specified by the pref, use default + // font which is already set to the IM context. And also in vertical writing + // mode, insert '@' to the start of the font. + if (!sCompositionFontsInitialized) { + sCompositionFontsInitialized = true; + // sCompositionFontH must not start with '@' and its length is less than + // LF_FACESIZE since it needs to end with null terminating character. + if (sCompositionFont.IsEmpty() || + sCompositionFont.Length() > LF_FACESIZE - 1 || + sCompositionFont[0] == '@') { + LOGFONTW defaultLogFont; + if (NS_WARN_IF( + !::ImmGetCompositionFont(aContext.get(), &defaultLogFont))) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("AdjustCompositionFont, ::ImmGetCompositionFont() failed")); + sCompositionFont.AssignLiteral("System"); + } else { + // The font face is typically, "System". + sCompositionFont.Assign(defaultLogFont.lfFaceName); + } + } + + MOZ_LOG(gIMMLog, LogLevel::Info, + ("AdjustCompositionFont, sCompositionFont=\"%s\" is initialized", + NS_ConvertUTF16toUTF8(sCompositionFont).get())); + } + + static nsString sCompositionFontForJapanist2003; + if (IsJapanist2003Active() && sCompositionFontForJapanist2003.IsEmpty()) { + const char* kCompositionFontForJapanist2003 = + "intl.imm.composition_font.japanist_2003"; + Preferences::GetString(kCompositionFontForJapanist2003, + sCompositionFontForJapanist2003); + // If the font name is not specified properly, let's use + // "MS PGothic" instead. + if (sCompositionFontForJapanist2003.IsEmpty() || + sCompositionFontForJapanist2003.Length() > LF_FACESIZE - 2 || + sCompositionFontForJapanist2003[0] == '@') { + sCompositionFontForJapanist2003.AssignLiteral("MS PGothic"); + } + } + + sWritingModeOfCompositionFont = aWritingMode; + sCurrentIMEName = sIMEName; + + LOGFONTW logFont; + memset(&logFont, 0, sizeof(logFont)); + if (!::ImmGetCompositionFont(aContext.get(), &logFont)) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("AdjustCompositionFont, ::ImmGetCompositionFont() failed")); + logFont.lfFaceName[0] = 0; + } + // Need to reset some information which should be recomputed with new font. + logFont.lfWidth = 0; + logFont.lfWeight = FW_DONTCARE; + logFont.lfOutPrecision = OUT_DEFAULT_PRECIS; + logFont.lfClipPrecision = CLIP_DEFAULT_PRECIS; + logFont.lfPitchAndFamily = DEFAULT_PITCH; + + if (aWritingMode.IsVertical() && IsVerticalWritingSupported()) { + SetVerticalFontToLogFont(IsJapanist2003Active() + ? sCompositionFontForJapanist2003 + : sCompositionFont, + logFont); + } else { + SetHorizontalFontToLogFont(IsJapanist2003Active() + ? sCompositionFontForJapanist2003 + : sCompositionFont, + logFont); + } + MOZ_LOG(gIMMLog, LogLevel::Warning, + ("AdjustCompositionFont, calling ::ImmSetCompositionFont(\"%s\")", + NS_ConvertUTF16toUTF8(nsDependentString(logFont.lfFaceName)).get())); + ::ImmSetCompositionFontW(aContext.get(), &logFont); +} + +// static +nsresult IMMHandler::OnMouseButtonEvent( + nsWindow* aWindow, const IMENotification& aIMENotification) { + // We don't need to create the instance of the handler here. + if (!gIMMHandler) { + return NS_OK; + } + + if (!sWM_MSIME_MOUSE || !IsComposingOnOurEditor() || + !ShouldDrawCompositionStringOurselves()) { + return NS_OK; + } + + // We need to handle only mousedown event. + if (aIMENotification.mMouseButtonEventData.mEventMessage != eMouseDown) { + return NS_OK; + } + + // If the character under the cursor is not in the composition string, + // we don't need to notify IME of it. + uint32_t compositionStart = gIMMHandler->mCompositionStart; + uint32_t compositionEnd = + compositionStart + gIMMHandler->mCompositionString.Length(); + if (aIMENotification.mMouseButtonEventData.mOffset < compositionStart || + aIMENotification.mMouseButtonEventData.mOffset >= compositionEnd) { + return NS_OK; + } + + BYTE button; + switch (aIMENotification.mMouseButtonEventData.mButton) { + case MouseButton::ePrimary: + button = IMEMOUSE_LDOWN; + break; + case MouseButton::eMiddle: + button = IMEMOUSE_MDOWN; + break; + case MouseButton::eSecondary: + button = IMEMOUSE_RDOWN; + break; + default: + return NS_OK; + } + + // calcurate positioning and offset + // char : JCH1|JCH2|JCH3 + // offset: 0011 1122 2233 + // positioning: 2301 2301 2301 + nsIntPoint cursorPos = + aIMENotification.mMouseButtonEventData.mCursorPos.AsIntPoint(); + nsIntRect charRect = + aIMENotification.mMouseButtonEventData.mCharRect.AsIntRect(); + int32_t cursorXInChar = cursorPos.x - charRect.X(); + // The event might hit to zero-width character, see bug 694913. + // The reason might be: + // * There are some zero-width characters are actually. + // * font-size is specified zero. + // But nobody reproduced this bug actually... + // We should assume that user clicked on right most of the zero-width + // character in such case. + int positioning = 1; + if (charRect.Width() > 0) { + positioning = cursorXInChar * 4 / charRect.Width(); + positioning = (positioning + 2) % 4; + } + + int offset = + aIMENotification.mMouseButtonEventData.mOffset - compositionStart; + if (positioning < 2) { + offset++; + } + + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnMouseButtonEvent, x,y=%ld,%ld, offset=%ld, positioning=%ld", + cursorPos.x, cursorPos.y, offset, positioning)); + + // send MS_MSIME_MOUSE message to default IME window. + HWND imeWnd = ::ImmGetDefaultIMEWnd(aWindow->GetWindowHandle()); + IMEContext context(aWindow); + if (::SendMessageW(imeWnd, sWM_MSIME_MOUSE, + MAKELONG(MAKEWORD(button, positioning), offset), + (LPARAM)context.get()) == 1) { + return NS_SUCCESS_EVENT_CONSUMED; + } + return NS_OK; +} + +// static +bool IMMHandler::OnKeyDownEvent(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult) { + MOZ_LOG(gIMMLog, LogLevel::Info, + ("OnKeyDownEvent, hWnd=%08x, wParam=%08x, lParam=%08x", + aWindow->GetWindowHandle(), wParam, lParam)); + aResult.mConsumed = false; + switch (wParam) { + case VK_TAB: + case VK_PRIOR: + case VK_NEXT: + case VK_END: + case VK_HOME: + case VK_LEFT: + case VK_UP: + case VK_RIGHT: + case VK_DOWN: + case VK_RETURN: + // If IME didn't process the key message (the virtual key code wasn't + // converted to VK_PROCESSKEY), and the virtual key code event causes + // moving caret or editing text with keeping composing state, we should + // cancel the composition here because we cannot support moving + // composition string with DOM events (IE also cancels the composition + // in same cases). Then, this event will be dispatched. + if (IsComposingOnOurEditor()) { + // NOTE: We don't need to cancel the composition on another window. + CancelComposition(aWindow, false); + } + return false; + default: + return false; + } +} + +/****************************************************************************** + * IMMHandler::Selection + ******************************************************************************/ + +bool IMMHandler::Selection::IsValid() const { + if (!mIsValid || NS_WARN_IF(mOffset == UINT32_MAX)) { + return false; + } + CheckedInt<uint32_t> endOffset = CheckedInt<uint32_t>(mOffset) + Length(); + return endOffset.isValid(); +} + +bool IMMHandler::Selection::Update(const IMENotification& aIMENotification) { + mOffset = aIMENotification.mSelectionChangeData.mOffset; + mString = aIMENotification.mSelectionChangeData.String(); + mWritingMode = aIMENotification.mSelectionChangeData.GetWritingMode(); + mIsValid = true; + + MOZ_LOG(gIMMLog, LogLevel::Info, + ("Selection::Update, aIMENotification={ mSelectionChangeData={ " + "mOffset=%u, mLength=%u, GetWritingMode()=%s } }", + mOffset, mString.Length(), GetWritingModeName(mWritingMode).get())); + + if (!IsValid()) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("Selection::Update, FAILED, due to invalid range")); + Clear(); + return false; + } + return true; +} + +bool IMMHandler::Selection::Init(nsWindow* aWindow) { + Clear(); + + WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, + aWindow); + LayoutDeviceIntPoint point(0, 0); + aWindow->InitEvent(querySelectedTextEvent, &point); + DispatchEvent(aWindow, querySelectedTextEvent); + if (NS_WARN_IF(querySelectedTextEvent.DidNotFindSelection())) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("Selection::Init, FAILED, due to eQuerySelectedText failure")); + return false; + } + // If the window is destroyed during querying selected text, we shouldn't + // do anymore. + if (aWindow->Destroyed()) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("Selection::Init, FAILED, due to the widget destroyed")); + return false; + } + + MOZ_ASSERT(querySelectedTextEvent.mReply->mOffsetAndData.isSome()); + mOffset = querySelectedTextEvent.mReply->StartOffset(); + mString = querySelectedTextEvent.mReply->DataRef(); + mWritingMode = querySelectedTextEvent.mReply->WritingModeRef(); + mIsValid = true; + + MOZ_LOG(gIMMLog, LogLevel::Info, + ("Selection::Init, querySelectedTextEvent={ mReply=%s }", + ToString(querySelectedTextEvent.mReply).c_str())); + + if (!IsValid()) { + MOZ_LOG(gIMMLog, LogLevel::Error, + ("Selection::Init, FAILED, due to invalid range")); + Clear(); + return false; + } + return true; +} + +bool IMMHandler::Selection::EnsureValidSelection(nsWindow* aWindow) { + if (IsValid()) { + return true; + } + return Init(aWindow); +} + +} // namespace widget +} // namespace mozilla |