/* -*- 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 #include "mozilla/CheckedInt.h" #include "mozilla/MiscEvents.h" #include "mozilla/TextEvents.h" #include "mozilla/ToString.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 // For collecting other people's log, tell `MOZ_LOG=IMEHandler:4,sync` // rather than `MOZ_LOG=IMEHandler:5,sync` since using `5` may create too // big file. // Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior. extern mozilla::LazyLogModule gIMELog; 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 GetReconvertStringLog : public nsAutoCString { public: explicit GetReconvertStringLog(RECONVERTSTRING* aReconv) { AssignLiteral("{ dwSize="); AppendInt(static_cast(aReconv->dwSize)); AppendLiteral(", dwVersion="); AppendInt(static_cast(aReconv->dwVersion)); AppendLiteral(", dwStrLen="); AppendInt(static_cast(aReconv->dwStrLen)); AppendLiteral(", dwStrOffset="); AppendInt(static_cast(aReconv->dwStrOffset)); AppendLiteral(", dwCompStrLen="); AppendInt(static_cast(aReconv->dwCompStrLen)); AppendLiteral(", dwCompStrOffset="); AppendInt(static_cast(aReconv->dwCompStrOffset)); AppendLiteral(", dwTargetStrLen="); AppendInt(static_cast(aReconv->dwTargetStrLen)); AppendLiteral(", dwTargetStrOffset="); AppendInt(static_cast(aReconv->dwTargetStrOffset)); AppendLiteral(", result str=\""); if (aReconv->dwStrLen) { char16_t* strStart = reinterpret_cast( reinterpret_cast(aReconv) + aReconv->dwStrOffset); nsDependentString str(strStart, aReconv->dwStrLen); Append(NS_ConvertUTF16toUTF8(str)); } AppendLiteral("\" }"); } virtual ~GetReconvertStringLog() {} }; namespace mozilla { namespace widget { static IMMHandler* gIMMHandler = nullptr; /****************************************************************************** * IMEContext ******************************************************************************/ IMEContext::IMEContext(HWND aWnd) : mWnd(aWnd), mIMC(::ImmGetContext(aWnd)) {} IMEContext::IMEContext(nsWindow* aWindowBase) : mWnd(aWindowBase->GetWindowHandle()), mIMC(::ImmGetContext(aWindowBase->GetWindowHandle())) {} void IMEContext::Init(HWND aWnd) { Clear(); mWnd = aWnd; mIMC = ::ImmGetContext(mWnd); } void IMEContext::Init(nsWindow* 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(gIMELog, LogLevel::Info, ("IMMHandler::InitKeyboardLayout, aKeyboardLayout=%p (\"%s\"), " "sCodePage=%u, 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(gIMELog, LogLevel::Debug, ("IMMHandler::IMMHandler is created")); } IMMHandler::~IMMHandler() { if (mIsComposing) { MOZ_LOG( gIMELog, LogLevel::Error, (" IMMHandler::~IMMHandler, ERROR, the instance is still composing")); } MOZ_LOG(gIMELog, LogLevel::Debug, ("IMMHandler::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(gIMELog, LogLevel::Info, ("IMMHandler::CommitComposition, aForce=%s, aWindow=%p, hWnd=%p, " "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(gIMELog, LogLevel::Info, (" IMMHandler::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(gIMELog, LogLevel::Info, ("IMMHandler::CancelComposition, aForce=%s, aWindow=%p, hWnd=%p, " "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(gIMELog, LogLevel::Info, (" IMMHandler::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(gIMELog, LogLevel::Info, ("IMMHandler::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->mContentSelection.reset(); } 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->mContentSelection = Some(ContentSelection(aIMENotification.mSelectionChangeData)); } } // 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(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(gIMELog, LogLevel::Info, ("IMMHandler::OnInputLangChange, hWnd=%p, wParam=%08zx, " "lParam=%08" PRIxLPTR, 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(gIMELog, LogLevel::Info, ("IMMHandler::OnIMEStartComposition, hWnd=%p, 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( gIMELog, LogLevel::Info, ("IMMHandler::OnIMEComposition, hWnd=%p, lParam=%08" PRIxLPTR ", 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(gIMELog, LogLevel::Info, ("IMMHandler::OnIMEEndComposition, hWnd=%p, 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(gIMELog, LogLevel::Info, (" IMMHandler::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(gIMELog, LogLevel::Info, (" IMMHandler::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(gIMELog, LogLevel::Info, ("IMMHandler::OnIMEChar, hWnd=%p, char=%08zx", 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(gIMELog, LogLevel::Info, ("IMMHandler::OnIMECompositionFull, hWnd=%p", 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(gIMELog, LogLevel::Info, ("IMMHandler::OnIMENotify, hWnd=%p, IMN_CHANGECANDIDATE, " "lParam=%08" PRIxLPTR, aWindow->GetWindowHandle(), lParam)); break; case IMN_CLOSECANDIDATE: MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::OnIMENotify, hWnd=%p, IMN_CLOSECANDIDATE, " "lParam=%08" PRIxLPTR, aWindow->GetWindowHandle(), lParam)); break; case IMN_CLOSESTATUSWINDOW: MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::OnIMENotify, hWnd=%p, IMN_CLOSESTATUSWINDOW", aWindow->GetWindowHandle())); break; case IMN_GUIDELINE: MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::OnIMENotify, hWnd=%p, IMN_GUIDELINE", aWindow->GetWindowHandle())); break; case IMN_OPENCANDIDATE: MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::OnIMENotify, hWnd=%p, IMN_OPENCANDIDATE, " "lParam=%08" PRIxLPTR, aWindow->GetWindowHandle(), lParam)); break; case IMN_OPENSTATUSWINDOW: MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::OnIMENotify, hWnd=%p, IMN_OPENSTATUSWINDOW", aWindow->GetWindowHandle())); break; case IMN_SETCANDIDATEPOS: MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETCANDIDATEPOS, " "lParam=%08" PRIxLPTR, aWindow->GetWindowHandle(), lParam)); break; case IMN_SETCOMPOSITIONFONT: MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETCOMPOSITIONFONT", aWindow->GetWindowHandle())); break; case IMN_SETCOMPOSITIONWINDOW: MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETCOMPOSITIONWINDOW", aWindow->GetWindowHandle())); break; case IMN_SETCONVERSIONMODE: MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETCONVERSIONMODE", aWindow->GetWindowHandle())); break; case IMN_SETOPENSTATUS: MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETOPENSTATUS", aWindow->GetWindowHandle())); break; case IMN_SETSENTENCEMODE: MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETSENTENCEMODE", aWindow->GetWindowHandle())); break; case IMN_SETSTATUSWINDOWPOS: MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETSTATUSWINDOWPOS", aWindow->GetWindowHandle())); break; case IMN_PRIVATE: MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::OnIMENotify, hWnd=%p, 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(gIMELog, LogLevel::Info, ("IMMHandler::OnIMERequest, hWnd=%p, IMR_RECONVERTSTRING", aWindow->GetWindowHandle())); aResult.mConsumed = HandleReconvert(aWindow, lParam, &aResult.mResult); return true; case IMR_QUERYCHARPOSITION: MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::OnIMERequest, hWnd=%p, IMR_QUERYCHARPOSITION", aWindow->GetWindowHandle())); aResult.mConsumed = HandleQueryCharPosition(aWindow, lParam, &aResult.mResult); return true; case IMR_DOCUMENTFEED: MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::OnIMERequest, hWnd=%p, IMR_DOCUMENTFEED", aWindow->GetWindowHandle())); aResult.mConsumed = HandleDocumentFeed(aWindow, lParam, &aResult.mResult); return true; default: MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::OnIMERequest, hWnd=%p, wParam=%08zx", aWindow->GetWindowHandle(), wParam)); aResult.mConsumed = false; return true; } } // static bool IMMHandler::OnIMESelect(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, MSGResult& aResult) { MOZ_LOG( gIMELog, LogLevel::Info, ("IMMHandler::OnIMESelect, hWnd=%p, wParam=%08zx, lParam=%08" PRIxLPTR, 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(gIMELog, LogLevel::Info, ("IMMHandler::OnIMESetContext, hWnd=%p, %s, lParam=%08" PRIxLPTR, 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(gIMELog, LogLevel::Info, (" IMMHandler::OnIMESetContext, hWnd=%p is top level window", aWindow->GetWindowHandle())); 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(gIMELog, LogLevel::Info, (" IMMHandler::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( gIMELog, LogLevel::Info, ("IMMHandler::OnChar, aWindow=%p, wParam=%08zx, lParam=%08" PRIxLPTR ", " "recorded: wParam=%08zx, lParam=%08" PRIxLPTR, 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"); const Maybe& contentSelection = GetContentSelectionWithQueryIfNothing(aWindow); if (contentSelection.isNothing()) { MOZ_LOG(gIMELog, LogLevel::Error, (" IMMHandler::HandleStartComposition, FAILED, due to " "Selection::GetContentSelectionWithQueryIfNothing() failure")); return; } if (!contentSelection->HasRange()) { MOZ_LOG(gIMELog, LogLevel::Error, (" IMMHandler::HandleStartComposition, FAILED, due to " "there is no selection")); return; } AdjustCompositionFont(aWindow, aContext, contentSelection->WritingModeRef()); mCompositionStart = contentSelection->OffsetAndDataRef().StartOffset(); mCursorPosition = NO_IME_CARET; RefPtr dispatcher = GetTextEventDispatcherFor(aWindow); nsresult rv = dispatcher->BeginNativeInputTransaction(); if (NS_WARN_IF(NS_FAILED(rv))) { MOZ_LOG(gIMELog, LogLevel::Error, (" IMMHandler::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(gIMELog, LogLevel::Error, (" IMMHandler::HandleStartComposition, FAILED, due to " "TextEventDispatcher::StartComposition() failure")); return; } mIsComposing = true; mComposingWindow = aWindow; mDispatcher = dispatcher; MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::HandleStartComposition, START composition, " "mCompositionStart=%u", 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(gIMELog, LogLevel::Info, ("IMMHandler::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(gIMELog, LogLevel::Info, ("IMMHandler::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(gIMELog, LogLevel::Info, ("IMMHandler::HandleComposition, GCS_COMPSTR")); nsAutoString previousCompositionString(mCompositionString); GetCompositionString(aContext, GCS_COMPSTR, mCompositionString); if (!IS_COMPOSING_LPARAM(lParam)) { MOZ_LOG( gIMELog, LogLevel::Info, (" IMMHandler::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(gIMELog, LogLevel::Info, (" IMMHandler::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(gIMELog, LogLevel::Info, (" IMMHandler::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(gIMELog, LogLevel::Info, (" IMMHandler::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(0, clauseArrayLength)); MOZ_LOG(gIMELog, LogLevel::Info, (" IMMHandler::HandleComposition, GCS_COMPCLAUSE, mClauseLength=%zu", 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(0, attrArrayLength)); MOZ_LOG( gIMELog, LogLevel::Info, (" IMMHandler::HandleComposition, GCS_COMPATTR, mAttributeLength=%zu", 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(gIMELog, LogLevel::Info, (" IMMHandler::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(gIMELog, LogLevel::Info, ("IMMHandler::HandleEndComposition(aWindow=0x%p, aCommitString=0x%p " "(\"%s\"))", aWindow, aCommitString, aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : "")); IMEHandler::MaybeDestroyNativeCaret(); RefPtr dispatcher = GetTextEventDispatcherFor(aWindow); nsresult rv = dispatcher->BeginNativeInputTransaction(); if (NS_WARN_IF(NS_FAILED(rv))) { MOZ_LOG(gIMELog, LogLevel::Error, (" IMMHandler::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(gIMELog, LogLevel::Error, (" IMMHandler::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(lParam); const Maybe& contentSelection = GetContentSelectionWithQueryIfNothing(aWindow); if (contentSelection.isNothing()) { MOZ_LOG(gIMELog, LogLevel::Error, ("IMMHandler::HandleReconvert, FAILED, due to " "Selection::GetContentSelectionWithQueryIfNothing() failure")); return false; } const uint32_t len = contentSelection->HasRange() ? contentSelection->OffsetAndDataRef().Length() : 0u; uint32_t needSize = sizeof(RECONVERTSTRING) + len * sizeof(WCHAR); if (!pReconv) { // Return need size to reconvert. if (len == 0) { MOZ_LOG(gIMELog, LogLevel::Error, ("IMMHandler::HandleReconvert, There are not selected text")); return false; } *oResult = needSize; MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::HandleReconvert, succeeded, result=%" PRIdLPTR, *oResult)); return true; } if (pReconv->dwSize < needSize) { MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::HandleReconvert, FAILED, pReconv->dwSize=%ld, " "needSize=%u", 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; if (len) { ::CopyMemory(reinterpret_cast(lParam + sizeof(RECONVERTSTRING)), contentSelection->OffsetAndDataRef().DataRef().get(), len * sizeof(WCHAR)); } MOZ_LOG( gIMELog, LogLevel::Info, ("IMMHandler::HandleReconvert, SUCCEEDED, pReconv=%s, result=%" PRIdLPTR, 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(lParam); if (!pCharPosition) { MOZ_LOG(gIMELog, LogLevel::Error, ("IMMHandler::HandleQueryCharPosition, FAILED, due to " "pCharPosition is null")); return false; } if (pCharPosition->dwSize < sizeof(IMECHARPOSITION)) { MOZ_LOG(gIMELog, LogLevel::Error, ("IMMHandler::HandleReconvert, FAILED, pCharPosition->dwSize=%lu, " "sizeof(IMECHARPOSITION)=%zu", pCharPosition->dwSize, sizeof(IMECHARPOSITION))); return false; } if (::GetFocus() != aWindow->GetWindowHandle()) { MOZ_LOG(gIMELog, LogLevel::Error, ("IMMHandler::HandleReconvert, FAILED, ::GetFocus()=%p, " "OurWindowHandle=%p", ::GetFocus(), aWindow->GetWindowHandle())); return false; } if (pCharPosition->dwCharPos > len) { MOZ_LOG(gIMELog, LogLevel::Error, ("IMMHandler::HandleQueryCharPosition, FAILED, " "pCharPosition->dwCharPos=%ld, len=%u", 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(gIMELog, LogLevel::Error, (" IMMHandler::HandleQueryCharPosition, eQueryEditorRect failed")); ::GetWindowRect(aWindow->GetWindowHandle(), &pCharPosition->rcDocument); } else { LayoutDeviceIntRect editorRectInWindow = queryEditorRectEvent.mReply->mRect; nsWindow* window = !!queryEditorRectEvent.mReply->mFocusedWidget ? static_cast( 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( gIMELog, LogLevel::Info, ("IMMHandler::HandleQueryCharPosition, SUCCEEDED, pCharPosition={ " "pt={ x=%ld, y=%ld }, cLineHeight=%d, rcDocument={ left=%ld, top=%ld, " "right=%ld, bottom=%ld } }", 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(lParam); LayoutDeviceIntPoint point(0, 0); bool hasCompositionString = mIsComposing && ShouldDrawCompositionStringOurselves(); int32_t targetOffset, targetLength; if (!hasCompositionString) { const Maybe& contentSelection = GetContentSelectionWithQueryIfNothing(aWindow); if (contentSelection.isNothing()) { MOZ_LOG(gIMELog, LogLevel::Error, ("IMMHandler::HandleDocumentFeed, FAILED, due to " "Selection::GetContentSelectionWithQueryIfNothing() failure")); return false; } if (contentSelection->HasRange()) { targetOffset = static_cast( contentSelection->OffsetAndDataRef().StartOffset()); targetLength = static_cast(contentSelection->OffsetAndDataRef().Length()); } else { // If there is no selection range, let's return all text in the editor. targetOffset = 0; targetLength = INT32_MAX; } } 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(gIMELog, LogLevel::Error, ("IMMHandler::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(gIMELog, LogLevel::Error, ("IMMHandler::HandleDocumentFeed, FAILED, " "due to eQueryTextContent failure")); return false; } nsAutoString str(queryTextContentEvent.mReply->DataRef()); if (targetOffset > static_cast(str.Length())) { MOZ_LOG(gIMELog, LogLevel::Error, (" IMMHandler::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 = 0; if (targetOffset > 0) { paragraphStart = Substring(str, 0, targetOffset).RFind(u"\n") + 1; } int32_t paragraphEnd = str.Find(u"\r", targetOffset + targetLength); 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(gIMELog, LogLevel::Info, ("IMMHandler::HandleDocumentFeed, succeeded, result=%" PRIdLPTR, *oResult)); return true; } if (pReconv->dwSize < needSize) { MOZ_LOG(gIMELog, LogLevel::Error, ("IMMHandler::HandleDocumentFeed, FAILED, " "pReconv->dwSize=%ld, needSize=%u", 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(gIMELog, LogLevel::Error, ("IMMHandler::HandleDocumentFeed, FAILED, " "due to IMMHandler::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(lParam + sizeof(RECONVERTSTRING)), paragraph.BeginReading(), len * sizeof(WCHAR)); MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::HandleDocumentFeed, SUCCEEDED, pReconv=%s, " "result=%" PRIdLPTR, GetReconvertStringLog(pReconv).get(), *oResult)); return true; } bool IMMHandler::CommitCompositionOnPreviousWindow(nsWindow* aWindow) { if (!mComposingWindow || mComposingWindow == aWindow) { return false; } MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::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( gIMELog, LogLevel::Info, ("IMMHandler::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(gIMELog, LogLevel::Info, ("IMMHandler::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 kungFuDeathGrip(aWindow); RefPtr dispatcher = GetTextEventDispatcherFor(aWindow); nsresult rv = dispatcher->BeginNativeInputTransaction(); if (NS_WARN_IF(NS_FAILED(rv))) { MOZ_LOG(gIMELog, LogLevel::Error, (" IMMHandler::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(gIMELog, LogLevel::Info, (" IMMHandler::DispatchCompositionChangeEvent, " "mClauseArray.Length()=0")); rv = dispatcher->SetPendingComposition(mCompositionString, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { MOZ_LOG(gIMELog, LogLevel::Error, (" IMMHandler::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(gIMELog, LogLevel::Error, (" IMMHandler::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(gIMELog, LogLevel::Info, (" IMMHandler::DispatchCompositionChangeEvent, " "mClauseArray[%u]=%u. " "This is larger than mCompositionString.Length()=%zu", i + 1, current, mCompositionString.Length())); current = int32_t(mCompositionString.Length()); } uint32_t length = current - lastOffset; if (NS_WARN_IF(lastOffset >= mAttributeArray.Length())) { MOZ_LOG(gIMELog, LogLevel::Error, (" IMMHandler::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(gIMELog, LogLevel::Error, (" IMMHandler::DispatchCompositionChangeEvent, FAILED due to" "TextEventDispatcher::AppendClauseToPendingComposition() " "failure")); return; } lastOffset = current; MOZ_LOG(gIMELog, LogLevel::Info, (" IMMHandler::DispatchCompositionChangeEvent, index=%u, " "rangeType=%s, range length=%u", i, ToChar(textRangeType), length)); } } if (mCursorPosition == NO_IME_CARET) { MOZ_LOG(gIMELog, LogLevel::Info, (" IMMHandler::DispatchCompositionChangeEvent, no caret")); } else { uint32_t cursor = static_cast(mCursorPosition); if (cursor > mCompositionString.Length()) { MOZ_LOG(gIMELog, LogLevel::Info, (" IMMHandler::CreateTextRangeArray, mCursorPosition=%d. " "This is larger than mCompositionString.Length()=%zu", 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(gIMELog, LogLevel::Info, (" IMMHandler::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( gIMELog, LogLevel::Error, (" IMMHandler::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(gIMELog, LogLevel::Error, (" IMMHandler::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(gIMELog, LogLevel::Error, ("IMMHandler::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( gIMELog, LogLevel::Info, ("IMMHandler::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(gIMELog, LogLevel::Error, ("IMMHandler::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); const Maybe& contentSelection = GetContentSelectionWithQueryIfNothing(aWindow); if (contentSelection.isNothing()) { MOZ_LOG(gIMELog, LogLevel::Error, ("IMMHandler::GetCharacterRectOfSelectedTextAt, FAILED, due to " "Selection::GetContentSelectionWithQueryIfNothing() failure")); return false; } // If there is neither a selection range nor composition string, cannot return // character rect, of course. if (!contentSelection->HasRange() && !mIsComposing) { MOZ_LOG(gIMELog, LogLevel::Warning, ("IMMHandler::GetCharacterRectOfSelectedTextAt, FAILED, due to " "there is neither a selection range nor composition string")); 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. const uint32_t targetLength = mIsComposing ? mCompositionString.Length() : contentSelection->OffsetAndDataRef().Length(); if (NS_WARN_IF(aOffset > targetLength)) { MOZ_LOG( gIMELog, LogLevel::Error, ("IMMHandler::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 (contentSelection.isNothing() || contentSelection->OffsetAndDataRef().IsDataEmpty()) { 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( gIMELog, LogLevel::Debug, ("IMMHandler::GetCharacterRectOfSelectedTextAt, Succeeded, " "aOffset=%u, aCharRect={ x: %d, y: %d, width: %d, height: %d }, " "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( gIMELog, LogLevel::Info, ("IMMHandler::GetCaretRect, FAILED, due to eQueryCaretRect failure")); return false; } aCaretRect = queryCaretRectEvent.mReply->mRect; if (aWritingMode) { *aWritingMode = queryCaretRectEvent.mReply->WritingModeRef(); } MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::GetCaretRect, SUCCEEDED, " "aCaretRect={ x: %d, y: %d, width: %d, height: %d }, " "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(gIMELog, LogLevel::Info, ("IMMHandler::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(gIMELog, LogLevel::Error, (" IMMHandler::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(gIMELog, LogLevel::Info, (" IMMHandler::SetIMERelatedWindowsPos, Calling " "ImmSetCandidateWindow()... ptCurrentPos={ x=%ld, y=%ld }, " "rcArea={ left=%ld, top=%ld, right=%ld, bottom=%ld }, " "writingMode=%s", candForm.ptCurrentPos.x, candForm.ptCurrentPos.y, candForm.rcArea.left, candForm.rcArea.top, candForm.rcArea.right, candForm.rcArea.bottom, ToString(writingMode).c_str())); ::ImmSetCandidateWindow(aContext.get(), &candForm); } else { MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::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( gIMELog, LogLevel::Error, (" IMMHandler::AdjustCompositionFont, ::ImmGetCompositionFont() " "failed")); sCompositionFont.AssignLiteral("System"); } else { // The font face is typically, "System". sCompositionFont.Assign(defaultLogFont.lfFaceName); } } MOZ_LOG(gIMELog, LogLevel::Info, (" IMMHandler::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(gIMELog, LogLevel::Error, (" IMMHandler::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(gIMELog, LogLevel::Warning, (" IMMHandler::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 LayoutDeviceIntPoint cursorPos = aIMENotification.mMouseButtonEventData.mCursorPos; LayoutDeviceIntRect charRect = aIMENotification.mMouseButtonEventData.mCharRect; 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(gIMELog, LogLevel::Info, ("IMMHandler::OnMouseButtonEvent, x,y=%d,%d, offset=%d, " "positioning=%d", cursorPos.x.value, cursorPos.y.value, 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( gIMELog, LogLevel::Info, ("IMMHandler::OnKeyDownEvent, hWnd=%p, wParam=%08zx, lParam=%08" PRIxLPTR, 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; } } Maybe IMMHandler::QueryContentSelection(nsWindow* aWindow) { WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, aWindow); LayoutDeviceIntPoint point(0, 0); aWindow->InitEvent(querySelectedTextEvent, &point); DispatchEvent(aWindow, querySelectedTextEvent); if (NS_WARN_IF(querySelectedTextEvent.Failed())) { MOZ_LOG(gIMELog, LogLevel::Error, (" IMMHandler::Selection::Init, FAILED, due to eQuerySelectedText " "failure")); return Nothing(); } // If the window is destroyed during querying selected text, we shouldn't // do anymore. if (aWindow->Destroyed()) { MOZ_LOG( gIMELog, LogLevel::Error, (" IMMHandler::Selection::Init, FAILED, due to the widget destroyed")); return Nothing(); } ContentSelection contentSelection(querySelectedTextEvent); MOZ_LOG(gIMELog, LogLevel::Info, ("IMMHandler::Selection::Init, querySelectedTextEvent={ mReply=%s }", ToString(querySelectedTextEvent.mReply).c_str())); if (contentSelection.HasRange() && !contentSelection.OffsetAndDataRef().IsValid()) { MOZ_LOG(gIMELog, LogLevel::Error, (" IMMHandler::Selection::Init, FAILED, due to invalid range")); return Nothing(); } return Some(contentSelection); } } // namespace widget } // namespace mozilla